小游戏客户端云存档接入标准 管理后台 · 接口文档 · 开发者文档 · Markdown 源文件

小游戏客户端云存档接入标准

适用:当前 ZYMIX 后台;核对日期:2026-09-11。本文面向小游戏客户端开发人员,示例使用 Number Colour。字段、状态码以当前部署实现为准。

1. 接入参数

参数 Number Colour 当前值
baseUrl http://192.168.1.200:18080
appId mgrsy6xpjrothyr9
platform wx(本项目 TCMPP SDK 使用 wx 接口)
存档 key bfbxc_user_data
登录方法 POST /api/projects/mgrsy6xpjrothyr9/login
读取方法 GET /api/projects/mgrsy6xpjrothyr9/data/bfbxc_user_data
保存方法 PUT /api/projects/mgrsy6xpjrothyr9/data/bfbxc_user_data

当前地址供局域网联调。正式分发使用配置好的 HTTPS 服务域名及小游戏平台合法请求域名。

客户端只保存 baseUrl、appId 和业务 token。不要把 AppSecret、后台管理员账号或平台 session_key 放进游戏。游戏请求本后台的 login,后台读取项目配置中的 OpenServer 完整地址去换码。

2. 必须遵循的调用顺序

1. 调用平台 wx.login 获取新的 code。

2. 将 code 提交给后台 login,成功后保存 token、expiresAt、user.userId。

3. 携带 token 读取存档。只有读取成功,才能确定存档是否存在及当前 version。

4. exists:false 时创建本地初始状态;exists:true 时校验并加载 value

5. 保存时提交完整 value 和最近一次读取或保存成功得到的 version

6. 保存成功后立即更新客户端 version,再允许同一 key 的下一次保存。

登录失败、读取失败与“首次没有存档”是三种不同状态。网络失败或 401 时不得假设 version=0,更不能把默认存档上传覆盖已有进度。可继续离线游玩,但必须保留本地待同步状态,恢复身份并读取云端后再处理合并。

3. 自动登录与身份

请求不需要已有 token:

POST /api/projects/mgrsy6xpjrothyr9/login
Content-Type: application/json

{"code":"平台刚返回的code","platform":"wx"}

成功:HTTP 200。

{
  "ok": true,
  "token": "服务器签发的业务token",
  "expiresAt": "2026-09-18T09:00:00.000Z",
  "user": {"userId": "usr_example", "userInfo": {}}
}

后续存档请求必须携带:

Authorization: Bearer 服务器签发的业务token

小游戏使用业务 Bearer token,不使用管理员登录 Cookie。服务器根据 token 确定玩家,客户端不用提交 openid、userId;提交这些字段也不能改变存档归属。存档按项目、玩家、key 隔离;platform 必须保持一致,不能在 wx 和 tcmpp 之间随意切换,否则当前身份派生规则会产生不同 userId。

以返回的 expiresAt 为准安排重新登录,可提前留出时间;应用回到前台也应检查。当前没有小游戏 token refresh 接口,续期需要新的平台 code 再调用 login。并发请求只启动一个重新登录任务,其余等待。code 不可复用,即使首次请求超时也重新获取。重新登录后若 userId 改变,切换到该账号独立的本地状态,不能上传前一个玩家的存档。

4. 读取存档

GET /api/projects/mgrsy6xpjrothyr9/data/bfbxc_user_data
Authorization: Bearer <token>

首次无存档仍返回 HTTP 200,不是 404:

{
  "ok": true,
  "key": "bfbxc_user_data",
  "exists": false,
  "value": null,
  "version": 0,
  "updatedAt": null
}

已有存档返回 HTTP 200:

{
  "ok": true,
  "key": "bfbxc_user_data",
  "exists": true,
  "value": {"schemaVersion": 1, "level": 12, "coins": 80},
  "version": 3,
  "updatedAt": "2026-09-11T09:00:00.000Z"
}

schemaVersion 是推荐由游戏自行管理的存档格式版本,用于数据结构迁移;顶层 version 是服务器维护的并发控制版本。两者不是一回事。

