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