Files
sundynix-agentix/PAYMENT_DESIGN.md
T
Blizzard d5836fd190 docs: 支付设计一页纸(P5) —— 预付积分包路线,渠道适配器,双闸幂等入账
订阅制砍掉(等付费用户拉动),充值积分包复用现成 credit_ledger/GrantCredits;
指出必须先补的闸:GrantCredits 对 ref 无唯一约束,支付回调 at-least-once
会重复入账——订单状态机 CAS 主闸 + (kind,ref) 部分唯一索引兜底。
P5.1 用零资质的兑换码渠道先跑通全闭环,真渠道(支付宝/微信/Stripe)等拍板。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:47:41 +08:00

107 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 支付设计(SaaS P5)—— 充值积分包,不做订阅
> 2026-07-17。前置已就绪:P2 计量(`usage_event`/`credit_ledger`/物化余额/rollup)、
> P4 积分环(`GrantCredits` + 余额硬拦截)、薄 Web 面(账单页已有充值占位)。
> 本文一页纸定路线,增量照此施工;渠道选择(§3)留用户拍板。
## 1. 结论先行
- **做「预付积分包」充值,不做订阅**。理由:积分体系是现成的——支付入账只是给
`credit_ledger` 多一个自动化的 `grant` 来源,改动集中在「订单 + 回调」;订阅制
(周期扣费/升降级/按比例退差)是另一个量级的状态机,等有付费用户再议。
`Tenant.Plan`free/pro/enterprise)字段留着,将来订阅直接长在上面。
- **渠道做成适配器接口,先上一个「零资质渠道」跑通闭环**(兑换码/人工转账核销),
真渠道(支付宝/微信/Stripe)接进来只是多一个 adapter,不动骨架。
- **金额一律服务端说了算**:前端只传「包 ID」,价格/积分数由服务端订单锁定,
回调按订单校验金额,不信任任何客户端传值。
## 2. 与现有账本的对接点(都是现成的)
| 现有件 | 位置 | 支付怎么用 |
|---|---|---|
| `credit_ledger`append-onlykind: grant/usage/adjust`Ref` 字段) | `store/credit.go` | 支付入账 = `kind=grant, ref=order_id` |
| `GrantCredits(ctx, tenant, kind, micro, ref, memo)`(事务内:分录+物化余额) | `store/credit.go:151` | 回调确认后调用;**内部已 WithoutTenant**(跨租户写) |
| 余额硬拦截 `credit_enforce` | `store/setting.go` | 不动;充值到账即自然解封 |
| 薄 Web 面「用量与账单」页 | `sundynix-web/src/pages/Usage.tsx` | 占位文案换成真充值入口 |
| admin 计费页 + `/admin/usage` | 观测侧 | 加支付订单流观测(P5.3) |
**⚠️ 必须先补的闸:`GrantCredits``ref` 无唯一约束。** admin 手工充值无所谓;
支付回调会重复推送(渠道明文保证 at-least-once),不闸就是重复入账事故。
双保险:① 订单状态机 CAS 是主闸(见 §5);② `credit_ledger``(kind, ref)`
部分唯一索引(`WHERE kind='grant' AND ref<>''`)兜底,两道闸缺一不可。
## 3. 渠道决策(用户拍板,技术侧全兼容)
| 渠道 | 前提 | 形态 | 备注 |
|---|---|---|---|
| **兑换码/人工核销**P5.1 内置) | 无 | admin 生成码 → 用户在 Web 面输码入账 | 零资质立即可用;也是线下打款的核销通道 |
| 支付宝(当面付/电脑网站支付) | 企业主体 + 签约 | 二维码/跳转 | 国内首选,个体户也可签当面付 |
| 微信支付(Native) | 企业主体 + 商户号 | 二维码 | 与支付宝同形态,adapter 并列 |
| Stripe Checkout | 海外主体 | 跳转托管页 | 出海再接;回调形态同构(webhook+签名) |
Adapter 接口(`internal/payment/channel.go`):
```go
type Channel interface {
Name() string
// CreatePay 依据订单生成支付凭据(二维码内容/跳转 URL/兑换码提示)
CreatePay(ctx, o *PaymentOrder) (PayIntent, error)
// VerifyCallback 验签并解析回调 → (orderID, channelTxnID, paidAmountFen, error)
VerifyCallback(req *http.Request) (CallbackResult, error)
// QueryOrder 主动查单(掉单补偿用)
QueryOrder(ctx, orderID string) (OrderStatus, error)
}
```
## 4. 数据模型(新增两件)
```
sundynix_payment_order # 订单:支付侧事实源(与 credit_ledger 对账的另一条腿)
id 雪花 (= 对外 order_id, ledger.ref)
tenant_id 计费租户(下单时用 ResolveBillingTenantID 解析并锁定)
user_id 操作人(审计)
pack_id 积分包 ID
amount_fen 应付金额(分) # 服务端按包锁定
credits_micro 到账积分(micro) # 服务端按包锁定
channel redeem / alipay / wechat / stripe
status pending → paid | failed | expired ; paid → refunded(人工)
channel_txn 渠道流水号(回调带回)
paid_at
sundynix_credit_pack # 积分包配置(admin 可改,别硬编码)
id / name / credits_micro / price_fen / active / sort
```
## 5. 流程与幂等(核心就这一张图)
```
Web面账单页 → GET /billing/packs → 选包 → POST /billing/orders {pack_id}
→ 服务端建 pending 订单(锁价) → 返回 PayIntent(二维码/跳转/输码框)
用户支付 → 渠道回调 POST /billing/callback/:channel(公开路由,验签是唯一门)
→ VerifyCallback 验签 + 金额与订单核对
→ 一个事务内:UPDATE payment_order SET status='paid' WHERE id=? AND status='pending'
RowsAffected==0 → 已处理过,直接 200(幂等闸①)
==1 → GrantCredits(kind=grant, ref=order_id)(唯一索引兜底,幂等闸②)
→ 前端轮询 GET /billing/orders/:id 到 paid → 刷余额
掉单补偿:pending 超 15min 的订单定时 QueryOrder 补态;过期置 expired
```
- 回调路由是**公开**的(渠道服务器打不了 Bearer),安全完全靠验签 + 金额核对 +
订单状态机;这与 `/reports/:id/export` 公开的先例同构,但多了签名门。
- 退款先只做人工:admin 发起 → `adjust` 负分录 + 订单置 refunded;自动退款不做。
- 对账三条腿:渠道账单 ↔ `payment_order(paid)``ledger(kind=grant)`
日终脚本比对(P5.3,先出 admin 页面人肉看,再自动化)。
## 6. 分增量(每步「机制→单测→live」)
- **P5.1 订单骨架 + 兑换码渠道**(不依赖任何外部资质,全链路即刻可 live 验证):
两张表 + ledger 唯一索引 + Channel 接口 + redeem adapter + `/billing/*` 路由
(下单≥billing_admin? 不——**充值谁都该能充,挂 ≥member**viewer 只读仍拦)+
Web 面账单页真充值 UI + admin 生成兑换码。
- **P5.2 首个真渠道**(等拍板,推荐支付宝当面付):adapter + 回调路由 + 轮询查单。
- **P5.3 对账与观测**:admin「支付订单」流 + 日终对账 + 掉单补偿定时器。
- **P5.4 按需**:退款流程化、发票、微信/Stripe 并列。
## 7. 明确不做(本期)
订阅/自动续费、套餐权益(plan 仍是展示字段)、多币种、自动退款、发票自动化、
渠道分账。等真实付费流量拉动。