NBTiktokDOCS / Tài liệu phát triển

v2 · Hướng dẫn dành cho nhà phát triển

DOC / 03

API bảng xếp hạng

Mùa giải cấp game, ghi mười trường điểm, thứ hạng khu vực và toàn cầu, cùng sandbox Debug.

API bảng xếp hạng cho phép máy khách trò chơi đã được cấp quyền khám phá bảng, xem mùa giải, ghi điểm mười chiều và truy vấn thứ hạng khu vực hoặc toàn cầu. Base URL công khai:

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

Mọi yêu cầu dùng credentials.gameApi.token ngắn hạn từ phản hồi đăng nhập; lỗi xác thực ổn định có dạng { "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }. Không truy cập cổng container, /internal/* hay /admin/api/*. Xem API tích hợp trò chơi tương tác để biết hợp đồng đăng nhập, Session, refresh và WebSocket.

#Tích hợp nhanh

  1. Gọi POST /api/game-client/v2/login với mode: "production" hoặc mode: "debug".
  2. Đọc credentials.gameApi.tokencredentials.gameApi.expiresAt.
  3. Khám phá bảng bằng GET /openapi/v2/leaderboards.
  4. Chỉ ghi điểm khi có currentSeason.
  5. Gửi đủ score1 đến score10 cho mỗi người chơi và dùng requestId UUID riêng.
  6. Truy vấn danh sách hoặc thứ hạng một người chơi.
  7. Tại refreshAt của Session, chuyển nguyên tử sang Game API Token mới.

#Tuyến công khai và xác thực

Khả năngMethod / Path
Khám phá bảngGET /openapi/v2/leaderboards
Liệt kê người chơi mô phỏngGET /openapi/v2/leaderboards/simulation-players
Liệt kê mùa giảiGET /openapi/v2/leaderboards/:boardKey/seasons
Ghi điểm mười chiềuPOST /openapi/v2/leaderboards/:boardKey/score-mutations
Danh sách xếp hạngGET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/rankings
Hạng của một người chơiGET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/players/:playerId/rank
Xếp hạng tuần hiện tạiGET /openapi/v2/leaderboards/:boardKey/weeks/current/rankings
Hạng tuần của một người chơiGET /openapi/v2/leaderboards/:boardKey/weeks/current/players/:playerId/rank
Authorization: Bearer <gameApiToken>
Accept: application/json

Token là giá trị mờ, ngắn hạn, gắn với trò chơi, streamer và chế độ; nó mất hiệu lực cùng Session cha. Không lưu lâu dài hoặc ghi log. Production ánh xạ tới không gian production, Debug ánh xạ tới sandbox.

Token khóa gameId, space, streamerId, regionCode, authorizationId và Session; máy khách không thể ghi đè. Thành viên đại lý dùng vùng của đại lý, quyền cá nhân dùng vùng hồ sơ. Vùng ghi hỗ trợ là SG, MY, TH, ID, PH, TW; body không được chọn vùng. Token tối đa 10 phút và có thể ngắn hơn vì thời hạn tuyệt đối của Session.

credentials.gameApi.token khác Session access Token, refresh Token và WebSocket Ticket. Sau refresh, cài đặt nguyên tử bộ mới và ngừng chủ động dùng Game API Token cũ ngay. Hiệu lực bảo mật của Token cũ kéo dài tới exp 10 phút của chính nó, hoặc kết thúc sớm hơn khi Game nhận projection thu hồi Session; refresh không tự thu hồi ngay lập tức.

#Mô hình dữ liệu

Mỗi bảng có boardKey ổn định, tên, chiều sắp xếp, trường điểm, vùng hỗ trợ và currentSeason có thể rỗng. Bảng tăng dần ưu tiên điểm thấp; bảng giảm dần ưu tiên điểm cao.

Yêu cầu ghi không chứa seasonKey. Máy chủ tự chọn mùa đang hoạt động tại thời điểm yêu cầu; nếu không có, trả 409 CURRENT_SEASON_UNAVAILABLE.

Mỗi mutation phải có chính xác mười trường:

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

Điểm là số nguyên 64 bit có dấu, truyền dưới dạng chuỗi thập phân và phải phân tích bằng số 64 bit hoặc BigInt, không dùng JSON number. Giá trị, kết quả và tổng toàn cầu phải trong -9223372036854775808 đến 9223372036854775807.

Ghi chỉ sửa regionCode của Token. Với mỗi trường điểm, global là giá trị vùng tốt nhất của người chơi: desc chọn lớn nhất, asc chọn nhỏ nhất; nếu hòa, chọn regionCode nhỏ nhất theo thứ tự từ điển. Mười nguồn tốt nhất được tính lại trong cùng transaction. Kết quả global trả regionCode nguồn đó; truy vấn vùng trả vùng đã yêu cầu.

Điểm vùng Production cách ly theo game × bảng × mùa × người chơi × regionCode, rồi Game suy ra global tốt nhất. Sandbox thêm cách ly streamer × trò chơi; hai không gian không chia sẻ điểm hay bản ghi idempotency.

Season là timeline chuẩn ở cấp game: mọi bảng chuyển mùa cùng lúc với một scoreRetentionBasisPoints (0..10000, mặc định 50 nghĩa là 0,5%). Cả mười trường điểm Production giữ tỷ lệ đó và cắt về 0; Game tính lại global best cùng vùng nguồn. Cùng transaction sẽ reset player attribute Production ở sáu vùng theo seasonReset đã publish. Điểm Debug không được copy và thuộc tính Sandbox không reset. Chỉ đóng season mà không transition sẽ không reset điểm hay thuộc tính. Khi lưu rule tự động, chỉ timing của season tự động được cập nhật; season thủ công hiện tại giữ thời gian kết thúc đã đặt, sau đó rule tự động mới tiếp quản.

#Khám phá bảng

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": "Total score",
      "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": "Fastest completion",
      "scoreOrder": "asc",
      "currentSeason": null
    }
  ]
}

