forked from datawhale/whale-town-end
377 lines
10 KiB
TypeScript
377 lines
10 KiB
TypeScript
/**
|
||
* 聊天相关的 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 { ZulipWebSocketGateway } from './zulip_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: 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<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 接口: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<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 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',
|
||
};
|
||
}
|
||
} |