Files
MemeMind-Server/docs/api/game-api.md
2026-04-19 13:27:10 +08:00

1006 lines
28 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-获取游戏配置)
- [8. 获取单个游戏配置](#8-获取单个游戏配置)
- [错误码说明](#错误码说明)
- [接入流程](#接入流程)
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
---
## 概述
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` | `GET /api/v1/user/game-data`(路径不变,响应结构变更) |
| `GET /api/v1/wechat-game/levels` | `GET /api/v1/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 展示。
---
## 接口列表
### 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;
};
completedLevelIds: string[]; // 已通关的关卡 ID 列表
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"stamina": {
"current": 50,
"max": 50,
"nextRecoverAt": null
}
},
"completedLevelIds": ["level_001", "level_002", "level_005"]
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**客户端调用时机**
- 游戏 Loading 页面
- 进入关卡选择页面前
---
### 4. 获取关卡列表
获取所有关卡列表。**已通关的关卡**返回答案、谐音梗说明和线索,**未通关的关卡**不返回敏感数据。
**接口地址**`GET /api/v1/levels`
**是否需要认证**:是
**请求参数**:无
**响应数据**
```typescript
{
levels: LevelListItem[];
total: number;
}
interface LevelListItem {
id: string; // 关卡 ID
level: number; // 关卡编号(从 1 开始)
image1Url: string; // 图片1 URL
image1Description: string | null; // 图片1 文本说明
image2Url: string; // 图片2 URL
image2Description: string | null; // 图片2 文本说明
answer: string | null; // 答案(仅已通关时返回,否则 null
punchline: string | null; // 谐音梗说明(仅已通关时返回,否则 null
hint1: string | null; // 线索1仅已通关时返回否则 null
hint2: string | null; // 线索2仅已通关时返回否则 null
hint3: string | null; // 线索3仅已通关时返回否则 null
completed: boolean; // 是否已通关
timeSpent: number | null; // 通关时长(秒),未通关时为 null
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"levels": [
{
"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": "和某个明星有关",
"hint3": null,
"completed": true,
"timeSpent": 45
},
{
"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": null,
"punchline": null,
"hint1": null,
"hint2": null,
"hint3": null,
"completed": false,
"timeSpent": null
}
],
"total": 2
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**客户端使用说明**
- 关卡选择页面使用此接口获取关卡列表
- 每个关卡有两张图片(`image1Url``image2Url`)和对应的文本说明
- 根据 `completed` 字段展示不同的 UI 状态(已通关/未通关)
- 未通关关卡的 `answer``punchline``hint1``hint2``hint3` 均为 `null`**客户端不应缓存这些字段**
---
### 5. 进入关卡
消耗 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; // 消耗后的体力信息
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"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": "谐音梗:走和跑的故事",
"hint1": "第一个线索",
"hint2": "第二个线索",
"hint3": null,
"stamina": {
"current": 47,
"max": 50,
"nextRecoverAt": "2026-04-10T12:10:00.000Z"
}
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**业务逻辑**
| 场景 | 是否消耗体力 | 说明 |
|------|-------------|------|
| 首次进入未通关关卡 | ✅ 消耗 1 点 | 正常扣减 |
| 再次进入已通关关卡 | ❌ 不消耗 | 直接返回关卡详情 |
| 体力为 0 且关卡未通关 | ❌ 返回错误 | 返回 401 体力不足 |
**客户端调用时机**
- 用户在关卡选择页面点击某个关卡进入时
- **必须**调用此接口获取关卡详情后才能开始游戏
- 客户端应在调用前检查体力是否足够,体力不足时提示用户等待恢复
---
### 6. 通关上报
用户通关后上报通关时长。同一关卡不会重复记录。
**接口地址**`POST /api/v1/levels/{id}/complete`
**是否需要认证**:是
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 关卡 ID |
**请求体**
```json
{
"timeSpent": 45
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| timeSpent | number | 是 | 通关时长(秒),≥ 0 |
**响应数据**
```typescript
{
firstClear: boolean; // 是否为首次通关
levelId: string; // 关卡 ID
timeSpent: number; // 记录的通关时长(秒)
}
```
**成功响应示例(首次通关)**
```json
{
"success": true,
"data": {
"firstClear": true,
"levelId": "level_002",
"timeSpent": 45
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**成功响应示例(重复通关)**
```json
{
"success": true,
"data": {
"firstClear": false,
"levelId": "level_002",
"timeSpent": 30
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**业务逻辑**
- 首次通关:记录 `timeSpent`,返回 `firstClear: true`
- 重复通关:不覆盖记录,返回首次通关的 `timeSpent``firstClear: false`
**客户端调用时机**
- 用户成功回答正确答案后调用
- 只在通关成功时调用,答错不需要上报
---
### 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 → 获取游戏配置(可选) │
└──────────────────────┬───────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 3: 关卡选择页面 │
├──────────────────────────────────────────────────────────────────────┤
│ 6. GET /api/v1/levels → 获取关卡列表(含通关状态) │
│ 7. 展示关卡网格,已通关的显示已完成状态 │
│ 8. 用户点击某个关卡 │
│ ├─ 体力足够 → Phase 4 │
│ └─ 体力不足 → 提示等待恢复(显示 nextRecoverAt 倒计时) │
└──────────────────────┬───────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 4: 关卡游玩 │
├──────────────────────────────────────────────────────────────────────┤
│ 9. POST /api/v1/levels/{id}/enter → 消耗体力,获取关卡详情 │
│ 10. 展示关卡图片,开始计时 │
│ 11. 用户输入答案 │
│ ├─ 答案正确 → 停止计时 │
│ │ └─ POST /api/v1/levels/{id}/complete → 上报通关时长 │
│ └─ 答案错误 → 提示错误,可使用线索 │
└──────────────────────────────────────────────────────────────────────┘
```
### 客户端状态管理建议
```typescript
interface GameState {
// 用户信息
token: string | null;
userId: string | null;
// 体力信息
stamina: {
current: number;
max: number;
nextRecoverAt: string | null;
};
// 关卡数据
completedLevelIds: Set<string>; // 已通关关卡 ID
currentLevel: { // 当前正在玩的关卡
id: string;
answer: string;
punchline: string | null;
hints: (string | null)[];
images: { url: string; description: string | null }[];
startTime: number; // 开始时间戳,用于计算 timeSpent
} | null;
}
```
### 体力恢复倒计时实现建议
```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;
}
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 };
completedLevelIds: string[];
}
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 LevelListItem {
id: string;
level: number;
image1Url: string;
image1Description: string | null;
image2Url: string;
image2Description: string | null;
answer: string | null;
punchline: string | null;
hint1: string | null;
hint2: string | null;
hint3: string | null;
completed: boolean;
timeSpent: number | null;
}
async function getLevels(): Promise<LevelListItem[]> {
const resp = await http.get<{ levels: LevelListItem[]; total: number }>('/v1/levels');
if (resp.success && resp.data) {
return resp.data.levels;
}
throw new Error(resp.message || '获取关卡列表失败');
}
```
### 5. 进入关卡(核心接口)
```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;
}
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);
return resp.data;
}
throw new Error(resp.message || '进入关卡失败');
}
// 调用示例
async function onClickLevel(levelId: string, currentStamina: number, isCompleted: boolean) {
if (!isCompleted && currentStamina <= 0) {
showToast('体力不足,请等待恢复');
return;
}
try {
const levelData = await enterLevel(levelId);
// 跳转到游戏页面,传入关卡数据
startGame(levelData);
} catch (err) {
showToast(err.message);
}
}
```
### 6. 通关上报
```typescript
interface CompleteLevelResponse {
firstClear: boolean;
levelId: string;
timeSpent: number;
}
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('恭喜通关!');
}
return resp.data;
}
throw new Error(resp.message || '上报通关失败');
}
// 调用示例:用户答对时
async function onAnswerCorrect(levelId: string, startTime: number) {
const timeSpent = Math.floor((Date.now() - startTime) / 1000);
const result = await completeLevel(levelId, timeSpent);
// 更新关卡选择页面的状态
}
```
### 7. 完整启动流程
```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.completedLevelIds.length, '关');
// 4. 启动体力恢复倒计时
startStaminaTimer(gameData.user.stamina);
// 5. 获取关卡列表,进入关卡选择页面
const levels = await getLevels();
showLevelSelect(levels);
} 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 字段**:谐音梗说明,仅已通关时返回,未通关时为 `null`
8. **双图结构**:每个关卡有两张图片(`image1Url``image2Url`),分别有对应的文本说明
9. **网络异常处理**:建议所有接口调用加 loading 状态,并处理 401重新登录和网络错误