Files
whale-town-end/src/gateway/chat/chat.controller.ts
moyin 5bcf3cb678 feat(gateway/chat): 新增聊天网关模块
范围:src/gateway/chat/
- 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信
- 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口
- 新增 ChatGatewayModule 模块配置,整合网关层组件
- 新增请求/响应 DTO 定义,提供数据验证和类型约束
- 新增完整的单元测试覆盖
- 新增模块 README 文档,包含接口说明、核心特性和风险评估
2026-01-14 19:11:25 +08:00

196 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 聊天 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',
};
}
}