Files
whale-town-end/src/business/zulip/chat.controller.ts
moyin 73e3e0153c refactor(auth): 重构认证模块架构 - 将Gateway层组件从Business层分离
范围:src/gateway/auth/, src/business/auth/, src/app.module.ts
涉及文件:
- 新增:src/gateway/auth/ 目录及所有文件
- 移动:Controller、Guard、Decorator、DTO从business层移至gateway层
- 修改:src/business/auth/index.ts(移除Gateway层组件导出)
- 修改:src/app.module.ts(使用AuthGatewayModule替代AuthModule)

主要改进:
- 明确Gateway层和Business层的职责边界
- Controller、Guard、Decorator属于Gateway层职责
- Business层专注于业务逻辑和服务
- 符合分层架构设计原则
2026-01-14 13:07:11 +08:00

383 lines
11 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.
/**
* 聊天相关的 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 '../../gateway/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<ChatMessageResponseDto> {
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<ChatHistoryResponseDto> {
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<SystemStatusResponseDto> {
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', // 通用错误
],
quickLinks: {
testPage: '/websocket-test?from=chat-api',
apiDocs: '/api-docs',
connectionInfo: '/websocket-api/connection-info'
},
authRequired: true,
tokenType: 'JWT',
tokenFormat: {
issuer: 'whale-town',
audience: 'whale-town-users',
type: 'access',
requiredFields: ['sub', 'username', 'email', 'role']
},
documentation: '/api-docs',
};
}
}