NBTiktokDOCS / 開發文件

v2 · 開發者指南

DOC / 03

開放排行榜 API

遊戲級賽季、十維積分寫入、區域與全球排名及 Debug 沙箱。

開放排行榜 API 供已透過 NBTiktok 帳號與遊戲授權登入的用戶端探索榜單、查詢賽季、寫入十維積分,並讀取區域或全球排名。公開 Base URL 為:

https://<platform-origin>/openapi/v2

所有請求都使用登入回應中的短期 credentials.gameApi.token,穩定驗證錯誤格式為 { "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }。不得直接存取容器埠、/internal/*/admin/api/*。登入、Session、refresh 與 WebSocket 合同請參閱互動遊戲串接 API

#快速串接

  1. mode: "production"mode: "debug" 呼叫 POST /api/game-client/v2/login
  2. 讀取 credentials.gameApi.tokencredentials.gameApi.expiresAt
  3. 呼叫 GET /openapi/v2/leaderboards 探索榜單。
  4. 只有 currentSeason 存在時才寫入積分。
  5. 每名玩家都提交 score1score10,並使用獨立 UUID requestId
  6. 查詢排行榜或單一玩家名次。
  7. 到達 Session 的 refreshAt 時,原子切換到新的Game API Token。

#公開接口與驗證

能力Method / Path
探索榜單GET /openapi/v2/leaderboards
查詢模擬玩家GET /openapi/v2/leaderboards/simulation-players
查詢賽季GET /openapi/v2/leaderboards/:boardKey/seasons
寫入十維積分POST /openapi/v2/leaderboards/:boardKey/score-mutations
查詢排名列表GET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/rankings
查詢單一玩家排名GET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/players/:playerId/rank
查詢目前週榜GET /openapi/v2/leaderboards/:boardKey/weeks/current/rankings
查詢單一玩家目前週排名GET /openapi/v2/leaderboards/:boardKey/weeks/current/players/:playerId/rank
Authorization: Bearer <gameApiToken>
Accept: application/json

Token 是 Core 簽署的短期 JWT,綁定遊戲、主播與模式,並由 Game 本機驗簽;不得記錄或持久化。Production 對應 production 積分空間,Debug 對應 sandbox

Token 鎖定 gameIdspacestreamerIdregionCodeauthorizationId 與 Session,用戶端不得覆蓋。經銷商成員的寫入區域來自經銷商;個人授權來自帳號區域。支援 SGMYTHIDPHTW,寫入請求不能指定區域。Token 最長 10 分鐘,並可能受 Session 絕對期限縮短。

credentials.gameApi.token 與 Session access Token、refresh Token、WebSocket Ticket 都不同。Refresh 後必須原子安裝新憑證並立即停止主動使用舊 Game API Token;舊 Token 的安全有效性持續到自身 10 分鐘 exp,或更早收到 Session 撤銷投影。

#資料模型

每個榜單都有穩定 boardKey、名稱、排序方向、支援的積分欄位、區域與可能為空的 currentSeason。升冪榜以較小分數優先,降冪榜以較大分數優先。

寫入接口沒有 seasonKey,伺服器會解析請求當下的活動賽季;沒有活動賽季時回傳 409 CURRENT_SEASON_UNAVAILABLE

每次 mutation 必須恰好包含:

score1, score2, score3, score4, score5,
score6, score7, score8, score9, score10

積分是有符號 64 位元整數,以十進位字串傳輸,應以 64 位整數或 BigInt 解析,不能使用 JSON number。值、運算結果和全球彙總都必須在 -92233720368547758089223372036854775807

寫入只修改 Token regionCode。每個積分欄位的全球值是玩家最佳區域值:desc 取最大、asc 取最小,區域並列時取 regionCode 字典序最小者;十個最佳來源在同一交易重算。全球排名項目回傳該來源 regionCode,區域查詢回傳指定區域。

Production 區域分依遊戲 × 榜單 × 賽季 × 玩家 × regionCode 隔離,再推導玩家全球最佳值;Sandbox 額外依主播 × 遊戲隔離。兩個空間不共用分數或冪等紀錄。

賽季是遊戲級權威時間線,遊戲內所有榜單同時切季並共用 scoreRetentionBasisPoints0..10000,預設 500.5%)。Production 十個積分欄位依比例繼承並向零取整;全球最佳值與來源區域會重新計算。同一交易也會按已發佈 seasonReset 重置六個區域的 Production 玩家屬性。Debug 沙箱積分不複製,Sandbox 玩家屬性不重置;只提前關閉賽季不會重置任何資料。管理員儲存自動規則時,只有自動賽季依新規則更新時間;目前手動賽季保留明確結束時間,結束後才由自動規則接管。

