/** * WebSocket消息数据传输对象 * * 功能描述: * - 定义WebSocket通信的消息格式和验证规则 * - 提供客户端和服务端之间的数据交换标准 * - 支持位置广播系统的实时通信需求 * - 实现消息类型的统一管理和验证 * * 职责分离: * - 消息格式:定义WebSocket消息的标准结构 * - 数据验证:使用class-validator进行输入验证 * - 类型安全:提供TypeScript类型约束 * - 接口规范:统一的消息交换格式 * * 最近修改: * - 2026-01-08: 功能新增 - 创建WebSocket消息DTO,支持位置广播系统 * * @author moyin * @version 1.0.0 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { IsString, IsNumber, IsNotEmpty, IsOptional, IsObject, Length } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; /** * 加入会话消息DTO * * 职责: * - 定义用户加入游戏会话的请求数据 * - 验证会话ID和认证token的格式 * - 支持可选的初始位置设置 */ export class JoinSessionMessage { /** * 消息类型标识 */ @ApiProperty({ description: '消息类型', example: 'join_session', enum: ['join_session'] }) @IsString({ message: '消息类型必须是字符串' }) @IsOptional() type?: 'join_session' = 'join_session'; /** * 游戏会话ID */ @ApiProperty({ description: '游戏会话ID', example: 'session_12345', minLength: 1, maxLength: 100 }) @IsString({ message: '会话ID必须是字符串' }) @IsNotEmpty({ message: '会话ID不能为空' }) @Length(1, 100, { message: '会话ID长度必须在1-100个字符之间' }) sessionId: string; /** * JWT认证token */ @ApiProperty({ description: 'JWT认证token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }) @IsString({ message: 'Token必须是字符串' }) @IsNotEmpty({ message: 'Token不能为空' }) token: string; /** * 会话密码(可选) */ @ApiPropertyOptional({ description: '会话密码(如果会话需要密码)', example: 'password123' }) @IsOptional() @IsString({ message: '会话密码必须是字符串' }) password?: string; /** * 初始位置(可选) */ @ApiPropertyOptional({ description: '用户初始位置', example: { mapId: 'plaza', x: 100, y: 200 } }) @IsOptional() @IsObject({ message: '初始位置必须是对象格式' }) initialPosition?: { mapId: string; x: number; y: number; }; } /** * 离开会话消息DTO * * 职责: * - 定义用户离开游戏会话的请求数据 * - 支持主动离开和被动断开的区分 * - 提供离开原因的记录 */ export class LeaveSessionMessage { /** * 消息类型标识 */ @ApiProperty({ description: '消息类型', example: 'leave_session', enum: ['leave_session'] }) @IsString({ message: '消息类型必须是字符串' }) @IsOptional() type?: 'leave_session' = 'leave_session'; /** * 游戏会话ID */ @ApiProperty({ description: '游戏会话ID', example: 'session_12345' }) @IsString({ message: '会话ID必须是字符串' }) @IsNotEmpty({ message: '会话ID不能为空' }) sessionId: string; /** * 离开原因(可选) */ @ApiPropertyOptional({ description: '离开原因', example: 'user_left', enum: ['user_left', 'connection_lost', 'kicked', 'error'] }) @IsOptional() @IsString({ message: '离开原因必须是字符串' }) reason?: string; } /** * 位置更新消息DTO * * 职责: * - 定义用户位置更新的请求数据 * - 验证位置坐标和地图ID的有效性 * - 支持位置元数据的扩展 */ export class PositionUpdateMessage { /** * 消息类型标识 */ @ApiProperty({ description: '消息类型', example: 'position_update', enum: ['position_update'] }) @IsString({ message: '消息类型必须是字符串' }) @IsOptional() type?: 'position_update' = 'position_update'; /** * 地图ID */ @ApiProperty({ description: '地图ID', example: 'plaza', minLength: 1, maxLength: 50 }) @IsString({ message: '地图ID必须是字符串' }) @IsNotEmpty({ message: '地图ID不能为空' }) @Length(1, 50, { message: '地图ID长度必须在1-50个字符之间' }) mapId: string; /** * X轴坐标 */ @ApiProperty({ description: 'X轴坐标', example: 100.5, type: 'number' }) @IsNumber({}, { message: 'X坐标必须是数字' }) @Type(() => Number) x: number; /** * Y轴坐标 */ @ApiProperty({ description: 'Y轴坐标', example: 200.3, type: 'number' }) @IsNumber({}, { message: 'Y坐标必须是数字' }) @Type(() => Number) y: number; /** * 时间戳(可选,服务端会自动设置) */ @ApiPropertyOptional({ description: '位置更新时间戳', example: 1641024000000 }) @IsOptional() @IsNumber({}, { message: '时间戳必须是数字' }) @Type(() => Number) timestamp?: number; /** * 扩展元数据(可选) */ @ApiPropertyOptional({ description: '位置扩展元数据', example: { speed: 5.2, direction: 'north' } }) @IsOptional() @IsObject({ message: '元数据必须是对象格式' }) metadata?: Record; } /** * 心跳消息DTO * * 职责: * - 定义WebSocket连接的心跳检测消息 * - 维持连接活跃状态 * - 检测连接质量和延迟 */ export class HeartbeatMessage { /** * 消息类型标识 */ @ApiProperty({ description: '消息类型', example: 'heartbeat', enum: ['heartbeat'] }) @IsString({ message: '消息类型必须是字符串' }) @IsOptional() type?: 'heartbeat' = 'heartbeat'; /** * 客户端时间戳 */ @ApiProperty({ description: '客户端发送时间戳', example: 1641024000000 }) @IsNumber({}, { message: '时间戳必须是数字' }) @Type(() => Number) timestamp: number; /** * 序列号(可选) */ @ApiPropertyOptional({ description: '心跳序列号', example: 1 }) @IsOptional() @IsNumber({}, { message: '序列号必须是数字' }) @Type(() => Number) sequence?: number; } /** * 通用WebSocket消息DTO * * 职责: * - 定义所有WebSocket消息的基础结构 * - 提供消息类型的统一管理 * - 支持消息的路由和处理 */ export class WebSocketMessage { /** * 消息类型 */ @ApiProperty({ description: '消息类型', example: 'join_session', enum: ['join_session', 'leave_session', 'position_update', 'heartbeat'] }) @IsString({ message: '消息类型必须是字符串' }) @IsNotEmpty({ message: '消息类型不能为空' }) type: string; /** * 消息数据 */ @ApiProperty({ description: '消息数据', example: {} }) @IsObject({ message: '消息数据必须是对象格式' }) data: any; /** * 消息ID(可选) */ @ApiPropertyOptional({ description: '消息唯一标识', example: 'msg_12345' }) @IsOptional() @IsString({ message: '消息ID必须是字符串' }) messageId?: string; /** * 时间戳 */ @ApiProperty({ description: '消息时间戳', example: 1641024000000 }) @IsNumber({}, { message: '时间戳必须是数字' }) @Type(() => Number) timestamp: number; }