Browse Source

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

大侠咬超人 6 days ago
parent
commit
e7e471f1e0
1 changed files with 134 additions and 150 deletions
  1. 134 150
      app/Services/ExamAnswerAnalysisService.php

+ 134 - 150
app/Services/ExamAnswerAnalysisService.php

@@ -53,12 +53,12 @@ class ExamAnswerAnalysisService
         // 【公司要求】1. 获取学案基准难度(取自学案的difficulty_category)
         $examBaseDifficulty = $this->getExamBaseDifficulty($examData['paper_id'] ?? '');
 
-        // 2. 保存答题记录到数据库
-        $this->saveExamAnswerRecords($examData);
-
-        // 3. 获取题目知识点映射
+        // 2. 获取题目知识点映射(批量查询,避免N+1)
         $questionMappings = $this->getQuestionKnowledgeMappings($questions);
 
+        // 3. 保存答题记录到数据库(复用已查询的知识点映射)
+        $this->saveExamAnswerRecords($examData, $questionMappings);
+
         // 【公司要求】4. 计算每个知识点的加权掌握度(传入学案基准难度)
         // 核心算法:难度映射 → 权重计算 → 数值更新(newMastery = oldMastery + change)
         $knowledgeMasteryVector = $this->calculateKnowledgeMasteryVector($questions, $questionMappings, $examBaseDifficulty, $studentId);
@@ -725,39 +725,44 @@ class ExamAnswerAnalysisService
     }
 
     /**
-     * 更新学生掌握度(与历史数据合并
+     * 更新学生掌握度(优化版:批量查询+批量upsert
      */
     private function updateStudentMastery(string $studentId, array $knowledgeMasteryVector): array
     {
+        if (empty($knowledgeMasteryVector)) {
+            return [];
+        }
+
         $updatedMastery = [];
+        $kpIds = array_keys($knowledgeMasteryVector);
+        $now = now();
 
-        foreach ($knowledgeMasteryVector as $kpId => $data) {
-            // 获取历史掌握度
-            $historyMastery = DB::connection('mysql')
-                ->table('student_knowledge_mastery')
-                ->where('student_id', $studentId)
-                ->where('kp_code', $kpId)
-                ->first();
+        // 【优化】批量查询所有历史掌握度(1次查询代替N次)
+        $historyRecords = DB::connection('mysql')
+            ->table('student_knowledge_mastery')
+            ->where('student_id', $studentId)
+            ->whereIn('kp_code', $kpIds)
+            ->get()
+            ->keyBy('kp_code');
 
+        // 准备批量upsert的数据
+        $upsertData = [];
+        foreach ($knowledgeMasteryVector as $kpId => $data) {
+            $historyMastery = $historyRecords->get($kpId);
             $historyMasteryLevel = $historyMastery->mastery_level ?? 0.0;
 
-            // 【公司要求】保存到数据库(只保存核心掌握度数据)
-            DB::connection('mysql')
-                ->table('student_knowledge_mastery')
-                ->updateOrInsert(
-                    ['student_id' => $studentId, 'kp_code' => $kpId],
-                    [
-                        'mastery_level' => $data['mastery'],
-                        'confidence_level' => 0.0, // 不再保存置信度
-                        'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
-                        'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_attempts'] > 0),
-                        'mastery_trend' => 'stable', // 不再判断趋势,统一设为stable
-                        'last_mastery_update' => now(),
-                        'updated_at' => now(),
-                    ]
-                );
+            $upsertData[] = [
+                'student_id' => $studentId,
+                'kp_code' => $kpId,
+                'mastery_level' => $data['mastery'],
+                'confidence_level' => 0.0,
+                'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
+                'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_attempts'] > 0),
+                'mastery_trend' => 'stable',
+                'last_mastery_update' => $now,
+                'updated_at' => $now,
+            ];
 
-            // 【公司要求】返回值:只返回核心掌握度数据
             $updatedMastery[$kpId] = [
                 'kp_id' => $kpId,
                 'current_mastery' => $data['mastery'],
@@ -768,8 +773,19 @@ class ExamAnswerAnalysisService
             ];
         }
 