#探索榜單

GET /openapi/v2/leaderboards
Authorization: Bearer <gameApiToken>
{
  "space": "production",
  "scoreFields": [
    "score1", "score2", "score3", "score4", "score5",
    "score6", "score7", "score8", "score9", "score10"
  ],
  "gameId": "game_one",
  "boards": [
    {
      "boardKey": "main",
      "displayName": "總積分榜",
      "scoreOrder": "desc",
      "currentSeason": {
        "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112",
        "seasonKey": "auto-20260819000000-r3",
        "startsAt": "2026-08-19T00:00:00.000Z",
        "endsAt": "2026-08-20T00:00:00.000Z",
        "status": "active"
      }
    },
    {
      "boardKey": "speedrun",
      "displayName": "最快通關",
      "scoreOrder": "asc",
      "currentSeason": null
    }
  ]
}

boardsboardKey 排序。currentSeason: null 表示可讀歷史賽季但不能寫入。登入或 refresh 後應重新探索,不要寫死榜單、排序或賽季時間。

#模擬玩家與賽季

Debug 用戶端可呼叫 GET /openapi/v2/leaderboards/simulation-players 取得目前主播 × 遊戲的預設與自訂模擬玩家。Production Token 也能呼叫,但回傳始終是 Sandbox 玩家。Production 寫入使用 canonical sourcePlayerId 並另傳 playerName;排行榜回傳 playerId 與來源 regionCode,不回直播暱稱。

{
  "players": [
    { "playerId": "debug-player-1", "displayName": "Debug Player 1" }
  ]
}

賽季請求為 GET /openapi/v2/leaderboards/main/seasons?limit=50limit 可選 1–100,預設 50;cursor 是同榜單上一頁的不透明游標。回應 seasonsstartsAt 新到舊,含 seasonKeystartsAtendsAtstatusscheduledactiveended)、sourcemanualautomatic)及建立賽季時保存的 scoreRetentionBasisPoints,另有 nextCursor。路徑可用真實 seasonKeycurrent;歷史賽季可查但不能公開寫入。

#寫入積分

POST /openapi/v2/leaderboards/main/score-mutations
Authorization: Bearer <gameApiToken>
Content-Type: application/json
{
  "mutations": [
    {
      "requestId": "533b2994-b638-45a8-9db2-f0fc926fe9e5",
      "playerId": "tiktok-user-id",
      "playerName": "Alice",
      "operation": "increment",
      "scores": {
        "score1": "100",
        "score2": "0",
        "score3": "0",
        "score4": "0",
        "score5": "0",
        "score6": "0",
        "score7": "0",
        "score8": "0",
        "score9": "0",
        "score10": "0"
      }
    }
  ]
}

mutations 必須包含 1–20 名玩家,整個本文最大 128 KiB。每個 requestId 是 UUID v1–v5,同批不得重複,建議全域唯一;playerId 是非空、不超過 256 字元的不透明值;scores 恰好含 score1score10 的帶可選負號十進位字串。operation 只能是 incrementsetmax

