|
@@ -1270,7 +1270,13 @@ class LearningAnalyticsService
|
|
|
$studentId = $params['student_id'] ?? null;
|
|
$studentId = $params['student_id'] ?? null;
|
|
|
$grade = $params['grade'] ?? null; // 用户选择的年级
|
|
$grade = $params['grade'] ?? null; // 用户选择的年级
|
|
|
$totalQuestions = $params['total_questions'] ?? 20;
|
|
$totalQuestions = $params['total_questions'] ?? 20;
|
|
|
- $kpCodes = $params['kp_codes'] ?? [];
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 【修复】参数映射:支持 kp_codes 和 kp_code_list 两种参数名
|
|
|
|
|
+ $kpCodes = $params['kp_codes'] ?? $params['kp_code_list'] ?? [];
|
|
|
|
|
+ if (!is_array($kpCodes)) {
|
|
|
|
|
+ $kpCodes = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
$skills = $params['skills'] ?? [];
|
|
$skills = $params['skills'] ?? [];
|
|
|
$questionTypeRatio = $params['question_type_ratio'] ?? [
|
|
$questionTypeRatio = $params['question_type_ratio'] ?? [
|
|
|
'选择题' => 40,
|
|
'选择题' => 40,
|
|
@@ -1343,7 +1349,7 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 获取学生错题的详细信息
|
|
// 获取学生错题的详细信息
|
|
|
- $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $truncatedMistakeIds, [], null);
|
|
|
|
|
|
|
+ $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $truncatedMistakeIds, [], null, null);
|
|
|
|
|
|
|
|
Log::info('LearningAnalyticsService: 错题获取完成', [
|
|
Log::info('LearningAnalyticsService: 错题获取完成', [
|
|
|
'priority_questions_count' => count($priorityQuestions),
|
|
'priority_questions_count' => count($priorityQuestions),
|
|
@@ -1381,7 +1387,7 @@ class LearningAnalyticsService
|
|
|
// 使用原卷子题目补充到目标数量
|
|
// 使用原卷子题目补充到目标数量
|
|
|
$additionalNeeded = max(5, count($priorityQuestions)) - count($priorityQuestions);
|
|
$additionalNeeded = max(5, count($priorityQuestions)) - count($priorityQuestions);
|
|
|
$paperQuestionIds = array_diff($paperQuestionIds, array_column($priorityQuestions, 'id'));
|
|
$paperQuestionIds = array_diff($paperQuestionIds, array_column($priorityQuestions, 'id'));
|
|
|
- $additionalPaperQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $additionalNeeded, $paperQuestionIds, [], null);
|
|
|
|
|
|
|
+ $additionalPaperQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $additionalNeeded, $paperQuestionIds, [], null, null);
|
|
|
|
|
|
|
|
$allQuestions = array_merge($priorityQuestions, $additionalPaperQuestions);
|
|
$allQuestions = array_merge($priorityQuestions, $additionalPaperQuestions);
|
|
|
Log::info('LearningAnalyticsService: 错题本题目补充完成', [
|
|
Log::info('LearningAnalyticsService: 错题本题目补充完成', [
|
|
@@ -1399,7 +1405,7 @@ class LearningAnalyticsService
|
|
|
// 获取原卷子的所有题目
|
|
// 获取原卷子的所有题目
|
|
|
$paperQuestionIds = $this->getPaperAllQuestions($params['paper_ids']);
|
|
$paperQuestionIds = $this->getPaperAllQuestions($params['paper_ids']);
|
|
|
if (!empty($paperQuestionIds)) {
|
|
if (!empty($paperQuestionIds)) {
|
|
|
- $allQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $paperQuestionIds, [], null);
|
|
|
|
|
|
|
+ $allQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $maxQuestions, $paperQuestionIds, [], null, null);
|
|
|
|
|
|
|
|
// 检查是否获取到题目
|
|
// 检查是否获取到题目
|
|
|
if (empty($allQuestions)) {
|
|
if (empty($allQuestions)) {
|
|
@@ -1440,7 +1446,10 @@ class LearningAnalyticsService
|
|
|
'assemble_type' => $assembleType
|
|
'assemble_type' => $assembleType
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $maxQuestions, [], [], $questionCategory);
|
|
|
|
|
|
|
+ // 【优化】获取textbook_catalog_node_ids参数(教材组卷时使用)
|
|
|
|
|
+ $textbookCatalogNodeIds = $params['textbook_catalog_node_ids'] ?? null;
|
|
|
|
|
+
|
|
|
|
|
+ $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $maxQuestions, [], [], $questionCategory, $textbookCatalogNodeIds);
|
|
|
$allQuestions = array_merge($priorityQuestions, $additionalQuestions);
|
|
$allQuestions = array_merge($priorityQuestions, $additionalQuestions);
|
|
|
|
|
|
|
|
Log::info('getQuestionsFromBank 调用完成', [
|
|
Log::info('getQuestionsFromBank 调用完成', [
|
|
@@ -1515,7 +1524,8 @@ class LearningAnalyticsService
|
|
|
$targetQuestionCount,
|
|
$targetQuestionCount,
|
|
|
$questionTypeRatio,
|
|
$questionTypeRatio,
|
|
|
$difficultyLevels,
|
|
$difficultyLevels,
|
|
|
- $weaknessFilter
|
|
|
|
|
|
|
+ $weaknessFilter,
|
|
|
|
|
+ $assembleType // 新增assembleType参数
|
|
|
);
|
|
);
|
|
|
$selectTime = (microtime(true) - $startTime) * 1000;
|
|
$selectTime = (microtime(true) - $startTime) * 1000;
|
|
|
|
|
|
|
@@ -1535,14 +1545,24 @@ class LearningAnalyticsService
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 如果启用了难度分布且不是排除类型,则应用难度分布
|
|
|
|
|
|
|
+ // 【恢复】简化难度分布检查
|
|
|
$difficultyCategory = $params['difficulty_category'] ?? 1;
|
|
$difficultyCategory = $params['difficulty_category'] ?? 1;
|
|
|
$enableDistribution = $params['enable_difficulty_distribution'] ?? false;
|
|
$enableDistribution = $params['enable_difficulty_distribution'] ?? false;
|
|
|
$isExcludedType = ($assembleType === 5); // 只有错题本类型(assembleType=5)不应用难度分布
|
|
$isExcludedType = ($assembleType === 5); // 只有错题本类型(assembleType=5)不应用难度分布
|
|
|
|
|
|
|
|
|
|
+ // 【重要】添加详细的difficulty_category追踪日志
|
|
|
|
|
+ Log::info('LearningAnalyticsService: 难度分布检查开始', [
|
|
|
|
|
+ 'input_difficulty_category' => $difficultyCategory,
|
|
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
|
|
+ 'enable_distribution' => $enableDistribution,
|
|
|
|
|
+ 'is_excluded_type' => $isExcludedType,
|
|
|
|
|
+ 'selected_questions_before' => count($selectedQuestions),
|
|
|
|
|
+ 'params_keys' => array_keys($params)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
if ($enableDistribution && !$isExcludedType) {
|
|
if ($enableDistribution && !$isExcludedType) {
|
|
|
Log::info('LearningAnalyticsService: 应用难度系数分布', [
|
|
Log::info('LearningAnalyticsService: 应用难度系数分布', [
|
|
|
- 'difficulty_category' => $difficultyCategory,
|
|
|
|
|
|
|
+ 'difficulty_category_before' => $difficultyCategory,
|
|
|
'assemble_type' => $assembleType,
|
|
'assemble_type' => $assembleType,
|
|
|
'before_count' => count($selectedQuestions)
|
|
'before_count' => count($selectedQuestions)
|
|
|
]);
|
|
]);
|
|
@@ -1560,11 +1580,13 @@ class LearningAnalyticsService
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
Log::info('LearningAnalyticsService: 难度分布应用完成', [
|
|
Log::info('LearningAnalyticsService: 难度分布应用完成', [
|
|
|
|
|
+ 'difficulty_category_after' => $difficultyCategory,
|
|
|
'after_count' => count($selectedQuestions)
|
|
'after_count' => count($selectedQuestions)
|
|
|
]);
|
|
]);
|
|
|
} catch (\Exception $e) {
|
|
} catch (\Exception $e) {
|
|
|
Log::warning('LearningAnalyticsService: 难度分布应用失败,继续使用原结果', [
|
|
Log::warning('LearningAnalyticsService: 难度分布应用失败,继续使用原结果', [
|
|
|
- 'error' => $e->getMessage()
|
|
|
|
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
|
|
+ 'difficulty_category_when_error' => $difficultyCategory
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1580,7 +1602,10 @@ class LearningAnalyticsService
|
|
|
return in_array($q['kp_code'] ?? '', $weaknessFilter);
|
|
return in_array($q['kp_code'] ?? '', $weaknessFilter);
|
|
|
})) : 0,
|
|
})) : 0,
|
|
|
'difficulty_distribution_applied' => $enableDistribution && !$isExcludedType,
|
|
'difficulty_distribution_applied' => $enableDistribution && !$isExcludedType,
|
|
|
- 'difficulty_category' => $difficultyCategory
|
|
|
|
|
|
|
+ 'difficulty_category' => $difficultyCategory,
|
|
|
|
|
+ // 【新增】章节知识点数量统计(教材组卷时)
|
|
|
|
|
+ 'chapter_knowledge_point_stats' => $params['chapter_knowledge_point_stats'] ?? null,
|
|
|
|
|
+ 'textbook_catalog_node_ids' => $params['textbook_catalog_node_ids'] ?? null
|
|
|
]
|
|
]
|
|
|
];
|
|
];
|
|
|
} catch (\Exception $e) {
|
|
} catch (\Exception $e) {
|
|
@@ -1600,8 +1625,9 @@ class LearningAnalyticsService
|
|
|
/**
|
|
/**
|
|
|
* 从本地题库获取题目(错题回顾优先)
|
|
* 从本地题库获取题目(错题回顾优先)
|
|
|
* 支持优先获取指定题目ID的题目
|
|
* 支持优先获取指定题目ID的题目
|
|
|
|
|
+ * 【优化】新增textbookCatalogNodeIds参数,支持按textbook_catalog_node_id筛选题目
|
|
|
*/
|
|
*/
|
|
|
- private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], int $totalNeeded = 100, array $priorityQuestionIds = [], array $excludeQuestionIds = [], ?int $questionCategory = null): array
|
|
|
|
|
|
|
+ private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], int $totalNeeded = 100, array $priorityQuestionIds = [], array $excludeQuestionIds = [], ?int $questionCategory = null, ?array $textbookCatalogNodeIds = null): array
|
|
|
{
|
|
{
|
|
|
$startTime = microtime(true);
|
|
$startTime = microtime(true);
|
|
|
|
|
|
|
@@ -1667,6 +1693,12 @@ class LearningAnalyticsService
|
|
|
Log::info('应用题目分类筛选', ['question_category' => $questionCategory]);
|
|
Log::info('应用题目分类筛选', ['question_category' => $questionCategory]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 【优化】按教材章节节点筛选(textbook_catalog_nodes_id)
|
|
|
|
|
+ if (!empty($textbookCatalogNodeIds)) {
|
|
|
|
|
+ $query->whereIn('textbook_catalog_nodes_id', $textbookCatalogNodeIds);
|
|
|
|
|
+ Log::info('应用教材章节节点筛选', ['textbook_catalog_nodes_ids' => $textbookCatalogNodeIds]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 筛选有解题思路的题目
|
|
// 筛选有解题思路的题目
|
|
|
$query->whereNotNull('solution')
|
|
$query->whereNotNull('solution')
|
|
|
->where('solution', '!=', '')
|
|
->where('solution', '!=', '')
|
|
@@ -1675,17 +1707,30 @@ class LearningAnalyticsService
|
|
|
// 注意: 难度筛选由 QuestionLocalService 的难度分布系统处理
|
|
// 注意: 难度筛选由 QuestionLocalService 的难度分布系统处理
|
|
|
// 不在这里进行难度筛选,让 QuestionLocalService 做精确的难度分布
|
|
// 不在这里进行难度筛选,让 QuestionLocalService 做精确的难度分布
|
|
|
|
|
|
|
|
- // 限制数量并随机排序
|
|
|
|
|
- $query->limit($totalNeeded * 2) // 多取一些用于后续筛选
|
|
|
|
|
- ->inRandomOrder();
|
|
|
|
|
|
|
+ // 【重要】移除数量限制,获取所有符合条件的题目
|
|
|
|
|
+ // 不使用limit()限制查询结果,让后续处理逻辑决定最终数量
|
|
|
|
|
+ $query->inRandomOrder();
|
|
|
|
|
|
|
|
$questions = $query->get();
|
|
$questions = $query->get();
|
|
|
|
|
|
|
|
Log::info('getQuestionsFromBank: 查询完成', [
|
|
Log::info('getQuestionsFromBank: 查询完成', [
|
|
|
'raw_count' => $questions->count(),
|
|
'raw_count' => $questions->count(),
|
|
|
|
|
+ 'total_needed' => $totalNeeded,
|
|
|
|
|
+ 'note' => '移除limit限制,获取所有符合条件的题目',
|
|
|
'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
|
|
+ // 【重要】添加更详细的日志来追踪题目筛选过程
|
|
|
|
|
+ Log::info('getQuestionsFromBank: 题目筛选过程详情', [
|
|
|
|
|
+ 'database_query_count' => $questions->count(),
|
|
|
|
|
+ 'kp_codes_filter' => $kpCodes,
|
|
|
|
|
+ 'skills_filter' => $skills,
|
|
|
|
|
+ 'exclude_count' => count($excludeQuestionIds),
|
|
|
|
|
+ 'question_category' => $questionCategory,
|
|
|
|
|
+ 'textbook_catalog_node_ids_filter' => $textbookCatalogNodeIds,
|
|
|
|
|
+ 'note' => '将返回所有符合条件的题目,不限制数量'
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
// 转换为标准格式
|
|
// 转换为标准格式
|
|
|
$formattedQuestions = $questions->map(function ($q) {
|
|
$formattedQuestions = $questions->map(function ($q) {
|
|
|
return [
|
|
return [
|
|
@@ -1707,12 +1752,13 @@ class LearningAnalyticsService
|
|
|
];
|
|
];
|
|
|
})->toArray();
|
|
})->toArray();
|
|
|
|
|
|
|
|
- // 注意: 题型和难度配比调整由 QuestionLocalService 处理
|
|
|
|
|
- // 这里只做初步筛选,让 QuestionLocalService 做精确的配比调整
|
|
|
|
|
- $selectedQuestions = array_slice($formattedQuestions, 0, $totalNeeded);
|
|
|
|
|
|
|
+ // 【重要】返回所有符合条件的题目,不限制数量
|
|
|
|
|
+ // 让上层调用者根据需要选择题目数量
|
|
|
|
|
+ $selectedQuestions = $formattedQuestions;
|
|
|
|
|
|
|
|
Log::info('getQuestionsFromBank 完成', [
|
|
Log::info('getQuestionsFromBank 完成', [
|
|
|
- 'selected_count' => count($selectedQuestions),
|
|
|
|
|
|
|
+ 'final_count' => count($selectedQuestions),
|
|
|
|
|
+ 'raw_database_count' => $questions->count(),
|
|
|
'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
@@ -1922,7 +1968,8 @@ class LearningAnalyticsService
|
|
|
int $totalQuestions,
|
|
int $totalQuestions,
|
|
|
array $questionTypeRatio,
|
|
array $questionTypeRatio,
|
|
|
array $difficultyLevels,
|
|
array $difficultyLevels,
|
|
|
- array $weaknessFilter
|
|
|
|
|
|
|
+ array $weaknessFilter,
|
|
|
|
|
+ int $assembleType // 新增assembleType参数
|
|
|
): array {
|
|
): array {
|
|
|
Log::info('selectQuestionsByMastery 开始', [
|
|
Log::info('selectQuestionsByMastery 开始', [
|
|
|
'question_count' => count($questions),
|
|
'question_count' => count($questions),
|
|
@@ -1930,32 +1977,30 @@ class LearningAnalyticsService
|
|
|
'total_questions' => $totalQuestions
|
|
'total_questions' => $totalQuestions
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 错题本类型:使用所有题目,不进行权重分配和筛选
|
|
|
|
|
- if ($totalQuestions >= count($questions)) {
|
|
|
|
|
- Log::info('错题本类型:使用所有题目,跳过权重分配', [
|
|
|
|
|
|
|
+ // 【修复】题目数量处理逻辑:无论题目数量多少,都要进行权重分配和筛选
|
|
|
|
|
+ // 如果题目数量超过目标,则截取到目标数量
|
|
|
|
|
+ // 如果题目数量不足,则使用所有题目,并记录警告
|
|
|
|
|
+ if (count($questions) > $totalQuestions) {
|
|
|
|
|
+ Log::info('题目数量超过目标,进行截取', [
|
|
|
'question_count' => count($questions),
|
|
'question_count' => count($questions),
|
|
|
'total_questions' => $totalQuestions,
|
|
'total_questions' => $totalQuestions,
|
|
|
- 'input_question_count' => func_num_args() > 0 ? count($questions) : 'N/A'
|
|
|
|
|
|
|
+ 'note' => '将按权重选择最合适的题目'
|
|
|
]);
|
|
]);
|
|
|
- return $questions;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 如果未选择难度,则不过滤(随机生成所有难度)
|
|
|
|
|
- if (empty($difficultyLevels)) {
|
|
|
|
|
- Log::info('用户未选择难度,将随机生成所有难度的题目');
|
|
|
|
|
- // 不过滤任何题目,保留所有难度
|
|
|
|
|
} else {
|
|
} else {
|
|
|
- // 按难度筛掉不在选择范围内的题目
|
|
|
|
|
- $questions = array_values(array_filter($questions, function ($q) use ($difficultyLevels) {
|
|
|
|
|
- $d = $q['difficulty'] ?? null;
|
|
|
|
|
- if ($d === null) return true; // 无难度信息则保留
|
|
|
|
|
- $level = $this->mapDifficultyLevel((float)$d);
|
|
|
|
|
- return in_array($level, $difficultyLevels);
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ Log::warning('题目数量不足,将使用所有可用题目', [
|
|
|
|
|
+ 'available_count' => count($questions),
|
|
|
|
|
+ 'requested_count' => $totalQuestions,
|
|
|
|
|
+ 'note' => '可能需要补充题库或放宽筛选条件'
|
|
|
|
|
+ ]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Log::info('难度筛选完成', [
|
|
|
|
|
- 'after_filter_count' => count($questions)
|
|
|
|
|
|
|
+ // 【移除】删除多余的难度筛选逻辑
|
|
|
|
|
+ // 题目本身就有难度系数,QuestionLocalService的难度分布系统会处理题目分布
|
|
|
|
|
+ // 不需要额外的难度筛选,让题目保持原始的难度分布
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('跳过多余的难度筛选,使用题目原始难度分布', [
|
|
|
|
|
+ 'question_count' => count($questions),
|
|
|
|
|
+ 'note' => '由QuestionLocalService处理难度分布'
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 1. 按知识点分组
|
|
// 1. 按知识点分组
|
|
@@ -2085,11 +2130,20 @@ class LearningAnalyticsService
|
|
|
$questionsByType[$type][] = $q;
|
|
$questionsByType[$type][] = $q;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ========== 步骤1:按知识点分组,优先选不同知识点 ==========
|
|
|
|
|
|
|
+ // ========== 步骤1:按题型分配题目 ==========
|
|
|
$selectedQuestions = [];
|
|
$selectedQuestions = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 【区分】根据assembleType决定是否使用知识点优先机制
|
|
|
|
|
+ $useKnowledgePointPriority = ($assembleType === 0); // 摸底测试需要知识点优先
|
|
|
$kpSelected = []; // 已选知识点记录
|
|
$kpSelected = []; // 已选知识点记录
|
|
|
|
|
|
|
|
- // 先确保每种题型至少选1题(来自不同知识点)
|
|
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 知识点优先策略', [
|
|
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
|
|
+ 'use_knowledge_point_priority' => $useKnowledgePointPriority,
|
|
|
|
|
+ 'note' => $useKnowledgePointPriority ? '摸底测试:需要均衡分配知识点' : '知识点组卷:允许同一知识点选多题'
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 确保每种题型至少选1题
|
|
|
foreach (['choice', 'fill', 'answer'] as $type) {
|
|
foreach (['choice', 'fill', 'answer'] as $type) {
|
|
|
if (empty($questionsByType[$type])) {
|
|
if (empty($questionsByType[$type])) {
|
|
|
Log::warning('题型分配:题型无题目', ['type' => $type]);
|
|
Log::warning('题型分配:题型无题目', ['type' => $type]);
|
|
@@ -2105,78 +2159,127 @@ class LearningAnalyticsService
|
|
|
return $weightB <=> $weightA;
|
|
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;
|
|
|
|
|
|
|
+ // 根据策略选择题目
|
|
|
|
|
+ if ($useKnowledgePointPriority) {
|
|
|
|
|
+ // 摸底测试:选择第一个未选过知识点的题目
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 【修复】知识点组卷:随机选择该题型的一道题,避免固定选择第一个导致知识点分布不均
|
|
|
|
|
+ $randomIndex = array_rand($questionsByType[$type]);
|
|
|
|
|
+ $selectedQuestions[] = $questionsByType[$type][$randomIndex];
|
|
|
|
|
+ Log::debug('题型基础分配(随机选择)', [
|
|
|
|
|
+ 'type' => $type,
|
|
|
|
|
+ 'kp' => $questionsByType[$type][$randomIndex]['kp_code'] ?? 'unknown',
|
|
|
|
|
+ 'random_index' => $randomIndex,
|
|
|
|
|
+ 'total_in_type' => count($questionsByType[$type])
|
|
|
|
|
+ ]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ========== 步骤2:继续选不同知识点,直到达到目标数量 ==========
|
|
|
|
|
|
|
+ // ========== 步骤2:继续选题目,直到达到目标数量 ==========
|
|
|
$allQuestions = array_merge($questionsByType['choice'], $questionsByType['fill'], $questionsByType['answer']);
|
|
$allQuestions = array_merge($questionsByType['choice'], $questionsByType['fill'], $questionsByType['answer']);
|
|
|
|
|
+
|
|
|
|
|
+ // 【重要】添加排序前的知识点分布日志
|
|
|
|
|
+ $preSortKpDistribution = [];
|
|
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
|
|
+ $kpCode = $q['kp_code'] ?? '';
|
|
|
|
|
+ if (!isset($preSortKpDistribution[$kpCode])) {
|
|
|
|
|
+ $preSortKpDistribution[$kpCode] = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ $preSortKpDistribution[$kpCode]++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 排序前知识点分布', [
|
|
|
|
|
+ 'total_questions' => count($allQuestions),
|
|
|
|
|
+ 'kp_distribution' => $preSortKpDistribution,
|
|
|
|
|
+ 'note' => '用于对比排序后的分布变化'
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 【修复】添加随机因子到排序中,避免因ID排序导致知识点分布不均
|
|
|
usort($allQuestions, function ($a, $b) use ($kpWeights) {
|
|
usort($allQuestions, function ($a, $b) use ($kpWeights) {
|
|
|
$kpA = $a['kp_code'] ?? '';
|
|
$kpA = $a['kp_code'] ?? '';
|
|
|
$kpB = $b['kp_code'] ?? '';
|
|
$kpB = $b['kp_code'] ?? '';
|
|
|
$weightA = $kpWeights[$kpA] ?? 1.0;
|
|
$weightA = $kpWeights[$kpA] ?? 1.0;
|
|
|
$weightB = $kpWeights[$kpB] ?? 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;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 主要按权重排序
|
|
|
|
|
+ if ($weightA != $weightB) {
|
|
|
|
|
+ return $weightB <=> $weightA;
|
|
|
}
|
|
}
|
|
|
- return $weightB <=> $weightA;
|
|
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
- // 选择未选过知识点的题目(优先)
|
|
|
|
|
- foreach ($allQuestions as $q) {
|
|
|
|
|
- if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
|
|
+ // 权重相同时,添加随机排序而不是按ID排序
|
|
|
|
|
+ // 使用随机因子确保相同权重的知识点有公平的选中机会
|
|
|
|
|
+ return mt_rand(-1, 1);
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
|
|
+ // 【重要】添加排序后的知识点分布日志
|
|
|
|
|
+ $postSortKpDistribution = [];
|
|
|
|
|
+ $postSortFirst50 = []; // 记录前50题的知识点分布
|
|
|
|
|
+ foreach ($allQuestions as $idx => $q) {
|
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
|
- if (!isset($kpSelected[$kpCode])) {
|
|
|
|
|
- $selectedQuestions[] = $q;
|
|
|
|
|
- $kpSelected[$kpCode] = true;
|
|
|
|
|
|
|
+ if (!isset($postSortKpDistribution[$kpCode])) {
|
|
|
|
|
+ $postSortKpDistribution[$kpCode] = 0;
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ $postSortKpDistribution[$kpCode]++;
|
|
|
|
|
|
|
|
- // ========== 步骤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;
|
|
|
|
|
|
|
+ // 记录前50题的知识点
|
|
|
|
|
+ if ($idx < 50) {
|
|
|
|
|
+ $postSortFirst50[] = $kpCode;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 排序后知识点分布', [
|
|
|
|
|
+ 'total_questions' => count($allQuestions),
|
|
|
|
|
+ 'kp_distribution' => $postSortKpDistribution,
|
|
|
|
|
+ 'first_50_kp_codes' => $postSortFirst50,
|
|
|
|
|
+ 'note' => '观察排序是否均衡分布A07和A08'
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
- // 优先补充知识点数量少的题目
|
|
|
|
|
|
|
+ // 根据策略继续选择题目
|
|
|
|
|
+ if ($useKnowledgePointPriority) {
|
|
|
|
|
+ // 摸底测试:选择未选过知识点的题目(优先)
|
|
|
foreach ($allQuestions as $q) {
|
|
foreach ($allQuestions as $q) {
|
|
|
if (count($selectedQuestions) >= $totalQuestions) break;
|
|
if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
|
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
$kpCode = $q['kp_code'] ?? '';
|
|
|
- $count = $kpCount[$kpCode] ?? 0;
|
|
|
|
|
|
|
+ if (!isset($kpSelected[$kpCode])) {
|
|
|
|
|
+ $selectedQuestions[] = $q;
|
|
|
|
|
+ $kpSelected[$kpCode] = true;
|
|
|
|
|
+ Log::debug('继续选择题目(知识点优先)', ['kp' => $kpCode, 'id' => $q['id'] ?? 'unknown']);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 知识点组卷:选择未选过的题目(不要求知识点不重复)
|
|
|
|
|
+ $selectedIds = array_column($selectedQuestions, 'id');
|
|
|
|
|
+ foreach ($allQuestions as $q) {
|
|
|
|
|
+ if (count($selectedQuestions) >= $totalQuestions) break;
|
|
|
|
|
|
|
|
- // 如果该知识点题目数量少于2题,且题目不在已选列表中
|
|
|
|
|
- if ($count < 2 && !in_array($q, $selectedQuestions)) {
|
|
|
|
|
|
|
+ $qid = $q['id'] ?? null;
|
|
|
|
|
+ if ($qid && !in_array($qid, $selectedIds)) {
|
|
|
$selectedQuestions[] = $q;
|
|
$selectedQuestions[] = $q;
|
|
|
- $kpCount[$kpCode] = $count + 1;
|
|
|
|
|
|
|
+ $selectedIds[] = $qid;
|
|
|
|
|
+ Log::debug('继续选择题目(无知识点限制)', ['kp' => $q['kp_code'] ?? 'unknown', 'id' => $qid]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Log::info('知识点优先选择完成', [
|
|
|
|
|
|
|
+ // 【移除】删除步骤3的多余逻辑
|
|
|
|
|
+ // 前面的逻辑已经能选够题目,不需要额外的补充步骤
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 题目选择完成', [
|
|
|
'total_questions' => $totalQuestions,
|
|
'total_questions' => $totalQuestions,
|
|
|
'selected_count' => count($selectedQuestions),
|
|
'selected_count' => count($selectedQuestions),
|
|
|
- 'kp_covered' => count($kpSelected),
|
|
|
|
|
|
|
+ 'success' => count($selectedQuestions) === $totalQuestions,
|
|
|
|
|
+ 'assemble_type' => $assembleType,
|
|
|
|
|
+ 'strategy' => $useKnowledgePointPriority ? '知识点优先' : '无知识点限制',
|
|
|
'type_distribution' => array_count_values(array_map(function($q) {
|
|
'type_distribution' => array_count_values(array_map(function($q) {
|
|
|
$qid = $q['id'] ?? $q['question_id'] ?? null;
|
|
$qid = $q['id'] ?? $q['question_id'] ?? null;
|
|
|
if ($qid && isset($questionTypeCache[$qid])) {
|
|
if ($qid && isset($questionTypeCache[$qid])) {
|
|
@@ -2184,10 +2287,10 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
return $this->determineQuestionType($q);
|
|
return $this->determineQuestionType($q);
|
|
|
}, $selectedQuestions)),
|
|
}, $selectedQuestions)),
|
|
|
- 'top_kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code'))
|
|
|
|
|
|
|
+ 'kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code'))
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- // 最终截取到目标数量(如果超过)
|
|
|
|
|
|
|
+ // 【重要】最终截取到目标数量(如果超过)
|
|
|
if (count($selectedQuestions) > $totalQuestions) {
|
|
if (count($selectedQuestions) > $totalQuestions) {
|
|
|
Log::info('题目数量超过目标,进行最终截取', [
|
|
Log::info('题目数量超过目标,进行最终截取', [
|
|
|
'before' => count($selectedQuestions),
|
|
'before' => count($selectedQuestions),
|
|
@@ -2196,6 +2299,13 @@ class LearningAnalyticsService
|
|
|
$selectedQuestions = array_slice($selectedQuestions, 0, $totalQuestions);
|
|
$selectedQuestions = array_slice($selectedQuestions, 0, $totalQuestions);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 【重要】添加最终数量验证日志
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 最终题目数量验证', [
|
|
|
|
|
+ 'before_final_check' => count($selectedQuestions),
|
|
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
|
|
+ 'is_array' => is_array($selectedQuestions)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
// ========== 最终排查:确保无重复题目且题型分布合理 ==========
|
|
// ========== 最终排查:确保无重复题目且题型分布合理 ==========
|
|
|
$finalQuestions = [];
|
|
$finalQuestions = [];
|
|
|
$seenQuestionIds = [];
|
|
$seenQuestionIds = [];
|
|
@@ -2231,11 +2341,49 @@ class LearningAnalyticsService
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 【重要】如果去重后数量不足,补充到目标数量
|
|
|
|
|
+ if (count($finalQuestions) < $totalQuestions) {
|
|
|
|
|
+ Log::warning('selectQuestionsByMastery: 去重后数量不足,尝试补充', [
|
|
|
|
|
+ 'final_count' => count($finalQuestions),
|
|
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
|
|
+ 'need_more' => $totalQuestions - count($finalQuestions)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 从原始题目中选择未选过的题目补充
|
|
|
|
|
+ $usedIds = array_column($finalQuestions, 'id');
|
|
|
|
|
+ $supplementCount = 0;
|
|
|
|
|
+ foreach ($selectedQuestions as $question) {
|
|
|
|
|
+ if ($supplementCount >= ($totalQuestions - count($finalQuestions))) break;
|
|
|
|
|
+
|
|
|
|
|
+ $qid = $question['id'] ?? null;
|
|
|
|
|
+ if ($qid && !in_array($qid, $usedIds)) {
|
|
|
|
|
+ $finalQuestions[] = $question;
|
|
|
|
|
+ $usedIds[] = $qid;
|
|
|
|
|
+ $supplementCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 补充完成', [
|
|
|
|
|
+ 'supplement_added' => $supplementCount,
|
|
|
|
|
+ 'final_count_after_supplement' => count($finalQuestions)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
Log::info('最终排查完成', [
|
|
Log::info('最终排查完成', [
|
|
|
'original_count' => count($selectedQuestions),
|
|
'original_count' => count($selectedQuestions),
|
|
|
'final_count' => count($finalQuestions),
|
|
'final_count' => count($finalQuestions),
|
|
|
'duplicate_removed' => $duplicateCount,
|
|
'duplicate_removed' => $duplicateCount,
|
|
|
- 'final_type_distribution' => $typeDistribution
|
|
|
|
|
|
|
+ 'final_type_distribution' => $typeDistribution,
|
|
|
|
|
+ 'target_count' => $totalQuestions
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 【重要】确保返回的题目数量正确
|
|
|
|
|
+ $finalQuestions = array_slice($finalQuestions, 0, $totalQuestions);
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('selectQuestionsByMastery: 返回结果', [
|
|
|
|
|
+ 'return_count' => count($finalQuestions),
|
|
|
|
|
+ 'target_count' => $totalQuestions,
|
|
|
|
|
+ 'success' => count($finalQuestions) === $totalQuestions
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 注意:题型平衡已在上面完成,不需要再调用adjustQuestionsByRatio
|
|
// 注意:题型平衡已在上面完成,不需要再调用adjustQuestionsByRatio
|
|
@@ -2497,16 +2645,8 @@ class LearningAnalyticsService
|
|
|
return 'answer';
|
|
return 'answer';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function mapDifficultyLevel(float $d): string
|
|
|
|
|
- {
|
|
|
|
|
- if ($d <= 0.4) {
|
|
|
|
|
- return '基础';
|
|
|
|
|
- }
|
|
|
|
|
- if ($d <= 0.7) {
|
|
|
|
|
- return '中等';
|
|
|
|
|
- }
|
|
|
|
|
- return '拔高';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 【移除】删除未使用的mapDifficultyLevel方法
|
|
|
|
|
+ // 难度分布由QuestionLocalService处理,不需要额外的难度映射
|
|
|
|
|
|
|
|
private function computeTypeTargets(int $targetCount, array $questionTypeRatio): array
|
|
private function computeTypeTargets(int $targetCount, array $questionTypeRatio): array
|
|
|
{
|
|
{
|