feature/websocket-unify-and-openapi-update #38
@@ -42,7 +42,6 @@ import {
|
|||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
||||||
import { ZulipService } from './zulip.service';
|
import { ZulipService } from './zulip.service';
|
||||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
|
||||||
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
||||||
import {
|
import {
|
||||||
SendChatMessageDto,
|
SendChatMessageDto,
|
||||||
@@ -106,7 +105,7 @@ export class ChatController {
|
|||||||
// 注意:这里需要一个有效的 socketId,但 REST API 没有 WebSocket 连接
|
// 注意:这里需要一个有效的 socketId,但 REST API 没有 WebSocket 连接
|
||||||
// 这是一个限制,实际使用中应该通过 WebSocket 发送消息
|
// 这是一个限制,实际使用中应该通过 WebSocket 发送消息
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:ws://localhost:3000/game',
|
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:wss://whaletownend.xinghangee.icu',
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -318,7 +317,7 @@ export class ChatController {
|
|||||||
properties: {
|
properties: {
|
||||||
websocketUrl: {
|
websocketUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: 'ws://localhost:3000/game',
|
example: 'wss://whaletownend.xinghangee.icu/game',
|
||||||
description: 'WebSocket 连接地址'
|
description: 'WebSocket 连接地址'
|
||||||
},
|
},
|
||||||
namespace: {
|
namespace: {
|
||||||
@@ -347,7 +346,9 @@ export class ChatController {
|
|||||||
})
|
})
|
||||||
async getWebSocketInfo() {
|
async getWebSocketInfo() {
|
||||||
return {
|
return {
|
||||||
websocketUrl: 'ws://localhost:3001',
|
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||||
|
protocol: 'native-websocket',
|
||||||
|
path: '/game',
|
||||||
namespace: '/',
|
namespace: '/',
|
||||||
supportedEvents: [
|
supportedEvents: [
|
||||||
'login', // 用户登录
|
'login', // 用户登录
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ export class CleanWebSocketGateway implements OnModuleInit, OnModuleDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
const port = 3001;
|
const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : 3001;
|
||||||
|
|
||||||
this.server = new WebSocket.Server({ port });
|
this.server = new WebSocket.Server({
|
||||||
|
port,
|
||||||
|
path: '/game' // 统一使用 /game 路径
|
||||||
|
});
|
||||||
|
|
||||||
this.server.on('connection', (ws: ExtendedWebSocket) => {
|
this.server.on('connection', (ws: ExtendedWebSocket) => {
|
||||||
ws.id = this.generateClientId();
|
ws.id = this.generateClientId();
|
||||||
@@ -71,7 +74,7 @@ export class CleanWebSocketGateway implements OnModuleInit, OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`WebSocket服务器启动成功,端口: ${port}`);
|
this.logger.log(`WebSocket服务器启动成功,端口: ${port},路径: /game`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class WebSocketDocsController {
|
|||||||
properties: {
|
properties: {
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: 'ws://localhost:3000/game',
|
example: 'wss://whaletownend.xinghangee.icu/game',
|
||||||
description: 'WebSocket 连接地址'
|
description: 'WebSocket 连接地址'
|
||||||
},
|
},
|
||||||
namespace: {
|
namespace: {
|
||||||
@@ -92,8 +92,8 @@ export class WebSocketDocsController {
|
|||||||
getWebSocketDocs() {
|
getWebSocketDocs() {
|
||||||
return {
|
return {
|
||||||
connection: {
|
connection: {
|
||||||
url: 'ws://localhost:3000/game',
|
url: 'wss://whaletownend.xinghangee.icu/game',
|
||||||
namespace: '/game',
|
namespace: '/',
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
options: {
|
options: {
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
@@ -262,52 +262,52 @@ export class WebSocketDocsController {
|
|||||||
examples: {
|
examples: {
|
||||||
javascript: {
|
javascript: {
|
||||||
connection: `
|
connection: `
|
||||||
// 使用 Socket.IO 客户端连接
|
// 使用原生 WebSocket 客户端连接
|
||||||
const io = require('socket.io-client');
|
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
|
||||||
|
|
||||||
const socket = io('ws://localhost:3000/game', {
|
ws.onopen = function() {
|
||||||
transports: ['websocket', 'polling'],
|
console.log('连接成功');
|
||||||
timeout: 20000,
|
|
||||||
forceNew: true,
|
|
||||||
reconnection: true,
|
|
||||||
reconnectionAttempts: 3,
|
|
||||||
reconnectionDelay: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
// 连接成功
|
|
||||||
socket.on('connect', () => {
|
|
||||||
console.log('连接成功:', socket.id);
|
|
||||||
|
|
||||||
// 发送登录消息
|
// 发送登录消息
|
||||||
socket.emit('login', {
|
ws.send(JSON.stringify({
|
||||||
type: 'login',
|
type: 'login',
|
||||||
token: 'YOUR_JWT_TOKEN_HERE'
|
token: 'YOUR_JWT_TOKEN_HERE'
|
||||||
});
|
}));
|
||||||
});
|
};
|
||||||
|
|
||||||
// 登录成功
|
ws.onmessage = function(event) {
|
||||||
socket.on('login_success', (data) => {
|
const data = JSON.parse(event.data);
|
||||||
console.log('登录成功:', data);
|
console.log('收到消息:', data);
|
||||||
|
|
||||||
// 发送聊天消息
|
// 处理不同类型的消息
|
||||||
socket.emit('chat', {
|
if (data.t === 'login_success') {
|
||||||
t: 'chat',
|
console.log('登录成功:', data);
|
||||||
content: '大家好!',
|
|
||||||
scope: 'local'
|
// 发送聊天消息
|
||||||
});
|
ws.send(JSON.stringify({
|
||||||
});
|
t: 'chat',
|
||||||
|
content: '大家好!',
|
||||||
|
scope: 'local'
|
||||||
|
}));
|
||||||
|
} else if (data.t === 'chat_render') {
|
||||||
|
console.log('收到消息:', data.from, '说:', data.txt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 接收聊天消息
|
ws.onclose = function(event) {
|
||||||
socket.on('chat_render', (data) => {
|
console.log('连接关闭:', event.code, event.reason);
|
||||||
console.log('收到消息:', data.from, '说:', data.txt);
|
};
|
||||||
});
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
console.error('连接错误:', error);
|
||||||
|
};
|
||||||
`,
|
`,
|
||||||
godot: `
|
godot: `
|
||||||
# Godot WebSocket 客户端示例
|
# Godot WebSocket 客户端示例
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
var socket = WebSocketClient.new()
|
var socket = WebSocketClient.new()
|
||||||
var url = "ws://localhost:3000/game"
|
var url = "wss://whaletownend.xinghangee.icu/game"
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
socket.connect("connection_closed", self, "_closed")
|
socket.connect("connection_closed", self, "_closed")
|
||||||
@@ -320,18 +320,18 @@ func _ready():
|
|||||||
print("连接失败")
|
print("连接失败")
|
||||||
|
|
||||||
func _connected(protocol):
|
func _connected(protocol):
|
||||||
print("WebSocket 连接成功")
|
print("连接成功")
|
||||||
# 发送登录消息
|
|
||||||
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():
|
func _on_data():
|
||||||
var packet = socket.get_peer(1).get_packet()
|
var packet = socket.get_peer(1).get_packet()
|
||||||
var message = JSON.parse(packet.get_string_from_utf8())
|
var message = JSON.parse(packet.get_string_from_utf8())
|
||||||
print("收到消息: ", message.result)
|
print("收到消息: ", message.result)
|
||||||
|
|
||||||
|
func _closed(was_clean_close):
|
||||||
|
print("连接关闭")
|
||||||
|
|
||||||
|
func _error():
|
||||||
|
print("连接错误")
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
817
src/business/zulip/websocket_openapi.controller.ts
Normal file
817
src/business/zulip/websocket_openapi.controller.ts
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket OpenAPI 文档控制器
|
||||||
|
*
|
||||||
|
* 专门用于在OpenAPI/Swagger中展示WebSocket接口
|
||||||
|
* 通过REST API的方式描述WebSocket的消息格式和交互流程
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2026-01-09
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 路径
|
||||||
|
- ✅ 支持地图房间管理
|
||||||
|
- ✅ 实现消息广播机制
|
||||||
|
`
|
||||||
|
})
|
||||||
|
@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. 获取有效的JWT Token(通过 /auth/login 接口)',
|
||||||
|
'2. 使用WebSocket客户端连接到 wss://whaletownend.xinghangee.icu/game',
|
||||||
|
'3. 发送login消息进行认证',
|
||||||
|
'4. 验证收到login_success响应',
|
||||||
|
'5. 发送chat消息测试聊天功能',
|
||||||
|
'6. 发送position消息测试位置更新',
|
||||||
|
'7. 观察其他客户端的消息广播'
|
||||||
|
],
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
451
src/business/zulip/websocket_test.controller.ts
Normal file
451
src/business/zulip/websocket_test.controller.ts
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket 测试页面控制器
|
||||||
|
*
|
||||||
|
* 提供一个简单的WebSocket测试界面,可以直接在浏览器中测试WebSocket连接
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2026-01-09
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Controller, Get, Res } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('websocket')
|
||||||
|
@Controller('websocket-test')
|
||||||
|
export class WebSocketTestController {
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'WebSocket 测试页面',
|
||||||
|
description: '提供一个简单的WebSocket测试界面,可以直接在浏览器中测试连接和消息发送'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'WebSocket测试页面HTML',
|
||||||
|
content: {
|
||||||
|
'text/html': {
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
getTestPage(@Res() res: Response) {
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket 测试工具 - Pixel Game Server</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status.disconnected { background-color: #ffebee; color: #c62828; }
|
||||||
|
.status.connected { background-color: #e8f5e8; color: #2e7d32; }
|
||||||
|
.status.connecting { background-color: #fff3e0; color: #ef6c00; }
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
input, textarea, select, button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
button:hover { background-color: #1565c0; }
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-log {
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.message-sent { background-color: #e3f2fd; }
|
||||||
|
.message-received { background-color: #f3e5f5; }
|
||||||
|
.message-system { background-color: #fff3e0; }
|
||||||
|
.message-error { background-color: #ffebee; }
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.quick-action {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.quick-action:hover { background-color: #eeeeee; }
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
background-color: #e8f4fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.two-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🎮 Pixel Game Server - WebSocket 测试工具</h1>
|
||||||
|
|
||||||
|
<div class="info-panel">
|
||||||
|
<h3>📋 使用说明</h3>
|
||||||
|
<p><strong>1. 获取JWT Token:</strong> 先通过 <code>/auth/login</code> 接口获取有效的JWT Token</p>
|
||||||
|
<p><strong>2. 建立连接:</strong> 点击"连接"按钮建立WebSocket连接</p>
|
||||||
|
<p><strong>3. 用户登录:</strong> 输入JWT Token并点击"登录"进行认证</p>
|
||||||
|
<p><strong>4. 发送消息:</strong> 认证成功后可以发送聊天消息和位置更新</p>
|
||||||
|
<p><strong>WebSocket地址:</strong> <code>wss://whaletownend.xinghangee.icu/game</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="two-column">
|
||||||
|
<div class="container">
|
||||||
|
<h2>🔌 连接控制</h2>
|
||||||
|
<div id="connectionStatus" class="status disconnected">未连接</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="wsUrl">WebSocket 地址:</label>
|
||||||
|
<input type="text" id="wsUrl" value="wss://whaletownend.xinghangee.icu/game" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="connectBtn" onclick="toggleConnection()">连接</button>
|
||||||
|
|
||||||
|
<h3>🔐 用户认证</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jwtToken">JWT Token:</label>
|
||||||
|
<textarea id="jwtToken" rows="3" placeholder="请输入从 /auth/login 获取的JWT Token"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn" onclick="login()" disabled>登录</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>💬 消息发送</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="messageType">消息类型:</label>
|
||||||
|
<select id="messageType">
|
||||||
|
<option value="chat">聊天消息</option>
|
||||||
|
<option value="position">位置更新</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chatFields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chatContent">消息内容:</label>
|
||||||
|
<input type="text" id="chatContent" placeholder="输入聊天消息" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chatScope">消息范围:</label>
|
||||||
|
<select id="chatScope">
|
||||||
|
<option value="local">本地 (当前地图)</option>
|
||||||
|
<option value="global">全局 (所有玩家)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="positionFields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="posX">X坐标:</label>
|
||||||
|
<input type="number" id="posX" value="150" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="posY">Y坐标:</label>
|
||||||
|
<input type="number" id="posY" value="400" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mapId">地图ID:</label>
|
||||||
|
<select id="mapId">
|
||||||
|
<option value="whale_port">Whale Port (鲸鱼港)</option>
|
||||||
|
<option value="pumpkin_valley">Pumpkin Valley (南瓜谷)</option>
|
||||||
|
<option value="novice_village">Novice Village (新手村)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
|
||||||
|
|
||||||
|
<div class="quick-actions">
|
||||||
|
<div class="quick-action" onclick="sendQuickMessage('Hello!')">快速发送: Hello!</div>
|
||||||
|
<div class="quick-action" onclick="sendQuickMessage('大家好!')">快速发送: 大家好!</div>
|
||||||
|
<div class="quick-action" onclick="sendQuickPosition()">发送位置更新</div>
|
||||||
|
<div class="quick-action" onclick="clearLog()">清空日志</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>📋 消息日志</h2>
|
||||||
|
<div id="messageLog" class="message-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
let isLoggedIn = false;
|
||||||
|
|
||||||
|
// 切换消息类型时显示对应字段
|
||||||
|
document.getElementById('messageType').addEventListener('change', function() {
|
||||||
|
const chatFields = document.getElementById('chatFields');
|
||||||
|
const positionFields = document.getElementById('positionFields');
|
||||||
|
|
||||||
|
if (this.value === 'chat') {
|
||||||
|
chatFields.style.display = 'block';
|
||||||
|
positionFields.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
chatFields.style.display = 'none';
|
||||||
|
positionFields.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus(status, message) {
|
||||||
|
const statusEl = document.getElementById('connectionStatus');
|
||||||
|
statusEl.className = 'status ' + status;
|
||||||
|
statusEl.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(type, content, timestamp = new Date()) {
|
||||||
|
const log = document.getElementById('messageLog');
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = 'message-item message-' + type;
|
||||||
|
|
||||||
|
const timeStr = timestamp.toLocaleTimeString();
|
||||||
|
messageEl.innerHTML = '<strong>[' + timeStr + ']</strong> ' + content;
|
||||||
|
|
||||||
|
log.appendChild(messageEl);
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConnection() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
} else {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const url = document.getElementById('wsUrl').value;
|
||||||
|
updateStatus('connecting', '连接中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
updateStatus('connected', '已连接');
|
||||||
|
document.getElementById('connectBtn').textContent = '断开';
|
||||||
|
document.getElementById('loginBtn').disabled = false;
|
||||||
|
addMessage('system', '✅ WebSocket连接成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
addMessage('received', '📥 ' + JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
// 处理特定消息类型
|
||||||
|
if (data.t === 'login_success') {
|
||||||
|
isLoggedIn = true;
|
||||||
|
document.getElementById('sendBtn').disabled = false;
|
||||||
|
addMessage('system', '✅ 登录成功!用户: ' + data.username);
|
||||||
|
} else if (data.t === 'login_error') {
|
||||||
|
addMessage('error', '❌ 登录失败: ' + data.message);
|
||||||
|
} else if (data.t === 'chat_render') {
|
||||||
|
addMessage('system', '💬 收到消息: ' + data.from + ' 说: ' + data.txt);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addMessage('received', '📥 ' + event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function() {
|
||||||
|
updateStatus('disconnected', '未连接');
|
||||||
|
document.getElementById('connectBtn').textContent = '连接';
|
||||||
|
document.getElementById('loginBtn').disabled = true;
|
||||||
|
document.getElementById('sendBtn').disabled = true;
|
||||||
|
isLoggedIn = false;
|
||||||
|
addMessage('system', '🔌 WebSocket连接已关闭');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
addMessage('error', '❌ 连接错误: ' + error);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('disconnected', '连接失败');
|
||||||
|
addMessage('error', '❌ 连接失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = document.getElementById('jwtToken').value.trim();
|
||||||
|
if (!token) {
|
||||||
|
addMessage('error', '❌ 请输入JWT Token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
type: 'login',
|
||||||
|
token: token
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageType = document.getElementById('messageType').value;
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (messageType === 'chat') {
|
||||||
|
const content = document.getElementById('chatContent').value.trim();
|
||||||
|
if (!content) {
|
||||||
|
addMessage('error', '❌ 请输入消息内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = {
|
||||||
|
t: 'chat',
|
||||||
|
content: content,
|
||||||
|
scope: document.getElementById('chatScope').value
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
message = {
|
||||||
|
t: 'position',
|
||||||
|
x: parseInt(document.getElementById('posX').value),
|
||||||
|
y: parseInt(document.getElementById('posY').value),
|
||||||
|
mapId: document.getElementById('mapId').value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||||
|
|
||||||
|
// 清空聊天输入框
|
||||||
|
if (messageType === 'chat') {
|
||||||
|
document.getElementById('chatContent').value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQuickMessage(content) {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('messageType').value = 'chat';
|
||||||
|
document.getElementById('chatContent').value = content;
|
||||||
|
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQuickPosition() {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('messageType').value = 'position';
|
||||||
|
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
document.getElementById('messageLog').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后的初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
addMessage('system', '🎮 WebSocket测试工具已就绪');
|
||||||
|
addMessage('system', '💡 提示: 请先通过 /auth/login 接口获取JWT Token');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
* 业务服务:
|
* 业务服务:
|
||||||
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
|
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
|
||||||
* - ZulipWebSocketGateway: WebSocket统一网关,处理客户端连接
|
* - CleanWebSocketGateway: WebSocket统一网关,处理客户端连接
|
||||||
* - SessionManagerService: 会话状态管理和业务逻辑
|
* - SessionManagerService: 会话状态管理和业务逻辑
|
||||||
* - MessageFilterService: 消息过滤和业务规则控制
|
* - MessageFilterService: 消息过滤和业务规则控制
|
||||||
*
|
*
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
|
||||||
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
||||||
import { ZulipService } from './zulip.service';
|
import { ZulipService } from './zulip.service';
|
||||||
import { SessionManagerService } from './services/session_manager.service';
|
import { SessionManagerService } from './services/session_manager.service';
|
||||||
@@ -52,6 +51,8 @@ import { ZulipEventProcessorService } from './services/zulip_event_processor.ser
|
|||||||
import { SessionCleanupService } from './services/session_cleanup.service';
|
import { SessionCleanupService } from './services/session_cleanup.service';
|
||||||
import { ChatController } from './chat.controller';
|
import { ChatController } from './chat.controller';
|
||||||
import { WebSocketDocsController } from './websocket_docs.controller';
|
import { WebSocketDocsController } from './websocket_docs.controller';
|
||||||
|
import { WebSocketOpenApiController } from './websocket_openapi.controller';
|
||||||
|
import { WebSocketTestController } from './websocket_test.controller';
|
||||||
import { ZulipAccountsController } from './zulip_accounts.controller';
|
import { ZulipAccountsController } from './zulip_accounts.controller';
|
||||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
||||||
@@ -94,6 +95,10 @@ import { AuthModule } from '../auth/auth.module';
|
|||||||
ChatController,
|
ChatController,
|
||||||
// WebSocket API文档控制器
|
// WebSocket API文档控制器
|
||||||
WebSocketDocsController,
|
WebSocketDocsController,
|
||||||
|
// WebSocket OpenAPI规范控制器
|
||||||
|
WebSocketOpenApiController,
|
||||||
|
// WebSocket测试页面控制器
|
||||||
|
WebSocketTestController,
|
||||||
// Zulip账号关联管理控制器
|
// Zulip账号关联管理控制器
|
||||||
ZulipAccountsController,
|
ZulipAccountsController,
|
||||||
],
|
],
|
||||||
|
|||||||
42
src/main.ts
42
src/main.ts
@@ -89,6 +89,12 @@ async function bootstrap() {
|
|||||||
- 系统状态监控
|
- 系统状态监控
|
||||||
- Zulip 集成状态
|
- Zulip 集成状态
|
||||||
|
|
||||||
|
### 🔌 WebSocket 接口 (websocket)
|
||||||
|
- 实时消息传输
|
||||||
|
- 位置同步
|
||||||
|
- 地图房间管理
|
||||||
|
- 连接状态监控
|
||||||
|
|
||||||
### 👑 管理员后台 (admin)
|
### 👑 管理员后台 (admin)
|
||||||
- 用户管理
|
- 用户管理
|
||||||
- 系统监控
|
- 系统监控
|
||||||
@@ -98,12 +104,16 @@ async function bootstrap() {
|
|||||||
|
|
||||||
游戏聊天功能主要通过 WebSocket 实现:
|
游戏聊天功能主要通过 WebSocket 实现:
|
||||||
|
|
||||||
**连接地址**: \`ws://localhost:3000/game\`
|
**连接地址**: \`wss://whaletownend.xinghangee.icu/game\` (原生WebSocket)
|
||||||
|
|
||||||
|
**重要变更**: 已从Socket.IO迁移到原生WebSocket,提升性能和稳定性
|
||||||
|
|
||||||
|
**连接路径**: \`/game\` - 统一的WebSocket入口
|
||||||
|
|
||||||
**支持的事件**:
|
**支持的事件**:
|
||||||
- \`login\`: 用户登录(需要 JWT Token)
|
- \`login\`: 用户登录(需要 JWT Token)
|
||||||
- \`chat\`: 发送聊天消息
|
- \`chat\`: 发送聊天消息
|
||||||
- \`position_update\`: 位置更新
|
- \`position\`: 位置更新
|
||||||
|
|
||||||
**JWT Token 要求**:
|
**JWT Token 要求**:
|
||||||
- issuer: \`whale-town\`
|
- issuer: \`whale-town\`
|
||||||
@@ -119,10 +129,30 @@ async function bootstrap() {
|
|||||||
- Whale Port (鲸鱼港)
|
- Whale Port (鲸鱼港)
|
||||||
- Pumpkin Valley (南瓜谷)
|
- Pumpkin Valley (南瓜谷)
|
||||||
- Novice Village (新手村)
|
- Novice Village (新手村)
|
||||||
|
|
||||||
|
## 最近更新 (v2.1.0)
|
||||||
|
|
||||||
|
### 🚀 WebSocket 架构升级
|
||||||
|
- ✅ 移除Socket.IO依赖,使用原生WebSocket
|
||||||
|
- ✅ 实现地图房间分组管理
|
||||||
|
- ✅ 支持本地和全局消息广播
|
||||||
|
- ✅ 新增实时连接监控
|
||||||
|
|
||||||
|
### 📚 文档完善
|
||||||
|
- ✅ 新增WebSocket专用API文档
|
||||||
|
- ✅ 提供交互式消息格式展示
|
||||||
|
- ✅ 包含测试工具和示例代码
|
||||||
|
- ✅ 完整的开发者指南
|
||||||
|
|
||||||
|
### 🔧 性能优化
|
||||||
|
- ✅ 更高效的消息路由机制
|
||||||
|
- ✅ 优化连接池管理
|
||||||
|
- ✅ 增强错误处理和日志记录
|
||||||
`)
|
`)
|
||||||
.setVersion('2.0.0')
|
.setVersion('2.1.0')
|
||||||
.addTag('auth', '🔐 用户认证相关接口')
|
.addTag('auth', '🔐 用户认证相关接口')
|
||||||
.addTag('chat', '💬 聊天系统相关接口')
|
.addTag('chat', '💬 聊天系统相关接口')
|
||||||
|
.addTag('websocket', '🔌 WebSocket接口文档和测试')
|
||||||
.addTag('admin', '👑 管理员后台相关接口')
|
.addTag('admin', '👑 管理员后台相关接口')
|
||||||
.addBearerAuth(
|
.addBearerAuth(
|
||||||
{
|
{
|
||||||
@@ -135,8 +165,10 @@ async function bootstrap() {
|
|||||||
},
|
},
|
||||||
'JWT-auth',
|
'JWT-auth',
|
||||||
)
|
)
|
||||||
.addServer('http://localhost:3000', '开发环境')
|
.addServer('http://localhost:3000', '开发环境 - REST API')
|
||||||
.addServer('https://whaletownend.xinghangee.icu', '生产环境')
|
.addServer('https://whaletownend.xinghangee.icu', '生产环境 - REST API')
|
||||||
|
.addServer('wss://whaletownend.xinghangee.icu/game', '生产环境 - WebSocket')
|
||||||
|
.addServer('ws://localhost:3001/game', '开发环境 - WebSocket')
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
|||||||
Reference in New Issue
Block a user