# 组卷逻辑修复总结（本分支）

## 一、修改概览

| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `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 下填空题目数量），可视情况调整题型池或策略。
