升鲜宝 供应链管理系统 SaaS 自动计费引擎详细算法说明书(Algorithm Spec)

升鲜宝  生鲜配送供应链管理系统SaaS 自动计费引擎详细算法说明书(Algorithm Spec)

适用架构:阿里云 RDS + ECS + Nginx;SaaS 平台库 + 每租户独立业务库(可分片)
目标:订阅计费 + 用量计费 + 账单生成 + 支付对账 + 到期冻结 + 授权快照,一套闭环
关键原则:可重算、可审计、幂等、安全、可扩展


1. 术语与对象

  • Tenant(租户):购买升鲜宝 SaaS 服务的企业实体

  • Plan(套餐):订阅基础(包含模块、基础价、周期)

  • Component(计费组件):收费项(订阅/用量/一次性)

  • Meter(用量指标):用量计费的统计口径(门店数、订单数、API 次数等)

  • Invoice(账单):一个计费周期内的应收单

  • Entitlement Snapshot(授权快照):业务系统只读的最终模块+配额+到期信息

  • Proration(按比例补差):周期中途升级产生的补差价


2. 计费引擎职责边界

2.1 输入

  • 租户订阅:tenant_subscription

  • 套餐/价格:saas_plan / saas_price_component / saas_price_tier

  • 用量快照:tenant_usage_daily(由采集器写入)

  • 变更记录:subscription_change

  • 支付结果:tenant_payment 回调

2.2 输出

  • 账单:tenant_invoice / tenant_invoice_item

  • 审计:invoice_calculation_log

  • 授权快照:tenant_entitlement_snapshot

  • 状态变更:tenant.statustenant_account_state_log


3. 数据一致性与幂等规范(必须)

3.1 幂等键规范(建议)

  • 账单幂等键invoice_biz_key = tenantId + ':' + periodStart + ':' + periodEnd + ':' + subscriptionId

  • 账单号INV-YYYYMM-<tenantId>-<seq>

  • 支付单幂等键pay_biz_key = invoiceId + ':' + channel

3.2 账单生成幂等策略

  • 插入 invoice 前先查:同 invoice_biz_key 是否存在

  • 存在则:

    • 若状态=已支付 → 返回已生成

    • 若状态=待支付/逾期 → 可“重算并覆盖 item”(需带版本号与审计记录)

3.3 审计要求

每次计算必须写:invoice_calculation_log

  • 输入快照:订阅信息、组件信息、用量数据、折扣、税率、proration

  • 输出快照:明细、汇总金额、四舍五入规则


4. 计费类型与价格模型

4.1 计费类型(charge_type)

  • 1 = 订阅(Subscription)

  • 2 = 用量(Usage)

  • 3 = 一次性(One-time)

4.2 价格模型(pricing_model)

  • 1 = 固定价(Fixed)

  • 2 = 阶梯价(Tiered)

  • 3 = 按量单价(PerUnit)


5. 用量计费:数据采集与口径

5.1 推荐采集方式:日聚合快照

业务系统每日写入平台库(或平台拉取):

  • tenant_usage_daily(tenant_id, meter_code, stat_date, used_value)

计费只读 usage 表,不要扫描业务大表(订单、流水会非常大)。

5.2 用量统计口径建议

  • SHOP_COUNT:门店数量(当天快照)

  • USER_COUNT:用户数量(当天快照)

  • ORDER_COUNT:订单量(当日新增量)

  • API_CALL:网关计数(当日新增量)

  • STORAGE_GB:存储量(当日快照/最大值)


6. 计费周期与结算规则

6.1 周期定义

  • 月付:periodStart ~ periodEnd(自然月或按订阅起始日滚动,推荐自然月)

  • 年付:自然年或按起始日滚动(推荐按起始日滚动,避免跨年复杂)

6.2 结算窗口

  • 账单生成时间:到期前 T-7天(可配置)

  • 宽限期:到期后 grace_days(例如 7 天)

6.3 取数策略(用量)

  • 累计型(如 ORDER_COUNT):对 stat_date 在周期内求和

  • 快照型(如 SHOP_COUNT):取周期内最大值(或最后一天值),推荐 最大值 防止规避


