# pangolin-pay 单地址 + 唯一金额收款模型(定稿) > #34/34A 收款模型**定稿**。取代"每单唯一 HD 地址"(激活/归集随订单数线性涨)与"地址池"(仍多地址)。 > 最终选:**单个固定收款地址 + 每单唯一金额**。归集 = **1 个地址**(激活一次、扫一笔),成本最省; > 订单靠**金额**区分,不靠地址。 > > **前端契约不变**:`POST /order` 仍返回 `{address, expect_amount}`,client 只用每单返回值 → 以后要升 > 多地址/GasFree 是**纯后端换实现**,前端零改动(见下"契约约定")。 > > 已实现的 wallet 派生 / tron(TronGrid 读到账、建交易、签名)/ cmd/sweep 复用;主要改 **store / pay(建单)/ > watcher(匹配)**。现有代码是"每单派生新址",本轮改为"单地址 + 唯一金额"。 ## 决策(已定) - **收款地址**:钱包 A 的地址 0(`m/44'/195'/0'/0/0`),从配置注入或由 xpub 派生。所有订单收到**这一个地址**。 - **唯一金额** ⭐:`expect_amount = base + tail`。base = 价格 ×1e6(micro-USDT);**tail 取方案 A(微尾数)**: `tail ∈ [1, 9999]` micro(偏差 ≤ 0.009999 USDT,**价格几乎不变**)。tail 在**迟到窗口内不复用**。 - **匹配 = 精确 `==`**:watcher 找"到收款地址、`value == expect_amount`、`block_ts > order.created`"的 TRC20 转入 → paid。 支付页显示**可一键复制的精确金额** + "请付精确金额"提示。 - **15min 超时** → expired → 提示重建订单(该 tail 一段时间内不复用)。 - **同用户单订单**:同时只能一个活跃(pending)订单。 - **孤儿付款**:到该地址但 `value` 不匹配任何活跃订单(付错/迟到抹了尾数)→ 记 `orphan_payments`,人工对账/补发。 - **归集**:定期扫**这一个地址**余额 → 冷钱包(**能量租赁** ~$0.1–1/笔)。激活一次、归集一笔。 ## 契约约定(钉死,保证以后单↔多地址纯后端) > **client(下单页/独角数卡)只使用每单 `POST /order` 返回的 `address` + `expect_amount`,绝不硬编码/缓存地址。** > 只要守住这条,单地址 ↔ 多地址 ↔ GasFree 都是后端内部换实现,前端与 VPN app 都不改。 ## Phase A — 数据模型 - [ ] `pay_orders` 改:加 `user_ref`、`expect_amount`(唯一金额,micro-USDT)、`matched_tx_id`;`address` 恒为收款地址。 - [ ] tail 分配支撑:记录**当前活跃订单已用 tail**(避免撞)+ **近期已用 tail 冷却**(迟到窗口内不复用);或全局计数器 + 去重校验。 - [ ] `orphan_payments` 表:`tx_id(唯一) / value / block_ts / created_at / handled(bool)`。 - [ ] 配置:`PAY_RECEIVE_ADDRESS`(或从 `PAY_ACCOUNT_XPUB` 派生 index 0,与钱包 A 地址 0 一致)。 ## Phase B — 建单(pay 服务) - [ ] `CreateOrder(userRef, sku, priceMicro)`:① 校验**同用户无活跃单**(有则返回现有/拒);② 分配**唯一金额**(base + 未占用 tail);③ 写 `pending` 单(TTL 15min),`address` = 收款地址。 - [ ] **tail 分配**:与当前所有活跃订单不撞 + 迟到窗口内不复用(记录+回收)。 - [ ] `GetOrder`。 ## Phase C — watcher 改造(匹配 / 超时 / 孤儿) - [ ] **匹配**:对每个 `pending` 单,查**收款地址**的 TRC20 转入,筛 `value == expect_amount` **且 `block_ts > order.created`** → `paid` + 记 `matched_tx_id`。 - [ ] **超时**:`pending` 过期 → `expired`。 - [ ] **孤儿**:收款地址的到账 tx 匹配不到任何活跃订单 → 记 `orphan_payments`(按 tx_id 去重)。 - [ ] **幂等**(tx_id 全局去重,含 orphan)+ **崩溃恢复**;TronGrid 需取 `block_timestamp` 供时间过滤。 ## Phase D — 归集(复用 cmd/sweep) - [ ] 扫**收款地址**余额 → 冷钱包(**能量租赁**,先手动 runbook);一地址一笔,与订单状态解耦。 ## Phase E — 测试 + 真链验证 - [ ] 单测:唯一金额分配不撞、精确匹配、**付错/迟到金额成孤儿**、超时、同用户单订单、幂等、**时间戳过滤**(旧到账不误配新单)、崩溃恢复。 - [ ] 真链:两并发订单**不同唯一金额、同一地址**各自付款到 paid;故意**付错金额** → 成孤儿不误配;归集该地址到冷钱包。 ## Verification / 判据 - `go test ./...`(store/pay/watcher 新逻辑全绿,重点覆盖唯一金额/精确匹配/孤儿/时间戳)。 - 真链:并发不同金额 + 付错成孤儿 两场景正确;归集一笔搞定。 - 成本:激活一次、归集 O(1);小额高频不再被激活/归集费拖累。 ## 不在本轮 / 以后(纯后端可切) - 多地址 / 地址池(容错更好但更贵,以后纯后端切,前端不动)。 - GasFree(#35,免 TRX 但每笔固定 1 USDT,适合少地址批量)。 - 能量租赁自动化;发货侧(独角数卡 + /internal/codes/mint)另排。