Files
whale-town-end/src/gateway/zulip/websocket_openapi.controller.ts
moyin ed04b8c92d docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md
- 补充对外提供的接口章节(14个公共方法)
- 添加使用的项目内部依赖说明(7个依赖)
- 完善核心特性描述(5个特性)
- 添加潜在风险评估(4个风险及缓解措施)
- 优化文档结构和内容完整性
2026-01-15 10:53:04 +08:00

839 lines
21 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.
/**
* 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'
}
};
}
}