# 加密货币收银台 · 对接文档

> 托管式加密支付网关。商户只实现**两个 HTTP 接口**（一个**调用**创建订单 + 一个**接收**我们的 webhook），我们负责：生成收款地址、监听链上到账、签名回调、自动归集转账到你的提现地址。商户**不需要部署任何东西**，也不需要自己跑节点。
>
> **支持链**：TRON · Ethereum · BSC · Arbitrum · Polygon · Base · Optimism · Solana（共 8 条）
> **支持币种**：USDT、USDC
> **基础 URL**：`https://cryptocashier.shop`
> **文档版本**：v2（2026-04）
> **商户中心**：<https://cryptocashier.shop/merchant.html>

---

## 目录

1. [接入路线图（5 步）](#1-接入路线图)
2. [项目概览](#2-项目概览)
3. [第 1 步 · 注册拿到 api_key 和 callback_secret](#3-第-1-步--注册拿到-api_key-和-callback_secret)
4. [第 2 步 · 配置回调 URL + 提现地址](#4-第-2-步--配置回调-url--提现地址)
5. [第 3 步 · 创建订单 API](#5-第-3-步--创建订单-api)
6. [支付成功后用户去哪儿（return_url vs webhook）](#6-支付成功后用户去哪儿)
7. [第 4 步 · 接收回调 Webhook](#7-第-4-步--接收回调-webhook)
8. [用户反馈接口（可选）](#8-用户反馈接口可选)
9. [查询订单 API](#9-查询订单-api)
10. [支持的链（chain id）](#10-支持的链chain-id)
11. [第 5 步 · 联调 / 演示模式](#11-第-5-步--联调--演示模式)
12. [上线前检查清单](#12-上线前检查清单)
13. [常见问题 FAQ](#13-常见问题-faq)

---

## 1. 接入路线图

商户侧一共 **4 件必做 + 1 件可选**，按顺序做完即可上线：

| # | 做什么 | 一次性 / 每笔订单 | 跳转 |
|---|---|---|---|
| 1 | 注册账号，拿到 `api_key` 和 `callback_secret` | 一次性 | §3 |
| 2 | 配置回调 URL + 各链提现地址 | 一次性 | §4 |
| 3 | 下单时调 `POST /api/v1/orders` | 每笔订单 | §5 |
| 4 | 实现回调接口，接收 Webhook 发货 | 每笔订单 | §7 |
| 5 | 联调演示 + 上线自检 | 可选但强烈建议 | §11–12 |

---

## 2. 项目概览

### 完整支付路径

1. 用户在你网站点「加密货币支付」
2. 你后端 `POST /api/v1/orders`（带 `uid` / `amount_usd`），我们返回一个 `pay_url`
3. 你把用户 302 到 `pay_url`（我们的收银台页面），用户看到 QR 码和收款地址，用任何钱包扫码付款
4. 我们后台每分钟扫链，检测到用户付款 → 更新订单状态 → **POST 带签名 Webhook** 到你注册的 `callback_url`
5. 你验签成功后给用户加金币 / 发货，返回 `200 OK`
6. 我们把款从一次性收款地址自动归集转发到你在商户中心填的提现地址
7. 收银台页面轮询到订单已 `paid` → 显示「收款成功」，用户点「确定」跳回 `return_url`（未传则停在完成页）

### 核心好处

- **0 基础设施**：你不部署任何东西，只实现两个 HTTP 接口（一个调用 + 一个接收）
- **链上到账真相由网关监听**：不依赖用户浏览器；用户关掉页面 / 断网也照样结算
- **每个 uid 一组永久 HD 派生地址**：同用户二次支付回同一地址，便于对账
- **HMAC 签名 Webhook + 8 次指数退避重试**（1m / 5m / 30m / 2h / 8h / 24h / 48h）
- **支持 8 条主流链**，USDT + USDC
- **自动归集**：到账后自动把款从一次性地址转到你的提现地址，你只需看着提现地址余额上涨

### 时序图

```
用户浏览器                  商户后端                    加密网关                  区块链
    |                         |                           |                         |
    | 选择加密货币支付         |                           |                         |
    |------------------------>|                           |                         |
    |                         | POST /api/v1/orders       |                         |
    |                         |  (api_key, uid, amount)   |                         |
    |                         |-------------------------->|                         |
    |                         |                           | 派生 HD 地址             |
    |                         |                           | 写入订单表               |
    |                         |   { pay_url, addresses }  |                         |
    |                         |<--------------------------|                         |
    |   302 redirect to pay_url                           |                         |
    |<------------------------|                           |                         |
    |                                                     |                         |
    |   GET /pay/:id                                      |                         |
    |---------------------------------------------------->|                         |
    |                                                     | 302 to /cashier.html    |
    |<----------------------------------------------------|                         |
    |                                                     |                         |
    |   用户用钱包转 USDT                                  |                         |
    |----------------------------------------------------------------------------->|
    |                                                     |                         |
    |                                                     | 后台每分钟扫链           |
    |                                                     |<------------------------|
    |                                                     | 检测到到账               |
    |                                                     | 更新订单状态             |
    |                                       POST webhook (带签名)                   |
    |                         |<--------------------------|                         |
    |                         | 验签 + 加金币              |                         |
    |                         |------> 200 OK             |                         |
    |                         |-------------------------->|                         |
    |                                                     | 自动归集转账到你的提现地址|
    |                                                     |------------------------>|
    |                                     POST webhook (event=forwarded)            |
    |                         |<--------------------------|                         |
```

---

## 3. 第 1 步 · 注册拿到 api_key 和 callback_secret

去 <https://cryptocashier.shop/merchant.html> 用邮箱密码注册。系统立刻签发两样东西（**仅出现一次，务必复制保存**）：

| 名称 | 用途 |
|---|---|
| `api_key` | 调用我们 API 时用，放在 `Authorization: Bearer <api_key>` header |
| `callback_secret` | 我们回调你时用它做 HMAC 签名，你用它来**验签**你收到的 webhook |

---

## 4. 第 2 步 · 配置回调 URL + 提现地址

登录商户中心，完成三项配置（**前两项必做**，第三项可选但强烈建议）：

| 配置项 | 必填 | 用途 |
|---|---|---|
| **🔔 回调 URL** (`callback_url`) | 必 | 订单状态变更时我们 POST 到这里（paid / overpaid / partial / expired / forwarded）。例 `https://site.com/api/crypto-webhook`。接收端实现见 §7。 |
| **💸 提现地址**（每条启用的链一个） | 必 | 你自己控制的收款地址。我们检测到订单到账后自动把款从一次性收款地址**归集转发**到这些地址。**没填的链，前端会直接禁用**，用户无法在该链支付。 |
| **📮 反馈 URL** (`feedback_url`) | 选 | 用户在收银台卡单、点「上传反馈」时，我们把**用户上传的钱包地址 + 截图 + 留言** POST 到这里。例 `https://site.com/api/crypto-feedback`。**不填**则复用回调 URL（按 `event: "feedback"` 分流）。详见 §8。 |

> **小提示**：回调接收端可以等你实现完第 4 步再填。提现地址必须在**创建订单前**填好。反馈 URL 强烈建议配——用户卡单时你能第一时间拿到诊断信息，人工跟进发货或退款。

---

## 5. 第 3 步 · 创建订单 API

```
POST /api/v1/orders
```

### 请求

```bash
curl -X POST https://cryptocashier.shop/api/v1/orders \
  -H "Authorization: Bearer pk_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_order_id": "ORD-20260419-001",
    "uid": "USER_12345",
    "amount_usd": "9.99",
    "product": "100 金币",
    "return_url": "https://site.com/order/paid?id=ORD-20260419-001",
    "expires_min": 10
  }'
```

### 请求字段

| 字段 | 必填 | 说明 |
|---|---|---|
| `merchant_order_id` | 必 | 你系统里订单 ID，唯一。重复调用同一 ID 返回同一订单（幂等） |
| `uid` | 必 | 付款用户 ID（你系统里的 user_id）。**同一 uid 永远得到同一组收款地址** |
| `amount_usd` | 必 | 订单金额，按 1:1 当作 USDT/USDC 金额。字符串类型避免浮点精度。范围 `(0, 1_000_000]` |
| `product` | 选 | 商品名，仅展示 |
| `return_url` | 选 | 用户在收银台点「确定」后**浏览器跳转的地址**（通常是你的"订单完成"页）。不传则停在我们的「支付完成」静态页。详见 §6 |
| `expires_min` | 选 | 订单有效期（分钟），默认 10，范围 1–180 |

### 响应（HTTP 200）

```json
{
  "id":                "f6c8a97b-4569-4e92-a8fb-7f15b1c5a840",
  "merchant_order_id": "ORD-20260419-001",
  "uid":               "USER_12345",
  "amount_usd":        "9.99",
  "amount_received":   "0",
  "status":            "pending",
  "currency":          null,
  "chain":             null,
  "product":           "100 金币",
  "return_url":        "https://site.com/order/paid?id=ORD-20260419-001",
  "pay_url":           "https://cryptocashier.shop/pay/f6c8...",
  "addresses": {
    "evm":  "0x9858effd232b4033e47d90003d41ec34ecaeda94",
    "tron": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
    "sol":  "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk",
    "sol_ata": {
      "USDT": "6pXYguUixhfdydHDMgsvSy3sAWjStL2eEYh4KeYJAUDk",
      "USDC": "5N3f1tj9v1vc5TUZ8S7mCAnVmjVKrfnzXWhxLaxyZAgt"
    }
  },
  "created_at": 1776613832602,
  "expires_at": 1776614432602,
  "updated_at": 1776613832602
}
```

- `status` 取值：`pending | partial | paid | overpaid | expired | cancelled`
- `evm` 地址 ETH / BSC / Arbitrum / Polygon / Base / Optimism **共用**
- `currency` / `chain` 会在用户付款后被填充
- 拿到 `pay_url` 后用户浏览器跳转过去即可（`302` redirect 或 `location.href = pay_url`）

### 错误响应

| HTTP | 说明 | 响应 body |
|---|---|---|
| `400` | 缺少必填字段 / 金额非法（非数字 / ≤0 / >1,000,000） | `{"error":"missing merchant_order_id"}` 等 |
| `401` | `Authorization` header 缺失或 `api_key` 无效 | `{"error":"unauthorized"}` |
| `402` | 手续费余额不足（新订单需 `fee_balance ≥ 阈值`） | `{"error":"insufficient fee balance","fee_balance":0.5,"required":2,"hint":"去商户中心充值"}` |
| `429` | 触发限流（每商户有速率限制） | `{"error":"rate limited"}` |

> **幂等**：用同一个 `merchant_order_id` 重复调用会返回**相同的订单**（不会重复创建，不消耗额外手续费）。网络重试安全。

---

## 6. 支付成功后用户去哪儿？

这块有两条**互相独立**的链路，职责分清楚就不会搞混：

| 链路 | 干什么 | 你需要做 |
|---|---|---|
| **后端：Webhook（`callback_url`）** | 我们的服务器 → 你的服务器。通知"这笔订单到账了"，触发你**发货 / 加金币 / 改订单状态**。 | 实现一个 HTTP 端点接收（见 §7）。完全**看不见浏览器**，用户关不关页都照发。 |
| **前端：`return_url`（浏览器跳转）** | 用户在收银台看到「收款成功」弹窗 → 点**「确定」** → 浏览器跳到 `return_url`。 | 创建订单时把 `return_url` 传上就行，其余全自动。不传则用户停在我们的「支付完成」静态页。 |

### 收银台是如何"消失"的？

你什么都不用做——支付成功那一刻收银台自己会显示成功弹窗。用户点「确定」后：

- 如果你传了 `return_url` → 浏览器 `location.href = return_url`（你的页面**替换掉我们的收银台**）
- 如果没传 → 用户停在我们的「支付完成」页，关闭或返回由用户自己决定

**用户跳去哪由你决定**（就是 `return_url` 这个字段）。我们不替你关闭商户站的 tab，也不替你做任何商户站业务逻辑。

### 常见疏漏

- 只设 `return_url` 不实现 webhook → 用户若不点"确定"就关页，你就**永远收不到发货通知**。**错的用法**。
- 只实现 webhook 不传 `return_url` → 用户会停在我们的"支付完成"页，略尴尬但不影响发货。可以接受。
- **两条都配才完整。**

### return_url 设计建议

- **带上订单 ID**：`https://site.com/order/paid?id=ORD-20260419-001`
- **不要把它当发货触发器**：用户可能直接关浏览器不点确定；也可能复制 URL 分享。**发货逻辑永远以 webhook 为准**。
- **`return_url` 到达 ≠ 订单已发货**：落地页建议再查一次你自己的 DB 显示真实订单状态。
- **iframe 嵌入场景（进阶）**：如果把 `pay_url` 嵌进 iframe，可监听 `postMessage` 代替 `return_url`：

```js
window.addEventListener("message", (e) => {
  if (e.origin !== "https://cryptocashier.shop") return;
  if (e.data?.type === "crypto-payment-received") { /* 支付到账事件 */ }
  if (e.data?.type === "crypto-payment-close")    { /* 用户点确定，该关 iframe 了 */ }
});
```

---

## 7. 第 4 步 · 接收回调 Webhook

我们在订单状态变化时 `POST` 到你注册的 `callback_url`。

### 事件类型

| event | 触发时机 | 布尔标志 |
|---|---|---|
| `paid` | 累计收到金额**恰好等于**订单金额 | `fully_paid=true, overpaid=false, underpaid=false` |
| `overpaid` | 累计收到金额 **>** 订单金额（多付了） | `fully_paid=true, overpaid=true, underpaid=false` |
| `partial` | 收到部分款（累计 < 订单金额） | `fully_paid=false, overpaid=false, underpaid=true` |
| `expired` | 订单有效期（默认 10 分钟）结束仍未足额——**包括用户完全没付款的情况**。由服务端 cron 每分钟扫描触发，**不依赖用户是否打开过收银台** | `fully_paid=false, underpaid=true` |
| `forwarded` | 款已从一次性收款地址**归集转账到你的提现地址**（通常在 `paid` 之后 30 秒–数分钟内到达） | `fully_paid=true`，带 `forward_tx` / `forwarded_to` / `token_contract` / `forward_amount_raw` / `verify` 等链上校验字段 |

**使用策略**：
- **收到 `paid` 或 `overpaid` 就直接发货**（这时钱已上链确认，归集是我们内部的事）
- `forwarded` 作"入账确认"——**强烈建议**用 payload 里的 `verify` 块独立链上校验一次（见 §7.x）
- `partial` → 记录后等后续到账或 `expired`
- `expired` → 取消订单（但注意下面的"晚到特性"）
- **不要等 `forwarded` 才发货**，否则用户体感等得太久
- **30 分钟红线**：`paid` 之后没在 30 分钟内收到 `forwarded`，视为异常，应禁用我方支付通道并联系我们

### ⚠ 重要：`expired` 之后仍可能收到 `paid`（特性，不是 bug）

即使订单已标 `expired`，如果用户在**过期后 24 小时内**把钱真的转到了链上（常见场景：用户超时后才点确认、钱包网络拥堵延迟确认、用户"我先把钱付了再联系客服"），我们的 monitor 仍会扫到并**补发 `paid` 回调**。

我们这么设计是为了**不让用户的钱石沉大海**——链上的钱我们一定会识别并归集给你。

**你需要做的：收到 `paid` 就发货，不管订单之前是不是 `expired`。** 简单适配一下，彻底避免"用户付了款但我这边已取消订单不发货"的争议。

```js
if (event === "paid" || event === "overpaid") {
  // 若订单之前被取消/过期，解锁后再发货
  if (order.status === "cancelled" || order.status === "expired") {
    order.status = "paid_late";   // 标记为"迟付"便于审计
  }
  if (!order.fulfilled) {
    await grantCredits(order.uid, p.amount_received);
    await markFulfilled(order);
  }
}
```

**24 小时后的付款我们不再识别**（由 `LATE_WINDOW_MS` 控制）。超过 24h 要处理请通过[用户反馈接口](#8-用户反馈接口可选--强烈建议配)走人工流程。

### 请求 Headers

| Header | 值 / 说明 |
|---|---|
| `Content-Type` | `application/json` |
| `X-Signature` | `HMAC-SHA256(callback_secret, raw_body)` — 十六进制小写 |
| `X-Signature-Alg` | 固定 `HMAC-SHA256` |
| `X-Timestamp` | 毫秒时间戳（等于 payload 里的 `ts`）。建议拒绝 `|now - X-Timestamp| > 5 分钟` 的请求（防重放） |
| `X-Nonce` | 16 字节十六进制随机串（等于 payload 里的 `nonce`）。可缓存近期 nonce 几分钟，拒重复 |

### Payload（通用字段，`paid` / `overpaid` / `partial` / `expired` / `forwarded` 都有）

```json
{
  "event":             "paid",
  "order_id":          "f6c8a97b-...",
  "merchant_order_id": "ORD-20260419-001",
  "uid":               "USER_12345",
  "amount_requested":  "9.99",
  "amount_received":   "9.99",
  "currency":          "USDT",
  "chain":             "trc20",
  "fully_paid":        true,
  "overpaid":          false,
  "underpaid":         false,
  "latest_tx": {
    "tx":       "0xabc...",
    "chain":    "trc20",
    "currency": "USDT",
    "from":     "TXxxxxxxxxxx",
    "amount":   "9.99"
  },
  "created_at": 1776613832602,
  "expires_at": 1776614432602,
  "ts":         1776613890123,
  "nonce":      "a1b2c3d4e5f6..."
}
```

字段说明：
- `amount_requested` / `amount_received` 是**字符串**（避免浮点精度丢失）
- `latest_tx` 在 `expired`（用户没付）时为 `null`
- `ts` 毫秒时间戳 = `X-Timestamp` header 的值
- `nonce` 16 字节十六进制 = `X-Nonce` header 的值

### `forwarded` 事件的额外字段

款已归集到你的提现地址时，除通用字段外还会带**完整的链上校验信息**，方便你**不依赖我们的服务**独立去链上核对这笔钱真的到了：

```json
{
  "event":            "forwarded",
  "forward_tx":       "0xabc123...",
  "forward_from":     "0x175fa10f95ec430d1ff91eedd3adcd3fafd91081",
  "forwarded_to":     "0xb11272...",
  "forward_amount":   "0.1",
  "forward_amount_raw": "100000000000000000",
  "forward_currency": "USDT",
  "forward_chain":    "bsc",
  "token_contract":   "0x55d398326f99059ff775485246999027b3197955",
  "token_decimals":   18,
  "explorer_url":     "https://bscscan.com/tx/0xabc123...",
  "forward_delay_ms": 45000,
  "paid_ts":          1776850000000,
  "forwarded_ts":     1776850045000,
  "verify": {
    "chain_type": "evm",
    "rpc": "https://bsc-rpc.publicnode.com",
    "method": "eth_getTransactionReceipt",
    "params": ["0xabc123..."],
    "curl": "curl -s -X POST https://bsc-rpc.publicnode.com ...",
    "expect": {
      "result.status": "0x1",
      "result.logs[i].address": "0x55d398326f99059ff775485246999027b3197955",
      "result.logs[i].topics[0]": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      "result.logs[i].topics[2]": "0x000000000000000000000000b11272...",
      "BigInt(result.logs[i].data)": "100000000000000000"
    }
  }
}
```

字段解释：
- `forward_tx` 我们发给你的归集 tx hash
- `forward_from` 派生地址（我们给用户的那个一次性收款地址，钱从这里出去）
- `forwarded_to` 你配置的提现地址（钱最终到这里）
- `forward_amount_raw` 链上真实数值（字符串形式的 BigInt，不带小数点）
- `token_contract` 代币合约地址（用来识别是 USDT 还是 USDC）
- `verify` **完全自包含的校验指令**——把它 copy 下来 curl 一下就能独立验证

---

### 🔒 §7.x 独立链上校验（一行代码，强烈建议高价值商户接入）

#### 为什么要自己校验

我们的 webhook 有 HMAC 签名保证**来源可信**，但**最终可信的只有链上数据**。假设极端情况——我们的服务器被攻破、内部人员作恶、bug 误判——攻击者可能伪造 `forwarded` webhook 让你误以为收到钱。链上校验是你的**最后防线**。

#### 一个函数搞定：`verifyForwardOnChain`

我们提供一个**零依赖、Node/浏览器通用、覆盖 8 条链** 的校验函数。下载地址：

**👉 https://cryptocashier.shop/verify-forward.cjs**

把它 copy 到你项目里（或 `wget`、`curl -O`），然后一行调用：

```js
const { verifyForwardOnChain } = require("./verify-forward.cjs");

// =============================================
// ← 改成你自己的提现地址。没用到的链可以删掉或留空字符串。
// =============================================
const MY_PAYOUTS = {
  bsc:     "0xYourEVMWallet",                 // 0x 开头 42 字符（大小写无关）
  erc20:   "0xYourEVMWallet",                 // Ethereum 主网
  arb:     "0xYourEVMWallet",                 // Arbitrum
  polygon: "0xYourEVMWallet",                 // Polygon
  base:    "0xYourEVMWallet",                 // Base
  op:      "0xYourEVMWallet",                 // Optimism
  trc20:   "TYourTRONWallet",                 // T 开头 34 字符（Base58，大小写敏感）
  sol:     "YourSolanaWalletBase58Address",   // 32-44 字符 Base58，不带 0x / T 前缀
};

// 在你的 webhook handler 里（已验过 HMAC 签名之后）：
if (payload.event === "forwarded") {
  const result = await verifyForwardOnChain({
    payload,                                  // 我们发给你的完整 JSON
    myPayouts: MY_PAYOUTS,                    // ← 按链自动匹配（推荐）
    expectedAmountRaw: order.expected_raw,    // ← 你订单上记录的应付金额（选填但推荐）
  });

  if (result.ok) {
    console.log("归集已链上验证", result.onChain);
    await markOrderVerified(order.id);
  } else {
    console.error("归集校验失败:", result.reason);
    await disableCryptoGateway();             // ← 立即禁用我方支付通道
    await alertOps(payload, result.reason);
  }
}
```

返回 `{ ok: true/false, reason, onChain? }`。`ok=false` 时 `reason` 会写清楚哪不对（地址被篡改 / tx 不存在 / tx 回滚 / 金额不对 等）。

**ESM 项目** 用 `createRequire` 桥接加载；**浏览器** 用 `<script>` 引入即可（函数挂到 `globalThis`）。

```js
// ESM 桥接
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { verifyForwardOnChain } = require("./verify-forward.cjs");
```

#### 这只是参考实现

> **你也可以用自己熟悉的方式验证**——web3.js / ethers.js / tronweb / @solana/web3.js 调 RPC、走你信任的 block explorer，都行。核心三步：**① `payload.forwarded_to` 是你自己的地址 · ② tx 链上确认成功 · ③ log 里 token 合约 + 接收方 + 金额 对得上**。我们的函数只是把这三步写好了。

#### 💡 30 分钟兜底保护

即使没收到 `forwarded`，我们内部也会在 30 分钟内把钱打给你。建议收到 `paid` 后启定时器，30 分钟内仍未验证成功则告警并禁用通道：

```js
if (event === "paid") {
  setTimeout(async () => {
    const fresh = await loadOrder(order.id);
    if (!fresh.forwarded_verified_at) await disableCryptoGateway();
  }, 30 * 60_000);
}
```

---

### §7.y 手动校验详解（附录 · 可跳过）

> 💡 用上面的 `verify-forward.cjs` 已经够了，这一节是给想用其他语言（Python/Go/PHP）或自己实现的商户作参考。

<details>
<summary>▸ EVM 链（BSC / Ethereum / Arbitrum / Polygon / Base / Optimism）</summary>

**RPC 调用**：
```bash
curl -s -X POST https://bsc-rpc.publicnode.com \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<forward_tx>"]}'
```

**5 条必须全部成立的规则**：

| 要求 | 怎么查 | 值对照 |
|---|---|---|
| ① 交易成功 | `result.status` | `"0x1"`（`"0x0"` = 失败） |
| ② 来自 USDT/USDC 合约 | `result.logs[i].address` | == `token_contract`（见合约表） |
| ③ 是 Transfer 事件 | `result.logs[i].topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` |
| ④ 钱到了你地址 | `result.logs[i].topics[2]` | `0x000000000000000000000000` + 你地址（去 0x 前缀） |
| ⑤ 金额正确 | `BigInt(result.logs[i].data)` | == `forward_amount_raw` |

</details>

<details>
<summary>▸ TRON（TRC-20）</summary>

**RPC 调用**：
```bash
curl -s -X POST https://api.trongrid.io/wallet/gettransactioninfobyid \
  -H 'Content-Type: application/json' \
  -d '{"value":"<forward_tx>"}'
```

**规则**：
- `receipt.result == "SUCCESS"`
- `log[i].address` 的十六进制对应 USDT 合约（`TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` 解码后 hex）
- `log[i].topics[0] == "ddf252ad...523b3ef"`（无 0x 前缀）
- `log[i].topics[2]` 末尾 40 hex 是你 TRON 地址 Base58→hex 去掉 `41` 前缀的结果
- `BigInt("0x" + log[i].data) == forward_amount_raw`

</details>

<details>
<summary>▸ Solana（SPL）</summary>

**RPC 调用**：
```bash
curl -s -X POST https://solana.publicnode.com \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"getTransaction","params":["<forward_tx>",{"encoding":"jsonParsed","maxSupportedTransactionVersion":0}]}'
```

**规则**：
- `result.meta.err == null`
- `result.meta.postTokenBalances` 里有一条 `owner == forwarded_to && mint == token_contract`
- 同一 `accountIndex` 下 `post - pre == forward_amount_raw`

</details>

<details>
<summary>▸ 合约 / RPC 对照表</summary>

| 链 id | 链名 | USDT 合约 | USDC 合约 | RPC |
|---|---|---|---|---|
| `bsc` | BSC | `0x55d398...b3197955` | `0x8ac76a...cd580d` | https://bsc-rpc.publicnode.com |
| `erc20` | Ethereum | `0xdac17f...d831ec7` | `0xa0b869...6eb48` | https://ethereum-rpc.publicnode.com |
| `arb` | Arbitrum | `0xfd086b...fcbb9` | `0xaf88d0...e5831` | https://arbitrum-one-rpc.publicnode.com |
| `polygon` | Polygon | `0xc2132d...58e8f` | `0x3c499c...c3359` | https://polygon-bor-rpc.publicnode.com |
| `base` | Base | `0xfde4c9...99bb2` | `0x833589...a02913` | https://base-rpc.publicnode.com |
| `op` | Optimism | `0x94b008...e58e58` | `0x0b2c63...97ff85` | https://optimism-rpc.publicnode.com |
| `trc20` | TRON | `TR7NHq...gjLj6t` | `TEkxiT...66rdz8` | https://api.trongrid.io |
| `sol` | Solana | `Es9vMF...BenwNYB` | `EPjFWd...wyTDt1v` | https://solana.publicnode.com |

</details>

---

#### 主动对账（不依赖 webhook）

想定期核对你的 payout 地址有没有漏款？查最近 30 分钟的 USDT 收入：

```bash
# BSC 示例
curl -s -X POST https://bsc-rpc.publicnode.com \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc":"2.0","id":1,"method":"eth_getLogs",
    "params":[{
      "fromBlock":"latest-1500",
      "toBlock":"latest",
      "address":"0x55d398326f99059ff775485246999027b3197955",
      "topics":[
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        null,
        "0x000000000000000000000000<你的地址去掉0x>"
      ]
    }]
  }'
```

返回的每条 log 里 `transactionHash` 都能跟我们发给你的 `forward_tx` 对号入座。

---

## Webhook 接收端完整示例

你的服务器需要**挂一个 HTTP POST 路由**来接收我们发的 webhook。下面是三种主流后端语言的完整实现（HMAC 验签 + 幂等去重 + 事件分发 + 入账/发货逻辑骨架），**直接抄走改 `CALLBACK_SECRET` 和业务函数即可**。选你项目用的那一份，剩下的忽略。

### Node.js + Express

```js
import express from "express";
import crypto  from "crypto";

const CALLBACK_SECRET = process.env.CALLBACK_SECRET;
const app = express();

// 必须用 raw body，否则 HMAC 算出的签名会因字节差异不一致
app.post("/api/crypto-webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const rawBody = req.body;                          // Buffer
    const sig = req.headers["x-signature"];
    const ts  = Number(req.headers["x-timestamp"] || 0);

    // 1. 防重放：拒绝时间戳偏差 > 5 分钟的请求
    if (Math.abs(Date.now() - ts) > 5 * 60_000) {
      return res.sendStatus(401);
    }

    // 2. 验签：用 timingSafeEqual 防时序攻击
    const expected = crypto
      .createHmac("sha256", CALLBACK_SECRET)
      .update(rawBody)
      .digest("hex");
    if (!sig || sig.length !== expected.length ||
        !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.sendStatus(401);
    }

    const p = JSON.parse(rawBody.toString());

    // 3. 幂等：按 merchant_order_id + nonce 去重
    const order = await findOrder(p.merchant_order_id);
    if (!order) return res.sendStatus(404);
    if (order.fulfilled) return res.json({ ok: true, dup: true });

    // 4. 分发：收到 paid/overpaid 就直接发货，不用等 forwarded
    if (p.event === "paid" || p.event === "overpaid") {
      // 注意：订单可能之前被标为 expired/cancelled（用户超时后才付款），
      // 此时仍应发货——链上的钱已真实到账，24h 内的迟付我们照常识别。
      if (order.status === "expired" || order.status === "cancelled") {
        await unCancelOrder(order);   // 解锁，标记为迟付
      }
      await grantCredits(p.uid, p.amount_received);
      await markFulfilled(order);
    } else if (p.event === "partial") {
      const ratio = Number(p.amount_received) / Number(p.amount_requested);
      await grantCredits(p.uid, ratio * order.credits);
    } else if (p.event === "expired") {
      await cancelOrder(order);
    } else if (p.event === "forwarded") {
      // 审计用：款已到你的提现地址。业务发货应在 paid 时已完成。
      await recordForwarded(order, p.forward_tx, p.forwarded_to);
    }

    res.json({ ok: true });
  }
);
```

### Python + Flask

```python
import os, hmac, hashlib, time
from flask import Flask, request, jsonify

CALLBACK_SECRET = os.environ["CALLBACK_SECRET"].encode()
app = Flask(__name__)

@app.post("/api/crypto-webhook")
def webhook():
    raw = request.get_data()                                           # bytes
    sig = request.headers.get("X-Signature", "")
    ts  = int(request.headers.get("X-Timestamp", "0"))

    # 1. 防重放
    if abs(int(time.time() * 1000) - ts) > 5 * 60_000:
        return ("", 401)

    # 2. 验签（timing-safe）
    expected = hmac.new(CALLBACK_SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        return ("", 401)

    p = request.get_json()

    # 3. 幂等
    order = find_order(p["merchant_order_id"])
    if not order:
        return ("", 404)
    if order["fulfilled"]:
        return jsonify(ok=True, dup=True)

    # 4. 分发
    event = p["event"]
    if event in ("paid", "overpaid"):
        grant_credits(p["uid"], p["amount_received"])
        mark_fulfilled(order)
    elif event == "partial":
        ratio = float(p["amount_received"]) / float(p["amount_requested"])
        grant_credits(p["uid"], ratio * order["credits"])
    elif event == "expired":
        cancel_order(order)
    elif event == "forwarded":
        record_forwarded(order, p["forward_tx"], p["forwarded_to"])

    return jsonify(ok=True)
```

### PHP

```php
<?php
$secret = getenv("CALLBACK_SECRET");
$raw    = file_get_contents("php://input");
$sig    = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
$ts     = (int)($_SERVER["HTTP_X_TIMESTAMP"] ?? 0);

// 1. 防重放
if (abs((int)(microtime(true) * 1000) - $ts) > 5 * 60_000) {
    http_response_code(401); exit;
}

// 2. 验签
$expected = hash_hmac("sha256", $raw, $secret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401); exit;
}

$p = json_decode($raw, true);

// 3. 幂等
$order = findOrder($p["merchant_order_id"]);
if (!$order) { http_response_code(404); exit; }
if ($order["fulfilled"]) { echo json_encode(["ok" => true, "dup" => true]); exit; }

// 4. 分发
$event = $p["event"];
if ($event === "paid" || $event === "overpaid") {
    grantCredits($p["uid"], $p["amount_received"]);
    markFulfilled($order);
} elseif ($event === "partial") {
    $ratio = (float)$p["amount_received"] / (float)$p["amount_requested"];
    grantCredits($p["uid"], $ratio * $order["credits"]);
} elseif ($event === "expired") {
    cancelOrder($order);
} elseif ($event === "forwarded") {
    recordForwarded($order, $p["forward_tx"], $p["forwarded_to"]);
}

echo json_encode(["ok" => true]);
```

### 重要约定

1. **必须在 15 秒内返回 2xx**。否则我们判定失败并重试。
2. **重试策略**：1m / 5m / 30m / 2h / 8h / 24h / 48h（共 8 次）。48h 后放弃，你可以通过查询接口自行核对。
3. **回调可能重复**（网络抖动 / 我方重启）。你必须按 `merchant_order_id` + 是否已处理做**幂等**。
4. **`partial` 事件可能收到多次**（用户分期付款）。每次 `amount_received` 会累加。
5. **状态机**：`pending → partial → paid/overpaid/expired`；`paid → forwarded`。

---

## 8. 用户反馈接口（可选 · 强烈建议配）

### 是什么

当用户付了款但在收银台没看到到账状态、点「已支付但没反应」自助查询**仍无结果**时，收银台会显示「上传反馈」按钮。用户填入付款钱包地址 + 可选截图 + 留言，提交后我们 POST 到你指定的反馈接收端。

### 为什么重要

用户卡单几乎总是诊断性问题（选错链、给错地址、写错数量、网络延迟）。**有反馈接口你能第一时间拿到钱包地址和截图**，排查后要么补发、要么退款、要么帮用户正确操作。没有它，用户只能关页消失——你甚至不知道丢了单。

### 路由

- 若在商户中心配了 `feedback_url` → POST 到该地址
- 没配 → POST 到 `callback_url`（和支付回调同一端点，按 `event: "feedback"` 分流处理）

### 请求

```
POST <你的 feedback_url 或 callback_url>
```

**Headers**（与支付回调完全一致）：

| Header | 值 |
|---|---|
| `Content-Type` | `application/json` |
| `X-Signature` | `HMAC-SHA256(callback_secret, raw_body)` — 十六进制小写 |
| `X-Signature-Alg` | `HMAC-SHA256` |
| `X-Timestamp` | 毫秒时间戳（= payload `ts`） |
| `X-Nonce` | 16 字节十六进制（= payload `nonce`） |

### Payload

```json
{
  "event":             "feedback",
  "order_id":          "f6c8a97b-...",
  "merchant_order_id": "ORD-20260419-001",
  "uid":               "USER_12345",
  "topic":             "no-response",
  "user_address":      "0xe13428d7...",
  "chain":             "bsc",
  "currency":          "USDT",
  "amount_requested":  "0.16",
  "suggestion":        "我的钱包显示已扣款...",
  "screenshots":       ["data:image/png;base64,..."],
  "ts":                1745000000000,
  "nonce":             "a1b2c3d4e5f6..."
}
```

字段说明：

- `topic`：`no-response | wrong-network | suggest`
- `user_address`：用户自述的付款钱包地址（`topic=suggest` 时可能为 `null`）
- `chain`：用户**声称**付到的链（`wrong-network` 场景下可能 ≠ 订单链）
- `suggestion`：用户留言（`topic=suggest` 时必填）
- `screenshots`：base64 data URI 数组，最多 6 张，每张一般 < 1 MB

### topic 详解

| topic | 触发场景 | 关键字段 |
|---|---|---|
| `no-response` | 用户点「已支付但没反应」，填入钱包地址后服务端扫链仍未找到 | `user_address` / `chain`（= 订单链）/ `screenshots` |
| `wrong-network` | 用户点「支付时选错了网络」，声明实际转到了哪条链 | `user_address` / `chain`（= 用户**实际**转的链，可能 ≠ 订单链）/ `screenshots` |
| `suggest` | 用户主动提交的反馈 / 建议（非卡单相关） | `suggestion`（文字）/ `screenshots`（可选） |

### 响应

**15 秒内返回 2xx** 即可。失败按 1m/5m/30m/2h/8h/24h/48h 指数退避重试 8 次（与支付回调一致）。

### 最简接收端实现（Node.js）

```js
// 若单独配了 feedback_url 就直接挂这个路由；
// 若复用 callback_url，就在原 webhook handler 里按 p.event === "feedback" 分流。
app.post("/api/crypto-feedback",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    // 验签流程同支付回调（略，见 §7 完整示例）
    const p = JSON.parse(req.body.toString());
    if (p.event !== "feedback") return res.sendStatus(400);

    // 1. 截图建议落到对象存储后存 URL，别直接塞 DB
    const screenshotUrls = [];
    for (const dataUri of (p.screenshots || [])) {
      screenshotUrls.push(await uploadToS3(dataUri));
    }
    await db.insertTicket({
      order_id:    p.merchant_order_id,
      uid:         p.uid,
      topic:       p.topic,
      user_addr:   p.user_address,
      chain:       p.chain,
      note:        p.suggestion,
      screenshots: screenshotUrls,
      created_at:  p.ts,
    });

    // 2. 通知客服 / 运营
    await notifySupport(`用户 ${p.uid} 订单 ${p.merchant_order_id} 反馈：${p.topic}`);

    res.json({ ok: true });
  }
);
```

### 处理建议

1. 把 base64 截图上传到对象存储（OSS / S3 / R2）后只存 URL，别直接塞 DB——单次反馈 base64 可能 > 1 MB
2. 建工单 / 客服系统，**24 小时内人工回复用户**（可通过订单关联的 uid 反查联系方式）
3. 常见处理：
   - 确认链上有款但未识别 → 联系网关管理员提供 tx hash 人工入账
   - 链上无款 → 引导用户重新支付或退款
   - 用户转错链（`wrong-network`）→ 一般网关已自动识别；如果没有，把 tx hash 给网关管理员

---

## 9. 查询订单 API

```
GET /api/v1/orders/:id
```

`id` 是创建订单返回的我们的 UUID。

```bash
curl https://cryptocashier.shop/api/v1/orders/f6c8a97b-4569-4e92-a8fb-7f15b1c5a840 \
  -H "Authorization: Bearer pk_YOUR_API_KEY"
```

响应同 §5，额外带 `payments` 数组（该订单所有到账记录）。

**什么时候用这个接口？** 正常情况下你是被动接收 webhook，不需要主动查。但可用于：① 订单列表页显示最新状态；② 回调失败后的兜底查询；③ 客服手动核对。

---

## 10. 支持的链（chain id）

回调 `chain` 字段的取值：

| chain id | 链 | 协议 | USDT 合约 / mint | USDC 合约 / mint | 精度 |
|---|---|---|---|---|---|
| `trc20` | TRON | TRC-20 | `TR7N...j6t` | `TEkx...dz8` | 6 |
| `erc20` | Ethereum | ERC-20 | `0xdac1...1ec7` | `0xa0b8...eb48` | 6 |
| `sol` | Solana | SPL | `Es9v...NYB` | `EPjF...Dt1v` | 6 |
| `bsc` | BSC | BEP-20 | `0x55d3...7955` | `0x8ac7...580d` | **18** |
| `arb` | Arbitrum | Arbitrum One | `0xfd08...cbb9` | `0xaf88...5831` | 6 |
| `polygon` | Polygon | PoS | `0xc213...e8f` | `0x3c49...3359` | 6 |
| `base` | Base | Coinbase L2 | `0xfde4...9bb2` | `0x8335...2913` | 6 |
| `op` | Optimism | OP Mainnet | `0x94b0...8e58` | `0x0b2c...ff85` | 6 |

> **BSC 精度注意**：BSC 上 USDT 和 USDC 都是 **18 位小数**（Binance-Peg 版本），其它链都是 6 位。前端 / 网关已处理，服务端如果从原始 raw 值解析，注意按 chain 区分精度。

---

## 11. 第 5 步 · 联调 / 演示模式

> **联调演示默认完全关闭**——只有商户中心的「🎲 一键测试」按钮创建的订单才会启用模拟面板。真实订单看不到任何演示按钮，上线无需任何改动。

### 唯一开启方式

1. 登录 <https://cryptocashier.shop/merchant.html>
2. （可选）先在「🔔 回调 URL」填上 <https://webhook.site> 的临时 URL，直接看我们发来的 webhook JSON
3. 点「🎲 生成测试订单并打开收银台」
4. 新窗口打开的收银台底部有 3 个模拟按钮：
    - **模拟足额支付** → 触发 `event: "paid"` 回调
    - **模拟欠费（少 0.01）** → 触发 `event: "partial"` + 欠费弹框
    - **模拟多付（+0.5）** → 触发 `event: "overpaid"`

测试订单的 `merchant_order_id` 以 `test_` 开头，数据库 `kind='test'`。所有模拟数据隔离于真实订单。

### 安全设计

真实订单（`kind='normal'`）的支付状态**只接受后台链上扫描确认**，绝不接受来自浏览器前端的"已支付"声明。即使攻击者知道某真实订单的 ID 并访问收银台页面，也**无法伪造 webhook**——所有模拟按钮对真实订单都是空操作。

这意味着：
- ✅ 真实订单绝无"白拿商品"风险
- ✅ 真实订单的 webhook 只会在链上真转账后才触发（延迟 ≤ 1 分钟）
- ✅ 你的 callback 接收端无需额外校验，只要 HMAC 签名对就可以信任

---

## 12. 上线前检查清单

商户接入侧只需确认以下几项，部署与链监听由网关负责：

- [ ] 商户中心已保存：**回调 URL** + **提现地址**（各链的接收地址，用于自动归集）
- [ ] 妥善保存 `api_key` 和 `callback_secret`（两者仅在注册时出现一次）
- [ ] 回调接口：**HMAC-SHA256 验签**通过才处理（用 `callback_secret` 校验 `X-Signature`）
- [ ] 回调接口：**拒绝 `|now - X-Timestamp| > 5 分钟` 的请求**（防重放）
- [ ] 回调接口：按 `merchant_order_id` / `nonce` **幂等处理**（同一事件可能重复到达）
- [ ] 回调接口：**15 秒内返回 2xx**（否则我们按 1m/5m/30m/2h/8h/24h/48h 重试，共 8 次）
- [ ] 回调路径走 **HTTPS** 且公网可达（从我们服务器能 DNS 解析并建立 TCP 连接）
- [ ] 用商户中心「🎲 一键测试」先跑一遍联调，确认 webhook 到达、验签通过、发货链路可达
- [ ] 正式上线前用真实链做一笔小额支付（至少 TRON USDT 一笔），确认归集转账到你的提现地址成功

---

## 13. 常见问题 FAQ

### Q：用户付了钱但页面一直没反应？

我们后台每分钟扫链兜底。正常用户付款后 30–90 秒内收银台页面会自动变「收款成功」。若仍未识别，用户可在收银台点「已支付但没反应」输入付款钱包地址主动触发一次服务端核查（1 小时内的转账都能命中）；仍查不到可上传截图由你人工跟进。

### Q：支付时用户选错了网络怎么办？

收银台的「支付时选错了网络」自助流程会让用户选实际转的链 + 填地址 + 上传截图。服务端到目标链扫一遍，只要用户转的是 USDT/USDC，我们都能识别并入账，回调中的 `chain` 字段会反映实际到账的链。

### Q：回调你们重试几次？还不成功怎么办？

失败后按 1 分钟 / 5 分钟 / 30 分钟 / 2 小时 / 8 小时 / 24 小时 / 48 小时指数退避重试，共 **8 次**。全部失败后放弃并打标记。你可以随时用 `GET /api/v1/orders/:id` 查询最新状态做兜底核对。

### Q：同一订单的 webhook 会重复吗？

会。网络抖动 / 我方重启 / 分期付款都可能造成多次触达。**必须幂等**：按 `merchant_order_id`（或 `nonce`）去重。`partial → paid` 的跳转也可能让同一订单先后发出多个事件。

### Q：用户多付了会怎样？

触发 `event: "overpaid"` 回调，`amount_received` 是实际到账金额。多付的部分同样会自动归集到你的提现地址，发货不发货由你决定（推荐：足额以上均正常发货）。

### Q：订单已 `expired` 之后，又收到了 `paid` 回调，这是 bug 吗？

**不是 bug，是特性**。订单过期后 24 小时内，如果用户真的把钱转到了链上（可能超时才点确认、网络拥堵延迟、钱包审核慢等），我们会照常识别并触发 `paid` 回调——因为链上的钱我们一定要归集给你，不能让用户的钱石沉大海。

**你要做的**：收到 `paid` 就发货，哪怕订单之前是 `expired`。建议在 webhook 分发里加一个"若订单已取消/过期，先解锁再发货"的分支（见 §7 事件表下方「晚到特性」小节）。超过 24h 的付款我们不再识别——此时请走用户反馈接口由客服人工处理。

### Q：用户付款后刷新页面 / 关掉页面，会丢单吗？

不会。订单真相完全在服务端 DB + 链上，页面只是显示层。用户重新打开 `pay_url`（或你通过 `merchant_order_id` 重新生成订单）都会回到同一个支付状态，且**同一 `uid` 的收款地址保持不变**。

### Q：能自定义支持更多链吗？

能。需联系网关管理员在 `server/chains.js` 里追加链配置（RPC 池 + 代币合约 + 精度）并重新部署。TRON / EVM 系列加链成本很低；Solana 以外的非 EVM 链需定制扫链逻辑。

### Q：归集转账失败怎么办？

监控会告警。常见原因：目标提现地址格式错 / 某链 gas 费（TRON Energy / EVM 原生币）不足 / RPC 暂时异常。网关会自动重试并在管理员后台提示；极端情况可人工干预（归集失败**不会影响 webhook 接收**，支付已入账的订单依然会正常回调给你）。

### Q：为什么用字符串表示金额而不是数字？

避免 JavaScript / JSON 浮点精度问题。`9.99` 在 IEEE 754 里可能变成 `9.99000000000000...`，影响比对和求和。用字符串可 100% 保真，做数值运算时按需 `Number()` / `BigInt` / `Decimal` 转换。

### Q：地址池会用完吗？

不会。我们采用 **BIP32 HD 派生**，每个 `(merchant_id, uid)` 组合派生一个固定的 `hd_index`，对应 EVM / TRON / Solana 三套地址。派生空间为 $2^{31}$ 个，对任何商户量级都绰绰有余。同一 `uid` 永远得到同一组地址，便于对账和用户复购。

---

## 附录 A · 商户端数据模型建议

```sql
CREATE TABLE crypto_orders (
  id                   VARCHAR(64) PRIMARY KEY,      -- 你的 merchant_order_id
  gateway_order_id     VARCHAR(64) UNIQUE,           -- 我们的 UUID (response.id)
  uid                  VARCHAR(64) NOT NULL,
  amount_usd           DECIMAL(20,8) NOT NULL,
  amount_received      DECIMAL(20,8) DEFAULT 0,
  currency             VARCHAR(16),                  -- USDT / USDC
  chain                VARCHAR(32),
  status               VARCHAR(16) DEFAULT 'pending',-- pending|paid|partial|expired|forwarded
  fulfilled            BOOLEAN DEFAULT FALSE,        -- 发货幂等锁
  fulfilled_at         BIGINT,
  last_webhook_nonce   VARCHAR(64),                  -- 防重放
  created_at           BIGINT NOT NULL,
  updated_at           BIGINT NOT NULL
);

CREATE TABLE crypto_webhook_log (
  id                   SERIAL PRIMARY KEY,
  order_id             VARCHAR(64) REFERENCES crypto_orders(id),
  event                VARCHAR(16) NOT NULL,
  nonce                VARCHAR(64),
  payload              JSONB,
  received_at          BIGINT NOT NULL,
  UNIQUE (order_id, nonce)                          -- 幂等 key
);
```

## 附录 B · 状态流转图

```
          ┌─────────────┐
          │   pending   │  创建订单，未收到任何款
          └──────┬──────┘
                 │ 收到款但未足额
                 ▼
          ┌─────────────┐
          │   partial   │──→ 可能多次（分期付款）
          └──────┬──────┘
                 │ 累计到达订单金额
          ┌──────┴──────┐
          │             │
          ▼             ▼
      ┌──────┐    ┌──────────┐
      │ paid │    │ overpaid │
      └──┬───┘    └────┬─────┘
         └──────┬──────┘
                ▼
         ┌─────────────┐
         │  forwarded  │  款已归集到你的提现地址
         └─────────────┘

    ┌─────────────┐
    │   pending   │
    └──────┬──────┘
           │ 10 分钟内未足额
           ▼
    ┌─────────────┐
    │   expired   │
    └─────────────┘
```

---

_文档版本 v2 · 托管式服务端 API 模式_
_有问题联系网关管理员_
_源文档在线版：<https://cryptocashier.shop/integration.html>_
