GenerateExamPdfJob.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. namespace App\Jobs;
  3. use App\Models\Paper;
  4. use App\Services\ExamPdfExportService;
  5. use App\Services\QuestionBankService;
  6. use App\Services\PaperPayloadService;
  7. use App\Services\TaskManager;
  8. use Illuminate\Bus\Queueable;
  9. use Illuminate\Contracts\Queue\ShouldQueue;
  10. use Illuminate\Foundation\Bus\Dispatchable;
  11. use Illuminate\Queue\InteractsWithQueue;
  12. use Illuminate\Queue\SerializesModels;
  13. use Illuminate\Support\Facades\Log;
  14. class GenerateExamPdfJob implements ShouldQueue
  15. {
  16. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  17. public string $taskId;
  18. public string $paperId;
  19. public int $maxAttempts = 3;
  20. public function __construct(string $taskId, string $paperId)
  21. {
  22. $this->taskId = $taskId;
  23. $this->paperId = $paperId;
  24. }
  25. public function handle(
  26. ExamPdfExportService $pdfExportService,
  27. QuestionBankService $questionBankService,
  28. PaperPayloadService $paperPayloadService,
  29. TaskManager $taskManager
  30. ): void {
  31. try {
  32. Log::info('开始处理PDF生成队列任务', [
  33. 'task_id' => $this->taskId,
  34. 'paper_id' => $this->paperId,
  35. 'attempt' => $this->attempts(),
  36. ]);
  37. // 【新增】快速检查:如果任务已完成,直接跳过
  38. $task = $taskManager->getTaskStatus($this->taskId);
  39. if ($task && $task['status'] === 'completed') {
  40. Log::info('【跳过执行】任务已完成,无需重复生成PDF', [
  41. 'task_id' => $this->taskId,
  42. 'paper_id' => $this->paperId,
  43. 'status' => $task['status']
  44. ]);
  45. return;
  46. }
  47. // 【修复】首先检查试卷是否存在
  48. $paperModel = Paper::with('questions')->find($this->paperId);
  49. if (!$paperModel) {
  50. Log::error('PDF生成队列任务失败:试卷不存在', [
  51. 'task_id' => $this->taskId,
  52. 'paper_id' => $this->paperId,
  53. 'attempt' => $this->attempts(),
  54. ]);
  55. // 如果试卷不存在,判断是否需要重试
  56. if ($this->attempts() < $this->maxAttempts) {
  57. Log::info('试卷不存在,将在2秒后重试', [
  58. 'task_id' => $this->taskId,
  59. 'paper_id' => $this->paperId,
  60. 'attempt' => $this->attempts(),
  61. 'next_attempt' => $this->attempts() + 1,
  62. ]);
  63. // 延迟2秒后重试(缩短间隔,减少对回调的影响)
  64. $this->release(2);
  65. return;
  66. } else {
  67. Log::error('试卷不存在且已达到最大重试次数,标记任务失败', [
  68. 'task_id' => $this->taskId,
  69. 'paper_id' => $this->paperId,
  70. 'attempts' => $this->attempts(),
  71. ]);
  72. $taskManager->markTaskFailed($this->taskId, "试卷不存在: {$this->paperId}");
  73. return;
  74. }
  75. }
  76. // 检查试卷是否有题目
  77. if ($paperModel->questions->isEmpty()) {
  78. Log::error('PDF生成队列任务失败:试卷没有题目数据', [
  79. 'task_id' => $this->taskId,
  80. 'paper_id' => $this->paperId,
  81. 'question_count' => 0,
  82. ]);
  83. if ($this->attempts() < $this->maxAttempts) {
  84. Log::info('试卷没有题目,将在1秒后重试', [
  85. 'task_id' => $this->taskId,
  86. 'paper_id' => $this->paperId,
  87. 'attempt' => $this->attempts(),
  88. ]);
  89. // 延迟1秒后重试(更短间隔)
  90. $this->release(1);
  91. return;
  92. } else {
  93. $taskManager->markTaskFailed($this->taskId, "试卷没有题目数据: {$this->paperId}");
  94. return;
  95. }
  96. }
  97. $taskManager->updateTaskProgress($this->taskId, 10, '开始生成试卷PDF...');
  98. // 生成试卷PDF
  99. $pdfUrl = $pdfExportService->generateExamPdf($this->paperId)
  100. ?? $questionBankService->exportExamToPdf($this->paperId)
  101. ?? route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $this->paperId, 'answer' => 'false']);
  102. $taskManager->updateTaskProgress($this->taskId, 50, '试卷PDF生成完成,开始生成判卷PDF...');
  103. // 生成判卷PDF
  104. $gradingPdfUrl = $pdfExportService->generateGradingPdf($this->paperId)
  105. ?? route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $this->paperId, 'answer' => 'true']);
  106. $taskManager->updateTaskProgress($this->taskId, 70, '判卷PDF生成完成,开始合并PDF...');
  107. // 【优化】生成合并PDF(试卷 + 判卷) - 使用快速合并模式
  108. $mergedPdfUrl = $pdfExportService->generateMergedPdf($this->paperId, function($percentage, $message) use ($taskManager) {
  109. // 进度更新:70% 开始,最高到 95%
  110. $progress = 70 + ($percentage / 100) * 25;
  111. $taskManager->updateTaskProgress($this->taskId, round($progress, 0), $message);
  112. });
  113. // 【新增】验证合并后的PDF URL
  114. if (!$mergedPdfUrl) {
  115. Log::error('PDF生成队列任务失败:合并PDF失败', [
  116. 'task_id' => $this->taskId,
  117. 'paper_id' => $this->paperId,
  118. 'attempt' => $this->attempts(),
  119. ]);
  120. if ($this->attempts() < $this->maxAttempts) {
  121. Log::info('合并PDF失败,将在3秒后重试', [
  122. 'task_id' => $this->taskId,
  123. 'paper_id' => $this->paperId,
  124. 'attempt' => $this->attempts(),
  125. 'next_attempt' => $this->attempts() + 1,
  126. ]);
  127. // 延迟3秒后重试
  128. $this->release(3);
  129. return;
  130. } else {
  131. Log::error('合并PDF失败且已达到最大重试次数,标记任务失败', [
  132. 'task_id' => $this->taskId,
  133. 'paper_id' => $this->paperId,
  134. 'attempts' => $this->attempts(),
  135. ]);
  136. $taskManager->markTaskFailed($this->taskId, "合并PDF失败: {$this->paperId}");
  137. return;
  138. }
  139. }
  140. Log::info('PDF合并成功验证', [
  141. 'task_id' => $this->taskId,
  142. 'paper_id' => $this->paperId,
  143. 'merged_pdf_url' => $mergedPdfUrl,
  144. 'url_length' => strlen($mergedPdfUrl)
  145. ]);
  146. // 构建完整的试卷内容
  147. $examContent = $paperPayloadService->buildExamContent($paperModel);
  148. // 标记任务完成(包含合并后的PDF URL)
  149. $taskManager->markTaskCompleted($this->taskId, [
  150. 'exam_content' => $examContent,
  151. 'pdfs' => [
  152. 'exam_paper_pdf' => $pdfUrl,
  153. 'grading_pdf' => $gradingPdfUrl,
  154. 'all_pdf' => $mergedPdfUrl, // 【新增】合并后的完整PDF
  155. ],
  156. ]);
  157. Log::info('PDF生成队列任务完成', [
  158. 'task_id' => $this->taskId,
  159. 'paper_id' => $this->paperId,
  160. 'pdf_url' => $pdfUrl,
  161. 'grading_pdf_url' => $gradingPdfUrl,
  162. 'merged_pdf_url' => $mergedPdfUrl,
  163. 'question_count' => $paperModel->questions->count(),
  164. ]);
  165. // 发送回调通知(在合并PDF完成后)
  166. $taskManager->sendCallback($this->taskId);
  167. } catch (\Exception $e) {
  168. Log::error('PDF生成队列任务失败', [
  169. 'task_id' => $this->taskId,
  170. 'paper_id' => $this->paperId,
  171. 'error' => $e->getMessage(),
  172. 'trace' => $e->getTraceAsString(),
  173. ]);
  174. // 如果是第一次失败且试卷可能还在创建中,等待后重试
  175. if ($this->attempts() < $this->maxAttempts && strpos($e->getMessage(), '不存在') !== false) {
  176. Log::info('检测到试卷不存在错误,将在2秒后重试', [
  177. 'task_id' => $this->taskId,
  178. 'paper_id' => $this->paperId,
  179. 'attempt' => $this->attempts(),
  180. ]);
  181. $this->release(2);
  182. return;
  183. }
  184. $taskManager->markTaskFailed($this->taskId, $e->getMessage());
  185. }
  186. }
  187. }