Răsfoiți Sursa

智能出卷(摸底+智能组卷)

yemeishu 2 zile în urmă
părinte
comite
50f222b9ca
2 a modificat fișierele cu 91 adăugiri și 21 ștergeri
  1. 44 7
      app/Services/ExamTypeStrategy.php
  2. 47 14
      app/Services/LearningAnalyticsService.php

+ 44 - 7
app/Services/ExamTypeStrategy.php

@@ -293,8 +293,47 @@ class ExamTypeStrategy
     {
         Log::info('ExamTypeStrategy: 构建摸底测试参数', $params);
 
+        $textbookId = $params['textbook_id'] ?? null;
+        $grade = $params['grade'] ?? null;
+        $totalQuestions = $params['total_questions'] ?? 20;
+
+        if (!$textbookId) {
+            Log::warning('ExamTypeStrategy: 摸底测试需要 textbook_id 参数');
+            return $this->buildGeneralParams($params);
+        }
+
+        // 第一步:根据 textbook_id 查询章节
+        $catalogChapterIds = $this->getTextbookChapterIds($textbookId);
+
+        if (empty($catalogChapterIds)) {
+            Log::warning('ExamTypeStrategy: 未找到课本章节', ['textbook_id' => $textbookId]);
+            return $this->buildGeneralParams($params);
+        }
+
+        Log::info('ExamTypeStrategy: 获取到课本章节(摸底测试)', [
+            'textbook_id' => $textbookId,
+            'chapter_count' => count($catalogChapterIds)
+        ]);
+
+        // 第二步:根据章节ID查询知识点关联
+        $kpCodes = $this->getKnowledgePointsFromChapters($catalogChapterIds, 25);
+
+        if (empty($kpCodes)) {
+            Log::warning('ExamTypeStrategy: 未找到知识点关联', ['textbook_id' => $textbookId]);
+            return $this->buildGeneralParams($params);
+        }
+
+        Log::info('ExamTypeStrategy: 获取到知识点(摸底测试)', [
+            'kp_count' => count($kpCodes),
+            'kp_codes' => array_slice($kpCodes, 0, 5)
+        ]);
+
         // 摸底测试:平衡所有难度,覆盖多个知识点
         $enhanced = array_merge($params, [
+            'kp_codes' => $kpCodes,
+            'textbook_id' => $textbookId,
+            'grade' => $grade,
+            'catalog_chapter_ids' => $catalogChapterIds,
             // 难度配比:相对平衡,基础题稍多
             'difficulty_ratio' => [
                 '基础' => 40,
@@ -307,16 +346,14 @@ class ExamTypeStrategy
                 '填空题' => 25,
                 '解答题' => 25,
             ],
-            // 确保覆盖多个知识点
-            'kp_codes' => $this->expandKpCodesForDiagnostic($params['kp_codes'] ?? []),
-            // 摸底测试名称
+            'question_category' => 1, // question_category=1 代表摸底题目
             'paper_name' => $params['paper_name'] ?? ('摸底测试_' . now()->format('Ymd_His')),
         ]);
 
         Log::info('ExamTypeStrategy: 摸底测试参数构建完成', [
-            'difficulty_ratio' => $enhanced['difficulty_ratio'],
-            'question_type_ratio' => $enhanced['question_type_ratio'],
-            'kp_codes_count' => count($enhanced['kp_codes'])
+            'textbook_id' => $textbookId,
+            'kp_count' => count($kpCodes),
+            'total_questions' => $totalQuestions
         ]);
 
         return $enhanced;
@@ -782,7 +819,7 @@ class ExamTypeStrategy
                 '中等' => 50,
                 '拔高' => 25,
             ],
-            'question_category' => 1, // question_category=1 代表摸底题目
+            'question_category' => 0, // question_category=0 代表普通题目(智能组卷)
             'is_intelligent_assemble' => true,
         ]);
 

+ 47 - 14
app/Services/LearningAnalyticsService.php

@@ -1277,6 +1277,8 @@ class LearningAnalyticsService
                 '填空题' => 30,
                 '解答题' => 30,
             ];
+            // 新增:题目分类筛选
+            $questionCategory = $params['question_category'] ?? null;
             // 注意: difficulty_ratio 参数已废弃,使用 difficulty_category 控制难度分布
             $difficultyLevels = $params['difficulty_levels'] ?? [];
             // 如果用户没有选择任何难度,difficultyLevels 为空数组,表示随机难度
@@ -1288,6 +1290,7 @@ class LearningAnalyticsService
                 'skills' => $skills,
                 'assemble_type' => $assembleType,
                 'exam_type_legacy' => $examTypeLegacy,
+                'question_category' => $questionCategory,
             ]);
 
             // 1. 如果指定了学生,获取学生的薄弱点
@@ -1327,7 +1330,7 @@ class LearningAnalyticsService
                 ]);
 
                 // 获取学生错题的详细信息
