forked from datawhale/whale-town-end
docs:添加 WebSocket API 文档控制器
- 实现 /websocket/docs 接口提供完整的 WebSocket API 文档 - 实现 /websocket/message-examples 接口提供消息格式示例 - 包含连接配置、认证要求、事件格式说明 - 提供 JavaScript 和 Godot 客户端连接示例 - 包含故障排除指南和测试工具推荐
This commit is contained in:
421
src/business/zulip/controllers/websocket-docs.controller.ts
Normal file
421
src/business/zulip/controllers/websocket-docs.controller.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user