# 组卷逻辑修复总结(本分支) ## 一、修改概览 | 文件 | 修改类型 | 说明 | |------|----------|------| | `app/Services/ExamTypeStrategy.php` | 组卷去重、参数传递 | 排除已做题、grade 推断 | | `app/Services/LearningAnalyticsService.php` | 智能补充、已学知识点 | 补充范围、totalNeeded、新方法 | --- ## 二、具体改动清单 ### 2.1 ExamTypeStrategy.php #### ① 组卷去重:`getStudentAnsweredQuestionIds` **原逻辑**:从 `student_answer_questions` 表按 `student_id` 查 `question_id` **新逻辑**:从 `paper_questions` 表,按该学生名下试卷的 `question_bank_id` 去重 ```php // 唯一来源:paper_questions → question_bank_id(对应 questions.id) // 确保新卷子不出现学生做过的重复题目 PaperQuestion::query() ->whereHas('paper', fn ($q) => $q->where('student_id', $studentId)) ->whereNotNull('question_bank_id') ->where('question_bank_id', '>', 0) ->distinct() ->pluck('question_bank_id') ``` **原因**:与组卷实际数据源(`paper_questions`)保持一致,去重更准确。 --- #### ② grade 推断:教材组卷 `buildTextbookAssembleParams` **新增**:当传入 `textbook_id` 但 `grade` 为空时,从 `textbooks` 表按 `textbook_id` 查询 `grade` ```php if ($textbookId && $grade === null) { $grade = DB::table('textbooks')->where('id', $textbookId)->value('grade'); } ``` **保证**:`params` 中同时传递 `textbook_id` 和 `grade`,供智能补充使用。 --- #### ③ grade 推断:智能组卷 `buildIntelligentAssembleParams` **新增**:同上,从教材推断 `grade`,保证智能补充可触发。 --- ### 2.2 LearningAnalyticsService.php #### ① totalNeeded 传参修复(智能补充触发) **原逻辑**:传入 `$poolLimit`(固定为 0),`totalNeeded` 恒为 0,智能补充条件 `totalNeeded > 0 && count < totalNeeded` 恒不成立 **新逻辑**:传入 `$needCount = $totalQuestions - count($priorityQuestions)`,题目不足时可正确触发智能补充 --- #### ② 智能补充范围:`getSupplementaryQuestionsForGrade` **补充范围策略**: | 场景 | 补充来源 | 说明 | |------|----------|------| | 有 `textbookId` | `getGradeKnowledgePoints(grade, textbookId)` | 同教材知识点 | | 有 `textbookId` + `textbookCatalogNodeIds` | 上述 + `getEarlierChapterNodeIds` | 仅同教材前章节 | | 无 `textbookId` + 有 `studentId` | `getStudentLearnedKpCodes(studentId)` | 仅学生已学知识点 | | 无 `textbookId` + 无 `studentId` | 不补充 | 无法确定学过的内容 | --- #### ③ 新增方法 - **`getStudentLearnedKpCodes(?string $studentId)`** 从 `paper_questions` + `Paper` + `Question` 获取该学生做过的题目的 `kp_code`,作为「已学知识点」。 - **`getEarlierChapterNodeIds(int $textbookId, array $chapterNodeIds)`** 按 `sort_order <= max(选中章节)` 筛选同教材节点,用于「前章节补充」。 --- #### ④ 新增参数 `getSupplementaryQuestionsForGrade` 新增参数: - `excludeQuestionIds`:排除学生已做题目,避免重复 - `textbookCatalogNodeIds`:教材组卷时限制为前章节 - `studentId`:无教材时获取已学知识点 --- ## 三、潜在风险与应对 | 风险点 | 分析 | 结论 | |--------|------|------| | **新学生无做题记录** | `getStudentLearnedKpCodes` 返回 [],`gradeKpCodes` 为空,不补充 | ✅ 正确:宁可少题,不补未学内容 | | **无教材且无 studentId** | 如教师预览卷子,不补充 | ✅ 正确:无法确定学过的内容时不补充 | | **textbooks 表无 grade** | `value('grade')` 返回 null,`grade` 为 null,智能补充不触发 | ⚠️ 依赖数据:需保证教材配置完整 | | **paper_questions.question_bank_id 为空** | `getStudentAnsweredQuestionIds` 已过滤 `whereNotNull` 且 `> 0` | ✅ 已处理 | | **poolLimit 改为 needCount** | 原 `poolLimit=0` 导致智能补充从不触发;现传入实际需要数量 | ✅ 修复正确 | | **getStudentAnsweredQuestionIds 的 $kpCodes 参数** | 仍保留参数,内部已不再使用 | ✅ 兼容调用方,无影响 | --- ## 四、数据流校验 ``` 组卷请求 → ExamTypeStrategy::buildParams → getStudentAnsweredQuestionIds(studentId) → exclude_question_ids → 教材组卷:grade 推断、textbook_id、textbook_catalog_node_ids → 智能组卷:grade 推断 → LearningAnalyticsService::generateIntelligentExam → getQuestionsFromBank(kpCodes, ..., excludeQuestionIds, textbookCatalogNodeIds, grade, textbookId) → 主查询:whereNotIn('id', excludeQuestionIds) → 不足时:getSupplementaryQuestionsForGrade(grade, existingKps, deficit, ..., textbookId, excludeQuestionIds, textbookCatalogNodeIds, studentId) → 有教材:getGradeKnowledgePoints + getEarlierChapterNodeIds(若有章节) → 无教材:getStudentLearnedKpCodes(studentId) → whereNotIn('id', excludeQuestionIds) 再次排除 ``` --- ## 五、验证结论 1. **去重**:paper_132736368400673、paper_132736388400529、paper_132736538400759、paper_132736648500556 均无重复出题。 2. **智能补充**:paper_132736648500556(修复后)仅从学生已学 31 个知识点补充,无 10/12 年级或未学章节内容。 3. **教材组卷**:`textbook_id` 和 `textbookCatalogNodeIds` 传递正确,前章节补充逻辑生效。 4. **totalNeeded**:题目不足时可正常触发智能补充。 --- ## 六、建议后续关注 1. **教材配置**:确认 `textbooks.grade` 有值,避免 grade 推断失败。 2. **新学生首卷**:无做题记录时可能题量不足,可在产品侧提示或放宽策略(若业务允许)。 3. **题型分布**:智能补充后填空可能偏少(取决于已学 KP 下填空题目数量),可视情况调整题型池或策略。