# 收款闭环 · 独角数卡 + 自托管 USDT(TRC20)+ webhook JIT 发码(最干净长期方案) > #34。目标:把「下单 → USDT 付款 → 自动发激活码 → 客户端/用户中心兑换 → 订阅生效」端到端打通, > 全程自托管、售出才发码、无跑分、无第三方支付主体。 > > **关键前提(已摸底)**:激活码后端大半已就绪——`POST /v1/redeem`(JWT 兑换,含幂等+限频+账户锁)、 > 批次生成/导出(`server/cmd/codegen` + `server/internal/codes`,码为 Crockford Base32 75-bit 带校验位、只存 > hash)、发卡店回调 `POST /webhook/store/codes`(HMAC+时间戳+nonce 防重放)都在。本方案**新增的核心只有一个 > 后端端点**(JIT 发码 `/internal/codes/mint`)+ 一个独立收款服务(`pangolin-pay`:TronGrid watcher + epay 支付口)。 > > **三大组件 / 数据流**: > ``` > 买家 ──下单──▶ 独角数卡(门面, 独立VPS) > │ 建单 + epay 跳转 > ▼ > pangolin-pay(自建收款服务, 同VPS) > ├ 派生每单唯一 TRC20 收款地址(watch-only, 只持 xpub) > ├ TronGrid watcher 轮询到账 + 确认 > └ 到账 ──epay notify──▶ 独角数卡「已支付」 > │ > ▼ 独角数卡 API-提货 > pangolin-pay /issue ──HMAC──▶ pangolin 控制面 POST /internal/codes/mint > └ 生成合法码 + 存 unused + 返回明文 > │ > ▼ 独角数卡把码交付买家 > 买家 ──输码──▶ 客户端/用户中心 POST /v1/redeem ──▶ 订阅生效 > ``` > > **安全红线(贯穿全程)**:① 热服务 `pangolin-pay` **只持 xpub 派生收款地址、绝不持私钥**(能收不能转, > 被脱库也转不走钱);② 私钥/助记词冷备 + Bitwarden,不入 git、不上热机;③ 收款终点=自托管钱包,变现经 > 交易所出金 US LLC,记账走 accounting agent;④ 全程不碰跑分/中国支付。 > > **主体归属**:`pangolin-pay` + 独角数卡部署在**独立海外小 VPS**(不放 pangolin1:内存紧 + 风险隔离), > 只经 HTTPS + HMAC 与 pangolin 控制面(`api.yanmeiai.com`)通信。 > > 参考文档:`docs/payment-a-selfhosted-store-plan.html` / `payment-channels-overview.html` / > `payment-reseller-fulfillment-design.html`。 --- ## Phase 0 — 计划落库 + todo - [ ] 0.1 本 `.md` 定稿 + 生成 HTML 阅读版 `docs/payment-clean-usdt-plan.html`,登记进 `docs/index.html`「实现计划」 - [ ] 0.2 `/todo` 把 #34 propose 为「待确认」(tier-1,`--ref` 指向本 `.md`),等用户 approve 再进 Phase 1 --- ## Phase 0.5 — 钱包选型(用哪个钱包) **先厘清:这套架构的「钱包」是两件事,别当成装一个 app。** - **主种子(钱在哪)**:一套 BIP39 助记词,冷存。这是资产本体。 - **watcher 收款(派生地址)**:热服务用**库**从 account **xpub(公钥)**派生每单地址——**watch-only,不是钱包 app**,也拿不到私钥。 - **归集签名**:需要动钱时,在冷端用私钥离线签 sweep 交易。 所以「用哪个钱包」= 选①主种子怎么冷存 + ②watcher 用哪个派生库。 **① 主种子 / 冷存 候选** | 方案 | 冷存安全 | xpub 导出(给 watcher) | 归集签名 | 结论 | |---|---|---|---|---| | **离线生成助记词 + 库派生**(推荐) | 高(纸/钢板离线保存,永不联网) | 直接从种子导 account xpub | 离线机用库签 | **最贴合 watch-only 架构、最灵活** | | **Ledger 硬件钱包**(可叠加) | 最高(私钥永不出设备) | 可导 account 公钥(需配套工具) | 插 Ledger 签 | 想再加一层硬件冷存就用它兜底 | | TronLink / imToken / Trust(手机热钱包) | 低(热钱包) | 不便批量 per-order 派生 | app 内签 | **不推荐**当收款主钱包(热 + 难集成) | **② watcher 派生库 候选**(给热服务 watch-only 派生 TRON 地址,非钱包) | 语言 | 库 | |---|---| | Go(与 server 同栈,推荐) | `tyler-smith/go-bip39`(助记词)+ `btcsuite/btcd/btcutil/hdkeychain`(BIP32 派生)→ 公钥转 TRON base58 地址(`0x41` 前缀 + Keccak);或成熟 TRON Go SDK | | Python | `tronpy` + `bip_utils`(一步到位派生 TRON 地址) | **推荐:** 主种子用 **离线生成 BIP39 助记词**(想再稳就叠一层 **Ledger 冷存**);watcher 用 **Go 库从 account xpub watch-only 派生**(与 server 同栈、好集成、热机无私钥)。**收款钱包 = 你自己的 HD 种子,不是某个 app。** - [ ] 0.5.1 定主种子方案:离线助记词(推荐)/ 是否叠加 Ledger 冷存;记录选型入本计划 - [ ] 0.5.2 定 watcher 派生库(Go bip39+hdkeychain→TRON 地址 / Python tronpy+bip_utils),跑通派生并与钱包 app 交叉核对同一地址(防派生实现不一致收不到钱) - [ ] 0.5.3 定归集冷签工具(离线机脚本 / Ledger),确认能对 sweep 交易离线签名 --- ## Phase 1 — 自托管钱包(TRC20 HD,watch-only 收款) 目标:一套助记词派生**无限个收款地址**,热服务只拿 xpub(公钥)派生地址收款、拿不到私钥;私钥冷备用于归集。 - [ ] 1.1 **离线**生成 BIP39 助记词(24 词)。TRON 派生路径 `m/44'/195'/0'/0/i`(195=TRON coin type)。 工具:离线机器用 `tronpy` / `tronweb` 或硬件钱包(Ledger,导出 account xpub)。**生成过程不联网。** - [ ] 1.2 密钥分级存放: - **助记词/私钥** → 冷备(纸/钢板离线)+ Bitwarden 记录,**永不上热机、不入 git** - **account xpub(公钥)** → 给 `pangolin-pay` 用来 watch-only 派生收款地址(无私钥) - **HMAC secret**(Phase 3/4 用)→ Bitwarden - [ ] 1.3 定**冷钱包归集地址**(cold sweep 目标)+ 记录 USDT-TRC20 合约地址(主网 `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`,**核对无误再写死**) - [ ] 1.4 备少量 **TRX** 作归集 gas(TRON 转账吃 energy/bandwidth,账户无 energy 时烧 TRX);记 energy 租赁备选 - [ ] 1.5 验证:用 xpub 离线派生前 5 个地址;往 index 0 打 1 USDT 小额测试,确认 watch-only 能查到该地址收款 --- ## Phase 2 — pangolin-pay:TronGrid watcher(自建收款侦测) 目标:一个独立 Go 服务,派生每单唯一地址、轮询 TronGrid 侦测到账、确认后置单已付。 - [ ] 2.1 建服务骨架 `pangolin-pay`(Go,复用 server 的 DB 方言层思路;独立仓/独立 cmd 均可)。自带轻量 DB(SQLite 即可) - [ ] 2.2 数据模型: - `pay_orders(order_no PK, sku, plan, duration_days, addr_index, address, expect_amount, status, tx_id, confirmations, created_at, expires_at)` - `addr_cursor(next_index)` — HD 派生游标,单调递增 - [ ] 2.3 **每单唯一地址**:建单时从 xpub 派生 `m/.../0/next_index` 分配给该单(最准,免金额撞车)。次选:单地址+唯一金额(备选降级) - [ ] 2.4 TronGrid 轮询:对每个 pending 单地址查 TRC20 转入 - endpoint:`GET https://api.trongrid.io/v1/accounts/{addr}/transactions/trc20?only_confirmed=true&contract_address=` - 比对:`to==address`、`token==USDT`、`value≥expect_amount`(含小额容差处理) - [ ] 2.5 **确认数**:只认 `only_confirmed`(已固化)交易,或等 ≥N 区块(TRON 约 1 分钟)。到账+确认 → `status=paid`、记 `tx_id` - [ ] 2.6 到账后触发发货(Phase 4 的 epay notify);**幂等**:同 `tx_id` 只认一次,一个 order 只发一次货 - [ ] 2.7 订单超时(默认 15min)未付 → `status=expired`、释放地址(地址不复用,避免旧付款串单) - [ ] 2.8 TronGrid API key + 限流退避 + 失败重试;watcher 崩溃可恢复(从 DB 未决单继续轮询) - [ ] 2.9 测试:mock TronGrid → 单测 金额匹配 / 确认门槛 / 超时 / 幂等(同 tx 重放)/ 崩溃恢复 --- ## Phase 3 — pangolin 控制面:JIT 发码端点 `POST /internal/codes/mint` 目标:付款确认时**即时生成一个合法激活码**(这才是 webhook JIT 的落点:售出才产码、才有效)。 现有 `/webhook/store/codes` 要求调用方自带合法码;自建场景由 pangolin 产码更干净(保证格式+校验位单源)。 - [ ] 3.1 新增 `POST /internal/codes/mint`(挂 `/internal` 组,**无 JWT**,HMAC 同 webhook: `X-Pangolin-Signature: sha256=` + `X-Pangolin-Timestamp`(±5min) + `X-Pangolin-Nonce`(Redis 去重)) - [ ] 3.2 body `{plan: free|pro|team, duration_days, order_ref}` → 复用 `codes.GenerateCode()` 产码 + `Store.CreateCode(hash, planID, duration, batchID)` 存 unused(channel=ChannelStore)→ 返回 `{code, expires_at}` - [ ] 3.3 **幂等**:以 `order_ref` 为幂等键(存 `order_ref→code_hash` 映射);同单重放返回**同一个码**,绝不重复发 - [ ] 3.4 secret 走 Bitwarden → `server.env`(`CODES_MINT_HMAC_SECRET`);路由挂载(`main.go`),仅 HMAC 保护、不经公网 JWT - [ ] 3.5 复用现有 webhook 的验签/时间戳/nonce 逻辑(抽公共中间件,避免重复实现) - [ ] 3.6 测试:验签通过/失败、时间戳越界、nonce 重放、`order_ref` 幂等、产码格式(Canonicalize 可还原+校验位对) --- ## Phase 4 — 独角数卡对接(门面 + 自动发货) 目标:独角数卡做门面/订单/交付,`pangolin-pay` 做 USDT 支付口 + 发货时取码。 - [ ] 4.1 (研究·先做)摸清所用独角数卡版本的**支付接口(epay 兼容规范)**与**API-提货/自动发货商品**机制 (版本相关,需实测;产出对接点清单再实现) - [ ] 4.2 `pangolin-pay` 实现 **epay 兼容支付接口**:独角数卡建单 → 跳我方支付页(展示该单 USDT 地址 + 应付金额 + 倒计时 + 网络必须 TRC20 强提示) - [ ] 4.3 到账(Phase 2.5)→ 按 epay 规范 **notify 独角数卡**(带签名)→ 独角数卡标「已支付」 - [ ] 4.4 商品设为 **API-提货**:独角数卡发货时调 `pangolin-pay /issue` → 内部以 HMAC 调 pangolin `/internal/codes/mint` 取合法码 → 返回独角数卡 → 交付买家(页面/邮件/站内) - [ ] 4.5 **SKU 映射表**(配置化,不散在代码):独角数卡商品 → `plan + duration_days` (Pro 月=pro/30、Pro 年=pro/365、Team 月/年=team/30|365;对齐 `design/CLAUDE.md §7` 与官网 Pricing) - [ ] 4.6 幂等贯通:独角数卡 order_no 作为 `order_ref` 一路透传到 mint,保证一单一码 --- ## Phase 5 — 部署 · 运维 · 安全 - [ ] 5.1 独立海外小 VPS(2C/2G 起)部署:独角数卡(Docker:PHP+MySQL+Redis)+ `pangolin-pay` + 域名 + HTTPS (**不放 pangolin1**) - [ ] 5.2 密钥落位核对:`pangolin-pay` 只有 **xpub + TronGrid key + epay 签名密钥 + mint HMAC secret**; **无私钥/助记词**(冷备)。全部 Bitwarden,不入 git - [ ] 5.3 **归集 runbook**:定期把各收款地址 USDT sweep 到冷钱包(**离线签名**,热机不碰私钥);备 TRX gas - [ ] 5.4 **变现 runbook**:冷钱包 → 交易所(能出金到 US LLC 的,自行核实)→ 卖 USDT→USD → US LLC 银行; 入账交 `code/accounting` agent(本项目不做费用台账) - [ ] 5.5 监控告警(接现有 Telegram 告警):watcher 存活、TronGrid 配额、到账延迟、mint 失败、订单异常堆积 - [ ] 5.6 **对账**:独角数卡订单 ↔ `pay_orders` ↔ codes(mint 条数/redeem 条数)三方日结,差异告警 --- ## Phase 6 — 端到端验证 - [ ] 6.1 真单闭环:下单 → USDT 付款(TRC20)→ watcher 确认 → mint 码 → 独角数卡交付 → 客户端/用户中心 `POST /v1/redeem` → 订阅/时长生效、全端同步 - [ ] 6.2 异常路径:超时未付 / 金额不足 / 重复付款 / 确认期网络抖动 / mint 重放 → 各自表现符合预期 - [ ] 6.3 安全核对:热服务 `otool`/进程无私钥;密钥不在 git(红线扫描);redeem 幂等+限频生效; 每单唯一地址不复用 --- ## Verification(端到端判据) - 后端:`cd server && go test ./internal/codes/...`(新增 mint 端点验签/幂等/格式测试全绿)。 - pangolin-pay:`go test ./...`(watcher 匹配/确认/超时/幂等/恢复)。 - 真机:一笔真 USDT 小额单,从下单到订阅生效全通;异常路径逐条演练。 - 安全:热服务无私钥、密钥不入 git、每单唯一地址、redeem 幂等限频——逐项核对。 ## 不在本轮 - 支付宝/微信第三方通道(E 层,灰产,另议且当可替换插件)。 - HD 自动 sweep 归集(先人工离线归集,量大再自动化)。 - 多币种(先只 USDT-TRC20)、watcher 高可用/多实例、加密网关备选路子(B 方案)。 - Telegram Stars / Apple IAP 独立收款口(海外补充,另排)。