docs(pay): 架构总览字符画换成 SVG 时序图
4 参与者 lifeline(客户端/控制面/Provider/外部平台)+ 14 条消息: 实线=请求(cyan)、虚线=返回(grey)、accent=异步(用户付款/webhook)、绿=paid。 含落库/render 分发/开通管线/Query 兜底 说明框。合规范式记入全局记忆 docs-diagrams-use-svg(文档图一律 SVG,禁字符画)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,20 +79,102 @@
|
||||
<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>
|
||||
<div style="overflow-x:auto;background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:8px 4px;margin:14px 0">
|
||||
<svg viewBox="0 0 960 664" style="width:100%;min-width:780px;height:auto;display:block" font-family="'SF Mono',ui-monospace,'PingFang SC',sans-serif" role="img" aria-label="统一支付编排层时序图">
|
||||
<defs>
|
||||
<marker id="mreq" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#5fb0c9"/></marker>
|
||||
<marker id="mret" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#9aa3b2"/></marker>
|
||||
<marker id="masy" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#e0884f"/></marker>
|
||||
<marker id="mok" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#5ec27a"/></marker>
|
||||
</defs>
|
||||
<style>
|
||||
.ll{stroke:#2f3542;stroke-width:1.2;stroke-dasharray:3 4}
|
||||
.hb{fill:#171a22;stroke:#313846;stroke-width:1}
|
||||
.ht{fill:#e6e8ee;font-size:12.5px;font-weight:600;text-anchor:middle}
|
||||
.hs{fill:#8b93a3;font-size:9.5px;text-anchor:middle}
|
||||
.req{stroke:#5fb0c9;stroke-width:1.5;fill:none}
|
||||
.ret{stroke:#9aa3b2;stroke-width:1.4;stroke-dasharray:5 4;fill:none}
|
||||
.asy{stroke:#e0884f;stroke-width:1.6;fill:none}
|
||||
.okl{stroke:#5ec27a;stroke-width:1.6;stroke-dasharray:5 4;fill:none}
|
||||
.lb{fill:#cbd2de;font-size:10.5px;text-anchor:middle}
|
||||
.lo{fill:#e0a86a;font-size:10.5px;text-anchor:middle}
|
||||
.lg{fill:#7fd39a;font-size:10.5px;text-anchor:middle}
|
||||
.nb{fill:#1d2129;stroke:#3a4150;stroke-width:1}
|
||||
.nt{fill:#b3bccb;font-size:10px}
|
||||
</style>
|
||||
|
||||
<!-- lifelines -->
|
||||
<line class="ll" x1="120" y1="58" x2="120" y2="644"/>
|
||||
<line class="ll" x1="380" y1="58" x2="380" y2="644"/>
|
||||
<line class="ll" x1="620" y1="58" x2="620" y2="644"/>
|
||||
<line class="ll" x1="860" y1="58" x2="860" y2="644"/>
|
||||
|
||||
<!-- headers -->
|
||||
<rect class="hb" x="45" y="14" width="150" height="44" rx="9"/>
|
||||
<text class="ht" x="120" y="34">客户端</text><text class="hs" x="120" y="49">Flutter 四端</text>
|
||||
<rect class="hb" x="305" y="14" width="150" height="44" rx="9"/>
|
||||
<text class="ht" x="380" y="34">控制面</text><text class="hs" x="380" y="49">编排层 :8080</text>
|
||||
<rect class="hb" x="545" y="14" width="150" height="44" rx="9"/>
|
||||
<text class="ht" x="620" y="34">Provider</text><text class="hs" x="620" y="49">适配器</text>
|
||||
<rect class="hb" x="785" y="14" width="150" height="44" rx="9"/>
|
||||
<text class="ht" x="860" y="34">外部平台</text><text class="hs" x="860" y="49">pangolin-pay · 哪吒</text>
|
||||
|
||||
<!-- r1 -->
|
||||
<text class="lb" x="250" y="86">GET /v1/pay/methods</text>
|
||||
<line class="req" x1="120" y1="92" x2="374" y2="92" marker-end="url(#mreq)"/>
|
||||
<!-- r2 -->
|
||||
<text class="lb" x="250" y="120">启用渠道 [{id,name,render_type}]</text>
|
||||
<line class="ret" x1="380" y1="126" x2="126" y2="126" marker-end="url(#mret)"/>
|
||||
<!-- r3 -->
|
||||
<text class="lb" x="250" y="154">POST /v1/pay/orders {sku, method}</text>
|
||||
<line class="req" x1="120" y1="160" x2="374" y2="160" marker-end="url(#mreq)"/>
|
||||
<!-- note: 落库 -->
|
||||
<rect class="nb" x="238" y="171" width="284" height="22" rx="6"/>
|
||||
<text class="nt" x="248" y="186">落库 Order(谁/买啥/几天)+ Attempt(provider/单号)</text>
|
||||
<!-- r4 -->
|
||||
<text class="lb" x="500" y="208">adapter.Create(order)</text>
|
||||
<line class="req" x1="380" y1="214" x2="614" y2="214" marker-end="url(#mreq)"/>
|
||||
<!-- r5 -->
|
||||
<text class="lb" x="740" y="242">下单(/order | /pay/create)</text>
|
||||
<line class="req" x1="620" y1="248" x2="854" y2="248" marker-end="url(#mreq)"/>
|
||||
<!-- r6 -->
|
||||
<text class="lb" x="740" y="276">地址+金额 | payurl</text>
|
||||
<line class="ret" x1="860" y1="282" x2="626" y2="282" marker-end="url(#mret)"/>
|
||||
<!-- r7 -->
|
||||
<text class="lb" x="500" y="310">Session{render_type, payload}</text>
|
||||
<line class="ret" x1="620" y1="316" x2="386" y2="316" marker-end="url(#mret)"/>
|
||||
<!-- r8 -->
|
||||
<text class="lb" x="250" y="344">{order_no, session}</text>
|
||||
<line class="ret" x1="380" y1="350" x2="126" y2="350" marker-end="url(#mret)"/>
|
||||
<!-- note: render dispatch -->
|
||||
<rect class="nb" x="56" y="361" width="452" height="22" rx="6"/>
|
||||
<text class="nt" x="66" y="376">客户端按 render_type 渲染:redirect→浏览器 / display_details→地址卡</text>
|
||||
<!-- r9 user pays -->
|
||||
<text class="lo" x="490" y="398">用户付款(链上转账 | 支付宝跳转)</text>
|
||||
<line class="asy" x1="120" y1="404" x2="854" y2="404" marker-end="url(#masy)"/>
|
||||
<!-- r10 poll -->
|
||||
<text class="lb" x="250" y="432">轮询 GET /v1/pay/orders/{no}</text>
|
||||
<line class="req" x1="120" y1="438" x2="374" y2="438" marker-end="url(#mreq)"/>
|
||||
<!-- r11 -->
|
||||
<text class="lb" x="250" y="464">pending…</text>
|
||||
<line class="ret" x1="380" y1="470" x2="126" y2="470" marker-end="url(#mret)"/>
|
||||
<!-- r12 webhook -->
|
||||
<text class="lo" x="620" y="500">webhook:/webhooks/pay/crypto(HMAC) | /nazha(RSA·GET)</text>
|
||||
<line class="asy" x1="860" y1="506" x2="386" y2="506" marker-end="url(#masy)"/>
|
||||
<!-- note: grant pipeline -->
|
||||
<rect class="nb" x="242" y="517" width="536" height="22" rx="6"/>
|
||||
<text class="nt" x="252" y="532">验签 → 定位订单 → 幂等 → 金额核对 → 开通订阅(source=pay)</text>
|
||||
<!-- r13 -->
|
||||
<text class="lb" x="250" y="554">GET /v1/pay/orders/{no}</text>
|
||||
<line class="req" x1="120" y1="560" x2="374" y2="560" marker-end="url(#mreq)"/>
|
||||
<!-- r14 paid -->
|
||||
<text class="lg" x="250" y="588">paid ✓ 已开通</text>
|
||||
<line class="okl" x1="380" y1="594" x2="126" y2="594" marker-end="url(#mok)"/>
|
||||
<!-- bottom note: query fallback -->
|
||||
<rect class="nb" x="56" y="607" width="848" height="24" rx="6"/>
|
||||
<text class="nt" x="66" y="623">兜底:控制面 →(Query 主动查单) 外部平台 —— crypto 靠链上轮询;哪吒回调可能被 GFW/CF 吞</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p><b>为什么 Order 与 PaymentAttempt 拆两层</b>:同一笔订单,用户可能先扫哪吒超时、再换 crypto 付成功。换渠道 = 新建一个 Attempt,Order 不变。只有一个对象会让"换渠道重试"污染状态机。</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user