Balance 服务
Hook 中直接扣费
在 Service 内部用 before hook 做余额前置检查,成功后再按真实费用扣钱包。
Balance 是钱包,不是价格规则引擎。
推荐模式是:
- 在 City 里注册
BalanceService() - 把 balance 作为依赖传给需要余额能力的 Service
- Service 在自身定义阶段挂
before hook - 调用前用
precheck()做准入 - 调用成功后用
charge()真实记账
为什么这样更自由
因为真正的价格规则通常属于业务层:
- 哪些 action 要收费
- 某个 model 要不要更贵
- 某个 downcity 要不要免费
- 某类用户要不要打折
这些都更适合写在你自己的 hook 里。
调用前检查
precheck(user_id) 默认使用 needed_credits = 0,只拦截已经欠费的用户:
await balance.precheck(ctx.user!.user_id);固定价格或预估价格场景可以传最低余额:
await balance.precheck(ctx.user!.user_id, 10_000);定义在 Service 内部
不要在 fed.use(service) 的地方再外部追加余额规则。哪个 action 需要余额检查,应该由 Service 自己定义。
const balance = new BalanceService({
init_credits: 100_000_000,
});
base.use(balance);
class ExportService extends Service {
constructor(options: { balance: BalanceService }) {
super({ id: "export", name: "Export" });
this.action("pdf", async (ctx) => {
return await export_pdf(ctx.input);
}, {
auth: ["user"],
}).before(options.balance.precheck_hook({
needed_credits: 10_000,
}));
}
}
base.use(new ExportService({ balance }));复杂规则可以直接写业务 hook:
this.action("render", render_action, { auth: ["user"] })
.before(async (ctx) => {
const needed_credits = await estimate_render_credits(ctx.input);
await balance.precheck(ctx.user!.user_id, needed_credits);
});成功后扣费
charge() 负责真实记账,允许把钱包扣成负数。固定价格 action 可以在成功后直接扣;AIService 则会读取 Provider 返回的 charge line 后自动扣。
this.action("pdf", async (ctx) => {
const output = await export_pdf(ctx.input);
await balance.charge({
user_id: ctx.user!.user_id,
credits: 10_000,
idempotency_key: `pdf_export:${output.id}`,
note: "pdf export",
metadata: {
city_id: ctx.city?.city_id,
},
});
return output;
});可能因队列重投、网络超时或进程恢复而重复执行的扣费必须传稳定的 idempotency_key。相同键的后续 charge() 会返回第一次扣费记录,不会再次修改余额;余额、流水和扣费记录会在同一个数据库原子操作中提交。
AIService
AIService 传入 balance 后,会在内部对真实消费模型的 action 挂默认 precheck():
const ai = new AIService({ balance });
base.use(ai);AIService 默认只拦截已经负余额的用户;本次调用成功后,再按 Provider 实际 usage 扣费。
最小 API
这一页最相关的方法是:
balance.read(user_id)balance.require(user_id, credits)balance.precheck(user_id, needed_credits?)balance.precheck_hook(options?)balance.add(user_id, credits, extra)balance.sub(user_id, credits, extra)balance.charge({ user_id, credits, idempotency_key?, note, ref, metadata })