Balance 服务

Hook 中直接扣费

在 Service 内部用 before hook 做余额前置检查,成功后再按真实费用扣钱包。

Balance 是钱包,不是价格规则引擎。

推荐模式是:

  1. 在 City 里注册 BalanceService()
  2. 把 balance 作为依赖传给需要余额能力的 Service
  3. Service 在自身定义阶段挂 before hook
  4. 调用前用 precheck() 做准入
  5. 调用成功后用 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 })

继续阅读