Files
pangolin/docs/payment-a-selfhosted-store-plan.html
wangjia 5fd0d3c148 feat(pay): 同源收款测试页 GET /_test(Phase E 手动闭环验证)
pay-server 自带 /_test 测试页:创建订单→显示收款地址+精确金额(可复制)
+过期倒计时→轮询状态到 paid。同源、无 CORS/CSP 摩擦,不碰营销站安全头;
纯调 POST /order + GET /order/{id},无私钥无密钥。生产收银台后续走独角数卡门面。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:51:10 +08:00

245 lines
16 KiB
HTML
Raw Permalink 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>方案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=&lt;hex&gt; # HMAC-SHA256(raw body, secret)
X-Pangolin-Timestamp: &lt;unix&gt; # ±5 分钟窗口
X-Pangolin-Nonce: &lt;唯一串&gt; # Redis 去重,防重放
Body:
{ "code": "&lt;明文激活码&gt;", # 必须是 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)+时间戳+noncePOST <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>