service

package
v0.0.1-beta.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: AGPL-3.0 Imports: 35 Imported by: 0

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

View Source
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 钉死)。

View Source
const EmailVerificationRequired = "email verification required"

EmailVerificationRequired 缺验证码时 400 响应的固定哨兵片段(前端发现机制)。

Variables

View Source
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))同一哨兵实例语义。

View Source
var (
	ErrInvalidCredentials = serviceerr.ErrInvalidCredentials
	ErrSignupDisabled     = serviceerr.ErrSignupDisabled
)

错误哨兵定义下沉 internal/service/errors(叶子包,单一真相);别名 re-export 保持既有引用同一实例语义。

View Source
var ErrGroupNotEligible = fmt.Errorf("%w: group is private and not granted to user", ErrInvalidInput)

ErrGroupNotEligible private 组未授予(key 创建组可选性校验;包装 ErrInvalidInput → 400)。

View Source
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

type KeyRegistrar interface {
	Upsert(hash string, meta domain.KeyMeta)
	Delete(hash string)
}

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。

func (*MailWorker) Start

func (w *MailWorker) Start(ctx context.Context) error

Start 启动单条 sender goroutine(panic 自捕,对齐 worker.Manager.Go 模式)。

func (*MailWorker) Stats

func (w *MailWorker) Stats() any

Stats 实现 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

type OverviewErrTop struct {
	Name     string
	ErrRate  float64
	ErrCount int
}

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 PricingPreviewEntry struct {
	Model  string `json:"model"`
	Mode   string `json:"mode"`
	Action string `json:"action"` // add/update
}

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 PricingSyncStats struct {
	Rows     int
	Skipped  int
	Updated  int
	Variants int
}

type Publisher

type Publisher interface {
	Publish(ctx context.Context, ch notify.Change) error
}

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

type RuleReloader interface {
	Reload(ctx context.Context) error
}

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

func (s *Service) ChangePassword(ctx context.Context, userID int64, old, new string) error

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 (s *Service) CreateAccount(ctx context.Context, a *domain.Account) (*domain.Account, error)

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

func (s *Service) CreateRule(ctx context.Context, in RuleInput) (*domain.Rule, error)

CreateRule 创建规则:name 必填 → when/then 反序列化(未知键拒绝)→ 语义校验 → 写入 → 规则引擎 Reload。priority/name 唯一冲突 → ErrConflict(409)。

func (*Service) CreateTemplate

func (s *Service) CreateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)

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

func (s *Service) DeactivateCode(ctx context.Context, id int64) error

DeactivateCode 单码失效(/api/admin/redemption-codes/{id}/deactivate): 不存在 → 404 含详情;已 disabled → no-op 成功(幂等重放友好,决策 6)。

func (*Service) DeactivateCodesBatch

func (s *Service) DeactivateCodesBatch(ctx context.Context, ids []int64) (int64, error)

DeactivateCodesBatch 批量失效(/api/admin/redemption-codes/batch-deactivate, 决策 6):validateIDs → 逐 id 先查(缺失 id → 404 含缺失详情,对齐批量删除 范式)→ DeactivateCodes 单事务(已 disabled no-op)→ 返回新失效数。 先查后失效窗口竞态可接受:失效不新增行,检查到的 id 不会消失(评审 M-2)。

func (*Service) DeleteAccount

func (s *Service) DeleteAccount(ctx context.Context, id int64) error

func (*Service) DeleteAccountsBatch

func (s *Service) DeleteAccountsBatch(ctx context.Context, ids []int64) error

func (*Service) DeleteGroup

func (s *Service) DeleteGroup(ctx context.Context, id int64) error

DeleteGroup 删除组:删组前校验组内账号(含账号 → 409 "group has accounts", F1 契约修正——软删 UPDATE 无 FK 约束,不再依赖仓库错误兜底)、前置清理组内 全部 key(key.group_id 外键约束;Auth 增量清理),再删组。key 清理与组删除 非同一事务——组删除失败时 key 已删,重试删除即可(key 被删组未删的中间态 不提供服务——Auth 快照已移除)。

func (*Service) DeleteGroupsBatch

func (s *Service) DeleteGroupsBatch(ctx context.Context, ids []int64) error

func (*Service) DeleteKey

func (s *Service) DeleteKey(ctx context.Context, userID, keyID int64) error

DeleteKey 删除自己的 key(/api/user/keys/{id} DELETE;Auth 增量移除——旧明文 立即失效)。

func (*Service) DeletePriceEntry

