Explorar el Código

optimize PDF HTML rendering fallback path

Prefer in-process Blade rendering for exam and knowledge explanation HTML, keep HTTP as fallback, and add configurable timeout docs for KP explanation fetch.

Made-with: Cursor
yemeishu hace 2 semanas
padre
commit
4d4c743f6d
Se han modificado 3 ficheros con 106 adiciones y 8 borrados
  1. 99 8
      app/Services/ExamPdfExportService.php
  2. 1 0
      config/pdf.php
  3. 6 0
      docs/pdf-generation.md

+ 99 - 8
app/Services/ExamPdfExportService.php

@@ -1795,10 +1795,16 @@ class ExamPdfExportService
      */
     private function fetchKnowledgeExplanationHtml(string $paperId): ?string
     {
+        $html = $this->renderKnowledgeExplanationHtmlFromView($paperId);
+        if ($html !== null) {
+            return $html;
+        }
+
         try {
             $url = route('filament.admin.auth.intelligent-exam.knowledge-explanation', ['paper_id' => $paperId]);
+            $timeout = max(1, (int) config('pdf.kp_explain_fetch_timeout_seconds', 2));
 
-            $response = Http::get($url);
+            $response = Http::timeout($timeout)->get($url);
             if ($response->successful()) {
                 $html = $response->body();
                 if (! empty(trim($html))) {
@@ -1817,6 +1823,7 @@ class ExamPdfExportService
             Log::warning('ExamPdfExportService: 获取知识点讲解HTML失败', [
                 'paper_id' => $paperId,
                 'url' => $url,
+                'timeout_seconds' => $timeout,
             ]);
 
             return null;
@@ -1825,6 +1832,92 @@ class ExamPdfExportService
             Log::warning('ExamPdfExportService: 获取知识点讲解HTML异常', [
                 'paper_id' => $paperId,
                 'error' => $e->getMessage(),
+                'timeout_seconds' => config('pdf.kp_explain_fetch_timeout_seconds', 2),
+            ]);
+
+            return null;
+        }
+    }
+
+    private function renderKnowledgeExplanationHtmlFromView(string $paperId): ?string
+    {
+        try {
+            $paper = Paper::query()->where('paper_id', $paperId)->first();
+            if (! $paper) {
+                Log::warning('ExamPdfExportService: 本地渲染知识点讲解失败,试卷不存在', ['paper_id' => $paperId]);
+
+                return null;
+            }
+
+            $studentModel = Student::find($paper->student_id);
+            $studentName = $studentModel->name ?? ($paper->student_id ?? '________');
+            $examCode = PaperNaming::extractExamCode((string) $paper->paper_id);
+            try {
+                $assembleTypeLabel = PaperNaming::assembleTypeLabel((int) $paper->paper_type);
+            } catch (\Throwable $e) {
+                $assembleTypeLabel = '未知类型';
+            }
+            $pdfMeta = [
+                'student_name' => $studentName,
+                'exam_code' => $examCode,
+                'assemble_type_label' => $assembleTypeLabel,
+                'header_title' => $examCode,
+                'exam_pdf_title' => '试卷_'.$examCode,
+                'grading_pdf_title' => '判卷_'.$examCode,
+                'knowledge_pdf_title' => '知识点梳理_'.$examCode,
+            ];
+
+            $kpCodes = $paper->explanation_kp_codes ?? [];
+            if (empty($kpCodes)) {
+                $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paperId)->get();
+                $seen = [];
+                $questionBankIds = $paperQuestions
+                    ->pluck('question_bank_id')
+                    ->filter()
+                    ->unique()
+                    ->values();
+                $questionKpMap = [];
+                if ($questionBankIds->isNotEmpty()) {
+                    $questionKpMap = Question::whereIn('id', $questionBankIds)
+                        ->pluck('kp_code', 'id')
+                        ->toArray();
+                }
+
+                foreach ($paperQuestions as $paperQuestion) {
+                    $kpCode = trim((string) ($paperQuestion->knowledge_point ?? ''));
+                    if ($kpCode === '' && ! empty($paperQuestion->question_bank_id)) {
+                        $kpCode = trim((string) ($questionKpMap[$paperQuestion->question_bank_id] ?? ''));
+                    }
+                    if ($kpCode === '' || isset($seen[$kpCode])) {
+                        continue;
+                    }
+                    $seen[$kpCode] = true;
+                    $kpCodes[] = $kpCode;
+                }
+            }
+
+            $html = view('pdf.exam-knowledge-explanation', [
+                'paperId' => $paperId,
+                'examCode' => $examCode,
+                'studentName' => $studentName,
+                'generateDateTime' => now()->format('Y年m月d日 H:i:s'),
+                'knowledgePoints' => $this->buildExplanations($kpCodes),
+                'pdfMeta' => $pdfMeta,
+            ])->render();
+
+            if (empty(trim($html))) {
+                Log::warning('ExamPdfExportService: 本地渲染知识点讲解结果为空', ['paper_id' => $paperId]);
+
+                return null;
+            }
+
+            $html = $this->ensureUtf8Html($html);
+
+            return $this->renderKpExplainMarkdown($html);
+        } catch (\Throwable $e) {
+            Log::warning('ExamPdfExportService: 本地渲染知识点讲解异常', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage(),
             ]);
 
             return null;
@@ -1851,18 +1944,16 @@ class ExamPdfExportService
         );
     }
 
-    /**
-     * 【新增】渲染试卷HTML(通过HTTP调用路由)
-     */
     private function renderExamHtml(string $paperId, bool $includeAnswer, bool $useGradingView): ?string
     {
-        // 判卷部分启用答案详情页时,优先本地渲染,避免跨进程配置不一致。
-        if ($useGradingView && config('exam.pdf_grading_append_scan_sheet', false)) {
-            return $this->renderExamHtmlFromView($paperId, $includeAnswer, $useGradingView);
+        // PDF worker 已经运行在 Laravel 进程内,优先直接渲染 Blade,避免 HTTP 自调用吞掉数秒。
+        $html = $this->renderExamHtmlFromView($paperId, $includeAnswer, $useGradingView);
+        if ($html !== null) {
+            return $html;
         }
 
         try {
-            // 通过HTTP客户端获取渲染后的HTML(与知识点讲解相同的逻辑)
+            // 兜底:保留原 HTTP 路由渲染路径,避免特殊页面上下文下直接视图失败。
             $routeName = $useGradingView
                 ? 'filament.admin.auth.intelligent-exam.grading'
                 : 'filament.admin.auth.intelligent-exam.pdf';

+ 1 - 0
config/pdf.php

@@ -23,6 +23,7 @@ return [
     |
     */
     'include_kp_explain_default' => env('PDF_INCLUDE_KP_EXPLAIN', false),
+    'kp_explain_fetch_timeout_seconds' => (int) env('PDF_KP_EXPLAIN_FETCH_TIMEOUT_SECONDS', 2),
 
     /*
     |--------------------------------------------------------------------------

+ 6 - 0
docs/pdf-generation.md

@@ -204,3 +204,9 @@ docker exec math_cms_app php artisan queue:failed
 ## 配置文件
 
 - `config/pdf.php` - PDF调试设置
+
+关键环境变量:
+
+```env
+PDF_KP_EXPLAIN_FETCH_TIMEOUT_SECONDS=2
+```