|
|
@@ -1555,7 +1555,9 @@ class LearningAnalyticsService
|
|
|
|
|
|
Log::info('LearningAnalyticsService: 难度分布应用完成', [
|
|
|
'difficulty_category_after' => $difficultyCategory,
|
|
|
- 'after_count' => count($selectedQuestions)
|
|
|
+ 'before_count' => count($selectedQuestions),
|
|
|
+ 'after_count' => count($selectedQuestions),
|
|
|
+ 'success' => count($selectedQuestions) >= $totalQuestions
|
|
|
]);
|
|
|
} catch (\Exception $e) {
|
|
|
Log::warning('LearningAnalyticsService: 难度分布应用失败,继续使用原结果', [
|
|
|
@@ -1730,9 +1732,39 @@ class LearningAnalyticsService
|
|
|
// 让上层调用者根据需要选择题目数量
|
|
|
$selectedQuestions = $formattedQuestions;
|
|
|
|
|
|
+ // 【修复】重新启用智能补充功能
|
|
|
+ if (count($selectedQuestions) < $totalNeeded) {
|
|
|
+ $deficit = $totalNeeded - count($selectedQuestions);
|
|
|
+ Log::warning('getQuestionsFromBank: 指定知识点题目不足,尝试智能补充', [
|
|
|
+ 'deficit' => $deficit,
|
|
|
+ 'available_count' => count($selectedQuestions),
|
|
|
+ 'strategy' => '从同年级其他知识点或相邻难度补充'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 补充策略:从同年级其他知识点补充
|
|
|
+ $supplementaryQuestions = $this->getSupplementaryQuestionsForGrade(
|
|
|
+ $params['grade'] ?? 9,
|
|
|
+ array_column($selectedQuestions, 'kp_code'),
|
|
|
+ $deficit,
|
|
|
+ $params['difficulty_category'] ?? 1
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!empty($supplementaryQuestions)) {
|
|
|
+ $selectedQuestions = array_merge($selectedQuestions, $supplementaryQuestions);
|
|
|
+ Log::info('getQuestionsFromBank: 智能补充完成', [
|
|
|
+ 'supplementary_count' => count($supplementaryQuestions),
|
|
|
+ 'total_after_supplement' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ } else {
|
|
|
+ Log::warning('getQuestionsFromBank: 智能补充失败,未找到合适的题目');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
Log::info('getQuestionsFromBank 完成', [
|
|
|
'final_count' => count($selectedQuestions),
|
|
|
'raw_database_count' => $questions->count(),
|
|
|
+ 'total_needed' => $totalNeeded,
|
|
|
+ 'success' => count($selectedQuestions) >= $totalNeeded,
|
|
|
'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
]);
|
|
|
|
|
|
@@ -1948,7 +1980,10 @@ class LearningAnalyticsService
|
|
|
Log::info('selectQuestionsByMastery 开始', [
|
|
|
'question_count' => count($questions),
|
|
|
'student_id' => $studentId,
|
|
|
- 'total_questions' => $totalQuestions
|
|
|
+ 'total_questions' => $totalQuestions,
|
|
|
+ 'unique_kp_count' => count(array_unique(array_column($questions, 'kp_code'))),
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
+ 'note' => '追踪输入题目数量和知识点多样性'
|
|
|
]);
|
|
|
|
|
|
// 【修复】题目数量处理逻辑:无论题目数量多少,都要进行权重分配和筛选
|
|
|
@@ -2133,18 +2168,47 @@ class LearningAnalyticsService
|
|
|
return $weightB <=> $weightA;
|
|
|
});
|
|
|
|
|
|
+ // 【修复】摸底测试题型基础分配:记录每个题型的知识点分布
|
|
|
+ $typeKpDistribution = [];
|
|
|
+ foreach ($questionsByType[$type] as $q) {
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
+ if (!isset($typeKpDistribution[$kpCode])) {
|
|
|
+ $typeKpDistribution[$kpCode] = 0;
|
|
|
+ }
|
|
|
+ $typeKpDistribution[$kpCode]++;
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('摸底测试题型基础分配开始', [
|
|
|
+ 'type' => $type,
|
|
|
+ 'total_questions_in_type' => count($questionsByType[$type]),
|
|
|
+ 'kp_distribution_in_type' => $typeKpDistribution,
|
|
|
+ 'available_kp_count' => count($typeKpDistribution)
|
|
|
+ ]);
|
|
|
+
|
|
|
// 根据策略选择题目
|
|
|
if ($useKnowledgePointPriority) {
|
|
|
// 摸底测试:选择第一个未选过知识点的题目
|
|
|
+ $selectedInThisType = 0;
|
|
|
foreach ($questionsByType[$type] as $q) {
|
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
|
if (!isset($kpSelected[$kpCode])) {
|
|
|
$selectedQuestions[] = $q;
|
|
|
$kpSelected[$kpCode] = true;
|
|
|
- Log::debug('题型基础分配(知识点优先)', ['type' => $type, 'kp' => $kpCode]);
|
|
|
- break;
|
|
|
+ $selectedInThisType++;
|
|
|
+ Log::debug('题型基础分配(知识点优先)', [
|
|
|
+ 'type' => $type,
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'question_id' => $q['id'] ?? 'unknown',
|
|
|
+ 'selected_in_type' => $selectedInThisType
|
|
|
+ ]);
|
|
|
+ break; // 只选1题
|
|
|
}
|
|
|
}
|
|
|
+ Log::info('摸底测试题型基础分配完成', [
|
|
|
+ 'type' => $type,
|
|
|
+ 'selected_count' => $selectedInThisType,
|
|
|
+ 'note' => $selectedInThisType > 0 ? '成功选择' : '无可用知识点'
|
|
|
+ ]);
|
|
|
} else {
|
|
|
// 【修复】知识点组卷:随机选择该题型的一道题,避免固定选择第一个导致知识点分布不均
|
|
|
$randomIndex = array_rand($questionsByType[$type]);
|
|
|
@@ -2217,9 +2281,19 @@ class LearningAnalyticsService
|
|
|
'note' => '观察排序是否均衡分布A07和A08'
|
|
|
]);
|
|
|
|
|
|
- // 根据策略继续选择题目
|
|
|
+ // 【修复】摸底测试继续选择题目:记录详细的选题过程
|
|
|
+ Log::info('摸底测试继续选择题目开始', [
|
|
|
+ 'selected_count_after_basic' => count($selectedQuestions),
|
|
|
+ 'total_questions' => $totalQuestions,
|
|
|
+ 'selected_kp_codes' => array_keys($kpSelected),
|
|
|
+ 'available_kp_count' => count($preSortKpDistribution),
|
|
|
+ 'strategy' => $useKnowledgePointPriority ? '知识点优先' : '无知识点限制'
|
|
|
+ ]);
|
|
|
+
|
|
|
if ($useKnowledgePointPriority) {
|
|
|
- // 摸底测试:选择未选过知识点的题目(优先)
|
|
|
+ // 摸底测试:优先选择未选过知识点的题目
|
|
|
+ $initialSelectedCount = count($selectedQuestions);
|
|
|
+ $prioritySelectedCount = 0;
|
|
|
foreach ($allQuestions as $q) {
|
|
|
if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
|
@@ -2227,8 +2301,58 @@ class LearningAnalyticsService
|
|
|
if (!isset($kpSelected[$kpCode])) {
|
|
|
$selectedQuestions[] = $q;
|
|
|
$kpSelected[$kpCode] = true;
|
|
|
- Log::debug('继续选择题目(知识点优先)', ['kp' => $kpCode, 'id' => $q['id'] ?? 'unknown']);
|
|
|
+ $prioritySelectedCount++;
|
|
|
+ Log::debug('继续选择题目(知识点优先)', [
|
|
|
+ 'kp' => $kpCode,
|
|
|
+ 'id' => $q['id'] ?? 'unknown',
|
|
|
+ 'priority_selected_count' => $prioritySelectedCount,
|
|
|
+ 'total_selected' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('摸底测试优先阶段完成', [
|
|
|
+ 'priority_selected_count' => $prioritySelectedCount,
|
|
|
+ 'total_selected' => count($selectedQuestions),
|
|
|
+ 'unique_kp_count' => count($kpSelected),
|
|
|
+ 'note' => '优先选择未选过知识点的题目'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 【修复】降级策略:如果仍未达到目标数量,允许重复选择知识点
|
|
|
+ if (count($selectedQuestions) < $totalQuestions) {
|
|
|
+ Log::warning('selectQuestionsByMastery: 知识点优先策略无法满足数量要求,启用降级策略', [
|
|
|
+ 'requested_count' => $totalQuestions,
|
|
|
+ 'selected_count' => count($selectedQuestions),
|
|
|
+ 'priority_selected_count' => $prioritySelectedCount,
|
|
|
+ 'unique_kp_count' => count($kpSelected),
|
|
|
+ 'available_kp_count' => count($preSortKpDistribution),
|
|
|
+ 'note' => '将允许重复选择知识点以达到目标数量'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $fallbackSelectedCount = 0;
|
|
|
+ $selectedIds = array_column($selectedQuestions, 'id');
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
+
|
|
|
+ $qid = $q['id'] ?? null;
|
|
|
+ if ($qid && !in_array($qid, $selectedIds)) {
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
+ $selectedIds[] = $qid;
|
|
|
+ $fallbackSelectedCount++;
|
|
|
+ Log::debug('降级选择题目(允许知识点重复)', [
|
|
|
+ 'kp' => $q['kp_code'] ?? 'unknown',
|
|
|
+ 'id' => $qid,
|
|
|
+ 'fallback_selected_count' => $fallbackSelectedCount,
|
|
|
+ 'current_count' => count($selectedQuestions)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ Log::info('摸底测试降级阶段完成', [
|
|
|
+ 'fallback_selected_count' => $fallbackSelectedCount,
|
|
|
+ 'total_selected' => count($selectedQuestions),
|
|
|
+ 'note' => '允许重复选择知识点补充数量'
|
|
|
+ ]);
|
|
|
}
|
|
|
} else {
|
|
|
// 知识点组卷:选择未选过的题目(不要求知识点不重复)
|
|
|
@@ -2280,6 +2404,17 @@ class LearningAnalyticsService
|
|
|
'is_array' => is_array($selectedQuestions)
|
|
|
]);
|
|
|
|
|
|
+ // 【新增】最终知识点分布统计
|
|
|
+ $finalKpDistribution = array_count_values(array_column($selectedQuestions, 'kp_code'));
|
|
|
+ Log::info('摸底测试最终知识点分布', [
|
|
|
+ 'final_total_count' => count($selectedQuestions),
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
+ 'final_kp_distribution' => $finalKpDistribution,
|
|
|
+ 'unique_kp_count' => count($finalKpDistribution),
|
|
|
+ 'success' => count($selectedQuestions) === $totalQuestions,
|
|
|
+ 'kp_coverage_rate' => count($finalKpDistribution) / max(count($preSortKpDistribution), 1) * 100
|
|
|
+ ]);
|
|
|
+
|
|
|
// ========== 最终排查:确保无重复题目且题型分布合理 ==========
|
|
|
$finalQuestions = [];
|
|
|
$seenQuestionIds = [];
|
|
|
@@ -2739,4 +2874,183 @@ class LearningAnalyticsService
|
|
|
'message' => 'analysis_api_disabled',
|
|
|
];
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【新增】为指定年级智能补充题目
|
|
|
+ * 当指定知识点题目不足时,从同年级其他知识点补充
|
|
|
+ */
|
|
|
+ private function getSupplementaryQuestionsForGrade(
|
|
|
+ int $grade,
|
|
|
+ array $existingKpCodes,
|
|
|
+ int $needCount,
|
|
|
+ int $difficultyCategory
|
|
|
+ ): array {
|
|
|
+ try {
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 开始智能补充', [
|
|
|
+ 'grade' => $grade,
|
|
|
+ 'existing_kp_count' => count($existingKpCodes),
|
|
|
+ 'need_count' => $needCount,
|
|
|
+ 'difficulty_category' => $difficultyCategory
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 查询同年级其他知识点的题目
|
|
|
+ $query = \App\Models\Question::query();
|
|
|
+
|
|
|
+ // 排除已选知识点
|
|
|
+ if (!empty($existingKpCodes)) {
|
|
|
+ $query->whereNotIn('kp_code', $existingKpCodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 限制同年级(通过教材关联)
|
|
|
+ $gradeKpCodes = $this->getGradeKnowledgePoints($grade);
|
|
|
+ if (!empty($gradeKpCodes)) {
|
|
|
+ $query->whereIn('kp_code', $gradeKpCodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 筛选有解题思路的题目
|
|
|
+ $query->whereNotNull('solution')
|
|
|
+ ->where('solution', '!=', '')
|
|
|
+ ->where('solution', '!=', '[]');
|
|
|
+
|
|
|
+ // 【重要】移除题目分类限制,允许补充其他分类的题目
|
|
|
+ // $query->where('question_category', 1); // 移除这行
|
|
|
+
|
|
|
+ // 根据难度类别调整查询
|
|
|
+ $difficultyRanges = $this->getDifficultyRangesForCategory($difficultyCategory);
|
|
|
+ $query->where(function ($q) use ($difficultyRanges) {
|
|
|
+ foreach ($difficultyRanges as $range) {
|
|
|
+ $q->orWhereBetween('difficulty', [$range['min'], $range['max']]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 随机排序,增加多样性
|
|
|
+ $query->inRandomOrder()
|
|
|
+ ->limit($needCount * 2); // 多取一些,后续筛选
|
|
|
+
|
|
|
+ $supplementaryQuestions = $query->get();
|
|
|
+
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 查询完成', [
|
|
|
+ 'found_count' => $supplementaryQuestions->count(),
|
|
|
+ 'need_count' => $needCount
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 格式化题目
|
|
|
+ $formatted = [];
|
|
|
+ foreach ($supplementaryQuestions as $question) {
|
|
|
+ // 手动判断题目类型,避免类型问题
|
|
|
+ $stem = $question->stem ?? '';
|
|
|
+ $options = is_array($question->options) ? $question->options : (json_decode($question->options ?? '[]', true) ?: []);
|
|
|
+ $correctAnswer = $question->correct_answer ?? '';
|
|
|
+
|
|
|
+ // 简单判断逻辑
|
|
|
+ if (!empty($options) && is_array($options) && count($options) >= 2) {
|
|
|
+ $questionType = 'choice';
|
|
|
+ } elseif (!empty($correctAnswer) && (strlen($correctAnswer) <= 10 || strpos($correctAnswer, '|') !== false)) {
|
|
|
+ $questionType = 'fill';
|
|
|
+ } else {
|
|
|
+ $questionType = 'answer';
|
|
|
+ }
|
|
|
+
|
|
|
+ $formatted[] = [
|
|
|
+ 'id' => $question->id,
|
|
|
+ 'question_bank_id' => $question->id,
|
|
|
+ 'question_type' => $questionType,
|
|
|
+ 'kp_code' => $question->kp_code,
|
|
|
+ 'difficulty' => (float) $question->difficulty,
|
|
|
+ 'stem' => $stem,
|
|
|
+ 'options' => $options,
|
|
|
+ 'correct_answer' => $correctAnswer,
|
|
|
+ 'solution' => $question->solution,
|
|
|
+ 'score' => $question->score ?? 4,
|
|
|
+ 'estimated_time' => $question->estimated_time ?? 300,
|
|
|
+ 'metadata' => [
|
|
|
+ 'question_type_label' => $questionType === 'choice' ? '选择题' : ($questionType === 'fill' ? '填空题' : '解答题'),
|
|
|
+ 'difficulty_label' => $this->getDifficultyLabelFromObject($question->difficulty),
|
|
|
+ 'is_supplementary' => true
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 随机选择需要的数量
|
|
|
+ shuffle($formatted);
|
|
|
+ $selected = array_slice($formatted, 0, $needCount);
|
|
|
+
|
|
|
+ Log::info('getSupplementaryQuestionsForGrade: 补充完成', [
|
|
|
+ 'supplementary_count' => count($selected),
|
|
|
+ 'kp_distribution' => array_count_values(array_column($selected, 'kp_code'))
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $selected;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('getSupplementaryQuestionsForGrade: 补充失败', [
|
|
|
+ 'grade' => $grade,
|
|
|
+ 'need_count' => $needCount,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【新增】获取指定年级的所有知识点
|
|
|
+ */
|
|
|
+ private function getGradeKnowledgePoints(int $grade): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $kpCodes = DB::table('textbook_chapter_knowledge_relation as tckr')
|
|
|
+ ->join('textbook_catalog_nodes as tcn', 'tckr.catalog_chapter_id', '=', 'tcn.id')
|
|
|
+ ->join('textbooks as t', 'tcn.textbook_id', '=', 't.id')
|
|
|
+ ->where('t.grade', $grade)
|
|
|
+ ->distinct()
|
|
|
+ ->pluck('tckr.kp_code')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ return array_filter($kpCodes);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('getGradeKnowledgePoints: 查询失败', [
|
|
|
+ 'grade' => $grade,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【新增】根据难度类别获取难度范围
|
|
|
+ */
|
|
|
+ private function getDifficultyRangesForCategory(int $difficultyCategory): array
|
|
|
+ {
|
|
|
+ return match($difficultyCategory) {
|
|
|
+ 1 => [
|
|
|
+ ['min' => 0.0, 'max' => 0.5], // 基础型:偏向低难度
|
|
|
+ ['min' => 0.5, 'max' => 1.0],
|
|
|
+ ],
|
|
|
+ 2 => [
|
|
|
+ ['min' => 0.0, 'max' => 0.5], // 进阶型:均衡分布
|
|
|
+ ['min' => 0.5, 'max' => 1.0],
|
|
|
+ ],
|
|
|
+ 3 => [
|
|
|
+ ['min' => 0.25, 'max' => 0.75], // 中等型:偏向中等难度
|
|
|
+ ['min' => 0.0, 'max' => 1.0],
|
|
|
+ ],
|
|
|
+ 4 => [
|
|
|
+ ['min' => 0.5, 'max' => 1.0], // 拔高型:偏向高难度
|
|
|
+ ['min' => 0.0, 'max' => 0.5],
|
|
|
+ ],
|
|
|
+ default => [
|
|
|
+ ['min' => 0.0, 'max' => 1.0],
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【新增】获取难度标签(重载版本)
|
|
|
+ */
|
|
|
+ private function getDifficultyLabelFromObject(float $difficulty): string
|
|
|
+ {
|
|
|
+ if ($difficulty < 0.3) return '基础';
|
|
|
+ if ($difficulty < 0.7) return '中等';
|
|
|
+ return '拔高';
|
|
|
+ }
|
|
|
}
|