From b327798511d972178829351107a1666909ab84e7 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 9 Jul 2026 22:05:20 +0800 Subject: [PATCH] =?UTF-8?q?docs(pay):=20=E7=BB=9F=E4=B8=80=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E7=BC=96=E6=8E=92=E5=B1=82=E8=AE=BE=E8=AE=A1(provider?= =?UTF-8?q?=20=E6=97=A0=E5=85=B3,=E5=8A=A0=E6=B8=A0=E9=81=93=E4=B8=8D?= =?UTF-8?q?=E6=94=B9=E5=AE=A2=E6=88=B7=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Order+PaymentAttempt+Provider adapter(Create/HandleCallback/Query)三动作, 客户端只认 render_type 联合体(redirect/display_details/…)。首批 crypto (包裹 pangolin-pay,display_details)+ 哪吒(支付宝跳转 redirect,RSA/GET 回调, 灰产四方可随时换持牌)。统一开通管线复用 codes,source=pay。四端 Flutter 支付页。 登记 docs/index.html。 Co-Authored-By: Claude Opus 4.8 --- docs/index.html | 5 + docs/pay-orchestration-design.html | 273 +++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 docs/pay-orchestration-design.html diff --git a/docs/index.html b/docs/index.html index 174cc1e..270e573 100644 --- a/docs/index.html +++ b/docs/index.html @@ -44,6 +44,11 @@

设计方案 / Specs

+ +
统一支付编排层 · provider 无关架构(加渠道不改客户端)HTML
+
控制面新增支付编排层:Order 业务订单 + PaymentAttempt 支付尝试 + Provider 适配器(Create/HandleCallback/Query),客户端只认服务端下发的少数 render_type(redirect/display_details/…)联合体,不认具体平台 → 同形态新网关零改客户端。首批接 crypto(USDT-TRC20,包裹已部署 pangolin-pay,display_details) + 哪吒聚合支付(支付宝跳转,redirect,RSA 签/GET 回调,灰产四方·可随时换持牌)。统一开通管线(验签→定位→幂等→金额核对→复用 codes 开通,source=pay)+ Query 轮询兜底。四端 Flutter 统一支付页。含 SKU 目录/双币定价/两表 schema/边界与合规。
+
docs/pay-orchestration-design.html
+
支付落地方案 · 发卡/Reseller 收款 + 激活码自动发货 HTML
把「收钱」与品牌 VPN 主体解耦:收款外包给发卡平台/Reseller(他们承担支付宝/微信跑分、冻卡、跑路风险),你只交付「激活码」;客户端/用户中心只认码,codes 模块核销即生效。含完整流程图(钱流/码流/结算)、两种对接模型(A 预充卡密库存 / B API 实时签发 POST /v1/codes/issue)、三阶段落地节奏、对账与风险边界(主体永不碰跑分/中国支付)。灰产“付完秒发货”体验的合规化替身。
diff --git a/docs/pay-orchestration-design.html b/docs/pay-orchestration-design.html new file mode 100644 index 0000000..9338619 --- /dev/null +++ b/docs/pay-orchestration-design.html @@ -0,0 +1,273 @@ + + + + + +Pangolin 统一支付编排层设计 + + + +
+← 返回文档索引 + +

Pangolin 统一支付编排层设计

+

支付方式无关(provider-agnostic)架构 · client↔控制面协议固定 · 加渠道不改客户端 · 2026-07-09

+ +
+一句话:控制面新增一个支付编排层,把不同支付平台(哪吒聚合支付、加密货币、未来 Stripe/信用卡/epay)统一到「Order 业务订单 + PaymentAttempt 支付尝试 + Provider 适配器」模型;客户端只认服务端下发的少数几种渲染形态(render_type),不认具体平台。同形态的新网关 = 零改客户端;只有全新交互形态才动客户端。 +
+ +

1. 背景与目标

+

Pangolin 当前没有 App 内支付:用户变 pro 的唯一路径是兑换激活码(POST /v1/redeem),客户端"购买"卡片是 onTap: () {} 空实现。控制面没有订单系统(migration 000001–000021 无 orders/payment 表),只有 codes 激活码履约。这是一块绿地。

+

目标:

+ + +

2. 可行性结论 可行

+

调研(Stripe next_action 模型 + 多 PSP 编排实践)确认:不同支付平台的交互形态是有限的,而平台是无限的。把形态收敛成客户端认识的 render_type 联合体,即可让客户端 provider 无关。

+

首批两渠道的形态都落在客户端已有能力上:

+ + + + + + +
渠道render_type客户端如何渲染客户端现状
哪吒(支付宝)redirect拿 payurl → 外部浏览器打开已有 url_launcher + SSO 跳转范式
加密货币(USDT)display_details展示地址+精确金额卡片已有 就是把 /_test 卡搬进 App
+

边界(诚实说):客户端改动频率 = 新 render_type 出现频率(极低)+ OS scheme 白名单变化,不是 provider 频率。真正要碰客户端的只有:全新交互形态、拉起 App 的 scheme 白名单(Info.plist/manifest)、信用卡 3DS / Apple·Google Pay 的专有 SDK。

+ +

3. 架构总览

+
客户端(只认协议,不认平台) 控制面 :8080 编排层 外部平台 + GET /v1/pay/methods ─────────────▶ 下发启用渠道 [{id,name,icon,render_type}] + POST /v1/pay/orders {sku,method} ─▶ ┌─ Order(业务:谁/买啥/多少钱/开通什么) ← 真相源, order_no 幂等 + ◀── {order_no, session: │ └─ PaymentAttempt(一次尝试:provider/单号/状态) ← 支持换渠道重试 + {render_type,payload}} │ │ + 按 render_type 分发渲染: │ ▼ Provider 注册表 map[method]Provider + redirect → 开浏览器 │ ├─ crypto adapter ──▶ pangolin-pay(已部署) ──▶ TRON 链 + display_details→ 地址+金额卡片 │ ├─ nazha adapter ──▶ nzzf.org /api/pay/create ─▶ 支付宝 + GET /v1/pay/orders/{no} ──────────▶ │ └─ (未来) stripe / epay … 加 adapter,客户端零改 + ◀── {status} 轮询到 paid │ + │ 统一开通管线:验签(adapter)→定位订单→幂等→金额核对→开通订阅(source=pay) + ┌────────────────────────────────────┤◀── POST /v1/webhooks/pay/crypto (pangolin-pay, HMAC) + │ 所有形态最终都收敛到"轮询到 paid" │◀── GET /v1/webhooks/pay/nazha (哪吒, RSA, 注意是 GET) + └────────────────────────────────────┘ 兜底:Query 主动查单(crypto 靠链上轮询;哪吒回调可能被 GFW/CF 吞)
+ +

为什么 Order 与 PaymentAttempt 拆两层:同一笔订单,用户可能先扫哪吒超时、再换 crypto 付成功。换渠道 = 新建一个 Attempt,Order 不变。只有一个对象会让"换渠道重试"污染状态机。

+ +

4. 数据模型

+

控制面新增两张表(mysql + sqlite 双套 migration,金额一律 int64 最小单位,禁 float)。

+ +

4.1 pay_orders — 业务订单(购买/权益账本)

+
order_no      TEXT UNIQUE     -- 幂等主键, 形如 PAY2026...
+user_id       INTEGER         -- FK users.id(内部关联订阅)
+user_uuid     TEXT            -- 对外/跨系统标识(= pay 侧 user_ref)
+sku           TEXT            -- pro-month / pro-quarter / pro-year
+plan_code     TEXT            -- pro(建单时锁定,回调改不了)
+duration_days INTEGER         -- 30 / 90 / 365(建单时锁定)
+status        TEXT            -- created|pending|paid|expired|canceled
+subscription_id INTEGER NULL  -- 开通后回填(审计)
+created_at / paid_at / expires_at DATETIME
+

建单时就把 order_no → user / plan / 天数 落库。回调只能推进已存在的订单、开通其中预先锁定的套餐——回调无法指定"给谁开多久"。这是防回调伪造的纵深。

+ +

4.2 pay_attempts — 支付尝试(收款账本)

+
id            INTEGER PK
+order_no      TEXT            -- FK pay_orders
+method        TEXT            -- usdt_trc20 / alipay_nazha
+provider      TEXT            -- crypto / nazha
+provider_ref  TEXT            -- pangolin-pay 单号 / 哪吒 trade_no
+render_type   TEXT            -- redirect / display_details
+amount_minor  INTEGER         -- 该尝试的应收(按币种最小单位)
+currency      TEXT            -- USDT / CNY
+status        TEXT            -- created|pending|paid|failed|expired
+created_at / paid_at DATETIME
+UNIQUE(provider, provider_ref)   -- 回调幂等的命门
+ +

4.3 SKU 目录 + 定价 待你确认价格

+

SKU → (plan, 天数, 各币种价格)。哪吒收人民币(支付宝)、crypto 收 USDT,故按币种各定一价(不做实时汇率换算,固定价是产品决策)。首版 SKU 目录作为控制面代码常量(后续可迁 DB)。

+ + + + + + + +
SKUplan天数USDT(crypto)CNY(哪吒/支付宝)
pro-monthpro30$3.99¥28(待定)
pro-quarterpro90$9.99¥68(待定)
pro-yearpro365$29.99¥198(待定)
+

USDT 价你已拍板;CNY 价是我锚定现有 ¥25/月 的提案,发布前请确认

+ +

5. Provider 适配器接口

+

每个支付平台实现同一个接口,控制面只依赖接口。三个动作是最小完备集。

+
type Provider interface {
+    Method() MethodInfo   // 元信息:{ID, Name, IconURL, RenderType, Currency, Enabled, Min, Max}
+    Create(ctx, o Order) (Session, error)                       // ① 下单→返回渲染指令
+    HandleCallback(ctx, r *http.Request) (CallbackResult, error) // ② 验签+解析+归一化
+    Query(ctx, providerRef string) (PaymentStatus, *PaidEvent, error) // ③ 主动查单兜底
+}
+
+type Session struct {   // Create 的返回,客户端按 RenderType 分发
+    ProviderRef string
+    RenderType  string          // redirect / display_details / ...
+    Payload     json.RawMessage // 按 RenderType 定 shape
+    ExpiresAt   time.Time
+}
+type PaidEvent struct { OrderNo string; ProviderRef string; AmountMinor int64; Currency string; PaidAt time.Time; TxRef string }
+type CallbackResult struct { Handled bool; Event *PaidEvent; AckBody []byte } // AckBody: 哪吒要回 "success"
+ +

5.1 crypto adapter(包裹已部署的 pangolin-pay)

+

pay-server 保持独立进程(持 xpub、看链,最小权限),控制面 crypto adapter 只是它的 HTTP 客户端:

+ + +

5.2 nazha adapter(哪吒聚合支付)

+ + +

6. 客户端契约

+

6.1 REST 端点(控制面,全部 Bearer)

+
GET  /v1/pay/methods                 → 200 [{id,name,icon,render_type,currency,enabled,min,max}]
+POST /v1/pay/orders {sku, method}    → 201 {order_no, status, session:{render_type,payload,expires_at}}
+GET  /v1/pay/orders/{order_no}       → 200 {status, plan, expires_at, session?}
+POST /v1/pay/orders/{order_no}/retry {method} → 201 新 session(Order 不变)
+--- 平台→控制面(公开,不带 Bearer)---
+POST /v1/webhooks/pay/crypto         (pangolin-pay, HMAC 验签)
+GET  /v1/webhooks/pay/nazha          (哪吒, RSA 验签, GET)
+ +

6.2 render_type 联合体(写死在协议里)

+ + + + + + + + + +
render_typepayload首版
redirect{url, return_hint}v1 哪吒
display_details{fields:[{label,value,copyable}], amount, currency, expires_at, poll_interval_sec}v1 crypto
qr_code{qr_content, display_amount, expires_at}预留
redirect_native{app_scheme, universal_link, fallback_url}预留
sdk_handoff{sdk, params}预留(逃逸口)
+

护栏:客户端遇到未知 render_type → 不显示该方法 / 提示"请升级 App"。无论哪种形态,客户端最终都收敛到同一个"轮询 GET /orders/{no} 到 paid"。

+ +

6.3 客户端改动:一次 vs 永不

+ + + + + + + +
动作
一次(本轮做)新增 PaymentClient(Dart, 四端共享)+ 支付页:方法选择器(读 /v1/pay/methods)→ 建单 → 按 render_type 分发(redirect=url_launcher 外部浏览器;display_details=地址卡)→ 轮询到 paid → 成功页。接上现有 no-op 购买入口。无需新增原生依赖(url_launcher 已在,无需 webview)。
永不(加渠道零改)新增 provider 若落在已有 render_type(epay/Stripe Checkout = redirect;另一条链 = display_details)→ 仅服务端加 adapter + 下发一项。
未来才碰全新交互形态(新 render_type);拉起 App 的 scheme 白名单;信用卡 3DS / Apple·Google Pay 专有 SDK。
+ +

7. 统一开通管线

+

webhook 与 Query 轮询最终都产出 PaidEvent,走同一条幂等开通逻辑:

+
① 验签           adapter.HandleCallback / Query  (各平台不同:HMAC / RSA / 链上确认)
+② 定位订单       provider_ref → pay_attempts → pay_orders
+③ 幂等           attempt 已 paid?  order 已 granted?  → 是则直接回 200,不重复开通
+④ 金额/币种核对   PaidEvent.amount == attempt.amount_minor && currency 一致
+⑤ 事务开通       { order→paid; attempt→paid; 开通订阅(source=pay); 回填 subscription_id; 审计 }
+ +

7.1 复用并重构现有开通逻辑

+

开通能力现埋在 codes.Service.applySubscription(server/internal/codes/service.go:235),仅作为 Redeem 事务的一步、且 CreateSubscriptionsource 硬编码 'code'。改动:

+ + +

8. 哪吒合规风险 务必知情

+
+调研明确:哪吒是匿名注册(Telegram 开户、无 KYC)、USDT 结算、带代付出款、仅支付宝单通道、无公司主体 / ICP / 牌照的四方聚合/代收平台。风险:资金冻结、平台跑路(无主体可追索)、商户帮信罪敞口。 +
+

缓解 = 本架构天生解耦:哪吒只是一个 redirect adapter,将来换成持牌支付宝直连 / Stripe / epay,客户端零改、订单与开通逻辑零改,只换服务端一个 adapter。可"先用它跑量、随时替换",不锁死。crypto(USDT 自托管)作为干净主轨并存。

+ +

9. 错误处理与边界

+ + +

10. 测试策略

+ + +

11. 首版范围与实施阶段

+

范围:后端编排层 + crypto & 哪吒两 adapter + 四端 Flutter 统一支付页一次到位。用户中心(web)支付页作为快速跟进(复用同一套 REST,小改)。实施在计划里分阶段:

+
    +
  1. 控制面:schema(pay_orders/pay_attempts)+ store + 开通重构 + source=pay migration。
  2. +
  3. Provider 抽象 + 注册表 + crypto adapter + pay-server 出站 webhook。
  4. +
  5. 哪吒 adapter(RSA 签/验、GET 回调、查单)。
  6. +
  7. 客户端 REST(methods/orders/retry/webhooks)+ 统一开通管线 + Query 轮询 worker。
  8. +
  9. Flutter 支付页(方法选择器 + render_type 分发 + 轮询)+ 接上购买入口。
  10. +
  11. 端到端联调(两轨小额)。
  12. +
+ +

12. 待确认清单(评审 gate)

+ + + + + + + + + + +
状态
SKU 三档 + USDT 价($3.99/$9.99/$29.99)已定
CNY 价(¥28/¥68/¥198 提案)待你确认
哪吒商户注册(pid + RSA 密钥对)你去 Telegram 开户后给我
return_url 成功落地页(用户中心 or 官网一页)待定
Order + PaymentAttempt 两层模型本设计采用
用户中心 web 支付页是否本轮做提案:快速跟进,非 v1
+ +

相关文档:支付渠道选型总览 · 干净 USDT 方案 · 单地址收款模型 · 加密交易引擎

+
+ +