fix: reconcile ambiguous Stripe event ordering
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
237899745
2026-07-26 03:08:23 +08:00
parent 08000cc16e
commit 326a678249
6 changed files with 1081 additions and 262 deletions

View File

@@ -130,14 +130,18 @@ RETURNING used_units;
- `payments.provider_payment_id` ↔ Stripe `payment_intent.id`(或 charge id按实现选
### 4.2 Checkout / Portal
- Checkout后端创建 Stripe Checkout Session前端跳转 `checkout_url`
- Checkout后端按用户行锁串行创建 Stripe Checkout Session前端跳转 `checkout_url`服务端使用稳定 Customer 幂等键和待支付记录 ID 对应的 Session 幂等键,不依赖客户端 `Idempotency-Key`
- 每个用户只能映射一个非空 Stripe Customer同时只能存在一个未过期待支付记录和一个未取消 Stripe 订阅。并发请求复用同一 SessionCustomer 映射未持久化时禁止返回或创建 Session。
- 已有未取消 Stripe 订阅的用户不能再次进入订阅 Checkout升级、降级、续费和取消统一走 Portal避免多重周期扣费。
- Portal后端创建 Stripe Billing Portal Session前端跳转管理支付方式/取消订阅。
### 4.3 Stripe Webhook商用必须
要求:
- **验签**:使用 `STRIPE_WEBHOOK_SECRET` 校验 `Stripe-Signature`
- **事件幂等**:按 `provider_event_id` 去重(落库 `webhook_events`)。
- **乱序容忍**:订阅对象按 `(event.created, 事件优先级, event.id)` 保存独立水位;`deleted` 即使先到也会保留 tombstone`created/updated` 不得恢复已取消订阅。
- **乱序容忍**:订阅对象按 `(event.created, 事件优先级)` 保存独立水位;`deleted` 即使先到也会保留 tombstone`created/updated` 不得恢复已取消订阅。
- **同秒歧义**:两个不同事件具有相同 `(event.created, 事件优先级)` 时,不能用不透明的 Event ID 排序,必须从 Stripe 拉取当前订阅快照并以快照响应时间推进水位。
- **迁移对账**:历史版本用本地 `subscriptions.updated_at` 播种的非终态水位会标记为待对账API 后台任务持租约获取 Stripe 快照,成功后才清除标记。未映射 Customer 或 Price 的受管订阅事件返回失败并等待重试,不能标记为已处理。
- **并发一致性**`subscriptions(provider, provider_subscription_id)` 唯一,订阅业务写入与 `webhook_events=processed` 在同一事务提交。
- **可重放**:保存原始 payload脱敏用于排查。