+        // 【优化】批量upsert(1次查询代替N次 updateOrInsert)
+        if (!empty($upsertData)) {
+            DB::connection('mysql')
+                ->table('student_knowledge_mastery')
+                ->upsert(
+                    $upsertData,
+                    ['student_id', 'kp_code'], // 唯一键
+                    ['mastery_level', 'confidence_level', 'total_attempts', 'correct_attempts', 'mastery_trend', 'last_mastery_update', 'updated_at']
+                );
+        }
+
         // 【修复】计算并更新父节点掌握度,同时添加到返回数组中
-        $parentMasteryData = $this->updateParentMasteryLevels($studentId, array_keys($knowledgeMasteryVector));
+        $parentMasteryData = $this->updateParentMasteryLevels($studentId, $kpIds);
 
         // 合并父节点数据到返回数组
         $updatedMastery = array_merge($updatedMastery, $parentMasteryData);
@@ -1299,13 +1315,18 @@ class ExamAnswerAnalysisService
         }
     }
 
-    private function saveExamAnswerRecords(array $examData): void
+    /**
+     * 保存答题记录(优化版:批量INSERT)
+     * @param array $examData 考试数据
+     * @param array $questionMappings 已批量查询的知识点映射,避免N+1
+     */
+    private function saveExamAnswerRecords(array $examData, array $questionMappings = []): void
     {
         $studentId = $examData['student_id'];
         $examId = $examData['paper_id'];
+        $now = now();
 
         // 先清理该考试的所有答题记录(支持重复提交)
-        // delete() 方法即使没有匹配数据也不会报错,返回0
         try {
             DB::connection('mysql')->table('student_answer_questions')
                 ->where('student_id', $studentId)
@@ -1324,176 +1345,139 @@ class ExamAnswerAnalysisService
             ]);
         }
 
