6da6964451
- Go/Gin/GORM + 纯 Go SQLite(无 cgo) - Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位 - 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底 - 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook - 密钥走环境变量/Bitwarden,不入库 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
271 lines
19 KiB
HTML
271 lines
19 KiB
HTML
<!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.pay(RSA2 加签下单)</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_url(POST)</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>
|