# MemeMind 分享挑战功能 API 文档 > 本文档面向微信小游戏客户端(Cocos Creator)开发人员 ## Changelog - 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. **关卡排序**:关卡全局顺序按 `levels.sort_key` 的应用层字节序计算,接口中的 `sortOrder` 为排序后的 0-based 连续序号 --- ## 认证方式 除微信登录接口外,所有接口均需通过 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) 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`。 --- ### 5. 获取我创建的分享挑战 获取当前登录用户创建过的分享挑战列表。 **接口地址**:`GET /api/v1/share/created` **是否需要认证**:是(JWT Bearer Token) **请求头**: ``` Authorization: Bearer ``` **响应数据**: ```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. 提交分享挑战结果 用户完成分享挑战后,一次性提交分享中每一关的耗时和答案。服务端校验答案、持久化结果,并返回当前用户的排名和完整关卡答案。 **接口地址**:`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 { 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; 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 { // 确保已登录 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 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 { // 确保已登录 if (!httpManager.getToken()) { await wxLogin(); } const response = await httpManager.get( `/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 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 { // 确保已登录 if (!httpManager.getToken()) { await wxLogin(); } const response = await httpManager.post( `/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}`); } ``` ### 7. 启动流程示例 ```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` 表示该关卡没有时间限制