ソースを参照

fix: 优化计算知识点掌握度的算法,优化兼容题目难度可为空

过卫栋 3 週間 前
コミット
e23ab4ef8b

+ 3 - 3
app/Filament/Pages/ExamHistory.php

@@ -447,9 +447,9 @@ class ExamHistory extends Page
                 'knowledge_point' => $question->kp_code,
                 'question_type' => $this->getQuestionTypeFromQuestion($question),
                 'question_text' => $question->stem,
-                'difficulty' => $question->difficulty,
-                'score' => $this->calculateScore($question->difficulty),
-                'estimated_time' => $this->calculateEstimatedTime($question->difficulty),
+                'difficulty' => $question->difficulty ?? 0.5,
+                'score' => $this->calculateScore($question->difficulty ?? 0.5),
+                'estimated_time' => $this->calculateEstimatedTime($question->difficulty ?? 0.5),
                 'question_number' => $maxQuestionNumber + 1,
             ]);
 

+ 6 - 0
app/Models/Question.php

@@ -43,6 +43,9 @@ class Question extends Model
      */
     public function getDifficultyLabelAttribute(): string
     {
+        if ($this->difficulty === null) {
+            return '未知';
+        }
         $normalized = (float) $this->difficulty;
         if ($normalized > 1) {
             $normalized = $normalized / 5;
@@ -60,6 +63,9 @@ class Question extends Model
      */
     public function getDifficultyColorAttribute(): string
     {
+        if ($this->difficulty === null) {
+            return 'gray';  // 未知难度用灰色
+        }
         $normalized = (float) $this->difficulty;
         if ($normalized > 1) {
             $normalized = $normalized / 5;

+ 10 - 6
app/Services/LearningAnalyticsService.php

@@ -1747,7 +1747,7 @@ class LearningAnalyticsService
                     'question_code' => $q->question_code,
                     'kp_code' => $q->kp_code,
                     'question_type' => $q->question_type,
-                    'difficulty' => (float) $q->difficulty,
+                    'difficulty' => $q->difficulty !== null ? (float) $q->difficulty : 0.5,
                     'stem' => $q->stem,
                     'solution' => $q->solution,
                     'metadata' => [
@@ -1755,7 +1755,7 @@ class LearningAnalyticsService
                         'is_choice' => $q->question_type === 'choice',
                         'is_fill' => $q->question_type === 'fill',
                         'is_answer' => $q->question_type === 'answer',
-                        'difficulty_label' => $this->getDifficultyLabel($q->difficulty),
+                        'difficulty_label' => $this->getDifficultyLabel($q->difficulty ?? 0.5),
                         'question_type_label' => $this->getQuestionTypeLabel($q->question_type)
                     ]
                 ];
@@ -1920,8 +1920,11 @@ class LearningAnalyticsService
     /**
      * 获取难度标签
      */
-    private function getDifficultyLabel(float $difficulty): string
+    private function getDifficultyLabel(?float $difficulty): string
     {
+        if ($difficulty === null) {
+            return '未知';
+        }
         return match (true) {
             $difficulty < 0.4 => '基础',
             $difficulty < 0.7 => '中等',
@@ -3018,7 +3021,7 @@ class LearningAnalyticsService
                     'question_bank_id' => $question->id,
                     'question_type' => $questionType,
                     'kp_code' => $question->kp_code,
-                    'difficulty' => (float) $question->difficulty,
+                    'difficulty' => $question->difficulty !== null ? (float) $question->difficulty : 0.5,
                     'stem' => $stem,
                     'options' => $options,
                     'correct_answer' => $correctAnswer,
@@ -3027,7 +3030,7 @@ class LearningAnalyticsService
                     'estimated_time' => $question->estimated_time ?? 300,
                     'metadata' => [
                         'question_type_label' => $questionType === 'choice' ? '选择题' : ($questionType === 'fill' ? '填空题' : '解答题'),
-                        'difficulty_label' => $this->getDifficultyLabelFromObject($question->difficulty),
+                        'difficulty_label' => $this->getDifficultyLabelFromObject($question->difficulty ?? 0.5),
                         'is_supplementary' => true
                     ]
                 ];
@@ -3131,8 +3134,9 @@ class LearningAnalyticsService
     /**
      * 【新增】获取难度标签(重载版本)
      */
-    private function getDifficultyLabelFromObject(float $difficulty): string
+    private function getDifficultyLabelFromObject(?float $difficulty): string
     {
+        if ($difficulty === null) return '未知';
         if ($difficulty < 0.3) return '基础';
         if ($difficulty < 0.7) return '中等';
         return '拔高';

+ 116 - 12
app/Services/MasteryCalculator.php

@@ -562,9 +562,13 @@ class MasteryCalculator
 
     /**
      * 【增强】获取学生所有知识点的掌握度概览(支持父节点计算)
+     * 【优化】预加载所有数据到内存,避免 N+1 查询问题
      */
     public function getStudentMasteryOverviewWithHierarchy(string $studentId): array
     {
+        $startTime = microtime(true);
+
+        // 1. 一次性查询学生所有知识点的掌握度
         $masteryList = DB::table('student_knowledge_mastery')
             ->where('student_id', $studentId)
             ->get();
@@ -578,14 +582,20 @@ class MasteryCalculator
                 'weak_knowledge_points' => 0,
                 'weak_knowledge_points_list' => [],
                 'details' => [],
-                'parent_mastery_levels' => [], // 新增:父节点掌握度
+                'parent_mastery_levels' => [],
             ];
         }
 
         $masteryArray = $masteryList->toArray();
 
+        // 构建掌握度映射表(kp_code => mastery_level)
+        $masteryMap = [];
+        foreach ($masteryArray as $item) {
+            $masteryMap[$item->kp_code] = floatval($item->mastery_level);
+        }
+
         $total = count($masteryArray);
-        $average = $masteryArray ? array_sum(array_column($masteryArray, 'mastery_level')) / $total : 0;
+        $average = array_sum($masteryMap) / $total;
 
         $mastered = [];
         $good = [];
@@ -602,19 +612,55 @@ class MasteryCalculator
             }
         }
 
-        // 【新功能】计算父节点掌握度
-        $parentMasteryLevels = [];
-        $parentKpCodes = DB::table('knowledge_points')
+        // 2. 一次性查询所有知识点的层级关系
+        $allKpRelations = DB::table('knowledge_points')
             ->whereNotNull('parent_kp_code')
-            ->distinct()
-            ->pluck('parent_kp_code')
-            ->toArray();
+            ->select('kp_code', 'parent_kp_code')
+            ->get();
+
+        // 构建父子关系映射(parent_kp_code => [child_kp_codes])
+        $childrenMap = [];
+        $allParentKpCodes = [];
+        foreach ($allKpRelations as $relation) {
+            $parentCode = $relation->parent_kp_code;
+            $childCode = $relation->kp_code;
 
-        foreach ($parentKpCodes as $parentKpCode) {
-            $parentMastery = $this->calculateParentMastery($studentId, $parentKpCode);
-            $parentMasteryLevels[$parentKpCode] = $parentMastery;
+            if (!isset($childrenMap[$parentCode])) {
+                $childrenMap[$parentCode] = [];
+            }
+            $childrenMap[$parentCode][] = $childCode;
+            $allParentKpCodes[$parentCode] = true;
         }
 
+        Log::debug('MasteryCalculator: 预加载数据完成', [
+            'student_id' => $studentId,
+            'mastery_count' => count($masteryMap),
+            'parent_count' => count($allParentKpCodes),
+            'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+        ]);
+
+        // 3. 在内存中计算所有父节点的掌握度(不再查询数据库)
+        $parentMasteryLevels = [];
+        foreach (array_keys($allParentKpCodes) as $parentKpCode) {
+            $parentMastery = $this->calculateParentMasteryInMemory(
+                $parentKpCode,
+                $childrenMap,
+                $masteryMap,
+                1,
+                3
+            );
+            if ($parentMastery > 0) {
+                $parentMasteryLevels[$parentKpCode] = $parentMastery;
+            }
+        }
+
+        Log::info('MasteryCalculator: getStudentMasteryOverviewWithHierarchy 完成', [
+            'student_id' => $studentId,
+            'total_kp' => $total,
+            'parent_mastery_count' => count($parentMasteryLevels),
+            'total_time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+        ]);
+
         return [
             'total_knowledge_points' => $total,
             'average_mastery_level' => round($average, 4),
@@ -623,7 +669,65 @@ class MasteryCalculator
             'weak_knowledge_points' => count($weak),
             'weak_knowledge_points_list' => $weak,
             'details' => $masteryArray,
-            'parent_mastery_levels' => $parentMasteryLevels, // 新增:父节点掌握度
+            'parent_mastery_levels' => $parentMasteryLevels,
         ];
     }
+
+    /**
+     * 【优化】在内存中递归计算父节点掌握度(不查询数据库)
+     *
+     * @param string $parentKpCode 父节点编码
+     * @param array $childrenMap 父子关系映射(parent => [children])
+     * @param array $masteryMap 掌握度映射(kp_code => mastery_level)
+     * @param int $currentDepth 当前递归深度
+     * @param int $maxDepth 最大递归深度
+     * @return float 父节点掌握度
+     */
+    private function calculateParentMasteryInMemory(
+        string $parentKpCode,
+        array $childrenMap,
+        array $masteryMap,
+        int $currentDepth,
+        int $maxDepth
+    ): float {
+        // 防止无限递归
+        if ($currentDepth > $maxDepth) {
+            return 0.0;
+        }
+
+        // 获取子节点
+        $childKpCodes = $childrenMap[$parentKpCode] ?? [];
+        if (empty($childKpCodes)) {
+            return 0.0;
+        }
+
+        $masteryLevels = [];
+        foreach ($childKpCodes as $childKpCode) {
+            // 如果子节点也是父节点,递归计算
+            if (isset($childrenMap[$childKpCode])) {
+                $childMastery = $this->calculateParentMasteryInMemory(
+                    $childKpCode,
+                    $childrenMap,
+                    $masteryMap,
+                    $currentDepth + 1,
+                    $maxDepth
+                );
+                if ($childMastery > 0) {
+                    $masteryLevels[] = $childMastery;
+                }
+            }
+
+            // 如果子节点有掌握度数据,使用它
+            if (isset($masteryMap[$childKpCode])) {
+                $masteryLevels[] = $masteryMap[$childKpCode];
+            }
+        }
+
+        if (empty($masteryLevels)) {
+            return 0.0;
+        }
+
+        // 计算平均值
+        return round(array_sum($masteryLevels) / count($masteryLevels), 4);
+    }
 }