Просмотр исходного кода

feat(video-media): enhance API documentation and DTOs for video media management

Dave 4 месяцев назад
Родитель
Сommit
2f0b74e309

+ 123 - 23
apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts

@@ -18,6 +18,9 @@ import {
   ApiConsumes,
   ApiBody,
   ApiResponse,
+  ApiOkResponse,
+  ApiNotFoundResponse,
+  ApiBadRequestResponse,
 } from '@nestjs/swagger';
 import { VideoMediaService } from './video-media.service';
 import {
@@ -25,22 +28,43 @@ import {
   UpdateVideoMediaManageDto,
   UpdateVideoMediaStatusDto,
   BatchUpdateVideoMediaStatusDto,
+  VideoMediaListItemDto,
+  VideoMediaDetailDto,
+  UpdateVideoMediaCoverResponseDto,
 } from './video-media.dto';
 
-@ApiTags('视频管理')
+@ApiTags('视频管理 (Video Media Management)')
 @Controller('video-media')
 export class VideoMediaController {
   constructor(private readonly videoMediaService: VideoMediaService) {}
 
   /**
    * 列表查询
-   * GET /video-media
+   * POST /video-media/list
    */
   @ApiOperation({
     summary: '获取视频媒体列表',
-    description: '支持分页、关键词搜索、分类和标签过滤',
+    description:
+      '分页查询视频列表,支持关键词搜索、分类过滤、标签过滤和上下架状态过滤',
+  })
+  @ApiOkResponse({
+    description: '返回视频媒体分页列表',
+    schema: {
+      type: 'object',
+      properties: {
+        data: {
+          type: 'array',
+          items: { $ref: '#/components/schemas/VideoMediaListItemDto' },
+        },
+        total: { type: 'number', example: 100 },
+        page: { type: 'number', example: 1 },
+        pageSize: { type: 'number', example: 20 },
+      },
+    },
+  })
+  @ApiBadRequestResponse({
+    description: '请求参数验证失败',
   })
-  @ApiResponse({ status: 200, description: '返回视频媒体列表' })
   @Post('list')
   async findAll(@Body() dto: VideoMediaListQueryDto) {
     return this.videoMediaService.findAll(dto);
@@ -52,11 +76,22 @@ export class VideoMediaController {
    */
   @ApiOperation({
     summary: '获取视频媒体详情',
-    description: '获取单个视频媒体的详细信息',
+    description:
+      '获取单个视频媒体的完整详细信息,包括基础属性、管理信息和反范式化数据',
+  })
+  @ApiParam({
+    name: 'id',
+    type: String,
+    description: '视频媒体 MongoDB ID',
+    example: '507f1f77bcf86cd799439011',
+  })
+  @ApiOkResponse({
+    description: '返回视频媒体详情',
+    type: VideoMediaDetailDto,
+  })
+  @ApiNotFoundResponse({
+    description: '视频媒体不存在',
   })
-  @ApiParam({ name: 'id', description: '视频媒体ID', type: String })
-  @ApiResponse({ status: 200, description: '返回视频媒体详情' })
-  @ApiResponse({ status: 404, description: '视频媒体不存在' })
   @Get(':id')
   async findOne(@Param('id') id: string) {
     return this.videoMediaService.findOne(id);
@@ -68,11 +103,28 @@ export class VideoMediaController {
    */
   @ApiOperation({
     summary: '更新视频媒体管理信息',
-    description: '更新标题、分类、标签、上下架状态等信息',
+    description: '更新视频的标题、分类、标签、上下架状态等管理级别信息',
+  })
+  @ApiParam({
+    name: 'id',
+    type: String,
+    description: '视频媒体 MongoDB ID',
+    example: '507f1f77bcf86cd799439011',
+  })
+  @ApiBody({
+    type: UpdateVideoMediaManageDto,
+    description: '更新的管理信息',
+  })
+  @ApiOkResponse({
+    description: '更新成功',
+    type: VideoMediaDetailDto,
+  })
+  @ApiNotFoundResponse({
+    description: '视频媒体不存在',
+  })
+  @ApiBadRequestResponse({
+    description: '请求参数验证失败',
   })
-  @ApiParam({ name: 'id', description: '视频媒体ID', type: String })
-  @ApiResponse({ status: 200, description: '更新成功' })
-  @ApiResponse({ status: 404, description: '视频媒体不存在' })
   @Patch(':id/manage')
   async updateManage(
     @Param('id') id: string,
@@ -87,11 +139,28 @@ export class VideoMediaController {
    */
   @ApiOperation({
     summary: '更新视频媒体上下架状态',
-    description: '单个视频的上架或下架操作',
+    description: '对单个视频进行上架(1)或下架(0)操作',
+  })
+  @ApiParam({
+    name: 'id',
+    type: String,
+    description: '视频媒体 MongoDB ID',
+    example: '507f1f77bcf86cd799439011',
+  })
+  @ApiBody({
+    type: UpdateVideoMediaStatusDto,
+    description: '上下架状态信息',
+  })
+  @ApiOkResponse({
+    description: '状态更新成功',
+    type: VideoMediaDetailDto,
+  })
+  @ApiNotFoundResponse({
+    description: '视频媒体不存在',
+  })
+  @ApiBadRequestResponse({
+    description: '请求参数验证失败',
   })
-  @ApiParam({ name: 'id', description: '视频媒体ID', type: String })
-  @ApiResponse({ status: 200, description: '状态更新成功' })
-  @ApiResponse({ status: 404, description: '视频媒体不存在' })
   @Patch(':id/status')
   async updateStatus(
     @Param('id') id: string,
@@ -106,9 +175,25 @@ export class VideoMediaController {
    */
   @ApiOperation({
     summary: '批量更新视频媒体上下架状态',
-    description: '批量对多个视频进行上架或下架操作',
+    description: '对多个视频进行批量上架或下架操作',
+  })
+  @ApiBody({
+    type: BatchUpdateVideoMediaStatusDto,
+    description: '批量更新信息,包含 ID 列表和目标状态',
+  })
+  @ApiOkResponse({
+    description: '批量更新成功',
+    schema: {
+      type: 'object',
+      properties: {
+        success: { type: 'number', example: 10 },
+        failed: { type: 'number', example: 0 },
+      },
+    },
+  })
+  @ApiBadRequestResponse({
+    description: '请求参数验证失败',
   })
-  @ApiResponse({ status: 200, description: '批量更新成功' })
   @Post('batch/status')
   async batchUpdateStatus(@Body() dto: BatchUpdateVideoMediaStatusDto) {
     return this.videoMediaService.batchUpdateStatus(dto);
@@ -122,24 +207,39 @@ export class VideoMediaController {
    */
   @ApiOperation({
     summary: '上传视频封面',
-    description: '为指定视频上传自定义封面图片',
+    description: '为指定视频上传或更新自定义封面图片,支持上传至 S3 存储',
+  })
+  @ApiParam({
+    name: 'id',
+    type: String,
+    description: '视频媒体 MongoDB ID',
+    example: '507f1f77bcf86cd799439011',
   })
-  @ApiParam({ name: 'id', description: '视频媒体ID', type: String })
   @ApiConsumes('multipart/form-data')
   @ApiBody({
-    description: '封面图片文件',
+    description: '封面图片文件上传',
     schema: {
       type: 'object',
       properties: {
         file: {
           type: 'string',
           format: 'binary',
+          description: '图片文件(支持 JPG、PNG 等常见格式)',
         },
       },
+      required: ['file'],
     },
   })
-  @ApiResponse({ status: 200, description: '封面上传成功' })
-  @ApiResponse({ status: 404, description: '视频媒体不存在' })
+  @ApiOkResponse({
+    description: '封面上传成功',
+    type: UpdateVideoMediaCoverResponseDto,
+  })
+  @ApiNotFoundResponse({
+    description: '视频媒体不存在',
+  })
+  @ApiBadRequestResponse({
+    description: '文件格式或大小不符合要求',
+  })
   @Post(':id/cover')
   @UseInterceptors(FileInterceptor('file'))
   async updateCover(

+ 231 - 1
apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.dto.ts

@@ -12,12 +12,19 @@ import {
   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)
@@ -26,6 +33,11 @@ export class VideoMediaListQueryDto extends PageListDto {
   /**
    * 分类过滤:可选
    */
+  @ApiPropertyOptional({
+    type: String,
+    description: '视频分类 MongoDB ID',
+    example: '507f1f77bcf86cd799439011',
+  })
   @IsOptional()
   @IsMongoId()
   categoryId?: string;
@@ -33,6 +45,11 @@ export class VideoMediaListQueryDto extends PageListDto {
   /**
    * 标签过滤(通常前端只会传一个 tagId)
    */
+  @ApiPropertyOptional({
+    type: String,
+    description: '视频标签 MongoDB ID',
+    example: '507f1f77bcf86cd799439012',
+  })
   @IsOptional()
   @IsMongoId()
   tagId?: string;
@@ -41,6 +58,12 @@ export class VideoMediaListQueryDto extends PageListDto {
    * 上/下架状态过滤
    * 0 = 下架, 1 = 上架
    */
+  @ApiPropertyOptional({
+    type: Number,
+    enum: [0, 1],
+    description: '上下架状态过滤: 0=下架, 1=上架',
+    example: 1,
+  })
   @IsOptional()
   @Type(() => Number)
   @IsInt()
@@ -49,6 +72,12 @@ export class VideoMediaListQueryDto extends PageListDto {
 }
 
 export class UpdateVideoMediaManageDto {
+  @ApiPropertyOptional({
+    type: String,
+    maxLength: 200,
+    description: '视频标题',
+    example: '产品演示视频 2025',
+  })
   @IsOptional()
   @IsString()
   @MaxLength(200)
@@ -57,6 +86,12 @@ export class UpdateVideoMediaManageDto {
   /**
    * 分类 ID,可为空(取消分类)
    */
+  @ApiPropertyOptional({
+    type: String,
+    nullable: true,
+    description: '视频分类 MongoDB ID,设为 null 可取消分类',
+    example: '507f1f77bcf86cd799439011',
+  })
   @IsOptional()
   @IsMongoId()
   categoryId?: string | null;
@@ -64,6 +99,12 @@ export class UpdateVideoMediaManageDto {
   /**
    * 标签 ID 列表,最多 5 个
    */
+  @ApiPropertyOptional({
+    type: [String],
+    maxItems: 5,
+    description: '视频标签 MongoDB ID 列表,最多 5 个',
+    example: ['507f1f77bcf86cd799439012', '507f1f77bcf86cd799439013'],
+  })
   @IsOptional()
   @IsArray()
   @IsMongoId({ each: true })
@@ -73,6 +114,12 @@ export class UpdateVideoMediaManageDto {
    * 上/下架状态,也可以在这里一起保存(可选)
    * 0 = 下架, 1 = 上架
    */
+  @ApiPropertyOptional({
+    type: Number,
+    enum: [0, 1],
+    description: '上下架状态: 0=下架, 1=上架',
+    example: 1,
+  })
   @IsOptional()
   @Type(() => Number)
   @IsInt()
@@ -81,6 +128,12 @@ export class UpdateVideoMediaManageDto {
 }
 
 export class UpdateVideoMediaStatusDto {
+  @ApiProperty({
+    type: Number,
+    enum: [0, 1],
+    description: '上下架状态: 0=下架, 1=上架',
+    example: 1,
+  })
   @Type(() => Number)
   @IsInt()
   @IsIn([0, 1])
@@ -88,10 +141,21 @@ export class UpdateVideoMediaStatusDto {
 }
 
 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])
@@ -99,32 +163,68 @@ export class BatchUpdateVideoMediaStatusDto {
 }
 
 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: String,
+    format: 'date-time',
+    description: '更新后的编辑时间戳(ISO 8601 格式)',
+    example: '2025-12-04T10:30:00.000Z',
+  })
   @IsString()
   editedAt!: string;
 }
 
 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;
@@ -132,18 +232,34 @@ export class VideoMediaListItemDto {
   /**
    * 文件大小(业务上 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;
@@ -151,6 +267,11 @@ export class VideoMediaListItemDto {
   /**
    * 当前选中的标签 ID 列表(最多 5 个)
    */
+  @ApiProperty({
+    type: [String],
+    description: '视频标签 MongoDB ID 列表',
+    example: ['507f1f77bcf86cd799439013', '507f1f77bcf86cd799439014'],
+  })
   @IsArray()
   @IsMongoId({ each: true })
   tagIds!: string[];
@@ -158,6 +279,12 @@ export class VideoMediaListItemDto {
   /**
    * 上/下架状态 0 = 下架, 1 = 上架
    */
+  @ApiProperty({
+    type: Number,
+    enum: [0, 1],
+    description: '上下架状态: 0=下架, 1=上架',
+    example: 1,
+  })
   @Type(() => Number)
   @IsInt()
   @IsIn([0, 1])
@@ -166,76 +293,179 @@ export class VideoMediaListItemDto {
   /**
    * 本地编辑时间(BigInt epoch)— 字符串返回
    */
+  @ApiProperty({
+    type: String,
+    format: 'date-time',
+    description: '编辑时间戳(ISO 8601 格式)',
+    example: '2025-12-04T10:30:00.000Z',
+  })
   @IsString()
   editedAt!: string;
 }
 
 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; // from BigInt
+  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: String,
+    format: 'date-time',
+    description: '编辑时间戳(ISO 8601 格式)',
+    example: '2025-12-04T10:30:00.000Z',
+  })
   @IsString()
   editedAt!: string;
 
   // --- 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 }[];