/** * WebSocket OpenAPI 文档控制器 * * 功能描述: * - 专门用于在OpenAPI/Swagger中展示WebSocket接口 * - 通过REST API的方式描述WebSocket的消息格式和交互流程 * - 提供WebSocket连接信息和测试工具推荐 * * 架构定位: * - 层级:Gateway层(网关层) * - 职责:HTTP协议处理、OpenAPI文档暴露 * - 依赖:无业务逻辑依赖,纯文档展示 * * 职责分离: * - 文档展示:在Swagger中展示WebSocket消息格式 * - 连接信息:提供WebSocket连接配置和认证信息 * - 消息流程:展示WebSocket消息交互流程 * - 测试工具:提供测试工具推荐和示例代码 * * 最近修改: * - 2026-01-14: 架构优化 - 从Business层迁移到Gateway层,符合四层架构规范 (修改者: moyin) * - 2026-01-14: 代码规范优化 - 完善文件头注释和职责分离描述 (修改者: moyin) * - 2026-01-09: 功能新增 - 初始创建WebSocket OpenAPI文档控制器 (修改者: moyin) * * @author moyin * @version 2.0.0 * @since 2026-01-09 * @lastModified 2026-01-14 */ import { Controller, Get, Post, Body } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiProperty, ApiExtraModels, } from '@nestjs/swagger'; // WebSocket 消息格式 DTO class WebSocketLoginRequest { @ApiProperty({ description: '消息类型', example: 'login', enum: ['login'] }) type: string; @ApiProperty({ description: 'JWT认证令牌', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }) token: string; } class WebSocketLoginSuccessResponse { @ApiProperty({ description: '响应类型', example: 'login_success' }) t: string; @ApiProperty({ description: '会话ID', example: '89aff162-52d9-484e-9a35-036ba63a2280' }) sessionId: string; @ApiProperty({ description: '用户ID', example: 'user_123' }) userId: string; @ApiProperty({ description: '用户名', example: 'Player_123' }) username: string; @ApiProperty({ description: '当前地图', example: 'whale_port' }) currentMap: string; } class WebSocketChatRequest { @ApiProperty({ description: '消息类型', example: 'chat', enum: ['chat'] }) t: string; @ApiProperty({ description: '消息内容', example: '大家好!我刚进入游戏', maxLength: 1000 }) content: string; @ApiProperty({ description: '消息范围', example: 'local', enum: ['local', 'global'] }) scope: string; } class WebSocketChatResponse { @ApiProperty({ description: '响应类型', example: 'chat_render' }) t: string; @ApiProperty({ description: '发送者用户名', example: 'Player_456' }) from: string; @ApiProperty({ description: '消息内容', example: '欢迎新玩家!' }) txt: string; @ApiProperty({ description: '是否显示气泡', example: true }) bubble: boolean; @ApiProperty({ description: '消息范围', example: 'local' }) scope: string; @ApiProperty({ description: '地图ID(本地消息时)', example: 'whale_port', required: false }) mapId?: string; } class WebSocketPositionRequest { @ApiProperty({ description: '消息类型', example: 'position' }) t: string; @ApiProperty({ description: 'X坐标', example: 150 }) x: number; @ApiProperty({ description: 'Y坐标', example: 400 }) y: number; @ApiProperty({ description: '地图ID', example: 'whale_port' }) mapId: string; } class WebSocketErrorResponse { @ApiProperty({ description: '错误类型', example: 'error' }) type: string; @ApiProperty({ description: '错误消息', example: '请先登录' }) message: string; } @ApiTags('websocket') @ApiExtraModels( WebSocketLoginRequest, WebSocketLoginSuccessResponse, WebSocketChatRequest, WebSocketChatResponse, WebSocketPositionRequest, WebSocketErrorResponse ) @Controller('websocket-api') export class WebSocketOpenApiController { @Get('connection-info') @ApiOperation({ summary: 'WebSocket 连接信息', description: ` 获取WebSocket连接的基本信息和配置 **连接地址**: \`wss://whaletownend.xinghangee.icu/game\` **协议**: 原生WebSocket (非Socket.IO) **认证**: 需要JWT Token **架构更新**: - ✅ 已从Socket.IO迁移到原生WebSocket - ✅ 统一使用 /game 路径 - ✅ 支持地图房间管理 - ✅ 实现消息广播机制 **快速测试**: - 🧪 [WebSocket 测试页面](/websocket-test?from=api-docs) - 交互式测试工具 - 📚 [完整 API 文档](/api-docs) - 返回 Swagger 文档 ` }) @ApiResponse({ status: 200, description: 'WebSocket连接配置信息', schema: { type: 'object', properties: { url: { type: 'string', example: 'wss://whaletownend.xinghangee.icu/game', description: 'WebSocket服务器地址' }, protocol: { type: 'string', example: 'native-websocket', description: '使用原生WebSocket协议' }, authentication: { type: 'object', properties: { required: { type: 'boolean', example: true }, method: { type: 'string', example: 'JWT Token' }, tokenFormat: { type: 'object', properties: { issuer: { type: 'string', example: 'whale-town' }, audience: { type: 'string', example: 'whale-town-users' }, type: { type: 'string', example: 'access' } } } } }, supportedMaps: { type: 'array', items: { type: 'string' }, example: ['whale_port', 'pumpkin_valley', 'novice_village'] } } } }) getConnectionInfo() { return { url: 'wss://whaletownend.xinghangee.icu/game', protocol: 'native-websocket', path: '/game', port: { development: 3001, production: 'via_nginx_proxy' }, authentication: { required: true, method: 'JWT Token', tokenFormat: { issuer: 'whale-town', audience: 'whale-town-users', type: 'access', requiredFields: ['sub', 'username', 'email', 'role'] } }, supportedMaps: [ 'whale_port', 'pumpkin_valley', 'novice_village' ], features: [ '实时聊天', '位置同步', '地图房间管理', '消息广播', '连接状态监控', '自动重连支持' ], messageTypes: { clientToServer: [ { type: 'login', description: '用户登录认证', required: ['type', 'token'] }, { type: 'chat', description: '发送聊天消息', required: ['t', 'content', 'scope'] }, { type: 'position', description: '更新位置信息', required: ['t', 'x', 'y', 'mapId'] } ], serverToClient: [ { type: 'connected', description: '连接确认' }, { type: 'login_success', description: '登录成功' }, { type: 'login_error', description: '登录失败' }, { type: 'chat_render', description: '接收聊天消息' }, { type: 'position_update', description: '位置更新广播' }, { type: 'error', description: '通用错误消息' } ] }, connectionLimits: { maxConnections: 1000, sessionTimeout: 1800, // 30分钟 heartbeatInterval: 30000 // 30秒 } }; } @Post('login') @ApiOperation({ summary: '用户登录 (WebSocket消息格式)', description: ` **注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示 通过WebSocket发送此格式的消息来进行用户登录认证 **WebSocket连接后发送**: \`\`\`json { "type": "login", "token": "your_jwt_token_here" } \`\`\` **成功响应**: \`\`\`json { "t": "login_success", "sessionId": "uuid", "userId": "user_id", "username": "username", "currentMap": "whale_port" } \`\`\` ` }) @ApiResponse({ status: 200, description: '登录成功响应格式', type: WebSocketLoginSuccessResponse }) @ApiResponse({ status: 400, description: '登录失败响应格式', schema: { type: 'object', properties: { t: { type: 'string', example: 'login_error' }, message: { type: 'string', example: 'Token验证失败' } } } }) websocketLogin(@Body() loginRequest: WebSocketLoginRequest) { // 这个方法不会被实际调用,仅用于文档展示 return { note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息', websocketUrl: 'wss://whaletownend.xinghangee.icu/game', messageFormat: loginRequest }; } @Post('chat') @ApiOperation({ summary: '发送聊天消息 (WebSocket消息格式)', description: ` **注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示 通过WebSocket发送聊天消息的格式 **发送消息**: \`\`\`json { "t": "chat", "content": "消息内容", "scope": "local" } \`\`\` **接收消息**: \`\`\`json { "t": "chat_render", "from": "发送者", "txt": "消息内容", "bubble": true, "scope": "local", "mapId": "whale_port" } \`\`\` **消息范围说明**: - \`local\`: 仅当前地图的玩家可见 - \`global\`: 所有在线玩家可见 ` }) @ApiResponse({ status: 200, description: '聊天消息广播格式', type: WebSocketChatResponse }) @ApiResponse({ status: 400, description: '发送失败响应', schema: { type: 'object', properties: { t: { type: 'string', example: 'chat_error' }, message: { type: 'string', example: '消息内容不能为空' } } } }) websocketChat(@Body() chatRequest: WebSocketChatRequest) { return { note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息', websocketUrl: 'wss://whaletownend.xinghangee.icu/game', messageFormat: chatRequest }; } @Post('position') @ApiOperation({ summary: '位置更新 (WebSocket消息格式)', description: ` **注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示 更新玩家位置信息,支持地图切换 **发送格式**: \`\`\`json { "t": "position", "x": 150, "y": 400, "mapId": "whale_port" } \`\`\` **功能说明**: - 自动处理地图房间切换 - 向同地图其他玩家广播位置更新 - 支持实时位置同步 ` }) @ApiResponse({ status: 200, description: '位置更新成功,无特定响应消息' }) websocketPosition(@Body() positionRequest: WebSocketPositionRequest) { return { note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息', websocketUrl: 'wss://whaletownend.xinghangee.icu/game', messageFormat: positionRequest }; } @Get('message-flow') @ApiOperation({ summary: 'WebSocket 消息流程图', description: '展示WebSocket连接和消息交互的完整流程' }) @ApiResponse({ status: 200, description: 'WebSocket交互流程', schema: { type: 'object', properties: { connectionFlow: { type: 'array', items: { type: 'string' }, example: [ '1. 建立WebSocket连接到 wss://whaletownend.xinghangee.icu/game', '2. 发送login消息进行认证', '3. 接收login_success确认', '4. 发送chat/position消息进行交互', '5. 接收其他玩家的消息广播' ] } } } }) getMessageFlow() { return { connectionFlow: [ '1. 建立WebSocket连接到 wss://whaletownend.xinghangee.icu/game', '2. 发送login消息进行认证', '3. 接收login_success确认', '4. 发送chat/position消息进行交互', '5. 接收其他玩家的消息广播' ], messageTypes: { clientToServer: [ 'login - 用户登录认证', 'chat - 发送聊天消息', 'position - 更新位置信息' ], serverToClient: [ 'connected - 连接确认', 'login_success/login_error - 登录结果', 'chat_sent/chat_error - 消息发送结果', 'chat_render - 接收聊天消息', 'position_update - 位置更新广播', 'error - 通用错误消息' ] }, exampleSession: { step1: { action: '建立连接', client: 'new WebSocket("wss://whaletownend.xinghangee.icu/game")', server: '{"type":"connected","message":"连接成功","socketId":"ws_123"}' }, step2: { action: '用户登录', client: '{"type":"login","token":"jwt_token"}', server: '{"t":"login_success","sessionId":"uuid","userId":"user_123","username":"Player","currentMap":"whale_port"}' }, step3: { action: '发送消息', client: '{"t":"chat","content":"Hello!","scope":"local"}', server: '{"t":"chat_sent","messageId":137,"message":"消息发送成功"}' }, step4: { action: '接收广播', server: '{"t":"chat_render","from":"Player","txt":"Hello!","bubble":true,"scope":"local","mapId":"whale_port"}' } } }; } @Get('testing-tools') @ApiOperation({ summary: 'WebSocket 测试工具推荐', description: '推荐的WebSocket测试工具和示例代码' }) @ApiResponse({ status: 200, description: '测试工具和示例代码', }) getTestingTools() { return { onlineTools: [ { name: 'WebSocket King', url: 'https://websocketking.com/', description: '在线WebSocket测试工具,支持消息发送和接收' }, { name: 'WebSocket Test Client', url: 'https://www.websocket.org/echo.html', description: '简单的WebSocket回显测试' }, { name: '内置测试页面', url: '/websocket-test', description: '项目内置的WebSocket测试界面,支持完整功能测试' } ], codeExamples: { javascript: ` // JavaScript WebSocket 客户端示例 const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game'); ws.onopen = function() { console.log('连接成功'); // 发送登录消息 ws.send(JSON.stringify({ type: 'login', token: 'your_jwt_token_here' })); }; ws.onmessage = function(event) { const data = JSON.parse(event.data); console.log('收到消息:', data); if (data.t === 'login_success') { // 登录成功,发送聊天消息 ws.send(JSON.stringify({ t: 'chat', content: 'Hello from JavaScript!', scope: 'local' })); } }; ws.onclose = function(event) { console.log('连接关闭:', event.code, event.reason); }; ws.onerror = function(error) { console.error('WebSocket错误:', error); }; `, python: ` # Python WebSocket 客户端示例 import websocket import json import threading def on_message(ws, message): data = json.loads(message) print(f"收到消息: {data}") if data.get('t') == 'login_success': # 登录成功,发送聊天消息 ws.send(json.dumps({ 't': 'chat', 'content': 'Hello from Python!', 'scope': 'local' })) def on_error(ws, error): print(f"WebSocket错误: {error}") def on_close(ws, close_status_code, close_msg): print(f"连接关闭: {close_status_code} - {close_msg}") def on_open(ws): print("连接成功") # 发送登录消息 ws.send(json.dumps({ 'type': 'login', 'token': 'your_jwt_token_here' })) # 创建WebSocket连接 ws = websocket.WebSocketApp("wss://whaletownend.xinghangee.icu/game", on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open) # 启动连接 ws.run_forever() `, nodejs: ` // Node.js WebSocket 客户端示例 const WebSocket = require('ws'); const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game'); ws.on('open', function() { console.log('连接成功'); // 发送登录消息 ws.send(JSON.stringify({ type: 'login', token: 'your_jwt_token_here' })); }); ws.on('message', function(data) { const message = JSON.parse(data.toString()); console.log('收到消息:', message); if (message.t === 'login_success') { // 登录成功,发送聊天消息 ws.send(JSON.stringify({ t: 'chat', content: 'Hello from Node.js!', scope: 'local' })); } }); ws.on('close', function(code, reason) { console.log(\`连接关闭: \${code} - \${reason}\`); }); ws.on('error', function(error) { console.error('WebSocket错误:', error); }); ` }, testingSteps: [ '1. 访问测试页面: /websocket-test?from=api-docs', '2. 点击"🚀 一键测试"按钮自动完成所有步骤', '3. 或手动操作: 获取JWT Token → 连接 → 登录', '4. 发送chat消息测试聊天功能', '5. 发送position消息测试位置更新', '6. 观察其他客户端的消息广播' ], troubleshooting: { connectionFailed: [ '检查网络连接是否正常', '验证WebSocket服务器是否启动', '确认防火墙设置允许WebSocket连接', '检查SSL证书是否有效(WSS连接)' ], authenticationFailed: [ '验证JWT Token是否有效且未过期', '检查Token格式是否正确', '确认Token包含必需的字段(sub, username, email, role)', '验证Token的issuer和audience是否匹配' ], messageFailed: [ '确认已完成登录认证', '检查消息格式是否符合API规范', '验证必需字段是否都已提供', '检查消息内容是否符合长度限制' ] } }; } @Get('architecture') @ApiOperation({ summary: 'WebSocket 架构信息', description: '展示WebSocket服务的技术架构和实现细节' }) @ApiResponse({ status: 200, description: 'WebSocket架构信息', }) getArchitecture() { return { overview: { title: 'WebSocket 架构概览', description: '基于原生WebSocket的实时通信架构', version: '2.1.0', migrationFrom: 'Socket.IO', migrationDate: '2026-01-09' }, technicalStack: { server: { framework: 'NestJS', websocketLibrary: 'ws (原生WebSocket)', adapter: '@nestjs/platform-ws', port: 3001, path: '/game' }, proxy: { server: 'Nginx', sslTermination: true, loadBalancing: 'Single Instance', pathRouting: '/game -> localhost:3001' }, authentication: { method: 'JWT Bearer Token', validation: 'Real-time on connection', sessionManagement: 'In-memory with Redis backup' } }, features: { connectionManagement: { maxConnections: 1000, connectionPooling: true, automaticReconnection: 'Client-side', heartbeat: '30s interval' }, messaging: { messageTypes: ['login', 'chat', 'position'], messageRouting: 'Room-based (by map)', messageFiltering: 'Content and rate limiting', messageHistory: 'Not stored (real-time only)' }, roomManagement: { strategy: 'Map-based rooms', autoJoin: 'On position update', autoLeave: 'On disconnect or map change', broadcasting: 'Room-scoped and global' } }, performance: { latency: '< 50ms (local network)', throughput: '1000+ messages/second', memoryUsage: '~1MB per 100 connections', cpuUsage: 'Low (event-driven)' }, monitoring: { metrics: [ 'Active connections count', 'Messages per second', 'Authentication success rate', 'Error rate by type' ], logging: [ 'Connection events', 'Authentication attempts', 'Message routing', 'Error conditions' ], healthCheck: '/chat/status endpoint' }, security: { authentication: 'JWT Token validation', authorization: 'Role-based access control', rateLimit: 'Per-user message rate limiting', contentFilter: 'Sensitive word filtering', inputValidation: 'Message format validation' }, deployment: { environment: 'Production ready', scaling: 'Horizontal scaling supported', backup: 'Session state in Redis', monitoring: 'Integrated with application monitoring' } }; } }