Production 每名玩家必須提供 1–200 字元 playerName,Game 會保存該區域名稱;Sandbox 必須省略,名稱使用已登記模擬玩家。

一次 operation 套用全部十欄。只增加部分欄位時,其餘用 "0"set 覆蓋全部,max 逐欄比較,不存在省略欄位保持不變。舊版單欄 scoreFieldvalue 形狀會回傳 400 INVALID_MUTATIONS

整批在同一資料庫交易中序列處理;任一 mutation、積分或冪等檢查失敗都會回滾。相同玩家與區域的並行寫入會被序列化,區域分與十個全球最佳來源一起更新。

{
  "season": {
    "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112",
    "seasonKey": "auto-20260819000000-r3",
    "startsAt": "2026-08-19T00:00:00.000Z",
    "endsAt": "2026-08-20T00:00:00.000Z",
    "status": "active"
  },
  "mutations": [
    {
      "requestId": "533b2994-b638-45a8-9db2-f0fc926fe9e5",
      "playerId": "tiktok-user-id",
      "displayName": "Alice",
      "regionCode": "SG",
      "operation": "increment",
      "scores": {
        "score1": "1280",
        "score2": "0",
        "score3": "0",
        "score4": "0",
        "score5": "0",
        "score6": "0",
        "score7": "0",
        "score8": "0",
        "score9": "0",
        "score10": "0"
      }
    }
  ]
}

season.seasonKey 是實際寫入賽季;每項包含已保存 displayName 與 Token regionCodescores 是該區域寫入後的十個新值,不是全球最佳值。

#冪等與重試

requestId 代表單一玩家的一次十維 mutation。Production 的冪等範圍涵蓋全部正式資料;Sandbox 的範圍是遊戲 × 主播。兩種模式都應為新 mutation 產生全域唯一 UUID。

  • 完全相同的重送會回傳原結果,不重複寫入。
  • 在同一範圍重用 requestId,但改變遊戲、玩家、操作、區域或任何積分,回傳 409 IDEMPOTENCY_CONFLICT
  • 逾時或暫時性錯誤後,以相同 requestId 重送完全相同的內容。
  • 不得以新 UUID 掩蓋可能已提交的資料。

新請求會解析當時的 current 賽季。即使重試跨過賽季邊界,完全相同的 requestId 與內容仍回傳最初解析的原賽季和原 mutation 結果,不會寫入新賽季。批次若混合全新 mutation 與歷史重播,或重播來自不同原賽季,整批回傳 409 IDEMPOTENCY_CONFLICT 並回滾。同一 HTTP 批次重複 requestId 回傳 400 INVALID_MUTATIONS。必須保存原請求並核對,不能換 UUID。可靠直播事件影響單玩家時可用 eventId 作為 requestId;多玩家需為每人確定性產生不同 UUID。積分寫入成功後才能 ACK 原事件。

#排名查詢

GET /openapi/v2/leaderboards/main/seasons/current/rankings?scoreField=score2&scope=global&limit=100
Authorization: Bearer <gameApiToken>

scoreField 必填,只接受 score1score10scopeglobalregion;區域查詢還要提供合法 regionlimit 最大 100;下一頁使用不透明 cursor

{
  "scoreField": "score2",
  "scoreOrder": "desc",
  "rankings": [
    {
      "rank": 1,
      "playerId": "tiktok-user-id",
      "regionCode": "SG",
      "score": "980"
    }
  ],
  "nextCursor": null,
  "space": "production"
}

每組空間 + 遊戲 + Sandbox 主播 + 榜單 + 賽季 + 範圍 + 區域 + 積分欄位最多公開排序後前 100 行;limit 僅在其中分頁。相同分數使用 SQL RANK 共用名次,後續名次可跳號,以 playerId 穩定同分順序;scoreOrder=asc 時低分較前。游標綁定原查詢上下文,不得解析、修改或換條件重用,否則 400 INVALID_CURSOR

