本文档记录试卷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
app/Http/Controllers/Api/IntelligentExamController.php
store() - 创建试卷的API入口triggerPdfGeneration() - 触发PDF生成队列任务app/Jobs/GenerateExamPdfJob.php
pdf 队列中执行ExamPdfExportService::generateUnifiedPdf()app/Services/ExamPdfExportService.php
generateUnifiedPdf() - 生成统一PDF(试卷+判卷)renderExamHtml() - 通过HTTP请求获取渲染后的HTMLmergeHtmlWithPageBreak() - 合并两个HTML页面buildPdf() - 调用Chrome生成PDFrenderWithChrome() - Chrome Headless渲染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() - 根据内容推断题目类型resources/views/pdf/exam-paper.blade.php - 试卷主模板resources/views/pdf/exam-grading.blade.php - 判卷主模板resources/views/components/exam/paper-body.blade.php - 题目渲染组件(核心)app/Services/MathFormulaProcessor.php - LaTeX公式处理routes/web.php 第15-16行
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 - 题库题目IDquestion_text - 题目内容paper_questions 表数据
↓
ExamPdfController::show()
↓
按 question_type 字段分类到 ['choice' => [], 'fill' => [], 'answer' => []]
↓
传递给 Blade 模板
↓
paper-body.blade.php 按类型渲染
症状:网页能正常显示所有题目,但PDF中缺少部分题目
排查点:
ExamPdfController 中的正则表达式是否误匹配SVG内容paper-body.blade.php 中的正则表达式PDF_DEBUG_SAVE_HTML=true,查看生成的HTML已修复的问题(2026-01-05):
BD:DC)被误识别为选项标记(如D:)(?:^|\s) 前缀,要求选项标记在行首或空白后症状:选择题被分到填空题,或反之
排查点:
paper_questions.question_type 字段是否有值normalizeQuestionTypeValue() 方法determineQuestionType() 方法的推断逻辑已修复的问题(2026-01-05):
()被误判为填空题question_type 字段,而非根据内容推断症状:PDF生成失败或内容不完整
排查点:
which google-chrome 或 which chromium症状:页面样式异常或部分内容不显示
排查点:
PDF_DEBUG_SAVE_HTML=true 保存HTML后在浏览器中检查# 在 .env 中添加
PDF_DEBUG_SAVE_HTML=true
HTML会保存到 storage/app/debug_pdf_*.html
docker exec math_cms_app tail -100 storage/logs/laravel.log | grep -E "(PDF|ExamPdf|题目)"
直接访问URL查看HTML渲染结果:
/admin/intelligent-exam/pdf/{paper_id}?answer=false # 试卷
/admin/intelligent-exam/grading/{paper_id} # 判卷
SELECT question_number, question_type, question_bank_id
FROM paper_questions
WHERE paper_id = 'paper_xxx'
ORDER BY question_number;
// 正确的正则(要求选项标记在行首或空白后)
$pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
// 错误的正则(会匹配SVG中的 BD:DC 等内容)
$pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su';
// 正确的正则
preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $content, $match);
math_cms_app - 主应用 (Nginx + PHP-FPM)math_cms_queue - 默认队列 workermath_cms_pdf - PDF队列 worker(带Chrome)
# 重启PDF队列
docker compose restart pdf-worker
# 查看队列状态
docker exec math_cms_app php artisan queue:failed
config/pdf.php - PDF调试设置