Parcourir la source

fix: paper_type = 5 追练逻辑增加

yemeishu il y a 1 semaine
Parent
commit
c4b586bf7f

+ 4 - 0
app/Http/Controllers/Api/IntelligentExamController.php

@@ -223,6 +223,10 @@ class IntelligentExamController extends Controller
                     ], 400);
                 }
 
+                if (isset($result['stats']['difficulty_category'])) {
+                    $difficultyCategory = $result['stats']['difficulty_category'];
+                }
+
                 $questions = $this->hydrateQuestions($result['questions'] ?? [], $data['kp_codes']);
             }
 

+ 189 - 49
app/Services/ExamTypeStrategy.php

@@ -45,7 +45,7 @@ class ExamTypeStrategy
             2 => $this->applyDifficultyDistribution($this->buildKnowledgePointAssembleParams($baseParams)), // 知识点组卷
             3 => $this->applyDifficultyDistribution($this->buildTextbookAssembleParams($baseParams)), // 教材组卷
             4 => $this->applyDifficultyDistribution($this->buildGeneralParams($baseParams)), // 通用
-            5 => $this->buildMistakeParams($baseParams), // 错题本不应用难度分布
+            5 => $this->applyDifficultyDistribution($this->buildMistakeParams($baseParams)), // 追练
             6 => $this->applyDifficultyDistribution($this->buildKnowledgePointsParams($baseParams)), // 按知识点组卷
             default => $this->applyDifficultyDistribution($this->buildGeneralParams($baseParams))
         };
