1073 lines
31 KiB
Markdown
1073 lines
31 KiB
Markdown
# 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` 字段直接告知下一关
|