|
|
@@ -138,29 +138,52 @@ class ExamAnswerAnalysisService
|
|
|
|
|
|
/**
|
|
|
* 获取题目知识点映射
|
|
|
+ * 【优化】优先使用批量查询,失败时降级到单条查询
|
|
|
*/
|
|
|
private function getQuestionKnowledgeMappings(array $questions): array
|
|
|
{
|
|
|
$mappings = [];
|
|
|
$failedQuestions = [];
|
|
|
|
|
|
- // 直接从题目数据中提取知识点信息(不再调用外部服务)
|
|
|
+ // 收集所有题目ID
|
|
|
+ $questionIds = [];
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $questionId = $question['question_id'] ?? null;
|
|
|
+ if ($questionId) {
|
|
|
+ $questionIds[] = $questionId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【优化】尝试批量查询
|
|
|
+ $batchResult = [];
|
|
|
+ if (!empty($questionIds)) {
|
|
|
+ $batchResult = $this->batchGetQuestionKnowledgePoints($questionIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 遍历题目,优先使用批量结果,否则降级到单条查询
|
|
|
foreach ($questions as $question) {
|
|
|
$questionId = $question['question_id'] ?? null;
|
|
|
if (!$questionId) continue;
|
|
|
|
|
|
- // 提取知识点信息(优先使用请求数据中的字段)
|
|
|
$kpMapping = [];
|
|
|
|
|
|
- // 使用新的多知识点获取方法
|
|
|
- $dbKpMappings = $this->getQuestionKnowledgePointsFromDb($questionId);
|
|
|
- if (!empty($dbKpMappings)) {
|
|
|
- $kpMapping = $dbKpMappings; // 直接使用所有知识点
|
|
|
- Log::debug('从数据库获取题目知识点', [
|
|
|
+ // 优先使用批量查询结果
|
|
|
+ if (isset($batchResult[$questionId])) {
|
|
|
+ $kpMapping = $batchResult[$questionId];
|
|
|
+ Log::debug('使用批量查询结果', [
|
|
|
'question_id' => $questionId,
|
|
|
- 'kp_count' => count($dbKpMappings),
|
|
|
- 'kp_codes' => array_column($dbKpMappings, 'kp_id')
|
|
|
+ 'kp_count' => count($kpMapping)
|
|
|
]);
|
|
|
+ } else {
|
|
|
+ // 降级:使用单条查询(批量查询可能遗漏的题目)
|
|
|
+ $dbKpMappings = $this->getQuestionKnowledgePointsFromDb($questionId);
|
|
|
+ if (!empty($dbKpMappings)) {
|
|
|
+ $kpMapping = $dbKpMappings;
|
|
|
+ Log::debug('降级到单条查询', [
|
|
|
+ 'question_id' => $questionId,
|
|
|
+ 'kp_count' => count($dbKpMappings)
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 如果仍然没有知识点信息,跳过该题目
|
|
|
@@ -189,7 +212,8 @@ class ExamAnswerAnalysisService
|
|
|
Log::info('题目知识点映射构建完成', [
|
|
|
'total_questions' => count($questions),
|
|
|
'mapped_questions' => count($mappings),
|
|
|
- 'failed_questions' => count($failedQuestions)
|
|
|
+ 'failed_questions' => count($failedQuestions),
|
|
|
+ 'batch_hit_count' => count($batchResult)
|
|
|
]);
|
|
|
|
|
|
return $mappings;
|
|
|
@@ -351,6 +375,158 @@ class ExamAnswerAnalysisService
|
|
|
return !empty($kpMappings) ? $kpMappings[0] : null;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 【优化】批量获取题目的知识点信息
|
|
|
+ * 将 N+1 查询优化为 2-3 次批量查询
|
|
|
+ *
|
|
|
+ * @param array $questionIds 题目ID数组
|
|
|
+ * @return array [questionId => [kpMappings...]] 映射
|
|
|
+ */
|
|
|
+ private function batchGetQuestionKnowledgePoints(array $questionIds): array
|
|
|
+ {
|
|
|
+ if (empty($questionIds)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $result = [];
|
|
|
+ $questionsNeedCatalogLookup = []; // 需要通过目录查找知识点的题目
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 【第1步】批量查询 questions 表(1次查询)
|
|
|
+ $questionsData = DB::connection('mysql')
|
|
|
+ ->table('questions')
|
|
|
+ ->whereIn('id', $questionIds)
|
|
|
+ ->orWhereIn('question_code', $questionIds)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ // 建立 ID 到题目数据的映射
|
|
|
+ $questionMap = [];
|
|
|
+ foreach ($questionsData as $q) {
|
|
|
+ $questionMap[$q->id] = $q;
|
|
|
+ if ($q->question_code) {
|
|
|
+ $questionMap[$q->question_code] = $q;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【第2步】收集所有直接关联的 kp_code
|
|
|
+ $allKpCodes = [];
|
|
|
+ foreach ($questionsData as $q) {
|
|
|
+ if (!empty($q->kp_code)) {
|
|
|
+ $allKpCodes[] = $q->kp_code;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【第3步】批量查询 knowledge_points 表(1次查询)
|
|
|
+ $kpInfoMap = [];
|
|
|
+ if (!empty($allKpCodes)) {
|
|
|
+ $kpInfos = DB::connection('mysql')
|
|
|
+ ->table('knowledge_points')
|
|
|
+ ->whereIn('kp_code', array_unique($allKpCodes))
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ foreach ($kpInfos as $kp) {
|
|
|
+ $kpInfoMap[$kp->kp_code] = $kp;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【第4步】收集需要通过目录查找的题目
|
|
|
+ $catalogNodeIds = [];
|
|
|
+ foreach ($questionsData as $q) {
|
|
|
+ if (empty($q->kp_code) && !empty($q->textbook_catalog_nodes_id)) {
|
|
|
+ $catalogNodeIds[] = $q->textbook_catalog_nodes_id;
|
|
|
+ $questionsNeedCatalogLookup[$q->id] = $q;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【第5步】批量查询目录-知识点关系(1次查询)
|
|
|
+ $catalogKpMap = [];
|
|
|
+ if (!empty($catalogNodeIds)) {
|
|
|
+ $catalogRelations = DB::connection('mysql')
|
|
|
+ ->table('textbook_chapter_knowledge_relation')
|
|
|
+ ->whereIn('catalog_chapter_id', array_unique($catalogNodeIds))
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ // 收集目录关联的 kp_code
|
|
|
+ $catalogKpCodes = [];
|
|
|
+ foreach ($catalogRelations as $rel) {
|
|
|
+ $catalogKpCodes[] = $rel->kp_code;
|
|
|
+ if (!isset($catalogKpMap[$rel->catalog_chapter_id])) {
|
|
|
+ $catalogKpMap[$rel->catalog_chapter_id] = [];
|
|
|
+ }
|
|
|
+ $catalogKpMap[$rel->catalog_chapter_id][] = $rel->kp_code;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量查询这些知识点的信息
|
|
|
+ if (!empty($catalogKpCodes)) {
|
|
|
+ $catalogKpInfos = DB::connection('mysql')
|
|
|
+ ->table('knowledge_points')
|
|
|
+ ->whereIn('kp_code', array_unique($catalogKpCodes))
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ foreach ($catalogKpInfos as $kp) {
|
|
|
+ $kpInfoMap[$kp->kp_code] = $kp;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【第6步】组装结果
|
|
|
+ foreach ($questionIds as $questionId) {
|
|
|
+ $qData = $questionMap[$questionId] ?? null;
|
|
|
+ if (!$qData) {
|
|
|
+ Log::debug('批量查询未找到题目', ['question_id' => $questionId]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpMappings = [];
|
|
|
+
|
|
|
+ // 方式1:直接从 kp_code 字段获取
|
|
|
+ if (!empty($qData->kp_code)) {
|
|
|
+ $kpCode = $qData->kp_code;
|
|
|
+ $kpInfo = $kpInfoMap[$kpCode] ?? null;
|
|
|
+ $kpMappings[] = [
|
|
|
+ 'kp_id' => $kpCode,
|
|
|
+ 'kp_name' => $kpInfo->name ?? $kpCode,
|
|
|
+ 'weight' => 1.0
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ // 方式2:通过目录关系获取
|
|
|
+ elseif (!empty($qData->textbook_catalog_nodes_id)) {
|
|
|
+ $catalogId = $qData->textbook_catalog_nodes_id;
|
|
|
+ $catalogKpCodes = $catalogKpMap[$catalogId] ?? [];
|
|
|
+ foreach ($catalogKpCodes as $kpCode) {
|
|
|
+ $kpInfo = $kpInfoMap[$kpCode] ?? null;
|
|
|
+ $kpMappings[] = [
|
|
|
+ 'kp_id' => $kpCode,
|
|
|
+ 'kp_name' => $kpInfo->name ?? $kpCode,
|
|
|
+ 'weight' => 1.0
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($kpMappings)) {
|
|
|
+ $result[$questionId] = $kpMappings;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('批量获取题目知识点完成', [
|
|
|
+ 'input_count' => count($questionIds),
|
|
|
+ 'output_count' => count($result),
|
|
|
+ 'query_count' => '2-4次(优化后)'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('批量获取题目知识点失败,将降级到单条查询', [
|
|
|
+ 'question_ids' => $questionIds,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ // 降级:返回空,让调用方使用单条查询
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 从 QuestionBank API 获取题目信息
|
|
|
*/
|