# 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调试设置
