Add per-channel quota limits with reset scheduling and runtime checks so channel capacity can be capped and restored on configured periods.
7.9 KiB
渠道订阅类型额度设计文档
版本: v2.0 日期: 2026-05-19 状态: 审阅版
1. 背景与问题
1.1 现状
Channel 模型仅有两种额度相关字段:
| 字段 | 语义 | 来源 |
|---|---|---|
Balance |
上游 API 实际余额(USD) | 上游 API 查询 |
UsedQuota |
累计消耗量 | 本地扣减,永不重置 |
两者都无法满足"固定额度上限 + 周期重置"的运营需求。
1.2 需求场景
运营者希望对渠道配额管控:
- 渠道 A:每天最多 1 万 quota,超限拒绝请求
- 渠道 B:每月最多 50 万 quota,超限拒绝请求
- 渠道 C:不设上限,仅记录消耗(与现有
UsedQuota等价)
1.3 已有参考实现
model/subscription.go 中已实现完整的用户订阅额度重置机制:
// 订阅套餐可配置重置周期
QuotaResetPeriod // daily/weekly/monthly/custom/never
// 用户订阅记录
AmountTotal // 总额度
AmountUsed // 已消耗
LastResetTime // 上次重置
NextResetTime // 下次重置
渠道额度直接复用此设计模式,但消费模式不同(见 3.1)。
2. 目标与非目标
2.1 目标
- 渠道可配置额度上限(
QuotaLimit) - 渠道可配置重置周期(
QuotaResetPeriod:daily / weekly / monthly / custom / never) - 周期到达时自动重置
UsedQuota为 0 - 额度耗尽时拒绝请求(返回 429)
2.2 非目标
- 不支持预扣 + 结算模式(与订阅的
PreConsume不同) - 不支持按渠道内单个 key 分别管控
- 不支持按模型分组分别设置额度
- 不支持额度耗尽自动降级到备用渠道(仅拒绝)
- 不支持额度使用率告警通知
3. 核心决策
3.1 消费模式:实时扣减(非预扣 + 结算)
订阅采用"预扣 + 结算"双阶段,因为订阅钱包需要精确余额。
渠道采用实时扣减,更简单:
- 请求前检查:
UsedQuota >= QuotaLimit→ 拒绝 - 请求完成:
UsedQuota += actualQuota
不做预估算额。UsedQuota >= QuotaLimit 时拒绝,意味着当前周期内已消耗量达到上限。由于请求完成才扣减,最后一个被放行的请求可能实际消耗后超过 QuotaLimit,但这是可接受的——与 UpdateChannelUsedQuota 的原子递增语义一致。
3.2 重置时机:定时任务 + 懒检查兜底
与订阅一致:
// 定时任务(每分钟)
ResetDueChannels()
// 请求前懒检查(定时任务未及时触发时补救)
maybeResetChannelQuotaTx()
关键:懒检查必须在额度检查之前执行,否则重置周期刚到时旧 UsedQuota 仍会拒绝合法请求。
3.3 并发安全:FOR UPDATE 行锁 + Updates 精确更新
tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ? AND next_reset_time <= ?", channelId, now).
First(&locked)
重置时只更新相关字段,不用 Save 避免覆盖并发更新:
tx.Model(&Channel{}).Where("id = ?", channel.Id).Updates(map[string]interface{}{
"used_quota": 0,
"last_reset_time": base.Unix(),
"next_reset_time": next,
})
3.4 额度耗尽行为:返回 429
额度耗尽时返回 HTTP 429,不自动降级。降级涉及渠道选择逻辑改动,复杂度高,列为非目标。
3.5 Balance 与 QuotaLimit 的关系
两者独立共存:
| 字段 | 语义 | 来源 |
|---|---|---|
Balance |
上游 API 实际余额 | 上游 API 查询 |
UsedQuota |
本地累计消耗 | 本地扣减 |
QuotaLimit |
运营者设定的额度上限 | 配置 |
Balance 是上游真实余额,QuotaLimit 是本地配额管控,两者互不影响。运营者可同时使用或只选其一。
3.6 多 Key 模式
UsedQuota 是整个渠道的累计,不区分单个 key。多 key 模式下仅全局管控,与现有行为一致。
4. 影响范围
4.1 新增文件
| 文件 | 说明 |
|---|---|
model/channel_quota.go |
渠道额度核心逻辑(重置时间计算、重置事务、ResetDueChannels、CheckChannelQuota) |
service/channel_quota_reset_task.go |
渠道额度重置定时任务 |
4.2 修改文件
| 文件 | 修改内容 |
|---|---|
model/channel.go |
新增 5 个字段 |
service/text_quota.go |
请求前懒重置 + 额度检查 |
controller/channel.go |
校验新字段 + 创建/更新时初始化 NextResetTime |
main.go |
启动定时任务 |
4.3 数据库字段变更
ALTER TABLE channels ADD COLUMN quota_limit BIGINT DEFAULT 0;
ALTER TABLE channels ADD COLUMN quota_reset_period VARCHAR(16) DEFAULT 'never';
ALTER TABLE channels ADD COLUMN quota_reset_custom_seconds INT DEFAULT 0;
ALTER TABLE channels ADD COLUMN last_reset_time BIGINT DEFAULT 0;
ALTER TABLE channels ADD COLUMN next_reset_time BIGINT DEFAULT 0;
used_quota 已存在,沿用。现有渠道 quota_reset_period = 'never' 即为原有行为。
5. 数据流
sequenceDiagram
participant Req as 请求
participant Relay as Relay 层 (text_quota.go)
participant Model as channel_quota.go
participant DB as Database
Req->>Relay: 请求完成,准备扣减
Relay->>Model: 懒重置检查 (channelId)
Model->>DB: SELECT ... FOR UPDATE
alt NextResetTime <= now
Model->>DB: UPDATE used_quota=0, last/next_reset_time
end
Relay->>Model: 额度检查 (channelId)
alt UsedQuota >= QuotaLimit (且 QuotaLimit > 0)
Relay-->>Req: 429 Channel quota exceeded
else
Relay->>DB: UPDATE used_quota += actual
end
sequenceDiagram
participant Cron as 定时任务 (每分钟)
participant Model as channel_quota.go
participant DB as Database
Cron->>Model: ResetDueChannels(300)
Model->>DB: SELECT next_reset_time <= now AND quota_limit > 0
loop 每条渠道
Model->>DB: FOR UPDATE 锁行
Model->>DB: UPDATE used_quota=0, last/next_reset_time
end
6. 风险与兼容性
| 风险 | 影响 | 缓解措施 |
|---|---|---|
quota_reset_period = 'never' 时 UsedQuota 行为不变 |
无 | 默认值即为此 |
历史渠道 next_reset_time = 0 |
无 | 定时任务跳过,懒检查也跳过 |
| 重置时刻有进行中请求 | 低 | FOR UPDATE 锁保证串行 |
| 最后一个放行请求可能超用 | 低 | 可接受,与现有行为一致 |
SQLite 下 FOR UPDATE 无效 |
低 | SQLite 自带数据库级锁,事务串行化 |
缓存中 UsedQuota 可能过期 |
低 | 额度检查直接查 DB,不依赖缓存 |
7. 验收标准
7.1 功能验收
- 新增/编辑渠道时可配置
quota_limit和quota_reset_period QuotaLimit > 0且UsedQuota >= QuotaLimit时请求返回 429QuotaLimit = 0时不做额度检查(原有行为)- 每日重置:次日 00:00
UsedQuota清零 - 每月重置:每月 1 日 00:00 清零
- 自定义秒数重置:按配置精确重置
- 永不重置:
UsedQuota持续累计 - 创建/更新渠道时正确初始化
NextResetTime
7.2 兼容性验收
- 未设置
QuotaLimit的历史渠道行为不变 - GORM AutoMigrate 正确添加新字段
- SQLite / MySQL / PostgreSQL 均兼容
8. 回滚与降级
8.1 回滚
部署后如需回滚:新字段由 GORM AutoMigrate 添加,旧代码忽略未知字段,向前兼容。回滚后定时任务不运行,UsedQuota 恢复持续累计行为。
8.2 降级
关闭单个渠道的额度管控:设 QuotaLimit = 0 + QuotaResetPeriod = 'never',等价于原有行为。
9. 参考实现
model/subscription.go:300-307—NormalizeResetPeriodmodel/subscription.go:309-348—calcNextResetTimemodel/subscription.go:933-967—maybeResetUserSubscriptionWithPlanTxmodel/subscription.go:1100-1140—ResetDueSubscriptionsservice/subscription_reset_task.go— 定时任务