7. 核心算法:账单生成(Invoice Build)

7.1 总流程(伪代码)

 
buildInvoice(tenantId, subscriptionId, periodStart, periodEnd): sub = loadSubscription(subscriptionId) plan = loadPlan(sub.planId) components = loadComponents(plan.id) invoiceKey = makeInvoiceBizKey(tenantId, periodStart, periodEnd, subscriptionId) if invoiceExists(invoiceKey): return existingInvoice (or rebuildItems by policy) items = [] for component in components where enabled: item = calcComponent(component, tenantId, periodStart, periodEnd, sub) items.add(item) prorationItems = calcProrationIfAny(tenantId, subscriptionId, periodStart, periodEnd) items.addAll(prorationItems) subtotal = sum(item.amount) discount = calcDiscount(tenantId, items) tax = calcTax(tenantId, subtotal - discount) total = roundMoney(subtotal - discount + tax) invoice = persistInvoice(invoiceKey, subtotal, discount, tax, total, dueTime) persistItems(invoice.id, items) writeCalcLog(invoice.id, inputSnapshot, outputSnapshot) return invoice

8. 组件计算算法(Component Calc)

8.1 订阅组件(charge_type=1)

固定价(pricing_model=1)

  • quantity = 1

  • unit_price = plan.base_price 或 component 固定价

  • amount = unit_price

阶梯(订阅一般不用阶梯)

  • 不建议:订阅类保持固定价,阶梯放到用量


8.2 用量组件(charge_type=2)

获取用量(usageValue)

 
usageValue = getUsage(meter_code, tenantId, periodStart, periodEnd, meter_policy)

meter_policy 建议:

  • 快照型:max(daily.used_value)

  • 累计型:sum(daily.used_value)

价格模型 1:固定价(Fixed)

  • amount = fixed_price

  • 不依赖 usageValue(用于“包含多少量的包月”也可以配合阶梯做)

价格模型 3:按量单价(PerUnit)

  • quantity = usageValue

  • amount = roundMoney(quantity * unit_price)

价格模型 2:阶梯价(Tiered)

阶梯表:tier_start, tier_end, unit_price, fixed_price

两种阶梯模式(强烈建议选一种并固化):

  1. 分段累进(推荐,类似电费)

  • 每段按照该段单价计算

  1. 整段匹配(不推荐)

  • 落在哪一段,全部按该段单价计算(容易引争议)

分段累进伪代码:

 
calcTiered(usageValue, tiers): remain = usageValue amount = 0 for tier in tiers order by tier_start asc: segStart = tier_start segEnd = tier_end (0 means inf) segCap = (segEnd==0 ? INF : segEnd - segStart + 1) segQty = min(remain, segCap) if segQty <= 0: continue amount += segQty * tier.unit_price + tier.fixed_price remain -= segQty if remain <= 0: break return roundMoney(amount)

9. Proration(升级补差)算法

9.1 触发条件

订阅周期内发生:

  • 升级(new_plan_price > old_plan_price)

  • 或增购模块(等价升级)

9.2 计算原则(推荐)

按剩余天数补差(按日计)

 
remainingDays = daysBetween(changeTime, periodEnd) periodDays = daysBetween(periodStart, periodEnd) diffPrice = (newPlanBasePrice - oldPlanBasePrice) proration = diffPrice * remainingDays / periodDays proration = roundMoney(proration)

9.3 输出为账单明细

生成 tenant_invoice_item

  • component_code = PRORATION

  • amount = proration

  • meta_json 记录 old/new plan 与天数

降级一般下期生效,不退费(除非你要做更复杂的退款策略)。


10. 折扣与优惠算法(可选但建议预留)

折扣来源:

  • 优惠券(一次性抵扣)

  • 年付折扣(比例)

  • 渠道代理折扣(比例/固定)

计算顺序建议:

  1. subtotal(所有 item)

  2. discount(可按 item 分类:订阅折扣、用量折扣)

  3. tax(对折后金额计税)

  4. total

