|
@@ -1322,34 +1322,113 @@ class LearningAnalyticsService
|
|
|
// 2. 优先使用学生错题(如果存在)
|
|
// 2. 优先使用学生错题(如果存在)
|
|
|
$mistakeQuestionIds = $params['mistake_question_ids'] ?? [];
|
|
$mistakeQuestionIds = $params['mistake_question_ids'] ?? [];
|
|
|
$priorityQuestions = [];
|
|
$priorityQuestions = [];
|
|
|
|
|
+ $maxQuestions = 50; // 全局最大题目数限制
|
|
|
|
|
|
|
|
if (!empty($mistakeQuestionIds)) {
|
|
if (!empty($mistakeQuestionIds)) {
|
|
|
Log::info('LearningAnalyticsService: 优先获取学生错题', [
|
|
Log::info('LearningAnalyticsService: 优先获取学生错题', [
|
|
|
'mistake_question_ids' => $mistakeQuestionIds,
|
|
'mistake_question_ids' => $mistakeQuestionIds,
|
|
|
- 'count' => count($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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 获取学生错题的详细信息
|
|
// 获取学生错题的详细信息
|
|
|
- $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, 200, $mistakeQuestionIds, [], null);
|
|
|
|
|
|
|
+ $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $truncatedMistakeIds, [], null);
|
|
|
|
|
|
|
|
Log::info('LearningAnalyticsService: 错题获取完成', [
|
|
Log::info('LearningAnalyticsService: 错题获取完成', [
|
|
|
'priority_questions_count' => count($priorityQuestions),
|
|
'priority_questions_count' => count($priorityQuestions),
|
|
|
- 'expected_count' => count($mistakeQuestionIds)
|
|
|
|
|
|
|
+ 'expected_count' => count($truncatedMistakeIds)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 如果获取的错题数量少于预期,记录警告
|
|
// 如果获取的错题数量少于预期,记录警告
|
|
|
- if (count($priorityQuestions) < count($mistakeQuestionIds)) {
|
|
|
|
|
|
|
+ if (count($priorityQuestions) < count($truncatedMistakeIds)) {
|
|
|
Log::warning('LearningAnalyticsService: 错题获取不完整', [
|
|
Log::warning('LearningAnalyticsService: 错题获取不完整', [
|
|
|
- 'expected' => count($mistakeQuestionIds),
|
|
|
|
|
|
|
+ 'expected' => count($truncatedMistakeIds),
|
|
|
'actual' => count($priorityQuestions),
|
|
'actual' => count($priorityQuestions),
|
|
|
- 'missing_ids' => array_diff($mistakeQuestionIds, array_column($priorityQuestions, 'id'))
|
|
|
|
|
|
|
+ 'missing_ids' => array_diff($truncatedMistakeIds, array_column($priorityQuestions, 'id'))
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 如果错题数量不足,补充其他题目(错题本类型不补充)
|
|
|
|
|
|
|
+ // 3. 处理错题本逻辑
|
|
|
$allQuestions = $priorityQuestions;
|
|
$allQuestions = $priorityQuestions;
|
|
|
- $isMistakeBook = ($assembleType === 5); // 错题本类型不补充题目
|
|
|
|
|
|
|
+ $isMistakeBook = ($assembleType === 5); // 错题本类型
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是错题本类型,且有错题但数量不足,获取原卷子的所有题目
|
|
|
|
|
+ if ($isMistakeBook && !empty($priorityQuestions) && count($priorityQuestions) < 5 && !empty($params['paper_ids'])) {
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 错题本错题不足,获取原卷子题目补充', [
|
|
|
|
|
+ 'mistake_count' => count($priorityQuestions),
|
|
|
|
|
+ 'paper_ids' => $params['paper_ids']
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取原卷子的所有题目
|
|
|
|
|
+ $paperQuestionIds = $this->getPaperAllQuestions($params['paper_ids']);
|
|
|
|
|
+ if (!empty($paperQuestionIds)) {
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 获取原卷子题目', [
|
|
|
|
|
+ 'paper_question_count' => count($paperQuestionIds)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 使用原卷子题目补充到目标数量
|
|
|
|
|
+ $additionalNeeded = max(5, count($priorityQuestions)) - count($priorityQuestions);
|
|
|
|
|
+ $paperQuestionIds = array_diff($paperQuestionIds, array_column($priorityQuestions, 'id'));
|
|
|
|
|
+ $additionalPaperQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $additionalNeeded, $paperQuestionIds, [], null);
|
|
|
|
|
+
|
|
|
|
|
+ $allQuestions = array_merge($priorityQuestions, $additionalPaperQuestions);
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 错题本题目补充完成', [
|
|
|
|
|
+ 'final_count' => count($allQuestions)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是错题本类型,但完全没有错题,使用原卷子的所有题目
|
|
|
|
|
+ if ($isMistakeBook && empty($priorityQuestions) && !empty($params['paper_ids'])) {
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 错题本无错题,使用原卷子题目', [
|
|
|
|
|
+ 'paper_ids' => $params['paper_ids']
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取原卷子的所有题目
|
|
|
|
|
+ $paperQuestionIds = $this->getPaperAllQuestions($params['paper_ids']);
|
|
|
|
|
+ if (!empty($paperQuestionIds)) {
|
|
|
|
|
+ $allQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $paperQuestionIds, [], null);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否获取到题目
|
|
|
|
|
+ if (empty($allQuestions)) {
|
|
|
|
|
+ Log::warning('LearningAnalyticsService: 错题本无法获取任何题目', [
|
|
|
|
|
+ 'paper_ids' => $params['paper_ids'],
|
|
|
|
|
+ 'paper_question_ids' => $paperQuestionIds,
|
|
|
|
|
+ 'reason' => '原卷子题目在题库中不存在或已删除'
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ throw new \Exception('抱歉,您选择的卷子中的题目在题库中不存在或已被删除,无法生成错题本。请选择其他卷子或联系管理员更新题库。');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 错题本使用原卷子题目', [
|
|
|
|
|
+ 'question_count' => count($allQuestions)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new \Exception('抱歉,指定的卷子中没有找到题目,无法生成错题本。');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 错题本类型但既无错题又无卷子题目
|
|
|
|
|
+ if ($isMistakeBook && empty($priorityQuestions) && empty($allQuestions)) {
|
|
|
|
|
+ Log::warning('LearningAnalyticsService: 错题本既无错题又无卷子题目', [
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'paper_ids' => $params['paper_ids'] ?? []
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ throw new \Exception('抱歉,您在这个卷子中没有错题记录,且卷子题目无法获取,无法生成错题本。请确认卷子ID是否正确或联系管理员。');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if (!$isMistakeBook && count($priorityQuestions) < $totalQuestions) {
|
|
if (!$isMistakeBook && count($priorityQuestions) < $totalQuestions) {
|
|
|
try {
|
|
try {
|
|
@@ -1361,7 +1440,7 @@ class LearningAnalyticsService
|
|
|
'assemble_type' => $assembleType
|
|
'assemble_type' => $assembleType
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, 200, [], [], $questionCategory);
|
|
|
|
|
|
|
+ $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $maxQuestions, [], [], $questionCategory);
|
|
|
$allQuestions = array_merge($priorityQuestions, $additionalQuestions);
|
|
$allQuestions = array_merge($priorityQuestions, $additionalQuestions);
|
|
|
|
|
|
|
|
Log::info('getQuestionsFromBank 调用完成', [
|
|
Log::info('getQuestionsFromBank 调用完成', [
|
|
@@ -1418,8 +1497,8 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 3. 根据掌握度对题目进行筛选和排序
|
|
// 3. 根据掌握度对题目进行筛选和排序
|
|
|
- // 错题本类型:使用所有错题,不限制数量
|
|
|
|
|
- $targetQuestionCount = $isMistakeBook ? count($allQuestions) : $totalQuestions;
|
|
|
|
|
|
|
+ // 错题本类型:使用所有错题,但不超过最大值限制
|
|
|
|
|
+ $targetQuestionCount = $isMistakeBook ? min(count($allQuestions), $maxQuestions) : $totalQuestions;
|
|
|
|
|
|
|
|
Log::info('开始调用 selectQuestionsByMastery', [
|
|
Log::info('开始调用 selectQuestionsByMastery', [
|
|
|
'input_count' => count($allQuestions),
|
|
'input_count' => count($allQuestions),
|
|
@@ -1543,7 +1622,9 @@ class LearningAnalyticsService
|
|
|
|
|
|
|
|
return $priorityQuestions;
|
|
return $priorityQuestions;
|
|
|
} else {
|
|
} else {
|
|
|
- Log::warning('getQuestionsFromBank: 优先错题获取失败');
|
|
|
|
|
|
|
+ Log::warning('getQuestionsFromBank: 优先错题获取失败,返回空数组让上层处理');
|
|
|
|
|
+ // 错题本类型获取不到错题时,返回空数组,不回退到题库随机选题
|
|
|
|
|
+ return [];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1648,34 +1729,48 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 从本地数据库获取指定ID的题目
|
|
|
|
|
|
|
+ * 获取指定ID的题目详情
|
|
|
*/
|
|
*/
|
|
|
private function getLocalQuestionsByIds(array $questionIds): array
|
|
private function getLocalQuestionsByIds(array $questionIds): array
|
|
|
{
|
|
{
|
|
|
try {
|
|
try {
|
|
|
- $questions = \App\Models\Question::whereIn('id', $questionIds)
|
|
|
|
|
- ->select(['id', 'question_code', 'kp_code', 'question_type', 'difficulty', 'stem'])
|
|
|
|
|
- ->get();
|
|
|
|
|
|
|
+ // 通过QuestionBankService获取题目详情
|
|
|
|
|
+ $questionBankService = $this->questionBankService ?? app(\App\Services\QuestionBankService::class);
|
|
|
|
|
+
|
|
|
|
|
+ // 批量获取题目详情
|
|
|
|
|
+ $response = $questionBankService->getQuestionsByIds($questionIds);
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($response['data'])) {
|
|
|
|
|
+ Log::warning('getLocalQuestionsByIds: 未获取到任何题目', [
|
|
|
|
|
+ 'requested_ids' => $questionIds
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 转换为数组格式
|
|
|
|
|
- $result = $questions->map(function ($q) {
|
|
|
|
|
|
|
+ $questions = $response['data'];
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为标准格式
|
|
|
|
|
+ $result = array_map(function ($q) {
|
|
|
|
|
+ $difficulty = $q['difficulty'] ?? 0.5;
|
|
|
return [
|
|
return [
|
|
|
- 'id' => $q->id,
|
|
|
|
|
- 'question_code' => $q->question_code,
|
|
|
|
|
- 'kp_code' => $q->kp_code,
|
|
|
|
|
- 'question_type' => $q->question_type,
|
|
|
|
|
- 'difficulty' => $q->difficulty,
|
|
|
|
|
- 'stem' => $q->stem,
|
|
|
|
|
|
|
+ 'id' => $q['id'],
|
|
|
|
|
+ 'question_code' => $q['question_code'] ?? '',
|
|
|
|
|
+ 'kp_code' => $q['kp_code'] ?? '',
|
|
|
|
|
+ 'question_type' => $q['question_type'] ?? 'choice',
|
|
|
|
|
+ 'difficulty' => (float) $difficulty,
|
|
|
|
|
+ 'stem' => $q['stem'] ?? '',
|
|
|
|
|
+ 'solution' => $q['solution'] ?? '',
|
|
|
|
|
+ 'answer' => $q['answer'] ?? '',
|
|
|
'metadata' => [
|
|
'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),
|
|
|
|
|
- 'question_type_label' => $this->getQuestionTypeLabel($q->question_type)
|
|
|
|
|
|
|
+ 'has_solution' => !empty($q['solution']),
|
|
|
|
|
+ 'is_choice' => ($q['question_type'] ?? 'choice') === 'choice',
|
|
|
|
|
+ 'is_fill' => ($q['question_type'] ?? 'choice') === 'fill',
|
|
|
|
|
+ 'is_answer' => ($q['question_type'] ?? 'choice') === 'answer',
|
|
|
|
|
+ 'difficulty_label' => $this->getDifficultyLabel((float) $difficulty),
|
|
|
|
|
+ 'question_type_label' => $this->getQuestionTypeLabel($q['question_type'] ?? 'choice')
|
|
|
]
|
|
]
|
|
|
];
|
|
];
|
|
|
- })->toArray();
|
|
|
|
|
|
|
+ }, $questions);
|
|
|
|
|
|
|
|
Log::info('getLocalQuestionsByIds 获取成功', [
|
|
Log::info('getLocalQuestionsByIds 获取成功', [
|
|
|
'requested_count' => count($questionIds),
|
|
'requested_count' => count($questionIds),
|
|
@@ -1690,13 +1785,44 @@ class LearningAnalyticsService
|
|
|
} catch (\Exception $e) {
|
|
} catch (\Exception $e) {
|
|
|
Log::error('getLocalQuestionsByIds 获取失败', [
|
|
Log::error('getLocalQuestionsByIds 获取失败', [
|
|
|
'question_ids' => $questionIds,
|
|
'question_ids' => $questionIds,
|
|
|
- 'error' => $e->getMessage()
|
|
|
|
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return [];
|
|
return [];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取卷子的所有题目ID(不区分对错)
|
|
|
|
|
+ */
|
|
|
|
|
+ private function getPaperAllQuestions(array $paperIds): array
|
|
|
|
|
+ {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 查询 paper_questions 表获取所有题目ID
|
|
|
|
|
+ $questionIds = DB::table('paper_questions')
|
|
|
|
|
+ ->whereIn('paper_id', $paperIds)
|
|
|
|
|
+ ->pluck('question_bank_id')
|
|
|
|
|
+ ->unique()
|
|
|
|
|
+ ->filter()
|
|
|
|
|
+ ->values()
|
|
|
|
|
+ ->toArray();
|
|
|
|
|
+
|
|
|
|
|
+ Log::debug('LearningAnalyticsService: 获取卷子所有题目', [
|
|
|
|
|
+ 'paper_ids' => $paperIds,
|
|
|
|
|
+ 'question_count' => count($questionIds)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $questionIds;
|
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
|
+ Log::error('LearningAnalyticsService: 获取卷子题目失败', [
|
|
|
|
|
+ 'paper_ids' => $paperIds,
|
|
|
|
|
+ 'error' => $e->getMessage()
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取难度标签
|
|
* 获取难度标签
|
|
|
*/
|
|
*/
|
|
@@ -1853,10 +1979,10 @@ class LearningAnalyticsService
|
|
|
'group_time_ms' => round($groupTime, 2)
|
|
'group_time_ms' => round($groupTime, 2)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 2. 为每个知识点计算权重
|
|
|
|
|
|
|
+ // 2. 为每个知识点计算权重(用于题型内的题目排序)
|
|
|
$kpWeights = [];
|
|
$kpWeights = [];
|
|
|
$kpCodes = array_keys($questionsByKp);
|
|
$kpCodes = array_keys($questionsByKp);
|
|
|
- Log::info('开始计算知识点权重', [
|
|
|
|
|
|
|
+ Log::info('开始计算知识点权重(用于题型内排序)', [
|
|
|
'kp_count' => count($kpCodes),
|
|
'kp_count' => count($kpCodes),
|
|
|
'student_id' => $studentId
|
|
'student_id' => $studentId
|
|
|
]);
|
|
]);
|
|
@@ -1919,68 +2045,202 @@ class LearningAnalyticsService
|
|
|
'avg_time_per_kp_ms' => count($kpCodes) > 0 ? round($totalWeightTime / count($kpCodes), 2) : 0
|
|
'avg_time_per_kp_ms' => count($kpCodes) > 0 ? round($totalWeightTime / count($kpCodes), 2) : 0
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 3. 按权重分配题目数量(修复:按权重排序取前N道题)
|
|
|
|
|
- $totalWeight = array_sum($kpWeights);
|
|
|
|
|
|
|
+ // 3. 按题型分配题目数量(权重用于题型内排序)
|
|
|
$selectedQuestions = [];
|
|
$selectedQuestions = [];
|
|
|
|
|
|
|
|
- // 将所有题目合并并添加权重信息
|
|
|
|
|
|
|
+ // 将所有题目合并
|
|
|
$weightedQuestions = [];
|
|
$weightedQuestions = [];
|
|
|
foreach ($questionsByKp as $kpCode => $kpQuestions) {
|
|
foreach ($questionsByKp as $kpCode => $kpQuestions) {
|
|
|
- $weight = $kpWeights[$kpCode];
|
|
|
|
|
foreach ($kpQuestions as $q) {
|
|
foreach ($kpQuestions as $q) {
|
|
|
- $q['kp_weight'] = $weight;
|
|
|
|
|
$q['kp_code'] = $kpCode;
|
|
$q['kp_code'] = $kpCode;
|
|
|
$weightedQuestions[] = $q;
|
|
$weightedQuestions[] = $q;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 打乱题目顺序(避免固定模式)
|
|
|
|
|
- shuffle($weightedQuestions);
|
|
|
|
|
|
|
+ // ========== 知识点优先选择机制 ==========
|
|
|
|
|
+ // 摸底测试的核心目标是最大化知识点覆盖
|
|
|
|
|
+ // 题型只是约束条件(每种至少1题)
|
|
|
|
|
+
|
|
|
|
|
+ // 首先按题型分组
|
|
|
|
|
+ $questionsByType = [
|
|
|
|
|
+ 'choice' => [],
|
|
|
|
|
+ 'fill' => [],
|
|
|
|
|
+ 'answer' => [],
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($weightedQuestions as $q) {
|
|
|
|
|
+ $qid = $q['id'] ?? $q['question_id'] ?? null;
|
|
|
|
|
+ if ($qid && isset($questionTypeCache[$qid])) {
|
|
|
|
|
+ $type = $questionTypeCache[$qid];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $type = $this->determineQuestionType($q);
|
|
|
|
|
+ if ($qid) {
|
|
|
|
|
+ $questionTypeCache[$qid] = $type;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!isset($questionsByType[$type])) {
|
|
|
|
|
+ $type = 'answer'; // 默认归类为answer
|
|
|
|
|
+ }
|
|
|
|
|
+ $questionsByType[$type][] = $q;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 步骤1:按知识点分组,优先选不同知识点 ==========
|
|
|
|
|
+ $selectedQuestions = [];
|
|
|
|
|
+ $kpSelected = []; // 已选知识点记录
|
|
|
|
|
+
|
|
|
|
|
+ // 先确保每种题型至少选1题(来自不同知识点)
|
|
|
|
|
+ foreach (['choice', 'fill', 'answer'] as $type) {
|
|
|
|
|
+ if (empty($questionsByType[$type])) {
|
|
|
|
|
+ Log::warning('题型分配:题型无题目', ['type' => $type]);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按权重排序该题型的题目
|
|
|
|
|
+ usort($questionsByType[$type], function ($a, $b) use ($kpWeights) {
|
|
|
|
|
+ $kpA = $a['kp_code'] ?? '';
|
|
|
|
|
+ $kpB = $b['kp_code'] ?? '';
|
|
|
|
|
+ $weightA = $kpWeights[$kpA] ?? 1.0;
|
|
|
|
|
+ $weightB = $kpWeights[$kpB] ?? 1.0;
|
|
|
|
|
+ return $weightB <=> $weightA;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 选择第一个未选过知识点的题目
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 按权重排序(权重高的在前)
|
|
|
|
|
- usort($weightedQuestions, function ($a, $b) {
|
|
|
|
|
- return ($b['kp_weight'] ?? 1.0) <=> ($a['kp_weight'] ?? 1.0);
|
|
|
|
|
|
|
+ // ========== 步骤2:继续选不同知识点,直到达到目标数量 ==========
|
|
|
|
|
+ $allQuestions = array_merge($questionsByType['choice'], $questionsByType['fill'], $questionsByType['answer']);
|
|
|
|
|
+ usort($allQuestions, function ($a, $b) use ($kpWeights) {
|
|
|
|
|
+ $kpA = $a['kp_code'] ?? '';
|
|
|
|
|
+ $kpB = $b['kp_code'] ?? '';
|
|
|
|
|
+ $weightA = $kpWeights[$kpA] ?? 1.0;
|
|
|
|
|
+ $weightB = $kpWeights[$kpB] ?? 1.0;
|
|
|
|
|
+ if ($weightA == $weightB) {
|
|
|
|
|
+ $idA = $a['id'] ?? $a['question_id'] ?? 0;
|
|
|
|
|
+ $idB = $b['id'] ?? $b['question_id'] ?? 0;
|
|
|
|
|
+ return $idA <=> $idB;
|
|
|
|
|
+ }
|
|
|
|
|
+ return $weightB <=> $weightA;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 选择前 N 道题
|
|
|
|
|
- $selectedQuestions = array_slice($weightedQuestions, 0, $totalQuestions);
|
|
|
|
|
|
|
+ // 选择未选过知识点的题目(优先)
|
|
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
+
|
|
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
|
|
+ if (!isset($kpSelected[$kpCode])) {
|
|
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
|
|
+ $kpSelected[$kpCode] = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 步骤3:如果还有空缺,从已选知识点中补充 ==========
|
|
|
|
|
+ if (count($selectedQuestions) < $totalQuestions) {
|
|
|
|
|
+ Log::info('知识点分配后题目不足,开始补充', [
|
|
|
|
|
+ 'selected_count' => count($selectedQuestions),
|
|
|
|
|
+ 'need_more' => $totalQuestions - count($selectedQuestions),
|
|
|
|
|
+ 'kp_covered' => count($kpSelected)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 统计每个知识点的题目数量
|
|
|
|
|
+ $kpCount = [];
|
|
|
|
|
+ foreach ($selectedQuestions as $q) {
|
|
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
|
|
+ $kpCount[$kpCode] = ($kpCount[$kpCode] ?? 0) + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 优先补充知识点数量少的题目
|
|
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
+
|
|
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
|
|
+ $count = $kpCount[$kpCode] ?? 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果该知识点题目数量少于2题,且题目不在已选列表中
|
|
|
|
|
+ if ($count < 2 && !in_array($q, $selectedQuestions)) {
|
|
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
|
|
+ $kpCount[$kpCode] = $count + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Log::info('知识点题目分配完成', [
|
|
|
|
|
|
|
+ Log::info('知识点优先选择完成', [
|
|
|
'total_questions' => $totalQuestions,
|
|
'total_questions' => $totalQuestions,
|
|
|
'selected_count' => count($selectedQuestions),
|
|
'selected_count' => count($selectedQuestions),
|
|
|
|
|
+ 'kp_covered' => count($kpSelected),
|
|
|
|
|
+ 'type_distribution' => array_count_values(array_map(function($q) {
|
|
|
|
|
+ $qid = $q['id'] ?? $q['question_id'] ?? null;
|
|
|
|
|
+ if ($qid && isset($questionTypeCache[$qid])) {
|
|
|
|
|
+ return $questionTypeCache[$qid];
|
|
|
|
|
+ }
|
|
|
|
|
+ return $this->determineQuestionType($q);
|
|
|
|
|
+ }, $selectedQuestions)),
|
|
|
'top_kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code'))
|
|
'top_kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code'))
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 4. 如果题目过多,按权重排序后截取
|
|
|
|
|
|
|
+ // 最终截取到目标数量(如果超过)
|
|
|
if (count($selectedQuestions) > $totalQuestions) {
|
|
if (count($selectedQuestions) > $totalQuestions) {
|
|
|
- Log::info('开始按权重排序题目', [
|
|
|
|
|
- 'before_sort_count' => count($selectedQuestions),
|
|
|
|
|
- 'target_count' => $totalQuestions
|
|
|
|
|
|
|
+ Log::info('题目数量超过目标,进行最终截取', [
|
|
|
|
|
+ 'before' => count($selectedQuestions),
|
|
|
|
|
+ 'after' => $totalQuestions
|
|
|
]);
|
|
]);
|
|
|
|
|
+ $selectedQuestions = array_slice($selectedQuestions, 0, $totalQuestions);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- $startTime = microtime(true);
|
|
|
|
|
- usort($selectedQuestions, function ($a, $b) use ($kpWeights) {
|
|
|
|
|
- $weightA = $kpWeights[$a['kp_code']] ?? 1.0;
|
|
|
|
|
- $weightB = $kpWeights[$b['kp_code']] ?? 1.0;
|
|
|
|
|
- return $weightB <=> $weightA;
|
|
|
|
|
- });
|
|
|
|
|
- $sortTime = (microtime(true) - $startTime) * 1000;
|
|
|
|
|
|
|
+ // ========== 最终排查:确保无重复题目且题型分布合理 ==========
|
|
|
|
|
+ $finalQuestions = [];
|
|
|
|
|
+ $seenQuestionIds = [];
|
|
|
|
|
+ $duplicateCount = 0;
|
|
|
|
|
+ $typeDistribution = ['choice' => 0, 'fill' => 0, 'answer' => 0];
|
|
|
|
|
|
|
|
- Log::info('权重排序完成', [
|
|
|
|
|
- 'sort_time_ms' => round($sortTime, 2),
|
|
|
|
|
- 'after_sort_count' => count($selectedQuestions)
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ foreach ($selectedQuestions as $question) {
|
|
|
|
|
+ $qbId = $question['question_bank_id'] ?? $question['id'];
|
|
|
|
|
+ if (!in_array($qbId, $seenQuestionIds)) {
|
|
|
|
|
+ $seenQuestionIds[] = $qbId;
|
|
|
|
|
+ $finalQuestions[] = $question;
|
|
|
|
|
|
|
|
- $selectedQuestions = array_slice($selectedQuestions, 0, $totalQuestions);
|
|
|
|
|
|
|
+ // 统计题型分布
|
|
|
|
|
+ $qid = $question['id'] ?? $question['question_id'] ?? null;
|
|
|
|
|
+ $type = null;
|
|
|
|
|
+ if ($qid && isset($questionTypeCache[$qid])) {
|
|
|
|
|
+ $type = $questionTypeCache[$qid];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $type = $this->determineQuestionType($question);
|
|
|
|
|
+ if ($qid) {
|
|
|
|
|
+ $questionTypeCache[$qid] = $type;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isset($typeDistribution[$type])) {
|
|
|
|
|
+ $typeDistribution[$type]++;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $duplicateCount++;
|
|
|
|
|
+ Log::warning('发现重复题目(已自动移除)', [
|
|
|
|
|
+ 'question_id' => $qbId,
|
|
|
|
|
+ 'duplicate_count' => $duplicateCount
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Log::info('开始题型配比调整', [
|
|
|
|
|
- 'input_count' => count($selectedQuestions),
|
|
|
|
|
- 'target_count' => $totalQuestions
|
|
|
|
|
|
|
+ Log::info('最终排查完成', [
|
|
|
|
|
+ 'original_count' => count($selectedQuestions),
|
|
|
|
|
+ 'final_count' => count($finalQuestions),
|
|
|
|
|
+ 'duplicate_removed' => $duplicateCount,
|
|
|
|
|
+ 'final_type_distribution' => $typeDistribution
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 5. 按题型进行微调(难度分布由 QuestionLocalService 处理)
|
|
|
|
|
- return $this->adjustQuestionsByRatio($selectedQuestions, $questionTypeRatio, $totalQuestions);
|
|
|
|
|
|
|
+ // 注意:题型平衡已在上面完成,不需要再调用adjustQuestionsByRatio
|
|
|
|
|
+ // adjustQuestionsByRatio主要处理题型配比,但我们的题型平衡机制已经确保了题型分布符合要求
|
|
|
|
|
+ return $finalQuestions;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|