pdf-generation.md 7.2 KB

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() → Gotenberg(默认)/ Chrome CLI(兜底)转 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() - 按配置选择 PDF 渲染后端
    • renderWithGotenberg() - Gotenberg 常驻 Chromium 服务渲染
    • renderWithChrome() - Chrome CLI 兜底渲染

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行

    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:PDF渲染问题

症状:PDF生成失败或内容不完整

排查点

  1. 默认检查 Gotenberg 是否健康:curl http://gotenberg:3000/health(容器内)或 docker compose ps gotenberg
  2. 检查渲染后端配置:PDF_RENDERER=gotenberg|chrome
  3. 检查 Gotenberg 超时:PDF_GOTENBERG_TIMEOUT_SECONDS
  4. 如果启用 Chrome 兜底,检查 Chrome 是否安装:which google-chromewhich chromium
  5. 检查外部资源加载(KaTeX 已内联;远程图片仍需网络可达)

问题4:HTML结构问题

症状:页面样式异常或部分内容不显示

排查点

  1. 检查div标签是否正确闭合
  2. 使用 PDF_DEBUG_SAVE_HTML=true 保存HTML后在浏览器中检查

调试方法

1. 保存HTML副本

# 在 .env 中添加
PDF_DEBUG_SAVE_HTML=true

HTML会保存到 storage/app/debug_pdf_*.html

2. 查看队列日志

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. 检查题目数据

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)

// 正确的正则(要求选项标记在行首或空白后)
$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);

Docker服务

  • math_cms_app - 主应用 (Nginx + PHP-FPM)
  • math_cms_queue - 默认队列 worker
  • math_cms_pdf - PDF队列 worker(带Chrome)
  • math_cms_gotenberg - 默认 PDF 转换服务(常驻 Chromium)

    # 重启PDF队列
    docker compose restart pdf-worker
    
    # 重启Gotenberg
    docker compose restart gotenberg
    
    # 查看队列状态
    docker exec math_cms_app php artisan queue:failed
    

配置文件

  • config/pdf.php - PDF调试设置

当前知识点讲解开关规则(2026-04)

  • 默认行为统一为:仅当 papers.paper_type = 2(知识点组题)时,统一 PDF 包含“知识点讲解”。
  • paper_type = 3(教材组题)等其他类型,统一 PDF 不包含“知识点讲解”。
  • /api/papers/{paper_id}/regenerate 与组卷队列保持一致:仅知识点组卷类型关联“知识点讲解”,不再提供外部覆盖参数。

关键环境变量:

PDF_RENDERER=gotenberg
GOTENBERG_URL=http://gotenberg:3000
PDF_GOTENBERG_CONNECT_TIMEOUT_SECONDS=3
PDF_GOTENBERG_TIMEOUT_SECONDS=60
PDF_FALLBACK_TO_CHROME=true
PDF_CHROME_POLL_TIMEOUT_SECONDS=40
PDF_KP_EXPLAIN_FETCH_TIMEOUT_SECONDS=2