5fd0d3c148
pay-server 自带 /_test 测试页:创建订单→显示收款地址+精确金额(可复制)
+过期倒计时→轮询状态到 paid。同源、无 CORS/CSP 摩擦,不碰营销站安全头;
纯调 POST /order + GET /order/{id},无私钥无密钥。生产收银台后续走独角数卡门面。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
245 lines
16 KiB
HTML
245 lines
16 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>方案A · 自建发卡网(独角数卡)落地细化</title>
|
||
<style>
|
||
:root{
|
||
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
|
||
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
|
||
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
|
||
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
|
||
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
|
||
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
h3{font-size:16px;margin:24px 0 8px;color:var(--fg)}
|
||
p{margin:10px 0}
|
||
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd3df}
|
||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
|
||
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
|
||
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
|
||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||
.tag.bad{background:rgba(224,106,106,.16);color:var(--bad)}
|
||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
|
||
.card h3{margin-top:0;color:var(--accent2)}
|
||
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:13.5px}
|
||
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||
th{color:var(--fg2);font-weight:600;font-size:13px}
|
||
ul,ol{padding-left:22px;margin:8px 0}
|
||
li{margin:5px 0}
|
||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||
.small{color:var(--fg2);font-size:13px}
|
||
a{color:var(--accent2)}
|
||
.back{display:inline-block;margin-bottom:24px;font-size:13px}
|
||
b{color:#fff}
|
||
.phase{font-size:19px;margin:40px 0 6px;color:var(--accent);font-weight:700}
|
||
.diagram{overflow-x:auto;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:20px;margin:16px 0}
|
||
.diagram svg{display:block;width:100%;height:auto;max-width:840px;margin:0 auto}
|
||
.legend{display:flex;gap:20px;flex-wrap:wrap;font-size:13px;color:var(--fg2);margin-top:12px;justify-content:center}
|
||
.legend span{display:inline-flex;align-items:center;gap:6px}
|
||
.legend i{width:22px;height:0;border-top-width:3px;border-top-style:solid;display:inline-block}
|
||
.warnbox{background:rgba(224,106,106,.08);border:1px solid rgba(224,106,106,.35);border-radius:12px;padding:14px 18px;margin:14px 0}
|
||
.warnbox b{color:var(--bad)}
|
||
.okbox{background:rgba(94,194,122,.07);border:1px solid rgba(94,194,122,.32);border-radius:12px;padding:14px 18px;margin:14px 0}
|
||
.done{color:var(--ok);font-weight:700}
|
||
.todo{color:var(--warn);font-weight:700}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="index.html">← 文档索引</a>
|
||
<h1>方案 A · 自建发卡网(独角数卡)落地细化</h1>
|
||
<p class="sub">2026-07-08 · <span class="tag info">实现计划</span> · 门面自建 + USDT 收款 + 激活码自动发货 · 基于<b>已就绪</b>的 <code>server/internal/codes</code></p>
|
||
|
||
<div class="lead">
|
||
<b>关键前提(好消息):</b>激活码后端<b>基本已就绪</b>——发卡店回调 <code>POST /webhook/store/codes</code>(HMAC + 时间戳 + nonce 防重放)、用户兑换 <code>POST /v1/redeem</code>(JWT)、批次生成/导出/作废都在 <code>server/internal/codes</code>。所以方案 A 的工作量集中在<b>门面(独角数卡)+ 一小段发货胶水 + 运维</b>,<b>不是重写后端</b>。
|
||
</div>
|
||
|
||
<h2>已就绪的后端契约(照这个对接,别新造)</h2>
|
||
|
||
<div class="card">
|
||
<h3><span class="done">已实现</span> · 发卡店回调 <code>POST /webhook/store/codes</code></h3>
|
||
<p>发卡店卖出一个码后调它,把该码登记为 pangolin 的有效激活码。挂在 <code>/v1</code> 之外、无需 JWT,靠三重校验。</p>
|
||
<pre>POST /webhook/store/codes # 无 JWT
|
||
Headers:
|
||
X-Pangolin-Signature: sha256=<hex> # HMAC-SHA256(raw body, secret)
|
||
X-Pangolin-Timestamp: <unix> # ±5 分钟窗口
|
||
X-Pangolin-Nonce: <唯一串> # Redis 去重,防重放
|
||
Body:
|
||
{ "code": "<明文激活码>", # 必须是 pangolin 格式合法码(见下)
|
||
"plan": "pro", # free | pro | team
|
||
"duration_days": 365,
|
||
"note": "dujiaoka #订单号" } # 可选备注
|
||
→ 201 {"status":"created"}
|
||
→ 200 {"status":"already_exists"} # code_hash 已存在,幂等
|
||
→ 200 {"status":"duplicate_ignored"} # nonce 重复,幂等
|
||
→ 400 / 401 # 校验失败</pre>
|
||
<p class="small">仅存 <b>hash</b>(明文不落库)。secret 走 Bitwarden,配到 <code>server.env</code>。</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3><span class="done">已实现</span> · 用户兑换 <code>POST /v1/redeem</code></h3>
|
||
<pre>POST /v1/redeem (JWT)
|
||
Body: { "code": "PGL-XXXX-..." }
|
||
→ 200 { "idempotent":false, "plan":"pro", "duration_days":365,
|
||
"expires_at":"2027-07-08T00:00:00Z" }
|
||
错误码: CODE_NOT_FOUND / CODE_REDEEMED / CODE_VOID / INVALID_CODE / RATE_LIMITED / ACCOUNT_LOCKED</pre>
|
||
<p class="small">已含幂等(同用户同码重放返回 idempotent=true)+ 限频 + 账户锁。用户中心/客户端只要有"输码兑换"入口即可。</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3><span class="done">已实现</span> · 批次生成 / 导出 / 作废</h3>
|
||
<p><code>server/cmd/codegen</code> 生成一批合法码(Crockford Base32、75-bit、带校验位、可 <code>Canonicalize</code> 纠错),<code>export.go</code> 导出明文,<code>VoidBatch</code> 批量作废未使用码。<b>码格式带校验位——第三方要产出合法码不能乱编</b>(见下模型 B 的待补点)。</p>
|
||
</div>
|
||
|
||
<h2>整体拓扑</h2>
|
||
<div class="diagram">
|
||
<svg viewBox="0 0 840 360" role="img" aria-label="自建发卡网拓扑">
|
||
<defs>
|
||
<marker id="a-code" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#5fb0c9"/></marker>
|
||
<marker id="a-money" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#e0884f"/></marker>
|
||
</defs>
|
||
<!-- 用户 -->
|
||
<rect x="30" y="150" width="130" height="60" rx="10" fill="#1d2129" stroke="#272c36"/>
|
||
<text x="95" y="177" text-anchor="middle" font-size="13" font-weight="700" fill="#e6e8ee">用户</text>
|
||
<text x="95" y="195" text-anchor="middle" font-size="11" fill="#a8afbd">浏览器 / TG</text>
|
||
|
||
<!-- 独角数卡 (独立 VPS) -->
|
||
<rect x="240" y="40" width="230" height="130" rx="12" fill="#141922" stroke="#5fb0c9"/>
|
||
<text x="355" y="66" text-anchor="middle" font-size="13" font-weight="700" fill="#5fb0c9">独角数卡(独立 VPS)</text>
|
||
<text x="355" y="87" text-anchor="middle" font-size="11" fill="#a8afbd">门面 · 商品/SKU · 卡密库存</text>
|
||
<text x="355" y="104" text-anchor="middle" font-size="11" fill="#a8afbd">USDT 支付插件 → 收款</text>
|
||
<text x="355" y="121" text-anchor="middle" font-size="11" fill="#a8afbd">PHP + MySQL + Redis</text>
|
||
<text x="355" y="145" text-anchor="middle" font-size="11" fill="#e0b84f">发货 hook → 回调 pangolin</text>
|
||
|
||
<!-- USDT 钱包 -->
|
||
<rect x="240" y="250" width="230" height="60" rx="10" fill="#1d2129" stroke="#5ec27a" stroke-dasharray="4 3"/>
|
||
<text x="355" y="277" text-anchor="middle" font-size="12.5" font-weight="700" fill="#e6e8ee">USDT 钱包 / 网关</text>
|
||
<text x="355" y="295" text-anchor="middle" font-size="11" fill="#a8afbd">TRC20 · 你 US LLC 自托管</text>
|
||
|
||
<!-- pangolin -->
|
||
<rect x="560" y="40" width="250" height="130" rx="12" fill="#141922" stroke="#e0884f"/>
|
||
<text x="685" y="66" text-anchor="middle" font-size="13" font-weight="700" fill="#e0884f">Pangolin 控制面</text>
|
||
<text x="685" y="87" text-anchor="middle" font-size="11" fill="#a8afbd">POST /webhook/store/codes</text>
|
||
<text x="685" y="104" text-anchor="middle" font-size="11" fill="#a8afbd">codes:登记/作废/批次</text>
|
||
<text x="685" y="126" text-anchor="middle" font-size="11" fill="#a8afbd">POST /v1/redeem(兑换)</text>
|
||
<text x="685" y="146" text-anchor="middle" font-size="11" fill="#5ec27a">→ 订阅/时长生效</text>
|
||
|
||
<!-- 客户端/用户中心 -->
|
||
<rect x="560" y="250" width="250" height="60" rx="10" fill="#1d2129" stroke="#272c36"/>
|
||
<text x="685" y="277" text-anchor="middle" font-size="12.5" font-weight="700" fill="#e6e8ee">客户端 / 用户中心</text>
|
||
<text x="685" y="295" text-anchor="middle" font-size="11" fill="#a8afbd">输码兑换 → /v1/redeem</text>
|
||
|
||
<!-- arrows -->
|
||
<line x1="160" y1="170" x2="238" y2="130" stroke="#e0884f" stroke-width="2.2" marker-end="url(#a-money)"/>
|
||
<text x="150" y="128" font-size="10.5" fill="#e0884f">①下单付款</text>
|
||
<line x1="355" y1="170" x2="355" y2="248" stroke="#e0884f" stroke-width="2.2" marker-end="url(#a-money)"/>
|
||
<text x="362" y="215" font-size="10.5" fill="#e0884f">②USDT 收款</text>
|
||
<line x1="470" y1="95" x2="558" y2="95" stroke="#5fb0c9" stroke-width="2.2" marker-end="url(#a-code)"/>
|
||
<text x="482" y="86" font-size="10.5" fill="#5fb0c9">③发货 hook 登记码</text>
|
||
<line x1="160" y1="188" x2="558" y2="285" stroke="#5fb0c9" stroke-width="2.2" marker-end="url(#a-code)"/>
|
||
<text x="250" y="245" font-size="10.5" fill="#5fb0c9">④用户拿码 → 输码兑换</text>
|
||
<line x1="685" y1="248" x2="685" y2="172" stroke="#5fb0c9" stroke-width="2.2" marker-end="url(#a-code)"/>
|
||
</svg>
|
||
<div class="legend">
|
||
<span><i style="border-color:#e0884f"></i>钱流</span>
|
||
<span><i style="border-color:#5fb0c9"></i>码流</span>
|
||
<span><i style="border-color:#5ec27a;border-top-style:dashed"></i>自托管资产</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>部署选址 · 风险隔离</h2>
|
||
<div class="warnbox">
|
||
<b>独角数卡必须独立部署,别放 pangolin1。</b> 两个理由:① pangolin1 只有 ~1GB 内存,跑控制面+agent+sing-box 已经紧,再塞 PHP+MySQL+Redis 会 OOM;② <b>风险隔离</b>——发卡/收款站点和 VPN 控制面绑一起,一处被盯上会牵连另一处。放<b>独立小 VPS</b>(海外,2C/2G 起),与控制面只经 HTTPS webhook 通信。
|
||
</div>
|
||
|
||
<h2>两种发货模型(映射到已有能力)</h2>
|
||
<table>
|
||
<thead><tr><th></th><th>模型 A · 预充卡密库存 <span class="tag ok">最快 MVP</span></th><th>模型 B · webhook JIT 登记 <span class="tag info">更安全</span></th></tr></thead>
|
||
<tbody>
|
||
<tr><td>怎么做</td><td><code>codegen</code> 生成一批 → 导出明文 → 充进独角数卡卡密库 → 发货直接给码</td><td>独角数卡发货后调 <code>/webhook/store/codes</code> 才把码登记为有效</td></tr>
|
||
<tr><td>码何时有效</td><td>生成即有效(在 codes 表 status=unused)</td><td>付款+发货后才有效</td></tr>
|
||
<tr><td>泄漏风险</td><td>独角数卡库被脱 → 码可被无票兑换</td><td>未售出的码无效,脱库也没用 <span class="tag ok">优</span></td></tr>
|
||
<tr><td>要写的胶水</td><td>几乎零(导入卡密即可)</td><td>发货 hook 签 HMAC 调 webhook + 解决"码由谁生成"</td></tr>
|
||
<tr><td>webhook</td><td>不用</td><td>用 <code>/webhook/store/codes</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="okbox">
|
||
<b>建议:MVP 先用模型 A</b>(零胶水、当天能卖),跑通量后升级模型 B 拿安全性。<br>
|
||
<b class="todo">模型 B 的待补点:</b>webhook 要求发卡店发送<b>pangolin 格式合法码(带校验位)</b>。独角数卡自己产不出合法码,两条路二选一:(1) 在独角数卡侧复刻 <code>GenerateCode</code> 生成算法;(2) 给 pangolin 加一个受保护的 <code>POST /internal/codes/mint</code>(HMAC 同款)让发卡网"先领一个合法码再发货登记"。<b>推荐 (2)</b>,格式单源、不重复实现。
|
||
</div>
|
||
|
||
<h2>商品 / SKU 映射</h2>
|
||
<p>独角数卡里每个商品 = 一个套餐档,映射到 webhook 的 <code>plan</code> + <code>duration_days</code>:</p>
|
||
<table>
|
||
<thead><tr><th>独角数卡商品</th><th>plan</th><th>duration_days</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Pro · 月</td><td>pro</td><td>30</td></tr>
|
||
<tr><td>Pro · 年</td><td>pro</td><td>365</td></tr>
|
||
<tr><td>Team · 月 / 年</td><td>team</td><td>30 / 365</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">档位/定价对齐 <code>design/CLAUDE.md §7</code> 与官网 Pricing;SKU 表落进配置,别散在代码里。</p>
|
||
|
||
<h2>USDT 收款接入</h2>
|
||
<ul>
|
||
<li><b>路子 1 · 独角数卡 USDT 插件 / 加密网关</b>(NOWPayments/Cryptomus 等):最快,插件配好即"支付完成"回调独角数卡触发发货。查 AUP/抽成/是否非托管。</li>
|
||
<li><b>路子 2 · 自建 TronGrid watcher</b> 作为独角数卡的自定义支付网关:单地址+唯一金额匹配(MVP)或 HD 每单地址(规模化),到账 N 确认后回调独角数卡。最干净,代码量小。详见 <a href="payment-reseller-fulfillment-design.html">发卡+发货架构</a> 的 USDT 一节。</li>
|
||
</ul>
|
||
|
||
<h2>要建的胶水(很小)</h2>
|
||
<ol>
|
||
<li><b>发货 hook → pangolin webhook</b>(仅模型 B):独角数卡发货成功后,组 body + 签 <code>X-Pangolin-Signature</code>(HMAC)+时间戳+nonce,POST <code>/webhook/store/codes</code>。独角数卡支持"自动发货 API 商品/webhook",写个小中间脚本或插件即可。</li>
|
||
<li><b>HMAC secret</b>:生成一把,存 Bitwarden;配 pangolin <code>server.env</code> 与独角数卡侧。</li>
|
||
<li><b>(模型 B 推荐)</b><code>POST /internal/codes/mint</code>:受 HMAC 保护,返回一个合法码给发卡网。<span class="tag warn">需新增</span></li>
|
||
<li><b>兑换入口自检</b>:确认客户端/用户中心已有"输码兑换 → /v1/redeem"的 UI(后端已就绪)。</li>
|
||
</ol>
|
||
|
||
<h2>落地步骤</h2>
|
||
<div class="phase">第 1 步 · 门面起来(半天)</div>
|
||
<ul>
|
||
<li>独立小 VPS 部署独角数卡(Docker 最省事),配域名+HTTPS。建 Pro/Team 商品与 SKU。</li>
|
||
</ul>
|
||
<div class="phase">第 2 步 · 收款接上(USDT)</div>
|
||
<ul>
|
||
<li>选路子 1(网关插件)或路子 2(自建 watcher),配到独角数卡,小额实测"付款→自动发货"。</li>
|
||
</ul>
|
||
<div class="phase">第 3 步 · 发货接 pangolin</div>
|
||
<ul>
|
||
<li><b>MVP(模型 A)</b>:<code>codegen</code> 生成一批 → 导出 → 充进独角数卡卡密库。<b>此步即可开卖。</b></li>
|
||
<li><b>升级(模型 B)</b>:加 <code>/internal/codes/mint</code> + 发货 hook 调 webhook,切到"售出才有效"。</li>
|
||
</ul>
|
||
<div class="phase">第 4 步 · 闭环验证</div>
|
||
<ul>
|
||
<li>真机走一遍:下单→USDT付款→拿码→客户端/用户中心 <code>/v1/redeem</code>→订阅生效、全端同步。</li>
|
||
<li>对账:独角数卡订单 ↔ codes 登记/核销记录,日结核对。</li>
|
||
</ul>
|
||
|
||
<h2>密钥 · 备份 · 风险边界</h2>
|
||
<ul>
|
||
<li>HMAC secret、USDT 种子/私钥、独角数卡 DB → 全走 Bitwarden,不入 git、不落明文。</li>
|
||
<li>独角数卡 DB 定期备份(订单/卡密)。</li>
|
||
<li><b>红线</b>:这条链全程不碰跑分/中国支付;USDT 进你自托管钱包,法币结算走 US LLC。支付宝/微信便利若要,另起第三方通道(E 层),当可替换插件,不进本链核心。</li>
|
||
</ul>
|
||
|
||
<h2>待你拍板 / 下一步</h2>
|
||
<ul>
|
||
<li>门面选 <b>独角数卡</b> 还是同类?独立 VPS 放哪(海外区域/配置)?</li>
|
||
<li>USDT 走<b>路子 1(网关)</b>还是<b>路子 2(自建 watcher)</b>?</li>
|
||
<li>起步用<b>模型 A(预充)</b>先开卖,确认?后续再上模型 B + <code>/internal/codes/mint</code>。</li>
|
||
<li>确认后我把上面拆成 todo(门面部署 / USDT 接入 / mint 端点 / 发货 hook / 闭环验证)进入落地。</li>
|
||
</ul>
|
||
|
||
<p class="small" style="margin-top:32px">相关:<a href="payment-channels-overview.html">支付渠道选型总览</a> · <a href="payment-reseller-fulfillment-design.html">发卡/Reseller 收款 + 激活码自动发货架构</a></p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|