|
@@ -1442,7 +1442,37 @@ class LearningAnalyticsService
|
|
|
$textbookId, // 教材ID(避免超纲)
|
|
$textbookId, // 教材ID(避免超纲)
|
|
|
$difficultyCategory // 难度类别
|
|
$difficultyCategory // 难度类别
|
|
|
);
|
|
);
|
|
|
- $allQuestions = array_merge($priorityQuestions, $additionalQuestions);
|
|
|
|
|
|
|
+ $allQuestions = $this->dedupeQuestionsByBankId(array_merge($priorityQuestions, $additionalQuestions));
|
|
|
|
|
+
|
|
|
|
|
+ // assemble_type=2:本源 KP 合并后仍不足,用父树补题 KP 拉题(与 getQuestionsFromBank 主条件一致,走专用列表,不混入本源查询)
|
|
|
|
|
+ if (
|
|
|
|
|
+ $assembleType === 2
|
|
|
|
|
+ && count($allQuestions) < $totalQuestions
|
|
|
|
|
+ && ! empty($params['kp_supplement_subtree_codes'] ?? [])
|
|
|
|
|
+ && $grade !== null
|
|
|
|
|
+ ) {
|
|
|
|
|
+ $excludeForSupp = array_values(array_unique(array_filter(array_merge(
|
|
|
|
|
+ $excludeQuestionIds,
|
|
|
|
|
+ array_column($allQuestions, 'id')
|
|
|
|
|
+ ))));
|
|
|
|
|
+ $supp = $this->fetchQuestionsForKpAssembleSupplement(
|
|
|
|
|
+ $params['kp_supplement_subtree_codes'],
|
|
|
|
|
+ $excludeForSupp,
|
|
|
|
|
+ (int) $grade,
|
|
|
|
|
+ $skills,
|
|
|
|
|
+ $questionCategory,
|
|
|
|
|
+ $textbookCatalogNodeIds
|
|
|
|
|
+ );
|
|
|
|
|
+ $before = count($allQuestions);
|
|
|
|
|
+ $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('getQuestionsFromBank 完成', [
|
|
Log::info('getQuestionsFromBank 完成', [
|
|
|
'questions_count' => count($allQuestions),
|
|
'questions_count' => count($allQuestions),
|
|
@@ -1588,6 +1618,104 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param array<int, array<string, mixed>> $questions
|
|
|
|
|
+ * @return array<int, array<string, mixed>>
|
|
|
|
|
+ */
|
|
|
|
|
+ private function dedupeQuestionsByBankId(array $questions): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $seen = [];
|
|
|
|
|
+ $out = [];
|
|
|
|
|
+ foreach ($questions as $q) {
|
|
|
|
|
+ $id = $q['id'] ?? null;
|
|
|
|
|
+ if ($id === null || $id === '') {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $k = (string) $id;
|
|
|
|
|
+ if (isset($seen[$k])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $seen[$k] = true;
|
|
|
|
|
+ $out[] = $q;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $out;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 知识点组卷补题:仅按补充 KP 列表查询,筛选条件与 getQuestionsFromBank 主查询一致(不调用教材/已学那条智能补题)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param array<string> $kpCodes
|
|
|
|
|
+ * @param array<int|string> $excludeQuestionIds
|
|
|
|
|
+ * @return array<int, array<string, mixed>>
|
|
|
|
|
+ */
|
|
|
|
|
+ private function fetchQuestionsForKpAssembleSupplement(
|
|
|
|
|
+ array $kpCodes,
|
|
|
|
|
+ array $excludeQuestionIds,
|
|
|
|
|
+ int $grade,
|
|
|
|
|
+ array $skills = [],
|
|
|
|
|
+ ?int $questionCategory = null,
|
|
|
|
|
+ ?array $textbookCatalogNodeIds = null
|
|
|
|
|
+ ): array {
|
|
|
|
|
+ $kpCodes = array_values(array_unique(array_filter($kpCodes)));
|
|
|
|
|
+ if ($kpCodes === []) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $query = \App\Models\Question::query()
|
|
|
|
|
+ ->where('audit_status', 0)
|
|
|
|
|
+ ->whereIn('kp_code', $kpCodes);
|
|
|
|
|
+
|
|
|
|
|
+ $stageGrade = $this->normalizeQuestionStageGrade($grade);
|
|
|
|
|
+ if ($stageGrade !== null) {
|
|
|
|
|
+ $query->where('grade', $stageGrade);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! empty($skills)) {
|
|
|
|
|
+ $query->where(function ($q) use ($skills) {
|
|
|
|
|
+ foreach ($skills as $skill) {
|
|
|
|
|
+ $q->orWhere('tags', 'like', '%'.$skill.'%');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! empty($excludeQuestionIds)) {
|
|
|
|
|
+ $query->whereNotIn('id', $excludeQuestionIds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($questionCategory !== null) {
|
|
|
|
|
+ $query->where('question_category', $questionCategory);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $query->whereNotNull('solution')
|
|
|
|
|
+ ->where('solution', '!=', '')
|
|
|
|
|
+ ->where('solution', '!=', '[]');
|
|
|
|
|
+
|
|
|
|
|
+ if (! empty($textbookCatalogNodeIds)) {
|
|
|
|
|
+ $query->whereIn('textbook_catalog_nodes_id', $textbookCatalogNodeIds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $query->inRandomOrder()->get()->map(function ($q) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'id' => $q->id,
|
|
|
|
|
+ 'question_code' => $q->question_code,
|
|
|
|
|
+ 'kp_code' => $q->kp_code,
|
|
|
|
|
+ 'question_type' => $q->question_type,
|
|
|
|
|
+ 'difficulty' => $q->difficulty !== null ? (float) $q->difficulty : 0.5,
|
|
|
|
|
+ 'stem' => $q->stem,
|
|
|
|
|
+ 'solution' => $q->solution,
|
|
|
|
|
+ 'metadata' => [
|
|
|
|
|
+ 'has_solution' => true,
|
|
|
|
|
+ 'is_choice' => $q->question_type === 'choice',
|
|
|
|
|
+ 'is_fill' => $q->question_type === 'fill',
|
|
|
|
|
+ 'is_answer' => $q->question_type === 'answer',
|
|
|
|
|
+ 'difficulty_label' => $this->getDifficultyLabel($q->difficulty ?? 0.5),
|
|
|
|
|
+ 'question_type_label' => $this->getQuestionTypeLabel($q->question_type),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ];
|
|
|
|
|
+ })->toArray();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 从题库获取题目
|
|
* 从题库获取题目
|
|
|
*
|
|
*
|