fd5f56f7de
- 微信支付 V3 Native 渠道 (wechat.go):Native下单/回调AES-GCM解密验签/查单 - 支付宝:手机网站支付 wap.pay + 按 UA 自适应(PC page.pay扫码 / 手机拉App);qr_pay_mode=2 完整扫码收银台 - 业务对接:下单接口扩展(biz_system/biz_ref/return_url)+ HMAC 签名鉴权;支付成功 webhook 主动推送业务方 + 60s 重试 + BizNotifyLog - 通用多业务:config.biz 改 map,加业务只改配置(BIZ_<SYS>_SECRET/_CALLBACK_URL) - seedPlans:四档真实套餐 + promo_first_month(¥1) + test_liandiao(0.01),均挂 biz_code;/products 暴露 biz_code - 删除沙箱 pay.html;对接设计文档入 docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
215 lines
17 KiB
HTML
215 lines
17 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>pay × jiu · 授权续费对接方案</title>
|
||
<style>
|
||
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--green:#3fb950;--amber:#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:920px;margin:0 auto;padding:48px 22px 90px}
|
||
.eyebrow{color:var(--accent);font-weight:600;letter-spacing:.12em;font-size:12px;text-transform:uppercase}
|
||
h1{font-size:30px;margin:10px 0 6px}
|
||
.lead{color:var(--fg-soft);margin:0 0 10px}
|
||
.meta{color:var(--muted);font-size:12.5px;margin-bottom:26px}
|
||
h2{font-size:19px;margin:38px 0 12px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:8px}
|
||
h3{font-size:15.5px;margin:22px 0 8px;color:var(--fg)}
|
||
a{color:var(--accent);text-decoration:none}
|
||
p{margin:8px 0}
|
||
code{background:#0b1020;border:1px solid var(--border);border-radius:6px;padding:1px 6px;font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:13px;color:#c9d6e6}
|
||
pre{background:#0b1020;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:12.8px;line-height:1.6;color:#c9d6e6}
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;margin:14px 0}
|
||
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13.6px}
|
||
th,td{border:1px solid var(--border);padding:8px 11px;text-align:left;vertical-align:top}
|
||
th{background:var(--card-2);color:var(--fg-soft);font-weight:600}
|
||
td code{font-size:12.5px}
|
||
.tag{display:inline-block;font-size:11.5px;padding:2px 9px;border-radius:999px;border:1px solid var(--border);margin-right:6px}
|
||
.tag.pay{color:var(--accent);border-color:var(--accent)}
|
||
.tag.jiu{color:var(--green);border-color:var(--green)}
|
||
.tag.new{color:var(--amber);border-color:var(--amber)}
|
||
.flow{background:#0b1020;border:1px solid var(--border);border-radius:10px;padding:16px;overflow-x:auto;font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:12.6px;line-height:1.75;color:#c9d6e6;white-space:pre}
|
||
.warn{background:rgba(210,153,34,.08);border:1px solid rgba(210,153,34,.4);border-radius:10px;padding:12px 15px;margin:14px 0;color:#e8d3a0;font-size:13.6px}
|
||
.ok{color:var(--green)} .amber{color:var(--amber)} .red{color:var(--red)}
|
||
ol,ul{margin:8px 0;padding-left:22px} li{margin:5px 0}
|
||
.step{display:flex;gap:12px;margin:10px 0}
|
||
.step .n{flex:0 0 26px;height:26px;border-radius:50%;background:var(--accent);color:#08111f;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:13px}
|
||
.back{display:inline-block;margin-bottom:18px;font-size:13px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="./index.html">← 返回文档索引</a>
|
||
<div class="eyebrow">Integration Design · pay × jiu</div>
|
||
<h1>pay 接入 jiu 授权续费 · 对接方案</h1>
|
||
<p class="lead">让 jiu(岩美酒库)门店在应用内购买/续费授权:付款走 pay 收款中枢,付成功后 pay 回调 jiu,jiu 直接给该门店续期。</p>
|
||
<p class="meta">v1.0 · 2026-07-03 · 方案 B(付款直接续期门店)· server-to-server + 签名 webhook · 待评审</p>
|
||
|
||
<h2>1. 背景与目标</h2>
|
||
<div class="card">
|
||
<p>pay 已是独立收款中枢(支付宝电脑网站支付 <code>page.pay</code> + 手机网站支付 <code>wap.pay</code> UA 自适应,已上线验证到账)。jiu 已有完整授权体系(<code>License</code> / <code>LicenseCode</code> / 兑换续期)。<b>目标</b>:把两者接起来——jiu 门店在应用内选套餐付款,付成功后<b>无感自动续期</b>,不再靠线下发兑换码。</p>
|
||
</div>
|
||
|
||
<h2>2. 现状盘点</h2>
|
||
<h3>pay 现有对外接口</h3>
|
||
<table>
|
||
<tr><th>方法/路径</th><th>说明</th></tr>
|
||
<tr><td><code>POST /api/v1/orders</code></td><td>下单,返回 <code>pay_url</code>(UA 自适应 PC 扫码 / 手机拉 App)。<span class="red">当前只收 product_id,无业务标识、无鉴权</span></td></tr>
|
||
<tr><td><code>GET /api/v1/orders/:out_trade_no</code></td><td>查订单状态</td></tr>
|
||
<tr><td><code>POST /api/v1/notify/{alipay,wechat}</code></td><td>支付宝/微信异步回调(入账,已实现)</td></tr>
|
||
</table>
|
||
<h3>jiu 现有授权模型</h3>
|
||
<table>
|
||
<tr><th>模型</th><th>关键字段</th><th>语义</th></tr>
|
||
<tr><td><code>License</code></td><td>ShopID · Type(trial/monthly/annual/lifetime) · Tier · ExpiresAt · MaxDevices · IsActive</td><td>门店当前授权状态</td></tr>
|
||
<tr><td><code>LicenseCode</code></td><td>Code · DurationDays · Tier · Type · Status(unused/redeemed/void) · RedeemedShopID</td><td>兑换券:兑换时把 DurationDays 叠加到门店到期时间;一码一次</td></tr>
|
||
</table>
|
||
<p>现有续期路径:平台生成码 → 用户获得 → 门店兑换。<b>「购买」这步目前是线下/手动</b>——本方案就是把它自动化。</p>
|
||
|
||
<h2>3. 总体架构(方案 B)</h2>
|
||
<div class="flow">用户(jiu 客户端, 已登录, 知道自己 shop) jiu 后端 pay 服务 支付宝/微信
|
||
│ │ │ │
|
||
①点“购买/续费”选套餐 ───────────────────────►│ │ │
|
||
│ ②建 purchase(pending) │ │
|
||
│ 调 pay 下单(biz_ref=shop_id, 签名) ───►│ │
|
||
│ │◄──── pay_url ────────────│ │
|
||
│◄──────────── 返回 pay_url ─────────────────│ │ │
|
||
③打开 pay_url 付款(PC扫码/手机拉App) ───────────────────────────────────────────────────────────►│
|
||
│ │ ④收 notify → 入账 ✓ │
|
||
│ │◄─ ⑤pay 签名 webhook ─────│ │
|
||
│ ⑥验签+幂等 → 给 shop 续期(复用 license) │ │
|
||
│ 回 ack ────────────────────────────►│ │
|
||
④'客户端刷新 → 已续期 ✓ │ │ │</div>
|
||
<p><b>为什么是 B</b>:购买发生在已登录门店,jiu 天然知道是哪个 <code>ShopID</code>,付成功直接续期,用户无感,体验最好。</p>
|
||
|
||
<h2>4. 数据模型改动</h2>
|
||
<h3><span class="tag pay">pay</span> Order 加业务字段 <span class="tag new">新增</span></h3>
|
||
<table>
|
||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||
<tr><td><code>BizSystem</code></td><td>string(index)</td><td>业务来源,如 <code>jiu</code>;决定回调哪个 webhook / 用哪个密钥</td></tr>
|
||
<tr><td><code>BizRef</code></td><td>string</td><td>业务引用,jiu 传(如 <code>shop_id</code> 或 jiu purchase_id);webhook 原样回传</td></tr>
|
||
<tr><td><code>BizReturnURL</code></td><td>string(可选)</td><td>付完跳回业务自己的页面;空则用 pay 默认结果页</td></tr>
|
||
</table>
|
||
<h3><span class="tag pay">pay</span> Product 加稳定套餐码 <span class="tag new">新增</span></h3>
|
||
<p><code>BizCode</code>(如 <code>annual_standard</code>):webhook 回传给 jiu,jiu 按<b>稳定码</b>而非数字 product_id 映射权益,避免硬编码耦合。价格权威仍在 pay 套餐表。</p>
|
||
<h3><span class="tag pay">pay</span> BizNotifyLog 业务回调日志 <span class="tag new">新增</span></h3>
|
||
<p>记录每次向业务系统回调的 URL / payload / 响应 / 重试次数,用于排障与重放。</p>
|
||
<h3><span class="tag jiu">jiu</span> Purchase 购买记录 <span class="tag new">新增</span></h3>
|
||
<table>
|
||
<tr><th>字段</th><th>说明</th></tr>
|
||
<tr><td>ShopID · ProductBizCode · Amount</td><td>谁买、买什么、多少钱</td></tr>
|
||
<tr><td>OutTradeNo</td><td>pay 订单号(对账 + 幂等键)</td></tr>
|
||
<tr><td>Status(pending/paid/failed) · PaidAt</td><td>状态机</td></tr>
|
||
</table>
|
||
|
||
<h2>5. 接口契约</h2>
|
||
|
||
<h3>5.1 <span class="tag pay">pay</span> 下单接口扩展 <code>POST /api/v1/orders</code></h3>
|
||
<p>请求头带签名(见 §6)。Body 扩展:</p>
|
||
<pre>{
|
||
"product_id": 3,
|
||
"biz_system": "jiu",
|
||
"biz_ref": "shop_1024",
|
||
"return_url": "https://jiu.51yanmei.com/license/result" // 可选
|
||
}
|
||
Headers:
|
||
X-Pay-System: jiu
|
||
X-Pay-Timestamp: 1751520000
|
||
X-Pay-Nonce: 8f3a...
|
||
X-Pay-Sign: base64(HMAC-SHA256(secret, system+timestamp+nonce+body))</pre>
|
||
<p>pay 校验签名 → 按 product_id 取<b>服务端权威价格</b>建单、存 biz 字段 → 返回 <code>pay_url</code>。</p>
|
||
|
||
<h3>5.2 <span class="tag pay">pay</span> → <span class="tag jiu">jiu</span> 业务 webhook(入账后主动推)</h3>
|
||
<p>pay 在 <code>applyPaid</code> 成功后,按 order 的 <code>biz_system</code> 找到回调配置(URL+secret),POST:</p>
|
||
<pre>POST {jiu 配置的回调 URL}
|
||
Headers: X-Pay-Timestamp, X-Pay-Nonce, X-Pay-Sign (HMAC-SHA256)
|
||
Body:
|
||
{
|
||
"out_trade_no": "yanmei-20260703...-xxxx",
|
||
"biz_system": "jiu",
|
||
"biz_ref": "shop_1024",
|
||
"product_biz_code": "annual_standard",
|
||
"amount": "268.00",
|
||
"trade_no": "20260703...", // 支付宝/微信交易号
|
||
"channel": "alipay",
|
||
"paid_at": "2026-07-03T14:36:30+08:00"
|
||
}
|
||
期望响应:HTTP 200 + {"code":"SUCCESS"};否则 pay 按退避重试</pre>
|
||
|
||
<h3>5.3 <span class="tag jiu">jiu</span> 购买接口 <code>POST /api/v1/license/purchase</code></h3>
|
||
<p>鉴权=jiu 自己的登录态(当前用户/门店)。选套餐 → 建 Purchase(pending) → 调 pay §5.1 下单 → 返回 <code>pay_url</code> 给客户端。</p>
|
||
|
||
<h3>5.4 <span class="tag jiu">jiu</span> webhook 接收器 <code>POST /api/v1/pay/callback</code></h3>
|
||
<div class="step"><div class="n">1</div><div><b>验签</b>:用共享密钥校验 X-Pay-Sign + 时间戳窗口,确认真是 pay 发来的</div></div>
|
||
<div class="step"><div class="n">2</div><div><b>幂等</b>:按 <code>out_trade_no</code> 查 Purchase,已处理则直接回 SUCCESS</div></div>
|
||
<div class="step"><div class="n">3</div><div><b>映射</b>:<code>product_biz_code</code> → (duration_days, tier, type)</div></div>
|
||
<div class="step"><div class="n">4</div><div><b>续期</b>:给该 ShopID 延长授权(复用现有 license 服务:生成 LicenseCode 即兑换,或直接叠加 <code>License.ExpiresAt</code>)</div></div>
|
||
<div class="step"><div class="n">5</div><div><b>回 ack</b>:HTTP 200 + <code>{"code":"SUCCESS"}</code>;标记 Purchase=paid</div></div>
|
||
|
||
<h2>6. 安全设计</h2>
|
||
<div class="card">
|
||
<ul>
|
||
<li><b>共享密钥</b>:jiu↔pay 各方向一对 HMAC 密钥,存 Bitwarden(如条目 <code>pay-jiu webhook secret</code>),部署经 rbw 灌环境变量,<b>不落配置/仓库</b>。</li>
|
||
<li><b>签名</b>:HMAC-SHA256 覆盖 <code>system+timestamp+nonce+body</code>,防篡改。</li>
|
||
<li><b>防重放</b>:timestamp 5 分钟窗口 + nonce 去重(拒绝过期/重复请求)。</li>
|
||
<li><b>幂等</b>:两端都按 <code>out_trade_no</code> 唯一处理——pay 可能重发 webhook、jiu 只续一次。</li>
|
||
<li><b>价格权威</b>:pay 按 product_id 取服务端价格,不信任传入金额(延续既有红线)。</li>
|
||
<li><b>传输</b>:全程 HTTPS(jiu.51yanmei.com / pay.51yanmei.com 均已 HTTPS)。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>7. 套餐映射</h2>
|
||
<p>pay 套餐表持有价格(99/268/888/2280),每个套餐挂稳定 <code>biz_code</code>;jiu 侧一张映射表把 <code>biz_code</code> 翻成时长/档位:</p>
|
||
<table>
|
||
<tr><th>pay product</th><th>biz_code</th><th>价格</th><th>jiu 续期(直接叠加 ExpiresAt)</th></tr>
|
||
<tr><td>月付·标准</td><td><code>monthly_standard</code></td><td>¥299</td><td>+30 天, tier=standard, type=monthly</td></tr>
|
||
<tr><td>月付·高级</td><td><code>monthly_pro</code></td><td>¥599</td><td>+30 天, tier=pro, type=monthly</td></tr>
|
||
<tr><td>年付·标准</td><td><code>annual_standard</code></td><td>¥2999</td><td>+365 天, tier=standard, type=annual</td></tr>
|
||
<tr><td>年付·高级</td><td><code>annual_pro</code></td><td>¥5999</td><td>+365 天, tier=pro, type=annual</td></tr>
|
||
</table>
|
||
<p><b>权益差异(2026-07-03 已定,都是单店):</b></p>
|
||
<table>
|
||
<tr><th>档</th><th>客户端</th><th>仓库</th><th>图片分享</th><th>AI 商业分析</th></tr>
|
||
<tr><td>标准(299/2999)</td><td>2</td><td>单仓库</td><td>千张</td><td>—</td></tr>
|
||
<tr><td>高级(599/5999)</td><td>5</td><td>多仓库</td><td>万张</td><td>免费 AI 周/月度商业数据分析</td></tr>
|
||
</table>
|
||
<p class="amber">pay 侧只透传 biz_code,不关心权益细节;权益→License(tier/max_devices/features) 的落地在 jiu 侧,见 jiu 仓 <code>docs/pay支付对接开发指南.html</code>。</p>
|
||
|
||
<h2>8. 失败 / 重试 / 对账</h2>
|
||
<ul>
|
||
<li><b>webhook 失败重试</b>:pay 对未收到 SUCCESS ack 的回调按退避重试若干次(如 1m/5m/30m/2h…),记 BizNotifyLog。</li>
|
||
<li><b>兜底查单</b>:jiu 侧对 pending 超时的 Purchase 主动查 pay <code>GET /orders/:out_trade_no</code> 补偿(防 webhook 全丢)。</li>
|
||
<li><b>对账</b>:pay orders 与 jiu purchases 按 out_trade_no 对齐,定期核对金额/状态。</li>
|
||
<li><b>用户已付但续期失败</b>:webhook 重试 + 查单兜底应能自愈;仍失败则告警人工处理(钱已到,不可让用户吃亏)。</li>
|
||
</ul>
|
||
|
||
<h2>9. 分阶段实施计划</h2>
|
||
<div class="card">
|
||
<p><b>阶段 1 · pay 侧</b>:Order 加 biz 字段 + Product 加 biz_code;下单接口收 biz + 签名鉴权;BizNotifyLog。</p>
|
||
<p><b>阶段 2 · pay 侧</b>:入账后业务 webhook 推送 + 退避重试;回调配置(每个 biz_system 的 URL+secret)。</p>
|
||
<p><b>阶段 3 · jiu 侧</b>:Purchase 模型 + 购买接口(调 pay 下单)。</p>
|
||
<p><b>阶段 4 · jiu 侧</b>:webhook 接收器(验签+幂等+映射+续期,复用 license 服务)+ 查单兜底。</p>
|
||
<p><b>阶段 5</b>:jiu 客户端购买/续费 UI;真实 1 分钱端到端联调;对账脚本。</p>
|
||
</div>
|
||
|
||
<h2>10. 决策(2026-07-03 已定)</h2>
|
||
<div class="card">
|
||
<table>
|
||
<tr><th>#</th><th>问题</th><th class="ok">决策</th></tr>
|
||
<tr><td>1</td><td>套餐定义</td><td>月 ¥299/¥599、年 ¥2999/¥5999(2 档 × 月/年,见 §7)。标准/高级权益差异待定</td></tr>
|
||
<tr><td>2</td><td>续期实现</td><td><b>直接叠加 <code>License.ExpiresAt</code></b>(不走生成兑换码)</td></tr>
|
||
<tr><td>3</td><td>biz_ref</td><td><b>jiu 的 <code>purchase_id</code></b>(利对账/多态)</td></tr>
|
||
<tr><td>4</td><td>付款后</td><td><b>跳回 jiu 结果页</b>(return_url)</td></tr>
|
||
<tr><td>5</td><td>客户端形态</td><td><b>Web 入口 + App 入口都要</b>;App 用<b>内置 webview</b> 打开 pay_url。付完 return_url 跳回;webview 内拉起支付宝 App 需处理 scheme 唤起/回跳</td></tr>
|
||
</table>
|
||
</div>
|
||
<div class="warn">
|
||
<b>权益已定</b>(见 §7 表):标准=2 客户端/单仓库/千图;高级=5 客户端/多仓库/万图/AI 分析。
|
||
<br><b>App webview 注意:</b>应用内 webview 里跳支付宝需 scheme 白名单 / 拦截 <code>alipays://</code> 唤起 App,付完靠 return_url 回跳 jiu 页面(比 PC 多一层客户端处理,Phase 5 联调)。
|
||
</div>
|
||
|
||
<p style="margin-top:30px;color:var(--muted);font-size:12.5px">相关:<a href="./支付宝收款页时序图与架构.html">支付宝收款页时序与架构</a> · <a href="./上线状态与部署Runbook.html">上线状态与部署 Runbook</a></p>
|
||
</div>
|
||
</body>
|
||
</html>
|