Files
whale-town-end/src/business/location_broadcast/dto/api.dto.ts
moyin c31cbe559d feat:实现位置广播系统
- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
2026-01-08 23:05:52 +08:00

522 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}