boards sắp theo boardKey. currentSeason: null cho phép đọc lịch sử nhưng không ghi. Khám phá lại sau đăng nhập hoặc refresh; không mã hóa cứng bảng, thứ tự hay thời gian mùa.

#Người chơi mô phỏng và mùa giải

Máy khách Debug gọi GET /openapi/v2/leaderboards/simulation-players để lấy simulation player của streamer × trò chơi. Production dùng canonical sourcePlayerId và gửi riêng playerName; xếp hạng trả playerId cùng regionCode nguồn, không trả biệt danh live.

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

Truy vấn mùa bằng GET /openapi/v2/leaderboards/main/seasons?limit=50. limit tùy chọn 1–100, mặc định 50; cursor là giá trị mờ của trang trước trên cùng bảng. seasons mới nhất trước, có seasonKey, startsAt, endsAt, status (scheduled, active, ended), source (manual, automatic), scoreRetentionBasisPoints được chụp khi tạo mùa và nextCursor. Đường dẫn nhận khóa thật hoặc current; mùa cũ đọc được nhưng không ghi công khai.

#Ghi điểm

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 chứa 1–20 người chơi và body không quá 128 KiB. Mỗi requestId là UUID v1–v5, không lặp trong lô, nên duy nhất toàn cục; playerId là chuỗi mờ không rỗng, tối đa 256 ký tự; scores chứa đúng score1score10 dạng chuỗi số nguyên thập phân có thể âm. operationincrement, set hoặc max.

Production bắt buộc playerName dài 1–200 ký tự và Game lưu tên theo vùng; Sandbox phải bỏ qua trường này và dùng tên simulation player đã đăng ký.

