| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- // 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 { 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;
- 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) {
- const getBuffer = async (file: any): Promise<Buffer | undefined> => {
- if (!file) return undefined;
- if (Buffer.isBuffer(file)) return file;
- if (file instanceof Uint8Array) return Buffer.from(file);
- const candidate =
- file.buffer ?? file.data ?? file.value ?? file._buf ?? undefined;
- if (Buffer.isBuffer(candidate)) return candidate;
- if (candidate instanceof Uint8Array) return Buffer.from(candidate);
- if (typeof candidate === 'string') return Buffer.from(candidate);
- if (typeof file.toBuffer === 'function') {
- const buf = await file.toBuffer();
- if (Buffer.isBuffer(buf)) return buf;
- }
- if (file.file) {
- const chunks: Buffer[] = [];
- for await (const chunk of file.file) {
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
- }
- if (chunks.length > 0) {
- return Buffer.concat(chunks);
- }
- }
- return undefined;
- };
- // fastify multipart
- const reqAny = req as any;
- const bodyFile = reqAny.body?.file;
- let mpFile = Array.isArray(bodyFile) ? bodyFile[0] : bodyFile;
- if (!mpFile && reqAny.isMultipart?.()) {
- mpFile = await reqAny.file();
- }
- if (!mpFile) {
- throw new BadRequestException('No file uploaded');
- }
- const buf = await getBuffer(mpFile);
- if (!buf?.length) {
- throw new BadRequestException('Empty file');
- }
- return this.videoMediaService.importExcelTags(buf);
- }
- /**
- * 导出所有视频媒体为 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);
- }
- }
|