钱包(转账)
本平台使用转账钱包:每位玩家在本平台有一个游戏钱包,你的系统以 API 把金额转入、转出。游戏中的下注、派彩都在这个钱包内自动处理:
- 下注时本金立即从余额扣除(牛牛押翻倍时连同预扣一起扣,见牛牛的预扣)。
- 结算时派彩(含本金;牛牛另含退回的预扣)自动入账;结果修正或作废时自动补差额或退款。
- 所以任何时候的余额都是可用余额,未结算注单的本金不会被转出。
| 端点 | 用途 |
|---|---|
POST /wallet/deposit | 转入(你的系统 → 游戏钱包);玩家不存在时自动创建 |
POST /wallet/withdraw | 转出(游戏钱包 → 你的系统) |
GET /wallet/transfer?txnId= | 查询一笔转账的结果 |
GET /wallet/balance?username= | 查询目前的可用余额 |
转入与转出
json
{ "username": "alice", "txnId": "dep-20260924-000123", "amount": "1000", "currency": "TWD" }| 字段 | 说明 |
|---|---|
username | 玩家账号。转入时玩家不存在会以 currency 自动创建;转出时玩家必须存在 |
txnId | 你的转账单号,1–64 个字符(字母或数字与 _ . : -),作为幂等键 |
amount | 大于 0 的十进制字符串,最多 4 位小数,例如 "1000"、"0.5"。不要用浮点数 |
currency | 必须等于玩家的币种,否则回 CURRENCY_MISMATCH |
响应:
json
{ "ok": true, "data": { "txnId": "dep-20260924-000123", "status": "done", "balance": "2500.5", "duplicate": false } }- 转账是单阶段的:响应成功就是已经完成(
status一律是done),没有「处理中」或需要再确认的步骤。 balance是这笔转账完成当下的余额。- 响应成功的转账一定查得到:
GET /wallet/transfer?txnId=…&username=…。 - 转出金额大于可用余额时回
409 INSUFFICIENT_BALANCE,余额不变。
幂等:txnId 的规则
| 情况 | 结果 |
|---|---|
新的 txnId | 执行转账,duplicate: false |
同一个 txnId、同一位玩家、相同金额与方向 | 不会再执行,返回第一次的结果(balance 为那笔转账完成当下的余额)与 duplicate: true |
同一个 txnId,但金额或方向(转入/转出)不同 | 409 TXN_CONFLICT,余额不变 |
同一个 txnId 在最近两个月内已用在另一位玩家 | 409 TXN_CONFLICT,余额不变 |
- 请让
txnId在你的整个租户内永久唯一,例如dep-<你的交易编号>。 - 每一笔新的转账用新的
txnId;重试时则必须沿用同一个txnId与完全相同的内容。
超时与重试
网络超时、连接中断、收到 5xx 或 429 时,你无法确定转账是否已经执行。正确的处理方式:
- 送出前,先在你的数据库记下这笔转账(
txnId、金额、方向,状态「处理中」)。 - 调用
deposit/withdraw(建议超时 10 秒)。 - 2xx(包含
duplicate: true):标记完成,以响应的balance更新显示。 - 4xx(例如
INSUFFICIENT_BALANCE、TXN_CONFLICT、INVALID_AMOUNT):明确失败,标记失败;转出失败时把金额退回玩家在你系统的余额。 - 超时、网络错误、5xx、429:状态未知。以相同的
txnId、相同内容重送(每次换新的 nonce 与时间戳,退避例如 1、2、4、8 秒),直到得到 2xx 或 4xx。
js
// 以 sign.js 的 call()(见〈认证与签名〉)为例
async function transferWithRetry(kind, payload, auth) {
for (let attempt = 0; ; attempt++) {
try {
return await call('POST', `/api/tenant/v1/wallet/${kind}`, payload, auth); // 2xx:完成
} catch (err) {
const unknown = !err.status || err.status >= 500 || err.status === 429;
if (!unknown || attempt >= 8) throw err; // 4xx:明确失败;或交给人工/调度稍后再试
await new Promise((r) => setTimeout(r, Math.min(30_000, 1000 * 2 ** attempt)));
// 同一个 payload(同一个 txnId)重送
}
}
}也可以先用 GET /wallet/transfer?txnId=…&username=… 查询(请带 username):
- 200:转账已完成,响应包含方向、金额与完成后的余额。响应成功的转账一定查得到。
- 404
TXN_NOT_FOUND:这笔转账没有执行(或仍在处理中)。请以同一个txnId重送转账,直到得到明确结果;不要改用新的txnId。
绝对不要
超时后换一个新的 txnId 重送。如果第一次其实已经成功,玩家会被转两次。
查询转账
text
GET /api/tenant/v1/wallet/transfer?txnId=dep-20260924-000123&username=alicejson
{
"ok": true,
"data": {
"txnId": "dep-20260924-000123",
"username": "alice",
"dir": "in",
"amount": "1000",
"status": "done",
"balance": "2500.5",
"at": "2026-09-24T03:00:01.250Z"
}
}dir:in转入、out转出、adjustConsole 的人工上下分。username选填但建议带:转账一完成,玩家钱包就有记录,租户流水的副本会在稍后送达;带username时两边都会查,所以成功的转账一定查得到。- 租户流水的查询范围是当月与前 3 个月(依 UTC 月份)。更早的转账请以 Console 注单与账务 → 转账记录 或导出核对。
查询余额
text
GET /api/tenant/v1/wallet/balance?username=alicejson
{ "ok": true, "data": { "balance": "2500.5", "currency": "TWD" } }余额是可用余额,已扣除进行中注单的本金(牛牛翻倍另扣预扣)。要转出玩家全部的钱时,先查余额再以这个金额转出;如果玩家在两次调用之间又下注,转出会回 INSUFFICIENT_BALANCE,重新查询后再试即可。
金额与币种
- 所有金额都是十进制字符串,最多 4 位小数;响应会去掉小数尾端的 0(
"100"、"0.95"、"1000.5")。 - 请以字符串(或十进制类型)处理金额,不要转成浮点数再加减。
- 玩家的币种在创建时决定,之后不能变更;转账的
currency必须与玩家相同。
每日对账建议
转账:你的转账记录中状态不是「完成」的,每天以同一个
txnId重送或查询,直到每一笔都有明确结果。余额:对每位有活动的玩家,验证
text期初余额 + 转入 − 转出 + Σ 注单输赢(每张注单取最新版次的 winLoss)± 人工上下分 = 期末余额注单输赢以注单同步取得;对账时点若仍有未结算的注单,差额等于那些注单的本金(牛牛再加预扣)。
每日汇总:以
GET /bets/summary?date=与你自己汇总的数字核对。
错误码
| 错误码 | HTTP | 原因 |
|---|---|---|
INVALID_PARAMETER | 400 | 缺少字段,或 txnId 格式错误 |
INVALID_AMOUNT | 400 | 金额不是大于 0、最多 4 位小数的字符串 |
INVALID_USERNAME | 400 | 账号不符合规则 |
INVALID_CURRENCY | 400 | 转入时创建新玩家,但币种代码不合法 |
CURRENCY_MISMATCH | 400 | 币种与玩家不同 |
PLAYER_NOT_FOUND | 404 | 转出或查询的玩家不存在 |
PLAYER_LIMIT | 403 | 转入时要创建新玩家,但玩家数已达方案上限(免费展示方案 50、沙箱 500) |
TXN_CONFLICT | 409 | txnId 已用在不同的转账,或最近两个月内已用在另一位玩家 |
INSUFFICIENT_BALANCE | 409 | 可用余额不足 |
TXN_NOT_FOUND | 404 | 查不到这笔转账(带 username 时表示没有执行) |
其他共通错误见错误码。