Files
MemeMind-Server/docs/api/game-api.md
2026-04-26 17:08:27 +08:00

1073 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MemeMind 主玩法 API 协议文档
> 本文档面向微信小游戏客户端Cocos Creator开发人员涵盖认证、用户体力、关卡闯关等核心玩法接口。
## 目录
- [概述](#概述)
- [认证方式](#认证方式)
- [通用响应格式](#通用响应格式)
- [体力值系统说明](#体力值系统说明)
- [通用数据结构](#通用数据结构)
- [接口列表](#接口列表)
- [1. 微信登录](#1-微信登录)
- [2. 获取用户资料](#2-获取用户资料)
- [3. 获取游戏数据](#3-获取游戏数据)
- [4. 进入关卡](#4-进入关卡)
- [5. 通关上报](#5-通关上报)
- [6. 获取游戏配置](#6-获取游戏配置)
- [7. 获取单个游戏配置](#7-获取单个游戏配置)
- [错误码说明](#错误码说明)
- [接入流程](#接入流程)
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
---
## Changelog
- **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 结构:
```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/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"
}
```
---
### 7. 获取单个游戏配置
根据配置 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<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. 微信登录
```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<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. 进入关卡(核心接口)
```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<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. 通关上报
```typescript
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. 完整启动流程
```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` 字段直接告知下一关