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

35 KiB
Raw Blame History

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,客户端一次提交整场挑战的每关答案和耗时,服务端校验答案后返回排名、答对题数、参与人数和完整关卡答案,并持久化提交结果。

目录


概述

分享挑战功能允许用户创建包含 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 结构:

{
  "success": boolean,      // 请求是否成功
  "data": T | null,        // 成功时返回的数据,失败时为 null
  "message": string | null,// 错误信息,成功时为 null
  "timestamp": string      // 服务器响应时间ISO 8601 格式)
}

成功响应示例

{
  "success": true,
  "data": {
    "shareCode": "abc12345",
    "title": "我的挑战",
    "levelCount": 6
  },
  "message": null,
  "timestamp": "2026-04-08T12:00:00.000Z"
}

失败响应示例

{
  "success": false,
  "data": null,
  "message": "分享不存在或已过期",
  "timestamp": "2026-04-08T12:00:00.000Z"
}

接口列表

1. 微信登录

获取用户身份令牌。

接口地址POST /api/v1/auth/wx-login

是否需要认证:否

请求体

{
  "code": "微信 wx.login 返回的 code"
}

响应数据

{
  token: string; // JWT 访问令牌,有效期 7 天
  user: {
    id: string; // 用户 ID
    nickname: string | null; // 用户昵称(微信昵称)
    stamina: number; // 当前体力值
  }
}

成功响应示例

{
  "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

请求体

{
  "title": "我的挑战", // 分享标题,不超过 100 字符
  "levelIds": [
    // 恰好 6 个关卡 ID
    "level_id_1",
    "level_id_2",
    "level_id_3",
    "level_id_4",
    "level_id_5",
    "level_id_6"
  ]
}

响应数据

{
  shareCode: string; // 8 位分享码,用于分享和加入
  title: string; // 分享标题
  levelCount: number; // 关卡数量(固定为 6
}

成功响应示例

{
  "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 位)

响应数据

{
  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;    // 排序顺序
    }
  ]
}

成功响应示例

{
  "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 位)

响应数据

{
  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;// 总耗时(秒)
    }
  ];
}

成功响应示例

{
  "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/createdGET /api/v1/share/participated

5. 获取我创建的分享挑战

获取当前登录用户创建过的分享挑战列表。

接口地址GET /api/v1/share/created

是否需要认证JWT Bearer Token

请求头

Authorization: Bearer <token>

响应数据

{
  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 字符串
    }
  ]
}

成功响应示例

{
  "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>

响应数据

{
  items: [
    {
      title: string;            // 挑战名称
      participantCount: number; // 已提交结果的参与人数
      userRank: number | null;  // 当前用户在该挑战中的排名;尚未提交结果时为 null
    }
  ]
}

成功响应示例

{
  "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 位)

请求体

{
  "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

响应数据

{
  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;
    }
  ];
}

成功响应示例

{
  "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. 结果持久化:每关的 submittedAnswertimeSpentisCorrect 会保存到 share_level_progress,整场的 correctCounttotalTimeSpentsubmittedAt 会保存到 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 提交结果 │
└─────────────────────────────────────────────────────────────────┘

客户端状态管理建议

// 建议在客户端维护以下状态

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 请求工具类

// 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. 微信登录

// 调用时机:小游戏启动时,或缓存的 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. 创建分享

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. 加入分享

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. 获取挑战详情

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. 获取我参与的挑战列表

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. 提交挑战结果

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. 启动流程示例

// 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. 时间限制timeLimitnull 表示该关卡没有时间限制