/** * 聊天相关的 REST API 控制器 * * 功能描述: * - 提供聊天消息的 REST API 接口 * - 获取聊天历史记录 * - 查看系统状态和统计信息 * - 管理 WebSocket 连接状态 * * 职责分离: * - REST接口:提供HTTP方式的聊天功能访问 * - 状态查询:提供系统运行状态和统计信息 * - 文档支持:提供WebSocket API的使用文档 * - 监控支持:提供连接数和性能监控接口 * * 最近修改: * - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin) * * @author angjustinl * @version 1.0.1 * @since 2025-01-07 * @lastModified 2026-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/jwt_auth.guard'; import { ZulipService } from './zulip.service'; import { CleanWebSocketGateway } from './clean_websocket.gateway'; import { SendChatMessageDto, ChatMessageResponseDto, GetChatHistoryDto, ChatHistoryResponseDto, SystemStatusResponseDto, } from './chat.dto'; @ApiTags('chat') @Controller('chat') export class ChatController { private readonly logger = new Logger(ChatController.name); constructor( private readonly zulipService: ZulipService, private readonly websocketGateway: CleanWebSocketGateway, ) {} /** * 发送聊天消息(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 接口:wss://whaletownend.xinghangee.icu', 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 mapPlayerCounts = await this.websocketGateway.getMapPlayerCounts(); // 获取内存使用情况 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: mapPlayerCounts, }, 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: 'wss://whaletownend.xinghangee.icu/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: 'wss://whaletownend.xinghangee.icu/game', protocol: 'native-websocket', path: '/game', namespace: '/', supportedEvents: [ 'login', // 用户登录 'chat', // 发送聊天消息 'position', // 位置更新 ], supportedResponses: [ 'connected', // 连接确认 'login_success', // 登录成功 'login_error', // 登录失败 'chat_sent', // 消息发送成功 'chat_error', // 消息发送失败 'chat_render', // 接收到聊天消息 'error', // 通用错误 ], authRequired: true, tokenType: 'JWT', tokenFormat: { issuer: 'whale-town', audience: 'whale-town-users', type: 'access', requiredFields: ['sub', 'username', 'email', 'role'] }, documentation: '/api-docs', }; } }