feat:实现位置广播系统
- 添加位置广播核心控制器和服务 - 实现健康检查和位置同步功能 - 添加WebSocket实时位置更新支持 - 完善位置广播的测试覆盖
This commit is contained in:
522
src/business/location_broadcast/dto/api.dto.ts
Normal file
522
src/business/location_broadcast/dto/api.dto.ts
Normal 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;
|
||||
}
|
||||
36
src/business/location_broadcast/dto/index.ts
Normal file
36
src/business/location_broadcast/dto/index.ts
Normal 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';
|
||||
334
src/business/location_broadcast/dto/websocket_message.dto.ts
Normal file
334
src/business/location_broadcast/dto/websocket_message.dto.ts
Normal 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;
|
||||
}
|
||||
524
src/business/location_broadcast/dto/websocket_response.dto.ts
Normal file
524
src/business/location_broadcast/dto/websocket_response.dto.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user