# 学情分析报告生成逻辑深度分析(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%` 是**代码层面的口径混用问题**,不是你的理解问题。