feat: 支持获取我创建的分享挑战列表以及详情数据

This commit is contained in:
richarjiang
2026-04-13 09:08:11 +08:00
parent fe2c13258e
commit 1d6cd0cdc0
10 changed files with 569 additions and 82 deletions

View File

@@ -23,6 +23,7 @@
1. **关卡时间限制**`levels` 表新增 `time_limit` 字段,支持关卡通关时间限制
2. **单关进度上报**`POST /api/v1/share/progress` 接口,用于上报用户单关通关状态和时间
3. **进度查询**`reportLevelProgress` 返回是否在时间限制内通过
4. **我创建的挑战列表**`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数和本人排名
---
@@ -103,11 +104,11 @@ Authorization: Bearer <token>
```typescript
{
token: string; // JWT 访问令牌,有效期 7 天
token: string; // JWT 访问令牌,有效期 7 天
user: {
id: string; // 用户 ID
nickname: string | null; // 用户昵称(微信昵称)
stamina: number; // 当前体力值
id: string; // 用户 ID
nickname: string | null; // 用户昵称(微信昵称)
stamina: number; // 当前体力值
}
}
```
@@ -131,6 +132,7 @@ Authorization: Bearer <token>
```
**客户端调用时机**
- 用户首次进入游戏时调用
- 小游戏冷启动时调用(建议缓存 token
@@ -145,6 +147,7 @@ Authorization: Bearer <token>
**是否需要认证**JWT Bearer Token
**请求头**
```
Authorization: Bearer <token>
Content-Type: application/json
@@ -154,8 +157,9 @@ Content-Type: application/json
```json
{
"title": "我的挑战", // 分享标题,不超过 100 字符
"levelIds": [ // 恰好 6 个关卡 ID
"title": "我的挑战", // 分享标题,不超过 100 字符
"levelIds": [
// 恰好 6 个关卡 ID
"level_id_1",
"level_id_2",
"level_id_3",
@@ -170,8 +174,8 @@ Content-Type: application/json
```typescript
{
shareCode: string; // 8 位分享码,用于分享和加入
title: string; // 分享标题
shareCode: string; // 8 位分享码,用于分享和加入
title: string; // 分享标题
levelCount: number; // 关卡数量(固定为 6
}
```
@@ -192,11 +196,13 @@ Content-Type: application/json
```
**分享码生成规则**
- 使用 nanoid 生成 8 位字符
- 字符集为 a-z, A-Z, 0-9
- 发生碰撞时最多重试 3 次
**客户端调用场景**
- 用户点击「分享挑战」按钮时调用
- 用户选择 6 个关卡后,生成分享码
- 将分享码拼接为分享链接或二维码
@@ -213,9 +219,9 @@ Content-Type: application/json
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| code | string | 是 | 分享码8 位) |
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | -------------- |
| code | string | 是 | 分享码8 位) |
**响应数据**
@@ -265,16 +271,101 @@ Content-Type: application/json
```
**特殊逻辑**
- 如果 `userId` 与分享创建者相同,不会创建 `ShareParticipant` 记录
- 返回的关卡列表按 `levelIds` 创建时的顺序排列
**客户端调用场景**
- 用户通过分享码/链接进入游戏时调用
- 解析 URL 参数中的分享码,调用此接口获取关卡数据
---
### 4. 上报单关进度
### 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. 上报单关进度
用户在分享挑战中完成单关后,上报进度。
@@ -286,29 +377,29 @@ Content-Type: application/json
```json
{
"shareCode": "abc12345", // 分享码
"levelId": "level_001", // 关卡 ID
"passed": true, // 是否通过true/false
"timeSpent": 30 // 通关时间(秒)
"shareCode": "abc12345", // 分享码
"levelId": "level_001", // 关卡 ID
"passed": true, // 是否通过true/false
"timeSpent": 30 // 通关时间(秒)
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| shareCode | string | 是 | 分享码 |
| levelId | string | 是 | 关卡 ID |
| passed | boolean | 是 | 是否通过 |
| timeSpent | number | 是 | 通关时间(秒),最小值为 0 |
| 字段 | 类型 | 必填 | 说明 |
| --------- | ------- | ---- | -------------------------- |
| shareCode | string | 是 | 分享码 |
| levelId | string | 是 | 关卡 ID |
| passed | boolean | 是 | 是否通过 |
| timeSpent | number | 是 | 通关时间(秒),最小值为 0 |
**响应数据**
```typescript
{
passed: boolean; // 是否通过
timeLimit: number | null; // 该关卡时间限制null 表示无限制
withinTimeLimit: boolean; // 是否在时间限制内通过
passed: boolean; // 是否通过
timeLimit: number | null; // 该关卡时间限制null 表示无限制
withinTimeLimit: boolean; // 是否在时间限制内通过
}
```
@@ -337,6 +428,7 @@ Content-Type: application/json
- 如果 `timeLimit` 不为 `null`,只有 `timeSpent <= timeLimit``withinTimeLimit` 才为 `true`
**客户端调用场景**
- 用户完成一个关卡后调用
- 无论通关还是失败都需要调用
- 失败时 `passed=false``timeSpent` 可以传入实际用时或关卡时间上限
@@ -345,16 +437,16 @@ Content-Type: application/json
## 错误码说明
| HTTP Status | message | 说明 |
|-------------|---------|------|
| 400 | 关卡ID不能重复需要恰好6个不同的关卡 | 创建分享时 levelIds 格式错误 |
| 400 | 以下关卡不存在: xxx | 创建分享时关卡 ID 不存在 |
| 400 | 生成分享码失败,请重试 | 服务器生成分享码失败 |
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期 |
| 404 | 分享不存在或已过期 | 分享码不存在或已被删除 |
| 404 | 关卡不存在 | levelId 不存在于 levels 表 |
| 500 | Internal server error | 服务器内部错误 |
| HTTP Status | message | 说明 |
| ----------- | ------------------------------------- | ---------------------------- |
| 400 | 关卡ID不能重复需要恰好6个不同的关卡 | 创建分享时 levelIds 格式错误 |
| 400 | 以下关卡不存在: xxx | 创建分享时关卡 ID 不存在 |
| 400 | 生成分享码失败,请重试 | 服务器生成分享码失败 |
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期 |
| 404 | 分享不存在或已过期 | 分享码不存在或已被删除 |
| 404 | 关卡不存在 | levelId 不存在于 levels 表 |
| 500 | Internal server error | 服务器内部错误 |
---
@@ -391,16 +483,16 @@ Content-Type: application/json
// 建议在客户端维护以下状态
interface ShareChallengeState {
isInChallenge: boolean; // 是否正在参与分享挑战
shareCode: string | null; // 当前分享码
currentLevelIndex: number; // 当前关卡索引0-5
levels: LevelData[]; // 关卡数据
isInChallenge: boolean; // 是否正在参与分享挑战
shareCode: string | null; // 当前分享码
currentLevelIndex: number; // 当前关卡索引0-5
levels: LevelData[]; // 关卡数据
progress: {
[levelId: string]: {
passed: boolean;
timeSpent: number;
withinTimeLimit: boolean;
}
};
};
}
```
@@ -445,7 +537,7 @@ export class HttpManager {
async request<T>(
method: 'GET' | 'POST',
url: string,
body?: object
body?: object,
): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@@ -504,7 +596,7 @@ async function wxLogin() {
const wxLoginRes = await new Promise<{ code: string }>((resolve, reject) => {
wx.login({
success: (res) => resolve({ code: res.code }),
fail: reject
fail: reject,
});
});
@@ -514,7 +606,7 @@ async function wxLogin() {
token: string;
user: { id: string; nickname: string | null; stamina: number };
}>('/v1/auth/wx-login', {
code: wxLoginRes.code
code: wxLoginRes.code,
});
if (response.success && response.data) {
@@ -543,7 +635,10 @@ interface CreateShareResponse {
levelCount: number;
}
async function createShare(title: string, levelIds: string[]): Promise<CreateShareResponse> {
async function createShare(
title: string,
levelIds: string[],
): Promise<CreateShareResponse> {
// 确保已登录
if (!httpManager.getToken()) {
await wxLogin();
@@ -551,7 +646,7 @@ async function createShare(title: string, levelIds: string[]): Promise<CreateSha
const response = await httpManager.post<CreateShareResponse>('/v1/share', {
title,
levelIds
levelIds,
});
if (response.success && response.data) {
@@ -563,7 +658,14 @@ async function createShare(title: string, levelIds: string[]): Promise<CreateSha
}
// 使用示例
const levelIds = ['level_1', 'level_2', 'level_3', 'level_4', 'level_5', 'level_6'];
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}`
@@ -596,7 +698,7 @@ async function joinShare(shareCode: string): Promise<JoinShareResponse> {
}
const response = await httpManager.post<JoinShareResponse>(
`/v1/share/${shareCode}/join`
`/v1/share/${shareCode}/join`,
);
if (response.success && response.data) {
@@ -627,7 +729,7 @@ async function reportLevelProgress(
shareCode: string,
levelId: string,
passed: boolean,
timeSpent: number
timeSpent: number,
): Promise<ReportProgressResponse> {
// 确保已登录
if (!httpManager.getToken()) {
@@ -640,8 +742,8 @@ async function reportLevelProgress(
shareCode,
levelId,
passed,
timeSpent
}
timeSpent,
},
);
if (response.success && response.data) {
@@ -655,10 +757,17 @@ async function reportLevelProgress(
// 使用示例
async function onLevelComplete(levelId: string, timeSpent: number) {
const passed = true; // 根据游戏逻辑判断是否通过
const result = await reportLevelProgress(this.shareCode, levelId, passed, timeSpent);
const result = await reportLevelProgress(
this.shareCode,
levelId,
passed,
timeSpent,
);
if (result.passed) {
console.log(`通关成功!${result.withinTimeLimit ? '在' : '超出'}时间限制内完成`);
console.log(
`通关成功!${result.withinTimeLimit ? '在' : '超出'}时间限制内完成`,
);
if (result.timeLimit) {
console.log(`本关时间限制: ${result.timeLimit}`);
}
@@ -708,22 +817,23 @@ export class GameEntry extends Component {
### 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 | 排序顺序 |
| 字段 | 类型 | 说明 |
| --------- | -------------- | --------------- |
| 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` 字段判断是否在时间内完成