Skip to content

實作錢包回調

這通常是整個整合裡工作量最大的一部分。其他所有事情——簽名一次啟動呼叫、嵌入一個 iframe、監聽 shell 事件——都只是幾行程式碼的事。而這裡是你把平台接到自己真正的玩家帳本上的地方。

moose-platform 從不持有玩家資金。你的錢包才是唯一的真相來源,平台的職責是即時呼叫你實作的兩個介面,讓每一筆下注和派彩都恰好一次地落到你的錢包上。本指南所依據的欄位級契約,見錢包回調介面參考

兩個介面

ts
// POST /v1/wallet/transaction —— 結算一筆 BET、WIN、ROLLBACK 或 ADJUSTMENT
// GET  /v1/wallet/balance?playerRef=<ref> —— 回答一次餘額查詢

兩者都由平台呼叫,並用你的共用密鑰簽名——在信任任何一次呼叫之前先驗證它(見簽名與身份驗證:校驗平台的簽名(錢包回調))。平台會把呼叫送到哪個基礎網址,由你透過租戶的自助入口網站註冊——見註冊你的錢包回調 URL

處理每一種交易類型

ts
async function handleTransaction(req: TransactionRequest): Promise<TransactionResponse> {
  const cached = await ledger.getCachedResponse(req.transactionId)
  if (cached) return cached // 冪等重放 —— 見下文,永遠排在其他任何邏輯之前

  let result: TransactionResponse
  switch (req.type) {
    case 'BET':
      result = await ledger.debit(req.playerRef, req.amount, req.currency)
      // 如果玩家餘額不足,result.status 在這裡會是 'DECLINED' ——
      // 這是正常的業務結果,不是錯誤
      break
    case 'WIN':
      result = await ledger.credit(req.playerRef, req.amount, req.currency)
      // 這裡永遠不會是 DECLINED —— 真正的失敗應該拋出例外,而不是回傳 DECLINED
      break
    case 'ROLLBACK':
      const original = await ledger.getTransaction(req.originalTransactionId!)
      result = await ledger.credit(req.playerRef, original.amount, req.currency)
      break
    case 'ADJUSTMENT':
      result = req.direction === 'CREDIT'
        ? await ledger.credit(req.playerRef, req.amount, req.currency)
        : await ledger.debit(req.playerRef, req.amount, req.currency)
      break
  }

  await ledger.cacheResponse(req.transactionId, result)
  return result
}
  • BET 是唯一一種 DECLINED 屬於合法回應的類型——餘額不足,或超出你自己設定的下注限額。其餘類型(WINROLLBACKADJUSTMENT)必須要嘛以 OK 成功,要嘛讓這次 HTTP 呼叫直接失敗(非 200)——絕不能回傳 DECLINED
  • WIN 是和它的 BET 相互獨立的一筆交易。 不要等一筆 WIN 來核銷一筆還沒結束的 BET——它們是各自獨立到達的呼叫,兩者中帶有 roundComplete: true 的那一筆才是這一局的最後一筆。
  • ROLLBACK 透過 originalTransactionId 沖正某一筆特定的 BET——從你自己的帳本查出那筆下注的金額並退款回去。你也會看到一種兜底回滾,其 sessionToken"system:stale-round-reconciler",代表玩家客戶端斷線後平台強制關閉了這一局——按同樣的方式處理即可。
  • ADJUSTMENT 很少見,由平台管理員發起。direction 告訴你餘額該往哪個方向調整。

冪等性是強制要求

平台會用完全相同的 transactionId 重試失敗或逾時的呼叫。如果你的處理器在重試時重新套用一次餘額變動,玩家就會被重複扣款或重複派彩。在做任何帳本操作之前,先為你處理過的每一個 transactionId 快取回應,並在收到重複請求時原樣回放——上面的偽代碼之所以第一步就做這個檢查,正是這個原因。

一個安全的模式:把 transactionId 設成帳本資料表上的唯一約束,讓插入時的重複鍵錯誤成為訊號,去查出並回傳快取的回應,而不是再寫一次。

查詢餘額

ts
async function handleBalance(playerRef: string): Promise<{ balance: number }> {
  return { balance: await ledger.getBalance(playerRef) }
}

請讓它保持快速且可靠——它會被遊戲廠商(透過平台)即時呼叫用來顯示最新餘額,而不只是一個背景檢查。雙方呼叫方的細節見錢包回調介面參考:GET /v1/wallet/balance

Jackpot 與其他 metadata

TransactionRequest.metadata 是一個不透明的 JSON 物件,平台只會儲存並原樣轉發給你——它從不校驗或解讀其內容。最常見的情況是 jackpot WIN 攜帶著獎池/等級/金額等細節:

ts
if (req.metadata?.jackpot?.won) {
  // 展示或記錄 jackpot 細節;你帳本裡的 amount 欄位依然是
  // 權威的派彩金額 —— metadata 只是補充資訊,不是「該入帳多少」
  // 的第二個真相來源
}

精確(且不被強制校驗)的結構,見資料模型與列舉:Metadata 與 jackpot 派彩

在你的逾時內回應

平台會等待你回應一段有界的時間(預設 5000ms——見環境與基礎網址:註冊你的錢包回調 URL),超過就會把這次呼叫視為 TIMED_OUT,並重試或之後透過補償機制解決。只要你的冪等性處理正確,偶爾一次慢回應觸發重試並無大礙——但如果你的錢包後端持續偏慢,就意味著更多重試、更多補償流量,以及更差的玩家體驗(因為遊戲客戶端也在等待同一個往返)。

接下來

  • 如果你會發放自助式免費旋轉,見免費旋轉
  • 在真正涉及真實資金之前,端到端驗證整合,見測試
  • 在第一個真實玩家 session 之前,見上線檢查清單