// video-media.dto.ts import { IsInt, IsOptional, IsString, IsIn, IsArray, IsMongoId, Min, Max, MaxLength, } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { PageListDto } from '@box/common/dto/page-list.dto'; export class VideoMediaListQueryDto extends PageListDto { /** * 搜索关键词:匹配标题、文件名等(具体逻辑在 service 里实现) */ @ApiPropertyOptional({ type: String, maxLength: 100, description: '搜索关键词,用于匹配视频标题、文件名等', example: '产品演示视频', }) @IsOptional() @IsString() @MaxLength(100) keyword?: string; // search tags @ApiPropertyOptional({ type: String, maxLength: 100, description: '搜索标签关键词,用于匹配视频标签', example: '演示', }) @IsOptional() @IsString() @MaxLength(100) tag?: string; /** * 上/下架状态过滤 * 0 = 下架, 1 = 上架 */ @ApiPropertyOptional({ type: Number, enum: [0, 1], description: '上下架状态过滤: 0=下架, 1=上架', example: 1, }) @IsOptional() @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus?: number; /** * 搜索 更新时间 范围 - 开始时间(EPOCH 秒) */ @ApiPropertyOptional({ type: Number, description: '更新时间范围过滤的开始时间(EPOCH 毫秒)', example: 1700000000, }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) updatedFrom?: number; /** * 搜索 更新时间 范围 - 结束时间(EPOCH 秒) */ @ApiPropertyOptional({ type: Number, description: '更新时间范围过滤的结束时间(EPOCH 毫秒)', example: 1705000000, }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) updatedTo?: number; } export class UpdateVideoMediaManageDto { @ApiPropertyOptional({ type: String, maxLength: 200, description: '视频标题', example: '产品演示视频 2025', }) @IsOptional() @IsString() @MaxLength(200) title?: string; /** * 分类 ID,可为空(取消分类) */ @ApiPropertyOptional({ type: String, nullable: true, description: '视频分类 MongoDB ID,设为 null 可取消分类', example: '507f1f77bcf86cd799439011', }) @IsOptional() @IsMongoId() categoryId?: string | null; /** * 标签 ID 列表,最多 5 个 */ @ApiPropertyOptional({ type: [String], maxItems: 5, description: '视频标签 MongoDB ID 列表,最多 5 个', example: ['507f1f77bcf86cd799439012', '507f1f77bcf86cd799439013'], }) @IsOptional() @IsArray() @IsMongoId({ each: true }) tagIds?: string[]; /** * 上/下架状态,也可以在这里一起保存(可选) * 0 = 下架, 1 = 上架 */ @ApiPropertyOptional({ type: Number, enum: [0, 1], description: '上下架状态: 0=下架, 1=上架', example: 1, }) @IsOptional() @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus?: number; } export class UpdateVideoMediaStatusDto { @ApiProperty({ type: Number, enum: [0, 1], description: '上下架状态: 0=下架, 1=上架', example: 1, }) @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus!: number; } export class BatchUpdateVideoMediaStatusDto { @ApiProperty({ type: [String], description: 'MongoDB 视频 ID 列表', example: ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'], }) @IsArray() @IsMongoId({ each: true }) ids!: string[]; @ApiProperty({ type: Number, enum: [0, 1], description: '上下架状态: 0=下架, 1=上架', example: 1, }) @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus!: number; } export class UpdateVideoMediaCoverResponseDto { @ApiProperty({ type: String, description: '视频 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @IsString() id!: string; @ApiProperty({ type: String, description: '新的封面图片 URL 或 S3 key', example: 'https://s3.example.com/covers/507f1f77bcf86cd799439011.jpg', }) @IsString() coverImg!: string; /** * 新 editedAt 值 */ @ApiProperty({ type: Number, description: '编辑时间戳(EPOCH 毫秒)', example: 1733304600000, }) @IsInt() editedAt!: number; } export class VideoMediaListItemDto { @ApiProperty({ type: String, description: '视频 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @IsString() id!: string; @ApiProperty({ type: String, description: '视频标题', example: '产品演示视频', }) @IsString() title!: string; @ApiProperty({ type: String, description: '视频文件名', example: 'product-demo.mp4', }) @IsString() filename!: string; /** * 视频时长(秒) */ @ApiProperty({ type: Number, description: '视频时长(秒)', example: 120, }) @Type(() => Number) @IsInt() videoTime!: number; /** * 文件大小(业务上 BigInt 存储,DTO 建议用字符串避免精度问题) */ @ApiProperty({ type: String, description: '文件大小(字节),以字符串形式存储避免精度问题', example: '1048576000', }) @IsString() size!: string; /** * 封面 URL / key */ @ApiProperty({ type: String, description: '视频封面 URL 或 S3 key', example: 'https://s3.example.com/covers/507f1f77bcf86cd799439011.jpg', }) @IsString() coverImg!: string; /** * 分类 ID,可空 */ @ApiPropertyOptional({ type: String, nullable: true, description: '视频分类 MongoDB ID', example: '507f1f77bcf86cd799439012', }) @IsOptional() @IsMongoId() categoryId?: string | null; /** * 当前选中的标签 ID 列表(最多 5 个) */ @ApiProperty({ type: [String], description: '视频标签 MongoDB ID 列表', example: ['507f1f77bcf86cd799439013', '507f1f77bcf86cd799439014'], }) @IsArray() @IsMongoId({ each: true }) tagIds!: string[]; /** * 上/下架状态 0 = 下架, 1 = 上架 */ @ApiProperty({ type: Number, enum: [0, 1], description: '上下架状态: 0=下架, 1=上架', example: 1, }) @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus!: number; /** * 编辑时间戳(EPOCH 毫秒) */ @ApiProperty({ type: Number, description: '编辑时间戳(EPOCH 毫秒)', example: 1733304600000, }) @IsInt() editedAt!: number; } export class VideoMediaDetailDto { @ApiProperty({ type: String, description: '视频 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @IsString() id!: string; // --- Provider fields (read-only for mgnt) --- @ApiProperty({ type: String, description: '视频标题', example: '产品演示视频', }) @IsString() title!: string; @ApiProperty({ type: String, description: '视频文件名', example: 'product-demo.mp4', }) @IsString() filename!: string; @ApiProperty({ type: Number, description: '视频时长(秒)', example: 120, }) @Type(() => Number) @IsInt() videoTime!: number; @ApiProperty({ type: String, description: '文件大小(字节),以字符串形式存储避免精度问题', example: '1048576000', }) @IsString() size!: string; @ApiProperty({ type: String, description: '视频封面 URL 或 S3 key', example: 'https://s3.example.com/covers/507f1f77bcf86cd799439011.jpg', }) @IsString() coverImg!: string; @ApiProperty({ type: String, description: '视频类型', example: 'video/mp4', }) @IsString() type!: string; @ApiProperty({ type: Number, description: '视频格式类型编码', example: 1, }) @Type(() => Number) @IsInt() formatType!: number; @ApiProperty({ type: Number, description: '内容类型编码', example: 1, }) @Type(() => Number) @IsInt() contentType!: number; @ApiProperty({ type: String, description: '国家/地区代码', example: 'CN', }) @IsString() country!: string; @ApiProperty({ type: String, description: '视频状态', example: 'active', }) @IsString() status!: string; @ApiProperty({ type: String, description: '视频描述', example: '这是一个产品演示视频', }) @IsString() desc!: string; // --- Local business fields --- @ApiPropertyOptional({ type: String, nullable: true, description: '视频分类 MongoDB ID', example: '507f1f77bcf86cd799439012', }) @IsOptional() @IsMongoId() categoryId?: string | null; @ApiProperty({ type: [String], description: '视频标签 MongoDB ID 列表', example: ['507f1f77bcf86cd799439013', '507f1f77bcf86cd799439014'], }) @IsArray() @IsMongoId({ each: true }) tagIds!: string[]; @ApiProperty({ type: Number, enum: [0, 1], description: '上下架状态: 0=下架, 1=上架', example: 1, }) @Type(() => Number) @IsInt() @IsIn([0, 1]) listStatus!: number; @ApiProperty({ type: Number, description: '编辑时间戳(EPOCH 毫秒)', example: 1733304600000, }) @IsInt() editedAt!: number; // --- Optional denormalized information for UI convenience --- @ApiPropertyOptional({ type: String, nullable: true, description: '分类名称(反范式化字段,用于 UI 便利性)', example: '产品介绍', }) @IsOptional() @IsString() categoryName?: string | null; @ApiPropertyOptional({ type: [Object], description: '标签对象数组(反范式化字段,包含 id 和 name)', example: [ { id: '507f1f77bcf86cd799439013', name: '演示' }, { id: '507f1f77bcf86cd799439014', name: '产品' }, ], }) @IsOptional() @IsArray() tags?: { id: string; name: string }[]; }