/** * 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; } /** * 加入会话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; } /** * 会话查询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 { @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 { @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; }