This commit is contained in:
richarjiang
2026-03-15 11:35:39 +08:00
commit 6413d4f34c
27 changed files with 7589 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
export class GameConfigResponseDto {
@ApiProperty({ description: '配置ID' })
id: string;
@ApiProperty({ description: '配置键名' })
configKey: string;
@ApiProperty({ description: '配置值' })
configValue: string;
@ApiProperty({ description: '配置描述', nullable: true })
description: string | null;
@ApiProperty({ description: '是否激活' })
isActive: boolean;
@ApiProperty({ description: '创建时间' })
createdAt: Date;
@ApiProperty({ description: '更新时间' })
updatedAt: Date;
}
export class GameConfigListResponseDto {
@ApiProperty({ type: [GameConfigResponseDto], description: '配置列表' })
configs: GameConfigResponseDto[];
@ApiProperty({ description: '配置总数' })
total: number;
}

View File

@@ -0,0 +1,35 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('game_configs')
export class GameConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255, name: 'config_key' })
configKey: string;
@Column({ type: 'text', name: 'config_value' })
configValue: string;
@Column({
type: 'varchar',
length: 100,
nullable: true,
})
description: string | null;
@Column({ type: 'boolean', default: true, name: 'is_active' })
isActive: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,8 @@
import { GameConfig } from '../entities/game-config.entity';
export interface IGameConfigRepository {
findAll(): Promise<GameConfig[]>;
findById(id: string): Promise<GameConfig | null>;
findByKey(key: string): Promise<GameConfig | null>;
findActiveConfigs(): Promise<GameConfig[]>;
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GameConfig } from '../entities/game-config.entity';
import { IGameConfigRepository } from './game-config.repository.interface';
@Injectable()
export class GameConfigRepository implements IGameConfigRepository {
constructor(
@InjectRepository(GameConfig)
private readonly repository: Repository<GameConfig>,
) {}
async findAll(): Promise<GameConfig[]> {
return this.repository.find();
}
async findById(id: string): Promise<GameConfig | null> {
return this.repository.findOne({ where: { id } });
}
async findByKey(key: string): Promise<GameConfig | null> {
return this.repository.findOne({ where: { configKey: key } });
}
async findActiveConfigs(): Promise<GameConfig[]> {
return this.repository.find({ where: { isActive: true } });
}
}

View File

@@ -0,0 +1,33 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { WechatGameService } from './wechat-game.service';
import {
GameConfigResponseDto,
GameConfigListResponseDto,
} from './dto/game-config-response.dto';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
@ApiTags('微信小游戏')
@Controller('v1/wechat-game')
export class WechatGameController {
constructor(private readonly wechatGameService: WechatGameService) {}
@Get('configs')
@ApiOperation({ summary: '获取所有游戏配置', description: '获取所有激活的游戏配置列表' })
@ApiResponse({ status: 200, description: '成功获取配置列表' })
async getAllConfigs(): Promise<ApiResponseDto<GameConfigListResponseDto>> {
const data = await this.wechatGameService.getAllConfigs();
return ApiResponseDto.success(data);
}
@Get('configs/:key')
@ApiOperation({ summary: '根据key获取配置', description: '根据配置键名获取单个游戏配置' })
@ApiResponse({ status: 200, description: '成功获取配置' })
@ApiResponse({ status: 404, description: '配置不存在' })
async getConfigByKey(
@Param('key') key: string,
): Promise<ApiResponseDto<GameConfigResponseDto>> {
const data = await this.wechatGameService.getConfigByKey(key);
return ApiResponseDto.success(data);
}
}

View File

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

View File

@@ -0,0 +1,86 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WechatGameService } from './wechat-game.service';
import { GameConfigRepository } from './repositories/game-config.repository';
import { NotFoundException } from '@nestjs/common';
import { GameConfig } from './entities/game-config.entity';
describe('WechatGameService', () => {
let service: WechatGameService;
let repository: GameConfigRepository;
const mockGameConfig: GameConfig = {
id: 'test-uuid',
configKey: 'game_speed',
configValue: '1.5',
description: 'Game speed multiplier',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockRepository = {
findActiveConfigs: jest.fn(),
findByKey: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WechatGameService,
{
provide: GameConfigRepository,
useValue: mockRepository,
},
],
}).compile();
service = module.get<WechatGameService>(WechatGameService);
repository = module.get<GameConfigRepository>(GameConfigRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getAllConfigs', () => {
it('should return all active configs', async () => {
mockRepository.findActiveConfigs.mockResolvedValue([mockGameConfig]);
const result = await service.getAllConfigs();
expect(result.configs).toHaveLength(1);
expect(result.total).toBe(1);
expect(result.configs[0].configKey).toBe('game_speed');
expect(mockRepository.findActiveConfigs).toHaveBeenCalled();
});
it('should return empty array when no configs found', async () => {
mockRepository.findActiveConfigs.mockResolvedValue([]);
const result = await service.getAllConfigs();
expect(result.configs).toHaveLength(0);
expect(result.total).toBe(0);
});
});
describe('getConfigByKey', () => {
it('should return config by key', async () => {
mockRepository.findByKey.mockResolvedValue(mockGameConfig);
const result = await service.getConfigByKey('game_speed');
expect(result.configKey).toBe('game_speed');
expect(result.configValue).toBe('1.5');
expect(mockRepository.findByKey).toHaveBeenCalledWith('game_speed');
});
it('should throw NotFoundException when config not found', async () => {
mockRepository.findByKey.mockResolvedValue(null);
await expect(service.getConfigByKey('nonexistent')).rejects.toThrow(
NotFoundException,
);
});
});
});

View File

@@ -0,0 +1,42 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { GameConfigRepository } from './repositories/game-config.repository';
import {
GameConfigResponseDto,
GameConfigListResponseDto,
} from './dto/game-config-response.dto';
@Injectable()
export class WechatGameService {
constructor(private readonly gameConfigRepository: GameConfigRepository) {}
async getAllConfigs(): Promise<GameConfigListResponseDto> {
const configs = await this.gameConfigRepository.findActiveConfigs();
return {
configs: configs.map((config) => this.toResponseDto(config)),
total: configs.length,
};
}
async getConfigByKey(key: string): Promise<GameConfigResponseDto> {
const config = await this.gameConfigRepository.findByKey(key);
if (!config) {
throw new NotFoundException(`Game config with key "${key}" not found`);
}
return this.toResponseDto(config);
}
private toResponseDto(config: import('./entities/game-config.entity').GameConfig): GameConfigResponseDto {
return {
id: config.id,
configKey: config.configKey,
configValue: config.configValue,
description: config.description,
isActive: config.isActive,
createdAt: config.createdAt,
updatedAt: config.updatedAt,
};
}
}