Files
ystp/docs/billing.md

7.9 KiB
Raw Blame History

计费与用量设计Stripe + 硬配额)- ImageForge

目标:形成“套餐/订阅/用量/发票/支付/风控”可落地的闭环,为后续实现提供清晰边界与数据模型依据。


1. 计费模型(已确认)

已确认口径:

  • 支付渠道:Stripe
  • 超额策略:硬配额(超过当期额度返回 QUOTA_EXCEEDED / HTTP 402
  • 配额周期:按订阅周期Stripe current_period_start ~ current_period_end),不是自然月

1.0 周期定义(统一口径)

为避免前后端与账本口径不一致,本项目将“周期”分为三类:

  • Pro/Business付费:按 Stripe 订阅周期(current_period_start ~ current_period_end)。
  • Free未订阅按自然月UTC+8重置用于展示“本月已用/剩余/重置时间”。
  • 匿名试用按自然日UTC+8重置仅用于网站试用默认每日 10 次)。

1.1 计量单位

compression_unit:每成功压缩 1 个输出文件计 1 单位。

规则:

  • 同一任务中失败的文件不计费。
  • 同一请求重试若携带相同 Idempotency-Key,不重复计费(返回相同结果)。
  • 输出格式转换png->webp 等)不额外加价(首期),后续可按“高级功能”计价。

1.2 套餐Plan

套餐是“功能 + 配额 + 限制”的集合建议存为可配置DB 或配置中心)。

最小字段:

  • included_units_per_period:周期含量(对付费为订阅周期;对 Free 为自然月)。
  • max_file_size_mb / max_files_per_batch / concurrency_limit
  • retention_days:结果保留期。
  • features:例如 webhook、团队、IP 白名单等开关。

1.3 订阅Subscription

采用“月度订阅 + 含量(硬配额)”:

  • 用户在每个订阅周期获得固定含量(included_units_per_period)。
  • 超出含量:直接拒绝(QUOTA_EXCEEDED / HTTP 402)。

2. 用量计算与配额扣减Metering

2.1 何时扣减

  • 成功生成输出时扣减(以文件粒度)。
  • 对于异步任务Worker 完成文件压缩后写入用量事件API/前端通过任务查询看到实时用量。

2.2 幂等与去重

需要两层保护:

  1. 请求幂等Idempotency-Key 防止重复创建任务/重复扣减。
  2. 用量幂等:每个输出文件生成唯一 usage_event_id(或以 task_file_id 唯一)确保不会重复入账。

2.3 用量数据结构(建议)

  • usage_events明细账本append-only用于可追溯与对账。
    • user_id / api_key_id / sourceweb/api
    • task_id / task_file_id
    • units(通常为 1
    • bytes_in / bytes_out / format_in / format_out
    • occurred_at
  • usage_periods:按订阅周期聚合(加速配额判断)。
    • user_id + period_start + period_end 唯一
    • used_unitsbytes_inbytes_out

配额判断:

  • 写入 usage_events 前先检查本周期 used_units 是否已达 included_units_per_period(并发场景需原子性:事务/锁/原子计数)。
  • 失败时返回 QUOTA_EXCEEDED,并在响应中附带“已用/上限/重置时间”。

2.4 并发安全的扣减算法(推荐)

目标:在高并发下也不能“超扣”。

推荐做法Worker 侧每成功文件 1 次扣减):

  • 先拿到当期 included_units(来自 plan
  • 用单条 SQL 做“条件更新”,只有未超额时才 +1
UPDATE usage_periods
SET used_units = used_units + 1,
    bytes_in   = bytes_in + $1,
    bytes_out  = bytes_out + $2,
    updated_at = NOW()
WHERE user_id = $3
  AND period_start = $4
  AND period_end = $5
  AND used_units + 1 <= $6
RETURNING used_units;

如果 RETURNING 无行0 行更新),表示额度已耗尽:该文件应标记为 QUOTA_EXCEEDED,并丢弃输出(不返回/不落盘/不入账)。

