# 学情分析报告生成逻辑深度分析（2026-03-11）

## 1. 分析目标与结论摘要

### 目标
- 梳理“学情分析报告（PDF）”完整流程（入口、数据、算法、渲染、存储）。
- 定位客户反馈问题：`上一份学案 20% -> 本次 65%，变化值却显示 80%`。
- 给出功能、体验、算法三个维度的可落地优化方案。

### 核心结论
1. 当前系统的“变化值”在**子知识点**与**父知识点（层级掌握度）**上存在**口径不一致**。
2. 父节点当前值来自“全量子节点平均”，但父节点变化值来自“本次考试命中的子节点平均变化”，导致可出现 `当前65%，变化80%` 这种反直觉结果。
3. 报告中的“变化值”实际是**绝对变化（百分点）**，UI 用 `%` 容易被理解成“相对增长率”，语义也不清晰。
4. 存在多处“默认 previous=0”的兜底，会在数据缺失时放大变化值。

---

## 2. 端到端流程（当前实现）

## 2.1 入口与任务
- API 入口：`POST /api/exam-analysis/report`
- 代码：`app/Http/Controllers/Api/ExamAnalysisApiController.php:24`
- 控制器调用：`ExamAnalysisService::generateReport(...)`
- 注意：名义上是“异步任务”，但当前实现是**同步执行模拟异步**：
  - `app/Services/ExamAnalysisService.php:49`

## 2.2 服务编排
- `ExamAnalysisService::processReportGeneration(...)` 主要步骤：
  1. 组装分析数据 `getAnalysisData`
  2. 生成 PDF `ExamPdfExportService::generateAnalysisReportPdf`
  3. 保存 URL 到 `student_reports` 或 OCR 记录
- 代码：`app/Services/ExamAnalysisService.php:117`

## 2.3 PDF 数据构建核心
- 主入口：`ExamPdfExportService::generateAnalysisReportPdf`
- 代码：`app/Services/ExamPdfExportService.php:201`
- 内部调用：`buildAnalysisData($paperId, $studentId)`

### buildAnalysisData 数据源优先级
1. 试卷与学生基础信息（`papers` / `students`）
2. 分析结果（`exam_analysis_results.analysis_data`）
3. 掌握度数据（优先 `analysis_data.knowledge_point_analysis`，否则走 `MasteryCalculator` 概览）
4. 快照表 `knowledge_point_mastery_snapshots` 用于补充 `previous_mastery`
5. 题目列表（`paper_questions` + 题库详情）

关键代码：
- `app/Services/ExamPdfExportService.php:735-923`
- 报告模板：`resources/views/exam-analysis/pdf-report.blade.php`

---

## 3. “20% -> 65%，变化却80%”问题根因

## 3.1 现象复现逻辑
报告中“层级掌握度分析”显示的是父节点：
- 当前掌握度：`65%`
- 变化值：`↑80%`

用户直觉：如果上次是 `20%`，那这次 `65%`，变化应约 `+45`（百分点），而不是 `+80`。

## 3.2 代码级根因（关键）

### 根因A：父节点“当前值”和“变化值”来自不同口径
- 父节点当前掌握度来自：`MasteryCalculator::getStudentMasteryOverviewWithHierarchy`（全量子节点聚合）
  - `app/Services/MasteryCalculator.php:555`
- 父节点变化值来自：只对“本次考试涉及子节点(relevantChildren)”计算平均变化
  - `app/Services/ExamPdfExportService.php:815-834`

也就是说：
- `mastery_level(parent)` = 全兄弟子节点平均
- `mastery_change(parent)` = 本次命中子节点平均变化

这两个分母不同，数值天然不可直接对比，导致“当前值不大、变化却很大”的感知错误。

### 根因B：变化值展示语义不清
模板展示逻辑：
- `changeText = number_format(abs($delta) * 100, 1) . '%'`
- `resources/views/exam-analysis/pdf-report.blade.php:111-113, 151-153`

这里把 `delta(0~1)` 直接乘100并显示 `%`，但未说明是“百分点变化”。
用户容易理解成“相对增幅%”，语义冲突。