@@ -93,17 +93,8 @@ class ExamTypeStrategy
      */
     private function applyDifficultyDistribution(array $params, bool $forceApply = false): array
     {
-        // 检查是否为排除类型(错题本 assembleType=5
+        // 统一应用难度分布(错题本类型也允许应用
         $assembleType = (int) ($params['assemble_type'] ?? 4);
-        $isExcludedType = ($assembleType === 5); // 只有错题本类型不应用难度分布
-
-        // 如果不是强制应用且为排除类型,则不应用难度分布
-        if (!$forceApply && $isExcludedType) {
-            Log::info('ExamTypeStrategy: 跳过难度分布(错题本类型)', [
-                'assemble_type' => $assembleType
-            ]);
-            return $params;
-        }
 
         $difficultyCategory = (int) ($params['difficulty_category'] ?? 1);
         $totalQuestions = (int) ($params['total_questions'] ?? 20);
@@ -274,90 +265,110 @@ class ExamTypeStrategy
     }
 
     /**
-     * 错题本 (assembleType=5)
-     * 根据 paper_ids 数组查询卷子中的错题,组合成新卷子
+     * 追练 (assembleType=5)
+     * 根据 paper_ids 获取卷子题目知识点列表,再按知识点组卷
      * 不需要 total_questions 参数
      */
     private function buildMistakeParams(array $params): array
     {
-        Log::info('ExamTypeStrategy: 构建错题本参数', $params);
+        Log::info('ExamTypeStrategy: 构建追练参数', $params);
 
         $paperIds = $params['paper_ids'] ?? [];
         $studentId = $params['student_id'] ?? null;
 
         // 检查是否有 paper_ids 参数
         if (empty($paperIds)) {
-            Log::warning('ExamTypeStrategy: 错题本需要 paper_ids 参数');
+            Log::warning('ExamTypeStrategy: 追练需要 paper_ids 参数');
             return $this->buildGeneralParams($params);
         }
 
-        Log::info('ExamTypeStrategy: 错题本组卷', [
+        Log::info('ExamTypeStrategy: 追练组卷', [
             'paper_ids' => $paperIds,
             'student_id' => $studentId,
             'paper_count' => count($paperIds)
         ]);
 
-        // 通过 paper_ids 查询卷子中的错题
-        $mistakeQuestionIds = $this->getMistakeQuestionsFromPapers($paperIds, $studentId);
+        // 通过 paper_ids 查询卷子中的全部题目
+        $paperQuestionIds = $this->getQuestionIdsFromPapers($paperIds);
 
-        if (empty($mistakeQuestionIds)) {
-            Log::warning('ExamTypeStrategy: 未找到错题', [
+        if (empty($paperQuestionIds)) {
+            Log::warning('ExamTypeStrategy: 卷子题目为空,无法生成追练', [
                 'paper_ids' => $paperIds
             ]);
             return $this->buildGeneralParams($params);
         }
 
-        Log::info('ExamTypeStrategy: 获取到错题', [
+        Log::info('ExamTypeStrategy: 获取到卷子题目', [
             'paper_count' => count($paperIds),
-            'mistake_count' => count($mistakeQuestionIds),
-            'mistake_question_ids' => array_slice($mistakeQuestionIds, 0, 10) // 只记录前10个
+            'question_count' => count($paperQuestionIds),
+            'question_ids' => array_slice($paperQuestionIds, 0, 10) // 只记录前10个
         ]);
 
-        // 获取错题知识点
-        $mistakeKnowledgePoints = $this->getKnowledgePointsFromQuestions($mistakeQuestionIds);
+        // 获取卷子题目知识点(直接从 paper_questions.knowledge_point)
+        $paperKnowledgePoints = $this->getKnowledgePointsFromPaperQuestions($paperIds);
 
-        // 组装增强参数
-        $mistakeCount = count($mistakeQuestionIds);
+        if (empty($paperKnowledgePoints)) {
+            Log::warning('ExamTypeStrategy: 卷子题目未找到知识点,无法生成错题本', [
+                'paper_ids' => $paperIds,
+                'question_count' => count($paperQuestionIds)
+            ]);
+            return $this->buildGeneralParams($params);
+        }
+
+        // 取卷子中最小难度等级、最大题量与最高总分作为默认
+        $paperStats = $this->getPaperAggregateStats($paperIds);
+        $difficultyCategory = $paperStats['difficulty_category_min'] ?? ($params['difficulty_category'] ?? 1);
+        $maxTotalQuestions = $paperStats['total_questions_max'] ?? null;
+        $maxTotalScore = $paperStats['total_score_max'] ?? null;
+
+        // 组装增强参数(复用知识点组卷逻辑)
+        $questionCount = count($paperQuestionIds);
         $maxQuestions = 50; // 错题本最大题目数限制
+            $targetQuestions = (int) ($maxTotalQuestions ?? $questionCount);
+            $targetQuestions = min($targetQuestions, $maxQuestions);
 
-        // 如果错题超过最大值,截取到最大值
-        if ($mistakeCount > $maxQuestions) {
+        // 如果题超过最大值,按上限截取
+        if ($questionCount > $maxQuestions) {
             Log::warning('ExamTypeStrategy: 错题数量超过最大值限制,已截取', [
-                'mistake_count' => $mistakeCount,
+                'question_count' => $questionCount,
                 'max_limit' => $maxQuestions,
                 'truncated_count' => $maxQuestions
             ]);
-            $mistakeQuestionIds = array_slice($mistakeQuestionIds, 0, $maxQuestions);
-            $mistakeCount = $maxQuestions;
+            $questionCount = $maxQuestions;
         }
 
         $enhanced = array_merge($params, [
-            'mistake_question_ids' => $mistakeQuestionIds,
+            'kp_code_list' => array_values($paperKnowledgePoints),
             'paper_ids' => $paperIds,
-            'priority_knowledge_points' => array_values($mistakeKnowledgePoints),
-            'paper_name' => $params['paper_name'] ?? ('错题本_' . now()->format('Ymd_His')),
-            'total_questions' => $mistakeCount, // 错题本题目数量由实际错题数量决定
-            // 错题本:保持原有题型配比
-            'question_type_ratio' => [
-                '选择题' => 35,
-                '填空题' => 30,
-                '解答题' => 35,
-            ],
-            // 错题本不应用难度分布
+            'paper_name' => $params['paper_name'] ?? ('追练_' . now()->format('Ymd_His')),
+            'total_questions' => $targetQuestions, // 题目数量由卷子题目规模/参数决定
+            'total_score' => $maxTotalScore ?? ($params['total_score'] ?? null),
+            'difficulty_category' => $difficultyCategory,
             'is_mistake_exam' => true,
             'is_paper_based_mistake' => true, // 标记是基于卷子的错题本
-            'mistake_count' => $mistakeCount,
-            'knowledge_points_count' => count($mistakeKnowledgePoints),
+            'source_paper_question_count' => $questionCount,
+            'knowledge_points_count' => count($paperKnowledgePoints),
             'max_questions_limit' => $maxQuestions,
         ]);
 
-        Log::info('ExamTypeStrategy: 错题本参数构建完成', [
+        Log::info('ExamTypeStrategy: 错题本参数构建完成(按卷子知识点)', [
             'paper_count' => count($paperIds),
-            'mistake_count' => count($mistakeQuestionIds),
-            'knowledge_points_count' => count($mistakeKnowledgePoints)
+            'question_count' => $questionCount,
+            'knowledge_points_count' => count($paperKnowledgePoints),
+            'total_questions' => $targetQuestions,
+            'difficulty_category' => $difficultyCategory,
+            'total_score' => $maxTotalScore
         ]);
 
-        return $enhanced;
+        Log::debug('ExamTypeStrategy: 错题本组卷关键参数', [
+            'paper_ids' => $paperIds,
+            'kp_code_list' => array_values($paperKnowledgePoints),
+            'difficulty_category' => $difficultyCategory,
+            'total_questions' => $targetQuestions,
+            'total_score' => $maxTotalScore
+        ]);
+
+        return $this->buildKnowledgePointAssembleParams($enhanced);
     }
 
     /**
@@ -1282,6 +1293,135 @@ class ExamTypeStrategy
         }
     }
 
+    /**
+     * 通过卷子ID列表获取卷子题目ID
+     */
+    private function getQuestionIdsFromPapers(array $paperIds): array
+    {
+        try {
+            $questionIds = DB::table('paper_questions')
+                ->whereIn('paper_id', $paperIds)
+                ->pluck('question_bank_id')
+                ->unique()
+                ->filter()
+                ->values()
+                ->toArray();
+
+            Log::debug('ExamTypeStrategy: 获取卷子题目ID', [
+                'paper_ids' => $paperIds,
+                'question_count' => count($questionIds)
+            ]);
+
+            return $questionIds;
+        } catch (\Exception $e) {
+            Log::error('ExamTypeStrategy: 获取卷子题目ID失败', [
+                'paper_ids' => $paperIds,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * 通过卷子ID列表获取题目知识点(直接使用 paper_questions.knowledge_point)
+     */
+    private function getKnowledgePointsFromPaperQuestions(array $paperIds): array
+    {
+        try {
+            $knowledgePoints = DB::table('paper_questions')
+                ->whereIn('paper_id', $paperIds)
+                ->whereNotNull('knowledge_point')
+                ->where('knowledge_point', '!=', '')
+                ->pluck('knowledge_point')
+                ->unique()
+                ->filter()
+                ->values()
+                ->toArray();
+
+            Log::debug('ExamTypeStrategy: 获取卷子题目知识点', [
+                'paper_ids' => $paperIds,
+                'kp_count' => count($knowledgePoints),
+            ]);
+
+            return $knowledgePoints;
+        } catch (\Exception $e) {
+            Log::error('ExamTypeStrategy: 获取卷子题目知识点失败', [
+                'paper_ids' => $paperIds,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * 通过卷子ID列表获取难度/题量/总分的聚合信息
+     */
+    private function getPaperAggregateStats(array $paperIds): array
+    {
+        try {
+            $rows = DB::table('papers')
+                ->whereIn('paper_id', $paperIds)
+                ->select(['paper_id', 'difficulty_category', 'total_questions', 'total_score'])
+                ->get();
+
+            $perPaper = [];
+            $difficultyValues = [];
+            $questionValues = [];
+            $scoreValues = [];
+            foreach ($rows as $row) {
+                $difficulty = is_null($row->difficulty_category) ? null : (int) $row->difficulty_category;
+                $totalQuestions = is_null($row->total_questions) ? null : (int) $row->total_questions;
+                $totalScore = is_null($row->total_score) ? null : (float) $row->total_score;
+
+                $perPaper[$row->paper_id] = [
+                    'difficulty_category' => $difficulty,
+                    'total_questions' => $totalQuestions,
+                    'total_score' => $totalScore,
+                    'raw_total_questions' => $row->total_questions,
+                ];
+
+                if (!is_null($difficulty)) {
+                    $difficultyValues[] = $difficulty;
+                }
+                if (!empty($totalQuestions)) {
+                    $questionValues[] = $totalQuestions;
+                }
+                if (!is_null($totalScore)) {
+                    $scoreValues[] = $totalScore;
+                }
+            }
+
+            $stats = [
+                'difficulty_category_min' => empty($difficultyValues) ? null : min($difficultyValues),
+                'total_questions_max' => empty($questionValues) ? null : max($questionValues),
+                'total_score_max' => empty($scoreValues) ? null : max($scoreValues),
+            ];
+
+            if (empty($stats['total_questions_max'])) {
+                $stats['total_questions_max'] = DB::table('paper_questions')
+                    ->whereIn('paper_id', $paperIds)
+                    ->selectRaw('paper_id, COUNT(*) as cnt')
+                    ->groupBy('paper_id')
+                    ->pluck('cnt')
+                    ->max();
+            }
+
+            Log::info('ExamTypeStrategy: 获取卷子聚合信息', [
+                'paper_ids' => $paperIds,
+                'per_paper' => $perPaper,
+                'stats' => $stats,
+            ]);
+
+            return $stats;
+        } catch (\Exception $e) {
+            Log::error('ExamTypeStrategy: 获取卷子聚合信息失败', [
+                'paper_ids' => $paperIds,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
     /**
      * 【新增】获取每个章节对应的知识点数量统计
      * 通过textbook_chapter_knowledge_relation表关联查询

+ 13 - 8
app/Services/LearningAnalyticsService.php

@@ -1381,6 +1381,8 @@ class LearningAnalyticsService
             // 3. 处理错题本逻辑
             $allQuestions = $priorityQuestions;
             $isMistakeBook = ($assembleType === 5); // 错题本类型
+            $hasMistakePriority = !empty($priorityQuestions);
+            $strictMistakeBook = $isMistakeBook && $hasMistakePriority;
 
             // 【修改】错题本类型严格按错题组卷,不补充题目
             if ($isMistakeBook) {
@@ -1391,18 +1393,20 @@ class LearningAnalyticsService
                     'action' => '只使用学生错题,不从原卷子补充'
                 ]);
 
-                // 如果完全没有错题,抛出异常
-                if (empty($priorityQuestions)) {
+                // 如果完全没有错题,回退到知识点/题库组卷
+                if (!$hasMistakePriority) {
                     Log::warning('LearningAnalyticsService: 错题本无错题', [
                         'student_id' => $studentId,
                         'paper_ids' => $params['paper_ids'] ?? []
                     ]);
-
-                    throw new \Exception('抱歉,您在这个卷子中没有错题记录,无法生成错题本。请确认卷子ID是否正确或联系管理员。');
+                    Log::info('LearningAnalyticsService: 错题本无错题,允许补充题目', [
+                        'assemble_type' => $assembleType,
+                        'action' => 'fallback_to_kp_or_bank'
+                    ]);
                 }
             }
 
-            if (!$isMistakeBook && count($priorityQuestions) < $totalQuestions) {
+            if (!$strictMistakeBook && count($priorityQuestions) < $totalQuestions) {
                 try {
                     // 【优化】教材出卷(assemble_type=3)保留知识点筛选,但额外使用章节筛选
                     if ($assembleType == 3) {
@@ -1468,7 +1472,7 @@ class LearningAnalyticsService
 
                     throw $e;
                 }
-            } elseif ($isMistakeBook) {
+            } elseif ($strictMistakeBook) {
                 // 错题本类型:不补充题目,只使用错题
                 Log::info('错题本类型:不补充题目,只使用错题', [
                     'assemble_type' => $assembleType,
@@ -1504,12 +1508,13 @@ class LearningAnalyticsService
 
             // 3. 根据掌握度对题目进行筛选和排序
             // 错题本类型:使用所有错题,但不超过最大值限制
-            $targetQuestionCount = $isMistakeBook ? min(count($allQuestions), $maxQuestions) : $totalQuestions;
+            $targetQuestionCount = $strictMistakeBook ? min(count($allQuestions), $maxQuestions) : $totalQuestions;
 
             Log::info('开始调用 selectQuestionsByMastery', [
                 'input_count' => count($allQuestions),
                 'target_count' => $targetQuestionCount,
                 'is_mistake_book' => $isMistakeBook,
+                'strict_mistake_book' => $strictMistakeBook,
                 'assemble_type' => $assembleType,
                 'total_questions_param' => $totalQuestions
             ]);
@@ -1545,7 +1550,7 @@ class LearningAnalyticsService
             // 【恢复】简化难度分布检查
             $difficultyCategory = $params['difficulty_category'] ?? 1;
             $enableDistribution = $params['enable_difficulty_distribution'] ?? false;
-            $isExcludedType = ($assembleType === 5); // 只有错题本类型(assembleType=5)不应用难度分布
+            $isExcludedType = false; // 统一允许应用难度分布(错题本类型也允许)
 
             // 【重要】添加详细的difficulty_category追踪日志
             Log::info('LearningAnalyticsService: 难度分布检查开始', [