+ {{
+ localText(
+ '启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。',
+ 'When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.'
+ )
+ }}
+
{{
localText(
- '留空表示由后端使用默认来源;可填 easypay、alipay、wxpay 等来源标识。',
- 'Leave blank to let the backend decide. Typical values are easypay, alipay, or wxpay.'
+ '留空表示自动路由;仅允许当前系统支持的官方或易支付来源。',
+ 'Leave blank for automatic routing. Only supported official or EasyPay sources are allowed.'
)
}}
{{ t('admin.channelMonitor.form.extraModels') }}
@@ -137,6 +145,7 @@ import type { ApiKey } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Toggle from '@/components/common/Toggle.vue'
import ModelTagInput from '@/components/admin/channel/ModelTagInput.vue'
+import { getPlatformTextClass } from '@/components/admin/channel/types'
import MonitorKeyPickerDialog from '@/components/admin/monitor/MonitorKeyPickerDialog.vue'
import ProviderIcon from '@/components/user/monitor/ProviderIcon.vue'
import { useChannelMonitorFormat } from '@/composables/useChannelMonitorFormat'
From 0c48f08f5c748bdec005e31344a6d672b1d8ad83 Mon Sep 17 00:00:00 2001
From: erio
Date: Tue, 21 Apr 2026 12:12:08 +0800
Subject: [PATCH 077/326] refactor(channel-status): drop breadcrumb + subtitle
from MonitorHero
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The "CHANNEL · STATUS" breadcrumb and the zh/en subtitles above the
window-picker were redundant with the existing "渠道状态" page title
shown in the layout header. Remove the left column and right-align the
7d/15d/30d tabs + overall chip.
Also drop the now-unreferenced channelStatus.hero.* i18n keys from both
locales (grep confirms no remaining usage).
chore: bump version to 0.1.114.31
---
.../src/components/user/monitor/MonitorHero.vue | 14 +-------------
frontend/src/i18n/locales/en.ts | 5 -----
frontend/src/i18n/locales/zh.ts | 5 -----
3 files changed, 1 insertion(+), 23 deletions(-)
diff --git a/frontend/src/components/user/monitor/MonitorHero.vue b/frontend/src/components/user/monitor/MonitorHero.vue
index 6857a6fe..7fc4d846 100644
--- a/frontend/src/components/user/monitor/MonitorHero.vue
+++ b/frontend/src/components/user/monitor/MonitorHero.vue
@@ -1,18 +1,6 @@
-
- {{ t('channelStatus.hero.breadcrumb') }}
-
-
-
-
- {{ t('channelStatus.hero.subtitleZh') }}
-
-
- {{ t('channelStatus.hero.subtitleEn') }}
-
-
-
+
Date: Tue, 21 Apr 2026 14:14:49 +0800
Subject: [PATCH 078/326] feat(channel-monitor): request templates with
snapshot apply + headers/body override
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.
Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.
Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
+body_override_mode, +body_override (the three runtime snapshot fields).
Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
snapshot fields. mergeHeaders applies user headers on top of adapter
defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
Connection / Content-Encoding).
- buildRequestBody:
off -> adapter default body
merge -> shallow-merge over default; per-provider deny list
(model/messages/contents) protects the challenge contract
replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.
Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.
Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
+ body mode radio + body JSON editor; used by both template and monitor
forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
(filtered by form.provider, clears on provider change) + embedded
AdvancedRequestConfig. Picking a template copies its fields into the
form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.
chore: bump version to 0.1.114.32
---
_parse_upstream.py | 78 +
backend/cmd/server/wire_gen.go | 5 +-
backend/ent/channelmonitor.go | 78 +-
backend/ent/channelmonitor/channelmonitor.go | 51 +
backend/ent/channelmonitor/where.go | 138 ++
backend/ent/channelmonitor_create.go | 308 ++++
backend/ent/channelmonitor_query.go | 108 +-
backend/ent/channelmonitor_update.go | 247 ++++
backend/ent/channelmonitorrequesttemplate.go | 216 +++
.../channelmonitorrequesttemplate.go | 172 +++
.../channelmonitorrequesttemplate/where.go | 434 ++++++
.../channelmonitorrequesttemplate_create.go | 942 ++++++++++++
.../channelmonitorrequesttemplate_delete.go | 88 ++
.../channelmonitorrequesttemplate_query.go | 648 +++++++++
.../channelmonitorrequesttemplate_update.go | 639 ++++++++
backend/ent/client.go | 347 +++--
backend/ent/ent.go | 68 +-
backend/ent/hook/hook.go | 12 +
backend/ent/intercept/intercept.go | 30 +
backend/ent/migrate/schema.go | 47 +
backend/ent/mutation.go | 1285 ++++++++++++++++-
backend/ent/predicate/predicate.go | 3 +
backend/ent/runtime/runtime.go | 60 +
backend/ent/schema/channel_monitor.go | 27 +
.../channel_monitor_request_template.go | 80 +
backend/ent/tx.go | 3 +
.../handler/admin/channel_monitor_handler.go | 105 +-
.../admin/channel_monitor_template_handler.go | 195 +++
backend/internal/handler/handler.go | 55 +-
backend/internal/handler/wire.go | 57 +-
.../repository/channel_monitor_repo.go | 83 +-
.../channel_monitor_template_repo.go | 168 +++
backend/internal/repository/wire.go | 1 +
backend/internal/server/routes/admin.go | 10 +
.../service/channel_monitor_checker.go | 134 +-
.../channel_monitor_checker_body_test.go | 173 +++
.../service/channel_monitor_service.go | 72 +-
.../channel_monitor_template_service.go | 225 +++
.../service/channel_monitor_template_types.go | 74 +
.../internal/service/channel_monitor_types.go | 51 +-
backend/internal/service/wire.go | 1 +
..._add_channel_monitor_request_templates.sql | 70 +
frontend/src/api/admin/channelMonitor.ts | 16 +-
.../src/api/admin/channelMonitorTemplate.ts | 108 ++
frontend/src/api/admin/index.ts | 3 +
.../monitor/MonitorAdvancedRequestConfig.vue | 205 +++
.../admin/monitor/MonitorFiltersBar.vue | 9 +
.../admin/monitor/MonitorFormDialog.vue | 118 +-
.../monitor/MonitorTemplateManagerDialog.vue | 465 ++++++
.../components/user/monitor/MonitorHero.vue | 87 +-
frontend/src/i18n/locales/en.ts | 52 +-
frontend/src/i18n/locales/zh.ts | 52 +-
.../src/views/admin/ChannelMonitorView.vue | 9 +
53 files changed, 8318 insertions(+), 394 deletions(-)
create mode 100644 _parse_upstream.py
create mode 100644 backend/ent/channelmonitorrequesttemplate.go
create mode 100644 backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go
create mode 100644 backend/ent/channelmonitorrequesttemplate/where.go
create mode 100644 backend/ent/channelmonitorrequesttemplate_create.go
create mode 100644 backend/ent/channelmonitorrequesttemplate_delete.go
create mode 100644 backend/ent/channelmonitorrequesttemplate_query.go
create mode 100644 backend/ent/channelmonitorrequesttemplate_update.go
create mode 100644 backend/ent/schema/channel_monitor_request_template.go
create mode 100644 backend/internal/handler/admin/channel_monitor_template_handler.go
create mode 100644 backend/internal/repository/channel_monitor_template_repo.go
create mode 100644 backend/internal/service/channel_monitor_checker_body_test.go
create mode 100644 backend/internal/service/channel_monitor_template_service.go
create mode 100644 backend/internal/service/channel_monitor_template_types.go
create mode 100644 backend/migrations/128_add_channel_monitor_request_templates.sql
create mode 100644 frontend/src/api/admin/channelMonitorTemplate.ts
create mode 100644 frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue
create mode 100644 frontend/src/components/admin/monitor/MonitorTemplateManagerDialog.vue
diff --git a/_parse_upstream.py b/_parse_upstream.py
new file mode 100644
index 00000000..807d1cac
--- /dev/null
+++ b/_parse_upstream.py
@@ -0,0 +1,78 @@
+"""
+严格按模型拆分 upstream 的 token 和 quota;并按【我们的定价表】重算每个模型的 token 应得金额。
+对比 upstream provider-side (/group_ratio) 与我们 Anthropic 官方价的计算结果。
+"""
+import re
+import json
+from collections import defaultdict
+
+# 按账号(token_name) + 模型拆
+by_key = defaultdict(lambda: {
+ 'count': 0,
+ 'prompt': 0,
+ 'completion': 0,
+ 'cache_create': 0,
+ 'cache_read': 0,
+ 'quota_pre_group_sum': 0.0,
+ 'flat_price_reqs': 0,
+ 'flat_price_value': 0.0,
+ 'model_ratios': set(),
+ 'model_prices': set(),
+})
+
+with open(r"C:\Users\16790\xwechat_files\wxid_8tc8tfooo5rs22_fef8\msg\file\2026-04\asakifeng_consume.txt", 'r', encoding='utf-8') as f:
+ for line in f:
+ m = re.match(r'\[INFO\] (\d{4}/\d{2}/\d{2} - \d{2}:\d{2}:\d{2}) \|.*params=(\{.*\})\s*$', line.strip())
+ if not m: continue
+ try: p = json.loads(m.group(2))
+ except Exception: continue
+ tn = p.get('token_name', '')
+ model = p.get('model_name', '')
+ other = p.get('other') or {}
+ gr = other.get('group_ratio', 1.0) or 1.0
+ q = p.get('quota', 0) or 0
+ k = (tn, model)
+ d = by_key[k]
+ d['count'] += 1
+ d['prompt'] += p.get('prompt_tokens', 0) or 0
+ d['completion'] += p.get('completion_tokens', 0) or 0
+ d['cache_create'] += other.get('cache_creation_tokens', 0) or 0
+ d['cache_read'] += other.get('cache_tokens', 0) or 0
+ d['quota_pre_group_sum'] += q / gr if gr else q
+ mp = other.get('model_price') or 0
+ mr = other.get('model_ratio')
+ if mr is not None: d['model_ratios'].add(mr)
+ d['model_prices'].add(mp)
+ if mp and mp > 0:
+ d['flat_price_reqs'] += 1
+ d['flat_price_value'] += mp # flat $ per request
+
+# 我们定价表(从 backend/resources/.../model_prices_and_context_window.json 读的真实值)
+OUR_PRICE = {
+ 'claude-haiku-4-5-20251001': {'input': 1e-6, 'output': 5e-6, 'cc5m': 1.25e-6, 'cr': 1e-7},
+ 'claude-sonnet-4-6': {'input': 3e-6, 'output': 1.5e-5, 'cc5m': 3.75e-6, 'cr': 3e-7},
+ 'claude-sonnet-4-5-20250929': {'input': 3e-6, 'output': 1.5e-5, 'cc5m': 3.75e-6, 'cr': 3e-7},
+ 'claude-opus-4-6': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7},
+ 'claude-opus-4-5-20251101': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7},
+ 'claude-opus-4-7': {'input': 5e-6, 'output': 2.5e-5, 'cc5m': 6.25e-6, 'cr': 5e-7}, # 我们回退到 opus-4-6 价
+}
+
+print("%-40s %-28s %5s %12s %12s %12s %12s" % ("TOKEN", "MODEL", "req", "upstream$", "our_calc$", "diff$", "note"))
+print("-" * 150)
+total_up = 0.0; total_ours = 0.0
+for (tn, model), d in sorted(by_key.items()):
+ up = d['quota_pre_group_sum'] / 500000
+ p = OUR_PRICE.get(model)
+ if p:
+ ours = (d['prompt']*p['input'] + d['completion']*p['output']
+ + d['cache_create']*p['cc5m'] + d['cache_read']*p['cr'])
+ else:
+ ours = 0.0
+ diff = up - ours
+ note = ""
+ if d['flat_price_reqs']:
+ note = f"flat_price {d['flat_price_reqs']}/{d['count']}"
+ total_up += up; total_ours += ours
+ print("%-40s %-28s %5d %12.4f %12.4f %+12.4f %s" % (tn[:40], model, d['count'], up, ours, diff, note))
+print("-" * 150)
+print("%-40s %-28s %5s %12.4f %12.4f %+12.4f" % ("TOTAL", "", "", total_up, total_ours, total_up - total_ours))
diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go
index a878ea68..4e95035a 100644
--- a/backend/cmd/server/wire_gen.go
+++ b/backend/cmd/server/wire_gen.go
@@ -215,6 +215,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
return nil, err
}
channelMonitorRepository := repository.NewChannelMonitorRepository(client, sqlDB)
+ channelMonitorRequestTemplateRepository := repository.NewChannelMonitorRequestTemplateRepository(client, sqlDB)
+ channelMonitorRequestTemplateService := service.NewChannelMonitorRequestTemplateService(channelMonitorRequestTemplateRepository)
+ channelMonitorRequestTemplateHandler := admin.NewChannelMonitorRequestTemplateHandler(channelMonitorRequestTemplateService)
channelMonitorService := service.ProvideChannelMonitorService(channelMonitorRepository, secretEncryptor)
channelMonitorHandler := admin.NewChannelMonitorHandler(channelMonitorService)
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService, settingService)
@@ -231,7 +234,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService)
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService)
paymentHandler := admin.NewPaymentHandler(paymentService, paymentConfigService)
- adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, paymentHandler)
+ adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, paymentHandler)
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
diff --git a/backend/ent/channelmonitor.go b/backend/ent/channelmonitor.go
index 58886884..dbb73362 100644
--- a/backend/ent/channelmonitor.go
+++ b/backend/ent/channelmonitor.go
@@ -11,6 +11,7 @@ import (
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
)
// ChannelMonitor is the model entity for the ChannelMonitor schema.
@@ -44,6 +45,14 @@ type ChannelMonitor struct {
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
// CreatedBy holds the value of the "created_by" field.
CreatedBy int64 `json:"created_by,omitempty"`
+ // TemplateID holds the value of the "template_id" field.
+ TemplateID *int64 `json:"template_id,omitempty"`
+ // ExtraHeaders holds the value of the "extra_headers" field.
+ ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
+ // BodyOverrideMode holds the value of the "body_override_mode" field.
+ BodyOverrideMode string `json:"body_override_mode,omitempty"`
+ // BodyOverride holds the value of the "body_override" field.
+ BodyOverride map[string]interface{} `json:"body_override,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the ChannelMonitorQuery when eager-loading is set.
Edges ChannelMonitorEdges `json:"edges"`
@@ -56,9 +65,11 @@ type ChannelMonitorEdges struct {
History []*ChannelMonitorHistory `json:"history,omitempty"`
// DailyRollups holds the value of the daily_rollups edge.
DailyRollups []*ChannelMonitorDailyRollup `json:"daily_rollups,omitempty"`
+ // RequestTemplate holds the value of the request_template edge.
+ RequestTemplate *ChannelMonitorRequestTemplate `json:"request_template,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
- loadedTypes [2]bool
+ loadedTypes [3]bool
}
// HistoryOrErr returns the History value or an error if the edge
@@ -79,18 +90,29 @@ func (e ChannelMonitorEdges) DailyRollupsOrErr() ([]*ChannelMonitorDailyRollup,
return nil, &NotLoadedError{edge: "daily_rollups"}
}
+// RequestTemplateOrErr returns the RequestTemplate value or an error if the edge
+// was not loaded in eager-loading, or loaded but was not found.
+func (e ChannelMonitorEdges) RequestTemplateOrErr() (*ChannelMonitorRequestTemplate, error) {
+ if e.RequestTemplate != nil {
+ return e.RequestTemplate, nil
+ } else if e.loadedTypes[2] {
+ return nil, &NotFoundError{label: channelmonitorrequesttemplate.Label}
+ }
+ return nil, &NotLoadedError{edge: "request_template"}
+}
+
// scanValues returns the types for scanning values from sql.Rows.
func (*ChannelMonitor) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
- case channelmonitor.FieldExtraModels:
+ case channelmonitor.FieldExtraModels, channelmonitor.FieldExtraHeaders, channelmonitor.FieldBodyOverride:
values[i] = new([]byte)
case channelmonitor.FieldEnabled:
values[i] = new(sql.NullBool)
- case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldCreatedBy:
+ case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
values[i] = new(sql.NullInt64)
- case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName:
+ case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
values[i] = new(sql.NullString)
case channelmonitor.FieldCreatedAt, channelmonitor.FieldUpdatedAt, channelmonitor.FieldLastCheckedAt:
values[i] = new(sql.NullTime)
@@ -196,6 +218,35 @@ func (_m *ChannelMonitor) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.CreatedBy = value.Int64
}
+ case channelmonitor.FieldTemplateID:
+ if value, ok := values[i].(*sql.NullInt64); !ok {
+ return fmt.Errorf("unexpected type %T for field template_id", values[i])
+ } else if value.Valid {
+ _m.TemplateID = new(int64)
+ *_m.TemplateID = value.Int64
+ }
+ case channelmonitor.FieldExtraHeaders:
+ if value, ok := values[i].(*[]byte); !ok {
+ return fmt.Errorf("unexpected type %T for field extra_headers", values[i])
+ } else if value != nil && len(*value) > 0 {
+ if err := json.Unmarshal(*value, &_m.ExtraHeaders); err != nil {
+ return fmt.Errorf("unmarshal field extra_headers: %w", err)
+ }
+ }
+ case channelmonitor.FieldBodyOverrideMode:
+ if value, ok := values[i].(*sql.NullString); !ok {
+ return fmt.Errorf("unexpected type %T for field body_override_mode", values[i])
+ } else if value.Valid {
+ _m.BodyOverrideMode = value.String
+ }
+ case channelmonitor.FieldBodyOverride:
+ if value, ok := values[i].(*[]byte); !ok {
+ return fmt.Errorf("unexpected type %T for field body_override", values[i])
+ } else if value != nil && len(*value) > 0 {
+ if err := json.Unmarshal(*value, &_m.BodyOverride); err != nil {
+ return fmt.Errorf("unmarshal field body_override: %w", err)
+ }
+ }
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -219,6 +270,11 @@ func (_m *ChannelMonitor) QueryDailyRollups() *ChannelMonitorDailyRollupQuery {
return NewChannelMonitorClient(_m.config).QueryDailyRollups(_m)
}
+// QueryRequestTemplate queries the "request_template" edge of the ChannelMonitor entity.
+func (_m *ChannelMonitor) QueryRequestTemplate() *ChannelMonitorRequestTemplateQuery {
+ return NewChannelMonitorClient(_m.config).QueryRequestTemplate(_m)
+}
+
// Update returns a builder for updating this ChannelMonitor.
// Note that you need to call ChannelMonitor.Unwrap() before calling this method if this ChannelMonitor
// was returned from a transaction, and the transaction was committed or rolled back.
@@ -281,6 +337,20 @@ func (_m *ChannelMonitor) String() string {
builder.WriteString(", ")
builder.WriteString("created_by=")
builder.WriteString(fmt.Sprintf("%v", _m.CreatedBy))
+ builder.WriteString(", ")
+ if v := _m.TemplateID; v != nil {
+ builder.WriteString("template_id=")
+ builder.WriteString(fmt.Sprintf("%v", *v))
+ }
+ builder.WriteString(", ")
+ builder.WriteString("extra_headers=")
+ builder.WriteString(fmt.Sprintf("%v", _m.ExtraHeaders))
+ builder.WriteString(", ")
+ builder.WriteString("body_override_mode=")
+ builder.WriteString(_m.BodyOverrideMode)
+ builder.WriteString(", ")
+ builder.WriteString("body_override=")
+ builder.WriteString(fmt.Sprintf("%v", _m.BodyOverride))
builder.WriteByte(')')
return builder.String()
}
diff --git a/backend/ent/channelmonitor/channelmonitor.go b/backend/ent/channelmonitor/channelmonitor.go
index ff6d7105..e5a6bfe7 100644
--- a/backend/ent/channelmonitor/channelmonitor.go
+++ b/backend/ent/channelmonitor/channelmonitor.go
@@ -41,10 +41,20 @@ const (
FieldLastCheckedAt = "last_checked_at"
// FieldCreatedBy holds the string denoting the created_by field in the database.
FieldCreatedBy = "created_by"
+ // FieldTemplateID holds the string denoting the template_id field in the database.
+ FieldTemplateID = "template_id"
+ // FieldExtraHeaders holds the string denoting the extra_headers field in the database.
+ FieldExtraHeaders = "extra_headers"
+ // FieldBodyOverrideMode holds the string denoting the body_override_mode field in the database.
+ FieldBodyOverrideMode = "body_override_mode"
+ // FieldBodyOverride holds the string denoting the body_override field in the database.
+ FieldBodyOverride = "body_override"
// EdgeHistory holds the string denoting the history edge name in mutations.
EdgeHistory = "history"
// EdgeDailyRollups holds the string denoting the daily_rollups edge name in mutations.
EdgeDailyRollups = "daily_rollups"
+ // EdgeRequestTemplate holds the string denoting the request_template edge name in mutations.
+ EdgeRequestTemplate = "request_template"
// Table holds the table name of the channelmonitor in the database.
Table = "channel_monitors"
// HistoryTable is the table that holds the history relation/edge.
@@ -61,6 +71,13 @@ const (
DailyRollupsInverseTable = "channel_monitor_daily_rollups"
// DailyRollupsColumn is the table column denoting the daily_rollups relation/edge.
DailyRollupsColumn = "monitor_id"
+ // RequestTemplateTable is the table that holds the request_template relation/edge.
+ RequestTemplateTable = "channel_monitors"
+ // RequestTemplateInverseTable is the table name for the ChannelMonitorRequestTemplate entity.
+ // It exists in this package in order to avoid circular dependency with the "channelmonitorrequesttemplate" package.
+ RequestTemplateInverseTable = "channel_monitor_request_templates"
+ // RequestTemplateColumn is the table column denoting the request_template relation/edge.
+ RequestTemplateColumn = "template_id"
)
// Columns holds all SQL columns for channelmonitor fields.
@@ -79,6 +96,10 @@ var Columns = []string{
FieldIntervalSeconds,
FieldLastCheckedAt,
FieldCreatedBy,
+ FieldTemplateID,
+ FieldExtraHeaders,
+ FieldBodyOverrideMode,
+ FieldBodyOverride,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -116,6 +137,12 @@ var (
DefaultEnabled bool
// IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save.
IntervalSecondsValidator func(int) error
+ // DefaultExtraHeaders holds the default value on creation for the "extra_headers" field.
+ DefaultExtraHeaders map[string]string
+ // DefaultBodyOverrideMode holds the default value on creation for the "body_override_mode" field.
+ DefaultBodyOverrideMode string
+ // BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
+ BodyOverrideModeValidator func(string) error
)
// Provider defines the type for the "provider" enum field.
@@ -210,6 +237,16 @@ func ByCreatedBy(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedBy, opts...).ToFunc()
}
+// ByTemplateID orders the results by the template_id field.
+func ByTemplateID(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldTemplateID, opts...).ToFunc()
+}
+
+// ByBodyOverrideMode orders the results by the body_override_mode field.
+func ByBodyOverrideMode(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldBodyOverrideMode, opts...).ToFunc()
+}
+
// ByHistoryCount orders the results by history count.
func ByHistoryCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
@@ -237,6 +274,13 @@ func ByDailyRollups(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
sqlgraph.OrderByNeighborTerms(s, newDailyRollupsStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
+
+// ByRequestTemplateField orders the results by request_template field.
+func ByRequestTemplateField(field string, opts ...sql.OrderTermOption) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborTerms(s, newRequestTemplateStep(), sql.OrderByField(field, opts...))
+ }
+}
func newHistoryStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
@@ -251,3 +295,10 @@ func newDailyRollupsStep() *sqlgraph.Step {
sqlgraph.Edge(sqlgraph.O2M, false, DailyRollupsTable, DailyRollupsColumn),
)
}
+func newRequestTemplateStep() *sqlgraph.Step {
+ return sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.To(RequestTemplateInverseTable, FieldID),
+ sqlgraph.Edge(sqlgraph.M2O, false, RequestTemplateTable, RequestTemplateColumn),
+ )
+}
diff --git a/backend/ent/channelmonitor/where.go b/backend/ent/channelmonitor/where.go
index abb8484d..755d83a3 100644
--- a/backend/ent/channelmonitor/where.go
+++ b/backend/ent/channelmonitor/where.go
@@ -110,6 +110,16 @@ func CreatedBy(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldCreatedBy, v))
}
+// TemplateID applies equality check predicate on the "template_id" field. It's identical to TemplateIDEQ.
+func TemplateID(v int64) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldEQ(FieldTemplateID, v))
+}
+
+// BodyOverrideMode applies equality check predicate on the "body_override_mode" field. It's identical to BodyOverrideModeEQ.
+func BodyOverrideMode(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldEQ(FieldBodyOverrideMode, v))
+}
+
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldEQ(FieldCreatedAt, v))
@@ -685,6 +695,111 @@ func CreatedByLTE(v int64) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.FieldLTE(FieldCreatedBy, v))
}
+// TemplateIDEQ applies the EQ predicate on the "template_id" field.
+func TemplateIDEQ(v int64) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldEQ(FieldTemplateID, v))
+}
+
+// TemplateIDNEQ applies the NEQ predicate on the "template_id" field.
+func TemplateIDNEQ(v int64) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNEQ(FieldTemplateID, v))
+}
+
+// TemplateIDIn applies the In predicate on the "template_id" field.
+func TemplateIDIn(vs ...int64) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldIn(FieldTemplateID, vs...))
+}
+
+// TemplateIDNotIn applies the NotIn predicate on the "template_id" field.
+func TemplateIDNotIn(vs ...int64) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNotIn(FieldTemplateID, vs...))
+}
+
+// TemplateIDIsNil applies the IsNil predicate on the "template_id" field.
+func TemplateIDIsNil() predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldIsNull(FieldTemplateID))
+}
+
+// TemplateIDNotNil applies the NotNil predicate on the "template_id" field.
+func TemplateIDNotNil() predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNotNull(FieldTemplateID))
+}
+
+// BodyOverrideModeEQ applies the EQ predicate on the "body_override_mode" field.
+func BodyOverrideModeEQ(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldEQ(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeNEQ applies the NEQ predicate on the "body_override_mode" field.
+func BodyOverrideModeNEQ(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNEQ(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeIn applies the In predicate on the "body_override_mode" field.
+func BodyOverrideModeIn(vs ...string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldIn(FieldBodyOverrideMode, vs...))
+}
+
+// BodyOverrideModeNotIn applies the NotIn predicate on the "body_override_mode" field.
+func BodyOverrideModeNotIn(vs ...string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNotIn(FieldBodyOverrideMode, vs...))
+}
+
+// BodyOverrideModeGT applies the GT predicate on the "body_override_mode" field.
+func BodyOverrideModeGT(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldGT(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeGTE applies the GTE predicate on the "body_override_mode" field.
+func BodyOverrideModeGTE(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldGTE(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeLT applies the LT predicate on the "body_override_mode" field.
+func BodyOverrideModeLT(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldLT(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeLTE applies the LTE predicate on the "body_override_mode" field.
+func BodyOverrideModeLTE(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldLTE(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeContains applies the Contains predicate on the "body_override_mode" field.
+func BodyOverrideModeContains(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldContains(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeHasPrefix applies the HasPrefix predicate on the "body_override_mode" field.
+func BodyOverrideModeHasPrefix(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldHasPrefix(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeHasSuffix applies the HasSuffix predicate on the "body_override_mode" field.
+func BodyOverrideModeHasSuffix(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldHasSuffix(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeEqualFold applies the EqualFold predicate on the "body_override_mode" field.
+func BodyOverrideModeEqualFold(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldEqualFold(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeContainsFold applies the ContainsFold predicate on the "body_override_mode" field.
+func BodyOverrideModeContainsFold(v string) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldContainsFold(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideIsNil applies the IsNil predicate on the "body_override" field.
+func BodyOverrideIsNil() predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldIsNull(FieldBodyOverride))
+}
+
+// BodyOverrideNotNil applies the NotNil predicate on the "body_override" field.
+func BodyOverrideNotNil() predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(sql.FieldNotNull(FieldBodyOverride))
+}
+
// HasHistory applies the HasEdge predicate on the "history" edge.
func HasHistory() predicate.ChannelMonitor {
return predicate.ChannelMonitor(func(s *sql.Selector) {
@@ -731,6 +846,29 @@ func HasDailyRollupsWith(preds ...predicate.ChannelMonitorDailyRollup) predicate
})
}
+// HasRequestTemplate applies the HasEdge predicate on the "request_template" edge.
+func HasRequestTemplate() predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(func(s *sql.Selector) {
+ step := sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.Edge(sqlgraph.M2O, false, RequestTemplateTable, RequestTemplateColumn),
+ )
+ sqlgraph.HasNeighbors(s, step)
+ })
+}
+
+// HasRequestTemplateWith applies the HasEdge predicate on the "request_template" edge with a given conditions (other predicates).
+func HasRequestTemplateWith(preds ...predicate.ChannelMonitorRequestTemplate) predicate.ChannelMonitor {
+ return predicate.ChannelMonitor(func(s *sql.Selector) {
+ step := newRequestTemplateStep()
+ sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
+ for _, p := range preds {
+ p(s)
+ }
+ })
+ })
+}
+
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.ChannelMonitor) predicate.ChannelMonitor {
return predicate.ChannelMonitor(sql.AndPredicates(predicates...))
diff --git a/backend/ent/channelmonitor_create.go b/backend/ent/channelmonitor_create.go
index 30a7b40d..2f70c300 100644
--- a/backend/ent/channelmonitor_create.go
+++ b/backend/ent/channelmonitor_create.go
@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
)
// ChannelMonitorCreate is the builder for creating a ChannelMonitor entity.
@@ -142,6 +143,46 @@ func (_c *ChannelMonitorCreate) SetCreatedBy(v int64) *ChannelMonitorCreate {
return _c
}
+// SetTemplateID sets the "template_id" field.
+func (_c *ChannelMonitorCreate) SetTemplateID(v int64) *ChannelMonitorCreate {
+ _c.mutation.SetTemplateID(v)
+ return _c
+}
+
+// SetNillableTemplateID sets the "template_id" field if the given value is not nil.
+func (_c *ChannelMonitorCreate) SetNillableTemplateID(v *int64) *ChannelMonitorCreate {
+ if v != nil {
+ _c.SetTemplateID(*v)
+ }
+ return _c
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_c *ChannelMonitorCreate) SetExtraHeaders(v map[string]string) *ChannelMonitorCreate {
+ _c.mutation.SetExtraHeaders(v)
+ return _c
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_c *ChannelMonitorCreate) SetBodyOverrideMode(v string) *ChannelMonitorCreate {
+ _c.mutation.SetBodyOverrideMode(v)
+ return _c
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_c *ChannelMonitorCreate) SetNillableBodyOverrideMode(v *string) *ChannelMonitorCreate {
+ if v != nil {
+ _c.SetBodyOverrideMode(*v)
+ }
+ return _c
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_c *ChannelMonitorCreate) SetBodyOverride(v map[string]interface{}) *ChannelMonitorCreate {
+ _c.mutation.SetBodyOverride(v)
+ return _c
+}
+
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by IDs.
func (_c *ChannelMonitorCreate) AddHistoryIDs(ids ...int64) *ChannelMonitorCreate {
_c.mutation.AddHistoryIDs(ids...)
@@ -172,6 +213,25 @@ func (_c *ChannelMonitorCreate) AddDailyRollups(v ...*ChannelMonitorDailyRollup)
return _c.AddDailyRollupIDs(ids...)
}
+// SetRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID.
+func (_c *ChannelMonitorCreate) SetRequestTemplateID(id int64) *ChannelMonitorCreate {
+ _c.mutation.SetRequestTemplateID(id)
+ return _c
+}
+
+// SetNillableRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID if the given value is not nil.
+func (_c *ChannelMonitorCreate) SetNillableRequestTemplateID(id *int64) *ChannelMonitorCreate {
+ if id != nil {
+ _c = _c.SetRequestTemplateID(*id)
+ }
+ return _c
+}
+
+// SetRequestTemplate sets the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (_c *ChannelMonitorCreate) SetRequestTemplate(v *ChannelMonitorRequestTemplate) *ChannelMonitorCreate {
+ return _c.SetRequestTemplateID(v.ID)
+}
+
// Mutation returns the ChannelMonitorMutation object of the builder.
func (_c *ChannelMonitorCreate) Mutation() *ChannelMonitorMutation {
return _c.mutation
@@ -227,6 +287,14 @@ func (_c *ChannelMonitorCreate) defaults() {
v := channelmonitor.DefaultEnabled
_c.mutation.SetEnabled(v)
}
+ if _, ok := _c.mutation.ExtraHeaders(); !ok {
+ v := channelmonitor.DefaultExtraHeaders
+ _c.mutation.SetExtraHeaders(v)
+ }
+ if _, ok := _c.mutation.BodyOverrideMode(); !ok {
+ v := channelmonitor.DefaultBodyOverrideMode
+ _c.mutation.SetBodyOverrideMode(v)
+ }
}
// check runs all checks and user-defined validators on the builder.
@@ -299,6 +367,17 @@ func (_c *ChannelMonitorCreate) check() error {
if _, ok := _c.mutation.CreatedBy(); !ok {
return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "ChannelMonitor.created_by"`)}
}
+ if _, ok := _c.mutation.ExtraHeaders(); !ok {
+ return &ValidationError{Name: "extra_headers", err: errors.New(`ent: missing required field "ChannelMonitor.extra_headers"`)}
+ }
+ if _, ok := _c.mutation.BodyOverrideMode(); !ok {
+ return &ValidationError{Name: "body_override_mode", err: errors.New(`ent: missing required field "ChannelMonitor.body_override_mode"`)}
+ }
+ if v, ok := _c.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitor.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.body_override_mode": %w`, err)}
+ }
+ }
return nil
}
@@ -378,6 +457,18 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
_spec.SetField(channelmonitor.FieldCreatedBy, field.TypeInt64, value)
_node.CreatedBy = value
}
+ if value, ok := _c.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitor.FieldExtraHeaders, field.TypeJSON, value)
+ _node.ExtraHeaders = value
+ }
+ if value, ok := _c.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverrideMode, field.TypeString, value)
+ _node.BodyOverrideMode = value
+ }
+ if value, ok := _c.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverride, field.TypeJSON, value)
+ _node.BodyOverride = value
+ }
if nodes := _c.mutation.HistoryIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -410,6 +501,23 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
}
_spec.Edges = append(_spec.Edges, edge)
}
+ if nodes := _c.mutation.RequestTemplateIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.M2O,
+ Inverse: false,
+ Table: channelmonitor.RequestTemplateTable,
+ Columns: []string{channelmonitor.RequestTemplateColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _node.TemplateID = &nodes[0]
+ _spec.Edges = append(_spec.Edges, edge)
+ }
return _node, _spec
}
@@ -630,6 +738,66 @@ func (u *ChannelMonitorUpsert) AddCreatedBy(v int64) *ChannelMonitorUpsert {
return u
}
+// SetTemplateID sets the "template_id" field.
+func (u *ChannelMonitorUpsert) SetTemplateID(v int64) *ChannelMonitorUpsert {
+ u.Set(channelmonitor.FieldTemplateID, v)
+ return u
+}
+
+// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
+func (u *ChannelMonitorUpsert) UpdateTemplateID() *ChannelMonitorUpsert {
+ u.SetExcluded(channelmonitor.FieldTemplateID)
+ return u
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (u *ChannelMonitorUpsert) ClearTemplateID() *ChannelMonitorUpsert {
+ u.SetNull(channelmonitor.FieldTemplateID)
+ return u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorUpsert) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsert {
+ u.Set(channelmonitor.FieldExtraHeaders, v)
+ return u
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorUpsert) UpdateExtraHeaders() *ChannelMonitorUpsert {
+ u.SetExcluded(channelmonitor.FieldExtraHeaders)
+ return u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorUpsert) SetBodyOverrideMode(v string) *ChannelMonitorUpsert {
+ u.Set(channelmonitor.FieldBodyOverrideMode, v)
+ return u
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorUpsert) UpdateBodyOverrideMode() *ChannelMonitorUpsert {
+ u.SetExcluded(channelmonitor.FieldBodyOverrideMode)
+ return u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorUpsert) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsert {
+ u.Set(channelmonitor.FieldBodyOverride, v)
+ return u
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorUpsert) UpdateBodyOverride() *ChannelMonitorUpsert {
+ u.SetExcluded(channelmonitor.FieldBodyOverride)
+ return u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorUpsert) ClearBodyOverride() *ChannelMonitorUpsert {
+ u.SetNull(channelmonitor.FieldBodyOverride)
+ return u
+}
+
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -871,6 +1039,76 @@ func (u *ChannelMonitorUpsertOne) UpdateCreatedBy() *ChannelMonitorUpsertOne {
})
}
+// SetTemplateID sets the "template_id" field.
+func (u *ChannelMonitorUpsertOne) SetTemplateID(v int64) *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetTemplateID(v)
+ })
+}
+
+// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertOne) UpdateTemplateID() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateTemplateID()
+ })
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (u *ChannelMonitorUpsertOne) ClearTemplateID() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.ClearTemplateID()
+ })
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorUpsertOne) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetExtraHeaders(v)
+ })
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertOne) UpdateExtraHeaders() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateExtraHeaders()
+ })
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorUpsertOne) SetBodyOverrideMode(v string) *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetBodyOverrideMode(v)
+ })
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertOne) UpdateBodyOverrideMode() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateBodyOverrideMode()
+ })
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorUpsertOne) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetBodyOverride(v)
+ })
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertOne) UpdateBodyOverride() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateBodyOverride()
+ })
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorUpsertOne) ClearBodyOverride() *ChannelMonitorUpsertOne {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.ClearBodyOverride()
+ })
+}
+
// Exec executes the query.
func (u *ChannelMonitorUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -1278,6 +1516,76 @@ func (u *ChannelMonitorUpsertBulk) UpdateCreatedBy() *ChannelMonitorUpsertBulk {
})
}
+// SetTemplateID sets the "template_id" field.
+func (u *ChannelMonitorUpsertBulk) SetTemplateID(v int64) *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetTemplateID(v)
+ })
+}
+
+// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertBulk) UpdateTemplateID() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateTemplateID()
+ })
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (u *ChannelMonitorUpsertBulk) ClearTemplateID() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.ClearTemplateID()
+ })
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorUpsertBulk) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetExtraHeaders(v)
+ })
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertBulk) UpdateExtraHeaders() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateExtraHeaders()
+ })
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorUpsertBulk) SetBodyOverrideMode(v string) *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetBodyOverrideMode(v)
+ })
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertBulk) UpdateBodyOverrideMode() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateBodyOverrideMode()
+ })
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorUpsertBulk) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.SetBodyOverride(v)
+ })
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorUpsertBulk) UpdateBodyOverride() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.UpdateBodyOverride()
+ })
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorUpsertBulk) ClearBodyOverride() *ChannelMonitorUpsertBulk {
+ return u.Update(func(s *ChannelMonitorUpsert) {
+ s.ClearBodyOverride()
+ })
+}
+
// Exec executes the query.
func (u *ChannelMonitorUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {
diff --git a/backend/ent/channelmonitor_query.go b/backend/ent/channelmonitor_query.go
index 2ebd95bb..b6722e78 100644
--- a/backend/ent/channelmonitor_query.go
+++ b/backend/ent/channelmonitor_query.go
@@ -16,19 +16,21 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/predicate"
)
// ChannelMonitorQuery is the builder for querying ChannelMonitor entities.
type ChannelMonitorQuery struct {
config
- ctx *QueryContext
- order []channelmonitor.OrderOption
- inters []Interceptor
- predicates []predicate.ChannelMonitor
- withHistory *ChannelMonitorHistoryQuery
- withDailyRollups *ChannelMonitorDailyRollupQuery
- modifiers []func(*sql.Selector)
+ ctx *QueryContext
+ order []channelmonitor.OrderOption
+ inters []Interceptor
+ predicates []predicate.ChannelMonitor
+ withHistory *ChannelMonitorHistoryQuery
+ withDailyRollups *ChannelMonitorDailyRollupQuery
+ withRequestTemplate *ChannelMonitorRequestTemplateQuery
+ modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -109,6 +111,28 @@ func (_q *ChannelMonitorQuery) QueryDailyRollups() *ChannelMonitorDailyRollupQue
return query
}
+// QueryRequestTemplate chains the current query on the "request_template" edge.
+func (_q *ChannelMonitorQuery) QueryRequestTemplate() *ChannelMonitorRequestTemplateQuery {
+ query := (&ChannelMonitorRequestTemplateClient{config: _q.config}).Query()
+ query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
+ if err := _q.prepareQuery(ctx); err != nil {
+ return nil, err
+ }
+ selector := _q.sqlQuery(ctx)
+ if err := selector.Err(); err != nil {
+ return nil, err
+ }
+ step := sqlgraph.NewStep(
+ sqlgraph.From(channelmonitor.Table, channelmonitor.FieldID, selector),
+ sqlgraph.To(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.FieldID),
+ sqlgraph.Edge(sqlgraph.M2O, false, channelmonitor.RequestTemplateTable, channelmonitor.RequestTemplateColumn),
+ )
+ fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
+ return fromU, nil
+ }
+ return query
+}
+
// First returns the first ChannelMonitor entity from the query.
// Returns a *NotFoundError when no ChannelMonitor was found.
func (_q *ChannelMonitorQuery) First(ctx context.Context) (*ChannelMonitor, error) {
@@ -296,13 +320,14 @@ func (_q *ChannelMonitorQuery) Clone() *ChannelMonitorQuery {
return nil
}
return &ChannelMonitorQuery{
- config: _q.config,
- ctx: _q.ctx.Clone(),
- order: append([]channelmonitor.OrderOption{}, _q.order...),
- inters: append([]Interceptor{}, _q.inters...),
- predicates: append([]predicate.ChannelMonitor{}, _q.predicates...),
- withHistory: _q.withHistory.Clone(),
- withDailyRollups: _q.withDailyRollups.Clone(),
+ config: _q.config,
+ ctx: _q.ctx.Clone(),
+ order: append([]channelmonitor.OrderOption{}, _q.order...),
+ inters: append([]Interceptor{}, _q.inters...),
+ predicates: append([]predicate.ChannelMonitor{}, _q.predicates...),
+ withHistory: _q.withHistory.Clone(),
+ withDailyRollups: _q.withDailyRollups.Clone(),
+ withRequestTemplate: _q.withRequestTemplate.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
path: _q.path,
@@ -331,6 +356,17 @@ func (_q *ChannelMonitorQuery) WithDailyRollups(opts ...func(*ChannelMonitorDail
return _q
}
+// WithRequestTemplate tells the query-builder to eager-load the nodes that are connected to
+// the "request_template" edge. The optional arguments are used to configure the query builder of the edge.
+func (_q *ChannelMonitorQuery) WithRequestTemplate(opts ...func(*ChannelMonitorRequestTemplateQuery)) *ChannelMonitorQuery {
+ query := (&ChannelMonitorRequestTemplateClient{config: _q.config}).Query()
+ for _, opt := range opts {
+ opt(query)
+ }
+ _q.withRequestTemplate = query
+ return _q
+}
+
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
@@ -409,9 +445,10 @@ func (_q *ChannelMonitorQuery) sqlAll(ctx context.Context, hooks ...queryHook) (
var (
nodes = []*ChannelMonitor{}
_spec = _q.querySpec()
- loadedTypes = [2]bool{
+ loadedTypes = [3]bool{
_q.withHistory != nil,
_q.withDailyRollups != nil,
+ _q.withRequestTemplate != nil,
}
)
_spec.ScanValues = func(columns []string) ([]any, error) {
@@ -451,6 +488,12 @@ func (_q *ChannelMonitorQuery) sqlAll(ctx context.Context, hooks ...queryHook) (
return nil, err
}
}
+ if query := _q.withRequestTemplate; query != nil {
+ if err := _q.loadRequestTemplate(ctx, query, nodes, nil,
+ func(n *ChannelMonitor, e *ChannelMonitorRequestTemplate) { n.Edges.RequestTemplate = e }); err != nil {
+ return nil, err
+ }
+ }
return nodes, nil
}
@@ -514,6 +557,38 @@ func (_q *ChannelMonitorQuery) loadDailyRollups(ctx context.Context, query *Chan
}
return nil
}
+func (_q *ChannelMonitorQuery) loadRequestTemplate(ctx context.Context, query *ChannelMonitorRequestTemplateQuery, nodes []*ChannelMonitor, init func(*ChannelMonitor), assign func(*ChannelMonitor, *ChannelMonitorRequestTemplate)) error {
+ ids := make([]int64, 0, len(nodes))
+ nodeids := make(map[int64][]*ChannelMonitor)
+ for i := range nodes {
+ if nodes[i].TemplateID == nil {
+ continue
+ }
+ fk := *nodes[i].TemplateID
+ if _, ok := nodeids[fk]; !ok {
+ ids = append(ids, fk)
+ }
+ nodeids[fk] = append(nodeids[fk], nodes[i])
+ }
+ if len(ids) == 0 {
+ return nil
+ }
+ query.Where(channelmonitorrequesttemplate.IDIn(ids...))
+ neighbors, err := query.All(ctx)
+ if err != nil {
+ return err
+ }
+ for _, n := range neighbors {
+ nodes, ok := nodeids[n.ID]
+ if !ok {
+ return fmt.Errorf(`unexpected foreign-key "template_id" returned %v`, n.ID)
+ }
+ for i := range nodes {
+ assign(nodes[i], n)
+ }
+ }
+ return nil
+}
func (_q *ChannelMonitorQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
@@ -543,6 +618,9 @@ func (_q *ChannelMonitorQuery) querySpec() *sqlgraph.QuerySpec {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
+ if _q.withRequestTemplate != nil {
+ _spec.Node.AddColumnOnce(channelmonitor.FieldTemplateID)
+ }
}
if ps := _q.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
diff --git a/backend/ent/channelmonitor_update.go b/backend/ent/channelmonitor_update.go
index 7ba4e449..4bbcd564 100644
--- a/backend/ent/channelmonitor_update.go
+++ b/backend/ent/channelmonitor_update.go
@@ -15,6 +15,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/predicate"
)
@@ -215,6 +216,58 @@ func (_u *ChannelMonitorUpdate) AddCreatedBy(v int64) *ChannelMonitorUpdate {
return _u
}
+// SetTemplateID sets the "template_id" field.
+func (_u *ChannelMonitorUpdate) SetTemplateID(v int64) *ChannelMonitorUpdate {
+ _u.mutation.SetTemplateID(v)
+ return _u
+}
+
+// SetNillableTemplateID sets the "template_id" field if the given value is not nil.
+func (_u *ChannelMonitorUpdate) SetNillableTemplateID(v *int64) *ChannelMonitorUpdate {
+ if v != nil {
+ _u.SetTemplateID(*v)
+ }
+ return _u
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (_u *ChannelMonitorUpdate) ClearTemplateID() *ChannelMonitorUpdate {
+ _u.mutation.ClearTemplateID()
+ return _u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_u *ChannelMonitorUpdate) SetExtraHeaders(v map[string]string) *ChannelMonitorUpdate {
+ _u.mutation.SetExtraHeaders(v)
+ return _u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_u *ChannelMonitorUpdate) SetBodyOverrideMode(v string) *ChannelMonitorUpdate {
+ _u.mutation.SetBodyOverrideMode(v)
+ return _u
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_u *ChannelMonitorUpdate) SetNillableBodyOverrideMode(v *string) *ChannelMonitorUpdate {
+ if v != nil {
+ _u.SetBodyOverrideMode(*v)
+ }
+ return _u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_u *ChannelMonitorUpdate) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpdate {
+ _u.mutation.SetBodyOverride(v)
+ return _u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (_u *ChannelMonitorUpdate) ClearBodyOverride() *ChannelMonitorUpdate {
+ _u.mutation.ClearBodyOverride()
+ return _u
+}
+
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by IDs.
func (_u *ChannelMonitorUpdate) AddHistoryIDs(ids ...int64) *ChannelMonitorUpdate {
_u.mutation.AddHistoryIDs(ids...)
@@ -245,6 +298,25 @@ func (_u *ChannelMonitorUpdate) AddDailyRollups(v ...*ChannelMonitorDailyRollup)
return _u.AddDailyRollupIDs(ids...)
}
+// SetRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID.
+func (_u *ChannelMonitorUpdate) SetRequestTemplateID(id int64) *ChannelMonitorUpdate {
+ _u.mutation.SetRequestTemplateID(id)
+ return _u
+}
+
+// SetNillableRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID if the given value is not nil.
+func (_u *ChannelMonitorUpdate) SetNillableRequestTemplateID(id *int64) *ChannelMonitorUpdate {
+ if id != nil {
+ _u = _u.SetRequestTemplateID(*id)
+ }
+ return _u
+}
+
+// SetRequestTemplate sets the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (_u *ChannelMonitorUpdate) SetRequestTemplate(v *ChannelMonitorRequestTemplate) *ChannelMonitorUpdate {
+ return _u.SetRequestTemplateID(v.ID)
+}
+
// Mutation returns the ChannelMonitorMutation object of the builder.
func (_u *ChannelMonitorUpdate) Mutation() *ChannelMonitorMutation {
return _u.mutation
@@ -292,6 +364,12 @@ func (_u *ChannelMonitorUpdate) RemoveDailyRollups(v ...*ChannelMonitorDailyRoll
return _u.RemoveDailyRollupIDs(ids...)
}
+// ClearRequestTemplate clears the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (_u *ChannelMonitorUpdate) ClearRequestTemplate() *ChannelMonitorUpdate {
+ _u.mutation.ClearRequestTemplate()
+ return _u
+}
+
// Save executes the query and returns the number of nodes affected by the update operation.
func (_u *ChannelMonitorUpdate) Save(ctx context.Context) (int, error) {
_u.defaults()
@@ -365,6 +443,11 @@ func (_u *ChannelMonitorUpdate) check() error {
return &ValidationError{Name: "interval_seconds", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.interval_seconds": %w`, err)}
}
}
+ if v, ok := _u.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitor.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.body_override_mode": %w`, err)}
+ }
+ }
return nil
}
@@ -433,6 +516,18 @@ func (_u *ChannelMonitorUpdate) sqlSave(ctx context.Context) (_node int, err err
if value, ok := _u.mutation.AddedCreatedBy(); ok {
_spec.AddField(channelmonitor.FieldCreatedBy, field.TypeInt64, value)
}
+ if value, ok := _u.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitor.FieldExtraHeaders, field.TypeJSON, value)
+ }
+ if value, ok := _u.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverrideMode, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverride, field.TypeJSON, value)
+ }
+ if _u.mutation.BodyOverrideCleared() {
+ _spec.ClearField(channelmonitor.FieldBodyOverride, field.TypeJSON)
+ }
if _u.mutation.HistoryCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -523,6 +618,35 @@ func (_u *ChannelMonitorUpdate) sqlSave(ctx context.Context) (_node int, err err
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
+ if _u.mutation.RequestTemplateCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.M2O,
+ Inverse: false,
+ Table: channelmonitor.RequestTemplateTable,
+ Columns: []string{channelmonitor.RequestTemplateColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
+ },
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.RequestTemplateIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.M2O,
+ Inverse: false,
+ Table: channelmonitor.RequestTemplateTable,
+ Columns: []string{channelmonitor.RequestTemplateColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Add = append(_spec.Edges.Add, edge)
+ }
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{channelmonitor.Label}
@@ -727,6 +851,58 @@ func (_u *ChannelMonitorUpdateOne) AddCreatedBy(v int64) *ChannelMonitorUpdateOn
return _u
}
+// SetTemplateID sets the "template_id" field.
+func (_u *ChannelMonitorUpdateOne) SetTemplateID(v int64) *ChannelMonitorUpdateOne {
+ _u.mutation.SetTemplateID(v)
+ return _u
+}
+
+// SetNillableTemplateID sets the "template_id" field if the given value is not nil.
+func (_u *ChannelMonitorUpdateOne) SetNillableTemplateID(v *int64) *ChannelMonitorUpdateOne {
+ if v != nil {
+ _u.SetTemplateID(*v)
+ }
+ return _u
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (_u *ChannelMonitorUpdateOne) ClearTemplateID() *ChannelMonitorUpdateOne {
+ _u.mutation.ClearTemplateID()
+ return _u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_u *ChannelMonitorUpdateOne) SetExtraHeaders(v map[string]string) *ChannelMonitorUpdateOne {
+ _u.mutation.SetExtraHeaders(v)
+ return _u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_u *ChannelMonitorUpdateOne) SetBodyOverrideMode(v string) *ChannelMonitorUpdateOne {
+ _u.mutation.SetBodyOverrideMode(v)
+ return _u
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_u *ChannelMonitorUpdateOne) SetNillableBodyOverrideMode(v *string) *ChannelMonitorUpdateOne {
+ if v != nil {
+ _u.SetBodyOverrideMode(*v)
+ }
+ return _u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_u *ChannelMonitorUpdateOne) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpdateOne {
+ _u.mutation.SetBodyOverride(v)
+ return _u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (_u *ChannelMonitorUpdateOne) ClearBodyOverride() *ChannelMonitorUpdateOne {
+ _u.mutation.ClearBodyOverride()
+ return _u
+}
+
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by IDs.
func (_u *ChannelMonitorUpdateOne) AddHistoryIDs(ids ...int64) *ChannelMonitorUpdateOne {
_u.mutation.AddHistoryIDs(ids...)
@@ -757,6 +933,25 @@ func (_u *ChannelMonitorUpdateOne) AddDailyRollups(v ...*ChannelMonitorDailyRoll
return _u.AddDailyRollupIDs(ids...)
}
+// SetRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID.
+func (_u *ChannelMonitorUpdateOne) SetRequestTemplateID(id int64) *ChannelMonitorUpdateOne {
+ _u.mutation.SetRequestTemplateID(id)
+ return _u
+}
+
+// SetNillableRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID if the given value is not nil.
+func (_u *ChannelMonitorUpdateOne) SetNillableRequestTemplateID(id *int64) *ChannelMonitorUpdateOne {
+ if id != nil {
+ _u = _u.SetRequestTemplateID(*id)
+ }
+ return _u
+}
+
+// SetRequestTemplate sets the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (_u *ChannelMonitorUpdateOne) SetRequestTemplate(v *ChannelMonitorRequestTemplate) *ChannelMonitorUpdateOne {
+ return _u.SetRequestTemplateID(v.ID)
+}
+
// Mutation returns the ChannelMonitorMutation object of the builder.
func (_u *ChannelMonitorUpdateOne) Mutation() *ChannelMonitorMutation {
return _u.mutation
@@ -804,6 +999,12 @@ func (_u *ChannelMonitorUpdateOne) RemoveDailyRollups(v ...*ChannelMonitorDailyR
return _u.RemoveDailyRollupIDs(ids...)
}
+// ClearRequestTemplate clears the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (_u *ChannelMonitorUpdateOne) ClearRequestTemplate() *ChannelMonitorUpdateOne {
+ _u.mutation.ClearRequestTemplate()
+ return _u
+}
+
// Where appends a list predicates to the ChannelMonitorUpdate builder.
func (_u *ChannelMonitorUpdateOne) Where(ps ...predicate.ChannelMonitor) *ChannelMonitorUpdateOne {
_u.mutation.Where(ps...)
@@ -890,6 +1091,11 @@ func (_u *ChannelMonitorUpdateOne) check() error {
return &ValidationError{Name: "interval_seconds", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.interval_seconds": %w`, err)}
}
}
+ if v, ok := _u.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitor.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.body_override_mode": %w`, err)}
+ }
+ }
return nil
}
@@ -975,6 +1181,18 @@ func (_u *ChannelMonitorUpdateOne) sqlSave(ctx context.Context) (_node *ChannelM
if value, ok := _u.mutation.AddedCreatedBy(); ok {
_spec.AddField(channelmonitor.FieldCreatedBy, field.TypeInt64, value)
}
+ if value, ok := _u.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitor.FieldExtraHeaders, field.TypeJSON, value)
+ }
+ if value, ok := _u.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverrideMode, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitor.FieldBodyOverride, field.TypeJSON, value)
+ }
+ if _u.mutation.BodyOverrideCleared() {
+ _spec.ClearField(channelmonitor.FieldBodyOverride, field.TypeJSON)
+ }
if _u.mutation.HistoryCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -1065,6 +1283,35 @@ func (_u *ChannelMonitorUpdateOne) sqlSave(ctx context.Context) (_node *ChannelM
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
+ if _u.mutation.RequestTemplateCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.M2O,
+ Inverse: false,
+ Table: channelmonitor.RequestTemplateTable,
+ Columns: []string{channelmonitor.RequestTemplateColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
+ },
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.RequestTemplateIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.M2O,
+ Inverse: false,
+ Table: channelmonitor.RequestTemplateTable,
+ Columns: []string{channelmonitor.RequestTemplateColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Add = append(_spec.Edges.Add, edge)
+ }
_node = &ChannelMonitor{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
diff --git a/backend/ent/channelmonitorrequesttemplate.go b/backend/ent/channelmonitorrequesttemplate.go
new file mode 100644
index 00000000..b8429a4d
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate.go
@@ -0,0 +1,216 @@
+// Code generated by ent, DO NOT EDIT.
+
+package ent
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "entgo.io/ent"
+ "entgo.io/ent/dialect/sql"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+)
+
+// ChannelMonitorRequestTemplate is the model entity for the ChannelMonitorRequestTemplate schema.
+type ChannelMonitorRequestTemplate struct {
+ config `json:"-"`
+ // ID of the ent.
+ ID int64 `json:"id,omitempty"`
+ // CreatedAt holds the value of the "created_at" field.
+ CreatedAt time.Time `json:"created_at,omitempty"`
+ // UpdatedAt holds the value of the "updated_at" field.
+ UpdatedAt time.Time `json:"updated_at,omitempty"`
+ // Name holds the value of the "name" field.
+ Name string `json:"name,omitempty"`
+ // Provider holds the value of the "provider" field.
+ Provider channelmonitorrequesttemplate.Provider `json:"provider,omitempty"`
+ // Description holds the value of the "description" field.
+ Description string `json:"description,omitempty"`
+ // ExtraHeaders holds the value of the "extra_headers" field.
+ ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
+ // BodyOverrideMode holds the value of the "body_override_mode" field.
+ BodyOverrideMode string `json:"body_override_mode,omitempty"`
+ // BodyOverride holds the value of the "body_override" field.
+ BodyOverride map[string]interface{} `json:"body_override,omitempty"`
+ // Edges holds the relations/edges for other nodes in the graph.
+ // The values are being populated by the ChannelMonitorRequestTemplateQuery when eager-loading is set.
+ Edges ChannelMonitorRequestTemplateEdges `json:"edges"`
+ selectValues sql.SelectValues
+}
+
+// ChannelMonitorRequestTemplateEdges holds the relations/edges for other nodes in the graph.
+type ChannelMonitorRequestTemplateEdges struct {
+ // Monitors holds the value of the monitors edge.
+ Monitors []*ChannelMonitor `json:"monitors,omitempty"`
+ // loadedTypes holds the information for reporting if a
+ // type was loaded (or requested) in eager-loading or not.
+ loadedTypes [1]bool
+}
+
+// MonitorsOrErr returns the Monitors value or an error if the edge
+// was not loaded in eager-loading.
+func (e ChannelMonitorRequestTemplateEdges) MonitorsOrErr() ([]*ChannelMonitor, error) {
+ if e.loadedTypes[0] {
+ return e.Monitors, nil
+ }
+ return nil, &NotLoadedError{edge: "monitors"}
+}
+
+// scanValues returns the types for scanning values from sql.Rows.
+func (*ChannelMonitorRequestTemplate) scanValues(columns []string) ([]any, error) {
+ values := make([]any, len(columns))
+ for i := range columns {
+ switch columns[i] {
+ case channelmonitorrequesttemplate.FieldExtraHeaders, channelmonitorrequesttemplate.FieldBodyOverride:
+ values[i] = new([]byte)
+ case channelmonitorrequesttemplate.FieldID:
+ values[i] = new(sql.NullInt64)
+ case channelmonitorrequesttemplate.FieldName, channelmonitorrequesttemplate.FieldProvider, channelmonitorrequesttemplate.FieldDescription, channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ values[i] = new(sql.NullString)
+ case channelmonitorrequesttemplate.FieldCreatedAt, channelmonitorrequesttemplate.FieldUpdatedAt:
+ values[i] = new(sql.NullTime)
+ default:
+ values[i] = new(sql.UnknownType)
+ }
+ }
+ return values, nil
+}
+
+// assignValues assigns the values that were returned from sql.Rows (after scanning)
+// to the ChannelMonitorRequestTemplate fields.
+func (_m *ChannelMonitorRequestTemplate) assignValues(columns []string, values []any) error {
+ if m, n := len(values), len(columns); m < n {
+ return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
+ }
+ for i := range columns {
+ switch columns[i] {
+ case channelmonitorrequesttemplate.FieldID:
+ value, ok := values[i].(*sql.NullInt64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field id", value)
+ }
+ _m.ID = int64(value.Int64)
+ case channelmonitorrequesttemplate.FieldCreatedAt:
+ if value, ok := values[i].(*sql.NullTime); !ok {
+ return fmt.Errorf("unexpected type %T for field created_at", values[i])
+ } else if value.Valid {
+ _m.CreatedAt = value.Time
+ }
+ case channelmonitorrequesttemplate.FieldUpdatedAt:
+ if value, ok := values[i].(*sql.NullTime); !ok {
+ return fmt.Errorf("unexpected type %T for field updated_at", values[i])
+ } else if value.Valid {
+ _m.UpdatedAt = value.Time
+ }
+ case channelmonitorrequesttemplate.FieldName:
+ if value, ok := values[i].(*sql.NullString); !ok {
+ return fmt.Errorf("unexpected type %T for field name", values[i])
+ } else if value.Valid {
+ _m.Name = value.String
+ }
+ case channelmonitorrequesttemplate.FieldProvider:
+ if value, ok := values[i].(*sql.NullString); !ok {
+ return fmt.Errorf("unexpected type %T for field provider", values[i])
+ } else if value.Valid {
+ _m.Provider = channelmonitorrequesttemplate.Provider(value.String)
+ }
+ case channelmonitorrequesttemplate.FieldDescription:
+ if value, ok := values[i].(*sql.NullString); !ok {
+ return fmt.Errorf("unexpected type %T for field description", values[i])
+ } else if value.Valid {
+ _m.Description = value.String
+ }
+ case channelmonitorrequesttemplate.FieldExtraHeaders:
+ if value, ok := values[i].(*[]byte); !ok {
+ return fmt.Errorf("unexpected type %T for field extra_headers", values[i])
+ } else if value != nil && len(*value) > 0 {
+ if err := json.Unmarshal(*value, &_m.ExtraHeaders); err != nil {
+ return fmt.Errorf("unmarshal field extra_headers: %w", err)
+ }
+ }
+ case channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ if value, ok := values[i].(*sql.NullString); !ok {
+ return fmt.Errorf("unexpected type %T for field body_override_mode", values[i])
+ } else if value.Valid {
+ _m.BodyOverrideMode = value.String
+ }
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ if value, ok := values[i].(*[]byte); !ok {
+ return fmt.Errorf("unexpected type %T for field body_override", values[i])
+ } else if value != nil && len(*value) > 0 {
+ if err := json.Unmarshal(*value, &_m.BodyOverride); err != nil {
+ return fmt.Errorf("unmarshal field body_override: %w", err)
+ }
+ }
+ default:
+ _m.selectValues.Set(columns[i], values[i])
+ }
+ }
+ return nil
+}
+
+// Value returns the ent.Value that was dynamically selected and assigned to the ChannelMonitorRequestTemplate.
+// This includes values selected through modifiers, order, etc.
+func (_m *ChannelMonitorRequestTemplate) Value(name string) (ent.Value, error) {
+ return _m.selectValues.Get(name)
+}
+
+// QueryMonitors queries the "monitors" edge of the ChannelMonitorRequestTemplate entity.
+func (_m *ChannelMonitorRequestTemplate) QueryMonitors() *ChannelMonitorQuery {
+ return NewChannelMonitorRequestTemplateClient(_m.config).QueryMonitors(_m)
+}
+
+// Update returns a builder for updating this ChannelMonitorRequestTemplate.
+// Note that you need to call ChannelMonitorRequestTemplate.Unwrap() before calling this method if this ChannelMonitorRequestTemplate
+// was returned from a transaction, and the transaction was committed or rolled back.
+func (_m *ChannelMonitorRequestTemplate) Update() *ChannelMonitorRequestTemplateUpdateOne {
+ return NewChannelMonitorRequestTemplateClient(_m.config).UpdateOne(_m)
+}
+
+// Unwrap unwraps the ChannelMonitorRequestTemplate entity that was returned from a transaction after it was closed,
+// so that all future queries will be executed through the driver which created the transaction.
+func (_m *ChannelMonitorRequestTemplate) Unwrap() *ChannelMonitorRequestTemplate {
+ _tx, ok := _m.config.driver.(*txDriver)
+ if !ok {
+ panic("ent: ChannelMonitorRequestTemplate is not a transactional entity")
+ }
+ _m.config.driver = _tx.drv
+ return _m
+}
+
+// String implements the fmt.Stringer.
+func (_m *ChannelMonitorRequestTemplate) String() string {
+ var builder strings.Builder
+ builder.WriteString("ChannelMonitorRequestTemplate(")
+ builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
+ builder.WriteString("created_at=")
+ builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
+ builder.WriteString(", ")
+ builder.WriteString("updated_at=")
+ builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
+ builder.WriteString(", ")
+ builder.WriteString("name=")
+ builder.WriteString(_m.Name)
+ builder.WriteString(", ")
+ builder.WriteString("provider=")
+ builder.WriteString(fmt.Sprintf("%v", _m.Provider))
+ builder.WriteString(", ")
+ builder.WriteString("description=")
+ builder.WriteString(_m.Description)
+ builder.WriteString(", ")
+ builder.WriteString("extra_headers=")
+ builder.WriteString(fmt.Sprintf("%v", _m.ExtraHeaders))
+ builder.WriteString(", ")
+ builder.WriteString("body_override_mode=")
+ builder.WriteString(_m.BodyOverrideMode)
+ builder.WriteString(", ")
+ builder.WriteString("body_override=")
+ builder.WriteString(fmt.Sprintf("%v", _m.BodyOverride))
+ builder.WriteByte(')')
+ return builder.String()
+}
+
+// ChannelMonitorRequestTemplates is a parsable slice of ChannelMonitorRequestTemplate.
+type ChannelMonitorRequestTemplates []*ChannelMonitorRequestTemplate
diff --git a/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go b/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go
new file mode 100644
index 00000000..65b8d641
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go
@@ -0,0 +1,172 @@
+// Code generated by ent, DO NOT EDIT.
+
+package channelmonitorrequesttemplate
+
+import (
+ "fmt"
+ "time"
+
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+)
+
+const (
+ // Label holds the string label denoting the channelmonitorrequesttemplate type in the database.
+ Label = "channel_monitor_request_template"
+ // FieldID holds the string denoting the id field in the database.
+ FieldID = "id"
+ // FieldCreatedAt holds the string denoting the created_at field in the database.
+ FieldCreatedAt = "created_at"
+ // FieldUpdatedAt holds the string denoting the updated_at field in the database.
+ FieldUpdatedAt = "updated_at"
+ // FieldName holds the string denoting the name field in the database.
+ FieldName = "name"
+ // FieldProvider holds the string denoting the provider field in the database.
+ FieldProvider = "provider"
+ // FieldDescription holds the string denoting the description field in the database.
+ FieldDescription = "description"
+ // FieldExtraHeaders holds the string denoting the extra_headers field in the database.
+ FieldExtraHeaders = "extra_headers"
+ // FieldBodyOverrideMode holds the string denoting the body_override_mode field in the database.
+ FieldBodyOverrideMode = "body_override_mode"
+ // FieldBodyOverride holds the string denoting the body_override field in the database.
+ FieldBodyOverride = "body_override"
+ // EdgeMonitors holds the string denoting the monitors edge name in mutations.
+ EdgeMonitors = "monitors"
+ // Table holds the table name of the channelmonitorrequesttemplate in the database.
+ Table = "channel_monitor_request_templates"
+ // MonitorsTable is the table that holds the monitors relation/edge.
+ MonitorsTable = "channel_monitors"
+ // MonitorsInverseTable is the table name for the ChannelMonitor entity.
+ // It exists in this package in order to avoid circular dependency with the "channelmonitor" package.
+ MonitorsInverseTable = "channel_monitors"
+ // MonitorsColumn is the table column denoting the monitors relation/edge.
+ MonitorsColumn = "template_id"
+)
+
+// Columns holds all SQL columns for channelmonitorrequesttemplate fields.
+var Columns = []string{
+ FieldID,
+ FieldCreatedAt,
+ FieldUpdatedAt,
+ FieldName,
+ FieldProvider,
+ FieldDescription,
+ FieldExtraHeaders,
+ FieldBodyOverrideMode,
+ FieldBodyOverride,
+}
+
+// ValidColumn reports if the column name is valid (part of the table columns).
+func ValidColumn(column string) bool {
+ for i := range Columns {
+ if column == Columns[i] {
+ return true
+ }
+ }
+ return false
+}
+
+var (
+ // DefaultCreatedAt holds the default value on creation for the "created_at" field.
+ DefaultCreatedAt func() time.Time
+ // DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
+ DefaultUpdatedAt func() time.Time
+ // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
+ UpdateDefaultUpdatedAt func() time.Time
+ // NameValidator is a validator for the "name" field. It is called by the builders before save.
+ NameValidator func(string) error
+ // DefaultDescription holds the default value on creation for the "description" field.
+ DefaultDescription string
+ // DescriptionValidator is a validator for the "description" field. It is called by the builders before save.
+ DescriptionValidator func(string) error
+ // DefaultExtraHeaders holds the default value on creation for the "extra_headers" field.
+ DefaultExtraHeaders map[string]string
+ // DefaultBodyOverrideMode holds the default value on creation for the "body_override_mode" field.
+ DefaultBodyOverrideMode string
+ // BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
+ BodyOverrideModeValidator func(string) error
+)
+
+// Provider defines the type for the "provider" enum field.
+type Provider string
+
+// Provider values.
+const (
+ ProviderOpenai Provider = "openai"
+ ProviderAnthropic Provider = "anthropic"
+ ProviderGemini Provider = "gemini"
+)
+
+func (pr Provider) String() string {
+ return string(pr)
+}
+
+// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
+func ProviderValidator(pr Provider) error {
+ switch pr {
+ case ProviderOpenai, ProviderAnthropic, ProviderGemini:
+ return nil
+ default:
+ return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
+ }
+}
+
+// OrderOption defines the ordering options for the ChannelMonitorRequestTemplate queries.
+type OrderOption func(*sql.Selector)
+
+// ByID orders the results by the id field.
+func ByID(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldID, opts...).ToFunc()
+}
+
+// ByCreatedAt orders the results by the created_at field.
+func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
+}
+
+// ByUpdatedAt orders the results by the updated_at field.
+func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
+}
+
+// ByName orders the results by the name field.
+func ByName(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldName, opts...).ToFunc()
+}
+
+// ByProvider orders the results by the provider field.
+func ByProvider(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldProvider, opts...).ToFunc()
+}
+
+// ByDescription orders the results by the description field.
+func ByDescription(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldDescription, opts...).ToFunc()
+}
+
+// ByBodyOverrideMode orders the results by the body_override_mode field.
+func ByBodyOverrideMode(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldBodyOverrideMode, opts...).ToFunc()
+}
+
+// ByMonitorsCount orders the results by monitors count.
+func ByMonitorsCount(opts ...sql.OrderTermOption) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborsCount(s, newMonitorsStep(), opts...)
+ }
+}
+
+// ByMonitors orders the results by monitors terms.
+func ByMonitors(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborTerms(s, newMonitorsStep(), append([]sql.OrderTerm{term}, terms...)...)
+ }
+}
+func newMonitorsStep() *sqlgraph.Step {
+ return sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.To(MonitorsInverseTable, FieldID),
+ sqlgraph.Edge(sqlgraph.O2M, true, MonitorsTable, MonitorsColumn),
+ )
+}
diff --git a/backend/ent/channelmonitorrequesttemplate/where.go b/backend/ent/channelmonitorrequesttemplate/where.go
new file mode 100644
index 00000000..b95e5df0
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate/where.go
@@ -0,0 +1,434 @@
+// Code generated by ent, DO NOT EDIT.
+
+package channelmonitorrequesttemplate
+
+import (
+ "time"
+
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/Wei-Shaw/sub2api/ent/predicate"
+)
+
+// ID filters vertices based on their ID field.
+func ID(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldID, id))
+}
+
+// IDEQ applies the EQ predicate on the ID field.
+func IDEQ(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldID, id))
+}
+
+// IDNEQ applies the NEQ predicate on the ID field.
+func IDNEQ(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldID, id))
+}
+
+// IDIn applies the In predicate on the ID field.
+func IDIn(ids ...int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldID, ids...))
+}
+
+// IDNotIn applies the NotIn predicate on the ID field.
+func IDNotIn(ids ...int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldID, ids...))
+}
+
+// IDGT applies the GT predicate on the ID field.
+func IDGT(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldID, id))
+}
+
+// IDGTE applies the GTE predicate on the ID field.
+func IDGTE(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldID, id))
+}
+
+// IDLT applies the LT predicate on the ID field.
+func IDLT(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldID, id))
+}
+
+// IDLTE applies the LTE predicate on the ID field.
+func IDLTE(id int64) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldID, id))
+}
+
+// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
+func CreatedAt(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldCreatedAt, v))
+}
+
+// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ.
+func UpdatedAt(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldUpdatedAt, v))
+}
+
+// Name applies equality check predicate on the "name" field. It's identical to NameEQ.
+func Name(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldName, v))
+}
+
+// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ.
+func Description(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldDescription, v))
+}
+
+// BodyOverrideMode applies equality check predicate on the "body_override_mode" field. It's identical to BodyOverrideModeEQ.
+func BodyOverrideMode(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldBodyOverrideMode, v))
+}
+
+// CreatedAtEQ applies the EQ predicate on the "created_at" field.
+func CreatedAtEQ(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldCreatedAt, v))
+}
+
+// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
+func CreatedAtNEQ(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldCreatedAt, v))
+}
+
+// CreatedAtIn applies the In predicate on the "created_at" field.
+func CreatedAtIn(vs ...time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldCreatedAt, vs...))
+}
+
+// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
+func CreatedAtNotIn(vs ...time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldCreatedAt, vs...))
+}
+
+// CreatedAtGT applies the GT predicate on the "created_at" field.
+func CreatedAtGT(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldCreatedAt, v))
+}
+
+// CreatedAtGTE applies the GTE predicate on the "created_at" field.
+func CreatedAtGTE(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldCreatedAt, v))
+}
+
+// CreatedAtLT applies the LT predicate on the "created_at" field.
+func CreatedAtLT(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldCreatedAt, v))
+}
+
+// CreatedAtLTE applies the LTE predicate on the "created_at" field.
+func CreatedAtLTE(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldCreatedAt, v))
+}
+
+// UpdatedAtEQ applies the EQ predicate on the "updated_at" field.
+func UpdatedAtEQ(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldUpdatedAt, v))
+}
+
+// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field.
+func UpdatedAtNEQ(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldUpdatedAt, v))
+}
+
+// UpdatedAtIn applies the In predicate on the "updated_at" field.
+func UpdatedAtIn(vs ...time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldUpdatedAt, vs...))
+}
+
+// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field.
+func UpdatedAtNotIn(vs ...time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldUpdatedAt, vs...))
+}
+
+// UpdatedAtGT applies the GT predicate on the "updated_at" field.
+func UpdatedAtGT(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldUpdatedAt, v))
+}
+
+// UpdatedAtGTE applies the GTE predicate on the "updated_at" field.
+func UpdatedAtGTE(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldUpdatedAt, v))
+}
+
+// UpdatedAtLT applies the LT predicate on the "updated_at" field.
+func UpdatedAtLT(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldUpdatedAt, v))
+}
+
+// UpdatedAtLTE applies the LTE predicate on the "updated_at" field.
+func UpdatedAtLTE(v time.Time) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldUpdatedAt, v))
+}
+
+// NameEQ applies the EQ predicate on the "name" field.
+func NameEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldName, v))
+}
+
+// NameNEQ applies the NEQ predicate on the "name" field.
+func NameNEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldName, v))
+}
+
+// NameIn applies the In predicate on the "name" field.
+func NameIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldName, vs...))
+}
+
+// NameNotIn applies the NotIn predicate on the "name" field.
+func NameNotIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldName, vs...))
+}
+
+// NameGT applies the GT predicate on the "name" field.
+func NameGT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldName, v))
+}
+
+// NameGTE applies the GTE predicate on the "name" field.
+func NameGTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldName, v))
+}
+
+// NameLT applies the LT predicate on the "name" field.
+func NameLT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldName, v))
+}
+
+// NameLTE applies the LTE predicate on the "name" field.
+func NameLTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldName, v))
+}
+
+// NameContains applies the Contains predicate on the "name" field.
+func NameContains(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContains(FieldName, v))
+}
+
+// NameHasPrefix applies the HasPrefix predicate on the "name" field.
+func NameHasPrefix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasPrefix(FieldName, v))
+}
+
+// NameHasSuffix applies the HasSuffix predicate on the "name" field.
+func NameHasSuffix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasSuffix(FieldName, v))
+}
+
+// NameEqualFold applies the EqualFold predicate on the "name" field.
+func NameEqualFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEqualFold(FieldName, v))
+}
+
+// NameContainsFold applies the ContainsFold predicate on the "name" field.
+func NameContainsFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContainsFold(FieldName, v))
+}
+
+// ProviderEQ applies the EQ predicate on the "provider" field.
+func ProviderEQ(v Provider) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldProvider, v))
+}
+
+// ProviderNEQ applies the NEQ predicate on the "provider" field.
+func ProviderNEQ(v Provider) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldProvider, v))
+}
+
+// ProviderIn applies the In predicate on the "provider" field.
+func ProviderIn(vs ...Provider) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldProvider, vs...))
+}
+
+// ProviderNotIn applies the NotIn predicate on the "provider" field.
+func ProviderNotIn(vs ...Provider) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldProvider, vs...))
+}
+
+// DescriptionEQ applies the EQ predicate on the "description" field.
+func DescriptionEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldDescription, v))
+}
+
+// DescriptionNEQ applies the NEQ predicate on the "description" field.
+func DescriptionNEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldDescription, v))
+}
+
+// DescriptionIn applies the In predicate on the "description" field.
+func DescriptionIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldDescription, vs...))
+}
+
+// DescriptionNotIn applies the NotIn predicate on the "description" field.
+func DescriptionNotIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldDescription, vs...))
+}
+
+// DescriptionGT applies the GT predicate on the "description" field.
+func DescriptionGT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldDescription, v))
+}
+
+// DescriptionGTE applies the GTE predicate on the "description" field.
+func DescriptionGTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldDescription, v))
+}
+
+// DescriptionLT applies the LT predicate on the "description" field.
+func DescriptionLT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldDescription, v))
+}
+
+// DescriptionLTE applies the LTE predicate on the "description" field.
+func DescriptionLTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldDescription, v))
+}
+
+// DescriptionContains applies the Contains predicate on the "description" field.
+func DescriptionContains(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContains(FieldDescription, v))
+}
+
+// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field.
+func DescriptionHasPrefix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasPrefix(FieldDescription, v))
+}
+
+// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field.
+func DescriptionHasSuffix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasSuffix(FieldDescription, v))
+}
+
+// DescriptionIsNil applies the IsNil predicate on the "description" field.
+func DescriptionIsNil() predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIsNull(FieldDescription))
+}
+
+// DescriptionNotNil applies the NotNil predicate on the "description" field.
+func DescriptionNotNil() predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotNull(FieldDescription))
+}
+
+// DescriptionEqualFold applies the EqualFold predicate on the "description" field.
+func DescriptionEqualFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEqualFold(FieldDescription, v))
+}
+
+// DescriptionContainsFold applies the ContainsFold predicate on the "description" field.
+func DescriptionContainsFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContainsFold(FieldDescription, v))
+}
+
+// BodyOverrideModeEQ applies the EQ predicate on the "body_override_mode" field.
+func BodyOverrideModeEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeNEQ applies the NEQ predicate on the "body_override_mode" field.
+func BodyOverrideModeNEQ(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeIn applies the In predicate on the "body_override_mode" field.
+func BodyOverrideModeIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldBodyOverrideMode, vs...))
+}
+
+// BodyOverrideModeNotIn applies the NotIn predicate on the "body_override_mode" field.
+func BodyOverrideModeNotIn(vs ...string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldBodyOverrideMode, vs...))
+}
+
+// BodyOverrideModeGT applies the GT predicate on the "body_override_mode" field.
+func BodyOverrideModeGT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeGTE applies the GTE predicate on the "body_override_mode" field.
+func BodyOverrideModeGTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeLT applies the LT predicate on the "body_override_mode" field.
+func BodyOverrideModeLT(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeLTE applies the LTE predicate on the "body_override_mode" field.
+func BodyOverrideModeLTE(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeContains applies the Contains predicate on the "body_override_mode" field.
+func BodyOverrideModeContains(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContains(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeHasPrefix applies the HasPrefix predicate on the "body_override_mode" field.
+func BodyOverrideModeHasPrefix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasPrefix(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeHasSuffix applies the HasSuffix predicate on the "body_override_mode" field.
+func BodyOverrideModeHasSuffix(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldHasSuffix(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeEqualFold applies the EqualFold predicate on the "body_override_mode" field.
+func BodyOverrideModeEqualFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldEqualFold(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideModeContainsFold applies the ContainsFold predicate on the "body_override_mode" field.
+func BodyOverrideModeContainsFold(v string) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldContainsFold(FieldBodyOverrideMode, v))
+}
+
+// BodyOverrideIsNil applies the IsNil predicate on the "body_override" field.
+func BodyOverrideIsNil() predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldIsNull(FieldBodyOverride))
+}
+
+// BodyOverrideNotNil applies the NotNil predicate on the "body_override" field.
+func BodyOverrideNotNil() predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.FieldNotNull(FieldBodyOverride))
+}
+
+// HasMonitors applies the HasEdge predicate on the "monitors" edge.
+func HasMonitors() predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(func(s *sql.Selector) {
+ step := sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.Edge(sqlgraph.O2M, true, MonitorsTable, MonitorsColumn),
+ )
+ sqlgraph.HasNeighbors(s, step)
+ })
+}
+
+// HasMonitorsWith applies the HasEdge predicate on the "monitors" edge with a given conditions (other predicates).
+func HasMonitorsWith(preds ...predicate.ChannelMonitor) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(func(s *sql.Selector) {
+ step := newMonitorsStep()
+ sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
+ for _, p := range preds {
+ p(s)
+ }
+ })
+ })
+}
+
+// And groups predicates with the AND operator between them.
+func And(predicates ...predicate.ChannelMonitorRequestTemplate) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.AndPredicates(predicates...))
+}
+
+// Or groups predicates with the OR operator between them.
+func Or(predicates ...predicate.ChannelMonitorRequestTemplate) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.OrPredicates(predicates...))
+}
+
+// Not applies the not operator on the given predicate.
+func Not(p predicate.ChannelMonitorRequestTemplate) predicate.ChannelMonitorRequestTemplate {
+ return predicate.ChannelMonitorRequestTemplate(sql.NotPredicates(p))
+}
diff --git a/backend/ent/channelmonitorrequesttemplate_create.go b/backend/ent/channelmonitorrequesttemplate_create.go
new file mode 100644
index 00000000..1ba842cd
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate_create.go
@@ -0,0 +1,942 @@
+// Code generated by ent, DO NOT EDIT.
+
+package ent
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "entgo.io/ent/schema/field"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitor"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+)
+
+// ChannelMonitorRequestTemplateCreate is the builder for creating a ChannelMonitorRequestTemplate entity.
+type ChannelMonitorRequestTemplateCreate struct {
+ config
+ mutation *ChannelMonitorRequestTemplateMutation
+ hooks []Hook
+ conflict []sql.ConflictOption
+}
+
+// SetCreatedAt sets the "created_at" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetCreatedAt(v time.Time) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetCreatedAt(v)
+ return _c
+}
+
+// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
+func (_c *ChannelMonitorRequestTemplateCreate) SetNillableCreatedAt(v *time.Time) *ChannelMonitorRequestTemplateCreate {
+ if v != nil {
+ _c.SetCreatedAt(*v)
+ }
+ return _c
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetUpdatedAt(v)
+ return _c
+}
+
+// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
+func (_c *ChannelMonitorRequestTemplateCreate) SetNillableUpdatedAt(v *time.Time) *ChannelMonitorRequestTemplateCreate {
+ if v != nil {
+ _c.SetUpdatedAt(*v)
+ }
+ return _c
+}
+
+// SetName sets the "name" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetName(v string) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetName(v)
+ return _c
+}
+
+// SetProvider sets the "provider" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetProvider(v)
+ return _c
+}
+
+// SetDescription sets the "description" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetDescription(v string) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetDescription(v)
+ return _c
+}
+
+// SetNillableDescription sets the "description" field if the given value is not nil.
+func (_c *ChannelMonitorRequestTemplateCreate) SetNillableDescription(v *string) *ChannelMonitorRequestTemplateCreate {
+ if v != nil {
+ _c.SetDescription(*v)
+ }
+ return _c
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetExtraHeaders(v)
+ return _c
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetBodyOverrideMode(v)
+ return _c
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_c *ChannelMonitorRequestTemplateCreate) SetNillableBodyOverrideMode(v *string) *ChannelMonitorRequestTemplateCreate {
+ if v != nil {
+ _c.SetBodyOverrideMode(*v)
+ }
+ return _c
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_c *ChannelMonitorRequestTemplateCreate) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.SetBodyOverride(v)
+ return _c
+}
+
+// AddMonitorIDs adds the "monitors" edge to the ChannelMonitor entity by IDs.
+func (_c *ChannelMonitorRequestTemplateCreate) AddMonitorIDs(ids ...int64) *ChannelMonitorRequestTemplateCreate {
+ _c.mutation.AddMonitorIDs(ids...)
+ return _c
+}
+
+// AddMonitors adds the "monitors" edges to the ChannelMonitor entity.
+func (_c *ChannelMonitorRequestTemplateCreate) AddMonitors(v ...*ChannelMonitor) *ChannelMonitorRequestTemplateCreate {
+ ids := make([]int64, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
+ }
+ return _c.AddMonitorIDs(ids...)
+}
+
+// Mutation returns the ChannelMonitorRequestTemplateMutation object of the builder.
+func (_c *ChannelMonitorRequestTemplateCreate) Mutation() *ChannelMonitorRequestTemplateMutation {
+ return _c.mutation
+}
+
+// Save creates the ChannelMonitorRequestTemplate in the database.
+func (_c *ChannelMonitorRequestTemplateCreate) Save(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ _c.defaults()
+ return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
+}
+
+// SaveX calls Save and panics if Save returns an error.
+func (_c *ChannelMonitorRequestTemplateCreate) SaveX(ctx context.Context) *ChannelMonitorRequestTemplate {
+ v, err := _c.Save(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Exec executes the query.
+func (_c *ChannelMonitorRequestTemplateCreate) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
+ return err
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_c *ChannelMonitorRequestTemplateCreate) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
+
+// defaults sets the default values of the builder before save.
+func (_c *ChannelMonitorRequestTemplateCreate) defaults() {
+ if _, ok := _c.mutation.CreatedAt(); !ok {
+ v := channelmonitorrequesttemplate.DefaultCreatedAt()
+ _c.mutation.SetCreatedAt(v)
+ }
+ if _, ok := _c.mutation.UpdatedAt(); !ok {
+ v := channelmonitorrequesttemplate.DefaultUpdatedAt()
+ _c.mutation.SetUpdatedAt(v)
+ }
+ if _, ok := _c.mutation.Description(); !ok {
+ v := channelmonitorrequesttemplate.DefaultDescription
+ _c.mutation.SetDescription(v)
+ }
+ if _, ok := _c.mutation.ExtraHeaders(); !ok {
+ v := channelmonitorrequesttemplate.DefaultExtraHeaders
+ _c.mutation.SetExtraHeaders(v)
+ }
+ if _, ok := _c.mutation.BodyOverrideMode(); !ok {
+ v := channelmonitorrequesttemplate.DefaultBodyOverrideMode
+ _c.mutation.SetBodyOverrideMode(v)
+ }
+}
+
+// check runs all checks and user-defined validators on the builder.
+func (_c *ChannelMonitorRequestTemplateCreate) check() error {
+ if _, ok := _c.mutation.CreatedAt(); !ok {
+ return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.created_at"`)}
+ }
+ if _, ok := _c.mutation.UpdatedAt(); !ok {
+ return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.updated_at"`)}
+ }
+ if _, ok := _c.mutation.Name(); !ok {
+ return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.name"`)}
+ }
+ if v, ok := _c.mutation.Name(); ok {
+ if err := channelmonitorrequesttemplate.NameValidator(v); err != nil {
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.name": %w`, err)}
+ }
+ }
+ if _, ok := _c.mutation.Provider(); !ok {
+ return &ValidationError{Name: "provider", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.provider"`)}
+ }
+ if v, ok := _c.mutation.Provider(); ok {
+ if err := channelmonitorrequesttemplate.ProviderValidator(v); err != nil {
+ return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)}
+ }
+ }
+ if v, ok := _c.mutation.Description(); ok {
+ if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil {
+ return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)}
+ }
+ }
+ if _, ok := _c.mutation.ExtraHeaders(); !ok {
+ return &ValidationError{Name: "extra_headers", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.extra_headers"`)}
+ }
+ if _, ok := _c.mutation.BodyOverrideMode(); !ok {
+ return &ValidationError{Name: "body_override_mode", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.body_override_mode"`)}
+ }
+ if v, ok := _c.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitorrequesttemplate.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.body_override_mode": %w`, err)}
+ }
+ }
+ return nil
+}
+
+func (_c *ChannelMonitorRequestTemplateCreate) sqlSave(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ if err := _c.check(); err != nil {
+ return nil, err
+ }
+ _node, _spec := _c.createSpec()
+ if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
+ if sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ return nil, err
+ }
+ id := _spec.ID.Value.(int64)
+ _node.ID = int64(id)
+ _c.mutation.id = &_node.ID
+ _c.mutation.done = true
+ return _node, nil
+}
+
+func (_c *ChannelMonitorRequestTemplateCreate) createSpec() (*ChannelMonitorRequestTemplate, *sqlgraph.CreateSpec) {
+ var (
+ _node = &ChannelMonitorRequestTemplate{config: _c.config}
+ _spec = sqlgraph.NewCreateSpec(channelmonitorrequesttemplate.Table, sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64))
+ )
+ _spec.OnConflict = _c.conflict
+ if value, ok := _c.mutation.CreatedAt(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldCreatedAt, field.TypeTime, value)
+ _node.CreatedAt = value
+ }
+ if value, ok := _c.mutation.UpdatedAt(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldUpdatedAt, field.TypeTime, value)
+ _node.UpdatedAt = value
+ }
+ if value, ok := _c.mutation.Name(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldName, field.TypeString, value)
+ _node.Name = value
+ }
+ if value, ok := _c.mutation.Provider(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value)
+ _node.Provider = value
+ }
+ if value, ok := _c.mutation.Description(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value)
+ _node.Description = value
+ }
+ if value, ok := _c.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldExtraHeaders, field.TypeJSON, value)
+ _node.ExtraHeaders = value
+ }
+ if value, ok := _c.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverrideMode, field.TypeString, value)
+ _node.BodyOverrideMode = value
+ }
+ if value, ok := _c.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverride, field.TypeJSON, value)
+ _node.BodyOverride = value
+ }
+ if nodes := _c.mutation.MonitorsIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges = append(_spec.Edges, edge)
+ }
+ return _node, _spec
+}
+
+// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
+// of the `INSERT` statement. For example:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// SetCreatedAt(v).
+// OnConflict(
+// // Update the row with the new values
+// // the was proposed for insertion.
+// sql.ResolveWithNewValues(),
+// ).
+// // Override some of the fields with custom
+// // update values.
+// Update(func(u *ent.ChannelMonitorRequestTemplateUpsert) {
+// SetCreatedAt(v+v).
+// }).
+// Exec(ctx)
+func (_c *ChannelMonitorRequestTemplateCreate) OnConflict(opts ...sql.ConflictOption) *ChannelMonitorRequestTemplateUpsertOne {
+ _c.conflict = opts
+ return &ChannelMonitorRequestTemplateUpsertOne{
+ create: _c,
+ }
+}
+
+// OnConflictColumns calls `OnConflict` and configures the columns
+// as conflict target. Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(sql.ConflictColumns(columns...)).
+// Exec(ctx)
+func (_c *ChannelMonitorRequestTemplateCreate) OnConflictColumns(columns ...string) *ChannelMonitorRequestTemplateUpsertOne {
+ _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
+ return &ChannelMonitorRequestTemplateUpsertOne{
+ create: _c,
+ }
+}
+
+type (
+ // ChannelMonitorRequestTemplateUpsertOne is the builder for "upsert"-ing
+ // one ChannelMonitorRequestTemplate node.
+ ChannelMonitorRequestTemplateUpsertOne struct {
+ create *ChannelMonitorRequestTemplateCreate
+ }
+
+ // ChannelMonitorRequestTemplateUpsert is the "OnConflict" setter.
+ ChannelMonitorRequestTemplateUpsert struct {
+ *sql.UpdateSet
+ }
+)
+
+// SetUpdatedAt sets the "updated_at" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldUpdatedAt, v)
+ return u
+}
+
+// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateUpdatedAt() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldUpdatedAt)
+ return u
+}
+
+// SetName sets the "name" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetName(v string) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldName, v)
+ return u
+}
+
+// UpdateName sets the "name" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateName() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldName)
+ return u
+}
+
+// SetProvider sets the "provider" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldProvider, v)
+ return u
+}
+
+// UpdateProvider sets the "provider" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateProvider() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldProvider)
+ return u
+}
+
+// SetDescription sets the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetDescription(v string) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldDescription, v)
+ return u
+}
+
+// UpdateDescription sets the "description" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateDescription() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldDescription)
+ return u
+}
+
+// ClearDescription clears the value of the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsert) ClearDescription() *ChannelMonitorRequestTemplateUpsert {
+ u.SetNull(channelmonitorrequesttemplate.FieldDescription)
+ return u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldExtraHeaders, v)
+ return u
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateExtraHeaders() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldExtraHeaders)
+ return u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldBodyOverrideMode, v)
+ return u
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateBodyOverrideMode() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldBodyOverrideMode)
+ return u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsert) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateUpsert {
+ u.Set(channelmonitorrequesttemplate.FieldBodyOverride, v)
+ return u
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsert) UpdateBodyOverride() *ChannelMonitorRequestTemplateUpsert {
+ u.SetExcluded(channelmonitorrequesttemplate.FieldBodyOverride)
+ return u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsert) ClearBodyOverride() *ChannelMonitorRequestTemplateUpsert {
+ u.SetNull(channelmonitorrequesttemplate.FieldBodyOverride)
+ return u
+}
+
+// UpdateNewValues updates the mutable fields using the new values that were set on create.
+// Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(
+// sql.ResolveWithNewValues(),
+// ).
+// Exec(ctx)
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateNewValues() *ChannelMonitorRequestTemplateUpsertOne {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
+ u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
+ if _, exists := u.create.mutation.CreatedAt(); exists {
+ s.SetIgnore(channelmonitorrequesttemplate.FieldCreatedAt)
+ }
+ }))
+ return u
+}
+
+// Ignore sets each column to itself in case of conflict.
+// Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(sql.ResolveWithIgnore()).
+// Exec(ctx)
+func (u *ChannelMonitorRequestTemplateUpsertOne) Ignore() *ChannelMonitorRequestTemplateUpsertOne {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
+ return u
+}
+
+// DoNothing configures the conflict_action to `DO NOTHING`.
+// Supported only by SQLite and PostgreSQL.
+func (u *ChannelMonitorRequestTemplateUpsertOne) DoNothing() *ChannelMonitorRequestTemplateUpsertOne {
+ u.create.conflict = append(u.create.conflict, sql.DoNothing())
+ return u
+}
+
+// Update allows overriding fields `UPDATE` values. See the ChannelMonitorRequestTemplateCreate.OnConflict
+// documentation for more info.
+func (u *ChannelMonitorRequestTemplateUpsertOne) Update(set func(*ChannelMonitorRequestTemplateUpsert)) *ChannelMonitorRequestTemplateUpsertOne {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
+ set(&ChannelMonitorRequestTemplateUpsert{UpdateSet: update})
+ }))
+ return u
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetUpdatedAt(v)
+ })
+}
+
+// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateUpdatedAt() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateUpdatedAt()
+ })
+}
+
+// SetName sets the "name" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetName(v string) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetName(v)
+ })
+}
+
+// UpdateName sets the "name" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateName() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateName()
+ })
+}
+
+// SetProvider sets the "provider" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetProvider(v)
+ })
+}
+
+// UpdateProvider sets the "provider" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateProvider() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateProvider()
+ })
+}
+
+// SetDescription sets the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetDescription(v string) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetDescription(v)
+ })
+}
+
+// UpdateDescription sets the "description" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateDescription() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateDescription()
+ })
+}
+
+// ClearDescription clears the value of the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) ClearDescription() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.ClearDescription()
+ })
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetExtraHeaders(v)
+ })
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateExtraHeaders() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateExtraHeaders()
+ })
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetBodyOverrideMode(v)
+ })
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateBodyOverrideMode() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateBodyOverrideMode()
+ })
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetBodyOverride(v)
+ })
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateBodyOverride() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateBodyOverride()
+ })
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsertOne) ClearBodyOverride() *ChannelMonitorRequestTemplateUpsertOne {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.ClearBodyOverride()
+ })
+}
+
+// Exec executes the query.
+func (u *ChannelMonitorRequestTemplateUpsertOne) Exec(ctx context.Context) error {
+ if len(u.create.conflict) == 0 {
+ return errors.New("ent: missing options for ChannelMonitorRequestTemplateCreate.OnConflict")
+ }
+ return u.create.Exec(ctx)
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (u *ChannelMonitorRequestTemplateUpsertOne) ExecX(ctx context.Context) {
+ if err := u.create.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
+
+// Exec executes the UPSERT query and returns the inserted/updated ID.
+func (u *ChannelMonitorRequestTemplateUpsertOne) ID(ctx context.Context) (id int64, err error) {
+ node, err := u.create.Save(ctx)
+ if err != nil {
+ return id, err
+ }
+ return node.ID, nil
+}
+
+// IDX is like ID, but panics if an error occurs.
+func (u *ChannelMonitorRequestTemplateUpsertOne) IDX(ctx context.Context) int64 {
+ id, err := u.ID(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return id
+}
+
+// ChannelMonitorRequestTemplateCreateBulk is the builder for creating many ChannelMonitorRequestTemplate entities in bulk.
+type ChannelMonitorRequestTemplateCreateBulk struct {
+ config
+ err error
+ builders []*ChannelMonitorRequestTemplateCreate
+ conflict []sql.ConflictOption
+}
+
+// Save creates the ChannelMonitorRequestTemplate entities in the database.
+func (_c *ChannelMonitorRequestTemplateCreateBulk) Save(ctx context.Context) ([]*ChannelMonitorRequestTemplate, error) {
+ if _c.err != nil {
+ return nil, _c.err
+ }
+ specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
+ nodes := make([]*ChannelMonitorRequestTemplate, len(_c.builders))
+ mutators := make([]Mutator, len(_c.builders))
+ for i := range _c.builders {
+ func(i int, root context.Context) {
+ builder := _c.builders[i]
+ builder.defaults()
+ var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
+ mutation, ok := m.(*ChannelMonitorRequestTemplateMutation)
+ if !ok {
+ return nil, fmt.Errorf("unexpected mutation type %T", m)
+ }
+ if err := builder.check(); err != nil {
+ return nil, err
+ }
+ builder.mutation = mutation
+ var err error
+ nodes[i], specs[i] = builder.createSpec()
+ if i < len(mutators)-1 {
+ _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
+ } else {
+ spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
+ spec.OnConflict = _c.conflict
+ // Invoke the actual operation on the latest mutation in the chain.
+ if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
+ if sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ }
+ }
+ if err != nil {
+ return nil, err
+ }
+ mutation.id = &nodes[i].ID
+ if specs[i].ID.Value != nil {
+ id := specs[i].ID.Value.(int64)
+ nodes[i].ID = int64(id)
+ }
+ mutation.done = true
+ return nodes[i], nil
+ })
+ for i := len(builder.hooks) - 1; i >= 0; i-- {
+ mut = builder.hooks[i](mut)
+ }
+ mutators[i] = mut
+ }(i, ctx)
+ }
+ if len(mutators) > 0 {
+ if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
+ return nil, err
+ }
+ }
+ return nodes, nil
+}
+
+// SaveX is like Save, but panics if an error occurs.
+func (_c *ChannelMonitorRequestTemplateCreateBulk) SaveX(ctx context.Context) []*ChannelMonitorRequestTemplate {
+ v, err := _c.Save(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Exec executes the query.
+func (_c *ChannelMonitorRequestTemplateCreateBulk) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
+ return err
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_c *ChannelMonitorRequestTemplateCreateBulk) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
+
+// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
+// of the `INSERT` statement. For example:
+//
+// client.ChannelMonitorRequestTemplate.CreateBulk(builders...).
+// OnConflict(
+// // Update the row with the new values
+// // the was proposed for insertion.
+// sql.ResolveWithNewValues(),
+// ).
+// // Override some of the fields with custom
+// // update values.
+// Update(func(u *ent.ChannelMonitorRequestTemplateUpsert) {
+// SetCreatedAt(v+v).
+// }).
+// Exec(ctx)
+func (_c *ChannelMonitorRequestTemplateCreateBulk) OnConflict(opts ...sql.ConflictOption) *ChannelMonitorRequestTemplateUpsertBulk {
+ _c.conflict = opts
+ return &ChannelMonitorRequestTemplateUpsertBulk{
+ create: _c,
+ }
+}
+
+// OnConflictColumns calls `OnConflict` and configures the columns
+// as conflict target. Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(sql.ConflictColumns(columns...)).
+// Exec(ctx)
+func (_c *ChannelMonitorRequestTemplateCreateBulk) OnConflictColumns(columns ...string) *ChannelMonitorRequestTemplateUpsertBulk {
+ _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
+ return &ChannelMonitorRequestTemplateUpsertBulk{
+ create: _c,
+ }
+}
+
+// ChannelMonitorRequestTemplateUpsertBulk is the builder for "upsert"-ing
+// a bulk of ChannelMonitorRequestTemplate nodes.
+type ChannelMonitorRequestTemplateUpsertBulk struct {
+ create *ChannelMonitorRequestTemplateCreateBulk
+}
+
+// UpdateNewValues updates the mutable fields using the new values that
+// were set on create. Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(
+// sql.ResolveWithNewValues(),
+// ).
+// Exec(ctx)
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateNewValues() *ChannelMonitorRequestTemplateUpsertBulk {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
+ u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
+ for _, b := range u.create.builders {
+ if _, exists := b.mutation.CreatedAt(); exists {
+ s.SetIgnore(channelmonitorrequesttemplate.FieldCreatedAt)
+ }
+ }
+ }))
+ return u
+}
+
+// Ignore sets each column to itself in case of conflict.
+// Using this option is equivalent to using:
+//
+// client.ChannelMonitorRequestTemplate.Create().
+// OnConflict(sql.ResolveWithIgnore()).
+// Exec(ctx)
+func (u *ChannelMonitorRequestTemplateUpsertBulk) Ignore() *ChannelMonitorRequestTemplateUpsertBulk {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
+ return u
+}
+
+// DoNothing configures the conflict_action to `DO NOTHING`.
+// Supported only by SQLite and PostgreSQL.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) DoNothing() *ChannelMonitorRequestTemplateUpsertBulk {
+ u.create.conflict = append(u.create.conflict, sql.DoNothing())
+ return u
+}
+
+// Update allows overriding fields `UPDATE` values. See the ChannelMonitorRequestTemplateCreateBulk.OnConflict
+// documentation for more info.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) Update(set func(*ChannelMonitorRequestTemplateUpsert)) *ChannelMonitorRequestTemplateUpsertBulk {
+ u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
+ set(&ChannelMonitorRequestTemplateUpsert{UpdateSet: update})
+ }))
+ return u
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetUpdatedAt(v)
+ })
+}
+
+// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateUpdatedAt() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateUpdatedAt()
+ })
+}
+
+// SetName sets the "name" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetName(v string) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetName(v)
+ })
+}
+
+// UpdateName sets the "name" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateName() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateName()
+ })
+}
+
+// SetProvider sets the "provider" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetProvider(v)
+ })
+}
+
+// UpdateProvider sets the "provider" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateProvider() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateProvider()
+ })
+}
+
+// SetDescription sets the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetDescription(v string) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetDescription(v)
+ })
+}
+
+// UpdateDescription sets the "description" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateDescription() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateDescription()
+ })
+}
+
+// ClearDescription clears the value of the "description" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) ClearDescription() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.ClearDescription()
+ })
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetExtraHeaders(v)
+ })
+}
+
+// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateExtraHeaders() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateExtraHeaders()
+ })
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetBodyOverrideMode(v)
+ })
+}
+
+// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateBodyOverrideMode() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateBodyOverrideMode()
+ })
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.SetBodyOverride(v)
+ })
+}
+
+// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateBodyOverride() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.UpdateBodyOverride()
+ })
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) ClearBodyOverride() *ChannelMonitorRequestTemplateUpsertBulk {
+ return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) {
+ s.ClearBodyOverride()
+ })
+}
+
+// Exec executes the query.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) Exec(ctx context.Context) error {
+ if u.create.err != nil {
+ return u.create.err
+ }
+ for i, b := range u.create.builders {
+ if len(b.conflict) != 0 {
+ return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ChannelMonitorRequestTemplateCreateBulk instead", i)
+ }
+ }
+ if len(u.create.conflict) == 0 {
+ return errors.New("ent: missing options for ChannelMonitorRequestTemplateCreateBulk.OnConflict")
+ }
+ return u.create.Exec(ctx)
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (u *ChannelMonitorRequestTemplateUpsertBulk) ExecX(ctx context.Context) {
+ if err := u.create.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
diff --git a/backend/ent/channelmonitorrequesttemplate_delete.go b/backend/ent/channelmonitorrequesttemplate_delete.go
new file mode 100644
index 00000000..98d365c8
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate_delete.go
@@ -0,0 +1,88 @@
+// Code generated by ent, DO NOT EDIT.
+
+package ent
+
+import (
+ "context"
+
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "entgo.io/ent/schema/field"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+ "github.com/Wei-Shaw/sub2api/ent/predicate"
+)
+
+// ChannelMonitorRequestTemplateDelete is the builder for deleting a ChannelMonitorRequestTemplate entity.
+type ChannelMonitorRequestTemplateDelete struct {
+ config
+ hooks []Hook
+ mutation *ChannelMonitorRequestTemplateMutation
+}
+
+// Where appends a list predicates to the ChannelMonitorRequestTemplateDelete builder.
+func (_d *ChannelMonitorRequestTemplateDelete) Where(ps ...predicate.ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateDelete {
+ _d.mutation.Where(ps...)
+ return _d
+}
+
+// Exec executes the deletion query and returns how many vertices were deleted.
+func (_d *ChannelMonitorRequestTemplateDelete) Exec(ctx context.Context) (int, error) {
+ return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_d *ChannelMonitorRequestTemplateDelete) ExecX(ctx context.Context) int {
+ n, err := _d.Exec(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return n
+}
+
+func (_d *ChannelMonitorRequestTemplateDelete) sqlExec(ctx context.Context) (int, error) {
+ _spec := sqlgraph.NewDeleteSpec(channelmonitorrequesttemplate.Table, sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64))
+ if ps := _d.mutation.predicates; len(ps) > 0 {
+ _spec.Predicate = func(selector *sql.Selector) {
+ for i := range ps {
+ ps[i](selector)
+ }
+ }
+ }
+ affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
+ if err != nil && sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ _d.mutation.done = true
+ return affected, err
+}
+
+// ChannelMonitorRequestTemplateDeleteOne is the builder for deleting a single ChannelMonitorRequestTemplate entity.
+type ChannelMonitorRequestTemplateDeleteOne struct {
+ _d *ChannelMonitorRequestTemplateDelete
+}
+
+// Where appends a list predicates to the ChannelMonitorRequestTemplateDelete builder.
+func (_d *ChannelMonitorRequestTemplateDeleteOne) Where(ps ...predicate.ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateDeleteOne {
+ _d._d.mutation.Where(ps...)
+ return _d
+}
+
+// Exec executes the deletion query.
+func (_d *ChannelMonitorRequestTemplateDeleteOne) Exec(ctx context.Context) error {
+ n, err := _d._d.Exec(ctx)
+ switch {
+ case err != nil:
+ return err
+ case n == 0:
+ return &NotFoundError{channelmonitorrequesttemplate.Label}
+ default:
+ return nil
+ }
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_d *ChannelMonitorRequestTemplateDeleteOne) ExecX(ctx context.Context) {
+ if err := _d.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
diff --git a/backend/ent/channelmonitorrequesttemplate_query.go b/backend/ent/channelmonitorrequesttemplate_query.go
new file mode 100644
index 00000000..6491ea60
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate_query.go
@@ -0,0 +1,648 @@
+// Code generated by ent, DO NOT EDIT.
+
+package ent
+
+import (
+ "context"
+ "database/sql/driver"
+ "fmt"
+ "math"
+
+ "entgo.io/ent"
+ "entgo.io/ent/dialect"
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "entgo.io/ent/schema/field"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitor"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+ "github.com/Wei-Shaw/sub2api/ent/predicate"
+)
+
+// ChannelMonitorRequestTemplateQuery is the builder for querying ChannelMonitorRequestTemplate entities.
+type ChannelMonitorRequestTemplateQuery struct {
+ config
+ ctx *QueryContext
+ order []channelmonitorrequesttemplate.OrderOption
+ inters []Interceptor
+ predicates []predicate.ChannelMonitorRequestTemplate
+ withMonitors *ChannelMonitorQuery
+ modifiers []func(*sql.Selector)
+ // intermediate query (i.e. traversal path).
+ sql *sql.Selector
+ path func(context.Context) (*sql.Selector, error)
+}
+
+// Where adds a new predicate for the ChannelMonitorRequestTemplateQuery builder.
+func (_q *ChannelMonitorRequestTemplateQuery) Where(ps ...predicate.ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateQuery {
+ _q.predicates = append(_q.predicates, ps...)
+ return _q
+}
+
+// Limit the number of records to be returned by this query.
+func (_q *ChannelMonitorRequestTemplateQuery) Limit(limit int) *ChannelMonitorRequestTemplateQuery {
+ _q.ctx.Limit = &limit
+ return _q
+}
+
+// Offset to start from.
+func (_q *ChannelMonitorRequestTemplateQuery) Offset(offset int) *ChannelMonitorRequestTemplateQuery {
+ _q.ctx.Offset = &offset
+ return _q
+}
+
+// Unique configures the query builder to filter duplicate records on query.
+// By default, unique is set to true, and can be disabled using this method.
+func (_q *ChannelMonitorRequestTemplateQuery) Unique(unique bool) *ChannelMonitorRequestTemplateQuery {
+ _q.ctx.Unique = &unique
+ return _q
+}
+
+// Order specifies how the records should be ordered.
+func (_q *ChannelMonitorRequestTemplateQuery) Order(o ...channelmonitorrequesttemplate.OrderOption) *ChannelMonitorRequestTemplateQuery {
+ _q.order = append(_q.order, o...)
+ return _q
+}
+
+// QueryMonitors chains the current query on the "monitors" edge.
+func (_q *ChannelMonitorRequestTemplateQuery) QueryMonitors() *ChannelMonitorQuery {
+ query := (&ChannelMonitorClient{config: _q.config}).Query()
+ query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
+ if err := _q.prepareQuery(ctx); err != nil {
+ return nil, err
+ }
+ selector := _q.sqlQuery(ctx)
+ if err := selector.Err(); err != nil {
+ return nil, err
+ }
+ step := sqlgraph.NewStep(
+ sqlgraph.From(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.FieldID, selector),
+ sqlgraph.To(channelmonitor.Table, channelmonitor.FieldID),
+ sqlgraph.Edge(sqlgraph.O2M, true, channelmonitorrequesttemplate.MonitorsTable, channelmonitorrequesttemplate.MonitorsColumn),
+ )
+ fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
+ return fromU, nil
+ }
+ return query
+}
+
+// First returns the first ChannelMonitorRequestTemplate entity from the query.
+// Returns a *NotFoundError when no ChannelMonitorRequestTemplate was found.
+func (_q *ChannelMonitorRequestTemplateQuery) First(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
+ if err != nil {
+ return nil, err
+ }
+ if len(nodes) == 0 {
+ return nil, &NotFoundError{channelmonitorrequesttemplate.Label}
+ }
+ return nodes[0], nil
+}
+
+// FirstX is like First, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) FirstX(ctx context.Context) *ChannelMonitorRequestTemplate {
+ node, err := _q.First(ctx)
+ if err != nil && !IsNotFound(err) {
+ panic(err)
+ }
+ return node
+}
+
+// FirstID returns the first ChannelMonitorRequestTemplate ID from the query.
+// Returns a *NotFoundError when no ChannelMonitorRequestTemplate ID was found.
+func (_q *ChannelMonitorRequestTemplateQuery) FirstID(ctx context.Context) (id int64, err error) {
+ var ids []int64
+ if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
+ return
+ }
+ if len(ids) == 0 {
+ err = &NotFoundError{channelmonitorrequesttemplate.Label}
+ return
+ }
+ return ids[0], nil
+}
+
+// FirstIDX is like FirstID, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) FirstIDX(ctx context.Context) int64 {
+ id, err := _q.FirstID(ctx)
+ if err != nil && !IsNotFound(err) {
+ panic(err)
+ }
+ return id
+}
+
+// Only returns a single ChannelMonitorRequestTemplate entity found by the query, ensuring it only returns one.
+// Returns a *NotSingularError when more than one ChannelMonitorRequestTemplate entity is found.
+// Returns a *NotFoundError when no ChannelMonitorRequestTemplate entities are found.
+func (_q *ChannelMonitorRequestTemplateQuery) Only(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
+ if err != nil {
+ return nil, err
+ }
+ switch len(nodes) {
+ case 1:
+ return nodes[0], nil
+ case 0:
+ return nil, &NotFoundError{channelmonitorrequesttemplate.Label}
+ default:
+ return nil, &NotSingularError{channelmonitorrequesttemplate.Label}
+ }
+}
+
+// OnlyX is like Only, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) OnlyX(ctx context.Context) *ChannelMonitorRequestTemplate {
+ node, err := _q.Only(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return node
+}
+
+// OnlyID is like Only, but returns the only ChannelMonitorRequestTemplate ID in the query.
+// Returns a *NotSingularError when more than one ChannelMonitorRequestTemplate ID is found.
+// Returns a *NotFoundError when no entities are found.
+func (_q *ChannelMonitorRequestTemplateQuery) OnlyID(ctx context.Context) (id int64, err error) {
+ var ids []int64
+ if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
+ return
+ }
+ switch len(ids) {
+ case 1:
+ id = ids[0]
+ case 0:
+ err = &NotFoundError{channelmonitorrequesttemplate.Label}
+ default:
+ err = &NotSingularError{channelmonitorrequesttemplate.Label}
+ }
+ return
+}
+
+// OnlyIDX is like OnlyID, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) OnlyIDX(ctx context.Context) int64 {
+ id, err := _q.OnlyID(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return id
+}
+
+// All executes the query and returns a list of ChannelMonitorRequestTemplates.
+func (_q *ChannelMonitorRequestTemplateQuery) All(ctx context.Context) ([]*ChannelMonitorRequestTemplate, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
+ if err := _q.prepareQuery(ctx); err != nil {
+ return nil, err
+ }
+ qr := querierAll[[]*ChannelMonitorRequestTemplate, *ChannelMonitorRequestTemplateQuery]()
+ return withInterceptors[[]*ChannelMonitorRequestTemplate](ctx, _q, qr, _q.inters)
+}
+
+// AllX is like All, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) AllX(ctx context.Context) []*ChannelMonitorRequestTemplate {
+ nodes, err := _q.All(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return nodes
+}
+
+// IDs executes the query and returns a list of ChannelMonitorRequestTemplate IDs.
+func (_q *ChannelMonitorRequestTemplateQuery) IDs(ctx context.Context) (ids []int64, err error) {
+ if _q.ctx.Unique == nil && _q.path != nil {
+ _q.Unique(true)
+ }
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
+ if err = _q.Select(channelmonitorrequesttemplate.FieldID).Scan(ctx, &ids); err != nil {
+ return nil, err
+ }
+ return ids, nil
+}
+
+// IDsX is like IDs, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) IDsX(ctx context.Context) []int64 {
+ ids, err := _q.IDs(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return ids
+}
+
+// Count returns the count of the given query.
+func (_q *ChannelMonitorRequestTemplateQuery) Count(ctx context.Context) (int, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
+ if err := _q.prepareQuery(ctx); err != nil {
+ return 0, err
+ }
+ return withInterceptors[int](ctx, _q, querierCount[*ChannelMonitorRequestTemplateQuery](), _q.inters)
+}
+
+// CountX is like Count, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) CountX(ctx context.Context) int {
+ count, err := _q.Count(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return count
+}
+
+// Exist returns true if the query has elements in the graph.
+func (_q *ChannelMonitorRequestTemplateQuery) Exist(ctx context.Context) (bool, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
+ switch _, err := _q.FirstID(ctx); {
+ case IsNotFound(err):
+ return false, nil
+ case err != nil:
+ return false, fmt.Errorf("ent: check existence: %w", err)
+ default:
+ return true, nil
+ }
+}
+
+// ExistX is like Exist, but panics if an error occurs.
+func (_q *ChannelMonitorRequestTemplateQuery) ExistX(ctx context.Context) bool {
+ exist, err := _q.Exist(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return exist
+}
+
+// Clone returns a duplicate of the ChannelMonitorRequestTemplateQuery builder, including all associated steps. It can be
+// used to prepare common query builders and use them differently after the clone is made.
+func (_q *ChannelMonitorRequestTemplateQuery) Clone() *ChannelMonitorRequestTemplateQuery {
+ if _q == nil {
+ return nil
+ }
+ return &ChannelMonitorRequestTemplateQuery{
+ config: _q.config,
+ ctx: _q.ctx.Clone(),
+ order: append([]channelmonitorrequesttemplate.OrderOption{}, _q.order...),
+ inters: append([]Interceptor{}, _q.inters...),
+ predicates: append([]predicate.ChannelMonitorRequestTemplate{}, _q.predicates...),
+ withMonitors: _q.withMonitors.Clone(),
+ // clone intermediate query.
+ sql: _q.sql.Clone(),
+ path: _q.path,
+ }
+}
+
+// WithMonitors tells the query-builder to eager-load the nodes that are connected to
+// the "monitors" edge. The optional arguments are used to configure the query builder of the edge.
+func (_q *ChannelMonitorRequestTemplateQuery) WithMonitors(opts ...func(*ChannelMonitorQuery)) *ChannelMonitorRequestTemplateQuery {
+ query := (&ChannelMonitorClient{config: _q.config}).Query()
+ for _, opt := range opts {
+ opt(query)
+ }
+ _q.withMonitors = query
+ return _q
+}
+
+// GroupBy is used to group vertices by one or more fields/columns.
+// It is often used with aggregate functions, like: count, max, mean, min, sum.
+//
+// Example:
+//
+// var v []struct {
+// CreatedAt time.Time `json:"created_at,omitempty"`
+// Count int `json:"count,omitempty"`
+// }
+//
+// client.ChannelMonitorRequestTemplate.Query().
+// GroupBy(channelmonitorrequesttemplate.FieldCreatedAt).
+// Aggregate(ent.Count()).
+// Scan(ctx, &v)
+func (_q *ChannelMonitorRequestTemplateQuery) GroupBy(field string, fields ...string) *ChannelMonitorRequestTemplateGroupBy {
+ _q.ctx.Fields = append([]string{field}, fields...)
+ grbuild := &ChannelMonitorRequestTemplateGroupBy{build: _q}
+ grbuild.flds = &_q.ctx.Fields
+ grbuild.label = channelmonitorrequesttemplate.Label
+ grbuild.scan = grbuild.Scan
+ return grbuild
+}
+
+// Select allows the selection one or more fields/columns for the given query,
+// instead of selecting all fields in the entity.
+//
+// Example:
+//
+// var v []struct {
+// CreatedAt time.Time `json:"created_at,omitempty"`
+// }
+//
+// client.ChannelMonitorRequestTemplate.Query().
+// Select(channelmonitorrequesttemplate.FieldCreatedAt).
+// Scan(ctx, &v)
+func (_q *ChannelMonitorRequestTemplateQuery) Select(fields ...string) *ChannelMonitorRequestTemplateSelect {
+ _q.ctx.Fields = append(_q.ctx.Fields, fields...)
+ sbuild := &ChannelMonitorRequestTemplateSelect{ChannelMonitorRequestTemplateQuery: _q}
+ sbuild.label = channelmonitorrequesttemplate.Label
+ sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
+ return sbuild
+}
+
+// Aggregate returns a ChannelMonitorRequestTemplateSelect configured with the given aggregations.
+func (_q *ChannelMonitorRequestTemplateQuery) Aggregate(fns ...AggregateFunc) *ChannelMonitorRequestTemplateSelect {
+ return _q.Select().Aggregate(fns...)
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) prepareQuery(ctx context.Context) error {
+ for _, inter := range _q.inters {
+ if inter == nil {
+ return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
+ }
+ if trv, ok := inter.(Traverser); ok {
+ if err := trv.Traverse(ctx, _q); err != nil {
+ return err
+ }
+ }
+ }
+ for _, f := range _q.ctx.Fields {
+ if !channelmonitorrequesttemplate.ValidColumn(f) {
+ return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
+ }
+ }
+ if _q.path != nil {
+ prev, err := _q.path(ctx)
+ if err != nil {
+ return err
+ }
+ _q.sql = prev
+ }
+ return nil
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*ChannelMonitorRequestTemplate, error) {
+ var (
+ nodes = []*ChannelMonitorRequestTemplate{}
+ _spec = _q.querySpec()
+ loadedTypes = [1]bool{
+ _q.withMonitors != nil,
+ }
+ )
+ _spec.ScanValues = func(columns []string) ([]any, error) {
+ return (*ChannelMonitorRequestTemplate).scanValues(nil, columns)
+ }
+ _spec.Assign = func(columns []string, values []any) error {
+ node := &ChannelMonitorRequestTemplate{config: _q.config}
+ nodes = append(nodes, node)
+ node.Edges.loadedTypes = loadedTypes
+ return node.assignValues(columns, values)
+ }
+ if len(_q.modifiers) > 0 {
+ _spec.Modifiers = _q.modifiers
+ }
+ for i := range hooks {
+ hooks[i](ctx, _spec)
+ }
+ if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
+ return nil, err
+ }
+ if len(nodes) == 0 {
+ return nodes, nil
+ }
+ if query := _q.withMonitors; query != nil {
+ if err := _q.loadMonitors(ctx, query, nodes,
+ func(n *ChannelMonitorRequestTemplate) { n.Edges.Monitors = []*ChannelMonitor{} },
+ func(n *ChannelMonitorRequestTemplate, e *ChannelMonitor) {
+ n.Edges.Monitors = append(n.Edges.Monitors, e)
+ }); err != nil {
+ return nil, err
+ }
+ }
+ return nodes, nil
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) loadMonitors(ctx context.Context, query *ChannelMonitorQuery, nodes []*ChannelMonitorRequestTemplate, init func(*ChannelMonitorRequestTemplate), assign func(*ChannelMonitorRequestTemplate, *ChannelMonitor)) error {
+ fks := make([]driver.Value, 0, len(nodes))
+ nodeids := make(map[int64]*ChannelMonitorRequestTemplate)
+ for i := range nodes {
+ fks = append(fks, nodes[i].ID)
+ nodeids[nodes[i].ID] = nodes[i]
+ if init != nil {
+ init(nodes[i])
+ }
+ }
+ if len(query.ctx.Fields) > 0 {
+ query.ctx.AppendFieldOnce(channelmonitor.FieldTemplateID)
+ }
+ query.Where(predicate.ChannelMonitor(func(s *sql.Selector) {
+ s.Where(sql.InValues(s.C(channelmonitorrequesttemplate.MonitorsColumn), fks...))
+ }))
+ neighbors, err := query.All(ctx)
+ if err != nil {
+ return err
+ }
+ for _, n := range neighbors {
+ fk := n.TemplateID
+ if fk == nil {
+ return fmt.Errorf(`foreign-key "template_id" is nil for node %v`, n.ID)
+ }
+ node, ok := nodeids[*fk]
+ if !ok {
+ return fmt.Errorf(`unexpected referenced foreign-key "template_id" returned %v for node %v`, *fk, n.ID)
+ }
+ assign(node, n)
+ }
+ return nil
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) sqlCount(ctx context.Context) (int, error) {
+ _spec := _q.querySpec()
+ if len(_q.modifiers) > 0 {
+ _spec.Modifiers = _q.modifiers
+ }
+ _spec.Node.Columns = _q.ctx.Fields
+ if len(_q.ctx.Fields) > 0 {
+ _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
+ }
+ return sqlgraph.CountNodes(ctx, _q.driver, _spec)
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) querySpec() *sqlgraph.QuerySpec {
+ _spec := sqlgraph.NewQuerySpec(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.Columns, sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64))
+ _spec.From = _q.sql
+ if unique := _q.ctx.Unique; unique != nil {
+ _spec.Unique = *unique
+ } else if _q.path != nil {
+ _spec.Unique = true
+ }
+ if fields := _q.ctx.Fields; len(fields) > 0 {
+ _spec.Node.Columns = make([]string, 0, len(fields))
+ _spec.Node.Columns = append(_spec.Node.Columns, channelmonitorrequesttemplate.FieldID)
+ for i := range fields {
+ if fields[i] != channelmonitorrequesttemplate.FieldID {
+ _spec.Node.Columns = append(_spec.Node.Columns, fields[i])
+ }
+ }
+ }
+ if ps := _q.predicates; len(ps) > 0 {
+ _spec.Predicate = func(selector *sql.Selector) {
+ for i := range ps {
+ ps[i](selector)
+ }
+ }
+ }
+ if limit := _q.ctx.Limit; limit != nil {
+ _spec.Limit = *limit
+ }
+ if offset := _q.ctx.Offset; offset != nil {
+ _spec.Offset = *offset
+ }
+ if ps := _q.order; len(ps) > 0 {
+ _spec.Order = func(selector *sql.Selector) {
+ for i := range ps {
+ ps[i](selector)
+ }
+ }
+ }
+ return _spec
+}
+
+func (_q *ChannelMonitorRequestTemplateQuery) sqlQuery(ctx context.Context) *sql.Selector {
+ builder := sql.Dialect(_q.driver.Dialect())
+ t1 := builder.Table(channelmonitorrequesttemplate.Table)
+ columns := _q.ctx.Fields
+ if len(columns) == 0 {
+ columns = channelmonitorrequesttemplate.Columns
+ }
+ selector := builder.Select(t1.Columns(columns...)...).From(t1)
+ if _q.sql != nil {
+ selector = _q.sql
+ selector.Select(selector.Columns(columns...)...)
+ }
+ if _q.ctx.Unique != nil && *_q.ctx.Unique {
+ selector.Distinct()
+ }
+ for _, m := range _q.modifiers {
+ m(selector)
+ }
+ for _, p := range _q.predicates {
+ p(selector)
+ }
+ for _, p := range _q.order {
+ p(selector)
+ }
+ if offset := _q.ctx.Offset; offset != nil {
+ // limit is mandatory for offset clause. We start
+ // with default value, and override it below if needed.
+ selector.Offset(*offset).Limit(math.MaxInt32)
+ }
+ if limit := _q.ctx.Limit; limit != nil {
+ selector.Limit(*limit)
+ }
+ return selector
+}
+
+// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
+// updated, deleted or "selected ... for update" by other sessions, until the transaction is
+// either committed or rolled-back.
+func (_q *ChannelMonitorRequestTemplateQuery) ForUpdate(opts ...sql.LockOption) *ChannelMonitorRequestTemplateQuery {
+ if _q.driver.Dialect() == dialect.Postgres {
+ _q.Unique(false)
+ }
+ _q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
+ s.ForUpdate(opts...)
+ })
+ return _q
+}
+
+// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
+// on any rows that are read. Other sessions can read the rows, but cannot modify them
+// until your transaction commits.
+func (_q *ChannelMonitorRequestTemplateQuery) ForShare(opts ...sql.LockOption) *ChannelMonitorRequestTemplateQuery {
+ if _q.driver.Dialect() == dialect.Postgres {
+ _q.Unique(false)
+ }
+ _q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
+ s.ForShare(opts...)
+ })
+ return _q
+}
+
+// ChannelMonitorRequestTemplateGroupBy is the group-by builder for ChannelMonitorRequestTemplate entities.
+type ChannelMonitorRequestTemplateGroupBy struct {
+ selector
+ build *ChannelMonitorRequestTemplateQuery
+}
+
+// Aggregate adds the given aggregation functions to the group-by query.
+func (_g *ChannelMonitorRequestTemplateGroupBy) Aggregate(fns ...AggregateFunc) *ChannelMonitorRequestTemplateGroupBy {
+ _g.fns = append(_g.fns, fns...)
+ return _g
+}
+
+// Scan applies the selector query and scans the result into the given value.
+func (_g *ChannelMonitorRequestTemplateGroupBy) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
+ if err := _g.build.prepareQuery(ctx); err != nil {
+ return err
+ }
+ return scanWithInterceptors[*ChannelMonitorRequestTemplateQuery, *ChannelMonitorRequestTemplateGroupBy](ctx, _g.build, _g, _g.build.inters, v)
+}
+
+func (_g *ChannelMonitorRequestTemplateGroupBy) sqlScan(ctx context.Context, root *ChannelMonitorRequestTemplateQuery, v any) error {
+ selector := root.sqlQuery(ctx).Select()
+ aggregation := make([]string, 0, len(_g.fns))
+ for _, fn := range _g.fns {
+ aggregation = append(aggregation, fn(selector))
+ }
+ if len(selector.SelectedColumns()) == 0 {
+ columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
+ for _, f := range *_g.flds {
+ columns = append(columns, selector.C(f))
+ }
+ columns = append(columns, aggregation...)
+ selector.Select(columns...)
+ }
+ selector.GroupBy(selector.Columns(*_g.flds...)...)
+ if err := selector.Err(); err != nil {
+ return err
+ }
+ rows := &sql.Rows{}
+ query, args := selector.Query()
+ if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
+ return err
+ }
+ defer rows.Close()
+ return sql.ScanSlice(rows, v)
+}
+
+// ChannelMonitorRequestTemplateSelect is the builder for selecting fields of ChannelMonitorRequestTemplate entities.
+type ChannelMonitorRequestTemplateSelect struct {
+ *ChannelMonitorRequestTemplateQuery
+ selector
+}
+
+// Aggregate adds the given aggregation functions to the selector query.
+func (_s *ChannelMonitorRequestTemplateSelect) Aggregate(fns ...AggregateFunc) *ChannelMonitorRequestTemplateSelect {
+ _s.fns = append(_s.fns, fns...)
+ return _s
+}
+
+// Scan applies the selector query and scans the result into the given value.
+func (_s *ChannelMonitorRequestTemplateSelect) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
+ if err := _s.prepareQuery(ctx); err != nil {
+ return err
+ }
+ return scanWithInterceptors[*ChannelMonitorRequestTemplateQuery, *ChannelMonitorRequestTemplateSelect](ctx, _s.ChannelMonitorRequestTemplateQuery, _s, _s.inters, v)
+}
+
+func (_s *ChannelMonitorRequestTemplateSelect) sqlScan(ctx context.Context, root *ChannelMonitorRequestTemplateQuery, v any) error {
+ selector := root.sqlQuery(ctx)
+ aggregation := make([]string, 0, len(_s.fns))
+ for _, fn := range _s.fns {
+ aggregation = append(aggregation, fn(selector))
+ }
+ switch n := len(*_s.selector.flds); {
+ case n == 0 && len(aggregation) > 0:
+ selector.Select(aggregation...)
+ case n != 0 && len(aggregation) > 0:
+ selector.AppendSelect(aggregation...)
+ }
+ rows := &sql.Rows{}
+ query, args := selector.Query()
+ if err := _s.driver.Query(ctx, query, args, rows); err != nil {
+ return err
+ }
+ defer rows.Close()
+ return sql.ScanSlice(rows, v)
+}
diff --git a/backend/ent/channelmonitorrequesttemplate_update.go b/backend/ent/channelmonitorrequesttemplate_update.go
new file mode 100644
index 00000000..8f55ba04
--- /dev/null
+++ b/backend/ent/channelmonitorrequesttemplate_update.go
@@ -0,0 +1,639 @@
+// Code generated by ent, DO NOT EDIT.
+
+package ent
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "entgo.io/ent/schema/field"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitor"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+ "github.com/Wei-Shaw/sub2api/ent/predicate"
+)
+
+// ChannelMonitorRequestTemplateUpdate is the builder for updating ChannelMonitorRequestTemplate entities.
+type ChannelMonitorRequestTemplateUpdate struct {
+ config
+ hooks []Hook
+ mutation *ChannelMonitorRequestTemplateMutation
+}
+
+// Where appends a list predicates to the ChannelMonitorRequestTemplateUpdate builder.
+func (_u *ChannelMonitorRequestTemplateUpdate) Where(ps ...predicate.ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.Where(ps...)
+ return _u
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetUpdatedAt(v)
+ return _u
+}
+
+// SetName sets the "name" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetName(v string) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetName(v)
+ return _u
+}
+
+// SetNillableName sets the "name" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableName(v *string) *ChannelMonitorRequestTemplateUpdate {
+ if v != nil {
+ _u.SetName(*v)
+ }
+ return _u
+}
+
+// SetProvider sets the "provider" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetProvider(v)
+ return _u
+}
+
+// SetNillableProvider sets the "provider" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableProvider(v *channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpdate {
+ if v != nil {
+ _u.SetProvider(*v)
+ }
+ return _u
+}
+
+// SetDescription sets the "description" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetDescription(v string) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetDescription(v)
+ return _u
+}
+
+// SetNillableDescription sets the "description" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableDescription(v *string) *ChannelMonitorRequestTemplateUpdate {
+ if v != nil {
+ _u.SetDescription(*v)
+ }
+ return _u
+}
+
+// ClearDescription clears the value of the "description" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) ClearDescription() *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.ClearDescription()
+ return _u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetExtraHeaders(v)
+ return _u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetBodyOverrideMode(v)
+ return _u
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableBodyOverrideMode(v *string) *ChannelMonitorRequestTemplateUpdate {
+ if v != nil {
+ _u.SetBodyOverrideMode(*v)
+ }
+ return _u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.SetBodyOverride(v)
+ return _u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (_u *ChannelMonitorRequestTemplateUpdate) ClearBodyOverride() *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.ClearBodyOverride()
+ return _u
+}
+
+// AddMonitorIDs adds the "monitors" edge to the ChannelMonitor entity by IDs.
+func (_u *ChannelMonitorRequestTemplateUpdate) AddMonitorIDs(ids ...int64) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.AddMonitorIDs(ids...)
+ return _u
+}
+
+// AddMonitors adds the "monitors" edges to the ChannelMonitor entity.
+func (_u *ChannelMonitorRequestTemplateUpdate) AddMonitors(v ...*ChannelMonitor) *ChannelMonitorRequestTemplateUpdate {
+ ids := make([]int64, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
+ }
+ return _u.AddMonitorIDs(ids...)
+}
+
+// Mutation returns the ChannelMonitorRequestTemplateMutation object of the builder.
+func (_u *ChannelMonitorRequestTemplateUpdate) Mutation() *ChannelMonitorRequestTemplateMutation {
+ return _u.mutation
+}
+
+// ClearMonitors clears all "monitors" edges to the ChannelMonitor entity.
+func (_u *ChannelMonitorRequestTemplateUpdate) ClearMonitors() *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.ClearMonitors()
+ return _u
+}
+
+// RemoveMonitorIDs removes the "monitors" edge to ChannelMonitor entities by IDs.
+func (_u *ChannelMonitorRequestTemplateUpdate) RemoveMonitorIDs(ids ...int64) *ChannelMonitorRequestTemplateUpdate {
+ _u.mutation.RemoveMonitorIDs(ids...)
+ return _u
+}
+
+// RemoveMonitors removes "monitors" edges to ChannelMonitor entities.
+func (_u *ChannelMonitorRequestTemplateUpdate) RemoveMonitors(v ...*ChannelMonitor) *ChannelMonitorRequestTemplateUpdate {
+ ids := make([]int64, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
+ }
+ return _u.RemoveMonitorIDs(ids...)
+}
+
+// Save executes the query and returns the number of nodes affected by the update operation.
+func (_u *ChannelMonitorRequestTemplateUpdate) Save(ctx context.Context) (int, error) {
+ _u.defaults()
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
+}
+
+// SaveX is like Save, but panics if an error occurs.
+func (_u *ChannelMonitorRequestTemplateUpdate) SaveX(ctx context.Context) int {
+ affected, err := _u.Save(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return affected
+}
+
+// Exec executes the query.
+func (_u *ChannelMonitorRequestTemplateUpdate) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
+ return err
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_u *ChannelMonitorRequestTemplateUpdate) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
+
+// defaults sets the default values of the builder before save.
+func (_u *ChannelMonitorRequestTemplateUpdate) defaults() {
+ if _, ok := _u.mutation.UpdatedAt(); !ok {
+ v := channelmonitorrequesttemplate.UpdateDefaultUpdatedAt()
+ _u.mutation.SetUpdatedAt(v)
+ }
+}
+
+// check runs all checks and user-defined validators on the builder.
+func (_u *ChannelMonitorRequestTemplateUpdate) check() error {
+ if v, ok := _u.mutation.Name(); ok {
+ if err := channelmonitorrequesttemplate.NameValidator(v); err != nil {
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.name": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.Provider(); ok {
+ if err := channelmonitorrequesttemplate.ProviderValidator(v); err != nil {
+ return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.Description(); ok {
+ if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil {
+ return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitorrequesttemplate.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.body_override_mode": %w`, err)}
+ }
+ }
+ return nil
+}
+
+func (_u *ChannelMonitorRequestTemplateUpdate) sqlSave(ctx context.Context) (_node int, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
+ }
+ _spec := sqlgraph.NewUpdateSpec(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.Columns, sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64))
+ if ps := _u.mutation.predicates; len(ps) > 0 {
+ _spec.Predicate = func(selector *sql.Selector) {
+ for i := range ps {
+ ps[i](selector)
+ }
+ }
+ }
+ if value, ok := _u.mutation.UpdatedAt(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldUpdatedAt, field.TypeTime, value)
+ }
+ if value, ok := _u.mutation.Name(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldName, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.Provider(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value)
+ }
+ if value, ok := _u.mutation.Description(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value)
+ }
+ if _u.mutation.DescriptionCleared() {
+ _spec.ClearField(channelmonitorrequesttemplate.FieldDescription, field.TypeString)
+ }
+ if value, ok := _u.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldExtraHeaders, field.TypeJSON, value)
+ }
+ if value, ok := _u.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverrideMode, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverride, field.TypeJSON, value)
+ }
+ if _u.mutation.BodyOverrideCleared() {
+ _spec.ClearField(channelmonitorrequesttemplate.FieldBodyOverride, field.TypeJSON)
+ }
+ if _u.mutation.MonitorsCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.RemovedMonitorsIDs(); len(nodes) > 0 && !_u.mutation.MonitorsCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.MonitorsIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Add = append(_spec.Edges.Add, edge)
+ }
+ if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
+ if _, ok := err.(*sqlgraph.NotFoundError); ok {
+ err = &NotFoundError{channelmonitorrequesttemplate.Label}
+ } else if sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ return 0, err
+ }
+ _u.mutation.done = true
+ return _node, nil
+}
+
+// ChannelMonitorRequestTemplateUpdateOne is the builder for updating a single ChannelMonitorRequestTemplate entity.
+type ChannelMonitorRequestTemplateUpdateOne struct {
+ config
+ fields []string
+ hooks []Hook
+ mutation *ChannelMonitorRequestTemplateMutation
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetUpdatedAt(v time.Time) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetUpdatedAt(v)
+ return _u
+}
+
+// SetName sets the "name" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetName(v string) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetName(v)
+ return _u
+}
+
+// SetNillableName sets the "name" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableName(v *string) *ChannelMonitorRequestTemplateUpdateOne {
+ if v != nil {
+ _u.SetName(*v)
+ }
+ return _u
+}
+
+// SetProvider sets the "provider" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetProvider(v channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetProvider(v)
+ return _u
+}
+
+// SetNillableProvider sets the "provider" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableProvider(v *channelmonitorrequesttemplate.Provider) *ChannelMonitorRequestTemplateUpdateOne {
+ if v != nil {
+ _u.SetProvider(*v)
+ }
+ return _u
+}
+
+// SetDescription sets the "description" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetDescription(v string) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetDescription(v)
+ return _u
+}
+
+// SetNillableDescription sets the "description" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableDescription(v *string) *ChannelMonitorRequestTemplateUpdateOne {
+ if v != nil {
+ _u.SetDescription(*v)
+ }
+ return _u
+}
+
+// ClearDescription clears the value of the "description" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) ClearDescription() *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.ClearDescription()
+ return _u
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetExtraHeaders(v map[string]string) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetExtraHeaders(v)
+ return _u
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetBodyOverrideMode(v string) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetBodyOverrideMode(v)
+ return _u
+}
+
+// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableBodyOverrideMode(v *string) *ChannelMonitorRequestTemplateUpdateOne {
+ if v != nil {
+ _u.SetBodyOverrideMode(*v)
+ }
+ return _u
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SetBodyOverride(v map[string]interface{}) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.SetBodyOverride(v)
+ return _u
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) ClearBodyOverride() *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.ClearBodyOverride()
+ return _u
+}
+
+// AddMonitorIDs adds the "monitors" edge to the ChannelMonitor entity by IDs.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) AddMonitorIDs(ids ...int64) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.AddMonitorIDs(ids...)
+ return _u
+}
+
+// AddMonitors adds the "monitors" edges to the ChannelMonitor entity.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) AddMonitors(v ...*ChannelMonitor) *ChannelMonitorRequestTemplateUpdateOne {
+ ids := make([]int64, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
+ }
+ return _u.AddMonitorIDs(ids...)
+}
+
+// Mutation returns the ChannelMonitorRequestTemplateMutation object of the builder.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) Mutation() *ChannelMonitorRequestTemplateMutation {
+ return _u.mutation
+}
+
+// ClearMonitors clears all "monitors" edges to the ChannelMonitor entity.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) ClearMonitors() *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.ClearMonitors()
+ return _u
+}
+
+// RemoveMonitorIDs removes the "monitors" edge to ChannelMonitor entities by IDs.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) RemoveMonitorIDs(ids ...int64) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.RemoveMonitorIDs(ids...)
+ return _u
+}
+
+// RemoveMonitors removes "monitors" edges to ChannelMonitor entities.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) RemoveMonitors(v ...*ChannelMonitor) *ChannelMonitorRequestTemplateUpdateOne {
+ ids := make([]int64, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
+ }
+ return _u.RemoveMonitorIDs(ids...)
+}
+
+// Where appends a list predicates to the ChannelMonitorRequestTemplateUpdate builder.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) Where(ps ...predicate.ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.mutation.Where(ps...)
+ return _u
+}
+
+// Select allows selecting one or more fields (columns) of the returned entity.
+// The default is selecting all fields defined in the entity schema.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) Select(field string, fields ...string) *ChannelMonitorRequestTemplateUpdateOne {
+ _u.fields = append([]string{field}, fields...)
+ return _u
+}
+
+// Save executes the query and returns the updated ChannelMonitorRequestTemplate entity.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) Save(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ _u.defaults()
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
+}
+
+// SaveX is like Save, but panics if an error occurs.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) SaveX(ctx context.Context) *ChannelMonitorRequestTemplate {
+ node, err := _u.Save(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return node
+}
+
+// Exec executes the query on the entity.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
+ return err
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
+ panic(err)
+ }
+}
+
+// defaults sets the default values of the builder before save.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) defaults() {
+ if _, ok := _u.mutation.UpdatedAt(); !ok {
+ v := channelmonitorrequesttemplate.UpdateDefaultUpdatedAt()
+ _u.mutation.SetUpdatedAt(v)
+ }
+}
+
+// check runs all checks and user-defined validators on the builder.
+func (_u *ChannelMonitorRequestTemplateUpdateOne) check() error {
+ if v, ok := _u.mutation.Name(); ok {
+ if err := channelmonitorrequesttemplate.NameValidator(v); err != nil {
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.name": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.Provider(); ok {
+ if err := channelmonitorrequesttemplate.ProviderValidator(v); err != nil {
+ return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.Description(); ok {
+ if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil {
+ return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)}
+ }
+ }
+ if v, ok := _u.mutation.BodyOverrideMode(); ok {
+ if err := channelmonitorrequesttemplate.BodyOverrideModeValidator(v); err != nil {
+ return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.body_override_mode": %w`, err)}
+ }
+ }
+ return nil
+}
+
+func (_u *ChannelMonitorRequestTemplateUpdateOne) sqlSave(ctx context.Context) (_node *ChannelMonitorRequestTemplate, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
+ }
+ _spec := sqlgraph.NewUpdateSpec(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.Columns, sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64))
+ id, ok := _u.mutation.ID()
+ if !ok {
+ return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "ChannelMonitorRequestTemplate.id" for update`)}
+ }
+ _spec.Node.ID.Value = id
+ if fields := _u.fields; len(fields) > 0 {
+ _spec.Node.Columns = make([]string, 0, len(fields))
+ _spec.Node.Columns = append(_spec.Node.Columns, channelmonitorrequesttemplate.FieldID)
+ for _, f := range fields {
+ if !channelmonitorrequesttemplate.ValidColumn(f) {
+ return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
+ }
+ if f != channelmonitorrequesttemplate.FieldID {
+ _spec.Node.Columns = append(_spec.Node.Columns, f)
+ }
+ }
+ }
+ if ps := _u.mutation.predicates; len(ps) > 0 {
+ _spec.Predicate = func(selector *sql.Selector) {
+ for i := range ps {
+ ps[i](selector)
+ }
+ }
+ }
+ if value, ok := _u.mutation.UpdatedAt(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldUpdatedAt, field.TypeTime, value)
+ }
+ if value, ok := _u.mutation.Name(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldName, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.Provider(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value)
+ }
+ if value, ok := _u.mutation.Description(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value)
+ }
+ if _u.mutation.DescriptionCleared() {
+ _spec.ClearField(channelmonitorrequesttemplate.FieldDescription, field.TypeString)
+ }
+ if value, ok := _u.mutation.ExtraHeaders(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldExtraHeaders, field.TypeJSON, value)
+ }
+ if value, ok := _u.mutation.BodyOverrideMode(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverrideMode, field.TypeString, value)
+ }
+ if value, ok := _u.mutation.BodyOverride(); ok {
+ _spec.SetField(channelmonitorrequesttemplate.FieldBodyOverride, field.TypeJSON, value)
+ }
+ if _u.mutation.BodyOverrideCleared() {
+ _spec.ClearField(channelmonitorrequesttemplate.FieldBodyOverride, field.TypeJSON)
+ }
+ if _u.mutation.MonitorsCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.RemovedMonitorsIDs(); len(nodes) > 0 && !_u.mutation.MonitorsCleared() {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
+ }
+ if nodes := _u.mutation.MonitorsIDs(); len(nodes) > 0 {
+ edge := &sqlgraph.EdgeSpec{
+ Rel: sqlgraph.O2M,
+ Inverse: true,
+ Table: channelmonitorrequesttemplate.MonitorsTable,
+ Columns: []string{channelmonitorrequesttemplate.MonitorsColumn},
+ Bidi: false,
+ Target: &sqlgraph.EdgeTarget{
+ IDSpec: sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64),
+ },
+ }
+ for _, k := range nodes {
+ edge.Target.Nodes = append(edge.Target.Nodes, k)
+ }
+ _spec.Edges.Add = append(_spec.Edges.Add, edge)
+ }
+ _node = &ChannelMonitorRequestTemplate{config: _u.config}
+ _spec.Assign = _node.assignValues
+ _spec.ScanValues = _node.scanValues
+ if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
+ if _, ok := err.(*sqlgraph.NotFoundError); ok {
+ err = &NotFoundError{channelmonitorrequesttemplate.Label}
+ } else if sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ return nil, err
+ }
+ _u.mutation.done = true
+ return _node, nil
+}
diff --git a/backend/ent/client.go b/backend/ent/client.go
index ebc7fc5e..df20ddfa 100644
--- a/backend/ent/client.go
+++ b/backend/ent/client.go
@@ -25,6 +25,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
@@ -77,6 +78,8 @@ type Client struct {
ChannelMonitorDailyRollup *ChannelMonitorDailyRollupClient
// ChannelMonitorHistory is the client for interacting with the ChannelMonitorHistory builders.
ChannelMonitorHistory *ChannelMonitorHistoryClient
+ // ChannelMonitorRequestTemplate is the client for interacting with the ChannelMonitorRequestTemplate builders.
+ ChannelMonitorRequestTemplate *ChannelMonitorRequestTemplateClient
// ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders.
ErrorPassthroughRule *ErrorPassthroughRuleClient
// Group is the client for interacting with the Group builders.
@@ -144,6 +147,7 @@ func (c *Client) init() {
c.ChannelMonitor = NewChannelMonitorClient(c.config)
c.ChannelMonitorDailyRollup = NewChannelMonitorDailyRollupClient(c.config)
c.ChannelMonitorHistory = NewChannelMonitorHistoryClient(c.config)
+ c.ChannelMonitorRequestTemplate = NewChannelMonitorRequestTemplateClient(c.config)
c.ErrorPassthroughRule = NewErrorPassthroughRuleClient(c.config)
c.Group = NewGroupClient(c.config)
c.IdempotencyRecord = NewIdempotencyRecordClient(c.config)
@@ -257,41 +261,42 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
cfg := c.config
cfg.driver = tx
return &Tx{
- ctx: ctx,
- config: cfg,
- APIKey: NewAPIKeyClient(cfg),
- Account: NewAccountClient(cfg),
- AccountGroup: NewAccountGroupClient(cfg),
- Announcement: NewAnnouncementClient(cfg),
- AnnouncementRead: NewAnnouncementReadClient(cfg),
- AuthIdentity: NewAuthIdentityClient(cfg),
- AuthIdentityChannel: NewAuthIdentityChannelClient(cfg),
- ChannelMonitor: NewChannelMonitorClient(cfg),
- ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
- ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
- ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
- Group: NewGroupClient(cfg),
- IdempotencyRecord: NewIdempotencyRecordClient(cfg),
- IdentityAdoptionDecision: NewIdentityAdoptionDecisionClient(cfg),
- PaymentAuditLog: NewPaymentAuditLogClient(cfg),
- PaymentOrder: NewPaymentOrderClient(cfg),
- PaymentProviderInstance: NewPaymentProviderInstanceClient(cfg),
- PendingAuthSession: NewPendingAuthSessionClient(cfg),
- PromoCode: NewPromoCodeClient(cfg),
- PromoCodeUsage: NewPromoCodeUsageClient(cfg),
- Proxy: NewProxyClient(cfg),
- RedeemCode: NewRedeemCodeClient(cfg),
- SecuritySecret: NewSecuritySecretClient(cfg),
- Setting: NewSettingClient(cfg),
- SubscriptionPlan: NewSubscriptionPlanClient(cfg),
- TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
- UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
- UsageLog: NewUsageLogClient(cfg),
- User: NewUserClient(cfg),
- UserAllowedGroup: NewUserAllowedGroupClient(cfg),
- UserAttributeDefinition: NewUserAttributeDefinitionClient(cfg),
- UserAttributeValue: NewUserAttributeValueClient(cfg),
- UserSubscription: NewUserSubscriptionClient(cfg),
+ ctx: ctx,
+ config: cfg,
+ APIKey: NewAPIKeyClient(cfg),
+ Account: NewAccountClient(cfg),
+ AccountGroup: NewAccountGroupClient(cfg),
+ Announcement: NewAnnouncementClient(cfg),
+ AnnouncementRead: NewAnnouncementReadClient(cfg),
+ AuthIdentity: NewAuthIdentityClient(cfg),
+ AuthIdentityChannel: NewAuthIdentityChannelClient(cfg),
+ ChannelMonitor: NewChannelMonitorClient(cfg),
+ ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
+ ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
+ ChannelMonitorRequestTemplate: NewChannelMonitorRequestTemplateClient(cfg),
+ ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
+ Group: NewGroupClient(cfg),
+ IdempotencyRecord: NewIdempotencyRecordClient(cfg),
+ IdentityAdoptionDecision: NewIdentityAdoptionDecisionClient(cfg),
+ PaymentAuditLog: NewPaymentAuditLogClient(cfg),
+ PaymentOrder: NewPaymentOrderClient(cfg),
+ PaymentProviderInstance: NewPaymentProviderInstanceClient(cfg),
+ PendingAuthSession: NewPendingAuthSessionClient(cfg),
+ PromoCode: NewPromoCodeClient(cfg),
+ PromoCodeUsage: NewPromoCodeUsageClient(cfg),
+ Proxy: NewProxyClient(cfg),
+ RedeemCode: NewRedeemCodeClient(cfg),
+ SecuritySecret: NewSecuritySecretClient(cfg),
+ Setting: NewSettingClient(cfg),
+ SubscriptionPlan: NewSubscriptionPlanClient(cfg),
+ TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
+ UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
+ UsageLog: NewUsageLogClient(cfg),
+ User: NewUserClient(cfg),
+ UserAllowedGroup: NewUserAllowedGroupClient(cfg),
+ UserAttributeDefinition: NewUserAttributeDefinitionClient(cfg),
+ UserAttributeValue: NewUserAttributeValueClient(cfg),
+ UserSubscription: NewUserSubscriptionClient(cfg),
}, nil
}
@@ -309,41 +314,42 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
cfg := c.config
cfg.driver = &txDriver{tx: tx, drv: c.driver}
return &Tx{
- ctx: ctx,
- config: cfg,
- APIKey: NewAPIKeyClient(cfg),
- Account: NewAccountClient(cfg),
- AccountGroup: NewAccountGroupClient(cfg),
- Announcement: NewAnnouncementClient(cfg),
- AnnouncementRead: NewAnnouncementReadClient(cfg),
- AuthIdentity: NewAuthIdentityClient(cfg),
- AuthIdentityChannel: NewAuthIdentityChannelClient(cfg),
- ChannelMonitor: NewChannelMonitorClient(cfg),
- ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
- ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
- ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
- Group: NewGroupClient(cfg),
- IdempotencyRecord: NewIdempotencyRecordClient(cfg),
- IdentityAdoptionDecision: NewIdentityAdoptionDecisionClient(cfg),
- PaymentAuditLog: NewPaymentAuditLogClient(cfg),
- PaymentOrder: NewPaymentOrderClient(cfg),
- PaymentProviderInstance: NewPaymentProviderInstanceClient(cfg),
- PendingAuthSession: NewPendingAuthSessionClient(cfg),
- PromoCode: NewPromoCodeClient(cfg),
- PromoCodeUsage: NewPromoCodeUsageClient(cfg),
- Proxy: NewProxyClient(cfg),
- RedeemCode: NewRedeemCodeClient(cfg),
- SecuritySecret: NewSecuritySecretClient(cfg),
- Setting: NewSettingClient(cfg),
- SubscriptionPlan: NewSubscriptionPlanClient(cfg),
- TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
- UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
- UsageLog: NewUsageLogClient(cfg),
- User: NewUserClient(cfg),
- UserAllowedGroup: NewUserAllowedGroupClient(cfg),
- UserAttributeDefinition: NewUserAttributeDefinitionClient(cfg),
- UserAttributeValue: NewUserAttributeValueClient(cfg),
- UserSubscription: NewUserSubscriptionClient(cfg),
+ ctx: ctx,
+ config: cfg,
+ APIKey: NewAPIKeyClient(cfg),
+ Account: NewAccountClient(cfg),
+ AccountGroup: NewAccountGroupClient(cfg),
+ Announcement: NewAnnouncementClient(cfg),
+ AnnouncementRead: NewAnnouncementReadClient(cfg),
+ AuthIdentity: NewAuthIdentityClient(cfg),
+ AuthIdentityChannel: NewAuthIdentityChannelClient(cfg),
+ ChannelMonitor: NewChannelMonitorClient(cfg),
+ ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
+ ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
+ ChannelMonitorRequestTemplate: NewChannelMonitorRequestTemplateClient(cfg),
+ ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
+ Group: NewGroupClient(cfg),
+ IdempotencyRecord: NewIdempotencyRecordClient(cfg),
+ IdentityAdoptionDecision: NewIdentityAdoptionDecisionClient(cfg),
+ PaymentAuditLog: NewPaymentAuditLogClient(cfg),
+ PaymentOrder: NewPaymentOrderClient(cfg),
+ PaymentProviderInstance: NewPaymentProviderInstanceClient(cfg),
+ PendingAuthSession: NewPendingAuthSessionClient(cfg),
+ PromoCode: NewPromoCodeClient(cfg),
+ PromoCodeUsage: NewPromoCodeUsageClient(cfg),
+ Proxy: NewProxyClient(cfg),
+ RedeemCode: NewRedeemCodeClient(cfg),
+ SecuritySecret: NewSecuritySecretClient(cfg),
+ Setting: NewSettingClient(cfg),
+ SubscriptionPlan: NewSubscriptionPlanClient(cfg),
+ TLSFingerprintProfile: NewTLSFingerprintProfileClient(cfg),
+ UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
+ UsageLog: NewUsageLogClient(cfg),
+ User: NewUserClient(cfg),
+ UserAllowedGroup: NewUserAllowedGroupClient(cfg),
+ UserAttributeDefinition: NewUserAttributeDefinitionClient(cfg),
+ UserAttributeValue: NewUserAttributeValueClient(cfg),
+ UserSubscription: NewUserSubscriptionClient(cfg),
}, nil
}
@@ -375,8 +381,9 @@ func (c *Client) Use(hooks ...Hook) {
for _, n := range []interface{ Use(...Hook) }{
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
c.AuthIdentity, c.AuthIdentityChannel, c.ChannelMonitor,
- c.ChannelMonitorDailyRollup, c.ChannelMonitorHistory, c.ErrorPassthroughRule,
- c.Group, c.IdempotencyRecord, c.IdentityAdoptionDecision, c.PaymentAuditLog,
+ c.ChannelMonitorDailyRollup, c.ChannelMonitorHistory,
+ c.ChannelMonitorRequestTemplate, c.ErrorPassthroughRule, c.Group,
+ c.IdempotencyRecord, c.IdentityAdoptionDecision, c.PaymentAuditLog,
c.PaymentOrder, c.PaymentProviderInstance, c.PendingAuthSession, c.PromoCode,
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
c.SubscriptionPlan, c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog,
@@ -393,8 +400,9 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
for _, n := range []interface{ Intercept(...Interceptor) }{
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
c.AuthIdentity, c.AuthIdentityChannel, c.ChannelMonitor,
- c.ChannelMonitorDailyRollup, c.ChannelMonitorHistory, c.ErrorPassthroughRule,
- c.Group, c.IdempotencyRecord, c.IdentityAdoptionDecision, c.PaymentAuditLog,
+ c.ChannelMonitorDailyRollup, c.ChannelMonitorHistory,
+ c.ChannelMonitorRequestTemplate, c.ErrorPassthroughRule, c.Group,
+ c.IdempotencyRecord, c.IdentityAdoptionDecision, c.PaymentAuditLog,
c.PaymentOrder, c.PaymentProviderInstance, c.PendingAuthSession, c.PromoCode,
c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
c.SubscriptionPlan, c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog,
@@ -428,6 +436,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
return c.ChannelMonitorDailyRollup.mutate(ctx, m)
case *ChannelMonitorHistoryMutation:
return c.ChannelMonitorHistory.mutate(ctx, m)
+ case *ChannelMonitorRequestTemplateMutation:
+ return c.ChannelMonitorRequestTemplate.mutate(ctx, m)
case *ErrorPassthroughRuleMutation:
return c.ErrorPassthroughRule.mutate(ctx, m)
case *GroupMutation:
@@ -1761,6 +1771,22 @@ func (c *ChannelMonitorClient) QueryDailyRollups(_m *ChannelMonitor) *ChannelMon
return query
}
+// QueryRequestTemplate queries the request_template edge of a ChannelMonitor.
+func (c *ChannelMonitorClient) QueryRequestTemplate(_m *ChannelMonitor) *ChannelMonitorRequestTemplateQuery {
+ query := (&ChannelMonitorRequestTemplateClient{config: c.config}).Query()
+ query.path = func(context.Context) (fromV *sql.Selector, _ error) {
+ id := _m.ID
+ step := sqlgraph.NewStep(
+ sqlgraph.From(channelmonitor.Table, channelmonitor.FieldID, id),
+ sqlgraph.To(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.FieldID),
+ sqlgraph.Edge(sqlgraph.M2O, false, channelmonitor.RequestTemplateTable, channelmonitor.RequestTemplateColumn),
+ )
+ fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
+ return fromV, nil
+ }
+ return query
+}
+
// Hooks returns the client hooks.
func (c *ChannelMonitorClient) Hooks() []Hook {
return c.hooks.ChannelMonitor
@@ -2084,6 +2110,155 @@ func (c *ChannelMonitorHistoryClient) mutate(ctx context.Context, m *ChannelMoni
}
}
+// ChannelMonitorRequestTemplateClient is a client for the ChannelMonitorRequestTemplate schema.
+type ChannelMonitorRequestTemplateClient struct {
+ config
+}
+
+// NewChannelMonitorRequestTemplateClient returns a client for the ChannelMonitorRequestTemplate from the given config.
+func NewChannelMonitorRequestTemplateClient(c config) *ChannelMonitorRequestTemplateClient {
+ return &ChannelMonitorRequestTemplateClient{config: c}
+}
+
+// Use adds a list of mutation hooks to the hooks stack.
+// A call to `Use(f, g, h)` equals to `channelmonitorrequesttemplate.Hooks(f(g(h())))`.
+func (c *ChannelMonitorRequestTemplateClient) Use(hooks ...Hook) {
+ c.hooks.ChannelMonitorRequestTemplate = append(c.hooks.ChannelMonitorRequestTemplate, hooks...)
+}
+
+// Intercept adds a list of query interceptors to the interceptors stack.
+// A call to `Intercept(f, g, h)` equals to `channelmonitorrequesttemplate.Intercept(f(g(h())))`.
+func (c *ChannelMonitorRequestTemplateClient) Intercept(interceptors ...Interceptor) {
+ c.inters.ChannelMonitorRequestTemplate = append(c.inters.ChannelMonitorRequestTemplate, interceptors...)
+}
+
+// Create returns a builder for creating a ChannelMonitorRequestTemplate entity.
+func (c *ChannelMonitorRequestTemplateClient) Create() *ChannelMonitorRequestTemplateCreate {
+ mutation := newChannelMonitorRequestTemplateMutation(c.config, OpCreate)
+ return &ChannelMonitorRequestTemplateCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
+}
+
+// CreateBulk returns a builder for creating a bulk of ChannelMonitorRequestTemplate entities.
+func (c *ChannelMonitorRequestTemplateClient) CreateBulk(builders ...*ChannelMonitorRequestTemplateCreate) *ChannelMonitorRequestTemplateCreateBulk {
+ return &ChannelMonitorRequestTemplateCreateBulk{config: c.config, builders: builders}
+}
+
+// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
+// a builder and applies setFunc on it.
+func (c *ChannelMonitorRequestTemplateClient) MapCreateBulk(slice any, setFunc func(*ChannelMonitorRequestTemplateCreate, int)) *ChannelMonitorRequestTemplateCreateBulk {
+ rv := reflect.ValueOf(slice)
+ if rv.Kind() != reflect.Slice {
+ return &ChannelMonitorRequestTemplateCreateBulk{err: fmt.Errorf("calling to ChannelMonitorRequestTemplateClient.MapCreateBulk with wrong type %T, need slice", slice)}
+ }
+ builders := make([]*ChannelMonitorRequestTemplateCreate, rv.Len())
+ for i := 0; i < rv.Len(); i++ {
+ builders[i] = c.Create()
+ setFunc(builders[i], i)
+ }
+ return &ChannelMonitorRequestTemplateCreateBulk{config: c.config, builders: builders}
+}
+
+// Update returns an update builder for ChannelMonitorRequestTemplate.
+func (c *ChannelMonitorRequestTemplateClient) Update() *ChannelMonitorRequestTemplateUpdate {
+ mutation := newChannelMonitorRequestTemplateMutation(c.config, OpUpdate)
+ return &ChannelMonitorRequestTemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
+}
+
+// UpdateOne returns an update builder for the given entity.
+func (c *ChannelMonitorRequestTemplateClient) UpdateOne(_m *ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateUpdateOne {
+ mutation := newChannelMonitorRequestTemplateMutation(c.config, OpUpdateOne, withChannelMonitorRequestTemplate(_m))
+ return &ChannelMonitorRequestTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
+}
+
+// UpdateOneID returns an update builder for the given id.
+func (c *ChannelMonitorRequestTemplateClient) UpdateOneID(id int64) *ChannelMonitorRequestTemplateUpdateOne {
+ mutation := newChannelMonitorRequestTemplateMutation(c.config, OpUpdateOne, withChannelMonitorRequestTemplateID(id))
+ return &ChannelMonitorRequestTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
+}
+
+// Delete returns a delete builder for ChannelMonitorRequestTemplate.
+func (c *ChannelMonitorRequestTemplateClient) Delete() *ChannelMonitorRequestTemplateDelete {
+ mutation := newChannelMonitorRequestTemplateMutation(c.config, OpDelete)
+ return &ChannelMonitorRequestTemplateDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
+}
+
+// DeleteOne returns a builder for deleting the given entity.
+func (c *ChannelMonitorRequestTemplateClient) DeleteOne(_m *ChannelMonitorRequestTemplate) *ChannelMonitorRequestTemplateDeleteOne {
+ return c.DeleteOneID(_m.ID)
+}
+
+// DeleteOneID returns a builder for deleting the given entity by its id.
+func (c *ChannelMonitorRequestTemplateClient) DeleteOneID(id int64) *ChannelMonitorRequestTemplateDeleteOne {
+ builder := c.Delete().Where(channelmonitorrequesttemplate.ID(id))
+ builder.mutation.id = &id
+ builder.mutation.op = OpDeleteOne
+ return &ChannelMonitorRequestTemplateDeleteOne{builder}
+}
+
+// Query returns a query builder for ChannelMonitorRequestTemplate.
+func (c *ChannelMonitorRequestTemplateClient) Query() *ChannelMonitorRequestTemplateQuery {
+ return &ChannelMonitorRequestTemplateQuery{
+ config: c.config,
+ ctx: &QueryContext{Type: TypeChannelMonitorRequestTemplate},
+ inters: c.Interceptors(),
+ }
+}
+
+// Get returns a ChannelMonitorRequestTemplate entity by its id.
+func (c *ChannelMonitorRequestTemplateClient) Get(ctx context.Context, id int64) (*ChannelMonitorRequestTemplate, error) {
+ return c.Query().Where(channelmonitorrequesttemplate.ID(id)).Only(ctx)
+}
+
+// GetX is like Get, but panics if an error occurs.
+func (c *ChannelMonitorRequestTemplateClient) GetX(ctx context.Context, id int64) *ChannelMonitorRequestTemplate {
+ obj, err := c.Get(ctx, id)
+ if err != nil {
+ panic(err)
+ }
+ return obj
+}
+
+// QueryMonitors queries the monitors edge of a ChannelMonitorRequestTemplate.
+func (c *ChannelMonitorRequestTemplateClient) QueryMonitors(_m *ChannelMonitorRequestTemplate) *ChannelMonitorQuery {
+ query := (&ChannelMonitorClient{config: c.config}).Query()
+ query.path = func(context.Context) (fromV *sql.Selector, _ error) {
+ id := _m.ID
+ step := sqlgraph.NewStep(
+ sqlgraph.From(channelmonitorrequesttemplate.Table, channelmonitorrequesttemplate.FieldID, id),
+ sqlgraph.To(channelmonitor.Table, channelmonitor.FieldID),
+ sqlgraph.Edge(sqlgraph.O2M, true, channelmonitorrequesttemplate.MonitorsTable, channelmonitorrequesttemplate.MonitorsColumn),
+ )
+ fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
+ return fromV, nil
+ }
+ return query
+}
+
+// Hooks returns the client hooks.
+func (c *ChannelMonitorRequestTemplateClient) Hooks() []Hook {
+ return c.hooks.ChannelMonitorRequestTemplate
+}
+
+// Interceptors returns the client interceptors.
+func (c *ChannelMonitorRequestTemplateClient) Interceptors() []Interceptor {
+ return c.inters.ChannelMonitorRequestTemplate
+}
+
+func (c *ChannelMonitorRequestTemplateClient) mutate(ctx context.Context, m *ChannelMonitorRequestTemplateMutation) (Value, error) {
+ switch m.Op() {
+ case OpCreate:
+ return (&ChannelMonitorRequestTemplateCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdate:
+ return (&ChannelMonitorRequestTemplateUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdateOne:
+ return (&ChannelMonitorRequestTemplateUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpDelete, OpDeleteOne:
+ return (&ChannelMonitorRequestTemplateDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
+ default:
+ return nil, fmt.Errorf("ent: unknown ChannelMonitorRequestTemplate mutation op: %q", m.Op())
+ }
+}
+
// ErrorPassthroughRuleClient is a client for the ErrorPassthroughRule schema.
type ErrorPassthroughRuleClient struct {
config
@@ -5845,22 +6020,22 @@ type (
hooks struct {
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, AuthIdentity,
AuthIdentityChannel, ChannelMonitor, ChannelMonitorDailyRollup,
- ChannelMonitorHistory, ErrorPassthroughRule, Group, IdempotencyRecord,
- IdentityAdoptionDecision, PaymentAuditLog, PaymentOrder,
- PaymentProviderInstance, PendingAuthSession, PromoCode, PromoCodeUsage, Proxy,
- RedeemCode, SecuritySecret, Setting, SubscriptionPlan, TLSFingerprintProfile,
- UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
- UserAttributeValue, UserSubscription []ent.Hook
+ ChannelMonitorHistory, ChannelMonitorRequestTemplate, ErrorPassthroughRule,
+ Group, IdempotencyRecord, IdentityAdoptionDecision, PaymentAuditLog,
+ PaymentOrder, PaymentProviderInstance, PendingAuthSession, PromoCode,
+ PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
+ TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
+ UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Hook
}
inters struct {
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, AuthIdentity,
AuthIdentityChannel, ChannelMonitor, ChannelMonitorDailyRollup,
- ChannelMonitorHistory, ErrorPassthroughRule, Group, IdempotencyRecord,
- IdentityAdoptionDecision, PaymentAuditLog, PaymentOrder,
- PaymentProviderInstance, PendingAuthSession, PromoCode, PromoCodeUsage, Proxy,
- RedeemCode, SecuritySecret, Setting, SubscriptionPlan, TLSFingerprintProfile,
- UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
- UserAttributeValue, UserSubscription []ent.Interceptor
+ ChannelMonitorHistory, ChannelMonitorRequestTemplate, ErrorPassthroughRule,
+ Group, IdempotencyRecord, IdentityAdoptionDecision, PaymentAuditLog,
+ PaymentOrder, PaymentProviderInstance, PendingAuthSession, PromoCode,
+ PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
+ TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
+ UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Interceptor
}
)
diff --git a/backend/ent/ent.go b/backend/ent/ent.go
index 71d17624..c9fcc314 100644
--- a/backend/ent/ent.go
+++ b/backend/ent/ent.go
@@ -22,6 +22,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
@@ -105,39 +106,40 @@ var (
func checkColumn(t, c string) error {
initCheck.Do(func() {
columnCheck = sql.NewColumnCheck(map[string]func(string) bool{
- apikey.Table: apikey.ValidColumn,
- account.Table: account.ValidColumn,
- accountgroup.Table: accountgroup.ValidColumn,
- announcement.Table: announcement.ValidColumn,
- announcementread.Table: announcementread.ValidColumn,
- authidentity.Table: authidentity.ValidColumn,
- authidentitychannel.Table: authidentitychannel.ValidColumn,
- channelmonitor.Table: channelmonitor.ValidColumn,
- channelmonitordailyrollup.Table: channelmonitordailyrollup.ValidColumn,
- channelmonitorhistory.Table: channelmonitorhistory.ValidColumn,
- errorpassthroughrule.Table: errorpassthroughrule.ValidColumn,
- group.Table: group.ValidColumn,
- idempotencyrecord.Table: idempotencyrecord.ValidColumn,
- identityadoptiondecision.Table: identityadoptiondecision.ValidColumn,
- paymentauditlog.Table: paymentauditlog.ValidColumn,
- paymentorder.Table: paymentorder.ValidColumn,
- paymentproviderinstance.Table: paymentproviderinstance.ValidColumn,
- pendingauthsession.Table: pendingauthsession.ValidColumn,
- promocode.Table: promocode.ValidColumn,
- promocodeusage.Table: promocodeusage.ValidColumn,
- proxy.Table: proxy.ValidColumn,
- redeemcode.Table: redeemcode.ValidColumn,
- securitysecret.Table: securitysecret.ValidColumn,
- setting.Table: setting.ValidColumn,
- subscriptionplan.Table: subscriptionplan.ValidColumn,
- tlsfingerprintprofile.Table: tlsfingerprintprofile.ValidColumn,
- usagecleanuptask.Table: usagecleanuptask.ValidColumn,
- usagelog.Table: usagelog.ValidColumn,
- user.Table: user.ValidColumn,
- userallowedgroup.Table: userallowedgroup.ValidColumn,
- userattributedefinition.Table: userattributedefinition.ValidColumn,
- userattributevalue.Table: userattributevalue.ValidColumn,
- usersubscription.Table: usersubscription.ValidColumn,
+ apikey.Table: apikey.ValidColumn,
+ account.Table: account.ValidColumn,
+ accountgroup.Table: accountgroup.ValidColumn,
+ announcement.Table: announcement.ValidColumn,
+ announcementread.Table: announcementread.ValidColumn,
+ authidentity.Table: authidentity.ValidColumn,
+ authidentitychannel.Table: authidentitychannel.ValidColumn,
+ channelmonitor.Table: channelmonitor.ValidColumn,
+ channelmonitordailyrollup.Table: channelmonitordailyrollup.ValidColumn,
+ channelmonitorhistory.Table: channelmonitorhistory.ValidColumn,
+ channelmonitorrequesttemplate.Table: channelmonitorrequesttemplate.ValidColumn,
+ errorpassthroughrule.Table: errorpassthroughrule.ValidColumn,
+ group.Table: group.ValidColumn,
+ idempotencyrecord.Table: idempotencyrecord.ValidColumn,
+ identityadoptiondecision.Table: identityadoptiondecision.ValidColumn,
+ paymentauditlog.Table: paymentauditlog.ValidColumn,
+ paymentorder.Table: paymentorder.ValidColumn,
+ paymentproviderinstance.Table: paymentproviderinstance.ValidColumn,
+ pendingauthsession.Table: pendingauthsession.ValidColumn,
+ promocode.Table: promocode.ValidColumn,
+ promocodeusage.Table: promocodeusage.ValidColumn,
+ proxy.Table: proxy.ValidColumn,
+ redeemcode.Table: redeemcode.ValidColumn,
+ securitysecret.Table: securitysecret.ValidColumn,
+ setting.Table: setting.ValidColumn,
+ subscriptionplan.Table: subscriptionplan.ValidColumn,
+ tlsfingerprintprofile.Table: tlsfingerprintprofile.ValidColumn,
+ usagecleanuptask.Table: usagecleanuptask.ValidColumn,
+ usagelog.Table: usagelog.ValidColumn,
+ user.Table: user.ValidColumn,
+ userallowedgroup.Table: userallowedgroup.ValidColumn,
+ userattributedefinition.Table: userattributedefinition.ValidColumn,
+ userattributevalue.Table: userattributevalue.ValidColumn,
+ usersubscription.Table: usersubscription.ValidColumn,
})
})
return columnCheck(t, c)
diff --git a/backend/ent/hook/hook.go b/backend/ent/hook/hook.go
index ff86c90d..414eba24 100644
--- a/backend/ent/hook/hook.go
+++ b/backend/ent/hook/hook.go
@@ -129,6 +129,18 @@ func (f ChannelMonitorHistoryFunc) Mutate(ctx context.Context, m ent.Mutation) (
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ChannelMonitorHistoryMutation", m)
}
+// The ChannelMonitorRequestTemplateFunc type is an adapter to allow the use of ordinary
+// function as ChannelMonitorRequestTemplate mutator.
+type ChannelMonitorRequestTemplateFunc func(context.Context, *ent.ChannelMonitorRequestTemplateMutation) (ent.Value, error)
+
+// Mutate calls f(ctx, m).
+func (f ChannelMonitorRequestTemplateFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
+ if mv, ok := m.(*ent.ChannelMonitorRequestTemplateMutation); ok {
+ return f(ctx, mv)
+ }
+ return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ChannelMonitorRequestTemplateMutation", m)
+}
+
// The ErrorPassthroughRuleFunc type is an adapter to allow the use of ordinary
// function as ErrorPassthroughRule mutator.
type ErrorPassthroughRuleFunc func(context.Context, *ent.ErrorPassthroughRuleMutation) (ent.Value, error)
diff --git a/backend/ent/intercept/intercept.go b/backend/ent/intercept/intercept.go
index 0c83fc38..95b68e09 100644
--- a/backend/ent/intercept/intercept.go
+++ b/backend/ent/intercept/intercept.go
@@ -18,6 +18,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
@@ -370,6 +371,33 @@ func (f TraverseChannelMonitorHistory) Traverse(ctx context.Context, q ent.Query
return fmt.Errorf("unexpected query type %T. expect *ent.ChannelMonitorHistoryQuery", q)
}
+// The ChannelMonitorRequestTemplateFunc type is an adapter to allow the use of ordinary function as a Querier.
+type ChannelMonitorRequestTemplateFunc func(context.Context, *ent.ChannelMonitorRequestTemplateQuery) (ent.Value, error)
+
+// Query calls f(ctx, q).
+func (f ChannelMonitorRequestTemplateFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
+ if q, ok := q.(*ent.ChannelMonitorRequestTemplateQuery); ok {
+ return f(ctx, q)
+ }
+ return nil, fmt.Errorf("unexpected query type %T. expect *ent.ChannelMonitorRequestTemplateQuery", q)
+}
+
+// The TraverseChannelMonitorRequestTemplate type is an adapter to allow the use of ordinary function as Traverser.
+type TraverseChannelMonitorRequestTemplate func(context.Context, *ent.ChannelMonitorRequestTemplateQuery) error
+
+// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
+func (f TraverseChannelMonitorRequestTemplate) Intercept(next ent.Querier) ent.Querier {
+ return next
+}
+
+// Traverse calls f(ctx, q).
+func (f TraverseChannelMonitorRequestTemplate) Traverse(ctx context.Context, q ent.Query) error {
+ if q, ok := q.(*ent.ChannelMonitorRequestTemplateQuery); ok {
+ return f(ctx, q)
+ }
+ return fmt.Errorf("unexpected query type %T. expect *ent.ChannelMonitorRequestTemplateQuery", q)
+}
+
// The ErrorPassthroughRuleFunc type is an adapter to allow the use of ordinary function as a Querier.
type ErrorPassthroughRuleFunc func(context.Context, *ent.ErrorPassthroughRuleQuery) (ent.Value, error)
@@ -1014,6 +1042,8 @@ func NewQuery(q ent.Query) (Query, error) {
return &query[*ent.ChannelMonitorDailyRollupQuery, predicate.ChannelMonitorDailyRollup, channelmonitordailyrollup.OrderOption]{typ: ent.TypeChannelMonitorDailyRollup, tq: q}, nil
case *ent.ChannelMonitorHistoryQuery:
return &query[*ent.ChannelMonitorHistoryQuery, predicate.ChannelMonitorHistory, channelmonitorhistory.OrderOption]{typ: ent.TypeChannelMonitorHistory, tq: q}, nil
+ case *ent.ChannelMonitorRequestTemplateQuery:
+ return &query[*ent.ChannelMonitorRequestTemplateQuery, predicate.ChannelMonitorRequestTemplate, channelmonitorrequesttemplate.OrderOption]{typ: ent.TypeChannelMonitorRequestTemplate, tq: q}, nil
case *ent.ErrorPassthroughRuleQuery:
return &query[*ent.ErrorPassthroughRuleQuery, predicate.ErrorPassthroughRule, errorpassthroughrule.OrderOption]{typ: ent.TypeErrorPassthroughRule, tq: q}, nil
case *ent.GroupQuery:
diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go
index dba43ddf..38366e95 100644
--- a/backend/ent/migrate/schema.go
+++ b/backend/ent/migrate/schema.go
@@ -437,12 +437,24 @@ var (
{Name: "interval_seconds", Type: field.TypeInt},
{Name: "last_checked_at", Type: field.TypeTime, Nullable: true},
{Name: "created_by", Type: field.TypeInt64},
+ {Name: "extra_headers", Type: field.TypeJSON},
+ {Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"},
+ {Name: "body_override", Type: field.TypeJSON, Nullable: true},
+ {Name: "template_id", Type: field.TypeInt64, Nullable: true},
}
// ChannelMonitorsTable holds the schema information for the "channel_monitors" table.
ChannelMonitorsTable = &schema.Table{
Name: "channel_monitors",
Columns: ChannelMonitorsColumns,
PrimaryKey: []*schema.Column{ChannelMonitorsColumns[0]},
+ ForeignKeys: []*schema.ForeignKey{
+ {
+ Symbol: "channel_monitors_channel_monitor_request_templates_request_template",
+ Columns: []*schema.Column{ChannelMonitorsColumns[17]},
+ RefColumns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[0]},
+ OnDelete: schema.SetNull,
+ },
+ },
Indexes: []*schema.Index{
{
Name: "channelmonitor_enabled_last_checked_at",
@@ -459,6 +471,11 @@ var (
Unique: false,
Columns: []*schema.Column{ChannelMonitorsColumns[9]},
},
+ {
+ Name: "channelmonitor_template_id",
+ Unique: false,
+ Columns: []*schema.Column{ChannelMonitorsColumns[17]},
+ },
},
}
// ChannelMonitorDailyRollupsColumns holds the columns for the "channel_monitor_daily_rollups" table.
@@ -542,6 +559,31 @@ var (
},
},
}
+ // ChannelMonitorRequestTemplatesColumns holds the columns for the "channel_monitor_request_templates" table.
+ ChannelMonitorRequestTemplatesColumns = []*schema.Column{
+ {Name: "id", Type: field.TypeInt64, Increment: true},
+ {Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
+ {Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
+ {Name: "name", Type: field.TypeString, Size: 100},
+ {Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini"}},
+ {Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
+ {Name: "extra_headers", Type: field.TypeJSON},
+ {Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"},
+ {Name: "body_override", Type: field.TypeJSON, Nullable: true},
+ }
+ // ChannelMonitorRequestTemplatesTable holds the schema information for the "channel_monitor_request_templates" table.
+ ChannelMonitorRequestTemplatesTable = &schema.Table{
+ Name: "channel_monitor_request_templates",
+ Columns: ChannelMonitorRequestTemplatesColumns,
+ PrimaryKey: []*schema.Column{ChannelMonitorRequestTemplatesColumns[0]},
+ Indexes: []*schema.Index{
+ {
+ Name: "channelmonitorrequesttemplate_provider_name",
+ Unique: true,
+ Columns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[4], ChannelMonitorRequestTemplatesColumns[3]},
+ },
+ },
+ }
// ErrorPassthroughRulesColumns holds the columns for the "error_passthrough_rules" table.
ErrorPassthroughRulesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
@@ -1644,6 +1686,7 @@ var (
ChannelMonitorsTable,
ChannelMonitorDailyRollupsTable,
ChannelMonitorHistoriesTable,
+ ChannelMonitorRequestTemplatesTable,
ErrorPassthroughRulesTable,
GroupsTable,
IdempotencyRecordsTable,
@@ -1701,6 +1744,7 @@ func init() {
AuthIdentityChannelsTable.Annotation = &entsql.Annotation{
Table: "auth_identity_channels",
}
+ ChannelMonitorsTable.ForeignKeys[0].RefTable = ChannelMonitorRequestTemplatesTable
ChannelMonitorsTable.Annotation = &entsql.Annotation{
Table: "channel_monitors",
}
@@ -1712,6 +1756,9 @@ func init() {
ChannelMonitorHistoriesTable.Annotation = &entsql.Annotation{
Table: "channel_monitor_histories",
}
+ ChannelMonitorRequestTemplatesTable.Annotation = &entsql.Annotation{
+ Table: "channel_monitor_request_templates",
+ }
ErrorPassthroughRulesTable.Annotation = &entsql.Annotation{
Table: "error_passthrough_rules",
}
diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go
index 43e52371..568b3eb5 100644
--- a/backend/ent/mutation.go
+++ b/backend/ent/mutation.go
@@ -22,6 +22,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
@@ -58,39 +59,40 @@ const (
OpUpdateOne = ent.OpUpdateOne
// Node types.
- TypeAPIKey = "APIKey"
- TypeAccount = "Account"
- TypeAccountGroup = "AccountGroup"
- TypeAnnouncement = "Announcement"
- TypeAnnouncementRead = "AnnouncementRead"
- TypeAuthIdentity = "AuthIdentity"
- TypeAuthIdentityChannel = "AuthIdentityChannel"
- TypeChannelMonitor = "ChannelMonitor"
- TypeChannelMonitorDailyRollup = "ChannelMonitorDailyRollup"
- TypeChannelMonitorHistory = "ChannelMonitorHistory"
- TypeErrorPassthroughRule = "ErrorPassthroughRule"
- TypeGroup = "Group"
- TypeIdempotencyRecord = "IdempotencyRecord"
- TypeIdentityAdoptionDecision = "IdentityAdoptionDecision"
- TypePaymentAuditLog = "PaymentAuditLog"
- TypePaymentOrder = "PaymentOrder"
- TypePaymentProviderInstance = "PaymentProviderInstance"
- TypePendingAuthSession = "PendingAuthSession"
- TypePromoCode = "PromoCode"
- TypePromoCodeUsage = "PromoCodeUsage"
- TypeProxy = "Proxy"
- TypeRedeemCode = "RedeemCode"
- TypeSecuritySecret = "SecuritySecret"
- TypeSetting = "Setting"
- TypeSubscriptionPlan = "SubscriptionPlan"
- TypeTLSFingerprintProfile = "TLSFingerprintProfile"
- TypeUsageCleanupTask = "UsageCleanupTask"
- TypeUsageLog = "UsageLog"
- TypeUser = "User"
- TypeUserAllowedGroup = "UserAllowedGroup"
- TypeUserAttributeDefinition = "UserAttributeDefinition"
- TypeUserAttributeValue = "UserAttributeValue"
- TypeUserSubscription = "UserSubscription"
+ TypeAPIKey = "APIKey"
+ TypeAccount = "Account"
+ TypeAccountGroup = "AccountGroup"
+ TypeAnnouncement = "Announcement"
+ TypeAnnouncementRead = "AnnouncementRead"
+ TypeAuthIdentity = "AuthIdentity"
+ TypeAuthIdentityChannel = "AuthIdentityChannel"
+ TypeChannelMonitor = "ChannelMonitor"
+ TypeChannelMonitorDailyRollup = "ChannelMonitorDailyRollup"
+ TypeChannelMonitorHistory = "ChannelMonitorHistory"
+ TypeChannelMonitorRequestTemplate = "ChannelMonitorRequestTemplate"
+ TypeErrorPassthroughRule = "ErrorPassthroughRule"
+ TypeGroup = "Group"
+ TypeIdempotencyRecord = "IdempotencyRecord"
+ TypeIdentityAdoptionDecision = "IdentityAdoptionDecision"
+ TypePaymentAuditLog = "PaymentAuditLog"
+ TypePaymentOrder = "PaymentOrder"
+ TypePaymentProviderInstance = "PaymentProviderInstance"
+ TypePendingAuthSession = "PendingAuthSession"
+ TypePromoCode = "PromoCode"
+ TypePromoCodeUsage = "PromoCodeUsage"
+ TypeProxy = "Proxy"
+ TypeRedeemCode = "RedeemCode"
+ TypeSecuritySecret = "SecuritySecret"
+ TypeSetting = "Setting"
+ TypeSubscriptionPlan = "SubscriptionPlan"
+ TypeTLSFingerprintProfile = "TLSFingerprintProfile"
+ TypeUsageCleanupTask = "UsageCleanupTask"
+ TypeUsageLog = "UsageLog"
+ TypeUser = "User"
+ TypeUserAllowedGroup = "UserAllowedGroup"
+ TypeUserAttributeDefinition = "UserAttributeDefinition"
+ TypeUserAttributeValue = "UserAttributeValue"
+ TypeUserSubscription = "UserSubscription"
)
// APIKeyMutation represents an operation that mutates the APIKey nodes in the graph.
@@ -8743,35 +8745,40 @@ func (m *AuthIdentityChannelMutation) ResetEdge(name string) error {
// ChannelMonitorMutation represents an operation that mutates the ChannelMonitor nodes in the graph.
type ChannelMonitorMutation struct {
config
- op Op
- typ string
- id *int64
- created_at *time.Time
- updated_at *time.Time
- name *string
- provider *channelmonitor.Provider
- endpoint *string
- api_key_encrypted *string
- primary_model *string
- extra_models *[]string
- appendextra_models []string
- group_name *string
- enabled *bool
- interval_seconds *int
- addinterval_seconds *int
- last_checked_at *time.Time
- created_by *int64
- addcreated_by *int64
- clearedFields map[string]struct{}
- history map[int64]struct{}
- removedhistory map[int64]struct{}
- clearedhistory bool
- daily_rollups map[int64]struct{}
- removeddaily_rollups map[int64]struct{}
- cleareddaily_rollups bool
- done bool
- oldValue func(context.Context) (*ChannelMonitor, error)
- predicates []predicate.ChannelMonitor
+ op Op
+ typ string
+ id *int64
+ created_at *time.Time
+ updated_at *time.Time
+ name *string
+ provider *channelmonitor.Provider
+ endpoint *string
+ api_key_encrypted *string
+ primary_model *string
+ extra_models *[]string
+ appendextra_models []string
+ group_name *string
+ enabled *bool
+ interval_seconds *int
+ addinterval_seconds *int
+ last_checked_at *time.Time
+ created_by *int64
+ addcreated_by *int64
+ extra_headers *map[string]string
+ body_override_mode *string
+ body_override *map[string]interface{}
+ clearedFields map[string]struct{}
+ history map[int64]struct{}
+ removedhistory map[int64]struct{}
+ clearedhistory bool
+ daily_rollups map[int64]struct{}
+ removeddaily_rollups map[int64]struct{}
+ cleareddaily_rollups bool
+ request_template *int64
+ clearedrequest_template bool
+ done bool
+ oldValue func(context.Context) (*ChannelMonitor, error)
+ predicates []predicate.ChannelMonitor
}
var _ ent.Mutation = (*ChannelMonitorMutation)(nil)
@@ -9421,6 +9428,176 @@ func (m *ChannelMonitorMutation) ResetCreatedBy() {
m.addcreated_by = nil
}
+// SetTemplateID sets the "template_id" field.
+func (m *ChannelMonitorMutation) SetTemplateID(i int64) {
+ m.request_template = &i
+}
+
+// TemplateID returns the value of the "template_id" field in the mutation.
+func (m *ChannelMonitorMutation) TemplateID() (r int64, exists bool) {
+ v := m.request_template
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldTemplateID returns the old "template_id" field's value of the ChannelMonitor entity.
+// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorMutation) OldTemplateID(ctx context.Context) (v *int64, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldTemplateID is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldTemplateID requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldTemplateID: %w", err)
+ }
+ return oldValue.TemplateID, nil
+}
+
+// ClearTemplateID clears the value of the "template_id" field.
+func (m *ChannelMonitorMutation) ClearTemplateID() {
+ m.request_template = nil
+ m.clearedFields[channelmonitor.FieldTemplateID] = struct{}{}
+}
+
+// TemplateIDCleared returns if the "template_id" field was cleared in this mutation.
+func (m *ChannelMonitorMutation) TemplateIDCleared() bool {
+ _, ok := m.clearedFields[channelmonitor.FieldTemplateID]
+ return ok
+}
+
+// ResetTemplateID resets all changes to the "template_id" field.
+func (m *ChannelMonitorMutation) ResetTemplateID() {
+ m.request_template = nil
+ delete(m.clearedFields, channelmonitor.FieldTemplateID)
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (m *ChannelMonitorMutation) SetExtraHeaders(value map[string]string) {
+ m.extra_headers = &value
+}
+
+// ExtraHeaders returns the value of the "extra_headers" field in the mutation.
+func (m *ChannelMonitorMutation) ExtraHeaders() (r map[string]string, exists bool) {
+ v := m.extra_headers
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldExtraHeaders returns the old "extra_headers" field's value of the ChannelMonitor entity.
+// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorMutation) OldExtraHeaders(ctx context.Context) (v map[string]string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldExtraHeaders is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldExtraHeaders requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldExtraHeaders: %w", err)
+ }
+ return oldValue.ExtraHeaders, nil
+}
+
+// ResetExtraHeaders resets all changes to the "extra_headers" field.
+func (m *ChannelMonitorMutation) ResetExtraHeaders() {
+ m.extra_headers = nil
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (m *ChannelMonitorMutation) SetBodyOverrideMode(s string) {
+ m.body_override_mode = &s
+}
+
+// BodyOverrideMode returns the value of the "body_override_mode" field in the mutation.
+func (m *ChannelMonitorMutation) BodyOverrideMode() (r string, exists bool) {
+ v := m.body_override_mode
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldBodyOverrideMode returns the old "body_override_mode" field's value of the ChannelMonitor entity.
+// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorMutation) OldBodyOverrideMode(ctx context.Context) (v string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldBodyOverrideMode is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldBodyOverrideMode requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldBodyOverrideMode: %w", err)
+ }
+ return oldValue.BodyOverrideMode, nil
+}
+
+// ResetBodyOverrideMode resets all changes to the "body_override_mode" field.
+func (m *ChannelMonitorMutation) ResetBodyOverrideMode() {
+ m.body_override_mode = nil
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (m *ChannelMonitorMutation) SetBodyOverride(value map[string]interface{}) {
+ m.body_override = &value
+}
+
+// BodyOverride returns the value of the "body_override" field in the mutation.
+func (m *ChannelMonitorMutation) BodyOverride() (r map[string]interface{}, exists bool) {
+ v := m.body_override
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldBodyOverride returns the old "body_override" field's value of the ChannelMonitor entity.
+// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorMutation) OldBodyOverride(ctx context.Context) (v map[string]interface{}, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldBodyOverride is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldBodyOverride requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldBodyOverride: %w", err)
+ }
+ return oldValue.BodyOverride, nil
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (m *ChannelMonitorMutation) ClearBodyOverride() {
+ m.body_override = nil
+ m.clearedFields[channelmonitor.FieldBodyOverride] = struct{}{}
+}
+
+// BodyOverrideCleared returns if the "body_override" field was cleared in this mutation.
+func (m *ChannelMonitorMutation) BodyOverrideCleared() bool {
+ _, ok := m.clearedFields[channelmonitor.FieldBodyOverride]
+ return ok
+}
+
+// ResetBodyOverride resets all changes to the "body_override" field.
+func (m *ChannelMonitorMutation) ResetBodyOverride() {
+ m.body_override = nil
+ delete(m.clearedFields, channelmonitor.FieldBodyOverride)
+}
+
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by ids.
func (m *ChannelMonitorMutation) AddHistoryIDs(ids ...int64) {
if m.history == nil {
@@ -9529,6 +9706,46 @@ func (m *ChannelMonitorMutation) ResetDailyRollups() {
m.removeddaily_rollups = nil
}
+// SetRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by id.
+func (m *ChannelMonitorMutation) SetRequestTemplateID(id int64) {
+ m.request_template = &id
+}
+
+// ClearRequestTemplate clears the "request_template" edge to the ChannelMonitorRequestTemplate entity.
+func (m *ChannelMonitorMutation) ClearRequestTemplate() {
+ m.clearedrequest_template = true
+ m.clearedFields[channelmonitor.FieldTemplateID] = struct{}{}
+}
+
+// RequestTemplateCleared reports if the "request_template" edge to the ChannelMonitorRequestTemplate entity was cleared.
+func (m *ChannelMonitorMutation) RequestTemplateCleared() bool {
+ return m.TemplateIDCleared() || m.clearedrequest_template
+}
+
+// RequestTemplateID returns the "request_template" edge ID in the mutation.
+func (m *ChannelMonitorMutation) RequestTemplateID() (id int64, exists bool) {
+ if m.request_template != nil {
+ return *m.request_template, true
+ }
+ return
+}
+
+// RequestTemplateIDs returns the "request_template" edge IDs in the mutation.
+// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
+// RequestTemplateID instead. It exists only for internal usage by the builders.
+func (m *ChannelMonitorMutation) RequestTemplateIDs() (ids []int64) {
+ if id := m.request_template; id != nil {
+ ids = append(ids, *id)
+ }
+ return
+}
+
+// ResetRequestTemplate resets all changes to the "request_template" edge.
+func (m *ChannelMonitorMutation) ResetRequestTemplate() {
+ m.request_template = nil
+ m.clearedrequest_template = false
+}
+
// Where appends a list predicates to the ChannelMonitorMutation builder.
func (m *ChannelMonitorMutation) Where(ps ...predicate.ChannelMonitor) {
m.predicates = append(m.predicates, ps...)
@@ -9563,7 +9780,7 @@ func (m *ChannelMonitorMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ChannelMonitorMutation) Fields() []string {
- fields := make([]string, 0, 13)
+ fields := make([]string, 0, 17)
if m.created_at != nil {
fields = append(fields, channelmonitor.FieldCreatedAt)
}
@@ -9603,6 +9820,18 @@ func (m *ChannelMonitorMutation) Fields() []string {
if m.created_by != nil {
fields = append(fields, channelmonitor.FieldCreatedBy)
}
+ if m.request_template != nil {
+ fields = append(fields, channelmonitor.FieldTemplateID)
+ }
+ if m.extra_headers != nil {
+ fields = append(fields, channelmonitor.FieldExtraHeaders)
+ }
+ if m.body_override_mode != nil {
+ fields = append(fields, channelmonitor.FieldBodyOverrideMode)
+ }
+ if m.body_override != nil {
+ fields = append(fields, channelmonitor.FieldBodyOverride)
+ }
return fields
}
@@ -9637,6 +9866,14 @@ func (m *ChannelMonitorMutation) Field(name string) (ent.Value, bool) {
return m.LastCheckedAt()
case channelmonitor.FieldCreatedBy:
return m.CreatedBy()
+ case channelmonitor.FieldTemplateID:
+ return m.TemplateID()
+ case channelmonitor.FieldExtraHeaders:
+ return m.ExtraHeaders()
+ case channelmonitor.FieldBodyOverrideMode:
+ return m.BodyOverrideMode()
+ case channelmonitor.FieldBodyOverride:
+ return m.BodyOverride()
}
return nil, false
}
@@ -9672,6 +9909,14 @@ func (m *ChannelMonitorMutation) OldField(ctx context.Context, name string) (ent
return m.OldLastCheckedAt(ctx)
case channelmonitor.FieldCreatedBy:
return m.OldCreatedBy(ctx)
+ case channelmonitor.FieldTemplateID:
+ return m.OldTemplateID(ctx)
+ case channelmonitor.FieldExtraHeaders:
+ return m.OldExtraHeaders(ctx)
+ case channelmonitor.FieldBodyOverrideMode:
+ return m.OldBodyOverrideMode(ctx)
+ case channelmonitor.FieldBodyOverride:
+ return m.OldBodyOverride(ctx)
}
return nil, fmt.Errorf("unknown ChannelMonitor field %s", name)
}
@@ -9772,6 +10017,34 @@ func (m *ChannelMonitorMutation) SetField(name string, value ent.Value) error {
}
m.SetCreatedBy(v)
return nil
+ case channelmonitor.FieldTemplateID:
+ v, ok := value.(int64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetTemplateID(v)
+ return nil
+ case channelmonitor.FieldExtraHeaders:
+ v, ok := value.(map[string]string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetExtraHeaders(v)
+ return nil
+ case channelmonitor.FieldBodyOverrideMode:
+ v, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetBodyOverrideMode(v)
+ return nil
+ case channelmonitor.FieldBodyOverride:
+ v, ok := value.(map[string]interface{})
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetBodyOverride(v)
+ return nil
}
return fmt.Errorf("unknown ChannelMonitor field %s", name)
}
@@ -9835,6 +10108,12 @@ func (m *ChannelMonitorMutation) ClearedFields() []string {
if m.FieldCleared(channelmonitor.FieldLastCheckedAt) {
fields = append(fields, channelmonitor.FieldLastCheckedAt)
}
+ if m.FieldCleared(channelmonitor.FieldTemplateID) {
+ fields = append(fields, channelmonitor.FieldTemplateID)
+ }
+ if m.FieldCleared(channelmonitor.FieldBodyOverride) {
+ fields = append(fields, channelmonitor.FieldBodyOverride)
+ }
return fields
}
@@ -9855,6 +10134,12 @@ func (m *ChannelMonitorMutation) ClearField(name string) error {
case channelmonitor.FieldLastCheckedAt:
m.ClearLastCheckedAt()
return nil
+ case channelmonitor.FieldTemplateID:
+ m.ClearTemplateID()
+ return nil
+ case channelmonitor.FieldBodyOverride:
+ m.ClearBodyOverride()
+ return nil
}
return fmt.Errorf("unknown ChannelMonitor nullable field %s", name)
}
@@ -9902,19 +10187,34 @@ func (m *ChannelMonitorMutation) ResetField(name string) error {
case channelmonitor.FieldCreatedBy:
m.ResetCreatedBy()
return nil
+ case channelmonitor.FieldTemplateID:
+ m.ResetTemplateID()
+ return nil
+ case channelmonitor.FieldExtraHeaders:
+ m.ResetExtraHeaders()
+ return nil
+ case channelmonitor.FieldBodyOverrideMode:
+ m.ResetBodyOverrideMode()
+ return nil
+ case channelmonitor.FieldBodyOverride:
+ m.ResetBodyOverride()
+ return nil
}
return fmt.Errorf("unknown ChannelMonitor field %s", name)
}
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *ChannelMonitorMutation) AddedEdges() []string {
- edges := make([]string, 0, 2)
+ edges := make([]string, 0, 3)
if m.history != nil {
edges = append(edges, channelmonitor.EdgeHistory)
}
if m.daily_rollups != nil {
edges = append(edges, channelmonitor.EdgeDailyRollups)
}
+ if m.request_template != nil {
+ edges = append(edges, channelmonitor.EdgeRequestTemplate)
+ }
return edges
}
@@ -9934,13 +10234,17 @@ func (m *ChannelMonitorMutation) AddedIDs(name string) []ent.Value {
ids = append(ids, id)
}
return ids
+ case channelmonitor.EdgeRequestTemplate:
+ if id := m.request_template; id != nil {
+ return []ent.Value{*id}
+ }
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *ChannelMonitorMutation) RemovedEdges() []string {
- edges := make([]string, 0, 2)
+ edges := make([]string, 0, 3)
if m.removedhistory != nil {
edges = append(edges, channelmonitor.EdgeHistory)
}
@@ -9972,13 +10276,16 @@ func (m *ChannelMonitorMutation) RemovedIDs(name string) []ent.Value {
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *ChannelMonitorMutation) ClearedEdges() []string {
- edges := make([]string, 0, 2)
+ edges := make([]string, 0, 3)
if m.clearedhistory {
edges = append(edges, channelmonitor.EdgeHistory)
}
if m.cleareddaily_rollups {
edges = append(edges, channelmonitor.EdgeDailyRollups)
}
+ if m.clearedrequest_template {
+ edges = append(edges, channelmonitor.EdgeRequestTemplate)
+ }
return edges
}
@@ -9990,6 +10297,8 @@ func (m *ChannelMonitorMutation) EdgeCleared(name string) bool {
return m.clearedhistory
case channelmonitor.EdgeDailyRollups:
return m.cleareddaily_rollups
+ case channelmonitor.EdgeRequestTemplate:
+ return m.clearedrequest_template
}
return false
}
@@ -9998,6 +10307,9 @@ func (m *ChannelMonitorMutation) EdgeCleared(name string) bool {
// if that edge is not defined in the schema.
func (m *ChannelMonitorMutation) ClearEdge(name string) error {
switch name {
+ case channelmonitor.EdgeRequestTemplate:
+ m.ClearRequestTemplate()
+ return nil
}
return fmt.Errorf("unknown ChannelMonitor unique edge %s", name)
}
@@ -10012,6 +10324,9 @@ func (m *ChannelMonitorMutation) ResetEdge(name string) error {
case channelmonitor.EdgeDailyRollups:
m.ResetDailyRollups()
return nil
+ case channelmonitor.EdgeRequestTemplate:
+ m.ResetRequestTemplate()
+ return nil
}
return fmt.Errorf("unknown ChannelMonitor edge %s", name)
}
@@ -12266,6 +12581,844 @@ func (m *ChannelMonitorHistoryMutation) ResetEdge(name string) error {
return fmt.Errorf("unknown ChannelMonitorHistory edge %s", name)
}
+// ChannelMonitorRequestTemplateMutation represents an operation that mutates the ChannelMonitorRequestTemplate nodes in the graph.
+type ChannelMonitorRequestTemplateMutation struct {
+ config
+ op Op
+ typ string
+ id *int64
+ created_at *time.Time
+ updated_at *time.Time
+ name *string
+ provider *channelmonitorrequesttemplate.Provider
+ description *string
+ extra_headers *map[string]string
+ body_override_mode *string
+ body_override *map[string]interface{}
+ clearedFields map[string]struct{}
+ monitors map[int64]struct{}
+ removedmonitors map[int64]struct{}
+ clearedmonitors bool
+ done bool
+ oldValue func(context.Context) (*ChannelMonitorRequestTemplate, error)
+ predicates []predicate.ChannelMonitorRequestTemplate
+}
+
+var _ ent.Mutation = (*ChannelMonitorRequestTemplateMutation)(nil)
+
+// channelmonitorrequesttemplateOption allows management of the mutation configuration using functional options.
+type channelmonitorrequesttemplateOption func(*ChannelMonitorRequestTemplateMutation)
+
+// newChannelMonitorRequestTemplateMutation creates new mutation for the ChannelMonitorRequestTemplate entity.
+func newChannelMonitorRequestTemplateMutation(c config, op Op, opts ...channelmonitorrequesttemplateOption) *ChannelMonitorRequestTemplateMutation {
+ m := &ChannelMonitorRequestTemplateMutation{
+ config: c,
+ op: op,
+ typ: TypeChannelMonitorRequestTemplate,
+ clearedFields: make(map[string]struct{}),
+ }
+ for _, opt := range opts {
+ opt(m)
+ }
+ return m
+}
+
+// withChannelMonitorRequestTemplateID sets the ID field of the mutation.
+func withChannelMonitorRequestTemplateID(id int64) channelmonitorrequesttemplateOption {
+ return func(m *ChannelMonitorRequestTemplateMutation) {
+ var (
+ err error
+ once sync.Once
+ value *ChannelMonitorRequestTemplate
+ )
+ m.oldValue = func(ctx context.Context) (*ChannelMonitorRequestTemplate, error) {
+ once.Do(func() {
+ if m.done {
+ err = errors.New("querying old values post mutation is not allowed")
+ } else {
+ value, err = m.Client().ChannelMonitorRequestTemplate.Get(ctx, id)
+ }
+ })
+ return value, err
+ }
+ m.id = &id
+ }
+}
+
+// withChannelMonitorRequestTemplate sets the old ChannelMonitorRequestTemplate of the mutation.
+func withChannelMonitorRequestTemplate(node *ChannelMonitorRequestTemplate) channelmonitorrequesttemplateOption {
+ return func(m *ChannelMonitorRequestTemplateMutation) {
+ m.oldValue = func(context.Context) (*ChannelMonitorRequestTemplate, error) {
+ return node, nil
+ }
+ m.id = &node.ID
+ }
+}
+
+// Client returns a new `ent.Client` from the mutation. If the mutation was
+// executed in a transaction (ent.Tx), a transactional client is returned.
+func (m ChannelMonitorRequestTemplateMutation) Client() *Client {
+ client := &Client{config: m.config}
+ client.init()
+ return client
+}
+
+// Tx returns an `ent.Tx` for mutations that were executed in transactions;
+// it returns an error otherwise.
+func (m ChannelMonitorRequestTemplateMutation) Tx() (*Tx, error) {
+ if _, ok := m.driver.(*txDriver); !ok {
+ return nil, errors.New("ent: mutation is not running in a transaction")
+ }
+ tx := &Tx{config: m.config}
+ tx.init()
+ return tx, nil
+}
+
+// ID returns the ID value in the mutation. Note that the ID is only available
+// if it was provided to the builder or after it was returned from the database.
+func (m *ChannelMonitorRequestTemplateMutation) ID() (id int64, exists bool) {
+ if m.id == nil {
+ return
+ }
+ return *m.id, true
+}
+
+// IDs queries the database and returns the entity ids that match the mutation's predicate.
+// That means, if the mutation is applied within a transaction with an isolation level such
+// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
+// or updated by the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) IDs(ctx context.Context) ([]int64, error) {
+ switch {
+ case m.op.Is(OpUpdateOne | OpDeleteOne):
+ id, exists := m.ID()
+ if exists {
+ return []int64{id}, nil
+ }
+ fallthrough
+ case m.op.Is(OpUpdate | OpDelete):
+ return m.Client().ChannelMonitorRequestTemplate.Query().Where(m.predicates...).IDs(ctx)
+ default:
+ return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
+ }
+}
+
+// SetCreatedAt sets the "created_at" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetCreatedAt(t time.Time) {
+ m.created_at = &t
+}
+
+// CreatedAt returns the value of the "created_at" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) CreatedAt() (r time.Time, exists bool) {
+ v := m.created_at
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldCreatedAt returns the old "created_at" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldCreatedAt requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err)
+ }
+ return oldValue.CreatedAt, nil
+}
+
+// ResetCreatedAt resets all changes to the "created_at" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetCreatedAt() {
+ m.created_at = nil
+}
+
+// SetUpdatedAt sets the "updated_at" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetUpdatedAt(t time.Time) {
+ m.updated_at = &t
+}
+
+// UpdatedAt returns the value of the "updated_at" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) UpdatedAt() (r time.Time, exists bool) {
+ v := m.updated_at
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldUpdatedAt returns the old "updated_at" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldUpdatedAt requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err)
+ }
+ return oldValue.UpdatedAt, nil
+}
+
+// ResetUpdatedAt resets all changes to the "updated_at" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetUpdatedAt() {
+ m.updated_at = nil
+}
+
+// SetName sets the "name" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetName(s string) {
+ m.name = &s
+}
+
+// Name returns the value of the "name" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) Name() (r string, exists bool) {
+ v := m.name
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldName returns the old "name" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldName(ctx context.Context) (v string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldName is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldName requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldName: %w", err)
+ }
+ return oldValue.Name, nil
+}
+
+// ResetName resets all changes to the "name" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetName() {
+ m.name = nil
+}
+
+// SetProvider sets the "provider" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetProvider(c channelmonitorrequesttemplate.Provider) {
+ m.provider = &c
+}
+
+// Provider returns the value of the "provider" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) Provider() (r channelmonitorrequesttemplate.Provider, exists bool) {
+ v := m.provider
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldProvider returns the old "provider" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldProvider(ctx context.Context) (v channelmonitorrequesttemplate.Provider, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldProvider is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldProvider requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldProvider: %w", err)
+ }
+ return oldValue.Provider, nil
+}
+
+// ResetProvider resets all changes to the "provider" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetProvider() {
+ m.provider = nil
+}
+
+// SetDescription sets the "description" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetDescription(s string) {
+ m.description = &s
+}
+
+// Description returns the value of the "description" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) Description() (r string, exists bool) {
+ v := m.description
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldDescription returns the old "description" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldDescription(ctx context.Context) (v string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldDescription is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldDescription requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldDescription: %w", err)
+ }
+ return oldValue.Description, nil
+}
+
+// ClearDescription clears the value of the "description" field.
+func (m *ChannelMonitorRequestTemplateMutation) ClearDescription() {
+ m.description = nil
+ m.clearedFields[channelmonitorrequesttemplate.FieldDescription] = struct{}{}
+}
+
+// DescriptionCleared returns if the "description" field was cleared in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) DescriptionCleared() bool {
+ _, ok := m.clearedFields[channelmonitorrequesttemplate.FieldDescription]
+ return ok
+}
+
+// ResetDescription resets all changes to the "description" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetDescription() {
+ m.description = nil
+ delete(m.clearedFields, channelmonitorrequesttemplate.FieldDescription)
+}
+
+// SetExtraHeaders sets the "extra_headers" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetExtraHeaders(value map[string]string) {
+ m.extra_headers = &value
+}
+
+// ExtraHeaders returns the value of the "extra_headers" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) ExtraHeaders() (r map[string]string, exists bool) {
+ v := m.extra_headers
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldExtraHeaders returns the old "extra_headers" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldExtraHeaders(ctx context.Context) (v map[string]string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldExtraHeaders is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldExtraHeaders requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldExtraHeaders: %w", err)
+ }
+ return oldValue.ExtraHeaders, nil
+}
+
+// ResetExtraHeaders resets all changes to the "extra_headers" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetExtraHeaders() {
+ m.extra_headers = nil
+}
+
+// SetBodyOverrideMode sets the "body_override_mode" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetBodyOverrideMode(s string) {
+ m.body_override_mode = &s
+}
+
+// BodyOverrideMode returns the value of the "body_override_mode" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) BodyOverrideMode() (r string, exists bool) {
+ v := m.body_override_mode
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldBodyOverrideMode returns the old "body_override_mode" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldBodyOverrideMode(ctx context.Context) (v string, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldBodyOverrideMode is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldBodyOverrideMode requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldBodyOverrideMode: %w", err)
+ }
+ return oldValue.BodyOverrideMode, nil
+}
+
+// ResetBodyOverrideMode resets all changes to the "body_override_mode" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetBodyOverrideMode() {
+ m.body_override_mode = nil
+}
+
+// SetBodyOverride sets the "body_override" field.
+func (m *ChannelMonitorRequestTemplateMutation) SetBodyOverride(value map[string]interface{}) {
+ m.body_override = &value
+}
+
+// BodyOverride returns the value of the "body_override" field in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) BodyOverride() (r map[string]interface{}, exists bool) {
+ v := m.body_override
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldBodyOverride returns the old "body_override" field's value of the ChannelMonitorRequestTemplate entity.
+// If the ChannelMonitorRequestTemplate object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *ChannelMonitorRequestTemplateMutation) OldBodyOverride(ctx context.Context) (v map[string]interface{}, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldBodyOverride is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldBodyOverride requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldBodyOverride: %w", err)
+ }
+ return oldValue.BodyOverride, nil
+}
+
+// ClearBodyOverride clears the value of the "body_override" field.
+func (m *ChannelMonitorRequestTemplateMutation) ClearBodyOverride() {
+ m.body_override = nil
+ m.clearedFields[channelmonitorrequesttemplate.FieldBodyOverride] = struct{}{}
+}
+
+// BodyOverrideCleared returns if the "body_override" field was cleared in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) BodyOverrideCleared() bool {
+ _, ok := m.clearedFields[channelmonitorrequesttemplate.FieldBodyOverride]
+ return ok
+}
+
+// ResetBodyOverride resets all changes to the "body_override" field.
+func (m *ChannelMonitorRequestTemplateMutation) ResetBodyOverride() {
+ m.body_override = nil
+ delete(m.clearedFields, channelmonitorrequesttemplate.FieldBodyOverride)
+}
+
+// AddMonitorIDs adds the "monitors" edge to the ChannelMonitor entity by ids.
+func (m *ChannelMonitorRequestTemplateMutation) AddMonitorIDs(ids ...int64) {
+ if m.monitors == nil {
+ m.monitors = make(map[int64]struct{})
+ }
+ for i := range ids {
+ m.monitors[ids[i]] = struct{}{}
+ }
+}
+
+// ClearMonitors clears the "monitors" edge to the ChannelMonitor entity.
+func (m *ChannelMonitorRequestTemplateMutation) ClearMonitors() {
+ m.clearedmonitors = true
+}
+
+// MonitorsCleared reports if the "monitors" edge to the ChannelMonitor entity was cleared.
+func (m *ChannelMonitorRequestTemplateMutation) MonitorsCleared() bool {
+ return m.clearedmonitors
+}
+
+// RemoveMonitorIDs removes the "monitors" edge to the ChannelMonitor entity by IDs.
+func (m *ChannelMonitorRequestTemplateMutation) RemoveMonitorIDs(ids ...int64) {
+ if m.removedmonitors == nil {
+ m.removedmonitors = make(map[int64]struct{})
+ }
+ for i := range ids {
+ delete(m.monitors, ids[i])
+ m.removedmonitors[ids[i]] = struct{}{}
+ }
+}
+
+// RemovedMonitors returns the removed IDs of the "monitors" edge to the ChannelMonitor entity.
+func (m *ChannelMonitorRequestTemplateMutation) RemovedMonitorsIDs() (ids []int64) {
+ for id := range m.removedmonitors {
+ ids = append(ids, id)
+ }
+ return
+}
+
+// MonitorsIDs returns the "monitors" edge IDs in the mutation.
+func (m *ChannelMonitorRequestTemplateMutation) MonitorsIDs() (ids []int64) {
+ for id := range m.monitors {
+ ids = append(ids, id)
+ }
+ return
+}
+
+// ResetMonitors resets all changes to the "monitors" edge.
+func (m *ChannelMonitorRequestTemplateMutation) ResetMonitors() {
+ m.monitors = nil
+ m.clearedmonitors = false
+ m.removedmonitors = nil
+}
+
+// Where appends a list predicates to the ChannelMonitorRequestTemplateMutation builder.
+func (m *ChannelMonitorRequestTemplateMutation) Where(ps ...predicate.ChannelMonitorRequestTemplate) {
+ m.predicates = append(m.predicates, ps...)
+}
+
+// WhereP appends storage-level predicates to the ChannelMonitorRequestTemplateMutation builder. Using this method,
+// users can use type-assertion to append predicates that do not depend on any generated package.
+func (m *ChannelMonitorRequestTemplateMutation) WhereP(ps ...func(*sql.Selector)) {
+ p := make([]predicate.ChannelMonitorRequestTemplate, len(ps))
+ for i := range ps {
+ p[i] = ps[i]
+ }
+ m.Where(p...)
+}
+
+// Op returns the operation name.
+func (m *ChannelMonitorRequestTemplateMutation) Op() Op {
+ return m.op
+}
+
+// SetOp allows setting the mutation operation.
+func (m *ChannelMonitorRequestTemplateMutation) SetOp(op Op) {
+ m.op = op
+}
+
+// Type returns the node type of this mutation (ChannelMonitorRequestTemplate).
+func (m *ChannelMonitorRequestTemplateMutation) Type() string {
+ return m.typ
+}
+
+// Fields returns all fields that were changed during this mutation. Note that in
+// order to get all numeric fields that were incremented/decremented, call
+// AddedFields().
+func (m *ChannelMonitorRequestTemplateMutation) Fields() []string {
+ fields := make([]string, 0, 8)
+ if m.created_at != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldCreatedAt)
+ }
+ if m.updated_at != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldUpdatedAt)
+ }
+ if m.name != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldName)
+ }
+ if m.provider != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldProvider)
+ }
+ if m.description != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldDescription)
+ }
+ if m.extra_headers != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldExtraHeaders)
+ }
+ if m.body_override_mode != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldBodyOverrideMode)
+ }
+ if m.body_override != nil {
+ fields = append(fields, channelmonitorrequesttemplate.FieldBodyOverride)
+ }
+ return fields
+}
+
+// Field returns the value of a field with the given name. The second boolean
+// return value indicates that this field was not set, or was not defined in the
+// schema.
+func (m *ChannelMonitorRequestTemplateMutation) Field(name string) (ent.Value, bool) {
+ switch name {
+ case channelmonitorrequesttemplate.FieldCreatedAt:
+ return m.CreatedAt()
+ case channelmonitorrequesttemplate.FieldUpdatedAt:
+ return m.UpdatedAt()
+ case channelmonitorrequesttemplate.FieldName:
+ return m.Name()
+ case channelmonitorrequesttemplate.FieldProvider:
+ return m.Provider()
+ case channelmonitorrequesttemplate.FieldDescription:
+ return m.Description()
+ case channelmonitorrequesttemplate.FieldExtraHeaders:
+ return m.ExtraHeaders()
+ case channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ return m.BodyOverrideMode()
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ return m.BodyOverride()
+ }
+ return nil, false
+}
+
+// OldField returns the old value of the field from the database. An error is
+// returned if the mutation operation is not UpdateOne, or the query to the
+// database failed.
+func (m *ChannelMonitorRequestTemplateMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
+ switch name {
+ case channelmonitorrequesttemplate.FieldCreatedAt:
+ return m.OldCreatedAt(ctx)
+ case channelmonitorrequesttemplate.FieldUpdatedAt:
+ return m.OldUpdatedAt(ctx)
+ case channelmonitorrequesttemplate.FieldName:
+ return m.OldName(ctx)
+ case channelmonitorrequesttemplate.FieldProvider:
+ return m.OldProvider(ctx)
+ case channelmonitorrequesttemplate.FieldDescription:
+ return m.OldDescription(ctx)
+ case channelmonitorrequesttemplate.FieldExtraHeaders:
+ return m.OldExtraHeaders(ctx)
+ case channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ return m.OldBodyOverrideMode(ctx)
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ return m.OldBodyOverride(ctx)
+ }
+ return nil, fmt.Errorf("unknown ChannelMonitorRequestTemplate field %s", name)
+}
+
+// SetField sets the value of a field with the given name. It returns an error if
+// the field is not defined in the schema, or if the type mismatched the field
+// type.
+func (m *ChannelMonitorRequestTemplateMutation) SetField(name string, value ent.Value) error {
+ switch name {
+ case channelmonitorrequesttemplate.FieldCreatedAt:
+ v, ok := value.(time.Time)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetCreatedAt(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldUpdatedAt:
+ v, ok := value.(time.Time)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetUpdatedAt(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldName:
+ v, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetName(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldProvider:
+ v, ok := value.(channelmonitorrequesttemplate.Provider)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetProvider(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldDescription:
+ v, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetDescription(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldExtraHeaders:
+ v, ok := value.(map[string]string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetExtraHeaders(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ v, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetBodyOverrideMode(v)
+ return nil
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ v, ok := value.(map[string]interface{})
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetBodyOverride(v)
+ return nil
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate field %s", name)
+}
+
+// AddedFields returns all numeric fields that were incremented/decremented during
+// this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) AddedFields() []string {
+ return nil
+}
+
+// AddedField returns the numeric value that was incremented/decremented on a field
+// with the given name. The second boolean return value indicates that this field
+// was not set, or was not defined in the schema.
+func (m *ChannelMonitorRequestTemplateMutation) AddedField(name string) (ent.Value, bool) {
+ return nil, false
+}
+
+// AddField adds the value to the field with the given name. It returns an error if
+// the field is not defined in the schema, or if the type mismatched the field
+// type.
+func (m *ChannelMonitorRequestTemplateMutation) AddField(name string, value ent.Value) error {
+ switch name {
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate numeric field %s", name)
+}
+
+// ClearedFields returns all nullable fields that were cleared during this
+// mutation.
+func (m *ChannelMonitorRequestTemplateMutation) ClearedFields() []string {
+ var fields []string
+ if m.FieldCleared(channelmonitorrequesttemplate.FieldDescription) {
+ fields = append(fields, channelmonitorrequesttemplate.FieldDescription)
+ }
+ if m.FieldCleared(channelmonitorrequesttemplate.FieldBodyOverride) {
+ fields = append(fields, channelmonitorrequesttemplate.FieldBodyOverride)
+ }
+ return fields
+}
+
+// FieldCleared returns a boolean indicating if a field with the given name was
+// cleared in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) FieldCleared(name string) bool {
+ _, ok := m.clearedFields[name]
+ return ok
+}
+
+// ClearField clears the value of the field with the given name. It returns an
+// error if the field is not defined in the schema.
+func (m *ChannelMonitorRequestTemplateMutation) ClearField(name string) error {
+ switch name {
+ case channelmonitorrequesttemplate.FieldDescription:
+ m.ClearDescription()
+ return nil
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ m.ClearBodyOverride()
+ return nil
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate nullable field %s", name)
+}
+
+// ResetField resets all changes in the mutation for the field with the given name.
+// It returns an error if the field is not defined in the schema.
+func (m *ChannelMonitorRequestTemplateMutation) ResetField(name string) error {
+ switch name {
+ case channelmonitorrequesttemplate.FieldCreatedAt:
+ m.ResetCreatedAt()
+ return nil
+ case channelmonitorrequesttemplate.FieldUpdatedAt:
+ m.ResetUpdatedAt()
+ return nil
+ case channelmonitorrequesttemplate.FieldName:
+ m.ResetName()
+ return nil
+ case channelmonitorrequesttemplate.FieldProvider:
+ m.ResetProvider()
+ return nil
+ case channelmonitorrequesttemplate.FieldDescription:
+ m.ResetDescription()
+ return nil
+ case channelmonitorrequesttemplate.FieldExtraHeaders:
+ m.ResetExtraHeaders()
+ return nil
+ case channelmonitorrequesttemplate.FieldBodyOverrideMode:
+ m.ResetBodyOverrideMode()
+ return nil
+ case channelmonitorrequesttemplate.FieldBodyOverride:
+ m.ResetBodyOverride()
+ return nil
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate field %s", name)
+}
+
+// AddedEdges returns all edge names that were set/added in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) AddedEdges() []string {
+ edges := make([]string, 0, 1)
+ if m.monitors != nil {
+ edges = append(edges, channelmonitorrequesttemplate.EdgeMonitors)
+ }
+ return edges
+}
+
+// AddedIDs returns all IDs (to other nodes) that were added for the given edge
+// name in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) AddedIDs(name string) []ent.Value {
+ switch name {
+ case channelmonitorrequesttemplate.EdgeMonitors:
+ ids := make([]ent.Value, 0, len(m.monitors))
+ for id := range m.monitors {
+ ids = append(ids, id)
+ }
+ return ids
+ }
+ return nil
+}
+
+// RemovedEdges returns all edge names that were removed in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) RemovedEdges() []string {
+ edges := make([]string, 0, 1)
+ if m.removedmonitors != nil {
+ edges = append(edges, channelmonitorrequesttemplate.EdgeMonitors)
+ }
+ return edges
+}
+
+// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
+// the given name in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) RemovedIDs(name string) []ent.Value {
+ switch name {
+ case channelmonitorrequesttemplate.EdgeMonitors:
+ ids := make([]ent.Value, 0, len(m.removedmonitors))
+ for id := range m.removedmonitors {
+ ids = append(ids, id)
+ }
+ return ids
+ }
+ return nil
+}
+
+// ClearedEdges returns all edge names that were cleared in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) ClearedEdges() []string {
+ edges := make([]string, 0, 1)
+ if m.clearedmonitors {
+ edges = append(edges, channelmonitorrequesttemplate.EdgeMonitors)
+ }
+ return edges
+}
+
+// EdgeCleared returns a boolean which indicates if the edge with the given name
+// was cleared in this mutation.
+func (m *ChannelMonitorRequestTemplateMutation) EdgeCleared(name string) bool {
+ switch name {
+ case channelmonitorrequesttemplate.EdgeMonitors:
+ return m.clearedmonitors
+ }
+ return false
+}
+
+// ClearEdge clears the value of the edge with the given name. It returns an error
+// if that edge is not defined in the schema.
+func (m *ChannelMonitorRequestTemplateMutation) ClearEdge(name string) error {
+ switch name {
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate unique edge %s", name)
+}
+
+// ResetEdge resets all changes to the edge with the given name in this mutation.
+// It returns an error if the edge is not defined in the schema.
+func (m *ChannelMonitorRequestTemplateMutation) ResetEdge(name string) error {
+ switch name {
+ case channelmonitorrequesttemplate.EdgeMonitors:
+ m.ResetMonitors()
+ return nil
+ }
+ return fmt.Errorf("unknown ChannelMonitorRequestTemplate edge %s", name)
+}
+
// ErrorPassthroughRuleMutation represents an operation that mutates the ErrorPassthroughRule nodes in the graph.
type ErrorPassthroughRuleMutation struct {
config
diff --git a/backend/ent/predicate/predicate.go b/backend/ent/predicate/predicate.go
index adb9a085..dc86471e 100644
--- a/backend/ent/predicate/predicate.go
+++ b/backend/ent/predicate/predicate.go
@@ -36,6 +36,9 @@ type ChannelMonitorDailyRollup func(*sql.Selector)
// ChannelMonitorHistory is the predicate function for channelmonitorhistory builders.
type ChannelMonitorHistory func(*sql.Selector)
+// ChannelMonitorRequestTemplate is the predicate function for channelmonitorrequesttemplate builders.
+type ChannelMonitorRequestTemplate func(*sql.Selector)
+
// ErrorPassthroughRule is the predicate function for errorpassthroughrule builders.
type ErrorPassthroughRule func(*sql.Selector)
diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go
index 63552bb5..aaa939c5 100644
--- a/backend/ent/runtime/runtime.go
+++ b/backend/ent/runtime/runtime.go
@@ -15,6 +15,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
@@ -521,6 +522,16 @@ func init() {
channelmonitorDescIntervalSeconds := channelmonitorFields[8].Descriptor()
// channelmonitor.IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save.
channelmonitor.IntervalSecondsValidator = channelmonitorDescIntervalSeconds.Validators[0].(func(int) error)
+ // channelmonitorDescExtraHeaders is the schema descriptor for extra_headers field.
+ channelmonitorDescExtraHeaders := channelmonitorFields[12].Descriptor()
+ // channelmonitor.DefaultExtraHeaders holds the default value on creation for the extra_headers field.
+ channelmonitor.DefaultExtraHeaders = channelmonitorDescExtraHeaders.Default.(map[string]string)
+ // channelmonitorDescBodyOverrideMode is the schema descriptor for body_override_mode field.
+ channelmonitorDescBodyOverrideMode := channelmonitorFields[13].Descriptor()
+ // channelmonitor.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field.
+ channelmonitor.DefaultBodyOverrideMode = channelmonitorDescBodyOverrideMode.Default.(string)
+ // channelmonitor.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
+ channelmonitor.BodyOverrideModeValidator = channelmonitorDescBodyOverrideMode.Validators[0].(func(string) error)
channelmonitordailyrollupFields := schema.ChannelMonitorDailyRollup{}.Fields()
_ = channelmonitordailyrollupFields
// channelmonitordailyrollupDescModel is the schema descriptor for model field.
@@ -617,6 +628,55 @@ func init() {
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[6].Descriptor()
// channelmonitorhistory.DefaultCheckedAt holds the default value on creation for the checked_at field.
channelmonitorhistory.DefaultCheckedAt = channelmonitorhistoryDescCheckedAt.Default.(func() time.Time)
+ channelmonitorrequesttemplateMixin := schema.ChannelMonitorRequestTemplate{}.Mixin()
+ channelmonitorrequesttemplateMixinFields0 := channelmonitorrequesttemplateMixin[0].Fields()
+ _ = channelmonitorrequesttemplateMixinFields0
+ channelmonitorrequesttemplateFields := schema.ChannelMonitorRequestTemplate{}.Fields()
+ _ = channelmonitorrequesttemplateFields
+ // channelmonitorrequesttemplateDescCreatedAt is the schema descriptor for created_at field.
+ channelmonitorrequesttemplateDescCreatedAt := channelmonitorrequesttemplateMixinFields0[0].Descriptor()
+ // channelmonitorrequesttemplate.DefaultCreatedAt holds the default value on creation for the created_at field.
+ channelmonitorrequesttemplate.DefaultCreatedAt = channelmonitorrequesttemplateDescCreatedAt.Default.(func() time.Time)
+ // channelmonitorrequesttemplateDescUpdatedAt is the schema descriptor for updated_at field.
+ channelmonitorrequesttemplateDescUpdatedAt := channelmonitorrequesttemplateMixinFields0[1].Descriptor()
+ // channelmonitorrequesttemplate.DefaultUpdatedAt holds the default value on creation for the updated_at field.
+ channelmonitorrequesttemplate.DefaultUpdatedAt = channelmonitorrequesttemplateDescUpdatedAt.Default.(func() time.Time)
+ // channelmonitorrequesttemplate.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
+ channelmonitorrequesttemplate.UpdateDefaultUpdatedAt = channelmonitorrequesttemplateDescUpdatedAt.UpdateDefault.(func() time.Time)
+ // channelmonitorrequesttemplateDescName is the schema descriptor for name field.
+ channelmonitorrequesttemplateDescName := channelmonitorrequesttemplateFields[0].Descriptor()
+ // channelmonitorrequesttemplate.NameValidator is a validator for the "name" field. It is called by the builders before save.
+ channelmonitorrequesttemplate.NameValidator = func() func(string) error {
+ validators := channelmonitorrequesttemplateDescName.Validators
+ fns := [...]func(string) error{
+ validators[0].(func(string) error),
+ validators[1].(func(string) error),
+ }
+ return func(name string) error {
+ for _, fn := range fns {
+ if err := fn(name); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ }()
+ // channelmonitorrequesttemplateDescDescription is the schema descriptor for description field.
+ channelmonitorrequesttemplateDescDescription := channelmonitorrequesttemplateFields[2].Descriptor()
+ // channelmonitorrequesttemplate.DefaultDescription holds the default value on creation for the description field.
+ channelmonitorrequesttemplate.DefaultDescription = channelmonitorrequesttemplateDescDescription.Default.(string)
+ // channelmonitorrequesttemplate.DescriptionValidator is a validator for the "description" field. It is called by the builders before save.
+ channelmonitorrequesttemplate.DescriptionValidator = channelmonitorrequesttemplateDescDescription.Validators[0].(func(string) error)
+ // channelmonitorrequesttemplateDescExtraHeaders is the schema descriptor for extra_headers field.
+ channelmonitorrequesttemplateDescExtraHeaders := channelmonitorrequesttemplateFields[3].Descriptor()
+ // channelmonitorrequesttemplate.DefaultExtraHeaders holds the default value on creation for the extra_headers field.
+ channelmonitorrequesttemplate.DefaultExtraHeaders = channelmonitorrequesttemplateDescExtraHeaders.Default.(map[string]string)
+ // channelmonitorrequesttemplateDescBodyOverrideMode is the schema descriptor for body_override_mode field.
+ channelmonitorrequesttemplateDescBodyOverrideMode := channelmonitorrequesttemplateFields[4].Descriptor()
+ // channelmonitorrequesttemplate.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field.
+ channelmonitorrequesttemplate.DefaultBodyOverrideMode = channelmonitorrequesttemplateDescBodyOverrideMode.Default.(string)
+ // channelmonitorrequesttemplate.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
+ channelmonitorrequesttemplate.BodyOverrideModeValidator = channelmonitorrequesttemplateDescBodyOverrideMode.Validators[0].(func(string) error)
errorpassthroughruleMixin := schema.ErrorPassthroughRule{}.Mixin()
errorpassthroughruleMixinFields0 := errorpassthroughruleMixin[0].Fields()
_ = errorpassthroughruleMixinFields0
diff --git a/backend/ent/schema/channel_monitor.go b/backend/ent/schema/channel_monitor.go
index f6a6578d..355ade4b 100644
--- a/backend/ent/schema/channel_monitor.go
+++ b/backend/ent/schema/channel_monitor.go
@@ -62,6 +62,26 @@ func (ChannelMonitor) Fields() []ent.Field {
Optional().
Nillable(),
field.Int64("created_by"),
+
+ // ---- 自定义请求快照字段(来自模板 / 手动编辑) ----
+
+ // template_id: 关联的请求模板 ID(仅用于 UI 分组 + 一键应用)。
+ // 实际运行时 checker 只读下面 3 个快照字段,**不再回查模板表**。
+ // 模板被删除时此字段会被 SET NULL(见 Edges 的 OnDelete 注解)。
+ field.Int64("template_id").
+ Optional().
+ Nillable(),
+ // extra_headers: 自定义 HTTP 头快照(来自模板 or 用户手填)。
+ // 运行时 merge 进 adapter 默认 headers。
+ field.JSON("extra_headers", map[string]string{}).
+ Default(map[string]string{}),
+ // body_override_mode: 同 ChannelMonitorRequestTemplate.body_override_mode
+ field.String("body_override_mode").
+ Default("off").
+ MaxLen(10),
+ // body_override: 同 ChannelMonitorRequestTemplate.body_override
+ field.JSON("body_override", map[string]any{}).
+ Optional(),
}
}
@@ -71,6 +91,12 @@ func (ChannelMonitor) Edges() []ent.Edge {
Annotations(entsql.OnDelete(entsql.Cascade)),
edge.To("daily_rollups", ChannelMonitorDailyRollup.Type).
Annotations(entsql.OnDelete(entsql.Cascade)),
+ // 关联请求模板:模板被删除时 template_id 自动置空,
+ // 监控本身保留(继续用快照字段跑)。
+ edge.To("request_template", ChannelMonitorRequestTemplate.Type).
+ Field("template_id").
+ Unique().
+ Annotations(entsql.OnDelete(entsql.SetNull)),
}
}
@@ -79,5 +105,6 @@ func (ChannelMonitor) Indexes() []ent.Index {
index.Fields("enabled", "last_checked_at"),
index.Fields("provider"),
index.Fields("group_name"),
+ index.Fields("template_id"),
}
}
diff --git a/backend/ent/schema/channel_monitor_request_template.go b/backend/ent/schema/channel_monitor_request_template.go
new file mode 100644
index 00000000..59df2f29
--- /dev/null
+++ b/backend/ent/schema/channel_monitor_request_template.go
@@ -0,0 +1,80 @@
+package schema
+
+import (
+ "github.com/Wei-Shaw/sub2api/ent/schema/mixins"
+
+ "entgo.io/ent"
+ "entgo.io/ent/dialect/entsql"
+ "entgo.io/ent/schema"
+ "entgo.io/ent/schema/edge"
+ "entgo.io/ent/schema/field"
+ "entgo.io/ent/schema/index"
+)
+
+// ChannelMonitorRequestTemplate 请求模板:一组可复用的 headers + 可选 body 覆盖配置。
+//
+// 语义为快照:模板被"应用"到监控时,extra_headers / body_override_mode / body_override
+// 会被**拷贝**到 channel_monitors 同名字段;后续模板变动不会自动影响已应用的监控——
+// 必须用户主动在模板编辑 Dialog 里点「应用到关联监控」才会覆盖快照。
+// 这样模板改错不会瞬间打挂所有已经跑起来的监控。
+type ChannelMonitorRequestTemplate struct {
+ ent.Schema
+}
+
+func (ChannelMonitorRequestTemplate) Annotations() []schema.Annotation {
+ return []schema.Annotation{
+ entsql.Annotation{Table: "channel_monitor_request_templates"},
+ }
+}
+
+func (ChannelMonitorRequestTemplate) Mixin() []ent.Mixin {
+ return []ent.Mixin{
+ mixins.TimeMixin{},
+ }
+}
+
+func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
+ return []ent.Field{
+ field.String("name").
+ NotEmpty().
+ MaxLen(100),
+ field.Enum("provider").
+ Values("openai", "anthropic", "gemini"),
+ field.String("description").
+ Optional().
+ Default("").
+ MaxLen(500),
+ // extra_headers: 用户自定义 HTTP 头(如 User-Agent 伪装)。
+ // 运行时 merge 进 adapter 默认 headers,用户值优先;
+ // hop-by-hop 黑名单(Host/Content-Length/...)由 checker 过滤。
+ field.JSON("extra_headers", map[string]string{}).
+ Default(map[string]string{}),
+ // body_override_mode: 'off' | 'merge' | 'replace'
+ // off - 用 adapter 默认 body(忽略 body_override)
+ // merge - adapter 默认 body 与 body_override 浅合并(body_override 优先,
+ // model/messages/contents 等关键字段在 checker 里走黑名单跳过)
+ // replace - 直接用 body_override 作为完整 body;此时跳过 challenge 校验,
+ // 改为 HTTP 2xx + 响应文本非空即视为可用
+ field.String("body_override_mode").
+ Default("off").
+ MaxLen(10),
+ // body_override: JSON 对象,根据 body_override_mode 使用。
+ // 用 map[string]any 以便前端传任意结构(含嵌套)。
+ field.JSON("body_override", map[string]any{}).
+ Optional(),
+ }
+}
+
+func (ChannelMonitorRequestTemplate) Edges() []ent.Edge {
+ return []ent.Edge{
+ edge.From("monitors", ChannelMonitor.Type).
+ Ref("request_template"),
+ }
+}
+
+func (ChannelMonitorRequestTemplate) Indexes() []ent.Index {
+ return []ent.Index{
+ // 同一 provider 内 name 唯一:允许 Anthropic + OpenAI 重名 "伪装官方客户端"。
+ index.Fields("provider", "name").Unique(),
+ }
+}
diff --git a/backend/ent/tx.go b/backend/ent/tx.go
index 0e65a940..611028e9 100644
--- a/backend/ent/tx.go
+++ b/backend/ent/tx.go
@@ -34,6 +34,8 @@ type Tx struct {
ChannelMonitorDailyRollup *ChannelMonitorDailyRollupClient
// ChannelMonitorHistory is the client for interacting with the ChannelMonitorHistory builders.
ChannelMonitorHistory *ChannelMonitorHistoryClient
+ // ChannelMonitorRequestTemplate is the client for interacting with the ChannelMonitorRequestTemplate builders.
+ ChannelMonitorRequestTemplate *ChannelMonitorRequestTemplateClient
// ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders.
ErrorPassthroughRule *ErrorPassthroughRuleClient
// Group is the client for interacting with the Group builders.
@@ -221,6 +223,7 @@ func (tx *Tx) init() {
tx.ChannelMonitor = NewChannelMonitorClient(tx.config)
tx.ChannelMonitorDailyRollup = NewChannelMonitorDailyRollupClient(tx.config)
tx.ChannelMonitorHistory = NewChannelMonitorHistoryClient(tx.config)
+ tx.ChannelMonitorRequestTemplate = NewChannelMonitorRequestTemplateClient(tx.config)
tx.ErrorPassthroughRule = NewErrorPassthroughRuleClient(tx.config)
tx.Group = NewGroupClient(tx.config)
tx.IdempotencyRecord = NewIdempotencyRecordClient(tx.config)
diff --git a/backend/internal/handler/admin/channel_monitor_handler.go b/backend/internal/handler/admin/channel_monitor_handler.go
index ce86c3dc..e92c81fe 100644
--- a/backend/internal/handler/admin/channel_monitor_handler.go
+++ b/backend/internal/handler/admin/channel_monitor_handler.go
@@ -36,27 +36,36 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
// --- Request / Response ---
type channelMonitorCreateRequest struct {
- Name string `json:"name" binding:"required,max=100"`
- Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
- Endpoint string `json:"endpoint" binding:"required,max=500"`
- APIKey string `json:"api_key" binding:"required,max=2000"`
- PrimaryModel string `json:"primary_model" binding:"required,max=200"`
- ExtraModels []string `json:"extra_models"`
- GroupName string `json:"group_name" binding:"max=100"`
- Enabled *bool `json:"enabled"`
- IntervalSeconds int `json:"interval_seconds" binding:"required,min=15,max=3600"`
+ Name string `json:"name" binding:"required,max=100"`
+ Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
+ Endpoint string `json:"endpoint" binding:"required,max=500"`
+ APIKey string `json:"api_key" binding:"required,max=2000"`
+ PrimaryModel string `json:"primary_model" binding:"required,max=200"`
+ ExtraModels []string `json:"extra_models"`
+ GroupName string `json:"group_name" binding:"max=100"`
+ Enabled *bool `json:"enabled"`
+ IntervalSeconds int `json:"interval_seconds" binding:"required,min=15,max=3600"`
+ TemplateID *int64 `json:"template_id"`
+ ExtraHeaders map[string]string `json:"extra_headers"`
+ BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
+ BodyOverride map[string]any `json:"body_override"`
}
type channelMonitorUpdateRequest struct {
- Name *string `json:"name" binding:"omitempty,max=100"`
- Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini"`
- Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
- APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
- PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
- ExtraModels *[]string `json:"extra_models"`
- GroupName *string `json:"group_name" binding:"omitempty,max=100"`
- Enabled *bool `json:"enabled"`
- IntervalSeconds *int `json:"interval_seconds" binding:"omitempty,min=15,max=3600"`
+ Name *string `json:"name" binding:"omitempty,max=100"`
+ Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini"`
+ Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
+ APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
+ PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
+ ExtraModels *[]string `json:"extra_models"`
+ GroupName *string `json:"group_name" binding:"omitempty,max=100"`
+ Enabled *bool `json:"enabled"`
+ IntervalSeconds *int `json:"interval_seconds" binding:"omitempty,min=15,max=3600"`
+ TemplateID *int64 `json:"template_id"`
+ ClearTemplate bool `json:"clear_template"` // true 时把 template_id 置空,忽略 TemplateID
+ ExtraHeaders *map[string]string `json:"extra_headers"`
+ BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
+ BodyOverride *map[string]any `json:"body_override"`
}
type channelMonitorResponse struct {
@@ -79,6 +88,11 @@ type channelMonitorResponse struct {
PrimaryLatencyMs *int `json:"primary_latency_ms"`
Availability7d float64 `json:"availability_7d"`
ExtraModelsStatus []dto.ChannelMonitorExtraModelStatus `json:"extra_models_status"`
+ // 请求自定义快照:前端编辑 / 展示「高级设置」用
+ TemplateID *int64 `json:"template_id"`
+ ExtraHeaders map[string]string `json:"extra_headers"`
+ BodyOverrideMode string `json:"body_override_mode"`
+ BodyOverride map[string]any `json:"body_override"`
}
type channelMonitorCheckResultResponse struct {
@@ -116,6 +130,10 @@ func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse
if extras == nil {
extras = []string{}
}
+ headers := m.ExtraHeaders
+ if headers == nil {
+ headers = map[string]string{}
+ }
resp := &channelMonitorResponse{
ID: m.ID,
Name: m.Name,
@@ -131,6 +149,10 @@ func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse
CreatedBy: m.CreatedBy,
CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: m.UpdatedAt.UTC().Format(time.RFC3339),
+ TemplateID: m.TemplateID,
+ ExtraHeaders: headers,
+ BodyOverrideMode: m.BodyOverrideMode,
+ BodyOverride: m.BodyOverride,
// PrimaryStatus / PrimaryLatencyMs / Availability7d 由 List handler 在批量聚合后填充。
}
if m.LastCheckedAt != nil {
@@ -279,16 +301,20 @@ func (h *ChannelMonitorHandler) Create(c *gin.Context) {
}
m, err := h.monitorService.Create(c.Request.Context(), service.ChannelMonitorCreateParams{
- Name: req.Name,
- Provider: req.Provider,
- Endpoint: req.Endpoint,
- APIKey: req.APIKey,
- PrimaryModel: req.PrimaryModel,
- ExtraModels: req.ExtraModels,
- GroupName: req.GroupName,
- Enabled: enabled,
- IntervalSeconds: req.IntervalSeconds,
- CreatedBy: subject.UserID,
+ Name: req.Name,
+ Provider: req.Provider,
+ Endpoint: req.Endpoint,
+ APIKey: req.APIKey,
+ PrimaryModel: req.PrimaryModel,
+ ExtraModels: req.ExtraModels,
+ GroupName: req.GroupName,
+ Enabled: enabled,
+ IntervalSeconds: req.IntervalSeconds,
+ CreatedBy: subject.UserID,
+ TemplateID: req.TemplateID,
+ ExtraHeaders: req.ExtraHeaders,
+ BodyOverrideMode: req.BodyOverrideMode,
+ BodyOverride: req.BodyOverride,
})
if err != nil {
response.ErrorFrom(c, err)
@@ -310,15 +336,20 @@ func (h *ChannelMonitorHandler) Update(c *gin.Context) {
}
m, err := h.monitorService.Update(c.Request.Context(), id, service.ChannelMonitorUpdateParams{
- Name: req.Name,
- Provider: req.Provider,
- Endpoint: req.Endpoint,
- APIKey: req.APIKey,
- PrimaryModel: req.PrimaryModel,
- ExtraModels: req.ExtraModels,
- GroupName: req.GroupName,
- Enabled: req.Enabled,
- IntervalSeconds: req.IntervalSeconds,
+ Name: req.Name,
+ Provider: req.Provider,
+ Endpoint: req.Endpoint,
+ APIKey: req.APIKey,
+ PrimaryModel: req.PrimaryModel,
+ ExtraModels: req.ExtraModels,
+ GroupName: req.GroupName,
+ Enabled: req.Enabled,
+ IntervalSeconds: req.IntervalSeconds,
+ TemplateID: req.TemplateID,
+ ClearTemplate: req.ClearTemplate,
+ ExtraHeaders: req.ExtraHeaders,
+ BodyOverrideMode: req.BodyOverrideMode,
+ BodyOverride: req.BodyOverride,
})
if err != nil {
response.ErrorFrom(c, err)
diff --git a/backend/internal/handler/admin/channel_monitor_template_handler.go b/backend/internal/handler/admin/channel_monitor_template_handler.go
new file mode 100644
index 00000000..8c1191ea
--- /dev/null
+++ b/backend/internal/handler/admin/channel_monitor_template_handler.go
@@ -0,0 +1,195 @@
+package admin
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/response"
+ "github.com/Wei-Shaw/sub2api/internal/service"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ChannelMonitorRequestTemplateHandler 请求模板管理后台 handler。
+type ChannelMonitorRequestTemplateHandler struct {
+ templateService *service.ChannelMonitorRequestTemplateService
+}
+
+// NewChannelMonitorRequestTemplateHandler 创建 handler。
+func NewChannelMonitorRequestTemplateHandler(templateService *service.ChannelMonitorRequestTemplateService) *ChannelMonitorRequestTemplateHandler {
+ return &ChannelMonitorRequestTemplateHandler{templateService: templateService}
+}
+
+// --- DTO ---
+
+type channelMonitorTemplateCreateRequest struct {
+ Name string `json:"name" binding:"required,max=100"`
+ Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
+ Description string `json:"description" binding:"max=500"`
+ ExtraHeaders map[string]string `json:"extra_headers"`
+ BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
+ BodyOverride map[string]any `json:"body_override"`
+}
+
+type channelMonitorTemplateUpdateRequest struct {
+ Name *string `json:"name" binding:"omitempty,max=100"`
+ Description *string `json:"description" binding:"omitempty,max=500"`
+ ExtraHeaders *map[string]string `json:"extra_headers"`
+ BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
+ BodyOverride *map[string]any `json:"body_override"`
+}
+
+type channelMonitorTemplateResponse struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Provider string `json:"provider"`
+ Description string `json:"description"`
+ ExtraHeaders map[string]string `json:"extra_headers"`
+ BodyOverrideMode string `json:"body_override_mode"`
+ BodyOverride map[string]any `json:"body_override"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+ AssociatedMonitors int64 `json:"associated_monitors"`
+}
+
+func (h *ChannelMonitorRequestTemplateHandler) toResponse(c *gin.Context, t *service.ChannelMonitorRequestTemplate) *channelMonitorTemplateResponse {
+ if t == nil {
+ return nil
+ }
+ headers := t.ExtraHeaders
+ if headers == nil {
+ headers = map[string]string{}
+ }
+ count, _ := h.templateService.CountAssociatedMonitors(c.Request.Context(), t.ID)
+ return &channelMonitorTemplateResponse{
+ ID: t.ID,
+ Name: t.Name,
+ Provider: t.Provider,
+ Description: t.Description,
+ ExtraHeaders: headers,
+ BodyOverrideMode: t.BodyOverrideMode,
+ BodyOverride: t.BodyOverride,
+ CreatedAt: t.CreatedAt.UTC().Format(time.RFC3339),
+ UpdatedAt: t.UpdatedAt.UTC().Format(time.RFC3339),
+ AssociatedMonitors: count,
+ }
+}
+
+// parseTemplateID 提取并校验 :id。
+func parseTemplateID(c *gin.Context) (int64, bool) {
+ id, err := strconv.ParseInt(c.Param("id"), 10, 64)
+ if err != nil || id <= 0 {
+ response.ErrorFrom(c, infraerrors.BadRequest("INVALID_TEMPLATE_ID", "invalid template id"))
+ return 0, false
+ }
+ return id, true
+}
+
+// --- Handlers ---
+
+// List GET /api/v1/admin/channel-monitor-templates?provider=anthropic
+func (h *ChannelMonitorRequestTemplateHandler) List(c *gin.Context) {
+ items, err := h.templateService.List(c.Request.Context(), service.ChannelMonitorRequestTemplateListParams{
+ Provider: strings.TrimSpace(c.Query("provider")),
+ })
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ out := make([]*channelMonitorTemplateResponse, 0, len(items))
+ for _, t := range items {
+ out = append(out, h.toResponse(c, t))
+ }
+ response.Success(c, gin.H{"items": out})
+}
+
+// Get GET /api/v1/admin/channel-monitor-templates/:id
+func (h *ChannelMonitorRequestTemplateHandler) Get(c *gin.Context) {
+ id, ok := parseTemplateID(c)
+ if !ok {
+ return
+ }
+ t, err := h.templateService.Get(c.Request.Context(), id)
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ response.Success(c, h.toResponse(c, t))
+}
+
+// Create POST /api/v1/admin/channel-monitor-templates
+func (h *ChannelMonitorRequestTemplateHandler) Create(c *gin.Context) {
+ var req channelMonitorTemplateCreateRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
+ return
+ }
+ t, err := h.templateService.Create(c.Request.Context(), service.ChannelMonitorRequestTemplateCreateParams{
+ Name: req.Name,
+ Provider: req.Provider,
+ Description: req.Description,
+ ExtraHeaders: req.ExtraHeaders,
+ BodyOverrideMode: req.BodyOverrideMode,
+ BodyOverride: req.BodyOverride,
+ })
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ response.Created(c, h.toResponse(c, t))
+}
+
+// Update PUT /api/v1/admin/channel-monitor-templates/:id
+func (h *ChannelMonitorRequestTemplateHandler) Update(c *gin.Context) {
+ id, ok := parseTemplateID(c)
+ if !ok {
+ return
+ }
+ var req channelMonitorTemplateUpdateRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
+ return
+ }
+ t, err := h.templateService.Update(c.Request.Context(), id, service.ChannelMonitorRequestTemplateUpdateParams{
+ Name: req.Name,
+ Description: req.Description,
+ ExtraHeaders: req.ExtraHeaders,
+ BodyOverrideMode: req.BodyOverrideMode,
+ BodyOverride: req.BodyOverride,
+ })
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ response.Success(c, h.toResponse(c, t))
+}
+
+// Delete DELETE /api/v1/admin/channel-monitor-templates/:id
+func (h *ChannelMonitorRequestTemplateHandler) Delete(c *gin.Context) {
+ id, ok := parseTemplateID(c)
+ if !ok {
+ return
+ }
+ if err := h.templateService.Delete(c.Request.Context(), id); err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ response.Success(c, nil)
+}
+
+// Apply POST /api/v1/admin/channel-monitor-templates/:id/apply
+// 一键把模板当前配置覆盖到所有关联监控上。
+func (h *ChannelMonitorRequestTemplateHandler) Apply(c *gin.Context) {
+ id, ok := parseTemplateID(c)
+ if !ok {
+ return
+ }
+ affected, err := h.templateService.ApplyToMonitors(c.Request.Context(), id)
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ response.Success(c, gin.H{"affected": affected})
+}
diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go
index 58480c93..bedb81ae 100644
--- a/backend/internal/handler/handler.go
+++ b/backend/internal/handler/handler.go
@@ -6,33 +6,34 @@ import (
// AdminHandlers contains all admin-related HTTP handlers
type AdminHandlers struct {
- Dashboard *admin.DashboardHandler
- User *admin.UserHandler
- Group *admin.GroupHandler
- Account *admin.AccountHandler
- Announcement *admin.AnnouncementHandler
- DataManagement *admin.DataManagementHandler
- Backup *admin.BackupHandler
- OAuth *admin.OAuthHandler
- OpenAIOAuth *admin.OpenAIOAuthHandler
- GeminiOAuth *admin.GeminiOAuthHandler
- AntigravityOAuth *admin.AntigravityOAuthHandler
- Proxy *admin.ProxyHandler
- Redeem *admin.RedeemHandler
- Promo *admin.PromoHandler
- Setting *admin.SettingHandler
- Ops *admin.OpsHandler
- System *admin.SystemHandler
- Subscription *admin.SubscriptionHandler
- Usage *admin.UsageHandler
- UserAttribute *admin.UserAttributeHandler
- ErrorPassthrough *admin.ErrorPassthroughHandler
- TLSFingerprintProfile *admin.TLSFingerprintProfileHandler
- APIKey *admin.AdminAPIKeyHandler
- ScheduledTest *admin.ScheduledTestHandler
- Channel *admin.ChannelHandler
- ChannelMonitor *admin.ChannelMonitorHandler
- Payment *admin.PaymentHandler
+ Dashboard *admin.DashboardHandler
+ User *admin.UserHandler
+ Group *admin.GroupHandler
+ Account *admin.AccountHandler
+ Announcement *admin.AnnouncementHandler
+ DataManagement *admin.DataManagementHandler
+ Backup *admin.BackupHandler
+ OAuth *admin.OAuthHandler
+ OpenAIOAuth *admin.OpenAIOAuthHandler
+ GeminiOAuth *admin.GeminiOAuthHandler
+ AntigravityOAuth *admin.AntigravityOAuthHandler
+ Proxy *admin.ProxyHandler
+ Redeem *admin.RedeemHandler
+ Promo *admin.PromoHandler
+ Setting *admin.SettingHandler
+ Ops *admin.OpsHandler
+ System *admin.SystemHandler
+ Subscription *admin.SubscriptionHandler
+ Usage *admin.UsageHandler
+ UserAttribute *admin.UserAttributeHandler
+ ErrorPassthrough *admin.ErrorPassthroughHandler
+ TLSFingerprintProfile *admin.TLSFingerprintProfileHandler
+ APIKey *admin.AdminAPIKeyHandler
+ ScheduledTest *admin.ScheduledTestHandler
+ Channel *admin.ChannelHandler
+ ChannelMonitor *admin.ChannelMonitorHandler
+ ChannelMonitorTemplate *admin.ChannelMonitorRequestTemplateHandler
+ Payment *admin.PaymentHandler
}
// Handlers contains all HTTP handlers
diff --git a/backend/internal/handler/wire.go b/backend/internal/handler/wire.go
index 7c1a5d1b..6584eb70 100644
--- a/backend/internal/handler/wire.go
+++ b/backend/internal/handler/wire.go
@@ -35,36 +35,38 @@ func ProvideAdminHandlers(
scheduledTestHandler *admin.ScheduledTestHandler,
channelHandler *admin.ChannelHandler,
channelMonitorHandler *admin.ChannelMonitorHandler,
+ channelMonitorTemplateHandler *admin.ChannelMonitorRequestTemplateHandler,
paymentHandler *admin.PaymentHandler,
) *AdminHandlers {
return &AdminHandlers{
- Dashboard: dashboardHandler,
- User: userHandler,
- Group: groupHandler,
- Account: accountHandler,
- Announcement: announcementHandler,
- DataManagement: dataManagementHandler,
- Backup: backupHandler,
- OAuth: oauthHandler,
- OpenAIOAuth: openaiOAuthHandler,
- GeminiOAuth: geminiOAuthHandler,
- AntigravityOAuth: antigravityOAuthHandler,
- Proxy: proxyHandler,
- Redeem: redeemHandler,
- Promo: promoHandler,
- Setting: settingHandler,
- Ops: opsHandler,
- System: systemHandler,
- Subscription: subscriptionHandler,
- Usage: usageHandler,
- UserAttribute: userAttributeHandler,
- ErrorPassthrough: errorPassthroughHandler,
- TLSFingerprintProfile: tlsFingerprintProfileHandler,
- APIKey: apiKeyHandler,
- ScheduledTest: scheduledTestHandler,
- Channel: channelHandler,
- ChannelMonitor: channelMonitorHandler,
- Payment: paymentHandler,
+ Dashboard: dashboardHandler,
+ User: userHandler,
+ Group: groupHandler,
+ Account: accountHandler,
+ Announcement: announcementHandler,
+ DataManagement: dataManagementHandler,
+ Backup: backupHandler,
+ OAuth: oauthHandler,
+ OpenAIOAuth: openaiOAuthHandler,
+ GeminiOAuth: geminiOAuthHandler,
+ AntigravityOAuth: antigravityOAuthHandler,
+ Proxy: proxyHandler,
+ Redeem: redeemHandler,
+ Promo: promoHandler,
+ Setting: settingHandler,
+ Ops: opsHandler,
+ System: systemHandler,
+ Subscription: subscriptionHandler,
+ Usage: usageHandler,
+ UserAttribute: userAttributeHandler,
+ ErrorPassthrough: errorPassthroughHandler,
+ TLSFingerprintProfile: tlsFingerprintProfileHandler,
+ APIKey: apiKeyHandler,
+ ScheduledTest: scheduledTestHandler,
+ Channel: channelHandler,
+ ChannelMonitor: channelMonitorHandler,
+ ChannelMonitorTemplate: channelMonitorTemplateHandler,
+ Payment: paymentHandler,
}
}
@@ -162,6 +164,7 @@ var ProviderSet = wire.NewSet(
admin.NewScheduledTestHandler,
admin.NewChannelHandler,
admin.NewChannelMonitorHandler,
+ admin.NewChannelMonitorRequestTemplateHandler,
admin.NewPaymentHandler,
// AdminHandlers and Handlers constructors
diff --git a/backend/internal/repository/channel_monitor_repo.go b/backend/internal/repository/channel_monitor_repo.go
index f4e2a0ec..67dccd6c 100644
--- a/backend/internal/repository/channel_monitor_repo.go
+++ b/backend/internal/repository/channel_monitor_repo.go
@@ -44,7 +44,15 @@ func (r *channelMonitorRepository) Create(ctx context.Context, m *service.Channe
SetGroupName(m.GroupName).
SetEnabled(m.Enabled).
SetIntervalSeconds(m.IntervalSeconds).
- SetCreatedBy(m.CreatedBy)
+ SetCreatedBy(m.CreatedBy).
+ SetExtraHeaders(emptyHeadersIfNilRepo(m.ExtraHeaders)).
+ SetBodyOverrideMode(defaultBodyModeRepo(m.BodyOverrideMode))
+ if m.TemplateID != nil {
+ builder = builder.SetTemplateID(*m.TemplateID)
+ }
+ if m.BodyOverride != nil {
+ builder = builder.SetBodyOverride(m.BodyOverride)
+ }
created, err := builder.Save(ctx)
if err != nil {
@@ -77,7 +85,19 @@ func (r *channelMonitorRepository) Update(ctx context.Context, m *service.Channe
SetExtraModels(emptySliceIfNil(m.ExtraModels)).
SetGroupName(m.GroupName).
SetEnabled(m.Enabled).
- SetIntervalSeconds(m.IntervalSeconds)
+ SetIntervalSeconds(m.IntervalSeconds).
+ SetExtraHeaders(emptyHeadersIfNilRepo(m.ExtraHeaders)).
+ SetBodyOverrideMode(defaultBodyModeRepo(m.BodyOverrideMode))
+ if m.TemplateID != nil {
+ updater = updater.SetTemplateID(*m.TemplateID)
+ } else {
+ updater = updater.ClearTemplateID()
+ }
+ if m.BodyOverride != nil {
+ updater = updater.SetBodyOverride(m.BodyOverride)
+ } else {
+ updater = updater.ClearBodyOverride()
+ }
updated, err := updater.Save(ctx)
if err != nil {
@@ -716,22 +736,51 @@ func entToServiceMonitor(row *dbent.ChannelMonitor) *service.ChannelMonitor {
if extras == nil {
extras = []string{}
}
- return &service.ChannelMonitor{
- ID: row.ID,
- Name: row.Name,
- Provider: string(row.Provider),
- Endpoint: row.Endpoint,
- APIKey: row.APIKeyEncrypted, // 仍为密文,service 层负责解密
- PrimaryModel: row.PrimaryModel,
- ExtraModels: extras,
- GroupName: row.GroupName,
- Enabled: row.Enabled,
- IntervalSeconds: row.IntervalSeconds,
- LastCheckedAt: row.LastCheckedAt,
- CreatedBy: row.CreatedBy,
- CreatedAt: row.CreatedAt,
- UpdatedAt: row.UpdatedAt,
+ headers := row.ExtraHeaders
+ if headers == nil {
+ headers = map[string]string{}
}
+ out := &service.ChannelMonitor{
+ ID: row.ID,
+ Name: row.Name,
+ Provider: string(row.Provider),
+ Endpoint: row.Endpoint,
+ APIKey: row.APIKeyEncrypted, // 仍为密文,service 层负责解密
+ PrimaryModel: row.PrimaryModel,
+ ExtraModels: extras,
+ GroupName: row.GroupName,
+ Enabled: row.Enabled,
+ IntervalSeconds: row.IntervalSeconds,
+ LastCheckedAt: row.LastCheckedAt,
+ CreatedBy: row.CreatedBy,
+ CreatedAt: row.CreatedAt,
+ UpdatedAt: row.UpdatedAt,
+ ExtraHeaders: headers,
+ BodyOverrideMode: row.BodyOverrideMode,
+ BodyOverride: row.BodyOverride,
+ }
+ if row.TemplateID != nil {
+ id := *row.TemplateID
+ out.TemplateID = &id
+ }
+ return out
+}
+
+// emptyHeadersIfNilRepo 与 service.emptyHeadersIfNil 功能一致,
+// repo 独立一份避免 import 循环。
+func emptyHeadersIfNilRepo(h map[string]string) map[string]string {
+ if h == nil {
+ return map[string]string{}
+ }
+ return h
+}
+
+// defaultBodyModeRepo 空串归一为 off(同上不循环)。
+func defaultBodyModeRepo(mode string) string {
+ if mode == "" {
+ return "off"
+ }
+ return mode
}
func emptySliceIfNil(in []string) []string {
diff --git a/backend/internal/repository/channel_monitor_template_repo.go b/backend/internal/repository/channel_monitor_template_repo.go
new file mode 100644
index 00000000..03f3692b
--- /dev/null
+++ b/backend/internal/repository/channel_monitor_template_repo.go
@@ -0,0 +1,168 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ dbent "github.com/Wei-Shaw/sub2api/ent"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitor"
+ "github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
+ "github.com/Wei-Shaw/sub2api/internal/service"
+)
+
+// channelMonitorRequestTemplateRepository 实现 service.ChannelMonitorRequestTemplateRepository。
+// 与 channelMonitorRepository 分开一个文件,职责清晰。
+type channelMonitorRequestTemplateRepository struct {
+ client *dbent.Client
+ db *sql.DB
+}
+
+// NewChannelMonitorRequestTemplateRepository 创建模板仓储实例。
+func NewChannelMonitorRequestTemplateRepository(client *dbent.Client, db *sql.DB) service.ChannelMonitorRequestTemplateRepository {
+ return &channelMonitorRequestTemplateRepository{client: client, db: db}
+}
+
+// ---------- CRUD ----------
+
+func (r *channelMonitorRequestTemplateRepository) Create(ctx context.Context, t *service.ChannelMonitorRequestTemplate) error {
+ client := clientFromContext(ctx, r.client)
+ builder := client.ChannelMonitorRequestTemplate.Create().
+ SetName(t.Name).
+ SetProvider(channelmonitorrequesttemplate.Provider(t.Provider)).
+ SetDescription(t.Description).
+ SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)).
+ SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode))
+ if t.BodyOverride != nil {
+ builder = builder.SetBodyOverride(t.BodyOverride)
+ }
+
+ created, err := builder.Save(ctx)
+ if err != nil {
+ return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
+ }
+ t.ID = created.ID
+ t.CreatedAt = created.CreatedAt
+ t.UpdatedAt = created.UpdatedAt
+ return nil
+}
+
+func (r *channelMonitorRequestTemplateRepository) GetByID(ctx context.Context, id int64) (*service.ChannelMonitorRequestTemplate, error) {
+ row, err := r.client.ChannelMonitorRequestTemplate.Query().
+ Where(channelmonitorrequesttemplate.IDEQ(id)).
+ Only(ctx)
+ if err != nil {
+ return nil, translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
+ }
+ return entToServiceTemplate(row), nil
+}
+
+func (r *channelMonitorRequestTemplateRepository) Update(ctx context.Context, t *service.ChannelMonitorRequestTemplate) error {
+ client := clientFromContext(ctx, r.client)
+ updater := client.ChannelMonitorRequestTemplate.UpdateOneID(t.ID).
+ SetName(t.Name).
+ SetDescription(t.Description).
+ SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)).
+ SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode))
+ if t.BodyOverride != nil {
+ updater = updater.SetBodyOverride(t.BodyOverride)
+ } else {
+ updater = updater.ClearBodyOverride()
+ }
+ updated, err := updater.Save(ctx)
+ if err != nil {
+ return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
+ }
+ t.UpdatedAt = updated.UpdatedAt
+ return nil
+}
+
+func (r *channelMonitorRequestTemplateRepository) Delete(ctx context.Context, id int64) error {
+ client := clientFromContext(ctx, r.client)
+ if err := client.ChannelMonitorRequestTemplate.DeleteOneID(id).Exec(ctx); err != nil {
+ return translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
+ }
+ return nil
+}
+
+func (r *channelMonitorRequestTemplateRepository) List(ctx context.Context, params service.ChannelMonitorRequestTemplateListParams) ([]*service.ChannelMonitorRequestTemplate, error) {
+ q := r.client.ChannelMonitorRequestTemplate.Query()
+ if params.Provider != "" {
+ q = q.Where(channelmonitorrequesttemplate.ProviderEQ(channelmonitorrequesttemplate.Provider(params.Provider)))
+ }
+ rows, err := q.
+ Order(dbent.Asc(channelmonitorrequesttemplate.FieldProvider), dbent.Asc(channelmonitorrequesttemplate.FieldName)).
+ All(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("list monitor templates: %w", err)
+ }
+ out := make([]*service.ChannelMonitorRequestTemplate, 0, len(rows))
+ for _, row := range rows {
+ out = append(out, entToServiceTemplate(row))
+ }
+ return out, nil
+}
+
+// ApplyToMonitors 把模板当前配置批量覆盖到 template_id = id 的监控上。
+//
+// 用一条 UPDATE 完成:extra_headers / body_override_mode / body_override 都覆盖。
+// 走 ent 的 UpdateMany 保证走 ent hooks;走原生 SQL 也可以但 ent jsonb 序列化更省心。
+func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Context, id int64) (int64, error) {
+ client := clientFromContext(ctx, r.client)
+ tpl, err := client.ChannelMonitorRequestTemplate.Query().
+ Where(channelmonitorrequesttemplate.IDEQ(id)).
+ Only(ctx)
+ if err != nil {
+ return 0, translatePersistenceError(err, service.ErrChannelMonitorTemplateNotFound, nil)
+ }
+
+ updater := client.ChannelMonitor.Update().
+ Where(channelmonitor.TemplateIDEQ(id)).
+ SetExtraHeaders(emptyHeadersIfNilRepo(tpl.ExtraHeaders)).
+ SetBodyOverrideMode(defaultBodyModeRepo(tpl.BodyOverrideMode))
+ if tpl.BodyOverride != nil {
+ updater = updater.SetBodyOverride(tpl.BodyOverride)
+ } else {
+ updater = updater.ClearBodyOverride()
+ }
+
+ affected, err := updater.Save(ctx)
+ if err != nil {
+ return 0, fmt.Errorf("apply template to monitors: %w", err)
+ }
+ return int64(affected), nil
+}
+
+// CountAssociatedMonitors 统计关联监控数(UI 展示「N 个配置」用)。
+func (r *channelMonitorRequestTemplateRepository) CountAssociatedMonitors(ctx context.Context, id int64) (int64, error) {
+ count, err := r.client.ChannelMonitor.Query().
+ Where(channelmonitor.TemplateIDEQ(id)).
+ Count(ctx)
+ if err != nil {
+ return 0, fmt.Errorf("count monitors for template %d: %w", id, err)
+ }
+ return int64(count), nil
+}
+
+// ---------- helpers ----------
+
+func entToServiceTemplate(row *dbent.ChannelMonitorRequestTemplate) *service.ChannelMonitorRequestTemplate {
+ if row == nil {
+ return nil
+ }
+ headers := row.ExtraHeaders
+ if headers == nil {
+ headers = map[string]string{}
+ }
+ return &service.ChannelMonitorRequestTemplate{
+ ID: row.ID,
+ Name: row.Name,
+ Provider: string(row.Provider),
+ Description: row.Description,
+ ExtraHeaders: headers,
+ BodyOverrideMode: row.BodyOverrideMode,
+ BodyOverride: row.BodyOverride,
+ CreatedAt: row.CreatedAt,
+ UpdatedAt: row.UpdatedAt,
+ }
+}
diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go
index 7427cd04..b1d5e36a 100644
--- a/backend/internal/repository/wire.go
+++ b/backend/internal/repository/wire.go
@@ -90,6 +90,7 @@ var ProviderSet = wire.NewSet(
NewTLSFingerprintProfileRepository,
NewChannelRepository,
NewChannelMonitorRepository,
+ NewChannelMonitorRequestTemplateRepository,
// Cache implementations
NewGatewayCache,
diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go
index 0381dc57..13cecd59 100644
--- a/backend/internal/server/routes/admin.go
+++ b/backend/internal/server/routes/admin.go
@@ -579,4 +579,14 @@ func registerChannelMonitorRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
monitors.POST("/:id/run", h.Admin.ChannelMonitor.Run)
monitors.GET("/:id/history", h.Admin.ChannelMonitor.History)
}
+
+ templates := admin.Group("/channel-monitor-templates")
+ {
+ templates.GET("", h.Admin.ChannelMonitorTemplate.List)
+ templates.POST("", h.Admin.ChannelMonitorTemplate.Create)
+ templates.GET("/:id", h.Admin.ChannelMonitorTemplate.Get)
+ templates.PUT("/:id", h.Admin.ChannelMonitorTemplate.Update)
+ templates.DELETE("/:id", h.Admin.ChannelMonitorTemplate.Delete)
+ templates.POST("/:id/apply", h.Admin.ChannelMonitorTemplate.Apply)
+ }
}
diff --git a/backend/internal/service/channel_monitor_checker.go b/backend/internal/service/channel_monitor_checker.go
index e03c2e3a..33570629 100644
--- a/backend/internal/service/channel_monitor_checker.go
+++ b/backend/internal/service/channel_monitor_checker.go
@@ -37,9 +37,23 @@ func newSSRFSafeHTTPClient(timeout time.Duration) *http.Client {
return &http.Client{Timeout: timeout, Transport: tr}
}
+// CheckOptions 承载一次检测的自定义入参。
+// 所有字段都是可选(零值即等价于"用默认行为")。
+type CheckOptions struct {
+ // ExtraHeaders 用户自定义 HTTP 头(merge 到 adapter 默认 headers,用户优先)。
+ ExtraHeaders map[string]string
+ // BodyOverrideMode: off | merge | replace
+ BodyOverrideMode string
+ // BodyOverride 在 merge 模式下做浅合并(key 命中黑名单时静默丢弃),
+ // 在 replace 模式下直接当作完整 body。
+ BodyOverride map[string]any
+}
+
// runCheckForModel 对单个 (provider, model) 做一次完整检测。
// 不返回 error:所有失败都包装进 CheckResult.Status=error/failed。
-func runCheckForModel(ctx context.Context, provider, endpoint, apiKey, model string) *CheckResult {
+//
+// opts 承载模板 / 监控快照带来的自定义配置。nil 等同于 "off + 无 extra headers"。
+func runCheckForModel(ctx context.Context, provider, endpoint, apiKey, model string, opts *CheckOptions) *CheckResult {
res := &CheckResult{
Model: model,
Status: MonitorStatusError,
@@ -47,9 +61,10 @@ func runCheckForModel(ctx context.Context, provider, endpoint, apiKey, model str
}
challenge := generateChallenge()
+ mode := bodyOverrideMode(opts)
start := time.Now()
- respText, rawBody, statusCode, err := callProvider(ctx, provider, endpoint, apiKey, model, challenge.Prompt)
+ respText, rawBody, statusCode, err := callProvider(ctx, provider, endpoint, apiKey, model, challenge.Prompt, opts)
latency := time.Since(start)
latencyMs := int(latency / time.Millisecond)
res.LatencyMs = &latencyMs
@@ -68,22 +83,47 @@ func runCheckForModel(ctx context.Context, provider, endpoint, apiKey, model str
return res
}
+ // Replace 模式:跳过 challenge 校验(用户 body 是静态的,challenge 没法嵌入)。
+ // 改用「HTTP 2xx + 响应文本(adapter.textPath 抽取)非空」作为 operational 判定。
+ // 响应文本为空则降级为 failed(视为上游回了 200 但没实际内容)。
+ if mode == MonitorBodyOverrideModeReplace {
+ if strings.TrimSpace(respText) == "" {
+ res.Status = MonitorStatusFailed
+ res.Message = truncateMessage("replace-mode: upstream returned 2xx with empty text")
+ return res
+ }
+ return finalizeOperationalOrDegraded(res, latency, latencyMs)
+ }
+
if !validateChallenge(respText, challenge.Expected) {
res.Status = MonitorStatusFailed
res.Message = truncateMessage(sanitizeErrorMessage(fmt.Sprintf("challenge mismatch (expected %s, got %q)", challenge.Expected, respText)))
return res
}
+ return finalizeOperationalOrDegraded(res, latency, latencyMs)
+}
+
+// finalizeOperationalOrDegraded 负责走到最后一步的 operational/degraded 判定。
+// 拆出来是为了让 runCheckForModel 不超过 30 行。
+func finalizeOperationalOrDegraded(res *CheckResult, latency time.Duration, latencyMs int) *CheckResult {
if latency >= monitorDegradedThreshold {
res.Status = MonitorStatusDegraded
res.Message = truncateMessage(fmt.Sprintf("slow response: %dms", latencyMs))
return res
}
-
res.Status = MonitorStatusOperational
return res
}
+// bodyOverrideMode 归一取 opts.BodyOverrideMode,nil opts / 空串都视为 off。
+func bodyOverrideMode(opts *CheckOptions) string {
+ if opts == nil || opts.BodyOverrideMode == "" {
+ return MonitorBodyOverrideModeOff
+ }
+ return opts.BodyOverrideMode
+}
+
// pingEndpointOrigin 对 endpoint 的 origin (scheme://host) 发起 HEAD 请求,返回耗时。
// 失败时返回 nil(不影响主状态判定)。
func pingEndpointOrigin(ctx context.Context, endpoint string) *int {
@@ -183,29 +223,109 @@ func isSupportedProvider(p string) bool {
}
// callProvider 通过 providerAdapters 分发到具体实现。
+// opts 承载用户的自定义 headers / body 覆盖(可为 nil)。
//
// 返回值:
// - extractedText: 按 textPath 抽出的成功文本,仅在 status 2xx 时有意义;非 2xx 时通常为空串
// - rawBody: 完整响应体的字符串形式(已被 monitorResponseMaxBytes 截断),用于错误路径保留上游真实回包
// - status: HTTP 状态码
// - err: 网络 / 序列化错误
-func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt string) (extractedText, rawBody string, status int, err error) {
+func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt string, opts *CheckOptions) (extractedText, rawBody string, status int, err error) {
adapter, ok := providerAdapters[provider]
if !ok {
return "", "", 0, fmt.Errorf("unsupported provider %q", provider)
}
- body, err := adapter.buildBody(model, prompt)
+ body, err := buildRequestBody(adapter, provider, model, prompt, opts)
if err != nil {
- return "", "", 0, fmt.Errorf("marshal body: %w", err)
+ return "", "", 0, err
}
+ headers := mergeHeaders(adapter.buildHeaders(apiKey), opts)
full := joinURL(endpoint, adapter.buildPath(model))
- respBytes, status, err := postRawJSON(ctx, full, body, adapter.buildHeaders(apiKey))
+ respBytes, status, err := postRawJSON(ctx, full, body, headers)
if err != nil {
return "", "", status, err
}
return gjson.GetBytes(respBytes, adapter.textPath).String(), string(respBytes), status, nil
}
+// mergeHeaders 把用户自定义 headers 合并到 adapter 默认 headers 上。
+// 用户值覆盖默认;命中黑名单(hop-by-hop / 由 http.Client 自管的)的 key 静默丢弃。
+func mergeHeaders(base map[string]string, opts *CheckOptions) map[string]string {
+ if opts == nil || len(opts.ExtraHeaders) == 0 {
+ return base
+ }
+ out := make(map[string]string, len(base)+len(opts.ExtraHeaders))
+ for k, v := range base {
+ out[k] = v
+ }
+ for k, v := range opts.ExtraHeaders {
+ if IsForbiddenHeaderName(k) {
+ continue
+ }
+ out[k] = v
+ }
+ return out
+}
+
+// buildRequestBody 根据 body_override_mode 构造请求 body。
+//
+// - off: adapter 默认 body
+// - merge: adapter 默认 body 与 BodyOverride 浅合并;BodyOverride 中命中
+// bodyMergeKeyDenyList[provider] 的 key 会被静默丢弃,避免破坏 challenge / model 路由
+// - replace: 直接 marshal BodyOverride 作为完整 body
+//
+// 任何 mode 返回的 []byte 都已经是合法 JSON,可直接送入 postRawJSON。
+func buildRequestBody(adapter providerAdapter, provider, model, prompt string, opts *CheckOptions) ([]byte, error) {
+ mode := bodyOverrideMode(opts)
+
+ if mode == MonitorBodyOverrideModeReplace {
+ if opts == nil || len(opts.BodyOverride) == 0 {
+ return nil, fmt.Errorf("replace mode: body_override is empty")
+ }
+ body, err := json.Marshal(opts.BodyOverride)
+ if err != nil {
+ return nil, fmt.Errorf("marshal body_override (replace): %w", err)
+ }
+ return body, nil
+ }
+
+ defaultBody, err := adapter.buildBody(model, prompt)
+ if err != nil {
+ return nil, fmt.Errorf("marshal default body: %w", err)
+ }
+ if mode != MonitorBodyOverrideModeMerge || opts == nil || len(opts.BodyOverride) == 0 {
+ return defaultBody, nil
+ }
+
+ var defaultMap map[string]any
+ if err := json.Unmarshal(defaultBody, &defaultMap); err != nil {
+ return nil, fmt.Errorf("unmarshal default body for merge: %w", err)
+ }
+ deny := bodyMergeKeyDenyList[provider]
+ for k, v := range opts.BodyOverride {
+ if deny[k] {
+ continue
+ }
+ defaultMap[k] = v
+ }
+ merged, err := json.Marshal(defaultMap)
+ if err != nil {
+ return nil, fmt.Errorf("marshal merged body: %w", err)
+ }
+ return merged, nil
+}
+
+// bodyMergeKeyDenyList 在 merge 模式下,禁止用户覆盖这些 provider-specific 的关键字段。
+// 思路抄 check-cx 的 EXCLUDED_METADATA_KEYS:保护 challenge / model 路由不被用户误伤。
+// 用户想动这些字段就用 replace 模式(已知会跳 challenge 校验)。
+//
+//nolint:gochecknoglobals // 静态查表,初始化后不变。
+var bodyMergeKeyDenyList = map[string]map[string]bool{
+ MonitorProviderOpenAI: {"model": true, "messages": true, "stream": true},
+ MonitorProviderAnthropic: {"model": true, "messages": true},
+ MonitorProviderGemini: {"contents": true},
+}
+
// postRawJSON 发送 POST + 已序列化好的 JSON 字节,限制响应体大小,返回响应字节、HTTP status、错误。
// adapter 自行 marshal 是为了精确控制字段顺序与类型,所以这里直接收 []byte 而不是 any。
func postRawJSON(ctx context.Context, fullURL string, payload []byte, headers map[string]string) ([]byte, int, error) {
diff --git a/backend/internal/service/channel_monitor_checker_body_test.go b/backend/internal/service/channel_monitor_checker_body_test.go
new file mode 100644
index 00000000..323cf8b7
--- /dev/null
+++ b/backend/internal/service/channel_monitor_checker_body_test.go
@@ -0,0 +1,173 @@
+//go:build unit
+
+package service
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+)
+
+// swapMonitorHTTPClient 临时替换 monitorHTTPClient 为不带 SSRF 校验的普通 client,
+// 让 httptest (127.0.0.1) 能连通。测试结束后恢复。
+func swapMonitorHTTPClient(t *testing.T) {
+ t.Helper()
+ orig := monitorHTTPClient
+ monitorHTTPClient = &http.Client{Timeout: 5 * time.Second}
+ t.Cleanup(func() { monitorHTTPClient = orig })
+}
+
+// captureHandler 把每次收到的请求 body 和 headers 存起来,测试断言用。
+type captureHandler struct {
+ lastBody map[string]any
+ lastHeaders http.Header
+ respondText string // 写到 Anthropic content[0].text 里(校验用)
+ status int
+}
+
+func (h *captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ h.lastHeaders = r.Header.Clone()
+ defer func() { _ = r.Body.Close() }()
+ var parsed map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&parsed)
+ h.lastBody = parsed
+
+ if h.status == 0 {
+ h.status = 200
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(h.status)
+ // 构造 Anthropic 格式的响应:content[0].text = h.respondText
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "content": []map[string]any{
+ {"type": "text", "text": h.respondText},
+ },
+ })
+}
+
+func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
+ t.Helper()
+ swapMonitorHTTPClient(t)
+ srv := httptest.NewServer(handler)
+ t.Cleanup(srv.Close)
+ return srv.URL
+}
+
+func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) {
+ h := &captureHandler{respondText: "the answer is 42"}
+ endpoint := setupFakeAnthropic(t, h)
+
+ // 跑一次 off 模式(opts=nil),确认默认 body 行为未变
+ _ = runCheckForModel(context.Background(), MonitorProviderAnthropic, endpoint, "sk-fake", "claude-x", nil)
+
+ if h.lastBody["model"] != "claude-x" {
+ t.Errorf("default body should contain model=claude-x, got %v", h.lastBody["model"])
+ }
+ if _, ok := h.lastBody["messages"]; !ok {
+ t.Error("default body should contain messages")
+ }
+ if h.lastHeaders.Get("x-api-key") != "sk-fake" {
+ t.Errorf("expected adapter's x-api-key header, got %q", h.lastHeaders.Get("x-api-key"))
+ }
+}
+
+func TestRunCheckForModel_MergeMode_UserFieldsWinButDenyListProtects(t *testing.T) {
+ h := &captureHandler{respondText: "the answer is 42"}
+ endpoint := setupFakeAnthropic(t, h)
+
+ opts := &CheckOptions{
+ BodyOverrideMode: MonitorBodyOverrideModeMerge,
+ BodyOverride: map[string]any{
+ "system": "You are Claude Code...",
+ "max_tokens": float64(999), // 应该覆盖默认 50
+ "model": "hacked-model", // 应该被黑名单挡住,保留原 model
+ "messages": []any{}, // 同上,被挡
+ },
+ ExtraHeaders: map[string]string{
+ "User-Agent": "claude-cli/1.0",
+ "Content-Length": "999", // 黑名单
+ "x-custom": "ok",
+ },
+ }
+ _ = runCheckForModel(context.Background(), MonitorProviderAnthropic, endpoint, "sk-fake", "claude-x", opts)
+
+ if h.lastBody["system"] != "You are Claude Code..." {
+ t.Errorf("merge mode should inject system, got %v", h.lastBody["system"])
+ }
+ // max_tokens 覆盖生效
+ if mt, ok := h.lastBody["max_tokens"].(float64); !ok || mt != 999 {
+ t.Errorf("merge mode should override max_tokens to 999, got %v", h.lastBody["max_tokens"])
+ }
+ // model 在黑名单 — 应该保留默认值
+ if h.lastBody["model"] != "claude-x" {
+ t.Errorf("model should be protected by deny list, got %v", h.lastBody["model"])
+ }
+ // messages 在黑名单 — 应该保留默认值(非空)
+ msgs, _ := h.lastBody["messages"].([]any)
+ if len(msgs) == 0 {
+ t.Error("messages should be protected by deny list (kept default, non-empty)")
+ }
+ // header 合并
+ if h.lastHeaders.Get("User-Agent") != "claude-cli/1.0" {
+ t.Errorf("extra User-Agent should override, got %q", h.lastHeaders.Get("User-Agent"))
+ }
+ if h.lastHeaders.Get("x-custom") != "ok" {
+ t.Errorf("extra custom header should be present, got %q", h.lastHeaders.Get("x-custom"))
+ }
+ // Content-Length 黑名单:会被 net/http 自动重算,但不应由用户的 "999" 决定。
+ // 我们无法直接断言丢弃(http.Client 总会填上),只断言请求成功即可。
+}
+
+func TestRunCheckForModel_ReplaceMode_FullBodyUsedAndChallengeSkipped(t *testing.T) {
+ // replace 模式下我们的 body 完全自定义,challenge 数学题不会出现在请求里,
+ // 上游也不会回正确答案 — 但只要 2xx + 响应文本非空,就算 operational
+ h := &captureHandler{respondText: "any non-empty text"}
+ endpoint := setupFakeAnthropic(t, h)
+
+ userBody := map[string]any{
+ "model": "user-forced-model",
+ "messages": []any{map[string]any{"role": "user", "content": "hi"}},
+ "max_tokens": float64(10),
+ "system": "You are someone else",
+ }
+ opts := &CheckOptions{
+ BodyOverrideMode: MonitorBodyOverrideModeReplace,
+ BodyOverride: userBody,
+ }
+ res := runCheckForModel(context.Background(), MonitorProviderAnthropic, endpoint, "sk-fake", "claude-x", opts)
+
+ // 请求 body = 用户提供的原样
+ if h.lastBody["model"] != "user-forced-model" {
+ t.Errorf("replace mode should use user's model, got %v", h.lastBody["model"])
+ }
+ if h.lastBody["system"] != "You are someone else" {
+ t.Errorf("replace mode should use user's system, got %v", h.lastBody["system"])
+ }
+ // challenge 虽然没命中,但由于 replace 模式跳过 challenge 校验 + 响应非空 → operational
+ if res.Status != MonitorStatusOperational {
+ t.Errorf("replace mode with 2xx + non-empty text should be operational, got status=%s message=%q",
+ res.Status, res.Message)
+ }
+}
+
+func TestRunCheckForModel_ReplaceMode_EmptyResponseIsFailed(t *testing.T) {
+ h := &captureHandler{respondText: ""} // 上游 200 但 content[0].text 为空
+ endpoint := setupFakeAnthropic(t, h)
+
+ opts := &CheckOptions{
+ BodyOverrideMode: MonitorBodyOverrideModeReplace,
+ BodyOverride: map[string]any{"model": "x", "messages": []any{}},
+ }
+ res := runCheckForModel(context.Background(), MonitorProviderAnthropic, endpoint, "sk-fake", "claude-x", opts)
+
+ if res.Status != MonitorStatusFailed {
+ t.Errorf("replace mode with empty text should be failed, got status=%s", res.Status)
+ }
+ if !strings.Contains(res.Message, "replace-mode") {
+ t.Errorf("failure message should hint replace-mode, got %q", res.Message)
+ }
+}
diff --git a/backend/internal/service/channel_monitor_service.go b/backend/internal/service/channel_monitor_service.go
index 144c66a0..ec1107a3 100644
--- a/backend/internal/service/channel_monitor_service.go
+++ b/backend/internal/service/channel_monitor_service.go
@@ -104,21 +104,31 @@ func (s *ChannelMonitorService) Create(ctx context.Context, p ChannelMonitorCrea
if err := validateCreateParams(p); err != nil {
return nil, err
}
+ if err := validateBodyModeParams(p.BodyOverrideMode, p.BodyOverride); err != nil {
+ return nil, err
+ }
+ if err := validateExtraHeaders(p.ExtraHeaders); err != nil {
+ return nil, err
+ }
encrypted, err := s.encryptor.Encrypt(p.APIKey)
if err != nil {
return nil, fmt.Errorf("encrypt api key: %w", err)
}
m := &ChannelMonitor{
- Name: strings.TrimSpace(p.Name),
- Provider: p.Provider,
- Endpoint: normalizeEndpoint(p.Endpoint),
- APIKey: encrypted, // 注意:传入 repository 时该字段为密文
- PrimaryModel: strings.TrimSpace(p.PrimaryModel),
- ExtraModels: normalizeModels(p.ExtraModels),
- GroupName: strings.TrimSpace(p.GroupName),
- Enabled: p.Enabled,
- IntervalSeconds: p.IntervalSeconds,
- CreatedBy: p.CreatedBy,
+ Name: strings.TrimSpace(p.Name),
+ Provider: p.Provider,
+ Endpoint: normalizeEndpoint(p.Endpoint),
+ APIKey: encrypted, // 注意:传入 repository 时该字段为密文
+ PrimaryModel: strings.TrimSpace(p.PrimaryModel),
+ ExtraModels: normalizeModels(p.ExtraModels),
+ GroupName: strings.TrimSpace(p.GroupName),
+ Enabled: p.Enabled,
+ IntervalSeconds: p.IntervalSeconds,
+ CreatedBy: p.CreatedBy,
+ TemplateID: p.TemplateID,
+ ExtraHeaders: emptyHeadersIfNil(p.ExtraHeaders),
+ BodyOverrideMode: defaultBodyMode(p.BodyOverrideMode),
+ BodyOverride: p.BodyOverride,
}
if err := s.repo.Create(ctx, m); err != nil {
return nil, fmt.Errorf("create channel monitor: %w", err)
@@ -272,12 +282,19 @@ func (s *ChannelMonitorService) runChecksConcurrent(ctx context.Context, m *Chan
// ping 共享一次,所有模型记录同一个 ping 延迟。
pingMs := pingEndpointOrigin(ctx, m.Endpoint)
+ // 所有模型共用同一份 CheckOptions(来自监控的快照字段)。
+ opts := &CheckOptions{
+ ExtraHeaders: m.ExtraHeaders,
+ BodyOverrideMode: m.BodyOverrideMode,
+ BodyOverride: m.BodyOverride,
+ }
+
var eg errgroup.Group
var mu sync.Mutex
for i, model := range models {
i, model := i, model
eg.Go(func() error {
- r := runCheckForModel(ctx, m.Provider, m.Endpoint, m.APIKey, model)
+ r := runCheckForModel(ctx, m.Provider, m.Endpoint, m.APIKey, model, opts)
r.PingLatencyMs = pingMs
mu.Lock()
results[i] = r
@@ -476,5 +493,38 @@ func applyMonitorUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams)
}
existing.IntervalSeconds = *p.IntervalSeconds
}
+ return applyMonitorAdvancedUpdate(existing, p)
+}
+
+// applyMonitorAdvancedUpdate 处理自定义请求快照相关字段,从 applyMonitorUpdate 拆出避免过长。
+func applyMonitorAdvancedUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams) error {
+ if p.ClearTemplate {
+ existing.TemplateID = nil
+ } else if p.TemplateID != nil {
+ id := *p.TemplateID
+ existing.TemplateID = &id
+ }
+ if p.ExtraHeaders != nil {
+ if err := validateExtraHeaders(*p.ExtraHeaders); err != nil {
+ return err
+ }
+ existing.ExtraHeaders = emptyHeadersIfNil(*p.ExtraHeaders)
+ }
+ // BodyOverrideMode / BodyOverride 联合校验,和模板一致。
+ newMode := existing.BodyOverrideMode
+ newBody := existing.BodyOverride
+ if p.BodyOverrideMode != nil {
+ newMode = *p.BodyOverrideMode
+ }
+ if p.BodyOverride != nil {
+ newBody = *p.BodyOverride
+ }
+ if p.BodyOverrideMode != nil || p.BodyOverride != nil {
+ if err := validateBodyModeParams(newMode, newBody); err != nil {
+ return err
+ }
+ existing.BodyOverrideMode = defaultBodyMode(newMode)
+ existing.BodyOverride = newBody
+ }
return nil
}
diff --git a/backend/internal/service/channel_monitor_template_service.go b/backend/internal/service/channel_monitor_template_service.go
new file mode 100644
index 00000000..98fc930b
--- /dev/null
+++ b/backend/internal/service/channel_monitor_template_service.go
@@ -0,0 +1,225 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+// ChannelMonitorRequestTemplateRepository 模板数据访问接口。
+type ChannelMonitorRequestTemplateRepository interface {
+ Create(ctx context.Context, t *ChannelMonitorRequestTemplate) error
+ GetByID(ctx context.Context, id int64) (*ChannelMonitorRequestTemplate, error)
+ Update(ctx context.Context, t *ChannelMonitorRequestTemplate) error
+ Delete(ctx context.Context, id int64) error
+ List(ctx context.Context, params ChannelMonitorRequestTemplateListParams) ([]*ChannelMonitorRequestTemplate, error)
+ // ApplyToMonitors 把模板当前的 extra_headers / body_override_mode / body_override
+ // 批量覆盖到所有 template_id = id 的监控上。返回被覆盖的监控数量。
+ ApplyToMonitors(ctx context.Context, id int64) (int64, error)
+ // CountAssociatedMonitors 统计 template_id = id 的监控数(用于 UI 展示「应用到 N 个配置」)。
+ CountAssociatedMonitors(ctx context.Context, id int64) (int64, error)
+}
+
+// ChannelMonitorRequestTemplateService 模板管理 service。
+type ChannelMonitorRequestTemplateService struct {
+ repo ChannelMonitorRequestTemplateRepository
+}
+
+// NewChannelMonitorRequestTemplateService 创建模板 service。
+func NewChannelMonitorRequestTemplateService(repo ChannelMonitorRequestTemplateRepository) *ChannelMonitorRequestTemplateService {
+ return &ChannelMonitorRequestTemplateService{repo: repo}
+}
+
+// ---------- CRUD ----------
+
+// List 按 provider 过滤(空串 = 全部),不分页(模板量级小)。
+func (s *ChannelMonitorRequestTemplateService) List(ctx context.Context, params ChannelMonitorRequestTemplateListParams) ([]*ChannelMonitorRequestTemplate, error) {
+ if params.Provider != "" {
+ if err := validateProvider(params.Provider); err != nil {
+ return nil, err
+ }
+ }
+ return s.repo.List(ctx, params)
+}
+
+// Get 返回单个模板。
+func (s *ChannelMonitorRequestTemplateService) Get(ctx context.Context, id int64) (*ChannelMonitorRequestTemplate, error) {
+ return s.repo.GetByID(ctx, id)
+}
+
+// Create 创建模板(会校验 headers 黑名单和 body 模式匹配)。
+func (s *ChannelMonitorRequestTemplateService) Create(ctx context.Context, p ChannelMonitorRequestTemplateCreateParams) (*ChannelMonitorRequestTemplate, error) {
+ if err := validateTemplateCreateParams(p); err != nil {
+ return nil, err
+ }
+ t := &ChannelMonitorRequestTemplate{
+ Name: strings.TrimSpace(p.Name),
+ Provider: p.Provider,
+ Description: strings.TrimSpace(p.Description),
+ ExtraHeaders: emptyHeadersIfNil(p.ExtraHeaders),
+ BodyOverrideMode: defaultBodyMode(p.BodyOverrideMode),
+ BodyOverride: p.BodyOverride,
+ }
+ if err := s.repo.Create(ctx, t); err != nil {
+ return nil, fmt.Errorf("create template: %w", err)
+ }
+ return t, nil
+}
+
+// Update 更新模板(provider 不可改)。
+func (s *ChannelMonitorRequestTemplateService) Update(ctx context.Context, id int64, p ChannelMonitorRequestTemplateUpdateParams) (*ChannelMonitorRequestTemplate, error) {
+ existing, err := s.repo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ if err := applyTemplateUpdate(existing, p); err != nil {
+ return nil, err
+ }
+ if err := s.repo.Update(ctx, existing); err != nil {
+ return nil, fmt.Errorf("update template: %w", err)
+ }
+ return existing, nil
+}
+
+// Delete 删除模板。关联监控的 template_id 会被 SET NULL,监控保留快照继续跑。
+func (s *ChannelMonitorRequestTemplateService) Delete(ctx context.Context, id int64) error {
+ if err := s.repo.Delete(ctx, id); err != nil {
+ return fmt.Errorf("delete template: %w", err)
+ }
+ return nil
+}
+
+// ApplyToMonitors 把模板当前配置一键应用到所有关联监控。
+// 返回被影响的监控数。
+func (s *ChannelMonitorRequestTemplateService) ApplyToMonitors(ctx context.Context, id int64) (int64, error) {
+ if _, err := s.repo.GetByID(ctx, id); err != nil {
+ return 0, err
+ }
+ affected, err := s.repo.ApplyToMonitors(ctx, id)
+ if err != nil {
+ return 0, fmt.Errorf("apply template to monitors: %w", err)
+ }
+ return affected, nil
+}
+
+// CountAssociatedMonitors 返回关联监控数。
+func (s *ChannelMonitorRequestTemplateService) CountAssociatedMonitors(ctx context.Context, id int64) (int64, error) {
+ return s.repo.CountAssociatedMonitors(ctx, id)
+}
+
+// ---------- 校验 & 工具 ----------
+
+// validateTemplateCreateParams 聚合 create 入参校验,避免函数超过 30 行。
+func validateTemplateCreateParams(p ChannelMonitorRequestTemplateCreateParams) error {
+ if strings.TrimSpace(p.Name) == "" {
+ return ErrChannelMonitorTemplateMissingName
+ }
+ if err := validateProvider(p.Provider); err != nil {
+ return ErrChannelMonitorTemplateInvalidProvider
+ }
+ if err := validateBodyModeParams(p.BodyOverrideMode, p.BodyOverride); err != nil {
+ return err
+ }
+ if err := validateExtraHeaders(p.ExtraHeaders); err != nil {
+ return err
+ }
+ return nil
+}
+
+// applyTemplateUpdate 把 update params 中非 nil 字段应用到 existing 上。
+func applyTemplateUpdate(existing *ChannelMonitorRequestTemplate, p ChannelMonitorRequestTemplateUpdateParams) error {
+ if p.Name != nil {
+ name := strings.TrimSpace(*p.Name)
+ if name == "" {
+ return ErrChannelMonitorTemplateMissingName
+ }
+ existing.Name = name
+ }
+ if p.Description != nil {
+ existing.Description = strings.TrimSpace(*p.Description)
+ }
+ if p.ExtraHeaders != nil {
+ if err := validateExtraHeaders(*p.ExtraHeaders); err != nil {
+ return err
+ }
+ existing.ExtraHeaders = emptyHeadersIfNil(*p.ExtraHeaders)
+ }
+ // BodyOverrideMode / BodyOverride 联合校验:任一变化都用「更新后的值」做校验。
+ newMode := existing.BodyOverrideMode
+ newBody := existing.BodyOverride
+ if p.BodyOverrideMode != nil {
+ newMode = *p.BodyOverrideMode
+ }
+ if p.BodyOverride != nil {
+ newBody = *p.BodyOverride
+ }
+ if err := validateBodyModeParams(newMode, newBody); err != nil {
+ return err
+ }
+ existing.BodyOverrideMode = defaultBodyMode(newMode)
+ existing.BodyOverride = newBody
+ return nil
+}
+
+// validateBodyModeParams 校验 body_override_mode 合法,且 merge/replace 模式下 body_override 非空。
+func validateBodyModeParams(mode string, body map[string]any) error {
+ switch mode {
+ case "", MonitorBodyOverrideModeOff:
+ return nil
+ case MonitorBodyOverrideModeMerge, MonitorBodyOverrideModeReplace:
+ if len(body) == 0 {
+ return ErrChannelMonitorTemplateBodyRequired
+ }
+ return nil
+ default:
+ return ErrChannelMonitorTemplateInvalidBodyMode
+ }
+}
+
+// headerNameRegex 合法 header 名:RFC 7230 token(ASCII 可见字符减特殊符号)。
+var headerNameRegex = regexp.MustCompile(`^[A-Za-z0-9!#$%&'*+\-.^_` + "`" + `|~]+$`)
+
+// forbiddenHeaderNames hop-by-hop + HTTP 客户端自管的 header;禁止用户覆盖,
+// 否则会让 Go http.Client 行为异常(双重 Content-Length、连接复用错乱等)。
+var forbiddenHeaderNames = map[string]bool{
+ "host": true,
+ "content-length": true,
+ "content-encoding": true,
+ "transfer-encoding": true,
+ "connection": true,
+}
+
+// IsForbiddenHeaderName 对外暴露,checker 运行时也会再过滤一次做兜底。
+func IsForbiddenHeaderName(name string) bool {
+ return forbiddenHeaderNames[strings.ToLower(strings.TrimSpace(name))]
+}
+
+// validateExtraHeaders 校验 header 名字格式 + 黑名单。保存时就拒绝非法 header,早失败。
+func validateExtraHeaders(h map[string]string) error {
+ for k := range h {
+ if !headerNameRegex.MatchString(k) {
+ return ErrChannelMonitorTemplateHeaderInvalidName
+ }
+ if IsForbiddenHeaderName(k) {
+ return ErrChannelMonitorTemplateHeaderForbidden
+ }
+ }
+ return nil
+}
+
+// emptyHeadersIfNil 把 nil map 归一成空 map(repo 层写库时 JSONB 需要非 nil)。
+func emptyHeadersIfNil(h map[string]string) map[string]string {
+ if h == nil {
+ return map[string]string{}
+ }
+ return h
+}
+
+// defaultBodyMode 空串归一为 off。
+func defaultBodyMode(mode string) string {
+ if mode == "" {
+ return MonitorBodyOverrideModeOff
+ }
+ return mode
+}
diff --git a/backend/internal/service/channel_monitor_template_types.go b/backend/internal/service/channel_monitor_template_types.go
new file mode 100644
index 00000000..a6e2bb59
--- /dev/null
+++ b/backend/internal/service/channel_monitor_template_types.go
@@ -0,0 +1,74 @@
+package service
+
+import (
+ infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
+ "time"
+)
+
+// ChannelMonitorRequestTemplate 请求模板(service 层模型)。
+// 作用:把一组可复用的 headers + 可选 body 覆盖配置抽出来管理,
+// 被监控「应用」时以快照方式拷贝到监控本身的同名字段。
+type ChannelMonitorRequestTemplate struct {
+ ID int64
+ Name string
+ Provider string
+ Description string
+ ExtraHeaders map[string]string
+ BodyOverrideMode string
+ BodyOverride map[string]any
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// ChannelMonitorRequestTemplateListParams 列表过滤。
+type ChannelMonitorRequestTemplateListParams struct {
+ Provider string // 空 = 全部;非空则按 provider 过滤
+}
+
+// ChannelMonitorRequestTemplateCreateParams 创建参数。
+type ChannelMonitorRequestTemplateCreateParams struct {
+ Name string
+ Provider string
+ Description string
+ ExtraHeaders map[string]string
+ BodyOverrideMode string
+ BodyOverride map[string]any
+}
+
+// ChannelMonitorRequestTemplateUpdateParams 更新参数(指针字段 = 不修改)。
+// 注意 Provider 不可修改:改 provider 会让已关联监控的 body 黑名单语义错乱。
+type ChannelMonitorRequestTemplateUpdateParams struct {
+ Name *string
+ Description *string
+ ExtraHeaders *map[string]string
+ BodyOverrideMode *string
+ BodyOverride *map[string]any
+}
+
+// 模板相关错误(命名与现有 ErrChannelMonitor* 风格保持一致)。
+var (
+ ErrChannelMonitorTemplateNotFound = infraerrors.NotFound(
+ "CHANNEL_MONITOR_TEMPLATE_NOT_FOUND", "channel monitor request template not found",
+ )
+ ErrChannelMonitorTemplateInvalidProvider = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_INVALID_PROVIDER", "template provider must be one of openai/anthropic/gemini",
+ )
+ ErrChannelMonitorTemplateMissingName = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_MISSING_NAME", "template name is required",
+ )
+ ErrChannelMonitorTemplateInvalidBodyMode = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_INVALID_BODY_MODE", "body_override_mode must be one of off/merge/replace",
+ )
+ ErrChannelMonitorTemplateBodyRequired = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_BODY_REQUIRED", "body_override is required when body_override_mode is merge or replace",
+ )
+ ErrChannelMonitorTemplateHeaderForbidden = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_HEADER_FORBIDDEN", "header name is forbidden (hop-by-hop or computed by HTTP client)",
+ )
+ ErrChannelMonitorTemplateHeaderInvalidName = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_HEADER_INVALID_NAME", "header name contains invalid characters",
+ )
+ ErrChannelMonitorTemplateProviderMismatch = infraerrors.BadRequest(
+ "CHANNEL_MONITOR_TEMPLATE_PROVIDER_MISMATCH", "monitor provider does not match template provider",
+ )
+)
diff --git a/backend/internal/service/channel_monitor_types.go b/backend/internal/service/channel_monitor_types.go
index 739c82fb..b797a89b 100644
--- a/backend/internal/service/channel_monitor_types.go
+++ b/backend/internal/service/channel_monitor_types.go
@@ -2,6 +2,19 @@ package service
import "time"
+// MonitorBodyOverrideMode 自定义请求体处理模式。
+//
+// - off 使用 adapter 默认 body(忽略 BodyOverride)
+// - merge adapter 默认 body 与 BodyOverride 浅合并(用户优先;
+// model/messages/contents 等关键字段在 checker 黑名单内会被静默丢弃)
+// - replace 完全用 BodyOverride 作为 body;跳过 challenge 校验,
+// 改成 HTTP 2xx + 响应非空即视为可用(用户负责构造 body)
+const (
+ MonitorBodyOverrideModeOff = "off"
+ MonitorBodyOverrideModeMerge = "merge"
+ MonitorBodyOverrideModeReplace = "replace"
+)
+
// ChannelMonitor 渠道监控配置(service 层模型,不直接暴露 ent 类型)。
type ChannelMonitor struct {
ID int64
@@ -19,6 +32,12 @@ type ChannelMonitor struct {
CreatedAt time.Time
UpdatedAt time.Time
+ // 请求自定义快照(来自模板拷贝 or 用户手填,运行时直接读取)
+ TemplateID *int64 // 仅用于 UI 分组 + 一键应用,运行时不用
+ ExtraHeaders map[string]string // 与 adapter 默认 headers 合并,用户优先
+ BodyOverrideMode string // off / merge / replace
+ BodyOverride map[string]any // 仅 mode != off 时使用
+
// APIKeyDecryptFailed 表示 APIKey 字段无法解密(密钥不一致或损坏)。
// 此时 APIKey 为空字符串,runner / RunCheck 必须跳过该监控并提示重填。
APIKeyDecryptFailed bool
@@ -35,16 +54,20 @@ type ChannelMonitorListParams struct {
// ChannelMonitorCreateParams 创建参数。
type ChannelMonitorCreateParams struct {
- Name string
- Provider string
- Endpoint string
- APIKey string
- PrimaryModel string
- ExtraModels []string
- GroupName string
- Enabled bool
- IntervalSeconds int
- CreatedBy int64
+ Name string
+ Provider string
+ Endpoint string
+ APIKey string
+ PrimaryModel string
+ ExtraModels []string
+ GroupName string
+ Enabled bool
+ IntervalSeconds int
+ CreatedBy int64
+ TemplateID *int64
+ ExtraHeaders map[string]string
+ BodyOverrideMode string
+ BodyOverride map[string]any
}
// ChannelMonitorUpdateParams 更新参数(指针字段表示"未提供则不更新")。
@@ -58,6 +81,14 @@ type ChannelMonitorUpdateParams struct {
GroupName *string
Enabled *bool
IntervalSeconds *int
+ // 自定义快照字段:指针为 nil 表示不更新,非 nil 覆盖
+ // TemplateID *(*int64):用 ** 表达三态:nil=不更新;&nil=清空;&&id=设为 id。
+ // 简化处理:用 ClearTemplate 显式标志 + TemplateID(普通指针)
+ TemplateID *int64
+ ClearTemplate bool // true 时无视 TemplateID,把监控的 template_id 置空
+ ExtraHeaders *map[string]string
+ BodyOverrideMode *string
+ BodyOverride *map[string]any
}
// CheckResult 单个模型一次检测的结果。
diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go
index 1482d650..3148f865 100644
--- a/backend/internal/service/wire.go
+++ b/backend/internal/service/wire.go
@@ -472,6 +472,7 @@ var ProviderSet = wire.NewSet(
ProvideBalanceNotifyService,
ProvideChannelMonitorService,
ProvideChannelMonitorRunner,
+ NewChannelMonitorRequestTemplateService,
)
// ProvidePaymentConfigService wraps NewPaymentConfigService to accept the named
diff --git a/backend/migrations/128_add_channel_monitor_request_templates.sql b/backend/migrations/128_add_channel_monitor_request_templates.sql
new file mode 100644
index 00000000..2db8fef6
--- /dev/null
+++ b/backend/migrations/128_add_channel_monitor_request_templates.sql
@@ -0,0 +1,70 @@
+-- Migration: 128_add_channel_monitor_request_templates
+-- 加请求模板表 + 给 channel_monitors 加 4 个快照字段(template_id 关联引用 + extra_headers /
+-- body_override_mode / body_override 三个真正运行时使用的快照)。
+--
+-- 设计要点:
+-- 1) 模板与监控之间是「应用即拷贝」的快照语义,运行时 checker 不再回查模板表。
+-- 模板 UPDATE 不会自动影响监控;只有用户主动「应用到关联监控」才会刷新快照。
+-- 2) ON DELETE SET NULL:模板删除不级联清理监控;监控保留快照继续工作。
+-- 3) extra_headers / body_override 都是 JSONB;body_override_mode 用 varchar(不是 enum)
+-- 便于将来加新模式无需 ALTER TYPE。
+-- 4) 同一 provider 内模板 name 唯一(允许 Anthropic + OpenAI 重名 "伪装官方客户端")。
+
+CREATE TABLE IF NOT EXISTS channel_monitor_request_templates (
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ provider VARCHAR(20) NOT NULL,
+ description VARCHAR(500) NOT NULL DEFAULT '',
+ extra_headers JSONB NOT NULL DEFAULT '{}'::jsonb,
+ body_override_mode VARCHAR(10) NOT NULL DEFAULT 'off',
+ body_override JSONB NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT channel_monitor_request_templates_provider_check
+ CHECK (provider IN ('openai', 'anthropic', 'gemini')),
+ CONSTRAINT channel_monitor_request_templates_body_mode_check
+ CHECK (body_override_mode IN ('off', 'merge', 'replace'))
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS channel_monitor_request_templates_provider_name
+ ON channel_monitor_request_templates (provider, name);
+
+-- channel_monitors 加 4 列(ADD COLUMN IF NOT EXISTS 需要 PG 9.6+,生产使用 PG 16)
+ALTER TABLE channel_monitors
+ ADD COLUMN IF NOT EXISTS template_id BIGINT NULL;
+ALTER TABLE channel_monitors
+ ADD COLUMN IF NOT EXISTS extra_headers JSONB NOT NULL DEFAULT '{}'::jsonb;
+ALTER TABLE channel_monitors
+ ADD COLUMN IF NOT EXISTS body_override_mode VARCHAR(10) NOT NULL DEFAULT 'off';
+ALTER TABLE channel_monitors
+ ADD COLUMN IF NOT EXISTS body_override JSONB NULL;
+
+-- 约束 + 外键(DO 块里 IF NOT EXISTS 判断,保证幂等)
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'channel_monitors_body_mode_check'
+ AND table_name = 'channel_monitors'
+ ) THEN
+ ALTER TABLE channel_monitors
+ ADD CONSTRAINT channel_monitors_body_mode_check
+ CHECK (body_override_mode IN ('off', 'merge', 'replace'));
+ END IF;
+
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'channel_monitors_template_id_fkey'
+ AND table_name = 'channel_monitors'
+ ) THEN
+ ALTER TABLE channel_monitors
+ ADD CONSTRAINT channel_monitors_template_id_fkey
+ FOREIGN KEY (template_id)
+ REFERENCES channel_monitor_request_templates (id)
+ ON DELETE SET NULL;
+ END IF;
+END $$;
+
+CREATE INDEX IF NOT EXISTS idx_channel_monitors_template_id
+ ON channel_monitors (template_id)
+ WHERE template_id IS NOT NULL;
diff --git a/frontend/src/api/admin/channelMonitor.ts b/frontend/src/api/admin/channelMonitor.ts
index d9cc6aed..949c4bc8 100644
--- a/frontend/src/api/admin/channelMonitor.ts
+++ b/frontend/src/api/admin/channelMonitor.ts
@@ -7,6 +7,7 @@ import { apiClient } from '../client'
export type Provider = 'openai' | 'anthropic' | 'gemini'
export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error'
+export type BodyOverrideMode = 'off' | 'merge' | 'replace'
export interface ChannelMonitor {
id: number
@@ -37,6 +38,11 @@ export interface ChannelMonitor {
availability_7d: number
/** Latest status per extra model (used for hover tooltip) */
extra_models_status: ExtraModelStatus[]
+ /** 请求自定义快照字段(高级设置) */
+ template_id: number | null
+ extra_headers: Record
+ body_override_mode: BodyOverrideMode
+ body_override: Record | null
}
export interface ExtraModelStatus {
@@ -71,10 +77,16 @@ export interface CreateParams {
group_name?: string
enabled?: boolean
interval_seconds: number
+ template_id?: number | null
+ extra_headers?: Record
+ body_override_mode?: BodyOverrideMode
+ body_override?: Record | null
}
-// Update request: api_key empty string means "do not modify"
-export type UpdateParams = Partial
+// Update request: api_key 空串 = 不修改;clear_template=true 时把 template_id 置空
+export type UpdateParams = Partial & {
+ clear_template?: boolean
+}
export interface CheckResult {
model: string
diff --git a/frontend/src/api/admin/channelMonitorTemplate.ts b/frontend/src/api/admin/channelMonitorTemplate.ts
new file mode 100644
index 00000000..258adab8
--- /dev/null
+++ b/frontend/src/api/admin/channelMonitorTemplate.ts
@@ -0,0 +1,108 @@
+/**
+ * Admin Channel Monitor Request Template API.
+ *
+ * 模板 = 一组可复用的 headers + 可选 body 覆盖配置。
+ * 应用到监控 = 拷贝快照;模板后续变动不自动同步,需手动点「应用到关联监控」刷新。
+ */
+
+import { apiClient } from '../client'
+import type { BodyOverrideMode, Provider } from './channelMonitor'
+
+export interface ChannelMonitorTemplate {
+ id: number
+ name: string
+ provider: Provider
+ description: string
+ extra_headers: Record
+ body_override_mode: BodyOverrideMode
+ body_override: Record | null
+ created_at: string
+ updated_at: string
+ /** 关联的监控数量(快照来自此模板,仅 template_id 匹配即可) */
+ associated_monitors: number
+}
+
+export interface ListParams {
+ provider?: Provider
+}
+
+export interface ListResponse {
+ items: ChannelMonitorTemplate[]
+}
+
+export interface CreateParams {
+ name: string
+ provider: Provider
+ description?: string
+ extra_headers?: Record
+ body_override_mode?: BodyOverrideMode
+ body_override?: Record | null
+}
+
+export interface UpdateParams {
+ name?: string
+ description?: string
+ extra_headers?: Record
+ body_override_mode?: BodyOverrideMode
+ body_override?: Record | null
+}
+
+export interface ApplyResponse {
+ affected: number
+}
+
+export async function list(params: ListParams = {}): Promise {
+ const { data } = await apiClient.get('/admin/channel-monitor-templates', {
+ params,
+ })
+ return data
+}
+
+export async function get(id: number): Promise {
+ const { data } = await apiClient.get(
+ `/admin/channel-monitor-templates/${id}`,
+ )
+ return data
+}
+
+export async function create(params: CreateParams): Promise {
+ const { data } = await apiClient.post(
+ '/admin/channel-monitor-templates',
+ params,
+ )
+ return data
+}
+
+export async function update(id: number, params: UpdateParams): Promise {
+ const { data } = await apiClient.put(
+ `/admin/channel-monitor-templates/${id}`,
+ params,
+ )
+ return data
+}
+
+export async function del(id: number): Promise {
+ await apiClient.delete(`/admin/channel-monitor-templates/${id}`)
+}
+
+/**
+ * Apply the template to all associated monitors (overwrite snapshot fields).
+ * Returns count of affected monitors.
+ */
+export async function apply(id: number): Promise {
+ const { data } = await apiClient.post(
+ `/admin/channel-monitor-templates/${id}/apply`,
+ )
+ return data
+}
+
+export const channelMonitorTemplateAPI = {
+ list,
+ get,
+ create,
+ update,
+ del,
+ apply,
+}
+
+export default channelMonitorTemplateAPI
diff --git a/frontend/src/api/admin/index.ts b/frontend/src/api/admin/index.ts
index 5e2a9959..9cda5814 100644
--- a/frontend/src/api/admin/index.ts
+++ b/frontend/src/api/admin/index.ts
@@ -27,6 +27,7 @@ import backupAPI from './backup'
import tlsFingerprintProfileAPI from './tlsFingerprintProfile'
import channelsAPI from './channels'
import channelMonitorAPI from './channelMonitor'
+import channelMonitorTemplateAPI from './channelMonitorTemplate'
import adminPaymentAPI from './payment'
/**
@@ -57,6 +58,7 @@ export const adminAPI = {
tlsFingerprintProfiles: tlsFingerprintProfileAPI,
channels: channelsAPI,
channelMonitor: channelMonitorAPI,
+ channelMonitorTemplate: channelMonitorTemplateAPI,
payment: adminPaymentAPI
}
@@ -85,6 +87,7 @@ export {
tlsFingerprintProfileAPI,
channelsAPI,
channelMonitorAPI,
+ channelMonitorTemplateAPI,
adminPaymentAPI
}
diff --git a/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue b/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue
new file mode 100644
index 00000000..24827316
--- /dev/null
+++ b/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue
@@ -0,0 +1,205 @@
+
+
{{
localText(
- '留空表示自动路由;仅允许当前系统支持的官方或易支付来源。',
- 'Leave blank for automatic routing. Only supported official or EasyPay sources are allowed.'
+ '启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。',
+ 'Choose an explicit source before enabling the method. Not configured methods are not exposed.'
)
}}
{{
localText(
- '切换 OpenAI 侧新增的高级调度开关,供当前分支实验性调度逻辑使用。',
- 'Toggles the new OpenAI advanced scheduler flag for the experimental routing logic on this branch.'
+ '默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。',
+ 'Disabled by default. When enabled, this only changes the gateway\'s experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability.'
)
}}
@@ -160,7 +160,7 @@ watch(
() => props.user,
(user) => {
localUser.value = null
- if (!user || getBindingStatusForUser(user, 'email')) {
+ if (!user) {
return
}
if (typeof user.email === 'string' && !user.email.endsWith('.invalid')) {
@@ -171,6 +171,17 @@ watch(
)
const currentUser = computed(() => localUser.value ?? props.user)
+const emailBound = computed(() => getBindingStatus('email'))
+const emailPasswordPlaceholder = computed(() =>
+ emailBound.value
+ ? t('profile.authBindings.replaceEmailPasswordPlaceholder')
+ : t('profile.authBindings.passwordPlaceholder')
+)
+const emailSubmitActionLabel = computed(() =>
+ emailBound.value
+ ? t('profile.authBindings.confirmEmailReplaceAction')
+ : t('profile.authBindings.confirmEmailBindAction')
+)
const wechatOAuthSettings = computed(() => {
if (hasExplicitWeChatOAuthCapabilities(appStore.cachedPublicSettings)) {
@@ -286,7 +297,7 @@ function validateEmailBindingForm(requireCode: boolean): boolean {
appStore.showError(t('auth.passwordRequired'))
return false
}
- if (requireCode && emailBindingForm.password.length < 6) {
+ if (requireCode && !emailBound.value && emailBindingForm.password.length < 6) {
appStore.showError(t('auth.passwordMinLength'))
return false
}
@@ -321,10 +332,15 @@ async function bindEmail(): Promise {
verify_code: emailBindingForm.verifyCode,
password: emailBindingForm.password,
})
+ const replacingBoundEmail = emailBound.value
applyUpdatedUser(user)
emailBindingForm.verifyCode = ''
emailBindingForm.password = ''
- appStore.showSuccess(t('profile.authBindings.bindSuccess'))
+ appStore.showSuccess(
+ replacingBoundEmail
+ ? t('profile.authBindings.replaceSuccess')
+ : t('profile.authBindings.bindSuccess')
+ )
} catch (error) {
appStore.showError((error as { message?: string }).message || t('common.tryAgain'))
} finally {
diff --git a/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts b/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
index c07acf18..8821cdc5 100644
--- a/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
+++ b/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
@@ -51,10 +51,14 @@ vi.mock('vue-i18n', async (importOriginal) => {
if (key === 'profile.authBindings.emailPlaceholder') return 'Email address'
if (key === 'profile.authBindings.codePlaceholder') return 'Verification code'
if (key === 'profile.authBindings.passwordPlaceholder') return 'Set password'
+ if (key === 'profile.authBindings.replaceEmailPasswordPlaceholder')
+ return 'Current password'
if (key === 'profile.authBindings.sendCodeAction') return 'Send code'
if (key === 'profile.authBindings.confirmEmailBindAction') return 'Bind email'
+ if (key === 'profile.authBindings.confirmEmailReplaceAction') return 'Replace primary email'
if (key === 'profile.authBindings.codeSentTo') return `Code sent to ${params?.email || ''}`.trim()
if (key === 'profile.authBindings.bindSuccess') return 'Bind success'
+ if (key === 'profile.authBindings.replaceSuccess') return 'Primary email updated'
return key
},
}),
@@ -324,4 +328,68 @@ describe('ProfileIdentityBindingsSection', () => {
expect(wrapper.get('[data-testid="profile-binding-email-status"]').text()).toBe('Not bound')
expect(wrapper.get('[data-testid="profile-binding-email-input"]').exists()).toBe(true)
})
+
+ it('keeps the email form available for replacing a bound primary email', async () => {
+ userApiMocks.sendEmailBindingCode.mockResolvedValue(undefined)
+ userApiMocks.bindEmailIdentity.mockResolvedValue(
+ createUser({
+ email: 'new@example.com',
+ email_bound: true,
+ auth_bindings: {
+ email: { bound: true },
+ },
+ })
+ )
+
+ const appStore = useAppStore()
+ const authStore = useAuthStore()
+ authStore.user = createUser({
+ email: 'current@example.com',
+ email_bound: true,
+ auth_bindings: {
+ email: { bound: true },
+ },
+ })
+ const showSuccessSpy = vi.spyOn(appStore, 'showSuccess')
+
+ const wrapper = mount(ProfileIdentityBindingsSection, {
+ global: {
+ plugins: [pinia],
+ },
+ props: {
+ user: authStore.user,
+ linuxdoEnabled: false,
+ oidcEnabled: false,
+ wechatEnabled: false,
+ },
+ })
+
+ expect(wrapper.get('[data-testid="profile-binding-email-status"]').text()).toBe('Bound')
+ expect(wrapper.get('[data-testid="profile-binding-email-input"]').exists()).toBe(true)
+ expect(wrapper.get('[data-testid="profile-binding-email-submit"]').text()).toBe(
+ 'Replace primary email'
+ )
+ expect(
+ (wrapper.get('[data-testid="profile-binding-email-password-input"]').element as HTMLInputElement)
+ .placeholder
+ ).toBe('Current password')
+
+ await wrapper.get('[data-testid="profile-binding-email-input"]').setValue('new@example.com')
+ await wrapper.get('[data-testid="profile-binding-email-send-code"]').trigger('click')
+ expect(userApiMocks.sendEmailBindingCode).toHaveBeenCalledWith('new@example.com')
+
+ await wrapper.get('[data-testid="profile-binding-email-code-input"]').setValue('123456')
+ await wrapper.get('[data-testid="profile-binding-email-password-input"]').setValue(
+ 'current-password'
+ )
+ await wrapper.get('[data-testid="profile-binding-email-submit"]').trigger('click')
+
+ expect(userApiMocks.bindEmailIdentity).toHaveBeenCalledWith({
+ email: 'new@example.com',
+ verify_code: '123456',
+ password: 'current-password',
+ })
+ expect(authStore.user?.email).toBe('new@example.com')
+ expect(showSuccessSpy).toHaveBeenCalledWith('Primary email updated')
+ })
})
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 2b41a3c3..345770a8 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -967,9 +967,12 @@ export default {
emailPlaceholder: 'Enter email address',
codePlaceholder: 'Enter verification code',
passwordPlaceholder: 'Set a login password',
+ replaceEmailPasswordPlaceholder: 'Enter current password',
sendCodeAction: 'Send code',
confirmEmailBindAction: 'Bind email',
+ confirmEmailReplaceAction: 'Replace primary email',
codeSentTo: 'Code sent to {email}',
+ replaceSuccess: 'Primary email updated',
status: {
bound: 'Bound',
notBound: 'Not bound',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index b60a69d6..6493ffe8 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -971,9 +971,12 @@ export default {
emailPlaceholder: '输入邮箱地址',
codePlaceholder: '输入验证码',
passwordPlaceholder: '设置登录密码',
+ replaceEmailPasswordPlaceholder: '输入当前密码',
sendCodeAction: '发送验证码',
confirmEmailBindAction: '绑定邮箱',
+ confirmEmailReplaceAction: '更换主邮箱',
codeSentTo: '验证码已发送到 {email}',
+ replaceSuccess: '主邮箱已更新',
status: {
bound: '已绑定',
notBound: '未绑定',
From ace082066a14824462cb45386f43626ed7db9547 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 13:50:55 +0800
Subject: [PATCH 128/326] fix: honor ws transport when scheduler is disabled
---
.../service/openai_account_scheduler.go | 59 ++++++++++--
.../service/openai_account_scheduler_test.go | 92 +++++++++++++++++++
2 files changed, 143 insertions(+), 8 deletions(-)
diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go
index 09e60220..38b92b47 100644
--- a/backend/internal/service/openai_account_scheduler.go
+++ b/backend/internal/service/openai_account_scheduler.go
@@ -767,14 +767,10 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
}
func (s *defaultOpenAIAccountScheduler) isAccountTransportCompatible(account *Account, requiredTransport OpenAIUpstreamTransport) bool {
- // HTTP 入站可回退到 HTTP 线路,不需要在账号选择阶段做传输协议强过滤。
- if requiredTransport == OpenAIUpstreamTransportAny || requiredTransport == OpenAIUpstreamTransportHTTPSSE {
- return true
- }
- if s == nil || s.service == nil || account == nil {
+ if s == nil || s.service == nil {
return false
}
- return s.service.getOpenAIWSProtocolResolver().Resolve(account).Transport == requiredTransport
+ return s.service.isOpenAIAccountTransportCompatible(account, requiredTransport)
}
func (s *defaultOpenAIAccountScheduler) ReportResult(accountID int64, success bool, firstTokenMs *int) {
@@ -899,9 +895,35 @@ func (s *OpenAIGatewayService) SelectAccountWithScheduler(
decision := OpenAIAccountScheduleDecision{}
scheduler := s.getOpenAIAccountScheduler(ctx)
if scheduler == nil {
- selection, err := s.SelectAccountWithLoadAwareness(ctx, groupID, sessionHash, requestedModel, excludedIDs)
decision.Layer = openAIAccountScheduleLayerLoadBalance
- return selection, decision, err
+ if requiredTransport == OpenAIUpstreamTransportAny || requiredTransport == OpenAIUpstreamTransportHTTPSSE {
+ selection, err := s.SelectAccountWithLoadAwareness(ctx, groupID, sessionHash, requestedModel, excludedIDs)
+ return selection, decision, err
+ }
+
+ effectiveExcludedIDs := cloneExcludedAccountIDs(excludedIDs)
+ for {
+ selection, err := s.SelectAccountWithLoadAwareness(ctx, groupID, sessionHash, requestedModel, effectiveExcludedIDs)
+ if err != nil {
+ return nil, decision, err
+ }
+ if selection == nil || selection.Account == nil {
+ return selection, decision, nil
+ }
+ if s.isOpenAIAccountTransportCompatible(selection.Account, requiredTransport) {
+ return selection, decision, nil
+ }
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+ if effectiveExcludedIDs == nil {
+ effectiveExcludedIDs = make(map[int64]struct{})
+ }
+ if _, exists := effectiveExcludedIDs[selection.Account.ID]; exists {
+ return nil, decision, ErrNoAvailableAccounts
+ }
+ effectiveExcludedIDs[selection.Account.ID] = struct{}{}
+ }
}
var stickyAccountID int64
@@ -922,6 +944,27 @@ func (s *OpenAIGatewayService) SelectAccountWithScheduler(
})
}
+func cloneExcludedAccountIDs(excludedIDs map[int64]struct{}) map[int64]struct{} {
+ if len(excludedIDs) == 0 {
+ return nil
+ }
+ cloned := make(map[int64]struct{}, len(excludedIDs))
+ for id := range excludedIDs {
+ cloned[id] = struct{}{}
+ }
+ return cloned
+}
+
+func (s *OpenAIGatewayService) isOpenAIAccountTransportCompatible(account *Account, requiredTransport OpenAIUpstreamTransport) bool {
+ if requiredTransport == OpenAIUpstreamTransportAny || requiredTransport == OpenAIUpstreamTransportHTTPSSE {
+ return true
+ }
+ if s == nil || account == nil {
+ return false
+ }
+ return s.getOpenAIWSProtocolResolver().Resolve(account).Transport == requiredTransport
+}
+
func (s *OpenAIGatewayService) ReportOpenAIAccountScheduleResult(accountID int64, success bool, firstTokenMs *int) {
scheduler := s.getOpenAIAccountScheduler(context.Background())
if scheduler == nil {
diff --git a/backend/internal/service/openai_account_scheduler_test.go b/backend/internal/service/openai_account_scheduler_test.go
index a54f2614..b02370cb 100644
--- a/backend/internal/service/openai_account_scheduler_test.go
+++ b/backend/internal/service/openai_account_scheduler_test.go
@@ -298,6 +298,98 @@ func TestOpenAIGatewayService_SelectAccountWithScheduler_DefaultDisabledUsesLega
require.False(t, decision.StickyPreviousHit)
}
+func TestOpenAIGatewayService_SelectAccountWithScheduler_DefaultDisabled_RequiredWSV2_SkipsHTTPOnlyAccount(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ ctx := context.Background()
+ groupID := int64(10108)
+ accounts := []Account{
+ {
+ ID: 36011,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ },
+ {
+ ID: 36012,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 5,
+ Extra: map[string]any{
+ "openai_apikey_responses_websockets_v2_enabled": true,
+ },
+ },
+ }
+ cfg := newSchedulerTestOpenAIWSV2Config()
+ cfg.Gateway.Scheduling.LoadBatchEnabled = false
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: cfg,
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(
+ ctx,
+ &groupID,
+ "",
+ "",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportResponsesWebsocketV2,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(36012), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_DefaultDisabled_RequiredWSV2_NoAvailableAccount(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ ctx := context.Background()
+ groupID := int64(10109)
+ accounts := []Account{
+ {
+ ID: 36021,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ },
+ }
+ cfg := newSchedulerTestOpenAIWSV2Config()
+ cfg.Gateway.Scheduling.LoadBatchEnabled = false
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: cfg,
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(
+ ctx,
+ &groupID,
+ "",
+ "",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportResponsesWebsocketV2,
+ )
+ require.ErrorContains(t, err, "no available OpenAI accounts")
+ require.Nil(t, selection)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+}
+
func TestOpenAIGatewayService_SelectAccountWithScheduler_EnabledUsesAdvancedPreviousResponseRouting(t *testing.T) {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
From 0fcddce69edd1fca78dbd5d1b1099d8a6fe4c3b8 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 13:53:12 +0800
Subject: [PATCH 129/326] fix: reject http responses continuation ids
---
.../handler/openai_gateway_handler.go | 9 ++-
.../handler/openai_gateway_handler_test.go | 58 +++++++++++++++++++
2 files changed, 65 insertions(+), 2 deletions(-)
diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go
index 5319b55d..43999a01 100644
--- a/backend/internal/handler/openai_gateway_handler.go
+++ b/backend/internal/handler/openai_gateway_handler.go
@@ -187,6 +187,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "previous_response_id must be a response.id (resp_*), not a message id")
return
}
+ reqLog.Warn("openai.request_validation_failed",
+ zap.String("reason", "previous_response_id_requires_wsv2"),
+ )
+ h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "previous_response_id is only supported on Responses WebSocket v2")
+ return
}
setOpsRequestContext(c, reqModel, reqStream, body)
@@ -856,7 +861,7 @@ func (h *OpenAIGatewayHandler) validateFunctionCallOutputRequest(c *gin.Context,
reqLog.Warn("openai.request_validation_failed",
zap.String("reason", "function_call_output_missing_call_id"),
)
- h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires call_id or previous_response_id; if relying on history, ensure store=true and reuse previous_response_id")
+ h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires call_id on HTTP requests; continuation via previous_response_id is only supported on Responses WebSocket v2")
return false
}
if validation.HasItemReferenceForAllCallIDs {
@@ -866,7 +871,7 @@ func (h *OpenAIGatewayHandler) validateFunctionCallOutputRequest(c *gin.Context,
reqLog.Warn("openai.request_validation_failed",
zap.String("reason", "function_call_output_missing_item_reference"),
)
- h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires item_reference ids matching each call_id, or previous_response_id/tool_call context; if relying on history, ensure store=true and reuse previous_response_id")
+ h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires item_reference ids matching each call_id on HTTP requests; continuation via previous_response_id is only supported on Responses WebSocket v2")
return false
}
diff --git a/backend/internal/handler/openai_gateway_handler_test.go b/backend/internal/handler/openai_gateway_handler_test.go
index d299fb81..8ecee59a 100644
--- a/backend/internal/handler/openai_gateway_handler_test.go
+++ b/backend/internal/handler/openai_gateway_handler_test.go
@@ -494,6 +494,64 @@ func TestOpenAIResponses_RejectsMessageIDAsPreviousResponseID(t *testing.T) {
require.Contains(t, w.Body.String(), "previous_response_id must be a response.id")
}
+func TestOpenAIResponses_RejectsHTTPContinuationPreviousResponseID(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", strings.NewReader(
+ `{"model":"gpt-5.1","stream":false,"previous_response_id":"resp_123456","input":[{"type":"input_text","text":"hello"}]}`,
+ ))
+ c.Request.Header.Set("Content-Type", "application/json")
+
+ groupID := int64(2)
+ c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
+ ID: 101,
+ GroupID: &groupID,
+ User: &service.User{ID: 1},
+ })
+ c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
+ UserID: 1,
+ Concurrency: 1,
+ })
+
+ h := newOpenAIHandlerForPreviousResponseIDValidation(t, nil)
+ h.Responses(c)
+
+ require.Equal(t, http.StatusBadRequest, w.Code)
+ require.Contains(t, w.Body.String(), "Responses WebSocket v2")
+ require.Contains(t, w.Body.String(), "previous_response_id")
+}
+
+func TestOpenAIResponses_FunctionCallOutputHTTPGuidanceDoesNotSuggestPreviousResponseReuse(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", strings.NewReader(
+ `{"model":"gpt-5.1","stream":false,"input":[{"type":"function_call_output","output":"{}"}]}`,
+ ))
+ c.Request.Header.Set("Content-Type", "application/json")
+
+ groupID := int64(2)
+ c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
+ ID: 101,
+ GroupID: &groupID,
+ User: &service.User{ID: 1},
+ })
+ c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
+ UserID: 1,
+ Concurrency: 1,
+ })
+
+ h := newOpenAIHandlerForPreviousResponseIDValidation(t, nil)
+ h.Responses(c)
+
+ require.Equal(t, http.StatusBadRequest, w.Code)
+ require.Contains(t, w.Body.String(), "Responses WebSocket v2")
+ require.NotContains(t, w.Body.String(), "reuse previous_response_id")
+}
+
func TestOpenAIResponsesWebSocket_SetsClientTransportWSWhenUpgradeValid(t *testing.T) {
gin.SetMode(gin.TestMode)
From 62ff2d803f172defdc6648599edd7d3379539b35 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 13:56:02 +0800
Subject: [PATCH 130/326] fix: normalize chat completions service tier
---
.../openai_gateway_chat_completions.go | 42 +++++++++++++++++-
.../openai_gateway_chat_completions_test.go | 44 +++++++++++++++++++
2 files changed, 85 insertions(+), 1 deletion(-)
create mode 100644 backend/internal/service/openai_gateway_chat_completions_test.go
diff --git a/backend/internal/service/openai_gateway_chat_completions.go b/backend/internal/service/openai_gateway_chat_completions.go
index ac7d28a7..663066a3 100644
--- a/backend/internal/service/openai_gateway_chat_completions.go
+++ b/backend/internal/service/openai_gateway_chat_completions.go
@@ -107,11 +107,15 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
responsesBody = stripped
}
}
+ responsesBody, normalizedServiceTier, err := normalizeResponsesBodyServiceTier(responsesBody)
+ if err != nil {
+ return nil, fmt.Errorf("normalize service_tier in responses-shape body: %w", err)
+ }
// Minimal stub populated from the raw body so downstream billing
// propagation (ServiceTier, ReasoningEffort) keeps working.
responsesReq = &apicompat.ResponsesRequest{
Model: upstreamModel,
- ServiceTier: gjson.GetBytes(responsesBody, "service_tier").String(),
+ ServiceTier: normalizedServiceTier,
}
if effort := gjson.GetBytes(responsesBody, "reasoning.effort").String(); effort != "" {
responsesReq.Reasoning = &apicompat.ResponsesReasoning{Effort: effort}
@@ -124,6 +128,7 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
return nil, fmt.Errorf("convert chat completions to responses: %w", err)
}
responsesReq.Model = upstreamModel
+ normalizeResponsesRequestServiceTier(responsesReq)
responsesBody, err = json.Marshal(responsesReq)
if err != nil {
return nil, fmt.Errorf("marshal responses request: %w", err)
@@ -274,6 +279,41 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
return result, handleErr
}
+func normalizeResponsesRequestServiceTier(req *apicompat.ResponsesRequest) {
+ if req == nil {
+ return
+ }
+ req.ServiceTier = normalizedOpenAIServiceTierValue(req.ServiceTier)
+}
+
+func normalizeResponsesBodyServiceTier(body []byte) ([]byte, string, error) {
+ if len(body) == 0 {
+ return body, "", nil
+ }
+ rawServiceTier := gjson.GetBytes(body, "service_tier").String()
+ if rawServiceTier == "" {
+ return body, "", nil
+ }
+ normalizedServiceTier := normalizedOpenAIServiceTierValue(rawServiceTier)
+ if normalizedServiceTier == "" {
+ trimmed, err := sjson.DeleteBytes(body, "service_tier")
+ return trimmed, "", err
+ }
+ if normalizedServiceTier == rawServiceTier {
+ return body, normalizedServiceTier, nil
+ }
+ trimmed, err := sjson.SetBytes(body, "service_tier", normalizedServiceTier)
+ return trimmed, normalizedServiceTier, err
+}
+
+func normalizedOpenAIServiceTierValue(raw string) string {
+ normalized := normalizeOpenAIServiceTier(raw)
+ if normalized == nil {
+ return ""
+ }
+ return *normalized
+}
+
// handleChatCompletionsErrorResponse reads an upstream error and returns it in
// OpenAI Chat Completions error format.
func (s *OpenAIGatewayService) handleChatCompletionsErrorResponse(
diff --git a/backend/internal/service/openai_gateway_chat_completions_test.go b/backend/internal/service/openai_gateway_chat_completions_test.go
new file mode 100644
index 00000000..a00fb71c
--- /dev/null
+++ b/backend/internal/service/openai_gateway_chat_completions_test.go
@@ -0,0 +1,44 @@
+package service
+
+import (
+ "testing"
+
+ "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
+ "github.com/stretchr/testify/require"
+ "github.com/tidwall/gjson"
+)
+
+func TestNormalizeResponsesRequestServiceTier(t *testing.T) {
+ t.Parallel()
+
+ req := &apicompat.ResponsesRequest{ServiceTier: " fast "}
+ normalizeResponsesRequestServiceTier(req)
+ require.Equal(t, "priority", req.ServiceTier)
+
+ req.ServiceTier = "flex"
+ normalizeResponsesRequestServiceTier(req)
+ require.Equal(t, "flex", req.ServiceTier)
+
+ req.ServiceTier = "default"
+ normalizeResponsesRequestServiceTier(req)
+ require.Empty(t, req.ServiceTier)
+}
+
+func TestNormalizeResponsesBodyServiceTier(t *testing.T) {
+ t.Parallel()
+
+ body, tier, err := normalizeResponsesBodyServiceTier([]byte(`{"model":"gpt-5.1","service_tier":"fast"}`))
+ require.NoError(t, err)
+ require.Equal(t, "priority", tier)
+ require.Equal(t, "priority", gjson.GetBytes(body, "service_tier").String())
+
+ body, tier, err = normalizeResponsesBodyServiceTier([]byte(`{"model":"gpt-5.1","service_tier":"flex"}`))
+ require.NoError(t, err)
+ require.Equal(t, "flex", tier)
+ require.Equal(t, "flex", gjson.GetBytes(body, "service_tier").String())
+
+ body, tier, err = normalizeResponsesBodyServiceTier([]byte(`{"model":"gpt-5.1","service_tier":"default"}`))
+ require.NoError(t, err)
+ require.Empty(t, tier)
+ require.False(t, gjson.GetBytes(body, "service_tier").Exists())
+}
From 147ed42ad355ec4a100c0b29ba65ed3e1aeef233 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 14:10:30 +0800
Subject: [PATCH 131/326] fix: restrict payment return urls to internal result
page
---
backend/internal/service/payment_order.go | 2 +-
.../service/payment_resume_service.go | 46 +++++++++++++++++--
.../service/payment_resume_service_test.go | 24 ++++++++--
3 files changed, 63 insertions(+), 9 deletions(-)
diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go
index 254af5fe..354f3cd1 100644
--- a/backend/internal/service/payment_order.go
+++ b/backend/internal/service/payment_order.go
@@ -350,7 +350,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
}
subject := s.buildPaymentSubject(plan, limitAmount, cfg)
outTradeNo := order.OutTradeNo
- canonicalReturnURL, err := CanonicalizeReturnURL(req.ReturnURL)
+ canonicalReturnURL, err := CanonicalizeReturnURL(req.ReturnURL, req.SrcHost)
if err != nil {
return nil, err
}
diff --git a/backend/internal/service/payment_resume_service.go b/backend/internal/service/payment_resume_service.go
index 486aaac0..1806f5da 100644
--- a/backend/internal/service/payment_resume_service.go
+++ b/backend/internal/service/payment_resume_service.go
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "net"
"net/url"
"strconv"
"strings"
@@ -16,6 +17,8 @@ import (
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
+const paymentResultReturnPath = "/payment/result"
+
const (
PaymentSourceHostedRedirect = "hosted_redirect"
PaymentSourceWechatInAppResume = "wechat_in_app_resume"
@@ -215,7 +218,7 @@ func visibleMethodSourceSettingKey(method string) string {
}
}
-func CanonicalizeReturnURL(raw string) (string, error) {
+func CanonicalizeReturnURL(raw string, srcHost string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
@@ -231,19 +234,29 @@ func CanonicalizeReturnURL(raw string) (string, error) {
if parsed.Path == "" {
parsed.Path = "/"
}
+ if parsed.Path != paymentResultReturnPath {
+ return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must target the canonical internal payment result page")
+ }
+ if !sameOriginHost(parsed.Host, srcHost) {
+ return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must use the same host as the current site")
+ }
return parsed.String(), nil
}
func buildPaymentReturnURL(base string, orderID int64, resumeToken string) (string, error) {
- canonical, err := CanonicalizeReturnURL(base)
- if err != nil || canonical == "" {
- return canonical, err
+ canonical := strings.TrimSpace(base)
+ if canonical == "" {
+ return "", nil
}
parsed, err := url.Parse(canonical)
if err != nil {
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must be a valid URL")
}
+ if !parsed.IsAbs() || parsed.Host == "" {
+ return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must be a valid absolute URL")
+ }
+ parsed.Fragment = ""
query := parsed.Query()
if orderID > 0 {
@@ -258,6 +271,31 @@ func buildPaymentReturnURL(base string, orderID int64, resumeToken string) (stri
return parsed.String(), nil
}
+func sameOriginHost(returnURLHost string, requestHost string) bool {
+ returnHost := strings.TrimSpace(returnURLHost)
+ reqHost := strings.TrimSpace(requestHost)
+ if returnHost == "" || reqHost == "" {
+ return false
+ }
+ if strings.EqualFold(returnHost, reqHost) {
+ return true
+ }
+
+ returnName, returnPort := splitHostPortDefault(returnHost)
+ reqName, reqPort := splitHostPortDefault(reqHost)
+ if returnName == "" || reqName == "" {
+ return false
+ }
+ return strings.EqualFold(returnName, reqName) && returnPort == reqPort
+}
+
+func splitHostPortDefault(raw string) (string, string) {
+ if host, port, err := net.SplitHostPort(raw); err == nil {
+ return host, port
+ }
+ return raw, ""
+}
+
func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, error) {
if err := s.ensureSigningKey(); err != nil {
return "", err
diff --git a/backend/internal/service/payment_resume_service_test.go b/backend/internal/service/payment_resume_service_test.go
index 12d67be2..7fa8dca1 100644
--- a/backend/internal/service/payment_resume_service_test.go
+++ b/backend/internal/service/payment_resume_service_test.go
@@ -64,23 +64,39 @@ func TestNormalizePaymentSource(t *testing.T) {
func TestCanonicalizeReturnURL(t *testing.T) {
t.Parallel()
- got, err := CanonicalizeReturnURL("https://example.com/pay/result?b=2#a")
+ got, err := CanonicalizeReturnURL("https://example.com/payment/result?b=2#a", "example.com")
if err != nil {
t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
}
- if got != "https://example.com/pay/result?b=2" {
- t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/pay/result?b=2")
+ if got != "https://example.com/payment/result?b=2" {
+ t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result?b=2")
}
}
func TestCanonicalizeReturnURLRejectsRelativeURL(t *testing.T) {
t.Parallel()
- if _, err := CanonicalizeReturnURL("/payment/result"); err == nil {
+ if _, err := CanonicalizeReturnURL("/payment/result", "example.com"); err == nil {
t.Fatal("CanonicalizeReturnURL should reject relative URLs")
}
}
+func TestCanonicalizeReturnURLRejectsExternalHost(t *testing.T) {
+ t.Parallel()
+
+ if _, err := CanonicalizeReturnURL("https://evil.example/payment/result", "app.example.com"); err == nil {
+ t.Fatal("CanonicalizeReturnURL should reject external hosts")
+ }
+}
+
+func TestCanonicalizeReturnURLRejectsNonCanonicalPath(t *testing.T) {
+ t.Parallel()
+
+ if _, err := CanonicalizeReturnURL("https://app.example.com/orders/42", "app.example.com"); err == nil {
+ t.Fatal("CanonicalizeReturnURL should reject non-canonical result paths")
+ }
+}
+
func TestBuildPaymentReturnURL(t *testing.T) {
t.Parallel()
From 4a3652ec09d54fa0973ac93d7b3b501a550098aa Mon Sep 17 00:00:00 2001
From: erio
Date: Tue, 21 Apr 2026 14:10:53 +0800
Subject: [PATCH 132/326] refactor(channels): normalize at cache fill and
eliminate frontend as-cast
- channel.go: convert normalizeBillingModelSource into a (*Channel) method for entity cohesion
- channel_service.go: normalize in populateChannelCache so every cache-backed reader (gateway, billing, future endpoints) sees the default; drop the duplicate fallback inside resolveMapping
- table: tighten Row with status?: ChannelStatus / billing_model_source?: BillingModelSource, remove the [key: string]: unknown index signature
- admin view: drop the `as ChannelStatus` / `as BillingModelSource` assertions and add statusStyleOf / billingSourceLabelOf helpers with runtime fallback so unseen values render as "-" instead of crashing
---
backend/internal/service/channel.go | 12 +++++++++
backend/internal/service/channel_available.go | 2 +-
backend/internal/service/channel_service.go | 27 ++++++++-----------
.../channels/AvailableChannelsTable.vue | 6 ++++-
.../src/views/admin/AvailableChannelsView.vue | 21 ++++++++++++---
5 files changed, 46 insertions(+), 22 deletions(-)
diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go
index dcb68dc5..fa1a87c1 100644
--- a/backend/internal/service/channel.go
+++ b/backend/internal/service/channel.go
@@ -111,6 +111,18 @@ func (c *Channel) IsActive() bool {
return c.Status == StatusActive
}
+// normalizeBillingModelSource 若 BillingModelSource 为空则回填默认值 ChannelMapped。
+// 作为 *Channel 的实体方法集中管理默认值,service 层只需在 Channel 进入内存
+// (缓存装填、repo 读出)时调用一次,下游读路径就无需重复兜底。
+func (c *Channel) normalizeBillingModelSource() {
+ if c == nil {
+ return
+ }
+ if c.BillingModelSource == "" {
+ c.BillingModelSource = BillingModelSourceChannelMapped
+ }
+}
+
// GetModelPricing 根据模型名查找渠道定价,未找到返回 nil。
// 精确匹配,大小写不敏感。返回值拷贝,不污染缓存。
func (c *Channel) GetModelPricing(model string) *ChannelModelPricing {
diff --git a/backend/internal/service/channel_available.go b/backend/internal/service/channel_available.go
index a162d81d..7f6d1e85 100644
--- a/backend/internal/service/channel_available.go
+++ b/backend/internal/service/channel_available.go
@@ -66,7 +66,7 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel,
}
sort.SliceStable(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })
- normalizeBillingModelSource(ch)
+ ch.normalizeBillingModelSource()
out = append(out, AvailableChannel{
ID: ch.ID,
diff --git a/backend/internal/service/channel_service.go b/backend/internal/service/channel_service.go
index 4f22e205..51984400 100644
--- a/backend/internal/service/channel_service.go
+++ b/backend/internal/service/channel_service.go
@@ -301,6 +301,9 @@ func (s *ChannelService) fetchChannelData(ctx context.Context) ([]Channel, map[i
}
// populateChannelCache 将渠道列表和分组平台映射填充到缓存快照中。
+// 装填时对每个 Channel 统一归一化 BillingModelSource,让缓存命中的所有下游
+// (gateway routing / billing / 未来任何 cache-backed 读路径)都拿到已归一化的实体,
+// 避免"每个出口各自记得 normalize"反模式。
func populateChannelCache(channels []Channel, groupPlatforms map[int64]string) *channelCache {
cache := newEmptyChannelCache()
cache.groupPlatform = groupPlatforms
@@ -308,6 +311,7 @@ func populateChannelCache(channels []Channel, groupPlatforms map[int64]string) *
cache.loadedAt = time.Now()
for i := range channels {
+ channels[i].normalizeBillingModelSource()
ch := &channels[i]
cache.byID[ch.ID] = ch
for _, gid := range ch.GroupIDs {
@@ -518,14 +522,13 @@ func (s *ChannelService) ResolveChannelMappingAndRestrict(ctx context.Context, g
// resolveMapping 基于已查找的渠道信息解析模型映射。
// antigravity 分组依次尝试所有匹配平台,确保跨平台同名映射各自独立。
func resolveMapping(lk *channelLookup, groupID int64, model string) ChannelMappingResult {
+ // lk.channel 来自已装填的缓存,BillingModelSource 已在 populateChannelCache 阶段归一化,
+ // 这里无需重复兜底。
result := ChannelMappingResult{
MappedModel: model,
ChannelID: lk.channel.ID,
BillingModelSource: lk.channel.BillingModelSource,
}
- if result.BillingModelSource == "" {
- result.BillingModelSource = BillingModelSourceChannelMapped
- }
modelLower := strings.ToLower(model)
if mapped := lookupMappingAcrossPlatforms(lk.cache, groupID, lk.platform, modelLower); mapped != "" {
@@ -686,7 +689,7 @@ func (s *ChannelService) Create(ctx context.Context, input *CreateChannelInput)
ApplyPricingToAccountStats: input.ApplyPricingToAccountStats,
AccountStatsPricingRules: input.AccountStatsPricingRules,
}
- normalizeBillingModelSource(channel)
+ channel.normalizeBillingModelSource()
if err := validateChannelConfig(channel.ModelPricing, channel.ModelMapping); err != nil {
return nil, err
@@ -706,7 +709,7 @@ func (s *ChannelService) Create(ctx context.Context, input *CreateChannelInput)
if err != nil {
return nil, err
}
- normalizeBillingModelSource(created)
+ created.normalizeBillingModelSource()
return created, nil
}
@@ -717,18 +720,10 @@ func (s *ChannelService) GetByID(ctx context.Context, id int64) (*Channel, error
if err != nil {
return nil, err
}
- normalizeBillingModelSource(ch)
+ ch.normalizeBillingModelSource()
return ch, nil
}
-// normalizeBillingModelSource 若 BillingModelSource 为空则回填默认值 ChannelMapped。
-// 统一在 service 层完成,避免 handler 响应层重复兜底。
-func normalizeBillingModelSource(ch *Channel) {
- if ch != nil && ch.BillingModelSource == "" {
- ch.BillingModelSource = BillingModelSourceChannelMapped
- }
-}
-
// Update 更新渠道
func (s *ChannelService) Update(ctx context.Context, id int64, input *UpdateChannelInput) (*Channel, error) {
channel, err := s.repo.GetByID(ctx, id)
@@ -762,7 +757,7 @@ func (s *ChannelService) Update(ctx context.Context, id int64, input *UpdateChan
if err != nil {
return nil, err
}
- normalizeBillingModelSource(updated)
+ updated.normalizeBillingModelSource()
return updated, nil
}
@@ -886,7 +881,7 @@ func (s *ChannelService) List(ctx context.Context, params pagination.PaginationP
return nil, nil, err
}
for i := range channels {
- normalizeBillingModelSource(&channels[i])
+ channels[i].normalizeBillingModelSource()
}
return channels, res, nil
}
diff --git a/frontend/src/components/channels/AvailableChannelsTable.vue b/frontend/src/components/channels/AvailableChannelsTable.vue
index 0bd19518..96aa82a9 100644
--- a/frontend/src/components/channels/AvailableChannelsTable.vue
+++ b/frontend/src/components/channels/AvailableChannelsTable.vue
@@ -62,6 +62,7 @@ import DataTable from '@/components/common/DataTable.vue'
import Icon from '@/components/icons/Icon.vue'
import SupportedModelChip from './SupportedModelChip.vue'
import type { UserSupportedModel } from '@/api/channels'
+import type { ChannelStatus, BillingModelSource } from '@/constants/channel'
interface GroupRef {
id: number
@@ -75,7 +76,10 @@ interface Row {
groups: GroupRef[]
// 复用 user 侧最小 DTO;admin 侧 SupportedModel 结构上是其超集,可直接传入。
supported_models: UserSupportedModel[]
- [key: string]: unknown
+ // admin 独有字段:用精确类型代替 `unknown`,让消费端无需 `as` 断言,
+ // 也能在后端新增 union 成员时让前端 Record 查表立刻出空而非崩溃。
+ status?: ChannelStatus
+ billing_model_source?: BillingModelSource
}
interface Column {
diff --git a/frontend/src/views/admin/AvailableChannelsView.vue b/frontend/src/views/admin/AvailableChannelsView.vue
index 74e85618..a9b2462f 100644
--- a/frontend/src/views/admin/AvailableChannelsView.vue
+++ b/frontend/src/views/admin/AvailableChannelsView.vue
@@ -46,16 +46,16 @@
- {{ statusStyles[row.status as ChannelStatus].label }}
+ {{ statusStyleOf(row.status).label }}
- {{ billingSourceLabels[row.billing_model_source as BillingModelSource] }}
+ {{ billingSourceLabelOf(row.billing_model_source) }}
@@ -101,7 +101,7 @@ const columns = computed(() => [
/**
* 显示样式:i18n label + Tailwind class,按 ChannelStatus 完整穷举。
- * 用 Record 强制未来新增状态时 TS 编译失败,避免遗漏分支。
+ * Record 键类型强制未来新增 ChannelStatus 成员时 TS 编译失败,避免遗漏分支。
*/
const statusStyles = computed>(() => ({
[CHANNEL_STATUS_ACTIVE]: {
@@ -124,6 +124,19 @@ const billingSourceLabels = computed>(() => (
[BILLING_MODEL_SOURCE_CHANNEL_MAPPED]: t('admin.availableChannels.billingSource.channel_mapped')
}))
+// 运行时兜底:即便 service 层归一化漏点或后端新增未同步的 enum 值传入,
+// 也不会触发 undefined.cls 崩溃;统一降级为 "-"。
+const DEFAULT_STATUS_STYLE = { label: '-', cls: '' }
+const DEFAULT_BILLING_LABEL = '-'
+
+function statusStyleOf(status: ChannelStatus | undefined): { label: string; cls: string } {
+ return status ? statusStyles.value[status] : DEFAULT_STATUS_STYLE
+}
+
+function billingSourceLabelOf(src: BillingModelSource | undefined): string {
+ return src ? billingSourceLabels.value[src] : DEFAULT_BILLING_LABEL
+}
+
const filteredChannels = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return channels.value
From 422f3449a23f937f147f6db7740a550f7ae6c5d0 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 14:54:42 +0800
Subject: [PATCH 133/326] chore: remove local docs from repo
---
.gitignore | 5 +-
docs/ADMIN_PAYMENT_INTEGRATION_API.md | 243 ------
docs/PAYMENT.md | 287 -------
docs/PAYMENT_CN.md | 287 -------
...-04-20-auth-identity-payment-foundation.md | 539 -------------
...auth-identity-payment-foundation-design.md | 763 ------------------
6 files changed, 1 insertion(+), 2123 deletions(-)
delete mode 100644 docs/ADMIN_PAYMENT_INTEGRATION_API.md
delete mode 100644 docs/PAYMENT.md
delete mode 100644 docs/PAYMENT_CN.md
delete mode 100644 docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md
delete mode 100644 docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md
diff --git a/.gitignore b/.gitignore
index 1a92ea3e..cf2bda9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -126,12 +126,9 @@ backend/cmd/server/server
deploy/docker-compose.override.yml
.gocache/
vite.config.js
-docs/*
-!docs/PAYMENT.md
-!docs/PAYMENT_CN.md
+docs/
.serena/
.codex/
frontend/coverage/
aicodex
output/
-
diff --git a/docs/ADMIN_PAYMENT_INTEGRATION_API.md b/docs/ADMIN_PAYMENT_INTEGRATION_API.md
deleted file mode 100644
index f674f86c..00000000
--- a/docs/ADMIN_PAYMENT_INTEGRATION_API.md
+++ /dev/null
@@ -1,243 +0,0 @@
-# ADMIN_PAYMENT_INTEGRATION_API
-
-> 单文件中英双语文档 / Single-file bilingual documentation (Chinese + English)
-
----
-
-## 中文
-
-### 目标
-本文档用于对接外部支付系统(如 `sub2apipay`)与 Sub2API 的 Admin API,覆盖:
-- 支付成功后充值
-- 用户查询
-- 人工余额修正
-- 前端购买页参数透传
-
-### 基础地址
-- 生产:`https://`
-- Beta:`http://:8084`
-
-### 认证
-推荐使用:
-- `x-api-key: admin-<64hex>`
-- `Content-Type: application/json`
-- 幂等接口额外传:`Idempotency-Key`
-
-说明:管理员 JWT 也可访问 admin 路由,但服务间调用建议使用 Admin API Key。
-
-### 1) 一步完成创建并兑换
-`POST /api/v1/admin/redeem-codes/create-and-redeem`
-
-用途:原子完成“创建兑换码 + 兑换到指定用户”。
-
-请求头:
-- `x-api-key`
-- `Idempotency-Key`
-
-请求体示例:
-```json
-{
- "code": "s2p_cm1234567890",
- "type": "balance",
- "value": 100.0,
- "user_id": 123,
- "notes": "sub2apipay order: cm1234567890"
-}
-```
-
-幂等语义:
-- 同 `code` 且 `used_by` 一致:`200`
-- 同 `code` 但 `used_by` 不一致:`409`
-- 缺少 `Idempotency-Key`:`400`(`IDEMPOTENCY_KEY_REQUIRED`)
-
-curl 示例:
-```bash
-curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
- -H "x-api-key: ${KEY}" \
- -H "Idempotency-Key: pay-cm1234567890-success" \
- -H "Content-Type: application/json" \
- -d '{
- "code":"s2p_cm1234567890",
- "type":"balance",
- "value":100.00,
- "user_id":123,
- "notes":"sub2apipay order: cm1234567890"
- }'
-```
-
-### 2) 查询用户(可选前置校验)
-`GET /api/v1/admin/users/:id`
-
-```bash
-curl -s "${BASE}/api/v1/admin/users/123" \
- -H "x-api-key: ${KEY}"
-```
-
-### 3) 余额调整(已有接口)
-`POST /api/v1/admin/users/:id/balance`
-
-用途:人工补偿 / 扣减,支持 `set` / `add` / `subtract`。
-
-请求体示例(扣减):
-```json
-{
- "balance": 100.0,
- "operation": "subtract",
- "notes": "manual correction"
-}
-```
-
-```bash
-curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
- -H "x-api-key: ${KEY}" \
- -H "Idempotency-Key: balance-subtract-cm1234567890" \
- -H "Content-Type: application/json" \
- -d '{
- "balance":100.00,
- "operation":"subtract",
- "notes":"manual correction"
- }'
-```
-
-### 4) 购买页 / 自定义页面 URL Query 透传(iframe / 新窗口一致)
-当 Sub2API 打开 `purchase_subscription_url` 或用户侧自定义页面 iframe URL 时,会统一追加:
-- `user_id`
-- `token`
-- `theme`(`light` / `dark`)
-- `lang`(例如 `zh` / `en`,用于向嵌入页传递当前界面语言)
-- `ui_mode`(固定 `embedded`)
-
-示例:
-```text
-https://pay.example.com/pay?user_id=123&token=&theme=light&lang=zh&ui_mode=embedded
-```
-
-### 5) 失败处理建议
-- 支付成功与充值成功分状态落库
-- 回调验签成功后立即标记“支付成功”
-- 支付成功但充值失败的订单允许后续重试
-- 重试保持相同 `code`,并使用新的 `Idempotency-Key`
-
-### 6) `doc_url` 配置建议
-- 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
-- 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
-
----
-
-## English
-
-### Purpose
-This document describes the minimal Sub2API Admin API surface for external payment integrations (for example, `sub2apipay`), including:
-- Recharge after payment success
-- User lookup
-- Manual balance correction
-- Purchase page query parameter forwarding
-
-### Base URL
-- Production: `https://`
-- Beta: `http://:8084`
-
-### Authentication
-Recommended headers:
-- `x-api-key: admin-<64hex>`
-- `Content-Type: application/json`
-- `Idempotency-Key` for idempotent endpoints
-
-Note: Admin JWT can also access admin routes, but Admin API Key is recommended for server-to-server integration.
-
-### 1) Create and Redeem in one step
-`POST /api/v1/admin/redeem-codes/create-and-redeem`
-
-Use case: atomically create a redeem code and redeem it to a target user.
-
-Headers:
-- `x-api-key`
-- `Idempotency-Key`
-
-Request body:
-```json
-{
- "code": "s2p_cm1234567890",
- "type": "balance",
- "value": 100.0,
- "user_id": 123,
- "notes": "sub2apipay order: cm1234567890"
-}
-```
-
-Idempotency behavior:
-- Same `code` and same `used_by`: `200`
-- Same `code` but different `used_by`: `409`
-- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`)
-
-curl example:
-```bash
-curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
- -H "x-api-key: ${KEY}" \
- -H "Idempotency-Key: pay-cm1234567890-success" \
- -H "Content-Type: application/json" \
- -d '{
- "code":"s2p_cm1234567890",
- "type":"balance",
- "value":100.00,
- "user_id":123,
- "notes":"sub2apipay order: cm1234567890"
- }'
-```
-
-### 2) Query User (optional pre-check)
-`GET /api/v1/admin/users/:id`
-
-```bash
-curl -s "${BASE}/api/v1/admin/users/123" \
- -H "x-api-key: ${KEY}"
-```
-
-### 3) Balance Adjustment (existing API)
-`POST /api/v1/admin/users/:id/balance`
-
-Use case: manual correction with `set` / `add` / `subtract`.
-
-Request body example (`subtract`):
-```json
-{
- "balance": 100.0,
- "operation": "subtract",
- "notes": "manual correction"
-}
-```
-
-```bash
-curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
- -H "x-api-key: ${KEY}" \
- -H "Idempotency-Key: balance-subtract-cm1234567890" \
- -H "Content-Type: application/json" \
- -d '{
- "balance":100.00,
- "operation":"subtract",
- "notes":"manual correction"
- }'
-```
-
-### 4) Purchase / Custom Page URL query forwarding (iframe and new tab)
-When Sub2API opens `purchase_subscription_url` or a user-facing custom page iframe URL, it appends:
-- `user_id`
-- `token`
-- `theme` (`light` / `dark`)
-- `lang` (for example `zh` / `en`, used to pass the current UI language to the embedded page)
-- `ui_mode` (fixed: `embedded`)
-
-Example:
-```text
-https://pay.example.com/pay?user_id=123&token=&theme=light&lang=zh&ui_mode=embedded
-```
-
-### 5) Failure handling recommendations
-- Persist payment success and recharge success as separate states
-- Mark payment as successful immediately after verified callback
-- Allow retry for orders with payment success but recharge failure
-- Keep the same `code` for retry, and use a new `Idempotency-Key`
-
-### 6) Recommended `doc_url`
-- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
-- Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
diff --git a/docs/PAYMENT.md b/docs/PAYMENT.md
deleted file mode 100644
index 9322f7bf..00000000
--- a/docs/PAYMENT.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# Payment System Configuration Guide
-
-Sub2API has a built-in payment system that enables user self-service top-up without deploying a separate payment service.
-
----
-
-## Table of Contents
-
-- [Supported Payment Methods](#supported-payment-methods)
-- [Quick Start](#quick-start)
-- [System Settings](#system-settings)
-- [Provider Configuration](#provider-configuration)
-- [Provider Instance Management](#provider-instance-management)
-- [Webhook Configuration](#webhook-configuration)
-- [Payment Flow](#payment-flow)
-- [Migrating from Sub2ApiPay](#migrating-from-sub2apipay)
-
----
-
-## Supported Payment Methods
-
-| Provider | Payment Methods | Description |
-|----------|----------------|-------------|
-| **EasyPay** | Alipay, WeChat Pay | Third-party aggregation via EasyPay protocol |
-| **Alipay (Direct)** | Desktop QR code, mobile Alipay redirect | Direct integration with Alipay Open Platform, returning desktop QR codes and mobile WAP/app launch links |
-| **WeChat Pay (Direct)** | Native QR, H5, MP/JSAPI Pay | Direct integration with WeChat Pay APIv3 with environment-aware routing |
-| **Stripe** | Card, Alipay, WeChat Pay, Link, etc. | International payments, multi-currency support |
-
-> Alipay/WeChat Pay direct and EasyPay can both exist as backend provider instances, but the frontend always exposes only two visible buttons: `Alipay` and `WeChat Pay`. Admins choose exactly one source for each visible method: direct or EasyPay. Direct channels connect to payment APIs directly with lower fees; EasyPay aggregates through third-party platforms with easier setup.
-
-> **EasyPay Provider Recommendations**: Both options below are third-party aggregators compatible with the EasyPay protocol. Pick based on the funding channel and settlement currency you need:
->
-> - **Domestic channel / CNY settlement** — [ZPay](https://z-pay.cn/?uid=23808) (`https://z-pay.cn/?uid=23808`): direct integration with official Alipay / WeChat Pay APIs, fee **1.6%**; funds go straight to the merchant account with **T+1 automatic settlement**. Supports **individual users** (no business license required) with up to 10,000 CNY daily transactions; business-licensed accounts have no limit. Link contains the referral code of [Sub2ApiPay](https://github.com/touwaeriol/sub2apipay) original author [@touwaeriol](https://github.com/touwaeriol) — feel free to remove it.
-> - **International channel / USDT or USD settlement** — [Kyren Topup](https://kyren.top/?code=SUB2API) (`https://kyren.top/?code=SUB2API`): a ready-to-launch global payment stack for AI startups with WeChat Pay and Alipay support, local-currency checkout, and USD settlement. Fees: WeChat 2%, Alipay 2.5%; withdrawal 0.1% (min $40, max $150), settled in **USDT or USD**. No qualification review required — sign up and use immediately, making it the lowest barrier to entry. Withdrawal threshold is relatively high, recommended for users **who do not use domestic Chinese payment channels, cannot tolerate Stripe's 6%+ fees, have high transaction volume, and have USD or USDT channels to receive withdrawn funds**. Kyren Topup charges a $200 account opening fee; signing up via this link (which contains Sub2Api author [@Wei-Shaw](https://github.com/Wei-Shaw)'s referral code) **waives the opening fee**. Feel free to remove it if you prefer.
->
-> Please evaluate the security, reliability, and compliance of any third-party payment provider on your own — this project does not endorse or guarantee any of them.
-
----
-
-## Quick Start
-
-1. Go to Admin Dashboard → **Settings** → **Payment Settings** tab
-2. Enable **Payment**
-3. Configure basic parameters (amount range, timeout, etc.)
-4. Add at least one provider instance in **Provider Management**
-5. Users can now top up from the frontend
-
----
-
-## System Settings
-
-Configure the following in Admin Dashboard **Settings → Payment Settings**:
-
-### Basic Settings
-
-| Setting | Description | Default |
-|---------|-------------|---------|
-| **Enable Payment** | Enable or disable the payment system | Off |
-| **Product Name Prefix** | Prefix shown on payment page | - |
-| **Product Name Suffix** | Suffix (e.g., "Credits") | - |
-| **Minimum Amount** | Minimum single top-up amount | 1 |
-| **Maximum Amount** | Maximum single top-up amount (empty = unlimited) | - |
-| **Daily Limit** | Per-user daily cumulative limit (empty = unlimited) | - |
-| **Order Timeout** | Order timeout in minutes (minimum 1) | 30 |
-| **Max Pending Orders** | Maximum concurrent pending orders per user | 3 |
-| **Load Balance Strategy** | Strategy for selecting provider instances | Round Robin |
-
-### Frontend Visible Method Routing
-
-The current payment UX keeps the frontend method list unified and does not expose provider brands directly:
-
-- **Alipay**: when enabled, this button must be routed to either `Alipay (Direct)` or `EasyPay Alipay`
-- **WeChat Pay**: when enabled, this button must be routed to either `WeChat Pay (Direct)` or `EasyPay WeChat`
-- Each visible method can route to only one source at a time
-- If a visible method is enabled without a selected source, the frontend will not expose that method
-
-### Load Balance Strategies
-
-| Strategy | Description |
-|----------|-------------|
-| **Round Robin** | Distribute orders to instances in rotation |
-| **Least Amount** | Prefer instances with the lowest daily cumulative amount |
-
-### Cancel Rate Limiting
-
-Prevents users from repeatedly creating and canceling orders:
-
-| Setting | Description |
-|---------|-------------|
-| **Enable Limit** | Toggle |
-| **Window Mode** | Sliding / Fixed window |
-| **Time Window** | Window duration |
-| **Window Unit** | Minutes / Hours |
-| **Max Cancels** | Maximum cancellations allowed within the window |
-
-### Help Information
-
-| Setting | Description |
-|---------|-------------|
-| **Help Image** | Customer service QR code or help image (supports upload) |
-| **Help Text** | Instructions displayed on the payment page |
-
----
-
-## Provider Configuration
-
-Each provider type requires different credentials. Select the type when adding a new provider instance in **Provider Management → Add Provider**.
-
-> **Callback URLs are auto-generated**: When adding a provider, the Notify URL and Return URL are automatically constructed from your site domain. You only need to confirm the domain is correct.
-
-### EasyPay
-
-Compatible with any payment service that implements the EasyPay protocol.
-
-| Parameter | Description | Required |
-|-----------|-------------|----------|
-| **Merchant ID (PID)** | EasyPay merchant ID | Yes |
-| **Merchant Key (PKey)** | EasyPay merchant secret key | Yes |
-| **API Base URL** | EasyPay API base address | Yes |
-| **Alipay Channel ID** | Specify Alipay channel (optional) | No |
-| **WeChat Channel ID** | Specify WeChat channel (optional) | No |
-
-### Alipay (Direct)
-
-Direct integration with Alipay Open Platform. Desktop flows return a QR code for in-page display, while mobile flows return an Alipay WAP/app redirect URL.
-
-| Parameter | Description | Required |
-|-----------|-------------|----------|
-| **AppID** | Alipay application AppID | Yes |
-| **Private Key** | RSA2 application private key | Yes |
-| **Alipay Public Key** | Alipay public key | Yes |
-
-### WeChat Pay (Direct)
-
-Direct integration with WeChat Pay APIv3. Supports Native QR code payment, H5 payment, and MP/JSAPI payment inside the WeChat environment.
-
-| Parameter | Description | Required |
-|-----------|-------------|----------|
-| **AppID** | WeChat Pay AppID | Yes |
-| **Merchant ID (MchID)** | WeChat Pay merchant ID | Yes |
-| **Merchant API Private Key** | Merchant API private key (PEM format) | Yes |
-| **APIv3 Key** | 32-byte APIv3 key | Yes |
-| **WeChat Pay Public Key** | WeChat Pay public key (PEM format) | Yes |
-| **WeChat Pay Public Key ID** | WeChat Pay public key ID | Yes |
-| **Certificate Serial Number** | Merchant certificate serial number | Yes |
-
-### Stripe
-
-International payment platform supporting multiple payment methods and currencies.
-
-| Parameter | Description | Required |
-|-----------|-------------|----------|
-| **Secret Key** | Stripe secret key (`sk_live_...` or `sk_test_...`) | Yes |
-| **Publishable Key** | Stripe publishable key (`pk_live_...` or `pk_test_...`) | Yes |
-| **Webhook Secret** | Stripe Webhook signing secret (`whsec_...`) | Yes |
-
----
-
-## Provider Instance Management
-
-You can create **multiple instances** of the same provider type for load balancing and risk control:
-
-- **Multi-instance load balancing** — Distribute orders via round-robin or least-amount strategy
-- **Independent limits** — Each instance can have its own min/max amount and daily limit
-- **Independent toggle** — Enable/disable individual instances without affecting others
-- **Refund control** — Enable or disable refunds per instance
-- **Payment methods** — Each instance can support a subset of payment methods
-- **Ordering** — Drag to reorder instances
-
-### Instance Limit Configuration
-
-Each instance supports these limits:
-
-| Limit | Description |
-|-------|-------------|
-| **Minimum Amount** | Minimum order amount accepted by this instance |
-| **Maximum Amount** | Maximum order amount accepted by this instance |
-| **Daily Limit** | Daily cumulative transaction limit for this instance |
-
-> During load balancing, instances that exceed their limits are automatically skipped.
-
----
-
-## Webhook Configuration
-
-Payment callbacks are essential for the payment system to work correctly.
-
-### Callback URL Format
-
-When adding a provider, the system auto-generates callback URLs from your site domain:
-
-| Provider | Callback Path |
-|----------|-------------|
-| **EasyPay** | `https://your-domain.com/api/v1/payment/webhook/easypay` |
-| **Alipay (Direct)** | `https://your-domain.com/api/v1/payment/webhook/alipay` |
-| **WeChat Pay (Direct)** | `https://your-domain.com/api/v1/payment/webhook/wxpay` |
-| **Stripe** | `https://your-domain.com/api/v1/payment/webhook/stripe` |
-
-> Replace `your-domain.com` with your actual domain. For EasyPay / Alipay / WeChat Pay, the callback URL is auto-filled when adding the provider — no manual configuration needed.
-
-### Stripe Webhook Setup
-
-1. Log in to [Stripe Dashboard](https://dashboard.stripe.com/)
-2. Go to **Developers → Webhooks**
-3. Add an endpoint with the callback URL
-4. Subscribe to events: `payment_intent.succeeded`, `payment_intent.payment_failed`
-5. Copy the generated Webhook Secret (`whsec_...`) to your provider configuration
-
-### Important Notes
-
-- Callback URLs must use **HTTPS** (required by Stripe, strongly recommended for others)
-- Ensure your firewall allows callback requests from payment platforms
-- The system automatically verifies callback signatures to prevent forgery
-- Balance top-up is processed automatically upon successful payment — no manual intervention needed
-
----
-
-## Payment Flow
-
-```
-User selects amount and payment method
- │
- ▼
- Create Order (PENDING)
- ├─ Validate amount range, pending order count, daily limit
- ├─ Load balance to select provider instance
- └─ Call provider to get payment info
- │
- ▼
- User completes payment
- ├─ EasyPay → QR code / H5 redirect
- ├─ Alipay → Desktop QR / mobile Alipay redirect
- ├─ WeChat Pay → Desktop Native QR / non-WeChat H5 / in-WeChat JSAPI
- └─ Stripe → Payment Element (card/Alipay/WeChat/etc.)
- │
- ▼
- Webhook callback verified → Order PAID
- │
- ▼
- Auto top-up to user balance → Order COMPLETED
-```
-
-### Order Status Reference
-
-| Status | Description |
-|--------|-------------|
-| `PENDING` | Waiting for user to complete payment |
-| `PAID` | Payment confirmed, awaiting balance credit |
-| `COMPLETED` | Balance credited successfully |
-| `EXPIRED` | Timed out without payment |
-| `CANCELLED` | Cancelled by user |
-| `FAILED` | Balance credit failed, admin can retry |
-| `REFUND_REQUESTED` | Refund requested |
-| `REFUNDING` | Refund in progress |
-| `REFUNDED` | Refund completed |
-
-### Timeout and Fallback
-
-- Before marking an order as expired, the background job queries the upstream payment status first
-- If the user has actually paid but the callback was delayed, the system will reconcile automatically
-- The background job runs every 60 seconds to check for timed-out orders
-
----
-
-## Migrating from Sub2ApiPay
-
-If you previously used [Sub2ApiPay](https://github.com/touwaeriol/sub2apipay) as an external payment system, you can migrate to the built-in payment system:
-
-### Key Differences
-
-| Aspect | Sub2ApiPay | Built-in Payment |
-|--------|-----------|-----------------|
-| Deployment | Separate service (Next.js + PostgreSQL) | Built into Sub2API, no extra deployment |
-| Payment Methods | EasyPay, Alipay, WeChat, Stripe | Same |
-| Configuration | Environment variables + separate admin UI | Unified in Sub2API admin dashboard |
-| Top-up Integration | Via Admin API callback | Internal processing, more reliable |
-| Subscription Plans | Supported | Not yet (planned) |
-| Order Management | Separate admin interface | Integrated in Sub2API admin dashboard |
-
-### Migration Steps
-
-1. Enable payment in Sub2API admin dashboard and configure providers (use the same payment credentials)
-2. Update webhook callback URLs to Sub2API's callback endpoints
-3. Verify that new orders are processed correctly via built-in payment
-4. Decommission the Sub2ApiPay service
-
-> **Note**: Historical order data from Sub2ApiPay will not be automatically migrated. Keep Sub2ApiPay running for a while to access historical records.
diff --git a/docs/PAYMENT_CN.md b/docs/PAYMENT_CN.md
deleted file mode 100644
index 0fbc198a..00000000
--- a/docs/PAYMENT_CN.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# 支付系统配置指南
-
-Sub2API 内置支付系统,支持用户自助充值,无需部署独立的支付服务。
-
----
-
-## 目录
-
-- [支持的支付方式](#支持的支付方式)
-- [快速开始](#快速开始)
-- [系统设置](#系统设置)
-- [服务商配置](#服务商配置)
-- [服务商实例管理](#服务商实例管理)
-- [Webhook 配置](#webhook-配置)
-- [支付流程](#支付流程)
-- [从 Sub2ApiPay 迁移](#从-sub2apipay-迁移)
-
----
-
-## 支持的支付方式
-
-| 服务商 | 支付方式 | 说明 |
-|--------|---------|------|
-| **EasyPay(易支付)** | 支付宝、微信支付 | 兼容易支付协议的第三方聚合支付 |
-| **支付宝官方** | 桌面二维码扫码、移动端支付宝跳转 | 直接对接支付宝开放平台,桌面端返回二维码,移动端返回 WAP/唤起链接 |
-| **微信官方** | Native 扫码、H5、公众号/JSAPI 支付 | 直接对接微信支付 APIv3,按终端环境自动分流 |
-| **Stripe** | 银行卡、支付宝、微信支付、Link 等 | 国际支付,支持多币种 |
-
-> 支付宝官方 / 微信官方与易支付可以同时作为后台服务商实例存在,但前台始终只展示 `支付宝`、`微信支付` 两个可见按钮。管理员需要分别为这两个按钮选择唯一支付来源:官方或易支付。官方渠道直接对接 API,资金直达商户账户,手续费更低;易支付通过第三方平台聚合,接入门槛更低。
-
-> **易支付服务商推荐**:以下两家均为兼容易支付协议的第三方聚合支付,按资金通道与结算方式选择:
->
-> - **国内渠道 / 人民币结算** — [ZPay](https://z-pay.cn/?uid=23808)(`https://z-pay.cn/?uid=23808`):支付宝 / 微信官方 API 直连,手续费 **1.6%**;资金直达商家账户,**T+1 自动到账**。支持**个人用户**(无营业执照)每日 1 万元以内交易;拥有营业执照则无限额。链接含 [Sub2ApiPay](https://github.com/touwaeriol/sub2apipay) 原作者 [@touwaeriol](https://github.com/touwaeriol) 的邀请码,介意可去掉。
-> - **国际渠道 / USDT 或美元结算** — [启润支付](https://kyren.top/?code=SUB2API)(`https://kyren.top/?code=SUB2API`):为 AI 项目提供低门槛国际收款通道,支持国际版微信支付与支付宝,本地货币支付、美元结算。手续费:微信 2%、支付宝 2.5%;提现 0.1%(最低 40 美元、最高 150 美元),以 **USDT 或美元**到账。无资质审核、注册即用,使用门槛最低;提现门槛略高,适合**不使用国内支付渠道、无法接受 Stripe 高达 6%+ 手续费、流水较大,且拥有美元或 USDT 渠道可接收提现资金**的用户。启润支付开户费 200 美元,通过本链接注册(含 Sub2Api 作者 [@Wei-Shaw](https://github.com/Wei-Shaw) 邀请码)可**免开户费**,介意可去掉。
->
-> 支付渠道的安全性、稳定性及合规性请自行鉴别,本项目不对任何第三方支付服务商做担保或背书。
-
----
-
-## 快速开始
-
-1. 进入管理后台 → **设置** → **支付设置** 标签页
-2. 开启 **启用支付**
-3. 配置基本参数(金额范围、超时时间等)
-4. 在 **服务商管理** 中添加至少一个服务商实例
-5. 用户即可在前端页面进行充值
-
----
-
-## 系统设置
-
-在管理后台 **设置 → 支付设置** 中配置以下参数:
-
-### 基本设置
-
-| 设置项 | 说明 | 默认值 |
-|--------|------|--------|
-| **启用支付** | 启用或禁用支付系统 | 关闭 |
-| **商品名前缀** | 支付页面显示的商品名前缀 | - |
-| **商品名后缀** | 商品名后缀(如"元") | - |
-| **最低金额** | 单笔最低充值金额 | 1 |
-| **最高金额** | 单笔最高充值金额(留空表示不限制) | - |
-| **每日限额** | 每用户每日累计充值上限(留空表示不限制) | - |
-| **订单超时时间** | 订单超时分钟数,至少 1 分钟 | 30 |
-| **最大待支付订单数** | 同一用户最大并行待支付订单数 | 3 |
-| **负载均衡策略** | 多服务商实例时的选择策略 | 轮询 |
-
-### 前台可见支付方式路由
-
-当前版本对用户统一展示支付方式,不区分官方渠道还是易支付:
-
-- **支付宝**:后台启用后,需要额外指定该按钮路由到 `支付宝官方` 或 `易支付支付宝`
-- **微信支付**:后台启用后,需要额外指定该按钮路由到 `微信官方` 或 `易支付微信`
-- 同一个可见支付方式在同一时刻只能路由到一个来源
-- 支付来源未选择时,即使对应按钮被开启,前台也不会暴露该支付方式
-
-### 负载均衡策略
-
-| 策略 | 说明 |
-|------|------|
-| **轮询(round-robin)** | 按顺序轮流分配到各服务商实例 |
-| **最少金额(least-amount)** | 优先分配到当日累计金额最少的实例 |
-
-### 取消频率限制
-
-防止用户频繁创建并取消订单:
-
-| 设置项 | 说明 |
-|--------|------|
-| **启用限制** | 开关 |
-| **窗口模式** | 滚动窗口 / 固定窗口 |
-| **时间窗口** | 窗口长度 |
-| **窗口单位** | 分钟 / 小时 |
-| **最大次数** | 窗口内允许的最大取消次数 |
-
-### 帮助信息
-
-| 设置项 | 说明 |
-|--------|------|
-| **帮助图片** | 充值页面显示的客服二维码等图片(支持上传) |
-| **帮助文本** | 充值页面显示的说明文字 |
-
----
-
-## 服务商配置
-
-每种服务商需要不同的凭证和参数。在 **服务商管理 → 添加服务商** 中选择类型后填写。
-
-> **回调地址自动生成**:添加服务商时,异步回调地址(Notify URL)和同步跳转地址(Return URL)由系统根据你的站点域名自动拼接,无需手动填写。管理员只需确认域名正确即可。
-
-### EasyPay(易支付)
-
-兼容任何 EasyPay 协议的支付服务商。
-
-| 参数 | 说明 | 必填 |
-|------|------|------|
-| **商户 ID(PID)** | EasyPay 商户 ID | 是 |
-| **商户密钥(PKey)** | EasyPay 商户密钥 | 是 |
-| **API 地址** | EasyPay API 基础地址 | 是 |
-| **支付宝通道 ID** | 指定支付宝通道(可选) | 否 |
-| **微信通道 ID** | 指定微信通道(可选) | 否 |
-
-### 支付宝官方
-
-直接对接支付宝开放平台。桌面端返回二维码供页面内展示和扫码,移动端返回支付宝手机网站支付跳转链接。
-
-| 参数 | 说明 | 必填 |
-|------|------|------|
-| **AppID** | 支付宝应用 AppID | 是 |
-| **应用私钥** | RSA2 应用私钥 | 是 |
-| **支付宝公钥** | 支付宝公钥 | 是 |
-
-### 微信官方
-
-直接对接微信支付 APIv3,支持 Native 扫码支付、H5 支付,以及在微信环境内的公众号/JSAPI 支付。
-
-| 参数 | 说明 | 必填 |
-|------|------|------|
-| **AppID** | 微信支付 AppID | 是 |
-| **商户号(MchID)** | 微信支付商户号 | 是 |
-| **商户 API 私钥** | 商户 API 私钥(PEM 格式) | 是 |
-| **APIv3 密钥** | 32 位 APIv3 密钥 | 是 |
-| **微信支付公钥** | 微信支付公钥(PEM 格式) | 是 |
-| **微信支付公钥 ID** | 微信支付公钥 ID | 是 |
-| **商户证书序列号** | 商户证书序列号 | 是 |
-
-### Stripe
-
-国际支付平台,支持多种支付方式和币种。
-
-| 参数 | 说明 | 必填 |
-|------|------|------|
-| **Secret Key** | Stripe 密钥(`sk_live_...` 或 `sk_test_...`) | 是 |
-| **Publishable Key** | Stripe 可公开密钥(`pk_live_...` 或 `pk_test_...`) | 是 |
-| **Webhook Secret** | Stripe Webhook 签名密钥(`whsec_...`) | 是 |
-
----
-
-## 服务商实例管理
-
-同一种服务商可以创建**多个实例**,实现负载均衡和风控:
-
-- **多实例负载均衡** — 按轮询或最少金额策略分流订单
-- **独立限额** — 每个实例可独立配置单笔最小/最大金额和每日限额
-- **独立启停** — 可单独启用/禁用某个实例,不影响其他实例
-- **退款控制** — 每个实例可单独开启或关闭退款功能
-- **支付方式** — 每个实例可选择支持的支付方式子集
-- **排序** — 拖拽调整实例顺序
-
-### 实例限额配置
-
-每个实例支持以下限额:
-
-| 限额项 | 说明 |
-|--------|------|
-| **单笔最小金额** | 该实例接受的最小订单金额 |
-| **单笔最大金额** | 该实例接受的最大订单金额 |
-| **每日限额** | 该实例每日累计交易上限 |
-
-> 负载均衡时,系统会自动跳过超出限额的实例。
-
----
-
-## Webhook 配置
-
-支付回调是支付系统的核心环节,必须正确配置:
-
-### 回调地址格式
-
-添加服务商时,系统会自动根据站点域名拼接回调地址,格式如下:
-
-| 服务商 | 回调路径 |
-|--------|---------|
-| **EasyPay** | `https://your-domain.com/api/v1/payment/webhook/easypay` |
-| **支付宝官方** | `https://your-domain.com/api/v1/payment/webhook/alipay` |
-| **微信官方** | `https://your-domain.com/api/v1/payment/webhook/wxpay` |
-| **Stripe** | `https://your-domain.com/api/v1/payment/webhook/stripe` |
-
-> 将 `your-domain.com` 替换为你的实际域名。EasyPay / 支付宝 / 微信的回调地址在添加服务商时自动填入,无需手动配置。
-
-### Stripe Webhook 设置
-
-1. 登录 [Stripe Dashboard](https://dashboard.stripe.com/)
-2. 进入 **Developers → Webhooks**
-3. 添加端点,填写回调地址
-4. 订阅事件:`payment_intent.succeeded`、`payment_intent.payment_failed`
-5. 将生成的 Webhook Secret(`whsec_...`)填入服务商配置
-
-### 注意事项
-
-- 回调地址必须是 **HTTPS**(Stripe 强制要求,其他服务商强烈推荐)
-- 确保服务器防火墙允许支付平台的回调请求
-- 系统会自动进行签名验证,防止伪造回调
-- 支付成功后自动完成余额充值,无需人工干预
-
----
-
-## 支付流程
-
-```
-用户选择充值金额和支付方式
- │
- ▼
- 创建订单 (PENDING)
- ├─ 校验金额范围、待支付订单数、每日限额
- ├─ 负载均衡选择服务商实例
- └─ 调用服务商获取支付信息
- │
- ▼
- 用户完成支付
- ├─ EasyPay → 扫码 / H5 跳转
- ├─ 支付宝官方 → 桌面二维码 / 移动端支付宝跳转
- ├─ 微信官方 → 桌面 Native 扫码 / 非微信 H5 / 微信内 JSAPI
- └─ Stripe → Payment Element(银行卡/支付宝/微信等)
- │
- ▼
- 支付回调验签 → 订单 PAID
- │
- ▼
- 自动充值到用户余额 → 订单 COMPLETED
-```
-
-### 订单状态说明
-
-| 状态 | 说明 |
-|------|------|
-| `PENDING` | 待支付,等待用户完成支付 |
-| `PAID` | 已支付,等待充值到账 |
-| `COMPLETED` | 已完成,余额已到账 |
-| `EXPIRED` | 已过期,超时未支付 |
-| `CANCELLED` | 已取消,用户主动取消 |
-| `FAILED` | 充值失败,可管理员重试 |
-| `REFUND_REQUESTED` | 已申请退款 |
-| `REFUNDING` | 退款处理中 |
-| `REFUNDED` | 已退款 |
-
-### 超时与兜底
-
-- 订单超时后,后台任务会先查询上游支付状态再标记过期
-- 如果用户实际已支付但回调延迟,系统会通过查询补单
-- 后台任务每 60 秒执行一次超时检查
-
----
-
-## 从 Sub2ApiPay 迁移
-
-如果你之前使用 [Sub2ApiPay](https://github.com/touwaeriol/sub2apipay) 作为外部支付系统,现在可以迁移到内置支付:
-
-### 主要差异
-
-| 对比项 | Sub2ApiPay | 内置支付 |
-|--------|-----------|---------|
-| 部署方式 | 独立服务(Next.js + PostgreSQL) | 内置于 Sub2API,无需额外部署 |
-| 支付方式 | EasyPay、支付宝、微信、Stripe | 相同 |
-| 配置方式 | 环境变量 + 独立管理后台 | Sub2API 管理后台内统一配置 |
-| 充值对接 | 通过 Admin API 回调 | 内部直接处理,更可靠 |
-| 订阅套餐 | 支持 | 暂不支持(计划中) |
-| 订单管理 | 独立管理界面 | 集成在 Sub2API 管理后台 |
-
-### 迁移步骤
-
-1. 在 Sub2API 管理后台启用支付并配置服务商(使用相同的支付凭证)
-2. 更新 Webhook 回调地址为 Sub2API 的回调地址
-3. 确认新订单通过内置支付正常处理
-4. 停用 Sub2ApiPay 服务
-
-> **注意**:Sub2ApiPay 中的历史订单数据不会自动迁移。建议保留 Sub2ApiPay 一段时间以便查询历史记录。
diff --git a/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md b/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md
deleted file mode 100644
index 2d44e058..00000000
--- a/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md
+++ /dev/null
@@ -1,539 +0,0 @@
-# Auth Identity Payment Foundation Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Rebuild the auth identity, profile binding, payment routing, and OpenAI advanced scheduler foundation on top of a clean `origin/main` branch while preserving historical compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only records.
-**Architecture:** A unified identity foundation centered on durable provider subjects (`email`, `linuxdo`, `oidc`, `wechat`) and transactional pending-auth sessions; backend-owned payment source routing behind stable frontend methods (`alipay`, `wxpay`); compatibility-first migration/backfill before feature enablement.
-**Tech Stack:** Go, Gin, Ent, PostgreSQL, Redis, Vue 3, Pinia, TypeScript, Vitest, pnpm.
-
----
-
-## Non-Negotiable Product Rules
-
-- [ ] Preserve login continuity for existing email users, existing LinuxDo users, and historically migrated third-party users.
-- [ ] During migration, backfill historical LinuxDo/WeChat/OIDC synthetic-email users into explicit third-party identities before first post-upgrade login whenever deterministic recovery is possible.
-- [ ] During migration, surface historical WeChat `openid`-only records through explicit migration reports and remediation rules; do not silently reinterpret them as valid canonical identities.
-- [ ] Keep existing email login and add third-party login/bind for `linuxdo`, `oidc`, and `wechat`.
-- [ ] On first third-party login:
- - identity exists: direct login.
- - identity does not exist: start pending-auth flow.
- - local email binding is required only when system config says so.
- - upstream provider email verification never counts as local email verification.
-- [ ] When user-entered and locally verified email already exists:
- - offer bind-existing-account after local re-authentication.
- - offer change-email-and-create-new-account.
- - when email binding is mandatory, do not allow bypass without changing to another email.
-- [ ] On first third-party login or first third-party bind, provider nickname/avatar must be presented as independent replace options for the current nickname and avatar. They are not auto-applied.
-- [ ] Source-specific initial grants must support per-source defaults for balance, concurrency, and subscriptions.
-- [ ] Default grant timing: on successful new-account creation.
-- [ ] Optional grant timing: on first successful bind for the configured source.
-- [ ] Migration/backfill must never trigger first-bind or first-signup grants retroactively.
-- [ ] Avatar profile supports:
- - direct URL storage.
- - image data URL upload compressed to `<=100KB` before storing in DB.
- - explicit delete.
-- [ ] Admin user management must expose and sort by `last_login_at` and `last_active_at`.
-- [ ] WeChat login rules:
- - WeChat environment uses MP login.
- - non-WeChat browser uses Open/QR login.
- - canonical identity uses `unionid`.
- - when `unionid` is unavailable, fail the login/bind flow under the approved option-1 policy.
-- [ ] OIDC rules:
- - browser authorization-code flow always uses PKCE `S256`.
- - discovery issuer and ID token `iss` must match exactly.
- - `userinfo.sub` must match ID token `sub` when UserInfo is used.
- - upstream `email_verified` does not satisfy local email verification.
-- [ ] Payment UI rules:
- - user-facing methods stay `支付宝` and `微信支付`.
- - backend decides whether each method routes to official provider instance or EasyPay.
- - at runtime, each visible method may only have one active source.
-- [ ] Alipay rules:
- - PC: in-page QR.
- - mobile browser: jump to Alipay payment.
-- [ ] WeChat Pay rules:
- - PC: in-page QR.
- - WeChat H5: MP/JSAPI first, fallback to H5 pay.
- - non-WeChat H5: H5 pay, or prompt to open in WeChat when unavailable.
-- [ ] Payment success pages are informational only; actual fulfillment depends on webhook or server-side reconciliation.
-- [ ] WeChat in-app payment requiring `openid` must use a dedicated server-backed payment OAuth resume flow rather than frontend-only recovery state.
-- [ ] OpenAI advanced scheduler is available but default-disabled.
-
-## Hard Technical Constraints From Audit
-
-- [ ] Browser-based third-party auth must use Authorization Code + PKCE `S256`.
-- [ ] PKCE must not be admin-configurable off for browser authorization-code providers.
-- [ ] OIDC identity primary key must be `(issuer, subject)`, not email.
-- [ ] Email equality must never auto-link accounts.
-- [ ] Bind-existing-account must require explicit local re-authentication and TOTP verification when enabled.
-- [ ] Bind-current-user must originate from an already-authenticated local user and preserve explicit bind intent across callback completion.
-- [ ] OAuth redirect URI must be fixed server config, exact-match, and never derived from user input.
-- [ ] User-supplied redirect may only choose a normalized same-origin internal route after completion.
-- [ ] WeChat canonical identity must be `unionid`; `openid` remains channel/app-scoped support data only.
-- [ ] Every canonical identity uniqueness rule must include provider namespace (`provider_key`) consistently.
-- [ ] Callback completion must use backend session completion or a one-time opaque exchange code that is short-lived, one-time, browser-session-bound, `POST`-redeemed, and unusable as a bearer token.
-- [ ] Every payment order must snapshot the selected provider instance plus the order-time verification inputs required for callback verification, reconciliation, refund, and audit.
-- [ ] Frontend must not receive first-party bearer tokens through callback URL fragments in the rebuilt flow.
-- [ ] Public payment result polling must not expose order data by raw `out_trade_no` alone; use authenticated lookup or signed opaque result token.
-- [ ] WeChat Pay webhook handling must verify signature, decrypt payload, and compare `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and provider trade state against the order snapshot before fulfillment.
-
-## Baseline Notes
-
-- [ ] Current clean branch head when this plan was written: `721d7ab3`.
-- [ ] Baseline backend verification on clean `origin/main`: `cd backend && go test ./...` passes.
-- [ ] Baseline frontend verification on clean `origin/main`: `cd frontend && pnpm test:run` currently fails in unrelated existing suites. New work must add targeted tests and avoid claiming full frontend green until those baseline failures are addressed separately.
-- [ ] Existing migration directory currently ends at `107_*`; this rebuild reserves `108` through `111`.
-
-## Target File Map
-
-### New backend migrations
-
-- [ ] `backend/migrations/108_auth_identity_foundation_core.sql`
-- [ ] `backend/migrations/109_auth_identity_compat_backfill.sql`
-- [ ] `backend/migrations/110_pending_auth_and_provider_default_grants.sql`
-- [ ] `backend/migrations/111_payment_routing_and_scheduler_flags.sql`
-
-### New or rebuilt Ent schema
-
-- [ ] `backend/ent/schema/auth_identity.go`
-- [ ] `backend/ent/schema/auth_identity_channel.go`
-- [ ] `backend/ent/schema/pending_auth_session.go`
-- [ ] `backend/ent/schema/identity_adoption_decision.go`
-
-### New or rebuilt backend repositories/services/handlers
-
-- [ ] `backend/internal/repository/user_profile_identity_repo.go`
-- [ ] `backend/internal/repository/user_profile_identity_repo_contract_test.go`
-- [ ] `backend/internal/repository/auth_identity_migration_report.go`
-- [ ] `backend/internal/service/auth_identity_flow.go`
-- [ ] `backend/internal/service/auth_identity_flow_test.go`
-- [ ] `backend/internal/service/auth_pending_identity_service.go`
-- [ ] `backend/internal/service/auth_pending_identity_service_test.go`
-- [ ] `backend/internal/service/payment_config_service.go`
-- [ ] `backend/internal/service/payment_order.go`
-- [ ] `backend/internal/service/payment_order_lifecycle.go`
-- [ ] `backend/internal/service/payment_fulfillment.go`
-- [ ] `backend/internal/service/payment_resume_service.go`
-- [ ] `backend/internal/service/payment_resume_service_test.go`
-- [ ] `backend/internal/service/openai_account_scheduler.go`
-- [ ] `backend/internal/handler/auth_pending_identity_flow.go`
-- [ ] `backend/internal/handler/auth_linuxdo_oauth.go`
-- [ ] `backend/internal/handler/auth_oidc_oauth.go`
-- [ ] `backend/internal/handler/auth_wechat_oauth.go`
-- [ ] `backend/internal/handler/auth_handler.go`
-- [ ] `backend/internal/handler/user_handler.go`
-- [ ] `backend/internal/handler/payment_handler.go`
-- [ ] `backend/internal/handler/payment_webhook_handler.go`
-- [ ] `backend/internal/handler/admin/user_handler.go`
-- [ ] `backend/internal/handler/admin/setting_handler.go`
-
-### New or rebuilt frontend API/store/views/components
-
-- [ ] `frontend/src/api/auth.ts`
-- [ ] `frontend/src/api/user.ts`
-- [ ] `frontend/src/api/payment.ts`
-- [ ] `frontend/src/api/admin/settings.ts`
-- [ ] `frontend/src/api/admin/users.ts`
-- [ ] `frontend/src/stores/auth.ts`
-- [ ] `frontend/src/stores/payment.ts`
-- [ ] `frontend/src/components/auth/ThirdPartyAuthCallbackFlow.vue`
-- [ ] `frontend/src/components/auth/LinuxDoOAuthSection.vue`
-- [ ] `frontend/src/components/auth/OidcOAuthSection.vue`
-- [ ] `frontend/src/components/auth/WechatOAuthSection.vue`
-- [ ] `frontend/src/components/user/profile/ProfileAccountBindingsCard.vue`
-- [ ] `frontend/src/components/user/profile/ProfileInfoCard.vue`
-- [ ] `frontend/src/views/auth/LinuxDoCallbackView.vue`
-- [ ] `frontend/src/views/auth/OidcCallbackView.vue`
-- [ ] `frontend/src/views/auth/WechatCallbackView.vue`
-- [ ] `frontend/src/views/user/ProfileView.vue`
-- [ ] `frontend/src/views/user/PaymentView.vue`
-- [ ] `frontend/src/views/user/PaymentQRCodeView.vue`
-- [ ] `frontend/src/views/user/PaymentResultView.vue`
-
-## Phase 1: Migration And Compatibility Foundation
-
-### Task 1. Create core identity schema migration
-
-- [ ] Implement `backend/migrations/108_auth_identity_foundation_core.sql` with:
- - `auth_identities`
- - `auth_identity_channels`
- - `pending_auth_sessions`
- - `identity_adoption_decisions`
- - `users.last_login_at`
- - `users.last_active_at`
- - grant-tracking columns/tables required to prevent double-award
-- [ ] Add uniqueness/index rules:
- - one canonical identity per `(provider, provider_key, provider_subject)`
- - one channel record per `(provider, provider_channel, provider_app_id, provider_channel_subject)`
- - one adoption decision per pending session
-- [ ] Model `pending_auth_sessions` so immutable upstream claims and mutable local flow state are stored separately; do not reintroduce a mixed `metadata` catch-all.
-- [ ] Preserve null-safe compatibility defaults so historical rows remain readable before backfill finishes.
-- [ ] Add explicit rollback blocks only where safe; never repeat the destructive pattern observed in old `112_update_pending_auth_sessions.sql`.
-
-### Task 2. Materialize historical identities before runtime
-
-- [ ] Implement `backend/migrations/109_auth_identity_compat_backfill.sql` to backfill:
- - existing email users into `auth_identities(provider=email, provider_subject=normalized_email)`
- - historical LinuxDo users into `auth_identities(provider=linuxdo, provider_subject=linuxdo_subject)`
- - historical synthetic-email LinuxDo users into explicit LinuxDo identity rows by parsing legacy email mode and legacy provider metadata
- - historical synthetic-email WeChat users into explicit WeChat identities where `unionid` or equivalent deterministic provider identity is recoverable
- - historical synthetic-email OIDC users into explicit OIDC identities where deterministic provider identity is recoverable
- - profile/channel rows from historical `user_external_identities`-style data when present in upgraded databases
-- [ ] Write migration report output in `backend/internal/repository/auth_identity_migration_report.go` so production can inspect unmatched rows, `openid`-only WeChat rows, and non-deterministic synthetic-email rows instead of silently skipping them.
-- [ ] Set `signup_source` and provider provenance when recoverable from historical data. Do not flatten everything to `email`.
-
-### Task 3. Provider default grant and scheduler config migration
-
-- [ ] Implement `backend/migrations/110_pending_auth_and_provider_default_grants.sql` for:
- - provider-specific initial balance/concurrency/subscription defaults
- - grant timing flags: `on_signup`, optional `on_first_bind`
- - email-required-on-third-party-signup flags
- - profile avatar storage columns/settings
-- [ ] Implement `backend/migrations/111_payment_routing_and_scheduler_flags.sql` for:
- - stable payment method to provider-instance routing
- - visible-method normalization from historical `supported_types`, `payment_mode`, and legacy aliases such as `wxpay_direct`
- - admin exclusivity flags for `alipay` and `wxpay`
- - advanced scheduler enable flag defaulting to disabled
-
-### Task 4. Generate Ent and compile migration-safe model layer
-
-- [ ] Add the schema definitions in:
- - `backend/ent/schema/auth_identity.go`
- - `backend/ent/schema/auth_identity_channel.go`
- - `backend/ent/schema/pending_auth_session.go`
- - `backend/ent/schema/identity_adoption_decision.go`
-- [ ] Run:
- ```bash
- cd backend
- go generate ./ent
- ```
-- [ ] Compile after generation:
- ```bash
- cd backend
- go test ./... -run '^$'
- ```
-- [ ] Commit checkpoint:
- ```bash
- git add backend/migrations backend/ent/schema backend/ent
- git commit -m "feat: add auth identity foundation schema"
- ```
-
-## Phase 2: Backend Identity Flow Rebuild
-
-### Task 5. Build a single repository contract for identity lookups and grants
-
-- [ ] Implement `backend/internal/repository/user_profile_identity_repo.go` with transactional helpers for:
- - get user by canonical identity
- - get user by channel identity
- - create canonical + channel identity together
- - bind identity to existing user after verified re-auth
- - record one-time provider grant award
- - record adoption preference decisions
- - update `last_login_at` and `last_active_at`
-- [ ] Add repository contract coverage in `backend/internal/repository/user_profile_identity_repo_contract_test.go`.
-- [ ] Enforce dual-write for email registration/login so `users.email` and `auth_identities(provider=email, ...)` stay consistent from this phase onward.
-- [ ] Add repository coverage proving `last_login_at` and `last_active_at` use the required field names and are not silently replaced by derived `last_used_at` logic.
-
-### Task 6. Rebuild transactional pending-auth service
-
-- [ ] Implement `backend/internal/service/auth_pending_identity_service.go` and tests to own these flows:
- - create pending session from third-party callback
- - verify local email code
- - create new account from pending session with correct `signup_source`
- - bind pending identity to existing account after password/TOTP re-auth
- - apply configured provider defaults on the correct trigger only once
- - store provider nickname/avatar candidates and user opt-in replacement decisions independently
-- [ ] Implement callback completion so pending auth can finish through backend session completion or a one-time exchange code:
- - short TTL
- - one-time use
- - browser-session binding
- - `POST` redemption only
- - safe mixed-version bridge to legacy pending-token aliases during rollout
-- [ ] Keep pending session payload normalized:
- - provider identity fields live in typed columns/JSON structure
- - mutable local progression lives separately from immutable upstream claims
- - avoid the old branch’s mixed `metadata` and `upstream_identity_payload` ambiguity
-- [ ] Do not call plain email registration helpers from this flow. The old feature branch bug where pending third-party signup fell back to `RegisterWithVerification` must not reappear.
-
-### Task 7. Rebuild provider callback adapters
-
-- [ ] Refactor these handlers to thin adapters over the shared pending-auth service:
- - `backend/internal/handler/auth_linuxdo_oauth.go`
- - `backend/internal/handler/auth_oidc_oauth.go`
- - `backend/internal/handler/auth_wechat_oauth.go`
-- [ ] For OIDC:
- - require PKCE `S256`, `state`, and `nonce`
- - validate discovery issuer, `iss`, `aud`, optional `azp`, `exp`, and `nonce`
- - verify `userinfo.sub == id_token.sub` when UserInfo is used
- - persist canonical identity as `(issuer, sub)`
-- [ ] For WeChat:
- - MP flow in WeChat UA
- - Open/QR flow outside WeChat UA
- - website login uses authorization-code flow and persists channel/app binding
- - persist channel identity by `(channel, appid, openid)`
- - persist canonical identity by `unionid`
- - hard-fail when `unionid` is absent under the approved product policy
-- [ ] Replace callback URL fragment token delivery with backend session completion or one-time exchange code consumed by `frontend/src/stores/auth.ts`.
-
-### Task 8. Rebuild auth endpoints and profile binding endpoints
-
-- [ ] Implement `backend/internal/handler/auth_pending_identity_flow.go` for:
- - fetch pending session summary
- - submit verified email
- - choose create-new-account or bind-existing-account
- - submit nickname/avatar replacement choices
-- [ ] Make bind-existing-account and bind-current-user flows explicit:
- - no automatic linking on matching email
- - fresh password/TOTP proof is scoped to the intended target account only
- - no automatic metadata merge beyond explicitly selected nickname/avatar adoption
-- [ ] Update `backend/internal/handler/auth_handler.go` and `backend/internal/handler/user_handler.go` to expose:
- - current bindings summary
- - start-bind endpoints for LinuxDo/OIDC/WeChat
- - disconnect endpoints with safety checks
- - avatar upload/delete endpoints
-- [ ] Avatar handling requirements:
- - allow external URL
- - allow data URL upload
- - compress image payload to `<=100KB`
- - store compressed value in DB
- - deleting custom avatar must not implicitly resurrect stale provider avatar unless the user explicitly chooses provider avatar again
-
-### Task 9. Add admin visibility and sorting
-
-- [ ] Update `backend/internal/handler/admin/user_handler.go` and supporting query/service code so admin list supports:
- - `last_login_at`
- - `last_active_at`
- - sorting by both
- - binding/provider summary columns
-- [ ] Update `backend/internal/handler/admin/setting_handler.go` and setting service code for:
- - provider initial grant config
- - mandatory-email-on-third-party-signup config
- - payment source exclusivity config
- - advanced scheduler toggle
-
-### Task 10. Backend verification checkpoint
-
-- [ ] Run targeted backend tests:
- ```bash
- cd backend
- go test ./internal/repository -run 'TestUserProfileIdentity|TestAuthIdentityMigration'
- go test ./internal/service -run 'TestAuthIdentityFlow|TestPendingAuthIdentity|TestOpenAIAccountScheduler'
- go test ./internal/handler -run 'TestLinuxDo|TestOidc|TestWechat|TestPaymentWebhook'
- go test ./...
- ```
-- [ ] Commit checkpoint:
- ```bash
- git add backend
- git commit -m "feat: rebuild auth identity backend flows"
- ```
-
-## Phase 3: Frontend Third-Party Flow And Profile UX
-
-### Task 11. Rebuild callback flow UI around pending session decisions
-
-- [ ] Rebuild `frontend/src/components/auth/ThirdPartyAuthCallbackFlow.vue` so it:
- - loads pending-session summary from backend
- - shows provider nickname/avatar candidates
- - lets user independently choose nickname replacement and avatar replacement
- - handles create-new-account vs bind-existing-account
- - enforces verified local email before completion when required
- - handles “email already exists” by branching to bind-existing-account or change-email-and-create-new-account
-- [ ] Update:
- - `frontend/src/views/auth/LinuxDoCallbackView.vue`
- - `frontend/src/views/auth/OidcCallbackView.vue`
- - `frontend/src/views/auth/WechatCallbackView.vue`
- - `frontend/src/api/auth.ts`
- - `frontend/src/stores/auth.ts`
-- [ ] Replace any token-fragment bootstrap with backend session completion or one-time exchange code flow.
-- [ ] During rollout, keep temporary compatibility readers for legacy pending-token aliases behind a bounded bridge contract and explicit removal step.
-
-### Task 12. Rebuild profile account binding and avatar UX
-
-- [ ] Rebuild `frontend/src/components/user/profile/ProfileAccountBindingsCard.vue` to:
- - show linked LinuxDo/OIDC/WeChat providers
- - start bind/unbind flows
- - show provider avatars and nicknames as reference only
- - prevent unsafe disconnect when it would strand the account
-- [ ] Rebuild `frontend/src/components/user/profile/ProfileInfoCard.vue` and `frontend/src/views/user/ProfileView.vue` to:
- - support avatar URL entry
- - support data URL upload/compression preview
- - support avatar delete
- - clearly separate current profile nickname/avatar from provider-sourced suggested nickname/avatar
-
-### Task 13. Add frontend tests for rebuilt auth/profile flows
-
-- [ ] Add or update:
- - `frontend/src/components/auth/__tests__/ThirdPartyAuthCallbackFlow.spec.ts`
- - `frontend/src/components/auth/__tests__/LinuxDoCallbackView.spec.ts`
- - `frontend/src/components/auth/__tests__/WechatCallbackView.spec.ts`
- - `frontend/src/components/user/profile/__tests__/ProfileAccountBindingsCard.spec.ts`
- - `frontend/src/components/user/profile/__tests__/ProfileInfoCard.spec.ts`
-- [ ] Cover:
- - email-required branch
- - email-conflict branch
- - bind-existing-account with re-auth prompt
- - nickname replacement only
- - avatar replacement only
- - neither replacement
- - avatar delete after prior provider adoption
-
-## Phase 4: Payment Routing Rebuild
-
-### Task 14. Normalize payment routing backend
-
-- [ ] Rebuild `backend/internal/service/payment_config_service.go` to expose a stable method-routing contract:
- - frontend visible methods remain `alipay` and `wxpay`
- - admin chooses which provider instance serves each method
- - runtime validation guarantees only one active source per visible method
-- [ ] Add migration logic and tests to normalize historical provider-instance config:
- - `supported_types`
- - `payment_mode`
- - legacy aliases such as `wxpay_direct`
- - historical limit config
-- [ ] Rebuild `backend/internal/service/payment_order.go` and `backend/internal/service/payment_order_lifecycle.go` so order creation snapshots:
- - visible method
- - selected provider instance id
- - provider type
- - provider capability mode
- - verification-critical provider fields needed for later callback/query/refund validation
-- [ ] Rebuild `backend/internal/handler/payment_handler.go` for UX rules:
- - Alipay PC: QR page
- - Alipay mobile: direct jump
- - WeChat PC: QR page
- - WeChat H5 in WeChat: MP/JSAPI first, fallback to H5
- - WeChat H5 outside WeChat: H5 or “open in WeChat” prompt when unavailable
-- [ ] Never derive canonical return URL from `Referer`; use configured or signed internal callback targets only.
-- [ ] Implement `backend/internal/service/payment_resume_service.go` so WeChat in-app payment OAuth resume is server-backed rather than localStorage-backed:
- - create `oauth_required` resume context
- - persist amount/order_type/plan_id/visible method/redirect/state
- - redeem callback into same-origin internal resume target
- - expire and consume resume context safely
-
-### Task 15. Make fulfillment and reconciliation provider-instance-safe
-
-- [ ] Rebuild `backend/internal/handler/payment_webhook_handler.go` and `backend/internal/service/payment_fulfillment.go` so:
- - verification uses the order’s original provider instance
- - webhook processing is idempotent by provider event id and internal order id
- - missed webhook recovery uses server-side provider query, not frontend success return
-- [ ] For WeChat Pay specifically, enforce:
- - fixed HTTPS `notify_url` with no query params
- - no dependency on user login state
- - signature verification before decrypt
- - APIv3 decrypt before business parsing
- - comparison of `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and trade state against the order snapshot
-- [ ] Harden `frontend/src/views/user/PaymentResultView.vue` and `frontend/src/api/payment.ts` so result polling uses an authenticated order lookup or signed opaque token, not a raw public `out_trade_no` query.
-
-### Task 16. Rebuild payment frontend views
-
-- [ ] Rebuild `frontend/src/views/user/PaymentView.vue`, `frontend/src/views/user/PaymentQRCodeView.vue`, and `frontend/src/stores/payment.ts` so:
- - only two buttons are shown to user: `支付宝` and `微信支付`
- - frontend does not leak official-vs-EasyPay distinction
- - route-specific copy handles QR, jump, MP, H5 fallback correctly
-- [ ] Rebuild WeChat in-app payment resume UX around the server-backed resume context:
- - handle `oauth_required`
- - continue from same-origin resume target
- - avoid long-lived localStorage as the source of truth
-- [ ] Add or update:
- - `frontend/src/views/user/__tests__/PaymentView.spec.ts`
- - `frontend/src/views/user/__tests__/PaymentResultView.spec.ts`
- - backend webhook/payment routing tests
-
-### Task 17. Payment verification checkpoint
-
-- [ ] Run:
- ```bash
- cd backend
- go test ./internal/service -run 'TestPayment'
- go test ./internal/handler -run 'TestPayment'
- cd ../frontend
- pnpm test:run src/views/user/__tests__/PaymentView.spec.ts src/views/user/__tests__/PaymentResultView.spec.ts
- ```
-- [ ] Commit checkpoint:
- ```bash
- git add backend frontend
- git commit -m "feat: rebuild payment routing foundation"
- ```
-
-## Phase 5: Scheduler, Rollout, And Final Compatibility Pass
-
-### Task 18. Gate advanced scheduler behind explicit config
-
-- [ ] Update `backend/internal/service/openai_account_scheduler.go` and related admin setting surfaces so:
- - advanced scheduler remains compiled and testable
- - default runtime state is disabled
- - enablement is explicit through admin settings
- - legacy scheduling behavior remains default on upgrade
-- [ ] Add targeted coverage in `backend/internal/service/openai_account_scheduler_test.go`.
-
-### Task 19. Complete compatibility and rollout safety checks
-
-- [ ] Add migration/repository tests covering:
- - historical email-only user login after upgrade
- - historical LinuxDo user login after upgrade
- - historical synthetic-email LinuxDo user login after upgrade
- - historical synthetic-email WeChat user login after upgrade
- - historical synthetic-email OIDC user login after upgrade
- - historical WeChat `openid`-only rows are reported or explicitly remediated
- - no retroactive grant replay during migration
- - first-bind grant fires once only when enabled
- - email identity dual-write stays consistent
- - bind-existing-account requires password and TOTP where configured
- - mixed-version callback token bridge works during rollout and is removable afterward
- - historical payment config is normalized into visible-method routing without refund/query regression
-- [ ] Add deploy sequencing note to release docs or internal runbook:
- 1. deploy schema and backfill release.
- 2. inspect migration report for unmatched rows.
- 3. deploy backend identity/payment compatibility code with exchange bridge and legacy token aliases still enabled.
- 4. deploy frontend callback/profile/payment UI using session completion, exchange code, and server-backed WeChat payment resume.
- 5. remove legacy callback/token parsing after mixed-version window closes.
- 6. enable strict email-required signup or provider bind grants only after metrics are healthy.
-
-### Task 20. Final verification and handoff
-
-- [ ] Run final backend verification:
- ```bash
- cd backend
- go test ./...
- ```
-- [ ] Run targeted frontend verification:
- ```bash
- cd frontend
- pnpm test:run \
- src/components/auth/__tests__/ThirdPartyAuthCallbackFlow.spec.ts \
- src/components/auth/__tests__/LinuxDoCallbackView.spec.ts \
- src/components/auth/__tests__/WechatCallbackView.spec.ts \
- src/components/user/profile/__tests__/ProfileAccountBindingsCard.spec.ts \
- src/components/user/profile/__tests__/ProfileInfoCard.spec.ts \
- src/views/user/__tests__/PaymentView.spec.ts \
- src/views/user/__tests__/PaymentResultView.spec.ts
- ```
-- [ ] Run focused manual smoke checks:
- - email login with existing account
- - LinuxDo existing-account login after migration
- - WeChat synthetic-email account login after migration
- - OIDC synthetic-email account login after migration
- - third-party first login create-new-account path
- - third-party first login bind-existing-account path
- - first third-party bind with optional nickname/avatar replacement
- - PC Alipay QR
- - mobile Alipay jump
- - PC WeChat QR
- - WeChat H5 MP/JSAPI path
- - WeChat in-app OAuth resume path
- - non-WeChat H5 fallback path
-- [ ] Commit final checkpoint:
- ```bash
- git add docs backend frontend
- git commit -m "feat: rebuild auth identity and payment foundation"
- ```
-
-## Review Checklist
-
-- [ ] No flow still relies on provider email equality for account linking.
-- [ ] No flow still creates third-party users through plain email registration helpers.
-- [ ] No callback still returns first-party bearer tokens in URL fragments.
-- [ ] No callback completion path can be replayed as a bearer token substitute.
-- [ ] No payment result view trusts provider return page as authoritative fulfillment.
-- [ ] No webhook verification path selects provider credentials from “currently active config” instead of the order snapshot.
-- [ ] Existing email users, historical LinuxDo/WeChat/OIDC users, and `openid`-only WeChat remediation cases are covered by migration tests.
-- [ ] Avatar adoption and deletion semantics are explicit and reversible.
-- [ ] Grant timing is source-aware and one-time only.
diff --git a/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md b/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md
deleted file mode 100644
index 23823cf0..00000000
--- a/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md
+++ /dev/null
@@ -1,763 +0,0 @@
-# Auth Identity And Payment Foundation Design
-
-**Date:** 2026-04-20
-
-**Status:** Draft approved in conversation, written for implementation planning
-
-**Goal**
-
-Rebuild the `feat/auth-identity-foundation` intent on a clean branch from `main`, covering unified user identity, third-party login and binding, profile adoption, source-based signup defaults, unified payment routing and UX, admin configuration, compatibility with existing `main` data, and an opt-in OpenAI advanced scheduling switch.
-
-## Scope
-
-This design includes:
-
-- Email login and registration
-- Third-party login and binding for `LinuxDo`, `OIDC`, and `WeChat`
-- Unified identity storage for email and third-party identities
-- Pending auth sessions for callback-to-login/register/bind continuation
-- User-controlled nickname/avatar adoption during first relevant third-party flow
-- Profile binding management and avatar upload/delete
-- Source-based initial grants for balance, concurrency, and subscriptions
-- User management support for `last_login_at` and `last_active_at` sorting
-- Unified payment display methods (`alipay`, `wxpay`) mapped to a single active backend source each
-- Alipay and WeChat UX routing rules across PC, mobile, H5, and WeChat environments
-- Admin settings for auth providers, source defaults, payment sources, and OpenAI advanced scheduling
-- Incremental migration and compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only identity records
-
-This design does not treat unrelated upstream merges, docs churn, or license changes from the old branch as required scope.
-
-## Product Rules
-
-### Auth and identity
-
-- Existing email users remain valid and continue to log in with no manual action.
-- Existing LinuxDo, OIDC, and WeChat users represented by historical third-party or synthetic-email data must remain recoverable during migration.
-- Third-party first login behavior:
- - Existing bound identity: direct login
- - Missing identity: start first-login flow
-- Browser-based third-party authorization-code login always uses PKCE `S256`; this is not an admin-toggleable feature.
-- If `force_email_on_third_party_signup` is disabled, a first-login user may create an account without binding an email.
-- If `force_email_on_third_party_signup` is enabled, the user must provide an email.
-- If the provided and verified email already exists:
- - show that the email already exists
- - allow "verify and bind existing account"
- - allow "change email and continue registration"
- - do not allow bypassing the email requirement
-- Upstream provider email verification is not trusted as a local bound email.
-- Matching upstream email must never auto-link to an existing local account.
-- Linking to an existing local account is allowed only when:
- - the user explicitly chooses that target account
- - the target account passes fresh local re-authentication
- - required TOTP verification succeeds
-- New third-party bind initiated from profile must start from an already logged-in local account and preserve explicit bind intent end-to-end.
-- `redirect_to` may only represent a normalized same-origin internal route. It must never contain a third-party URL and must never be derived from `Referer`.
-- OIDC validation rules:
- - canonical identity key is `issuer + sub`
- - discovery issuer and ID token `iss` must match exactly
- - `userinfo.sub` must match ID token `sub` when UserInfo is used
- - upstream `email_verified` may improve UX copy but does not satisfy local email-binding requirements
-- WeChat login chooses channel by environment:
- - in WeChat environment: `mp`
- - outside WeChat: `open`
-- WeChat primary identity key is `unionid`.
-- If a WeChat login/bind flow cannot produce `unionid`, the flow fails and no fallback `openid` identity is created.
-- Historical WeChat records that only contain `openid` are treated as migration-remediation cases, not as a valid long-term canonical identity model.
-- WeChat website login uses authorization code flow, random `state`, and the provider channel/app binding must be persisted alongside the resolved identity.
-
-### Profile adoption
-
-- During the first relevant third-party flow, the user can independently decide:
- - replace current nickname or not
- - replace current avatar or not
-- This applies to first third-party registration and first third-party binding.
-- The decision is explicit user choice, not automatic replacement.
-
-### Source-based initial grants
-
-- Source-specific defaults exist for `email`, `linuxdo`, `oidc`, and `wechat`.
-- Each source defines:
- - default balance
- - default concurrency
- - default subscriptions
- - grant on signup
- - grant on first bind
-- Default behavior:
- - grant on signup: enabled
- - grant on first bind: disabled
-- First-bind grants are optional and controlled per source.
-- Grants must be idempotent.
-
-### Avatar management
-
-- Avatar supports:
- - external URL
- - image `data:` URL
-- `data:` URL images are compressed to at most `100KB` before persistence.
-- Avatar storage is database-backed.
-- Avatar delete is supported.
-
-### Payment UX and routing
-
-- Frontend shows only two display methods:
- - `alipay`
- - `wxpay`
-- Users never choose between official providers and EasyPay explicitly.
-- Backend allows only one active source per display method at a time.
-- Alipay UX:
- - PC: show QR code in page
- - mobile: jump to Alipay app/payment flow
-- WeChat UX:
- - PC: show QR code in page
- - non-WeChat H5: prefer H5 pay; if unavailable, tell the user to open in WeChat
- - WeChat environment: prefer MP/JSAPI pay; if unavailable, fall back to H5 pay
-- Payment success is confirmed by backend order state, webhook, and/or query, not only frontend return.
-- Frontend-visible labels remain `支付宝` and `微信支付`, while internal visible-method identifiers remain `alipay` and `wxpay`.
-- Public result pages must not verify order state by exposing raw `out_trade_no`; they use authenticated lookup or a signed opaque result token instead.
-- Payment callback or return URLs must be fixed same-origin internal targets. They must not be inferred from `Referer`.
-- WeChat payment webhook handling must use a fixed HTTPS `notify_url` with no query parameters and must not depend on user login state.
-
-### OpenAI advanced scheduling
-
-- OpenAI advanced scheduling is supported.
-- It is disabled by default.
-- Admin can enable it explicitly.
-
-## Architecture
-
-Keep `users` as the account owner table and move login identities, channel mappings, pending auth state, callback completion state, and first-bind grant idempotency into dedicated tables and services. Keep email login working while progressively introducing unified identity reads and writes.
-
-Payment uses a similar split between user-visible display methods and backend provider sources. Frontend works only with stable display methods while backend resolves to the currently active source and capability matrix, and stores enough order-time snapshot data to survive later provider-config changes.
-
-Compatibility is a first-class concern: migrations are additive, reads are compatibility-aware, and rollout must tolerate existing `main` data and short-lived frontend/backend version skew.
-
-## Data Model
-
-### `users`
-
-Preserve existing account ownership and local-login fields. Extend or use:
-
-- `email`
-- `password_hash`
-- `totp_enabled`
-- `signup_source`
-- `last_login_at`
-- `last_active_at`
-
-The `users` table remains the primary business subject for balance, concurrency, subscriptions, permissions, and profile.
-
-### `auth_identities`
-
-Represents all canonical login or bindable identities.
-
-Fields:
-
-- `user_id`
-- `provider_type`: `email`, `linuxdo`, `oidc`, `wechat`
-- `provider_key`
-- `provider_subject`
-- `verified_at`
-- `issuer`
-- `metadata`
-- timestamps
-
-Uniqueness:
-
-- `provider_type + provider_key + provider_subject` must be unique
-
-Rules:
-
-- email identity uses canonicalized local email
-- LinuxDo uses stable provider subject under the configured provider namespace
-- OIDC uses stable issuer + subject, with issuer namespace represented consistently through `provider_key` and `issuer`
-- WeChat uses `unionid` as canonical subject under the configured Open Platform namespace
-
-### `auth_identity_channels`
-
-Stores channel-specific subject mappings for an identity.
-
-Primary use:
-
-- WeChat `open` / `mp` / payment channel mapping
-
-Fields:
-
-- `identity_id`
-- `provider_type`
-- `provider_key`
-- `channel`
-- `channel_app_id`
-- `channel_subject`
-- `metadata`
-- timestamps
-
-Rules:
-
-- canonical WeChat identity still keys on `unionid`
-- `openid` values live here as channel mappings
-
-### `pending_auth_sessions`
-
-Stores callback state between third-party callback and final account action.
-
-Fields:
-
-- `intent`
-- `provider_type`
-- `provider_key`
-- `provider_subject`
-- `target_user_id`
-- `redirect_to`
-- `resolved_email`
-- `registration_password_hash`
-- `upstream_identity_claims`
-- `local_flow_state`
-- `browser_session_key`
-- `completion_code_hash`
-- `completion_code_expires_at`
-- `email_verified_at`
-- `password_verified_at`
-- `totp_verified_at`
-- `expires_at`
-- `consumed_at`
-- timestamps
-
-Responsibilities:
-
-- continue provider callback into register/login/bind flows
-- persist nickname/avatar suggestions
-- persist explicit adoption decisions
-- survive navigation between auth pages
-- support mixed-version rollout through short-lived legacy token aliases when required
-
-Security rules:
-
-- callback completion uses backend session completion or a one-time exchange code
-- exchange codes are short-lived, one-time, bound to browser session and pending session, and redeemed via `POST`
-- exchange codes must not behave as bearer tokens and must not be logged, stored in URL fragments, or reused after redemption
-- `local_flow_state` stores mutable local progression only; immutable upstream claims remain in `upstream_identity_claims`
-
-### `identity_adoption_decisions`
-
-Persists user adoption preference collected during a pending-auth flow and resolved onto the bound identity.
-
-Fields:
-
-- `pending_auth_session_id`
-- `identity_id`
-- `adopt_display_name`
-- `adopt_avatar`
-- `decided_at`
-- timestamps
-
-Rules:
-
-- one adoption-decision row exists per pending session
-- `identity_id` is filled once final account creation or bind succeeds
-
-### `user_avatars`
-
-Stores the currently effective custom avatar.
-
-Fields:
-
-- `user_id`
-- `storage_provider`
-- `storage_key`
-- `url`
-- `content_type`
-- `byte_size`
-- `sha256`
-- timestamps
-
-Rules:
-
-- supports URL-backed and inline data-backed representations
-- hard maximum payload size is `100KB`
-
-### `user_provider_default_grants`
-
-Stores idempotency state for source grants.
-
-Fields:
-
-- `user_id`
-- `provider_type`
-- `granted_at`
-- timestamps
-
-Responsibilities:
-
-- prevent duplicate first-bind grants
-- allow signup grants and first-bind grants to be reasoned about independently
-
-## Identity Keys And Canonicalization
-
-- Email canonical key: `lower(trim(email))`
-- LinuxDo canonical key: provider subject from LinuxDo
-- OIDC canonical key: `issuer + sub`
-- WeChat canonical key: `unionid`
-
-WeChat-specific rule:
-
-- `openid` never becomes the primary stored identity key
-- if only `openid` is available, login/bind fails with a configuration/identity error
-- historical `openid`-only records must be reported and either remediated during migration or explicitly blocked from silent auto-upgrade
-
-## Core Flows
-
-### Email register/login
-
-- Existing email auth flow remains
-- On email registration, create canonical `email` identity
-- Apply `email` source signup defaults
-
-### Third-party login with existing identity
-
-- Resolve canonical identity
-- Login mapped `user`
-- Update `last_login_at`
-- Do not issue signup or first-bind grants again
-
-### Third-party first login with no identity
-
-- Create `pending_auth_session`
-- Frontend callback flow decides next action
-- Pending session creation stores immutable upstream claims separately from mutable local progress fields
-
-Branches:
-
-- no forced email binding:
- - user can create account directly
-- forced email binding:
- - user must supply local email
-
-If supplied local email already exists:
-
-- tell the user the email already exists
-- allow verify-and-bind-existing-account
-- allow changing email to continue registration
-
-On new account creation:
-
-- create `users` row
-- create canonical third-party identity
-- create or update canonical email identity when local email binding succeeds
-- apply source signup grants
-- apply adoption choices if selected
-
-### Bind third-party identity to current logged-in user
-
-- current user starts bind flow
-- callback resolves to `bind_current_user`
-- bind intent is tied to the initiating local user session and cannot be re-targeted by email match
-- bind canonical identity to current user
-- if configured and first bind for that provider, apply first-bind grants
-- present nickname/avatar replacement choice
-
-### Bind existing account during first-login flow
-
-- user explicitly selects bind-existing-account
-- verify password for existing account
-- if account requires TOTP, verify TOTP
-- bind canonical identity to target account
-- optionally apply first-bind grants
-- present nickname/avatar replacement choice
-- no automatic profile or metadata merge occurs beyond explicitly selected nickname/avatar replacement
-
-### Callback completion and exchange flow
-
-- third-party callback never returns first-party bearer tokens in URL fragments
-- callback completion uses either:
- - backend session completion tied to the initiating browser session
- - one-time opaque exchange code redeemed by `POST`
-- mixed-version rollout may temporarily emit legacy pending token aliases in addition to the new completion path
-- legacy alias support is transitional and bounded to rollout windows only
-
-### WeChat login and channel mapping
-
-- environment chooses `mp` or `open`
-- website login uses authorization-code flow with provider-configured app/channel binding
-- callback must resolve to `unionid`
-- channel `openid` is optionally recorded in `auth_identity_channels`
-- failure to obtain `unionid` aborts flow
-
-### Avatar upload and delete
-
-- URL avatar: validate and persist reference
-- data URL avatar:
- - decode
- - validate image type
- - compress to `<=100KB`
- - persist database-backed inline representation
-- delete removes current custom avatar entry
-
-## Payment Routing Model
-
-### User-visible methods
-
-- `alipay`
-- `wxpay`
-
-### Backend source abstraction
-
-Each display method maps to exactly one active configured backend source:
-
-- `official_alipay`
-- `easypay_alipay`
-- `official_wechat`
-- `easypay_wechat`
-
-Frontend submits display method only. Backend resolves display method to active source and capability set.
-
-### Legacy payment-config normalization
-
-- existing provider-instance `supported_types`, legacy aliases such as `wxpay_direct`, and per-type limit structures are migrated into the visible-method model
-- migration preserves historical payment capability and refund semantics
-- the system keeps one normalized visible-method mapping per provider instance for rollout and audit
-
-### Alipay routing
-
-- PC: create QR-oriented result and show QR in page
-- mobile: create jump/redirect-oriented result
-
-### WeChat routing
-
-- PC: QR result
-- non-WeChat H5:
- - prefer H5 pay
- - if unavailable, show "open in WeChat" requirement
-- WeChat environment:
- - prefer MP/JSAPI
- - if unavailable, fall back to H5 pay
-
-### WeChat payment OAuth recovery
-
-- if WeChat in-app payment requires `openid` and the current request does not already hold it, backend returns an `oauth_required` response instead of guessing
-- backend creates a server-backed payment-resume context containing:
- - target visible method
- - amount/order type/plan context
- - redirect target
- - anti-replay state
-- backend redirects through a dedicated WeChat payment OAuth start endpoint
-- callback exchanges the provider code server-side, stores `openid` in the payment-resume context, and returns a same-origin internal resume target
-- frontend resumes the original order flow through the resume context instead of trusting raw callback query state or long-lived local storage
-
-### Payment completion
-
-- frontend return restores context and UI state
-- backend order state remains source of truth
-- webhook and/or order query remain authoritative for fulfillment
-- order fulfillment validates webhook or query payload against order-time snapshot data including provider instance, merchant identifiers, amount, currency, and provider order references
-- result pages use authenticated lookup or signed opaque result tokens, never raw public `out_trade_no`
-
-## Admin Configuration Model
-
-### Auth provider settings
-
-- email registration and verification settings
-- force email on third-party signup
-- LinuxDo client settings
-- OIDC issuer/client settings and provider display name
-- WeChat `open` / `mp` capability indicators derived from environment-backed configuration, surfaced to the frontend/admin read models as effective availability rather than full in-panel credential editing
-
-### Source default settings
-
-Per source (`email`, `linuxdo`, `oidc`, `wechat`):
-
-- default balance
-- default concurrency
-- default subscriptions
-- grant on signup
-- grant on first bind
-
-### Payment settings
-
-- active source for `alipay`
-- active source for `wechat`
-- source-specific credentials and enablement
-- effective WeChat payment capabilities may differ by enabled provider instances and selected visible-method source:
- - QR available
- - H5 available
- - MP/JSAPI available
-
-### Scheduling settings
-
-- OpenAI advanced scheduling enabled/disabled
-- default disabled
-
-## Compatibility And Rollout
-
-Compatibility is mandatory, especially for:
-
-- existing email users
-- existing LinuxDo users
-- historical LinuxDo synthetic-email accounts
-- historical WeChat synthetic-email accounts
-- historical OIDC synthetic-email accounts
-- historical WeChat `openid`-only records created by older branches
-
-### Additive migrations
-
-- preserve existing `users` data and behavior
-- add identity and pending-session tables
-- avoid destructive schema swaps
-
-### Migration backfill
-
-- backfill canonical `email` identities for valid existing email users
-- backfill canonical `linuxdo` identities during migration for historical synthetic-email LinuxDo users
-- backfill canonical `wechat` and `oidc` identities when historical synthetic-email or `user_external_identities` data allows deterministic reconstruction
-- emit migration reports for historical WeChat `openid`-only records that cannot be safely promoted to canonical `unionid`
-- backfill must be idempotent and repeatable
-
-### Compatibility reads
-
-During rollout:
-
-- read new identity model first
-- where necessary, retain compatibility logic for existing email and historical LinuxDo/WeChat/OIDC synthetic-email recognition
-
-### Grant idempotency
-
-- migration backfill must not trigger signup or first-bind grants
-- first-bind grants must use explicit idempotency tracking
-
-### API compatibility
-
-Retain transitional support for legacy/new request and response shapes where needed, including:
-
-- `pending_auth_token`
-- `pending_oauth_token`
-- old callback parsing expectations
-- historical profile field mappings
-- legacy callback fragment readers during the bounded rollout window
-
-### Settings and payment compatibility
-
-- preserve existing payment configs and order semantics from `main`
-- add new settings incrementally
-- avoid rewriting the entire settings schema in one cutover
-- preserve legacy provider-instance capabilities by explicitly mapping historical `supported_types`, `payment_mode`, and limit config into normalized visible-method routing
-
-### Rolling upgrade tolerance
-
-- do not assume simultaneous frontend/backend deployment
-- new backend must tolerate short-lived older frontend request shapes
-- rollout must define the deployment order and removal point for legacy callback token parsing and legacy payment resume parsing
-
-## Testing Strategy
-
-### Repository tests
-
-- identity upsert and lookup
-- WeChat channel mapping
-- pending auth session persistence
-- source grant idempotency
-- avatar persistence and delete
-- migration backfill behavior
-
-### Service tests
-
-- direct login by existing identity
-- first third-party signup
-- forced email flow
-- existing-email bind-existing-account flow
-- first-bind grant on/off
-- nickname/avatar adoption choices
-- WeChat `unionid` required behavior
-- payment routing resolution
-
-### Handler and route tests
-
-- LinuxDo/OIDC/WeChat callback handling
-- bind-existing
-- bind-current-user
-- create-account
-- TOTP continuation
-- payment create and recovery
-
-### Frontend tests
-
-- third-party callback flow state machine
-- register/login continuation
-- profile bindings card
-- avatar interactions
-- payment page routing behavior
-- admin settings UI
-
-### Compatibility tests
-
-- existing email users
-- historical LinuxDo synthetic-email users
-- historical WeChat synthetic-email users
-- historical OIDC synthetic-email users
-- historical WeChat `openid`-only records reported or remediated correctly
-- historical payment config
-- legacy auth payload field names
-- historical payment result handling
-- mixed-version callback token bridge behavior
-
-## Implementation Phases
-
-1. Add schema, migrations, compatibility backfill, and repository support
-2. Implement unified identity services and pending auth session flows
-3. Integrate profile binding, avatar, and adoption decision flows
-4. Add per-source default grants and admin config surfaces
-5. Rebuild payment routing abstraction and frontend payment UX
-6. Add user-management sorting and OpenAI advanced scheduling switch
-7. Run compatibility, rollout, and regression hardening
-
-## External Constraints And Best Practices
-
-Implementation must follow current primary-source guidance:
-
-- OAuth 2.0 Security BCP (RFC 9700): strict redirect handling, state protection, mix-up resistant design
-- PKCE (RFC 7636): require `S256` on browser authorization-code flows
-- OpenID Connect Core: stable issuer/subject handling for OIDC identities
-- Account linking best practice: require explicit user confirmation or re-authentication before linking to existing accounts
-- WeChat UnionID and website-login guidance: treat `unionid` as canonical cross-channel subject and persist channel/app binding with website login responses
-- WeChat Pay webhook guidance: verify signatures, decrypt payloads, and confirm merchant/order/amount fields against order-time state before fulfillment
-- Payment success-page guidance: custom success pages are informational and must not be the only fulfillment trigger
-
-References:
-
-- RFC 9700:
-- RFC 7636:
-- OpenID Connect Core 1.0:
-- Auth0 account linking guidance:
-- WeChat UnionID guidance:
-- WeChat website login guidance:
-- WeChat Pay callback/signature guidance:
-- Stripe Checkout fulfillment guidance:
-
-## Audit Synthesis
-
-The clean rebuild direction is not to copy either existing branch directly.
-
-- `feat/auth-identity-foundation` has the better long-term model:
- - unified auth identities
- - pending auth sessions
- - identity adoption decisions
- - provider-scoped default grants
- - payment display-method abstraction
- - OpenAI advanced scheduler layering
-- `personal-dev-branch` has the better real-world closure:
- - LinuxDo and WeChat callback flows are more operationally complete
- - profile binding and avatar UX is more complete
- - historical synthetic-email users across multiple providers are recognized and recovered in live flows
- - WeChat payment OAuth and recovery behavior is more complete
-- Primary-source guidance supplies hard constraints for OAuth/OIDC, account linking, WeChat identity handling, and payment completion semantics.
-
-The final rebuild must therefore:
-
-- keep the `feat/auth-identity-foundation` data model direction
-- absorb the strongest business-flow behavior from `personal-dev-branch`
-- reject transitional or half-finished behavior from both branches
-- treat compatibility and rollout as first-class implementation scope
-
-## Keep / Adapt / Drop
-
-### Keep
-
-Keep these architectural choices essentially intact:
-
-- `auth_identities`, `auth_identity_channels`, `pending_auth_sessions`, `identity_adoption_decisions`
-- per-provider default grants with one-time grant tracking
-- WeChat canonical identity plus channel mapping model
-- pending-auth verification gates before final bind
-- payment visible-method abstraction (`alipay`, `wechat`) decoupled from backend provider source
-- OpenAI advanced scheduler layering and test-backed behavior
-
-Keep these operational flow ideas from `personal-dev-branch`:
-
-- LinuxDo pending identity callback flow
-- WeChat pending identity callback flow
-- profile bindings UX and “cannot disconnect last usable login method” rule
-- separate WeChat login OAuth and WeChat payment OAuth entry points
-- historical synthetic-email recognition logic as a migration bridge
-- explicit WeChat payment OAuth recovery protocol as a product requirement, but reimplemented with server-backed resume state
-
-### Adapt
-
-These areas must be reimplemented with the same intent but stricter boundaries:
-
-- third-party account creation from pending-auth state must be transactional and must not register a plain local user before identity finalization succeeds
-- email identity lifecycle must become real dual-write state, not just one migration-time backfill
-- `signup_source` must be backfilled more accurately for known historical third-party users
-- WeChat payment recovery state must move from frontend-only storage to server-backed continuation state
-- avatar adoption fetches must be security-hardened and failure-visible
-- pending-auth payload modeling must clearly separate immutable upstream payload from mutable local metadata
-- callback completion must use a real exchange/session model instead of fragment-delivered bearer tokens
-- profile binding/avatar DTOs must be simplified to one authoritative backend contract instead of sprawling frontend fallback parsing
-- admin settings should preserve capability while reducing duplicated or transitional config branches
-
-### Drop
-
-Drop these as long-term design choices:
-
-- `user_external_identities` as the primary long-term identity model
-- synthetic email as a long-term canonical identity representation
-- OIDC as a side-path that does not participate in the same identity foundation as LinuxDo and WeChat
-- frontend multi-endpoint probing and broad compatibility parsing once the clean branch becomes the sole supported contract
-- unrelated branch noise such as generated-file churn, locale-only churn, or upstream merge residue as design inputs
-
-## Audit-Driven Hard Constraints
-
-The audit and source review establish these hard constraints:
-
-### Auth
-
-- all browser authorization-code providers use PKCE `S256` and do not expose an admin-off switch
-- callback handling uses strict `redirect_uri` discipline and state validation
-- OIDC identity key is `issuer + sub`
-- existing-account linking after email conflict must require explicit user action plus local-account verification
-- WeChat canonical identity key is `unionid`; `openid` is channel-scoped only
-
-### Compatibility
-
-- existing email users must continue to work with no manual intervention
-- existing LinuxDo users must not split into duplicate accounts
-- historical LinuxDo/WeChat/OIDC synthetic-email users must be backfilled into canonical identities during migration when deterministic recovery is possible
-- historical WeChat `openid`-only records must be surfaced through migration reporting and explicit remediation rules
-- migration backfills must not trigger signup or first-bind grants
-- legacy `pending_auth_token` and `pending_oauth_token` contracts must remain accepted during rollout
-- legacy auth/public setting aliases needed by older frontend builds must remain available during rollout
-- existing payment configs and historical order semantics must remain valid
-
-### Payment
-
-- frontend return pages do not determine final payment success
-- backend order state, webhook processing, and/or provider status query remain authoritative
-- each visible method (`alipay`, `wxpay`) may have only one active backend source at a time
-- public result pages must not expose raw `out_trade_no` lookup
-- WeChat Pay callback handling must verify signature, decrypt payload, and compare order fields against order-time snapshot data
-
-## Known Risks To Eliminate In Implementation
-
-These are specifically observed problems in the existing branches that the clean rebuild must eliminate:
-
-- third-party forced-email account creation currently bypasses the provider-aware account creation path and can leave orphan local accounts if bind finalization fails
-- post-migration email accounts are not fully dual-written into `auth_identities`
-- avatar adoption currently risks silent failure and insecure outbound fetch behavior
-- pending-auth payload responsibilities are internally inconsistent
-- OIDC parity is incomplete in `personal-dev-branch`; it must become a first-class provider in the unified identity model
-- WeChat union/open/channel identity handling is conceptually correct in the feature branch but still partially transitional across the codebase
-- WeChat payment recovery in `personal-dev-branch` is frontend-local and not robust across tabs or concurrent attempts
-- the existing pending-auth migration update is too destructive to reuse unchanged in a safer rollout
-- historical provider provenance should not be permanently flattened to `signup_source = email`
-- design/plan drift can reintroduce ambiguous identity uniqueness or ambiguous adoption-decision ownership if not aligned before implementation
-
-## Rollout Gates
-
-The rebuild is not ready for rollout until all of these are satisfied:
-
-1. Identity schema and migration chain are linearized and production-safe.
-2. Email identity backfill is complete and idempotent.
-3. Historical LinuxDo/WeChat/OIDC synthetic-email backfill to canonical identity is complete where deterministic, and non-recoverable rows are reported.
-4. Historical WeChat `openid`-only rows are either remediated or explicitly blocked with operator-visible reporting.
-5. `signup_source` backfill is accurate for known historical provider-created users.
-6. Dual token acceptance, exchange bridge behavior, and required legacy field aliases are present for the bounded rollout window.
-7. Existing payment configs are normalized and verified against current frontend-visible capabilities.
-8. New frontend flows are verified against mixed-version backend compatibility windows.
-9. Duplicate-account creation, first-bind grants, and payment route selection have regression coverage.
From ed01c599161acc67a18ef19c73daeb9fbe1243ab Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 14:54:53 +0800
Subject: [PATCH 134/326] feat: track authenticated user activity
---
.../server/middleware/admin_auth_test.go | 29 ++++++++
.../internal/server/middleware/jwt_auth.go | 16 ++++-
.../server/middleware/jwt_auth_test.go | 59 ++++++++++++++++
.../service/admin_service_apikey_test.go | 3 +
.../service/admin_service_delete_test.go | 12 ++++
.../admin_service_email_identity_sync_test.go | 4 ++
backend/internal/service/user_service.go | 68 +++++++++++++++++++
backend/internal/service/user_service_test.go | 65 ++++++++++++++----
frontend/src/views/admin/UsersView.vue | 17 ++---
.../views/admin/__tests__/UsersView.spec.ts | 7 +-
10 files changed, 254 insertions(+), 26 deletions(-)
diff --git a/backend/internal/server/middleware/admin_auth_test.go b/backend/internal/server/middleware/admin_auth_test.go
index ed2578c8..cc5bead3 100644
--- a/backend/internal/server/middleware/admin_auth_test.go
+++ b/backend/internal/server/middleware/admin_auth_test.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
+ "time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
@@ -153,6 +154,18 @@ func (s *stubUserRepo) Delete(ctx context.Context, id int64) error {
panic("unexpected Delete call")
}
+func (s *stubUserRepo) GetUserAvatar(ctx context.Context, userID int64) (*service.UserAvatar, error) {
+ return nil, nil
+}
+
+func (s *stubUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
+ panic("unexpected UpsertUserAvatar call")
+}
+
+func (s *stubUserRepo) DeleteUserAvatar(ctx context.Context, userID int64) error {
+ panic("unexpected DeleteUserAvatar call")
+}
+
func (s *stubUserRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
panic("unexpected List call")
}
@@ -161,6 +174,18 @@ func (s *stubUserRepo) ListWithFilters(ctx context.Context, params pagination.Pa
panic("unexpected ListWithFilters call")
}
+func (s *stubUserRepo) GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error) {
+ panic("unexpected GetLatestUsedAtByUserIDs call")
+}
+
+func (s *stubUserRepo) GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error) {
+ panic("unexpected GetLatestUsedAtByUserID call")
+}
+
+func (s *stubUserRepo) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
+ panic("unexpected UpdateUserLastActiveAt call")
+}
+
func (s *stubUserRepo) UpdateBalance(ctx context.Context, id int64, amount float64) error {
panic("unexpected UpdateBalance call")
}
@@ -189,6 +214,10 @@ func (s *stubUserRepo) AddGroupToAllowedGroups(ctx context.Context, userID int64
panic("unexpected AddGroupToAllowedGroups call")
}
+func (s *stubUserRepo) ListUserAuthIdentities(ctx context.Context, userID int64) ([]service.UserAuthIdentityRecord, error) {
+ panic("unexpected ListUserAuthIdentities call")
+}
+
func (s *stubUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
panic("unexpected UpdateTotpSecret call")
}
diff --git a/backend/internal/server/middleware/jwt_auth.go b/backend/internal/server/middleware/jwt_auth.go
index 4aceb355..48cb9004 100644
--- a/backend/internal/server/middleware/jwt_auth.go
+++ b/backend/internal/server/middleware/jwt_auth.go
@@ -1,6 +1,7 @@
package middleware
import (
+ "context"
"errors"
"strings"
@@ -11,11 +12,19 @@ import (
// NewJWTAuthMiddleware 创建 JWT 认证中间件
func NewJWTAuthMiddleware(authService *service.AuthService, userService *service.UserService) JWTAuthMiddleware {
- return JWTAuthMiddleware(jwtAuth(authService, userService))
+ return JWTAuthMiddleware(jwtAuth(authService, userService, userService))
+}
+
+type jwtUserReader interface {
+ GetByID(ctx context.Context, id int64) (*service.User, error)
+}
+
+type userActivityToucher interface {
+ TouchLastActiveForUser(ctx context.Context, user *service.User)
}
// jwtAuth JWT认证中间件实现
-func jwtAuth(authService *service.AuthService, userService *service.UserService) gin.HandlerFunc {
+func jwtAuth(authService *service.AuthService, userService jwtUserReader, activityToucher userActivityToucher) gin.HandlerFunc {
return func(c *gin.Context) {
// 从Authorization header中提取token
authHeader := c.GetHeader("Authorization")
@@ -73,6 +82,9 @@ func jwtAuth(authService *service.AuthService, userService *service.UserService)
Concurrency: user.Concurrency,
})
c.Set(string(ContextKeyUserRole), user.Role)
+ if activityToucher != nil {
+ activityToucher.TouchLastActiveForUser(c.Request.Context(), user)
+ }
c.Next()
}
diff --git a/backend/internal/server/middleware/jwt_auth_test.go b/backend/internal/server/middleware/jwt_auth_test.go
index c483a51e..84fd6967 100644
--- a/backend/internal/server/middleware/jwt_auth_test.go
+++ b/backend/internal/server/middleware/jwt_auth_test.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
+ "time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/service"
@@ -30,6 +31,25 @@ func (r *stubJWTUserRepo) GetByID(_ context.Context, id int64) (*service.User, e
return u, nil
}
+func (r *stubJWTUserRepo) GetUserAvatar(_ context.Context, _ int64) (*service.UserAvatar, error) {
+ return nil, nil
+}
+
+func (r *stubJWTUserRepo) UpdateUserLastActiveAt(_ context.Context, _ int64, _ time.Time) error {
+ return nil
+}
+
+type recordingActivityToucher struct {
+ userIDs []int64
+}
+
+func (r *recordingActivityToucher) TouchLastActiveForUser(_ context.Context, user *service.User) {
+ if user == nil {
+ return
+ }
+ r.userIDs = append(r.userIDs, user.ID)
+}
+
// newJWTTestEnv 创建 JWT 认证中间件测试环境。
// 返回 gin.Engine(已注册 JWT 中间件)和 AuthService(用于生成 Token)。
func newJWTTestEnv(users map[int64]*service.User) (*gin.Engine, *service.AuthService) {
@@ -106,6 +126,45 @@ func TestJWTAuth_ValidToken_LowercaseBearer(t *testing.T) {
require.Equal(t, http.StatusOK, w.Code)
}
+func TestJWTAuth_ValidToken_TouchesLastActive(t *testing.T) {
+ user := &service.User{
+ ID: 1,
+ Email: "test@example.com",
+ Role: "user",
+ Status: service.StatusActive,
+ Concurrency: 5,
+ TokenVersion: 1,
+ }
+
+ gin.SetMode(gin.TestMode)
+
+ cfg := &config.Config{}
+ cfg.JWT.Secret = "test-jwt-secret-32bytes-long!!!"
+ cfg.JWT.AccessTokenExpireMinutes = 60
+
+ userRepo := &stubJWTUserRepo{users: map[int64]*service.User{1: user}}
+ authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil)
+ userSvc := service.NewUserService(userRepo, nil, nil, nil)
+ toucher := &recordingActivityToucher{}
+
+ r := gin.New()
+ r.Use(jwtAuth(authSvc, userSvc, toucher))
+ r.GET("/protected", func(c *gin.Context) {
+ c.Status(http.StatusOK)
+ })
+
+ token, err := authSvc.GenerateToken(user)
+ require.NoError(t, err)
+
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/protected", nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ r.ServeHTTP(w, req)
+
+ require.Equal(t, http.StatusOK, w.Code)
+ require.Equal(t, []int64{1}, toucher.userIDs)
+}
+
func TestJWTAuth_MissingAuthorizationHeader(t *testing.T) {
router, _ := newJWTTestEnv(nil)
diff --git a/backend/internal/service/admin_service_apikey_test.go b/backend/internal/service/admin_service_apikey_test.go
index e2eae0b4..aab35d25 100644
--- a/backend/internal/service/admin_service_apikey_test.go
+++ b/backend/internal/service/admin_service_apikey_test.go
@@ -88,6 +88,9 @@ func (s *userRepoStubForGroupUpdate) GetLatestUsedAtByUserIDs(context.Context, [
func (s *userRepoStubForGroupUpdate) GetLatestUsedAtByUserID(context.Context, int64) (*time.Time, error) {
panic("unexpected")
}
+func (s *userRepoStubForGroupUpdate) UpdateUserLastActiveAt(context.Context, int64, time.Time) error {
+ panic("unexpected")
+}
func (s *userRepoStubForGroupUpdate) RemoveGroupFromUserAllowedGroups(context.Context, int64, int64) error {
panic("unexpected")
}
diff --git a/backend/internal/service/admin_service_delete_test.go b/backend/internal/service/admin_service_delete_test.go
index ac1d8ee7..126faad9 100644
--- a/backend/internal/service/admin_service_delete_test.go
+++ b/backend/internal/service/admin_service_delete_test.go
@@ -107,6 +107,18 @@ func (s *userRepoStub) ListWithFilters(ctx context.Context, params pagination.Pa
panic("unexpected ListWithFilters call")
}
+func (s *userRepoStub) GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error) {
+ panic("unexpected GetLatestUsedAtByUserIDs call")
+}
+
+func (s *userRepoStub) GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error) {
+ panic("unexpected GetLatestUsedAtByUserID call")
+}
+
+func (s *userRepoStub) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
+ panic("unexpected UpdateUserLastActiveAt call")
+}
+
func (s *userRepoStub) UpdateBalance(ctx context.Context, id int64, amount float64) error {
panic("unexpected UpdateBalance call")
}
diff --git a/backend/internal/service/admin_service_email_identity_sync_test.go b/backend/internal/service/admin_service_email_identity_sync_test.go
index d6a7af9a..eaf4e84b 100644
--- a/backend/internal/service/admin_service_email_identity_sync_test.go
+++ b/backend/internal/service/admin_service_email_identity_sync_test.go
@@ -97,6 +97,10 @@ func (s *emailSyncRepoStub) GetLatestUsedAtByUserID(context.Context, int64) (*ti
return nil, nil
}
+func (s *emailSyncRepoStub) UpdateUserLastActiveAt(context.Context, int64, time.Time) error {
+ return nil
+}
+
func (s *emailSyncRepoStub) UpdateBalance(context.Context, int64, float64) error { return nil }
func (s *emailSyncRepoStub) DeductBalance(context.Context, int64, float64) error { return nil }
diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go
index e6053984..c6bf14c2 100644
--- a/backend/internal/service/user_service.go
+++ b/backend/internal/service/user_service.go
@@ -19,10 +19,13 @@ import (
"log/slog"
"net/url"
"sort"
+ "strconv"
"strings"
+ "sync"
"time"
xdraw "golang.org/x/image/draw"
+ "golang.org/x/sync/singleflight"
)
var (
@@ -47,6 +50,8 @@ const (
notifyCodeUserRateWindow = 10 * time.Minute
defaultUserIdentityRedirect = "/settings/profile"
+ userLastActiveMinTouch = 10 * time.Minute
+ userLastActiveFailBackoff = 30 * time.Second
)
var (
@@ -82,6 +87,7 @@ type UserRepository interface {
ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UserListFilters) ([]User, *pagination.PaginationResult, error)
GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error)
GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error)
+ UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error
UpdateBalance(ctx context.Context, id int64, amount float64) error
DeductBalance(ctx context.Context, id int64, amount float64) error
@@ -192,6 +198,8 @@ type UserService struct {
settingRepo SettingRepository
authCacheInvalidator APIKeyAuthCacheInvalidator
billingCache BillingCache
+ lastActiveTouchL1 sync.Map
+ lastActiveTouchSF singleflight.Group
}
// NewUserService 创建用户服务实例
@@ -788,6 +796,66 @@ func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) {
return user, nil
}
+// TouchLastActive 通过防抖更新 users.last_active_at,减少鉴权热路径写放大。
+// 该操作为尽力而为,不应中断正常请求。
+func (s *UserService) TouchLastActive(ctx context.Context, userID int64) {
+ if s == nil || s.userRepo == nil || userID <= 0 {
+ return
+ }
+
+ user, err := s.userRepo.GetByID(ctx, userID)
+ if err != nil {
+ slog.Debug("skip touch user last active after load failure", "user_id", userID, "error", err)
+ return
+ }
+ s.TouchLastActiveForUser(ctx, user)
+}
+
+// TouchLastActiveForUser 使用已加载的用户信息更新 last_active_at,避免重复读取数据库。
+func (s *UserService) TouchLastActiveForUser(ctx context.Context, user *User) {
+ if s == nil || s.userRepo == nil || user == nil || user.ID <= 0 {
+ return
+ }
+
+ now := time.Now()
+ if userLastActiveFresh(user.LastActiveAt, now) {
+ return
+ }
+ if v, ok := s.lastActiveTouchL1.Load(user.ID); ok {
+ if nextAllowedAt, ok := v.(time.Time); ok && now.Before(nextAllowedAt) {
+ return
+ }
+ }
+
+ _, err, _ := s.lastActiveTouchSF.Do(strconv.FormatInt(user.ID, 10), func() (any, error) {
+ latest := time.Now()
+ if v, ok := s.lastActiveTouchL1.Load(user.ID); ok {
+ if nextAllowedAt, ok := v.(time.Time); ok && latest.Before(nextAllowedAt) {
+ return nil, nil
+ }
+ }
+ if userLastActiveFresh(user.LastActiveAt, latest) {
+ return nil, nil
+ }
+ if err := s.userRepo.UpdateUserLastActiveAt(ctx, user.ID, latest); err != nil {
+ s.lastActiveTouchL1.Store(user.ID, latest.Add(userLastActiveFailBackoff))
+ return nil, fmt.Errorf("touch user last active: %w", err)
+ }
+ s.lastActiveTouchL1.Store(user.ID, latest.Add(userLastActiveMinTouch))
+ return nil, nil
+ })
+ if err != nil {
+ slog.Warn("touch user last active failed", "user_id", user.ID, "error", err)
+ }
+}
+
+func userLastActiveFresh(lastActiveAt *time.Time, now time.Time) bool {
+ if lastActiveAt == nil {
+ return false
+ }
+ return now.Before(lastActiveAt.Add(userLastActiveMinTouch))
+}
+
func (s *UserService) hydrateUserAvatar(ctx context.Context, user *User) error {
if s == nil || s.userRepo == nil || user == nil || user.ID == 0 {
return nil
diff --git a/backend/internal/service/user_service_test.go b/backend/internal/service/user_service_test.go
index d771cb75..2c11f8ec 100644
--- a/backend/internal/service/user_service_test.go
+++ b/backend/internal/service/user_service_test.go
@@ -23,18 +23,21 @@ import (
// --- mock: UserRepository ---
type mockUserRepo struct {
- updateBalanceErr error
- updateBalanceFn func(ctx context.Context, id int64, amount float64) error
- getByIDUser *User
- getByIDErr error
- updateFn func(ctx context.Context, user *User) error
- updateCalls int
- upsertAvatarFn func(ctx context.Context, userID int64, input UpsertUserAvatarInput) (*UserAvatar, error)
- upsertAvatarArgs []UpsertUserAvatarInput
- deleteAvatarFn func(ctx context.Context, userID int64) error
- deleteAvatarIDs []int64
- getAvatarFn func(ctx context.Context, userID int64) (*UserAvatar, error)
- txCalls int
+ updateBalanceErr error
+ updateBalanceFn func(ctx context.Context, id int64, amount float64) error
+ getByIDUser *User
+ getByIDErr error
+ updateLastActiveErr error
+ updateLastActiveUserIDs []int64
+ updateLastActiveAt []time.Time
+ updateFn func(ctx context.Context, user *User) error
+ updateCalls int
+ upsertAvatarFn func(ctx context.Context, userID int64, input UpsertUserAvatarInput) (*UserAvatar, error)
+ upsertAvatarArgs []UpsertUserAvatarInput
+ deleteAvatarFn func(ctx context.Context, userID int64) error
+ deleteAvatarIDs []int64
+ getAvatarFn func(ctx context.Context, userID int64) (*UserAvatar, error)
+ txCalls int
}
type mockUserRepoTxKey struct{}
@@ -144,6 +147,11 @@ func (m *mockUserRepo) UpdateBalance(ctx context.Context, id int64, amount float
}
return m.updateBalanceErr
}
+func (m *mockUserRepo) UpdateUserLastActiveAt(_ context.Context, userID int64, activeAt time.Time) error {
+ m.updateLastActiveUserIDs = append(m.updateLastActiveUserIDs, userID)
+ m.updateLastActiveAt = append(m.updateLastActiveAt, activeAt)
+ return m.updateLastActiveErr
+}
func (m *mockUserRepo) DeductBalance(context.Context, int64, float64) error { return nil }
func (m *mockUserRepo) UpdateConcurrency(context.Context, int64, int) error { return nil }
func (m *mockUserRepo) ExistsByEmail(context.Context, string) (bool, error) { return false, nil }
@@ -288,6 +296,39 @@ func TestUpdateBalance_CacheFailure_DoesNotAffectReturn(t *testing.T) {
}, 2*time.Second, 10*time.Millisecond, "即使失败也应调用 InvalidateUserBalance")
}
+func TestTouchLastActive_UpdatesWhenStale(t *testing.T) {
+ stale := time.Now().Add(-11 * time.Minute)
+ repo := &mockUserRepo{
+ getByIDUser: &User{
+ ID: 42,
+ LastActiveAt: &stale,
+ },
+ }
+ svc := NewUserService(repo, nil, nil, nil)
+
+ svc.TouchLastActive(context.Background(), 42)
+
+ require.Equal(t, []int64{42}, repo.updateLastActiveUserIDs)
+ require.Len(t, repo.updateLastActiveAt, 1)
+ require.WithinDuration(t, time.Now(), repo.updateLastActiveAt[0], 2*time.Second)
+}
+
+func TestTouchLastActive_SkipsWhenRecent(t *testing.T) {
+ recent := time.Now().Add(-time.Minute)
+ repo := &mockUserRepo{
+ getByIDUser: &User{
+ ID: 42,
+ LastActiveAt: &recent,
+ },
+ }
+ svc := NewUserService(repo, nil, nil, nil)
+
+ svc.TouchLastActive(context.Background(), 42)
+
+ require.Empty(t, repo.updateLastActiveUserIDs)
+ require.Empty(t, repo.updateLastActiveAt)
+}
+
func TestUpdateBalance_RepoError_ReturnsError(t *testing.T) {
repo := &mockUserRepo{updateBalanceErr: errors.New("database error")}
cache := &mockBillingCache{}
diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue
index 07c9d437..93cfdbbe 100644
--- a/frontend/src/views/admin/UsersView.vue
+++ b/frontend/src/views/admin/UsersView.vue
@@ -455,12 +455,6 @@
{{ formatDateTime(value) }}
-
-
- {{ value ? formatDateTime(value) : '-' }}
-
-
-
{{ value ? formatDateTime(value) : '-' }}
@@ -718,7 +712,6 @@ const allColumns = computed(() => [
{ key: 'usage', label: t('admin.users.columns.usage'), sortable: false },
{ key: 'concurrency', label: t('admin.users.columns.concurrency'), sortable: true },
{ key: 'status', label: t('admin.users.columns.status'), sortable: true },
- { key: 'last_login_at', label: t('admin.users.columns.lastLogin'), sortable: true },
{ key: 'last_used_at', label: t('admin.users.columns.lastUsed'), sortable: true },
{ key: 'last_active_at', label: t('admin.users.columns.lastActive'), sortable: true },
{ key: 'created_at', label: t('admin.users.columns.created'), sortable: true },
@@ -735,7 +728,9 @@ const toggleableColumns = computed(() =>
const hiddenColumns = reactive>(new Set())
// Default hidden columns (columns hidden by default on first load)
-const DEFAULT_HIDDEN_COLUMNS = ['notes', 'groups', 'subscriptions', 'usage', 'concurrency', 'last_login_at', 'last_active_at']
+const DEFAULT_HIDDEN_COLUMNS = ['notes', 'groups', 'subscriptions', 'usage', 'concurrency']
+const REMOVED_COLUMNS = new Set(['last_login_at'])
+const FORCED_VISIBLE_COLUMNS = new Set(['last_active_at'])
// localStorage key for column settings
const HIDDEN_COLUMNS_KEY = 'user-hidden-columns'
@@ -746,7 +741,9 @@ const loadSavedColumns = () => {
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
if (saved) {
const parsed = JSON.parse(saved) as string[]
- parsed.forEach(key => hiddenColumns.add(key))
+ parsed
+ .filter(key => !REMOVED_COLUMNS.has(key) && !FORCED_VISIBLE_COLUMNS.has(key))
+ .forEach(key => hiddenColumns.add(key))
} else {
// Use default hidden columns on first load
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
@@ -808,7 +805,7 @@ const searchQuery = ref('')
const USER_SORT_STORAGE_KEY = 'admin-users-table-sort'
const loadInitialSortState = (): { sort_by: string; sort_order: 'asc' | 'desc' } => {
const fallback = { sort_by: 'created_at', sort_order: 'desc' as 'asc' | 'desc' }
- const sortable = new Set(['email', 'id', 'username', 'role', 'balance', 'concurrency', 'status', 'last_login_at', 'last_used_at', 'last_active_at', 'created_at'])
+ const sortable = new Set(['email', 'id', 'username', 'role', 'balance', 'concurrency', 'status', 'last_used_at', 'last_active_at', 'created_at'])
try {
const raw = localStorage.getItem(USER_SORT_STORAGE_KEY)
if (!raw) return fallback
diff --git a/frontend/src/views/admin/__tests__/UsersView.spec.ts b/frontend/src/views/admin/__tests__/UsersView.spec.ts
index 1ea67b63..d9076777 100644
--- a/frontend/src/views/admin/__tests__/UsersView.spec.ts
+++ b/frontend/src/views/admin/__tests__/UsersView.spec.ts
@@ -113,7 +113,7 @@ describe('admin UsersView', () => {
getBatchUserAttributes.mockResolvedValue({ values: {} })
})
- it('shows last_used_at column and requests last_used_at sort', async () => {
+ it('shows active and used activity columns, hides last_login_at, and requests last_used_at sort', async () => {
const wrapper = mount(UsersView, {
global: {
stubs: {
@@ -144,7 +144,10 @@ describe('admin UsersView', () => {
await flushPromises()
- expect(wrapper.get('[data-test="columns"]').text()).toContain('last_used_at')
+ const columns = wrapper.get('[data-test="columns"]').text()
+ expect(columns).toContain('last_used_at')
+ expect(columns).toContain('last_active_at')
+ expect(columns).not.toContain('last_login_at')
await wrapper.get('[data-test="sort-last-used"]').trigger('click')
await flushPromises()
From 49258dd3f6dfaab31589c797c033620c498a21d3 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Tue, 21 Apr 2026 14:55:07 +0800
Subject: [PATCH 135/326] fix: preserve scheduler transport compatibility
defaults
---
backend/internal/service/openai_account_scheduler.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go
index 38b92b47..5fda3abd 100644
--- a/backend/internal/service/openai_account_scheduler.go
+++ b/backend/internal/service/openai_account_scheduler.go
@@ -767,6 +767,9 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
}
func (s *defaultOpenAIAccountScheduler) isAccountTransportCompatible(account *Account, requiredTransport OpenAIUpstreamTransport) bool {
+ if requiredTransport == OpenAIUpstreamTransportAny || requiredTransport == OpenAIUpstreamTransportHTTPSSE {
+ return true
+ }
if s == nil || s.service == nil {
return false
}
From 78f691d2de24d0d13ce68922e120c8119ea32856 Mon Sep 17 00:00:00 2001
From: shaw
Date: Tue, 21 Apr 2026 12:13:45 +0800
Subject: [PATCH 136/326] chore: update sponsors
---
README.md | 5 +++++
README_CN.md | 5 +++++
README_JA.md | 5 +++++
assets/partners/logos/bestproxy.png | Bin 0 -> 9716 bytes
4 files changed, 15 insertions(+)
create mode 100644 assets/partners/logos/bestproxy.png
diff --git a/README.md b/README.md
index bee2e8c3..3e609d65 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,11 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through BmoPlus - Premium AI Accounts & Top-ups, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF)
+
+
+
Thanks to Bestproxy for sponsoring this project! Bestproxy provides high-purity residential IPs with dedicated one-IP-per-account support. By combining real home networks with fingerprint isolation, it enables link environment isolation and reduces the probability of association-based risk control.
+
+
## Ecosystem
diff --git a/README_CN.md b/README_CN.md
index 892eee61..add32a17 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -95,6 +95,11 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的
感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格!
本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらのBmoPlus AIアカウント専門店/代行チャージ経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます!