video-generator.controller.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * 视频生成模块 - API 路由控制器
  3. */
  4. import Router from '@koa/router';
  5. import { requireAuth } from '../../middleware/auth';
  6. import {
  7. createVideoProject,
  8. getVideoProjects,
  9. getVideoProject,
  10. updateVideoProject,
  11. deleteVideoProject,
  12. generateVideoForProject,
  13. getGenerateProgress,
  14. getMaterials,
  15. uploadMaterial,
  16. deleteMaterial,
  17. createVideoProjectFromBook,
  18. } from './video-generator.service';
  19. const router = new Router();
  20. // 测试用户兜底(与其他模块一致)
  21. const TEST_USER_ID = 1;
  22. function currentUserId(ctx: any): number {
  23. const uid = ctx.state.user?.userId;
  24. return uid ? parseInt(String(uid)) : TEST_USER_ID;
  25. }
  26. // ============ 视频项目管理 ============
  27. /**
  28. * GET /api/video/projects
  29. * 获取视频项目列表(只返回当前用户的项目)
  30. */
  31. router.get('/projects', requireAuth, async (ctx) => {
  32. const query = {
  33. userId: currentUserId(ctx), // 强制用当前登录用户,忽略客户端传入的 userId
  34. status: ctx.query.status as any,
  35. page: ctx.query.page ? Number(ctx.query.page) : 1,
  36. pageSize: ctx.query.pageSize ? Number(ctx.query.pageSize) : 10,
  37. };
  38. const result = await getVideoProjects(query);
  39. ctx.body = { success: true, data: result };
  40. });
  41. /**
  42. * POST /api/video/projects
  43. * 创建视频项目
  44. */
  45. router.post('/projects', requireAuth, async (ctx) => {
  46. const body = ctx.request.body as any;
  47. const userId = currentUserId(ctx);
  48. const project = await createVideoProject(body, userId);
  49. ctx.body = { success: true, data: project };
  50. });
  51. /**
  52. * GET /api/video/projects/:id
  53. * 获取视频项目详情
  54. */
  55. router.get('/projects/:id', requireAuth, async (ctx) => {
  56. const id = Number(ctx.params.id);
  57. const project = await getVideoProject(id, currentUserId(ctx));
  58. if (!project) {
  59. ctx.status = 404;
  60. ctx.body = { success: false, error: '项目不存在' };
  61. return;
  62. }
  63. ctx.body = { success: true, data: project };
  64. });
  65. /**
  66. * PUT /api/video/projects/:id
  67. * 更新视频项目
  68. */
  69. router.put('/projects/:id', requireAuth, async (ctx) => {
  70. const id = Number(ctx.params.id);
  71. const body = ctx.request.body as any;
  72. const project = await updateVideoProject(id, body, currentUserId(ctx));
  73. if (!project) {
  74. ctx.status = 404;
  75. ctx.body = { success: false, error: '项目不存在' };
  76. return;
  77. }
  78. ctx.body = { success: true, data: project };
  79. });
  80. /**
  81. * DELETE /api/video/projects/:id
  82. * 删除视频项目
  83. */
  84. router.delete('/projects/:id', requireAuth, async (ctx) => {
  85. const id = Number(ctx.params.id);
  86. const success = await deleteVideoProject(id, currentUserId(ctx));
  87. if (!success) {
  88. ctx.status = 404;
  89. ctx.body = { success: false, error: '项目不存在' };
  90. return;
  91. }
  92. ctx.body = { success: true };
  93. });
  94. // ============ 视频生成 ============
  95. /**
  96. * POST /api/video/projects/:id/generate
  97. * 开始生成视频
  98. */
  99. router.post('/projects/:id/generate', requireAuth, async (ctx) => {
  100. const id = Number(ctx.params.id);
  101. const result = await generateVideoForProject(id, currentUserId(ctx));
  102. if (!result.success) {
  103. ctx.status = 400;
  104. ctx.body = { success: false, error: result.error };
  105. return;
  106. }
  107. ctx.body = {
  108. success: true,
  109. data: {
  110. outputUrl: result.outputUrl,
  111. duration: result.duration,
  112. fileSize: result.fileSize,
  113. },
  114. };
  115. });
  116. /**
  117. * GET /api/video/projects/:id/status
  118. * 获取生成状态
  119. */
  120. router.get('/projects/:id/status', requireAuth, async (ctx) => {
  121. const id = Number(ctx.params.id);
  122. const status = await getGenerateProgress(id, currentUserId(ctx));
  123. ctx.body = { success: true, data: status };
  124. });
  125. // ============ 素材管理 ============
  126. /**
  127. * GET /api/video/materials
  128. * 获取素材列表
  129. */
  130. router.get('/materials', requireAuth, async (ctx) => {
  131. const query = {
  132. userId: currentUserId(ctx),
  133. type: ctx.query.type as any,
  134. category: ctx.query.category as string,
  135. page: ctx.query.page ? Number(ctx.query.page) : 1,
  136. pageSize: ctx.query.pageSize ? Number(ctx.query.pageSize) : 20,
  137. };
  138. const result = await getMaterials(query);
  139. ctx.body = { success: true, data: result };
  140. });
  141. /**
  142. * POST /api/video/materials/upload
  143. * 上传素材(处理 multipart/form-data 文件上传)
  144. */
  145. router.post('/materials/upload', requireAuth, async (ctx) => {
  146. const userId = currentUserId(ctx);
  147. // 处理 multipart form data
  148. const body = ctx.request.body as any;
  149. const files = (ctx.request as any).files as any;
  150. // 获取上传的文件
  151. const file = files?.file;
  152. let materialUrl = '';
  153. if (file) {
  154. // 文件已上传,url 就是文件的路径
  155. materialUrl = `/uploads/materials/${file.newFilename || file.filename}`;
  156. } else if (body.url) {
  157. // 如果没有文件,使用传入的url
  158. materialUrl = body.url;
  159. } else {
  160. ctx.status = 400;
  161. ctx.body = { success: false, error: 'No file uploaded' };
  162. return;
  163. }
  164. const material = await uploadMaterial({
  165. type: body.type || 'image',
  166. name: body.name || file?.newFilename || 'unnamed',
  167. url: materialUrl,
  168. thumbnail: body.thumbnail,
  169. tags: body.tags ? JSON.parse(body.tags) : [],
  170. category: body.category,
  171. duration: body.duration ? Number(body.duration) : undefined,
  172. size: body.size ? Number(body.size) : undefined,
  173. width: body.width ? Number(body.width) : undefined,
  174. height: body.height ? Number(body.height) : undefined,
  175. }, userId);
  176. ctx.body = { success: true, data: material };
  177. });
  178. /**
  179. * DELETE /api/video/materials/:id
  180. * 删除素材
  181. */
  182. router.delete('/materials/:id', requireAuth, async (ctx) => {
  183. const id = Number(ctx.params.id);
  184. const success = await deleteMaterial(id);
  185. if (!success) {
  186. ctx.status = 404;
  187. ctx.body = { success: false, error: '素材不存在' };
  188. return;
  189. }
  190. ctx.body = { success: true };
  191. });
  192. // ============ 快捷入口 ============
  193. /**
  194. * POST /api/video/books/:bookId/generate
  195. * 从书籍生成视频项目
  196. */
  197. router.post('/books/:bookId/generate', requireAuth, async (ctx) => {
  198. const bookId = Number(ctx.params.bookId);
  199. const userId = currentUserId(ctx);
  200. const project = await createVideoProjectFromBook(bookId, undefined, userId);
  201. if (!project) {
  202. ctx.status = 404;
  203. ctx.body = { success: false, error: '书籍不存在' };
  204. return;
  205. }
  206. ctx.body = { success: true, data: project };
  207. });
  208. export default router;