想象一个场景:月底对账,财务拿着报表来问——这个客户 8 月账单怎么算出来的?他 8 月 10 号从基础版升到专业版,补了差价;15 号又加了 5 个席位;隔壁那个按用量计费的客户调了 120 万次 API……三笔钱三套算法,对不上账。
这就是 SaaS 计费的全部难度所在:计费不难,“变化中的计费”才难。客户随时升级、降级、加减席位、退订,每一笔变更都要折算成公平的金额。这篇把套餐、订阅、计费模式与升降级结算一次讲清。
一、三种计费模式
| 模式 | 公式 | 收入可预测性 | 适合 |
|---|---|---|---|
| 包周期 | 固定价 × 月/年 | 最高(订阅制核心) | 功能型 SaaS 基础定价 |
| 按席位 | 单价 × 席位数 | 高(随客户扩张增长) | 协作类产品 |
| 按用量 | 单价 × 消耗量 | 低(客户不可控) | API / 存储 / 消息类 |
实际产品里常见混合:基础月费 + 超出配额按量——“每月 1 万次 API 调用内免费,超出每次 0.01 元”。混合模式兼顾了可预测性(保底月费)和公平性(用得多付得多),但也把两种模式的复杂度都集齐了。
二、订阅模型:四个实体
计费系统的骨架是四个实体,先把关系立住:
图上三个带框的提醒,展开讲一层:
快照价格,别实时查价。 图上订阅那条
price_snapshot ★是整个模型里最不能省的列:套餐调价时,已生效的订阅按签约时的价格走,新订阅按新价。不止月费——升降级补差、中途退费的折算,全部按快照价算,审计对账时每张账单的金额都要能对回当期的合同价,实时查价会让历史账单变成一笔糊涂账。所以账单要留明细行:月费、超量、补差各记一行,每一行都能对回当期快照价——财务问“这笔 1330 怎么算出来的”,一行行指给看。账期对齐方式一开始就定死。 自然月(全员 1 号出账)对账省心,但客户月中开通要做首期按天折算;开通日对齐客户记忆友好,但账单日期散布全月,出账 job 得按每个租户各自的周期跑。两种都行,混着用必乱——选哪个是产品决策,定了就写进系统约定,别留到实现时随手挑。
订阅状态机与租户状态机联动,但不是一回事。 订阅自己有状态(生效 / 到期 / 续费宽限 / 取消),和《租户生命周期管理》的租户状态分属两个系统:订阅到期未续费,订阅系统发事件、租户系统消费去触发冻结——一次握手完成,别把两套状态塞进一个状态机里,那会让“暂停订阅”和“冻结租户”永远纠缠不清。
订阅变更:新开周期,不覆盖旧快照。 升级、续费都别就地改订阅行——
plan_id和快照一覆盖,之前按旧价计费的依据就没了,上一条“能对回当期合同价”直接断链。做法是加一张订阅周期表(subscription_period:周期起止 + 当期快照价),一次变更生成一条新周期记录,账单挂当期周期、按当期快照算。续费也走同一条路:新周期按续费那一刻的套餐价取快照(要不要给老客户锁价是产品决策,要锁就续费时沿用旧快照——在续费那一刻定死,别漂着)。这样“套餐调价只影响新订阅”对续费同样成立。
三、升降级怎么算钱
核心工具是按比例折算(proration):一个周期内的价值按天线性分配。
升级:立即生效 + 补差价。 客户 8 月 1 日订了 300 元月费的基础版,8 月 10 日要升级 900 元的专业版:
旧套餐剩余价值 = 300 × (30 − 10) / 30 = 200 元
新套餐剩余价值 = 900 × (30 − 10) / 30 = 600 元
补差价 = 600 − 200 = 400 元客户当场补 400,立刻用上专业版——这是客户预期里的“公平”。
月中加席位同理。 每席每月 50 元,月初 10 席,用了 15 天后加到 15 席(新增部分用 15 天):
原有 10 席整月 = 10 × 50 = 500 元
新增 5 席 15 天 = 5 × 50 × 15/30 = 125 元
本月账单 = 500 + 125 = 625 元降级:周期生效,当期不退钱。 8 月 20 日从专业版降回基础版:本月继续享受专业版到月底,9 月 1 日起按 300 出账。
为什么升级立即、降级下期?用户预期不对称:升级的人想马上用新功能,降级的人不介意月底再省;而且当期退款要走财务流程,成本高、周期长。Stripe 等成熟计费系统的默认行为也是如此。
这些数字不是拍脑袋,验证程序实跑结果(JDK 21):补差价 400.00、席位账单 625.00、降级下期生效,10 项断言全 PASS——proration 公式看着简单,实现时一个舍入错误就会在几万账单上放大成对账灾难。
四、按用量计费:先记账后出账
按用量模式的链路是“计量 → 聚合 → 出账”三段:
- 计量:业务链路上发计量事件(
tenant_id, feature, quantity, timestamp),异步写入。量大的话走消息队列削峰,绝不能让计量阻塞业务请求。 - 聚合:按账期聚合每个租户的用量,快照落库——出账依据必须可追溯,不能出账时再去原始明细里现算。
- 出账:账期结束后按聚合结果生成账单。
两个坑提前知道:封顶与配额——用量上不封顶会引来“恶意刷量”投诉,配额用超要不要硬停(影响生产客户)是产品决策,工程上要支持“软配额告警 + 硬配额拦截”两档;对账——计量事件会丢会重,月度账单要和业务侧日志抽样核对,重不丢靠幂等,幂等还要落到模型上:出账 job 以(订阅,账期)为唯一键生成账单,job 重复跑也写不出第二张。
五、常见误区
① 金额用 double。 300 × (30-10) / 30 这种浮点运算积累误差,账单金额一律 BigDecimal(更稳的是以“分”为单位的整数),舍入规则全局统一(如四舍五入到分)。
② 订阅不存价格快照。 套餐涨价后,老客户的账单跟着涨——这是违约级事故。价格跟着订阅走,套餐表只影响新订阅。
③ 升降级边界日期含糊。 “第 10 天升级”是第 10 天结束算还是当天就算?剩余 20 天还是 19 天?规则必须写死在代码里并全公司统一,两个算都能接受,漂着不行。
④ 退款和降级混为一谈。 降级是“下期少收”(周期生效),退款是“把已收的钱退回去”(财务流程 + 功能回收)。产品上把它们设计成两个动作,别共用一个接口。
系列导航
综合案例:《多租户 SaaS 平台架构分析与 Spring Boot 实现》——本文机制串成一条完整链路后的系统全貌
上一篇:《SaaS 套餐功能开关设计》——收了钱怎么“按单上菜”
《SaaS 架构演进》——系列开篇:商业模式如何驱动架构
《租户生命周期管理》——订阅到期如何触发租户冻结
《SaaS 多租户数据隔离》——所有租户机制的地基
《动态租户实现原理》——“当前租户”这个值的存储与清理