func (s *Service) DeletePriceEntry(ctx context.Context, model string) error

func (*Service) DeleteRule

func (s *Service) DeleteRule(ctx context.Context, id int64) error

DeleteRule 删除规则;不存在 → ErrNotFound(消息含 id)。成功后规则引擎 Reload。

func (*Service) DeleteRulesBatch

func (s *Service) DeleteRulesBatch(ctx context.Context, ids []int64) error

DeleteRulesBatch 批量删除规则(事务,全成或全败);ids 1–100 去重; 缺 id → ErrNotFound(消息含缺失 id)。成功后规则引擎 Reload。

func (*Service) DeleteTemplate

func (s *Service) DeleteTemplate(ctx context.Context, id int64) error

func (*Service) DeleteTemplatesBatch

func (s *Service) DeleteTemplatesBatch(ctx context.Context, ids []int64) error

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 (s *Service) GetAccount(ctx context.Context, id int64) (*domain.Account, error)

func (*Service) GetAccountExt

func (s *Service) GetAccountExt(ctx context.Context, accountID int64) (*domain.AccountExt, error)

GetAccountExt 账号 ext 行(编辑回显)。账号缺 id → 404。

func (*Service) GetAccountGroups

func (s *Service) GetAccountGroups(ctx context.Context, id int64) ([]int64, error)

GetAccountGroups 账号的分组 id 列表(编辑回显)。账号缺 id → 404。

func (*Service) GetCode

func (s *Service) GetCode(ctx context.Context, id int64) (*domain.RedemptionCode, error)

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) GetGroup

func (s *Service) GetGroup(ctx context.Context, id int64) (*domain.Group, error)

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) GetKey

func (s *Service) GetKey(ctx context.Context, userID, keyID int64) (*domain.Key, error)

GetKey 用户取自己的 key 详情(/api/user/keys/{id} GET)。

func (*Service) GetPriceEntry

func (s *Service) GetPriceEntry(ctx context.Context, model string) (*domain.PriceEntry, error)

func (*Service) GetSettings

func (s *Service) GetSettings(ctx context.Context) ([]*domain.Setting, error)

GetSettings 全部设置(默认值 + DB 覆盖;/api/admin/settings GET)。

func (*Service) GetTemplate

func (s *Service) GetTemplate(ctx context.Context, id int64) (*domain.Template, error)

func (*Service) GetTemplateExt

func (s *Service) GetTemplateExt(ctx context.Context, templateID int64) (*domain.TemplateExt, error)

GetTemplateExt 模板 ext 行(编辑回显)。模板缺 id → 404。

func (*Service) GetUser

func (s *Service) GetUser(ctx context.Context, id int64) (*domain.User, error)

GetUser 用户详情(/api/admin/users/{id} 更新前置读取)。

func (*Service) GetUserGroups

func (s *Service) GetUserGroups(ctx context.Context, userID int64) ([]int64, map[int64]*int, error)

GetUserGroups 读取用户被授予的组与各专属倍率(GET /api/admin/users/{id}/groups; 用户缺失 → 404)。mults 只含该用户有专属倍率的组。

func (*Service) GetUserMe

func (s *Service) GetUserMe(ctx context.Context, userID int64) (*domain.User, error)

GetUserMe 当前用户信息(/api/user/auth/me)。

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 (s *Service) ListAccounts(ctx context.Context, q repository.ListQuery) ([]*domain.Account, int64, error)

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

ListCodes 兑换码列表(/api/admin/redemption-codes):type/status 筛选 (nil = 不过滤;非法枚举 → 400)+ sort 白名单校验(非法 → 400)。

func (*Service) ListGroups

func (s *Service) ListGroups(ctx context.Context, q repository.ListQuery) ([]*domain.Group, int64, error)

func (*Service) ListGroupsForUser

func (s *Service) ListGroupsForUser(ctx context.Context, userID int64) ([]*domain.Group, error)

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

func (s *Service) ListMailTemplates(ctx context.Context) ([]*domain.EmailTemplate, error)

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 (s *Service) ListPriceVariants(ctx context.Context, model string) ([]*domain.PriceVariant, error)

func (*Service) ListRules

func (s *Service) ListRules(ctx context.Context, enabled *bool) ([]domain.Rule, int64, error)

ListRules 规则列表(enabled 过滤 + priority 升序 + {total, rows};无分页)。

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 (s *Service) ListTemplates(ctx context.Context, q repository.ListQuery) ([]*domain.Template, int64, error)

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) LoginUser

