Files
qianyuandClaude Fable 5.1 0e0f73e2c7 多收款账号:API 动态添加币安账号,订单按账号隔离轮询与匹配
- 新增 accounts 表与接口:POST /api/v1/accounts(用只读接口试拉流水校验,失败 ERR_BINANCE)、
  GET /api/v1/accounts/{id}、POST .../verify、.../disable、.../enable;Secret 以 AES-256-GCM 加密存储,密钥由 API_AUTH_KEY 派生
- 订单增加 account_id(旧库自动补列,默认 default);唯一金额按账号分配;Matcher 按账号各自轮询、缓存与匹配,
  无待核销订单的账号不调币安接口;回填(claim)只在订单所属账号的流水里找
- 收银页按订单所属账号展示 UID / 收款链接 / 二维码;订单与回调体新增 account_id,订单响应新增 receive_link / receive_email
- Go / Python SDK 同步(AccountID、账号方法);协议文档 §2.6;e2e 覆盖多账号隔离、回填、停用与默认账号保护

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 09:39:48 +08:00

161 lines
7.5 KiB
Markdown
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.
# BinancePayTool 接入协议 v1
> 签名头与错误码沿用 `X-BPG-` / `BPG` 前缀,它们是协议稳定标识,与仓库名无关。
所有接口均为 HTTP + JSON(UTF-8)。金额一律为**字符串**十进制(如 `"10.0037"`),最多 8 位小数。
时间戳一律为**毫秒** Unix 时间。
## 1. 签名
商户与网关共享一个密钥 `API_AUTH_KEY`(下称 `secret`),同时用于「商户调用网关」与「网关回调商户」。
### 1.1 商户 → 网关(API 请求签名)
请求头:
| Header | 说明 |
|---|---|
| `X-BPG-Timestamp` | 当前毫秒时间戳;与服务器偏差超过 300 秒拒绝 |
| `X-BPG-Nonce` | 随机串(≥16 字符);5 分钟内不得重复 |
| `X-BPG-Signature` | 小写十六进制签名 |
签名串(`\n` 为换行符;`path` 只含路径不含查询串与域名;GET/无 body 时 body 视为空串):
```
string_to_sign = timestamp + "\n" + nonce + "\n" + METHOD + "\n" + path + "\n" + SHA256_HEX(body)
signature = HMAC_SHA256_HEX(secret, string_to_sign)
```
### 1.2 网关 → 商户(回调签名)
回调为 `POST callback_url`,`Content-Type: application/json`,请求头同上三个,签名串少 METHOD 与 path:
```
string_to_sign = timestamp + "\n" + nonce + "\n" + SHA256_HEX(body)
```
商户校验:时间戳偏差 ≤300 秒、签名一致(常数时间比较)。校验通过返回 HTTP 2xx 即视为成功;
否则网关按 `0s, 15s, 60s, 5m, 15m, 1h, 3h` 重试 7 次后放弃。**回调可能重复投递,商户须按
`order_id` 幂等处理。**
## 2. 接口
### 2.1 创建订单 `POST /api/v1/orders`
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `merchant_order_id` | string | 是 | 商户侧唯一订单号(≤64 字符);重复返回 `ERR_DUPLICATE` |
| `account_id` | string | 否 | 收款账号 ID(多账号模式,见 §2.6);缺省为网关 `config.env` 里的默认账号 `default` |
| `currency` | string | 是 | 币种,须在网关 `CURRENCIES` 白名单内(如 `USDT`)|
| `amount` | string | 是 | 基础金额,>0,小数位 ≤ 网关 `AMOUNT_DECIMALS` |
| `callback_url` | string | 否 | 支付结果回调地址 |
| `return_url` | string | 否 | 支付成功后收银页跳转地址(自动附加 `?order_id=&status=`)|
| `timeout` | int | 否 | 有效期秒数,120–86400,缺省用网关 `ORDER_TTL` |
响应 `data`:
| 字段 | 说明 |
|---|---|
| `order_id` | 网关订单号 |
| `account_id` | 收款账号 ID(默认账号为 `default`)|
| `merchant_order_id` | 原样返回 |
| `status` | `pending` |
| `currency` / `base_amount` / `pay_amount` | `pay_amount` 为**实际应付的唯一金额**,付款方必须严格按它支付 |
| `note_code` | 6 位备注码;付款方转账备注里带上它可加速/兜底确认(可选)|
| `pay_url` | 网关托管收银页地址,直接跳转即可 |
| `receive_uid` | 收款方币安 UID(即该订单所属账号的 UID)|
| `receive_link` / `receive_email` | 该账号配置的收款链接(二维码内容,可自建收银页:生成二维码、手机端唤起 App)与收款邮箱,未配置为空 |
| `created_at` / `expires_at` | 毫秒时间戳 |
### 2.2 查询订单 `GET /api/v1/orders/{order_id}`
响应 `data` 在 2.1 基础上增加:`actual_amount`、`paid_at`、`matched_by`(`amount`/`note`/`claim`)、
`binance_order_id`(币安 18 位订单编号)、`payer_id`(付款方 Pay 账户 ID)、`overpaid`(bool)。
也可用 `GET /api/v1/orders/by-merchant/{merchant_order_id}` 按商户单号查询。
### 2.3 关闭订单 `POST /api/v1/orders/{order_id}/close`
仅 `pending` 可关闭;成功后状态为 `closed`,唯一金额进入冷却。
### 2.4 收银页(免签名,公开)
- `GET /pay/{token}`:托管收银页(token 含在 `pay_url` 中)
- `GET /pay/{token}/status`:`{status, paid_at, return_url}`,页面轮询用
- `POST /pay/{token}/claim`:`{"binance_order_id":"18位数字"}`——「我已付款」回填入口,限流 5 次/分钟
### 2.5 响应外层
```json
{"code":"OK","data":{...}}
{"code":"ERR_AUTH","message":"..."}
```
`code` 取值:`OK`、`ERR_AUTH`、`ERR_PARAM`、`ERR_DUPLICATE`、`ERR_NOT_FOUND`、`ERR_STATE`、
`ERR_RATE_LIMIT`、`ERR_ALLOC`(唯一金额耗尽)、`ERR_BINANCE`(账号接口:币安校验失败)、`ERR_INTERNAL`。
### 2.6 收款账号(多账号模式)
网关默认只用 `config.env` 里的一个币安账号收款。以下接口可**动态添加更多收款账号**(如平台型商户
让每个入驻用户用自己的币安账户收款):每个账号提供自己的**只读** API Key/Secret 与 UID,网关按账号
分别轮询流水,订单通过 `account_id` 指定收款账号,不同账号的订单与流水严格隔离(唯一金额也按账号分配)。
| 接口 | 说明 |
|---|---|
| `POST /api/v1/accounts` | 添加账号。请求体:`api_key`、`api_secret`、`uid`(必填),`label`、`receive_link`(https 收款链接)、`receive_email`(可选)。网关会先用该 Key 试拉一条流水,失败返回 `ERR_BINANCE`(消息含币安原因,如 Key 无效 / IP 未白名单 / 地区限制)。同一 `api_key` 重复添加视为更新并重新启用 |
| `GET /api/v1/accounts/{id}` | 查询账号(`default` 可查) |
| `POST /api/v1/accounts/{id}/verify` | 立即校验该账号的 Key;失败返回 `ERR_BINANCE` |
| `POST /api/v1/accounts/{id}/disable` / `/enable` | 停用 / 启用;停用后不能再用它下单(`default` 不可停用)|
账号响应 `data`:`account_id`、`label`、`api_key_masked`、`uid`、`receive_link`、`receive_email`、
`status`(`active`/`disabled`)、`last_ok`(最近一次轮询成功时间,毫秒)、`last_err`(最近轮询错误)、`created_at`。
**永不返回** Key 与 Secret。
安全提示:Secret 以 AES-256-GCM 加密存储,密钥由 `API_AUTH_KEY` 派生——更换 `API_AUTH_KEY` 后已存账号需重新添加。
各账号的 Key 只需「允许读取」权限,IP 白名单填网关服务器出口 IP。
## 3. 回调体
```json
{
"event": "paid", // paid | underpaid | expired
"order_id": "…",
"account_id": "default", // 收款账号 ID
"merchant_order_id": "…",
"status": "paid",
"currency": "USDT",
"base_amount": "10",
"pay_amount": "10.0037",
"actual_amount": "10.0037",
"overpaid": false,
"matched_by": "amount",
"binance_order_id": "452021922068888888",
"binance_txn_id": "P_EXAMPLE1234567890",
"payer_id": "1160000000",
"paid_at": 1788325910559,
"timestamp": 1788325912000
}
```
`underpaid`:备注/回填命中但实付低于基础金额,需人工处理;`expired`:超时未支付。
## 4. 订单状态机
```
pending ──精确金额/备注码/回填命中──▶ paid
pending ──实付不足(备注/回填)──▶ underpaid
pending ──到期──▶ expired ──宽限期内精确金额到账/回填──▶ paid
pending ──商户关闭──▶ closed
```
## 5. 匹配规则(网关内部,供理解)
对每条**入账**流水(金额>0、币种在白名单、币安 `orderId` 未被消费过):
1. 金额与某待支付订单的 `pay_amount` 完全相等,且到账时间在订单窗口内 → 自动确认;
2. 否则备注含某订单 `note_code`(不区分大小写)→ 实付 ≥ 基础金额则确认,否则 `underpaid`;
3. 否则保留,等付款方在收银页回填币安订单编号(7 天内有效)。
同一笔币安流水只能核销一个订单(以币安 `orderId` 幂等)。