feat:实现位置广播系统

- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:52 +08:00
parent 6924416bbd
commit c31cbe559d
27 changed files with 12212 additions and 0 deletions

View File

@@ -0,0 +1,522 @@
/**
* API数据传输对象
*
* 功能描述:
* - 定义HTTP API的请求和响应数据格式
* - 提供数据验证规则和类型约束
* - 支持Swagger API文档自动生成
* - 实现统一的API数据交换标准
*
* 职责分离:
* - 请求验证HTTP请求数据的格式验证
* - 类型安全TypeScript类型约束和检查
* - 文档生成Swagger API文档的自动生成
* - 数据转换:前端和后端数据格式的标准化
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建API DTO支持位置广播系统
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { IsString, IsNumber, IsOptional, IsBoolean, IsArray, Length, Min, Max, IsEnum } from 'class-validator';
import { Type, Transform } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* 创建会话DTO
*/
export class CreateSessionDto {
@ApiProperty({
description: '会话ID',
example: 'session_12345',
minLength: 1,
maxLength: 100
})
@IsString({ message: '会话ID必须是字符串' })
@Length(1, 100, { message: '会话ID长度必须在1-100个字符之间' })
sessionId: string;
@ApiPropertyOptional({
description: '会话名称',
example: '我的游戏会话'
})
@IsOptional()
@IsString({ message: '会话名称必须是字符串' })
@Length(1, 200, { message: '会话名称长度必须在1-200个字符之间' })
name?: string;
@ApiPropertyOptional({
description: '会话描述',
example: '这是一个多人游戏会话'
})
@IsOptional()
@IsString({ message: '会话描述必须是字符串' })
@Length(0, 500, { message: '会话描述长度不能超过500个字符' })
description?: string;
@ApiPropertyOptional({
description: '最大用户数',
example: 100,
minimum: 1,
maximum: 1000
})
@IsOptional()
@IsNumber({}, { message: '最大用户数必须是数字' })
@Min(1, { message: '最大用户数不能小于1' })
@Max(1000, { message: '最大用户数不能超过1000' })
@Type(() => Number)
maxUsers?: number;
@ApiPropertyOptional({
description: '是否允许观察者',
example: true
})
@IsOptional()
@IsBoolean({ message: '允许观察者必须是布尔值' })
allowObservers?: boolean;
@ApiPropertyOptional({
description: '会话密码',
example: 'password123'
})
@IsOptional()
@IsString({ message: '会话密码必须是字符串' })
@Length(1, 50, { message: '会话密码长度必须在1-50个字符之间' })
password?: string;
@ApiPropertyOptional({
description: '允许的地图列表',
example: ['plaza', 'forest', 'mountain'],
type: [String]
})
@IsOptional()
@IsArray({ message: '允许的地图必须是数组' })
@IsString({ each: true, message: '地图ID必须是字符串' })
allowedMaps?: string[];
@ApiPropertyOptional({
description: '广播范围(像素)',
example: 1000,
minimum: 0,
maximum: 10000
})
@IsOptional()
@IsNumber({}, { message: '广播范围必须是数字' })
@Min(0, { message: '广播范围不能小于0' })
@Max(10000, { message: '广播范围不能超过10000' })
@Type(() => Number)
broadcastRange?: number;
@ApiPropertyOptional({
description: '扩展元数据',
example: { theme: 'dark', language: 'zh-CN' }
})
@IsOptional()
metadata?: Record<string, any>;
}
/**
* 加入会话DTO
*/
export class JoinSessionDto {
@ApiProperty({
description: '会话ID',
example: 'session_12345'
})
@IsString({ message: '会话ID必须是字符串' })
@Length(1, 100, { message: '会话ID长度必须在1-100个字符之间' })
sessionId: string;
@ApiPropertyOptional({
description: '会话密码',
example: 'password123'
})
@IsOptional()
@IsString({ message: '会话密码必须是字符串' })
password?: string;
@ApiPropertyOptional({
description: '初始位置',
example: {
mapId: 'plaza',
x: 100,
y: 200
}
})
@IsOptional()
initialPosition?: {
mapId: string;
x: number;
y: number;
};
}
/**
* 更新位置DTO
*/
export class UpdatePositionDto {
@ApiProperty({
description: '地图ID',
example: 'plaza'
})
@IsString({ message: '地图ID必须是字符串' })
@Length(1, 50, { message: '地图ID长度必须在1-50个字符之间' })
mapId: string;
@ApiProperty({
description: 'X轴坐标',
example: 100.5
})
@IsNumber({}, { message: 'X坐标必须是数字' })
@Type(() => Number)
x: number;
@ApiProperty({
description: 'Y轴坐标',
example: 200.3
})
@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()
metadata?: Record<string, any>;
}
/**
* 会话查询DTO
*/
export class SessionQueryDto {
@ApiPropertyOptional({
description: '会话状态',
example: 'active',
enum: ['active', 'idle', 'paused', 'ended']
})
@IsOptional()
@IsEnum(['active', 'idle', 'paused', 'ended'], { message: '会话状态值无效' })
status?: string;
@ApiPropertyOptional({
description: '最小用户数',
example: 1,
minimum: 0
})
@IsOptional()
@IsNumber({}, { message: '最小用户数必须是数字' })
@Min(0, { message: '最小用户数不能小于0' })
@Type(() => Number)
minUsers?: number;
@ApiPropertyOptional({
description: '最大用户数',
example: 100,
minimum: 1
})
@IsOptional()
@IsNumber({}, { message: '最大用户数必须是数字' })
@Min(1, { message: '最大用户数不能小于1' })
@Type(() => Number)
maxUsers?: number;
@ApiPropertyOptional({
description: '只显示公开会话',
example: true
})
@IsOptional()
@IsBoolean({ message: '公开会话标志必须是布尔值' })
@Transform(({ value }) => value === 'true' || value === true)
publicOnly?: boolean;
@ApiPropertyOptional({
description: '创建者ID',
example: 'user123'
})
@IsOptional()
@IsString({ message: '创建者ID必须是字符串' })
creatorId?: string;
@ApiPropertyOptional({
description: '分页偏移',
example: 0,
minimum: 0
})
@IsOptional()
@IsNumber({}, { message: '分页偏移必须是数字' })
@Min(0, { message: '分页偏移不能小于0' })
@Type(() => Number)
offset?: number;
@ApiPropertyOptional({
description: '分页大小',
example: 10,
minimum: 1,
maximum: 100
})
@IsOptional()
@IsNumber({}, { message: '分页大小必须是数字' })
@Min(1, { message: '分页大小不能小于1' })
@Max(100, { message: '分页大小不能超过100' })
@Type(() => Number)
limit?: number;
}
/**
* 位置查询DTO
*/
export class PositionQueryDto {
@ApiPropertyOptional({
description: '用户ID列表逗号分隔',
example: 'user1,user2,user3'
})
@IsOptional()
@IsString({ message: '用户ID列表必须是字符串' })
userIds?: string;
@ApiPropertyOptional({
description: '地图ID',
example: 'plaza'
})
@IsOptional()
@IsString({ message: '地图ID必须是字符串' })
mapId?: string;
@ApiPropertyOptional({
description: '会话ID',
example: 'session_12345'
})
@IsOptional()
@IsString({ message: '会话ID必须是字符串' })
sessionId?: string;
@ApiPropertyOptional({
description: '范围查询中心X坐标',
example: 100
})
@IsOptional()
@IsNumber({}, { message: '中心X坐标必须是数字' })
@Type(() => Number)
centerX?: number;
@ApiPropertyOptional({
description: '范围查询中心Y坐标',
example: 200
})
@IsOptional()
@IsNumber({}, { message: '中心Y坐标必须是数字' })
@Type(() => Number)
centerY?: number;
@ApiPropertyOptional({
description: '范围查询半径',
example: 500,
minimum: 0,
maximum: 10000
})
@IsOptional()
@IsNumber({}, { message: '查询半径必须是数字' })
@Min(0, { message: '查询半径不能小于0' })
@Max(10000, { message: '查询半径不能超过10000' })
@Type(() => Number)
radius?: number;
@ApiPropertyOptional({
description: '分页偏移',
example: 0,
minimum: 0
})
@IsOptional()
@IsNumber({}, { message: '分页偏移必须是数字' })
@Min(0, { message: '分页偏移不能小于0' })
@Type(() => Number)
offset?: number;
@ApiPropertyOptional({
description: '分页大小',
example: 50,
minimum: 1,
maximum: 1000
})
@IsOptional()
@IsNumber({}, { message: '分页大小必须是数字' })
@Min(1, { message: '分页大小不能小于1' })
@Max(1000, { message: '分页大小不能超过1000' })
@Type(() => Number)
limit?: number;
}
/**
* 更新会话配置DTO
*/
export class UpdateSessionConfigDto {
@ApiPropertyOptional({
description: '最大用户数',
example: 150,
minimum: 1,
maximum: 1000
})
@IsOptional()
@IsNumber({}, { message: '最大用户数必须是数字' })
@Min(1, { message: '最大用户数不能小于1' })
@Max(1000, { message: '最大用户数不能超过1000' })
@Type(() => Number)
maxUsers?: number;
@ApiPropertyOptional({
description: '是否允许观察者',
example: false
})
@IsOptional()
@IsBoolean({ message: '允许观察者必须是布尔值' })
allowObservers?: boolean;
@ApiPropertyOptional({
description: '会话密码',
example: 'newpassword123'
})
@IsOptional()
@IsString({ message: '会话密码必须是字符串' })
@Length(0, 50, { message: '会话密码长度不能超过50个字符' })
password?: string;
@ApiPropertyOptional({
description: '允许的地图列表',
example: ['plaza', 'forest'],
type: [String]
})
@IsOptional()
@IsArray({ message: '允许的地图必须是数组' })
@IsString({ each: true, message: '地图ID必须是字符串' })
allowedMaps?: string[];
@ApiPropertyOptional({
description: '广播范围(像素)',
example: 1500,
minimum: 0,
maximum: 10000
})
@IsOptional()
@IsNumber({}, { message: '广播范围必须是数字' })
@Min(0, { message: '广播范围不能小于0' })
@Max(10000, { message: '广播范围不能超过10000' })
@Type(() => Number)
broadcastRange?: number;
@ApiPropertyOptional({
description: '是否公开',
example: true
})
@IsOptional()
@IsBoolean({ message: '公开标志必须是布尔值' })
isPublic?: boolean;
@ApiPropertyOptional({
description: '自动清理时间(分钟)',
example: 120,
minimum: 1,
maximum: 1440
})
@IsOptional()
@IsNumber({}, { message: '自动清理时间必须是数字' })
@Min(1, { message: '自动清理时间不能小于1分钟' })
@Max(1440, { message: '自动清理时间不能超过1440分钟24小时' })
@Type(() => Number)
autoCleanupMinutes?: number;
}
/**
* 通用API响应DTO
*/
export class ApiResponseDto<T = any> {
@ApiProperty({
description: '操作是否成功',
example: true
})
success: boolean;
@ApiPropertyOptional({
description: '响应数据'
})
data?: T;
@ApiPropertyOptional({
description: '响应消息',
example: '操作成功'
})
message?: string;
@ApiPropertyOptional({
description: '错误信息',
example: '参数验证失败'
})
error?: string;
@ApiPropertyOptional({
description: '响应时间戳',
example: 1641024000000
})
timestamp?: number;
}
/**
* 分页响应DTO
*/
export class PaginatedResponseDto<T = any> {
@ApiProperty({
description: '数据列表',
type: 'array'
})
items: T[];
@ApiProperty({
description: '总记录数',
example: 100
})
total: number;
@ApiProperty({
description: '当前页码',
example: 1
})
page: number;
@ApiProperty({
description: '每页大小',
example: 10
})
pageSize: number;
@ApiProperty({
description: '总页数',
example: 10
})
totalPages: number;
@ApiProperty({
description: '是否有下一页',
example: true
})
hasNext: boolean;
@ApiProperty({
description: '是否有上一页',
example: false
})
hasPrev: boolean;
}

