SaaS 席位订阅 Proration:区间账本、并发变更与账单接受点
月中增减席位不是把当前人数乘以剩余天数。本文冻结计费区间、价格版本和席位事件,用无重叠区段、稳定操作身份与供应商收据生成可复算的 Proration,并处理响应未知和账后更正。
本文要点
- 主题聚焦:DevOps、独立开发
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
客户在月中从 10 席加到 18 席,几分钟后另一名管理员又移除 3 人。若系统只把最终 quantity=15 发给支付平台,便丢失 10、18、15 各自生效多久;若两次请求都拿“当前订阅”预览后提交,后到响应还可能覆盖先到变更。Stripe 会为某些订阅修改生成 proration invoice item,但预览金额和最终发票仍可能受时间、折扣、税和并发更新影响,不能把一次 API 响应当成本地账本。
AI 定价迁移处理价格与 cohort 换代,AI SaaS 权益快照把已生效商业事实投影到运行时。本篇只拥有固定账期内“多少席位在哪段时间计费”和相应资金证据,不重新定义套餐,也不把席位数混入用量计费。
冻结 seatBillingPolicy.v1
每个订阅绑定不可变 subscription.seatBillingPolicy.v1:customer/subscription、provider API version、price ID 与摘要、币种和最小单位、账期 [start,end)、时间基准、席位身份或纯数量模式、最小承诺、增减席生效规则、proration behavior、折扣/税输入 generation、舍入位置和账后更正规则。区间使用带 offset 的 RFC 3339 瞬间并归一到 UTC;“按剩余天数”不能依赖本地午夜或把每月固定成 30 天。
schema: subscription.seatBillingPolicy.v1
subscription: sub_team_82
generation: 14
price: price_pro@7
currency: USD
period: {start: 2026-07-01T00:00:00Z, end: 2026-08-01T00:00:00Z}
quantityMode: NAMED_SEAT
changeEffectiveAt: ACCEPTED_AT
proration: CREATE_PRORATIONS
rounding: INVOICE_LINE_MINOR_UNIT
席位事实写成 seatChange.v1,包含稳定 operation ID、expected subscription generation、seat ID 集合或 quantity delta、requestedAt、effectiveAt、actor、policy digest 和 payload digest。命名席位要保证同一 seat 在同一订阅的有效区间不重叠;纯 quantity 也要保存每次 delta,不能只存覆盖后的总数。规范账本把整个 period 切成首尾相接的半开段,每段绑定 quantity 与 source event root;任一空洞、重叠或负数量都拒绝出账。
金额计算以价格最小单位和区间比例生成内部 oracle,但不宣称能替支付平台复刻税务引擎。Stripe 文档说明 proration 通常不立即收费,且折扣、未付款发票等情形需要显式策略。内部 preview 记录 policy、区段、计算精度和预计 line root;供应商 preview 另存 request parameters、返回时间与摘要。两者差异超过阈值进入 PRORATION_REVIEW,不能选择较便宜的一边静默通过。
一次席位变化只有一个接受点
命令走 PROPOSED -> VALIDATED -> READY -> ACCEPTED -> PROVIDER_PENDING -> INVOICED -> RECONCILED;另有 STALE_CHANGE、PROVIDER_UNKNOWN、PROVIDER_REJECTED -> COMPENSATING -> COMPENSATED 和 INVOICE_EXCEPTION。本地接受点以 CAS 匹配 expected generation/period,追加 seat event、区段和 outbox 并推进 generation;同时更新每订阅的 providerDesiredGeneration,按钮或 preview 不算接受。
provider dispatcher 受 per-subscription fence:只可 CAS 取得最小未终结 generation,按稳定 operation ID 发送该代明确 quantity/price;后继代必须等待。超时进入 PROVIDER_UNKNOWN 并阻断后继,先按 subscription/invoice 与幂等结果对账;旧代迟到 receipt 只关闭其 lineage,绝不能覆盖更高 providerDesiredGeneration。明确永久拒绝才进 PROVIDER_REJECTED,接受函数以新 generation 补偿权益/区段并保留原 event。若采用合并 reconcile,也必须读取权威 provider 状态并 CAS 匹配 desired root,只发送最新 desired generation。权益引用最新代,HTTP 幂等键正确性约束键不换 payload。
账单 finalized 后,将 provider invoice line、period、quantity、discount/tax 输入和本地 segment root 对账。已收款发票不能靠回写旧 seat event 修正;错误通过 credit note、补充 invoice 或下一期明确 adjustment 形成新 generation。AI 用量计量账本处理 token 等用量,预付 Credits 账本处理余额预留,二者都不能替席位时间区间。
用边界和并发破坏 Proration
故障套件注入并发增减席、账期/DST 边界、preview 后价格换代、A 代 quantity=18 超时而 B 代 quantity=15 就绪、B 先响应与 A 迟到、永久拒绝、重复 Webhook、旧 invoice、seat ID 重用和 outbox 崩溃。预期本地 CAS 串行,A 未终结时 B 不 dispatch;任何旧 receipt 不使 provider quantity 从最新 desired generation 倒退,永久拒绝以高代补偿且不删历史。
门禁要求区间覆盖率与 root 重算一致率 100%,重叠/空洞、并发双占、异 payload 接受、重复 proration line、provider generation 倒退均为 0;UNKNOWN 100% 阻断后继且有 owner,永久拒绝补偿闭环率 100%。上线先影子两个账期,再给内部和 5% 新订阅放量。异常时停止新变更并保留 outbox 对账;finalized/paid 金额只做补偿分录,绝不删除历史。
Related