响应字段 说明
ok 成功为 true;先检查 HTTP 状态和 ok,再读取业务字段
exists 仅 GET 返回;以它判断是否存在,不能用 value 的真假判断
value 游戏保存的 JSON 值;推荐对象。接口也支持数组、字符串、数字、布尔和 null
version 无存档为 0;已有存档从 1 开始,每次成功保存增加 1
updatedAt 仅 GET 返回;最后保存时间,无存档为 null

HTTP 响应 JSON 的业务字段直接在顶层。 使用 wx.request 时通常是 res.data.valueres.data.version;不要读取 res.data.data.value。如果你的网络封装已经返回了响应体,则读取 body.value

5. 写入存档

PUT /api/projects/mgrsy6xpjrothyr9/data/bfbxc_user_data
Content-Type: application/json
Authorization: Bearer <token>

{"version":3,"value":{"schemaVersion":1,"level":13,"coins":90}}

成功:HTTP 200。

{
  "ok": true,
  "key": "bfbxc_user_data",
  "value": {"schemaVersion": 1, "level": 13, "coins": 90},
  "version": 4
}

客户端把 version 更新为 响应中的 4,下一次保存提交 4。不要自己预先递增,也不要每次固定传 0。PUT 成功响应没有 exists、updatedAt,不要依赖这两个字段。

约束 标准
version 必填 JSON 数字,非负安全整数;"3" 字符串、null、负数、小数、缺失均不合法
value 必须有这个字段;undefined 会在 JSON 序列化时被丢弃;显式 null 合法,但会保存一个存在且值为 null 的记录
key 非空,最长 128 字符;推荐只用字母、数字、下划线、连字符,例如 bfbxc_user_data
请求体 JSON 对象,整体最多 1 MiB;推荐显著小于此值
写入语义 整个 value 替换;不是字段级 PATCH,不会自动合并
删除 当前没有存档 DELETE 接口;写入 null 不等于删除,也不会把 version 重置为 0
持久化 MySQL 事务提交成功后才返回成功;Redis 负责登录态和配置缓存,云存档本体存于 MySQL

金币等内容只是玩家提交的存档,不应当作服务端可信余额或发奖凭据。

6. 428、409 与重试

截图中的 HTTP 428 对应响应:

{"ok":false,"code":"data_version_required","message":"data_version_required"}

这表示 PUT 缺少合法的顶层 version。它不是登录失败。 应改成 {version: 已知版本, value: 存档对象}。首次必须先 GET;确认 exists=false 后才用 version=0 创建。

HTTP code / 情况 客户端处理
400 data_value_required 请求缺少 value,修复序列化或请求结构
400 data_key_invalid / 非法 JSON 修复 key 或 JSON;非法 JSON 当前 code 为 request_error
401 request_error / session_expired token 缺失、错误、过期或会话不存在;保留本地存档,重新登录后再读取
401 project_disabled 重新核对项目状态,不无限循环登录
404 project_not_found / not_found 检查 AppID、项目启用状态、路径;不当作无存档
409 data_version_conflict 当前版本已变化;重新 GET,再按游戏业务规则处理差异
413 request_too_large 请求体过大,缩减存档
428 data_version_required 修复 version 缺失或类型;不盲目重试
429 限流 合并保存请求,退避并加入随机抖动;不要紧密循环
502 / 504 登录换码上游错误 / 超时 暂停云同步,记录响应 code 和数字 providerCode;重试登录需新平台 code
503 / 网络超时 服务不可用 / 请求结果未知 保留待同步状态,恢复后先读取云端,确认是否已经提交

同一玩家同一 key 的 PUT 必须串行。建议合并频繁变化、在关卡完成等关键点保存,避免每帧上传。切后台请求可能来不及完成,应先保存本地副本并保留未同步标记。

两个设备都读到 version=3 时,只有一个 PUT 能成功并得到 4;另一个返回 409。冲突不会返回最新 value,必须重新 GET。不要仅换成新 version 就重发旧 value,否则仍会覆盖另一设备的进度。关卡解锁、消耗品、奖励等字段需要各自的合并规则;无法自动合并时让玩家选择存档。

PUT 超时也可能已经写入成功。不要把“没收到成功响应”等同于“没保存”;先 GET,结合待提交快照判断。当前没有存档写入幂等请求 ID;必要时由游戏在 value 中保存自己的操作标识辅助核对,但服务器不会替它执行幂等检查。

7. JSON.parse 与客户端参考代码

