# PDF 生成系统文档 本文档记录试卷PDF生成的完整流程、涉及的文件和常见问题排查点。 ## 生成流程概览 ``` API请求 /api/intelligent-exams ↓ IntelligentExamController::store() ↓ 保存试卷到数据库 (papers + paper_questions 表) ↓ dispatch(GenerateExamPdfJob) → 加入 pdf 队列 ↓ GenerateExamPdfJob::handle() ↓ ExamPdfExportService::generateUnifiedPdf() ↓ renderExamHtml() × 2 (试卷HTML + 判卷HTML) ↓ HTTP请求 → ExamPdfController::show() / showGrading() ↓ Blade模板渲染 (exam-paper.blade.php + paper-body.blade.php) ↓ mergeHtmlWithPageBreak() 合并两个HTML ↓ buildPdf() → renderWithChrome() Chrome Headless 转 PDF ↓ 上传到OSS,返回URL ``` ## 涉及的关键文件 ### 1. API入口 - `app/Http/Controllers/Api/IntelligentExamController.php` - `store()` - 创建试卷的API入口 - `triggerPdfGeneration()` - 触发PDF生成队列任务 ### 2. 队列任务 - `app/Jobs/GenerateExamPdfJob.php` - 在 `pdf` 队列中执行 - 调用 `ExamPdfExportService::generateUnifiedPdf()` ### 3. PDF生成服务 - `app/Services/ExamPdfExportService.php` - `generateUnifiedPdf()` - 生成统一PDF(试卷+判卷) - `renderExamHtml()` - 通过HTTP请求获取渲染后的HTML - `mergeHtmlWithPageBreak()` - 合并两个HTML页面 - `buildPdf()` - 调用Chrome生成PDF - `renderWithChrome()` - Chrome Headless渲染 ### 4. HTML渲染控制器 - `app/Http/Controllers/ExamPdfController.php` - `show()` - 渲染试卷HTML(路由:`/admin/intelligent-exam/pdf/{paper_id}`) - `showGrading()` - 渲染判卷HTML(路由:`/admin/intelligent-exam/grading/{paper_id}`) - `extractOptions()` - 从题目内容中提取选项 - `separateStemAndOptions()` - 分离题干和选项 - `normalizeQuestionTypeValue()` - 标准化题目类型 - `determineQuestionType()` - 根据内容推断题目类型 ### 5. Blade模板 - `resources/views/pdf/exam-paper.blade.php` - 试卷主模板 - `resources/views/pdf/exam-grading.blade.php` - 判卷主模板 - `resources/views/components/exam/paper-body.blade.php` - 题目渲染组件(核心) ### 6. 数学公式处理 - `app/Services/MathFormulaProcessor.php` - LaTeX公式处理 ### 7. 路由配置 - `routes/web.php` 第15-16行 ```php Route::get('/admin/intelligent-exam/pdf/{paper_id}', [ExamPdfController::class, 'show']) ->name('filament.admin.auth.intelligent-exam.pdf'); Route::get('/admin/intelligent-exam/grading/{paper_id}', [ExamPdfController::class, 'showGrading']) ->name('filament.admin.auth.intelligent-exam.grading'); ``` ## 数据流 ### 数据库表 - `papers` - 试卷基本信息 - `paper_questions` - 试卷题目关联 - `question_number` - 题目序号 - `question_type` - 题目类型 (choice/fill/answer) - `question_bank_id` - 题库题目ID - `question_text` - 题目内容 ### 题目分类流程 ``` paper_questions 表数据 ↓ ExamPdfController::show() ↓ 按 question_type 字段分类到 ['choice' => [], 'fill' => [], 'answer' => []] ↓ 传递给 Blade 模板 ↓ paper-body.blade.php 按类型渲染 ``` ## 常见问题排查 ### 问题1:题目丢失 **症状**:网页能正常显示所有题目,但PDF中缺少部分题目 **排查点**: 1. 检查 `ExamPdfController` 中的正则表达式是否误匹配SVG内容 2. 检查 `paper-body.blade.php` 中的正则表达式 3. 开启调试:设置环境变量 `PDF_DEBUG_SAVE_HTML=true`,查看生成的HTML **已修复的问题**(2026-01-05): - SVG中的坐标标注(如`BD:DC`)被误识别为选项标记(如`D:`) - 修复方法:正则表达式添加 `(?:^|\s)` 前缀,要求选项标记在行首或空白后 ### 问题2:题目类型分类错误 **症状**:选择题被分到填空题,或反之 **排查点**: 1. 检查 `paper_questions.question_type` 字段是否有值 2. 检查 `normalizeQuestionTypeValue()` 方法 3. 检查 `determineQuestionType()` 方法的推断逻辑 **已修复的问题**(2026-01-05): - 选择题因含有`()`被误判为填空题 - 修复方法:优先使用 `question_type` 字段,而非根据内容推断 ### 问题3:Chrome渲染问题 **症状**:PDF生成失败或内容不完整 **排查点**: 1. 检查Chrome是否安装:`which google-chrome` 或 `which chromium` 2. 检查超时设置(当前90秒) 3. 检查外部资源加载(KaTeX CDN等) ### 问题4:HTML结构问题 **症状**:页面样式异常或部分内容不显示 **排查点**: 1. 检查div标签是否正确闭合 2. 使用 `PDF_DEBUG_SAVE_HTML=true` 保存HTML后在浏览器中检查 ## 调试方法 ### 1. 保存HTML副本 ```bash # 在 .env 中添加 PDF_DEBUG_SAVE_HTML=true ``` HTML会保存到 `storage/app/debug_pdf_*.html` ### 2. 查看队列日志 ```bash docker exec math_cms_app tail -100 storage/logs/laravel.log | grep -E "(PDF|ExamPdf|题目)" ``` ### 3. 手动测试HTML渲染 直接访问URL查看HTML渲染结果: ``` /admin/intelligent-exam/pdf/{paper_id}?answer=false # 试卷 /admin/intelligent-exam/grading/{paper_id} # 判卷 ``` ### 4. 检查题目数据 ```sql SELECT question_number, question_type, question_bank_id FROM paper_questions WHERE paper_id = 'paper_xxx' ORDER BY question_number; ``` ## 关键正则表达式 ### 选项提取(ExamPdfController + paper-body.blade.php) ```php // 正确的正则(要求选项标记在行首或空白后) $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su'; // 错误的正则(会匹配SVG中的 BD:DC 等内容) $pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su'; ``` ### 题干分离 ```php // 正确的正则 preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $content, $match); ``` ## Docker服务 - `math_cms_app` - 主应用 (Nginx + PHP-FPM) - `math_cms_queue` - 默认队列 worker - `math_cms_pdf` - PDF队列 worker(带Chrome) ```bash # 重启PDF队列 docker compose restart pdf-worker # 查看队列状态 docker exec math_cms_app php artisan queue:failed ``` ## 配置文件 - `config/pdf.php` - PDF调试设置