Files
sub2api/backend/internal/repository/channel_monitor_template_repo.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

267 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repository
import (
"context"
"database/sql"
"encoding/json"
"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"
"github.com/lib/pq"
)
// 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)).
SetAPIMode(defaultAPIModeRepo(t.APIMode)).
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).
SetAPIMode(defaultAPIModeRepo(t.APIMode)).
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)))
}
if params.APIMode != "" {
q = q.Where(channelmonitorrequesttemplate.APIModeEQ(defaultAPIModeRepo(params.APIMode)))
}
rows, err := q.
Order(dbent.Asc(channelmonitorrequesttemplate.FieldProvider), dbent.Asc(channelmonitorrequesttemplate.FieldAPIMode), 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 把模板当前配置覆盖到 monitorIDs 列表里的关联监控。
// WHERE 双重过滤:template_id = id AND id IN (monitorIDs),防止用户传了未关联本模板的 id
// 就被覆盖。模板字段通过 ent UpdateMany 更新以保留 hooksextra_headers 在同一事务中
// 单独合并,以保留仅用于幂等恢复、绝不会发往上游的内部 operation ID。
func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error) {
if len(monitorIDs) == 0 {
return 0, nil
}
if tx := dbent.TxFromContext(ctx); tx != nil {
return r.applyToMonitorsWithClient(ctx, tx.Client(), id, monitorIDs)
}
tx, err := r.client.Tx(ctx)
if err != nil {
return 0, fmt.Errorf("begin apply template transaction: %w", err)
}
txCtx := dbent.NewTxContext(ctx, tx)
affected, err := r.applyToMonitorsWithClient(txCtx, tx.Client(), id, monitorIDs)
if err != nil {
_ = tx.Rollback()
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit apply template transaction: %w", err)
}
return affected, nil
}
func (r *channelMonitorRequestTemplateRepository) applyToMonitorsWithClient(
ctx context.Context,
client *dbent.Client,
id int64,
monitorIDs []int64,
) (int64, error) {
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),
channelmonitor.IDIn(monitorIDs...),
channelmonitor.ProviderEQ(channelmonitor.Provider(tpl.Provider)),
channelmonitor.APIModeEQ(defaultAPIModeRepo(tpl.APIMode)),
).
SetAPIMode(defaultAPIModeRepo(tpl.APIMode)).
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)
}
if affected == 0 {
return 0, nil
}
templateHeaders := channelMonitorHeadersForPersistence(&service.ChannelMonitor{
ExtraHeaders: tpl.ExtraHeaders,
})
templateHeadersJSON, err := json.Marshal(templateHeaders)
if err != nil {
return 0, fmt.Errorf("marshal template headers: %w", err)
}
result, err := client.ExecContext(ctx, `
UPDATE channel_monitors
SET extra_headers = $1::jsonb || CASE
WHEN COALESCE(extra_headers, '{}'::jsonb) ? ($2::text)
THEN jsonb_build_object($2::text, COALESCE(extra_headers, '{}'::jsonb) -> ($2::text))
ELSE '{}'::jsonb
END
WHERE template_id = $3
AND id = ANY($4)
AND provider = $5
AND api_mode = $6
`, string(templateHeadersJSON), service.ChannelMonitorDuplicateOperationIDMetadataKey,
id, pq.Array(monitorIDs), string(tpl.Provider), defaultAPIModeRepo(tpl.APIMode))
if err != nil {
return 0, fmt.Errorf("apply template headers to monitors: %w", err)
}
headersAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("count applied template headers: %w", err)
}
if headersAffected != int64(affected) {
return 0, fmt.Errorf("apply template headers: affected %d rows, expected %d", headersAffected, affected)
}
return headersAffected, 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
}
// ListAssociatedMonitors 列出模板关联的所有监控简略字段。
// ORDER BY name 稳定输出方便前端展示。
func (r *channelMonitorRequestTemplateRepository) ListAssociatedMonitors(ctx context.Context, id int64) ([]*service.AssociatedMonitorBrief, error) {
rows, err := r.client.ChannelMonitor.Query().
Where(channelmonitor.TemplateIDEQ(id)).
Order(dbent.Asc(channelmonitor.FieldName)).
All(ctx)
if err != nil {
return nil, fmt.Errorf("list associated monitors for template %d: %w", id, err)
}
out := make([]*service.AssociatedMonitorBrief, 0, len(rows))
for _, row := range rows {
out = append(out, &service.AssociatedMonitorBrief{
ID: row.ID,
Name: row.Name,
Provider: string(row.Provider),
APIMode: defaultAPIModeRepo(row.APIMode),
Enabled: row.Enabled,
})
}
return out, 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),
APIMode: defaultAPIModeRepo(row.APIMode),
Description: row.Description,
ExtraHeaders: headers,
BodyOverrideMode: row.BodyOverrideMode,
BodyOverride: row.BodyOverride,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}