|
|
@@ -0,0 +1,206 @@
|
|
|
+# 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调试设置
|