|
|
@@ -1447,13 +1447,15 @@ class LearningAnalyticsService
|
|
|
]);
|
|
|
|
|
|
// 【修复超纲问题】传入 grade 和 textbook_id,用于智能补充时限制范围
|
|
|
+ // 【修复】传入实际需要数量,否则 totalNeeded=0 会导致智能补充不触发
|
|
|
$excludeQuestionIds = $params['exclude_question_ids'] ?? [];
|
|
|
+ $needCount = $totalQuestions - count($priorityQuestions);
|
|
|
$additionalQuestions = $this->getQuestionsFromBank(
|
|
|
$kpCodes,
|
|
|
$skills,
|
|
|
$studentId,
|
|
|
$questionTypeRatio,
|
|
|
- $poolLimit,
|
|
|
+ $needCount,
|
|
|
[],
|
|
|
$excludeQuestionIds,
|
|
|
$questionCategory,
|
|
|
@@ -1805,17 +1807,22 @@ class LearningAnalyticsService
|
|
|
'available_count' => count($selectedQuestions),
|
|
|
'grade' => $grade,
|
|
|
'textbook_id' => $textbookId,
|
|
|
- 'strategy' => $textbookId ? '从指定教材的其他知识点补充(避免超纲)' : '从同年级其他知识点补充'
|
|
|
+ 'student_id' => $studentId ? '(有)' : '(无)',
|
|
|
+ 'strategy' => $textbookId ? '从同教材前章节补充' : ($studentId ? '从学生已学知识点补充' : '无教材且无学生ID,不补充')
|
|
|
]);
|
|
|
|
|
|
- // 【修复超纲问题】补充策略:从同年级同教材的其他知识点补充
|
|
|
- // 传入 textbook_id 参数,避免七年级上册学生拿到下册题目
|
|
|
+ // 【修复超纲问题】补充策略:只从「学过的」内容补充
|
|
|
+ // - 有 textbook_id:从同教材的前章节补充
|
|
|
+ // - 无 textbook_id:从学生做过的题目对应的知识点补充,避免未学内容
|
|
|
$supplementaryQuestions = $this->getSupplementaryQuestionsForGrade(
|
|
|
$grade,
|
|
|
array_column($selectedQuestions, 'kp_code'),
|
|
|
$deficit,
|
|
|
$difficultyCategory,
|
|
|
- $textbookId // 【关键】传入教材ID,限制补充范围
|
|
|
+ $textbookId,
|
|
|
+ $excludeQuestionIds,
|
|
|
+ $textbookCatalogNodeIds ?? null,
|
|
|
+ $studentId
|
|
|
);
|
|
|
|
|
|
if (!empty($supplementaryQuestions)) {
|
|
|
@@ -3024,6 +3031,9 @@ class LearningAnalyticsService
|
|
|
* @param int $needCount 需要补充的题目数量
|
|
|
* @param int $difficultyCategory 难度类别
|
|
|
* @param int|null $textbookId 教材ID(用于限制补充范围,避免超纲)
|
|
|
+ * @param array $excludeQuestionIds 排除的题目ID(学生已做过,避免重复)
|
|
|
+ * @param array|null $textbookCatalogNodeIds 当前选中的章节节点ID;与 textbookId 同时传入时,仅从同教材的前章节补充(未学章节不补充)
|
|
|
+ * @param string|null $studentId 学生ID(无教材时用于获取「已学知识点」,仅从已学内容补充,避免未学章节)
|
|
|
* @return array 补充的题目列表
|
|
|
*/
|
|
|
private function getSupplementaryQuestionsForGrade(
|
|
|
@@ -3031,7 +3041,10 @@ class LearningAnalyticsService
|
|
|
array $existingKpCodes,
|
|
|
int $needCount,
|
|
|
int $difficultyCategory,
|
|
|
- ?int $textbookId = null
|
|
|
+ ?int $textbookId = null,
|
|
|
+ array $excludeQuestionIds = [],
|
|
|
+ ?array $textbookCatalogNodeIds = null,
|
|
|
+ ?string $studentId = null
|
|
|
): array {
|
|
|
try {
|
|
|
Log::info('getSupplementaryQuestionsForGrade: 开始智能补充', [
|
|
|
@@ -3040,15 +3053,54 @@ class LearningAnalyticsService
|
|
|
'need_count' => $needCount,
|
|
|
'difficulty_category' => $difficultyCategory,
|
|
|
'textbook_id' => $textbookId,
|
|
|
- 'note' => $textbookId ? '限制在指定教材范围内,避免超纲' : '使用整个年级范围'
|
|
|
+ 'student_id' => $studentId ? '(有)' : '(无)',
|
|
|
+ 'exclude_count' => count($excludeQuestionIds),
|
|
|
+ 'has_chapter_scope' => !empty($textbookCatalogNodeIds),
|
|
|
]);
|
|
|
|
|
|
+ // 【核心】补充范围:只从「学过的」内容补充,不能从未学知识点或年级对应章节中选
|
|
|
+ // - 有 textbookId:从 getGradeKnowledgePoints(grade, textbookId) + 前章节(若有 textbookCatalogNodeIds)
|
|
|
+ // - 无 textbookId 且有 studentId:从学生做过的题目对应知识点(getStudentLearnedKpCodes)补充
|
|
|
+ // - 无 textbookId 且无 studentId:无法确定学过的内容,不补充
|
|
|
+ $gradeKpCodes = [];
|
|
|
+ if ($textbookId) {
|
|
|
+ $gradeKpCodes = $this->getGradeKnowledgePoints($grade, $textbookId);
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 教材模式,使用教材知识点', [
|
|
|
+ 'kp_count' => count($gradeKpCodes),
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
+ ]);
|
|
|
+ } elseif ($studentId) {
|
|
|
+ $learnedKps = $this->getStudentLearnedKpCodes($studentId);
|
|
|
+ $gradeKpCodes = array_values(array_diff($learnedKps, $existingKpCodes));
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 无教材模式,使用学生已学知识点', [
|
|
|
+ 'learned_count' => count($learnedKps),
|
|
|
+ 'after_exclude_existing' => count($gradeKpCodes),
|
|
|
+ ]);
|
|
|
+ } else {
|
|
|
+ Log::warning('getSupplementaryQuestionsForGrade: 无教材且无学生ID,无法确定学过的内容,不补充');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($gradeKpCodes)) {
|
|
|
+ Log::warning('getSupplementaryQuestionsForGrade: 无可用知识点,跳过补充', [
|
|
|
+ 'grade' => $grade,
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
+ 'note' => $textbookId ? '教材知识点不足' : '学生已学知识点不足或与当前选题重复',
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
// 查询同年级其他知识点的题目
|
|
|
$query = \App\Models\Question::query();
|
|
|
|
|
|
// 只获取审核通过的题目
|
|
|
$query->where('audit_status', 0);
|
|
|
|
|
|
+ // 【新增】排除学生已做过的题目
|
|
|
+ if (!empty($excludeQuestionIds)) {
|
|
|
+ $query->whereNotIn('id', $excludeQuestionIds);
|
|
|
+ }
|
|
|
+
|
|
|
$stageGrade = $this->normalizeQuestionStageGrade($grade);
|
|
|
if ($stageGrade !== null) {
|
|
|
$query->where('grade', $stageGrade);
|
|
|
@@ -3063,21 +3115,21 @@ class LearningAnalyticsService
|
|
|
$query->whereNotIn('kp_code', $existingKpCodes);
|
|
|
}
|
|
|
|
|
|
- // 【修复超纲问题】限制在指定教材范围内(通过教材关联)
|
|
|
- // 如果有 textbook_id,只从该教材的知识点中补充,避免七年级上册学生拿到下册题目
|
|
|
- $gradeKpCodes = $this->getGradeKnowledgePoints($grade, $textbookId);
|
|
|
- if (!empty($gradeKpCodes)) {
|
|
|
- $query->whereIn('kp_code', $gradeKpCodes);
|
|
|
- Log::info('getSupplementaryQuestionsForGrade: 应用知识点范围限制', [
|
|
|
- 'kp_codes_count' => count($gradeKpCodes),
|
|
|
- 'textbook_id' => $textbookId
|
|
|
- ]);
|
|
|
- } else {
|
|
|
- Log::warning('getSupplementaryQuestionsForGrade: 未找到年级知识点,跳过补充', [
|
|
|
- 'grade' => $grade,
|
|
|
- 'textbook_id' => $textbookId
|
|
|
- ]);
|
|
|
- return [];
|
|
|
+ $query->whereIn('kp_code', $gradeKpCodes);
|
|
|
+
|
|
|
+ // 【新增】仅从同教材前章节补充:部分章节尚未学过,不补充未学章节的题目
|
|
|
+ if ($textbookId && !empty($textbookCatalogNodeIds)) {
|
|
|
+ $allowedNodeIds = $this->getEarlierChapterNodeIds((int) $textbookId, $textbookCatalogNodeIds);
|
|
|
+ if (!empty($allowedNodeIds)) {
|
|
|
+ $query->whereIn('textbook_catalog_nodes_id', $allowedNodeIds);
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 限制为前章节', [
|
|
|
+ 'allowed_node_count' => count($allowedNodeIds),
|
|
|
+ 'max_sort_order' => '同选中章节及之前'
|
|
|
+ ]);
|
|
|
+ } else {
|
|
|
+ Log::warning('getSupplementaryQuestionsForGrade: 未找到前章节节点,跳过补充');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 筛选有解题思路的题目
|
|
|
@@ -3158,6 +3210,53 @@ class LearningAnalyticsService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取同教材中「当前选中章节及之前」的节点 ID 列表(用于前章节补充,避免未学章节)
|
|
|
+ *
|
|
|
+ * @param int $textbookId 教材ID
|
|
|
+ * @param array $chapterNodeIds 当前选中的章节节点ID
|
|
|
+ * @return array 允许的节点ID列表(sort_order <= 选中章节的最大 sort_order)
|
|
|
+ */
|
|
|
+ private function getEarlierChapterNodeIds(int $textbookId, array $chapterNodeIds): array
|
|
|
+ {
|
|
|
+ if (empty($chapterNodeIds)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $maxSortOrder = DB::table('textbook_catalog_nodes')
|
|
|
+ ->where('textbook_id', $textbookId)
|
|
|
+ ->whereIn('id', $chapterNodeIds)
|
|
|
+ ->max('sort_order');
|
|
|
+
|
|
|
+ if ($maxSortOrder === null) {
|
|
|
+ Log::warning('getEarlierChapterNodeIds: 未找到选中章节的 sort_order', [
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
+ 'chapter_node_ids' => $chapterNodeIds
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $allowedNodeIds = DB::table('textbook_catalog_nodes')
|
|
|
+ ->where('textbook_id', $textbookId)
|
|
|
+ ->where('sort_order', '<=', $maxSortOrder)
|
|
|
+ ->pluck('id')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ Log::info('getEarlierChapterNodeIds: 前章节节点', [
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
+ 'max_sort_order' => $maxSortOrder,
|
|
|
+ 'allowed_node_count' => count($allowedNodeIds)
|
|
|
+ ]);
|
|
|
+ return $allowedNodeIds;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('getEarlierChapterNodeIds: 查询失败', [
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取指定年级的知识点列表
|
|
|
*
|
|
|
@@ -3165,6 +3264,55 @@ class LearningAnalyticsService
|
|
|
* @param int|null $textbookId 教材ID(可选,用于限制在特定教材范围内,避免超纲)
|
|
|
* @return array 知识点代码列表
|
|
|
*/
|
|
|
+ /**
|
|
|
+ * 获取学生「已学知识点」:通过该学生做过的题目(paper_questions)对应的 kp_code 推断
|
|
|
+ * 用于无教材信息时,智能补充仅从已学知识点中选,避免出现未学内容
|
|
|
+ *
|
|
|
+ * @param string|null $studentId 学生ID
|
|
|
+ * @return array 知识点代码列表(去重、非空)
|
|
|
+ */
|
|
|
+ private function getStudentLearnedKpCodes(?string $studentId): array
|
|
|
+ {
|
|
|
+ if (empty($studentId)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $questionBankIds = \App\Models\PaperQuestion::query()
|
|
|
+ ->whereHas('paper', fn($q) => $q->where('student_id', $studentId))
|
|
|
+ ->pluck('question_bank_id')
|
|
|
+ ->unique()
|
|
|
+ ->filter()
|
|
|
+ ->values()
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ if (empty($questionBankIds)) {
|
|
|
+ Log::info('getStudentLearnedKpCodes: 学生暂无做题记录', ['student_id' => $studentId]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpCodes = \App\Models\Question::query()
|
|
|
+ ->whereIn('id', $questionBankIds)
|
|
|
+ ->whereNotNull('kp_code')
|
|
|
+ ->where('kp_code', '!=', '')
|
|
|
+ ->pluck('kp_code')
|
|
|
+ ->unique()
|
|
|
+ ->values()
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ Log::info('getStudentLearnedKpCodes: 已学知识点', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'kp_count' => count($kpCodes),
|
|
|
+ ]);
|
|
|
+ return $kpCodes;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('getStudentLearnedKpCodes: 查询失败', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private function getGradeKnowledgePoints(int $grade, ?int $textbookId = null): array
|
|
|
{
|
|
|
try {
|