Files
pay/docs/支付宝收款页时序图与架构.html
T
wangjia 6da6964451 初始提交:岩美 pay 收款服务(支付宝当面付 + 多商户多渠道架构)
- Go/Gin/GORM + 纯 Go SQLite(无 cgo)
- Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位
- 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底
- 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook
- 密钥走环境变量/Bitwarden,不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:21:48 +08:00

271 lines
19 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付宝收款页 · 支付时序图与架构</title>
<style>
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--green:#3fb950;--orange:#d29922;--radius:14px}
*{box-sizing:border-box}
body{margin:0;background:radial-gradient(1200px 600px at 80% -10%,rgba(88,166,255,.08),transparent 60%),var(--bg);color:var(--fg);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;-webkit-font-smoothing:antialiased}
.wrap{max-width:1040px;margin:0 auto;padding:44px 22px 90px}
.eyebrow{color:var(--accent);font-weight:600;letter-spacing:.12em;font-size:12px;text-transform:uppercase}
h1{font-size:29px;margin:10px 0 8px}
.lead{color:var(--fg-soft);margin:0 0 26px}
h2{font-size:19px;margin:38px 0 12px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:8px}
h3{font-size:16px;margin:24px 0 8px;color:var(--fg)}
p{color:var(--fg-soft)}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px 22px;margin:16px 0}
.diagram{overflow-x:auto;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px}
svg{display:block;margin:0 auto;min-width:920px}
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:14px}
th,td{border:1px solid var(--border);padding:9px 11px;text-align:left;vertical-align:top}
th{background:var(--card-2);color:var(--fg);font-weight:600}
td{color:var(--fg-soft)}
code{background:var(--card-2);border:1px solid var(--border);border-radius:5px;padding:1px 6px;font-size:13px;color:#e6edf3;font-family:"SF Mono",Menlo,Consolas,monospace}
.tag{display:inline-block;font-size:12px;padding:1px 8px;border-radius:20px;border:1px solid var(--border);background:var(--card-2);color:var(--fg-soft);margin-right:4px}
.tag.must{color:var(--green);border-color:rgba(63,185,80,.4)}
.tag.opt{color:var(--orange);border-color:rgba(210,153,34,.4)}
.legend{display:flex;flex-wrap:wrap;gap:16px;margin-top:14px;font-size:13px;color:var(--fg-soft)}
.legend span{display:inline-flex;align-items:center;gap:7px}
.lg-line{width:30px;height:0;border-top:2px solid var(--accent)}
.lg-line.dash{border-top-style:dashed;border-color:var(--muted)}
.lg-line.dot{border-top-style:dotted;border-color:var(--orange)}
.lg-box{width:14px;height:14px;border-radius:3px;background:var(--card-2);border:1px solid var(--border)}
.callout{border-left:3px solid var(--accent);background:var(--card-2);border-radius:0 8px 8px 0;padding:12px 16px;margin:16px 0;color:var(--fg-soft)}
.callout.warn{border-left-color:var(--orange)}
.callout.ok{border-left-color:var(--green)}
.cols{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:16px 0}
.cols .card{margin:0}
.cols h3{margin-top:0}
ul{color:var(--fg-soft);padding-left:20px}
.pill{font-size:12px;font-weight:600;padding:2px 9px;border-radius:6px}
.pill.a{background:rgba(88,166,255,.15);color:var(--accent)}
.pill.b{background:rgba(63,185,80,.15);color:var(--green)}
@media(max-width:760px){.cols{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="wrap">
<div class="eyebrow">Payment · Design</div>
<h1>支付宝收款页 · 支付时序图与架构</h1>
<p class="lead">PC 网页支付(<code>alipay.trade.page.pay</code>)· 固定套餐 · Go(Gin)+GORM · 沙箱联调阶段。本文说清楚一笔支付的完整时序、「独立服务」与「集成进 jiu」的区别,以及独立服务要不要建库、存哪些数据。</p>
<h2>一、一笔支付的完整时序</h2>
<div class="diagram">
<svg viewBox="0 0 1000 700" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,PingFang SC,sans-serif">
<defs>
<marker id="arrowBlue" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L9,3 L0,6 Z" fill="#58a6ff"/>
</marker>
<marker id="arrowMuted" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L9,3 L0,6 Z" fill="#7d8896"/>
</marker>
<marker id="arrowOrange" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L9,3 L0,6 Z" fill="#d29922"/>
</marker>
</defs>
<!-- lifelines -->
<g stroke="#283041" stroke-width="1.5" stroke-dasharray="4 5">
<line x1="140" y1="92" x2="140" y2="682"/>
<line x1="400" y1="92" x2="400" y2="682"/>
<line x1="660" y1="92" x2="660" y2="682"/>
<line x1="900" y1="92" x2="900" y2="682"/>
</g>
<!-- activation bars -->
<rect x="394" y="124" width="12" height="518" rx="2" fill="#1c2330" stroke="#283041"/>
<rect x="654" y="216" width="12" height="340" rx="2" fill="#1c2330" stroke="#283041"/>
<!-- actor boxes -->
<g font-size="13" font-weight="600" text-anchor="middle">
<rect x="65" y="44" width="150" height="46" rx="9" fill="#1c2330" stroke="#58a6ff"/>
<text x="140" y="66" fill="#e6edf3">客户浏览器</text>
<text x="140" y="82" fill="#7d8896" font-size="11" font-weight="400">收款页</text>
<rect x="325" y="44" width="150" height="46" rx="9" fill="#1c2330" stroke="#58a6ff"/>
<text x="400" y="66" fill="#e6edf3">收款服务后端</text>
<text x="400" y="82" fill="#7d8896" font-size="11" font-weight="400">Go · Gin · GORM</text>
<rect x="585" y="44" width="150" height="46" rx="9" fill="#1c2330" stroke="#3fb950"/>
<text x="660" y="66" fill="#e6edf3">支付宝网关</text>
<text x="660" y="82" fill="#7d8896" font-size="11" font-weight="400">收银台 / 服务端</text>
<rect x="825" y="44" width="150" height="46" rx="9" fill="#1c2330" stroke="#3fb950"/>
<text x="900" y="66" fill="#e6edf3">客户手机</text>
<text x="900" y="82" fill="#7d8896" font-size="11" font-weight="400">支付宝 App</text>
</g>
<!-- messages -->
<g font-size="12.5">
<!-- 1 -->
<line x1="140" y1="130" x2="398" y2="130" stroke="#58a6ff" stroke-width="2" marker-end="url(#arrowBlue)"/>
<text x="269" y="124" fill="#e6edf3" text-anchor="middle">① POST /order/create(选定套餐)</text>
<!-- note B1 -->
<rect x="290" y="150" width="244" height="30" rx="6" fill="#161b22" stroke="#283041"/>
<text x="412" y="170" fill="#aeb9c7" font-size="11.5" text-anchor="middle">生成 out_trade_no · 订单落库(待支付)</text>
<!-- 2 -->
<line x1="406" y1="222" x2="658" y2="222" stroke="#58a6ff" stroke-width="2" marker-end="url(#arrowBlue)"/>
<text x="532" y="216" fill="#e6edf3" text-anchor="middle">② alipay.trade.page.payRSA2 加签下单)</text>
<!-- 2r -->
<line x1="654" y1="262" x2="408" y2="262" stroke="#7d8896" stroke-width="1.8" stroke-dasharray="5 4" marker-end="url(#arrowMuted)"/>
<text x="532" y="256" fill="#aeb9c7" text-anchor="middle">返回收银台跳转 URL / 表单</text>
<!-- 3 -->
<line x1="394" y1="300" x2="142" y2="300" stroke="#7d8896" stroke-width="1.8" stroke-dasharray="5 4" marker-end="url(#arrowMuted)"/>
<text x="269" y="294" fill="#aeb9c7" text-anchor="middle">③ 返回跳转链接给浏览器</text>
<!-- 4 -->
<line x1="140" y1="338" x2="652" y2="338" stroke="#58a6ff" stroke-width="2" marker-end="url(#arrowBlue)"/>
<text x="396" y="332" fill="#e6edf3" text-anchor="middle">④ 跳转支付宝收银台 · 展示付款二维码</text>
<!-- 5 -->
<line x1="900" y1="378" x2="668" y2="378" stroke="#3fb950" stroke-width="2" marker-end="url(#arrowMuted)"/>
<text x="784" y="372" fill="#e6edf3" text-anchor="middle">⑤ 扫码 · 确认付款</text>
<!-- 6 -->
<line x1="660" y1="418" x2="408" y2="418" stroke="#3fb950" stroke-width="2" marker-end="url(#arrowMuted)"/>
<text x="534" y="412" fill="#e6edf3" text-anchor="middle">⑥ 异步通知 notify_urlPOST</text>
<!-- note B2 -->
<rect x="300" y="438" width="300" height="48" rx="6" fill="#161b22" stroke="#3fb950"/>
<text x="450" y="456" fill="#e6edf3" font-size="11.5" text-anchor="middle">验签 + 核对金额 + 核对订单号</text>
<text x="450" y="473" fill="#aeb9c7" font-size="11.5" text-anchor="middle">+ 防重复通知 → 更新订单为「已支付」</text>
<!-- 6r -->
<line x1="406" y1="512" x2="658" y2="512" stroke="#7d8896" stroke-width="1.8" stroke-dasharray="5 4" marker-end="url(#arrowMuted)"/>
<text x="532" y="506" fill="#aeb9c7" text-anchor="middle">应答 "success"(告知已处理,否则会重发)</text>
<!-- 7 -->
<line x1="660" y1="550" x2="142" y2="550" stroke="#3fb950" stroke-width="2" marker-end="url(#arrowMuted)"/>
<text x="396" y="544" fill="#e6edf3" text-anchor="middle">⑦ 浏览器同步跳转 return_url</text>
<!-- note A -->
<rect x="58" y="572" width="200" height="30" rx="6" fill="#161b22" stroke="#283041"/>
<text x="158" y="592" fill="#aeb9c7" font-size="11.5" text-anchor="middle">展示「支付成功」结果页</text>
<!-- 8 -->
<line x1="406" y1="640" x2="658" y2="640" stroke="#d29922" stroke-width="1.8" stroke-dasharray="2 3" marker-end="url(#arrowOrange)"/>
<text x="532" y="634" fill="#d29922" text-anchor="middle">⑧(兜底)alipay.trade.query 主动查单对账</text>
</g>
</svg>
</div>
<div class="legend">
<span><i class="lg-line"></i> 请求(同步调用)</span>
<span><i class="lg-line dash"></i> 应答 / 跳转返回</span>
<span><i class="lg-line dot"></i> 兜底主动查单</span>
<span><i class="lg-box"></i> 后端处理 / 备注</span>
</div>
<div class="callout warn">
<strong>两条“到账”路径,以异步为准:</strong>⑦ 同步跳转(return_url)只是把用户带回页面,<strong>不能作为到账依据</strong>(用户可能中途关页面)。真正确认收款的是 ⑥ 异步通知(notify_url)。两条都可能丢,所以 ⑧ 用 <code>alipay.trade.query</code> 主动查单兜底。<strong>订单状态以「⑥验签通过 / ⑧查单成功」为准。</strong>
</div>
<h2>二、整体架构(沙箱阶段)</h2>
<div class="card">
<p style="margin-top:0">三个角色、五个核心接口,全程不需要客户登录:</p>
<ul>
<li><span class="tag must">必做</span><strong>收款页</strong>:静态 HTML,列固定套餐 → 选一个 → 调下单接口 → 跳转支付宝。</li>
<li><span class="tag must">必做</span><strong>POST /order/create</strong>:生成 <code>out_trade_no</code>、订单落库(待支付)、调 <code>alipay.trade.page.pay</code> 加签、返回跳转链接。</li>
<li><span class="tag must">必做</span><strong>POST /alipay/notify</strong>:异步回调,验签 + 核对金额/订单号 + 幂等防重 → 更新订单为已支付。<strong>支付安全的核心。</strong></li>
<li><span class="tag must">必做</span><strong>GET /order/return</strong>:同步跳转回的结果页(仅展示,不作为到账依据)。</li>
<li><span class="tag opt">建议</span><strong>查单兜底</strong><code>alipay.trade.query</code> 对待支付订单轮询,防回调丢失。</li>
<li><span class="tag opt">可选</span><strong>商家后台</strong>:看订单/对账列表 —— <em>唯一需要登录鉴权的部分</em></li>
</ul>
</div>
<h2>三、独立服务 vs 集成进 jiu</h2>
<p>两种落地方式,差别核心就一句话:<strong>要不要复用 jiu 已有的数据库和 JWT 鉴权后台。</strong></p>
<table>
<thead><tr><th style="width:22%"></th><th><span class="pill a">独立服务</span>ai 下新建)</th><th><span class="pill b">集成进 jiu</span>jiu/backend 加模块)</th></tr></thead>
<tbody>
<tr><td><strong>代码位置</strong></td><td>/Users/wangjia/code/ai 下新建 <code>pay/</code>,独立 Go 进程</td><td>jiu/backend 里加 <code>handler/service/model/router</code></td></tr>
<tr><td><strong>数据库</strong></td><td>自建一套(SQLite 单文件起步即可,或 MySQL)</td><td>直接复用 jiu 的 GORM/MySQL,订单表加进去</td></tr>
<tr><td><strong>鉴权后台</strong></td><td>要自己做登录鉴权(或第一版不做后台)</td><td><strong>白送</strong>——复用 jiu 的 JWT 中间件</td></tr>
<tr><td><strong>部署</strong></td><td>单独一个二进制 + systemd,独立上线</td><td>跟 jiu 一起构建/部署,绑在一条流水线</td></tr>
<tr><td><strong>耦合度</strong></td><td>与 jiu 完全解耦,可给任意业务(如 yanmei)用</td><td>与 jiu 强耦合,jiu 是进销存系统,收款无关会让它变臃肿</td></tr>
<tr><td><strong>适合</strong></td><td>收款是<strong>独立业务</strong>、或要给多个项目复用</td><td>收款<strong>本就是 jiu 业务的一环</strong>(订单/客户都在 jiu 里)</td></tr>
<tr><td><strong>开发量</strong></td><td>核心一致,多了「建库 + 鉴权(若要后台)」</td><td>核心一致,省掉建库和鉴权</td></tr>
</tbody>
</table>
<div class="callout">
<strong>判断方法:</strong>收款数据要不要跟 jiu 的客户/订单/库存打通?<br>
· 要打通 → <span class="pill b">集成进 jiu</span>,省事且数据一体。<br>
· 不相干(给 yanmei 等独立收款)→ <span class="pill a">独立服务</span>,第一版连后台都不做,最快跑通沙箱。
</div>
<h2>四、独立服务需要建数据库吗?</h2>
<div class="callout ok"><strong>需要。哪怕一张表也得有。</strong>SQLite 单文件起步就够(零运维),后面要并发/多实例再换 MySQL。</div>
<p>不能「只下单不落库」,原因是支付的钱和安全全靠订单记录兜底:</p>
<table>
<thead><tr><th style="width:30%">为什么必须存</th><th>不存会怎样</th></tr></thead>
<tbody>
<tr><td><strong>回调核对金额防篡改</strong></td><td>⑥ 收到通知时要拿订单原始金额比对,没存就无法判断金额是否被改 → 可被刷单</td></tr>
<tr><td><strong>幂等防重复入账</strong></td><td>支付宝会重发通知,没有订单状态记录就会重复发货/重复确认</td></tr>
<tr><td><strong>查单对账兜底</strong></td><td>⑧ 主动查单需要 <code>out_trade_no</code>,没存就无单可查,回调一丢就丢钱</td></tr>
<tr><td><strong>给客户/商家看结果</strong></td><td>结果页、订单列表、退款都要读订单,无库无从谈起</td></tr>
</tbody>
</table>
<h2>五、存哪些数据</h2>
<h3>① 订单表 <code>orders</code>(核心,必建)</h3>
<table>
<thead><tr><th style="width:22%">字段</th><th style="width:18%">类型</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>uint PK</td><td>自增主键</td></tr>
<tr><td><code>out_trade_no</code></td><td>varchar · 唯一</td><td>商户订单号(我们生成,全局唯一),贯穿下单/回调/查单</td></tr>
<tr><td><code>subject</code></td><td>varchar</td><td>套餐/商品名称(展示在收银台)</td></tr>
<tr><td><code>amount</code></td><td>decimal(10,2)</td><td>订单金额(元),<strong>回调时用它核对防篡改</strong></td></tr>
<tr><td><code>status</code></td><td>varchar / enum</td><td>待支付 / 已支付 / 已关闭 / 已退款</td></tr>
<tr><td><code>trade_no</code></td><td>varchar</td><td>支付宝交易号(回调/查单回填)</td></tr>
<tr><td><code>buyer_logon_id</code></td><td>varchar</td><td>买家支付宝账号(可选,回调带回,脱敏存)</td></tr>
<tr><td><code>paid_at</code></td><td>datetime</td><td>支付完成时间(回调回填)</td></tr>
<tr><td><code>product_id</code></td><td>uint</td><td>指向套餐表(固定套餐场景)</td></tr>
<tr><td><code>client_ip</code> / <code>remark</code></td><td>varchar</td><td>下单来源 IP、备注(可选)</td></tr>
<tr><td><code>created_at</code> / <code>updated_at</code></td><td>datetime</td><td>GORM 自动维护</td></tr>
</tbody>
</table>
<h3>② 套餐表 <code>products</code>(固定套餐场景,建议建)</h3>
<p style="margin-top:0">固定商品/套餐就是从这张表来,避免金额写死在前端被篡改 —— <strong>下单时金额一律以服务端这张表为准,不信任前端传来的价格。</strong></p>
<table>
<thead><tr><th style="width:22%">字段</th><th style="width:18%">类型</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>uint PK</td><td>套餐 ID(前端只传这个)</td></tr>
<tr><td><code>name</code></td><td>varchar</td><td>套餐名(如「基础版 / 年付」)</td></tr>
<tr><td><code>price</code></td><td>decimal(10,2)</td><td>价格(元),<strong>服务端权威价格</strong></td></tr>
<tr><td><code>description</code></td><td>varchar</td><td>套餐说明</td></tr>
<tr><td><code>active</code></td><td>bool</td><td>是否上架</td></tr>
<tr><td><code>sort</code></td><td>int</td><td>展示排序</td></tr>
</tbody>
</table>
<h3>③ 回调日志 <code>notify_logs</code>(可选,建议)</h3>
<p style="margin-top:0">把每条异步通知的原始报文、验签结果、处理结果存一份,方便排查对账纠纷与审计。非必须,但出问题时极其有用。</p>
<div class="callout warn">
<strong>安全红线(无论独立还是集成都一样):</strong><br>
1. 金额、套餐价格<strong>一律以服务端 DB 为准</strong>,绝不信任前端传值;<br>
2. 回调 <strong>必须验签</strong>(支付宝公钥)+ 核对 <code>out_trade_no</code><code>total_amount</code><br>
3. 回调<strong>幂等</strong>:同一 <code>trade_no</code> 只处理一次;<br>
4. 处理成功才返回 <code>success</code>,否则支付宝会按策略重发。
</div>
<h2>六、结论与下一步</h2>
<ul>
<li>时序固定:<strong>下单→跳收银台→扫码付→异步回调验签入账→(兜底查单)</strong>,到账以异步回调为准。</li>
<li><strong>独立服务必须建库</strong>,最少 <code>orders</code>+ 固定套餐再加 <code>products</code>),SQLite 起步零运维。</li>
<li>独立 vs 集成的分水岭:<strong>收款数据要不要跟 jiu 打通</strong>、要不要复用 jiu 的鉴权后台。</li>
</ul>
<p>定了「独立 / 集成」后,我就把它写成正式设计文档 + 实现计划,照 jiu 的 Go(Gin)+GORM 约定搭骨架,用支付宝沙箱跑通全链路。</p>
</div>
</body>
</html>