Documentation
¶
Overview ¶
SPDX-License-Identifier: AGPL-3.0-or-later Dual-licensed: AGPL-3.0-or-later (open source) or commercial license (closed-source deployment exemption); see LICENSE and LICENSE.commercial. Copyright (c) 2026 is7Qin.
SPDX-License-Identifier: AGPL-3.0-or-later Dual-licensed: AGPL-3.0-or-later (open source) or commercial license (closed-source deployment exemption); see LICENSE and LICENSE.commercial. Copyright (c) 2026 is7Qin.
SPDX-License-Identifier: AGPL-3.0-or-later Dual-licensed: AGPL-3.0-or-later (open source) or commercial license (closed-source deployment exemption); see LICENSE and LICENSE.commercial. Copyright (c) 2026 is7Qin.
Package service 实现管理端业务逻辑:CRUD 校验 + 变更后失效调度/客户端缓存。
Index ¶
- Constants
- Variables
- func NewCodexIdentity() domain.CodexIdentity
- type AccountExtStore
- type AccountStore
- type AccountView
- type CodexUsageSnapshotter
- type EmailCodeStore
- type EmailTemplateStore
- type EntityTrendQuery
- type GenerateRequest
- type GroupAssignmentStore
- type GroupStore
- type Invalidator
- type KeyRegistrar
- type KeyStore
- type LogStore
- type MailSendTask
- type MailWorker
- type NopInvalidator
- type OverviewAccounts
- type OverviewData
- type OverviewErrTop
- type PricingPreview
- type PricingPreviewEntry
- type PricingStore
- type PricingSyncStats
- type Publisher
- type RedemptionStore
- type RuleInput
- type RulePatch
- type RuleReloader
- type RuleStore
- type RuntimeProvider
- type Service
- func (s *Service) AccountUsage(ctx context.Context, accountID int64) (*domain.CodexUsageSnapshot, error)
- func (s *Service) AccountsUsage(ctx context.Context, ids []int64, from, to time.Time) ([]domain.AccountUsage, error)
- func (s *Service) ChangePassword(ctx context.Context, userID int64, old, new string) error
- func (s *Service) CreateAccount(ctx context.Context, a *domain.Account) (*domain.Account, error)
- func (s *Service) CreateGroup(ctx context.Context, name string, visibility domain.GroupVisibility, ...) (*domain.Group, error)
- func (s *Service) CreateKey(ctx context.Context, userID int64, name string, groupID int64, ...) (*domain.Key, error)
- func (s *Service) CreateRule(ctx context.Context, in RuleInput) (*domain.Rule, error)
- func (s *Service) CreateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)
- func (s *Service) CreateUser(ctx context.Context, email, password string, role domain.Role, ...) (*domain.User, error)
- func (s *Service) DeactivateCode(ctx context.Context, id int64) error
- func (s *Service) DeactivateCodesBatch(ctx context.Context, ids []int64) (int64, error)
- func (s *Service) DeleteAccount(ctx context.Context, id int64) error
- func (s *Service) DeleteAccountsBatch(ctx context.Context, ids []int64) error
- func (s *Service) DeleteGroup(ctx context.Context, id int64) error
- func (s *Service) DeleteGroupsBatch(ctx context.Context, ids []int64) error
- func (s *Service) DeleteKey(ctx context.Context, userID, keyID int64) error
- func (s *Service) DeletePriceEntry(ctx context.Context, model string) error
- func (s *Service) DeleteRule(ctx context.Context, id int64) error
- func (s *Service) DeleteRulesBatch(ctx context.Context, ids []int64) error
- func (s *Service) DeleteTemplate(ctx context.Context, id int64) error
- func (s *Service) DeleteTemplatesBatch(ctx context.Context, ids []int64) error
- func (s *Service) GenerateCodes(ctx context.Context, req GenerateRequest, createdBy int64) ([]*domain.RedemptionCode, error)
- func (s *Service) GetAccount(ctx context.Context, id int64) (*domain.Account, error)
- func (s *Service) GetAccountExt(ctx context.Context, accountID int64) (*domain.AccountExt, error)
- func (s *Service) GetAccountGroups(ctx context.Context, id int64) ([]int64, error)
- func (s *Service) GetCode(ctx context.Context, id int64) (*domain.RedemptionCode, error)
- func (s *Service) GetCodeUses(ctx context.Context, codeID int64, q repository.ListQuery) ([]*domain.RedemptionUse, int64, error)
- func (s *Service) GetGroup(ctx context.Context, id int64) (*domain.Group, error)
- func (s *Service) GetGroupAssignments(ctx context.Context, groupID int64) ([]int64, map[int64]*int, error)
- func (s *Service) GetKey(ctx context.Context, userID, keyID int64) (*domain.Key, error)
- func (s *Service) GetPriceEntry(ctx context.Context, model string) (*domain.PriceEntry, error)
- func (s *Service) GetSettings(ctx context.Context) ([]*domain.Setting, error)
- func (s *Service) GetTemplate(ctx context.Context, id int64) (*domain.Template, error)
- func (s *Service) GetTemplateExt(ctx context.Context, templateID int64) (*domain.TemplateExt, error)
- func (s *Service) GetUser(ctx context.Context, id int64) (*domain.User, error)
- func (s *Service) GetUserGroups(ctx context.Context, userID int64) ([]int64, map[int64]*int, error)
- func (s *Service) GetUserMe(ctx context.Context, userID int64) (*domain.User, error)
- func (s *Service) ImportCodexOAuthAccounts(ctx context.Context, items []domain.CodexOAuthImportItem, ...) (*domain.ImportResult, error)
- func (s *Service) ImportCodexPATAccounts(ctx context.Context, items []domain.CodexPATImportItem, tplID, groupID *int64) (*domain.ImportResult, error)
- func (s *Service) ListAccountViews(ctx context.Context, q repository.ListQuery) ([]*AccountView, int64, error)
- func (s *Service) ListAccounts(ctx context.Context, q repository.ListQuery) ([]*domain.Account, int64, error)
- func (s *Service) ListAdminKeys(ctx context.Context, q repository.ListQuery) ([]*domain.Key, int64, error)
- func (s *Service) ListCodes(ctx context.Context, q repository.ListQuery, typ *domain.RedemptionType, ...) ([]*domain.RedemptionCode, int64, error)
- func (s *Service) ListGroups(ctx context.Context, q repository.ListQuery) ([]*domain.Group, int64, error)
- func (s *Service) ListGroupsForUser(ctx context.Context, userID int64) ([]*domain.Group, error)
- func (s *Service) ListKeys(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.Key, int64, error)
- func (s *Service) ListMailTemplates(ctx context.Context) ([]*domain.EmailTemplate, error)
- func (s *Service) ListMyRedemptions(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.RedemptionRecord, int64, error)
- func (s *Service) ListPriceEntries(ctx context.Context, q repository.ListQuery, source *domain.PricingSource, ...) ([]*domain.PriceEntry, int64, error)
- func (s *Service) ListPriceVariants(ctx context.Context, model string) ([]*domain.PriceVariant, error)
- func (s *Service) ListRules(ctx context.Context, enabled *bool) ([]domain.Rule, int64, error)
- func (s *Service) ListTempBalances(ctx context.Context, q repository.ListQuery, userID int64) ([]*domain.TempBalance, int64, error)
- func (s *Service) ListTemplates(ctx context.Context, q repository.ListQuery) ([]*domain.Template, int64, error)
- func (s *Service) ListUserTempBalances(ctx context.Context, userID int64) ([]*domain.TempBalance, error)
- func (s *Service) ListUsers(ctx context.Context, q repository.ListQuery) ([]*domain.User, int64, error)
- func (s *Service) LoginUser(ctx context.Context, email, password string) (*domain.User, error)
- func (s *Service) Overview(ctx context.Context, utcDay time.Time, days int, groupID int64) (*OverviewData, error)
- func (s *Service) PreviewPricingSync(ctx context.Context) (*PricingPreview, error)
- func (s *Service) PriceSourceURL() string
- func (s *Service) PriceSyncCron() string
- func (s *Service) QueryEntityTrend(ctx context.Context, q EntityTrendQuery) ([]*domain.EntityStatBucket, error)
- func (s *Service) QueryErrLogs(ctx context.Context, q repository.ErrLogQuery) ([]*domain.UsageLog, error)
- func (s *Service) QueryStatsTTFT(ctx context.Context, q TTFTQuery) (*domain.TTFTSummary, error)
- func (s *Service) QueryStatsTop(ctx context.Context, q TopQuery) ([]*domain.EntityStatBucket, error)
- func (s *Service) QueryStatsTrend(ctx context.Context, q TrendQuery) ([]*domain.StatBucket, error)
- func (s *Service) QueryUsages(ctx context.Context, q repository.UsageQuery) ([]*domain.UsageLog, error)
- func (s *Service) Redeem(ctx context.Context, code string, userID int64) (*domain.RedemptionApply, error)
- func (s *Service) RegisterUser(ctx context.Context, email, password string) (*domain.User, error)
- func (s *Service) RegisterUserWithCode(ctx context.Context, email, password, code string) (*domain.User, error)
- func (s *Service) ReloadFunctionPricing()
- func (s *Service) ReloadFunctionPricingCtx(ctx context.Context) error
- func (s *Service) ReloadImagePricing()
- func (s *Service) ReloadImagePricingCtx(ctx context.Context) error
- func (s *Service) ReloadPricing()
- func (s *Service) ReloadPricingCtx(ctx context.Context) error
- func (s *Service) ReloadSettings(ctx context.Context) error
- func (s *Service) RenderTemplate(ctx context.Context, purpose domain.EmailTemplatePurpose, ...) (string, string, error)
- func (s *Service) ReplacePriceVariants(ctx context.Context, model string, variants []*domain.PriceVariant) ([]*domain.PriceVariant, error)
- func (s *Service) ResetAccountsCooldownBatch(ctx context.Context, ids []int64) (int, error)
- func (s *Service) ResetPassword(ctx context.Context, email, code, newPassword string) error
- func (s *Service) ResolvePrices(model string, promptTokens int64, tier string, at time.Time) (domain.ResolvedPrices, bool)
- func (s *Service) RotateKey(ctx context.Context, userID, keyID int64) (*domain.Key, error)
- func (s *Service) SendForgotPasswordCode(ctx context.Context, email string) error
- func (s *Service) SendRegisterCode(ctx context.Context, email string) error
- func (s *Service) ServiceTierPolicy(tier billing.Tier) billing.TierPolicyMode
- func (s *Service) SetEmailCodeStore(store EmailCodeStore)
- func (s *Service) SetGroupAssignments(ctx context.Context, groupID int64, userIDs []int64, mults map[int64]*int) ([]int64, map[int64]*int, error)
- func (s *Service) SetLocalDispatcher(d notify.Dispatcher)
- func (s *Service) SetMailEnqueue(fn func(MailSendTask) error)
- func (s *Service) SetPriceFetcher(f pricing.Fetcher)
- func (s *Service) SetTimeLocation(l *time.Location)
- func (s *Service) SetUsageSnapshotter(u CodexUsageSnapshotter)
- func (s *Service) SetUserGroups(ctx context.Context, userID int64, groupIDs []int64, mults map[int64]*int) ([]int64, map[int64]*int, error)
- func (s *Service) SyncPricingNow(ctx context.Context) (*PricingSyncStats, error)
- func (s *Service) UpdateAccount(ctx context.Context, a *domain.Account) (*domain.Account, error)
- func (s *Service) UpdateAccountsBatch(ctx context.Context, ids []int64, p repository.AccountPatch) error
- func (s *Service) UpdateGroup(ctx context.Context, g *domain.Group) (*domain.Group, error)
- func (s *Service) UpdateGroupsBatch(ctx context.Context, ids []int64, p repository.GroupPatch) error
- func (s *Service) UpdateKey(ctx context.Context, userID, keyID int64, name *string, ...) (*domain.Key, error)
- func (s *Service) UpdateMailTemplate(ctx context.Context, purpose, subject, bodyText string) (*domain.EmailTemplate, error)
- func (s *Service) UpdateRule(ctx context.Context, id int64, p RulePatch) (*domain.Rule, error)
- func (s *Service) UpdateSetting(ctx context.Context, key, value string) (*domain.Setting, error)
- func (s *Service) UpdateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)
- func (s *Service) UpdateTemplatesBatch(ctx context.Context, ids []int64, p repository.TemplatePatch) error
- func (s *Service) UpdateUser(ctx context.Context, p *repository.UserPatch) (*domain.User, error)
- func (s *Service) UpsertAccountExt(ctx context.Context, e *domain.AccountExt) (*domain.AccountExt, error)
- func (s *Service) UpsertPriceEntry(ctx context.Context, m *repository.PriceEntryManual) (*domain.PriceEntry, error)
- func (s *Service) UpsertTemplateExt(ctx context.Context, e *domain.TemplateExt) (*domain.TemplateExt, error)
- func (s *Service) UserEmails(ctx context.Context, ids []int64) (map[int64]string, error)
- func (s *Service) UserStats(ctx context.Context, userID int64, q EntityTrendQuery) ([]*domain.EntityStatBucket, error)
- func (s *Service) UserStatsTTFT(ctx context.Context, userID int64, q TTFTQuery) (*domain.TTFTSummary, error)
- type SettingStore
- type StatStore
- type Store
- type TTFTQuery
- type TemplateExtStore
- type TemplateStore
- type TopQuery
- type TrendQuery
- type UserStore
Constants ¶
const ( // MaxStatsTrendSpan trend/top/entity-trend 共用窗口跨度上限(90 天): // cube 查询按小时桶扫描,90d × 维度基数是交互式端点的合理上界。 MaxStatsTrendSpan = 90 * 24 * time.Hour // MaxStatsSketchBuckets sketch 分支桶数上限 = 2160(= 90d × 24 小时桶, // 与 MaxStatsTrendSpan 自洽——同一窗口两种表述)。sketch 走 cube hist // 服务端合并(array_agg 带回逐行直方图),桶数直接决定合并成本。 MaxStatsSketchBuckets = 2160 // MaxStatsTTFTExactSpan exact 分支窗口跨度上限(168h = 7 天):打 // usage_logs 原始行 percentile_cont,无预聚合保护,窗口必须远小于走 // 预聚合 cube 的 sketch 分支。 MaxStatsTTFTExactSpan = 168 * time.Hour // DefaultStatsTopLimit top 排行缺省条数(repo 层 ≤0 归一同值,双保险)。 DefaultStatsTopLimit = 20 // MaxStatsListLimit top 排行上限钳制(对齐 httpface.ClampLimit(200) 惯例 // ——service 不 import handler 包,此处同语义本地化:超限裁剪不报错)。 MaxStatsListLimit = 200 )
校验上限常量(spec §5 校验规则;TTFT 双分支各自独立上限——Momus M5 钉死)。
const EmailVerificationRequired = "email verification required"
EmailVerificationRequired 缺验证码时 400 响应的固定哨兵片段(前端发现机制)。
Variables ¶
var ( ErrNotFound = serviceerr.ErrNotFound ErrInvalidInput = serviceerr.ErrInvalidInput ErrConflict = serviceerr.ErrConflict ErrTooManyRequests = serviceerr.ErrTooManyRequests ErrMailNotConfigured = serviceerr.ErrMailNotConfigured ErrMailQueueFull = serviceerr.ErrMailQueueFull )
错误哨兵定义下沉 internal/service/errors(叶子包,单一真相);此处别名 re-export 保持既有引用(errors.Is(err, service.ErrXxx))同一哨兵实例语义。
var ( ErrInvalidCredentials = serviceerr.ErrInvalidCredentials ErrSignupDisabled = serviceerr.ErrSignupDisabled )
错误哨兵定义下沉 internal/service/errors(叶子包,单一真相);别名 re-export 保持既有引用同一实例语义。
var ErrGroupNotEligible = fmt.Errorf("%w: group is private and not granted to user", ErrInvalidInput)
ErrGroupNotEligible private 组未授予(key 创建组可选性校验;包装 ErrInvalidInput → 400)。
var ErrPriceFetch = errors.New("service: price fetch failed")
Functions ¶
func NewCodexIdentity ¶
func NewCodexIdentity() domain.CodexIdentity
NewCodexIdentity 生成 codex 账号身份四元组(账号导入时自动生成、持久复用; 纯函数零依赖——标准库 crypto/rand + time 构造 UUID 形状):
- installation_id:UUIDv4(~/.codex/installation_id 语义,账号级唯一身份);
- session_id / thread_id:UUIDv7(真实客户端主线程 thread_id==session_id, 同值对齐;UUIDv7 = 48bit unix ms + 版本位 + 随机位,时间有序近似);
- window_id:{thread_id}:0(导入时生成后恒定不变——恒 0,用户裁决:高性能 网关不背透传解析(零分支零解析),上游不校验 n 单调性,形状正确即可)。
Types ¶
type AccountExtStore ¶
type AccountExtStore interface {
UpsertAccountExt(ctx context.Context, e *domain.AccountExt) (*domain.AccountExt, error)
TryInsertAccountExt(ctx context.Context, e *domain.AccountExt) (bool, error)
GetAccountExt(ctx context.Context, accountID int64) (*domain.AccountExt, error)
// FindAccountExtByCodexKey 组合幂等键查重(Task B 批量导入——(codex_email,
// codex_account_id);GetAccountExt 仅按 account_id,查重面不存在);缺行 →
// ErrNotFound。
FindAccountExtByCodexKey(ctx context.Context, codexEmail, codexAccountID string) (*domain.AccountExt, error)
// WriteOAuthRotation oauth 凭据三列部分更新(Task B 导入 updated 路径——
// identity/email/其余列零触碰;SDK 轮转回写 sdkbridge.RotationStore 同签名
// 独立面,repository.AccountExts 一实现双面);行缺失 → ErrNotFound。
WriteOAuthRotation(ctx context.Context, accountID int64, at, rt string, expiresAt *time.Time) error
// WritePATKey pat 凭据列部分更新(WriteOAuthRotation 的 pat 对称形态);
// 行缺失 → ErrNotFound。
WritePATKey(ctx context.Context, accountID int64, patKey string) error
}
AccountExtStore 账号类型化鉴权扩展持久化(account_ext 1:1;W1 数据层 CRUD, 消费接线留给 W6)。TryInsertAccountExt:首写原子性(ON CONFLICT DO NOTHING 先写者胜)——并发双导入同一账号不覆盖不报错。
type AccountStore ¶
type AccountStore interface {
CreateAccount(ctx context.Context, a *domain.Account) (*domain.Account, error)
GetAccount(ctx context.Context, id int64) (*domain.Account, error)
ListAccounts(ctx context.Context, q repository.ListQuery) ([]*domain.Account, int64, error)
UpdateAccount(ctx context.Context, a *domain.Account, cooldownUntil *time.Time) (*domain.Account, error)
DeleteAccount(ctx context.Context, id int64) error
DeleteAccountsBatch(ctx context.Context, ids []int64) error
UpdateAccountsBatch(ctx context.Context, ids []int64, p repository.AccountPatch) error
// SetAccountGroups 替换账号的全部分组(替换语义;空数组 = 清空)。
SetAccountGroups(ctx context.Context, accountID int64, groupIDs []int64) error
// GetAccountGroups 账号的分组 id 列表(编辑回显;账号缺 id 由调用方先
// GetAccount 拦截)。
GetAccountGroups(ctx context.Context, accountID int64) ([]int64, error)
}
type AccountView ¶
type AccountView struct {
*domain.Account
Status domain.AccountStatus `json:"Status"`
CooldownUntil *time.Time `json:"CooldownUntil"`
Concurrency int64 `json:"concurrency"`
ErrRate float64 `json:"err_rate"`
ErrCount int `json:"err_count"`
}
AccountView 是账号的管理端视图(含调度器运行时信息)。Status/CooldownUntil 覆盖嵌入 Account 的同名字段(Go 字段提升规则:同名顶层字段遮蔽嵌入字段): 合并后列表显示 = 调度器内存权威(A-4,2026-08-19)——回写丢失/失败时内存与 DB 不一致,管理端显示与 Select 请求行为同源(overview 聚合早已用内存状态, A-4 后列表口径与其统一);DB 列仍是持久化镜像。JSON 键名与嵌入字段默认 一致("Status"/"CooldownUntil",形状不变)。
type CodexUsageSnapshotter ¶
type CodexUsageSnapshotter interface {
GetUsageSnapshot(ctx context.Context, cred *domain.AccountCredential) (*domain.CodexUsageSnapshot, error)
}
CodexUsageSnapshotter codex 额度快照数据源(*sdkbridge.Codex 满足——装配侧 注入;接口化供测试注入,与 priceFetcher/local 同形态——依赖方向 service → sdkbridge 不反转)。
type EmailCodeStore ¶
type EmailCodeStore interface {
GetEmailCode(ctx context.Context, email, purpose string) (*domain.EmailCode, error)
UpsertEmailCode(ctx context.Context, email, purpose, sha256 string, expiresAt time.Time) (*domain.EmailCode, error)
IncrementEmailCodeAttempts(ctx context.Context, email, purpose string) (int, error)
DeleteEmailCode(ctx context.Context, email, purpose string) error
}
EmailCodeStore 验证码持久化。
type EmailTemplateStore ¶
type EmailTemplateStore interface {
GetEmailTemplate(ctx context.Context, purpose string) (*domain.EmailTemplate, error)
ListEmailTemplates(ctx context.Context) ([]*domain.EmailTemplate, error)
UpsertEmailTemplate(ctx context.Context, purpose, subject, bodyText string) (*domain.EmailTemplate, error)
DeleteEmailTemplate(ctx context.Context, purpose string) error
}
EmailTemplateStore 邮件模板持久化。
type EntityTrendQuery ¶
type EntityTrendQuery struct {
EntityType string
EntityID int64
From time.Time
To time.Time
Granularity string // hour|day;空 = day
Model string
}
EntityTrendQuery /stats/entity-trend 入参(强制实体过滤 + 可选 Model)。
type GenerateRequest ¶
type GenerateRequest struct {
Type domain.RedemptionType
Value int64 // 最小单位(分 / 并发数);> 0
Remark *string
ExpiresAt *time.Time // 码未兑换即过期;nil = 永久
ResourceExpiresAt *time.Time // 兑换后资源到期;temp_balance 必填(决策 4)
MaxUses int
Count int
}
GenerateRequest 兑换码生成参数(/api/admin/redemption-codes POST)。 MaxUses ≤ 0 → 1(单次码,决策 3);Count ≤ 0 → 1,上限 1000。
type GroupAssignmentStore ¶
type GroupAssignmentStore interface {
GrantGroup(ctx context.Context, groupID, userID int64) error
RevokeGroup(ctx context.Context, groupID, userID int64) error
// SetAssignmentMultiplier 设置/清除该用户在该组的专属价格倍率(T3.5 修正:
// 按组;m = nil → 清除为未设置 → 回退组倍率;0 = 免费)。
SetAssignmentMultiplier(ctx context.Context, groupID, userID int64, m *int) error
ListAssignmentsByUser(ctx context.Context, userID int64) ([]*domain.GroupAssignment, error)
ListAssignmentsByGroup(ctx context.Context, groupID int64) ([]*domain.GroupAssignment, error)
ListGroupsForUser(ctx context.Context, userID int64) ([]*domain.Group, error)
}
GroupAssignmentStore private 组授予持久化(/api/admin/groups/{id}/assignments + /api/user/groups 可选组列表)。
type GroupStore ¶
type GroupStore interface {
CreateGroup(ctx context.Context, g *domain.Group) (*domain.Group, error)
GetGroup(ctx context.Context, id int64) (*domain.Group, error)
ListGroups(ctx context.Context, q repository.ListQuery) ([]*domain.Group, int64, error)
UpdateGroup(ctx context.Context, g *domain.Group) (*domain.Group, error)
DeleteGroup(ctx context.Context, id int64) error
DeleteGroupsBatch(ctx context.Context, ids []int64) error
UpdateGroupsBatch(ctx context.Context, ids []int64, p repository.GroupPatch) error
// LoadGroupAccounts 单组账号(删组前组内账号校验:含账号组 → 409 拒绝;
// 与调度器 Loader 同一数据源)。
LoadGroupAccounts(ctx context.Context, groupID int64) ([]*domain.Account, error)
}
type Invalidator ¶
type Invalidator interface {
// Users 用户 CRUD(含创建)与用户余额变更(含 Redeem):auth + 余额快照
// 全量 Reload(去抖窗口内合并;新用户必须即刻进余额快照——评审 M-2,
// 防 ≤10s 402 窗口,回归测试 tools/e2e)。
Users()
// Templates 模板(base_url/models/映射)变更:sched 全量 + clients 失效
// (base_url 变更需按新地址重建 SDK 客户端)。
Templates()
// Accounts 账号变更(创建/更新/删除/批量):sched 组级定向重载受影响组
// (gids);keyChanged(upstream_key 变更)→ clients 失效。
Accounts(gids []int64, keyChanged bool)
// Multipliers 组倍率 / 用户-组专属倍率(price_multiplier)变更(含组创建/
// 删除与 group_assignment CRUD——新倍率须即刻进快照):余额倍率快照定向
// 刷新(EffectiveMultiplier 陈旧 ≤10s 不可接受)。
Multipliers()
}
Invalidator 管理面变更的去抖定向失效回调(O2 接线矩阵,评审 M-1): service 各 CRUD 在变更落库成功后调用对应方法;实现 = invalidate.Debouncer (去抖窗口合并 + 单 goroutine 串行执行 + 按矩阵定向重载,main 装配)。 key/pricing 变更不走此接口(auth 增量 Upsert/Delete / 内部 reloadPricing, 已轻量)。Mark 路径零锁零 DB,不阻塞任何调用方。
type KeyRegistrar ¶
KeyRegistrar 由 proxy.Auth 实现,供客户端 key 变更时增量刷新鉴权快照。
type KeyStore ¶
type KeyStore interface {
CreateKey(ctx context.Context, k *domain.Key) (*domain.Key, error)
GetKey(ctx context.Context, id int64) (*domain.Key, error)
ListKeysByUser(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.Key, int64, error)
// ListKeys 管理端全量 key 列表(/api/admin/keys:软删过滤 + UserID/GroupID
// 零值不过滤 + 3 键 sort 白名单;脱敏在 handler 转换面——明文字段不下发)。
ListKeys(ctx context.Context, q repository.ListQuery) ([]*domain.Key, int64, error)
// UpdateKey patch 语义更新(S3-F1):仅 Set 非 nil 字段,nil = 不改——并发
// 两个 PUT 改不同字段各自生效(对齐 UserPatch 范式)。
UpdateKey(ctx context.Context, p *repository.KeyPatch) (*domain.Key, error)
RotateKey(ctx context.Context, id int64, newRaw string) (*domain.Key, error)
DeleteKey(ctx context.Context, id int64) error
// DeleteKeysByGroup 组删除前置清理(key.group_id 外键约束;返回被删明文)。
DeleteKeysByGroup(ctx context.Context, groupID int64) ([]string, error)
}
KeyStore 客户端 key 持久化(/api/user/keys 面 + 组删除前置清理)。
type LogStore ¶
type LogStore interface {
QueryUsages(ctx context.Context, q repository.UsageQuery) ([]*domain.UsageLog, error)
QueryErrLogs(ctx context.Context, q repository.ErrLogQuery) ([]*domain.UsageLog, error)
// ScanUsageAgg 批量账号 usage_logs 区间聚合(/api/admin/accounts/usage 查询面:
// 单查询 ANY + GROUP BY;无记录账号无键——补零由 service 按 ids 全量组装)。
ScanUsageAgg(ctx context.Context, accountIDs []int64, from, to time.Time) (map[int64]*domain.UsageAgg, error)
}
type MailSendTask ¶
type MailSendTask struct {
To string
Purpose domain.EmailTemplatePurpose
Code string
TTLMin int
}
MailSendTask 邮件发送任务(明文 code 仅瞬态内存+通道,不落日志/不落库)。
type MailWorker ¶
type MailWorker struct {
// contains filtered or unexported fields
}
MailWorker 专用邮件发送后台 worker(D-W1..W7)。
func NewMailWorker ¶
func NewMailWorker(svc *Service) *MailWorker
NewMailWorker 构造邮件 worker(svc 复用 RenderTemplate/mailConfig 私有面)。
func (*MailWorker) Close ¶
func (w *MailWorker) Close(ctx context.Context) error
Close 关闭接收并限时排空已在队任务(反序排空中段关闭,D-W4)。 drain 与 loop 路径的 recover 对称(评审 F-1):drain 跑在 Manager 关停 goroutine 上、无 Manager.Go 托管,panic 会中断后续 worker 的 Close 链。
func (*MailWorker) Enqueue ¶
func (w *MailWorker) Enqueue(t MailSendTask) error
Enqueue 入队(有界 256;满或已关闭 → dropped++ + Warn + ErrMailQueueFull;永不 close ch)。
func (*MailWorker) Name ¶
func (w *MailWorker) Name() string
Name 实现 worker.Worker + handler.StatsProvider。
type NopInvalidator ¶
type NopInvalidator struct{}
NopInvalidator 无效化 no-op(测试与无关路径)。
func (NopInvalidator) Accounts ¶
func (NopInvalidator) Accounts([]int64, bool)
func (NopInvalidator) Multipliers ¶
func (NopInvalidator) Multipliers()
func (NopInvalidator) Templates ¶
func (NopInvalidator) Templates()
func (NopInvalidator) Users ¶
func (NopInvalidator) Users()
type OverviewAccounts ¶
type OverviewAccounts struct {
Active int
Unhealthy int
N429 int
Disabled int
Concurrency int64
MaxConcurrency int64
}
OverviewAccounts 账号健康分布 + 并发水位(调度器快照同源——与账号列表 运行时视图 ListAccountViews 一致:状态取快照 EWMA 状态,并发/水位取快照 原子计数器)。
type OverviewData ¶
type OverviewData struct {
Summary repository.StatSummary
Trend []*repository.StatDayAgg
Accounts OverviewAccounts
Resources repository.OverviewResourceCounts
ErrTop []OverviewErrTop
}
OverviewData 总览聚合结果(内部单位:cost 毫分——USD 换算在 handler 边界 /1e5,与价格 API 口径一致)。
type OverviewErrTop ¶
OverviewErrTop 账号维度错误率条目(err_top;name = 账号名)。
type PricingPreview ¶
type PricingPreview struct {
ToAdd int `json:"to_add"`
ToUpdate int `json:"to_update"`
Skipped int `json:"skipped"`
Entries []PricingPreviewEntry `json:"entries"`
VariantsChanged int `json:"variants_changed"`
}
type PricingPreviewEntry ¶
type PricingStore ¶
type PricingStore interface {
UpsertPriceEntriesFromLiteLLM(ctx context.Context, rows []*domain.PriceEntry) (int, error)
UpsertPriceVariantsFromLiteLLM(ctx context.Context, variants []*domain.PriceVariant) (int, error)
UpsertPriceEntryManual(ctx context.Context, m *repository.PriceEntryManual) (*domain.PriceEntry, error)
DeletePriceEntryManual(ctx context.Context, model string) error
ListPriceEntries(ctx context.Context, q repository.ListQuery, source *domain.PricingSource, mode *domain.PriceMode, provider *string, model string) ([]*domain.PriceEntry, int64, error)
GetPriceEntry(ctx context.Context, model string) (*domain.PriceEntry, error)
ListPriceVariants(ctx context.Context, model string) ([]*domain.PriceVariant, error)
ListAllPriceVariants(ctx context.Context) ([]*domain.PriceVariant, error)
ReplacePriceVariants(ctx context.Context, model string, variants []*domain.PriceVariant) ([]*domain.PriceVariant, error)
ManualEntryModels(ctx context.Context) ([]string, error)
}
PricingStore 统一价格持久化。
type PricingSyncStats ¶
type Publisher ¶
Publisher 多实例 NOTIFY 发布面(#14 T2):实现 = *notify.Publisher(Publish 在 DB 写成功后调用,与 inv.* 调用点并排);接口化供测试注入 fake(与 Invalidator 同模式)。nil = 单实例/未装配(T2 过渡),publish no-op。
type RedemptionStore ¶
type RedemptionStore interface {
CreateCodes(ctx context.Context, codes []*domain.RedemptionCode) error
GetByCode(ctx context.Context, code string) (*domain.RedemptionCode, error)
GetCode(ctx context.Context, id int64) (*domain.RedemptionCode, error)
ListCodes(ctx context.Context, q repository.ListQuery, typ *domain.RedemptionType, status *domain.RedemptionStatus) ([]*domain.RedemptionCode, int64, error)
ListCodeUses(ctx context.Context, codeID int64, q repository.ListQuery) ([]*domain.RedemptionUse, int64, error)
// ListUsesByUser 某用户的兑换记录(/api/user/redemptions;use + 码联查视图)。
ListUsesByUser(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.RedemptionRecord, int64, error)
DeactivateCodes(ctx context.Context, ids []int64) (int64, error)
GetUse(ctx context.Context, codeID, userID int64) (*domain.RedemptionUse, error)
CreateUse(ctx context.Context, use *domain.RedemptionUse) error
IncrementUsed(ctx context.Context, codeID int64) (bool, error)
}
RedemptionStore 兑换码 + 兑换审计持久化(Phase 5 计费前基础设施)。 兑换事务编排(Redeem)经 Store.WithTx 以 repository.TxStore 面访问。
type RuleInput ¶
type RuleInput struct {
Name string
Enabled bool
Priority int
When map[string]any
Then map[string]any
}
RuleInput 规则创建入参(when/then 为契约自由对象,service 层负责 DisallowUnknownFields 反序列化与语义校验)。
type RulePatch ¶
type RulePatch struct {
Name *string
Enabled *bool
Priority *int
When map[string]any // nil = 不修改;显式 {} = 清空 when
Then map[string]any
}
RulePatch 规则部分更新(nil 字段 = 不修改)。
type RuleReloader ¶
RuleReloader 由 rule.RuleEngine 实现:规则 CRUD 后全量重载(invalidate 钩子)。 独立于通用 invalidate——规则重载会重置窗口计数,不能随任意资源变更触发。
type RuleStore ¶
type RuleStore interface {
ListRules(ctx context.Context, enabled *bool) ([]domain.Rule, error) // nil = 全部;priority 升序
CreateRule(ctx context.Context, r domain.Rule) (int64, error)
UpdateRule(ctx context.Context, r domain.Rule) error
DeleteRule(ctx context.Context, id int64) error
DeleteRulesBatch(ctx context.Context, ids []int64) error
CountRules(ctx context.Context) (int64, error)
}
RuleStore 规则存储接口(repository.RuleStore 子集,Service 门面注入用)。
type RuntimeProvider ¶
type RuntimeProvider interface {
Runtime(accountID int64) (scheduler.RuntimeInfo, bool)
Runtimes() []scheduler.AccountRuntime
}
RuntimeProvider 由 scheduler 实现,供账号运行时视图(Runtimes = overview 聚合面:账号健康分布/并发水位/err_top 与列表运行时视图同源)。
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func New ¶
func New(store Store, sched RuntimeProvider, invalidate Invalidator, pub Publisher, ruleReload RuleReloader, keys KeyRegistrar, log *logx.Logger) *Service
func (*Service) AccountUsage ¶
func (s *Service) AccountUsage(ctx context.Context, accountID int64) (*domain.CodexUsageSnapshot, error)
AccountUsage 账号 codex 额度快照(纯编排零基础设施——用户裁决 2026-08-18: 缓存/并发节流/失败冷却全在 sdkbridge,service 只做凭据取 + 类型判定 + 调 用;错误分类透传——ErrAuthExpired/ErrUpstream(sdkbridge 哨兵)供 task 2 upstream_error 标记映射)。
数据流:store.GetAccountExt 取 ext 行(api-key 无 ext 行 → ErrNotFound → nil 快照零 sdkbridge 调用)→ CredentialFromExt 派生 cred(codex-oauth/ codex-pat → 非 nil cred)→ sdkbridge.GetUsageSnapshot(ctx, cred)。
func (*Service) AccountsUsage ¶
func (s *Service) AccountsUsage(ctx context.Context, ids []int64, from, to time.Time) ([]domain.AccountUsage, error)
AccountsUsage 账号 usage 批量视图(/api/admin/accounts/usage 查询面——统一 usage API spec 2026-08-18):repo 单查询聚合 + 按 ids 顺序组装全量 items (无记录账号补零——gateway 全 0,前端免补零)+ upstream 装配(task 3 AccountUsage:api-key 无凭据 → nil 快照/nil 标记;codex 成功 → 快照/nil; codex 失败 → nil/枚举标记——ErrAuthExpired → auth_expired,其余 → upstream_unavailable)。
失败语义:repo 聚合失败 → 整批失败(gateway 数据面不可用);upstream 逐 账号装配失败 → 仅记 upstream_error 标记不整批失败(单账号快照挂不影响 其余账号 gateway 栏返回)。批内装配 errgroup 有界并发(8——与 sdkbridge usageFetchSem 容量对齐:上游并发仍由 sdkbridge 恒保 ≤8,此处仅并行化编排 的 DB 往返/调用分发——调度属编排面,缓存/节流仍全在 sdkbridge);结果按 account_ids 顺序组装(goroutine 按 index 写 out,保序)。非 sdkbridge 错误 (store 故障——GetAccountExt 面)→ 记日志 + 该账号 null/null(不误标上游 问题,T2-2)。ids 去重/≤100 已由 handler 校验(service 兜底不再重复—— 防御性校验由调用方边界承担,对齐既有批量端点惯例)。
func (*Service) ChangePassword ¶
ChangePassword 修改密码(/api/user/auth/change-password):旧密码校验复用登录 语义(bcrypt 校验 + 状态检查——失败 ErrInvalidCredentials 401 同登录文案 防枚举);新密码非空 + ≤72 字节(bcrypt 截断限制,注册/建用户同款校验)→ 非法 ErrInvalidInput 400;成功 bcrypt 重哈希落库。**改密即撤销**(spec 2026-08-25-jwt-password-revocation):repo 单语句原子递增 token_version, 成功后 inv+publish 配对刷新全部实例快照 → 该用户既有 JWT 全部 401。
func (*Service) CreateAccount ¶
func (*Service) CreateGroup ¶
func (s *Service) CreateGroup(ctx context.Context, name string, visibility domain.GroupVisibility, priceMultiplier *int, protocolConverts []domain.ProtocolConvert) (*domain.Group, error)
CreateGroup 创建分组(平台容量池)。priceMultiplier 万分数:nil = 未指定 (归一 10000 = ×1,恒写入——API 边界 nullable 可表达显式 0 = 免费组); 0~100000 显式写入;超界 → 400。protocolConverts:转换方向集合(缺省 nil = 不转换)——off 元素归一剔除(空/仅 off → 空数组);非法方向/重复方向/ 同客户端格式多方向 → 400。创建后 Multipliers():新组倍率须即刻进余额倍率 快照(缺失 = ×1 计费窗口,评审 M-1 组倍率矩阵——组创建即倍率设定)。
func (*Service) CreateKey ¶
func (s *Service) CreateKey(ctx context.Context, userID int64, name string, groupID int64, maxConcurrency int, quota int64) (*domain.Key, error)
CreateKey 用户自建 key(/api/user/keys POST): 组可选性校验(public 或已授予 private)→ 用户门禁字段写库前预取(B1-1: GetUser 前置——写后注册退化为纯内存 Upsert 不可失败)→ cryptox 生成明文 → 落库 → Auth 增量纯内存 Upsert。明文长期可查看/复制(列表/详情回显)。
func (*Service) CreateRule ¶
CreateRule 创建规则:name 必填 → when/then 反序列化(未知键拒绝)→ 语义校验 → 写入 → 规则引擎 Reload。priority/name 唯一冲突 → ErrConflict(409)。
func (*Service) CreateTemplate ¶
func (*Service) CreateUser ¶
func (s *Service) CreateUser(ctx context.Context, email, password string, role domain.Role, status domain.UserStatus, maxConcurrency int, balance int64) (*domain.User, error)
CreateUser 管理面创建用户(platform_admin 专属):email 唯一/格式、密码 ≤72 字节 → bcrypt(sub2api 同参数)→ role/status/max_concurrency/balance 落库 → invalidate(新用户入 Auth 状态快照)。价格倍率按组(T3.5 修正)经 group_assignment 设置(SetGroupAssignments),用户本体无倍率字段。
func (*Service) DeactivateCode ¶
DeactivateCode 单码失效(/api/admin/redemption-codes/{id}/deactivate): 不存在 → 404 含详情;已 disabled → no-op 成功(幂等重放友好,决策 6)。
func (*Service) DeactivateCodesBatch ¶
DeactivateCodesBatch 批量失效(/api/admin/redemption-codes/batch-deactivate, 决策 6):validateIDs → 逐 id 先查(缺失 id → 404 含缺失详情,对齐批量删除 范式)→ DeactivateCodes 单事务(已 disabled no-op)→ 返回新失效数。 先查后失效窗口竞态可接受:失效不新增行,检查到的 id 不会消失(评审 M-2)。
func (*Service) DeleteAccount ¶
func (*Service) DeleteAccountsBatch ¶
func (*Service) DeleteGroup ¶
DeleteGroup 删除组:删组前校验组内账号(含账号 → 409 "group has accounts", F1 契约修正——软删 UPDATE 无 FK 约束,不再依赖仓库错误兜底)、前置清理组内 全部 key(key.group_id 外键约束;Auth 增量清理),再删组。key 清理与组删除 非同一事务——组删除失败时 key 已删,重试删除即可(key 被删组未删的中间态 不提供服务——Auth 快照已移除)。
func (*Service) DeleteGroupsBatch ¶
func (*Service) DeletePriceEntry ¶
func (*Service) DeleteRule ¶
DeleteRule 删除规则;不存在 → ErrNotFound(消息含 id)。成功后规则引擎 Reload。
func (*Service) DeleteRulesBatch ¶
DeleteRulesBatch 批量删除规则(事务,全成或全败);ids 1–100 去重; 缺 id → ErrNotFound(消息含缺失 id)。成功后规则引擎 Reload。
func (*Service) DeleteTemplate ¶
func (*Service) DeleteTemplatesBatch ¶
func (*Service) GenerateCodes ¶
func (s *Service) GenerateCodes(ctx context.Context, req GenerateRequest, createdBy int64) ([]*domain.RedemptionCode, error)
GenerateCodes 批量生成兑换码(1..1000):校验 → 生成 count 个随机码 → CreateCodes 批量插入;code 唯一冲突(repository.ErrConflict)重试换新码 (N=5,碰撞概率 ~0,兜底防御)。created_by 0 = 系统,>0 = platform_admin 用户 id(决策 5,无 NULL 分支)。
func (*Service) GetAccount ¶
func (*Service) GetAccountExt ¶
GetAccountExt 账号 ext 行(编辑回显)。账号缺 id → 404。
func (*Service) GetAccountGroups ¶
GetAccountGroups 账号的分组 id 列表(编辑回显)。账号缺 id → 404。
func (*Service) GetCode ¶
GetCode 按 id 取兑换码(只读访问器;缺失 → 404 含详情)。admin 审计端点 /redemption-codes/{id}/uses 的面值换算需要码的 type(use 行不存类型), handler 边界先取码再换算——存储语义不变。
func (*Service) GetCodeUses ¶
func (s *Service) GetCodeUses(ctx context.Context, codeID int64, q repository.ListQuery) ([]*domain.RedemptionUse, int64, error)
GetCodeUses 某码的兑换记录(审计,/api/admin/redemption-codes/{id}/uses): 码不存在 → 404(mapRepoErr 含详情)。use 快照不存码类型——handler 需先 GetCode 取 type 做面值换算。q 直透 ListCodeUses(limit/offset 缺省归一在 repo——≤0→20/<0→0;spec 2026-08-17 补分页参数)。
func (*Service) GetGroupAssignments ¶
func (s *Service) GetGroupAssignments(ctx context.Context, groupID int64) ([]int64, map[int64]*int, error)
GetGroupAssignments 读取组当前授予用户与专属倍率(GET /api/admin/groups/{id}/ assignments;组缺失 → 404)。mults 只含该组有专属倍率的用户(nil/缺省 = 未设置 → 用组倍率)。
func (*Service) GetPriceEntry ¶
func (*Service) GetSettings ¶
GetSettings 全部设置(默认值 + DB 覆盖;/api/admin/settings GET)。
func (*Service) GetTemplate ¶
func (*Service) GetTemplateExt ¶
func (s *Service) GetTemplateExt(ctx context.Context, templateID int64) (*domain.TemplateExt, error)
GetTemplateExt 模板 ext 行(编辑回显)。模板缺 id → 404。
func (*Service) GetUserGroups ¶
GetUserGroups 读取用户被授予的组与各专属倍率(GET /api/admin/users/{id}/groups; 用户缺失 → 404)。mults 只含该用户有专属倍率的组。
func (*Service) ImportCodexOAuthAccounts ¶
func (s *Service) ImportCodexOAuthAccounts(ctx context.Context, items []domain.CodexOAuthImportItem, tplID, groupID *int64) (*domain.ImportResult, error)
ImportCodexOAuthAccounts 批量导入 codex-oauth 凭据(ServerInterface 依赖面): 模板顶层校验(缺/不存在 → 400/404;**credential_type 必须 == codex-oauth—— 错配 → 400 整批拒绝**,防违反 ext 类型 == 模板类型的硬不变量 ext_codex.go:238); 逐行类型特定校验(必填/成对/expires RFC3339/email 格式——失败 → 行级 failed 收集继续);共享核心落库。
func (*Service) ImportCodexPATAccounts ¶
func (s *Service) ImportCodexPATAccounts(ctx context.Context, items []domain.CodexPATImportItem, tplID, groupID *int64) (*domain.ImportResult, error)
ImportCodexPATAccounts 批量导入 codex-pat 凭据(结构同 oauth 端点;模板 credential_type 必须 == codex-pat)。
func (*Service) ListAccountViews ¶
func (s *Service) ListAccountViews(ctx context.Context, q repository.ListQuery) ([]*AccountView, int64, error)
ListAccountViews 账号管理端视图(含调度器运行时信息)。handler 列表入口, 与 ListAccounts 一致做 sort/order 校验(非法 → ErrInvalidInput → 400)。
func (*Service) ListAccounts ¶
func (*Service) ListAdminKeys ¶
func (s *Service) ListAdminKeys(ctx context.Context, q repository.ListQuery) ([]*domain.Key, int64, error)
ListAdminKeys 管理端全量 key 列表(/api/admin/keys,spec 2026-08-16):全量 视角(不限归属用户)+ name/user_id/group_id 筛选 + sort 白名单 id/name/created_at。脱敏在 handler 转换面(AdminKey 无 key 明文字段—— 用户裁决,明文绝不下发管理端)。
func (*Service) ListCodes ¶
func (s *Service) ListCodes(ctx context.Context, q repository.ListQuery, typ *domain.RedemptionType, status *domain.RedemptionStatus) ([]*domain.RedemptionCode, int64, error)
ListCodes 兑换码列表(/api/admin/redemption-codes):type/status 筛选 (nil = 不过滤;非法枚举 → 400)+ sort 白名单校验(非法 → 400)。
func (*Service) ListGroups ¶
func (*Service) ListGroupsForUser ¶
ListGroupsForUser 用户可选组列表(public 全部 + 已授予 private;/api/user/groups 只读,key 创建时选组)。
func (*Service) ListKeys ¶
func (s *Service) ListKeys(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.Key, int64, error)
ListKeys 用户自己的 key 列表(/api/user/keys GET)。
func (*Service) ListMailTemplates ¶
ListMailTemplates 管理面列表(DB 行与默认合成,缺行用默认回填)。
func (*Service) ListMyRedemptions ¶
func (s *Service) ListMyRedemptions(ctx context.Context, userID int64, q repository.ListQuery) ([]*domain.RedemptionRecord, int64, error)
ListMyRedemptions 我的兑换记录(/api/user/redemptions):use 快照 + 码的 type/remark 联查;userID 由 handler 从 JWT 取(强制本人数据,防越权)。sort/order 白名单 校验(非法 → 400)。
func (*Service) ListPriceEntries ¶
func (s *Service) ListPriceEntries(ctx context.Context, q repository.ListQuery, source *domain.PricingSource, mode *domain.PriceMode, provider *string, model string) ([]*domain.PriceEntry, int64, error)
func (*Service) ListPriceVariants ¶
func (*Service) ListTempBalances ¶
func (s *Service) ListTempBalances(ctx context.Context, q repository.ListQuery, userID int64) ([]*domain.TempBalance, int64, error)
ListTempBalances 管理侧临时额度全量列表(/api/admin/temp-balances;userID 0 = 全部用户;sort/order 白名单校验——非法 → ErrInvalidInput 400)。
func (*Service) ListTemplates ¶
func (*Service) ListUserTempBalances ¶
func (s *Service) ListUserTempBalances(ctx context.Context, userID int64) ([]*domain.TempBalance, error)
ListUserTempBalances 当前用户有效临时额度(/api/user/temp-balances;userID 由 handler 强制 = 当前用户,无 user_id 参数防越权——对齐 /api/user/stats 模式)。
func (*Service) ListUsers ¶
func (s *Service) ListUsers(ctx context.Context, q repository.ListQuery) ([]*domain.User, int64, error)
ListUsers 用户列表(/api/admin/users;platform_admin 专属)。
func (*Service) Overview ¶
func (s *Service) Overview(ctx context.Context, utcDay time.Time, days int, groupID int64) (*OverviewData, error)
Overview 管理端总览聚合(/api/admin/overview 服务端聚合面):
summary = [utcDay, utcDay+1d) 区间单行 sum(SQL 侧);
trend = [utcDay-(days-1)d, utcDay+1d) 日桶(SQL 侧 GROUP BY
date_trunc('day', bucket_time)——usage_stats 分区键 range 毫秒级);
accounts/err_top = 调度器快照遍历(O(N) 冷面,30s 缓存摊薄);
resources = 三表冷面 count。
utcDay 由调用方传入(handler 缓存键与聚合区间同一日界源——跨 UTC 午夜 滚转不漂移);days 已由调用方钳制 [1,30];groupID > 0 = 按组过滤 summary/trend(accounts/err_top/resources 为全局面,spec 参数语义)。
func (*Service) PreviewPricingSync ¶
func (s *Service) PreviewPricingSync(ctx context.Context) (*PricingPreview, error)
func (*Service) PriceSourceURL ¶
func (*Service) PriceSyncCron ¶
func (*Service) QueryEntityTrend ¶
func (s *Service) QueryEntityTrend(ctx context.Context, q EntityTrendQuery) ([]*domain.EntityStatBucket, error)
QueryEntityTrend 单实体时间趋势(实体类型白名单前置拦截;EntityID 合法性由 数据语义兜底——卷积表无 ID=0 行,零值自然返回空集)。
func (*Service) QueryErrLogs ¶
func (s *Service) QueryErrLogs(ctx context.Context, q repository.ErrLogQuery) ([]*domain.UsageLog, error)
QueryErrLogs err_logs 错误明细分页查询(/err_logs API:完整错误面——拒绝 + 异常双轨,status_code/error_type 全值;行类型同为 *domain.UsageLog—— err_logs 表复用该领域类型)。keyset 游标分页同 QueryUsages。
func (*Service) QueryStatsTTFT ¶
QueryStatsTTFT TTFT 分位数卡片,双分支独立上限(Momus M5):
- EntityType == "":sketch 分支(cube hist 服务端合并),桶数 ≤ MaxStatsSketchBuckets;
- 非空:exact 分支(usage_logs percentile_cont),必须配 EntityID ≠ 0 且 entityType 过白名单,跨度 ≤ MaxStatsTTFTExactSpan。
校验通过后经 statsTTFTC TTL 缓存(P3 验收遗留尾巴:exact 冷缓存 × 系统饱和 排序致负载 p99 5-6s;仪表盘同参轮询命中率天然高,陈旧 ≤30s 为展示面可 接受语义——overview 先例)。
func (*Service) QueryStatsTop ¶
func (s *Service) QueryStatsTop(ctx context.Context, q TopQuery) ([]*domain.EntityStatBucket, error)
QueryStatsTop 实体排行(limit 归一化后透传;排序键/实体类型白名单前置拦截)。
func (*Service) QueryStatsTrend ¶
func (s *Service) QueryStatsTrend(ctx context.Context, q TrendQuery) ([]*domain.StatBucket, error)
QueryStatsTrend cube 时间趋势(校验顺序:必填 → to>from → 跨度 → 粒度白名单)。
func (*Service) QueryUsages ¶
func (s *Service) QueryUsages(ctx context.Context, q repository.UsageQuery) ([]*domain.UsageLog, error)
QueryUsages usage_logs 计费明细分页查询(/usage_logs API;错误行含 abort/failover 半异常标记——error_type 过滤保留)。keyset 游标分页: 返回行可能含 limit+1 探测行,next_cursor 组装在 handler。 返回 []*domain.UsageLog 直透(不再擦除为 []any——spec 2026-08-17 边界收敛, 类型信息保留,handler 断言删除)。
func (*Service) Redeem ¶
func (s *Service) Redeem(ctx context.Context, code string, userID int64) (*domain.RedemptionApply, error)
Redeem 兑换(/api/user/redemptions POST,决策 7/10-12 编排): 单事务内按序——① GetByCode 定位码(不存在 → 400 invalid code); ② GetUse 先查本用户已兑换(评审 M-1:重复请求稳定 409,不因码状态漂移); ③ 码状态检查(disabled/过期 → 400 invalid code,统一不泄露具体原因); ④ applier 应用资源(只经 tx 面,失败整体回滚——评审 I-1); ⑤ CreateUse 审计 + IncrementUsed 条件递增(false = 用尽 → 400 整体回滚, 防并发超卖——评审 I-2)。提交成功后 invalidate() 刷新 auth 快照(决策 8)。
func (*Service) RegisterUser ¶
RegisterUser 注册(注册即登录——handler 侧签发 JWT): 快照读 settings.signup_enabled 开关(UpdateSetting 即时生效)→ email 唯一/格式 → 密码 ≤72 字节 → bcrypt DefaultCost(10)(sub2api 同参数)→ 快照读 4 个新用户初始资源默认值 → CreateUser → temp_balance > 0 送临时 额度(插行失败不阻断注册,评审 M-2)。
func (*Service) RegisterUserWithCode ¶
func (s *Service) RegisterUserWithCode(ctx context.Context, email, password, code string) (*domain.User, error)
RegisterUserWithCode 带验证码校验的注册入口(handler 侧根据 verif 开关分发)。
func (*Service) ReloadFunctionPricing ¶
func (s *Service) ReloadFunctionPricing()
ReloadFunctionPricing intentional dispatch point over unified snapshot (not compat shim).
func (*Service) ReloadFunctionPricingCtx ¶
func (*Service) ReloadImagePricing ¶
func (s *Service) ReloadImagePricing()
ReloadImagePricing intentional dispatch point over unified snapshot (not compat shim).
func (*Service) ReloadImagePricingCtx ¶
func (*Service) ReloadPricing ¶
func (s *Service) ReloadPricing()
func (*Service) ReloadSettings ¶
ReloadSettings settings 快照全量重载(invalidate.SettingsReloader 接口实现, T3 main 装配注入 invalidate.Config.Settings;供 NOTIFY settings 分支触发全 实例重载)。与 UpdateSetting 内部路径同实现(reloadSettings 复用);失败 返回错误由调用方(去抖器 reloadAll)Warn。
func (*Service) RenderTemplate ¶
func (s *Service) RenderTemplate(ctx context.Context, purpose domain.EmailTemplatePurpose, vars map[string]string) (string, string, error)
RenderTemplate 渲染模板:缺行走编译内置默认;仅替换 {{code}}/{{ttl_minutes}}/{{app_name}}。
func (*Service) ReplacePriceVariants ¶
func (s *Service) ReplacePriceVariants(ctx context.Context, model string, variants []*domain.PriceVariant) ([]*domain.PriceVariant, error)
func (*Service) ResetAccountsCooldownBatch ¶
ResetAccountsCooldownBatch 批量重置账号冷却:validateIDs → 预取目标 存在性 + 旧组并集 → 合成 patch {Status:active, CooldownUntil:now} 复用 UpdateAccountsBatch(触发既有 failed_at/last_error 清理 + 新冷却写入) → 恢复审计 → 组级失效 + NOTIFY。
func (*Service) ResetPassword ¶
ResetPassword 验证码校验后更新密码。**重置即撤销**(spec 2026-08-25-jwt- password-revocation):repo 单语句原子递增 token_version,成功后 inv+publish 配对刷新全部实例快照 → 该用户既有 JWT 全部 401,需重新登录。
func (*Service) ResolvePrices ¶
func (s *Service) ResolvePrices(model string, promptTokens int64, tier string, at time.Time) (domain.ResolvedPrices, bool)
ResolvePrices 模型价格解析:快照零 DB 读 + 委托 domain 解析核 (entry→基底→首中变体;纯函数与测试假实现共用,防逻辑漂移)。
func (*Service) RotateKey ¶
RotateKey 轮换自己的 key(/api/user/keys/{id}/rotate):新明文落库;旧明文 增量移除(立即失效)、新明文增量注册。用户门禁字段写库前预取(B1-1: GetUser 前置——Delete 后只剩不可失败的内存 Upsert,失败窗口整体消失—— DB 已轮换只留新明文时新 raw 永不蒸发)。
func (*Service) SendForgotPasswordCode ¶
SendForgotPasswordCode 忘记密码发码:恒 200 空转,实际发送仅当 enabled && 账号存在 && 未限频。
func (*Service) SendRegisterCode ¶
SendRegisterCode 注册验证码发送(public endpoint):
func (*Service) ServiceTierPolicy ¶
func (s *Service) ServiceTierPolicy(tier billing.Tier) billing.TierPolicyMode
func (*Service) SetEmailCodeStore ¶
func (s *Service) SetEmailCodeStore(store EmailCodeStore)
SetEmailCodeStore 注入验证码存储(spec 2026-08-25-emailcode-redis-migration §2.2): 实现 = verification.Store(Redis HASH)。Redis 必选依赖 ⇒ 无 nil 分支,收到 nil 直接 panic fail-fast(与 redisx.Open 的 Ping fail-fast 同纪律)。main 在 svc 构造后回填;测试经同 setter 注入 fake。
func (*Service) SetGroupAssignments ¶
func (s *Service) SetGroupAssignments(ctx context.Context, groupID int64, userIDs []int64, mults map[int64]*int) ([]int64, map[int64]*int, error)
SetGroupAssignments 替换语义设置组的授予用户(PUT /api/admin/groups/{id}/ assignments):完整列表 = 授予结果(未列出即撤销,空数组 = 清空)。 幂等(Grant/Revoke 本身幂等);用户/组必须存在且组未软删(缺失/软删 → 404, F3)。整个替换循环包 WithTx(S3-F2):Grant/SetMultiplier/Revoke/组内读同一 事务,中途失败整体回滚——不再出现混合授予态。 mults 可选:user_id → 该用户在该组的专属价格倍率(万分数,T3.5 修正:按 组——用户在不同组可有不同倍率;nil 值 = 清除为未设置 → 回退组倍率;0 = 免费)。mults 的 key 必须 ⊆ userIDs(未列出的用户不改动既有倍率;未知用户 → 400)。返回生效的 user_ids 列表 + 该组各授予用户的 post-state 倍率 (user_ids 全量,nil = 未设置;response 回显用)。 授予/倍率变更影响计费倍率快照 → Multipliers() 定向刷新(assignment 倍率 变更不依赖全量 Reload)。
func (*Service) SetLocalDispatcher ¶
func (s *Service) SetLocalDispatcher(d notify.Dispatcher)
SetLocalDispatcher 注入本地变更分发器(#36 本地实例即时重算):main 装配序 上 dispatcher 需要 svc(SettingsReloader)、svc 需要 dispatcher(本地分发) ——构造环,svc 构造完成后回填(与 invalidate.Debouncer.SetSettings 同模式)。 未注入 = 单实例/测试:settings 变更不做本地 scope 分发(预算重算由 60s auth-sync / 下次变更兜底收敛,单实例无多实例分摊语义)。
func (*Service) SetMailEnqueue ¶
func (s *Service) SetMailEnqueue(fn func(MailSendTask) error)
SetMailEnqueue 注入邮件入队函数(D-W1异步化:svc 构造后回填 mailW.Enqueue—— 循环依赖先例 SetLocalDispatcher;未注入 → SendRegisterCode 退化为 ErrMailNotConfigured)。
func (*Service) SetPriceFetcher ¶
func (*Service) SetTimeLocation ¶
SetTimeLocation 注入定价时段解释用时区(D-TZ2):nil = 进程本地(现状), 非 nil = at.In(tzLoc) 后再进 domain.ResolveEntryPrices(零热路径额外 DB/锁)。
func (*Service) SetUsageSnapshotter ¶
func (s *Service) SetUsageSnapshotter(u CodexUsageSnapshotter)
SetUsageSnapshotter 装配 codex 额度快照数据源(main:codexAdapter 构造后调 用;nil = 未装配——AccountUsage 对 codex 账号返回 nil 快照,不 panic)。
func (*Service) SetUserGroups ¶
func (s *Service) SetUserGroups(ctx context.Context, userID int64, groupIDs []int64, mults map[int64]*int) ([]int64, map[int64]*int, error)
SetUserGroups 替换语义设置用户的授予组(PUT /api/admin/users/{id}/groups): group_ids = 完整授予组列表(未列出即撤销,空数组 = 清空)。multipliers 仅对 group_ids 中的组生效(key 必须 ∈ group_ids,否则 400;null = 清除为未设置 → 回退组倍率;未列出的组 = 撤销,谈不上倍率)。校验:用户存在(404)、组存在 且未软删(404,F3 逐组同校验)、非法/重复 id 与越界倍率(400);组数上限与 SetGroupAssignments 对齐 ≤100。整个替换循环(含逐组读)包 WithTx(S3-F2): 逐组读与写同一事务,中途失败整体回滚——不再出现混合授予态。实现按组复用组 维度替换核心:对每个目标组读现成员 → 现成员 ∪ {userID} 作为新授予集合 (SetAssignmentMultiplier 只传该用户,其他成员不传 = 沿用现倍率,互不影响); 不在 group_ids 的当前授予组逐个 RevokeGroup(组本身不动,组内其他用户保留)。 返回生效的 group_ids + 各授予组 post-state 倍率。
func (*Service) SyncPricingNow ¶
func (s *Service) SyncPricingNow(ctx context.Context) (*PricingSyncStats, error)
func (*Service) UpdateAccount ¶
func (*Service) UpdateAccountsBatch ¶
func (s *Service) UpdateAccountsBatch(ctx context.Context, ids []int64, p repository.AccountPatch) error
func (*Service) UpdateGroup ¶
func (*Service) UpdateGroupsBatch ¶
func (s *Service) UpdateGroupsBatch(ctx context.Context, ids []int64, p repository.GroupPatch) error
UpdateGroupsBatch 批量更新组(仅 name/visibility——GroupPatch 无倍率字段, 不触发任何快照重载;倍率批量变更走单组 UpdateGroup)。
func (*Service) UpdateKey ¶
func (s *Service) UpdateKey(ctx context.Context, userID, keyID int64, name *string, status *domain.KeyStatus, maxConcurrency *int, quota *int64) (*domain.Key, error)
UpdateKey 更新自己的 key(name/status/max_concurrency/quota;nil 字段不变)。 patch 化(S3-F1):只把显式字段传给 repo(nil = 不改),不再全行快照写回—— 并发两个 PUT 改不同字段各自生效(不再静默覆盖先写者)。全 nil = 无变更, 直接返回当前行(零写库零发布)。 变更后 Auth 增量 Upsert(禁用/额度调整即时生效——评审 I-2 的 key 级路径)。
func (*Service) UpdateMailTemplate ¶
func (s *Service) UpdateMailTemplate(ctx context.Context, purpose, subject, bodyText string) (*domain.EmailTemplate, error)
UpdateMailTemplate 管理面更新;空 bodyText 删除行=还原默认。
func (*Service) UpdateRule ¶
UpdateRule 部分更新规则:未提供字段保持原值;校验合并后的完整 when/then; 不存在 → ErrNotFound(消息含 id)。成功后规则引擎 Reload。
func (*Service) UpdateSetting ¶
UpdateSetting 类型化校验后更新(/api/admin/settings PUT): key ∈ 内置注册表(未知 key → 400);switch 必须 true/false;number 必须 数字且落在注册表 Min/Max 值域内(负值/越界 → 400);带 PolicyValues 枚举 域的条目(service_tier_policy_*)必须命中枚举。更新成功后同步内存快照—— 注册等读路径即时生效;本地直连分发器按 scope 精确重载(#36 auth gate 预算 按新 N 即时重算)+ NOTIFY 广播其余实例。
func (*Service) UpdateTemplate ¶
func (*Service) UpdateTemplatesBatch ¶
func (s *Service) UpdateTemplatesBatch(ctx context.Context, ids []int64, p repository.TemplatePatch) error
func (*Service) UpdateUser ¶
UpdateUser 用户管理面更新(role/status/max_concurrency/balance 按 patch 显式 字段生效——未提供字段不触碰 DB 列,杜绝 GET 快照陈旧值写回覆盖计费扣费)。 校验按 patch 字段生效(评审 P3-B:只改 balance 的 PUT 不被未提供字段的 零值误拒)。balance/max_concurrency 显式设置 → 条件更新(旧值 = GET 快照); 0 行 → 重读当前值刷新旧值条件重试 ≤3 次 → 超限 ErrConflict(409)。用户 状态/并发/额度变更 → invalidate → Auth.Reload 全量刷新(评审 I-2)。价格 倍率按组(T3.5 修正)经 group_assignment 设置,用户本体无倍率字段。
func (*Service) UpsertAccountExt ¶
func (s *Service) UpsertAccountExt(ctx context.Context, e *domain.AccountExt) (*domain.AccountExt, error)
UpsertAccountExt 幂等写入账号 ext 行。账号缺 id → 404。 类型一致性:ext 行 credential_type 必须与父行(账号所属模板)的 credential_type 一致(账号无独立类型列,类型继承自模板)——不一致 → 400。 身份恒等式(thread==session、window={thread}:0 零透传):显式部分提供自动 补齐(normalizeCodexIdentity);成对冲突 → 400;存量行上 window-only 反推 ≠ 存量 thread → 400(B1-3 方向 2:派生值不得冒充显式值改身份)。 身份四元组自动管理:无存量行 → NewCodexIdentity() 生成四元组并经 TryInsert(ON CONFLICT DO NOTHING 先写者胜)原子首写——并发双导入同一账号 不覆盖不报错,冲突方完全采用赢者身份后走普通 upsert 写令牌(B1-3 方向 3: 显式身份只在首写成功路径生效);后续写入缺省 → 沿用存量(持久复用,账号 存在期间稳定);调用方显式提供 → 采用。email 不在缺省沿用面——未提供 → NULL 清空(B1-5 契约)。 校验先于落库(B1-2):window 派生 + 列组校验在 TryInsert 之前——被拒凭据 零残留(400 前不写库;含 NULL window 问题同步消除);终校验保留(冲突路径 重改 e 后,早校验覆盖不到)。 W1 不接线失效/发布。
func (*Service) UpsertPriceEntry ¶
func (s *Service) UpsertPriceEntry(ctx context.Context, m *repository.PriceEntryManual) (*domain.PriceEntry, error)
validation helpers
func (*Service) UpsertTemplateExt ¶
func (s *Service) UpsertTemplateExt(ctx context.Context, e *domain.TemplateExt) (*domain.TemplateExt, error)
UpsertTemplateExt 幂等写入模板 ext 行(Create/Update 合一;update 全列更新 含 NULL 清空)。模板缺 id → 404(FK 由仓库保证)。 类型一致性:ext 行 credential_type 必须与父模板的 credential_type 一致 (api_key 模板无 ext 行;special/oauth/pat 模板只能挂同类型行)——不一致 → 400。 W1 不接线失效/发布——消费(快照加载/调度)留给 W3/W4/W6。
func (*Service) UserEmails ¶
UserEmails 批量取邮箱(/api/admin/users-top TopN 回填;users 表无 name 列—— 仅 email;id IN 一次查询)。缺失 id 不在 map(handler 兜底空串)。
func (*Service) UserStats ¶
func (s *Service) UserStats(ctx context.Context, userID int64, q EntityTrendQuery) ([]*domain.EntityStatBucket, error)
UserStats 用户台自己的用量趋势:忽略调用方传入的任何 entity 参数,userID 钉死注入(JWT 身份即过滤条件,防越权只看 service 层这一道钉死)。
func (*Service) UserStatsTTFT ¶
func (s *Service) UserStatsTTFT(ctx context.Context, userID int64, q TTFTQuery) (*domain.TTFTSummary, error)
UserStatsTTFT 用户台自己的 TTFT 卡片(self 钉死同 UserStats;恒走 exact 分支)。
type SettingStore ¶
type SettingStore interface {
GetSetting(ctx context.Context, key string) (*domain.Setting, error)
GetAllSettings(ctx context.Context) ([]*domain.Setting, error)
SetSetting(ctx context.Context, key string, typ domain.SettingType, value string) (*domain.Setting, error)
}
SettingStore 类型化配置持久化(Phase 3a)。
type StatStore ¶
type StatStore interface {
// /api/admin/overview 聚合面(spec 2026-08-14):SQL 侧聚合(F-P2-2 形态——
// 服务端 GROUP BY 返回日桶,不拉全行客户端聚合)。
SummarizeStats(ctx context.Context, from, to time.Time, groupID int64) (*repository.StatSummary, error)
ScanStatsDays(ctx context.Context, from, to time.Time, groupID int64) ([]*repository.StatDayAgg, error)
CountOverviewResources(ctx context.Context) (*repository.OverviewResourceCounts, error)
StatsTrend(ctx context.Context, from, to time.Time, unit string, groupID int64, model string) ([]*domain.StatBucket, error)
StatsTop(ctx context.Context, from, to time.Time, entityType string, by string, limit int) ([]*domain.EntityStatBucket, error)
StatsEntityTrend(ctx context.Context, from, to time.Time, unit string, entityType string, entityID int64, model string) ([]*domain.EntityStatBucket, error)
StatsTTFTSketch(ctx context.Context, from, to time.Time, model string) (*domain.TTFTSummary, error)
StatsTTFTExact(ctx context.Context, from, to time.Time, entityType string, entityID int64, model string) (*domain.TTFTSummary, error)
}
type Store ¶
type Store interface {
TemplateStore
AccountStore
GroupStore
KeyStore
GroupAssignmentStore
UserStore
SettingStore
RuleStore
LogStore
StatStore
RedemptionStore
PricingStore
TemplateExtStore
AccountExtStore
EmailTemplateStore
// EmailCodeStore 不在复合面:验证码已迁 Redis(spec 2026-08-25-emailcode-
// redis-migration §2.2/§2.3),经 SetEmailCodeStore 独立注入,repository
// 实现已随 PG 验证码表卸载。
// WithTx 在单事务内执行 fn(评审 I-1):真实仓库为 tx 版 Repository(全部走
// tx 连接);fake 为事务语义模拟(fn 内变更先入暂存、成功提交/失败丢弃——
// 回滚断言的前提)。
WithTx(ctx context.Context, fn func(repository.TxStore) error) error
}
type TTFTQuery ¶
TTFTQuery /stats/ttft 入参。EntityType 空 = 平台级 sketch 分支(cube hist 合并);非空 = 实体级 exact 分支(usage_logs percentile_cont)。
type TemplateExtStore ¶
type TemplateExtStore interface {
UpsertTemplateExt(ctx context.Context, e *domain.TemplateExt) (*domain.TemplateExt, error)
GetTemplateExt(ctx context.Context, templateID int64) (*domain.TemplateExt, error)
}
TemplateExtStore 模板类型化扩展持久化(template_ext 1:1;W1 数据层 CRUD, 消费接线留给 W3/W4/W6)。
type TemplateStore ¶
type TemplateStore interface {
CreateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)
GetTemplate(ctx context.Context, id int64) (*domain.Template, error)
// GetTemplatesByIDs 批量取模板(id IN 一次查询——UpdateTemplatesBatch 类型-
// 格式约束校验用,避免逐 id N+1);缺失 id 不报错(数量 < 请求数)。
GetTemplatesByIDs(ctx context.Context, ids []int64) ([]*domain.Template, error)
ListTemplates(ctx context.Context, q repository.ListQuery) ([]*domain.Template, int64, error)
UpdateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)
DeleteTemplate(ctx context.Context, id int64) error
DeleteTemplatesBatch(ctx context.Context, ids []int64) error
UpdateTemplatesBatch(ctx context.Context, ids []int64, p repository.TemplatePatch) error
}
type TopQuery ¶
type TopQuery struct {
From time.Time
To time.Time
EntityType string
By string
Limit int // ≤0 → 20;>200 裁剪到 200
}
TopQuery /stats/top 入参(EntityType ∈ account|user|key;By ∈ cost|requests|tokens)。
type TrendQuery ¶
type TrendQuery struct {
From time.Time
To time.Time
Granularity string // hour|day;空 = day
GroupID int64
Model string
}
TrendQuery /stats/trend 入参(GroupID > 0 / Model 非空 = 过滤,零值不过滤)。
type UserStore ¶
type UserStore interface {
CreateUser(ctx context.Context, u *domain.User) (*domain.User, error)
GetUser(ctx context.Context, id int64) (*domain.User, error)
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
// CountUsers 用户总数(注册 bootstrap 用:表空 = 首个注册 = platform_admin)。
CountUsers(ctx context.Context) (int64, error)
ListUsers(ctx context.Context, q repository.ListQuery) ([]*domain.User, int64, error)
UpdateUser(ctx context.Context, p *repository.UserPatch) (*domain.User, error)
UpdateUserPassword(ctx context.Context, id int64, passwordHash string) error
// 原子资源更新(评审 I-1:兑换码 applier 用;普通 client 与 tx client 均可用)。
UpdateUserBalance(ctx context.Context, userID, delta int64) error
UpdateUserMaxConcurrency(ctx context.Context, userID int64, value int) error
// CreateTempBalance 创建临时额度行(注册赠品、兑换码兑换等;user_id 外键必
// 存在)。expiresAt/note 为 nil 时不落该列(nil = 永久;兑换码路径必非零)。
CreateTempBalance(ctx context.Context, userID int64, amount int64, expiresAt *time.Time, note *string) error
// ListUserTempBalances 用户侧有效临时额度(/api/user/temp-balances:amount > 0
// 且未过期,expires_at 升序——PG ASC 默认 NULLS LAST,永久最后,与
// SettleFefoBatch 扣费顺序同源)。
ListUserTempBalances(ctx context.Context, userID int64) ([]*domain.TempBalance, error)
// ListTempBalances 管理侧全量临时额度(/api/admin/temp-balances:含过期/用尽/
// 负扣减行——全量视角;userID 0 = 全部;sort 白名单 + 分页)。
ListTempBalances(ctx context.Context, q repository.ListQuery, userID int64) ([]*domain.TempBalance, int64, error)
// ListUserEmails 批量取邮箱(/api/admin/users-top TopN 回填;id IN 一次查询)。
ListUserEmails(ctx context.Context, ids []int64) (map[int64]string, error)
}
UserStore 用户持久化(Phase 3a)。