feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
This commit is contained in:
634
开发者代码检查规范.md
634
开发者代码检查规范.md
@@ -1,8 +1,8 @@
|
||||
# 开发者代码检查规范
|
||||
# 开发者代码检查规范 - Whale Town 游戏服务器
|
||||
|
||||
## 📖 概述
|
||||
|
||||
本文档为开发者提供全面的代码检查规范,确保代码质量、可维护性和团队协作效率。规范涵盖命名、注释、代码质量、架构分层、测试覆盖和文档生成六个核心方面。
|
||||
本文档为Whale Town游戏服务器开发者提供全面的代码检查规范,确保代码质量、可维护性和团队协作效率。规范针对NestJS游戏服务器的双模式架构、实时通信、属性测试等特点进行了专门优化。
|
||||
|
||||
## 🎯 检查流程
|
||||
|
||||
@@ -21,24 +21,37 @@
|
||||
|
||||
### 📁 文件和文件夹命名
|
||||
|
||||
**核心规则:使用下划线分隔(snake_case)**
|
||||
**核心规则:使用下划线分隔(snake_case),保持项目一致性**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
- user_controller.ts
|
||||
- player_service.ts
|
||||
- create_room_dto.ts
|
||||
- src/business/auth/
|
||||
- src/core/db/users/
|
||||
- admin_operation_log_service.ts
|
||||
- location_broadcast_gateway.ts
|
||||
- websocket_auth_guard.ts
|
||||
- src/business/user_mgmt/
|
||||
- src/core/location_broadcast_core/
|
||||
|
||||
❌ 错误示例:
|
||||
- UserController.ts # 大驼峰命名
|
||||
- playerService.ts # 小驼峰命名
|
||||
- base-users.service.ts # 短横线分隔(常见错误!)
|
||||
- user-service.ts # 短横线分隔
|
||||
- adminOperationLog.service.ts # 小驼峰命名
|
||||
- src/Business/Auth/ # 大驼峰命名
|
||||
```
|
||||
|
||||
**⚠️ 特别注意:短横线(kebab-case)是最常见的文件命名错误!**
|
||||
**⚠️ 特别注意:保持项目现有的下划线命名风格,确保代码库一致性!**
|
||||
|
||||
**游戏服务器特殊文件类型:**
|
||||
```typescript
|
||||
✅ 游戏服务器专用文件类型:
|
||||
- location_broadcast.gateway.ts # WebSocket网关
|
||||
- users_memory.service.ts # 内存模式服务
|
||||
- file_redis.service.ts # 文件模式Redis
|
||||
- admin.property.spec.ts # 属性测试
|
||||
- zulip_integration.e2e.spec.ts # E2E测试
|
||||
- performance_monitor.middleware.ts # 性能监控中间件
|
||||
- websocket_docs.controller.ts # WebSocket文档控制器
|
||||
```
|
||||
|
||||
### 🏗️ 文件夹结构优化
|
||||
|
||||
@@ -62,12 +75,17 @@ src/
|
||||
- 不超过3个文件:移到上级目录(扁平化)
|
||||
- 4个以上文件:可以保持独立文件夹
|
||||
- 完整功能模块:即使文件较少也可以保持独立(需特殊说明)
|
||||
- **游戏服务器特殊考虑**:
|
||||
- WebSocket相关文件可以独立成文件夹(实时通信复杂性)
|
||||
- 双模式服务文件建议放在同一文件夹(便于对比)
|
||||
- 属性测试文件较多的模块可以保持独立结构
|
||||
|
||||
**检查方法(重要):**
|
||||
1. **必须使用工具详细检查**:不能凭印象判断文件夹内容
|
||||
2. **逐个统计文件数量**:使用`listDirectory(path, depth=2)`获取准确数据
|
||||
3. **识别单文件文件夹**:只有1个文件的文件夹必须扁平化
|
||||
4. **更新引用路径**:移动文件后必须更新所有import语句
|
||||
5. **考虑游戏服务器特殊性**:实时通信、双模式、测试复杂度
|
||||
|
||||
**常见检查错误:**
|
||||
- ❌ 只看到文件夹存在就认为结构合理
|
||||
@@ -79,7 +97,8 @@ src/
|
||||
1. 使用listDirectory工具查看详细结构
|
||||
2. 逐个文件夹统计文件数量
|
||||
3. 识别需要扁平化的文件夹(≤3个文件)
|
||||
4. 执行文件移动和路径更新操作
|
||||
4. 考虑游戏服务器特殊性(WebSocket、双模式、测试复杂度)
|
||||
5. 执行文件移动和路径更新操作
|
||||
|
||||
### 🔤 变量和函数命名
|
||||
|
||||
@@ -140,6 +159,8 @@ const saltRounds = 10;
|
||||
@Get('user/get-info')
|
||||
@Post('room/join-room')
|
||||
@Put('player/update-position')
|
||||
@WebSocketGateway({ path: '/location-broadcast' }) # WebSocket路径
|
||||
@MessagePattern('user-position-update') # 消息模式
|
||||
|
||||
❌ 错误示例:
|
||||
@Get('user/getInfo')
|
||||
@@ -292,12 +313,25 @@ async validateUser(loginRequest: LoginRequest): Promise<AuthResult> {
|
||||
```typescript
|
||||
// ✅ 正确:只导入使用的模块
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { User } from './user.entity';
|
||||
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||
import { Server } from 'socket.io';
|
||||
|
||||
// ❌ 错误:导入未使用的模块
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { User, Admin } from './user.entity';
|
||||
import * as crypto from 'crypto'; // 未使用
|
||||
import { RedisService } from '../redis/redis.service'; // 未使用
|
||||
```
|
||||
|
||||
**游戏服务器特殊导入检查:**
|
||||
```typescript
|
||||
// 检查双模式服务导入
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersMemoryService } from './users-memory.service'; // 确保两个都被使用
|
||||
|
||||
// 检查WebSocket相关导入
|
||||
import { Server, Socket } from 'socket.io'; // 确保Socket类型被使用
|
||||
import { WsException } from '@nestjs/websockets'; // 确保异常处理被使用
|
||||
```
|
||||
|
||||
### 📊 常量定义检查
|
||||
@@ -325,6 +359,66 @@ private generateVerificationCode(): string {
|
||||
const unusedVariable = 'test';
|
||||
```
|
||||
|
||||
### 🚫 TODO项处理
|
||||
|
||||
**强制要求:最终文件不能包含TODO项**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:包含TODO项的代码
|
||||
async getUserProfile(id: string): Promise<UserProfile> {
|
||||
// TODO: 实现用户档案查询
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
// ❌ 游戏服务器常见TODO(需要处理)
|
||||
async sendSmsVerification(phone: string): Promise<void> {
|
||||
// TODO: 集成短信服务提供商
|
||||
throw new Error('SMS service not implemented');
|
||||
}
|
||||
|
||||
async cleanupOldPositions(): Promise<void> {
|
||||
// TODO: 实现位置历史数据清理
|
||||
console.log('Position cleanup not implemented');
|
||||
}
|
||||
|
||||
// ✅ 正确:真正实现功能
|
||||
async getUserProfile(id: string): Promise<UserProfile> {
|
||||
const profile = await this.userProfileRepository.findOne({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
throw new NotFoundException('用户档案不存在');
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
// ✅ 正确:游戏服务器实现示例
|
||||
async broadcastPositionUpdate(userId: string, position: Position): Promise<void> {
|
||||
const room = await this.getRoomByUserId(userId);
|
||||
this.server.to(room.id).emit('position-update', {
|
||||
userId,
|
||||
position,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 记录位置历史(如果需要)
|
||||
await this.savePositionHistory(userId, position);
|
||||
}
|
||||
```
|
||||
|
||||
**游戏服务器TODO处理优先级:**
|
||||
- **高优先级**:实时通信功能、用户认证、数据持久化
|
||||
- **中优先级**:性能优化、监控告警、数据清理
|
||||
- **低优先级**:辅助功能、统计分析、第三方集成
|
||||
|
||||
**TODO处理原则:**
|
||||
- **真正实现**:如果功能需要,必须提供完整的实现
|
||||
- **删除代码**:如果功能不需要,删除相关方法和接口
|
||||
- **分阶段实现**:如果功能复杂,可以分多个版本实现,但每个版本都不能有TODO
|
||||
- **文档说明**:如果某些功能暂不实现,在README中说明原因和计划
|
||||
|
||||
### 📏 方法长度检查
|
||||
|
||||
```typescript
|
||||
@@ -365,12 +459,26 @@ src/
|
||||
|
||||
#### 命名规范
|
||||
- **检查范围**:仅检查当前执行检查的文件夹,不考虑其他同层功能模块
|
||||
- **业务支撑模块**:专门为特定业务功能提供技术支撑,使用`_core`后缀(如`location_broadcast_core`、`user_auth_core`)
|
||||
- **通用工具模块**:提供可复用的数据访问或基础技术服务,不使用`_core`后缀(如`user_profiles`、`redis_cache`、`logger`)
|
||||
- **业务支撑模块**:专门为特定业务功能提供技术支撑,使用`_core`后缀(如`location_broadcast_core`、`admin_core`)
|
||||
- **通用工具模块**:提供可复用的数据访问或基础技术服务,不使用`_core`后缀(如`user_profiles`、`redis`、`logger`)
|
||||
|
||||
**判断标准:**
|
||||
- **业务支撑模块**:模块名称体现特定业务领域,为该业务提供技术实现 → 使用`_core`后缀
|
||||
- **通用工具模块**:模块提供通用的数据访问或技术服务,可被多个业务复用 → 不使用后缀
|
||||
**游戏服务器Core层特殊模块:**
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
src/core/location_broadcast_core/ # 专门为位置广播业务提供技术支撑
|
||||
src/core/admin_core/ # 专门为管理员业务提供技术支撑
|
||||
src/core/zulip_core/ # 专门为Zulip集成提供技术支撑
|
||||
src/core/login_core/ # 专门为登录认证提供技术支撑
|
||||
src/core/security_core/ # 专门为安全功能提供技术支撑
|
||||
src/core/db/user_profiles/ # 通用的用户档案数据访问服务
|
||||
src/core/redis/ # 通用的Redis技术封装
|
||||
src/core/utils/logger/ # 通用的日志工具服务
|
||||
|
||||
❌ 错误示例:
|
||||
src/core/location_broadcast/ # 应该是location_broadcast_core
|
||||
src/core/db/user_profiles_core/ # 应该是user_profiles(通用工具)
|
||||
src/core/redis_core/ # 应该是redis(通用工具)
|
||||
```
|
||||
|
||||
**判断流程:**
|
||||
```
|
||||
@@ -407,26 +515,38 @@ src/core/redis_core/ # 应该是redis(通用工具)
|
||||
```typescript
|
||||
// ✅ 正确:Core层专注技术实现
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
export class LocationBroadcastCoreService {
|
||||
/**
|
||||
* 设置缓存数据
|
||||
* 广播位置更新到指定房间
|
||||
*
|
||||
* 技术实现:
|
||||
* 1. 验证key格式
|
||||
* 2. 序列化数据
|
||||
* 3. 设置过期时间
|
||||
* 4. 处理连接异常
|
||||
* 1. 验证WebSocket连接状态
|
||||
* 2. 序列化位置数据
|
||||
* 3. 通过Socket.IO广播消息
|
||||
* 4. 记录广播性能指标
|
||||
* 5. 处理广播异常和重试
|
||||
*/
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
// 专注Redis技术实现细节
|
||||
async broadcastToRoom(roomId: string, data: PositionData): Promise<void> {
|
||||
// 专注WebSocket技术实现细节
|
||||
const room = this.server.sockets.adapter.rooms.get(roomId);
|
||||
if (!room) {
|
||||
throw new NotFoundException(`Room ${roomId} not found`);
|
||||
}
|
||||
|
||||
this.server.to(roomId).emit('position-update', data);
|
||||
this.metricsService.recordBroadcast(roomId, data.userId);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:Core层包含业务逻辑
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
async setUserSession(userId: string, sessionData: any): Promise<void> {
|
||||
// 错误:包含了用户会话的业务概念
|
||||
export class LocationBroadcastCoreService {
|
||||
async broadcastUserPosition(userId: string, position: Position): Promise<void> {
|
||||
// 错误:包含了用户权限检查的业务概念
|
||||
const user = await this.userService.findById(userId);
|
||||
if (user.status !== UserStatus.ACTIVE) {
|
||||
throw new ForbiddenException('用户状态不允许位置广播');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -552,21 +672,41 @@ export class DatabaseService {
|
||||
|
||||
### 📋 测试文件存在性
|
||||
|
||||
**规则:每个Service都必须有对应的.spec.ts测试文件**
|
||||
**规则:每个Service、Controller、Gateway都必须有对应的测试文件**
|
||||
|
||||
**⚠️ Service定义(重要):**
|
||||
只有以下类型需要测试文件:
|
||||
**⚠️ 游戏服务器测试要求(重要):**
|
||||
以下类型需要测试文件:
|
||||
- ✅ **Service类**:文件名包含`.service.ts`的业务逻辑类
|
||||
- ✅ **Controller类**:文件名包含`.controller.ts`的控制器类
|
||||
- ✅ **Gateway类**:文件名包含`.gateway.ts`的WebSocket网关类
|
||||
- ✅ **Guard类**:文件名包含`.guard.ts`的守卫类(游戏服务器安全重要)
|
||||
- ✅ **Interceptor类**:文件名包含`.interceptor.ts`的拦截器类(日志监控重要)
|
||||
- ✅ **Middleware类**:文件名包含`.middleware.ts`的中间件类(性能监控重要)
|
||||
|
||||
**❌ 以下类型不需要测试文件:**
|
||||
- ❌ **Middleware类**:中间件(`.middleware.ts`)不需要测试文件
|
||||
- ❌ **Guard类**:守卫(`.guard.ts`)不需要测试文件
|
||||
- ❌ **DTO类**:数据传输对象(`.dto.ts`)不需要测试文件
|
||||
- ❌ **Interface文件**:接口定义(`.interface.ts`)不需要测试文件
|
||||
- ❌ **Utils工具类**:工具函数(`.utils.ts`)不需要测试文件
|
||||
- ❌ **简单Utils工具类**:简单工具函数(`.utils.ts`)不需要测试文件
|
||||
- ❌ **Config文件**:配置文件(`.config.ts`)不需要测试文件
|
||||
- ❌ **Constants文件**:常量定义(`.constants.ts`)不需要测试文件
|
||||
|
||||
**游戏服务器特殊测试要求:**
|
||||
```typescript
|
||||
// ✅ 必须有测试的文件类型
|
||||
src/business/location-broadcast/location-broadcast.gateway.ts
|
||||
src/business/location-broadcast/location-broadcast.gateway.spec.ts
|
||||
|
||||
src/core/security-core/websocket-auth.guard.ts
|
||||
src/core/security-core/websocket-auth.guard.spec.ts
|
||||
|
||||
src/business/admin/performance-monitor.middleware.ts
|
||||
src/business/admin/performance-monitor.middleware.spec.ts
|
||||
|
||||
// ❌ 不需要测试的文件类型
|
||||
src/business/location-broadcast/dto/position-update.dto.ts # DTO不需要测试
|
||||
src/core/location-broadcast-core/position.interface.ts # 接口不需要测试
|
||||
src/business/admin/admin.constants.ts # 常量不需要测试
|
||||
```
|
||||
|
||||
**测试文件位置规范(重要):**
|
||||
- ✅ **正确位置**:测试文件必须与对应源文件放在同一目录
|
||||
@@ -635,26 +775,65 @@ describe('UserService', () => {
|
||||
**要求:每个方法必须测试正常情况、异常情况和边界情况**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:完整的测试场景
|
||||
describe('createUser', () => {
|
||||
// 正常情况
|
||||
it('should create user with valid data', async () => {
|
||||
const userData = { name: 'John', email: 'john@example.com' };
|
||||
const result = await service.createUser(userData);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name).toBe('John');
|
||||
// ✅ 正确:游戏服务器完整测试场景
|
||||
describe('LocationBroadcastGateway', () => {
|
||||
describe('handleConnection', () => {
|
||||
// 正常情况
|
||||
it('should accept valid WebSocket connection with JWT token', async () => {
|
||||
const mockSocket = createMockSocket({ token: validJwtToken });
|
||||
const result = await gateway.handleConnection(mockSocket);
|
||||
expect(result).toBeTruthy();
|
||||
expect(mockSocket.join).toHaveBeenCalledWith(expectedRoomId);
|
||||
});
|
||||
|
||||
// 异常情况
|
||||
it('should reject connection with invalid JWT token', async () => {
|
||||
const mockSocket = createMockSocket({ token: 'invalid-token' });
|
||||
expect(() => gateway.handleConnection(mockSocket)).toThrow(WsException);
|
||||
});
|
||||
|
||||
// 边界情况
|
||||
it('should handle connection when room is at capacity limit', async () => {
|
||||
const mockSocket = createMockSocket({ token: validJwtToken });
|
||||
jest.spyOn(gateway, 'getRoomMemberCount').mockResolvedValue(MAX_ROOM_CAPACITY);
|
||||
|
||||
expect(() => gateway.handleConnection(mockSocket))
|
||||
.toThrow(new WsException('房间已满'));
|
||||
});
|
||||
});
|
||||
|
||||
// 异常情况
|
||||
it('should throw ConflictException when email already exists', async () => {
|
||||
const userData = { name: 'John', email: 'existing@example.com' };
|
||||
await expect(service.createUser(userData)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
describe('handlePositionUpdate', () => {
|
||||
// 实时通信测试
|
||||
it('should broadcast position to all room members', async () => {
|
||||
const positionData = { x: 100, y: 200, timestamp: Date.now() };
|
||||
await gateway.handlePositionUpdate(mockSocket, positionData);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith(roomId);
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('position-update', {
|
||||
userId: mockSocket.userId,
|
||||
position: positionData
|
||||
});
|
||||
});
|
||||
|
||||
// 边界情况
|
||||
it('should handle empty name gracefully', async () => {
|
||||
const userData = { name: '', email: 'test@example.com' };
|
||||
await expect(service.createUser(userData)).rejects.toThrow(BadRequestException);
|
||||
// 数据验证测试
|
||||
it('should validate position data format', async () => {
|
||||
const invalidPosition = { x: 'invalid', y: 200 };
|
||||
|
||||
expect(() => gateway.handlePositionUpdate(mockSocket, invalidPosition))
|
||||
.toThrow(WsException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 双模式服务测试
|
||||
describe('UsersService vs UsersMemoryService', () => {
|
||||
it('should have identical behavior for user creation', async () => {
|
||||
const userData = { name: 'Test User', email: 'test@example.com' };
|
||||
|
||||
const dbResult = await usersService.create(userData);
|
||||
const memoryResult = await usersMemoryService.create(userData);
|
||||
|
||||
expect(dbResult).toMatchObject(memoryResult);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -664,50 +843,89 @@ describe('createUser', () => {
|
||||
**要求:测试代码必须清晰、可维护、真实有效**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:高质量的测试代码
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
let mockRepository: jest.Mocked<Repository<User>>;
|
||||
// ✅ 正确:游戏服务器高质量测试代码
|
||||
describe('LocationBroadcastGateway', () => {
|
||||
let gateway: LocationBroadcastGateway;
|
||||
let mockServer: jest.Mocked<Server>;
|
||||
let mockLocationService: jest.Mocked<LocationBroadcastCoreService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockRepo = {
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
const mockServer = {
|
||||
to: jest.fn().mockReturnThis(),
|
||||
emit: jest.fn(),
|
||||
sockets: {
|
||||
adapter: {
|
||||
rooms: new Map()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockLocationService = {
|
||||
broadcastToRoom: jest.fn(),
|
||||
validatePosition: jest.fn(),
|
||||
getRoomMembers: jest.fn()
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UserService,
|
||||
{ provide: getRepositoryToken(User), useValue: mockRepo },
|
||||
LocationBroadcastGateway,
|
||||
{ provide: 'SERVER', useValue: mockServer },
|
||||
{ provide: LocationBroadcastCoreService, useValue: mockLocationService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
mockRepository = module.get(getRepositoryToken(User));
|
||||
gateway = module.get<LocationBroadcastGateway>(LocationBroadcastGateway);
|
||||
mockServer = module.get('SERVER');
|
||||
mockLocationService = module.get(LocationBroadcastCoreService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findUserById', () => {
|
||||
it('should return user when found', async () => {
|
||||
describe('handlePositionUpdate', () => {
|
||||
it('should broadcast valid position update to room members', async () => {
|
||||
// Arrange
|
||||
const userId = '123';
|
||||
const expectedUser = { id: userId, name: 'John', email: 'john@example.com' };
|
||||
mockRepository.findOne.mockResolvedValue(expectedUser);
|
||||
const mockSocket = createMockSocket({ userId: 'user123', roomId: 'room456' });
|
||||
const positionData = { x: 100, y: 200, timestamp: Date.now() };
|
||||
mockLocationService.validatePosition.mockResolvedValue(true);
|
||||
mockLocationService.getRoomMembers.mockResolvedValue(['user123', 'user456']);
|
||||
|
||||
// Act
|
||||
const result = await service.findUserById(userId);
|
||||
await gateway.handlePositionUpdate(mockSocket, positionData);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expectedUser);
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
|
||||
expect(mockLocationService.validatePosition).toHaveBeenCalledWith(positionData);
|
||||
expect(mockServer.to).toHaveBeenCalledWith('room456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('position-update', {
|
||||
userId: 'user123',
|
||||
position: positionData,
|
||||
timestamp: expect.any(Number)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 属性测试示例(管理员模块)
|
||||
describe('AdminService Properties', () => {
|
||||
it('should handle any valid user status update', () => {
|
||||
fc.assert(fc.property(
|
||||
fc.integer({ min: 1, max: 1000000 }), // userId
|
||||
fc.constantFrom(...Object.values(UserStatus)), // status
|
||||
async (userId, status) => {
|
||||
// 属性:任何有效的用户状态更新都应该成功或抛出明确的异常
|
||||
try {
|
||||
const result = await adminService.updateUserStatus(userId, status);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.status).toBe(status);
|
||||
} catch (error) {
|
||||
// 如果抛出异常,应该是已知的业务异常
|
||||
expect(error).toBeInstanceOf(NotFoundException || BadRequestException);
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 🔗 集成测试
|
||||
@@ -715,25 +933,43 @@ describe('UserService', () => {
|
||||
**要求:复杂Service需要集成测试文件(.integration.spec.ts)**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:提供集成测试
|
||||
src/core/db/users/users.service.ts
|
||||
src/core/db/users/users.service.spec.ts # 单元测试
|
||||
src/core/db/users/users.integration.spec.ts # 集成测试
|
||||
// ✅ 正确:游戏服务器集成测试
|
||||
src/core/location_broadcast_core/location_broadcast_core.service.ts
|
||||
src/core/location_broadcast_core/location_broadcast_core.service.spec.ts # 单元测试
|
||||
src/core/location_broadcast_core/location_broadcast_core.integration.spec.ts # 集成测试
|
||||
|
||||
src/business/zulip/zulip.service.ts
|
||||
src/business/zulip/zulip.service.spec.ts # 单元测试
|
||||
src/business/zulip/zulip_integration.e2e.spec.ts # E2E测试
|
||||
```
|
||||
|
||||
### ⚡ 测试执行
|
||||
|
||||
**推荐的测试命令:**
|
||||
**游戏服务器推荐的测试命令:**
|
||||
|
||||
```bash
|
||||
# 针对特定文件夹的测试(推荐)- 排除集成测试
|
||||
npx jest src/core/db/users --testPathIgnorePatterns="integration.spec.ts"
|
||||
# 单元测试(排除集成测试和E2E测试)
|
||||
npm run test:unit
|
||||
# 等价于: jest --testPathPattern=spec.ts --testPathIgnorePatterns="integration.spec.ts|e2e.spec.ts"
|
||||
|
||||
# 针对特定文件的测试
|
||||
npx jest src/core/db/users/users.service.spec.ts
|
||||
# 集成测试
|
||||
jest --testPathPattern=integration.spec.ts
|
||||
|
||||
# E2E测试(需要设置环境变量)
|
||||
npm run test:e2e
|
||||
# 等价于: cross-env RUN_E2E_TESTS=true jest --testPathPattern=e2e.spec.ts
|
||||
|
||||
# 属性测试(管理员模块)
|
||||
jest --testPathPattern=property.spec.ts
|
||||
|
||||
# 性能测试(WebSocket相关)
|
||||
jest --testPathPattern=perf.spec.ts
|
||||
|
||||
# 全部测试
|
||||
npm run test:all
|
||||
|
||||
# 带覆盖率的测试执行
|
||||
npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec.ts"
|
||||
npm run test:cov
|
||||
```
|
||||
---
|
||||
|
||||
@@ -764,6 +1000,51 @@ npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec
|
||||
更新用户状态,支持激活、禁用、待验证等状态切换。
|
||||
```
|
||||
|
||||
#### 2.1 API接口列表(如适用)
|
||||
**如果business模块开放了可访问的API,必须在此处列出:**
|
||||
|
||||
```markdown
|
||||
## 对外API接口
|
||||
|
||||
### POST /api/auth/login
|
||||
用户登录接口,支持用户名/邮箱/手机号多种方式登录。
|
||||
|
||||
### GET /api/users/:id
|
||||
根据用户ID获取用户详细信息。
|
||||
|
||||
### PUT /api/users/:id/status
|
||||
更新指定用户的状态(激活/禁用/待验证)。
|
||||
|
||||
### DELETE /api/users/:id
|
||||
删除指定用户账户及相关数据。
|
||||
|
||||
### GET /api/users/search
|
||||
根据条件搜索用户,支持邮箱、用户名、状态等筛选。
|
||||
|
||||
## WebSocket事件接口
|
||||
|
||||
### 'connection'
|
||||
客户端建立WebSocket连接,需要提供JWT认证token。
|
||||
|
||||
### 'position_update'
|
||||
接收客户端位置更新,广播给房间内其他用户。
|
||||
- 输入: `{ x: number, y: number, timestamp: number }`
|
||||
- 输出: 广播给房间成员
|
||||
|
||||
### 'join_room'
|
||||
用户加入游戏房间,建立实时通信连接。
|
||||
- 输入: `{ roomId: string }`
|
||||
- 输出: `{ success: boolean, members: string[] }`
|
||||
|
||||
### 'chat_message'
|
||||
处理聊天消息,支持Zulip集成和消息过滤。
|
||||
- 输入: `{ message: string, roomId: string }`
|
||||
- 输出: 广播给房间成员或转发到Zulip
|
||||
|
||||
### 'disconnect'
|
||||
客户端断开连接,清理相关资源和通知其他用户。
|
||||
```
|
||||
|
||||
#### 3. 使用的项目内部依赖
|
||||
```markdown
|
||||
## 使用的项目内部依赖
|
||||
@@ -786,36 +1067,85 @@ npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec
|
||||
- 数据库模式:使用TypeORM连接MySQL,适用于生产环境
|
||||
- 内存模式:使用Map存储,适用于开发测试和故障降级
|
||||
- 动态模块配置:通过UsersModule.forDatabase()和forMemory()灵活切换
|
||||
- 自动检测:根据环境变量自动选择存储模式
|
||||
|
||||
### 实时通信能力
|
||||
- WebSocket支持:基于Socket.IO的实时双向通信
|
||||
- 房间管理:支持用户加入/离开游戏房间
|
||||
- 位置广播:实时广播用户位置更新给房间成员
|
||||
- 连接管理:自动处理连接断开和重连机制
|
||||
|
||||
### 数据完整性保障
|
||||
- 唯一性约束检查:用户名、邮箱、手机号、GitHub ID
|
||||
- 数据验证:使用class-validator进行输入验证
|
||||
- 事务支持:批量操作支持回滚机制
|
||||
- 双模式一致性:确保内存模式和数据库模式行为一致
|
||||
|
||||
### 性能优化
|
||||
### 性能优化与监控
|
||||
- 查询优化:使用索引和查询缓存
|
||||
- 批量操作:支持批量创建和更新
|
||||
- 内存缓存:热点数据缓存机制
|
||||
- 性能监控:WebSocket连接数、消息处理延迟等指标
|
||||
- 属性测试:使用fast-check进行随机化测试
|
||||
|
||||
### 第三方集成
|
||||
- Zulip集成:支持与Zulip聊天系统的消息同步
|
||||
- 邮件服务:用户注册验证和通知
|
||||
- Redis缓存:支持Redis和文件存储双模式
|
||||
- JWT认证:完整的用户认证和授权体系
|
||||
```
|
||||
|
||||
#### 5. 潜在风险
|
||||
```markdown
|
||||
## 潜在风险
|
||||
|
||||
### 内存模式数据丢失
|
||||
### 内存模式数据丢失风险
|
||||
- 内存存储在应用重启后数据会丢失
|
||||
- 不适用于生产环境的持久化需求
|
||||
- 建议仅在开发测试环境使用
|
||||
- 缓解措施:提供数据导出/导入功能
|
||||
|
||||
### WebSocket连接管理风险
|
||||
- 大量并发连接可能导致内存泄漏
|
||||
- 网络不稳定时连接频繁断开重连
|
||||
- 房间成员过多时广播性能下降
|
||||
- 缓解措施:连接数限制、心跳检测、分片广播
|
||||
|
||||
### 实时通信性能风险
|
||||
- 高频位置更新可能导致服务器压力
|
||||
- 消息广播延迟影响游戏体验
|
||||
- WebSocket消息丢失或重复
|
||||
- 缓解措施:消息限流、优先级队列、消息确认机制
|
||||
|
||||
### 双模式一致性风险
|
||||
- 内存模式和数据库模式行为可能不一致
|
||||
- 模式切换时数据同步问题
|
||||
- 测试覆盖不完整导致隐藏差异
|
||||
- 缓解措施:统一接口抽象、完整的对比测试
|
||||
|
||||
### 第三方集成风险
|
||||
- Zulip服务不可用时影响聊天功能
|
||||
- 邮件服务故障影响用户注册
|
||||
- Redis连接失败时缓存降级
|
||||
- 缓解措施:服务降级、重试机制、监控告警
|
||||
|
||||
### 并发操作风险
|
||||
- 内存模式的ID生成锁机制相对简单
|
||||
- 高并发场景可能存在性能瓶颈
|
||||
- 建议在生产环境使用数据库模式
|
||||
- 位置更新冲突和数据竞争
|
||||
- 建议在生产环境使用数据库模式和分布式锁
|
||||
|
||||
### 数据一致性风险
|
||||
- 跨模块操作时可能存在数据不一致
|
||||
- WebSocket连接状态与用户状态不同步
|
||||
- 需要注意事务边界的设计
|
||||
- 建议使用分布式事务或补偿机制
|
||||
|
||||
### 安全风险
|
||||
- WebSocket连接缺少足够的认证验证
|
||||
- 用户位置信息泄露风险
|
||||
- 管理员权限过度集中
|
||||
- 缓解措施:JWT认证、数据脱敏、权限细分
|
||||
```
|
||||
|
||||
### 📝 文档质量要求
|
||||
@@ -861,6 +1191,7 @@ npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec
|
||||
- [ ] 常量使用正确的命名规范
|
||||
- [ ] 方法长度控制在合理范围内(建议不超过50行)
|
||||
- [ ] 避免代码重复
|
||||
- [ ] 处理所有TODO项(实现功能或删除代码)
|
||||
|
||||
#### 架构分层检查清单
|
||||
- [ ] Core层专注技术实现,不包含业务逻辑
|
||||
@@ -880,6 +1211,7 @@ npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec
|
||||
- [ ] 每个功能模块都有README.md文档
|
||||
- [ ] 文档包含模块概述、对外接口、内部依赖、核心特性、潜在风险
|
||||
- [ ] 所有公共接口都有准确的功能描述
|
||||
- [ ] 如果是business模块且开放了API,必须列出所有API接口及功能说明
|
||||
- [ ] 文档内容与代码实现一致
|
||||
- [ ] 语言表达简洁明了
|
||||
|
||||
@@ -887,17 +1219,20 @@ npx jest src/core/db/users --coverage --testPathIgnorePatterns="integration.spec
|
||||
|
||||
#### 测试相关命令
|
||||
```bash
|
||||
# 运行特定文件夹的单元测试
|
||||
npx jest src/core/db/users --testPathIgnorePatterns="integration.spec.ts"
|
||||
# 游戏服务器测试命令
|
||||
npm run test:unit # 单元测试
|
||||
npm run test:cov # 测试覆盖率
|
||||
npm run test:e2e # E2E测试
|
||||
npm run test:all # 全部测试
|
||||
|
||||
# 运行特定文件的测试
|
||||
npx jest src/core/db/users/users.service.spec.ts
|
||||
# Jest特定测试类型
|
||||
jest --testPathPattern=property.spec.ts # 属性测试
|
||||
jest --testPathPattern=integration.spec.ts # 集成测试
|
||||
jest --testPathPattern=perf.spec.ts # 性能测试
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npx jest src/core/db/users --coverage
|
||||
|
||||
# 静默模式运行测试
|
||||
npx jest src/core/db/users --silent
|
||||
# WebSocket测试(需要启动服务)
|
||||
npm run dev & # 后台启动开发服务器
|
||||
npm run test:e2e # 运行E2E测试
|
||||
```
|
||||
|
||||
#### 代码检查命令
|
||||
@@ -915,12 +1250,18 @@ npx prettier --write src/**/*.ts
|
||||
### 🚨 常见错误和解决方案
|
||||
|
||||
#### 命名规范常见错误
|
||||
1. **短横线命名错误**
|
||||
- 错误:`base-users.service.ts`
|
||||
- 正确:`base_users.service.ts`
|
||||
- 解决:统一使用下划线分隔
|
||||
1. **短横线命名错误(不符合项目规范)**
|
||||
- 错误:`admin-operation-log.service.ts`
|
||||
- 正确:`admin_operation_log.service.ts`
|
||||
- 解决:统一使用下划线分隔,保持项目一致性
|
||||
|
||||
2. **常量命名错误**
|
||||
2. **游戏服务器特殊文件命名错误**
|
||||
- 错误:`locationBroadcast.gateway.ts`
|
||||
- 正确:`location_broadcast.gateway.ts`
|
||||
- 错误:`websocketAuth.guard.ts`
|
||||
- 正确:`websocket_auth.guard.ts`
|
||||
|
||||
3. **常量命名错误**
|
||||
- 错误:`const saltRounds = 10;`
|
||||
- 正确:`const SALT_ROUNDS = 10;`
|
||||
- 解决:常量使用全大写+下划线
|
||||
@@ -937,14 +1278,24 @@ npx prettier --write src/**/*.ts
|
||||
- 解决:将业务逻辑移到Business层
|
||||
|
||||
#### 测试覆盖常见错误
|
||||
1. **测试文件缺失**
|
||||
- 错误:Service没有对应的.spec.ts文件
|
||||
- 解决:为每个Service创建测试文件
|
||||
1. **WebSocket测试文件缺失**
|
||||
- 错误:Gateway没有对应的.spec.ts文件
|
||||
- 解决:为每个Gateway创建完整的连接、消息处理测试
|
||||
|
||||
2. **测试场景不完整**
|
||||
- 错误:只测试正常情况
|
||||
- 正确:测试正常、异常、边界情况
|
||||
- 解决:补充异常和边界情况的测试用例
|
||||
2. **双模式测试不完整**
|
||||
- 错误:只测试数据库模式,忽略内存模式
|
||||
- 正确:确保两种模式行为一致性测试
|
||||
- 解决:创建对比测试用例
|
||||
|
||||
3. **属性测试缺失**
|
||||
- 错误:管理员模块缺少随机化测试
|
||||
- 正确:使用fast-check进行属性测试
|
||||
- 解决:补充基于属性的测试用例
|
||||
|
||||
4. **实时通信测试场景不完整**
|
||||
- 错误:只测试正常连接,忽略异常断开
|
||||
- 正确:测试连接、断开、重连、消息处理全流程
|
||||
- 解决:补充WebSocket生命周期测试
|
||||
|
||||
---
|
||||
|
||||
@@ -1009,4 +1360,73 @@ npx prettier --write src/**/*.ts
|
||||
3. 向团队架构师或技术负责人咨询
|
||||
4. 提交改进建议,持续优化规范
|
||||
|
||||
**记住:代码规范不是束缚,而是提高代码质量和团队协作效率的有力工具!** 🚀
|
||||
**记住:代码规范不是束缚,而是提高代码质量和团队协作效率的有力工具!** 🚀
|
||||
|
||||
---
|
||||
|
||||
## 🎮 游戏服务器特殊优化建议
|
||||
|
||||
### 🚀 实时通信优化
|
||||
|
||||
1. **WebSocket连接管理**
|
||||
- 实现连接池和心跳检测
|
||||
- 设置合理的连接超时和重连机制
|
||||
- 监控连接数量和消息处理延迟
|
||||
|
||||
2. **消息广播优化**
|
||||
- 使用房间分片减少广播范围
|
||||
- 实现消息优先级队列
|
||||
- 添加消息确认和重试机制
|
||||
|
||||
3. **位置更新优化**
|
||||
- 实现位置更新频率限制
|
||||
- 使用差分更新减少数据传输
|
||||
- 添加位置验证防止作弊
|
||||
|
||||
### 🔄 双模式架构优化
|
||||
|
||||
1. **模式切换优化**
|
||||
- 提供平滑的模式切换机制
|
||||
- 实现数据迁移和同步工具
|
||||
- 添加模式状态监控
|
||||
|
||||
2. **一致性保障**
|
||||
- 统一接口抽象层
|
||||
- 完整的行为对比测试
|
||||
- 自动化一致性检查
|
||||
|
||||
3. **性能对比**
|
||||
- 定期进行性能基准测试
|
||||
- 监控两种模式的资源使用
|
||||
- 优化内存模式的并发处理
|
||||
|
||||
### 🧪 测试策略优化
|
||||
|
||||
1. **属性测试应用**
|
||||
- 管理员模块使用fast-check
|
||||
- 随机化用户状态变更测试
|
||||
- 边界条件自动发现
|
||||
|
||||
2. **集成测试重点**
|
||||
- WebSocket连接生命周期
|
||||
- 双模式服务一致性
|
||||
- 第三方服务集成
|
||||
|
||||
3. **E2E测试场景**
|
||||
- 完整的用户游戏流程
|
||||
- 多用户实时交互
|
||||
- 异常恢复和降级
|
||||
|
||||
### 📊 监控和告警
|
||||
|
||||
1. **关键指标监控**
|
||||
- WebSocket连接数和延迟
|
||||
- 位置更新频率和处理时间
|
||||
- 内存使用和GC频率
|
||||
- 第三方服务可用性
|
||||
|
||||
2. **告警策略**
|
||||
- 连接数超过阈值
|
||||
- 消息处理延迟过高
|
||||
- 服务降级和故障转移
|
||||
- 数据一致性检查失败
|
||||
Reference in New Issue
Block a user