feat(gateway/chat): 新增聊天网关模块

范围:src/gateway/chat/
- 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信
- 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口
- 新增 ChatGatewayModule 模块配置,整合网关层组件
- 新增请求/响应 DTO 定义,提供数据验证和类型约束
- 新增完整的单元测试覆盖
- 新增模块 README 文档,包含接口说明、核心特性和风险评估
This commit is contained in:
moyin
2026-01-14 19:11:25 +08:00
parent 3f3c29354e
commit 5bcf3cb678
8 changed files with 1702 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
/**
* 聊天 HTTP 控制器
*
* 功能描述:
* - 处理聊天相关的 REST API 请求
* - 只做协议转换,不包含业务逻辑
* - 提供聊天历史查询和系统状态接口
*
* 架构层级Gateway Layer网关层
*
* 最近修改:
* - 2026-01-14: 代码规范优化 - 处理未使用的参数 (修改者: moyin)
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
*
* @author moyin
* @version 1.0.2
* @since 2026-01-14
* @lastModified 2026-01-14
*/
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 { ChatService } from '../../business/chat/chat.service';
import { ChatWebSocketGateway } from './chat.gateway';
import { SendChatMessageDto, GetChatHistoryDto } from './chat.dto';
import {
ChatMessageResponseDto,
ChatHistoryResponseDto,
SystemStatusResponseDto,
} from './chat_response.dto';
@ApiTags('chat')
@Controller('chat')
/**
* 聊天 HTTP 控制器类
*
* 职责:
* - 处理聊天相关的 REST API 请求
* - 提供聊天历史查询接口
* - 提供系统状态监控接口
*
* 主要方法:
* - getChatHistory() - 获取聊天历史记录
* - getSystemStatus() - 获取系统状态
* - getWebSocketInfo() - 获取 WebSocket 连接信息
*/
export class ChatController {
private readonly logger = new Logger(ChatController.name);
constructor(
private readonly chatService: ChatService,
private readonly websocketGateway: ChatWebSocketGateway,
) {}
/**
* 发送聊天消息REST API 方式)
*
* @param dto 发送消息请求参数
* @returns 消息发送响应
* @throws HttpException 聊天消息需要通过 WebSocket 发送
*/
@Post('send')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: '发送聊天消息',
description: '通过 REST API 发送聊天消息。推荐使用 WebSocket 接口以获得更好的实时性。'
})
@ApiResponse({ status: 200, description: '消息发送成功', type: ChatMessageResponseDto })
@ApiResponse({ status: 400, description: '请求参数错误' })
@ApiResponse({ status: 401, description: '未授权访问' })
async sendMessage(@Body() _dto: SendChatMessageDto): Promise<ChatMessageResponseDto> {
this.logger.log('收到REST API聊天消息发送请求');
// REST API 没有 WebSocket 连接,提示使用 WebSocket
throw new HttpException(
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口wss://whaletownend.xinghangee.icu/game',
HttpStatus.BAD_REQUEST,
);
}
/**
* 获取聊天历史记录
*
* @param query 查询参数mapId, limit, offset
* @returns 聊天历史响应
* @throws HttpException 获取失败时抛出异常
*/
@Get('history')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: '获取聊天历史记录' })
@ApiQuery({ name: 'mapId', required: false, description: '地图ID' })
@ApiQuery({ name: 'limit', required: false, description: '消息数量限制' })
@ApiQuery({ name: 'offset', required: false, description: '偏移量' })
@ApiResponse({ status: 200, description: '获取成功', type: ChatHistoryResponseDto })
async getChatHistory(@Query() query: GetChatHistoryDto): Promise<ChatHistoryResponseDto> {
this.logger.log('获取聊天历史记录', { mapId: query.mapId });
try {
const result = await this.chatService.getChatHistory(query);
return result;
} catch (error) {
this.logger.error('获取聊天历史失败', error);
throw new HttpException('获取聊天历史失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 获取系统状态
*
* @returns 系统状态响应WebSocket连接数、Zulip状态、内存使用等
* @throws HttpException 获取失败时抛出异常
*/
@Get('status')
@ApiOperation({ summary: '获取聊天系统状态' })
@ApiResponse({ status: 200, description: '获取成功', type: SystemStatusResponseDto })
async getSystemStatus(): Promise<SystemStatusResponseDto> {
try {
const totalConnections = this.websocketGateway.getConnectionCount();
const authenticatedConnections = this.websocketGateway.getAuthenticatedConnectionCount();
const mapPlayerCounts = 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,
},
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) {
this.logger.error('获取系统状态失败', error);
throw new HttpException('获取系统状态失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 获取 WebSocket 连接信息
*
* @returns WebSocket 连接配置信息
*/
@Get('websocket/info')
@ApiOperation({ summary: '获取 WebSocket 连接信息' })
async getWebSocketInfo() {
return {
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
protocol: 'native-websocket',
path: '/game',
supportedEvents: ['login', 'chat', 'position'],
supportedResponses: [
'connected', 'login_success', 'login_error',
'chat_sent', 'chat_error', 'chat_render', 'error'
],
authRequired: true,
tokenType: 'JWT',
};
}
}