|
|
@@ -1216,24 +1216,15 @@ class LearningAnalyticsService
|
|
|
$assembleType = (int) ($params['assemble_type'] ?? 4); // 默认为通用类型(4)
|
|
|
$examTypeLegacy = $params['exam_type'] ?? 'general'; // 兼容旧版参数
|
|
|
|
|
|
- Log::debug('LearningAnalyticsService: 检查组卷策略', [
|
|
|
- 'assemble_type' => $assembleType
|
|
|
- ]);
|
|
|
-
|
|
|
// 如果有 assemble_type 参数,优先使用新的参数系统
|
|
|
if (isset($params['assemble_type'])) {
|
|
|
try {
|
|
|
// 确保QuestionExpansionService和QuestionLocalService可用
|
|
|
$questionExpansionService = $this->questionExpansionService ?? app(QuestionExpansionService::class);
|
|
|
$questionLocalService = app(QuestionLocalService::class);
|
|
|
- Log::debug('LearningAnalyticsService: 从容器获取服务实例');
|
|
|
|
|
|
$strategy = new ExamTypeStrategy($questionExpansionService, $questionLocalService);
|
|
|
$params = $strategy->buildParams($params, $assembleType);
|
|
|
-
|
|
|
- Log::debug('LearningAnalyticsService: 已应用组卷策略', [
|
|
|
- 'assemble_type' => $assembleType
|
|
|
- ]);
|
|
|
} catch (Exception $e) {
|
|
|
Log::warning('LearningAnalyticsService: 组卷策略应用失败,使用默认策略', [
|
|
|
'assemble_type' => $assembleType,
|
|
|
@@ -1246,14 +1237,9 @@ class LearningAnalyticsService
|
|
|
try {
|
|
|
$questionExpansionService = $this->questionExpansionService ?? app(QuestionExpansionService::class);
|
|
|
$questionLocalService = app(QuestionLocalService::class);
|
|
|
- Log::info('LearningAnalyticsService: 从容器获取服务实例(兼容模式)');
|
|
|
|
|
|
$strategy = new ExamTypeStrategy($questionExpansionService, $questionLocalService);
|
|
|
$params = $strategy->buildParamsLegacy($params, $examTypeLegacy);
|
|
|
-
|
|
|
- Log::debug('LearningAnalyticsService: 已应用组卷策略(兼容模式)', [
|
|
|
- 'exam_type' => $examTypeLegacy
|
|
|
- ]);
|
|
|
} catch (Exception $e) {
|
|
|
Log::warning('LearningAnalyticsService: 组卷策略应用失败,使用默认策略', [
|
|
|
'exam_type' => $examTypeLegacy,
|
|
|
@@ -1261,10 +1247,6 @@ class LearningAnalyticsService
|
|
|
'trace' => $e->getTraceAsString()
|
|
|
]);
|
|
|
}
|
|
|
- } else {
|
|
|
- Log::info('LearningAnalyticsService: 跳过组卷策略', [
|
|
|
- 'reason' => '通用类型不需要特殊策略'
|
|
|
- ]);
|
|
|
}
|
|
|
|
|
|
$studentId = $params['student_id'] ?? null;
|
|
|
@@ -1289,43 +1271,24 @@ class LearningAnalyticsService
|
|
|
$difficultyLevels = $params['difficulty_levels'] ?? [];
|
|
|
// 如果用户没有选择任何难度,difficultyLevels 为空数组,表示随机难度
|
|
|
|
|
|
- Log::info("generateIntelligentExam 开始", [
|
|
|
- 'student_id' => $studentId,
|
|
|
- 'total_questions' => $totalQuestions,
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'kp_count' => count($kpCodes),
|
|
|
- ]);
|
|
|
-
|
|
|
// 1. 如果指定了学生,获取学生的薄弱点
|
|
|
$weaknessFilter = [];
|
|
|
if ($studentId) {
|
|
|
- Log::debug("获取学生薄弱点", ['student_id' => $studentId]);
|
|
|
-
|
|
|
$weaknesses = $this->getStudentWeaknesses($studentId, 20);
|
|
|
- Log::debug("薄弱点数量", ['count' => count($weaknesses)]);
|
|
|
|
|
|
$weaknessFilter = array_column($weaknesses, 'kp_code');
|
|
|
|
|
|
// 【修复】教材出卷(assemble_type=3)不使用薄弱点,严格按章节获取知识点
|
|
|
if ($assembleType == 3) {
|
|
|
- Log::debug("LearningAnalyticsService: 教材出卷不使用薄弱点");
|
|
|
// 教材组卷不使用薄弱点
|
|
|
} else {
|
|
|
// 如果用户没有指定知识点,使用学生的薄弱点(非教材组卷)
|
|
|
if (empty($kpCodes)) {
|
|
|
$kpCodes = $weaknessFilter;
|
|
|
- Log::info("用户未选择知识点,使用薄弱点作为kp_codes", [
|
|
|
- '最终kp_codes' => $kpCodes,
|
|
|
- ]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Log::debug("准备调用 getQuestionsFromBank", [
|
|
|
- 'kp_codes_count' => count($kpCodes),
|
|
|
- 'skills_count' => count($skills),
|
|
|
- ]);
|
|
|
-
|
|
|
// 2. 优先使用学生错题(如果存在)
|
|
|
$mistakeQuestionIds = $params['mistake_question_ids'] ?? [];
|
|
|
$priorityQuestions = [];
|
|
|
@@ -1333,39 +1296,14 @@ class LearningAnalyticsService
|
|
|
$poolLimit = 0; // 题库池不设上限,0 表示不限制
|
|
|
|
|
|
if (!empty($mistakeQuestionIds)) {
|
|
|
- Log::info('LearningAnalyticsService: 优先获取学生错题', [
|
|
|
- 'mistake_question_ids' => $mistakeQuestionIds,
|
|
|
- 'count' => count($mistakeQuestionIds),
|
|
|
- 'max_limit' => $maxQuestions
|
|
|
- ]);
|
|
|
-
|
|
|
// 如果错题超过最大值,截取到最大值
|
|
|
$truncatedMistakeIds = $mistakeQuestionIds;
|
|
|
if (count($mistakeQuestionIds) > $maxQuestions) {
|
|
|
- Log::warning('LearningAnalyticsService: 错题数量超过最大值限制,已截取', [
|
|
|
- 'mistake_count' => count($mistakeQuestionIds),
|
|
|
- 'max_limit' => $maxQuestions,
|
|
|
- 'truncated_count' => $maxQuestions
|
|
|
- ]);
|
|
|
$truncatedMistakeIds = array_slice($mistakeQuestionIds, 0, $maxQuestions);
|
|
|
}
|
|
|
|
|
|
// 获取学生错题的详细信息(错题获取不需要智能补充,不传入 grade/textbook_id)
|
|
|
$priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $truncatedMistakeIds, [], null, null, null, null, 1);
|
|
|
-
|
|
|
- Log::info('LearningAnalyticsService: 错题获取完成', [
|
|
|
- 'priority_questions_count' => count($priorityQuestions),
|
|
|
- 'expected_count' => count($truncatedMistakeIds)
|
|
|
- ]);
|
|
|
-
|
|
|
- // 如果获取的错题数量少于预期,记录警告
|
|
|
- if (count($priorityQuestions) < count($truncatedMistakeIds)) {
|
|
|
- Log::warning('LearningAnalyticsService: 错题获取不完整', [
|
|
|
- 'expected' => count($truncatedMistakeIds),
|
|
|
- 'actual' => count($priorityQuestions),
|
|
|
- 'missing_ids' => array_diff($truncatedMistakeIds, array_column($priorityQuestions, 'id'))
|
|
|
- ]);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
// 3. 处理错题本逻辑
|
|
|
@@ -1377,62 +1315,31 @@ class LearningAnalyticsService
|
|
|
// 【修改】错题本类型严格按错题组卷,不补充题目
|
|
|
if ($isMistakeBook) {
|
|
|
$mistakeKpSource = (bool) ($params['mistake_kp_source'] ?? false);
|
|
|
- Log::info('LearningAnalyticsService: 错题本严格按错题组卷,不补充题目', [
|
|
|
- 'mistake_count' => count($priorityQuestions),
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'mistake_kp_source' => $mistakeKpSource,
|
|
|
- 'action' => '只使用学生错题,不从原卷子补充'
|
|
|
- ]);
|
|
|
-
|
|
|
// 如果完全没有错题,回退到知识点/题库组卷
|
|
|
if (!$hasMistakePriority && !$mistakeKpSource) {
|
|
|
- Log::warning('LearningAnalyticsService: 错题本无错题', [
|
|
|
- 'student_id' => $studentId,
|
|
|
- 'paper_ids' => $params['paper_ids'] ?? []
|
|
|
- ]);
|
|
|
- Log::info('LearningAnalyticsService: 错题本无错题,允许补充题目', [
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'action' => 'fallback_to_kp_or_bank'
|
|
|
- ]);
|
|
|
} elseif (!$hasMistakePriority && $mistakeKpSource) {
|
|
|
- Log::info('LearningAnalyticsService: 错题本使用错题知识点组卷', [
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'action' => 'use_mistake_kp_codes'
|
|
|
- ]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!$strictMistakeBook && count($priorityQuestions) < $totalQuestions) {
|
|
|
try {
|
|
|
- // 【优化】教材出卷(assemble_type=3)保留知识点筛选,但额外使用章节筛选
|
|
|
- if ($assembleType == 3) {
|
|
|
- Log::debug('LearningAnalyticsService: 教材出卷模式,保留知识点筛选', [
|
|
|
- 'kp_codes_count' => count($kpCodes),
|
|
|
- ]);
|
|
|
- }
|
|
|
-
|
|
|
// 【优化】获取textbook_catalog_node_ids参数(教材组卷时使用)
|
|
|
$textbookCatalogNodeIds = $params['textbook_catalog_node_ids'] ?? null;
|
|
|
// 【修复超纲问题】获取 textbook_id 和 difficulty_category,用于智能补充时限制范围
|
|
|
$textbookId = isset($params['textbook_id']) ? (int) $params['textbook_id'] : null;
|
|
|
$difficultyCategory = (int) ($params['difficulty_category'] ?? 1);
|
|
|
|
|
|
- Log::debug('开始调用 getQuestionsFromBank 补充题目', [
|
|
|
- 'need_more' => $totalQuestions - count($priorityQuestions),
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'grade' => $grade,
|
|
|
- ]);
|
|
|
-
|
|
|
// 【修复超纲问题】传入 grade 和 textbook_id,用于智能补充时限制范围
|
|
|
// 【修复】传入实际需要数量,否则 totalNeeded=0 会导致智能补充不触发
|
|
|
$excludeQuestionIds = $params['exclude_question_ids'] ?? [];
|
|
|
$needCount = $totalQuestions - count($priorityQuestions);
|
|
|
+ $baseNeedCount = in_array($assembleType, [2, 3], true) ? 0 : $needCount;
|
|
|
$additionalQuestions = $this->getQuestionsFromBank(
|
|
|
$kpCodes,
|
|
|
$skills,
|
|
|
$studentId,
|
|
|
$questionTypeRatio,
|
|
|
- $needCount,
|
|
|
+ $baseNeedCount,
|
|
|
[],
|
|
|
$excludeQuestionIds,
|
|
|
$questionCategory,
|
|
|
@@ -1443,40 +1350,134 @@ class LearningAnalyticsService
|
|
|
);
|
|
|
$allQuestions = $this->dedupeQuestionsByBankId(array_merge($priorityQuestions, $additionalQuestions));
|
|
|
|
|
|
- // assemble_type=2:本源 KP 合并后仍不足,用父树补题 KP 拉题(与 getQuestionsFromBank 主条件一致,走专用列表,不混入本源查询)
|
|
|
+ // assemble_type=2/3:本源池不足,先用父树补题 KP 拉题(兄弟节点优先)
|
|
|
if (
|
|
|
- $assembleType === 2
|
|
|
+ in_array($assembleType, [2, 3], true)
|
|
|
&& count($allQuestions) < $totalQuestions
|
|
|
&& ! empty($params['kp_supplement_subtree_codes'] ?? [])
|
|
|
&& $grade !== null
|
|
|
) {
|
|
|
+ $before = count($allQuestions);
|
|
|
$excludeForSupp = array_values(array_unique(array_filter(array_merge(
|
|
|
$excludeQuestionIds,
|
|
|
array_column($allQuestions, 'id')
|
|
|
))));
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'kp_subtree',
|
|
|
+ 'status' => 'start',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ ]);
|
|
|
$supp = $this->fetchQuestionsForKpAssembleSupplement(
|
|
|
$params['kp_supplement_subtree_codes'],
|
|
|
$excludeForSupp,
|
|
|
(int) $grade,
|
|
|
$skills,
|
|
|
$questionCategory,
|
|
|
- $textbookCatalogNodeIds
|
|
|
+ $assembleType === 3 ? null : $textbookCatalogNodeIds
|
|
|
);
|
|
|
- $before = count($allQuestions);
|
|
|
+ $supplementStats = $this->buildSupplementQuestionStats($supp);
|
|
|
$allQuestions = $this->dedupeQuestionsByBankId(array_merge($allQuestions, $supp));
|
|
|
- Log::info('LearningAnalyticsService: 知识点组卷父树补题(本源池不足后)', [
|
|
|
- 'source_merged_count' => $before,
|
|
|
- 'supplement_fetched' => count($supp),
|
|
|
- 'after_merge_count' => count($allQuestions),
|
|
|
- 'target_total' => $totalQuestions,
|
|
|
- 'supplement_kp_sample' => array_slice($params['kp_supplement_subtree_codes'], 0, 12),
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'kp_subtree',
|
|
|
+ 'status' => empty($supp) ? 'empty' : 'done',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'supplement_count' => count($supp),
|
|
|
+ 'after_count' => count($allQuestions),
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ 'supplement_kp_distribution_topn' => $supplementStats['supplement_kp_distribution_topn'],
|
|
|
+ 'supplement_kp_sample' => $supplementStats['supplement_kp_sample'],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // assemble_type=3:兄弟补题后仍不足,再走教材前章节补题兜底
|
|
|
+ if (
|
|
|
+ $assembleType === 3
|
|
|
+ && count($allQuestions) < $totalQuestions
|
|
|
+ && $grade !== null
|
|
|
+ ) {
|
|
|
+ $before = count($allQuestions);
|
|
|
+ $deficit = $totalQuestions - count($allQuestions);
|
|
|
+ $excludeForChapterSupplement = array_values(array_unique(array_filter(array_merge(
|
|
|
+ $excludeQuestionIds,
|
|
|
+ array_column($allQuestions, 'id')
|
|
|
+ ))));
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'chapter_fallback',
|
|
|
+ 'status' => 'start',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ ]);
|
|
|
+ $chapterSupplement = $this->getSupplementaryQuestionsForGrade(
|
|
|
+ (int) $grade,
|
|
|
+ array_column($allQuestions, 'kp_code'),
|
|
|
+ $deficit,
|
|
|
+ $difficultyCategory,
|
|
|
+ $textbookId,
|
|
|
+ $excludeForChapterSupplement,
|
|
|
+ $textbookCatalogNodeIds ?? null,
|
|
|
+ $studentId
|
|
|
+ );
|
|
|
+
|
|
|
+ $chapterSupplementStats = $this->buildSupplementQuestionStats($chapterSupplement);
|
|
|
+ $allQuestions = $this->dedupeQuestionsByBankId(array_merge($allQuestions, $chapterSupplement));
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'chapter_fallback',
|
|
|
+ 'status' => empty($chapterSupplement) ? 'empty' : 'done',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'supplement_count' => count($chapterSupplement),
|
|
|
+ 'after_count' => count($allQuestions),
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ 'supplement_kp_distribution_topn' => $chapterSupplementStats['supplement_kp_distribution_topn'],
|
|
|
+ 'supplement_kp_sample' => $chapterSupplementStats['supplement_kp_sample'],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通用兜底:无论何种组卷类型,前序补题后仍不足时,最终都走常规补题策略
|
|
|
+ if (count($allQuestions) < $totalQuestions && $grade !== null) {
|
|
|
+ $before = count($allQuestions);
|
|
|
+ $deficit = $totalQuestions - count($allQuestions);
|
|
|
+ $excludeForCommonFallback = array_values(array_unique(array_filter(array_merge(
|
|
|
+ $excludeQuestionIds,
|
|
|
+ array_column($allQuestions, 'id')
|
|
|
+ ))));
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'common_fallback',
|
|
|
+ 'status' => 'start',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ ]);
|
|
|
+ $commonFallback = $this->getSupplementaryQuestionsForGrade(
|
|
|
+ (int) $grade,
|
|
|
+ array_column($allQuestions, 'kp_code'),
|
|
|
+ $deficit,
|
|
|
+ $difficultyCategory,
|
|
|
+ $textbookId,
|
|
|
+ $excludeForCommonFallback,
|
|
|
+ $assembleType === 3 ? null : ($textbookCatalogNodeIds ?? null),
|
|
|
+ $studentId
|
|
|
+ );
|
|
|
+
|
|
|
+ $commonFallbackStats = $this->buildSupplementQuestionStats($commonFallback);
|
|
|
+ $allQuestions = $this->dedupeQuestionsByBankId(array_merge($allQuestions, $commonFallback));
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'common_fallback',
|
|
|
+ 'status' => empty($commonFallback) ? 'empty' : 'done',
|
|
|
+ 'before_count' => $before,
|
|
|
+ 'supplement_count' => count($commonFallback),
|
|
|
+ 'after_count' => count($allQuestions),
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ 'supplement_kp_distribution_topn' => $commonFallbackStats['supplement_kp_distribution_topn'],
|
|
|
+ 'supplement_kp_sample' => $commonFallbackStats['supplement_kp_sample'],
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
- Log::info('getQuestionsFromBank 完成', [
|
|
|
- 'questions_count' => count($allQuestions),
|
|
|
- 'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
- ]);
|
|
|
} catch (\Exception $e) {
|
|
|
Log::error('getQuestionsFromBank 调用失败', [
|
|
|
'error' => $e->getMessage(),
|
|
|
@@ -1485,13 +1486,6 @@ class LearningAnalyticsService
|
|
|
|
|
|
throw $e;
|
|
|
}
|
|
|
- } elseif ($strictMistakeBook) {
|
|
|
- // 错题本类型:不补充题目,只使用错题
|
|
|
- Log::info('错题本类型:不补充题目,只使用错题', [
|
|
|
- 'assemble_type' => $assembleType,
|
|
|
- 'mistake_questions_count' => count($priorityQuestions),
|
|
|
- 'total_questions_requested' => $totalQuestions
|
|
|
- ]);
|
|
|
}
|
|
|
|
|
|
if (empty($allQuestions)) {
|
|
|
@@ -1504,14 +1498,6 @@ class LearningAnalyticsService
|
|
|
$message = '题库为空,请先添加题目到题库。您可以点击"生成练习题"按钮或手动上传题目。';
|
|
|
}
|
|
|
|
|
|
- Log::warning('智能出卷失败 - 未找到题目', [
|
|
|
- 'student_id' => $studentId,
|
|
|
- 'selected_kp_codes' => $kpCodes,
|
|
|
- 'kp_codes_count' => count($kpCodes),
|
|
|
- 'message' => $message,
|
|
|
- 'hint' => '如果选择了知识点但题库为空,请检查知识点代码是否正确,或尝试取消知识点选择'
|
|
|
- ]);
|
|
|
-
|
|
|
return [
|
|
|
'success' => false,
|
|
|
'message' => $message,
|
|
|
@@ -1530,17 +1516,9 @@ class LearningAnalyticsService
|
|
|
$questionTypeRatio,
|
|
|
$difficultyLevels,
|
|
|
$weaknessFilter,
|
|
|
- $assembleType // 新增assembleType参数
|
|
|
+ $assembleType, // 新增assembleType参数
|
|
|
+ $params['kp_code_list_original'] ?? $params['kp_codes'] ?? []
|
|
|
);
|
|
|
- $selectTime = (microtime(true) - $startTime) * 1000;
|
|
|
-
|
|
|
- Log::debug('题目筛选结果', [
|
|
|
- 'input_count' => count($allQuestions),
|
|
|
- 'selected_count' => count($selectedQuestions),
|
|
|
- 'target_count' => $targetQuestionCount,
|
|
|
- 'select_time_ms' => round($selectTime, 2)
|
|
|
- ]);
|
|
|
-
|
|
|
if (empty($selectedQuestions)) {
|
|
|
return [
|
|
|
'success' => false,
|
|
|
@@ -1552,7 +1530,8 @@ class LearningAnalyticsService
|
|
|
// 【恢复】简化难度分布检查
|
|
|
$difficultyCategory = $params['difficulty_category'] ?? 1;
|
|
|
$enableDistribution = $params['enable_difficulty_distribution'] ?? false;
|
|
|
- $isExcludedType = false; // 统一允许应用难度分布(错题本类型也允许)
|
|
|
+ // assemble_type=2(知识点组卷)优先保证请求知识点覆盖,避免二次难度重排把次要 KP 挤没
|
|
|
+ $isExcludedType = ($assembleType === 2);
|
|
|
|
|
|
if ($enableDistribution && !$isExcludedType) {
|
|
|
try {
|
|
|
@@ -1564,17 +1543,24 @@ class LearningAnalyticsService
|
|
|
$questionTypeRatio
|
|
|
);
|
|
|
|
|
|
- Log::debug('LearningAnalyticsService: 题型感知难度分布完成', [
|
|
|
- 'after_count' => count($selectedQuestions),
|
|
|
- 'type_breakdown' => $this->countByType($selectedQuestions),
|
|
|
- ]);
|
|
|
} catch (\Exception $e) {
|
|
|
- Log::warning('LearningAnalyticsService: 题型感知难度分布失败,继续使用原结果', [
|
|
|
- 'error' => $e->getMessage(),
|
|
|
- ]);
|
|
|
+ // 保持组卷主流程稳定,难度分布失败时继续返回原结果
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ $requestedKpSelectionStats = $this->buildRequestedKpSelectionStats(
|
|
|
+ $selectedQuestions,
|
|
|
+ $params['kp_code_list_original'] ?? $params['kp_codes'] ?? []
|
|
|
+ );
|
|
|
+
|
|
|
+ Log::info('LearningAnalyticsService: 最终选题分布', [
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ 'question_count' => count($selectedQuestions),
|
|
|
+ 'type_distribution' => $this->countByType($selectedQuestions),
|
|
|
+ 'kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code')),
|
|
|
+ 'requested_kp_selection' => $assembleType === 2 ? $requestedKpSelectionStats : null,
|
|
|
+ ]);
|
|
|
+
|
|
|
return [
|
|
|
'success' => true,
|
|
|
'message' => '智能出卷成功',
|
|
|
@@ -1597,6 +1583,7 @@ class LearningAnalyticsService
|
|
|
(int) $difficultyCategory,
|
|
|
(int) $totalQuestions
|
|
|
),
|
|
|
+ 'requested_kp_selection' => $assembleType === 2 ? $requestedKpSelectionStats : null,
|
|
|
// 【新增】章节知识点数量统计(教材组卷时)
|
|
|
'chapter_knowledge_point_stats' => $params['chapter_knowledge_point_stats'] ?? null,
|
|
|
'textbook_catalog_node_ids' => $params['textbook_catalog_node_ids'] ?? null
|
|
|
@@ -1738,21 +1725,12 @@ class LearningAnalyticsService
|
|
|
try {
|
|
|
// 错题回顾:优先获取指定的学生错题
|
|
|
if (!empty($priorityQuestionIds)) {
|
|
|
- Log::info('getQuestionsFromBank: 优先获取学生错题', [
|
|
|
- 'priority_count' => count($priorityQuestionIds),
|
|
|
- 'priority_ids' => $priorityQuestionIds
|
|
|
- ]);
|
|
|
|
|
|
$priorityQuestions = $this->getLocalQuestionsByIds($priorityQuestionIds);
|
|
|
|
|
|
if (!empty($priorityQuestions)) {
|
|
|
- Log::info('getQuestionsFromBank: 优先错题获取成功', [
|
|
|
- 'count' => count($priorityQuestions)
|
|
|
- ]);
|
|
|
-
|
|
|
return $priorityQuestions;
|
|
|
} else {
|
|
|
- Log::warning('getQuestionsFromBank: 优先错题获取失败,返回空数组让上层处理');
|
|
|
// 错题本类型获取不到错题时,返回空数组,不回退到题库随机选题
|
|
|
return [];
|
|
|
}
|
|
|
@@ -1809,13 +1787,6 @@ class LearningAnalyticsService
|
|
|
|
|
|
$questions = $query->get();
|
|
|
|
|
|
- Log::info('getQuestionsFromBank: 查询完成', [
|
|
|
- 'raw_count' => $questions->count(),
|
|
|
- 'total_needed' => $totalNeeded,
|
|
|
- 'exclude_count' => count($excludeQuestionIds),
|
|
|
- 'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
- ]);
|
|
|
-
|
|
|
// 转换为标准格式
|
|
|
$formattedQuestions = $questions->map(function ($q) {
|
|
|
return [
|
|
|
@@ -1844,13 +1815,15 @@ class LearningAnalyticsService
|
|
|
// 【修复】重新启用智能补充功能,增加 textbook_id 限制避免超纲
|
|
|
if ($totalNeeded > 0 && count($selectedQuestions) < $totalNeeded && $grade !== null) {
|
|
|
$deficit = $totalNeeded - count($selectedQuestions);
|
|
|
- Log::warning('getQuestionsFromBank: 指定知识点题目不足,尝试智能补充', [
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'start',
|
|
|
'deficit' => $deficit,
|
|
|
- 'available_count' => count($selectedQuestions),
|
|
|
+ 'current_count' => count($selectedQuestions),
|
|
|
+ 'target_count' => $totalNeeded,
|
|
|
'grade' => $grade,
|
|
|
'textbook_id' => $textbookId,
|
|
|
- 'student_id' => $studentId ? '(有)' : '(无)',
|
|
|
- 'strategy' => $textbookId ? '从同教材前章节补充' : ($studentId ? '从学生已学知识点补充' : '无教材且无学生ID,不补充')
|
|
|
+ 'has_student_id' => ! empty($studentId),
|
|
|
+ 'has_chapter_scope' => ! empty($textbookCatalogNodeIds),
|
|
|
]);
|
|
|
|
|
|
// 【修复超纲问题】补充策略:只从「学过的」内容补充
|
|
|
@@ -1869,28 +1842,26 @@ class LearningAnalyticsService
|
|
|
|
|
|
if (!empty($supplementaryQuestions)) {
|
|
|
$selectedQuestions = array_merge($selectedQuestions, $supplementaryQuestions);
|
|
|
- Log::info('getQuestionsFromBank: 智能补充完成', [
|
|
|
- 'supplementary_count' => count($supplementaryQuestions),
|
|
|
- 'total_after_supplement' => count($selectedQuestions),
|
|
|
- 'textbook_id' => $textbookId
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'done',
|
|
|
+ 'supplement_count' => count($supplementaryQuestions),
|
|
|
+ 'after_count' => count($selectedQuestions),
|
|
|
+ 'target_count' => $totalNeeded,
|
|
|
+ 'grade' => $grade,
|
|
|
+ 'textbook_id' => $textbookId,
|
|
|
]);
|
|
|
} else {
|
|
|
- Log::warning('getQuestionsFromBank: 智能补充失败,未找到合适的题目', [
|
|
|
+ Log::info('assemble.supplement', [
|
|
|
+ 'stage' => 'empty',
|
|
|
+ 'supplement_count' => 0,
|
|
|
+ 'after_count' => count($selectedQuestions),
|
|
|
+ 'target_count' => $totalNeeded,
|
|
|
'grade' => $grade,
|
|
|
'textbook_id' => $textbookId,
|
|
|
- 'note' => $textbookId ? '可能是该教材知识点题目不足' : '该年级题目不足'
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Log::info('getQuestionsFromBank 完成', [
|
|
|
- 'final_count' => count($selectedQuestions),
|
|
|
- 'raw_database_count' => $questions->count(),
|
|
|
- 'total_needed' => $totalNeeded,
|
|
|
- 'success' => $totalNeeded > 0 ? count($selectedQuestions) >= $totalNeeded : true,
|
|
|
- 'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
- ]);
|
|
|
-
|
|
|
return $selectedQuestions;
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
@@ -2101,7 +2072,8 @@ class LearningAnalyticsService
|
|
|
array $questionTypeRatio,
|
|
|
array $difficultyLevels,
|
|
|
array $weaknessFilter,
|
|
|
- int $assembleType // 新增assembleType参数
|
|
|
+ int $assembleType, // 新增assembleType参数
|
|
|
+ array $requestedKpCodes = []
|
|
|
): array {
|
|
|
// 【修复】题目数量处理逻辑:无论题目数量多少,都要进行权重分配和筛选
|
|
|
// 如果题目数量超过目标,则截取到目标数量
|
|
|
@@ -2123,6 +2095,35 @@ class LearningAnalyticsService
|
|
|
// 题目本身就有难度系数,QuestionLocalService的难度分布系统会处理题目分布
|
|
|
// 不需要额外的难度筛选,让题目保持原始的难度分布
|
|
|
|
|
|
+ $requestedKpCodes = array_values(array_unique(array_filter($requestedKpCodes)));
|
|
|
+ $requestedKpSet = array_fill_keys($requestedKpCodes, true);
|
|
|
+ $shouldRestrictToRequestedKps = ($assembleType === 2 && ! empty($requestedKpSet));
|
|
|
+
|
|
|
+ if ($shouldRestrictToRequestedKps) {
|
|
|
+ $requestedQuestions = array_values(array_filter($questions, function ($question) use ($requestedKpSet) {
|
|
|
+ $kpCode = $question['kp_code'] ?? '';
|
|
|
+ return isset($requestedKpSet[$kpCode]);
|
|
|
+ }));
|
|
|
+ $fallbackQuestions = array_values(array_filter($questions, function ($question) use ($requestedKpSet) {
|
|
|
+ $kpCode = $question['kp_code'] ?? '';
|
|
|
+ return ! isset($requestedKpSet[$kpCode]);
|
|
|
+ }));
|
|
|
+
|
|
|
+ Log::info('selectQuestionsByMastery: assemble_type=2 请求知识点约束', [
|
|
|
+ 'requested_kp_codes' => $requestedKpCodes,
|
|
|
+ 'requested_question_count' => count($requestedQuestions),
|
|
|
+ 'fallback_question_count' => count($fallbackQuestions),
|
|
|
+ 'target_question_count' => $totalQuestions,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 原始请求知识点足够时,最终选题不应被补题池中的兄弟/父树知识点带偏。
|
|
|
+ if (count($requestedQuestions) >= $totalQuestions) {
|
|
|
+ $questions = $requestedQuestions;
|
|
|
+ } else {
|
|
|
+ $questions = array_merge($requestedQuestions, $fallbackQuestions);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 1. 按知识点分组
|
|
|
$groupStartTime = microtime(true);
|
|
|
$questionsByKp = [];
|
|
|
@@ -2232,8 +2233,10 @@ class LearningAnalyticsService
|
|
|
// ========== 步骤1:按题型分配题目 ==========
|
|
|
$selectedQuestions = [];
|
|
|
|
|
|
- // 【区分】根据assembleType决定是否使用知识点优先机制
|
|
|
- $useKnowledgePointPriority = ($assembleType === 0); // 摸底测试需要知识点优先
|
|
|
+ // 【区分】根据 assembleType 决定是否使用知识点覆盖优先机制
|
|
|
+ // - 0:摸底测试,优先扩大知识点覆盖
|
|
|
+ // - 2:知识点组卷,也应尽量覆盖请求中的多个知识点,避免被单一 KP 吞掉
|
|
|
+ $useKnowledgePointPriority = in_array($assembleType, [0, 2], true);
|
|
|
$kpSelected = []; // 已选知识点记录
|
|
|
|
|
|
// 【新增】非摸底类型:按比例计算每种题型的目标数量
|
|
|
@@ -2299,24 +2302,45 @@ class LearningAnalyticsService
|
|
|
|
|
|
// 根据策略选择题目
|
|
|
if ($useKnowledgePointPriority) {
|
|
|
- // 摸底测试:选择第一个未选过知识点的题目
|
|
|
+ // 知识点优先:assemble_type=2 先从原始请求知识点里选;摸底则扩大覆盖
|
|
|
$selectedInThisType = 0;
|
|
|
+ $candidate = null;
|
|
|
+
|
|
|
foreach ($questionsByType[$type] as $q) {
|
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
|
+ if ($shouldRestrictToRequestedKps && ! isset($requestedKpSet[$kpCode])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
if (!isset($kpSelected[$kpCode])) {
|
|
|
- $selectedQuestions[] = $q;
|
|
|
- $kpSelected[$kpCode] = true;
|
|
|
- $selectedInThisType++;
|
|
|
- Log::debug('题型基础分配(知识点优先)', [
|
|
|
- 'type' => $type,
|
|
|
- 'kp' => $kpCode,
|
|
|
- 'question_id' => $q['id'] ?? 'unknown',
|
|
|
- 'selected_in_type' => $selectedInThisType
|
|
|
- ]);
|
|
|
+ $candidate = $q;
|
|
|
break; // 只选1题
|
|
|
}
|
|
|
}
|
|
|
- Log::debug('摸底测试题型基础分配完成', [
|
|
|
+
|
|
|
+ if ($candidate === null && $shouldRestrictToRequestedKps) {
|
|
|
+ foreach ($questionsByType[$type] as $q) {
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
+ if (isset($requestedKpSet[$kpCode])) {
|
|
|
+ $candidate = $q;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($candidate !== null) {
|
|
|
+ $kpCode = $candidate['kp_code'] ?? '';
|
|
|
+ $selectedQuestions[] = $candidate;
|
|
|
+ $kpSelected[$kpCode] = true;
|
|
|
+ $selectedInThisType++;
|
|
|
+ Log::debug('题型基础分配(知识点优先)', [
|
|
|
+ 'type' => $type,
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'question_id' => $candidate['id'] ?? 'unknown',
|
|
|
+ 'selected_in_type' => $selectedInThisType
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::debug('知识点优先题型基础分配完成', [
|
|
|
'type' => $type,
|
|
|
'selected_count' => $selectedInThisType
|
|
|
]);
|
|
|
@@ -2406,27 +2430,56 @@ class LearningAnalyticsService
|
|
|
'total_questions' => $totalQuestions,
|
|
|
'selected_kp_codes' => array_keys($kpSelected),
|
|
|
'available_kp_count' => count($preSortKpDistribution),
|
|
|
- 'strategy' => $useKnowledgePointPriority ? '知识点优先' : '无知识点限制'
|
|
|
+ 'strategy' => $useKnowledgePointPriority ? '知识点覆盖优先' : '无知识点限制'
|
|
|
]);
|
|
|
|
|
|
if ($useKnowledgePointPriority) {
|
|
|
- // 摸底测试:优先选择未选过知识点的题目
|
|
|
+ // 知识点优先:assemble_type=2 先补齐原始请求知识点,再允许请求知识点重复,最后才使用 fallback KP
|
|
|
$initialSelectedCount = count($selectedQuestions);
|
|
|
$prioritySelectedCount = 0;
|
|
|
- foreach ($allQuestions as $q) {
|
|
|
- if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
+ $selectedIds = array_values(array_filter(array_column($selectedQuestions, 'id')));
|
|
|
|
|
|
- $kpCode = $q['kp_code'] ?? '';
|
|
|
- if (!isset($kpSelected[$kpCode])) {
|
|
|
- $selectedQuestions[] = $q;
|
|
|
- $kpSelected[$kpCode] = true;
|
|
|
- $prioritySelectedCount++;
|
|
|
- Log::debug('继续选择题目(知识点优先)', [
|
|
|
- 'kp' => $kpCode,
|
|
|
- 'id' => $q['id'] ?? 'unknown',
|
|
|
- 'priority_selected_count' => $prioritySelectedCount,
|
|
|
- 'total_selected' => count($selectedQuestions)
|
|
|
- ]);
|
|
|
+ if ($shouldRestrictToRequestedKps) {
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
+ $qid = $q['id'] ?? null;
|
|
|
+ if (!isset($requestedKpSet[$kpCode]) || $qid === null || in_array($qid, $selectedIds)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($kpSelected[$kpCode])) {
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
+ $selectedIds[] = $qid;
|
|
|
+ $kpSelected[$kpCode] = true;
|
|
|
+ $prioritySelectedCount++;
|
|
|
+ Log::debug('继续选择题目(请求知识点优先)', [
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'id' => $qid,
|
|
|
+ 'priority_selected_count' => $prioritySelectedCount,
|
|
|
+ 'total_selected' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
+
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
+ if (!isset($kpSelected[$kpCode])) {
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
+ $kpSelected[$kpCode] = true;
|
|
|
+ $prioritySelectedCount++;
|
|
|
+ Log::debug('继续选择题目(知识点优先)', [
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'id' => $q['id'] ?? 'unknown',
|
|
|
+ 'priority_selected_count' => $prioritySelectedCount,
|
|
|
+ 'total_selected' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2449,7 +2502,32 @@ class LearningAnalyticsService
|
|
|
]);
|
|
|
|
|
|
$fallbackSelectedCount = 0;
|
|
|
- $selectedIds = array_column($selectedQuestions, 'id');
|
|
|
+ $selectedIds = array_values(array_filter(array_column($selectedQuestions, 'id')));
|
|
|
+
|
|
|
+ if ($shouldRestrictToRequestedKps) {
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
+ $qid = $q['id'] ?? null;
|
|
|
+ if ($qid === null || in_array($qid, $selectedIds) || !isset($requestedKpSet[$kpCode])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
+ $selectedIds[] = $qid;
|
|
|
+ $fallbackSelectedCount++;
|
|
|
+ Log::debug('降级选择题目(请求知识点重复)', [
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'id' => $qid,
|
|
|
+ 'fallback_selected_count' => $fallbackSelectedCount,
|
|
|
+ 'current_count' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
foreach ($allQuestions as $q) {
|
|
|
if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
|
@@ -2518,7 +2596,8 @@ class LearningAnalyticsService
|
|
|
'selected_count' => count($selectedQuestions),
|
|
|
'success' => count($selectedQuestions) === $totalQuestions,
|
|
|
'assemble_type' => $assembleType,
|
|
|
- 'strategy' => $useKnowledgePointPriority ? '知识点优先' : '无知识点限制',
|
|
|
+ 'strategy' => $useKnowledgePointPriority ? '知识点覆盖优先' : '无知识点限制',
|
|
|
+ 'requested_kp_selection' => $this->buildRequestedKpSelectionStats($selectedQuestions, $requestedKpCodes),
|
|
|
'type_distribution' => array_count_values(array_map(function($q) {
|
|
|
$qid = $q['id'] ?? $q['question_id'] ?? null;
|
|
|
if ($qid && isset($questionTypeCache[$qid])) {
|
|
|
@@ -2641,6 +2720,68 @@ class LearningAnalyticsService
|
|
|
return $finalQuestions;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @param array<int, array<string, mixed>> $questions
|
|
|
+ * @param array<int, string> $requestedKpCodes
|
|
|
+ * @return array<string, mixed>
|
|
|
+ */
|
|
|
+ private function buildRequestedKpSelectionStats(array $questions, array $requestedKpCodes): array
|
|
|
+ {
|
|
|
+ $requestedKpCodes = array_values(array_unique(array_filter($requestedKpCodes)));
|
|
|
+ if ($requestedKpCodes === []) {
|
|
|
+ return [
|
|
|
+ 'requested_kp_codes' => [],
|
|
|
+ 'requested_kp_selected_count' => 0,
|
|
|
+ 'related_kp_selected_count' => 0,
|
|
|
+ 'requested_kp_distribution' => [],
|
|
|
+ 'related_kp_distribution' => [],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $requestedKpSet = array_fill_keys($requestedKpCodes, true);
|
|
|
+ $requestedDistribution = [];
|
|
|
+ $relatedDistribution = [];
|
|
|
+
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $kpCode = (string) ($question['kp_code'] ?? '');
|
|
|
+ if ($kpCode === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($requestedKpSet[$kpCode])) {
|
|
|
+ $requestedDistribution[$kpCode] = ($requestedDistribution[$kpCode] ?? 0) + 1;
|
|
|
+ } else {
|
|
|
+ $relatedDistribution[$kpCode] = ($relatedDistribution[$kpCode] ?? 0) + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'requested_kp_codes' => $requestedKpCodes,
|
|
|
+ 'requested_kp_selected_count' => array_sum($requestedDistribution),
|
|
|
+ 'related_kp_selected_count' => array_sum($relatedDistribution),
|
|
|
+ 'requested_kp_distribution' => $requestedDistribution,
|
|
|
+ 'related_kp_distribution' => $relatedDistribution,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<int, array<string, mixed>> $questions
|
|
|
+ * @return array<string, mixed>
|
|
|
+ */
|
|
|
+ private function buildSupplementQuestionStats(array $questions): array
|
|
|
+ {
|
|
|
+ $kpDistribution = array_count_values(array_filter(array_map(
|
|
|
+ static fn ($question) => (string) ($question['kp_code'] ?? ''),
|
|
|
+ $questions
|
|
|
+ )));
|
|
|
+ arsort($kpDistribution);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'supplement_kp_distribution_topn' => array_slice($kpDistribution, 0, 10, true),
|
|
|
+ 'supplement_kp_sample' => array_slice(array_keys($kpDistribution), 0, 10),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取学生对特定知识点的掌握度
|
|
|
*/
|
|
|
@@ -3052,15 +3193,23 @@ class LearningAnalyticsService
|
|
|
]);
|
|
|
|
|
|
// 【核心】补充范围:只从「学过的」内容补充,不能从未学知识点或年级对应章节中选
|
|
|
- // - 有 textbookId:从 getGradeKnowledgePoints(grade, textbookId) + 前章节(若有 textbookCatalogNodeIds)
|
|
|
+ // - 有 textbookId:从 textbook_id -> 章节 -> 章节知识点关联表 取 kp_code(若有 chapter scope 再收缩到前章节)
|
|
|
// - 无 textbookId 且有 studentId:从学生做过的题目对应知识点(getStudentLearnedKpCodes)补充
|
|
|
// - 无 textbookId 且无 studentId:无法确定学过的内容,不补充
|
|
|
$gradeKpCodes = [];
|
|
|
if ($textbookId) {
|
|
|
- $gradeKpCodes = $this->getGradeKnowledgePoints($grade, $textbookId);
|
|
|
- Log::info('getSupplementaryQuestionsForGrade: 教材模式,使用教材知识点', [
|
|
|
+ $allCatalogNodeIds = DB::table('textbook_catalog_nodes')
|
|
|
+ ->where('textbook_id', $textbookId)
|
|
|
+ ->pluck('id')
|
|
|
+ ->map(static fn ($id) => (int) $id)
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ $gradeKpCodes = $this->getKpCodesForCatalogChapterIds($allCatalogNodeIds);
|
|
|
+
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 教材模式,按章节关联知识点补题', [
|
|
|
'kp_count' => count($gradeKpCodes),
|
|
|
'textbook_id' => $textbookId,
|
|
|
+ 'catalog_node_count' => count($allCatalogNodeIds),
|
|
|
]);
|
|
|
} elseif ($studentId) {
|
|
|
$learnedKps = $this->getStudentLearnedKpCodes($studentId);
|