View File

@@ -0,0 +1,36 @@
/**
* 位置广播DTO导出
*
* 功能描述:
* - 统一导出所有位置广播相关的DTO
* - 提供便捷的DTO导入接口
* - 支持模块化的数据传输对象管理
* - 简化数据类型的使用和维护
*
* 职责分离:
* - 类型导出:统一管理所有数据传输对象的导出
* - 接口简化:为外部模块提供简洁的导入方式
* - 版本管理统一管理DTO的版本变更和兼容性
* - 文档支持为DTO使用提供清晰的类型指南
*
* 技术实现:
* - TypeScript导出充分利用TypeScript的类型系统
* - 分类导出按功能和用途分类导出不同的DTO
* - 命名规范遵循统一的DTO命名和导出规范
* - 类型安全:确保导出的类型定义完整和准确
*
* 最近修改:
* - 2026-01-08: 规范优化 - 完善文件头注释,符合代码检查规范 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-08
* @lastModified 2026-01-08
*/
// WebSocket消息DTO
export * from './websocket_message.dto';
export * from './websocket_response.dto';
// API请求响应DTO
export * from './api.dto';

View File

@@ -0,0 +1,334 @@
/**
* 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<string, any>;
}
/**
* 心跳消息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;
}

View File

@@ -0,0 +1,524 @@
/**
* WebSocket响应数据传输对象
*
* 功能描述:
* - 定义WebSocket服务端响应的消息格式
* - 提供统一的响应结构和错误处理格式
* - 支持位置广播系统的实时响应需求
* - 实现响应类型的标准化管理
*
* 职责分离:
* - 响应格式:定义服务端响应的标准结构
* - 错误处理:统一的错误响应格式
* - 类型安全提供TypeScript类型约束
* - 数据完整性:确保响应数据的完整性
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建WebSocket响应DTO支持位置广播系统
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* 会话加入成功响应DTO
*
* 职责:
* - 定义用户成功加入会话后的响应数据
* - 包含会话信息和其他用户的位置数据
* - 提供完整的会话状态视图
*/
export class SessionJoinedResponse {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'session_joined',
enum: ['session_joined']
})
type: 'session_joined' = 'session_joined';
/**
* 会话ID
*/
@ApiProperty({
description: '会话ID',
example: 'session_12345'
})
sessionId: string;
/**
* 会话中的用户列表
*/
@ApiProperty({
description: '会话中的用户列表',
example: [
{
userId: 'user1',
socketId: 'socket1',
joinedAt: 1641024000000,
lastSeen: 1641024000000,
status: 'online'
}
]
})
users: Array<{
userId: string;
socketId: string;
joinedAt: number;
lastSeen: number;
status: string;
position?: {
x: number;
y: number;
mapId: string;
timestamp: number;
};
}>;
/**
* 其他用户的位置信息
*/
@ApiProperty({
description: '其他用户的位置信息',
example: [
{
userId: 'user2',
x: 150,
y: 250,
mapId: 'plaza',
timestamp: 1641024000000
}
]
})
positions: Array<{
userId: string;
x: number;
y: number;
mapId: string;
timestamp: number;
metadata?: Record<string, any>;
}>;
/**
* 会话配置信息
*/
@ApiPropertyOptional({
description: '会话配置信息',
example: {
maxUsers: 100,
allowObservers: true,
broadcastRange: 1000
}
})
config?: {
maxUsers: number;
allowObservers: boolean;
broadcastRange?: number;
mapRestriction?: string[];
};
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}
/**
* 用户加入通知响应DTO
*
* 职责:
* - 通知会话中其他用户有新用户加入
* - 包含新用户的基本信息和位置
* - 支持实时用户状态更新
*/
export class UserJoinedNotification {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'user_joined',
enum: ['user_joined']
})
type: 'user_joined' = 'user_joined';
/**
* 加入的用户信息
*/
@ApiProperty({
description: '加入的用户信息',
example: {
userId: 'user3',
socketId: 'socket3',
joinedAt: 1641024000000,
status: 'online'
}
})
user: {
userId: string;
socketId: string;
joinedAt: number;
status: string;
metadata?: Record<string, any>;
};
/**
* 用户位置信息(如果有)
*/
@ApiPropertyOptional({
description: '用户位置信息',
example: {
x: 100,
y: 200,
mapId: 'plaza',
timestamp: 1641024000000
}
})
position?: {
x: number;
y: number;
mapId: string;
timestamp: number;
metadata?: Record<string, any>;
};
/**
* 会话ID
*/
@ApiProperty({
description: '会话ID',
example: 'session_12345'
})
sessionId: string;
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}
/**
* 用户离开通知响应DTO
*
* 职责:
* - 通知会话中其他用户有用户离开
* - 包含离开用户的ID和离开原因
* - 支持会话状态的实时更新
*/
export class UserLeftNotification {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'user_left',
enum: ['user_left']
})
type: 'user_left' = 'user_left';
/**
* 离开的用户ID
*/
@ApiProperty({
description: '离开的用户ID',
example: 'user3'
})
userId: string;
/**
* 离开原因
*/
@ApiProperty({
description: '离开原因',
example: 'user_left',
enum: ['user_left', 'connection_lost', 'kicked', 'timeout', 'error']
})
reason: string;
/**
* 会话ID
*/
@ApiProperty({
description: '会话ID',
example: 'session_12345'
})
sessionId: string;
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}
/**
* 位置广播响应DTO
*
* 职责:
* - 广播用户位置更新给会话中的其他用户
* - 包含完整的位置信息和时间戳
* - 支持位置数据的实时同步
*/
export class PositionBroadcast {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'position_broadcast',
enum: ['position_broadcast']
})
type: 'position_broadcast' = 'position_broadcast';
/**
* 更新位置的用户ID
*/
@ApiProperty({
description: '更新位置的用户ID',
example: 'user1'
})
userId: string;
/**
* 位置信息
*/
@ApiProperty({
description: '位置信息',
example: {
x: 150,
y: 250,
mapId: 'forest',
timestamp: 1641024000000
}
})
position: {
x: number;
y: number;
mapId: string;
timestamp: number;
metadata?: Record<string, any>;
};
/**
* 会话ID
*/
@ApiProperty({
description: '会话ID',
example: 'session_12345'
})
sessionId: string;
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}
/**
* 心跳响应DTO
*
* 职责:
* - 响应客户端的心跳检测请求
* - 提供服务端时间戳用于延迟计算
* - 维持WebSocket连接的活跃状态
*/
export class HeartbeatResponse {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'heartbeat_response',
enum: ['heartbeat_response']
})
type: 'heartbeat_response' = 'heartbeat_response';
/**
* 客户端时间戳(回显)
*/
@ApiProperty({
description: '客户端时间戳',
example: 1641024000000
})
clientTimestamp: number;
/**
* 服务端时间戳
*/
@ApiProperty({
description: '服务端时间戳',
example: 1641024000100
})
serverTimestamp: number;
/**
* 序列号(回显)
*/
@ApiPropertyOptional({
description: '心跳序列号',
example: 1
})
sequence?: number;
}
/**
* 错误响应DTO
*
* 职责:
* - 定义WebSocket通信中的错误响应格式
* - 提供详细的错误信息和错误代码
* - 支持客户端的错误处理和用户提示
*/
export class ErrorResponse {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'error',
enum: ['error']
})
type: 'error' = 'error';
/**
* 错误代码
*/
@ApiProperty({
description: '错误代码',
example: 'INVALID_TOKEN',
enum: [
'INVALID_TOKEN',
'SESSION_NOT_FOUND',
'SESSION_FULL',
'INVALID_POSITION',
'RATE_LIMIT_EXCEEDED',
'INTERNAL_ERROR',
'VALIDATION_ERROR',
'PERMISSION_DENIED'
]
})
code: string;
/**
* 错误消息
*/
@ApiProperty({
description: '错误消息',
example: '无效的认证令牌'
})
message: string;
/**
* 错误详情(可选)
*/
@ApiPropertyOptional({
description: '错误详情',
example: {
field: 'token',
reason: 'expired'
}
})
details?: Record<string, any>;
/**
* 原始消息(可选,用于错误追踪)
*/
@ApiPropertyOptional({
description: '引起错误的原始消息',
example: {
type: 'join_session',
sessionId: 'invalid_session'
}
})
originalMessage?: any;
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}
/**
* 成功响应DTO
*
* 职责:
* - 定义通用的成功响应格式
* - 用于确认操作成功完成
* - 提供操作结果的反馈
*/
export class SuccessResponse {
/**
* 响应类型标识
*/
@ApiProperty({
description: '响应类型',
example: 'success',
enum: ['success']
})
type: 'success' = 'success';
/**
* 成功消息
*/
@ApiProperty({
description: '成功消息',
example: '操作成功完成'
})
message: string;
/**
* 操作类型
*/
@ApiProperty({
description: '操作类型',
example: 'position_update',
enum: ['join_session', 'leave_session', 'position_update', 'heartbeat']
})
operation: string;
/**
* 结果数据(可选)
*/
@ApiPropertyOptional({
description: '操作结果数据',
example: {
affected: 1,
duration: 50
}
})
data?: Record<string, any>;
/**
* 响应时间戳
*/
@ApiProperty({
description: '响应时间戳',
example: 1641024000000
})
timestamp: number;
}