feat(wechat-game): add level list API endpoints

Add CRUD endpoints for game levels with level field in response:
- GET /v1/wechat-game/levels - list all levels ordered by sort_order
- GET /v1/wechat-game/levels/:id - get single level by ID

New files:
- Level entity mapping to levels table
- LevelRepository with ordered query support
- LevelResponseDto with level field (1-based index from sort_order)
This commit is contained in:
richarjiang
2026-03-15 15:52:51 +08:00
parent ed1b5455a2
commit 46368b8c89
7 changed files with 205 additions and 6 deletions

View File

@@ -0,0 +1,41 @@
import { ApiProperty } from '@nestjs/swagger';
export class LevelResponseDto {
@ApiProperty({ description: '关卡编号' })
level: number;
@ApiProperty({ description: '关卡ID' })
id: string;
@ApiProperty({ description: '图片URL' })
imageUrl: string;
@ApiProperty({ description: '答案' })
answer: string;
@ApiProperty({ description: '提示1', nullable: true })
hint1: string | null;
@ApiProperty({ description: '提示2', nullable: true })
hint2: string | null;
@ApiProperty({ description: '提示3', nullable: true })
hint3: string | null;
@ApiProperty({ description: '排序顺序' })
sortOrder: number;
@ApiProperty({ description: '创建时间' })
createdAt: Date;
@ApiProperty({ description: '更新时间' })
updatedAt: Date;
}
export class LevelListResponseDto {
@ApiProperty({ type: [LevelResponseDto], description: '关卡列表' })
levels: LevelResponseDto[];
@ApiProperty({ description: '关卡总数' })
total: number;
}

View File

@@ -0,0 +1,37 @@
import {
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('levels')
export class Level {
@PrimaryColumn({ type: 'varchar', length: 191 })
id: string;
@Column({ type: 'varchar', length: 191, name: 'image_url' })
imageUrl: string;
@Column({ type: 'varchar', length: 191 })
answer: string;
@Column({ type: 'varchar', length: 191, nullable: true })
hint1: string | null;
@Column({ type: 'varchar', length: 191, nullable: true })
hint2: string | null;
@Column({ type: 'varchar', length: 191, nullable: true })
hint3: string | null;
@Column({ type: 'int', name: 'sort_order', default: 0 })
sortOrder: number;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,7 @@
import { Level } from '../entities/level.entity';
export interface ILevelRepository {
findAll(): Promise<Level[]>;
findById(id: string): Promise<Level | null>;
findAllOrdered(): Promise<Level[]>;
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Level } from '../entities/level.entity';
import { ILevelRepository } from './level.repository.interface';
@Injectable()
export class LevelRepository implements ILevelRepository {
constructor(
@InjectRepository(Level)
private readonly repository: Repository<Level>,
) {}
async findAll(): Promise<Level[]> {
return this.repository.find();
}
async findById(id: string): Promise<Level | null> {
return this.repository.findOne({ where: { id } });
}
async findAllOrdered(): Promise<Level[]> {
return this.repository.find({
order: { sortOrder: 'ASC' },
});
}
}

View File

@@ -5,6 +5,10 @@ import {
GameConfigResponseDto, GameConfigResponseDto,
GameConfigListResponseDto, GameConfigListResponseDto,
} from './dto/game-config-response.dto'; } from './dto/game-config-response.dto';
import {
LevelResponseDto,
LevelListResponseDto,
} from './dto/level-response.dto';
import { ApiResponseDto } from '../../common/dto/api-response.dto'; import { ApiResponseDto } from '../../common/dto/api-response.dto';
@ApiTags('微信小游戏') @ApiTags('微信小游戏')
@@ -13,7 +17,10 @@ export class WechatGameController {
constructor(private readonly wechatGameService: WechatGameService) {} constructor(private readonly wechatGameService: WechatGameService) {}
@Get('configs') @Get('configs')
@ApiOperation({ summary: '获取所有游戏配置', description: '获取所有激活的游戏配置列表' }) @ApiOperation({
summary: '获取所有游戏配置',
description: '获取所有激活的游戏配置列表',
})
@ApiResponse({ status: 200, description: '成功获取配置列表' }) @ApiResponse({ status: 200, description: '成功获取配置列表' })
async getAllConfigs(): Promise<ApiResponseDto<GameConfigListResponseDto>> { async getAllConfigs(): Promise<ApiResponseDto<GameConfigListResponseDto>> {
const data = await this.wechatGameService.getAllConfigs(); const data = await this.wechatGameService.getAllConfigs();
@@ -21,7 +28,10 @@ export class WechatGameController {
} }
@Get('configs/:key') @Get('configs/:key')
@ApiOperation({ summary: '根据key获取配置', description: '根据配置键名获取单个游戏配置' }) @ApiOperation({
summary: '根据key获取配置',
description: '根据配置键名获取单个游戏配置',
})
@ApiResponse({ status: 200, description: '成功获取配置' }) @ApiResponse({ status: 200, description: '成功获取配置' })
@ApiResponse({ status: 404, description: '配置不存在' }) @ApiResponse({ status: 404, description: '配置不存在' })
async getConfigByKey( async getConfigByKey(
@@ -30,4 +40,29 @@ export class WechatGameController {
const data = await this.wechatGameService.getConfigByKey(key); const data = await this.wechatGameService.getConfigByKey(key);
return ApiResponseDto.success(data); return ApiResponseDto.success(data);
} }
@Get('levels')
@ApiOperation({
summary: '获取所有关卡',
description: '获取所有关卡列表按sort_order排序',
})
@ApiResponse({ status: 200, description: '成功获取关卡列表' })
async getAllLevels(): Promise<ApiResponseDto<LevelListResponseDto>> {
const data = await this.wechatGameService.getAllLevels();
return ApiResponseDto.success(data);
}
@Get('levels/:id')
@ApiOperation({
summary: '根据ID获取关卡',
description: '根据关卡ID获取单个关卡信息',
})
@ApiResponse({ status: 200, description: '成功获取关卡' })
@ApiResponse({ status: 404, description: '关卡不存在' })
async getLevelById(
@Param('id') id: string,
): Promise<ApiResponseDto<LevelResponseDto>> {
const data = await this.wechatGameService.getLevelById(id);
return ApiResponseDto.success(data);
}
} }

View File

@@ -3,12 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { WechatGameController } from './wechat-game.controller'; import { WechatGameController } from './wechat-game.controller';
import { WechatGameService } from './wechat-game.service'; import { WechatGameService } from './wechat-game.service';
import { GameConfig } from './entities/game-config.entity'; import { GameConfig } from './entities/game-config.entity';
import { Level } from './entities/level.entity';
import { GameConfigRepository } from './repositories/game-config.repository'; import { GameConfigRepository } from './repositories/game-config.repository';
import { LevelRepository } from './repositories/level.repository';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([GameConfig])], imports: [TypeOrmModule.forFeature([GameConfig, Level])],
controllers: [WechatGameController], controllers: [WechatGameController],
providers: [WechatGameService, GameConfigRepository], providers: [WechatGameService, GameConfigRepository, LevelRepository],
exports: [WechatGameService], exports: [WechatGameService],
}) })
export class WechatGameModule {} export class WechatGameModule {}

