Balance 服务

全局钱包模型

为什么 @downcity/services 按 user 维护余额,而不是按 downcity 拆很多份余额。

@downcity/services 的核心是:余额按用户维护,不按 downcity 维护。

默认情况下,credits 和 USD 按固定比例挂钩:

  • 1 USD = 1,000,000 credits
  • credits 是唯一账务单位
  • 用户侧 GET /v1/balance/me 会返回以 credits 为单位的 balance,并补充 usddisplayconversion
  • 管理端与内部账务字段也统一使用 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_id
  • balance
  • unit

这里的 unit 可以是:

  • credits
  • points
  • tokens

这种模型最适合什么场景

  • 多个产品共用一套 AI 能力
  • 你想做统一账户余额
  • 你不想把钱包切碎到每个 downcity 各算一份

继续阅读