|
@@ -962,20 +962,32 @@ class ExamTypeStrategy
|
|
|
$kpCodeList = array_values(array_unique(array_filter($kpCodeList)));
|
|
$kpCodeList = array_values(array_unique(array_filter($kpCodeList)));
|
|
|
$studentId = $params['student_id'] ?? null;
|
|
$studentId = $params['student_id'] ?? null;
|
|
|
$totalQuestions = $params['total_questions'] ?? 20;
|
|
$totalQuestions = $params['total_questions'] ?? 20;
|
|
|
|
|
+ $assembleType = (int) ($params['assemble_type'] ?? 4);
|
|
|
|
|
|
|
|
if (empty($kpCodeList)) {
|
|
if (empty($kpCodeList)) {
|
|
|
Log::warning('ExamTypeStrategy: 知识点组卷需要 kp_code_list 参数');
|
|
Log::warning('ExamTypeStrategy: 知识点组卷需要 kp_code_list 参数');
|
|
|
return $this->buildGeneralParams($params);
|
|
return $this->buildGeneralParams($params);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ $kpCodesForPool = $kpCodeList;
|
|
|
|
|
+ if ($assembleType === 2 && filter_var(config('question_bank.kp_assemble_include_parent_subtree', true), FILTER_VALIDATE_BOOLEAN)) {
|
|
|
|
|
+ $kpCodesForPool = $this->expandKpCodesWithParentChapterSubtrees($kpCodeList);
|
|
|
|
|
+ Log::info('ExamTypeStrategy: 知识点组卷已按父节点扩展整棵子树 KP', [
|
|
|
|
|
+ 'original_kp_codes' => $kpCodeList,
|
|
|
|
|
+ 'expanded_kp_codes' => $kpCodesForPool,
|
|
|
|
|
+ 'original_count' => count($kpCodeList),
|
|
|
|
|
+ 'expanded_count' => count($kpCodesForPool),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
Log::debug('ExamTypeStrategy: 知识点组卷', [
|
|
Log::debug('ExamTypeStrategy: 知识点组卷', [
|
|
|
'kp_count' => count($kpCodeList),
|
|
'kp_count' => count($kpCodeList),
|
|
|
|
|
+ 'kp_pool_count' => count($kpCodesForPool),
|
|
|
'total_questions' => $totalQuestions
|
|
'total_questions' => $totalQuestions
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// assemble_type=2 走知识点扩展策略(复用 QuestionExpansionService)。
|
|
// assemble_type=2 走知识点扩展策略(复用 QuestionExpansionService)。
|
|
|
// 只在基础题池不足时,才会触发子知识点补充(由扩展策略内部控制)。
|
|
// 只在基础题池不足时,才会触发子知识点补充(由扩展策略内部控制)。
|
|
|
- $assembleType = (int) ($params['assemble_type'] ?? 4);
|
|
|
|
|
$priorityQuestionIds = [];
|
|
$priorityQuestionIds = [];
|
|
|
$basePoolCount = 0;
|
|
$basePoolCount = 0;
|
|
|
$finalPoolCount = 0;
|
|
$finalPoolCount = 0;
|
|
@@ -985,7 +997,7 @@ class ExamTypeStrategy
|
|
|
$expansionStrategy = $this->questionExpansionService->expandQuestionsByKnowledgePoints(
|
|
$expansionStrategy = $this->questionExpansionService->expandQuestionsByKnowledgePoints(
|
|
|
$params,
|
|
$params,
|
|
|
$studentId ? (string) $studentId : null,
|
|
$studentId ? (string) $studentId : null,
|
|
|
- $kpCodeList,
|
|
|
|
|
|
|
+ $kpCodesForPool,
|
|
|
[],
|
|
[],
|
|
|
(int) $totalQuestions
|
|
(int) $totalQuestions
|
|
|
);
|
|
);
|
|
@@ -1007,13 +1019,15 @@ class ExamTypeStrategy
|
|
|
|
|
|
|
|
Log::debug('ExamTypeStrategy: 知识点组卷题池评估', [
|
|
Log::debug('ExamTypeStrategy: 知识点组卷题池评估', [
|
|
|
'kp_count' => count($kpCodeList),
|
|
'kp_count' => count($kpCodeList),
|
|
|
|
|
+ 'kp_pool_count' => count($kpCodesForPool),
|
|
|
'pool_count' => $finalPoolCount,
|
|
'pool_count' => $finalPoolCount,
|
|
|
'exclude_count' => count($answeredQuestionIds),
|
|
'exclude_count' => count($answeredQuestionIds),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 组装增强参数
|
|
// 组装增强参数
|
|
|
$enhanced = array_merge($params, [
|
|
$enhanced = array_merge($params, [
|
|
|
- 'kp_codes' => $kpCodeList,
|
|
|
|
|
|
|
+ 'kp_codes' => $kpCodesForPool,
|
|
|
|
|
+ 'kp_code_list_original' => $kpCodeList,
|
|
|
'mistake_question_ids' => $basePoolCount < (int) $totalQuestions ? $priorityQuestionIds : [],
|
|
'mistake_question_ids' => $basePoolCount < (int) $totalQuestions ? $priorityQuestionIds : [],
|
|
|
'exclude_question_ids' => $answeredQuestionIds,
|
|
'exclude_question_ids' => $answeredQuestionIds,
|
|
|
'paper_name' => $params['paper_name'] ?? ('知识点组题_' . now()->format('Ymd_His')),
|
|
'paper_name' => $params['paper_name'] ?? ('知识点组题_' . now()->format('Ymd_His')),
|
|
@@ -1034,12 +1048,86 @@ class ExamTypeStrategy
|
|
|
|
|
|
|
|
Log::debug('ExamTypeStrategy: 知识点组卷参数构建完成', [
|
|
Log::debug('ExamTypeStrategy: 知识点组卷参数构建完成', [
|
|
|
'kp_count' => count($kpCodeList),
|
|
'kp_count' => count($kpCodeList),
|
|
|
|
|
+ 'kp_pool_count' => count($kpCodesForPool),
|
|
|
'exclude_count' => count($answeredQuestionIds),
|
|
'exclude_count' => count($answeredQuestionIds),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return $enhanced;
|
|
return $enhanced;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将用户选中的 kp 扩展为:各选中知识点在 knowledge_points 中「直接父节点」下的整棵子树(含父节点自身及所有后代)之并集。
|
|
|
|
|
+ * 多选且同属同一父章节(如 SIM01 下的 SIM01B/C/D)时,distinct parent 仅为 SIM01,等价于纳入 SIM01 整棵树(SIM01A–E 等)的全部 kp_code。
|
|
|
|
|
+ * 不在此再上溯祖父:祖父在库中往往挂在更高层(如整册根),会造成超范围并题。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array<string>
|
|
|
|
|
+ */
|
|
|
|
|
+ private function expandKpCodesWithParentChapterSubtrees(array $kpCodes): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $kpCodes = array_values(array_unique(array_filter($kpCodes)));
|
|
|
|
|
+ if ($kpCodes === []) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $expanded = [];
|
|
|
|
|
+ foreach ($kpCodes as $kp) {
|
|
|
|
|
+ $expanded[] = $kp;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $distinctParents = [];
|
|
|
|
|
+ foreach ($kpCodes as $kp) {
|
|
|
|
|
+ $p = KnowledgePoint::query()->where('kp_code', $kp)->value('parent_kp_code');
|
|
|
|
|
+ if ($p !== null && $p !== '') {
|
|
|
|
|
+ $distinctParents[(string) $p] = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (array_keys($distinctParents) as $parentKp) {
|
|
|
|
|
+ $expanded = array_merge($expanded, $this->getDescendantKpCodesIncludingRoot($parentKp));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return array_values(array_unique(array_filter($expanded)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * BFS 收集某 kp 节点在 knowledge_points 中的整棵后代子树(含根节点 kp_code)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array<string>
|
|
|
|
|
+ */
|
|
|
|
|
+ private function getDescendantKpCodesIncludingRoot(string $rootKpCode): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $rootKpCode = trim($rootKpCode);
|
|
|
|
|
+ if ($rootKpCode === '') {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $seen = [$rootKpCode => true];
|
|
|
|
|
+ $queue = [$rootKpCode];
|
|
|
|
|
+ $guard = 0;
|
|
|
|
|
+ $maxIter = 5000;
|
|
|
|
|
+
|
|
|
|
|
+ while ($queue !== [] && $guard++ < $maxIter) {
|
|
|
|
|
+ $cur = array_shift($queue);
|
|
|
|
|
+ $children = KnowledgePoint::query()
|
|
|
|
|
+ ->where('parent_kp_code', $cur)
|
|
|
|
|
+ ->pluck('kp_code')
|
|
|
|
|
+ ->all();
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($children as $child) {
|
|
|
|
|
+ $c = (string) $child;
|
|
|
|
|
+ if ($c === '') {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (! isset($seen[$c])) {
|
|
|
|
|
+ $seen[$c] = true;
|
|
|
|
|
+ $queue[] = $c;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return array_keys($seen);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 教材组卷 (assembleType=3)
|
|
* 教材组卷 (assembleType=3)
|
|
|
* 根据 chapter_id_list 查询课本章节,获取知识点,然后组卷
|
|
* 根据 chapter_id_list 查询课本章节,获取知识点,然后组卷
|