-                $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, 200, $mistakeQuestionIds);
+                $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, 200, $mistakeQuestionIds, [], null);
 
                 Log::info('LearningAnalyticsService: 错题获取完成', [
                     'priority_questions_count' => count($priorityQuestions),
@@ -1358,7 +1361,7 @@ class LearningAnalyticsService
                         'assemble_type' => $assembleType
                     ]);
 
-                    $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, 200);
+                    $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, 200, [], [], $questionCategory);
                     $allQuestions = array_merge($priorityQuestions, $additionalQuestions);
 
                     Log::info('getQuestionsFromBank 调用完成', [
@@ -1519,7 +1522,7 @@ class LearningAnalyticsService
      * 从本地题库获取题目(错题回顾优先)
      * 支持优先获取指定题目ID的题目
      */
-    private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], int $totalNeeded = 100, array $priorityQuestionIds = []): array
+    private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], int $totalNeeded = 100, array $priorityQuestionIds = [], array $excludeQuestionIds = [], ?int $questionCategory = null): array
     {
         $startTime = microtime(true);
 
@@ -1571,6 +1574,18 @@ class LearningAnalyticsService
                 Log::info('应用技能筛选', ['skills' => $skills]);
             }
 
+            // 排除学生已做过的题目
+            if (!empty($excludeQuestionIds)) {
+                $query->whereNotIn('id', $excludeQuestionIds);
+                Log::info('应用排除筛选', ['exclude_count' => count($excludeQuestionIds)]);
+            }
+
+            // 按题目分类筛选(如果指定了 question_category)
+            if ($questionCategory !== null) {
+                $query->where('question_category', $questionCategory);
+                Log::info('应用题目分类筛选', ['question_category' => $questionCategory]);
+            }
+
             // 筛选有解题思路的题目
             $query->whereNotNull('solution')
                   ->where('solution', '!=', '')
@@ -1904,21 +1919,37 @@ class LearningAnalyticsService
             'avg_time_per_kp_ms' => count($kpCodes) > 0 ? round($totalWeightTime / count($kpCodes), 2) : 0
         ]);
 
-        // 3. 按权重分配题目数量
+        // 3. 按权重分配题目数量(修复:按权重排序取前N道题)
         $totalWeight = array_sum($kpWeights);
         $selectedQuestions = [];
 
+        // 将所有题目合并并添加权重信息
+        $weightedQuestions = [];
         foreach ($questionsByKp as $kpCode => $kpQuestions) {
-            // 计算该知识点应该选择的题目数
-            $kpQuestionCount = max(1, round(($totalQuestions * $kpWeights[$kpCode]) / $totalWeight));
+            $weight = $kpWeights[$kpCode];
+            foreach ($kpQuestions as $q) {
+                $q['kp_weight'] = $weight;
+                $q['kp_code'] = $kpCode;
+                $weightedQuestions[] = $q;
+            }
+        }
 
-            // 打乱题目顺序(避免固定模式)
-            shuffle($kpQuestions);
+        // 打乱题目顺序(避免固定模式)
+        shuffle($weightedQuestions);
 
-            // 选择题目
-            $selectedFromKp = array_slice($kpQuestions, 0, $kpQuestionCount);
-            $selectedQuestions = array_merge($selectedQuestions, $selectedFromKp);
-        }
+        // 按权重排序(权重高的在前)
+        usort($weightedQuestions, function ($a, $b) {
+            return ($b['kp_weight'] ?? 1.0) <=> ($a['kp_weight'] ?? 1.0);
+        });
+
+        // 选择前 N 道题
+        $selectedQuestions = array_slice($weightedQuestions, 0, $totalQuestions);
+
+        Log::info('知识点题目分配完成', [
+            'total_questions' => $totalQuestions,
+            'selected_count' => count($selectedQuestions),
+            'top_kp_distribution' => array_count_values(array_column($selectedQuestions, 'kp_code'))
+        ]);
 
         // 4. 如果题目过多,按权重排序后截取
         if (count($selectedQuestions) > $totalQuestions) {
@@ -2012,12 +2043,14 @@ class LearningAnalyticsService
             $buckets[$type][] = $q;
         }
 
-        // 计算目标数(四舍五入,比例>0 则至少 1 道),并校正总数
-        $targetCount = min($targetCount, count($questions));
+        // 计算目标数(四舍五入,比例>0 则至少 1 道)
+        // 修复:不自动减少目标数量,确保达到用户要求
+        $availableCount = count($questions);
         $targets = $this->computeTypeTargets($targetCount, $typeRatio);
 
         Log::info('题型配比调整前桶统计', [
             'target_count' => $targetCount,
+            'available_count' => $availableCount,
             'targets' => $targets,
             'bucket_counts' => [
                 'choice' => count($buckets['choice']),