522 lines
12 KiB
TypeScript
522 lines
12 KiB
TypeScript
/**
|
||
* 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;
|
||
} |