# MemeMind 分享挑战功能 API 文档 > 本文档面向微信小游戏客户端(Cocos Creator)开发人员 ## 目录 - [概述](#概述) - [认证方式](#认证方式) - [通用响应格式](#通用响应格式) - [接口列表](#接口列表) - [错误码说明](#错误码说明) - [接入流程](#接入流程) - [Cocos Creator 调用示例](#cocos-creator-调用示例) --- ## 概述 分享挑战功能允许用户创建包含 6 个关卡的挑战链接,分享给好友。好友通过分享码加入挑战,独立完成关卡并上报进度。 ### 新增功能点(当前分支) 1. **关卡时间限制**:`levels` 表新增 `time_limit` 字段,支持关卡通关时间限制 2. **单关进度上报**:`POST /api/v1/share/progress` 接口,用于上报用户单关通关状态和时间 3. **进度查询**:`reportLevelProgress` 返回是否在时间限制内通过 --- ## 认证方式 除微信登录接口外,所有接口均需通过 JWT Token 进行身份认证。 ### 请求头格式 ``` Authorization: Bearer ``` `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 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. 上报单关进度 用户在分享挑战中完成单关后,上报进度。 **接口地址**:`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 { 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( method: 'GET' | 'POST', url: string, body?: object ): Promise> { 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(url: string): Promise> { return this.request('GET', url); } // POST 请求 async post(url: string, body: object): Promise> { return this.request('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 { // 确保已登录 if (!httpManager.getToken()) { await wxLogin(); } const response = await httpManager.post('/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 { // 确保已登录 if (!httpManager.getToken()) { await wxLogin(); } const response = await httpManager.post( `/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 { // 确保已登录 if (!httpManager.getToken()) { await wxLogin(); } const response = await httpManager.post( '/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` 表示该关卡没有时间限制