docs(pay): 统一支付编排层设计(provider 无关,加渠道不改客户端)

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 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-09 22:05:20 +08:00
parent bced7b35e0
commit b327798511
2 changed files with 278 additions and 0 deletions
+5
View File
@@ -44,6 +44,11 @@
</div>
<h2>设计方案 / Specs</h2>
<a class="doc" href="pay-orchestration-design.html">
<div class="t">统一支付编排层 · provider 无关架构(加渠道不改客户端)<span class="tag html">HTML</span></div>
<div class="d">控制面新增支付编排层:<b>Order 业务订单 + PaymentAttempt 支付尝试 + Provider 适配器(Create/HandleCallback/Query</b>,客户端只认服务端下发的少数 <b>render_type</b>redirect/display_details/…)联合体,不认具体平台 → 同形态新网关零改客户端。首批接 crypto(USDT-TRC20,包裹已部署 pangolin-paydisplay_details) + 哪吒聚合支付(支付宝跳转,redirect,RSA 签/GET 回调,<b>灰产四方·可随时换持牌</b>)。统一开通管线(验签→定位→幂等→金额核对→复用 codes 开通,source=pay+ Query 轮询兜底。四端 Flutter 统一支付页。含 SKU 目录/双币定价/两表 schema/边界与合规。</div>
<div class="path">docs/pay-orchestration-design.html</div>
</a>
<a class="doc" href="payment-reseller-fulfillment-design.html">
<div class="t">支付落地方案 · 发卡/Reseller 收款 + 激活码自动发货 <span class="tag html">HTML</span></div>
<div class="d">把「收钱」与品牌 VPN 主体解耦:收款外包给发卡平台/Reseller(他们承担支付宝/微信跑分、冻卡、跑路风险),你只交付「激活码」;客户端/用户中心只认码,codes 模块核销即生效。含完整流程图(钱流/码流/结算)、两种对接模型(A 预充卡密库存 / B API 实时签发 POST /v1/codes/issue)、三阶段落地节奏、对账与风险边界(主体永不碰跑分/中国支付)。灰产“付完秒发货”体验的合规化替身。</div>
+273
View File
@@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pangolin 统一支付编排层设计</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16px;margin:24px 0 8px;color:var(--fg)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd3df}
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
.tag.bad{background:rgba(224,106,106,.16);color:var(--bad)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
.card h3{margin-top:0;color:var(--accent2)}
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:13.5px}
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
th{color:var(--fg2);font-weight:600;font-size:13px}
ul,ol{padding-left:22px;margin:8px 0}
li{margin:5px 0}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
.small{color:var(--fg2);font-size:13px}
a{color:var(--accent2)}
.back{display:inline-block;margin-bottom:24px;font-size:13px}
b{color:#fff}
.warnbox{background:rgba(224,106,106,.08);border:1px solid rgba(224,106,106,.35);border-radius:12px;padding:14px 18px;margin:14px 0}
.warnbox b{color:var(--bad)}
.clean{color:var(--ok);font-weight:600}
.grey{color:var(--warn);font-weight:600}
.diagram{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:16px;overflow-x:auto;font-family:var(--mono);font-size:12px;line-height:1.5;color:#cdd3df;white-space:pre}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 返回文档索引</a>
<h1>Pangolin 统一支付编排层设计</h1>
<p class="sub">支付方式无关(provider-agnostic)架构 · client↔控制面协议固定 · 加渠道不改客户端 · 2026-07-09</p>
<div class="lead">
<b>一句话</b>:控制面新增一个<b>支付编排层</b>,把不同支付平台(哪吒聚合支付、加密货币、未来 Stripe/信用卡/epay)统一到「<code>Order</code> 业务订单 + <code>PaymentAttempt</code> 支付尝试 + <code>Provider</code> 适配器」模型;客户端只认服务端下发的少数几种<b>渲染形态(render_type)</b>,不认具体平台。<b>同形态的新网关 = 零改客户端;只有全新交互形态才动客户端。</b>
</div>
<h2>1. 背景与目标</h2>
<p>Pangolin 当前<b>没有 App 内支付</b>:用户变 pro 的唯一路径是兑换激活码(<code>POST /v1/redeem</code>),客户端"购买"卡片是 <code>onTap: () {}</code> 空实现。控制面<b>没有订单系统</b>(migration 000001000021 无 orders/payment 表),只有 codes 激活码履约。这是一块绿地。</p>
<p><b>目标</b>:</p>
<ul>
<li>client(Flutter 四端)与 pangolin 控制面之间的支付协议<b>固定</b>,底层支付平台<b>可插拔</b></li>
<li>首批接入 <b>加密货币(USDT-TRC20)</b><b>哪吒聚合支付(支付宝)</b> 两条渠道。</li>
<li>未来接 Stripe / 信用卡 / epay 等<b>尽量不改客户端</b></li>
<li>支付成功 → 自动给用户开通订阅(复用现有订阅/权益模型)。</li>
</ul>
<h2>2. 可行性结论 <span class="tag ok">可行</span></h2>
<p>调研(Stripe <code>next_action</code> 模型 + 多 PSP 编排实践)确认:不同支付平台的<b>交互形态是有限的</b>,而平台是无限的。把形态收敛成客户端认识的 <code>render_type</code> 联合体,即可让客户端 provider 无关。</p>
<p>首批两渠道的形态<b>都落在客户端已有能力上</b>:</p>
<table>
<thead><tr><th>渠道</th><th>render_type</th><th>客户端如何渲染</th><th>客户端现状</th></tr></thead>
<tbody>
<tr><td>哪吒(支付宝)</td><td><code>redirect</code></td><td>拿 payurl → 外部浏览器打开</td><td><span class="tag ok">已有</span> <code>url_launcher</code> + SSO 跳转范式</td></tr>
<tr><td>加密货币(USDT)</td><td><code>display_details</code></td><td>展示地址+精确金额卡片</td><td><span class="tag ok">已有</span> 就是把 <code>/_test</code> 卡搬进 App</td></tr>
</tbody>
</table>
<p><b>边界(诚实说)</b>:客户端改动频率 = <b>新 render_type 出现频率</b>(极低)+ OS scheme 白名单变化,<b>不是</b> provider 频率。真正要碰客户端的只有:全新交互形态、拉起 App 的 scheme 白名单(<code>Info.plist</code>/manifest)、信用卡 3DS / Apple·Google Pay 的专有 SDK。</p>
<h2>3. 架构总览</h2>
<div class="diagram">客户端(只认协议,不认平台) 控制面 :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 吞)</div>
<p><b>为什么 Order 与 PaymentAttempt 拆两层</b>:同一笔订单,用户可能先扫哪吒超时、再换 crypto 付成功。换渠道 = 新建一个 Attempt,Order 不变。只有一个对象会让"换渠道重试"污染状态机。</p>
<h2>4. 数据模型</h2>
<p>控制面新增两张表(<code>mysql</code> + <code>sqlite</code> 双套 migration,金额一律 <code>int64</code> 最小单位,禁 float)。</p>
<h3>4.1 <code>pay_orders</code> — 业务订单(购买/权益账本)</h3>
<pre>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</pre>
<p class="small">建单时就把 <code>order_no → user / plan / 天数</code> 落库。回调<b>只能推进已存在的订单、开通其中预先锁定的套餐</b>——回调无法指定"给谁开多久"。这是防回调伪造的纵深。</p>
<h3>4.2 <code>pay_attempts</code> — 支付尝试(收款账本)</h3>
<pre>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) -- 回调幂等的命门</pre>
<h3>4.3 SKU 目录 + 定价 <span class="tag warn">待你确认价格</span></h3>
<p>SKU → (plan, 天数, 各币种价格)。哪吒收人民币(支付宝)、crypto 收 USDT,故<b>按币种各定一价</b>(不做实时汇率换算,固定价是产品决策)。首版 SKU 目录作为控制面代码常量(后续可迁 DB)。</p>
<table>
<thead><tr><th>SKU</th><th>plan</th><th>天数</th><th>USDT(crypto)</th><th>CNY(哪吒/支付宝)</th></tr></thead>
<tbody>
<tr><td><code>pro-month</code></td><td>pro</td><td>30</td><td>$3.99</td><td>¥28(待定)</td></tr>
<tr><td><code>pro-quarter</code></td><td>pro</td><td>90</td><td>$9.99</td><td>¥68(待定)</td></tr>
<tr><td><code>pro-year</code></td><td>pro</td><td>365</td><td>$29.99</td><td>¥198(待定)</td></tr>
</tbody>
</table>
<p class="small">USDT 价你已拍板;CNY 价是我锚定现有 <code>¥25/月</code> 的提案,<b>发布前请确认</b></p>
<h2>5. Provider 适配器接口</h2>
<p>每个支付平台实现同一个接口,控制面只依赖接口。三个动作是最小完备集。</p>
<pre>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"</pre>
<h3>5.1 crypto adapter(包裹已部署的 pangolin-pay)</h3>
<p>pay-server 保持<b>独立进程</b>(持 xpub、看链,最小权限),控制面 crypto adapter 只是它的 HTTP 客户端:</p>
<ul>
<li><b>Create</b><code>POST pangolin-pay /order {user_ref=uuid, sku, amount=USDT微单位}</code> → 得地址+精确金额 → <code>render_type=display_details</code>,<code>provider_ref=pay 单号</code></li>
<li><b>HandleCallback</b> → 需给 pay-server <b>新增出站 webhook</b>:侦测到 paid → <code>POST 控制面 /v1/webhooks/pay/crypto</code>(HMAC 签名、失败重试)。</li>
<li><b>Query</b><code>GET pangolin-pay /order/{provider_ref}</code>(链上侦测无 webhook 时兜底)。</li>
</ul>
<h3>5.2 nazha adapter(哪吒聚合支付)</h3>
<ul>
<li><b>Create</b><code>POST nzzf.org/api/pay/create</code>,参数 <code>pid / type=alipay / out_trade_no=尝试号 / notify_url / return_url / name / money=元 / timestamp / sign / sign_type=RSA</code>,签名 <b>SHA256WithRSA</b>(商户私钥、参数 ASCII 升序拼 <code>k=v&amp;</code>、Base64)→ 得 <code>payurl</code><code>render_type=redirect</code>,<code>provider_ref=trade_no</code></li>
<li><b>HandleCallback</b> → 哪吒以 <b>GET</b> 回调 <code>notify_url</code>,平台公钥 RSA 验签 → <code>trade_status==TRADE_SUCCESS</code> → PaidEvent;响应纯文本 <code>success</code></li>
<li><b>Query</b><code>POST /api/pay/query</code>,<code>status==1</code> 为已支付(回调可能被吞,轮询兜底)。</li>
<li><b>配置</b>:<code>pid</code> + 商户 RSA 私钥 + 平台 RSA 公钥,走 Bitwarden/env。<b>前置</b>:你需在哪吒(Telegram 开户)拿到 pid 与密钥。</li>
</ul>
<h2>6. 客户端契约</h2>
<h3>6.1 REST 端点(控制面,全部 Bearer)</h3>
<pre>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)</pre>
<h3>6.2 render_type 联合体(写死在协议里)</h3>
<table>
<thead><tr><th>render_type</th><th>payload</th><th>首版</th></tr></thead>
<tbody>
<tr><td><code>redirect</code></td><td><code>{url, return_hint}</code></td><td><span class="tag ok">v1</span> 哪吒</td></tr>
<tr><td><code>display_details</code></td><td><code>{fields:[{label,value,copyable}], amount, currency, expires_at, poll_interval_sec}</code></td><td><span class="tag ok">v1</span> crypto</td></tr>
<tr><td><code>qr_code</code></td><td><code>{qr_content, display_amount, expires_at}</code></td><td><span class="tag info">预留</span></td></tr>
<tr><td><code>redirect_native</code></td><td><code>{app_scheme, universal_link, fallback_url}</code></td><td><span class="tag info">预留</span></td></tr>
<tr><td><code>sdk_handoff</code></td><td><code>{sdk, params}</code></td><td><span class="tag info">预留(逃逸口)</span></td></tr>
</tbody>
</table>
<p><b>护栏</b>:客户端遇到未知 render_type → 不显示该方法 / 提示"请升级 App"。无论哪种形态,客户端最终都收敛到同一个"轮询 <code>GET /orders/{no}</code> 到 paid"。</p>
<h3>6.3 客户端改动:一次 vs 永不</h3>
<table>
<thead><tr><th></th><th>动作</th></tr></thead>
<tbody>
<tr><td><b>一次(本轮做)</b></td><td>新增 <code>PaymentClient</code>(Dart, 四端共享)+ 支付页:方法选择器(读 <code>/v1/pay/methods</code>)→ 建单 → 按 render_type 分发(<code>redirect</code>=<code>url_launcher</code> 外部浏览器;<code>display_details</code>=地址卡)→ 轮询到 paid → 成功页。接上现有 no-op 购买入口。<b>无需新增原生依赖</b>(url_launcher 已在,无需 webview)。</td></tr>
<tr><td><b class="clean">永不(加渠道零改)</b></td><td>新增 provider 若落在已有 render_type(epay/Stripe Checkout = redirect;另一条链 = display_details)→ 仅服务端加 adapter + 下发一项。</td></tr>
<tr><td><b class="grey">未来才碰</b></td><td>全新交互形态(新 render_type);拉起 App 的 scheme 白名单;信用卡 3DS / Apple·Google Pay 专有 SDK。</td></tr>
</tbody>
</table>
<h2>7. 统一开通管线</h2>
<p>webhook 与 Query 轮询<b>最终都产出 <code>PaidEvent</code></b>,走同一条幂等开通逻辑:</p>
<pre>① 验签 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; 审计 }</pre>
<h3>7.1 复用并重构现有开通逻辑</h3>
<p>开通能力现埋在 <code>codes.Service.applySubscription</code>(<code>server/internal/codes/service.go:235</code>),仅作为 Redeem 事务的一步、且 <code>CreateSubscription</code><code>source</code> 硬编码 <code>'code'</code>。改动:</p>
<ul>
<li>抽出可复用、可传 <code>source</code> 的开通方法(codes.Redeem 与 pay 管线共用),避免两套开通逻辑漂移。</li>
<li><code>subscriptions.source</code> 的 CHECK 枚举现只允许 <code>('trial','code')</code><b><code>'pay'</code></b>(migration:sqlite 需表重建、mysql alter,双套)。</li>
<li>权益判定(<code>EntitlementForUser</code>)、<code>/me</code>、免费门 <b>无需改</b>——它们只看"最新未过期订阅",source 对它们透明。</li>
</ul>
<h2>8. 哪吒合规风险 <span class="tag bad">务必知情</span></h2>
<div class="warnbox">
调研明确:哪吒是<b>匿名注册(Telegram 开户、无 KYC)、USDT 结算、带代付出款、仅支付宝单通道、无公司主体 / ICP / 牌照</b>的四方聚合/代收平台。<b>风险:资金冻结、平台跑路(无主体可追索)、商户帮信罪敞口。</b>
</div>
<p><b>缓解 = 本架构天生解耦</b>:哪吒只是一个 <code>redirect</code> adapter,将来换成持牌支付宝直连 / Stripe / epay,<b>客户端零改、订单与开通逻辑零改</b>,只换服务端一个 adapter。可"先用它跑量、随时替换",不锁死。crypto(USDT 自托管)作为<b class="clean">干净主轨</b>并存。</p>
<h2>9. 错误处理与边界</h2>
<ul>
<li><b>回调不可信回跳</b>:浏览器从 return_url 回来时支付结果未必已到,客户端<b>不信任回跳</b>,只认轮询到的 <code>paid</code></li>
<li><b>webhook 可能丢</b>:crypto 无 webhook(只链上侦测)、哪吒 GET 回调可能被 GFW/CF 吞 → <b>Query 轮询兜底是刚需</b>,不只依赖 webhook。</li>
<li><b>重复回调</b>:<code>UNIQUE(provider, provider_ref)</code> + order granted 标记,已开通再来直接 200。</li>
<li><b>金额不符</b>:crypto 付错金额进 pay 侧 orphan;哪吒金额核对不过则拒绝并告警,不开通。</li>
<li><b>订单过期后到账</b>:订单已 expired 则不自动开通,转人工对账(避免迟到付款误开/漏开)。</li>
<li><b>换渠道重试</b>:新建 Attempt,老 Attempt 置 expired/canceled,Order 仍 pending。</li>
</ul>
<h2>10. 测试策略</h2>
<ul>
<li><b>控制面</b>:adapter Create/Callback/Query 单测(mock 平台 HTTP);管线幂等(双回调→单开通)、金额不符拒绝、RSA/HMAC 验签通过/失败、开通重构(codes + pay 共用、source 正确)、Order/Attempt 状态机、方法发现。</li>
<li><b>pay-server</b>:出站 webhook 投递(签名、重试)单测。</li>
<li><b>客户端</b>:render_type 分发 widget 测试;轮询到 paid;未知 render_type 降级。</li>
<li><b>端到端</b>:两轨各跑一笔小额真链/真单。</li>
</ul>
<h2>11. 首版范围与实施阶段</h2>
<p>范围:<b>后端编排层 + crypto & 哪吒两 adapter + 四端 Flutter 统一支付页</b>一次到位。用户中心(web)支付页作为快速跟进(复用同一套 REST,小改)。实施在计划里分阶段:</p>
<ol>
<li>控制面:schema(pay_orders/pay_attempts)+ store + 开通重构 + source=pay migration。</li>
<li>Provider 抽象 + 注册表 + crypto adapter + pay-server 出站 webhook。</li>
<li>哪吒 adapter(RSA 签/验、GET 回调、查单)。</li>
<li>客户端 REST(methods/orders/retry/webhooks)+ 统一开通管线 + Query 轮询 worker。</li>
<li>Flutter 支付页(方法选择器 + render_type 分发 + 轮询)+ 接上购买入口。</li>
<li>端到端联调(两轨小额)。</li>
</ol>
<h2>12. 待确认清单(评审 gate)</h2>
<table>
<thead><tr><th></th><th>状态</th></tr></thead>
<tbody>
<tr><td>SKU 三档 + USDT 价($3.99/$9.99/$29.99)</td><td><span class="tag ok">已定</span></td></tr>
<tr><td>CNY 价(¥28/¥68/¥198 提案)</td><td><span class="tag warn">待你确认</span></td></tr>
<tr><td>哪吒商户注册(pid + RSA 密钥对)</td><td><span class="tag warn">你去 Telegram 开户后给我</span></td></tr>
<tr><td>return_url 成功落地页(用户中心 or 官网一页)</td><td><span class="tag warn">待定</span></td></tr>
<tr><td>Order + PaymentAttempt 两层模型</td><td><span class="tag info">本设计采用</span></td></tr>
<tr><td>用户中心 web 支付页是否本轮做</td><td><span class="tag info">提案:快速跟进,非 v1</span></td></tr>
</tbody>
</table>
<p class="small" style="margin-top:32px">相关文档:<a href="payment-channels-overview.html">支付渠道选型总览</a> · <a href="payment-clean-usdt-plan.html">干净 USDT 方案</a> · <a href="pay-single-address-plan.html">单地址收款模型</a> · <a href="crypto-tx-engine-plan.html">加密交易引擎</a></p>
</div>
</body>
</html>