publish.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { Router } from 'express';
  2. import { body, param } from 'express-validator';
  3. import { PublishService } from '../services/PublishService.js';
  4. import { taskQueueService } from '../services/TaskQueueService.js';
  5. import { authenticate } from '../middleware/auth.js';
  6. import { asyncHandler } from '../middleware/error.js';
  7. import { validateRequest } from '../middleware/validate.js';
  8. const router = Router();
  9. const publishService = new PublishService();
  10. router.use(authenticate);
  11. // 获取发布任务列表
  12. router.get(
  13. '/',
  14. asyncHandler(async (req, res) => {
  15. const { page = 1, pageSize = 20, status } = req.query;
  16. const result = await publishService.getTasks(req.user!.userId, {
  17. page: Number(page),
  18. pageSize: Number(pageSize),
  19. status: status as string,
  20. });
  21. res.json({ success: true, data: result });
  22. })
  23. );
  24. // 获取任务详情
  25. router.get(
  26. '/:id',
  27. [
  28. param('id').isInt().withMessage('任务ID无效'),
  29. validateRequest,
  30. ],
  31. asyncHandler(async (req, res) => {
  32. const task = await publishService.getTaskById(
  33. req.user!.userId,
  34. Number(req.params.id)
  35. );
  36. res.json({ success: true, data: task });
  37. })
  38. );
  39. // 创建发布任务
  40. router.post(
  41. '/',
  42. [
  43. // 修复 #6069:videoPath 和 title 改为可选,具体校验由前端按平台要求完成
  44. body('videoPath').optional({ nullable: true }).isString().withMessage('视频路径格式无效'),
  45. body('title').optional({ nullable: true }).isString().withMessage('标题格式无效'),
  46. body('targetAccounts').isArray({ min: 1 }).withMessage('至少选择一个目标账号'),
  47. body('publishProxy').optional({ nullable: true }).isObject().withMessage('发布代理配置无效'),
  48. validateRequest,
  49. ],
  50. asyncHandler(async (req, res) => {
  51. const userId = req.user!.userId;
  52. // 1. 创建数据库记录
  53. const task = await publishService.createTask(userId, req.body);
  54. // 2. 如果不是定时任务,加入任务队列
  55. if (!req.body.scheduledAt) {
  56. await taskQueueService.createTask(userId, {
  57. type: 'publish_video',
  58. title: `发布视频: ${req.body.title}`,
  59. description: `发布到 ${req.body.targetAccounts.length} 个账号`,
  60. data: {
  61. publishTaskId: task.id,
  62. },
  63. });
  64. }
  65. res.status(201).json({ success: true, data: task });
  66. })
  67. );
  68. // 修改并重新发布(更新现有任务)
  69. router.put(
  70. '/:id',
  71. [
  72. param('id').isInt().withMessage('任务ID无效'),
  73. body('targetAccounts').isArray({ min: 1 }).withMessage('至少选择一个目标账号'),
  74. body('publishProxy').optional({ nullable: true }).isObject().withMessage('发布代理配置无效'),
  75. validateRequest,
  76. ],
  77. asyncHandler(async (req, res) => {
  78. const userId = req.user!.userId;
  79. const taskId = Number(req.params.id);
  80. // 修复 Bug #6145:更新现有任务而非创建新任务
  81. const task = await publishService.updateTask(userId, taskId, req.body);
  82. // 如果不是定时任务且状态允许,加入任务队列重新执行
  83. if (!req.body.scheduledAt && task.status !== 'cancelled') {
  84. await taskQueueService.createTask(userId, {
  85. type: 'publish_video',
  86. title: `重新发布: ${task.title}`,
  87. description: `发布到 ${task.targetAccounts.length} 个账号`,
  88. data: {
  89. publishTaskId: task.id,
  90. },
  91. });
  92. }
  93. res.json({ success: true, data: task });
  94. })
  95. );
  96. // 取消任务
  97. router.post(
  98. '/:id/cancel',
  99. [
  100. param('id').isInt().withMessage('任务ID无效'),
  101. validateRequest,
  102. ],
  103. asyncHandler(async (req, res) => {
  104. await publishService.cancelTask(req.user!.userId, Number(req.params.id));
  105. res.json({ success: true, message: '任务已取消' });
  106. })
  107. );
  108. // 重试任务
  109. router.post(
  110. '/:id/retry',
  111. [
  112. param('id').isInt().withMessage('任务ID无效'),
  113. validateRequest,
  114. ],
  115. asyncHandler(async (req, res) => {
  116. const userId = req.user!.userId;
  117. const taskId = Number(req.params.id);
  118. // 1. 更新数据库状态
  119. const task = await publishService.retryTask(userId, taskId);
  120. // 2. 加入任务队列重新执行
  121. await taskQueueService.createTask(userId, {
  122. type: 'publish_video',
  123. title: `重试发布: ${task.title}`,
  124. description: `重新发布到 ${task.targetAccounts.length} 个账号`,
  125. data: {
  126. publishTaskId: task.id,
  127. },
  128. });
  129. res.json({ success: true, data: task });
  130. })
  131. );
  132. // 删除任务
  133. router.delete(
  134. '/:id',
  135. [
  136. param('id').isInt().withMessage('任务ID无效'),
  137. validateRequest,
  138. ],
  139. asyncHandler(async (req, res) => {
  140. await publishService.deleteTask(req.user!.userId, Number(req.params.id));
  141. res.json({ success: true, message: '任务已删除' });
  142. })
  143. );
  144. // 手动确认发布结果(如小红书"发布结果待确认"时用户确认已发布成功)
  145. router.post(
  146. '/:taskId/results/:resultId/confirm',
  147. [
  148. param('taskId').isInt().withMessage('任务ID无效'),
  149. param('resultId').isInt().withMessage('结果ID无效'),
  150. validateRequest,
  151. ],
  152. asyncHandler(async (req, res) => {
  153. await publishService.confirmPublishResult(
  154. req.user!.userId,
  155. Number(req.params.taskId),
  156. Number(req.params.resultId)
  157. );
  158. res.json({ success: true, message: '已确认发布成功' });
  159. })
  160. );
  161. // 单账号有头浏览器重试发布(用于验证码场景)
  162. router.post(
  163. '/:taskId/retry-headful/:accountId',
  164. [
  165. param('taskId').isInt().withMessage('任务ID无效'),
  166. param('accountId').isInt().withMessage('账号ID无效'),
  167. validateRequest,
  168. ],
  169. asyncHandler(async (req, res) => {
  170. const userId = req.user!.userId;
  171. const taskId = Number(req.params.taskId);
  172. const accountId = Number(req.params.accountId);
  173. // 调用服务层执行有头浏览器发布
  174. const result = await publishService.retryAccountWithHeadfulBrowser(
  175. userId,
  176. taskId,
  177. accountId
  178. );
  179. res.json({ success: true, data: result });
  180. })
  181. );
  182. export default router;