NBTiktokDOCS / 開發文件

v2 · 開發者指南

DOC / 04

開放玩家屬性 API

版本化屬性、欄位級賽季重設、原子批次修改與 Production/Debug 隔離。

玩家屬性 API 儲存有版本的遊戲專屬狀態,例如等級、經驗、體力、角色與小型進度。排行榜分數、貨幣流水、背包、任務歷史等具關聯或審計需求的資料,應使用專用系統。

公開 Base URL:

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

OpenAPI 3.1 規格可於 /openapi/game-api-v2.yaml 下載。不得直接存取容器連接埠、/internal/*/admin/api/*

#快速串接

  1. 呼叫 POST /api/game-client/v2/login,選擇 productiondebug
  2. 從回應取得短期 credentials.gameApi.token
  3. 讀取 active Schema,不要在用戶端寫死屬性定義。
  4. 讀取單一或最多 100 名玩家;尚未寫入者會取得預設值與 revision: "0"
  5. 每個 mutation 使用唯一 requestId,以 setincrementmax 修改。
  6. 於 Session refreshAt 原子更換凭證;網路結果不明時只能原樣重試。

#介面與驗證

功能Method / Path
取得目前 SchemaGET /openapi/v2/player-attributes/schema
取得單一玩家GET /openapi/v2/player-attributes/players/:playerId
批次查詢POST /openapi/v2/player-attributes/query
原子批次修改POST /openapi/v2/player-attributes/mutations
Authorization: Bearer <gameApiToken>
Accept: application/json

JSON 請求須再加 Content-Type: application/json。穩定錯誤格式為:

{
  "error": {
    "code": "ERROR_CODE",
    "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420",
    "details": {}
  }
}

#登入與 refresh

POST /api/game-client/v2/login
Content-Type: application/json
{
  "email": "player@example.com",
  "password": "password",
  "gameId": "game_one",
  "mode": "production",
  "clientBuild": 120
}
{
  "session": {
    "gameId": "game_one",
    "mode": "production",
    "streamerId": "9f833c5b-ec62-4530-9ae4-0af8130a26b6"
  },
  "credentials": {
    "gameApi": {
      "token": "signed-game-api-token",
      "expiresAt": "2026-08-20T12:10:00.000Z"
    }
  }
}

Token 鎖定 gameId、資料空間、streamerId、授權與 Session,用戶端不能覆寫。credentials.gameApi.token 與Session access Token、單次 refresh Token 和 WebSocket Ticket 不同;不得混用、儲存、記錄、分享或放入 URL。

POST /api/game-client/v2/session/refresh 會回傳新的完整凭證集。將 refresh 序列化、原子安裝新凭證,並立即丟棄舊值。完整合同請參閱互動遊戲串接 API

#Production、Sandbox 與玩家 ID

  • Production 狀態依「遊戲 × regionCode × playerId」隔離。
  • Debug 對應 sandbox,再以「實況主 × 遊戲」隔離。
  • 任何 path、query 或 body 都無法切換空間。
  • Production 使用 canonical event 的 payload.player.sourcePlayerId
  • Sandbox 只接受目前實況主與遊戲已登記的模擬玩家,否則回傳 SANDBOX_PLAYER_NOT_FOUND
  • playerId 為 1–256 字元的不透明字串;放入 path 時作為單一 UTF-8 segment percent-encode。

簽署的 Game API Token 將 regionCode 固定為 SGMYTHIDPHTW,path、query 與 body 都不能覆寫。每個成功回應頂層都回傳此 regionCode,每筆玩家狀態包含已保存的 displayName

每筆 Production mutation 必須提交 1–200 字元的 playerName,Game 會與該區域玩家一併保存;Sandbox mutation 必須省略 playerName,名稱以已登記模擬玩家為準。

#Schema 與欄位

管理員發佈 Schema,用戶端只能使用已發佈欄位。目前版本最多支援 100 個頂層標量欄位:

類型JSON約束操作
integernumberJSON safe integer,minimum / maximumset, increment, max
numbernumber有限數,minimum / maximumset, increment, max
stringstringminLength / maxLengthset
booleanbooleanset
enumstring唯一非空 enumValuesset

key 須符合 ^[a-z][a-z0-9_]{0,63}$。每個欄位有合法預設值,最終 JSON object 必須通過 active Schema,儲存上限為 64 KiB。

writeOperations 可為空陣列,表示遊戲用戶端只讀;管理員定義時省略它會預設為 ["set"]。integer 的 increment/max 運算元本身必須是 JSON safe integer。

每個欄位必須包含 seasonResetintegernumber 可使用 keepreset_to_defaultretain_percentage;其他類型只能保持或回到預設值。basisPoints 範圍是 0..1000050 代表 0.5%。Production 會在遊戲級切季交易內重置,Sandbox 永不重置。數值公式為 新值 = 預設值 + (舊值 - 預設值) × basisPoints / 10000,integer 向預設值方向取整;六個 Production 區域各自計算。

64 KiB 有兩層保護:PLAYER_ATTRIBUTES_TOO_LARGE 表示本次 mutation 的目前完整狀態已超限;PLAYER_ATTRIBUTE_SEASON_RESET_TOO_LARGE 表示目前狀態合法,但依已發佈規則計算的任意後續賽季保守大小上界會超限。後者回傳 409,並在 mutation、Schema 發佈或切季前阻止操作。

所有成功回應頂層都有 seasonseasonIdseasonKeystartsAtendsAtstatus)。所有榜單與玩家屬性共用此遊戲時間線;只提前關閉賽季不會重置積分或屬性。

#取得 Schema

GET /openapi/v2/player-attributes/schema
Authorization: Bearer <gameApiToken>
{
  "gameId": "game_one",
  "space": "production",
  "regionCode": "SG",
  "schemaVersion": 2,
  "season": { "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112", "seasonKey": "2026-s2", "startsAt": "2026-08-01T00:00:00Z", "endsAt": "2026-09-01T00:00:00Z", "status": "active" },
  "fields": [
    {
      "key": "level",
      "title": "等級",
      "description": "目前等級",
      "type": "integer",
      "defaultValue": 1,
      "minimum": 1,
      "maximum": 100,
      "writeOperations": ["set", "max"],
      "seasonReset": { "strategy": "retain_percentage", "basisPoints": 50 }
    },
    {
      "key": "class",
      "title": "職業",
      "type": "enum",
      "defaultValue": "warrior",
      "enumValues": ["warrior", "mage", "archer"],
      "writeOperations": ["set"],
      "seasonReset": { "strategy": "reset_to_default" }
    }
  ],
  "defaultAttributes": { "level": 1, "class": "warrior" },
  "jsonSchema": {
    "type": "object",
    "properties": {
      "level": { "type": "integer", "title": "等級", "default": 1, "minimum": 1, "maximum": 100 },
      "class": { "type": "string", "title": "職業", "default": "warrior", "enum": ["warrior", "mage", "archer"] }
    },
    "required": ["level", "class"],
    "additionalProperties": false
  },
  "writeRules": { "level": ["set", "max"], "class": ["set"] }
}

登入、refresh 或看到新 schemaVersion 時重新取得,不要高頻輪詢。

#查詢單一玩家

GET /openapi/v2/player-attributes/players/tiktok-user-123
Authorization: Bearer <gameApiToken>
{
  "gameId": "game_one",
  "space": "production",
  "regionCode": "SG",
  "schemaVersion": 2,
  "season": { "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112", "seasonKey": "2026-s2", "startsAt": "2026-08-01T00:00:00Z", "endsAt": "2026-09-01T00:00:00Z", "status": "active" },
  "player": {
    "regionCode": "SG",
    "playerId": "tiktok-user-123",
    "displayName": "Alice",
    "revision": "7",
    "attributes": { "level": 12, "class": "mage" },
    "updatedAt": "2026-08-20T08:00:00.000Z"
  }
}

合法但尚未寫入的玩家不會因讀取而建立,而會得到:

{
  "gameId": "game_one",
  "space": "production",
  "regionCode": "SG",
  "schemaVersion": 2,
  "season": { "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112", "seasonKey": "2026-s2", "startsAt": "2026-08-01T00:00:00Z", "endsAt": "2026-09-01T00:00:00Z", "status": "active" },
  "player": {
    "regionCode": "SG",
    "playerId": "new-player",
    "displayName": "Alice",
    "revision": "0",
    "attributes": { "level": 1, "class": "warrior" },
    "updatedAt": null
  }
}

revision 是不透明十進位字串,不得以 JavaScript Number 儲存。讀取會投影新 Schema 的缺少預設值,但不寫入。

#批次查詢

POST /openapi/v2/player-attributes/query
Authorization: Bearer <gameApiToken>
Content-Type: application/json
{
  "playerIds": ["player-1", "player-2"]
}

playerIds 包含 1–100 個不重複 ID,body 最大 128 KiB。回應順序與請求相同;任一輸入無效時全部失敗。

{
  "gameId": "game_one",
  "space": "sandbox",
  "regionCode": "SG",
  "schemaVersion": 2,
  "season": { "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112", "seasonKey": "2026-s2", "startsAt": "2026-08-01T00:00:00Z", "endsAt": "2026-09-01T00:00:00Z", "status": "active" },
  "players": [
    { "regionCode": "SG", "playerId": "player-1", "displayName": "Alice", "revision": "4", "attributes": { "level": 5, "class": "mage" }, "updatedAt": "2026-08-20T08:00:00.000Z" },
    { "regionCode": "SG", "playerId": "player-2", "displayName": "Bob", "revision": "0", "attributes": { "level": 1, "class": "warrior" }, "updatedAt": null }
  ]
}

#原子批次修改

POST /openapi/v2/player-attributes/mutations
Authorization: Bearer <gameApiToken>
Content-Type: application/json
{
  "mutations": [
    {
      "requestId": "7747fe63-ed9d-4e8f-ae15-c90d42f20f0a",
      "playerId": "tiktok-user-123",
      "playerName": "Alice",
      "expectedRevision": "7",
      "operations": [
        { "field": "exp", "operation": "increment", "value": 50 },
        { "field": "level", "operation": "max", "value": 12 }
      ]
    }
  ]
}
  • 每批 1–20 個不重複玩家,body 最大 128 KiB。
  • requestId 為批內唯一 UUID v1–v5;每個新業務修改使用新值。
  • Production 必須提供 1–200 字元 playerName;Sandbox 必須省略。
  • expectedRevision 為可選十進位字串;未寫入狀態為 "0"
  • 每人 1–20 個 operation,欄位不重複,並必須符合 Schema。
  • set 指定值;數值欄位的 increment 相加,max 保留較大值。

若提供 expectedRevision,必須與當前版本相等。省略時服務器會鎖定後於最新狀態上操作,適合可交換的 increment/max。整批是單一事務;任一錯誤會回滾所有玩家,成功時每人 revision 增加一次。

{
  "gameId": "game_one",
  "space": "production",
  "regionCode": "SG",
  "schemaVersion": 2,
  "season": { "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112", "seasonKey": "2026-s2", "startsAt": "2026-08-01T00:00:00Z", "endsAt": "2026-09-01T00:00:00Z", "status": "active" },
  "mutations": [
    {
      "requestId": "7747fe63-ed9d-4e8f-ae15-c90d42f20f0a",
      "regionCode": "SG",
      "playerId": "tiktok-user-123",
      "displayName": "Alice",
      "revision": "8",
      "attributes": { "level": 12, "exp": 1400, "class": "mage" },
      "updatedAt": "2026-08-20T08:01:00.000Z"
    }
  ]
}

#幂等與 30 天視窗

Production 幂等範圍覆蓋所有正式資料,requestId 不得跨遊戲重用;Sandbox 範圍是目前實況主 × 遊戲。30 天內,相同 requestId 加完全相同內容回傳原結果,不重複寫入;內容不同回傳 409 IDEMPOTENCY_CONFLICT。記錄 30 天後過期,仍不得重用 UUID。可靠事件可由 eventId 為每名玩家派生 UUID,成功後才 ACK。

#錯誤代碼

HTTPCode處理
400INVALID_PLAYER_ID修正空值、過長、重複或編碼錯誤的 ID
400INVALID_REQUEST_BODY提供可解析 JSON POST body 與正確根結構
400INVALID_PLAYER_IDS提供 1–100 個不重複 ID;Sandbox ID 必須是 UUID
400INVALID_PLAYER_ATTRIBUTE_MUTATIONS修正批次、UUID 或 operation 格式
400INVALID_PLAYER_NAMEProduction 必須提供去除首尾空白後 1–200 字元的 playerName
400INVALID_EXPECTED_REVISION提供非負 bigint 十進位 revision 字串
400PLAYER_ATTRIBUTE_NOT_DEFINED重讀 Schema,使用已發佈欄位
400DUPLICATE_PLAYER_ATTRIBUTE_OPERATION每名玩家的同一欄位只修改一次
400PLAYER_ATTRIBUTE_OPERATION_NOT_ALLOWED使用欄位允許的操作
400PLAYER_ATTRIBUTE_VALUE_INVALID修正 JSON 類型或枚舉值
400PLAYER_ATTRIBUTE_VALUE_OUT_OF_RANGE修正數值範圍或字串長度
400PLAYER_ATTRIBUTES_INVALID最終狀態不是可用的 JSON object
400PLAYER_ATTRIBUTES_TOO_LARGE修改後完整狀態不得超過 64 KiB
401INVALID_ACCESS_TOKEN提供有效 Bearer
401INVALID_ACCESS_TOKENrefresh 或重新登入
404GAME_NOT_FOUNDToken 的遊戲或 active Schema 不存在
404SANDBOX_PLAYER_NOT_FOUND使用目前 Sandbox 已登記玩家
409CURRENT_SEASON_UNAVAILABLE遊戲沒有 active 權威賽季;請管理員先設定或啟用賽季
409PLAYER_ATTRIBUTE_SEASON_RESET_TOO_LARGE目前狀態未超限,但後續切季投影會超過 64 KiB;調整預設值或規則
409PLAYER_ATTRIBUTE_REVISION_CONFLICT讀取最新狀態後重算
409IDEMPOTENCY_CONFLICT對帳原請求,不要盲目更換 UUID
413REQUEST_BODY_TOO_LARGE切分至 128 KiB 以下
500INTERNAL_ERROR保留 requestId,安全退避重試

#重試、curl、TypeScript 與 Unity

GET 在網路、429 或 5xx 時使用指數退避與 jitter。Mutation 只能以相同內容與 requestId 重播。400/404 先修正;409 revision 先重讀;401 只進行一次序列化 refresh。

curl --fail-with-body \
  -H 'Authorization: Bearer <gameApiToken>' \
  'https://<platform-origin>/openapi/v2/player-attributes/players/tiktok-user-123'
const requestId = crypto.randomUUID();
const response = await fetch(`${origin}/openapi/v2/player-attributes/mutations`, {
  method: "POST",
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
  body: JSON.stringify({ mutations: [{
    requestId, playerId, playerName, expectedRevision: revision,
    operations: [{ field: "exp", operation: "increment", value: 50 }],
  }] }),
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status} ${body.error ?? "UNKNOWN_ERROR"}`);
using System;
using System.Collections;
using System.Text;
using UnityEngine.Networking;

[Serializable] public class Op { public string field; public string operation; public int value; }
[Serializable] public class Mutation { public string requestId; public string playerId; public string playerName; public string expectedRevision; public Op[] operations; }
[Serializable] public class Envelope { public Mutation[] mutations; }

public static IEnumerator AddExp(string origin, string token, string playerId, string playerName, string revision) {
    var json = UnityEngine.JsonUtility.ToJson(new Envelope { mutations = new[] {
        new Mutation { requestId = Guid.NewGuid().ToString(), playerId = playerId, playerName = playerName, expectedRevision = revision,
            operations = new[] { new Op { field = "exp", operation = "increment", value = 50 } } }
    }});
    using var request = new UnityWebRequest(origin + "/openapi/v2/player-attributes/mutations", "POST");
    request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
    request.downloadHandler = new DownloadHandlerBuffer();
    request.SetRequestHeader("Authorization", "Bearer " + token);
    request.SetRequestHeader("Content-Type", "application/json");
    yield return request.SendWebRequest();
}

Unity JsonUtility 無法解析動態 attributes object;請使用已審核 JSON 套件,或由 Schema 產生強類型 DTO。

#安全、隱私與上線清單

  • 不將密碼、Token、支付凭證、原始個資或不必要的敏感值放入屬性。
  • 日誌不包含 Token、玩家 ID、屬性值或完整回應。
  • 只使用公開 HTTPS Origin,Bearer 不放入 query。
  • login/refresh 原子安裝 Token,Schema 版本改變時重讀。
  • Production 使用 canonical sourcePlayerId,Debug 只用目前 Sandbox 玩家。
  • revision 保留為字串,依賴舊值的寫入使用 expectedRevision
  • 不確定寫入只重播原請求,不更換 requestId。
  • 預先強制批次與 128 KiB 限制,並處理 401、409、429、5xx。
  • 付費貨幣、物品所有權、審計歷史與大型集合另行建模。