全系统唯一一个凭空产生会员余额的端点 —— 六道闸一道都不能省。
充值上报
POST /v1/deposits
x-idempotency-key: <UUID>
{
"external_member_id": "u_88123",
"asset": "USDT",
"amount": "100.00",
"reference": "<你的入金流水号>",
"network": "BSC",
"txid": "0x…",
"from_address": "0x…",
"to_address": "0x…"
}
你上报,我方记账。这是全系统唯一一个凭空产生会员余额的端点 ——所以它上面的每一道闸都不会因为「是你调的」而放松。
幂等:两把键,防的是两件事
| 防什么 | 窗口 | |
|---|---|---|
x-idempotency-key(请求头) | 网络重试 | 24 小时 |
reference(请求体,必填) | 同一笔入金被记两次 | 永久 |
reference 用你系统里那笔入金的唯一标识 —— 不是随机 UUID。
⚠ 超过 24 小时之后,只有
reference还拦得住。用随机
reference的表现是:一次隔天的补偿任务变成了第二笔入账,而会员的余额凭空多了一份。
命中幂等时
- HTTP 200(首次是 201)
replayed: true- 回显的是库里存着的那一份,不是你这次传的
⚠ 最后这条要紧。同一个
reference换一把x-idempotency-key重发、并且改了
txid,账本与订单行不会被改写(首次那份为准)。我方如果照你的入参回显,你会以为新 txid 被接受了 ——
而等到对账时才发现存的是另一串,那时已经无从分辨哪一串是真的。
所以:
replayed: true时请拿返回的链上字段去核对,别假设它等于你发的。
六道闸
| # | 闸 | 失败时 |
|---|---|---|
| ① | 签名 | 401 —— 不可关闭 |
| ② | 幂等(带你的流水号) | 200 + replayed: true |
| ③ | 单笔上限 | 400 limit_exceeded,limit_type: "single" |
| ④ | 资产白名单,认不出一律拒 | 400 asset_not_allowed |
| ⑤ | 照常过平台风控与准入 | 400 request_rejected |
| ⑥ | 全量审计 | — |
⚠ 闸③ 存在的理由不是限额,是信号:异常放大的单笔入金是「商户被攻破」
的第一个可观测信号。
⚠ 闸④ 是拒绝不是挂账。认不出的资产挂在某个待处理队列里,
意味着我方账上多了一笔来路不明的钱。
⚠ 闸⑤:上报入金不等于绕开风控。 会员被封禁 / 拉黑 / 冻结时,
这笔入金会被拒 —— 而不是先记上再说。
日累计上限撞了也是 limit_exceeded,limit_type: "daily"。
金额是字符串定点
"amount": "100.00" ✓
"amount": 100.00 ✗ JSON number
位数按该资产的 ledger_scale(用GET /v1/assets 查)。
⚠ 链上资产 18 位小数超过
2^53,用 number 解析即精度事故 ——而它不报错,只是静静地少几位。
链上溯源字段
network / txid / from_address / to_address 都是选填,但有一条约束:
⚠ 不传
network却传了另外三个中的任何一个 → 400。一条没有网络的 txid 在对账时定位不到任何东西。
长度上限:network ≤ 32、txid ≤ 128、地址 ≤ 200。network 与 asset 都会被转成大写。
强烈建议全传。出事的时候,这四个字段是唯一能把一笔账本记录对回链上的东西。
上报之后
- 会员余额立刻增加(这条线没有异步、没有中间态)
- 发
deposit.credited事件 - 你的托管余额相应增加 —— 跟托管核对对上