赠金与签到
这份文档是设计:先写规则,再照着实现。规则里凡是"我替你定的"都标了,想改的话只改这里,再改代码。
名词
- 额度:现有的 5 小时 / 每周 / 每月三个窗口,按用户组或个人设定,单位是积分(credits,模型定价用的那个单位)。
- 赠金条:管理员另加的一条额度,有名字、有规则,和三个窗口是分开的。每个用户在一条赠金条里有若干笔赠金(每笔有数额、有到期时间)。
- 备用额度:一种赠金条,只在所有其他额度都用完之后才动用。
- 共享额度池:一种赠金条(
kind = shared),余额只有一份、属于整个站点,所有账户共用,没有「笔」。见下面「共享额度池」一节。
赠金的单位就是积分。这样它和模型定价、窗口的"积分"上限是同一把尺子;不需要单独给赠金定价。
一条赠金条有哪些规则
| 规则 | 取值 | 含义 |
|---|---|---|
| 类型 | 赠金 / 备用额度 | 备用额度永远排在最后动用,用户也没有开关 |
| 用户能否开关 | 用户自选 / 强制启用 / 强制关闭 | 见下面的"用完顺序" |
| 默认状态 | 开 / 关 | 用户自选、且用户还没选过时的状态(默认开) |
| 是否公开总量 | 是 / 否 | 否:用户只看到剩余百分比和"有余额/已用完",看不到 1000 这个数字。措辞另跟系统设置 quota.usage_display(和额度窗口一样):选「剩余」显示「还剩 60%」、选「已用」显示「已用 40%」,这时所有赠金条都只显示百分比;只有选「具体数值」时这一项才决定显示 6 / 10 还是百分比 |
| 可用模型 | 全部 / 指定若干个 | 只有这些模型的请求会动用这条赠金条 |
| 到期时间 | 无 / 某个时刻 | 建条时的默认到期;每一笔赠金也可以有自己的到期 |
每笔赠金是:谁、多少积分、已用多少、何时到期、来源(管理员发放 / 签到 / …)。
一次请求怎么扣(这是最要紧的规则)
请求先确定要用的模型,然后按下面的顺序找钱,找到够的就停:
- 优先使用的赠金:强制启用的,或用户自选且用户开着的赠金条(类型是"赠金"、这个模型可用、没过期)。同一用户有多笔时,先到期的先用。这一步用掉的部分不占用三个窗口的额度——这正是"启用后优先使用"的意义。
- 窗口额度:现有逻辑,原样。
- 后备赠金:窗口额度不够时,才动用"强制关闭"的、用户自选但关着的赠金条,以及备用额度。备用额度排在最后。
- 重置卡:用户开了"自动使用重置卡"时,再花一张卡把窗口重置。(我把后备赠金放在重置卡前面:赠金会过期,卡是用户自己攒的,先用会过期的。)
- 都不行:照旧 429,并告诉用户什么时候恢复。
"用户自选但关着"的赠金条不是不能用,只是"先不用":它退到第 3 步。想让一条赠金"用户绝对用不了",就别发给他。
数额怎么算
一次请求开始前要先按最坏情况预留(现有做法)。赠金也一样:
- 预留时,第 1 步能拿多少拿多少(不超过预留额);不够的部分由第 2 步的窗口去预留。
- 请求结束后按真实花费结算:先冲赠金已经预留的部分,超出的部分记到窗口里(窗口可以因此略微超过上限,和现在的结算超支一样)。
- 请求失败或取消:赠金和窗口的预留都原样退回。
- 第 3 步(后备)必须整笔覆盖预留额才动用,否则不动用、走第 4、5 步——后备不拆分。
管理员在"管理员不受额度限制"的设置下(默认),或者额度本来就不设限的用户,请求不动用赠金。
每分钟频率限制不受赠金影响
RPM 和 TPM 是保护上游服务商的突发限制,不是额度。赠金只顶替 5 小时 / 每周 / 每月三个窗口:一个请求即使整笔由赠金支付,也照样计入 RPM 和 TPM(TPM 计入整个请求的 token,不是窗口承担的那一部分),超了照样被拒绝——被拒绝的请求不会动用赠金。请求结束后 TPM 与窗口一样,先退回预留、再按真实用量记账。
赠金按积分计价:没有设定价格的模型(每次 0 积分)不会动用赠金,赠金条对它们没有意义。
共享额度池
赠金条的一种。规则(名称、开关、适用模型、是否公开总量、使用中)和别的赠金条共用 bonus_bars 那一行;余额在 bonus_pools,每个账户每天从里面用了多少在 bonus_pool_usage。
开关比别的赠金条多一个取值,少一个:
| 开关 | 含义 |
|---|---|
user 用户自选 | 开着:第 1 步先扣池子;关着:退到第 3 步。默认状态由「默认开启」决定,选择存在 bonus_choices,和赠金条一样 |
on 强制启用 | 永远在第 1 步先扣池子;池子不够的部分照常走窗口、后备、重置卡 |
only 只能用额度池 | 池子覆盖的模型只由池子付:不走窗口、不走用户自己的赠金、不花重置卡;池子付不起整笔就 429 pool_exhausted,details.reason 是 empty 或 daily_limit,details.resets_at 是恢复时间(不会自动恢复时为 0)。TPM / RPM 照常计入 |
(off 也接受,等于永远在第 3 步,后台不提供这个选项。)
顺序上池子排在同一阶段的赠金前面:第 1 步先池子再赠金,第 3 步也先池子。多个池子按建立顺序。
余额与重置:reset_period 为空表示不重置;day / week(周一)/ month 表示每个周期开始时余额变回 reset_amount,已用清零,手动充值的部分随之作废。周期按签到时区算。重置是惰性的:下一次有人动用或查看池子时才写回,没有定时任务。预留跨过重置时,退回不加回新周期(旧周期的预留已随重置清掉),结算记入新周期。
每人每天上限:per_user_daily > 0 时,一个账户一天里预留和结算的总和不超过它;按签到时区的日期记在 bonus_pool_usage,保留 35 天。
并发:池子是所有人共用的一行。每次预留、结算、退回都先锁这一行(UPDATE … SET used = used),再锁该账户当天那一行,然后读、算、写,所以多个请求同时到也不会超支、不会超过份额。锁顺序是:池子行、该账户的用量行,逐个池子按建立顺序;之后才是 bonus_grants。
结算超支:only 模式下,实际花费超过预留的部分也记到池子(最后一个付款的池子)上,余额可能因此略低于 0(显示为 0),和窗口结算超支一样。其他模式下池子只付到余额为止,剩下的照常归窗口。
池子不能发放(bonus_shared_no_grants),不能用作签到奖励,类型也不能和别的赠金条互相改(bonus_kind_change)。
管理员能做什么
- 建、改、停用赠金条(改规则对所有人立即生效;已发的赠金不受影响)。
- 给某条赠金条发放:所有人 / 某个用户组 / 指定用户,数额、到期时间(日期,或"N 天后"),可以附一句备注。发放会给对方一条站内通知。
- 看某条赠金条下每个用户的余额,撤销某一笔(收回未用完的部分)。
- 每个动作都有对应的控制台命令。
用户能做什么
- 在"用量"里看到自己的每条赠金条:名称、剩余(是否公开总量决定显示数字还是百分比)、到期时间、适用模型。
- 对"用户自选"的赠金条打开或关闭优先使用;强制的那几条只显示状态。
- 有新到账、快到期的赠金时收到站内通知(到期前 3 天一次)。
签到
- 每个用户每天一次;"一天"按管理员设定的时区算(默认
Asia/Shanghai)。 - 签到本身可以有每日奖励(比如每天 0.5 积分的赠金,7 天后到期),也可以什么都不给。
- 里程碑奖励:管理员配一张表,每行是"连续签到 N 天"或"本月累计签到 N 天" → 奖励。奖励是**用户自己点"领取"**才发,一个周期内每行只能领一次("连续"的周期是这一段连续;"本月累计"的周期是自然月)。
- 奖励的种类:一笔赠金(选哪条赠金条、数额、多少天后到期),或者一张重置卡(名称、窗口、有效天数)。
- 防刷:签到要求账户是正常状态;每人每天一行,靠数据库唯一键保证,多个请求同时来也只算一次;奖励领取同样靠唯一键。
- 管理员可以整体开关签到。
我建议但没有替你定的(想要就说一声)
- 补签:漏签一天,花一张"补签卡"补上,连续天数不断。
- 邀请奖励发赠金:现在邀请奖励发的是重置卡,可以改成(或加上)一笔赠金。
- 赠金流水:用户能看到"某天某个对话用掉了多少赠金"。
- 用完自动送备用额度:不是预先发好,而是用户第一次把所有额度用完时,一次性送一笔。现在的做法是预先发好、最后才动用,效果接近,但用户在到账前就能看见它。
- 每日免费额度:不需要签到,每天自动给一笔当日到期的赠金。
实现分几步
- 数据库:赠金条、赠金、用户偏好三张表;签到记录与领取记录两张表。
- 赠金的存取与扣款(
internal/bonus),接进额度预留 / 释放 / 结算(internal/quota)。 - 用户接口、管理接口、控制台命令。
- 签到(
internal/checkin):设置、接口、发奖励。 - 界面:用户的用量页和签到页;管理后台的赠金与签到。
- 每一步都带测试,扣款的并发规则用真的多个 goroutine 去撞。