Implement compression quota refunds and admin manual subscription

This commit is contained in:
2025-12-19 23:28:32 +08:00
commit 11f48fd3dd
106 changed files with 27848 additions and 0 deletions

191
docs/billing.md Normal file
View File

@@ -0,0 +1,191 @@
# 计费与用量设计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
- 匿名试用是否开放、开放到什么程度。