taskId = $taskId; $this->paperId = $paperId; // 指定使用 pdf 队列,由独立的 pdf-worker 容器处理 $this->onQueue('pdf'); // 避免事务未提交时 worker 提前消费导致“试卷不存在” $this->afterCommit(); } public function handle( ExamPdfExportService $pdfExportService, QuestionBankService $questionBankService, PaperPayloadService $paperPayloadService, TaskManager $taskManager ): void { $jobStartedAt = microtime(true); $taskCreatedAt = null; $queueWaitMs = null; $taskSnapshot = $taskManager->getTaskStatus($this->taskId); if (is_array($taskSnapshot) && !empty($taskSnapshot['created_at'])) { try { $taskCreatedAt = \Illuminate\Support\Carbon::parse($taskSnapshot['created_at']); $queueWaitMs = $taskCreatedAt ? now()->diffInMilliseconds($taskCreatedAt, false) * -1 : null; } catch (\Throwable $e) { $taskCreatedAt = null; $queueWaitMs = null; } } try { Log::info('开始处理PDF生成队列任务', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempt' => $this->attempts(), 'queue_wait_ms' => $queueWaitMs, ]); $taskManager->updateTaskStatus($this->taskId, [ 'queue_wait_ms' => $queueWaitMs, ]); // 【修复】首先检查试卷是否存在 // 强制走主库读取,避免读写分离下新建试卷短时不可见导致“试卷不存在” $paperModel = Paper::query() ->useWritePdo() ->with('questions') ->find($this->paperId); if (! $paperModel) { Log::error('PDF生成队列任务失败:试卷不存在', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempt' => $this->attempts(), ]); // 如果试卷不存在,判断是否需要重试 if ($this->attempts() < $this->tries) { Log::info('试卷不存在,将在2秒后重试', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempt' => $this->attempts(), 'next_attempt' => $this->attempts() + 1, ]); // 延迟2秒后重试(缩短间隔,减少对回调的影响) $this->release(2); return; } else { Log::error('试卷不存在且已达到最大重试次数,标记任务失败', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempts' => $this->attempts(), ]); $taskManager->markTaskFailed($this->taskId, "试卷不存在: {$this->paperId}"); return; } } // 检查试卷是否有题目 if ($paperModel->questions->isEmpty()) { Log::error('PDF生成队列任务失败:试卷没有题目数据', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'question_count' => 0, ]); if ($this->attempts() < $this->tries) { Log::info('试卷没有题目,将在1秒后重试', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempt' => $this->attempts(), ]); // 延迟1秒后重试(更短间隔) $this->release(1); return; } else { $taskManager->markTaskFailed($this->taskId, "试卷没有题目数据: {$this->paperId}"); return; } } $taskManager->updateTaskProgress($this->taskId, 10, '开始生成统一PDF(直接合并两个页面,效率最高)...'); // 根据 config 或 env 配置决定是否包含知识点讲解 // 还需要判断如果摸底(paper_type =0)的时候也是不需要插入知识点讲解内容 $includeKpExplain = null; if($paperModel->paper_type === 0) { $includeKpExplain = false; } info("includekpexplain", [$includeKpExplain, $paperModel->paper_type]); $unifiedPdfUrl = $pdfExportService->generateUnifiedPdf($this->paperId, $includeKpExplain); $taskManager->updateTaskProgress($this->taskId, 90, 'PDF生成完成,准备返回结果...'); $examContent = $paperPayloadService->buildExamContent($paperModel); // 标记任务完成(完整PDF存储到all_pdf_url字段) $taskManager->markTaskCompleted($this->taskId, [ 'exam_content' => $examContent, 'pdfs' => [ 'all_pdf' => $unifiedPdfUrl, // 【完整PDF】包含试卷和判卷,存储到all_pdf_url字段 ], ]); $beforeCallbackAt = microtime(true); $queueProcessingMs = (int) round((microtime(true) - $jobStartedAt) * 1000); $taskTotalMs = null; if ($taskCreatedAt) { $taskTotalMs = $taskCreatedAt->diffInMilliseconds(now()); } Log::info('PDF生成队列任务完成(终极优化:直接合并HTML生成一份完整PDF)', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'all_pdf_url' => $unifiedPdfUrl, 'question_count' => $paperModel->questions->count(), 'method' => 'generateUnifiedPdf (direct merge, fastest)', 'queue_processing_ms' => $queueProcessingMs, 'task_total_ms_before_callback' => $taskTotalMs, ]); $taskManager->updateTaskStatus($this->taskId, [ 'queue_processing_ms' => $queueProcessingMs, 'task_total_ms_before_callback' => $taskTotalMs, ]); // 发送回调通知(在合并PDF完成后) $taskManager->sendCallback($this->taskId); $callbackMs = (int) round((microtime(true) - $beforeCallbackAt) * 1000); $taskTotalAfterCallbackMs = null; if ($taskCreatedAt) { $taskTotalAfterCallbackMs = $taskCreatedAt->diffInMilliseconds(now()); } Log::info('PDF生成队列任务回调完成', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'callback_elapsed_ms' => $callbackMs, 'task_total_ms_after_callback' => $taskTotalAfterCallbackMs, ]); $taskManager->updateTaskStatus($this->taskId, [ 'callback_elapsed_ms' => $callbackMs, 'task_total_ms_after_callback' => $taskTotalAfterCallbackMs, ]); $taskSummary = $taskManager->getTaskStatus($this->taskId); Log::info('EXAM_TASK_TIMELINE_SUMMARY', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'request_trace_id' => $taskSummary['request_trace_id'] ?? null, 'sync_elapsed_sec_total' => isset($taskSummary['sync_elapsed_ms_total']) ? round(((float) $taskSummary['sync_elapsed_ms_total']) / 1000, 3) : null, 'queue_wait_sec' => round(((float) ($taskSummary['queue_wait_ms'] ?? $queueWaitMs ?? 0)) / 1000, 3), 'queue_processing_sec' => round(((float) ($taskSummary['queue_processing_ms'] ?? $queueProcessingMs ?? 0)) / 1000, 3), 'callback_sec' => round(((float) ($taskSummary['callback_elapsed_ms'] ?? $callbackMs ?? 0)) / 1000, 3), 'task_total_sec_after_callback' => round(((float) ($taskSummary['task_total_ms_after_callback'] ?? $taskTotalAfterCallbackMs ?? 0)) / 1000, 3), 'status' => $taskSummary['status'] ?? null, ]); } catch (\Exception $e) { Log::error('PDF生成队列任务失败', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'queue_elapsed_ms_before_error' => (int) round((microtime(true) - $jobStartedAt) * 1000), ]); // 如果是第一次失败且试卷可能还在创建中,等待后重试 if ($this->attempts() < $this->tries && strpos($e->getMessage(), '不存在') !== false) { Log::info('检测到试卷不存在错误,将在2秒后重试', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'attempt' => $this->attempts(), ]); $this->release(2); return; } $taskManager->markTaskFailed($this->taskId, $e->getMessage()); } } public function failed(Throwable $exception): void { try { app(TaskManager::class)->markTaskFailed($this->taskId, $exception->getMessage()); } catch (Throwable $innerException) { Log::error('PDF生成队列任务失败回调异常', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'error' => $innerException->getMessage(), ]); } Log::error('PDF生成队列任务最终失败', [ 'task_id' => $this->taskId, 'paper_id' => $this->paperId, 'error' => $exception->getMessage(), ]); } }