// apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts import { Controller, Get, Param, Query, Patch, Body, Post, Delete, Req, Res, BadRequestException, } from '@nestjs/common'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { ApiTags, ApiOperation, ApiParam, ApiConsumes, ApiBody, ApiResponse, ApiOkResponse, ApiNotFoundResponse, ApiBadRequestResponse, } from '@nestjs/swagger'; import type { MultipartFile } from '@fastify/multipart'; import { VideoMediaService } from './video-media.service'; import { VideoMediaListQueryDto, UpdateVideoMediaTagsDto, UpdateVideoMediaStatusDto, BatchUpdateVideoMediaStatusDto, VideoMediaListItemDto, VideoMediaDetailDto, UpdateVideoMediaCoverResponseDto, } from './video-media.dto'; @ApiTags('视频管理 (Video Media Management)') @Controller('video-media') export class VideoMediaController { constructor(private readonly videoMediaService: VideoMediaService) {} /** * 列表查询 * POST /video-media/list */ @ApiOperation({ summary: '获取视频媒体列表', 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: '请求参数验证失败', }) @Post('list') async findAll(@Body() dto: VideoMediaListQueryDto) { return this.videoMediaService.findAll(dto); } /** * 详情(管理弹窗) * GET /video-media/:id */ @ApiOperation({ summary: '获取视频媒体详情', description: '获取单个视频媒体的完整详细信息,包括基础属性、管理信息和反范式化数据', }) @ApiParam({ name: 'id', type: String, description: '视频媒体 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @ApiOkResponse({ description: '返回视频媒体详情', type: VideoMediaDetailDto, }) @ApiNotFoundResponse({ description: '视频媒体不存在', }) @Get(':id') async findOne(@Param('id') id: string) { return this.videoMediaService.findOne(id); } /** * 管理弹窗保存(标题 / 分类 / 标签 / 上下架) * PATCH /video-media/:id/manage */ @ApiOperation({ summary: '更新视频媒体管理信息', description: '更新视频的标题、分类、标签、上下架状态等管理级别信息', }) @ApiBody({ type: UpdateVideoMediaTagsDto, description: '更新的管理信息', }) @Post('update-video-tags') async updateManage(@Body() dto: UpdateVideoMediaTagsDto) { return this.videoMediaService.updateVideoTags(dto); } /** * 单个上/下架 * PATCH /video-media/:id/status */ @ApiOperation({ summary: '更新视频媒体上下架状态', 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: '请求参数验证失败', }) @Patch(':id/status') async updateStatus( @Param('id') id: string, @Body() dto: UpdateVideoMediaStatusDto, ) { return this.videoMediaService.updateStatus(id, dto); } /** * 批量上/下架 * POST /video-media/batch/status */ @ApiOperation({ summary: '批量更新视频媒体上下架状态', description: '对多个视频进行批量上架或下架操作', }) @ApiBody({ type: BatchUpdateVideoMediaStatusDto, description: '批量更新信息,包含 ID 列表和目标状态', }) @ApiOkResponse({ description: '批量更新成功', schema: { type: 'object', properties: { success: { type: 'number', example: 10 }, failed: { type: 'number', example: 0 }, }, }, }) @ApiBadRequestResponse({ description: '请求参数验证失败', }) @Post('batch/status') async batchUpdateStatus(@Body() dto: BatchUpdateVideoMediaStatusDto) { return this.videoMediaService.batchUpdateStatus(dto); } /** * 封面上传: * - 前端通过 multipart/form-data 上传文件 * - 这里示例使用 FileInterceptor;实际中你会上传到 S3,得到一个 URL / key * POST /video-media/:id/cover */ @ApiOperation({ summary: '上传视频封面', description: '为指定视频上传或更新自定义封面图片,支持上传至 S3 存储', }) @ApiParam({ name: 'id', type: String, description: '视频媒体 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @ApiConsumes('multipart/form-data') @ApiBody({ description: '封面图片文件上传', schema: { type: 'object', properties: { file: { type: 'string', format: 'binary', description: '图片文件(支持 JPG、PNG 等常见格式)', }, }, required: ['file'], }, }) @ApiOkResponse({ description: '封面上传成功', type: UpdateVideoMediaCoverResponseDto, }) @ApiNotFoundResponse({ description: '视频媒体不存在', }) @ApiBadRequestResponse({ description: '文件格式或大小不符合要求', }) @Post(':id/cover') async updateCover(@Param('id') id: string, @Req() req: FastifyRequest) { const reqAny = req as any; const bodyFile = reqAny.body?.file ?? reqAny.body?.files; let mpFile = Array.isArray(bodyFile) ? bodyFile[0] : bodyFile; if (!mpFile && reqAny.isMultipart?.()) { mpFile = await reqAny.file(); } if (!mpFile) { throw new BadRequestException('No file uploaded'); } return this.videoMediaService.updateCover(id, mpFile); } // TODO: 删除视频媒体 @ApiOperation({ summary: '删除视频媒体', description: '根据 ID 删除指定的视频媒体', }) @ApiParam({ name: 'id', type: String, description: '视频媒体 MongoDB ID', example: '507f1f77bcf86cd799439011', }) @ApiOkResponse({ description: '删除成功', schema: { type: 'object', properties: { id: { type: 'string', example: '507f1f77bcf86cd799439011' }, }, }, }) @ApiNotFoundResponse({ description: '视频媒体不存在', }) @Delete(':id') async delete(@Param('id') id: string) { return this.videoMediaService.delete(id); } /** * 导入 Excel 标签 * POST /video-media/import/excel-tags */ @ApiOperation({ summary: '导入视频标签', description: '从 Excel 文件导入视频标签并更新视频媒体', }) @ApiConsumes('multipart/form-data') @ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' }, }, required: ['file'], }, }) @Post('import/excel-tags') async importExcelTags(@Req() req: FastifyRequest, @Body() _body: any) { // fastify multipart const reqAny = req as any; const body = reqAny.body; const bodyFile = body?.file ?? body?.files; let mpFile: MultipartFile | undefined = Array.isArray(bodyFile) ? bodyFile[0] : bodyFile; if (!mpFile && body && typeof body === 'object') { const values = Object.values(body).flatMap((value: any) => Array.isArray(value) ? value : [value], ); mpFile = values.find( (value: any) => value && (typeof value.toBuffer === 'function' || value.file || value.filename), ); } if (!mpFile && reqAny.isMultipart?.()) { mpFile = await reqAny.file(); } if (!mpFile) { throw new BadRequestException('No file uploaded'); } return this.videoMediaService.importExcelTags(mpFile); } /** * 导出所有视频媒体为 Excel * GET /video-media/export/excel */ @Get('export/excel') async exportExcel(@Res() res: FastifyReply) { const buffer = await this.videoMediaService.exportExcel(); res .header( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ) .header('Content-Disposition', 'attachment; filename="video-media.xlsx"') .send(buffer); } }