Files
MemeMind-Server/docs/api/share-challenge-api.md
2026-05-14 16:45:24 +08:00

1227 lines
35 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开发人员
## Changelog
- 2026-05-14: 新增 `GET /api/v1/share/participated` 我参与的挑战列表接口,返回挑战名称、已提交参与人数和当前用户名次。
- 2026-05-13: 新增 `GET /api/v1/share/{code}` 分享挑战详情接口,返回挑战基本信息和所有已提交用户的排行榜;排行榜项包含用户头像、昵称、答对题数和总耗时。
- 2026-05-12: `GET /api/v1/share/created` 响应新增 `firstPlaceUser`,返回每个挑战当前第一名用户的昵称和头像;暂无提交结果时为 `null`
- 2026-05-10: 移除 `POST /api/v1/share/progress` 单关进度上报接口;新增 `POST /api/v1/share/{code}/submit`,客户端一次提交整场挑战的每关答案和耗时,服务端校验答案后返回排名、答对题数、参与人数和完整关卡答案,并持久化提交结果。
## 目录
- [概述](#概述)
- [认证方式](#认证方式)
- [通用响应格式](#通用响应格式)
- [接口列表](#接口列表)
- [错误码说明](#错误码说明)
- [接入流程](#接入流程)
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
---
## 概述
分享挑战功能允许用户创建包含 6 个关卡的挑战链接,分享给好友。好友通过分享码加入挑战,独立完成关卡,并在挑战结束后一次性提交每一关的答案与耗时。服务端校验答案后,会返回当前用户排名、答对题数、已提交挑战结果的参与人数,以及分享中每一关的完整内容和正确答案。
### 新增功能点(当前分支)
1. **关卡时间限制**`levels` 表新增 `time_limit` 字段,支持关卡通关时间限制
2. **整场挑战提交**`POST /api/v1/share/{code}/submit` 接口,用于一次性提交分享挑战中每一关的答案和耗时
3. **挑战结果返回**:提交后返回当前用户排名、答对题数、参与人数、总耗时和每关校验结果
4. **分享挑战详情**`GET /api/v1/share/{code}` 接口,用于查询单个挑战的基本信息和完整排行榜
5. **我创建的挑战列表**`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数、本人排名和当前第一名用户信息
6. **我参与的挑战列表**`GET /api/v1/share/participated` 接口,用于查询当前用户参与过的分享挑战、参与人数和本人排名
7. **关卡排序**:关卡全局顺序按 `levels.sort_key` 的应用层字节序计算,接口中的 `sortOrder` 为排序后的 0-based 连续序号
---
## 认证方式
除微信登录接口外,所有接口均需通过 JWT Token 进行身份认证。
### 请求头格式
```
Authorization: Bearer <token>
```
`token` 为微信登录接口返回的 JWT 令牌。
---
## 通用响应格式
所有接口均返回以下 JSON 结构:
```typescript
{
"success": boolean, // 请求是否成功
"data": T | null, // 成功时返回的数据,失败时为 null
"message": string | null,// 错误信息,成功时为 null
"timestamp": string // 服务器响应时间ISO 8601 格式)
}
```
### 成功响应示例
```json
{
"success": true,
"data": {
"shareCode": "abc12345",
"title": "我的挑战",
"levelCount": 6
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
### 失败响应示例
```json
{
"success": false,
"data": null,
"message": "分享不存在或已过期",
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
---
## 接口列表
### 1. 微信登录
获取用户身份令牌。
**接口地址**`POST /api/v1/auth/wx-login`
**是否需要认证**:否
**请求体**
```json
{
"code": "微信 wx.login 返回的 code"
}
```
**响应数据**
```typescript
{
token: string; // JWT 访问令牌,有效期 7 天
user: {
id: string; // 用户 ID
nickname: string | null; // 用户昵称(微信昵称)
stamina: number; // 当前体力值
}
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user_abc123",
"nickname": "游戏玩家",
"stamina": 5
}
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**客户端调用时机**
- 用户首次进入游戏时调用
- 小游戏冷启动时调用(建议缓存 token
---
### 2. 创建分享
创建一个新的分享挑战。
**接口地址**`POST /api/v1/share`
**是否需要认证**JWT Bearer Token
**请求头**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**请求体**
```json
{
"title": "我的挑战", // 分享标题,不超过 100 字符
"levelIds": [
// 恰好 6 个关卡 ID
"level_id_1",
"level_id_2",
"level_id_3",
"level_id_4",
"level_id_5",
"level_id_6"
]
}
```
**响应数据**
```typescript
{
shareCode: string; // 8 位分享码,用于分享和加入
title: string; // 分享标题
levelCount: number; // 关卡数量(固定为 6
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"shareCode": "abc12345",
"title": "我的挑战",
"levelCount": 6
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**分享码生成规则**
- 使用 nanoid 生成 8 位字符
- 字符集为 a-z, A-Z, 0-9
- 发生碰撞时最多重试 3 次
**客户端调用场景**
- 用户点击「分享挑战」按钮时调用
- 用户选择 6 个关卡后,生成分享码
- 将分享码拼接为分享链接或二维码
---
### 3. 加入分享
通过分享码加入一个分享挑战,获取关卡数据。
**接口地址**`POST /api/v1/share/{code}/join`
**是否需要认证**JWT Bearer Token
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | -------------- |
| code | string | 是 | 分享码8 位) |
**响应数据**
```typescript
{
shareCode: string; // 分享码
title: string; // 分享标题
levels: [
{
id: string; // 关卡 ID
level: number; // 关卡序号1-6
image1Url: string; // 图片 1 URL
image1Description: string | null;
image2Url: string; // 图片 2 URL
image2Description: string | null;
answer: string; // 正确答案
punchline: string | null;
hint1: string | null; // 提示 1
hint2: string | null; // 提示 2
hint3: string | null; // 提示 3
sortOrder: number; // 排序顺序
}
]
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"shareCode": "abc12345",
"title": "我的挑战",
"levels": [
{
"id": "level_001",
"level": 1,
"image1Url": "https://example.com/levels/1-a.png",
"image1Description": null,
"image2Url": "https://example.com/levels/1-b.png",
"image2Description": null,
"answer": "答案1",
"punchline": null,
"hint1": "提示1",
"hint2": null,
"hint3": null,
"sortOrder": 0
}
]
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**特殊逻辑**
- 如果 `userId` 与分享创建者相同,不会创建 `ShareParticipant` 记录
- `levels` 数组顺序保持分享创建时传入的 `levelIds` 顺序;每个关卡的 `sortOrder` 字段为按 `levels.sort_key` 全局排序后回填的 0-based 连续序号
- 返回的关卡列表按 `levelIds` 创建时的顺序排列
**客户端调用场景**
- 用户通过分享码/链接进入游戏时调用
- 解析 URL 参数中的分享码,调用此接口获取关卡数据
---
### 4. 获取分享挑战详情
获取单个分享挑战的基本信息,以及该挑战下所有已提交结果用户的排行榜。
**接口地址**`GET /api/v1/share/{code}`
**是否需要认证**JWT Bearer Token
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | -------------- |
| code | string | 是 | 分享码8 位) |
**响应数据**
```typescript
{
id: string; // 分享挑战 ID
shareCode: string; // 分享码
title: string; // 分享标题
levelCount: number; // 挑战关卡总数
participantCount: number; // 已提交结果的参与用户总数
userRank: number | null; // 当前用户排名;尚未提交结果时为 null
createdAt: string; // 创建时间ISO 8601 字符串
rankings: [
{
rank: number; // 名次,从 1 开始
participantId: string; // 参与用户 ID
nickname: string | null;
avatarUrl: string | null;
correctCount: number; // 答对题数
totalTimeSpent: number;// 总耗时(秒)
}
];
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"id": "share_001",
"shareCode": "abc12345",
"title": "我的挑战",
"levelCount": 6,
"participantCount": 3,
"userRank": 2,
"createdAt": "2026-04-13T10:00:00.000Z",
"rankings": [
{
"rank": 1,
"participantId": "user_002",
"nickname": "速度玩家",
"avatarUrl": "https://example.com/avatar-speed.png",
"correctCount": 6,
"totalTimeSpent": 90
},
{
"rank": 2,
"participantId": "user_001",
"nickname": "挑战创建者",
"avatarUrl": null,
"correctCount": 6,
"totalTimeSpent": 120
},
{
"rank": 3,
"participantId": "user_003",
"nickname": null,
"avatarUrl": null,
"correctCount": 5,
"totalTimeSpent": 60
}
]
},
"message": null,
"timestamp": "2026-04-13T12:00:00.000Z"
}
```
**业务逻辑说明**
1. 返回挑战基本信息:分享 ID、分享码、标题、关卡数量、已提交人数、当前用户排名和创建时间。
2. `rankings` 只包含已经调用 `POST /api/v1/share/{code}/submit` 提交整场挑战结果的用户。
3. 排行榜按答对题数降序排列;答对题数相同时,按总耗时升序排列。
4. 如果答对题数和总耗时都相同,服务端继续按提交时间升序、`participantId` 升序做稳定排序。
5. `userRank` 表示当前登录用户在该挑战中的排名;当前用户尚未提交结果时为 `null`
**客户端调用场景**
- 用户打开单个挑战详情页或结算页时调用。
- 需要展示完整排行榜时调用;列表页使用 `GET /api/v1/share/created``GET /api/v1/share/participated`
---
### 5. 获取我创建的分享挑战
获取当前登录用户创建过的分享挑战列表。
**接口地址**`GET /api/v1/share/created`
**是否需要认证**JWT Bearer Token
**请求头**
```
Authorization: Bearer <token>
```
**响应数据**
```typescript
{
items: [
{
id: string; // 分享挑战 ID
shareCode: string; // 分享码
title: string; // 分享标题
levelCount: number; // 关卡数量
participantCount: number; // 已提交结果的参与人数
userRank: number | null; // 当前用户在该挑战中的排名;尚未提交结果时为 null
firstPlaceUser: { // 当前第一名用户信息;暂无提交结果时为 null
nickname: string | null; // 第一名用户昵称
avatarUrl: string | null;// 第一名用户头像 URL
} | null;
createdAt: string; // 创建时间ISO 8601 字符串
}
]
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"items": [
{
"id": "share_001",
"shareCode": "abc12345",
"title": "我的挑战",
"levelCount": 6,
"participantCount": 8,
"userRank": 2,
"firstPlaceUser": {
"nickname": "第一名玩家",
"avatarUrl": "https://example.com/avatar-first.png"
},
"createdAt": "2026-04-13T10:00:00.000Z"
},
{
"id": "share_002",
"shareCode": "xyz67890",
"title": "速度挑战",
"levelCount": 6,
"participantCount": 1,
"userRank": null,
"firstPlaceUser": {
"nickname": null,
"avatarUrl": null
},
"createdAt": "2026-04-12T09:00:00.000Z"
}
]
},
"message": null,
"timestamp": "2026-04-13T12:00:00.000Z"
}
```
**排名规则**
1. 只有已经调用 `POST /api/v1/share/{code}/submit` 提交整场挑战结果的用户才会进入排名。
2. 排名按答对题数降序计算,答对越多排名越高。
3. 答对题数相同时,按总耗时升序、提交时间升序、`participantId` 升序做稳定排序。
4. `userRank` 表示当前登录用户在自己创建的该挑战中的排名。如果自己尚未提交挑战结果,则返回 `null`
5. `firstPlaceUser` 取当前排名第一的已提交用户资料。如果该挑战还没有任何提交结果,则返回 `null`
**参与人数统计规则**
- 统计该挑战中已提交完整挑战结果的用户数量。
- 创建者本人如果调用提交接口,也会作为参与用户进入统计和排名。
**客户端调用场景**
- 用户进入「我发起的挑战」页面时调用。
- 用于展示每个分享挑战的传播效果和本人当前成绩。
---
### 6. 获取我参与的分享挑战
获取当前登录用户参与过的分享挑战列表。
**接口地址**`GET /api/v1/share/participated`
**是否需要认证**JWT Bearer Token
**请求头**
```
Authorization: Bearer <token>
```
**响应数据**
```typescript
{
items: [
{
title: string; // 挑战名称
participantCount: number; // 已提交结果的参与人数
userRank: number | null; // 当前用户在该挑战中的排名;尚未提交结果时为 null
}
]
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"items": [
{
"title": "好友挑战",
"participantCount": 5,
"userRank": 3
},
{
"title": "速度挑战",
"participantCount": 1,
"userRank": null
}
]
},
"message": null,
"timestamp": "2026-05-14T12:00:00.000Z"
}
```
**排名规则**
1. 列表只返回当前用户在 `share_participants` 中有参与记录的挑战。
2. 只有已经调用 `POST /api/v1/share/{code}/submit` 提交整场挑战结果的用户才会进入排名。
3. 排名按答对题数降序计算,答对题数相同时,按总耗时升序、提交时间升序、`participantId` 升序做稳定排序。
4. `userRank` 表示当前登录用户在该挑战中的排名。如果当前用户已加入但尚未提交挑战结果,则返回 `null`
**参与人数统计规则**
- 统计该挑战中已提交完整挑战结果的用户数量。
- 创建者本人如果调用提交接口,也会作为参与用户进入统计和排名。
**客户端调用场景**
- 用户进入「我参与的挑战」页面时调用。
- 列表只展示挑战名称、参与人数和当前用户名次。
---
### 7. 提交分享挑战结果
用户完成分享挑战后,一次性提交分享中每一关的耗时和答案。服务端校验答案、持久化结果,并返回当前用户的排名和完整关卡答案。
**接口地址**`POST /api/v1/share/{code}/submit`
**是否需要认证**JWT Bearer Token
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | -------------- |
| code | string | 是 | 分享码8 位) |
**请求体**
```json
{
"levels": [
{
"levelId": "level_001",
"answer": "答案1",
"timeSpent": 30
},
{
"levelId": "level_002",
"answer": "答案2",
"timeSpent": 45
}
]
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
| ------------------ | ------ | ---- | ---------------------------------------------- |
| levels | array | 是 | 每一关的提交结果,必须覆盖分享挑战中的全部关卡 |
| levels[].levelId | string | 是 | 关卡 ID |
| levels[].answer | string | 是 | 用户提交的答案,空字符串表示未作答 |
| levels[].timeSpent | number | 是 | 本关耗时(秒),最小值为 0 |
**响应数据**
```typescript
{
shareCode: string;
title: string;
rank: number; // 当前用户在该挑战中的排名
correctCount: number; // 当前用户答对题数
levelCount: number; // 挑战关卡总数
participantCount: number; // 已提交结果的参与用户总数
totalTimeSpent: number; // 当前用户本次提交总耗时(秒)
levels: [
{
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;
sortOrder: number;
submittedAnswer: string;
timeSpent: number;
isCorrect: boolean;
timeLimit: number | null;
withinTimeLimit: boolean;
}
];
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"shareCode": "abc12345",
"title": "我的挑战",
"rank": 2,
"correctCount": 5,
"levelCount": 6,
"participantCount": 8,
"totalTimeSpent": 180,
"levels": [
{
"id": "level_001",
"level": 1,
"image1Url": "https://example.com/levels/1-a.png",
"image1Description": null,
"image2Url": "https://example.com/levels/1-b.png",
"image2Description": null,
"answer": "答案1",
"punchline": null,
"hint1": "提示1",
"hint2": null,
"hint3": null,
"sortOrder": 0,
"submittedAnswer": "答案1",
"timeSpent": 30,
"isCorrect": true,
"timeLimit": 60,
"withinTimeLimit": true
}
]
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**业务逻辑说明**
1. **完整提交**`levels` 必须覆盖分享中的全部关卡,不能缺少、重复或提交不属于该分享的关卡。
2. **答案校验**:服务端按去除首尾空白并忽略大小写后的答案进行比对,返回每关 `isCorrect`
3. **排名规则**:按答对题数降序排名;答对题数相同时,按总耗时升序、提交时间升序、`participantId` 升序稳定排序。
4. **参与者登记**:提交成功后会写入或更新 `share_participants` 的聚合成绩;创建者本人提交时也会计入参与人数和排名。
5. **结果持久化**:每关的 `submittedAnswer``timeSpent``isCorrect` 会保存到 `share_level_progress`,整场的 `correctCount``totalTimeSpent``submittedAt` 会保存到 `share_participants`
6. **重复提交**:同一用户重复提交同一挑战时,服务端会覆盖该用户本挑战的上一份结果,并重新计算排名。
7. **时间限制判断**`withinTimeLimit` 只表示耗时是否不超过关卡 `timeLimit`;答题正确与否由 `isCorrect` 表示。
8. **跨挑战独立**:结果按 `(shareConfigId, participantId, levelId)` 唯一记录,同一用户在不同分享挑战中对同一关卡的提交互不影响。
**客户端调用场景**
- 用户完成整场分享挑战后调用
- 结果页需要展示排名、答对题数、完整答案解析时调用
---
## 错误码说明
| HTTP Status | message | 说明 |
| ----------- | -------------------------------------- | ---------------------------- |
| 400 | 关卡ID不能重复需要恰好6个不同的关卡 | 创建分享时 levelIds 格式错误 |
| 400 | 以下关卡不存在: xxx | 创建分享时关卡 ID 不存在 |
| 400 | 生成分享码失败,请重试 | 服务器生成分享码失败 |
| 400 | 提交关卡数量必须与分享挑战关卡数量一致 | 提交挑战结果时关卡数量不完整 |
| 400 | 关卡 xxx 重复提交 | 提交挑战结果时同一关卡重复 |
| 400 | 关卡 xxx 不属于此分享挑战 | 提交挑战结果时包含外部关卡 |
| 400 | 缺少关卡提交: xxx | 提交挑战结果时缺少指定关卡 |
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期 |
| 404 | 分享不存在或已过期 | 分享码不存在或已被删除 |
| 404 | 以下关卡不存在: xxx | 分享配置中的关卡不存在 |
| 500 | Internal server error | 服务器内部错误 |
---
## 接入流程
### 整体流程
```
┌─────────────────────────────────────────────────────────────────┐
│ 分享发起方 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 用户选择 6 个关卡 │
│ 2. 调用 POST /api/v1/share 创建分享 │
│ 3. 获取 8 位分享码 │
│ 4. 生成分享链接/二维码,分享给好友 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 分享接收方 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 通过分享链接/二维码进入游戏 │
│ 2. 解析分享码 │
│ 3. 调用 POST /api/v1/auth/wx-login 获取/确认身份 │
│ 4. 调用 POST /api/v1/share/{code}/join 加入挑战 │
│ 5. 获取 6 个关卡数据,开始挑战 │
│ 6. 完成整场挑战后,调用 POST /api/v1/share/{code}/submit 提交结果 │
└─────────────────────────────────────────────────────────────────┘
```
### 客户端状态管理建议
```typescript
// 建议在客户端维护以下状态
interface ShareChallengeState {
isInChallenge: boolean; // 是否正在参与分享挑战
shareCode: string | null; // 当前分享码
currentLevelIndex: number; // 当前关卡索引0-5
levels: LevelData[]; // 关卡数据
submissions: {
[levelId: string]: {
answer: string;
timeSpent: number;
};
};
result: SubmitChallengeResponse | null;
}
```
### 分享链接格式建议
```
wechatminiprogram://pages/challenge?code=abc12345
```
或在微信中使用 `wx.openUrl` 打开 H5 页面H5 页面再跳转到小程序。
---
## Cocos Creator 调用示例
### 1. HTTP 请求工具类
```typescript
// HttpManager.ts
import { Color } from 'cc';
export interface ApiResponse<T> {
success: boolean;
data: T | null;
message: string | null;
timestamp: string;
}
export 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 = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
try {
const errorResp = JSON.parse(xhr.responseText);
reject(new Error(errorResp.message || '请求失败'));
} catch {
reject(new Error(`请求失败: ${xhr.status}`));
}
}
};
xhr.onerror = () => {
reject(new Error('网络错误'));
};
if (body) {
xhr.send(JSON.stringify(body));
} else {
xhr.send();
}
});
}
// GET 请求
async get<T>(url: string): Promise<ApiResponse<T>> {
return this.request<T>('GET', url);
}
// POST 请求
async post<T>(url: string, body: object): Promise<ApiResponse<T>> {
return this.request<T>('POST', url, body);
}
}
export const httpManager = new HttpManager();
```
### 2. 微信登录
```typescript
// 调用时机:小游戏启动时,或缓存的 token 过期时
async function wxLogin() {
// 1. 调用微信 wx.login 获取 code
const wxLoginRes = await new Promise<{ code: string }>((resolve, reject) => {
wx.login({
success: (res) => resolve({ code: res.code }),
fail: reject,
});
});
// 2. 将 code 发送到服务器换取 token
try {
const response = await httpManager.post<{
token: string;
user: { id: string; nickname: string | null; stamina: number };
}>('/v1/auth/wx-login', {
code: wxLoginRes.code,
});
if (response.success && response.data) {
// 3. 缓存 token
httpManager.setToken(response.data.token);
wx.setStorageSync('jwt_token', response.data.token);
console.log('登录成功,用户信息:', response.data.user);
return response.data;
} else {
throw new Error(response.message || '登录失败');
}
} catch (error) {
console.error('登录失败:', error);
throw error;
}
}
```
### 3. 创建分享
```typescript
interface CreateShareResponse {
shareCode: string;
title: string;
levelCount: number;
}
async function createShare(
title: string,
levelIds: string[],
): Promise<CreateShareResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.post<CreateShareResponse>('/v1/share', {
title,
levelIds,
});
if (response.success && response.data) {
console.log('分享创建成功:', response.data);
return response.data;
} else {
throw new Error(response.message || '创建分享失败');
}
}
// 使用示例
const levelIds = [
'level_1',
'level_2',
'level_3',
'level_4',
'level_5',
'level_6',
];
const share = await createShare('一起来挑战!', levelIds);
console.log('分享码:', share.shareCode);
// 生成分享链接: `https://your-game.com/invite?code=${share.shareCode}`
```
### 4. 加入分享
```typescript
interface LevelData {
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;
sortOrder: number;
}
interface JoinShareResponse {
shareCode: string;
title: string;
levels: LevelData[];
}
async function joinShare(shareCode: string): Promise<JoinShareResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.post<JoinShareResponse>(
`/v1/share/${shareCode}/join`,
);
if (response.success && response.data) {
console.log('加入分享成功:', response.data);
return response.data;
} else {
throw new Error(response.message || '加入分享失败');
}
}
// 使用示例
// 从分享链接中获取分享码
const shareCode = 'abc12345'; // 从 URL 参数解析
const shareData = await joinShare(shareCode);
// 保存关卡数据,开始游戏
```
### 5. 获取挑战详情
```typescript
interface ChallengeRankingItem {
rank: number;
participantId: string;
nickname: string | null;
avatarUrl: string | null;
correctCount: number;
totalTimeSpent: number;
}
interface ShareChallengeDetailResponse {
id: string;
shareCode: string;
title: string;
levelCount: number;
participantCount: number;
userRank: number | null;
createdAt: string;
rankings: ChallengeRankingItem[];
}
async function getShareChallengeDetail(
shareCode: string,
): Promise<ShareChallengeDetailResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.get<ShareChallengeDetailResponse>(
`/v1/share/${shareCode}`,
);
if (response.success && response.data) {
console.log('挑战详情:', response.data);
return response.data;
} else {
throw new Error(response.message || '获取挑战详情失败');
}
}
// 使用示例
const detail = await getShareChallengeDetail('abc12345');
console.log(`当前排名: ${detail.userRank ?? '暂未上榜'}`);
console.log(`已提交人数: ${detail.participantCount}`);
```
### 6. 获取我参与的挑战列表
```typescript
interface ParticipatedShareItem {
title: string;
participantCount: number;
userRank: number | null;
}
interface ParticipatedShareListResponse {
items: ParticipatedShareItem[];
}
async function getParticipatedShares(): Promise<ParticipatedShareListResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.get<ParticipatedShareListResponse>(
'/v1/share/participated',
);
if (response.success && response.data) {
console.log('我参与的挑战:', response.data.items);
return response.data;
} else {
throw new Error(response.message || '获取我参与的挑战失败');
}
}
// 使用示例
const participatedShares = await getParticipatedShares();
participatedShares.items.forEach((item) => {
console.log(
`${item.title}: ${item.participantCount} 人参与,当前排名 ${
item.userRank ?? '暂未上榜'
}`,
);
});
```
### 7. 提交挑战结果
```typescript
interface SubmitChallengeLevel {
levelId: string;
answer: string;
timeSpent: number;
}
interface SubmittedLevelResult extends LevelData {
submittedAnswer: string;
timeSpent: number;
isCorrect: boolean;
timeLimit: number | null;
withinTimeLimit: boolean;
}
interface SubmitChallengeResponse {
shareCode: string;
title: string;
rank: number;
correctCount: number;
levelCount: number;
participantCount: number;
totalTimeSpent: number;
levels: SubmittedLevelResult[];
}
async function submitShareChallenge(
shareCode: string,
levels: SubmitChallengeLevel[],
): Promise<SubmitChallengeResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.post<SubmitChallengeResponse>(
`/v1/share/${shareCode}/submit`,
{ levels },
);
if (response.success && response.data) {
console.log('挑战结果提交成功:', response.data);
return response.data;
} else {
throw new Error(response.message || '挑战结果提交失败');
}
}
// 使用示例
async function onChallengeFinished() {
const result = await submitShareChallenge(this.shareCode, [
{ levelId: 'level_001', answer: '答案1', timeSpent: 30 },
{ levelId: 'level_002', answer: '答案2', timeSpent: 45 },
{ levelId: 'level_003', answer: '', timeSpent: 60 },
{ levelId: 'level_004', answer: '答案4', timeSpent: 20 },
{ levelId: 'level_005', answer: '答案5', timeSpent: 15 },
{ levelId: 'level_006', answer: '答案6', timeSpent: 10 },
]);
console.log(`当前排名: 第 ${result.rank}`);
console.log(`答对: ${result.correctCount}/${result.levelCount}`);
console.log(`参与人数: ${result.participantCount}`);
}
```
### 8. 启动流程示例
```typescript
// GameEntry.ts - 游戏入口脚本
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('GameEntry')
export class GameEntry extends Component {
async start() {
// 1. 检查本地缓存的 token
const cachedToken = wx.getStorageSync('jwt_token');
if (cachedToken) {
httpManager.setToken(cachedToken);
}
// 2. 尝试登录(无论是否有缓存 token
try {
await wxLogin();
console.log('登录成功');
} catch (error) {
console.error('登录失败:', error);
}
// 3. 检查是否有分享码(从启动参数或 URL 获取)
const launchOptions = wx.getLaunchOptionsSync();
console.log('启动参数:', launchOptions);
// 解析分享码(具体解析方式根据你的分享链接格式)
// if (launchOptions.query && launchOptions.query.code) {
// await joinShare(launchOptions.query.code);
// }
}
}
```
---
## 关卡数据结构
### LevelData 完整字段
| 字段 | 类型 | 说明 |
| ----------------- | -------------- | --------------- |
| id | string | 关卡唯一标识 |
| level | number | 关卡序号1-6 |
| 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 个提示 |
| sortOrder | number | 全局排序顺序 |
### 关卡时间限制说明
`POST /api/v1/share/{code}/join` 返回的关卡数据不包含 `timeLimit`。挑战结束后调用 `POST /api/v1/share/{code}/submit`,返回的每关结果会包含:
- `timeLimit`:该关卡时间限制,`null` 表示无限制。
- `withinTimeLimit`:用户提交的 `timeSpent` 是否不超过 `timeLimit`
- `isCorrect`:用户提交答案是否正确。
---
## 注意事项
1. **Token 有效期**JWT Token 有效期为 7 天,客户端应缓存并在启动时使用
2. **重复提交**:同一用户重复提交同一分享挑战会覆盖上一份提交结果,并重新计算排名
3. **分享码格式**8 位字母数字组合,大小写敏感
4. **关卡数量**:每次分享挑战固定包含 6 个关卡
5. **网络异常**:建议在调用接口时显示 loading 状态,并处理网络异常情况
6. **hint 字段**`hint1/hint2/hint3` 可能为 `null`,表示该提示未配置
7. **时间限制**`timeLimit``null` 表示该关卡没有时间限制