折扣必须写入 invoice_calculation_log,并可复算。


11. 税费算法(可选)

如果你在美国也可能涉及税(州税),建议税费做成策略:

  • 税率来源:tenant.billing_address / tax_region

  • tax_amount = roundMoney((subtotal - discount) * taxRate)


12. 金额精度与舍入规范(升鲜宝统一规则)

建议全平台统一:

  • 金额:DECIMAL(18,2)四舍五入

  • 单价/成本/用量单价:DECIMAL(18,6)

  • 运算中间值用 BigDecimal,最后落库时 setScale(2, HALF_UP)


13. 自动计费调度(Jobs)详细规则

13.1 DailyInvoiceGenerateJob(每日生成账单)

运行频率:每天 02:00(可配置)

筛选条件:

  • tenant_subscription.status=有效

  • end_time - now <= invoice_generate_days(例如 7 天)

  • 当前周期账单不存在(幂等键)

输出:

  • invoice + items

  • 通知(短信/邮件/站内信)

13.2 AutoRenewJob(自动续费)

筛选:

  • auto_renew=1

  • invoice.status=待支付

  • 到期前 X 天或到期当天

动作:

  • 创建支付单(tenant_payment)

  • 拉起扣款(Stripe/支付宝/微信或线下)

13.3 ExpireEnforcementJob(到期冻结)

频率:每小时

规则:

  • now > subscription.end_time

  • 且 invoice 未支付

  • 且 now > grace_end_time

动作:

  • tenant.status = 欠费停用

  • tenant_entitlement_snapshot.status=冻结

  • 业务系统 AOP 拦截(返回“租户已过期/欠费”)

13.4 UsageAggregateJob(用量汇总)

频率:每日 01:00

  • 将业务系统上报的用量落 tenant_usage_daily

  • 或平台从业务库采集(不推荐扫大表)


14. 支付回调与对账算法

14.1 支付回调幂等

  • pay_no 或第三方交易号做唯一

  • 重复回调只更新一次

14.2 回调成功后的状态机

 
onPaymentSuccess(pay_no): payment.status = 成功 invoice.status = 已支付 extendSubscription(subscriptionId, nextPeriodEnd) refreshEntitlementSnapshot(tenantId) tenant.status = 正常

14.3 账单部分支付(可选)

如支持对公转账分笔:

  • invoice.status = 部分支付

  • paid_amount 记录累计

  • paid_amount >= total → 已支付


15. 授权快照(Entitlement Snapshot)刷新算法

15.1 输出内容

  • module_json:模块开关

  • quota_json:配额上限

  • expire_time:订阅到期时间

  • status:有效/冻结/过期

15.2 计算优先级

  1. 当前订阅 plan 的 module 列表

  2. tenant_module_override(运营后台强制开/关)

  3. 配额:plan 默认 quota + tenant_quota_override(可选)

  4. 到期:取 subscription.end_time(已支付后的)

15.3 刷新时机

  • 支付成功

  • 订阅升级/降级生效

  • 运营调整模块或配额

  • 到期冻结/解冻


16. 异常与重试策略(必须)

16.1 可重试的任务类型

  • 账单生成失败:DB 临时不可用、锁等待超时、网络抖动

  • 用量汇总失败:上报延迟、聚合任务异常

  • 授权快照刷新失败:平台库写入失败

  • 支付回调处理失败:验签失败/数据库异常/幂等冲突

  • 自动续费扣款失败:支付渠道超时/失败

16.2 重试机制(推荐 Outbox + Job)

建议计费引擎所有异步动作统一走 billing_outbox_event(可选新增表):

  • 事件写入平台库(与业务状态同事务)

  • 定时 Job 扫描未成功事件进行重试

  • 指数退避:1m / 5m / 15m / 1h / 6h

  • 最大重试次数达到阈值 → 标记 DEAD 并告警

事件示例

  • INVOICE_CREATED

  • PAYMENT_CREATED

  • ENTITLEMENT_REFRESH

  • SUBSCRIPTION_EXTEND

  • TENANT_FREEZE


17. 并发控制与锁策略(关键)

