1192 lines
34 KiB
Markdown
1192 lines
34 KiB
Markdown
# MemeMind-Server Backend Analysis
|
||
|
||
**Date**: 2026-04-05
|
||
**Project**: MemeMind-Server (NestJS + TypeORM)
|
||
**Location**: `/Users/richard/Documents/code/xieyingeng/MemeMind-Server`
|
||
|
||
## Executive Summary
|
||
|
||
MemeMind-Server is a NestJS backend for a WeChat mini-game with a clean layered architecture. It provides two main modules: **WechatGame** (game level and config management) and **AppConfig** (environment setup). The server uses TypeORM with MySQL as the database and follows enterprise patterns including repositories, DTOs, and global error handling.
|
||
|
||
---
|
||
|
||
## 1. Project Structure
|
||
|
||
```
|
||
MemeMind-Server/
|
||
├── src/
|
||
│ ├── main.ts # App entry point
|
||
│ ├── app.module.ts # Root module (imports config & wechat-game)
|
||
│ ├── common/
|
||
│ │ ├── dto/
|
||
│ │ │ └── api-response.dto.ts # Generic API response wrapper
|
||
│ │ └── filters/
|
||
│ │ └── http-exception.filter.ts # Global exception handler
|
||
│ ├── config/
|
||
│ │ ├── config.module.ts # Environment config setup
|
||
│ │ ├── database.config.ts # TypeORM & MySQL config
|
||
│ │ └── env.validation.ts # Environment variable validation
|
||
│ └── modules/
|
||
│ └── wechat-game/
|
||
│ ├── wechat-game.module.ts # Module definition
|
||
│ ├── wechat-game.controller.ts # 4 REST endpoints
|
||
│ ├── wechat-game.service.ts # Business logic
|
||
│ ├── entities/
|
||
│ │ ├── level.entity.ts # Level data model
|
||
│ │ └── game-config.entity.ts # Configuration model
|
||
│ ├── dto/
|
||
│ │ ├── level-response.dto.ts # Level API response
|
||
│ │ └── game-config-response.dto.ts # Config API response
|
||
│ └── repositories/
|
||
│ ├── level.repository.ts # Level DB access
|
||
│ ├── level.repository.interface.ts
|
||
│ ├── game-config.repository.ts # Config DB access
|
||
│ └── game-config.repository.interface.ts
|
||
├── test/
|
||
│ ├── app.e2e-spec.ts
|
||
│ └── jest-e2e.json
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── tsconfig.build.json
|
||
└── nest-cli.json
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Package Dependencies
|
||
|
||
### Core Framework
|
||
- **@nestjs/core** ^11.0.1 - NestJS core
|
||
- **@nestjs/common** ^11.0.1 - Common utilities
|
||
- **@nestjs/platform-express** ^11.0.1 - Express integration
|
||
- **@nestjs/config** ^4.0.3 - Environment management
|
||
- **@nestjs/typeorm** ^11.0.0 - TypeORM integration
|
||
- **@nestjs/swagger** ^11.2.6 - API documentation
|
||
|
||
### Database
|
||
- **typeorm** ^0.3.28 - ORM framework
|
||
- **mysql2** ^3.19.1 - MySQL driver
|
||
|
||
### Data Validation & Transformation
|
||
- **class-validator** ^0.15.1 - DTO validation
|
||
- **class-transformer** ^0.5.1 - DTO transformation
|
||
- **reflect-metadata** ^0.2.2 - Reflection for decorators
|
||
|
||
### Utilities
|
||
- **rxjs** ^7.8.1 - Reactive programming
|
||
|
||
### Development Tools
|
||
- **@nestjs/cli** ^11.0.0
|
||
- **@nestjs/testing** ^11.0.1
|
||
- **typescript** ^5.7.3
|
||
- **ts-jest** ^29.2.5
|
||
- **jest** ^30.0.0
|
||
- **prettier** ^3.4.2
|
||
- **eslint** ^9.18.0
|
||
|
||
---
|
||
|
||
## 3. Application Setup (main.ts)
|
||
|
||
**File**: `src/main.ts` (46 lines)
|
||
|
||
```typescript
|
||
async function bootstrap() {
|
||
const app = await NestFactory.create(AppModule);
|
||
|
||
// Global prefix for all routes: /api
|
||
app.setGlobalPrefix('api');
|
||
|
||
// CORS enabled (supports WeChat mini-game cross-origin requests)
|
||
app.enableCors({
|
||
origin: true,
|
||
credentials: true,
|
||
});
|
||
|
||
// Global validation pipe
|
||
app.useGlobalPipes(
|
||
new ValidationPipe({
|
||
whitelist: true, // Strip unknown properties
|
||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||
transform: true, // Auto-transform DTOs
|
||
}),
|
||
);
|
||
|
||
// Global exception filter
|
||
app.useGlobalFilters(new HttpExceptionFilter());
|
||
|
||
// Swagger documentation at /api/docs
|
||
const config = new DocumentBuilder()
|
||
.setTitle('MemeMind Server API')
|
||
.setDescription('微信小游戏 MemeMind 服务端 API 文档')
|
||
.setVersion('1.0')
|
||
.build();
|
||
|
||
const port = process.env.PORT ?? 3000;
|
||
await app.listen(port);
|
||
}
|
||
```
|
||
|
||
### Key Setup Details
|
||
- **Global prefix**: All routes start with `/api`
|
||
- **CORS**: Enabled for WeChat mini-game frontend
|
||
- **Validation**: Auto-validates all DTO inputs, strips unknown fields
|
||
- **Exception handling**: Global filter wraps all errors in `ApiResponseDto`
|
||
- **Swagger**: Auto-generated API docs at `/api/docs`
|
||
- **Default port**: 3000 (configurable via `PORT` env var)
|
||
|
||
---
|
||
|
||
## 4. Root Module Configuration (app.module.ts)
|
||
|
||
**File**: `src/app.module.ts` (30 lines)
|
||
|
||
```typescript
|
||
@Module({
|
||
imports: [
|
||
AppConfigModule, // Environment setup
|
||
TypeOrmModule.forRootAsync({ // Dynamic DB config
|
||
imports: [ConfigModule],
|
||
inject: [ConfigService],
|
||
useFactory: (configService: ConfigService) => ({
|
||
type: 'mysql',
|
||
host: configService.get<string>('database.host'),
|
||
port: configService.get<number>('database.port'),
|
||
username: configService.get<string>('database.username'),
|
||
password: configService.get<string>('database.password'),
|
||
database: configService.get<string>('database.database'),
|
||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||
synchronize: NODE_ENV !== 'production', // Auto-create tables in dev
|
||
logging: NODE_ENV !== 'production', // Log queries in dev
|
||
autoLoadEntities: true, // Auto-discover entities
|
||
}),
|
||
}),
|
||
WechatGameModule, // Game module
|
||
],
|
||
})
|
||
export class AppModule {}
|
||
```
|
||
|
||
### Key Configuration
|
||
- **Async TypeORM config**: Reads database settings from environment
|
||
- **Auto-synchronization**: In development, automatically creates/updates tables
|
||
- **Auto-load entities**: Discovers all `.entity.ts` files
|
||
- **Logging**: Database queries logged in development mode
|
||
|
||
---
|
||
|
||
## 5. Environment Configuration
|
||
|
||
### Files
|
||
- **config.module.ts**: Global config setup
|
||
- **database.config.ts**: Database connection parameters
|
||
- **env.validation.ts**: Type-safe environment variables
|
||
|
||
### Configuration Structure (env.validation.ts)
|
||
|
||
```typescript
|
||
class EnvironmentVariables {
|
||
@IsEnum(Environment)
|
||
NODE_ENV: Environment = 'development'; // 'development' | 'production' | 'test'
|
||
|
||
@IsNumber()
|
||
PORT: number = 3000;
|
||
|
||
@IsString()
|
||
DB_HOST: string = 'localhost';
|
||
|
||
@IsNumber()
|
||
DB_PORT: number = 3306;
|
||
|
||
@IsString()
|
||
DB_USERNAME: string = 'meme_user';
|
||
|
||
@IsString()
|
||
DB_PASSWORD: string = '';
|
||
|
||
@IsString()
|
||
DB_DATABASE: string = 'meme_mind';
|
||
}
|
||
```
|
||
|
||
### Environment Files
|
||
The app looks for environment files in this order:
|
||
1. `.env.local` (local override)
|
||
2. `.env.production` (production override)
|
||
3. `.env` (default)
|
||
|
||
### Database Configuration (database.config.ts)
|
||
- **Default MySQL connection**: localhost:3306
|
||
- **Fallback credentials**: meme_user / (no password)
|
||
- **Default database**: meme_mind
|
||
- **Auto-synchronize**: ✅ Enabled in development
|
||
- **Query logging**: ✅ Enabled in development
|
||
|
||
---
|
||
|
||
## 6. Global Response Format
|
||
|
||
### ApiResponseDto (src/common/dto/api-response.dto.ts)
|
||
|
||
All API responses follow this generic wrapper:
|
||
|
||
```typescript
|
||
interface ApiResponseDto<T> {
|
||
success: boolean; // Request successful?
|
||
data: T | null; // Response data (null on error)
|
||
message: string | null; // Error message (null on success)
|
||
timestamp: Date; // Response timestamp
|
||
|
||
// Static factory methods
|
||
static success<T>(data: T): ApiResponseDto<T>
|
||
static error<T>(message: string): ApiResponseDto<T | null>
|
||
}
|
||
```
|
||
|
||
### Example Success Response
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"levels": [...],
|
||
"total": 50
|
||
},
|
||
"message": null,
|
||
"timestamp": "2026-04-05T10:30:00.000Z"
|
||
}
|
||
```
|
||
|
||
### Example Error Response
|
||
```json
|
||
{
|
||
"success": false,
|
||
"data": null,
|
||
"message": "Level with id 'xyz' not found",
|
||
"timestamp": "2026-04-05T10:30:00.000Z",
|
||
"path": "/api/v1/wechat-game/levels/xyz"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Global Exception Handler
|
||
|
||
### HttpExceptionFilter (src/common/filters/http-exception.filter.ts)
|
||
|
||
```typescript
|
||
@Catch()
|
||
export class HttpExceptionFilter implements ExceptionFilter {
|
||
catch(exception: unknown, host: ArgumentsHost) {
|
||
// Extracts HTTP status code from exception
|
||
// Wraps error in ApiResponseDto format
|
||
// Includes request path for debugging
|
||
// Logs unexpected errors
|
||
}
|
||
}
|
||
```
|
||
|
||
**Features**:
|
||
- ✅ Catches all exceptions (HTTP and runtime errors)
|
||
- ✅ Wraps all errors in standard `ApiResponseDto` format
|
||
- ✅ Includes request URL in error response
|
||
- ✅ Logs errors to console
|
||
- ✅ Proper HTTP status codes (404, 400, 500, etc.)
|
||
|
||
---
|
||
|
||
## 8. WechatGame Module Architecture
|
||
|
||
### Module Structure (wechat-game.module.ts)
|
||
|
||
```typescript
|
||
@Module({
|
||
imports: [TypeOrmModule.forFeature([GameConfig, Level])],
|
||
controllers: [WechatGameController],
|
||
providers: [WechatGameService, GameConfigRepository, LevelRepository],
|
||
exports: [WechatGameService],
|
||
})
|
||
export class WechatGameModule {}
|
||
```
|
||
|
||
### Layer Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ HTTP Requests (REST Controller) │
|
||
│ Base: /api/v1/wechat-game │
|
||
└──────────────┬──────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ Business Logic (Service) │
|
||
│ • Query logic │
|
||
│ • DTO mapping │
|
||
│ • Error handling │
|
||
└──────────────┬──────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ Data Access (Repositories) │
|
||
│ • Level Repository │
|
||
│ • GameConfig Repository │
|
||
└──────────────┬──────────────────────┘
|
||
│
|
||
┌──────────────▼──────────────────────┐
|
||
│ TypeORM (Database Layer) │
|
||
│ • MySQL queries │
|
||
│ • Transaction management │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. REST API Endpoints
|
||
|
||
### Base URL: `/api/v1/wechat-game`
|
||
|
||
**Endpoint 1: Get All Game Configs**
|
||
```
|
||
GET /api/v1/wechat-game/configs
|
||
Response: ApiResponseDto<GameConfigListResponseDto>
|
||
Status: 200
|
||
```
|
||
|
||
**Endpoint 2: Get Config by Key**
|
||
```
|
||
GET /api/v1/wechat-game/configs/:key
|
||
Response: ApiResponseDto<GameConfigResponseDto>
|
||
Status: 200 | 404 (not found)
|
||
```
|
||
|
||
**Endpoint 3: Get All Levels**
|
||
```
|
||
GET /api/v1/wechat-game/levels
|
||
Response: ApiResponseDto<LevelListResponseDto>
|
||
Status: 200
|
||
Description: Returns levels sorted by sort_order
|
||
```
|
||
|
||
**Endpoint 4: Get Level by ID**
|
||
```
|
||
GET /api/v1/wechat-game/levels/:id
|
||
Response: ApiResponseDto<LevelResponseDto>
|
||
Status: 200 | 404 (not found)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Data Models
|
||
|
||
### Entity 1: Level (src/modules/wechat-game/entities/level.entity.ts)
|
||
|
||
```typescript
|
||
@Entity('levels')
|
||
export class Level {
|
||
@PrimaryColumn({ type: 'varchar', length: 191 })
|
||
id: string; // UUID or custom ID
|
||
|
||
@Column({ type: 'varchar', length: 191, name: 'image_url' })
|
||
imageUrl: string; // Game image URL
|
||
|
||
@Column({ type: 'varchar', length: 191 })
|
||
answer: string; // Correct answer (single string)
|
||
|
||
@Column({ type: 'varchar', length: 191, nullable: true })
|
||
hint1: string | null; // First hint (free)
|
||
|
||
@Column({ type: 'varchar', length: 191, nullable: true })
|
||
hint2: string | null; // Second hint (costs 1 life)
|
||
|
||
@Column({ type: 'varchar', length: 191, nullable: true })
|
||
hint3: string | null; // Third hint (costs 1 life)
|
||
|
||
@Column({ type: 'int', name: 'sort_order', default: 0 })
|
||
sortOrder: number; // Order of display
|
||
|
||
@CreateDateColumn({ name: 'created_at' })
|
||
createdAt: Date;
|
||
|
||
@UpdateDateColumn({ name: 'updated_at' })
|
||
updatedAt: Date;
|
||
}
|
||
```
|
||
|
||
**Database Table**: `levels`
|
||
|
||
### Entity 2: GameConfig (src/modules/wechat-game/entities/game-config.entity.ts)
|
||
|
||
```typescript
|
||
@Entity('game_configs')
|
||
export class GameConfig {
|
||
@PrimaryGeneratedColumn('uuid')
|
||
id: string; // Auto-generated UUID
|
||
|
||
@Column({ type: 'varchar', length: 255, name: 'config_key' })
|
||
configKey: string; // Unique config identifier
|
||
|
||
@Column({ type: 'text', name: 'config_value' })
|
||
configValue: string; // Config value (stored as text)
|
||
|
||
@Column({
|
||
type: 'varchar',
|
||
length: 100,
|
||
nullable: true,
|
||
})
|
||
description: string | null; // Optional description
|
||
|
||
@Column({ type: 'boolean', default: true, name: 'is_active' })
|
||
isActive: boolean; // Is this config active?
|
||
|
||
@CreateDateColumn({ name: 'created_at' })
|
||
createdAt: Date;
|
||
|
||
@UpdateDateColumn({ name: 'updated_at' })
|
||
updatedAt: Date;
|
||
}
|
||
```
|
||
|
||
**Database Table**: `game_configs`
|
||
|
||
---
|
||
|
||
## 11. Service Layer (WechatGameService)
|
||
|
||
**File**: `src/modules/wechat-game/wechat-game.service.ts` (93 lines)
|
||
|
||
### Methods
|
||
|
||
#### 1. getAllConfigs()
|
||
```typescript
|
||
async getAllConfigs(): Promise<GameConfigListResponseDto> {
|
||
const configs = await this.gameConfigRepository.findActiveConfigs();
|
||
return {
|
||
configs: configs.map((config) => this.toResponseDto(config)),
|
||
total: configs.length,
|
||
};
|
||
}
|
||
```
|
||
- Returns only `isActive: true` configurations
|
||
- Maps entities to DTOs
|
||
- Returns list with total count
|
||
|
||
#### 2. getConfigByKey(key: string)
|
||
```typescript
|
||
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);
|
||
}
|
||
```
|
||
- Throws 404 if not found
|
||
- Maps entity to DTO
|
||
|
||
#### 3. getAllLevels()
|
||
```typescript
|
||
async getAllLevels(): Promise<LevelListResponseDto> {
|
||
const levels = await this.levelRepository.findAllOrdered();
|
||
|
||
return {
|
||
levels: levels.map((level, index) =>
|
||
this.toLevelResponseDto(level, index + 1), // 1-indexed level number
|
||
),
|
||
total: levels.length,
|
||
};
|
||
}
|
||
```
|
||
- Fetches levels in sort_order
|
||
- Adds computed `level` number (1-indexed)
|
||
- Returns list with total count
|
||
|
||
#### 4. getLevelById(id: string)
|
||
```typescript
|
||
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);
|
||
}
|
||
```
|
||
- Fetches all levels and finds by ID (not optimal - see improvements)
|
||
- Throws 404 if not found
|
||
- Adds computed `level` number
|
||
|
||
#### 5. toResponseDto(config)
|
||
```typescript
|
||
private toResponseDto(config: GameConfig): GameConfigResponseDto {
|
||
return {
|
||
id: config.id,
|
||
configKey: config.configKey,
|
||
configValue: config.configValue,
|
||
description: config.description,
|
||
isActive: config.isActive,
|
||
createdAt: config.createdAt,
|
||
updatedAt: config.updatedAt,
|
||
};
|
||
}
|
||
```
|
||
|
||
#### 6. toLevelResponseDto(level, levelNumber)
|
||
```typescript
|
||
private toLevelResponseDto(level: Level, levelNumber: number): LevelResponseDto {
|
||
return {
|
||
level: levelNumber, // Computed field
|
||
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,
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Repository Pattern
|
||
|
||
### Repository Interfaces
|
||
|
||
**ILevelRepository**:
|
||
```typescript
|
||
interface ILevelRepository {
|
||
findAll(): Promise<Level[]>;
|
||
findById(id: string): Promise<Level | null>;
|
||
findAllOrdered(): Promise<Level[]>; // Ordered by sortOrder ASC
|
||
}
|
||
```
|
||
|
||
**IGameConfigRepository**:
|
||
```typescript
|
||
interface IGameConfigRepository {
|
||
findAll(): Promise<GameConfig[]>;
|
||
findById(id: string): Promise<GameConfig | null>;
|
||
findByKey(key: string): Promise<GameConfig | null>;
|
||
findActiveConfigs(): Promise<GameConfig[]>; // Where isActive = true
|
||
}
|
||
```
|
||
|
||
### Implementations
|
||
|
||
**LevelRepository** (src/modules/wechat-game/repositories/level.repository.ts):
|
||
```typescript
|
||
@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' },
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**GameConfigRepository** (src/modules/wechat-game/repositories/game-config.repository.ts):
|
||
```typescript
|
||
@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 } });
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Data Transfer Objects (DTOs)
|
||
|
||
### LevelResponseDto (src/modules/wechat-game/dto/level-response.dto.ts)
|
||
|
||
```typescript
|
||
export class LevelResponseDto {
|
||
@ApiProperty({ description: '关卡编号' })
|
||
level: number; // Computed level number (1-indexed)
|
||
|
||
@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;
|
||
}
|
||
```
|
||
|
||
### GameConfigResponseDto (src/modules/wechat-game/dto/game-config-response.dto.ts)
|
||
|
||
```typescript
|
||
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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Controller (WechatGameController)
|
||
|
||
**File**: `src/modules/wechat-game/wechat-game.controller.ts` (69 lines)
|
||
|
||
```typescript
|
||
@ApiTags('微信小游戏')
|
||
@Controller('v1/wechat-game')
|
||
export class WechatGameController {
|
||
constructor(private readonly wechatGameService: WechatGameService) {}
|
||
|
||
@Get('configs')
|
||
@ApiOperation({
|
||
summary: '获取所有游戏配置',
|
||
description: '获取所有激活的游戏配置列表',
|
||
})
|
||
async getAllConfigs(): Promise<ApiResponseDto<GameConfigListResponseDto>> {
|
||
const data = await this.wechatGameService.getAllConfigs();
|
||
return ApiResponseDto.success(data);
|
||
}
|
||
|
||
@Get('configs/:key')
|
||
@ApiOperation({
|
||
summary: '根据key获取配置',
|
||
})
|
||
async getConfigByKey(
|
||
@Param('key') key: string,
|
||
): Promise<ApiResponseDto<GameConfigResponseDto>> {
|
||
const data = await this.wechatGameService.getConfigByKey(key);
|
||
return ApiResponseDto.success(data);
|
||
}
|
||
|
||
@Get('levels')
|
||
@ApiOperation({
|
||
summary: '获取所有关卡',
|
||
description: '获取所有关卡列表,按sort_order排序',
|
||
})
|
||
async getAllLevels(): Promise<ApiResponseDto<LevelListResponseDto>> {
|
||
const data = await this.wechatGameService.getAllLevels();
|
||
return ApiResponseDto.success(data);
|
||
}
|
||
|
||
@Get('levels/:id')
|
||
@ApiOperation({
|
||
summary: '根据ID获取关卡',
|
||
})
|
||
async getLevelById(
|
||
@Param('id') id: string,
|
||
): Promise<ApiResponseDto<LevelResponseDto>> {
|
||
const data = await this.wechatGameService.getLevelById(id);
|
||
return ApiResponseDto.success(data);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Features
|
||
- ✅ All endpoints wrapped in generic `ApiResponseDto`
|
||
- ✅ Full Swagger documentation (`@ApiOperation`, `@ApiResponse`)
|
||
- ✅ Proper HTTP methods and status codes
|
||
- ✅ Route parameters validated by NestJS
|
||
|
||
---
|
||
|
||
## 15. Integration with Client
|
||
|
||
### Client API Call Pattern
|
||
|
||
From the Cocos Creator client (`LevelDataManager.ts`), the server is called:
|
||
|
||
```typescript
|
||
// Client calls
|
||
const response = await HttpUtil.get<ApiResponse>('https://ilookai.cn/api/v1/wechat-game/levels');
|
||
|
||
// Server Response
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"levels": [
|
||
{
|
||
"level": 1,
|
||
"id": "level-001",
|
||
"imageUrl": "https://example.com/image.jpg",
|
||
"answer": "answer text",
|
||
"hint1": "hint 1",
|
||
"hint2": "hint 2",
|
||
"hint3": "hint 3",
|
||
"sortOrder": 0,
|
||
"createdAt": "2026-04-05T00:00:00Z",
|
||
"updatedAt": "2026-04-05T00:00:00Z"
|
||
},
|
||
...
|
||
],
|
||
"total": 50
|
||
},
|
||
"message": null,
|
||
"timestamp": "2026-04-05T10:30:00.000Z"
|
||
}
|
||
```
|
||
|
||
### Client Integration Points
|
||
|
||
1. **Level Loading**: Client calls `GET /api/v1/wechat-game/levels`
|
||
2. **Single Level Fetch**: Client calls `GET /api/v1/wechat-game/levels/:id` (though it doesn't currently)
|
||
3. **Config Management**: Client can call `GET /api/v1/wechat-game/configs` for game settings
|
||
|
||
---
|
||
|
||
## 16. Architecture Patterns
|
||
|
||
### 1. Dependency Injection
|
||
- NestJS constructor injection for all dependencies
|
||
- Repository interfaces decoupled from implementations
|
||
- Testable design
|
||
|
||
### 2. Repository Pattern
|
||
- Repositories abstract database access
|
||
- Implements `IRepository` interfaces
|
||
- Easy to mock for testing
|
||
|
||
### 3. Service Layer Pattern
|
||
- Services contain business logic
|
||
- Controllers delegate to services
|
||
- Services delegate to repositories
|
||
|
||
### 4. DTO Pattern
|
||
- DTOs separate API contracts from internal entities
|
||
- Validation and transformation in DTOs
|
||
- Swagger documentation on DTO fields
|
||
|
||
### 5. Global Exception Handling
|
||
- Single exception filter wraps all errors
|
||
- Consistent error response format
|
||
- Centralized logging
|
||
|
||
### 6. Configuration Management
|
||
- Environment-based configuration
|
||
- Typed environment variables
|
||
- Validation on startup
|
||
|
||
---
|
||
|
||
## 17. TypeORM Configuration Details
|
||
|
||
### Auto-Discovery
|
||
- Entities auto-loaded from: `**/*.entity{.ts,.js}`
|
||
- Repositories auto-created by `@InjectRepository()`
|
||
|
||
### Development Features
|
||
- ✅ Database synchronization (auto-create tables)
|
||
- ✅ Query logging to console
|
||
- ✅ SQL visible in development
|
||
|
||
### Production Features
|
||
- ✅ No auto-sync (safer)
|
||
- ✅ No query logging (better performance)
|
||
- ✅ Connection pooling via mysql2
|
||
|
||
### Column Mapping
|
||
- Snake_case in database: `image_url`, `sort_order`, `created_at`
|
||
- Camel case in entities: `imageUrl`, `sortOrder`, `createdAt`
|
||
- Auto-converted via TypeORM decorators
|
||
|
||
---
|
||
|
||
## 18. Security Patterns
|
||
|
||
### Global Validation Pipe
|
||
```typescript
|
||
new ValidationPipe({
|
||
whitelist: true, // ✅ Remove unknown properties
|
||
forbidNonWhitelisted: true, // ✅ Throw if unknown properties
|
||
transform: true, // ✅ Auto-transform types
|
||
})
|
||
```
|
||
|
||
### Considerations for Production
|
||
- ⚠️ No authentication middleware currently
|
||
- ⚠️ No rate limiting
|
||
- ⚠️ No request logging middleware
|
||
- ⚠️ CORS allows all origins (fine for public API)
|
||
- ⚠️ No input sanitization beyond whitelist
|
||
|
||
### Recommendations
|
||
1. Add JWT authentication for admin endpoints
|
||
2. Add rate limiting (e.g., `@nestjs/throttler`)
|
||
3. Add request logging (e.g., Morgan middleware)
|
||
4. Add input validation for text fields (sanitize HTML)
|
||
5. Add API key authentication for WeChat client
|
||
|
||
---
|
||
|
||
## 19. Performance Considerations
|
||
|
||
### Current Implementation
|
||
```
|
||
GET /api/v1/wechat-game/levels/:id
|
||
└─> getAllLevels() - Fetches ALL levels
|
||
└─> findAllOrdered() - Database query for all levels
|
||
└─> Memory search for :id - O(n) in application
|
||
```
|
||
|
||
**Problem**: `getLevelById()` fetches all levels, then searches in memory
|
||
|
||
### Optimization
|
||
```typescript
|
||
// BETTER - Direct database query
|
||
async getLevelById(id: string): Promise<LevelResponseDto> {
|
||
const level = await this.levelRepository.findById(id);
|
||
if (!level) throw new NotFoundException();
|
||
|
||
// Need to get level index for number calculation
|
||
const index = await this.levelRepository.getLevelIndex(id);
|
||
return this.toLevelResponseDto(level, index + 1);
|
||
}
|
||
```
|
||
|
||
### Database Indexing
|
||
- Consider adding index on `levels.sort_order` for sorting
|
||
- Consider adding index on `game_configs.config_key` for lookups
|
||
- Consider adding composite index on `game_configs(config_key, is_active)`
|
||
|
||
---
|
||
|
||
## 20. API Response Flow Diagram
|
||
|
||
```
|
||
┌─────────────────────────┐
|
||
│ HTTP Request │
|
||
│ GET /api/v1/... │
|
||
└────────────┬────────────┘
|
||
│
|
||
┌────────────▼────────────┐
|
||
│ Global Validation Pipe │ ← Validates path params
|
||
└────────────┬────────────┘
|
||
│
|
||
┌────────────▼────────────────────────┐
|
||
│ Controller Method │
|
||
│ @Get('levels/:id') │
|
||
└────────────┬────────────────────────┘
|
||
│
|
||
┌────────────▼────────────────────────┐
|
||
│ Service Method │
|
||
│ getLevelById(id) │
|
||
│ - Repository query │
|
||
│ - DTO mapping │
|
||
│ - Null check → NotFoundException │
|
||
└────────────┬────────────────────────┘
|
||
│
|
||
Success ───────────────┐
|
||
or Error │
|
||
│ │
|
||
┌────────────▼────────────────────────┐
|
||
│ Global Exception Filter (if error) │
|
||
│ Wraps in ApiResponseDto │
|
||
└────────────┬────────────────────────┘
|
||
│
|
||
┌────────────▼────────────────────────┐
|
||
│ ApiResponseDto.success() or error()│
|
||
│ Wraps in standard response format │
|
||
└────────────┬────────────────────────┘
|
||
│
|
||
┌────────────▼────────────────────────┐
|
||
│ HTTP Response (200, 400, 404, 500) │
|
||
│ JSON: {success, data, message} │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 21. Key Findings
|
||
|
||
### Strengths ✅
|
||
1. **Clean architecture**: Proper separation of concerns (Controller → Service → Repository)
|
||
2. **Type safety**: Full TypeScript with DTOs and entity types
|
||
3. **Error handling**: Centralized exception filter with consistent format
|
||
4. **Documentation**: Swagger auto-documentation on all endpoints
|
||
5. **Configuration**: Type-safe environment variables with validation
|
||
6. **Database**: TypeORM with MySQL, auto-sync in dev
|
||
7. **Validation**: Global validation pipe with whitelist protection
|
||
8. **Modularity**: Feature-based module structure
|
||
|
||
### Potential Issues ⚠️
|
||
1. **Performance**: `getLevelById()` fetches all levels instead of direct query
|
||
2. **Authentication**: No auth mechanism for sensitive endpoints
|
||
3. **Rate limiting**: No rate limiting implemented
|
||
4. **Caching**: No caching layer (clients could cache locally)
|
||
5. **Pagination**: No pagination on large datasets
|
||
6. **Middleware**: Limited middleware (no logging, no request IDs)
|
||
7. **Testing**: No E2E tests implemented yet
|
||
|
||
### Missing Features 🔧
|
||
1. Authentication & Authorization (guards, JWT tokens)
|
||
2. POST/PUT/DELETE endpoints (read-only currently)
|
||
3. User management module
|
||
4. Score/points tracking
|
||
5. Level submission endpoints
|
||
6. Statistics/leaderboard endpoints
|
||
|
||
---
|
||
|
||
## 22. Database Schema
|
||
|
||
### levels table
|
||
```sql
|
||
CREATE TABLE levels (
|
||
id VARCHAR(191) PRIMARY KEY,
|
||
image_url VARCHAR(191) NOT NULL,
|
||
answer VARCHAR(191) NOT NULL,
|
||
hint1 VARCHAR(191),
|
||
hint2 VARCHAR(191),
|
||
hint3 VARCHAR(191),
|
||
sort_order INT DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
KEY idx_sort_order (sort_order)
|
||
);
|
||
```
|
||
|
||
### game_configs table
|
||
```sql
|
||
CREATE TABLE game_configs (
|
||
id CHAR(36) PRIMARY KEY, -- UUID
|
||
config_key VARCHAR(255) NOT NULL UNIQUE,
|
||
config_value TEXT NOT NULL,
|
||
description VARCHAR(100),
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
KEY idx_config_key (config_key),
|
||
KEY idx_active (is_active)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 23. Extension Points
|
||
|
||
### Adding User Authentication
|
||
|
||
```typescript
|
||
// 1. Create users.entity.ts
|
||
@Entity('users')
|
||
export class User {
|
||
@PrimaryGeneratedColumn('uuid')
|
||
id: string;
|
||
|
||
@Column({ unique: true })
|
||
wxOpenId: string; // WeChat OpenID
|
||
|
||
@Column()
|
||
nickname: string;
|
||
|
||
@Column()
|
||
lives: number; // Current lives
|
||
|
||
@Column()
|
||
currentLevel: number; // Progress
|
||
}
|
||
|
||
// 2. Create user.module.ts with UserController, UserService, UserRepository
|
||
// 3. Add JWT guard to protected endpoints
|
||
// 4. Add user context to requests
|
||
```
|
||
|
||
### Adding Level Submission Endpoint
|
||
|
||
```typescript
|
||
// POST /api/v1/wechat-game/levels/:id/submit
|
||
@Post('levels/:id/submit')
|
||
async submitAnswer(
|
||
@Param('id') id: string,
|
||
@Body() dto: SubmitAnswerDto,
|
||
): Promise<ApiResponseDto<SubmitAnswerResponseDto>> {
|
||
// Validate answer
|
||
// Update user progress
|
||
// Award points/lives
|
||
// Return result
|
||
}
|
||
```
|
||
|
||
### Adding Caching
|
||
|
||
```typescript
|
||
// Import cache module
|
||
import { CacheModule } from '@nestjs/cache-manager';
|
||
|
||
// In WechatGameModule
|
||
@Module({
|
||
imports: [
|
||
CacheModule.register({
|
||
ttl: 3600, // 1 hour
|
||
}),
|
||
TypeOrmModule.forFeature([GameConfig, Level]),
|
||
],
|
||
})
|
||
|
||
// In service
|
||
constructor(
|
||
private cacheManager: Cache,
|
||
private levelRepository: LevelRepository,
|
||
) {}
|
||
|
||
async getAllLevels() {
|
||
const cached = await this.cacheManager.get('all_levels');
|
||
if (cached) return cached;
|
||
|
||
const levels = await this.levelRepository.findAllOrdered();
|
||
await this.cacheManager.set('all_levels', levels, 3600);
|
||
return levels;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 24. Quick Reference
|
||
|
||
### Starting the Server
|
||
```bash
|
||
npm install # Install dependencies
|
||
npm run start:dev # Development with watch mode
|
||
npm run start:prod # Production mode
|
||
npm run build # Build TypeScript
|
||
npm test # Run tests
|
||
npm run test:e2e # End-to-end tests
|
||
```
|
||
|
||
### Project Commands
|
||
```bash
|
||
npm run format # Format code with Prettier
|
||
npm run lint # Lint and fix with ESLint
|
||
npm run test:watch # Watch tests
|
||
npm run test:cov # Test coverage
|
||
```
|
||
|
||
### Environment Setup
|
||
```bash
|
||
# .env file
|
||
NODE_ENV=development
|
||
PORT=3000
|
||
DB_HOST=localhost
|
||
DB_PORT=3306
|
||
DB_USERNAME=meme_user
|
||
DB_PASSWORD=password
|
||
DB_DATABASE=meme_mind
|
||
```
|
||
|
||
### Swagger UI
|
||
- **Location**: `http://localhost:3000/api/docs`
|
||
- **API Endpoint**: `http://localhost:3000/api/docs-json`
|
||
- Auto-updated from decorators
|
||
|
||
### Database Connection
|
||
```
|
||
Host: localhost
|
||
Port: 3306
|
||
Username: meme_user
|
||
Database: meme_mind
|
||
```
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
MemeMind-Server is a well-structured NestJS backend with:
|
||
- ✅ Clean layered architecture (Controller → Service → Repository)
|
||
- ✅ Type-safe with full TypeScript
|
||
- ✅ Comprehensive error handling and validation
|
||
- ✅ Auto-generated Swagger documentation
|
||
- ✅ MySQL persistence with TypeORM
|
||
- ✅ Environment-based configuration
|
||
- ✅ Ready for feature expansion (auth, scoring, leaderboards)
|
||
|
||
The server currently provides **read-only access** to game levels and configurations, with endpoints designed to support the WeChat mini-game client's level loading and configuration management needs.
|
||
|
||
---
|
||
|
||
*Generated: 2026-04-05 | Last Updated: 2026-04-05*
|