refactor: 拆分核心玩法模块并优化代码质量

将 WechatGame 单体模块拆分为独立的 User、Level、GameConfig 模块,
新增体力值系统、关卡闯关流程,并修复多项代码质量问题:
- 体力不足错误码从 401 修正为 400
- enterLevel 改用 findById 替代全表扫描
- consumeStamina 增加原子更新防止并发竞态
- 并行化独立数据库查询 (Promise.all)
- 移除 WechatGameService/Controller 死代码
This commit is contained in:
richarjiang
2026-04-10 09:07:50 +08:00
parent c775d5c6b0
commit fe2c13258e
33 changed files with 1681 additions and 978 deletions

View File

@@ -6,10 +6,8 @@
| 模块 | 文档文件 | 说明 | 状态 |
|------|----------|------|------|
| 用户认证 | [auth-api.md](./auth-api.md) | 微信登录、JWT Token | 待编写 |
| 分享挑战 | [share-challenge-api.md](./share-challenge-api.md) | 创建分享、加入挑战、进度上报 | 已完成 |
| 游戏关卡 | [game-api.md](./game-api.md) | 关卡数据、答案验证 | 待编写 |
| 用户资产 | [user-assets-api.md](./user-assets-api.md) | 积分获取与消耗 | 待编写 |
| 核心玩法 | [game-api.md](./game-api.md) | 认证、体力值、关卡闯关、游戏配置 | ✅ 已完成 |
| 分享挑战 | [share-challenge-api.md](./share-challenge-api.md) | 创建分享、加入挑战、进度上报 | 已完成 |
| 排行榜 | [leaderboard-api.md](./leaderboard-api.md) | 排名、分数上报 | 预留 |
## 文档维护规则
@@ -37,9 +35,9 @@ Authorization: Bearer <token>
```json
{
"success": true,
"data": { ... },
"data": { "..." : "..." },
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
"timestamp": "2026-04-10T12:00:00.000Z"
}
```

972
docs/api/game-api.md Normal file
View File

@@ -0,0 +1,972 @@
# 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替代了原有的积分系统用于控制用户进入关卡的频率。
| 属性 | 值 |
|------|-----|
| 默认体力 | 5新用户注册时 |
| 上限 | 5 |
| 恢复速度 | 每 **10 分钟** 恢复 1 点 |
| 消耗 | 进入**未通关**关卡时消耗 1 点 |
| 已通关关卡 | 再次进入不消耗体力 |
### 体力值数据结构
接口中体力信息统一使用以下结构返回:
```typescript
interface StaminaInfo {
current: number; // 当前体力值(已计算恢复)
max: number; // 体力上限,固定为 5
nextRecoverAt: string | null; // 下一点体力恢复的时间ISO 8601满体力时为 null
}
```
**示例**
```json
{
"current": 3,
"max": 5,
"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": 5
}
},
"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": 3,
"max": 5,
"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": 5,
"max": 5,
"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 开始)
imageUrl: string; // 关卡图片 URL
answer: 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,
"imageUrl": "https://cdn.example.com/levels/001.png",
"answer": "梗答案",
"hint1": "这是一个经典的...",
"hint2": "和某个明星有关",
"hint3": null,
"completed": true,
"timeSpent": 45
},
{
"id": "level_002",
"level": 2,
"imageUrl": "https://cdn.example.com/levels/002.png",
"answer": null,
"hint1": null,
"hint2": null,
"hint3": null,
"completed": false,
"timeSpent": null
}
],
"total": 2
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
**客户端使用说明**
- 关卡选择页面使用此接口获取关卡列表
- 根据 `completed` 字段展示不同的 UI 状态(已通关/未通关)
- 未通关关卡的 `answer``hint1``hint2``hint3` 均为 `null`**客户端不应缓存这些字段**
---
### 5. 进入关卡
消耗 1 点体力进入关卡,获取完整的关卡详情(含答案和线索)。
**接口地址**`POST /api/v1/levels/{id}/enter`
**是否需要认证**:是
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 关卡 ID |
**请求体**:无
**响应数据**
```typescript
{
id: string;
level: number;
imageUrl: string;
answer: string;
hint1: string | null;
hint2: string | null;
hint3: string | null;
stamina: StaminaInfo; // 消耗后的体力信息
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"id": "level_002",
"level": 2,
"imageUrl": "https://cdn.example.com/levels/002.png",
"answer": "这是答案",
"hint1": "第一个线索",
"hint2": "第二个线索",
"hint3": null,
"stamina": {
"current": 2,
"max": 5,
"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;
hints: (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;
imageUrl: string;
answer: 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;
imageUrl: string;
answer: string;
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. **网络异常处理**:建议所有接口调用加 loading 状态,并处理 401重新登录和网络错误

View File

@@ -107,7 +107,7 @@ Authorization: Bearer <token>
user: {
id: string; // 用户 ID
nickname: string | null; // 用户昵称(微信昵称)
points: number; // 当前积分
stamina: number; // 当前体力值
}
}
```
@@ -122,7 +122,7 @@ Authorization: Bearer <token>
"user": {
"id": "user_abc123",
"nickname": "游戏玩家",
"points": 10
"stamina": 5
}
},
"message": null,
@@ -512,7 +512,7 @@ async function wxLogin() {
try {
const response = await httpManager.post<{
token: string;
user: { id: string; nickname: string | null; points: number };
user: { id: string; nickname: string | null; stamina: number };
}>('/v1/auth/wx-login', {
code: wxLoginRes.code
});