View File

@@ -1,13 +1,21 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { GameConfigRepository } from './repositories/game-config.repository'; import { GameConfigRepository } from './repositories/game-config.repository';
import { LevelRepository } from './repositories/level.repository';
import { import {
GameConfigResponseDto, GameConfigResponseDto,
GameConfigListResponseDto, GameConfigListResponseDto,
} from './dto/game-config-response.dto'; } from './dto/game-config-response.dto';
import {
LevelResponseDto,
LevelListResponseDto,
} from './dto/level-response.dto';
@Injectable() @Injectable()
export class WechatGameService { export class WechatGameService {
constructor(private readonly gameConfigRepository: GameConfigRepository) {} constructor(
private readonly gameConfigRepository: GameConfigRepository,
private readonly levelRepository: LevelRepository,
) {}
async getAllConfigs(): Promise<GameConfigListResponseDto> { async getAllConfigs(): Promise<GameConfigListResponseDto> {
const configs = await this.gameConfigRepository.findActiveConfigs(); const configs = await this.gameConfigRepository.findActiveConfigs();
@@ -28,7 +36,31 @@ export class WechatGameService {
return this.toResponseDto(config); return this.toResponseDto(config);
} }
private toResponseDto(config: import('./entities/game-config.entity').GameConfig): GameConfigResponseDto { async getAllLevels(): Promise<LevelListResponseDto> {
const levels = await this.levelRepository.findAllOrdered();
return {
levels: levels.map((level, index) =>
this.toLevelResponseDto(level, index + 1),
),
total: levels.length,
};
}
async getLevelById(id: string): Promise<LevelResponseDto> {
const levels = await this.levelRepository.findAllOrdered();
const levelIndex = levels.findIndex((l) => l.id === id);
if (levelIndex === -1) {
throw new NotFoundException(`Level with id "${id}" not found`);
}
return this.toLevelResponseDto(levels[levelIndex], levelIndex + 1);
}
private toResponseDto(
config: import('./entities/game-config.entity').GameConfig,
): GameConfigResponseDto {
return { return {
id: config.id, id: config.id,
configKey: config.configKey, configKey: config.configKey,
@@ -39,4 +71,22 @@ export class WechatGameService {
updatedAt: config.updatedAt, updatedAt: config.updatedAt,
}; };
} }
private toLevelResponseDto(
level: import('./entities/level.entity').Level,
levelNumber: number,
): LevelResponseDto {
return {
level: levelNumber,
id: level.id,
imageUrl: level.imageUrl,
answer: level.answer,
hint1: level.hint1,
hint2: level.hint2,
hint3: level.hint3,
sortOrder: level.sortOrder,
createdAt: level.createdAt,
updatedAt: level.updatedAt,
};
}
} }