Browse Source

perform: 优化api/exam-answer-analysis接口

大侠咬超人 6 days ago
parent
commit
7a4d5dc270
1 changed files with 186 additions and 10 deletions
  1. 186 10
      app/Services/ExamAnswerAnalysisService.php

+ 186 - 10
app/Services/ExamAnswerAnalysisService.php

@@ -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 获取题目信息
      */