Files
MemeMind-Server/docs/api/game-api.md
2026-04-30 16:20:22 +08:00

34 KiB
Raw Permalink Blame History

MemeMind 主玩法 API 协议文档

本文档面向微信小游戏客户端Cocos Creator开发人员涵盖认证、用户体力、关卡闯关等核心玩法接口。

目录


Changelog

  • 2026-04-30

    • 新增 GET /api/v1/levels/completed:获取当前用户已通关关卡列表(按 sortOrder 升序,含完整关卡信息 + 通关时长 + 通关时间)
  • 2026-04-26

    • 删除 GET /api/v1/levels(关卡列表接口),客户端不再需要拉取全量关卡列表
    • 变更 GET /api/v1/user/game-data:移除 completedLevelIds,新增 completedLevelCount(数字)和 nextLevel(下一关完整数据)
    • 变更 POST /api/v1/levels/:id/enter:新增 preloadNextLevel(预加载下一关数据)
    • 变更 POST /api/v1/levels/:id/complete:新增 nextLevel(通关后的下一关数据)

概述

MemeMind 核心玩法接口分为以下模块:

模块 路由前缀 说明
认证 /api/v1/auth 微信登录、JWT 签发
用户 /api/v1/user 用户资料、体力值、游戏数据
关卡 /api/v1/levels 进入关卡、通关上报
游戏配置 /api/v1/game-configs 游戏全局配置

