v2 · Hướng dẫn dành cho nhà phát triển
DOC / 04API thuộc tính người chơi
Thuộc tính có phiên bản, reset theo field mỗi mùa, mutation nguyên tử và cách ly Production/Debug.
API thuộc tính người chơi lưu trạng thái có phiên bản riêng cho từng trò chơi, ví dụ cấp độ, kinh nghiệm, năng lượng, nhân vật và tiến trình nhỏ. Hãy dùng hệ thống chuyên dụng cho điểm xếp hạng, sổ cái tiền tệ, kho đồ, lịch sử nhiệm vụ và dữ liệu cần kiểm toán.
Base URL công khai:
https://<platform-origin>/openapi/v2
Có thể tải OpenAPI 3.1 tại /openapi/game-api-v2.yaml. Không gọi cổng container, /internal/* hay /admin/api/*.
#Quy trình tích hợp
- Gọi
POST /api/game-client/v2/loginvớimode: "production"hoặcmode: "debug". - Lấy
credentials.gameApi.tokenngắn hạn từ phản hồi. - Lấy Schema active; không ghi cứng định nghĩa thuộc tính trong client.
- Đọc một hoặc tối đa 100 người chơi. Người chơi chưa ghi có giá trị mặc định và
revision: "0". - Tạo
requestIdduy nhất cho mỗi mutation; dùngset,incrementhoặcmax. - Thay toàn bộ credential một cách nguyên tử tại
refreshAt. Khi kết quả mạng không rõ, chỉ gửi lại mutation ban đầu.
#Endpoint và xác thực
| Khả năng | Method / Path |
|---|---|
| Lấy Schema hiện tại | GET /openapi/v2/player-attributes/schema |
| Lấy một người chơi | GET /openapi/v2/player-attributes/players/:playerId |
| Truy vấn theo lô | POST /openapi/v2/player-attributes/query |
| Thay đổi nguyên tử theo lô | POST /openapi/v2/player-attributes/mutations |
Authorization: Bearer <gameApiToken>
Accept: application/json
Yêu cầu JSON cần Content-Type: application/json. Lỗi ổn định có dạng:
{
"error": {
"code": "ERROR_CODE",
"requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420",
"details": {}
}
}
#Đăng nhập và 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"
}
}
}
Server suy ra gameId, không gian dữ liệu, streamerId, quyền và Session từ Token; request không thể ghi đè. credentials.gameApi.token khác Session access Token, refresh Token dùng một lần và WebSocket Ticket. Không trộn, lưu bền, ghi log, chia sẻ hay đặt trong URL.
POST /api/game-client/v2/session/refresh trả bộ credential mới. Tuần tự hóa refresh, cài toàn bộ một cách nguyên tử và bỏ credential cũ ngay. Xem API tích hợp trò chơi tương tác.
#Production, Sandbox và playerId
- Trạng thái Production cách ly theo game ×
regionCode×playerId. - Debug chọn
sandbox, cách ly thêm theo streamer × trò chơi. - Path, query và body không thể chuyển không gian.
- Production dùng
payload.player.sourcePlayerIdtrong canonical event. - Sandbox chỉ chấp nhận simulation player đăng ký cho streamer và game hiện tại; nếu không trả
SANDBOX_PLAYER_NOT_FOUND. playerIdlà chuỗi mờ 1–256 ký tự; percent-encode thành một UTF-8 path segment.
Production và Sandbox không bao giờ đọc hay sửa trạng thái hoặc bản ghi idempotency của nhau.
Game API Token đã ký cố định regionCode là SG, MY, TH, ID, PH hoặc TW; path, query hay body không thể ghi đè. Mọi phản hồi thành công trả regionCode ở cấp cao nhất và mỗi trạng thái người chơi có displayName đã lưu.
Mỗi mutation Production phải có playerName dài 1–200 ký tự để Game lưu cùng người chơi trong vùng đó. Mutation Sandbox phải bỏ qua playerName; tên lấy từ simulation player đã đăng ký.
#Schema và mô hình field
Quản trị viên publish Schema; client chỉ dùng field đã publish. Phiên bản hiện tại hỗ trợ tối đa 100 scalar field cấp cao nhất:
| Type | JSON | Ràng buộc | Operation |
|---|---|---|---|
integer | number | JSON safe integer, minimum / maximum | set, increment, max |
number | number | số hữu hạn, minimum / maximum | set, increment, max |
string | string | minLength / maxLength | set |
boolean | boolean | không | set |
enum | string | enumValues không rỗng, duy nhất | set |
Key khớp ^[a-z][a-z0-9_]{0,63}$. Mỗi field có mặc định hợp lệ. JSON object cuối phải qua active Schema và tối đa 64 KiB khi lưu.
writeOperations có thể là mảng rỗng, nghĩa là client chỉ đọc field. Khi quản trị viên bỏ qua nó, mặc định là ["set"]. Toán hạng increment/max cho integer phải là JSON safe integer.
Mỗi field phải có seasonReset. integer và number hỗ trợ keep, reset_to_default hoặc retain_percentage; các type khác chỉ hỗ trợ giữ nguyên hay về mặc định. basisPoints là số nguyên 0..10000; 50 nghĩa là 0.5%. State Production được reset trong cùng transaction với chuyển mùa ở cấp game; Sandbox không bao giờ reset. Công thức là mới = mặc định + (cũ - mặc định) × basisPoints / 10000; integer làm tròn theo hướng về giá trị mặc định. Sáu vùng Production được tính độc lập.
Giới hạn 64 KiB có hai lớp. PLAYER_ATTRIBUTES_TOO_LARGE nghĩa là state hiện tại sau mutation đã vượt giới hạn. PLAYER_ATTRIBUTE_SEASON_RESET_TOO_LARGE nghĩa là state hiện tại vẫn hợp lệ nhưng cận trên kích thước bảo thủ cho bất kỳ projection reset season nào trong tương lai sẽ vượt 64 KiB. Lỗi 409 này chặn mutation, publish Schema hoặc transition trước khi dữ liệu không thể kết toán đi vào season.
Mọi response thành công đều có season chuẩn ở cấp cao nhất: seasonId, seasonKey, startsAt, endsAt, status. Tất cả leaderboard và player attribute dùng chung timeline này. Chỉ đóng season mà không transition sẽ không reset điểm hay thuộc tính.
#Lấy 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": "Cấp độ",
"description": "Cấp hiện tại",
"type": "integer",
"defaultValue": 1,
"minimum": 1,
"maximum": 100,
"writeOperations": ["set", "max"],
"seasonReset": { "strategy": "retain_percentage", "basisPoints": 50 }
},
{
"key": "class",
"title": "Lớp nhân vật",
"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": "Cấp độ", "default": 1, "minimum": 1, "maximum": 100 },
"class": { "type": "string", "title": "Lớp nhân vật", "default": "warrior", "enum": ["warrior", "mage", "archer"] }
},
"required": ["level", "class"],
"additionalProperties": false
},
"writeRules": { "level": ["set", "max"], "class": ["set"] }
}
Lấy lại sau login, refresh, hoặc khi thấy schemaVersion mới; không poll tần suất cao.
#Lấy một người chơi
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"
}
}
Người chơi hợp lệ nhưng chưa ghi nhận trạng thái ảo, không tạo row:
{
"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
}
}
Xem revision là chuỗi thập phân mờ, không lưu bằng JavaScript Number. Read chèn mặc định còn thiếu của Schema mới mà không ghi.
#Truy vấn theo lô
POST /openapi/v2/player-attributes/query
Authorization: Bearer <gameApiToken>
Content-Type: application/json
{
"playerIds": ["player-1", "player-2"]
}
Cho phép 1–100 ID duy nhất, body tối đa 128 KiB. Thứ tự response giống request; input sai làm hỏng toàn request.
{
"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 }
]
}
#Mutation nguyên tử theo lô
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 người chơi duy nhất; body tối đa 128 KiB.
requestIdlà UUID v1–v5 duy nhất trong lô và mới cho mỗi thay đổi nghiệp vụ.- Production bắt buộc
playerNamedài 1–200 ký tự; Sandbox phải bỏ qua trường này. expectedRevisionlà chuỗi thập phân tùy chọn; trạng thái chưa ghi là"0".- 1–20 operation mỗi người chơi, không lặp field, phải thỏa Schema.
setgán giá trị;incrementcộng vàmaxgiữ giá trị lớn hơn chỉ cho field số.
Khi có expectedRevision, nó phải bằng revision hiện tại. Khi bỏ qua, server khóa và thao tác trên trạng thái mới nhất; phù hợp cho increment/max có tính giao hoán. Toàn lô là một transaction; một lỗi rollback mọi người chơi. Mỗi revision thành công tăng đúng một.
{
"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"
}
]
}
#Idempotency trong 30 ngày
Idempotency Production bao trùm toàn bộ dữ liệu chính thức, vì vậy không tái sử dụng requestId giữa các game; Sandbox theo streamer × game hiện tại. Trong 30 ngày, cùng requestId và nội dung y hệt trả kết quả đầu tiên, không ghi hai lần. Cùng ID với nội dung khác trả 409 IDEMPOTENCY_CONFLICT. Sau 30 ngày bản ghi hết hạn; không bao giờ cố ý tái sử dụng UUID. Với event tin cậy, dẫn xuất UUID cho mỗi người chơi từ eventId và chỉ ACK sau khi thành công.
#Mã lỗi
| HTTP | Code | Cách xử lý |
|---|---|---|
| 400 | INVALID_PLAYER_ID | Sửa ID rỗng, quá dài, trùng hoặc encode sai |
| 400 | INVALID_REQUEST_BODY | Gửi JSON POST body parse được với root shape bắt buộc |
| 400 | INVALID_PLAYER_IDS | Gửi 1–100 ID duy nhất; Sandbox ID phải là UUID |
| 400 | INVALID_PLAYER_ATTRIBUTE_MUTATIONS | Sửa lô, UUID hoặc hình dạng operation |
| 400 | INVALID_PLAYER_NAME | Production cần playerName đã trim dài 1–200 ký tự |
| 400 | INVALID_EXPECTED_REVISION | Gửi revision chuỗi thập phân bigint không âm |
| 400 | PLAYER_ATTRIBUTE_NOT_DEFINED | Lấy lại Schema và dùng field đã publish |
| 400 | DUPLICATE_PLAYER_ATTRIBUTE_OPERATION | Mỗi field chỉ thay đổi một lần cho mỗi player |
| 400 | PLAYER_ATTRIBUTE_OPERATION_NOT_ALLOWED | Dùng operation field cho phép |
| 400 | PLAYER_ATTRIBUTE_VALUE_INVALID | Sửa JSON type hoặc enum value |
| 400 | PLAYER_ATTRIBUTE_VALUE_OUT_OF_RANGE | Sửa numeric range hoặc độ dài string |
| 400 | PLAYER_ATTRIBUTES_INVALID | Trạng thái kết quả không phải JSON object hợp lệ |
| 400 | PLAYER_ATTRIBUTES_TOO_LARGE | Giữ trạng thái kết quả không quá 64 KiB |
| 401 | INVALID_ACCESS_TOKEN | Gửi Bearer hợp lệ |
| 401 | INVALID_ACCESS_TOKEN | Refresh hoặc login lại |
| 404 | GAME_NOT_FOUND | Game hoặc active Schema của Token không tồn tại |
| 404 | SANDBOX_PLAYER_NOT_FOUND | Dùng simulation player của Sandbox hiện tại |
| 409 | CURRENT_SEASON_UNAVAILABLE | Game chưa có canonical season active; quản trị viên phải cấu hình hoặc kích hoạt season |
| 409 | PLAYER_ATTRIBUTE_SEASON_RESET_TOO_LARGE | State hiện tại chưa quá giới hạn nhưng projection reset season tương lai vượt 64 KiB; sửa mặc định hoặc rule |
| 409 | PLAYER_ATTRIBUTE_REVISION_CONFLICT | Đọc lại và tính lại |
| 409 | IDEMPOTENCY_CONFLICT | Đối soát request trước; không đổi UUID mù quáng |
| 413 | REQUEST_BODY_TOO_LARGE | Chia nhỏ dưới 128 KiB |
| 500 | INTERNAL_ERROR | Giữ requestId và retry an toàn với backoff |
#Retry, curl, TypeScript và Unity
Retry GET sau lỗi mạng, 429 hoặc 5xx với exponential backoff và jitter. Mutation chỉ được replay bằng nội dung và requestId gốc. Sửa 400/404 trước khi retry; 409 revision cần read lại; 401 chỉ kích hoạt một Session refresh được tuần tự hóa.
curl --fail-with-body \
-H 'Authorization: Bearer <gameApiToken>' \
'https://<platform-origin>/openapi/v2/player-attributes/players/tiktok-user-123'
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: crypto.randomUUID(), 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 không deserialize được object attributes động. Hãy dùng thư viện JSON đã kiểm tra hoặc sinh DTO kiểu mạnh từ Schema.
#Bảo mật, quyền riêng tư và checklist
- Không lưu mật khẩu, Token, credential thanh toán, hồ sơ thô hay dữ liệu nhạy cảm không cần thiết trong attributes.
- Log không chứa Token, player ID, giá trị attributes hay response đầy đủ.
- Chỉ gọi HTTPS origin công khai; không đặt Bearer trong query.
- Login/refresh cài Token nguyên tử; refetch khi Schema version thay đổi.
- Production dùng canonical
sourcePlayerId; Debug chỉ dùng player của Sandbox hiện tại. - Giữ revision là string; gửi
expectedRevisionkhi write phụ thuộc giá trị đã đọc. - Write không chắc chắn chỉ replay request gốc, không đổi requestId.
- Kiểm tra giới hạn batch/128 KiB và xử lý 401, 409, 429, 5xx.
- Mô hình hóa riêng tiền trả phí, quyền sở hữu vật phẩm, lịch sử kiểm toán và tập dữ liệu lớn.