# 计费与用量设计(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` / `source`(web/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_units`、`bytes_in`、`bytes_out` 等 配额判断: - 写入 `usage_events` 前先检查本周期 `used_units` 是否已达 `included_units_per_period`(并发场景需原子性:事务/锁/原子计数)。 - 失败时返回 `QUOTA_EXCEEDED`,并在响应中附带“已用/上限/重置时间”。 ### 2.4 并发安全的扣减算法(推荐) 目标:在高并发下也不能“超扣”。 推荐做法(Worker 侧每成功文件 1 次扣减): - 先拿到当期 `included_units`(来自 plan)。 - 用单条 SQL 做“条件更新”,只有未超额时才 +1: ```sql 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`(展示用) - `currency`、`total_amount` - `status`:draft/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.md`、`docs/database.md`、`docs/ui.md` 同步: - 套餐表(默认配额值)。 - 配额刷新周期(按订阅周期)。 - 超额策略(硬配额 / HTTP 402)。 - 匿名试用是否开放、开放到什么程度。