与旧版 API 的变更(⚠️ Breaking Changes

废弃接口 替代方案
GET /api/v1/user/assets GET /api/v1/user/profile
POST /api/v1/user/assets/consume 已删除,体力在「进入关卡」时自动扣减
POST /api/v1/user/assets/earn 已删除,通关不再奖励积分
GET /api/v1/user/game-data(旧版返回 completedLevelIds GET /api/v1/user/game-data(新版返回 completedLevelCount + nextLevel
GET /api/v1/levels 已删除,下一关数据由 game-data / enter / complete 接口直接返回
GET /api/v1/wechat-game/levels 已删除
GET /api/v1/wechat-game/levels/:id POST /api/v1/levels/:id/enter(需鉴权 + 消耗体力)
GET /api/v1/wechat-game/configs GET /api/v1/game-configs
GET /api/v1/wechat-game/configs/:key GET /api/v1/game-configs/:key

认证方式

除微信登录和游戏配置接口外,所有接口均需通过 JWT Token 进行身份认证。

请求头格式

Authorization: Bearer <token>

token 为微信登录接口返回的 JWT 令牌,有效期 7 天


通用响应格式

所有接口均返回以下 JSON 结构:

{
  "success": true,
  "data": { "..." : "..." },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}
字段 类型 说明
success boolean 请求是否成功
data T | null 成功时返回业务数据,失败时为 null
message string | null 失败时的错误信息,成功时为 null
timestamp string 服务器响应时间ISO 8601

体力值系统说明

体力值stamina替代了原有的积分系统用于控制用户进入关卡的频率。

属性
默认体力 50新用户注册时
上限 50
恢复速度 10 分钟 恢复 1 点
消耗 进入未通关关卡时消耗 1 点
已通关关卡 再次进入不消耗体力

体力值数据结构

接口中体力信息统一使用以下结构返回:

interface StaminaInfo {
  current: number;           // 当前体力值(已计算恢复)
  max: number;               // 体力上限,固定为 50
  nextRecoverAt: string | null; // 下一点体力恢复的时间ISO 8601满体力时为 null
}

示例

{
  "current": 45,
  "max": 50,
  "nextRecoverAt": "2026-04-10T12:10:00.000Z"
}

注意:体力恢复为服务端实时计算,无需客户端轮询。每次调用包含体力信息的接口时,服务端都会返回最新的体力值。客户端可根据 nextRecoverAt 自行做倒计时 UI 展示。


通用数据结构

NextLevel — 关卡完整数据

多个接口返回此结构,表示一个关卡的完整内容(供客户端渲染和预加载):

interface NextLevel {
  id: string;                      // 关卡 ID
  level: number;                   // 关卡编号sortOrder
  image1Url: string;               // 图片1 URL
  image1Description: string | null; // 图片1 文本说明
  image2Url: string;               // 图片2 URL
  image2Description: string | null; // 图片2 文本说明
  answer: string;                  // 答案
  punchline: string | null;        // 谐音梗说明
  hint1: string | null;            // 线索1
  hint2: string | null;            // 线索2
  hint3: string | null;            // 线索3
  timeLimit: number | null;        // 限时null 表示不限时
}

出现位置

  • GET /api/v1/user/game-datanextLevel
  • POST /api/v1/levels/:id/enterpreloadNextLevel
  • POST /api/v1/levels/:id/completenextLevel

接口列表

1. 微信登录

获取用户身份令牌。

接口地址POST /api/v1/auth/wx-login

是否需要认证:否

请求体

{
  "code": "微信 wx.login 返回的 code"
}
字段 类型 必填 说明
code string 微信 wx.login 返回的临时登录凭证

响应数据

{
  token: string;
  user: {
    id: string;
    nickname: string | null;
    stamina: number;         // 当前体力值(数据库原始值,不含实时恢复计算)
  }
}

成功响应示例

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "nickname": null,
      "stamina": 50
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

客户端调用时机

  • 小游戏冷启动时
  • 缓存的 token 过期后(收到 401 响应时)

2. 获取用户资料

获取当前用户的资料,包括实时计算后的体力值。

接口地址GET /api/v1/user/profile

是否需要认证:是

请求参数:无

响应数据

{
  id: string;
  nickname: string | null;
  stamina: StaminaInfo;      // 实时体力信息
}

成功响应示例

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "nickname": null,
    "stamina": {
      "current": 45,
      "max": 50,
      "nextRecoverAt": "2026-04-10T12:10:00.000Z"
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

客户端调用时机

  • 需要刷新用户体力显示时
  • 从后台切回前台时

3. 获取游戏数据

获取用户体力值、通关进度和下一关数据,适用于游戏 Loading 页面一次性加载。

接口地址GET /api/v1/user/game-data

是否需要认证:是

请求参数:无

响应数据

{
  user: {
    id: string;
    stamina: StaminaInfo;
  };
  completedLevelCount: number;      // 已通关的关卡数量
  nextLevel: NextLevel | null;      // 下一个待通关的关卡(全部通关时为 null
}

成功响应示例

{
  "success": true,
  "data": {
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "stamina": {
        "current": 50,
        "max": 50,
        "nextRecoverAt": null
      }
    },
    "completedLevelCount": 3,
    "nextLevel": {
      "id": "level_004",
      "level": 4,
      "image1Url": "https://cdn.example.com/levels/004_1.png",
      "image1Description": "一只狗在看电视",
      "image2Url": "https://cdn.example.com/levels/004_2.png",
      "image2Description": "一个遥控器",
      "answer": "汪汪队",
      "punchline": "谐音梗:汪和旺",
      "hint1": "和动物有关",
      "hint2": "和声音有关",
      "hint3": null,
      "timeLimit": null
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

全部通关时的响应示例

{
  "success": true,
  "data": {
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "stamina": { "current": 50, "max": 50, "nextRecoverAt": null }
    },
    "completedLevelCount": 20,
    "nextLevel": null
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

业务逻辑

  • nextLevel 为按 sortOrder 排序的第一个用户未通关的关卡
  • 全部通关时 nextLevelnull,客户端应展示通关庆祝页面

客户端调用时机

  • 游戏 Loading 页面
  • 进入主页面前

4. 进入关卡

消耗 1 点体力进入关卡,获取完整的关卡详情(含答案和线索),并预加载下一关数据。

接口地址POST /api/v1/levels/{id}/enter

是否需要认证:是

路径参数

参数 类型 必填 说明
id string 关卡 ID

请求体:无

响应数据

{
  id: string;
  level: number;
  image1Url: string;
  image1Description: string | null;
  image2Url: string;
  image2Description: string | null;
  answer: string;
  punchline: string | null;
  hint1: string | null;
  hint2: string | null;
  hint3: string | null;
  stamina: StaminaInfo;               // 消耗后的体力信息
  preloadNextLevel: NextLevel | null; // 预加载的下一关数据(无下一关时为 null
}

成功响应示例

{
  "success": true,
  "data": {
    "id": "level_004",
    "level": 4,
    "image1Url": "https://cdn.example.com/levels/004_1.png",
    "image1Description": "一只狗在看电视",
    "image2Url": "https://cdn.example.com/levels/004_2.png",
    "image2Description": "一个遥控器",
    "answer": "汪汪队",
    "punchline": "谐音梗:汪和旺",
    "hint1": "和动物有关",
    "hint2": "和声音有关",
    "hint3": null,
    "stamina": {
      "current": 47,
      "max": 50,
      "nextRecoverAt": "2026-04-10T12:10:00.000Z"
    },
    "preloadNextLevel": {
      "id": "level_005",
      "level": 5,
      "image1Url": "https://cdn.example.com/levels/005_1.png",
      "image1Description": "一个杯子",
      "image2Url": "https://cdn.example.com/levels/005_2.png",
      "image2Description": "一个人在喝水",
      "answer": "杯具",
      "punchline": "谐音梗:杯和悲",
      "hint1": "和容器有关",
      "hint2": null,
      "hint3": null,
      "timeLimit": 60
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

业务逻辑

场景 是否消耗体力 说明
首次进入未通关关卡 消耗 1 点 正常扣减
再次进入已通关关卡 不消耗 直接返回关卡详情
体力为 0 且关卡未通关 返回错误 返回 401 体力不足
  • preloadNextLevel 为按 sortOrder 排在当前关卡之后的第一个未完成关卡
  • 当前关卡是最后一关时,preloadNextLevelnull
  • 客户端可在用户答题时后台预加载 preloadNextLevel 中的图片资源

客户端调用时机

  • 用户在关卡选择页面点击某个关卡进入时
  • 必须调用此接口获取关卡详情后才能开始游戏
  • 客户端应在调用前检查体力是否足够,体力不足时提示用户等待恢复

5. 通关上报

用户通关后上报通关时长,返回下一关数据。同一关卡不会重复记录。

接口地址POST /api/v1/levels/{id}/complete

是否需要认证:是

路径参数

参数 类型 必填 说明
id string 关卡 ID

请求体

{
  "timeSpent": 45
}
字段 类型 必填 说明
timeSpent number 通关时长(秒),≥ 0

响应数据

{
  firstClear: boolean;          // 是否为首次通关
  levelId: string;              // 关卡 ID
  timeSpent: number;            // 记录的通关时长(秒)
  nextLevel: NextLevel | null;  // 下一个待通关的关卡(全部通关时为 null
}

成功响应示例(首次通关)

{
  "success": true,
  "data": {
    "firstClear": true,
    "levelId": "level_004",
    "timeSpent": 45,
    "nextLevel": {
      "id": "level_005",
      "level": 5,
      "image1Url": "https://cdn.example.com/levels/005_1.png",
      "image1Description": "一个杯子",
      "image2Url": "https://cdn.example.com/levels/005_2.png",
      "image2Description": "一个人在喝水",
      "answer": "杯具",
      "punchline": "谐音梗:杯和悲",
      "hint1": "和容器有关",
      "hint2": null,
      "hint3": null,
      "timeLimit": 60
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

成功响应示例(重复通关)

{
  "success": true,
  "data": {
    "firstClear": false,
    "levelId": "level_004",
    "timeSpent": 30,
    "nextLevel": {
      "id": "level_005",
      "level": 5,
      "image1Url": "https://cdn.example.com/levels/005_1.png",
      "image1Description": "一个杯子",
      "image2Url": "https://cdn.example.com/levels/005_2.png",
      "image2Description": "一个人在喝水",
      "answer": "杯具",
      "punchline": "谐音梗:杯和悲",
      "hint1": "和容器有关",
      "hint2": null,
      "hint3": null,
      "timeLimit": 60
    }
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

全部通关时的响应示例

{
  "success": true,
  "data": {
    "firstClear": true,
    "levelId": "level_020",
    "timeSpent": 60,
    "nextLevel": null
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

业务逻辑

  • 首次通关:记录 timeSpent,返回 firstClear: true
  • 重复通关:不覆盖记录,返回首次通关的 timeSpentfirstClear: false
  • nextLevel 为通关后按 sortOrder 排序的第一个未完成关卡
  • 全部通关时 nextLevelnull

客户端调用时机

  • 用户成功回答正确答案后调用
  • 只在通关成功时调用,答错不需要上报
  • 收到响应后,可直接使用 nextLevel 数据进入下一关(调用 enter 接口)

6. 获取已通关关卡列表

获取当前用户所有已通关的关卡,适用于「成就墙」「关卡回看」等场景。

接口地址GET /api/v1/levels/completed

是否需要认证:是

请求参数:无

响应数据

CompletedLevel[]

interface CompletedLevel {
  id: string;                       // 关卡 ID
  level: number;                    // 关卡编号sortOrder
  image1Url: string;
  image1Description: string | null;
  image2Url: string;
  image2Description: string | null;
  answer: string;
  punchline: string | null;
  hint1: string | null;
  hint2: string | null;
  hint3: string | null;
  timeLimit: number | null;
  timeSpent: number;                // 首次通关时长(秒)
  completedAt: string;              // 通关时间ISO 8601
}

成功响应示例

{
  "success": true,
  "data": [
    {
      "id": "level_001",
      "level": 1,
      "image1Url": "https://cdn.example.com/levels/001_1.png",
      "image1Description": "一只猫",
      "image2Url": "https://cdn.example.com/levels/001_2.png",
      "image2Description": "一个头",
      "answer": "猫头鹰",
      "punchline": "谐音梗示例",
      "hint1": "和动物有关",
      "hint2": null,
      "hint3": null,
      "timeLimit": null,
      "timeSpent": 42,
      "completedAt": "2026-04-10T11:58:12.000Z"
    },
    {
      "id": "level_002",
      "level": 2,
      "image1Url": "https://cdn.example.com/levels/002_1.png",
      "image1Description": "一条鱼",
      "image2Url": "https://cdn.example.com/levels/002_2.png",
      "image2Description": "一个缸",
      "answer": "鱼缸",
      "punchline": null,
      "hint1": "容器",
      "hint2": null,
      "hint3": null,
      "timeLimit": null,
      "timeSpent": 31,
      "completedAt": "2026-04-10T12:05:47.000Z"
    }
  ],
  "message": null,
  "timestamp": "2026-04-30T10:00:00.000Z"
}

用户未通关任何关卡时

{
  "success": true,
  "data": [],
  "message": null,
  "timestamp": "2026-04-30T10:00:00.000Z"
}

业务逻辑

  • 返回列表按关卡顺序(sortOrder)升序排列
  • timeSpent首次通关时上报的时长,重复通关不会覆盖
  • 每项包含完整关卡信息,客户端可直接在回看页面渲染图片、答案、线索

客户端调用时机

  • 用户打开「关卡回看 / 成就墙」页面时
  • 不建议在 Loading 阶段调用Loading 使用 game-data 即可)

7. 获取游戏配置

获取所有激活的游戏配置。

接口地址GET /api/v1/game-configs

是否需要认证:否

请求参数:无

响应数据

{
  configs: GameConfig[];
  total: number;
}

interface GameConfig {
  id: string;
  configKey: string;
  configValue: string;
  description: string | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
}

成功响应示例

{
  "success": true,
  "data": {
    "configs": [
      {
        "id": "cfg-001",
        "configKey": "hint_unlock_cost",
        "configValue": "1",
        "description": "解锁提示消耗体力值",
        "isActive": true,
        "createdAt": "2026-04-01T00:00:00.000Z",
        "updatedAt": "2026-04-01T00:00:00.000Z"
      }
    ],
    "total": 1
  },
  "message": null,
  "timestamp": "2026-04-10T12:00:00.000Z"
}

8. 获取单个游戏配置

根据配置 key 获取单个配置。

接口地址GET /api/v1/game-configs/{key}

是否需要认证:否

路径参数

参数 类型 必填 说明
key string 配置键名

响应数据

{
  id: string;
  configKey: string;
  configValue: string;
  description: string | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
}

错误码说明

HTTP Status message 说明
401 未提供访问令牌 请求头缺少 Authorization
401 访问令牌无效或已过期 JWT Token 无效或过期,需重新登录
401 微信登录失败,请重试 微信 code 无效
401 用户不存在 用户 ID 在数据库中不存在
401 体力不足 进入关卡时体力为 0
404 关卡 {id} 不存在 关卡 ID 不存在
404 Game config with key "xxx" not found 配置 key 不存在

接入流程

核心游戏流程

┌──────────────────────────────────────────────────────────────────────┐
│  Phase 1: 启动 & 登录                                                │
├──────────────────────────────────────────────────────────────────────┤
│  1. 小游戏启动,调用 wx.login 获取 code                               │
│  2. POST /api/v1/auth/wx-login → 获取 JWT token                     │
│  3. 缓存 token 到本地                                                │
└──────────────────────┬───────────────────────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Phase 2: Loading 页面                                               │
├──────────────────────────────────────────────────────────────────────┤
│  4. GET /api/v1/user/game-data → 获取体力 + 通关数 + 下一关数据       │
│  5. GET /api/v1/game-configs → 获取游戏配置(可选)                    │
│  6. nextLevel 不为 null → 预加载 nextLevel 的图片资源                 │
│     nextLevel 为 null → 全部通关,展示庆祝页面                        │
└──────────────────────┬───────────────────────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Phase 3: 进入关卡                                                    │
├──────────────────────────────────────────────────────────────────────┤
│  7. 客户端使用 game-data 中的 nextLevel.id 调用 enter 接口             │
│  8. POST /api/v1/levels/{id}/enter → 消耗体力,获取关卡详情             │
│     ├─ 体力足够 → 返回关卡数据 + preloadNextLevel                     │
│     └─ 体力不足 → 提示等待恢复(显示 nextRecoverAt 倒计时)             │
│  9. 后台预加载 preloadNextLevel 的图片资源(如果不为 null              │
└──────────────────────┬───────────────────────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Phase 4: 关卡游玩 & 通关                                             │
├──────────────────────────────────────────────────────────────────────┤
│ 10. 展示关卡图片,开始计时                                             │
│ 11. 用户输入答案                                                      │
│     ├─ 答案正确 → 停止计时                                            │
│     │   └─ POST /api/v1/levels/{id}/complete → 上报 + 获取 nextLevel  │
│     │       ├─ nextLevel 不为 null → 用 nextLevel.id 调用 enter       │
│     │       └─ nextLevel 为 null → 全部通关                           │
│     └─ 答案错误 → 提示错误,可使用线索                                  │
└──────────────────────────────────────────────────────────────────────┘

关卡数据获取链路

game-data.nextLevel → enter(nextLevel.id) → complete(id) → nextLevel
        ↓                    ↓                                ↓
    首关数据            preloadNextLevel                  下一关数据
                     (用于图片预加载)               (用于无缝衔接)

客户端不需要维护关卡列表,服务端在每个接口中直接告知下一关。

客户端状态管理建议

interface GameState {
  // 用户信息
  token: string | null;
  userId: string | null;

  // 体力信息
  stamina: StaminaInfo;

  // 关卡进度
  completedLevelCount: number;

  // 当前关卡
  currentLevel: NextLevel | null;   // 来自 game-data.nextLevel 或 complete.nextLevel

  // 预加载的下一关
  preloadedLevel: NextLevel | null; // 来自 enter.preloadNextLevel

  // 游戏中状态
  startTime: number | null;         // 开始时间戳,用于计算 timeSpent
}

体力恢复倒计时实现建议

// 客户端体力恢复倒计时(纯 UI 展示)
function startStaminaTimer(staminaInfo: StaminaInfo) {
  if (!staminaInfo.nextRecoverAt || staminaInfo.current >= staminaInfo.max) {
    // 满体力,无需倒计时
    return;
  }

  const targetTime = new Date(staminaInfo.nextRecoverAt).getTime();

  const timer = setInterval(() => {
    const remaining = targetTime - Date.now();
    if (remaining <= 0) {
      // 恢复一点体力(本地模拟)
      staminaInfo.current = Math.min(staminaInfo.current + 1, staminaInfo.max);
      clearInterval(timer);

      if (staminaInfo.current < staminaInfo.max) {
        // 继续下一轮倒计时
        staminaInfo.nextRecoverAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
        startStaminaTimer(staminaInfo);
      } else {
        staminaInfo.nextRecoverAt = null;
      }
    } else {
      // 更新倒计时 UI
      const minutes = Math.floor(remaining / 60000);
      const seconds = Math.floor((remaining % 60000) / 1000);
      updateStaminaUI(`${minutes}:${seconds.toString().padStart(2, '0')}`);
    }
  }, 1000);
}

⚠️ 客户端体力倒计时仅用于 UI 展示。实际体力值以服务端返回为准,每次调用接口时会获得最新体力。


Cocos Creator 调用示例

1. HTTP 请求工具类

// HttpManager.ts
export interface ApiResponse<T> {
  success: boolean;
  data: T | null;
  message: string | null;
  timestamp: string;
}

export interface StaminaInfo {
  current: number;
  max: number;
  nextRecoverAt: string | null;
}

export interface NextLevel {
  id: string;
  level: number;
  image1Url: string;
  image1Description: string | null;
  image2Url: string;
  image2Description: string | null;
  answer: string;
  punchline: string | null;
  hint1: string | null;
  hint2: string | null;
  hint3: string | null;
  timeLimit: number | null;
}

class HttpManager {
  private baseUrl = 'https://your-api-domain.com/api';
  private token: string | null = null;

  setToken(token: string) {
    this.token = token;
  }

  getToken(): string | null {
    return this.token;
  }

  async request<T>(
    method: 'GET' | 'POST',
    url: string,
    body?: object
  ): Promise<ApiResponse<T>> {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(method, this.baseUrl + url, true);
      xhr.setRequestHeader('Content-Type', 'application/json');

      if (this.token) {
        xhr.setRequestHeader('Authorization', `Bearer ${this.token}`);
      }

      xhr.onload = () => {
        try {
          const resp = JSON.parse(xhr.responseText);
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(resp);
          } else if (xhr.status === 401) {
            // Token 过期,触发重新登录
            this.token = null;
            reject(new Error(resp.message || '登录已过期,请重新登录'));
          } else {
            reject(new Error(resp.message || `请求失败: ${xhr.status}`));
          }
        } catch {
          reject(new Error(`请求失败: ${xhr.status}`));
        }
      };

      xhr.onerror = () => reject(new Error('网络错误'));
      xhr.send(body ? JSON.stringify(body) : undefined);
    });
  }

  get<T>(url: string) { return this.request<T>('GET', url); }
  post<T>(url: string, body?: object) { return this.request<T>('POST', url, body); }
}

export const http = new HttpManager();

2. 微信登录

async function wxLogin() {
  const { code } = await new Promise<{ code: string }>((resolve, reject) => {
    wx.login({ success: (res) => resolve({ code: res.code }), fail: reject });
  });

  const resp = await http.post<{
    token: string;
    user: { id: string; nickname: string | null; stamina: number };
  }>('/v1/auth/wx-login', { code });

  if (resp.success && resp.data) {
    http.setToken(resp.data.token);
    wx.setStorageSync('jwt_token', resp.data.token);
    return resp.data;
  }
  throw new Error(resp.message || '登录失败');
}

3. Loading 页面加载游戏数据

interface GameData {
  user: { id: string; stamina: StaminaInfo };
  completedLevelCount: number;
  nextLevel: NextLevel | null;
}

async function loadGameData(): Promise<GameData> {
  const resp = await http.get<GameData>('/v1/user/game-data');
  if (resp.success && resp.data) {
    return resp.data;
  }
  throw new Error(resp.message || '加载游戏数据失败');
}

4. 进入关卡(核心接口)

interface EnterLevelResponse {
  id: string;
  level: number;
  image1Url: string;
  image1Description: string | null;
  image2Url: string;
  image2Description: string | null;
  answer: string;
  punchline: string | null;
  hint1: string | null;
  hint2: string | null;
  hint3: string | null;
  stamina: StaminaInfo;
  preloadNextLevel: NextLevel | null;
}

async function enterLevel(levelId: string): Promise<EnterLevelResponse> {
  const resp = await http.post<EnterLevelResponse>(`/v1/levels/${levelId}/enter`);

  if (resp.success && resp.data) {
    // 更新本地体力状态
    updateLocalStamina(resp.data.stamina);

    // 后台预加载下一关图片
    if (resp.data.preloadNextLevel) {
      preloadImages([
        resp.data.preloadNextLevel.image1Url,
        resp.data.preloadNextLevel.image2Url,
      ]);
    }

    return resp.data;
  }
  throw new Error(resp.message || '进入关卡失败');
}

5. 通关上报

interface CompleteLevelResponse {
  firstClear: boolean;
  levelId: string;
  timeSpent: number;
  nextLevel: NextLevel | null;
}

async function completeLevel(
  levelId: string,
  timeSpent: number
): Promise<CompleteLevelResponse> {
  const resp = await http.post<CompleteLevelResponse>(
    `/v1/levels/${levelId}/complete`,
    { timeSpent }
  );

  if (resp.success && resp.data) {
    if (resp.data.firstClear) {
      showToast('恭喜通关!');
    }

    if (resp.data.nextLevel) {
      // 有下一关,可以直接进入
      const nextData = await enterLevel(resp.data.nextLevel.id);
      startGame(nextData);
    } else {
      // 全部通关
      showCelebration();
    }

    return resp.data;
  }
  throw new Error(resp.message || '上报通关失败');
}

6. 完整启动流程

// GameEntry.ts
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;

@ccclass('GameEntry')
export class GameEntry extends Component {
  async start() {
    try {
      // 1. 尝试使用缓存 token
      const cachedToken = wx.getStorageSync('jwt_token');
      if (cachedToken) {
        http.setToken(cachedToken);
      }

      // 2. 登录(获取最新 token
      const loginData = await wxLogin();
      console.log('登录成功:', loginData.user.id);

      // 3. 加载游戏数据(含下一关)
      const gameData = await loadGameData();
      console.log('体力:', gameData.user.stamina.current);
      console.log('已通关:', gameData.completedLevelCount, '关');

      // 4. 启动体力恢复倒计时
      startStaminaTimer(gameData.user.stamina);

      // 5. 根据 nextLevel 决定流程
      if (gameData.nextLevel) {
        // 预加载下一关图片
        preloadImages([
          gameData.nextLevel.image1Url,
          gameData.nextLevel.image2Url,
        ]);

        // 进入游戏主页面,展示下一关入口
        showMainPage(gameData);
      } else {
        // 全部通关
        showCelebration();
      }

    } catch (error) {
      console.error('启动失败:', error);
      showRetryDialog();
    }
  }
}

注意事项

  1. Token 有效期JWT Token 有效期 7 天,建议本地缓存并在启动时尝试复用
  2. 体力值以服务端为准:客户端倒计时仅为 UI 展示,实际体力以接口返回为准
  3. 进入关卡必须调用接口:不要使用缓存的关卡数据直接开始游戏,必须调用 enter 接口
  4. 已通关关卡免费进入:已通关关卡再次进入不消耗体力
  5. 通关上报仅限成功:只在用户答对后调用 complete 接口,答错不需要上报
  6. hint 字段hint1/hint2/hint3 可能为 null,表示该线索未配置
  7. punchline 字段:谐音梗说明
  8. 双图结构:每个关卡有两张图片(image1Urlimage2Url),分别有对应的文本说明
  9. 网络异常处理:建议所有接口调用加 loading 状态,并处理 401重新登录和网络错误
  10. 预加载策略enter 返回的 preloadNextLevel 用于图片预加载,在用户答题时后台加载下一关图片可优化体验
  11. 关卡列表已废弃:不再需要 GET /api/v1/levels,服务端通过 nextLevel 字段直接告知下一关