單一玩家接口:

GET /openapi/v2/leaderboards/main/seasons/current/players/tiktok-user-id/rank?scoreField=score2&scope=region&region=SG

此接口的 scoreField 預設為 score1scope 預設全球。ranking.regionCode 在全球範圍代表最佳來源區域,在區域範圍等於查詢區域。單玩家接口在完整榜單上計算名次,可超過 100;沒有資料時為 { "ranking": null }

#目前週榜

目前週固定依 Asia/Shanghai,從週一 00:00 到下週一 00:00。週分每週從 0 開始,獨立執行每次成功寫入的相同 incrementsetmax;Production 管理員調分以週分 increment 計入,冪等重放不會重複累計。週分跨賽季延續,不受切季繼承或清零影響。

列表使用 GET /openapi/v2/leaderboards/main/weeks/current/rankings?scoreField=score2&scope=global&limit=100,單玩家使用 GET /openapi/v2/leaderboards/main/weeks/current/players/tiktok-user-id/rank?scoreField=score2。篩選、前 100 名分頁、SQL RANK、排序方向與最佳來源區域都沿用賽季榜;單玩家 scoreField 預設 score1 並在完整週榜計算。

兩個回應都包含 spaceweek,其欄位為 startsAtendsAttimeZoneisPartialtrackingStartedAt。若追蹤在本週週一之後才上線,部署首週 isPartial 為 true;下一個完整週自動變為 false,平台不猜測或回填舊 mutation。Production、Sandbox 仍由 Token 決定並隔離,讀取週榜不要求目前存在活動賽季。

#Debug 沙箱

Debug 登入的回應空間固定是 sandbox。沙箱分數、全球彙總與 requestId 依主播 × 遊戲隔離;相同主播與遊戲重新登入或 refresh 後仍可存取,同遊戲不同主播或同主播不同遊戲不共享。榜單與賽季仍由平台管理員配置。

#錯誤與恢復

HTTP錯誤處理
400INVALID_REGION使用支援區域,寫入區域來自帳號權威
400INVALID_LIMIT使用接口規定的分頁範圍
400INVALID_SCORE_FIELD使用 score1score10
400INVALID_MUTATIONS / INVALID_SCORE修正完整十欄 mutation
400REGION_REQUIRED區域排名提供 region
400INVALID_CURSOR丟棄游標,從第一頁開始
401INVALID_ACCESS_TOKEN / INVALID_ACCESS_TOKEN透過 refresh 或登入更換憑證
404SEASON_NOT_FOUND重新整理賽季並檢查 seasonKey
409CURRENT_SEASON_UNAVAILABLE等待活動賽季或聯絡管理員
409SEASON_NOT_ACTIVE重新整理榜單與賽季
409IDEMPOTENCY_CONFLICT核對原請求,不得重複寫入
413本文過大拆成最多 20 人且不超過 128 KiB
500INTERNAL_ERROR保留 requestId 與上下文並聯絡平台

讀取遇到網路或 5xx 使用指數退避加隨機抖動;寫入只有原上下文、內容、requestId 完全相同才能重試。Token 失效時先序列 refresh,結果不確定時不能重用一次性 refresh Token。

#上線檢查清單

  • 只呼叫公開 HTTPS Origin 的 /openapi/v2/*,且只使用Game API Token;
  • 登入與 refresh 後重新探索榜單,寫入前確認 currentSeason
  • 每名玩家提交十個字串積分,以 64 位整數或 BigInt 處理;
  • 持久化唯一 requestId、請求與結果;
  • Production 使用 canonical sourcePlayerId,Debug 使用模擬玩家;
  • 區分 productionsandbox;游標只用於原查詢下一頁;
  • 衝突時核對原請求與賽季,不換 UUID 重複寫入;
  • 日誌僅記錄 gameIdboardKey、實際 seasonKey、狀態碼、requestId,絕不記錄 Authorization Header。