錢包(轉帳)
本平台使用轉帳錢包:每位玩家在本平台有一個遊戲錢包,你的系統以 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 時表示沒有執行) |
其他共通錯誤見錯誤碼。