feat: 支持通关接口
This commit is contained in:
@@ -15,8 +15,9 @@
|
|||||||
- [3. 获取游戏数据](#3-获取游戏数据)
|
- [3. 获取游戏数据](#3-获取游戏数据)
|
||||||
- [4. 进入关卡](#4-进入关卡)
|
- [4. 进入关卡](#4-进入关卡)
|
||||||
- [5. 通关上报](#5-通关上报)
|
- [5. 通关上报](#5-通关上报)
|
||||||
- [6. 获取游戏配置](#6-获取游戏配置)
|
- [6. 获取已通关关卡列表](#6-获取已通关关卡列表)
|
||||||
- [7. 获取单个游戏配置](#7-获取单个游戏配置)
|
- [7. 获取游戏配置](#7-获取游戏配置)
|
||||||
|
- [8. 获取单个游戏配置](#8-获取单个游戏配置)
|
||||||
- [错误码说明](#错误码说明)
|
- [错误码说明](#错误码说明)
|
||||||
- [接入流程](#接入流程)
|
- [接入流程](#接入流程)
|
||||||
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
|
- [Cocos Creator 调用示例](#cocos-creator-调用示例)
|
||||||
@@ -25,6 +26,9 @@
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
- **2026-04-30**:
|
||||||
|
- **新增** `GET /api/v1/levels/completed`:获取当前用户已通关关卡列表(按 sortOrder 升序,含完整关卡信息 + 通关时长 + 通关时间)
|
||||||
|
|
||||||
- **2026-04-26**:
|
- **2026-04-26**:
|
||||||
- **删除** `GET /api/v1/levels`(关卡列表接口),客户端不再需要拉取全量关卡列表
|
- **删除** `GET /api/v1/levels`(关卡列表接口),客户端不再需要拉取全量关卡列表
|
||||||
- **变更** `GET /api/v1/user/game-data`:移除 `completedLevelIds`,新增 `completedLevelCount`(数字)和 `nextLevel`(下一关完整数据)
|
- **变更** `GET /api/v1/user/game-data`:移除 `completedLevelIds`,新增 `completedLevelCount`(数字)和 `nextLevel`(下一关完整数据)
|
||||||
@@ -575,7 +579,106 @@ interface NextLevel {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. 获取游戏配置
|
### 6. 获取已通关关卡列表
|
||||||
|
|
||||||
|
获取当前用户所有已通关的关卡,适用于「成就墙」「关卡回看」等场景。
|
||||||
|
|
||||||
|
**接口地址**:`GET /api/v1/levels/completed`
|
||||||
|
|
||||||
|
**是否需要认证**:是
|
||||||
|
|
||||||
|
**请求参数**:无
|
||||||
|
|
||||||
|
**响应数据**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
CompletedLevel[]
|
||||||
|
|
||||||
|
interface CompletedLevel {
|
||||||
|
id: string; // 关卡 ID
|
||||||
|
level: number; // 关卡编号(sortOrder)
|
||||||
|
image1Url: string;
|
||||||
|
image1Description: string | null;
|
||||||
|
image2Url: string;
|
||||||
|
image2Description: string | null;
|
||||||
|
answer: string;
|
||||||
|
punchline: string | null;
|
||||||
|
hint1: string | null;
|
||||||
|
hint2: string | null;
|
||||||
|
hint3: string | null;
|
||||||
|
timeLimit: number | null;
|
||||||
|
timeSpent: number; // 首次通关时长(秒)
|
||||||
|
completedAt: string; // 通关时间(ISO 8601)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "level_001",
|
||||||
|
"level": 1,
|
||||||
|
"image1Url": "https://cdn.example.com/levels/001_1.png",
|
||||||
|
"image1Description": "一只猫",
|
||||||
|
"image2Url": "https://cdn.example.com/levels/001_2.png",
|
||||||
|
"image2Description": "一个头",
|
||||||
|
"answer": "猫头鹰",
|
||||||
|
"punchline": "谐音梗示例",
|
||||||
|
"hint1": "和动物有关",
|
||||||
|
"hint2": null,
|
||||||
|
"hint3": null,
|
||||||
|
"timeLimit": null,
|
||||||
|
"timeSpent": 42,
|
||||||
|
"completedAt": "2026-04-10T11:58:12.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "level_002",
|
||||||
|
"level": 2,
|
||||||
|
"image1Url": "https://cdn.example.com/levels/002_1.png",
|
||||||
|
"image1Description": "一条鱼",
|
||||||
|
"image2Url": "https://cdn.example.com/levels/002_2.png",
|
||||||
|
"image2Description": "一个缸",
|
||||||
|
"answer": "鱼缸",
|
||||||
|
"punchline": null,
|
||||||
|
"hint1": "容器",
|
||||||
|
"hint2": null,
|
||||||
|
"hint3": null,
|
||||||
|
"timeLimit": null,
|
||||||
|
"timeSpent": 31,
|
||||||
|
"completedAt": "2026-04-10T12:05:47.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": null,
|
||||||
|
"timestamp": "2026-04-30T10:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户未通关任何关卡时**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [],
|
||||||
|
"message": null,
|
||||||
|
"timestamp": "2026-04-30T10:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务逻辑**:
|
||||||
|
- 返回列表按关卡顺序(`sortOrder`)升序排列
|
||||||
|
- `timeSpent` 为**首次通关**时上报的时长,重复通关不会覆盖
|
||||||
|
- 每项包含完整关卡信息,客户端可直接在回看页面渲染图片、答案、线索
|
||||||
|
|
||||||
|
**客户端调用时机**:
|
||||||
|
- 用户打开「关卡回看 / 成就墙」页面时
|
||||||
|
- 不建议在 Loading 阶段调用(Loading 使用 `game-data` 即可)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 获取游戏配置
|
||||||
|
|
||||||
获取所有激活的游戏配置。
|
获取所有激活的游戏配置。
|
||||||
|
|
||||||
@@ -630,7 +733,7 @@ interface GameConfig {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 7. 获取单个游戏配置
|
### 8. 获取单个游戏配置
|
||||||
|
|
||||||
根据配置 key 获取单个配置。
|
根据配置 key 获取单个配置。
|
||||||
|
|
||||||
|
|||||||
10
src/modules/level/dto/completed-level.dto.ts
Normal file
10
src/modules/level/dto/completed-level.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { NextLevelDto } from './next-level.dto';
|
||||||
|
|
||||||
|
export class CompletedLevelDto extends NextLevelDto {
|
||||||
|
@ApiProperty({ description: '通关时长(秒)' })
|
||||||
|
timeSpent!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '通关时间(ISO 8601)' })
|
||||||
|
completedAt!: Date;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
CompleteLevelRequestDto,
|
CompleteLevelRequestDto,
|
||||||
CompleteLevelResponseDto,
|
CompleteLevelResponseDto,
|
||||||
} from './dto/complete-level.dto';
|
} from './dto/complete-level.dto';
|
||||||
|
import { CompletedLevelDto } from './dto/completed-level.dto';
|
||||||
import { ApiResponseDto } from '../../common/dto/api-response.dto';
|
import { ApiResponseDto } from '../../common/dto/api-response.dto';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import type { JwtPayload } from '../../common/guards/jwt-auth.guard';
|
import type { JwtPayload } from '../../common/guards/jwt-auth.guard';
|
||||||
@@ -23,6 +24,21 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|||||||
export class LevelController {
|
export class LevelController {
|
||||||
constructor(private readonly levelService: LevelService) {}
|
constructor(private readonly levelService: LevelService) {}
|
||||||
|
|
||||||
|
@Get('completed')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取已通关关卡列表',
|
||||||
|
description:
|
||||||
|
'返回当前用户所有已通关的关卡,按关卡顺序(sortOrder)升序排列。每项包含完整关卡信息 + 通关时长 + 通关时间。',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '成功' })
|
||||||
|
@ApiResponse({ status: 401, description: '未授权' })
|
||||||
|
async getCompletedLevels(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
): Promise<ApiResponseDto<CompletedLevelDto[]>> {
|
||||||
|
const data = await this.levelService.getCompletedLevels(user.sub);
|
||||||
|
return ApiResponseDto.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/enter')
|
@Post(':id/enter')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: '进入关卡',
|
summary: '进入关卡',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CompleteLevelRequestDto,
|
CompleteLevelRequestDto,
|
||||||
CompleteLevelResponseDto,
|
CompleteLevelResponseDto,
|
||||||
} from './dto/complete-level.dto';
|
} from './dto/complete-level.dto';
|
||||||
|
import { CompletedLevelDto } from './dto/completed-level.dto';
|
||||||
import { pickLevelImageFields } from '../wechat-game/level-fields.helper';
|
import { pickLevelImageFields } from '../wechat-game/level-fields.helper';
|
||||||
import { findNextUncompletedLevels, toNextLevelDto } from './next-level.helper';
|
import { findNextUncompletedLevels, toNextLevelDto } from './next-level.helper';
|
||||||
|
|
||||||
@@ -127,4 +128,38 @@ export class LevelService {
|
|||||||
nextLevel: nextLevels[0] ? toNextLevelDto(nextLevels[0]) : null,
|
nextLevel: nextLevels[0] ? toNextLevelDto(nextLevels[0]) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户已通关的关卡列表,按关卡顺序(sortOrder)升序返回
|
||||||
|
*/
|
||||||
|
async getCompletedLevels(userId: string): Promise<CompletedLevelDto[]> {
|
||||||
|
const progressList =
|
||||||
|
await this.userLevelProgressRepository.findByUserId(userId);
|
||||||
|
|
||||||
|
if (progressList.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelIds = progressList.map((p) => p.levelId);
|
||||||
|
const levels = await this.levelRepository.findByIds(levelIds);
|
||||||
|
|
||||||
|
// 构建 levelId -> progress 映射,便于合并 timeSpent / completedAt
|
||||||
|
const progressMap = new Map(progressList.map((p) => [p.levelId, p]));
|
||||||
|
|
||||||
|
return levels
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
.map((level) => {
|
||||||
|
const progress = progressMap.get(level.id)!;
|
||||||
|
return {
|
||||||
|
id: level.id,
|
||||||
|
level: level.sortOrder,
|
||||||
|
...pickLevelImageFields(level),
|
||||||
|
answer: level.answer,
|
||||||
|
timeLimit: level.timeLimit,
|
||||||
|
timeSpent: progress.timeSpent,
|
||||||
|
completedAt: progress.completedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user