说明为保持“成功才扣减”的口径2.4 默认按“成功后尝试扣减”实现;在配额临界点可能出现“先压缩、后发现额度不足”的少量无效计算。若要完全避免,可升级为“预留额度 + 失败回滚”的方案(复杂度更高,后续可做 V1+ 优化)。

2.5 批量任务与配额(硬配额)

为尽量减少“上传后才发现额度不足”的体验问题,建议双层策略:

  • 创建任务前预检remaining_units < files_count 时直接返回 402 QUOTA_EXCEEDED(不创建任务)。
  • 扣减时最终校验:写入账本/聚合时仍按 2.4 做原子扣减,防止并发竞态导致超额。

说明:

  • 批量任务的计量仍以“成功文件数”为准;失败文件(含 QUOTA_EXCEEDED)不计费。
  • 前端建议在上传前调用 GET /billing/usage登录或读取配额头API做本地提示/拦截。

3. 订阅生命周期(状态机)

3.1 状态

建议:

  • trialing:试用中(可选)
  • active:正常
  • past_due:欠费(支付失败)
  • paused:暂停(可选)
  • canceled:已取消
  • incomplete:创建未完成(例如支付页未完成)

3.2 关键动作

  • 升级/降级套餐:下周期生效(首期建议),避免按天结算复杂度。
  • 取消订阅:cancel_at_period_end=true,到期生效;到期后自动降级到 Free不续费

4. 支付集成Stripe

4.1 核心对象映射(建议)

  • users.billing_customer_id ↔ Stripe customer.id
  • plans ↔ Stripe product/price(建议每个 plan 对应一个 Stripe price
  • subscriptions.provider_subscription_id ↔ Stripe subscription.id
  • invoices.provider_invoice_id ↔ Stripe invoice.id
  • payments.provider_payment_id ↔ Stripe payment_intent.id(或 charge id按实现选

4.2 Checkout / Portal

  • Checkout后端创建 Stripe Checkout Session前端跳转 checkout_url
  • Portal后端创建 Stripe Billing Portal Session前端跳转管理支付方式/取消订阅。

4.3 Stripe Webhook商用必须

要求:

  • 验签:使用 STRIPE_WEBHOOK_SECRET 校验 Stripe-Signature
  • 事件幂等:按 provider_event_id 去重(落库 webhook_events)。
  • 乱序容忍:以 Stripe 事件时间 + 当前 DB 状态做“只前进不回退”更新。
  • 可重放:保存原始 payload脱敏用于排查。

建议首期处理的事件(示例):

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.paid
  • invoice.payment_failed

5. 发票与对账

5.1 发票Invoice

发票用于:

  • 向用户展示本期费用、税务信息(后续)、支付状态。
  • 与支付记录/订阅周期关联。

最小字段:

  • invoice_number(展示用)
  • currencytotal_amount
  • statusdraft/open/paid/void/uncollectible可简化
  • period_start / period_end
  • paid_at

5.2 对账原则

  • usage_events 为最终真相source of truth
  • invoice 为结算结果(面向用户)。
  • 管理后台可以按用户/周期拉取用量明细,支持导出 CSV。

6. 风控策略(与计费强相关)

6.1 防盗刷

  • API Key 支持权限范围、速率限制、可选IP 白名单、(可选)域名白名单(仅对 Web 场景有效)。
  • 异常检测:短时间内用量突增、来自异常国家/ASN、重复失败率高。

6.2 欠费与滥用处理

  • 欠费past_due建议限制创建新任务并在 UI 引导用户去 Portal 完成支付。
  • 严重滥用:冻结用户/API Key并记录审计日志。

7. 需要在文档中保持一致的“最终口径”

上线前必须定稿并在 docs/api.mddocs/database.mddocs/ui.md 同步:

  • 套餐表(默认配额值)。
  • 配额刷新周期(按订阅周期)。
  • 超额策略(硬配额 / HTTP 402
  • 匿名试用是否开放、开放到什么程度。