-        $stepsSavedCount = 0;
-        $questionsSavedCount = 0;
+        // 【优化】收集所有要插入的记录,最后批量INSERT
+        $stepsToInsert = [];
+        $questionsToInsert = [];
+        $mistakesToSave = []; // 收集错题记录
 
         foreach ($examData['questions'] as $question) {
-            // 兼容两种字段名:question_id 或 question_bank_id
             $questionId = $question['question_id'] ?? $question['question_bank_id'] ?? null;
             if (empty($questionId)) {
-                Log::warning('题目缺少ID,跳过保存', ['question' => $question]);
                 continue;
             }
 
-            // 从 is_correct 数组确定步骤数量
+            // 【优化】使用传入的映射,不再查询数据库
+            $kpMappings = $questionMappings[$questionId]['kp_mapping'] ?? [];
+
             $isCorrectArray = $question['is_correct'] ?? [];
             if (!is_array($isCorrectArray)) {
-                // 兼容非数组情况,转换为数组
                 $isCorrectArray = [$isCorrectArray ? 1 : 0];
             }
-
             $stepCount = count($isCorrectArray);
 
-            // 获取该题目关联的知识点(可能多个)
-            $kpMappings = $this->getQuestionKnowledgePointsFromDb($questionId);
-
             if ($stepCount > 1 || !empty($kpMappings)) {
-                // 多步骤题目:保存步骤级记录
-                // 每个步骤对应 is_correct 数组中的一个元素,先使用平均分来代替
+                // 多步骤题目:收集步骤记录
                 $scorePerStep = ($question['score'] ?? 0) / max($stepCount, 1);
-
-                // 【优化】先收集错误知识点,避免重复调用 saveMistakeRecord
                 $hasMistake = false;
                 $allErrorKpCodes = [];
 
                 foreach ($isCorrectArray as $stepIndex => $isCorrect) {
                     $isCorrectBool = (int) $isCorrect === 1;
 
-                    // 如果有知识点映射,为每个知识点保存记录
                     if (!empty($kpMappings)) {
                         foreach ($kpMappings as $kpMapping) {
-                            try {
-                                // 【修复】先检查记录是否已存在,避免重复键错误
-                                $exists = DB::connection('mysql')->table('student_answer_steps')
-                                    ->where('student_id', $studentId)
-                                    ->where('exam_id', $examId)
-                                    ->where('question_id', $questionId)
-                                    ->where('step_index', $stepIndex)
-                                    ->where('kp_id', $kpMapping['kp_id'])
-                                    ->exists();
-
-                                if (!$exists) {
-                                    DB::connection('mysql')->table('student_answer_steps')->insert([
-                                        'student_id' => $studentId,
-                                        'exam_id' => $examId,
-                                        'question_id' => $questionId,
-                                        'step_index' => $stepIndex,
-                                        'kp_id' => $kpMapping['kp_id'],
-                                        'is_correct' => $isCorrectBool ? 1 : 0,
-                                        'step_score' => $isCorrectBool ? $scorePerStep : 0,
-                                        'created_at' => now(),
-                                        'updated_at' => now(),
-                                    ]);
-                                    $stepsSavedCount++;
-                                    Log::debug('步骤记录保存成功', [
-                                        'student_id' => $studentId,
-                                        'question_id' => $questionId,
-                                        'step_index' => $stepIndex,
-                                        'kp_id' => $kpMapping['kp_id'],
-                                    ]);
-                                } else {
-                                    Log::debug('步骤记录已存在,跳过保存', [
-                                        'student_id' => $studentId,
-                                        'question_id' => $questionId,
-                                        'step_index' => $stepIndex,
-                                        'kp_id' => $kpMapping['kp_id'],
-                                    ]);
-                                }
-
-                                // 收集错误知识点(不重复调用 saveMistakeRecord)
-                                if (!$isCorrectBool) {
-                                    $hasMistake = true;
-                                    $allErrorKpCodes[] = $kpMapping['kp_id'];
-                                }
-                            } catch (\Exception $e) {
-                                Log::warning('保存步骤记录失败', [
-                                    'student_id' => $studentId,
-                                    'question_id' => $questionId,
-                                    'step_index' => $stepIndex,
-                                    'kp_id' => $kpMapping['kp_id'],
-                                    'error' => $e->getMessage(),
-                                ]);
-                            }
-                        }
-                    } else {
-                        // 没有知识点映射,仍保存步骤但 kp_id 为空
-                        try {
-                            DB::connection('mysql')->table('student_answer_steps')->insert([
+                            $stepsToInsert[] = [
                                 'student_id' => $studentId,
                                 'exam_id' => $examId,
                                 'question_id' => $questionId,
                                 'step_index' => $stepIndex,
-                                'kp_id' => null,
+                                'kp_id' => $kpMapping['kp_id'],
                                 'is_correct' => $isCorrectBool ? 1 : 0,
                                 'step_score' => $isCorrectBool ? $scorePerStep : 0,
-                                'created_at' => now(),
-                                'updated_at' => now(),
-                            ]);
-                            $stepsSavedCount++;
+                                'created_at' => $now,
+                                'updated_at' => $now,
+                            ];
 
-                            // 标记有错误(无知识点映射)
                             if (!$isCorrectBool) {
                                 $hasMistake = true;
+                                $allErrorKpCodes[] = $kpMapping['kp_id'];
                             }
-                        } catch (\Exception $e) {
-                            Log::warning('保存步骤记录失败(无知识点)', [
-                                'student_id' => $studentId,
-                                'question_id' => $questionId,
-                                'step_index' => $stepIndex,
-                                'error' => $e->getMessage(),
-                            ]);
+                        }
+                    } else {
+                        $stepsToInsert[] = [
+                            'student_id' => $studentId,
+                            'exam_id' => $examId,
+                            'question_id' => $questionId,
+                            'step_index' => $stepIndex,
+                            'kp_id' => null,
+                            'is_correct' => $isCorrectBool ? 1 : 0,
+                            'step_score' => $isCorrectBool ? $scorePerStep : 0,
+                            'created_at' => $now,
+                            'updated_at' => $now,
+                        ];
+
+                        if (!$isCorrectBool) {
+                            $hasMistake = true;
                         }
                     }
                 }
 
-                // 【错题本】一次性保存错题记录(合并所有错误知识点)
+                // 收集错题记录
                 if ($hasMistake) {
                     $uniqueKpCodes = array_values(array_unique($allErrorKpCodes));
-                    // 转换为 kpMapping 数组格式:[[kp_id: '...'], [kp_id: '...']]
-                    $kpMappingArray = array_map(fn($code) => ['kp_id' => $code], $uniqueKpCodes);
-                    $this->saveMistakeRecord($studentId, $questionId, $examId, $question, $kpMappingArray);
+                    $mistakesToSave[] = [
+                        'questionId' => $questionId,
+                        'question' => $question,
+                        'kpMappings' => array_map(fn($code) => ['kp_id' => $code], $uniqueKpCodes),
+                    ];
                 }
             } else {
-                // 单步骤题目:保存题目级记录
-                $isQuestionCorrect = (($question['score_obtained'] ?? 0) > 0);
-
-                try {
-                    DB::connection('mysql')->table('student_answer_questions')->insert([
-                        'student_id' => $studentId,
-                        'exam_id' => $examId,
-                        'question_id' => $questionId,
-                        'score_obtained' => $question['score_obtained'] ?? 0,
-                        'max_score' => $question['score'] ?? 0,
-                        'created_at' => now(),
-                        'updated_at' => now(),
-                    ]);
-                    $questionsSavedCount++;
+                // 单步骤题目:收集题目级记录
+                $questionsToInsert[] = [
+                    'student_id' => $studentId,
+                    'exam_id' => $examId,
+                    'question_id' => $questionId,
+                    'score_obtained' => $question['score_obtained'] ?? 0,
+                    'max_score' => $question['score'] ?? 0,
+                    'created_at' => $now,
+                    'updated_at' => $now,
+                ];
 
-                    // 【错题本】保存错题记录(题目级错误)
-                    if (!$isQuestionCorrect) {
-                        // 收集所有知识点,转换为数组格式
-                        $kpMappingArray = !empty($kpMappings)
+                // 收集错题记录
+                if (($question['score_obtained'] ?? 0) <= 0) {
+                    $mistakesToSave[] = [
+                        'questionId' => $questionId,
+                        'question' => $question,
+                        'kpMappings' => !empty($kpMappings)
                             ? array_map(fn($m) => ['kp_id' => $m['kp_id']], $kpMappings)
-                            : null;
-                        $this->saveMistakeRecord($studentId, $questionId, $examId, $question, $kpMappingArray);
-                    }
-                } catch (\Exception $e) {
-                    Log::warning('保存题目级记录失败', [
-                        'student_id' => $studentId,
-                        'exam_id' => $examId,
-                        'question_id' => $questionId,
-                        'error' => $e->getMessage(),
-                    ]);
+                            : null,
+                    ];
                 }
             }
         }
 
+        // 【优化】批量INSERT(每500条一批,避免超过MySQL限制)
+        try {
+            if (!empty($stepsToInsert)) {
+                foreach (array_chunk($stepsToInsert, 500) as $chunk) {
+                    DB::connection('mysql')->table('student_answer_steps')->insert($chunk);
+                }
+            }
+
+            if (!empty($questionsToInsert)) {
+                foreach (array_chunk($questionsToInsert, 500) as $chunk) {
+                    DB::connection('mysql')->table('student_answer_questions')->insert($chunk);
+                }
+            }
+        } catch (\Exception $e) {
+            Log::error('批量保存答题记录失败', [
+                'student_id' => $studentId,
+                'exam_id' => $examId,
+                'error' => $e->getMessage(),
+            ]);
+        }
+
+        // 保存错题记录(错题本逻辑相对复杂,保持单条处理)
+        foreach ($mistakesToSave as $mistake) {
+            $this->saveMistakeRecord($studentId, $mistake['questionId'], $examId, $mistake['question'], $mistake['kpMappings']);
+        }
+
         Log::info('答题记录保存完成', [
             'student_id' => $studentId,
             'exam_id' => $examId,
             'total_questions' => count($examData['questions']),
-            'steps_saved' => $stepsSavedCount,
-            'questions_saved' => $questionsSavedCount,
+            'steps_saved' => count($stepsToInsert),
+            'questions_saved' => count($questionsToInsert),
         ]);
     }