17.1 账单生成并发(同租户同周期只能生成一次)

推荐两种方式:

方式 A:数据库唯一约束(强烈推荐)

  • tenant_invoice 增加唯一键:uk_invoice_biz_key(invoice_biz_key)

  • 多线程并发插入时,只有一个成功,其它捕获重复键即可返回已存在账单

方式 B:分布式锁(可选)

  • Redis 锁 key:LOCK:INVOICE:{tenantId}:{periodStart}

  • 超时 30s,防止死锁

  • 仍建议保留数据库唯一约束做最终兜底

17.2 支付回调并发(同 pay_no 只处理一次)

  • tenant_payment.pay_no 唯一键

  • payment_callback_log.pay_no 唯一键(可选)

  • 回调处理逻辑:先落日志再处理,保证可追溯


18. 可重算与对账机制(审计级要求)

18.1 为什么必须可重算

  • 用量口径调整

  • 价格策略调整(tier 变更)

  • 税率变更

  • 折扣策略变更

  • Bug 修复后要能“回放”历史账单计算

18.2 重算策略(两种)

策略 1:只对未支付账单重算

  • invoice.status in (待支付, 逾期) 才允许重算

  • 重算会:删除旧 items → 写新 items → 写 calculation_log 新版本

策略 2:支付后不改账单,只做“差异单”

  • 已支付账单不允许改动

  • 差异通过下一期账单增加 ADJUSTMENT 明细体现(更符合财务审计)

18.3 建议增加字段(便于审计)

tenant_invoice 增加:

  • invoice_biz_key(幂等键)

  • calc_version(计算版本号,如 2026.02)

  • recalc_count(重算次数)

  • locked(支付后锁定)


19. 用量口径:最大值/累计值/去重(核心争议点)

19.1 快照型用量(门店数/用户数/SKU 数)

建议取 周期最大值

  • 避免用户在结算前临时删减规避计费

  • 更贴近资源占用事实

 
usageValue = max(daily.used_value within period)

19.2 累计型用量(订单数/API 次数)

建议取 周期内累计和

 
usageValue = sum(daily.used_value within period)

19.3 去重口径(如活跃用户数)

如果要统计“独立活跃用户”,不要用简单 sum,应改为:

  • 日去重集合 → 月去重(成本较高)

  • 或用数据仓库/日志系统(后续扩展)


20. 账单项目分类建议(升鲜宝标准)

20.1 component_code 命名规范

  • 订阅类:BASE_SUBSCRIPTION

  • 用量类:SHOP_COUNT / USER_COUNT / ORDER_COUNT / API_CALL / STORAGE_GB

  • 调整类:PRORATION / ADJUSTMENT

  • 税费类:TAX

  • 折扣类:DISCOUNT

20.2 invoice_item.meta_json 建议内容

用于可追溯与解释账单:

 
{ "meterCode": "ORDER_COUNT", "policy": "SUM", "periodStart": 1735689600000, "periodEnd": 1738367999000, "usageValue": 5231, "tierDetail": [ {"start":0,"end":1000,"unitPrice":0.02,"qty":1000,"amount":20.00}, {"start":1001,"end":0,"unitPrice":0.015,"qty":4231,"amount":63.465} ], "rounding": "HALF_UP", "calcVersion": "2026.02" }

21. 欠费与宽限期算法(Enforcement Spec)

21.1 状态机建议

  • tenant.status:正常 / 试用 / 冻结 / 欠费停用

  • subscription.status:有效 / 过期 / 取消 / 冻结

21.2 冻结触发规则(推荐)

 
if now > subscription.end_time: if invoice(本周期) 已支付: 续期,刷新 end_time else if now <= grace_end_time: 允许只读/限制写(可配置) else: tenant.status=欠费停用,授权快照=冻结

21.3 宽限期策略(建议给配置)

  • 宽限期只读:允许查询、不允许新增单据/审核

  • 宽限期限制写:只允许支付/续费相关操作

  • 无宽限期:到期立刻停用(不推荐)


22. 自动续费算法(Auto Renew)

