Balance 服务
全局钱包模型
为什么 @downcity/services 按 user 维护余额,而不是按 downcity 拆很多份余额。
@downcity/services 的核心是:余额按用户维护,不按 downcity 维护。
默认情况下,credits 和 USD 按固定比例挂钩:
1 USD = 1,000,000 credits- credits 是唯一账务单位
- 用户侧
GET /v1/balance/me会返回以 credits 为单位的balance,并补充usd、display、conversion - 管理端与内部账务字段也统一使用 credits
钱包余额允许为负数。扣费是账务事实;是否允许一次服务开始执行,应该在 action 前通过 precheck() 判断。
也就是说:
- user A 在 downcity X 消费
- user A 在 downcity Y 消费
- 扣的都是同一份全局余额
为什么这样更适合 Downcity
Downcity 的典型场景是:
- 一套 Federation 服务多个产品
- 同一个用户可能在多个产品之间切换
- 你希望这些产品复用同一套 AI 账户能力
如果余额再按 downcity 拆成很多份,用户体验和运营逻辑都会更碎。
downcity 还重要吗
重要,但它更适合出现在流水 metadata 里,而不是余额账户主键里。
比如一次扣费可以记录:
await balance.sub(ctx.user!.user_id, 10_000, {
note: "agent chat",
meta: {
city_id: ctx.downcity?.city_id,
model_id: ctx.variant?.id,
},
});这样你同时得到两件事:
- 余额仍然是全局的
- 历史记录里仍然知道这笔钱花在哪个 downcity
负余额和前置检查
如果某个 action 只需要拦截已经欠费的用户,调用:
await balance.precheck(ctx.user!.user_id);如果某个 action 在开始前要求固定最低余额,调用:
await balance.precheck(ctx.user!.user_id, 10_000);真正扣费仍然走 sub() 或 charge(),这两个方法都可以把钱包扣成负数。
账户里保存什么
最小账户只需要:
user_idbalanceunit
这里的 unit 可以是:
creditspointstokens
这种模型最适合什么场景
- 多个产品共用一套 AI 能力
- 你想做统一账户余额
- 你不想把钱包切碎到每个 downcity 各算一份
继续阅读
- 想看推荐扣费写法,读 Hook 中直接扣费
- 想看充值单和流水,读 充值单与流水