Files
Blizzard d032c198c8 feat: SaaS P2 计量·增量1 —— 用量按租户落持久明细 + 积分/成本折算
链路打通 tenant → 计费事实源:
- 契约:Task.Meta 加 MetaTenantID;UsageEvent 加 TenantID。
- 提交:网关 task.Meta[MetaTenantID]=tenantID(c);dispatcher emitUsage 带租户。
- 明细表 sundynix_usage_event(追加式,task_id 唯一→幂等防重投重复计费):
  tenant/owner/model/tokens + credits_micro + cost_micros。
- 折算:credits=total_tok/TOKENS_PER_CREDIT×credit_weight(token 基准,设 1 即 token 直计);
  cost 按 Pricing 折算;Pricing 加 credit_weight 列(每模型积分权重,缺省 1)。
  模型名空则回退激活 chat 模型(近似,忽略 failover 备用模型,已在设计标注)。
- 网关 SubscribeUsage 折算落明细(保留 Redis 日计数作快速配额校验)。

live 验证:提交任务→一行 usage_event,tenant 匹配用户租户、
credits=89tok/1000×2×1e6=178000 微积分、cost=45/1000×1+44/1000×2=133000 微元 CNY,
折算数学与幂等键均正确。

设计见 SAAS_P2_DESIGN.md。增量2(credit_ledger 余额软扣 + rollup)待做。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:54:29 +08:00

8.2 KiB
Raw Permalink Blame History

SaaS P2 —— 用量计量设计(Usage Metering

承接 P1(tenant_id 全链路已闭合,见 SAAS_DESIGN.md / DEPTH_ROADMAP T4.A)。 本阶段目标:让 token 用量可按租户持久归集,成为计费 / 配额的事实源。 纯后端、自包含;桌面端不改;不接支付(P5)。


1. 目标与非目标

目标

  • token 用量按 tenant 维度持久落库(不再只有易失的 Redis 日计数)。
  • 计量单位为积分(credits:可配 token↔积分汇率,扣量走扣积分; 「按 token 直计」= 汇率 1:1 的退化情形(同一条代码路径,配置切换,不做两套)。
  • 每条用量同时按 Pricing 折算成金额(内部账/毛利),可对账、可回溯重算。
  • 租户 / 天 rollup,供 admin 查用量 + 积分消耗 + 成本趋势。

非目标(划清边界,不过早做)

  • 支付 / 发票 / 订阅扣款 —— P5。
  • 套餐配额 enforcement(超额拒绝)—— P4,需先有 plan→quota 的产品定义。
  • 实时流式计量 —— 收尾一次事件已够,不做 per-token 流。
  • 前端计费页 —— 本阶段只出接口 + 数据,页面后续。

2. 现状(已有的一半)

提交(gateway)  task.Meta[MetaUserID]=userID  ──PublishTask──▶  dispatcher
dispatcher     收尾 emitUsage ── UsageEvent{task,user,model,tok...} ──▶  gateway
gateway        SubscribeUsage ── AddUsage(user,day,tok) ──▶  Redis 计数(48h TTL)
门控           提交前 GetUsage(user,today) vs USER_DAILY_TOKEN_BUDGET

够什么:单用户当日预算门控。 缺什么:① 无 tenant 维度;② Redis 48h 易失,计费要永久可审计;③ 有 Pricinginput/output per 1K)但没折算金额;④ 无聚合,查趋势要现算。


3. 设计

3.1 tenant_id 打通链路(接 P1

  • 提交task.Meta[MetaTenantID] = tenantID(c)(网关已有 tenantID(c) 助手)。
  • 契约contract.UsageEventTenantID string
  • dispatcheremitUsaget.Meta[MetaTenantID] 读租户,带进事件。
  • 一处新增 Meta 键、一个契约字段、一行读取 —— 复用今天刚打通的 owner/tenant 提交链。

3.2 持久明细表 sundynix_usage_event(追加式,只增不改)

说明
id / created_at 雪花 + 时间(BaseModel
tenant_id / owner 租户 + 提交者 user.id(隔离 + 归属)
task_id 关联任务
model 计费模型名
prompt_tok / comp_tok / total_tok 输入 / 输出 / 合计
credits_micro 折算积分,int64 微积分(×10⁻⁶),避取整损耗;展示时约整
cost_micros 折算金额,int64 微单位(币种最小单位 ×10⁻⁶),避浮点累积误差
currency 币种(跟 Pricing
exceeded 是否触顶单任务预算被中止
  • 每条 UsageEvent 落一行 —— 审计 / 对账 / 重算的事实源
  • 为何不复用 Redis:Redis 48h 就没了;计费要永久、可审计、可按新价重算历史。
  • 受租户表(标 isTenantScoped());但消费在 background ctxNATS 回写)→ tenant 从事件显式带,不靠插件自动填(与 Eval 同模式)。

3.3 成本折算

  • 落库时查 Pricing[model]cost = prompt/1000*input_per_1k + comp/1000*output_per_1k,存 cost_micros(整数)。
  • Pricing 缺失 → cost=0 + 照落事件(计量不因缺价而丢量;缺价可事后补价重算)。
  • tokens 是「估算」(契约原注释)→ 计费口径认这个近似;要精确则后续接 provider 真实 usage(已知项,非本轮)。

3.4 聚合表 sundynix_usage_rollup(租户 / 天 快照)

说明
tenant_id + day 联合唯一(20060102
total_tok / credits_micro / cost_micros / task_count 当天累加
currency 币种
  • 消费 UsageEvent同步 upsertON CONFLICT(tenant_id,day) DO UPDATE ... = 原值 + 增量)。
  • 明细表(event)给审计/重算,快照表(rollup)给快速读 —— 经典 append + materialized。
  • 配额/账单查 rollup 一行,不扫明细。

3.6 计量单位:积分(credits)与 token 汇率

核心:积分是租户面唯一计量/扣量单位;token 直计 = 汇率 1:1 的退化。一条路径,不做两套。

汇率配置admin 可配)

  • 全局默认 TOKENS_PER_CREDIT(如 1000 token = 1 积分;设 1 即 token 直计)。
  • 可选 每模型权重 credit_weightpremium 模型每 token 烧更多积分)——复用 Pricing 表加一列 credit_weight float64(缺省 1.0),不新建表。
  • 落库:credits_micro = total_tok * 1e6 / TOKENS_PER_CREDIT * credit_weight(整数运算,保精度)。

积分账本 sundynix_credit_ledgerappend-only,扣量的事实源)