func (s *Service) LoginUser(ctx context.Context, email, password string) (*domain.User, error)

LoginUser 登录校验:email → bcrypt 校验 → 状态检查(禁用与口令错误同 文案 401,防枚举)。

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 (s *Service) PriceSourceURL() string

func (*Service) PriceSyncCron

func (s *Service) PriceSyncCron() string

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

func (s *Service) QueryStatsTTFT(ctx context.Context, q TTFTQuery) (*domain.TTFTSummary, error)

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

func (s *Service) RegisterUser(ctx context.Context, email, password string) (*domain.User, error)

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 (s *Service) ReloadFunctionPricingCtx(ctx context.Context) error

func (*Service) ReloadImagePricing

func (s *Service) ReloadImagePricing()

ReloadImagePricing intentional dispatch point over unified snapshot (not compat shim).

func (*Service) ReloadImagePricingCtx

func (s *Service) ReloadImagePricingCtx(ctx context.Context) error

func (*Service) ReloadPricing

func (s *Service) ReloadPricing()

func (*Service) ReloadPricingCtx

func (s *Service) ReloadPricingCtx(ctx context.Context) error

func (*Service) ReloadSettings

func (s *Service) ReloadSettings(ctx context.Context) error

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

func (s *Service) ResetAccountsCooldownBatch(ctx context.Context, ids []int64) (int, error)

ResetAccountsCooldownBatch 批量重置账号冷却:validateIDs → 预取目标 存在性 + 旧组并集 → 合成 patch {Status:active, CooldownUntil:now} 复用 UpdateAccountsBatch(触发既有 failed_at/last_error 清理 + 新冷却写入) → 恢复审计 → 组级失效 + NOTIFY。

func (*Service) ResetPassword

func (s *Service) ResetPassword(ctx context.Context, email, code, newPassword string) error

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

func (s *Service) RotateKey(ctx context.Context, userID, keyID int64) (*domain.Key, error)

RotateKey 轮换自己的 key(/api/user/keys/{id}/rotate):新明文落库;旧明文 增量移除(立即失效)、新明文增量注册。用户门禁字段写库前预取(B1-1: GetUser 前置——Delete 后只剩不可失败的内存 Upsert,失败窗口整体消失—— DB 已轮换只留新明文时新 raw 永不蒸发)。

func (*Service) SendForgotPasswordCode

func (s *Service) SendForgotPasswordCode(ctx context.Context, email string) error

SendForgotPasswordCode 忘记密码发码:恒 200 空转,实际发送仅当 enabled && 账号存在 && 未限频。

func (*Service) SendRegisterCode

func (s *Service) SendRegisterCode(ctx context.Context, email string) error

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 (s *Service) SetPriceFetcher(f pricing.Fetcher)

func (*Service) SetTimeLocation

func (s *Service) SetTimeLocation(l *time.Location)

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 (s *Service) UpdateAccount(ctx context.Context, a *domain.Account) (*domain.Account, error)

func (*Service) UpdateAccountsBatch

func (s *Service) UpdateAccountsBatch(ctx context.Context, ids []int64, p repository.AccountPatch) error

func (*Service) UpdateGroup

func (s *Service) UpdateGroup(ctx context.Context, g *domain.Group) (*domain.Group, error)

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

func (s *Service) UpdateRule(ctx context.Context, id int64, p RulePatch) (*domain.Rule, error)

UpdateRule 部分更新规则:未提供字段保持原值;校验合并后的完整 when/then; 不存在 → ErrNotFound(消息含 id)。成功后规则引擎 Reload。

func (*Service) UpdateSetting

func (s *Service) UpdateSetting(ctx context.Context, key, value string) (*domain.Setting, error)

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 (s *Service) UpdateTemplate(ctx context.Context, t *domain.Template) (*domain.Template, error)

func (*Service) UpdateTemplatesBatch

func (s *Service) UpdateTemplatesBatch(ctx context.Context, ids []int64, p repository.TemplatePatch) error

func (*Service) UpdateUser

func (s *Service) UpdateUser(ctx context.Context, p *repository.UserPatch) (*domain.User, error)

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

func (s *Service) UserEmails(ctx context.Context, ids []int64) (map[int64]string, error)

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

type TTFTQuery struct {
	From       time.Time
	To         time.Time
	EntityType string
	EntityID   int64
	Model      string
}

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)。

Directories

Path Synopsis
Package serviceerr 定义 service 层错误哨兵(单一真相)。
Package serviceerr 定义 service 层错误哨兵(单一真相)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL