# MemeMind 主玩法 API 协议文档 > 本文档面向微信小游戏客户端(Cocos Creator)开发人员,涵盖认证、用户体力、关卡闯关等核心玩法接口。 ## 目录 - [概述](#概述) - [认证方式](#认证方式) - [通用响应格式](#通用响应格式) - [体力值系统说明](#体力值系统说明) - [通用数据结构](#通用数据结构) - [接口列表](#接口列表) - [1. 微信登录](#1-微信登录) - [2. 获取用户资料](#2-获取用户资料) - [3. 获取游戏数据](#3-获取游戏数据) - [4. 进入关卡](#4-进入关卡) - [5. 通关上报](#5-通关上报) - [6. 获取已通关关卡列表](#6-获取已通关关卡列表) - [7. 获取游戏配置](#7-获取游戏配置) - [8. 获取单个游戏配置](#8-获取单个游戏配置) - [错误码说明](#错误码说明) - [接入流程](#接入流程) - [Cocos Creator 调用示例](#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` 为微信登录接口返回的 JWT 令牌,有效期 **7 天**。 --- ## 通用响应格式 所有接口均返回以下 JSON 结构: ```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 点 | | 已通关关卡 | 再次进入不消耗体力 | ### 体力值数据结构 接口中体力信息统一使用以下结构返回: ```typescript interface StaminaInfo { current: number; // 当前体力值(已计算恢复) max: number; // 体力上限,固定为 50 nextRecoverAt: string | null; // 下一点体力恢复的时间(ISO 8601),满体力时为 null } ``` **示例**: ```json { "current": 45, "max": 50, "nextRecoverAt": "2026-04-10T12:10:00.000Z" } ``` > **注意**:体力恢复为服务端实时计算,无需客户端轮询。每次调用包含体力信息的接口时,服务端都会返回最新的体力值。客户端可根据 `nextRecoverAt` 自行做倒计时 UI 展示。 --- ## 通用数据结构 ### NextLevel — 关卡完整数据 多个接口返回此结构,表示一个关卡的完整内容(供客户端渲染和预加载): ```typescript 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-data` → `nextLevel` - `POST /api/v1/levels/:id/enter` → `preloadNextLevel` - `POST /api/v1/levels/:id/complete` → `nextLevel` --- ## 接口列表 ### 1. 微信登录 获取用户身份令牌。 **接口地址**:`POST /api/v1/auth/wx-login` **是否需要认证**:否 **请求体**: ```json { "code": "微信 wx.login 返回的 code" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | code | string | 是 | 微信 `wx.login` 返回的临时登录凭证 | **响应数据**: ```typescript { token: string; user: { id: string; nickname: string | null; stamina: number; // 当前体力值(数据库原始值,不含实时恢复计算) } } ``` **成功响应示例**: ```json { "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` **是否需要认证**:是 **请求参数**:无 **响应数据**: ```typescript { id: string; nickname: string | null; stamina: StaminaInfo; // 实时体力信息 } ``` **成功响应示例**: ```json { "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` **是否需要认证**:是 **请求参数**:无 **响应数据**: ```typescript { user: { id: string; stamina: StaminaInfo; }; completedLevelCount: number; // 已通关的关卡数量 nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null) } ``` **成功响应示例**: ```json { "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" } ``` **全部通关时的响应示例**: ```json { "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` 排序的第一个用户未通关的关卡 - 全部通关时 `nextLevel` 为 `null`,客户端应展示通关庆祝页面 **客户端调用时机**: - 游戏 Loading 页面 - 进入主页面前 --- ### 4. 进入关卡 消耗 1 点体力进入关卡,获取完整的关卡详情(含答案和线索),并预加载下一关数据。 **接口地址**:`POST /api/v1/levels/{id}/enter` **是否需要认证**:是 **路径参数**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | id | string | 是 | 关卡 ID | **请求体**:无 **响应数据**: ```typescript { 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) } ``` **成功响应示例**: ```json { "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` 排在当前关卡之后的第一个未完成关卡 - 当前关卡是最后一关时,`preloadNextLevel` 为 `null` - 客户端可在用户答题时后台预加载 `preloadNextLevel` 中的图片资源 **客户端调用时机**: - 用户在关卡选择页面点击某个关卡进入时 - **必须**调用此接口获取关卡详情后才能开始游戏 - 客户端应在调用前检查体力是否足够,体力不足时提示用户等待恢复 --- ### 5. 通关上报 用户通关后上报通关时长,返回下一关数据。同一关卡不会重复记录。 **接口地址**:`POST /api/v1/levels/{id}/complete` **是否需要认证**:是 **路径参数**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | id | string | 是 | 关卡 ID | **请求体**: ```json { "timeSpent": 45 } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | timeSpent | number | 是 | 通关时长(秒),≥ 0 | **响应数据**: ```typescript { firstClear: boolean; // 是否为首次通关 levelId: string; // 关卡 ID timeSpent: number; // 记录的通关时长(秒) nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null) } ``` **成功响应示例(首次通关)**: ```json { "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" } ``` **成功响应示例(重复通关)**: ```json { "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" } ``` **全部通关时的响应示例**: ```json { "success": true, "data": { "firstClear": true, "levelId": "level_020", "timeSpent": 60, "nextLevel": null }, "message": null, "timestamp": "2026-04-10T12:00:00.000Z" } ``` **业务逻辑**: - 首次通关:记录 `timeSpent`,返回 `firstClear: true` - 重复通关:不覆盖记录,返回首次通关的 `timeSpent`,`firstClear: false` - `nextLevel` 为通关后按 `sortOrder` 排序的第一个未完成关卡 - 全部通关时 `nextLevel` 为 `null` **客户端调用时机**: - 用户成功回答正确答案后调用 - 只在通关成功时调用,答错不需要上报 - 收到响应后,可直接使用 `nextLevel` 数据进入下一关(调用 `enter` 接口) --- ### 6. 获取已通关关卡列表 获取当前用户所有已通关的关卡,适用于「成就墙」「关卡回看」等场景。 **接口地址**:`GET /api/v1/levels/completed` **是否需要认证**:是 **请求参数**:无 **响应数据**: ```typescript 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) } ``` **成功响应示例**: ```json { "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" } ``` **用户未通关任何关卡时**: ```json { "success": true, "data": [], "message": null, "timestamp": "2026-04-30T10:00:00.000Z" } ``` **业务逻辑**: - 返回列表按关卡顺序(`sortOrder`)升序排列 - `timeSpent` 为**首次通关**时上报的时长,重复通关不会覆盖 - 每项包含完整关卡信息,客户端可直接在回看页面渲染图片、答案、线索 **客户端调用时机**: - 用户打开「关卡回看 / 成就墙」页面时 - 不建议在 Loading 阶段调用(Loading 使用 `game-data` 即可) --- ### 7. 获取游戏配置 获取所有激活的游戏配置。 **接口地址**:`GET /api/v1/game-configs` **是否需要认证**:否 **请求参数**:无 **响应数据**: ```typescript { configs: GameConfig[]; total: number; } interface GameConfig { id: string; configKey: string; configValue: string; description: string | null; isActive: boolean; createdAt: string; updatedAt: string; } ``` **成功响应示例**: ```json { "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 | 是 | 配置键名 | **响应数据**: ```typescript { 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 下一关数据 (用于图片预加载) (用于无缝衔接) ``` 客户端不需要维护关卡列表,服务端在每个接口中直接告知下一关。 ### 客户端状态管理建议 ```typescript 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 } ``` ### 体力恢复倒计时实现建议 ```typescript // 客户端体力恢复倒计时(纯 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 请求工具类 ```typescript // HttpManager.ts export interface ApiResponse { 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( method: 'GET' | 'POST', url: string, body?: object ): Promise> { 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(url: string) { return this.request('GET', url); } post(url: string, body?: object) { return this.request('POST', url, body); } } export const http = new HttpManager(); ``` ### 2. 微信登录 ```typescript 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 页面加载游戏数据 ```typescript interface GameData { user: { id: string; stamina: StaminaInfo }; completedLevelCount: number; nextLevel: NextLevel | null; } async function loadGameData(): Promise { const resp = await http.get('/v1/user/game-data'); if (resp.success && resp.data) { return resp.data; } throw new Error(resp.message || '加载游戏数据失败'); } ``` ### 4. 进入关卡(核心接口) ```typescript 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 { const resp = await http.post(`/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. 通关上报 ```typescript interface CompleteLevelResponse { firstClear: boolean; levelId: string; timeSpent: number; nextLevel: NextLevel | null; } async function completeLevel( levelId: string, timeSpent: number ): Promise { const resp = await http.post( `/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. 完整启动流程 ```typescript // 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. **双图结构**:每个关卡有两张图片(`image1Url`、`image2Url`),分别有对应的文本说明 9. **网络异常处理**:建议所有接口调用加 loading 状态,并处理 401(重新登录)和网络错误 10. **预加载策略**:`enter` 返回的 `preloadNextLevel` 用于图片预加载,在用户答题时后台加载下一关图片可优化体验 11. **关卡列表已废弃**:不再需要 `GET /api/v1/levels`,服务端通过 `nextLevel` 字段直接告知下一关