22.1 触发条件

  • subscription.auto_renew=1

  • 当前周期账单存在且 status=待支付

  • 当前时间进入扣款窗口(到期前 1 天/到期当天)

22.2 扣款重试策略

  • 第一次失败:5 分钟后重试

  • 连续失败:改为每日一次

  • 达到重试阈值:通知租户管理员 + 运营

22.3 成功后续期算法

月付:

  • newEnd = addMonths(end_time, 1)(按订阅规则:自然月或滚动月)
    年付:

  • newEnd = addYears(end_time, 1)

注意:一定以“原 end_time”做加法,避免多次回调导致延长过多。


23. 价格变更与历史账单的关系(Pricing Governance)

23.1 价格版本化(强烈建议)

当套餐/组件/阶梯调整时,不要覆盖旧记录,建议:

  • 新增 price_version 或直接用 created_at 做版本

  • 订阅绑定“价格版本”(或在 calculation_log 里冻结一份价格快照)

23.2 账单计算时冻结输入

每次生成账单时,把以下快照写入 invoice_calculation_log.input_snapshot_json

  • plan/base_price

  • component 定义

  • tiers 列表

  • 税率/折扣策略
    这样未来重算时能选择:

  • 用旧快照重算(对账一致)

  • 用新策略重算(纠错)


24. 性能与索引要求(平台库)

24.1 高查询表索引建议

  • tenant_subscription(end_time):到期扫描用

  • tenant_invoice(tenant_id, status, due_time):账单列表/逾期扫描

  • tenant_usage_daily(tenant_id, meter_code, stat_date):周期汇总

  • tenant_entitlement_snapshot(tenant_id):业务系统高频读

24.2 扫描任务的分页策略

  • 所有 Job 扫描必须分页(按主键/时间范围)

  • 避免一次扫描全表导致平台库抖动


25. 典型示例:一张账单如何计算(可对外解释)

25.1 场景

  • 月付套餐:基础订阅价 199 元

  • 用量:订单数 ORDER_COUNT 阶梯累进

    • 0~1000:0.02 元/单

    • 1001+:0.015 元/单

  • 本周期订单数:5231 单

  • 税率:0(示例)

25.2 计算

  • BASE_SUBSCRIPTION:199

  • ORDER_COUNT:

    • 1000 * 0.02 = 20

    • 4231 * 0.015 = 63.465 → 四舍五入 63.47

    • 合计 83.47

  • subtotal = 199 + 83.47 = 282.47

  • total = 282.47


26. 与业务系统的集成点(升鲜宝落地)

26.1 业务系统需要做什么

  • 业务库:无需关心计费表

  • 每次请求:

    • 读取 tenant_entitlement_snapshot(缓存)

    • 做模块授权 AOP + 配额校验(门店上限/用户上限等)

  • 用量采集:

    • 每日上报平台:tenant_usage_daily

26.2 平台系统需要做什么

  • 套餐/价格配置后台

  • 订阅管理后台(续费、升级、冻结)

  • 账单与支付后台

  • 用量报表与对账


27. 最小可上线版本(MVP 但可商业化)

必做:

  • plan / component / tier

  • subscription

  • invoice + items

  • payment 回调幂等

  • entitlement snapshot

  • 到期冻结 job

可延期:

  • 用量计费(先只做订阅固定价)

  • proration(先升级下期生效)

  • 税费/折扣/渠道分账


28. 你接下来落地时的“工程清单”(建议顺序)

    1. 平台库建表(Tenant/Catalog/Subscription/Billing/Payment/Entitlement)

    2. 定义模块枚举 + 配额枚举(升鲜宝标准)

    3. 用量采集器(daily)上线

    4. InvoiceGenerateJob(幂等 + 审计)上线

    5. Payment 回调处理(幂等 + 对账)上线

    6. EntitlementSnapshot 刷新机制上线

    7. ExpireEnforcementJob 上线(宽限期策略)

    8. 后台页面(运营/租户)逐步补齐

posted @ 2026-02-13 13:26  升鲜宝生鲜供应链系统  阅读(17)  评论(0)    收藏  举报