说明
tenant_id 租户
kind grant(充值/发放,+ / usage(消耗,−) / adjust(人工校正)
credits_micro 带符号增量
ref 关联(usage→task_idgrant→订单/发放单号)
memo 备注
  • 租户当前余额 = SUM(credits_micro);为快取在 sundynix_tenant.credit_balance_micro 维护物化余额(消费/充值时增量更新),查询读一列,不 SUM 全账本。
  • 一致性:先写 ledger 明细,再增量改 balancebalance 可由 ledger 随时重算兜底。
  • 幂等:usage 分录对 (tenant_id, ref=task_id, kind) 唯一,防重投重复扣分。

扣量 vs 拦截(分级,别过早强制)

  • P2(本阶段,记账):每条 usage 记 usage 分录 + 扣物化余额。余额可为负(软扣,先记账不拦)。
  • P4enforcement:提交前查余额≤0 则拒绝新任务(硬闸)。需先有套餐→发放积分的产品定义,故 enforcement 留后。 开关 CREDIT_ENFORCE(默认 off) 控制软/硬,schema 现在就备好,改行为时不改表。

3.5 观测接口(admin,系统级)

  • GET /api/v1/admin/usage?tenant=&from=&to= → 从 rollup 出某租户(或全平台)用量 + 积分消耗 + 成本趋势,附租户当前积分余额。
  • store.WithoutTenant(系统级跨租户,与 admin/overview 同口径)。
  • 前端页后续;本阶段先接口 + 数据。

4. 落地顺序(增量)

  • 增量1 — 链路 + 明细 + 折算MetaTenantID + UsageEvent.TenantID + dispatcher 带租户; Pricing.credit_weight 列 + TOKENS_PER_CREDIT 配置;usage_event 表 + 消费时折算 credits/cost 落明细。 live 验证:跑一个任务 → 一行带 tenant + owner + credits + cost。
  • 增量2 — 账本 + 余额 + rollupcredit_ledger 表(usage 分录,幂等)+ tenant.credit_balance_micro 物化余额(软扣,可负);usage_rollup 表 + upsert。 live 验证:多任务 → 余额递减正确、rollup 累加正确。
  • 增量3 — 观测接口GET /admin/usage(用量 + 积分 + 成本 + 余额)。
  • (前端用量页 / P4 硬拦截 CREDIT_ENFORCE / 充值发放 —— 后续,需求拉动)

5. 权衡与已知风险(诚实标注,不一次做满)

  1. 投递可靠性UsageEvent 现走普通 NATS、收尾 best-effort丢事件=丢计费。 → 本轮先落表(已远强于 Redis)。若要严格计费,把 usage 升级到 JetStream 持久投递(像入库队列那样带重投/幂等)——列为已知项,等"要精确对账"的需求真来了再上,不提前做。 → 幂等兜底:usage_event 可对 task_id 加唯一约束(一任务一计量),防重投重复计费。
  2. 成本依赖 Pricing 配全:缺价记 0 不丢量,可事后补价重算明细 → 修 rollup。
  3. token 估算:计费认近似值;要精确接 provider usage 是后续。
  4. rollup 与 event 一致性:先写 event 再 upsert rollup;若 rollup 失败,event 仍在 → 可由 event 重建 rollup(提供一个重算命令兜底)。

6. 与 GPT 重构方案的关系

  • 采纳其正确诊断:usage 先落事件、不接支付P2 与 P5 解耦)。
  • 不引入独立计费服务 / 消息中间件重构 —— 复用现有 NATS + PG,按需求拉动。