Một operation áp dụng cho cả mười trường. Muốn chỉ tăng vài trường, gửi "0" cho phần còn lại. set thay cả mười, max so từng trường; không có nghĩa “bỏ trường thì giữ nguyên”. Hình dạng một trường cũ với scoreFieldvalue trả 400 INVALID_MUTATIONS.

Toàn bộ lô được xử lý tuần tự trong một giao dịch cơ sở dữ liệu. Bất kỳ mutation, điểm hoặc kiểm tra chống lặp sai đều hoàn tác cả lô. Ghi đồng thời cho cùng người chơi và vùng được tuần tự hóa; điểm vùng và mười nguồn global tốt nhất thay đổi cùng nhau.

{
  "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 là mùa thực đã ghi; mỗi item có displayName đã lưu và regionCode của Token, còn scores là mười giá trị mới trong vùng, không phải global tốt nhất.

#Chống lặp và thử lại

requestId nhận diện một mutation mười trường của một người chơi. Phạm vi chống lặp Production bao phủ toàn bộ dữ liệu chính thức; Sandbox dùng phạm vi trò chơi × streamer. Hãy tạo UUID duy nhất toàn cục cho mọi mutation mới trong cả hai chế độ.

  • Gửi lại nội dung giống hệt trong cùng phạm vi trả kết quả cũ và không ghi lần nữa.
  • Tái sử dụng requestId nhưng đổi trò chơi, người chơi, thao tác, vùng hoặc điểm trả 409 IDEMPOTENCY_CONFLICT.
  • Sau timeout hoặc lỗi tạm thời, thử lại mutation gốc giống hệt với cùng requestId.
  • Không tạo UUID mới để che giấu dữ liệu có thể đã commit.

Request mới chọn season current tại thời điểm đó. Dù retry sau khi đã chuyển season, replay có cùng requestId và payload vẫn trả season cùng kết quả mutation gốc, không chạm dữ liệu season mới. Một batch trộn mutation mới với replay cũ, hoặc replay từ các season gốc khác nhau, trả 409 IDEMPOTENCY_CONFLICT và rollback toàn bộ. requestId lặp trong cùng batch trả 400 INVALID_MUTATIONS. Hãy lưu request gốc để đối soát, không đổi UUID. Sự kiện live tin cậy ảnh hưởng một người có thể dùng eventId làm requestId; nhiều người cần UUID xác định riêng. Chỉ ACK sự kiện nguồn sau khi ghi điểm thành công.

#Truy vấn danh sách xếp hạng

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

scoreField là bắt buộc, chỉ nhận score1score10. scopeglobal hoặc region; truy vấn vùng cần region hợp lệ. limit tối đa 100. Dùng cursor mờ từ trang trước để tiếp tục.

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

Mỗi tổ hợp không gian + trò chơi + streamer Sandbox + bảng + mùa + phạm vi + vùng + trường điểm chỉ công khai 100 hàng đầu; limit phân trang trong đó. Điểm bằng nhau dùng SQL RANK, nên hạng sau có thể nhảy; playerId ổn định thứ tự cùng điểm. Với scoreOrder=asc, điểm thấp xếp cao. Cursor gắn với ngữ cảnh đã tạo; không phân tích, sửa hoặc dùng sau khi đổi bộ lọc, nếu sai trả 400 INVALID_CURSOR.

#Thứ hạng một người chơi

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

Ở endpoint này, scoreField mặc định score1, scope mặc định toàn cầu. ranking.regionCode là vùng nguồn tốt nhất cho global, hoặc vùng truy vấn cho scope vùng. Hạng được tính trên toàn bảng và có thể lớn hơn 100; không có dữ liệu trả { "ranking": null }.

#Xếp hạng tuần hiện tại

Tuần hiện tại cố định từ thứ Hai 00:00 đến thứ Hai kế tiếp 00:00 theo Asia/Shanghai. Điểm tuần bắt đầu từ 0 và độc lập áp dụng cùng thao tác increment, set hoặc max đã thành công. Điều chỉnh Production của quản trị viên được tính như increment; replay idempotent không cộng lại. Điểm tuần tiếp tục qua chuyển season và không nhận carryover hay reset của season.

Danh sách dùng GET /openapi/v2/leaderboards/main/weeks/current/rankings?scoreField=score2&scope=global&limit=100; một người chơi dùng GET /openapi/v2/leaderboards/main/weeks/current/players/tiktok-user-id/rank?scoreField=score2. Bộ lọc, phân trang top 100, SQL RANK, chiều sắp xếp và vùng tốt nhất giống bảng season. Endpoint một người chơi mặc định scoreField=score1 và tính trên toàn bảng tuần.

Hai phản hồi đều có spaceweek gồm startsAt, endsAt, timeZone, isPartial, trackingStartedAt. Trong tuần triển khai, isPartial là true nếu tracking bắt đầu sau mốc thứ Hai; sang tuần đầy đủ kế tiếp nó tự thành false, không phỏng đoán hay backfill mutation cũ. Production và Sandbox vẫn do Token chọn và cách ly; đọc tuần không cần season đang hoạt động.

#Sandbox Debug

Đăng nhập Debug luôn cho phản hồi space = sandbox. Điểm Sandbox, tổng hợp toàn cầu và requestId được tách theo streamer × trò chơi. Cùng streamer và trò chơi vẫn truy cập dữ liệu sau khi đăng nhập lại hoặc refresh; các streamer hay trò chơi khác không chia sẻ. Cấu hình bảng và mùa vẫn do quản trị viên nền tảng cung cấp.

#Lỗi và khôi phục

HTTPLỗiHành động
400INVALID_REGIONDùng vùng hỗ trợ; vùng ghi đến từ thẩm quyền tài khoản
400INVALID_LIMITDùng phạm vi phân trang đã quy định
400INVALID_SCORE_FIELDDùng score1score10
400INVALID_MUTATIONS / INVALID_SCORESửa đầy đủ mutation mười trường
400REGION_REQUIREDThêm region cho xếp hạng vùng
400INVALID_CURSORBỏ cursor và bắt đầu từ trang đầu
401INVALID_ACCESS_TOKEN / INVALID_ACCESS_TOKENThay Token qua refresh hoặc đăng nhập
404SEASON_NOT_FOUNDTải lại mùa và kiểm tra seasonKey
409CURRENT_SEASON_UNAVAILABLEChờ mùa hoạt động hoặc liên hệ quản trị viên
409SEASON_NOT_ACTIVETải lại bảng và mùa
409IDEMPOTENCY_CONFLICTSo sánh yêu cầu gốc; không ghi lại
413Body quá lớnChia tối đa 20 người và 128 KiB
500INTERNAL_ERRORGiữ requestId, ghi ngữ cảnh và liên hệ nền tảng

Đọc gặp mạng hoặc 5xx dùng backoff lũy thừa có jitter. Chỉ retry ghi với ngữ cảnh, nội dung, requestId y hệt. Token bảng hết hạn thì refresh Session tuần tự; nếu kết quả refresh không rõ, không dùng lại refresh Token một lần.

#Danh sách kiểm tra phát hành

  • Chỉ gọi /openapi/v2/* trên HTTPS Origin công khai bằng Game API Token.
  • Sau đăng nhập/refresh, khám phá lại bảng; chỉ ghi khi có currentSeason.
  • Gửi mười chuỗi điểm mỗi người, xử lý bằng số 64 bit hoặc BigInt.
  • Lưu requestId duy nhất, yêu cầu và kết quả.
  • Production dùng canonical sourcePlayerId; Debug dùng người mô phỏng.
  • Phân biệt productionsandbox; cursor chỉ cho trang kế của truy vấn gốc.
  • Khi xung đột, đối chiếu yêu cầu và mùa, không đổi UUID để ghi lại.
  • Log chỉ gameId, boardKey, seasonKey thực, status, requestId; không log Authorization Header.