Files
MemeMind-Server/docs/api/share-challenge-api.md

852 lines
22 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开发人员
## 目录
- [概述](#概述)
- [认证方式](#认证方式)
- [通用响应格式](#通用响应格式)
- [接口列表](#接口列表)
- [错误码说明](#错误码说明)
- [接入流程](#接入流程)
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
---
## 概述
分享挑战功能允许用户创建包含 6 个关卡的挑战链接,分享给好友。好友通过分享码加入挑战,独立完成关卡并上报进度。
### 新增功能点(当前分支)
1. **关卡时间限制**`levels` 表新增 `time_limit` 字段,支持关卡通关时间限制
2. **单关进度上报**`POST /api/v1/share/progress` 接口,用于上报用户单关通关状态和时间
3. **进度查询**`reportLevelProgress` 返回是否在时间限制内通过
4. **我创建的挑战列表**`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数和本人排名
---
## 认证方式
除微信登录接口外,所有接口均需通过 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
imageUrl: string; // 关卡图片 URL
answer: string; // 正确答案
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,
"imageUrl": "https://example.com/levels/1.png",
"answer": "答案1",
"hint1": "提示1",
"hint2": null,
"hint3": null,
"sortOrder": 1
}
]
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**特殊逻辑**
- 如果 `userId` 与分享创建者相同,不会创建 `ShareParticipant` 记录
- 返回的关卡列表按 `levelIds` 创建时的顺序排列
**客户端调用场景**
- 用户通过分享码/链接进入游戏时调用
- 解析 URL 参数中的分享码,调用此接口获取关卡数据
---
### 4. 获取我创建的分享挑战
获取当前登录用户创建过的分享挑战列表。
**接口地址**`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
createdAt: string; // 创建时间ISO 8601 字符串
}
]
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"items": [
{
"id": "share_001",
"shareCode": "abc12345",
"title": "我的挑战",
"levelCount": 6,
"participantCount": 8,
"userRank": 2,
"createdAt": "2026-04-13T10:00:00.000Z"
},
{
"id": "share_002",
"shareCode": "xyz67890",
"title": "速度挑战",
"levelCount": 6,
"participantCount": 1,
"userRank": null,
"createdAt": "2026-04-12T09:00:00.000Z"
}
]
},
"message": null,
"timestamp": "2026-04-13T12:00:00.000Z"
}
```
**排名规则**
1. 只有完成该分享挑战全部关卡的用户才会进入排名。
2. 排名按通关总耗时升序计算,总耗时越短排名越高。
3. 当总耗时相同时,服务端会按 `participantId` 做稳定排序,保证返回顺序可重复。
4. `userRank` 表示当前登录用户在自己创建的该挑战中的排名。如果自己尚未完成全部关卡,则返回 `null`
**参与人数统计规则**
- 统计 `share_participants` 中该挑战的参与者数量。
- 创建者本人在调用创建接口时不会自动写入参与记录;只有真正以参与者身份产生挑战进度后,才可能出现在排名内。
**客户端调用场景**
- 用户进入「我发起的挑战」页面时调用。
- 用于展示每个分享挑战的传播效果和本人当前成绩。
---
### 5. 上报单关进度
用户在分享挑战中完成单关后,上报进度。
**接口地址**`POST /api/v1/share/progress`
**是否需要认证**JWT Bearer Token
**请求体**
```json
{
"shareCode": "abc12345", // 分享码
"levelId": "level_001", // 关卡 ID
"passed": true, // 是否通过true/false
"timeSpent": 30 // 通关时间(秒)
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
| --------- | ------- | ---- | -------------------------- |
| shareCode | string | 是 | 分享码 |
| levelId | string | 是 | 关卡 ID |
| passed | boolean | 是 | 是否通过 |
| timeSpent | number | 是 | 通关时间(秒),最小值为 0 |
**响应数据**
```typescript
{
passed: boolean; // 是否通过
timeLimit: number | null; // 该关卡时间限制null 表示无限制
withinTimeLimit: boolean; // 是否在时间限制内通过
}
```
**成功响应示例**
```json
{
"success": true,
"data": {
"passed": true,
"timeLimit": 60,
"withinTimeLimit": true
},
"message": null,
"timestamp": "2026-04-08T12:00:00.000Z"
}
```
**业务逻辑说明**
1. **首次通关记录**:只有首次 `passed=true` 才会记录通关时间
2. **重复通关**:如果用户再次通关同一关卡(且之前已通过),返回之前记录的时间判断结果,不会覆盖
3. **未通过**:可以多次上报 `passed=false`,更新通关时间记录
4. **时间限制判断**
- 如果关卡 `timeLimit``null``withinTimeLimit` 始终为 `true`
- 如果 `timeLimit` 不为 `null`,只有 `timeSpent <= timeLimit``withinTimeLimit` 才为 `true`
**客户端调用场景**
- 用户完成一个关卡后调用
- 无论通关还是失败都需要调用
- 失败时 `passed=false``timeSpent` 可以传入实际用时或关卡时间上限
---
## 错误码说明
| HTTP Status | message | 说明 |
| ----------- | ------------------------------------- | ---------------------------- |
| 400 | 关卡ID不能重复需要恰好6个不同的关卡 | 创建分享时 levelIds 格式错误 |
| 400 | 以下关卡不存在: xxx | 创建分享时关卡 ID 不存在 |
| 400 | 生成分享码失败,请重试 | 服务器生成分享码失败 |
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期 |
| 404 | 分享不存在或已过期 | 分享码不存在或已被删除 |
| 404 | 关卡不存在 | levelId 不存在于 levels 表 |
| 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/progress 上报进度 │
└─────────────────────────────────────────────────────────────────┘
```
### 客户端状态管理建议
```typescript
// 建议在客户端维护以下状态
interface ShareChallengeState {
isInChallenge: boolean; // 是否正在参与分享挑战
shareCode: string | null; // 当前分享码
currentLevelIndex: number; // 当前关卡索引0-5
levels: LevelData[]; // 关卡数据
progress: {
[levelId: string]: {
passed: boolean;
timeSpent: number;
withinTimeLimit: boolean;
};
};
}
```
### 分享链接格式建议
```
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;
imageUrl: string;
answer: string;
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 ReportProgressResponse {
passed: boolean;
timeLimit: number | null;
withinTimeLimit: boolean;
}
async function reportLevelProgress(
shareCode: string,
levelId: string,
passed: boolean,
timeSpent: number,
): Promise<ReportProgressResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
}
const response = await httpManager.post<ReportProgressResponse>(
'/v1/share/progress',
{
shareCode,
levelId,
passed,
timeSpent,
},
);
if (response.success && response.data) {
console.log('进度上报成功:', response.data);
return response.data;
} else {
throw new Error(response.message || '进度上报失败');
}
}
// 使用示例
async function onLevelComplete(levelId: string, timeSpent: number) {
const passed = true; // 根据游戏逻辑判断是否通过
const result = await reportLevelProgress(
this.shareCode,
levelId,
passed,
timeSpent,
);
if (result.passed) {
console.log(
`通关成功!${result.withinTimeLimit ? '在' : '超出'}时间限制内完成`,
);
if (result.timeLimit) {
console.log(`本关时间限制: ${result.timeLimit}`);
}
}
}
```
### 6. 启动流程示例
```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 |
| imageUrl | string | 关卡图片 URL |
| answer | string | 正确答案 |
| hint1 | string \| null | 第 1 个提示 |
| hint2 | string \| null | 第 2 个提示 |
| hint3 | string \| null | 第 3 个提示 |
| sortOrder | number | 排序顺序 |
### 关卡时间限制说明
`timeLimit` 字段在关卡数据结构中**不直接返回**,而是通过上报进度接口返回。
如果需要在前端判断时间限制:
1. 用户完成关卡后调用 `reportLevelProgress`
2. 从返回的 `timeLimit` 字段获取当前关卡的时间限制
3. 从返回的 `withinTimeLimit` 字段判断是否在时间内完成
---
## 注意事项
1. **Token 有效期**JWT Token 有效期为 7 天,客户端应缓存并在启动时使用
2. **重复通关**:首次通关记录会被保留,重复通关不会覆盖之前的记录
3. **分享码格式**8 位字母数字组合,大小写敏感
4. **关卡数量**:每次分享挑战固定包含 6 个关卡
5. **网络异常**:建议在调用接口时显示 loading 状态,并处理网络异常情况
6. **hint 字段**`hint1/hint2/hint3` 可能为 `null`,表示该提示未配置
7. **时间限制**`timeLimit``null` 表示该关卡没有时间限制