video-media.controller.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. // apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts
  2. import {
  3. Controller,
  4. Get,
  5. Param,
  6. Query,
  7. Patch,
  8. Body,
  9. Post,
  10. Delete,
  11. Req,
  12. Res,
  13. BadRequestException,
  14. } from '@nestjs/common';
  15. import type { FastifyReply, FastifyRequest } from 'fastify';
  16. import {
  17. ApiTags,
  18. ApiOperation,
  19. ApiParam,
  20. ApiConsumes,
  21. ApiBody,
  22. ApiResponse,
  23. ApiOkResponse,
  24. ApiNotFoundResponse,
  25. ApiBadRequestResponse,
  26. } from '@nestjs/swagger';
  27. import type { MultipartFile } from '@fastify/multipart';
  28. import { VideoMediaService } from './video-media.service';
  29. import {
  30. VideoMediaListQueryDto,
  31. UpdateVideoMediaTagsDto,
  32. UpdateVideoMediaStatusDto,
  33. BatchUpdateVideoMediaStatusDto,
  34. VideoMediaListItemDto,
  35. VideoMediaDetailDto,
  36. UpdateVideoMediaCoverResponseDto,
  37. } from './video-media.dto';
  38. @ApiTags('视频管理 (Video Media Management)')
  39. @Controller('video-media')
  40. export class VideoMediaController {
  41. constructor(private readonly videoMediaService: VideoMediaService) {}
  42. /**
  43. * 列表查询
  44. * POST /video-media/list
  45. */
  46. @ApiOperation({
  47. summary: '获取视频媒体列表',
  48. description: '分页查询视频列表,支持关键词搜索和上下架状态过滤',
  49. })
  50. @ApiOkResponse({
  51. description: '返回视频媒体分页列表',
  52. schema: {
  53. type: 'object',
  54. properties: {
  55. data: {
  56. type: 'array',
  57. items: { $ref: '#/components/schemas/VideoMediaListItemDto' },
  58. },
  59. total: { type: 'number', example: 100 },
  60. page: { type: 'number', example: 1 },
  61. pageSize: { type: 'number', example: 20 },
  62. },
  63. },
  64. })
  65. @ApiBadRequestResponse({
  66. description: '请求参数验证失败',
  67. })
  68. @Post('list')
  69. async findAll(@Body() dto: VideoMediaListQueryDto) {
  70. return this.videoMediaService.findAll(dto);
  71. }
  72. /**
  73. * 详情(管理弹窗)
  74. * GET /video-media/:id
  75. */
  76. @ApiOperation({
  77. summary: '获取视频媒体详情',
  78. description:
  79. '获取单个视频媒体的完整详细信息,包括基础属性、管理信息和反范式化数据',
  80. })
  81. @ApiParam({
  82. name: 'id',
  83. type: String,
  84. description: '视频媒体 MongoDB ID',
  85. example: '507f1f77bcf86cd799439011',
  86. })
  87. @ApiOkResponse({
  88. description: '返回视频媒体详情',
  89. type: VideoMediaDetailDto,
  90. })
  91. @ApiNotFoundResponse({
  92. description: '视频媒体不存在',
  93. })
  94. @Get(':id')
  95. async findOne(@Param('id') id: string) {
  96. return this.videoMediaService.findOne(id);
  97. }
  98. /**
  99. * 管理弹窗保存(标题 / 分类 / 标签 / 上下架)
  100. * PATCH /video-media/:id/manage
  101. */
  102. @ApiOperation({
  103. summary: '更新视频媒体管理信息',
  104. description: '更新视频的标题、分类、标签、上下架状态等管理级别信息',
  105. })
  106. @ApiBody({
  107. type: UpdateVideoMediaTagsDto,
  108. description: '更新的管理信息',
  109. })
  110. @Post('update-video-tags')
  111. async updateManage(@Body() dto: UpdateVideoMediaTagsDto) {
  112. return this.videoMediaService.updateVideoTags(dto);
  113. }
  114. /**
  115. * 单个上/下架
  116. * PATCH /video-media/:id/status
  117. */
  118. @ApiOperation({
  119. summary: '更新视频媒体上下架状态',
  120. description: '对单个视频进行上架(1)或下架(0)操作',
  121. })
  122. @ApiParam({
  123. name: 'id',
  124. type: String,
  125. description: '视频媒体 MongoDB ID',
  126. example: '507f1f77bcf86cd799439011',
  127. })
  128. @ApiBody({
  129. type: UpdateVideoMediaStatusDto,
  130. description: '上下架状态信息',
  131. })
  132. @ApiOkResponse({
  133. description: '状态更新成功',
  134. type: VideoMediaDetailDto,
  135. })
  136. @ApiNotFoundResponse({
  137. description: '视频媒体不存在',
  138. })
  139. @ApiBadRequestResponse({
  140. description: '请求参数验证失败',
  141. })
  142. @Patch(':id/status')
  143. async updateStatus(
  144. @Param('id') id: string,
  145. @Body() dto: UpdateVideoMediaStatusDto,
  146. ) {
  147. return this.videoMediaService.updateStatus(id, dto);
  148. }
  149. /**
  150. * 批量上/下架
  151. * POST /video-media/batch/status
  152. */
  153. @ApiOperation({
  154. summary: '批量更新视频媒体上下架状态',
  155. description: '对多个视频进行批量上架或下架操作',
  156. })
  157. @ApiBody({
  158. type: BatchUpdateVideoMediaStatusDto,
  159. description: '批量更新信息,包含 ID 列表和目标状态',
  160. })
  161. @ApiOkResponse({
  162. description: '批量更新成功',
  163. schema: {
  164. type: 'object',
  165. properties: {
  166. success: { type: 'number', example: 10 },
  167. failed: { type: 'number', example: 0 },
  168. },
  169. },
  170. })
  171. @ApiBadRequestResponse({
  172. description: '请求参数验证失败',
  173. })
  174. @Post('batch/status')
  175. async batchUpdateStatus(@Body() dto: BatchUpdateVideoMediaStatusDto) {
  176. return this.videoMediaService.batchUpdateStatus(dto);
  177. }
  178. /**
  179. * 封面上传:
  180. * - 前端通过 multipart/form-data 上传文件
  181. * - 这里示例使用 FileInterceptor;实际中你会上传到 S3,得到一个 URL / key
  182. * POST /video-media/:id/cover
  183. */
  184. @ApiOperation({
  185. summary: '上传视频封面',
  186. description: '为指定视频上传或更新自定义封面图片,支持上传至 S3 存储',
  187. })
  188. @ApiParam({
  189. name: 'id',
  190. type: String,
  191. description: '视频媒体 MongoDB ID',
  192. example: '507f1f77bcf86cd799439011',
  193. })
  194. @ApiConsumes('multipart/form-data')
  195. @ApiBody({
  196. description: '封面图片文件上传',
  197. schema: {
  198. type: 'object',
  199. properties: {
  200. file: {
  201. type: 'string',
  202. format: 'binary',
  203. description: '图片文件(支持 JPG、PNG 等常见格式)',
  204. },
  205. },
  206. required: ['file'],
  207. },
  208. })
  209. @ApiOkResponse({
  210. description: '封面上传成功',
  211. type: UpdateVideoMediaCoverResponseDto,
  212. })
  213. @ApiNotFoundResponse({
  214. description: '视频媒体不存在',
  215. })
  216. @ApiBadRequestResponse({
  217. description: '文件格式或大小不符合要求',
  218. })
  219. @Post(':id/cover')
  220. async updateCover(@Param('id') id: string, @Req() req: FastifyRequest) {
  221. const reqAny = req as any;
  222. const bodyFile = reqAny.body?.file ?? reqAny.body?.files;
  223. let mpFile = Array.isArray(bodyFile) ? bodyFile[0] : bodyFile;
  224. if (!mpFile && reqAny.isMultipart?.()) {
  225. mpFile = await reqAny.file();
  226. }
  227. if (!mpFile) {
  228. throw new BadRequestException('No file uploaded');
  229. }
  230. return this.videoMediaService.updateCover(id, mpFile);
  231. }
  232. // TODO: 删除视频媒体
  233. @ApiOperation({
  234. summary: '删除视频媒体',
  235. description: '根据 ID 删除指定的视频媒体',
  236. })
  237. @ApiParam({
  238. name: 'id',
  239. type: String,
  240. description: '视频媒体 MongoDB ID',
  241. example: '507f1f77bcf86cd799439011',
  242. })
  243. @ApiOkResponse({
  244. description: '删除成功',
  245. schema: {
  246. type: 'object',
  247. properties: {
  248. id: { type: 'string', example: '507f1f77bcf86cd799439011' },
  249. },
  250. },
  251. })
  252. @ApiNotFoundResponse({
  253. description: '视频媒体不存在',
  254. })
  255. @Delete(':id')
  256. async delete(@Param('id') id: string) {
  257. return this.videoMediaService.delete(id);
  258. }
  259. /**
  260. * 导入 Excel 标签
  261. * POST /video-media/import/excel-tags
  262. */
  263. @ApiOperation({
  264. summary: '导入视频标签',
  265. description: '从 Excel 文件导入视频标签并更新视频媒体',
  266. })
  267. @ApiConsumes('multipart/form-data')
  268. @ApiBody({
  269. schema: {
  270. type: 'object',
  271. properties: {
  272. file: { type: 'string', format: 'binary' },
  273. },
  274. required: ['file'],
  275. },
  276. })
  277. @Post('import/excel-tags')
  278. async importExcelTags(@Req() req: FastifyRequest, @Body() _body: any) {
  279. // fastify multipart
  280. const reqAny = req as any;
  281. const body = reqAny.body;
  282. const bodyFile = body?.file ?? body?.files;
  283. let mpFile: MultipartFile | undefined = Array.isArray(bodyFile)
  284. ? bodyFile[0]
  285. : bodyFile;
  286. if (!mpFile && body && typeof body === 'object') {
  287. const values = Object.values(body).flatMap((value: any) =>
  288. Array.isArray(value) ? value : [value],
  289. );
  290. mpFile = values.find(
  291. (value: any) =>
  292. value &&
  293. (typeof value.toBuffer === 'function' ||
  294. value.file ||
  295. value.filename),
  296. );
  297. }
  298. if (!mpFile && reqAny.isMultipart?.()) {
  299. mpFile = await reqAny.file();
  300. }
  301. if (!mpFile) {
  302. throw new BadRequestException('No file uploaded');
  303. }
  304. return this.videoMediaService.importExcelTags(mpFile);
  305. }
  306. /**
  307. * 导出所有视频媒体为 Excel
  308. * GET /video-media/export/excel
  309. */
  310. @Get('export/excel')
  311. async exportExcel(@Res() res: FastifyReply) {
  312. const buffer = await this.videoMediaService.exportExcel();
  313. res
  314. .header(
  315. 'Content-Type',
  316. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  317. )
  318. .header('Content-Disposition', 'attachment; filename="video-media.xlsx"')
  319. .send(buffer);
  320. }
  321. }