初始提交:岩美 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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pay · 文档索引</title>
|
||||
<style>
|
||||
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--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:860px;margin:0 auto;padding:48px 22px 80px}
|
||||
.eyebrow{color:var(--accent);font-weight:600;letter-spacing:.12em;font-size:12px;text-transform:uppercase}
|
||||
h1{font-size:30px;margin:10px 0 8px}
|
||||
.lead{color:var(--fg-soft);margin:0 0 24px}
|
||||
h2{font-size:18px;margin:34px 0 10px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:8px}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
.doc{display:block;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin:12px 0;transition:.15s}
|
||||
.doc:hover{border-color:var(--accent);background:var(--card-2)}
|
||||
.doc .title{font-size:16px;font-weight:600;color:var(--fg)}
|
||||
.doc .desc{color:var(--fg-soft);font-size:13.5px;margin-top:4px}
|
||||
.doc .meta{color:var(--muted);font-size:12px;margin-top:6px}
|
||||
.empty{color:var(--muted);font-size:13.5px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Project Docs · pay</div>
|
||||
<h1>pay 支付服务 · 文档索引</h1>
|
||||
<p class="lead">独立支付服务(Go·Gin·GORM),多业务 × 多渠道(支付宝先行,微信留接口)。新增文档请同步登记此处。</p>
|
||||
|
||||
<h2>🎨 设计方案 / 技术方案</h2>
|
||||
<a class="doc" href="./支付宝收款页时序图与架构.html">
|
||||
<div class="title">支付宝收款页 · 支付时序图与架构</div>
|
||||
<div class="desc">PC 网页支付(alipay.trade.page.pay)固定套餐收款页的设计说明:SVG 时序图讲清「下单→跳收银台→扫码付→异步回调验签入账→兜底查单」全链路(到账以异步通知为准);对比「独立服务 vs 集成进 jiu」的区别;说明独立服务为何必须建库、要存哪些数据(orders / products / notify_logs 字段表)与安全红线。</div>
|
||||
<div class="meta">v1.0 · 2026-06-24 · Go(Gin)+GORM · 沙箱联调阶段</div>
|
||||
</a>
|
||||
<a class="doc" href="./支付宝密钥申请与备案依赖.html">
|
||||
<div class="title">支付宝密钥申请与备案依赖关系</div>
|
||||
<div class="desc">沙箱 vs 生产两阶段各需什么;密钥怎么拿(沙箱 4 步 / 生产 5 步,APPID·应用私钥·支付宝公钥分别对应哪个配置);「不用域名是否就不用备案」的结论与三处卡点(商户签约/HTTPS/微信);含两线并行推进路线图(联调线 vs 上线线)。</div>
|
||||
<div class="meta">v1.0 · 2026-06-24 · onboarding · 含备案依赖路线图</div>
|
||||
</a>
|
||||
|
||||
<h2>📋 实现计划</h2>
|
||||
<p class="empty">见仓库根 README.md(运行与联调步骤)</p>
|
||||
|
||||
<h2>🔧 排障 Runbook</h2>
|
||||
<a class="doc" href="./上线状态与部署Runbook.html">
|
||||
<div class="title">上线状态与部署 Runbook</div>
|
||||
<div class="desc">支付宝当面付在阿里云的部署现状(systemd 守护、密钥走 Bitwarden→rbw→env、下单加签已验证、仅差支付宝审核)、部署架构图、服务器路径清单、运维常用命令(状态/重启/日志/更新密钥/更新程序/查订单)、上线检查清单;以及微信支付规划与硬前提(V3 回调强制 HTTPS→须先备案)。</div>
|
||||
<div class="meta">v1.0 · 2026-06-25 · 阿里云 182.92.213.171 · 当面付</div>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pay · 上线状态与部署 Runbook</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;--red:#f85149;--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:1000px;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:28px;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:22px 0 8px;color:var(--fg)}
|
||||
p{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}
|
||||
pre{background:var(--card-2);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-size:12.5px;line-height:1.6}
|
||||
pre code{background:none;border:0;padding:0}
|
||||
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
|
||||
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)}
|
||||
.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)}
|
||||
.callout.bad{border-left-color:var(--red)}
|
||||
.ok{color:var(--green);font-weight:600}.wait{color:var(--orange);font-weight:600}.no{color:var(--red);font-weight:600}
|
||||
.tag{display:inline-block;font-size:12px;padding:1px 9px;border-radius:20px;border:1px solid var(--border);background:var(--card-2);color:var(--fg-soft)}
|
||||
ul{color:var(--fg-soft)}
|
||||
svg{display:block;margin:8px auto;max-width:100%}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">pay · Status & Runbook</div>
|
||||
<h1>上线状态与部署 Runbook</h1>
|
||||
<p class="lead">岩美 pay 收款服务(支付宝当面付)在阿里云的部署现状、运维手册,以及微信支付的规划与硬前提。更新于 2026-06-25。</p>
|
||||
|
||||
<h2>一、当前状态总览</h2>
|
||||
<table>
|
||||
<thead><tr><th style="width:34%">事项</th><th style="width:14%">状态</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>pay 服务部署到阿里云</td><td class="ok">✅ 完成</td><td>systemd 守护、开机自启、health OK</td></tr>
|
||||
<tr><td>支付宝密钥(生产)</td><td class="ok">✅ 完成</td><td>存 Bitwarden「ali pay key」→ rbw 灌入服务器 env,私钥不落明文/不进聊天</td></tr>
|
||||
<tr><td>下单 + RSA2 加签</td><td class="ok">✅ 验证</td><td>测试下单返回「应用未上线」= 密钥/加签/网关全通,只差审核</td></tr>
|
||||
<tr><td>支付宝应用审核(当面付)</td><td class="wait">⏳ 审核中</td><td>1 天内出结果;通过后下单即出码,无需再改</td></tr>
|
||||
<tr><td>阿里云安全组开 8080</td><td class="wait">⏳ 待操作</td><td>用户在 ECS 控制台开:TCP 8080 / 源 0.0.0.0/0</td></tr>
|
||||
<tr><td>1 分钱真实实测</td><td class="wait">⏳ 待</td><td>审核过 + 端口开后,真支付宝扫码付</td></tr>
|
||||
<tr><td>微信支付</td><td class="no">⛔ 阻塞</td><td>V3 回调强制 HTTPS → 需先备案+域名+证书(见第四节)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>二、部署架构</h2>
|
||||
<svg viewBox="0 0 940 250" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,PingFang SC,sans-serif" font-size="12.5">
|
||||
<defs><marker id="a" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L9,3 L0,6 Z" fill="#58a6ff"/></marker></defs>
|
||||
<!-- 客户 -->
|
||||
<rect x="20" y="100" width="130" height="50" rx="9" fill="#1c2330" stroke="#58a6ff"/><text x="85" y="122" fill="#e6edf3" text-anchor="middle">客户</text><text x="85" y="139" fill="#7d8896" text-anchor="middle" font-size="11">支付宝扫码</text>
|
||||
<!-- 收款页/QR -->
|
||||
<line x1="150" y1="125" x2="198" y2="125" stroke="#58a6ff" stroke-width="2" marker-end="url(#a)"/>
|
||||
<text x="174" y="118" fill="#aeb9c7" text-anchor="middle" font-size="10.5">扫码</text>
|
||||
<!-- pay服务 -->
|
||||
<rect x="200" y="80" width="220" height="92" rx="10" fill="#161b22" stroke="#3fb950"/>
|
||||
<text x="310" y="104" fill="#e6edf3" text-anchor="middle" font-weight="600">pay 服务(阿里云)</text>
|
||||
<text x="310" y="124" fill="#aeb9c7" text-anchor="middle" font-size="11">182.92.213.171:8080</text>
|
||||
<text x="310" y="142" fill="#7d8896" text-anchor="middle" font-size="11">systemd · Go · SQLite</text>
|
||||
<text x="310" y="159" fill="#7d8896" text-anchor="middle" font-size="11">当面付 precreate / 回调验签 / 查单</text>
|
||||
<!-- 支付宝 -->
|
||||
<line x1="420" y1="110" x2="528" y2="110" stroke="#58a6ff" stroke-width="2" marker-end="url(#a)"/>
|
||||
<text x="474" y="103" fill="#aeb9c7" text-anchor="middle" font-size="10.5">下单(加签)</text>
|
||||
<line x1="528" y1="140" x2="420" y2="140" stroke="#7d8896" stroke-width="1.8" stroke-dasharray="5 4" marker-end="url(#a)"/>
|
||||
<text x="474" y="155" fill="#aeb9c7" text-anchor="middle" font-size="10.5">notify(验签)</text>
|
||||
<rect x="530" y="90" width="150" height="70" rx="10" fill="#1c2330" stroke="#3fb950"/>
|
||||
<text x="605" y="120" fill="#e6edf3" text-anchor="middle" font-weight="600">支付宝</text>
|
||||
<text x="605" y="138" fill="#7d8896" text-anchor="middle" font-size="11">正式网关</text>
|
||||
<!-- Bitwarden -->
|
||||
<rect x="200" y="200" width="220" height="40" rx="9" fill="#1c2330" stroke="#283041"/>
|
||||
<text x="310" y="225" fill="#aeb9c7" text-anchor="middle" font-size="11.5">🔑 Bitwarden「ali pay key」→ rbw → /etc/pay/pay.env</text>
|
||||
<line x1="310" y1="200" x2="310" y2="174" stroke="#7d8896" stroke-width="1.5" stroke-dasharray="3 3" marker-end="url(#a)"/>
|
||||
<!-- 备注 -->
|
||||
<text x="760" y="118" fill="#7d8896" font-size="11">notify 用 IP:8080(http)</text>
|
||||
<text x="760" y="136" fill="#7d8896" font-size="11">支付宝当面付不校验备案</text>
|
||||
</svg>
|
||||
|
||||
<h2>三、服务器部署详情</h2>
|
||||
<table>
|
||||
<thead><tr><th style="width:30%">项</th><th>值</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>服务器</td><td><code>ssh ali</code> → 182.92.213.171(Alibaba Cloud Linux 3, x86_64)</td></tr>
|
||||
<tr><td>二进制</td><td><code>/opt/pay/payd</code>(Go 交叉编译 linux/amd64,纯静态无 cgo)</td></tr>
|
||||
<tr><td>前端页面</td><td><code>/opt/pay/web/</code>(pay.html / result.html)</td></tr>
|
||||
<tr><td>配置</td><td><code>/opt/pay/config.yaml</code>(production:true、app_id、base_url、enabled:true)</td></tr>
|
||||
<tr><td>数据库</td><td><code>/opt/pay/pay.db</code>(SQLite)</td></tr>
|
||||
<tr><td>密钥 env</td><td><code>/etc/pay/pay.env</code>(600,含 ALIPAY_APP_PRIVATE_KEY / ALIPAY_PUBLIC_KEY,来自 rbw)</td></tr>
|
||||
<tr><td>systemd 单元</td><td><code>/etc/systemd/system/pay.service</code></td></tr>
|
||||
<tr><td>商户</td><td>code=<code>yanmei</code> · APPID <code>2021006166629060</code> · 当面付 · 正式网关</td></tr>
|
||||
<tr><td>notify_url</td><td><code>http://182.92.213.171:8080/api/v1/notify/alipay</code></td></tr>
|
||||
<tr><td>查单兜底</td><td>每 30s 主动 query 待支付订单(防回调丢失)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout"><strong>密钥来源(Bitwarden):</strong>条目 <code>ali pay key</code>,字段 <code>app_private_key</code> / <code>alipay_public_key</code> / <code>appid</code>。部署时 <code>rbw get</code> 管道直灌服务器 env,明文不落盘到仓库、不进聊天。</div>
|
||||
|
||||
<h2>四、运维 Runbook(常用命令)</h2>
|
||||
<pre><code># 状态 / 健康
|
||||
ssh ali 'systemctl status pay'
|
||||
ssh ali 'curl -s localhost:8080/health'
|
||||
|
||||
# 重启 / 看日志
|
||||
ssh ali 'systemctl restart pay'
|
||||
ssh ali 'journalctl -u pay -f'
|
||||
|
||||
# 更新密钥(从 Bitwarden 重新灌)
|
||||
{ printf 'ALIPAY_APP_PRIVATE_KEY='; rbw get "ali pay key" --field app_private_key | tr -d '\r\n'; \
|
||||
printf '\nALIPAY_PUBLIC_KEY='; rbw get "ali pay key" --field alipay_public_key | tr -d '\r\n'; printf '\n'; } \
|
||||
| ssh ali 'umask 077; cat > /etc/pay/pay.env; chmod 600 /etc/pay/pay.env'
|
||||
ssh ali 'systemctl restart pay'
|
||||
|
||||
# 更新程序(本地改完代码后)
|
||||
cd /Users/wangjia/code/pay && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /tmp/payd .
|
||||
scp /tmp/payd ali:/opt/pay/payd && ssh ali 'systemctl restart pay'
|
||||
|
||||
# 查订单(服务器上)
|
||||
ssh ali 'sqlite3 -header -column /opt/pay/pay.db "SELECT out_trade_no,amount,status,trade_no,paid_at FROM orders ORDER BY id DESC LIMIT 10;"'</code></pre>
|
||||
|
||||
<div class="callout ok"><strong>审核通过后无需任何改动</strong>——商户已 enabled、密钥已就位。支付宝应用一上线,下单即出码。直接开 <code>http://182.92.213.171:8080</code> 测 1 分钱即可。</div>
|
||||
|
||||
<h2>五、上线检查清单(支付宝当面付)</h2>
|
||||
<ul>
|
||||
<li>☑ 服务部署、systemd 自启、health OK</li>
|
||||
<li>☑ 生产密钥灌入(Bitwarden → env)</li>
|
||||
<li>☑ 下单加签验证通过(「应用未上线」是预期)</li>
|
||||
<li>☐ <span class="wait">支付宝应用 + 当面付审核通过</span>(1 天内)</li>
|
||||
<li>☐ <span class="wait">阿里云安全组开 8080</span>(TCP / 0.0.0.0/0)</li>
|
||||
<li>☐ <span class="wait">1 分钱真实扫码实测</span> → 订单变 paid</li>
|
||||
<li>☐ 把测试套餐(0.01/0.02)换成真实授权套餐(¥99/¥268/¥888…)</li>
|
||||
<li>☐ 配服务器 IP 白名单(资金接口加固,可选)</li>
|
||||
</ul>
|
||||
|
||||
<h2>六、微信支付规划</h2>
|
||||
<div class="callout warn"><strong>硬前提:微信支付 V3 回调强制 HTTPS</strong>——必须 <code>https://域名/notify</code>,不能像支付宝当面付那样用 <code>http://IP:8080</code>。HTTPS 要域名+证书,域名+大陆服务器又要 <strong>ICP 备案</strong>。所以<strong>微信必须等备案好才能上线</strong>。</div>
|
||||
<h3>不依赖备案、可现在并行申请</h3>
|
||||
<ul>
|
||||
<li>申请<strong>微信支付商户号</strong>(pay.weixin.qq.com,企业+营业执照+对公账户,审核约 1~7 天)</li>
|
||||
<li>注册一个<strong>认证的公众号/服务号或小程序</strong>(当 APPID 用;服务号认证约 300/年)</li>
|
||||
<li>配 <strong>API 证书 + APIv3 密钥 + 微信支付公钥</strong>(商户平台 → 账户中心 → API 安全)</li>
|
||||
</ul>
|
||||
<h3>依赖备案</h3>
|
||||
<ul>
|
||||
<li>域名转入 + ICP 备案 → 服务器配 HTTPS(域名+证书)→ 微信 notify 才能用</li>
|
||||
</ul>
|
||||
<h3>代码侧(待我实现)</h3>
|
||||
<ul>
|
||||
<li><code>internal/channel/wechat.go</code> 现为占位;需实现微信支付 V3:Native 下单 / 回调 AES-GCM 解密+验签 / 查单</li>
|
||||
<li>架构已预留:<code>Channel</code> 接口 + <code>merchants</code> 多渠道表,补实现 + 加一条商户即可,主流程不变</li>
|
||||
</ul>
|
||||
<div class="callout"><strong>节奏:</strong>现在支付宝当面付先收钱(不等备案);并行申请微信商户号+公众号;备案+域名+HTTPS 就绪后,我实现微信 V3 渠道并 1 分钱实测(微信无沙箱)。届时支付宝电脑网站支付(网页收款页)也能一并上。</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,171 @@
|
||||
<!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;--red:#f85149;--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:980px;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:28px;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:22px 0 8px;color:var(--fg)}
|
||||
p{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}
|
||||
pre{background:var(--card-2);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-size:13px;line-height:1.55}
|
||||
pre code{background:none;border:0;padding:0}
|
||||
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
|
||||
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)}
|
||||
.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)}
|
||||
.callout.bad{border-left-color:var(--red)}
|
||||
.step{display:flex;gap:14px;margin:14px 0;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px}
|
||||
.step .n{flex:0 0 30px;height:30px;border-radius:50%;background:var(--accent);color:#04122b;font-weight:700;display:flex;align-items:center;justify-content:center}
|
||||
.step .body{flex:1}
|
||||
.step .body h3{margin:2px 0 6px}
|
||||
.pill{font-size:12px;font-weight:600;padding:2px 9px;border-radius:6px}
|
||||
.pill.sand{background:rgba(63,185,80,.15);color:var(--green)}
|
||||
.pill.prod{background:rgba(210,153,34,.15);color:var(--orange)}
|
||||
.yes{color:var(--green);font-weight:600}.no{color:var(--red);font-weight:600}
|
||||
ul{color:var(--fg-soft)}
|
||||
svg{display:block;margin:8px auto;max-width:100%}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">pay · Onboarding</div>
|
||||
<h1>支付宝密钥申请与备案依赖关系</h1>
|
||||
<p class="lead">沙箱 vs 生产两阶段分别需要什么、密钥怎么拿、备案到底卡在哪一步。配套独立支付服务 <code>pay</code>(PC 网页支付 <code>alipay.trade.page.pay</code>)。</p>
|
||||
|
||||
<h2>一、两阶段总览</h2>
|
||||
<table>
|
||||
<thead><tr><th style="width:24%"></th><th><span class="pill sand">沙箱(现在就能做)</span></th><th><span class="pill prod">生产(收真钱)</span></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>企业实名</strong></td><td class="no">不需要</td><td class="yes">需要(营业执照/对公/法人)</td></tr>
|
||||
<tr><td><strong>产品签约</strong></td><td class="no">不需要</td><td class="yes">需签约「电脑网站支付」</td></tr>
|
||||
<tr><td><strong>域名</strong></td><td class="no">不需要</td><td>签约审核基本要备案域名</td></tr>
|
||||
<tr><td><strong>ICP 备案</strong></td><td class="no">不需要</td><td>基本绕不开(见第二节)</td></tr>
|
||||
<tr><td><strong>HTTPS</strong></td><td class="no">不需要(localhost 即可)</td><td class="yes">需要(域名证书)</td></tr>
|
||||
<tr><td><strong>能验证什么</strong></td><td>下单→跳收银台→扫码付→(查单兜底)全链路</td><td>真实到账 + 异步回调 notify</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>二、不用域名 = 不用备案?</h2>
|
||||
<p>技术层面:<strong>不绑域名就不触发 ICP 备案</strong>(备案=域名+大陆服务器对外提供服务)。而且有个对支付宝有利的事实:</p>
|
||||
<div class="callout ok">
|
||||
<strong>支付宝电脑网站支付的 <code>notify_url</code> / <code>return_url</code> 支付宝不校验是否备案</strong>——只要能 POST 回调到你的地址就行。这点和微信不同(微信 JSAPI/H5 要在后台填「已备案授权域名」)。所以单从回调技术看,甚至能用服务器公网 IP 当 notify_url 把真实支付跑通。
|
||||
</div>
|
||||
<p>但「想正经收真钱」时,备案基本还是绕不开,卡在这三处:</p>
|
||||
<table>
|
||||
<thead><tr><th style="width:26%">卡点</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>商户签约审核</strong></td><td>申请生产「电脑网站支付」要提交<strong>网站地址</strong>,支付宝审核要求网站可正常访问且合规,实践中基本要备案域名(裸 IP 网站过审很难)</td></tr>
|
||||
<tr><td><strong>HTTPS</strong></td><td>给纯 IP 签受信任 HTTPS 证书极难(Let's Encrypt 不给 IP 发证),无 HTTPS 收款页显示「不安全」</td></tr>
|
||||
<tr><td><strong>微信支付</strong></td><td>微信必须填备案授权域名,完全绕不开</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout bad">
|
||||
<strong>结论:</strong>沙箱阶段不用域名/备案/实名;真实收款备案是刚需。<strong>但你不必等备案</strong>就能先把支付宝沙箱联调全做完,备案/签约和联调可并行。
|
||||
</div>
|
||||
<div class="callout">
|
||||
完全不备案也想收真钱的旁路:支付宝<strong>「当面付」(扫码)</strong>签约不依赖网站,或用<strong>收款链接 / 第三方托管收银台</strong>——但那不是自研网页收款页这条路。
|
||||
</div>
|
||||
|
||||
<h2>三、沙箱密钥获取(4 步 · 现在就能做)</h2>
|
||||
<div class="step"><div class="n">1</div><div class="body">
|
||||
<h3>进沙箱控制台</h3>
|
||||
<p>支付宝账号登录 → <a href="https://open.alipay.com/develop/sandbox/app">https://open.alipay.com/develop/sandbox/app</a>。页面直接给你 <strong>沙箱 APPID</strong>(形如 <code>9021000xxxxxxxxx</code>)。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">2</div><div class="body">
|
||||
<h3>选「公钥模式」+ 生成密钥</h3>
|
||||
<p>加签方式选 <strong>公钥模式</strong>(对应代码 <code>LoadAliPayPublicKey</code>)。下载官方<a href="https://opendocs.alipay.com/common/02kipl">密钥生成工具</a>(有 Mac 版)→ 格式 <strong>PKCS8</strong>、长度 <strong>2048</strong> → 生成得到 <strong>应用私钥</strong> 和 <strong>应用公钥</strong>。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">3</div><div class="body">
|
||||
<h3>上传应用公钥 → 换回支付宝公钥</h3>
|
||||
<p>把<strong>应用公钥</strong>粘贴到沙箱「公钥模式」处保存,页面会生成一串 <strong>支付宝公钥</strong>。</p>
|
||||
<div class="callout warn" style="margin:8px 0 0">别搞混:你<strong>上传</strong>的是「应用公钥」;要<strong>复制回配置</strong>的是支付宝生成的「支付宝公钥」。</div>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">4</div><div class="body">
|
||||
<h3>拿沙箱买家账号</h3>
|
||||
<p>沙箱页「<strong>沙箱账号</strong>」标签提供买家账号(邮箱)+ 登录密码 + 支付密码(假钱)。扫码付需装<strong>沙箱版支付宝 App</strong>(同页二维码),或用沙箱网页登录买家付款。</p>
|
||||
</div></div>
|
||||
|
||||
<h2>四、生产密钥申请(5 步 · 收真钱)</h2>
|
||||
<div class="step"><div class="n">1</div><div class="body">
|
||||
<h3>企业实名</h3>
|
||||
<p>登录 <a href="https://open.alipay.com">open.alipay.com</a> → 用企业支付宝账号完成企业实名认证(营业执照、法人/对公信息)。个体户用对应类型亦可。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">2</div><div class="body">
|
||||
<h3>创建生产应用</h3>
|
||||
<p>控制台 → 创建应用 → 选「<strong>网页 & 移动应用</strong>」→ 得到 <strong>正式 APPID</strong>。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">3</div><div class="body">
|
||||
<h3>签约「电脑网站支付」</h3>
|
||||
<p>应用里「添加能力/产品签约」→ 签约「<strong>电脑网站支付</strong>」(对应 <code>alipay.trade.page.pay</code>)。需提交企业资质、<strong>网站地址(基本要备案域名)</strong>、经营信息。审核约 1–3 个工作日。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">4</div><div class="body">
|
||||
<h3>配置生产密钥(同沙箱操作)</h3>
|
||||
<p>密钥工具生成应用私钥/公钥 → 应用「接口加签方式 → 公钥模式」上传应用公钥 → 复制返回的「支付宝公钥」。</p>
|
||||
</div></div>
|
||||
<div class="step"><div class="n">5</div><div class="body">
|
||||
<h3>切到生产</h3>
|
||||
<p>把正式 APPID/应用私钥/支付宝公钥写入商户配置,<code>production: true</code>(代码走正式网关),<code>base_url</code> 用正式 HTTPS 备案域名。</p>
|
||||
</div></div>
|
||||
|
||||
<h2>五、三样东西 → 配置对应</h2>
|
||||
<table>
|
||||
<thead><tr><th style="width:24%">拿到的</th><th style="width:34%">来源</th><th>填到哪</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>APPID</td><td>沙箱页面 / 生产应用页直接给</td><td><code>config.yaml → alipay_sandbox.app_id</code>(或 merchants 表)</td></tr>
|
||||
<tr><td>应用私钥</td><td>你用密钥工具生成,自己保管</td><td>环境变量 <code>ALIPAY_APP_PRIVATE_KEY</code></td></tr>
|
||||
<tr><td>支付宝公钥</td><td>上传应用公钥后支付宝生成</td><td>环境变量 <code>ALIPAY_PUBLIC_KEY</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<pre><code>cd /Users/wangjia/code/pay
|
||||
export ALIPAY_APP_PRIVATE_KEY="MIIEv...应用私钥..."
|
||||
export ALIPAY_PUBLIC_KEY="MIIBIj...支付宝公钥..."
|
||||
# config.yaml 里 alipay_sandbox.enabled=true、填上 app_id
|
||||
go run .
|
||||
# 浏览器开 http://localhost:8080 选套餐 → 跳沙箱收银台 → 沙箱买家账号付款</code></pre>
|
||||
<div class="callout warn">
|
||||
本机能测「下单 → 跳收银台 → 扫码付成功」整条链路;唯一测不到的是<strong>异步回调 notify</strong>(支付宝回调不到本机)。但服务内置的 <code>query_sync</code> 主动查单兜底会在 30 秒内把订单补成「已支付」,结果页照样变 ✅。要完整验证 notify 需内网穿透或部署到公网服务器。
|
||||
</div>
|
||||
|
||||
<h2>六、推进路线(两线并行,互不等待)</h2>
|
||||
<svg viewBox="0 0 900 230" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,PingFang SC,sans-serif" font-size="13">
|
||||
<defs><marker id="ar" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L9,3 L0,6 Z" fill="#58a6ff"/></marker></defs>
|
||||
<!-- line A -->
|
||||
<text x="20" y="46" fill="#3fb950" font-weight="600">A · 联调线(现在)</text>
|
||||
<rect x="20" y="58" width="170" height="44" rx="8" fill="#161b22" stroke="#3fb950"/><text x="105" y="84" fill="#e6edf3" text-anchor="middle">拿沙箱密钥</text>
|
||||
<line x1="190" y1="80" x2="232" y2="80" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<rect x="234" y="58" width="200" height="44" rx="8" fill="#161b22" stroke="#3fb950"/><text x="334" y="84" fill="#e6edf3" text-anchor="middle">本机跑通支付宝沙箱</text>
|
||||
<line x1="434" y1="80" x2="476" y2="80" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<rect x="478" y="58" width="200" height="44" rx="8" fill="#161b22" stroke="#283041"/><text x="578" y="84" fill="#aeb9c7" text-anchor="middle">代码就绪 · 等生产</text>
|
||||
<!-- line B -->
|
||||
<text x="20" y="150" fill="#d29922" font-weight="600">B · 上线线(并行)</text>
|
||||
<rect x="20" y="162" width="150" height="44" rx="8" fill="#161b22" stroke="#d29922"/><text x="95" y="188" fill="#e6edf3" text-anchor="middle">域名转入</text>
|
||||
<line x1="170" y1="184" x2="208" y2="184" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<rect x="210" y="162" width="120" height="44" rx="8" fill="#161b22" stroke="#d29922"/><text x="270" y="188" fill="#e6edf3" text-anchor="middle">ICP 备案</text>
|
||||
<line x1="330" y1="184" x2="368" y2="184" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<rect x="370" y="162" width="170" height="44" rx="8" fill="#161b22" stroke="#d29922"/><text x="455" y="188" fill="#e6edf3" text-anchor="middle">企业实名+签约</text>
|
||||
<line x1="540" y1="184" x2="578" y2="184" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<rect x="580" y="162" width="150" height="44" rx="8" fill="#161b22" stroke="#d29922"/><text x="655" y="188" fill="#e6edf3" text-anchor="middle">生产密钥</text>
|
||||
<!-- merge -->
|
||||
<line x1="655" y1="102" x2="655" y2="160" stroke="#7d8896" stroke-width="1.5" stroke-dasharray="4 4"/>
|
||||
<rect x="740" y="110" width="140" height="46" rx="8" fill="#1c2330" stroke="#58a6ff"/><text x="810" y="130" fill="#e6edf3" text-anchor="middle" font-weight="600">换密钥+域名</text><text x="810" y="146" fill="#58a6ff" text-anchor="middle">→ 上线收真钱</text>
|
||||
<line x1="678" y1="80" x2="738" y2="120" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
<line x1="730" y1="184" x2="800" y2="158" stroke="#58a6ff" stroke-width="2" marker-end="url(#ar)"/>
|
||||
</svg>
|
||||
<p>两条线独立推进:A 线现在就能把代码全跑通,B 线(域名/备案/签约)慢慢走;B 线齐了,只需「换生产密钥 + 换备案域名」即可上线,主流程一行不改(<code>Merchant.Production</code> + <code>base_url</code>)。</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,270 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user