docs:添加 WebSocket API 文档控制器

- 实现 /websocket/docs 接口提供完整的 WebSocket API 文档
- 实现 /websocket/message-examples 接口提供消息格式示例
- 包含连接配置、认证要求、事件格式说明
- 提供 JavaScript 和 Godot 客户端连接示例
- 包含故障排除指南和测试工具推荐
This commit is contained in:
moyin
2026-01-07 15:05:57 +08:00
parent d1fc396db7
commit a30ef52c5a

View File

@@ -0,0 +1,421 @@
/**
* WebSocket API 文档控制器
*
* 功能描述:
* - 提供 WebSocket API 的详细文档
* - 展示消息格式和事件类型
* - 提供连接示例和测试工具
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-07
*/
import { Controller, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('chat')
@Controller('websocket')
export class WebSocketDocsController {
/**
* 获取 WebSocket API 文档
*/
@Get('docs')
@ApiOperation({
summary: 'WebSocket API 文档',
description: '获取 WebSocket 连接和消息格式的详细文档'
})
@ApiResponse({
status: 200,
description: 'WebSocket API 文档',
schema: {
type: 'object',
properties: {
connection: {
type: 'object',
properties: {
url: {
type: 'string',
example: 'ws://localhost:3000/game',
description: 'WebSocket 连接地址'
},
namespace: {
type: 'string',
example: '/game',
description: 'Socket.IO 命名空间'
},
transports: {
type: 'array',
items: { type: 'string' },
example: ['websocket', 'polling'],
description: '支持的传输协议'
}
}
},
authentication: {
type: 'object',
properties: {
required: {
type: 'boolean',
example: true,
description: '是否需要认证'
},
method: {
type: 'string',
example: 'JWT Token',
description: '认证方式'
},
tokenFormat: {
type: 'object',
description: 'JWT Token 格式要求'
}
}
},
events: {
type: 'object',
description: '支持的事件和消息格式'
}
}
}
})
getWebSocketDocs() {
return {
connection: {
url: 'ws://localhost:3000/game',
namespace: '/game',
transports: ['websocket', 'polling'],
options: {
timeout: 20000,
forceNew: true,
reconnection: true,
reconnectionAttempts: 3,
reconnectionDelay: 1000
}
},
authentication: {
required: true,
method: 'JWT Token',
tokenFormat: {
issuer: 'whale-town',
audience: 'whale-town-users',
type: 'access',
requiredFields: ['sub', 'username', 'email', 'role'],
example: {
sub: 'user_123',
username: 'player_name',
email: 'user@example.com',
role: 'user',
type: 'access',
aud: 'whale-town-users',
iss: 'whale-town',
iat: 1767768599,
exp: 1768373399
}
}
},
events: {
clientToServer: {
login: {
description: '用户登录',
format: {
type: 'login',
token: 'JWT_TOKEN_HERE'
},
example: {
type: 'login',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
},
responses: ['login_success', 'login_error']
},
chat: {
description: '发送聊天消息',
format: {
t: 'chat',
content: 'string',
scope: 'local | global'
},
example: {
t: 'chat',
content: '大家好!我刚进入游戏',
scope: 'local'
},
responses: ['chat_sent', 'chat_error']
},
position_update: {
description: '更新玩家位置',
format: {
t: 'position',
x: 'number',
y: 'number',
mapId: 'string'
},
example: {
t: 'position',
x: 150,
y: 400,
mapId: 'whale_port'
},
responses: []
}
},
serverToClient: {
login_success: {
description: '登录成功响应',
format: {
t: 'login_success',
sessionId: 'string',
userId: 'string',
username: 'string',
currentMap: 'string'
},
example: {
t: 'login_success',
sessionId: '89aff162-52d9-484e-9a35-036ba63a2280',
userId: 'user_123',
username: 'Player_123',
currentMap: 'whale_port'
}
},
login_error: {
description: '登录失败响应',
format: {
t: 'login_error',
message: 'string'
},
example: {
t: 'login_error',
message: 'Token验证失败'
}
},
chat_sent: {
description: '消息发送成功确认',
format: {
t: 'chat_sent',
messageId: 'number',
message: 'string'
},
example: {
t: 'chat_sent',
messageId: 137,
message: '消息发送成功'
}
},
chat_error: {
description: '消息发送失败',
format: {
t: 'chat_error',
message: 'string'
},
example: {
t: 'chat_error',
message: '消息内容不能为空'
}
},
chat_render: {
description: '接收到聊天消息',
format: {
t: 'chat_render',
from: 'string',
txt: 'string',
bubble: 'boolean'
},
example: {
t: 'chat_render',
from: 'Player_456',
txt: '欢迎新玩家!',
bubble: true
}
}
}
},
maps: {
whale_port: {
name: 'Whale Port',
displayName: '鲸鱼港',
zulipStream: 'Whale Port',
description: '游戏的主要港口区域'
},
pumpkin_valley: {
name: 'Pumpkin Valley',
displayName: '南瓜谷',
zulipStream: 'Pumpkin Valley',
description: '充满南瓜的神秘山谷'
},
novice_village: {
name: 'Novice Village',
displayName: '新手村',
zulipStream: 'Novice Village',
description: '新玩家的起始区域'
}
},
examples: {
javascript: {
connection: `
// 使用 Socket.IO 客户端连接
const io = require('socket.io-client');
const socket = io('ws://localhost:3000/game', {
transports: ['websocket', 'polling'],
timeout: 20000,
forceNew: true,
reconnection: true,
reconnectionAttempts: 3,
reconnectionDelay: 1000
});
// 连接成功
socket.on('connect', () => {
console.log('连接成功:', socket.id);
// 发送登录消息
socket.emit('login', {
type: 'login',
token: 'YOUR_JWT_TOKEN_HERE'
});
});
// 登录成功
socket.on('login_success', (data) => {
console.log('登录成功:', data);
// 发送聊天消息
socket.emit('chat', {
t: 'chat',
content: '大家好!',
scope: 'local'
});
});
// 接收聊天消息
socket.on('chat_render', (data) => {
console.log('收到消息:', data.from, '说:', data.txt);
});
`,
godot: `
# Godot WebSocket 客户端示例
extends Node
var socket = WebSocketClient.new()
var url = "ws://localhost:3000/game"
func _ready():
socket.connect("connection_closed", self, "_closed")
socket.connect("connection_error", self, "_error")
socket.connect("connection_established", self, "_connected")
socket.connect("data_received", self, "_on_data")
var err = socket.connect_to_url(url)
if err != OK:
print("连接失败")
func _connected(protocol):
print("WebSocket 连接成功")
# 发送登录消息
var login_msg = {
"type": "login",
"token": "YOUR_JWT_TOKEN_HERE"
}
socket.get_peer(1).put_packet(JSON.print(login_msg).to_utf8())
func _on_data():
var packet = socket.get_peer(1).get_packet()
var message = JSON.parse(packet.get_string_from_utf8())
print("收到消息: ", message.result)
`
}
},
troubleshooting: {
commonIssues: [
{
issue: 'Token验证失败',
solution: '确保JWT Token包含正确的issuer、audience和type字段'
},
{
issue: '连接超时',
solution: '检查服务器是否运行,防火墙设置是否正确'
},
{
issue: '消息发送失败',
solution: '确保已经成功登录,消息内容不为空'
}
],
testTools: [
{
name: 'WebSocket King',
url: 'https://websocketking.com/',
description: '在线WebSocket测试工具'
},
{
name: 'Postman',
description: 'Postman也支持WebSocket连接测试'
}
]
}
};
}
/**
* 获取消息格式示例
*/
@Get('message-examples')
@ApiOperation({
summary: '消息格式示例',
description: '获取各种 WebSocket 消息的格式示例'
})
@ApiResponse({
status: 200,
description: '消息格式示例',
})
getMessageExamples() {
return {
login: {
request: {
type: 'login',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0X3VzZXJfMTIzIiwidXNlcm5hbWUiOiJ0ZXN0X3VzZXIiLCJlbWFpbCI6InRlc3RfdXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwidHlwZSI6ImFjY2VzcyIsImF1ZCI6IndoYWxlLXRvd24tdXNlcnMiLCJpc3MiOiJ3aGFsZS10b3duIiwiaWF0IjoxNzY3NzY4NTk5LCJleHAiOjE3NjgzNzMzOTl9.Mq3YccSV_pMKxIAbeNRAUws1j7doqFqvlSv4Z9DhGjI'
},
successResponse: {
t: 'login_success',
sessionId: '89aff162-52d9-484e-9a35-036ba63a2280',
userId: 'test_user_123',
username: 'test_user',
currentMap: 'whale_port'
},
errorResponse: {
t: 'login_error',
message: 'Token验证失败'
}
},
chat: {
request: {
t: 'chat',
content: '大家好!我刚进入游戏',
scope: 'local'
},
successResponse: {
t: 'chat_sent',
messageId: 137,
message: '消息发送成功'
},
errorResponse: {
t: 'chat_error',
message: '消息内容不能为空'
},
incomingMessage: {
t: 'chat_render',
from: 'Player_456',
txt: '欢迎新玩家!',
bubble: true
}
},
position: {
request: {
t: 'position',
x: 150,
y: 400,
mapId: 'pumpkin_valley'
}
}
};
}
}