### 根因C：缺失数据时默认 previous=0，放大变化
- `previous_mastery` 缺失时被当成 0：
  - `app/Services/ExamPdfExportService.php:791, 905`

这会把本应“无对比基线”的记录显示成“大幅提升”。

---

## 4. 功能、体验、算法层面的改进建议

## 4.1 功能层（高优先）

1. 统一父节点变化值口径（P0）
- 方案A（推荐）：父节点变化值 = `当前父节点掌握度 - 上次父节点掌握度`
- 即都按“父节点维度”计算，不再用“命中子节点均值变化”替代。
- 收益：杜绝 `65% +80%` 类错觉。

2. 基线明确化（P0）
- 在报告中写明：
  - `对比基线：上一份已完成学案（时间：xxxx-xx-xx xx:xx）`
  - 如果无基线：显示 `首次分析，无历史对比`。
- 禁止 silently fallback 到 0。

3. 无基线不显示变化（P1）
- `previous_mastery` 缺失时显示 `--`，并附 `无可比历史数据`。

4. 报告数据版本化（P1）
- 在 `exam_analysis_results` 记录 `metric_version`、`baseline_snapshot_id`。
- 避免后续算法改动后旧报告口径不可追溯。

## 4.2 体验层（高优先）

1. 文案改造（P0）
- 当前：`↑ 80.0%`
- 建议：`↑ 45.0 个百分点`（绝对变化）
- 若要展示相对增幅，另加一行：`较上次提升 225%`。

2. 在卡片中显示“三元组”
- `上次 20.0% → 本次 65.0%（+45.0pp）`
- 直接消除认知歧义。

3. 父节点卡片增加口径说明（P1）
- 标注：`父节点掌握度=全部子知识点加权/平均`
- 与“本次命中知识点变化”分开展示。

4. 异常提示（P1）
- 当 `|delta_pp| > 60` 且题量很少时，标记 `样本偏小，变化波动较大`。

## 4.3 算法层（高优先）

1. 父节点变化值重算（P0）
- 按父节点历史快照直接对比：
  - `parent_delta = parent_current - parent_previous`
- 不再用 `relevantChildren` 的平均变化替代。

2. 子节点变化值稳健化（P1）
- 增加最小样本约束（如 attempts>=N）才展示变化。
- 否则显示“趋势待观察”。

3. 变化值双口径并存（P2）
- `delta_pp`（百分点）
- `delta_ratio`（相对变化率）
- 前端默认展示 `delta_pp`，tooltip 展示 `delta_ratio`。

---

## 5. 关键风险点清单

1. 伪异步（同步执行）
- `generateReport` 目前同步执行耗时任务，峰值时可能拖慢接口。
- 代码：`app/Services/ExamAnalysisService.php:49`

2. fallback 分支基线不稳定
- 当没有 `knowledge_point_analysis` 时，取“最新快照（不按 paper_id）”作为对比基线。
- 代码：`app/Services/ExamPdfExportService.php:894-899`
- 风险：跨学案串基线。

3. previous 缺失默认0
- 可能制造虚高变化。
- 代码：`app/Services/ExamPdfExportService.php:791, 905`

4. 父节点层级计算规则与展示说明不一致
- 代码注释写“所有兄弟节点历史数据”，实现却是 `relevantChildren`。
- 代码：`app/Services/ExamPdfExportService.php:818-833`

---

## 6. 建议的落地优先级（两周内）

### 第一优先（本周）
1. 修正父节点变化值口径统一（P0）
2. 无基线不显示变化，不再默认 previous=0（P0）
3. 报告文案改为“百分点变化”（P0）

### 第二优先（下周）
1. 报告增加“对比基线时间/来源”（P1）
2. 加样本量阈值与波动提示（P1）
3. 异步化真正落地（队列任务）（P1）

---

## 7. 对你问题的直接回答

> “是对比上一份学案吗？”

当前实现并不稳定地等价于“上一份学案”：
- 有的路径用“当前分析快照内的 previous_mastery”（接近上一次状态）；
- 有的路径用“学生最新快照”（可能跨学案）；
- 父节点变化还混用了“本次命中子节点变化”。

所以你看到 `20% -> 65%，却↑80%` 是**代码层面的口径混用问题**，不是你的理解问题。