截图里的 Unexpected token u in JSON at position 0 通常是对 undefined 或字符串 "undefined" 调用了 JSON.parse。它是客户端异常,不能用取消鉴权或固定 version 解决。

规则:只在收到的内容是 JSON 字符串时解析。对象不要重复 JSON.parse。响应失败不进入存档解析;exists=false 时使用默认状态。历史存档如果保存为字符串,可以做一次明确的兼容解析;解析失败应保留原始本地副本并报告,不能默认清档后立即上传。

下面示例展示请求层、登录、读取和单次保存。调用方必须按顺序 await,同一 key 不可并发 save;冲突/离线合并及本地备份由游戏业务层处理。示例中的 level、coins、schemaVersion 仅为说明,应替换成游戏实际结构。

const BASE_URL = 'http://192.168.1.200:18080';
const APP_ID = 'mgrsy6xpjrothyr9';
const KEY = 'bfbxc_user_data';
let token = '';
let version = null; // null = 尚未成功读取;不能默认为 0

function request(method, path, data, authenticated = true) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: BASE_URL + path,
      method,
      header: {
        'content-type': 'application/json',
        ...(authenticated ? { Authorization: 'Bearer ' + token } : {})
      },
      ...(data === undefined ? {} : { data }),
      success(res) {
        let body;
        try {
          body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
        } catch (_) {
          reject(new Error('invalid_response_json'));
          return;
        }
        if (res.statusCode < 200 || res.statusCode >= 300 || !body || body.ok !== true) {
          const error = new Error(body?.message || 'request_failed');
          error.status = res.statusCode;
          error.code = body?.code;
          error.details = body?.details;
          reject(error);
          return;
        }
        resolve(body); // 返回体本身,不再套一层 data
      },
      fail: reject
    });
  });
}

async function login() {
  token = '';
  version = null;
  const platformResult = await new Promise((resolve, reject) => {
    wx.login({ success: resolve, fail: reject });
  });
  if (!platformResult.code) throw new Error('platform_code_missing');
  const body = await request('POST', '/api/projects/' + APP_ID + '/login', {
    code: platformResult.code, platform: 'wx'
  }, false);
  token = body.token;
  return { userId: body.user.userId, expiresAt: body.expiresAt };
}

const savePath = '/api/projects/' + APP_ID + '/data/' + encodeURIComponent(KEY);

async function load() {
  version = null;
  const body = await request('GET', savePath);
  // 仅示例:游戏应按自己的 schema 完整校验及迁移。
  let value = body.exists ? body.value : { schemaVersion: 1, level: 1, coins: 0 };
  if (body.exists && typeof value === 'string') value = JSON.parse(value);
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error('invalid_save_schema');
  }
  version = body.version; // 数据校验通过后才启用保存
  return value;
}

async function save(value) {
  if (version === null) throw new Error('load_required_before_save');
  try {
    const body = await request('PUT', savePath, { version, value });
    version = body.version;
    return body.value;
  } catch (error) {
    version = null; // 冲突或结果未知后,禁止继续使用旧版本写入
    throw error; // 调用方保留待同步快照,读取后按业务规则合并
  }
}

// 游戏启动时由调用方捕获异常:
// await login();
// const state = await load();
// state.level += 1;
// await save(state);
// 下一次 save 必须等待上一次结束。

8. 客户端联调验收清单

场景 应观察到的结果
新玩家首次进入 login 200 → GET 200 exists=false/version=0 → PUT 200/version=1
已有玩家再次进入 GET 取得之前 value 和 version,不用默认状态覆盖
连续两次保存 按响应版本依次提交,版本递增,无 428
缺失 version PUT 428,保留本地进度并明确记录错误
另一设备修改 PUT 409,重新 GET 并执行已定义的冲突策略
token 失效 暂停云写入、单次重新登录、读取后恢复同步
断网或 PUT 超时 本地进度保留;恢复后先核对云端,不直接盲写
无存档 / JSON 对象 不触发 JSON.parse(undefined) 或重复解析
换玩家 本地副本按 appId/platform/userId/key 隔离,不串档

排障只记录方法、路径、HTTP 状态、响应 code、providerCode、存档 version 和脱敏上下文;不打印 token、平台 code、AppSecret 或完整玩家存档。

相关文档:接口总览TCMPP 登录配置部署运维