# 组卷验证案例与流程说明 ## 一、验证案例:教材组卷(assemble_type=3) ### 1.1 前置条件 - 存在学生、教师、教材及章节数据 - 学生已有作答历史(用于验证「排除已做题目」生效) - 选中章节的题目数量少于 `total_questions`(用于验证「智能补充」生效) ### 1.2 请求示例 **接口**:`POST /api/intelligent-exams` **请求体**(JSON)- 以七年级下册为例: ```json { "student_id": "1764913638", "teacher_id": "1764913637", "student_name": "张三", "teacher_name": "李老师", "grade": 7, "assemble_type": 3, "series_id": 1, "semester_code": 2, "chapter_id_list": [244, 248], "total_questions": 20, "difficulty_category": 1, "paper_name": "七年级下册第1-2章测试" } ``` > **说明**:`semester_code: 2` 表示下学期;`chapter_id_list: [244, 248]` 为教材 `textbook_id=2`(七年级下册)的前 2 个章节节点 ID。 **参数说明**: | 参数 | 必填 | 说明 | |------|------|------| | student_id | 是 | 学生ID | | teacher_id | 是 | 教师ID | | student_name | 是 | 学生姓名 | | teacher_name | 是 | 教师姓名 | | grade | 是 | 年级(如 7=初一) | | assemble_type | 否 | 3=教材组卷,默认为 4(通用) | | series_id | 教材组卷必填 | 教材系列ID | | semester_code | 教材组卷必填 | 1=上册,2=下册(当前学期示例用 2) | | chapter_id_list | 教材组卷推荐 | 章节节点ID 列表;不传则自动选该教材下有题目的章节 | | total_questions | 否 | 题目数量,默认 20 | | difficulty_category | 否 | 难度类别 1-4 | | paper_name | 否 | 试卷名称 | **说明**:API 通过 `series_id` + `semester_code` + `grade` 解析出 `textbook_id`。内部调用(如 Filament 页面)可直接传 `textbook_id`。 ### 1.3 预期行为 1. 不会出现学生已做过的题目(去重生效) 2. 选中章节题目不足时,会从同教材**前章节**补充(未学章节不补) 3. 日志中出现 `getSupplementaryQuestionsForGrade`、`排除学生已做过的题目`、`限制为前章节` 等字样 --- ## 二、流程说明:从请求到生效的每一步 ### Step 0:入口 ``` POST /api/intelligent-exams → IntelligentExamController::store() → 校验参数,resolveTextbookId(若用 series_id) → $params 传入 LearningAnalyticsService::generateIntelligentExam() ``` ### Step 1:组卷类型策略(ExamTypeStrategy) ``` generateIntelligentExam($params) → $params['assemble_type'] = 3(教材组卷) → ExamTypeStrategy::buildParams($params, 3) → buildTextbookAssembleParams($params) ``` **buildTextbookAssembleParams 主要逻辑**: 1. 解析 `chapter_id_list`,若无则从教材下自动选有题目的章节 2. 根据章节获取 `kp_codes`(知识点) 3. **排除已做题目**:`getStudentAnsweredQuestionIds($studentId, $kpCodes)` - 从 `paper_questions` 查学生试卷 - 取 `question_bank_id`(对应 `questions.id`) - 作为 `exclude_question_ids` 写入 `params` 4. **补全 grade**:若 `grade` 缺失,从 `textbooks` 表按 `textbook_id` 推断 5. 输出增强后参数:`kp_codes`、`textbook_catalog_node_ids`、`exclude_question_ids`、`grade`、`textbook_id` ### Step 2:智能出卷主体(LearningAnalyticsService) ``` generateIntelligentExam 继续 → 获取 kp_codes、exclude_question_ids 等 → 若有错题优先获取错题 → getQuestionsFromBank($kpCodes, ..., $excludeQuestionIds, $textbookCatalogNodeIds, $grade, $textbookId, ...) ``` ### Step 3:题库选题(getQuestionsFromBank) 1. **主查询** - 按 `kp_codes`、`grade`、难度等筛选 - `whereNotIn('id', $excludeQuestionIds)`,排除已做题目 - 若配置了 `textbook_catalog_node_ids`,再按章节节点筛选 2. **检查题目是否足够** - 若 `count($selectedQuestions) < $totalNeeded` 且 `$grade !== null` - 调用智能补充 3. **智能补充** ```php getSupplementaryQuestionsForGrade( $grade, array_column($selectedQuestions, 'kp_code'), // 排除已选知识点 $deficit, $difficultyCategory, $textbookId, $excludeQuestionIds, // ← 排除已做题目 $textbookCatalogNodeIds // ← 用于前章节限制 ) ``` ### Step 4:智能补充(getSupplementaryQuestionsForGrade) 1. **排除已做题目** ```php if (!empty($excludeQuestionIds)) { $query->whereNotIn('id', $excludeQuestionIds); } ``` - 补充题同样不会包含学生已做过的题 2. **同年级同教材** - `getGradeKnowledgePoints($grade, $textbookId)` 获取该教材知识点 - `whereIn('kp_code', $gradeKpCodes)` 3. **仅从前章节补充(未学章节不补)** - 若同时传入 `$textbookId` 和 `$textbookCatalogNodeIds` - 调用 `getEarlierChapterNodeIds($textbookId, $textbookCatalogNodeIds)` - 取选中章节的 `max(sort_order)`,得到「前章节」节点 ID - `whereIn('textbook_catalog_nodes_id', $allowedNodeIds)` - 只从当前章节及之前章节补充,不包含后续未学章节 4. 按难度、题型等筛选后返回补充题目 ### Step 5:筛选与难度分布 ``` getQuestionsFromBank 返回 → selectQuestionsByMastery():按掌握度、题型配比筛选 → applyTypeAwareDifficultyDistribution():题型感知难度分布 → 返回最终题目列表 ``` ### Step 6:持久化与返回 - 创建 Paper、PaperQuestion - 生成 PDF、判卷链接等 - 返回任务 ID 或试卷信息 --- ## 三、生效点汇总 | 能力 | 生效位置 | 实现方式 | |------|----------|----------| | 排除已做题目 | `getStudentAnsweredQuestionIds` | 从 `paper_questions` 取 `question_bank_id`,写入 `exclude_question_ids` | | 主选题排除 | `getQuestionsFromBank` 主查询 | `whereNotIn('id', $excludeQuestionIds)` | | 补充时排除 | `getSupplementaryQuestionsForGrade` | 传入 `excludeQuestionIds`,`whereNotIn('id', $excludeQuestionIds)` | | grade 推断 | `buildTextbookAssembleParams` | 无 grade 时从 `textbooks` 按 `textbook_id` 查 grade | | 前章节限制 | `getEarlierChapterNodeIds` | 按 `max(sort_order)` 取前章节节点,`whereIn('textbook_catalog_nodes_id', ...)` | --- ## 四、如何验证 ### 4.1 日志验证 在 `storage/logs/laravel.log` 中可看到类似日志: ``` ExamTypeStrategy: 教材组卷从教材推断 grade ExamTypeStrategy: 获取学生已答题目 ... answered_count: N getQuestionsFromBank: 指定知识点题目不足,尝试智能补充 getSupplementaryQuestionsForGrade: 开始智能补充 ... exclude_count: N, has_chapter_scope: true getSupplementaryQuestionsForGrade: 应用排除筛选 (或 排除学生已做过的题目) getSupplementaryQuestionsForGrade: 限制为前章节 ... allowed_node_count: M getQuestionsFromBank: 智能补充完成 ... supplementary_count: K ``` ### 4.2 数据验证 1. **排除已做**:对比 `paper_questions` 中该学生的 `question_bank_id`,与本次组卷题目 ID,应无交集。 2. **前章节**:补充题目对应的 `textbook_catalog_nodes_id`,其 `sort_order` 应 ≤ 选中章节的最大 `sort_order`。 ### 4.3 curl 快速测试 ```bash curl -X POST 'http://localhost/api/intelligent-exams' \ -H 'Content-Type: application/json' \ -d '{ "student_id": "1764913638", "teacher_id": "1764913637", "student_name": "张三", "teacher_name": "李老师", "grade": 7, "assemble_type": 3, "series_id": 1, "semester_code": 2, "chapter_id_list": [244, 248], "total_questions": 20, "paper_name": "七年级下册教材组卷验证测试" }' ``` 将 `student_id`、`teacher_id` 替换为环境中的真实数据。`series_id=1, semester_code=2, grade=7` 对应 **七年级下册**(textbook_id=2)。