From 4bda65d593f4b8fd5c1086a6ec338d8884ac035c Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:04:57 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=20ZulipAccou?= =?UTF-8?q?nts=20=E5=AE=9E=E4=BD=93=E7=B4=A2=E5=BC=95=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将索引字段从数据库列名改为实体属性名 - 修复 zulip_user_id 和 zulip_email 索引配置 - 解决服务启动时的 TypeORM 索引错误 --- src/core/db/zulip_accounts/zulip_accounts.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/db/zulip_accounts/zulip_accounts.entity.ts b/src/core/db/zulip_accounts/zulip_accounts.entity.ts index 10034bf..6c5c283 100644 --- a/src/core/db/zulip_accounts/zulip_accounts.entity.ts +++ b/src/core/db/zulip_accounts/zulip_accounts.entity.ts @@ -19,8 +19,8 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateCol import { Users } from '../users/users.entity'; @Entity('zulip_accounts') -@Index(['zulip_user_id'], { unique: true }) -@Index(['zulip_email'], { unique: true }) +@Index(['zulipUserId'], { unique: true }) +@Index(['zulipEmail'], { unique: true }) export class ZulipAccounts { /** * 主键ID -- 2.25.1 From 7fd67400903dd10f84187441f3dc6acc8b84d367 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:05:21 +0800 Subject: [PATCH 2/8] =?UTF-8?q?dto=EF=BC=9A=E6=B7=BB=E5=8A=A0=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=B3=BB=E7=BB=9F=E7=9B=B8=E5=85=B3=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BC=A0=E8=BE=93=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SendChatMessageDto 用于发送聊天消息请求 - 新增 ChatMessageResponseDto 用于消息发送响应 - 新增 GetChatHistoryDto 用于获取聊天历史请求 - 新增 ChatHistoryResponseDto 用于聊天历史响应 - 新增 SystemStatusResponseDto 用于系统状态查询 - 包含完整的 API 文档注解和数据验证规则 --- src/business/zulip/dto/chat.dto.ts | 313 +++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 src/business/zulip/dto/chat.dto.ts diff --git a/src/business/zulip/dto/chat.dto.ts b/src/business/zulip/dto/chat.dto.ts new file mode 100644 index 0000000..adc5eaf --- /dev/null +++ b/src/business/zulip/dto/chat.dto.ts @@ -0,0 +1,313 @@ +/** + * 聊天相关的 DTO 定义 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-07 + */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsOptional, IsNumber, IsBoolean, IsEnum, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +/** + * 发送聊天消息请求 DTO + */ +export class SendChatMessageDto { + @ApiProperty({ + description: '消息内容', + example: '大家好!我刚进入游戏', + maxLength: 1000 + }) + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ + description: '消息范围', + example: 'local', + enum: ['local', 'global'], + default: 'local' + }) + @IsString() + @IsNotEmpty() + scope: string; + + @ApiPropertyOptional({ + description: '地图ID(可选,用于地图相关消息)', + example: 'whale_port' + }) + @IsOptional() + @IsString() + mapId?: string; +} + +/** + * 聊天消息响应 DTO + */ +export class ChatMessageResponseDto { + @ApiProperty({ + description: '是否成功', + example: true + }) + success: boolean; + + @ApiProperty({ + description: '消息ID', + example: 12345 + }) + messageId: number; + + @ApiProperty({ + description: '响应消息', + example: '消息发送成功' + }) + message: string; + + @ApiPropertyOptional({ + description: '错误信息(失败时)', + example: '消息内容不能为空' + }) + error?: string; +} + +/** + * 获取聊天历史请求 DTO + */ +export class GetChatHistoryDto { + @ApiPropertyOptional({ + description: '地图ID(可选)', + example: 'whale_port' + }) + @IsOptional() + @IsString() + mapId?: string; + + @ApiPropertyOptional({ + description: '消息数量限制', + example: 50, + default: 50, + minimum: 1, + maximum: 100 + }) + @IsOptional() + @IsNumber() + @Type(() => Number) + limit?: number = 50; + + @ApiPropertyOptional({ + description: '偏移量(分页用)', + example: 0, + default: 0, + minimum: 0 + }) + @IsOptional() + @IsNumber() + @Type(() => Number) + offset?: number = 0; +} + +/** + * 聊天消息信息 DTO + */ +export class ChatMessageInfoDto { + @ApiProperty({ + description: '消息ID', + example: 12345 + }) + id: number; + + @ApiProperty({ + description: '发送者用户名', + example: 'Player_123' + }) + sender: string; + + @ApiProperty({ + description: '消息内容', + example: '大家好!' + }) + content: string; + + @ApiProperty({ + description: '消息范围', + example: 'local' + }) + scope: string; + + @ApiProperty({ + description: '地图ID', + example: 'whale_port' + }) + mapId: string; + + @ApiProperty({ + description: '发送时间', + example: '2025-01-07T14:30:00.000Z' + }) + timestamp: string; + + @ApiProperty({ + description: 'Zulip Stream 名称', + example: 'Whale Port' + }) + streamName: string; + + @ApiProperty({ + description: 'Zulip Topic 名称', + example: 'Game Chat' + }) + topicName: string; +} + +/** + * 聊天历史响应 DTO + */ +export class ChatHistoryResponseDto { + @ApiProperty({ + description: '是否成功', + example: true + }) + success: boolean; + + @ApiProperty({ + description: '消息列表', + type: [ChatMessageInfoDto] + }) + @ValidateNested({ each: true }) + @Type(() => ChatMessageInfoDto) + messages: ChatMessageInfoDto[]; + + @ApiProperty({ + description: '总消息数', + example: 150 + }) + total: number; + + @ApiProperty({ + description: '当前页消息数', + example: 50 + }) + count: number; + + @ApiPropertyOptional({ + description: '错误信息(失败时)', + example: '获取消息历史失败' + }) + error?: string; +} + +/** + * WebSocket 连接状态 DTO + */ +export class WebSocketStatusDto { + @ApiProperty({ + description: '总连接数', + example: 25 + }) + totalConnections: number; + + @ApiProperty({ + description: '已认证连接数', + example: 20 + }) + authenticatedConnections: number; + + @ApiProperty({ + description: '活跃会话数', + example: 18 + }) + activeSessions: number; + + @ApiProperty({ + description: '各地图在线人数', + example: { + 'whale_port': 8, + 'pumpkin_valley': 5, + 'novice_village': 7 + } + }) + mapPlayerCounts: Record; +} + +/** + * Zulip 集成状态 DTO + */ +export class ZulipIntegrationStatusDto { + @ApiProperty({ + description: 'Zulip 服务器连接状态', + example: true + }) + serverConnected: boolean; + + @ApiProperty({ + description: 'Zulip 服务器版本', + example: '11.4' + }) + serverVersion: string; + + @ApiProperty({ + description: '机器人账号状态', + example: true + }) + botAccountActive: boolean; + + @ApiProperty({ + description: '可用 Stream 数量', + example: 12 + }) + availableStreams: number; + + @ApiProperty({ + description: '游戏相关 Stream 列表', + example: ['Whale Port', 'Pumpkin Valley', 'Novice Village'] + }) + gameStreams: string[]; + + @ApiProperty({ + description: '最近24小时消息数', + example: 156 + }) + recentMessageCount: number; +} + +/** + * 系统状态响应 DTO + */ +export class SystemStatusResponseDto { + @ApiProperty({ + description: 'WebSocket 状态', + type: WebSocketStatusDto + }) + @ValidateNested() + @Type(() => WebSocketStatusDto) + websocket: WebSocketStatusDto; + + @ApiProperty({ + description: 'Zulip 集成状态', + type: ZulipIntegrationStatusDto + }) + @ValidateNested() + @Type(() => ZulipIntegrationStatusDto) + zulip: ZulipIntegrationStatusDto; + + @ApiProperty({ + description: '系统运行时间(秒)', + example: 86400 + }) + uptime: number; + + @ApiProperty({ + description: '内存使用情况', + example: { + used: '45.2 MB', + total: '64.0 MB', + percentage: 70.6 + } + }) + memory: { + used: string; + total: string; + percentage: number; + }; +} \ No newline at end of file -- 2.25.1 From d1fc396db7fcc41ee2fcf1a250d8fe37c58dfc75 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:05:40 +0800 Subject: [PATCH 3/8] =?UTF-8?q?api=EF=BC=9A=E6=B7=BB=E5=8A=A0=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=B3=BB=E7=BB=9F=20REST=20API=20=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 /chat/send 消息发送接口(引导使用 WebSocket) - 实现 /chat/history 聊天历史查询接口 - 实现 /chat/status 系统状态监控接口 - 实现 /chat/websocket/info WebSocket 连接信息接口 - 包含完整的 Swagger API 文档注解 - 集成 JWT 身份验证和错误处理 --- .../zulip/controllers/chat.controller.ts | 367 ++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 src/business/zulip/controllers/chat.controller.ts diff --git a/src/business/zulip/controllers/chat.controller.ts b/src/business/zulip/controllers/chat.controller.ts new file mode 100644 index 0000000..7eb7bd1 --- /dev/null +++ b/src/business/zulip/controllers/chat.controller.ts @@ -0,0 +1,367 @@ +/** + * 聊天相关的 REST API 控制器 + * + * 功能描述: + * - 提供聊天消息的 REST API 接口 + * - 获取聊天历史记录 + * - 查看系统状态和统计信息 + * - 管理 WebSocket 连接状态 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-07 + */ + +import { + Controller, + Post, + Get, + Body, + Query, + UseGuards, + HttpStatus, + HttpException, + Logger, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { ZulipService } from '../zulip.service'; +import { ZulipWebSocketGateway } from '../zulip_websocket.gateway'; +import { + SendChatMessageDto, + ChatMessageResponseDto, + GetChatHistoryDto, + ChatHistoryResponseDto, + SystemStatusResponseDto, +} from '../dto/chat.dto'; + +@ApiTags('chat') +@Controller('chat') +export class ChatController { + private readonly logger = new Logger(ChatController.name); + + constructor( + private readonly zulipService: ZulipService, + private readonly websocketGateway: ZulipWebSocketGateway, + ) {} + + /** + * 发送聊天消息(REST API 方式) + * + * 注意:这是 WebSocket 消息发送的 REST API 替代方案 + * 推荐使用 WebSocket 接口以获得更好的实时性 + */ + @Post('send') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ + summary: '发送聊天消息', + description: '通过 REST API 发送聊天消息到 Zulip。注意:推荐使用 WebSocket 接口以获得更好的实时性。' + }) + @ApiResponse({ + status: 200, + description: '消息发送成功', + type: ChatMessageResponseDto, + }) + @ApiResponse({ + status: 400, + description: '请求参数错误', + }) + @ApiResponse({ + status: 401, + description: '未授权访问', + }) + @ApiResponse({ + status: 500, + description: '服务器内部错误', + }) + async sendMessage( + @Body() sendMessageDto: SendChatMessageDto, + ): Promise { + this.logger.log('收到REST API聊天消息发送请求', { + operation: 'sendMessage', + content: sendMessageDto.content.substring(0, 50), + scope: sendMessageDto.scope, + timestamp: new Date().toISOString(), + }); + + try { + // 注意:这里需要一个有效的 socketId,但 REST API 没有 WebSocket 连接 + // 这是一个限制,实际使用中应该通过 WebSocket 发送消息 + throw new HttpException( + '聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:ws://localhost:3000/game', + HttpStatus.BAD_REQUEST, + ); + + } catch (error) { + const err = error as Error; + this.logger.error('REST API消息发送失败', { + operation: 'sendMessage', + error: err.message, + timestamp: new Date().toISOString(), + }); + + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + '消息发送失败,请稍后重试', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * 获取聊天历史记录 + */ + @Get('history') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ + summary: '获取聊天历史记录', + description: '获取指定地图或全局的聊天历史记录' + }) + @ApiQuery({ + name: 'mapId', + required: false, + description: '地图ID,不指定则获取全局消息', + example: 'whale_port' + }) + @ApiQuery({ + name: 'limit', + required: false, + description: '消息数量限制', + example: 50 + }) + @ApiQuery({ + name: 'offset', + required: false, + description: '偏移量(分页用)', + example: 0 + }) + @ApiResponse({ + status: 200, + description: '获取聊天历史成功', + type: ChatHistoryResponseDto, + }) + @ApiResponse({ + status: 401, + description: '未授权访问', + }) + @ApiResponse({ + status: 500, + description: '服务器内部错误', + }) + async getChatHistory( + @Query() query: GetChatHistoryDto, + ): Promise { + this.logger.log('获取聊天历史记录', { + operation: 'getChatHistory', + mapId: query.mapId, + limit: query.limit, + offset: query.offset, + timestamp: new Date().toISOString(), + }); + + try { + // 注意:这里需要实现从 Zulip 获取消息历史的逻辑 + // 目前返回模拟数据 + const mockMessages = [ + { + id: 1, + sender: 'Player_123', + content: '大家好!我刚进入游戏', + scope: 'local', + mapId: query.mapId || 'whale_port', + timestamp: new Date(Date.now() - 3600000).toISOString(), + streamName: 'Whale Port', + topicName: 'Game Chat', + }, + { + id: 2, + sender: 'Player_456', + content: '欢迎新玩家!', + scope: 'local', + mapId: query.mapId || 'whale_port', + timestamp: new Date(Date.now() - 1800000).toISOString(), + streamName: 'Whale Port', + topicName: 'Game Chat', + }, + ]; + + return { + success: true, + messages: mockMessages.slice(query.offset || 0, (query.offset || 0) + (query.limit || 50)), + total: mockMessages.length, + count: Math.min(mockMessages.length - (query.offset || 0), query.limit || 50), + }; + + } catch (error) { + const err = error as Error; + this.logger.error('获取聊天历史失败', { + operation: 'getChatHistory', + error: err.message, + timestamp: new Date().toISOString(), + }); + + throw new HttpException( + '获取聊天历史失败,请稍后重试', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * 获取系统状态 + */ + @Get('status') + @ApiOperation({ + summary: '获取聊天系统状态', + description: '获取 WebSocket 连接状态、Zulip 集成状态等系统信息' + }) + @ApiResponse({ + status: 200, + description: '获取系统状态成功', + type: SystemStatusResponseDto, + }) + @ApiResponse({ + status: 500, + description: '服务器内部错误', + }) + async getSystemStatus(): Promise { + this.logger.log('获取系统状态', { + operation: 'getSystemStatus', + timestamp: new Date().toISOString(), + }); + + try { + // 获取 WebSocket 连接状态 + const totalConnections = await this.websocketGateway.getConnectionCount(); + const authenticatedConnections = await this.websocketGateway.getAuthenticatedConnectionCount(); + + // 获取内存使用情况 + const memoryUsage = process.memoryUsage(); + const memoryUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(1); + const memoryTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(1); + const memoryPercentage = ((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100); + + return { + websocket: { + totalConnections, + authenticatedConnections, + activeSessions: authenticatedConnections, // 简化处理 + mapPlayerCounts: { + 'whale_port': Math.floor(authenticatedConnections * 0.4), + 'pumpkin_valley': Math.floor(authenticatedConnections * 0.3), + 'novice_village': Math.floor(authenticatedConnections * 0.3), + }, + }, + zulip: { + serverConnected: true, // 需要实际检查 + serverVersion: '11.4', + botAccountActive: true, + availableStreams: 12, + gameStreams: ['Whale Port', 'Pumpkin Valley', 'Novice Village'], + recentMessageCount: 156, // 需要从实际数据获取 + }, + uptime: Math.floor(process.uptime()), + memory: { + used: `${memoryUsedMB} MB`, + total: `${memoryTotalMB} MB`, + percentage: Math.round(memoryPercentage * 100) / 100, + }, + }; + + } catch (error) { + const err = error as Error; + this.logger.error('获取系统状态失败', { + operation: 'getSystemStatus', + error: err.message, + timestamp: new Date().toISOString(), + }); + + throw new HttpException( + '获取系统状态失败,请稍后重试', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * 获取 WebSocket 连接信息 + */ + @Get('websocket/info') + @ApiOperation({ + summary: '获取 WebSocket 连接信息', + description: '获取 WebSocket 连接的详细信息,包括连接地址、协议等' + }) + @ApiResponse({ + status: 200, + description: '获取连接信息成功', + schema: { + type: 'object', + properties: { + websocketUrl: { + type: 'string', + example: 'ws://localhost:3000/game', + description: 'WebSocket 连接地址' + }, + namespace: { + type: 'string', + example: '/game', + description: 'WebSocket 命名空间' + }, + supportedEvents: { + type: 'array', + items: { type: 'string' }, + example: ['login', 'chat', 'position_update'], + description: '支持的事件类型' + }, + authRequired: { + type: 'boolean', + example: true, + description: '是否需要认证' + }, + documentation: { + type: 'string', + example: 'https://docs.example.com/websocket', + description: '文档链接' + } + } + } + }) + async getWebSocketInfo() { + return { + websocketUrl: 'ws://localhost:3000/game', + namespace: '/game', + supportedEvents: [ + 'login', // 用户登录 + 'chat', // 发送聊天消息 + 'position_update', // 位置更新 + ], + supportedResponses: [ + 'login_success', // 登录成功 + 'login_error', // 登录失败 + 'chat_sent', // 消息发送成功 + 'chat_error', // 消息发送失败 + 'chat_render', // 接收到聊天消息 + ], + authRequired: true, + tokenType: 'JWT', + tokenFormat: { + issuer: 'whale-town', + audience: 'whale-town-users', + type: 'access', + requiredFields: ['sub', 'username', 'email', 'role'] + }, + documentation: '/api-docs', + }; + } +} \ No newline at end of file -- 2.25.1 From a30ef52c5a2b7b42ead7f7fb435746a348ce1cca Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:05:57 +0800 Subject: [PATCH 4/8] =?UTF-8?q?docs=EF=BC=9A=E6=B7=BB=E5=8A=A0=20WebSocket?= =?UTF-8?q?=20API=20=E6=96=87=E6=A1=A3=E6=8E=A7=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 /websocket/docs 接口提供完整的 WebSocket API 文档 - 实现 /websocket/message-examples 接口提供消息格式示例 - 包含连接配置、认证要求、事件格式说明 - 提供 JavaScript 和 Godot 客户端连接示例 - 包含故障排除指南和测试工具推荐 --- .../controllers/websocket-docs.controller.ts | 421 ++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 src/business/zulip/controllers/websocket-docs.controller.ts diff --git a/src/business/zulip/controllers/websocket-docs.controller.ts b/src/business/zulip/controllers/websocket-docs.controller.ts new file mode 100644 index 0000000..9694ef7 --- /dev/null +++ b/src/business/zulip/controllers/websocket-docs.controller.ts @@ -0,0 +1,421 @@ +/** + * WebSocket API 文档控制器 + * + * 功能描述: + * - 提供 WebSocket API 的详细文档 + * - 展示消息格式和事件类型 + * - 提供连接示例和测试工具 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-07 + */ + +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('chat') +@Controller('websocket') +export class WebSocketDocsController { + + /** + * 获取 WebSocket API 文档 + */ + @Get('docs') + @ApiOperation({ + summary: 'WebSocket API 文档', + description: '获取 WebSocket 连接和消息格式的详细文档' + }) + @ApiResponse({ + status: 200, + description: 'WebSocket API 文档', + schema: { + type: 'object', + properties: { + connection: { + type: 'object', + properties: { + url: { + type: 'string', + example: 'ws://localhost:3000/game', + description: 'WebSocket 连接地址' + }, + namespace: { + type: 'string', + example: '/game', + description: 'Socket.IO 命名空间' + }, + transports: { + type: 'array', + items: { type: 'string' }, + example: ['websocket', 'polling'], + description: '支持的传输协议' + } + } + }, + authentication: { + type: 'object', + properties: { + required: { + type: 'boolean', + example: true, + description: '是否需要认证' + }, + method: { + type: 'string', + example: 'JWT Token', + description: '认证方式' + }, + tokenFormat: { + type: 'object', + description: 'JWT Token 格式要求' + } + } + }, + events: { + type: 'object', + description: '支持的事件和消息格式' + } + } + } + }) + getWebSocketDocs() { + return { + connection: { + url: 'ws://localhost:3000/game', + namespace: '/game', + transports: ['websocket', 'polling'], + options: { + timeout: 20000, + forceNew: true, + reconnection: true, + reconnectionAttempts: 3, + reconnectionDelay: 1000 + } + }, + authentication: { + required: true, + method: 'JWT Token', + tokenFormat: { + issuer: 'whale-town', + audience: 'whale-town-users', + type: 'access', + requiredFields: ['sub', 'username', 'email', 'role'], + example: { + sub: 'user_123', + username: 'player_name', + email: 'user@example.com', + role: 'user', + type: 'access', + aud: 'whale-town-users', + iss: 'whale-town', + iat: 1767768599, + exp: 1768373399 + } + } + }, + events: { + clientToServer: { + login: { + description: '用户登录', + format: { + type: 'login', + token: 'JWT_TOKEN_HERE' + }, + example: { + type: 'login', + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' + }, + responses: ['login_success', 'login_error'] + }, + chat: { + description: '发送聊天消息', + format: { + t: 'chat', + content: 'string', + scope: 'local | global' + }, + example: { + t: 'chat', + content: '大家好!我刚进入游戏', + scope: 'local' + }, + responses: ['chat_sent', 'chat_error'] + }, + position_update: { + description: '更新玩家位置', + format: { + t: 'position', + x: 'number', + y: 'number', + mapId: 'string' + }, + example: { + t: 'position', + x: 150, + y: 400, + mapId: 'whale_port' + }, + responses: [] + } + }, + serverToClient: { + login_success: { + description: '登录成功响应', + format: { + t: 'login_success', + sessionId: 'string', + userId: 'string', + username: 'string', + currentMap: 'string' + }, + example: { + t: 'login_success', + sessionId: '89aff162-52d9-484e-9a35-036ba63a2280', + userId: 'user_123', + username: 'Player_123', + currentMap: 'whale_port' + } + }, + login_error: { + description: '登录失败响应', + format: { + t: 'login_error', + message: 'string' + }, + example: { + t: 'login_error', + message: 'Token验证失败' + } + }, + chat_sent: { + description: '消息发送成功确认', + format: { + t: 'chat_sent', + messageId: 'number', + message: 'string' + }, + example: { + t: 'chat_sent', + messageId: 137, + message: '消息发送成功' + } + }, + chat_error: { + description: '消息发送失败', + format: { + t: 'chat_error', + message: 'string' + }, + example: { + t: 'chat_error', + message: '消息内容不能为空' + } + }, + chat_render: { + description: '接收到聊天消息', + format: { + t: 'chat_render', + from: 'string', + txt: 'string', + bubble: 'boolean' + }, + example: { + t: 'chat_render', + from: 'Player_456', + txt: '欢迎新玩家!', + bubble: true + } + } + } + }, + maps: { + whale_port: { + name: 'Whale Port', + displayName: '鲸鱼港', + zulipStream: 'Whale Port', + description: '游戏的主要港口区域' + }, + pumpkin_valley: { + name: 'Pumpkin Valley', + displayName: '南瓜谷', + zulipStream: 'Pumpkin Valley', + description: '充满南瓜的神秘山谷' + }, + novice_village: { + name: 'Novice Village', + displayName: '新手村', + zulipStream: 'Novice Village', + description: '新玩家的起始区域' + } + }, + examples: { + javascript: { + connection: ` +// 使用 Socket.IO 客户端连接 +const io = require('socket.io-client'); + +const socket = io('ws://localhost:3000/game', { + transports: ['websocket', 'polling'], + timeout: 20000, + forceNew: true, + reconnection: true, + reconnectionAttempts: 3, + reconnectionDelay: 1000 +}); + +// 连接成功 +socket.on('connect', () => { + console.log('连接成功:', socket.id); + + // 发送登录消息 + socket.emit('login', { + type: 'login', + token: 'YOUR_JWT_TOKEN_HERE' + }); +}); + +// 登录成功 +socket.on('login_success', (data) => { + console.log('登录成功:', data); + + // 发送聊天消息 + socket.emit('chat', { + t: 'chat', + content: '大家好!', + scope: 'local' + }); +}); + +// 接收聊天消息 +socket.on('chat_render', (data) => { + console.log('收到消息:', data.from, '说:', data.txt); +}); + `, + godot: ` +# Godot WebSocket 客户端示例 +extends Node + +var socket = WebSocketClient.new() +var url = "ws://localhost:3000/game" + +func _ready(): + socket.connect("connection_closed", self, "_closed") + socket.connect("connection_error", self, "_error") + socket.connect("connection_established", self, "_connected") + socket.connect("data_received", self, "_on_data") + + var err = socket.connect_to_url(url) + if err != OK: + print("连接失败") + +func _connected(protocol): + print("WebSocket 连接成功") + # 发送登录消息 + var login_msg = { + "type": "login", + "token": "YOUR_JWT_TOKEN_HERE" + } + socket.get_peer(1).put_packet(JSON.print(login_msg).to_utf8()) + +func _on_data(): + var packet = socket.get_peer(1).get_packet() + var message = JSON.parse(packet.get_string_from_utf8()) + print("收到消息: ", message.result) + ` + } + }, + troubleshooting: { + commonIssues: [ + { + issue: 'Token验证失败', + solution: '确保JWT Token包含正确的issuer、audience和type字段' + }, + { + issue: '连接超时', + solution: '检查服务器是否运行,防火墙设置是否正确' + }, + { + issue: '消息发送失败', + solution: '确保已经成功登录,消息内容不为空' + } + ], + testTools: [ + { + name: 'WebSocket King', + url: 'https://websocketking.com/', + description: '在线WebSocket测试工具' + }, + { + name: 'Postman', + description: 'Postman也支持WebSocket连接测试' + } + ] + } + }; + } + + /** + * 获取消息格式示例 + */ + @Get('message-examples') + @ApiOperation({ + summary: '消息格式示例', + description: '获取各种 WebSocket 消息的格式示例' + }) + @ApiResponse({ + status: 200, + description: '消息格式示例', + }) + getMessageExamples() { + return { + login: { + request: { + type: 'login', + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0X3VzZXJfMTIzIiwidXNlcm5hbWUiOiJ0ZXN0X3VzZXIiLCJlbWFpbCI6InRlc3RfdXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwidHlwZSI6ImFjY2VzcyIsImF1ZCI6IndoYWxlLXRvd24tdXNlcnMiLCJpc3MiOiJ3aGFsZS10b3duIiwiaWF0IjoxNzY3NzY4NTk5LCJleHAiOjE3NjgzNzMzOTl9.Mq3YccSV_pMKxIAbeNRAUws1j7doqFqvlSv4Z9DhGjI' + }, + successResponse: { + t: 'login_success', + sessionId: '89aff162-52d9-484e-9a35-036ba63a2280', + userId: 'test_user_123', + username: 'test_user', + currentMap: 'whale_port' + }, + errorResponse: { + t: 'login_error', + message: 'Token验证失败' + } + }, + chat: { + request: { + t: 'chat', + content: '大家好!我刚进入游戏', + scope: 'local' + }, + successResponse: { + t: 'chat_sent', + messageId: 137, + message: '消息发送成功' + }, + errorResponse: { + t: 'chat_error', + message: '消息内容不能为空' + }, + incomingMessage: { + t: 'chat_render', + from: 'Player_456', + txt: '欢迎新玩家!', + bubble: true + } + }, + position: { + request: { + t: 'position', + x: 150, + y: 400, + mapId: 'pumpkin_valley' + } + } + }; + } +} \ No newline at end of file -- 2.25.1 From b01ea38a1751a2dcdb818d252dbbb11653f82b1c Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:06:40 +0800 Subject: [PATCH 5/8] =?UTF-8?q?config=EF=BC=9A=E6=9B=B4=E6=96=B0=20Zulip?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注册 ChatController 和 WebSocketDocsController - 添加聊天系统相关控制器到模块导出 - 完善模块依赖关系配置 --- src/business/zulip/zulip.module.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/business/zulip/zulip.module.ts b/src/business/zulip/zulip.module.ts index 5fcc0bc..06b350c 100644 --- a/src/business/zulip/zulip.module.ts +++ b/src/business/zulip/zulip.module.ts @@ -49,6 +49,8 @@ import { SessionManagerService } from './services/session_manager.service'; import { MessageFilterService } from './services/message_filter.service'; import { ZulipEventProcessorService } from './services/zulip_event_processor.service'; import { SessionCleanupService } from './services/session_cleanup.service'; +import { ChatController } from './controllers/chat.controller'; +import { WebSocketDocsController } from './controllers/websocket-docs.controller'; import { ZulipCoreModule } from '../../core/zulip/zulip-core.module'; import { RedisModule } from '../../core/redis/redis.module'; import { LoggerModule } from '../../core/utils/logger/logger.module'; @@ -82,7 +84,12 @@ import { AuthModule } from '../auth/auth.module'; // WebSocket网关 - 处理游戏客户端WebSocket连接 ZulipWebSocketGateway, ], - controllers: [], + controllers: [ + // 聊天相关的REST API控制器 + ChatController, + // WebSocket API文档控制器 + WebSocketDocsController, + ], exports: [ // 导出主服务供其他模块使用 ZulipService, -- 2.25.1 From 003091494f50e70d790aba177502e41a74ac7713 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:07:01 +0800 Subject: [PATCH 6/8] =?UTF-8?q?docs=EF=BC=9A=E5=8D=87=E7=BA=A7=20OpenAPI?= =?UTF-8?q?=20=E6=96=87=E6=A1=A3=E9=85=8D=E7=BD=AE=E5=88=B0=20v2.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 版本号从 1.1.1 升级到 2.0.0 - 新增聊天系统 (chat) API 标签和说明 - 完善 API 文档描述,包含 WebSocket 连接指南 - 添加 JWT Token 格式要求说明 - 新增开发环境和生产环境服务器配置 - 包含 Zulip 集成和地图系统说明 --- src/main.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index e12ee89..c7d70a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -65,10 +65,58 @@ async function bootstrap() { // 配置Swagger文档 const config = new DocumentBuilder() .setTitle('Pixel Game Server API') - .setDescription('像素游戏服务器API文档 - 包含用户认证、登录注册、验证码登录、邮箱冲突检测等功能') - .setVersion('1.1.1') - .addTag('auth', '用户认证相关接口') - .addTag('admin', '管理员后台相关接口') + .setDescription(` +像素游戏服务器API文档 - 包含用户认证、聊天系统、Zulip集成等功能 + +## 主要功能模块 + +### 🔐 用户认证 (auth) +- 用户注册、登录 +- JWT Token 管理 +- 邮箱验证和密码重置 +- 验证码登录 + +### 💬 聊天系统 (chat) +- WebSocket 实时聊天 +- 聊天历史记录 +- 系统状态监控 +- Zulip 集成状态 + +### 👑 管理员后台 (admin) +- 用户管理 +- 系统监控 +- 日志查看 + +## WebSocket 连接 + +游戏聊天功能主要通过 WebSocket 实现: + +**连接地址**: \`ws://localhost:3000/game\` + +**支持的事件**: +- \`login\`: 用户登录(需要 JWT Token) +- \`chat\`: 发送聊天消息 +- \`position_update\`: 位置更新 + +**JWT Token 要求**: +- issuer: \`whale-town\` +- audience: \`whale-town-users\` +- type: \`access\` +- 必需字段: \`sub\`, \`username\`, \`email\`, \`role\` + +## Zulip 集成 + +系统集成了 Zulip 聊天服务,实现游戏内聊天与 Zulip 社群的双向同步。 + +**支持的地图**: +- Whale Port (鲸鱼港) +- Pumpkin Valley (南瓜谷) +- Novice Village (新手村) + `) + .setVersion('2.0.0') + .addTag('auth', '🔐 用户认证相关接口') + .addTag('chat', '💬 聊天系统相关接口') + .addTag('admin', '👑 管理员后台相关接口') .addBearerAuth( { type: 'http', @@ -80,6 +128,8 @@ async function bootstrap() { }, 'JWT-auth', ) + .addServer('http://localhost:3000', '开发环境') + .addServer('https://whaletownend.xinghangee.icu', '生产环境') .build(); const document = SwaggerModule.createDocument(app, config); -- 2.25.1 From dd91264d0c4c6e88625f92d419040182bdb50e10 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:07:18 +0800 Subject: [PATCH 7/8] =?UTF-8?q?service=EF=BC=9A=E5=AE=8C=E5=96=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=9C=8D=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化用户查询和管理逻辑 - 更新相关单元测试 - 完善内存模式用户服务实现 --- src/core/db/users/users.service.spec.ts | 413 ++++++++++++++- src/core/db/users/users.service.ts | 579 +++++++++++++++++----- src/core/db/users/users_memory.service.ts | 6 +- 3 files changed, 844 insertions(+), 154 deletions(-) diff --git a/src/core/db/users/users.service.spec.ts b/src/core/db/users/users.service.spec.ts index 99b4992..4d84055 100644 --- a/src/core/db/users/users.service.spec.ts +++ b/src/core/db/users/users.service.spec.ts @@ -1,20 +1,42 @@ /** * 用户实体、DTO和服务的完整测试套件 * - * 功能: - * - 测试Users实体的结构和装饰器 - * - 测试CreateUserDto的验证规则 - * - 测试UsersService的所有CRUD操作 - * - 验证数据类型和约束条件 + * 功能描述: + * - 测试Users实体的结构和装饰器配置 + * - 测试CreateUserDto的数据验证规则和边界条件 + * - 测试UsersService的所有CRUD操作和业务逻辑 + * - 验证数据类型、约束条件和异常处理 + * - 确保服务层与数据库交互的正确性 + * + * 测试覆盖范围: + * - 实体字段映射和类型验证 + * - DTO数据验证和错误处理 + * - 服务方法的正常流程和异常流程 + * - 数据库操作的模拟和验证 + * - 业务规则和约束条件检查 + * + * 测试策略: + * - 单元测试:独立测试每个方法的功能 + * - 集成测试:测试DTO到Entity的完整流程 + * - 异常测试:验证各种错误情况的处理 + * - 边界测试:测试数据验证的边界条件 + * + * 依赖模块: + * - Jest: 测试框架和断言库 + * - NestJS Testing: 提供测试模块和依赖注入 + * - class-validator: DTO验证测试 + * - TypeORM: 数据库操作模拟 * * @author moyin * @version 1.0.0 * @since 2025-12-17 + * + * @lastModified 2025-01-07 by moyin + * @lastChange 添加完整的测试注释体系,增强测试覆盖率和方法测试 */ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @@ -25,10 +47,21 @@ import { UsersService } from './users.service'; describe('Users Entity, DTO and Service Tests', () => { let service: UsersService; - let repository: Repository; let module: TestingModule; - // 模拟的Repository方法 + /** + * 模拟的TypeORM Repository方法 + * + * 功能:模拟数据库操作,避免真实数据库依赖 + * 包含的方法: + * - save: 保存实体到数据库 + * - find: 查询多个实体 + * - findOne: 查询单个实体 + * - delete: 删除实体 + * - softRemove: 软删除实体 + * - count: 统计实体数量 + * - createQueryBuilder: 创建查询构建器 + */ const mockRepository = { save: jest.fn(), find: jest.fn(), @@ -39,22 +72,45 @@ describe('Users Entity, DTO and Service Tests', () => { createQueryBuilder: jest.fn(), }; - // 测试数据 + /** + * 测试用的模拟用户数据 + * + * 包含所有Users实体的字段: + * - 基础信息:id, username, nickname + * - 联系方式:email, phone + * - 认证信息:password_hash, github_id + * - 状态信息:role, status, email_verified + * - 时间戳:created_at, updated_at + * - 扩展信息:avatar_url + */ const mockUser: Users = { id: BigInt(1), username: 'testuser', email: 'test@example.com', + email_verified: false, phone: '+8613800138000', password_hash: 'hashed_password', nickname: '测试用户', github_id: 'github_123', avatar_url: 'https://example.com/avatar.jpg', role: 1, - email_verified: false, + status: 'active' as any, // UserStatus.ACTIVE created_at: new Date(), updated_at: new Date(), }; + /** + * 测试用的创建用户DTO数据 + * + * 包含创建用户所需的基本字段: + * - 必填字段:username, nickname + * - 可选字段:email, phone, password_hash, github_id, avatar_url, role + * + * 用于测试: + * - 数据验证规则 + * - 用户创建流程 + * - DTO到Entity的转换 + */ const createUserDto: CreateUserDto = { username: 'testuser', email: 'test@example.com', @@ -66,6 +122,17 @@ describe('Users Entity, DTO and Service Tests', () => { role: 1 }; + /** + * 测试前置设置 + * + * 功能: + * - 创建测试模块和依赖注入容器 + * - 配置UsersService和模拟的Repository + * - 初始化测试环境 + * - 清理之前测试的Mock状态 + * + * 执行时机:每个测试用例执行前 + */ beforeEach(async () => { module = await Test.createTestingModule({ providers: [ @@ -78,17 +145,47 @@ describe('Users Entity, DTO and Service Tests', () => { }).compile(); service = module.get(UsersService); - repository = module.get>(getRepositoryToken(Users)); - // 清理所有mock + // 清理所有mock状态,确保测试独立性 jest.clearAllMocks(); }); + /** + * 测试后置清理 + * + * 功能: + * - 关闭测试模块 + * - 释放资源和内存 + * - 防止测试间的状态污染 + * + * 执行时机:每个测试用例执行后 + */ afterEach(async () => { await module.close(); }); + /** + * Users实体测试组 + * + * 测试目标: + * - 验证Users实体类的基本功能 + * - 测试实体字段的设置和获取 + * - 确保实体结构符合设计要求 + * + * 测试内容: + * - 实体实例化和属性赋值 + * - 字段类型和数据完整性 + * - TypeORM装饰器的正确配置 + */ describe('Users Entity Tests', () => { + /** + * 测试用户实体的基本创建和属性设置 + * + * 验证点: + * - 实体可以正常实例化 + * - 所有字段可以正确赋值 + * - 字段值可以正确读取 + */ it('应该正确创建用户实体实例', () => { const user = new Users(); user.username = 'testuser'; @@ -136,7 +233,31 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * CreateUserDto数据验证测试组 + * + * 测试目标: + * - 验证DTO的数据验证规则 + * - 测试各种输入数据的验证结果 + * - 确保数据完整性和业务规则 + * + * 测试内容: + * - 有效数据的验证通过 + * - 无效数据的验证失败 + * - 必填字段的验证 + * - 格式验证(邮箱、手机号等) + * - 长度限制验证 + * - 数值范围验证 + */ describe('CreateUserDto Validation Tests', () => { + /** + * 测试有效数据的验证 + * + * 验证点: + * - 包含所有必填字段的数据应该通过验证 + * - 可选字段的数据格式正确 + * - 验证错误数组应该为空 + */ it('应该通过有效数据的验证', async () => { const validData = { username: 'testuser', @@ -155,6 +276,14 @@ describe('Users Entity, DTO and Service Tests', () => { expect(errors).toHaveLength(0); }); + /** + * 测试缺少必填字段时的验证失败 + * + * 验证点: + * - 缺少username和nickname时验证应该失败 + * - 验证错误数组包含对应的字段错误 + * - 错误信息准确指向缺失的字段 + */ it('应该拒绝缺少必填字段的数据', async () => { const invalidData = { email: 'test@example.com' @@ -173,6 +302,14 @@ describe('Users Entity, DTO and Service Tests', () => { expect(nicknameError).toBeDefined(); }); + /** + * 测试邮箱格式验证 + * + * 验证点: + * - 无效的邮箱格式应该被拒绝 + * - 验证错误指向email字段 + * - 错误信息提示格式不正确 + */ it('应该拒绝无效的邮箱格式', async () => { const invalidData = { username: 'testuser', @@ -215,11 +352,46 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * UsersService CRUD操作测试组 + * + * 测试目标: + * - 验证所有CRUD操作的正确性 + * - 测试业务逻辑和数据处理 + * - 确保异常情况的正确处理 + * + * 测试内容: + * - 创建操作:create, createWithDuplicateCheck, createBatch + * - 查询操作:findAll, findOne, findByUsername, findByEmail, findByGithubId, findByRole, search + * - 更新操作:update + * - 删除操作:remove, softRemove + * - 工具方法:count, exists + * + * 测试策略: + * - 正常流程测试:验证方法的基本功能 + * - 异常流程测试:验证错误处理和异常抛出 + * - 边界条件测试:验证参数边界和特殊情况 + */ describe('UsersService CRUD Tests', () => { + /** + * create()方法测试组 + * + * 测试目标: + * - 验证基础用户创建功能 + * - 测试数据验证和异常处理 + * - 确保数据库操作的正确性 + */ describe('create()', () => { + /** + * 测试成功创建用户的正常流程 + * + * 验证点: + * - Repository.save方法被正确调用 + * - 返回值与期望的用户数据一致 + * - 数据验证通过 + */ it('应该成功创建新用户', async () => { - mockRepository.findOne.mockResolvedValue(null); // 没有重复用户 mockRepository.save.mockResolvedValue(mockUser); const result = await service.create(createUserDto); @@ -228,18 +400,17 @@ describe('Users Entity, DTO and Service Tests', () => { expect(result).toEqual(mockUser); }); - it('应该在用户名重复时抛出ConflictException', async () => { - mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 - - await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); - expect(mockRepository.save).not.toHaveBeenCalled(); - }); - it('应该在数据验证失败时抛出BadRequestException', async () => { const invalidDto = { username: '', nickname: '' }; // 无效数据 await expect(service.create(invalidDto as CreateUserDto)).rejects.toThrow(BadRequestException); }); + + it('应该在系统异常时抛出BadRequestException', async () => { + mockRepository.save.mockRejectedValue(new Error('Database error')); + + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); + }); }); describe('findAll()', () => { @@ -310,17 +481,126 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); - describe('findByEmail()', () => { - it('应该根据邮箱返回用户', async () => { + describe('findByGithubId()', () => { + it('应该根据GitHub ID返回用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); - const result = await service.findByEmail('test@example.com'); + const result = await service.findByGithubId('github_123'); expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { email: 'test@example.com' } + where: { github_id: 'github_123' } }); expect(result).toEqual(mockUser); }); + + it('应该在GitHub ID不存在时返回null', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const result = await service.findByGithubId('nonexistent'); + + expect(result).toBeNull(); + }); + }); + + describe('createWithDuplicateCheck()', () => { + it('应该成功创建用户(带重复检查)', async () => { + // 模拟所有唯一性检查都通过 + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockResolvedValue(mockUser); + + const result = await service.createWithDuplicateCheck(createUserDto); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(4); // 检查用户名、邮箱、手机号、GitHub ID + expect(mockRepository.save).toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it('应该在用户名重复时抛出ConflictException', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + expect(mockRepository.save).not.toHaveBeenCalled(); + }); + + it('应该在邮箱重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(mockUser); // 邮箱已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + + it('应该在手机号重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(null) // 邮箱检查通过 + .mockResolvedValueOnce(mockUser); // 手机号已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + + it('应该在GitHub ID重复时抛出ConflictException', async () => { + mockRepository.findOne + .mockResolvedValueOnce(null) // 用户名检查通过 + .mockResolvedValueOnce(null) // 邮箱检查通过 + .mockResolvedValueOnce(null) // 手机号检查通过 + .mockResolvedValueOnce(mockUser); // GitHub ID已存在 + + await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); + }); + }); + + describe('softRemove()', () => { + it('应该成功软删除用户', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.softRemove.mockResolvedValue(mockUser); + + const result = await service.softRemove(BigInt(1)); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: BigInt(1) } + }); + expect(mockRepository.softRemove).toHaveBeenCalledWith(mockUser); + expect(result).toEqual(mockUser); + }); + + it('应该在软删除不存在的用户时抛出NotFoundException', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.softRemove(BigInt(999))).rejects.toThrow(NotFoundException); + }); + }); + + describe('createBatch()', () => { + it('应该成功批量创建用户', async () => { + const batchDto = [ + { ...createUserDto, username: 'user1', nickname: '用户1' }, + { ...createUserDto, username: 'user2', nickname: '用户2' } + ]; + + const batchUsers = [ + { ...mockUser, username: 'user1', nickname: '用户1' }, + { ...mockUser, username: 'user2', nickname: '用户2' } + ]; + + mockRepository.save.mockResolvedValueOnce(batchUsers[0]).mockResolvedValueOnce(batchUsers[1]); + + const result = await service.createBatch(batchDto); + + expect(mockRepository.save).toHaveBeenCalledTimes(2); + expect(result).toHaveLength(2); + expect(result[0].username).toBe('user1'); + expect(result[1].username).toBe('user2'); + }); + + it('应该在批量创建中某个用户失败时抛出异常', async () => { + const batchDto = [ + { ...createUserDto, username: 'user1' }, + { username: '', nickname: '' } // 无效数据 + ]; + + await expect(service.createBatch(batchDto)).rejects.toThrow(BadRequestException); + }); }); describe('update()', () => { @@ -435,7 +715,29 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * 集成测试组 + * + * 测试目标: + * - 验证DTO到Entity的完整数据流 + * - 测试组件间的协作和集成 + * - 确保端到端流程的正确性 + * + * 测试内容: + * - DTO验证 → 实体创建 → 数据库保存的完整流程 + * - 可选字段的默认值处理 + * - 数据转换和映射的正确性 + */ describe('Integration Tests', () => { + /** + * 测试从DTO到Entity的完整数据流 + * + * 验证点: + * - DTO验证成功 + * - 数据正确转换为Entity + * - 服务方法正确处理数据 + * - 返回结果符合预期 + */ it('应该完成从DTO到Entity的完整流程', async () => { // 1. 验证DTO const dto = plainToClass(CreateUserDto, createUserDto); @@ -474,19 +776,76 @@ describe('Users Entity, DTO and Service Tests', () => { }); }); + /** + * 错误处理测试组 + * + * 测试目标: + * - 验证各种异常情况的处理 + * - 测试错误恢复和降级机制 + * - 确保系统的健壮性和稳定性 + * + * 测试内容: + * - 数据库连接错误处理 + * - 并发操作冲突处理 + * - 系统异常的统一处理 + * - 搜索异常的降级处理 + * - 各种操作失败的异常抛出 + * + * 异常处理策略: + * - 业务异常:直接抛出对应的HTTP异常 + * - 系统异常:转换为BadRequestException + * - 搜索异常:返回空结果而不抛出异常 + */ describe('Error Handling Tests', () => { + /** + * 测试数据库连接错误的处理 + * + * 验证点: + * - 数据库操作失败时抛出正确的异常 + * - 异常类型为BadRequestException + * - 错误信息被正确记录 + */ it('应该正确处理数据库连接错误', async () => { mockRepository.save.mockRejectedValue(new Error('Database connection failed')); - await expect(service.create(createUserDto)).rejects.toThrow('Database connection failed'); + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); }); it('应该正确处理并发创建冲突', async () => { // 模拟并发情况:检查时不存在,保存时出现唯一约束错误 - mockRepository.findOne.mockResolvedValue(null); mockRepository.save.mockRejectedValue(new Error('Duplicate entry')); - await expect(service.create(createUserDto)).rejects.toThrow('Duplicate entry'); + await expect(service.create(createUserDto)).rejects.toThrow(BadRequestException); + }); + + it('应该正确处理搜索异常', async () => { + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockRejectedValue(new Error('Search failed')), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.search('test'); + + // 搜索失败时应该返回空数组,不抛出异常 + expect(result).toEqual([]); + }); + + it('应该正确处理更新时的系统异常', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.save.mockRejectedValue(new Error('Update failed')); + + await expect(service.update(BigInt(1), { nickname: '新昵称' })).rejects.toThrow(BadRequestException); + }); + + it('应该正确处理删除时的系统异常', async () => { + mockRepository.findOne.mockResolvedValue(mockUser); + mockRepository.delete.mockRejectedValue(new Error('Delete failed')); + + await expect(service.remove(BigInt(1))).rejects.toThrow(BadRequestException); }); }); }); \ No newline at end of file diff --git a/src/core/db/users/users.service.ts b/src/core/db/users/users.service.ts index 1b9a36e..3eb26af 100644 --- a/src/core/db/users/users.service.ts +++ b/src/core/db/users/users.service.ts @@ -9,9 +9,12 @@ * @author moyin * @version 1.0.0 * @since 2025-12-17 + * + * @lastModified 2025-01-07 by moyin + * @lastChange 添加完整的日志记录系统和详细的业务逻辑注释,优化异常处理和性能监控 */ -import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, FindOptionsWhere } from 'typeorm'; import { Users } from './users.entity'; @@ -22,6 +25,8 @@ import { plainToClass } from 'class-transformer'; @Injectable() export class UsersService { + private readonly logger = new Logger(UsersService.name); + constructor( @InjectRepository(Users) private readonly usersRepository: Repository, @@ -35,32 +40,81 @@ export class UsersService { * @throws BadRequestException 当数据验证失败时 */ async create(createUserDto: CreateUserDto): Promise { - // 验证DTO - const dto = plainToClass(CreateUserDto, createUserDto); - const validationErrors = await validate(dto); + const startTime = Date.now(); - if (validationErrors.length > 0) { - const errorMessages = validationErrors.map(error => - Object.values(error.constraints || {}).join(', ') - ).join('; '); - throw new BadRequestException(`数据验证失败: ${errorMessages}`); + this.logger.log('开始创建用户', { + operation: 'create', + username: createUserDto.username, + email: createUserDto.email, + timestamp: new Date().toISOString() + }); + + try { + // 验证DTO + const dto = plainToClass(CreateUserDto, createUserDto); + const validationErrors = await validate(dto); + + if (validationErrors.length > 0) { + const errorMessages = validationErrors.map(error => + Object.values(error.constraints || {}).join(', ') + ).join('; '); + + this.logger.warn('用户创建失败:数据验证失败', { + operation: 'create', + username: createUserDto.username, + email: createUserDto.email, + validationErrors: errorMessages + }); + + throw new BadRequestException(`数据验证失败: ${errorMessages}`); + } + + // 创建用户实体 + const user = new Users(); + user.username = createUserDto.username; + user.email = createUserDto.email || null; + user.phone = createUserDto.phone || null; + user.password_hash = createUserDto.password_hash || null; + user.nickname = createUserDto.nickname; + user.github_id = createUserDto.github_id || null; + user.avatar_url = createUserDto.avatar_url || null; + user.role = createUserDto.role || 1; + user.email_verified = createUserDto.email_verified || false; + user.status = createUserDto.status || UserStatus.ACTIVE; + + // 保存到数据库 + const savedUser = await this.usersRepository.save(user); + + const duration = Date.now() - startTime; + + this.logger.log('用户创建成功', { + operation: 'create', + userId: savedUser.id.toString(), + username: savedUser.username, + email: savedUser.email, + duration, + timestamp: new Date().toISOString() + }); + + return savedUser; + } catch (error) { + const duration = Date.now() - startTime; + + if (error instanceof BadRequestException) { + throw error; + } + + this.logger.error('用户创建系统异常', { + operation: 'create', + username: createUserDto.username, + email: createUserDto.email, + error: error instanceof Error ? error.message : String(error), + duration, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + throw new BadRequestException('用户创建失败,请稍后重试'); } - - // 创建用户实体 - const user = new Users(); - user.username = createUserDto.username; - user.email = createUserDto.email || null; - user.phone = createUserDto.phone || null; - user.password_hash = createUserDto.password_hash || null; - user.nickname = createUserDto.nickname; - user.github_id = createUserDto.github_id || null; - user.avatar_url = createUserDto.avatar_url || null; - user.role = createUserDto.role || 1; - user.email_verified = createUserDto.email_verified || false; - user.status = createUserDto.status || UserStatus.ACTIVE; - - // 保存到数据库 - return await this.usersRepository.save(user); } /** @@ -72,48 +126,110 @@ export class UsersService { * @throws BadRequestException 当数据验证失败时 */ async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise { - // 检查用户名是否已存在 - if (createUserDto.username) { - const existingUser = await this.usersRepository.findOne({ - where: { username: createUserDto.username } - }); - if (existingUser) { - throw new ConflictException('用户名已存在'); - } - } + const startTime = Date.now(); + + this.logger.log('开始创建用户(带重复检查)', { + operation: 'createWithDuplicateCheck', + username: createUserDto.username, + email: createUserDto.email, + phone: createUserDto.phone, + github_id: createUserDto.github_id, + timestamp: new Date().toISOString() + }); - // 检查邮箱是否已存在 - if (createUserDto.email) { - const existingEmail = await this.usersRepository.findOne({ - where: { email: createUserDto.email } - }); - if (existingEmail) { - throw new ConflictException('邮箱已存在'); + try { + // 检查用户名是否已存在 + if (createUserDto.username) { + const existingUser = await this.usersRepository.findOne({ + where: { username: createUserDto.username } + }); + if (existingUser) { + this.logger.warn('用户创建失败:用户名已存在', { + operation: 'createWithDuplicateCheck', + username: createUserDto.username, + existingUserId: existingUser.id.toString() + }); + throw new ConflictException('用户名已存在'); + } } - } - // 检查手机号是否已存在 - if (createUserDto.phone) { - const existingPhone = await this.usersRepository.findOne({ - where: { phone: createUserDto.phone } - }); - if (existingPhone) { - throw new ConflictException('手机号已存在'); + // 检查邮箱是否已存在 + if (createUserDto.email) { + const existingEmail = await this.usersRepository.findOne({ + where: { email: createUserDto.email } + }); + if (existingEmail) { + this.logger.warn('用户创建失败:邮箱已存在', { + operation: 'createWithDuplicateCheck', + email: createUserDto.email, + existingUserId: existingEmail.id.toString() + }); + throw new ConflictException('邮箱已存在'); + } } - } - // 检查GitHub ID是否已存在 - if (createUserDto.github_id) { - const existingGithub = await this.usersRepository.findOne({ - where: { github_id: createUserDto.github_id } - }); - if (existingGithub) { - throw new ConflictException('GitHub ID已存在'); + // 检查手机号是否已存在 + if (createUserDto.phone) { + const existingPhone = await this.usersRepository.findOne({ + where: { phone: createUserDto.phone } + }); + if (existingPhone) { + this.logger.warn('用户创建失败:手机号已存在', { + operation: 'createWithDuplicateCheck', + phone: createUserDto.phone, + existingUserId: existingPhone.id.toString() + }); + throw new ConflictException('手机号已存在'); + } } - } - // 调用普通的创建方法 - return await this.create(createUserDto); + // 检查GitHub ID是否已存在 + if (createUserDto.github_id) { + const existingGithub = await this.usersRepository.findOne({ + where: { github_id: createUserDto.github_id } + }); + if (existingGithub) { + this.logger.warn('用户创建失败:GitHub ID已存在', { + operation: 'createWithDuplicateCheck', + github_id: createUserDto.github_id, + existingUserId: existingGithub.id.toString() + }); + throw new ConflictException('GitHub ID已存在'); + } + } + + // 调用普通的创建方法 + const user = await this.create(createUserDto); + + const duration = Date.now() - startTime; + + this.logger.log('用户创建成功(带重复检查)', { + operation: 'createWithDuplicateCheck', + userId: user.id.toString(), + username: user.username, + duration, + timestamp: new Date().toISOString() + }); + + return user; + } catch (error) { + const duration = Date.now() - startTime; + + if (error instanceof ConflictException || error instanceof BadRequestException) { + throw error; + } + + this.logger.error('用户创建系统异常(带重复检查)', { + operation: 'createWithDuplicateCheck', + username: createUserDto.username, + email: createUserDto.email, + error: error instanceof Error ? error.message : String(error), + duration, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + throw new BadRequestException('用户创建失败,请稍后重试'); + } } /** @@ -189,77 +305,223 @@ export class UsersService { /** * 更新用户信息 * - * @param id 用户ID - * @param updateData 更新的数据 + * 功能描述: + * 更新指定用户的信息,包含完整的数据验证和唯一性检查 + * + * 业务逻辑: + * 1. 验证用户是否存在 + * 2. 检查更新字段的唯一性约束(用户名、邮箱、手机号、GitHub ID) + * 3. 合并更新数据到现有用户实体 + * 4. 保存更新后的用户信息 + * 5. 记录操作日志 + * + * @param id 用户ID,必须是有效的已存在用户 + * @param updateData 更新的数据,支持部分字段更新 * @returns 更新后的用户实体 * @throws NotFoundException 当用户不存在时 * @throws ConflictException 当更新的数据与其他用户冲突时 + * + * @example + * ```typescript + * const updatedUser = await usersService.update(BigInt(1), { + * nickname: '新昵称', + * email: 'new@example.com' + * }); + * ``` */ async update(id: bigint, updateData: Partial): Promise { - // 检查用户是否存在 - const existingUser = await this.findOne(id); - - // 检查更新数据的唯一性约束 - if (updateData.username && updateData.username !== existingUser.username) { - const usernameExists = await this.usersRepository.findOne({ - where: { username: updateData.username } - }); - if (usernameExists) { - throw new ConflictException('用户名已存在'); - } - } - - if (updateData.email && updateData.email !== existingUser.email) { - const emailExists = await this.usersRepository.findOne({ - where: { email: updateData.email } - }); - if (emailExists) { - throw new ConflictException('邮箱已存在'); - } - } - - if (updateData.phone && updateData.phone !== existingUser.phone) { - const phoneExists = await this.usersRepository.findOne({ - where: { phone: updateData.phone } - }); - if (phoneExists) { - throw new ConflictException('手机号已存在'); - } - } - - if (updateData.github_id && updateData.github_id !== existingUser.github_id) { - const githubExists = await this.usersRepository.findOne({ - where: { github_id: updateData.github_id } - }); - if (githubExists) { - throw new ConflictException('GitHub ID已存在'); - } - } - - // 更新用户数据 - Object.assign(existingUser, updateData); + const startTime = Date.now(); - return await this.usersRepository.save(existingUser); + this.logger.log('开始更新用户信息', { + operation: 'update', + userId: id.toString(), + updateFields: Object.keys(updateData), + timestamp: new Date().toISOString() + }); + + try { + // 1. 检查用户是否存在 - 确保要更新的用户确实存在 + const existingUser = await this.findOne(id); + + // 2. 检查更新数据的唯一性约束 - 防止违反数据库唯一约束 + + // 2.1 检查用户名唯一性 - 只有当用户名确实发生变化时才检查 + if (updateData.username && updateData.username !== existingUser.username) { + const usernameExists = await this.usersRepository.findOne({ + where: { username: updateData.username } + }); + if (usernameExists) { + this.logger.warn('用户更新失败:用户名已存在', { + operation: 'update', + userId: id.toString(), + conflictUsername: updateData.username, + existingUserId: usernameExists.id.toString() + }); + throw new ConflictException('用户名已存在'); + } + } + + // 2.2 检查邮箱唯一性 - 只有当邮箱确实发生变化时才检查 + if (updateData.email && updateData.email !== existingUser.email) { + const emailExists = await this.usersRepository.findOne({ + where: { email: updateData.email } + }); + if (emailExists) { + this.logger.warn('用户更新失败:邮箱已存在', { + operation: 'update', + userId: id.toString(), + conflictEmail: updateData.email, + existingUserId: emailExists.id.toString() + }); + throw new ConflictException('邮箱已存在'); + } + } + + // 2.3 检查手机号唯一性 - 只有当手机号确实发生变化时才检查 + if (updateData.phone && updateData.phone !== existingUser.phone) { + const phoneExists = await this.usersRepository.findOne({ + where: { phone: updateData.phone } + }); + if (phoneExists) { + this.logger.warn('用户更新失败:手机号已存在', { + operation: 'update', + userId: id.toString(), + conflictPhone: updateData.phone, + existingUserId: phoneExists.id.toString() + }); + throw new ConflictException('手机号已存在'); + } + } + + // 2.4 检查GitHub ID唯一性 - 只有当GitHub ID确实发生变化时才检查 + if (updateData.github_id && updateData.github_id !== existingUser.github_id) { + const githubExists = await this.usersRepository.findOne({ + where: { github_id: updateData.github_id } + }); + if (githubExists) { + this.logger.warn('用户更新失败:GitHub ID已存在', { + operation: 'update', + userId: id.toString(), + conflictGithubId: updateData.github_id, + existingUserId: githubExists.id.toString() + }); + throw new ConflictException('GitHub ID已存在'); + } + } + + // 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体 + Object.assign(existingUser, updateData); + + // 4. 保存更新后的用户信息 - TypeORM会自动更新updated_at字段 + const updatedUser = await this.usersRepository.save(existingUser); + + const duration = Date.now() - startTime; + + this.logger.log('用户信息更新成功', { + operation: 'update', + userId: id.toString(), + updateFields: Object.keys(updateData), + duration, + timestamp: new Date().toISOString() + }); + + return updatedUser; + } catch (error) { + const duration = Date.now() - startTime; + + if (error instanceof NotFoundException || error instanceof ConflictException) { + throw error; + } + + this.logger.error('用户更新系统异常', { + operation: 'update', + userId: id.toString(), + updateData, + error: error instanceof Error ? error.message : String(error), + duration, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + throw new BadRequestException('用户更新失败,请稍后重试'); + } } /** * 删除用户 * - * @param id 用户ID - * @returns 删除操作结果 + * 功能描述: + * 物理删除指定的用户记录,数据将从数据库中永久移除 + * + * 业务逻辑: + * 1. 验证用户是否存在 + * 2. 执行物理删除操作 + * 3. 返回删除结果统计 + * 4. 记录删除操作日志 + * + * 注意事项: + * - 这是物理删除,数据无法恢复 + * - 如需保留数据,请使用 softRemove 方法 + * - 删除前请确认用户没有关联的重要数据 + * + * @param id 用户ID,必须是有效的已存在用户 + * @returns 删除操作结果,包含影响行数和操作消息 * @throws NotFoundException 当用户不存在时 + * + * @example + * ```typescript + * const result = await usersService.remove(BigInt(1)); + * console.log(`删除了 ${result.affected} 个用户`); + * ``` */ async remove(id: bigint): Promise<{ affected: number; message: string }> { - // 检查用户是否存在 - await this.findOne(id); + const startTime = Date.now(); + + this.logger.log('开始删除用户', { + operation: 'remove', + userId: id.toString(), + timestamp: new Date().toISOString() + }); - // 执行删除 - 使用where条件来处理bigint类型 - const result = await this.usersRepository.delete({ id }); + try { + // 1. 检查用户是否存在 - 确保要删除的用户确实存在 + await this.findOne(id); - return { - affected: result.affected || 0, - message: `成功删除ID为 ${id} 的用户` - }; + // 2. 执行删除操作 - 使用where条件来处理bigint类型 + const result = await this.usersRepository.delete({ id }); + + const deleteResult = { + affected: result.affected || 0, + message: `成功删除ID为 ${id} 的用户` + }; + + const duration = Date.now() - startTime; + + this.logger.log('用户删除成功', { + operation: 'remove', + userId: id.toString(), + affected: deleteResult.affected, + duration, + timestamp: new Date().toISOString() + }); + + return deleteResult; + } catch (error) { + const duration = Date.now() - startTime; + + if (error instanceof NotFoundException) { + throw error; + } + + this.logger.error('用户删除系统异常', { + operation: 'remove', + userId: id.toString(), + error: error instanceof Error ? error.message : String(error), + duration, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + throw new BadRequestException('用户删除失败,请稍后重试'); + } } /** @@ -328,18 +590,83 @@ export class UsersService { /** * 搜索用户(根据用户名或昵称) * - * @param keyword 搜索关键词 - * @param limit 限制数量 - * @returns 用户列表 + * 功能描述: + * 根据关键词在用户名和昵称字段中进行模糊搜索,支持部分匹配 + * + * 业务逻辑: + * 1. 使用QueryBuilder构建复杂查询 + * 2. 对用户名和昵称字段进行LIKE模糊匹配 + * 3. 按创建时间倒序排列结果 + * 4. 限制返回数量防止性能问题 + * + * 性能考虑: + * - 使用数据库索引优化查询性能 + * - 限制返回数量避免大数据量问题 + * - 建议在用户名和昵称字段上建立索引 + * + * @param keyword 搜索关键词,支持中文、英文、数字等字符 + * @param limit 限制数量,默认20条,建议不超过100 + * @returns 匹配的用户列表,按创建时间倒序排列 + * + * @example + * ```typescript + * // 搜索包含"张三"的用户 + * const users = await usersService.search('张三', 10); + * + * // 搜索包含"admin"的用户 + * const adminUsers = await usersService.search('admin'); + * ``` */ async search(keyword: string, limit: number = 20): Promise { - return await this.usersRepository - .createQueryBuilder('user') - .where('user.username LIKE :keyword OR user.nickname LIKE :keyword', { - keyword: `%${keyword}%` - }) - .orderBy('user.created_at', 'DESC') - .limit(limit) - .getMany(); + const startTime = Date.now(); + + this.logger.log('开始搜索用户', { + operation: 'search', + keyword, + limit, + timestamp: new Date().toISOString() + }); + + try { + // 1. 构建查询 - 使用QueryBuilder支持复杂的WHERE条件 + const queryBuilder = this.usersRepository.createQueryBuilder('user'); + + // 2. 添加搜索条件 - 在用户名和昵称中进行模糊匹配 + // 使用参数化查询防止SQL注入攻击 + const result = await queryBuilder + .where('user.username LIKE :keyword OR user.nickname LIKE :keyword', { + keyword: `%${keyword}%` // 前后加%实现模糊匹配 + }) + .orderBy('user.created_at', 'DESC') // 按创建时间倒序 + .limit(limit) // 限制返回数量 + .getMany(); + + const duration = Date.now() - startTime; + + this.logger.log('用户搜索完成', { + operation: 'search', + keyword, + limit, + resultCount: result.length, + duration, + timestamp: new Date().toISOString() + }); + + return result; + } catch (error) { + const duration = Date.now() - startTime; + + this.logger.error('用户搜索异常', { + operation: 'search', + keyword, + limit, + error: error instanceof Error ? error.message : String(error), + duration, + timestamp: new Date().toISOString() + }, error instanceof Error ? error.stack : undefined); + + // 搜索失败时返回空数组,不影响用户体验 + return []; + } } } \ No newline at end of file diff --git a/src/core/db/users/users_memory.service.ts b/src/core/db/users/users_memory.service.ts index e417423..27b71e4 100644 --- a/src/core/db/users/users_memory.service.ts +++ b/src/core/db/users/users_memory.service.ts @@ -19,9 +19,12 @@ * @author angjustinl * @version 1.0.0 * @since 2025-12-17 + * + * @lastModified 2025-01-07 by Kiro + * @lastChange 添加日志记录系统,统一异常处理和性能监控 */ -import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; import { Users } from './users.entity'; import { CreateUserDto } from './users.dto'; import { UserStatus } from '../../../business/user-mgmt/enums/user-status.enum'; @@ -30,6 +33,7 @@ import { plainToClass } from 'class-transformer'; @Injectable() export class UsersMemoryService { + private readonly logger = new Logger(UsersMemoryService.name); private users: Map = new Map(); private currentId: bigint = BigInt(1); -- 2.25.1 From 2bcbaeb030cc8740f39631badbcee8c1bca5c834 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 7 Jan 2026 15:07:44 +0800 Subject: [PATCH 8/8] =?UTF-8?q?chore=EF=BC=9A=E6=B8=85=E7=90=86=E4=B8=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E7=9A=84=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=92=8C=E4=B8=B4=E6=97=B6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 test_zulip.js、test_zulip_registration.js、test_zulip_user_management.js - 删除 full_diagnosis.js 诊断脚本 - 删除 docs/development/backend_development_guide.md 重复文档 - 保持代码库整洁,移除临时测试文件 --- docs/development/backend_development_guide.md | 856 ------------------ full_diagnosis.js | 311 ------- test_zulip.js | 131 --- test_zulip_registration.js | 196 ---- test_zulip_user_management.js | 275 ------ 5 files changed, 1769 deletions(-) delete mode 100644 docs/development/backend_development_guide.md delete mode 100644 full_diagnosis.js delete mode 100644 test_zulip.js delete mode 100644 test_zulip_registration.js delete mode 100644 test_zulip_user_management.js diff --git a/docs/development/backend_development_guide.md b/docs/development/backend_development_guide.md deleted file mode 100644 index e416a99..0000000 --- a/docs/development/backend_development_guide.md +++ /dev/null @@ -1,856 +0,0 @@ -# 后端开发规范指南 - -## 一、文档概述 - -### 1.1 文档目的 - -本文档定义了 Datawhale Town 后端开发的编码规范、注释标准、业务逻辑设计原则和日志记录要求,确保代码质量、可维护性和系统稳定性。 - -### 1.2 适用范围 - -- 所有后端开发人员 -- 代码审查人员 -- 系统维护人员 - ---- - -## 二、注释规范 - -### 2.1 模块注释 - -每个功能模块文件必须包含模块级注释,说明模块用途、主要功能和依赖关系。 - -**格式要求:** - -```typescript -/** - * 玩家管理模块 - * - * 功能描述: - * - 处理玩家注册、登录、信息更新等核心功能 - * - 管理玩家角色皮肤和个人资料 - * - 提供玩家数据的 CRUD 操作 - * - * 依赖模块: - * - AuthService: 身份验证服务 - * - DatabaseService: 数据库操作服务 - * - LoggerService: 日志记录服务 - * - * @author 开发者姓名 - * @version 1.0.0 - * @since 2025-12-13 - */ -``` - -### 2.2 类注释 - -每个类必须包含类级注释,说明类的职责、主要方法和使用场景。 - -**格式要求:** - -```typescript -/** - * 玩家服务类 - * - * 职责: - * - 处理玩家相关的业务逻辑 - * - 管理玩家状态和数据 - * - 提供玩家操作的统一接口 - * - * 主要方法: - * - createPlayer(): 创建新玩家 - * - updatePlayerInfo(): 更新玩家信息 - * - getPlayerById(): 根据ID获取玩家信息 - * - * 使用场景: - * - 玩家注册登录流程 - * - 个人陈列室数据管理 - * - 广场玩家状态同步 - */ -@Injectable() -export class PlayerService { - // 类实现 -} -``` - -### 2.3 方法注释 - -每个方法必须包含详细的方法注释,说明功能、参数、返回值、异常和业务逻辑。 - -**格式要求:** - -```typescript -/** - * 创建新玩家 - * - * 功能描述: - * 根据邮箱创建新的玩家账户,包含基础信息初始化和默认配置设置 - * - * 业务逻辑: - * 1. 验证邮箱格式和白名单 - * 2. 检查邮箱是否已存在 - * 3. 生成唯一玩家ID - * 4. 初始化默认角色皮肤和个人信息 - * 5. 创建对应的个人陈列室 - * 6. 记录创建日志 - * - * @param email 玩家邮箱地址,必须符合邮箱格式且在白名单中 - * @param nickname 玩家昵称,长度3-20字符,不能包含特殊字符 - * @param avatarSkin 角色皮肤ID,必须是1-8之间的有效值 - * @returns Promise 创建成功的玩家对象 - * - * @throws BadRequestException 当邮箱格式错误或不在白名单中 - * @throws ConflictException 当邮箱已存在时 - * @throws InternalServerErrorException 当数据库操作失败时 - * - * @example - * ```typescript - * const player = await playerService.createPlayer( - * 'user@datawhale.club', - * '数据鲸鱼', - * '1' - * ); - * ``` - */ -async createPlayer( - email: string, - nickname: string, - avatarSkin: string -): Promise { - // 方法实现 -} -``` - -### 2.4 复杂业务逻辑注释 - -对于复杂的业务逻辑,必须添加行内注释说明每个步骤的目的和处理逻辑。 - -**示例:** - -```typescript -async joinRoom(roomId: string, playerId: string): Promise { - // 1. 参数验证 - 确保房间ID和玩家ID格式正确 - if (!roomId || !playerId) { - this.logger.warn(`房间加入失败:参数无效`, { roomId, playerId }); - throw new BadRequestException('房间ID和玩家ID不能为空'); - } - - // 2. 获取房间信息 - 检查房间是否存在 - const room = await this.roomRepository.findById(roomId); - if (!room) { - this.logger.warn(`房间加入失败:房间不存在`, { roomId, playerId }); - throw new NotFoundException('房间不存在'); - } - - // 3. 检查房间状态 - 只有等待中的房间才能加入 - if (room.status !== RoomStatus.WAITING) { - this.logger.warn(`房间加入失败:房间状态不允许加入`, { - roomId, - playerId, - currentStatus: room.status - }); - throw new BadRequestException('游戏已开始,无法加入房间'); - } - - // 4. 检查房间容量 - 防止超过最大人数限制 - if (room.players.length >= room.maxPlayers) { - this.logger.warn(`房间加入失败:房间已满`, { - roomId, - playerId, - currentPlayers: room.players.length, - maxPlayers: room.maxPlayers - }); - throw new BadRequestException('房间已满'); - } - - // 5. 检查玩家是否已在房间中 - 防止重复加入 - if (room.players.includes(playerId)) { - this.logger.info(`玩家已在房间中,跳过加入操作`, { roomId, playerId }); - return room; - } - - // 6. 执行加入操作 - 更新房间玩家列表 - try { - room.players.push(playerId); - const updatedRoom = await this.roomRepository.save(room); - - // 7. 记录成功日志 - this.logger.info(`玩家成功加入房间`, { - roomId, - playerId, - currentPlayers: updatedRoom.players.length, - maxPlayers: updatedRoom.maxPlayers - }); - - return updatedRoom; - } catch (error) { - // 8. 异常处理 - 记录错误并抛出 - this.logger.error(`房间加入操作数据库错误`, { - roomId, - playerId, - error: error.message, - stack: error.stack - }); - throw new InternalServerErrorException('房间加入失败,请稍后重试'); - } -} -``` - ---- - -## 三、业务逻辑设计原则 - -### 3.1 全面性原则 - -每个业务方法必须考虑所有可能的情况,包括正常流程、异常情况和边界条件。 - -**必须考虑的情况:** - -| 类别 | 具体情况 | 处理方式 | -|------|---------|---------| -| **输入验证** | 参数为空、格式错误、超出范围 | 参数校验 + 异常抛出 | -| **权限检查** | 未登录、权限不足、令牌过期 | 身份验证 + 权限验证 | -| **资源状态** | 资源不存在、状态不正确、已被占用 | 状态检查 + 业务规则验证 | -| **并发控制** | 同时操作、数据竞争、锁冲突 | 事务处理 + 乐观锁/悲观锁 | -| **系统异常** | 数据库连接失败、网络超时、内存不足 | 异常捕获 + 降级处理 | -| **业务规则** | 违反业务约束、超出限制、状态冲突 | 业务规则验证 + 友好提示 | - -### 3.2 防御性编程 - -采用防御性编程思想,对所有外部输入和依赖进行验证和保护。 - -**实现要求:** - -```typescript -/** - * 更新玩家信息 - 防御性编程示例 - */ -async updatePlayerInfo( - playerId: string, - updateData: UpdatePlayerDto -): Promise { - // 1. 输入参数防御性检查 - if (!playerId) { - this.logger.warn('更新玩家信息失败:玩家ID为空'); - throw new BadRequestException('玩家ID不能为空'); - } - - if (!updateData || Object.keys(updateData).length === 0) { - this.logger.warn('更新玩家信息失败:更新数据为空', { playerId }); - throw new BadRequestException('更新数据不能为空'); - } - - // 2. 数据格式验证 - if (updateData.nickname) { - if (updateData.nickname.length < 3 || updateData.nickname.length > 20) { - this.logger.warn('更新玩家信息失败:昵称长度不符合要求', { - playerId, - nicknameLength: updateData.nickname.length - }); - throw new BadRequestException('昵称长度必须在3-20字符之间'); - } - } - - if (updateData.avatarSkin) { - const validSkins = ['1', '2', '3', '4', '5', '6', '7', '8']; - if (!validSkins.includes(updateData.avatarSkin)) { - this.logger.warn('更新玩家信息失败:角色皮肤ID无效', { - playerId, - avatarSkin: updateData.avatarSkin - }); - throw new BadRequestException('角色皮肤ID必须在1-8之间'); - } - } - - // 3. 玩家存在性检查 - const existingPlayer = await this.playerRepository.findById(playerId); - if (!existingPlayer) { - this.logger.warn('更新玩家信息失败:玩家不存在', { playerId }); - throw new NotFoundException('玩家不存在'); - } - - // 4. 昵称唯一性检查(如果更新昵称) - if (updateData.nickname && updateData.nickname !== existingPlayer.nickname) { - const nicknameExists = await this.playerRepository.findByNickname(updateData.nickname); - if (nicknameExists) { - this.logger.warn('更新玩家信息失败:昵称已存在', { - playerId, - nickname: updateData.nickname - }); - throw new ConflictException('昵称已被使用'); - } - } - - // 5. 执行更新操作(使用事务保证数据一致性) - try { - const updatedPlayer = await this.playerRepository.update(playerId, updateData); - - this.logger.info('玩家信息更新成功', { - playerId, - updatedFields: Object.keys(updateData), - timestamp: new Date().toISOString() - }); - - return updatedPlayer; - } catch (error) { - this.logger.error('更新玩家信息数据库操作失败', { - playerId, - updateData, - error: error.message, - stack: error.stack - }); - throw new InternalServerErrorException('更新失败,请稍后重试'); - } -} -``` - -### 3.3 异常处理策略 - -建立统一的异常处理策略,确保所有异常都能被正确捕获和处理。 - -**异常分类和处理:** - -| 异常类型 | HTTP状态码 | 处理策略 | 日志级别 | -|---------|-----------|---------|---------| -| **BadRequestException** | 400 | 参数验证失败,返回具体错误信息 | WARN | -| **UnauthorizedException** | 401 | 身份验证失败,要求重新登录 | WARN | -| **ForbiddenException** | 403 | 权限不足,拒绝访问 | WARN | -| **NotFoundException** | 404 | 资源不存在,返回友好提示 | WARN | -| **ConflictException** | 409 | 资源冲突,如重复创建 | WARN | -| **InternalServerErrorException** | 500 | 系统内部错误,记录详细日志 | ERROR | - ---- - -## 四、日志系统使用指南 - -### 4.1 日志服务简介 - -项目使用统一的 `AppLoggerService` 提供日志记录功能,集成了 Pino 高性能日志库,支持自动敏感信息过滤和请求上下文绑定。 - -### 4.2 在服务中使用日志 - -**依赖注入:** - -```typescript -import { Injectable } from '@nestjs/common'; -import { AppLoggerService } from '../core/utils/logger/logger.service'; - -@Injectable() -export class UserService { - constructor( - private readonly logger: AppLoggerService - ) {} -} -``` - -### 4.3 日志级别和使用场景 - -| 级别 | 使用场景 | 示例 | -|------|---------|------| -| **ERROR** | 系统错误、异常情况、数据库连接失败 | 数据库连接超时、第三方服务调用失败 | -| **WARN** | 业务警告、参数错误、权限不足 | 用户输入无效参数、尝试访问不存在的资源 | -| **INFO** | 重要业务操作、状态变更、关键流程 | 用户登录成功、房间创建、玩家加入广场 | -| **DEBUG** | 调试信息、详细执行流程 | 方法调用参数、中间计算结果 | -| **FATAL** | 致命错误、系统不可用 | 数据库完全不可用、关键服务宕机 | -| **TRACE** | 极细粒度调试信息 | 循环内的变量状态、算法执行步骤 | - -### 4.4 标准日志格式 - -**推荐的日志上下文格式:** - -```typescript -// 成功操作日志 -this.logger.info('操作描述', { - operation: '操作类型', - userId: '用户ID', - resourceId: '资源ID', - params: '关键参数', - result: '操作结果', - duration: '执行时间(ms)', - timestamp: new Date().toISOString() -}); - -// 警告日志 -this.logger.warn('警告描述', { - operation: '操作类型', - userId: '用户ID', - reason: '警告原因', - params: '相关参数', - timestamp: new Date().toISOString() -}); - -// 错误日志 -this.logger.error('错误描述', { - operation: '操作类型', - userId: '用户ID', - error: error.message, - params: '相关参数', - timestamp: new Date().toISOString() -}, error.stack); -``` - -### 4.5 请求上下文绑定 - -**在 Controller 中使用:** - -```typescript -@Controller('users') -export class UserController { - constructor(private readonly logger: AppLoggerService) {} - - @Get(':id') - async getUser(@Param('id') id: string, @Req() req: Request) { - // 绑定请求上下文 - const requestLogger = this.logger.bindRequest(req, 'UserController'); - - requestLogger.info('开始获取用户信息', { userId: id }); - - try { - const user = await this.userService.findById(id); - requestLogger.info('用户信息获取成功', { userId: id }); - return user; - } catch (error) { - requestLogger.error('用户信息获取失败', error.stack, { - userId: id, - reason: error.message - }); - throw error; - } - } -} -``` - -### 4.6 业务方法日志记录最佳实践 - -**完整的业务方法日志记录示例:** - -```typescript -async createPlayer(email: string, nickname: string): Promise { - const startTime = Date.now(); - - this.logger.info('开始创建玩家', { - operation: 'createPlayer', - email, - nickname, - timestamp: new Date().toISOString() - }); - - try { - // 1. 参数验证 - if (!email || !nickname) { - this.logger.warn('创建玩家失败:参数无效', { - operation: 'createPlayer', - email, - nickname, - reason: 'invalid_parameters' - }); - throw new BadRequestException('邮箱和昵称不能为空'); - } - - // 2. 邮箱格式验证 - if (!this.isValidEmail(email)) { - this.logger.warn('创建玩家失败:邮箱格式无效', { - operation: 'createPlayer', - email, - nickname - }); - throw new BadRequestException('邮箱格式不正确'); - } - - // 3. 检查邮箱是否已存在 - const existingPlayer = await this.playerRepository.findByEmail(email); - if (existingPlayer) { - this.logger.warn('创建玩家失败:邮箱已存在', { - operation: 'createPlayer', - email, - nickname, - existingPlayerId: existingPlayer.id - }); - throw new ConflictException('邮箱已被使用'); - } - - // 4. 创建玩家 - const player = await this.playerRepository.create({ - email, - nickname, - avatarSkin: '1', // 默认皮肤 - createTime: new Date() - }); - - const duration = Date.now() - startTime; - - this.logger.info('玩家创建成功', { - operation: 'createPlayer', - playerId: player.id, - email, - nickname, - duration, - timestamp: new Date().toISOString() - }); - - return player; - - } catch (error) { - const duration = Date.now() - startTime; - - if (error instanceof BadRequestException || - error instanceof ConflictException) { - // 业务异常,重新抛出 - throw error; - } - - // 系统异常,记录详细日志 - this.logger.error('创建玩家系统异常', { - operation: 'createPlayer', - email, - nickname, - error: error.message, - duration, - timestamp: new Date().toISOString() - }, error.stack); - - throw new InternalServerErrorException('创建玩家失败,请稍后重试'); - } -} -``` - -### 4.7 必须记录日志的操作 - -| 操作类型 | 日志级别 | 记录内容 | -|---------|---------|---------| -| **用户认证** | INFO/WARN | 登录成功/失败、令牌生成/验证 | -| **数据变更** | INFO | 创建、更新、删除操作 | -| **权限检查** | WARN | 权限验证失败、非法访问尝试 | -| **系统异常** | ERROR | 异常堆栈、错误上下文、影响范围 | -| **性能监控** | INFO | 慢查询、高并发操作、资源使用 | -| **安全事件** | WARN/ERROR | 恶意请求、频繁操作、异常行为 | - -### 4.8 敏感信息保护 - -日志系统会自动过滤以下敏感字段: -- `password` - 密码 -- `token` - 令牌 -- `secret` - 密钥 -- `authorization` - 授权信息 -- `cardNo` - 卡号 - -**注意:** 包含这些关键词的字段会被自动替换为 `[REDACTED]` - ---- - -## 五、代码审查检查清单 - -### 5.1 注释检查 - -- [ ] 模块文件包含完整的模块级注释 -- [ ] 每个类都有详细的类级注释 -- [ ] 每个公共方法都有完整的方法注释 -- [ ] 复杂业务逻辑有行内注释说明 -- [ ] 注释内容准确,与代码实现一致 - -### 5.2 业务逻辑检查 - -- [ ] 考虑了所有可能的输入情况 -- [ ] 包含完整的参数验证 -- [ ] 处理了所有可能的异常情况 -- [ ] 实现了适当的权限检查 -- [ ] 考虑了并发和竞态条件 - -### 5.3 日志记录检查 - -- [ ] 关键业务操作都有日志记录 -- [ ] 日志级别使用正确 -- [ ] 日志格式符合规范 -- [ ] 包含足够的上下文信息 -- [ ] 敏感信息已脱敏处理 - -### 5.4 异常处理检查 - -- [ ] 所有异常都被正确捕获 -- [ ] 异常类型选择合适 -- [ ] 异常信息对用户友好 -- [ ] 系统异常有详细的错误日志 -- [ ] 不会泄露敏感的系统信息 - ---- - -## 六、最佳实践示例 - -### 6.1 完整的服务类示例 - -```typescript -/** - * 广场管理服务 - * - * 功能描述: - * - 管理中央广场的玩家状态和位置同步 - * - 处理玩家进入和离开广场的逻辑 - * - 维护广场在线玩家列表(最多50人) - * - * 依赖模块: - * - PlayerService: 玩家信息服务 - * - WebSocketGateway: WebSocket通信网关 - * - RedisService: 缓存服务 - * - LoggerService: 日志记录服务 - * - * @author 开发团队 - * @version 1.0.0 - * @since 2025-12-13 - */ -@Injectable() -export class PlazaService { - private readonly logger = new Logger(PlazaService.name); - private readonly MAX_PLAYERS = 50; - - constructor( - private readonly playerService: PlayerService, - private readonly redisService: RedisService, - private readonly webSocketGateway: WebSocketGateway - ) {} - - /** - * 玩家进入广场 - * - * 功能描述: - * 处理玩家进入中央广场的逻辑,包括人数限制检查、位置分配和状态同步 - * - * 业务逻辑: - * 1. 验证玩家身份和权限 - * 2. 检查广场当前人数是否超限 - * 3. 为玩家分配初始位置 - * 4. 更新Redis中的在线玩家列表 - * 5. 向其他玩家广播新玩家进入消息 - * 6. 向新玩家发送当前广场状态 - * - * @param playerId 玩家ID,必须是有效的已注册玩家 - * @param socketId WebSocket连接ID,用于消息推送 - * @returns Promise 玩家在广场的信息 - * - * @throws UnauthorizedException 当玩家身份验证失败时 - * @throws BadRequestException 当广场人数已满时 - * @throws InternalServerErrorException 当系统操作失败时 - */ - async enterPlaza(playerId: string, socketId: string): Promise { - const startTime = Date.now(); - - this.logger.info('玩家尝试进入广场', { - operation: 'enterPlaza', - playerId, - socketId, - timestamp: new Date().toISOString() - }); - - try { - // 1. 验证玩家身份 - const player = await this.playerService.getPlayerById(playerId); - if (!player) { - this.logger.warn('进入广场失败:玩家不存在', { - operation: 'enterPlaza', - playerId, - socketId - }); - throw new UnauthorizedException('玩家身份验证失败'); - } - - // 2. 检查广场人数限制 - const currentPlayers = await this.redisService.scard('plaza:online_players'); - if (currentPlayers >= this.MAX_PLAYERS) { - this.logger.warn('进入广场失败:人数已满', { - operation: 'enterPlaza', - playerId, - currentPlayers, - maxPlayers: this.MAX_PLAYERS - }); - throw new BadRequestException('广场人数已满,请稍后再试'); - } - - // 3. 检查玩家是否已在广场中 - const isAlreadyInPlaza = await this.redisService.sismember('plaza:online_players', playerId); - if (isAlreadyInPlaza) { - this.logger.info('玩家已在广场中,更新连接信息', { - operation: 'enterPlaza', - playerId, - socketId - }); - - // 更新Socket连接映射 - await this.redisService.hset('plaza:player_sockets', playerId, socketId); - - // 获取当前位置信息 - const existingInfo = await this.redisService.hget('plaza:player_positions', playerId); - return JSON.parse(existingInfo); - } - - // 4. 为玩家分配初始位置(广场中心附近随机位置) - const initialPosition = this.generateInitialPosition(); - - const playerInfo: PlazaPlayerInfo = { - playerId: player.id, - nickname: player.nickname, - avatarSkin: player.avatarSkin, - position: initialPosition, - lastUpdate: new Date(), - socketId - }; - - // 5. 更新Redis中的玩家状态 - await Promise.all([ - this.redisService.sadd('plaza:online_players', playerId), - this.redisService.hset('plaza:player_positions', playerId, JSON.stringify(playerInfo)), - this.redisService.hset('plaza:player_sockets', playerId, socketId), - this.redisService.expire('plaza:player_positions', 3600), // 1小时过期 - this.redisService.expire('plaza:player_sockets', 3600) - ]); - - // 6. 向其他玩家广播新玩家进入消息 - this.webSocketGateway.broadcastToPlaza('player_entered', { - playerId: player.id, - nickname: player.nickname, - avatarSkin: player.avatarSkin, - position: initialPosition - }, socketId); // 排除新进入的玩家 - - // 7. 向新玩家发送当前广场状态 - const allPlayers = await this.getAllPlazaPlayers(); - this.webSocketGateway.sendToPlayer(socketId, 'plaza_state', { - players: allPlayers.filter(p => p.playerId !== playerId), - totalPlayers: allPlayers.length - }); - - const duration = Date.now() - startTime; - - this.logger.info('玩家成功进入广场', { - operation: 'enterPlaza', - playerId, - socketId, - position: initialPosition, - totalPlayers: currentPlayers + 1, - duration, - timestamp: new Date().toISOString() - }); - - return playerInfo; - - } catch (error) { - const duration = Date.now() - startTime; - - if (error instanceof UnauthorizedException || - error instanceof BadRequestException) { - throw error; - } - - this.logger.error('玩家进入广场系统异常', { - operation: 'enterPlaza', - playerId, - socketId, - error: error.message, - stack: error.stack, - duration, - timestamp: new Date().toISOString() - }); - - throw new InternalServerErrorException('进入广场失败,请稍后重试'); - } - } - - /** - * 生成初始位置 - * - * 功能描述: - * 在广场中心附近生成随机的初始位置,避免玩家重叠 - * - * @returns Position 包含x、y坐标的位置对象 - * @private - */ - private generateInitialPosition(): Position { - // 广场中心坐标 (400, 300),在半径100像素范围内随机分配 - const centerX = 400; - const centerY = 300; - const radius = 100; - - const angle = Math.random() * 2 * Math.PI; - const distance = Math.random() * radius; - - const x = Math.round(centerX + distance * Math.cos(angle)); - const y = Math.round(centerY + distance * Math.sin(angle)); - - return { x, y }; - } - - /** - * 获取所有广场玩家信息 - * - * @returns Promise 广场中所有玩家的信息列表 - * @private - */ - private async getAllPlazaPlayers(): Promise { - try { - const playerIds = await this.redisService.smembers('plaza:online_players'); - const playerInfos = await Promise.all( - playerIds.map(async (playerId) => { - const info = await this.redisService.hget('plaza:player_positions', playerId); - return info ? JSON.parse(info) : null; - }) - ); - - return playerInfos.filter(info => info !== null); - } catch (error) { - this.logger.error('获取广场玩家列表失败', { - operation: 'getAllPlazaPlayers', - error: error.message - }); - return []; - } - } -} -``` - ---- - -## 七、工具和配置 - -### 7.1 推荐的开发工具 - -| 工具 | 用途 | 配置说明 | -|------|------|---------| -| **ESLint** | 代码规范检查 | 配置注释规范检查规则 | -| **Prettier** | 代码格式化 | 统一代码格式 | -| **TSDoc** | 文档生成 | 从注释生成API文档 | -| **SonarQube** | 代码质量分析 | 检查代码覆盖率和复杂度 | - -### 7.2 日志配置示例 - -```typescript -// logger.config.ts -export const loggerConfig = { - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.errors({ stack: true }), - winston.format.json() - ), - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: 'logs/error.log', - level: 'error' - }), - new winston.transports.File({ - filename: 'logs/combined.log' - }) - ] -}; -``` - ---- - -## 八、总结 - -本规范文档定义了后端开发的核心要求: - -1. **完整的注释体系**:模块、类、方法三级注释,确保代码可读性 -2. **全面的业务逻辑**:考虑所有可能情况,实现防御性编程 -3. **规范的日志记录**:关键操作必须记录,便于问题排查和系统监控 -4. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验 - -遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。 \ No newline at end of file diff --git a/full_diagnosis.js b/full_diagnosis.js deleted file mode 100644 index ec6a85d..0000000 --- a/full_diagnosis.js +++ /dev/null @@ -1,311 +0,0 @@ -const io = require('socket.io-client'); -const https = require('https'); -const http = require('http'); - -console.log('🔍 全面WebSocket连接诊断'); -console.log('='.repeat(60)); - -// 1. 测试基础网络连接 -async function testBasicConnection() { - console.log('\n1️⃣ 测试基础HTTPS连接...'); - - return new Promise((resolve) => { - const options = { - hostname: 'whaletownend.xinghangee.icu', - port: 443, - path: '/', - method: 'GET', - timeout: 10000 - }; - - const req = https.request(options, (res) => { - console.log(`✅ HTTPS连接成功 - 状态码: ${res.statusCode}`); - console.log(`📋 服务器: ${res.headers.server || '未知'}`); - resolve({ success: true, statusCode: res.statusCode }); - }); - - req.on('error', (error) => { - console.log(`❌ HTTPS连接失败: ${error.message}`); - resolve({ success: false, error: error.message }); - }); - - req.on('timeout', () => { - console.log('❌ HTTPS连接超时'); - req.destroy(); - resolve({ success: false, error: 'timeout' }); - }); - - req.end(); - }); -} - -// 2. 测试本地服务器 -async function testLocalServer() { - console.log('\n2️⃣ 测试本地服务器...'); - - const testPaths = [ - 'http://localhost:3000/', - 'http://localhost:3000/socket.io/?EIO=4&transport=polling' - ]; - - for (const url of testPaths) { - console.log(`🧪 测试: ${url}`); - - await new Promise((resolve) => { - const urlObj = new URL(url); - const options = { - hostname: urlObj.hostname, - port: urlObj.port, - path: urlObj.pathname + urlObj.search, - method: 'GET', - timeout: 5000 - }; - - const req = http.request(options, (res) => { - console.log(` 状态码: ${res.statusCode}`); - if (res.statusCode === 200) { - console.log(' ✅ 本地服务器正常'); - } else { - console.log(` ⚠️ 本地服务器响应: ${res.statusCode}`); - } - resolve(); - }); - - req.on('error', (error) => { - console.log(` ❌ 本地服务器连接失败: ${error.message}`); - resolve(); - }); - - req.on('timeout', () => { - console.log(' ❌ 本地服务器超时'); - req.destroy(); - resolve(); - }); - - req.end(); - }); - } -} - -// 3. 测试远程Socket.IO路径 -async function testRemoteSocketIO() { - console.log('\n3️⃣ 测试远程Socket.IO路径...'); - - const testPaths = [ - '/socket.io/?EIO=4&transport=polling', - '/game/socket.io/?EIO=4&transport=polling', - '/socket.io/?transport=polling', - '/api/socket.io/?EIO=4&transport=polling' - ]; - - const results = []; - - for (const path of testPaths) { - console.log(`🧪 测试路径: ${path}`); - - const result = await new Promise((resolve) => { - const options = { - hostname: 'whaletownend.xinghangee.icu', - port: 443, - path: path, - method: 'GET', - timeout: 8000, - headers: { - 'User-Agent': 'socket.io-diagnosis' - } - }; - - const req = https.request(options, (res) => { - console.log(` 状态码: ${res.statusCode}`); - - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode === 200) { - console.log(' ✅ 路径可用'); - console.log(` 📄 响应: ${data.substring(0, 50)}...`); - } else { - console.log(` ❌ 路径不可用: ${res.statusCode}`); - } - resolve({ path, statusCode: res.statusCode, success: res.statusCode === 200 }); - }); - }); - - req.on('error', (error) => { - console.log(` ❌ 请求失败: ${error.message}`); - resolve({ path, error: error.message, success: false }); - }); - - req.on('timeout', () => { - console.log(' ❌ 请求超时'); - req.destroy(); - resolve({ path, error: 'timeout', success: false }); - }); - - req.end(); - }); - - results.push(result); - } - - return results; -} - -// 4. 测试Socket.IO客户端连接 -async function testSocketIOClient() { - console.log('\n4️⃣ 测试Socket.IO客户端连接...'); - - const configs = [ - { - name: 'HTTPS + 所有传输方式', - url: 'https://whaletownend.xinghangee.icu', - options: { transports: ['websocket', 'polling'], timeout: 10000 } - }, - { - name: 'HTTPS + 仅Polling', - url: 'https://whaletownend.xinghangee.icu', - options: { transports: ['polling'], timeout: 10000 } - }, - { - name: 'HTTPS + /game namespace', - url: 'https://whaletownend.xinghangee.icu/game', - options: { transports: ['polling'], timeout: 10000 } - } - ]; - - const results = []; - - for (const config of configs) { - console.log(`🧪 测试: ${config.name}`); - console.log(` URL: ${config.url}`); - - const result = await new Promise((resolve) => { - const socket = io(config.url, config.options); - let resolved = false; - - const timeout = setTimeout(() => { - if (!resolved) { - resolved = true; - socket.disconnect(); - console.log(' ❌ 连接超时'); - resolve({ success: false, error: 'timeout' }); - } - }, config.options.timeout); - - socket.on('connect', () => { - if (!resolved) { - resolved = true; - clearTimeout(timeout); - console.log(' ✅ 连接成功'); - console.log(` 📡 Socket ID: ${socket.id}`); - console.log(` 🚀 传输方式: ${socket.io.engine.transport.name}`); - socket.disconnect(); - resolve({ success: true, transport: socket.io.engine.transport.name }); - } - }); - - socket.on('connect_error', (error) => { - if (!resolved) { - resolved = true; - clearTimeout(timeout); - console.log(` ❌ 连接失败: ${error.message}`); - resolve({ success: false, error: error.message }); - } - }); - }); - - results.push({ config: config.name, ...result }); - - // 等待1秒再测试下一个 - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - return results; -} - -// 5. 检查DNS解析 -async function testDNS() { - console.log('\n5️⃣ 检查DNS解析...'); - - const dns = require('dns'); - - return new Promise((resolve) => { - dns.lookup('whaletownend.xinghangee.icu', (err, address, family) => { - if (err) { - console.log(`❌ DNS解析失败: ${err.message}`); - resolve({ success: false, error: err.message }); - } else { - console.log(`✅ DNS解析成功: ${address} (IPv${family})`); - resolve({ success: true, address, family }); - } - }); - }); -} - -// 主诊断函数 -async function runFullDiagnosis() { - console.log('开始全面诊断...\n'); - - try { - const dnsResult = await testDNS(); - const basicResult = await testBasicConnection(); - await testLocalServer(); - const socketIOPaths = await testRemoteSocketIO(); - const clientResults = await testSocketIOClient(); - - console.log('\n' + '='.repeat(60)); - console.log('📊 诊断结果汇总'); - console.log('='.repeat(60)); - - console.log(`1. DNS解析: ${dnsResult.success ? '✅ 正常' : '❌ 失败'}`); - if (dnsResult.address) { - console.log(` IP地址: ${dnsResult.address}`); - } - - console.log(`2. HTTPS连接: ${basicResult.success ? '✅ 正常' : '❌ 失败'}`); - if (basicResult.error) { - console.log(` 错误: ${basicResult.error}`); - } - - const workingPaths = socketIOPaths.filter(r => r.success); - console.log(`3. Socket.IO路径: ${workingPaths.length}/${socketIOPaths.length} 个可用`); - workingPaths.forEach(p => { - console.log(` ✅ ${p.path}`); - }); - - const workingClients = clientResults.filter(r => r.success); - console.log(`4. Socket.IO客户端: ${workingClients.length}/${clientResults.length} 个成功`); - workingClients.forEach(c => { - console.log(` ✅ ${c.config} (${c.transport})`); - }); - - console.log('\n💡 建议:'); - - if (!dnsResult.success) { - console.log('❌ DNS解析失败 - 检查域名配置'); - } else if (!basicResult.success) { - console.log('❌ 基础HTTPS连接失败 - 检查服务器状态和防火墙'); - } else if (workingPaths.length === 0) { - console.log('❌ 所有Socket.IO路径都不可用 - 检查nginx配置和后端服务'); - } else if (workingClients.length === 0) { - console.log('❌ Socket.IO客户端无法连接 - 可能是CORS或协议问题'); - } else { - console.log('✅ 部分功能正常 - 使用可用的配置继续开发'); - - if (workingClients.length > 0) { - const bestConfig = workingClients.find(c => c.transport === 'websocket') || workingClients[0]; - console.log(`💡 推荐使用: ${bestConfig.config}`); - } - } - - } catch (error) { - console.error('诊断过程中发生错误:', error); - } - - process.exit(0); -} - -runFullDiagnosis(); \ No newline at end of file diff --git a/test_zulip.js b/test_zulip.js deleted file mode 100644 index d58f7db..0000000 --- a/test_zulip.js +++ /dev/null @@ -1,131 +0,0 @@ -const io = require('socket.io-client'); - -// 使用用户 API Key 测试 Zulip 集成 -async function testWithUserApiKey() { - console.log('🚀 使用用户 API Key 测试 Zulip 集成...'); - console.log('📡 用户 API Key: lCPWCPfGh7WU...pqNfGF8'); - console.log('📡 Zulip 服务器: https://zulip.xinghangee.icu/'); - console.log('📡 游戏服务器: https://whaletownend.xinghangee.icu/game'); - - const socket = io('wss://whaletownend.xinghangee.icu/game', { - transports: ['websocket', 'polling'], // WebSocket优先,polling备用 - timeout: 20000, - forceNew: true, - reconnection: true, - reconnectionAttempts: 3, - reconnectionDelay: 1000 - }); - - let testStep = 0; - - socket.on('connect', () => { - console.log('✅ WebSocket 连接成功'); - testStep = 1; - - // 使用包含用户 API Key 的 token - const loginMessage = { - type: 'login', - token: 'lCPWCPfGh7...fGF8_user_token' - }; - - console.log('📤 步骤 1: 发送登录消息(使用用户 API Key)'); - socket.emit('login', loginMessage); - }); - - socket.on('login_success', (data) => { - console.log('✅ 步骤 1 完成: 登录成功'); - console.log(' 会话ID:', data.sessionId); - console.log(' 用户ID:', data.userId); - console.log(' 用户名:', data.username); - console.log(' 当前地图:', data.currentMap); - testStep = 2; - - // 等待 Zulip 客户端初始化 - console.log('⏳ 等待 3 秒让 Zulip 客户端初始化...'); - setTimeout(() => { - const chatMessage = { - t: 'chat', - content: '🎮 【用户API Key测试】来自游戏的消息!\\n' + - '时间: ' + new Date().toLocaleString() + '\\n' + - '使用用户 API Key 发送此消息。', - scope: 'local' - }; - - console.log('📤 步骤 2: 发送消息到 Zulip(使用用户 API Key)'); - console.log(' 目标 Stream: Whale Port'); - socket.emit('chat', chatMessage); - }, 3000); - }); - - socket.on('chat_sent', (data) => { - console.log('✅ 步骤 2 完成: 消息发送成功'); - console.log(' 响应:', JSON.stringify(data, null, 2)); - - // 只在第一次收到 chat_sent 时发送第二条消息 - if (testStep === 2) { - testStep = 3; - - setTimeout(() => { - // 先切换到 Pumpkin Valley 地图 - console.log('📤 步骤 3: 切换到 Pumpkin Valley 地图'); - const positionUpdate = { - t: 'position', - x: 150, - y: 400, - mapId: 'pumpkin_valley' - }; - socket.emit('position_update', positionUpdate); - - // 等待位置更新后发送消息 - setTimeout(() => { - const chatMessage2 = { - t: 'chat', - content: '🎃 在南瓜谷发送的测试消息!', - scope: 'local' - }; - - console.log('📤 步骤 4: 在 Pumpkin Valley 发送消息'); - socket.emit('chat', chatMessage2); - }, 1000); - }, 2000); - } - }); - - socket.on('chat_render', (data) => { - console.log('📨 收到来自 Zulip 的消息:'); - console.log(' 发送者:', data.from); - console.log(' 内容:', data.txt); - console.log(' Stream:', data.stream || '未知'); - console.log(' Topic:', data.topic || '未知'); - }); - - socket.on('error', (error) => { - console.log('❌ 收到错误:', JSON.stringify(error, null, 2)); - }); - - socket.on('disconnect', () => { - console.log('🔌 WebSocket 连接已关闭'); - console.log(''); - console.log('📊 测试结果:'); - console.log(' 完成步骤:', testStep, '/ 4'); - if (testStep >= 3) { - console.log(' ✅ 核心功能正常!'); - console.log(' 💡 请检查 Zulip 中的 "Whale Port" 和 "Pumpkin Valley" Streams 查看消息'); - } - process.exit(0); - }); - - socket.on('connect_error', (error) => { - console.error('❌ 连接错误:', error.message); - process.exit(1); - }); - - // 20秒后自动关闭(给足够时间完成测试) - setTimeout(() => { - console.log('⏰ 测试时间到,关闭连接'); - socket.disconnect(); - }, 20000); -} - -console.log('🔧 准备测试环境...'); -testWithUserApiKey().catch(console.error); \ No newline at end of file diff --git a/test_zulip_registration.js b/test_zulip_registration.js deleted file mode 100644 index 132b6a0..0000000 --- a/test_zulip_registration.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Zulip用户注册真实环境测试脚本 - * - * 功能描述: - * - 测试Zulip用户注册功能在真实环境下的表现 - * - 验证API调用是否正常工作 - * - 检查配置是否正确 - * - * 使用方法: - * node test_zulip_registration.js - * - * @author angjustinl - * @version 1.0.0 - * @since 2025-01-06 - */ - -const https = require('https'); -const { URLSearchParams } = require('url'); - -// 配置信息 -const config = { - zulipServerUrl: 'https://zulip.xinghangee.icu', - zulipBotEmail: 'angjustinl@mail.angforever.top', - zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8', -}; - -/** - * 检查用户是否存在 - */ -async function checkUserExists(email) { - console.log(`🔍 检查用户是否存在: ${email}`); - - try { - const url = `${config.zulipServerUrl}/api/v1/users`; - const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Basic ${auth}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`); - return false; - } - - const data = await response.json(); - console.log(`📊 获取到 ${data.members?.length || 0} 个用户`); - - if (data.members && Array.isArray(data.members)) { - const userExists = data.members.some(user => - user.email && user.email.toLowerCase() === email.toLowerCase() - ); - - console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`); - return userExists; - } - - return false; - - } catch (error) { - console.error(`❌ 检查用户存在性失败:`, error.message); - return false; - } -} - -/** - * 创建测试用户 - */ -async function createTestUser(email, fullName, password) { - console.log(`🚀 开始创建用户: ${email}`); - - try { - const url = `${config.zulipServerUrl}/api/v1/users`; - const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); - - const requestBody = new URLSearchParams(); - requestBody.append('email', email); - requestBody.append('full_name', fullName); - - if (password) { - requestBody.append('password', password); - } - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Authorization': `Basic ${auth}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: requestBody.toString(), - }); - - const data = await response.json(); - - if (!response.ok) { - console.log(`❌ 用户创建失败: ${response.status} ${response.statusText}`); - console.log(`📝 错误信息: ${data.msg || data.message || '未知错误'}`); - return { success: false, error: data.msg || data.message }; - } - - console.log(`✅ 用户创建成功! 用户ID: ${data.user_id}`); - return { success: true, userId: data.user_id }; - - } catch (error) { - console.error(`❌ 创建用户异常:`, error.message); - return { success: false, error: error.message }; - } -} - -/** - * 测试连接 - */ -async function testConnection() { - console.log('🔗 测试Zulip服务器连接...'); - - try { - const url = `${config.zulipServerUrl}/api/v1/server_settings`; - const response = await fetch(url); - - if (response.ok) { - const data = await response.json(); - console.log(`✅ 连接成功! 服务器版本: ${data.zulip_version || '未知'}`); - return true; - } else { - console.log(`❌ 连接失败: ${response.status} ${response.statusText}`); - return false; - } - } catch (error) { - console.error(`❌ 连接异常:`, error.message); - return false; - } -} - -/** - * 主测试函数 - */ -async function main() { - console.log('🎯 开始Zulip用户注册测试'); - console.log('=' * 50); - - // 1. 测试连接 - const connected = await testConnection(); - if (!connected) { - console.log('❌ 无法连接到Zulip服务器,测试终止'); - return; - } - - console.log(''); - - // 2. 生成测试用户信息 - const timestamp = Date.now(); - const testEmail = `test_user_${timestamp}@example.com`; - const testFullName = `Test User ${timestamp}`; - const testPassword = 'test123456'; - - console.log(`📋 测试用户信息:`); - console.log(` 邮箱: ${testEmail}`); - console.log(` 姓名: ${testFullName}`); - console.log(` 密码: ${testPassword}`); - console.log(''); - - // 3. 检查用户是否已存在 - const userExists = await checkUserExists(testEmail); - if (userExists) { - console.log('⚠️ 用户已存在,跳过创建测试'); - return; - } - - console.log(''); - - // 4. 创建用户 - const createResult = await createTestUser(testEmail, testFullName, testPassword); - - console.log(''); - console.log('📊 测试结果:'); - if (createResult.success) { - console.log('✅ 用户注册功能正常工作'); - console.log(` 新用户ID: ${createResult.userId}`); - } else { - console.log('❌ 用户注册功能存在问题'); - console.log(` 错误信息: ${createResult.error}`); - } - - console.log(''); - console.log('🎉 测试完成'); -} - -// 运行测试 -main().catch(error => { - console.error('💥 测试过程中发生未处理的错误:', error); - process.exit(1); -}); \ No newline at end of file diff --git a/test_zulip_user_management.js b/test_zulip_user_management.js deleted file mode 100644 index dd14baf..0000000 --- a/test_zulip_user_management.js +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Zulip用户管理真实环境测试脚本 - * - * 功能描述: - * - 测试Zulip用户管理功能在真实环境下的表现 - * - 验证用户查询、验证等API调用是否正常工作 - * - 检查配置是否正确 - * - * 使用方法: - * node test_zulip_user_management.js - * - * @author angjustinl - * @version 1.0.0 - * @since 2025-01-06 - */ - -const https = require('https'); - -// 配置信息 -const config = { - zulipServerUrl: 'https://zulip.xinghangee.icu', - zulipBotEmail: 'angjustinl@mail.angforever.top', - zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8', -}; - -/** - * 获取所有用户列表 - */ -async function getAllUsers() { - console.log('📋 获取所有用户列表...'); - - try { - const url = `${config.zulipServerUrl}/api/v1/users`; - const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Basic ${auth}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`); - return { success: false, error: `${response.status} ${response.statusText}` }; - } - - const data = await response.json(); - const users = data.members?.map(user => ({ - userId: user.user_id, - email: user.email, - fullName: user.full_name, - isActive: user.is_active, - isAdmin: user.is_admin, - isBot: user.is_bot, - })) || []; - - console.log(`✅ 成功获取 ${users.length} 个用户`); - - // 显示前几个用户信息 - console.log('👥 用户列表预览:'); - users.slice(0, 5).forEach((user, index) => { - console.log(` ${index + 1}. ${user.fullName} (${user.email})`); - console.log(` ID: ${user.userId}, 活跃: ${user.isActive}, 管理员: ${user.isAdmin}, 机器人: ${user.isBot}`); - }); - - if (users.length > 5) { - console.log(` ... 还有 ${users.length - 5} 个用户`); - } - - return { success: true, users, totalCount: users.length }; - - } catch (error) { - console.error(`❌ 获取用户列表异常:`, error.message); - return { success: false, error: error.message }; - } -} - -/** - * 检查指定用户是否存在 - */ -async function checkUserExists(email) { - console.log(`🔍 检查用户是否存在: ${email}`); - - try { - const usersResult = await getAllUsers(); - if (!usersResult.success) { - console.log(`❌ 无法获取用户列表: ${usersResult.error}`); - return false; - } - - const userExists = usersResult.users.some(user => - user.email.toLowerCase() === email.toLowerCase() - ); - - console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`); - return userExists; - - } catch (error) { - console.error(`❌ 检查用户存在性失败:`, error.message); - return false; - } -} - -/** - * 获取用户详细信息 - */ -async function getUserInfo(email) { - console.log(`📝 获取用户信息: ${email}`); - - try { - const usersResult = await getAllUsers(); - if (!usersResult.success) { - console.log(`❌ 无法获取用户列表: ${usersResult.error}`); - return { success: false, error: usersResult.error }; - } - - const user = usersResult.users.find(u => - u.email.toLowerCase() === email.toLowerCase() - ); - - if (!user) { - console.log(`❌ 用户不存在: ${email}`); - return { success: false, error: '用户不存在' }; - } - - console.log(`✅ 用户信息获取成功:`); - console.log(` 用户ID: ${user.userId}`); - console.log(` 邮箱: ${user.email}`); - console.log(` 姓名: ${user.fullName}`); - console.log(` 状态: ${user.isActive ? '活跃' : '非活跃'}`); - console.log(` 权限: ${user.isAdmin ? '管理员' : '普通用户'}`); - console.log(` 类型: ${user.isBot ? '机器人' : '真实用户'}`); - - return { success: true, user }; - - } catch (error) { - console.error(`❌ 获取用户信息失败:`, error.message); - return { success: false, error: error.message }; - } -} - -/** - * 测试用户API Key - */ -async function testUserApiKey(email, apiKey) { - console.log(`🔑 测试用户API Key: ${email}`); - - try { - const url = `${config.zulipServerUrl}/api/v1/users/me`; - const auth = Buffer.from(`${email}:${apiKey}`).toString('base64'); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Basic ${auth}`, - 'Content-Type': 'application/json', - }, - }); - - const isValid = response.ok; - - if (isValid) { - const data = await response.json(); - console.log(`✅ API Key有效! 用户信息:`); - console.log(` 用户ID: ${data.user_id}`); - console.log(` 邮箱: ${data.email}`); - console.log(` 姓名: ${data.full_name}`); - } else { - console.log(`❌ API Key无效: ${response.status} ${response.statusText}`); - } - - return isValid; - - } catch (error) { - console.error(`❌ 测试API Key异常:`, error.message); - return false; - } -} - -/** - * 测试连接 - */ -async function testConnection() { - console.log('🔗 测试Zulip服务器连接...'); - - try { - const url = `${config.zulipServerUrl}/api/v1/server_settings`; - const response = await fetch(url); - - if (response.ok) { - const data = await response.json(); - console.log(`✅ 连接成功! 服务器信息:`); - console.log(` 版本: ${data.zulip_version || '未知'}`); - console.log(` 服务器: ${data.realm_name || '未知'}`); - return true; - } else { - console.log(`❌ 连接失败: ${response.status} ${response.statusText}`); - return false; - } - } catch (error) { - console.error(`❌ 连接异常:`, error.message); - return false; - } -} - -/** - * 主测试函数 - */ -async function main() { - console.log('🎯 开始Zulip用户管理测试'); - console.log('='.repeat(50)); - - // 1. 测试连接 - const connected = await testConnection(); - if (!connected) { - console.log('❌ 无法连接到Zulip服务器,测试终止'); - return; - } - - console.log(''); - - // 2. 获取所有用户列表 - const usersResult = await getAllUsers(); - if (!usersResult.success) { - console.log('❌ 无法获取用户列表,测试终止'); - return; - } - - console.log(''); - - // 3. 测试用户存在性检查 - const testEmails = [ - 'angjustinl@mail.angforever.top', // 应该存在 - 'nonexistent@example.com', // 应该不存在 - ]; - - console.log('🔍 测试用户存在性检查:'); - for (const email of testEmails) { - const exists = await checkUserExists(email); - console.log(` ${email}: ${exists ? '✅ 存在' : '❌ 不存在'}`); - } - - console.log(''); - - // 4. 测试获取用户信息 - console.log('📝 测试获取用户信息:'); - const existingEmail = 'angjustinl@mail.angforever.top'; - const userInfoResult = await getUserInfo(existingEmail); - - console.log(''); - - // 5. 测试API Key验证(如果有的话) - console.log('🔑 测试API Key验证:'); - const testApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8'; // 这是我们的测试API Key - const apiKeyValid = await testUserApiKey(existingEmail, testApiKey); - - console.log(''); - console.log('📊 测试结果总结:'); - console.log(`✅ 服务器连接: 正常`); - console.log(`✅ 用户列表获取: 正常 (${usersResult.totalCount} 个用户)`); - console.log(`✅ 用户存在性检查: 正常`); - console.log(`✅ 用户信息获取: ${userInfoResult.success ? '正常' : '异常'}`); - console.log(`✅ API Key验证: ${apiKeyValid ? '正常' : '异常'}`); - - console.log(''); - console.log('🎉 用户管理功能测试完成'); -} - -// 运行测试 -main().catch(error => { - console.error('💥 测试过程中发生未处理的错误:', error); - process.exit(1); -}); \ No newline at end of file -- 2.25.1