浏览代码

fix: 增加新的智能组卷

yemeishu 1 周之前
父节点
当前提交
31f568da30

+ 1 - 1
app/Http/Controllers/Api/IntelligentExamController.php

@@ -82,7 +82,7 @@ class IntelligentExamController extends Controller
             'mistake_question_ids.*' => 'string',
             'callback_url' => 'nullable|url',  // 异步完成后推送通知的URL
             // 新增:组卷类型
-            'assemble_type' => 'nullable|integer|in:0,1,2,3,4,5,9',
+            'assemble_type' => 'nullable|integer|in:0,1,2,3,4,5,8,9',
             'exam_type' => 'nullable|string|in:general,diagnostic,practice,mistake,textbook,knowledge,knowledge_points',
             // 错题本类型专用参数
             'paper_ids' => 'nullable|array',

+ 87 - 7
app/Services/DiagnosticChapterService.php

@@ -10,6 +10,66 @@ use App\Models\TextbookChapterKnowledgeRelation;
 
 class DiagnosticChapterService
 {
+    public function getTextbookKnowledgePointsInOrder(int $textbookId): array
+    {
+        $chapters = TextbookCatalog::query()
+            ->where('textbook_id', $textbookId)
+            ->where('node_type', 'chapter')
+            ->orderBy('display_no')
+            ->orderBy('sort_order')
+            ->orderBy('id')
+            ->get();
+
+        if ($chapters->isEmpty()) {
+            Log::warning('DiagnosticChapterService: 未找到章节节点', [
+                'textbook_id' => $textbookId,
+            ]);
+            return [];
+        }
+
+        $orderedCodes = [];
+        $seen = [];
+
+        foreach ($chapters as $chapter) {
+            $sectionIds = TextbookCatalog::query()
+                ->where('parent_id', $chapter->id)
+                ->where('node_type', 'section')
+                ->orderBy('display_no')
+                ->orderBy('sort_order')
+                ->orderBy('id')
+                ->pluck('id')
+                ->toArray();
+
+            if (empty($sectionIds)) {
+                continue;
+            }
+
+            $kpCodes = TextbookChapterKnowledgeRelation::query()
+                ->whereIn('catalog_chapter_id', $sectionIds)
+                ->pluck('kp_code')
+                ->filter()
+                ->unique()
+                ->values()
+                ->toArray();
+
+            $kpCodes = $this->expandWithChildKnowledgePoints($kpCodes);
+
+            foreach ($kpCodes as $kpCode) {
+                if (isset($seen[$kpCode])) {
+                    continue;
+                }
+                $seen[$kpCode] = true;
+                $orderedCodes[] = $kpCode;
+            }
+        }
+
+        Log::info('DiagnosticChapterService: 获取教材知识点顺序列表', [
+            'textbook_id' => $textbookId,
+            'kp_count' => count($orderedCodes),
+        ]);
+
+        return $orderedCodes;
+    }
 
     public function getInitialChapterKnowledgePoints(int $textbookId): array
     {
@@ -151,15 +211,35 @@ class DiagnosticChapterService
             return [];
         }
 
-        $baseCodes = collect($kpCodes)->filter()->unique()->values()->all();
+        $baseCodes = collect($kpCodes)->filter()->values()->all();
         $children = KnowledgePoint::query()
             ->whereIn('parent_kp_code', $baseCodes)
-            ->pluck('kp_code')
-            ->filter()
-            ->unique()
-            ->values()
-            ->toArray();
+            ->orderBy('kp_code')
+            ->get(['parent_kp_code', 'kp_code']);
+
+        $childrenMap = [];
+        foreach ($children as $child) {
+            $childrenMap[$child->parent_kp_code][] = $child->kp_code;
+        }
+
+        $ordered = [];
+        $seen = [];
+
+        foreach ($baseCodes as $kpCode) {
+            if (!isset($seen[$kpCode])) {
+                $seen[$kpCode] = true;
+                $ordered[] = $kpCode;
+            }
+
+            foreach ($childrenMap[$kpCode] ?? [] as $childCode) {
+                if (isset($seen[$childCode])) {
+                    continue;
+                }
+                $seen[$childCode] = true;
+                $ordered[] = $childCode;
+            }
+        }
 
-        return array_values(array_unique(array_merge($baseCodes, $children)));
+        return $ordered;
     }
 }

+ 49 - 25
app/Services/ExamTypeStrategy.php

@@ -30,7 +30,7 @@ class ExamTypeStrategy
 
     /**
      * 根据组卷类型构建参数
-     * assembleType: 0-新摸底, 1-智能组卷, 2-知识点组卷, 3-教材组卷, 4-通用, 5-错题本, 9-原摸底
+     * assembleType: 0-新摸底, 8-智能组卷, 2-知识点组卷, 3-教材组卷, 4-通用, 5-错题本, 9-原摸底
      */
     public function buildParams(array $baseParams, int $assembleType): array
     {
@@ -41,11 +41,12 @@ class ExamTypeStrategy
 
         return match($assembleType) {
             0 => $this->applyDifficultyDistribution($this->buildInitialDiagnosticParams($baseParams)), // 新摸底
-            1 => $this->applyDifficultyDistribution($this->buildIntelligentAssembleParams($baseParams)), // 智能组卷
+            1 => $this->applyDifficultyDistribution($this->buildIntelligentAssembleParams($baseParams)), // 智能组卷(旧参数兼容)
             2 => $this->applyDifficultyDistribution($this->buildKnowledgePointAssembleParams($baseParams)), // 知识点组卷
             3 => $this->applyDifficultyDistribution($this->buildTextbookAssembleParams($baseParams)), // 教材组卷
             4 => $this->applyDifficultyDistribution($this->buildGeneralParams($baseParams)), // 通用
             5 => $this->applyDifficultyDistribution($this->buildMistakeParams($baseParams)), // 追练
+            8 => $this->applyDifficultyDistribution($this->buildIntelligentAssembleParams($baseParams)), // 智能组卷
             9 => $this->applyDifficultyDistribution($this->buildDiagnosticParams($baseParams)), // 原摸底
             default => $this->applyDifficultyDistribution($this->buildGeneralParams($baseParams))
         };
@@ -729,9 +730,8 @@ class ExamTypeStrategy
     }
 
     /**
-     * 智能组卷 (assembleType=1)
-     * 根据 textbook_id 查询章节,获取知识点,然后组卷
-     * 增加年级概念选题逻辑
+     * 智能组卷 (assembleType=8, 兼容旧 assembleType=1)
+     * 基于教材知识点顺序与学生掌握度,选择首个未达标知识点(或其列表)组卷
      */
     private function buildIntelligentAssembleParams(array $params): array
     {
@@ -740,47 +740,71 @@ class ExamTypeStrategy
         $textbookId = $params['textbook_id'] ?? null;
         $grade = $params['grade'] ?? null; // 年级信息
         $totalQuestions = $params['total_questions'] ?? 20;
+        $studentId = (int) ($params['student_id'] ?? 0);
 
         if (!$textbookId) {
             Log::warning('ExamTypeStrategy: 智能组卷需要 textbook_id 参数');
             return $this->buildGeneralParams($params);
         }
 
-        // 第一步:根据 textbook_id 查询章节
-        $catalogChapterIds = $this->getTextbookChapterIds($textbookId);
+        $diagnosticService = app(DiagnosticChapterService::class);
+        $textbookKpCodes = $diagnosticService->getTextbookKnowledgePointsInOrder((int) $textbookId);
 
-        if (empty($catalogChapterIds)) {
-            Log::warning('ExamTypeStrategy: 未找到课本章节', ['textbook_id' => $textbookId]);
+        if (empty($textbookKpCodes)) {
+            Log::warning('ExamTypeStrategy: 教材未找到知识点', ['textbook_id' => $textbookId]);
             return $this->buildGeneralParams($params);
         }
 
-        Log::info('ExamTypeStrategy: 获取到课本章节', [
-            'textbook_id' => $textbookId,
-            'chapter_count' => count($catalogChapterIds)
-        ]);
+        $masteryMap = [];
+        if ($studentId) {
+            $masteryMap = StudentKnowledgeMastery::query()
+                ->where('student_id', $studentId)
+                ->whereIn('kp_code', $textbookKpCodes)
+                ->pluck('mastery_level', 'kp_code')
+                ->toArray();
+        }
 
-        // 第二步:根据章节ID查询知识点关联
-        $kpCodes = $this->getKnowledgePointsFromChapters($catalogChapterIds, 25);
+        $matchedKpCodes = array_values(array_intersect($textbookKpCodes, array_keys($masteryMap)));
 
-        if (empty($kpCodes)) {
-            Log::warning('ExamTypeStrategy: 未找到知识点关联', [
-                'textbook_id' => $textbookId,
-                'chapter_ids' => $catalogChapterIds
-            ]);
-            return $this->buildGeneralParams($params);
+        if (empty($matchedKpCodes)) {
+            $initial = $diagnosticService->getInitialChapterKnowledgePoints((int) $textbookId);
+            $kpCodes = $initial['kp_codes'] ?? [];
+
+            if (empty($kpCodes)) {
+                Log::warning('ExamTypeStrategy: 智能组卷未找到首章知识点', [
+                    'textbook_id' => $textbookId
+                ]);
+                return $this->buildGeneralParams($params);
+            }
+        } else {
+            $kpCodes = [];
+            foreach ($textbookKpCodes as $kpCode) {
+                if (!isset($masteryMap[$kpCode])) {
+                    continue;
+                }
+
+                $level = (float) $masteryMap[$kpCode];
+                if ($level < 0.9) {
+                    $kpCodes[] = $kpCode;
+                }
+            }
+
+            if (empty($kpCodes)) {
+                $initial = $diagnosticService->getInitialChapterKnowledgePoints((int) $textbookId);
+                $kpCodes = $initial['kp_codes'] ?? [];
+            }
         }
 
         Log::info('ExamTypeStrategy: 获取到知识点', [
             'kp_count' => count($kpCodes),
-            'kp_codes' => array_slice($kpCodes, 0, 5) // 只记录前5个
+            'kp_codes' => array_slice($kpCodes, 0, 5)
         ]);
 
         // 组装增强参数
         $enhanced = array_merge($params, [
-            'kp_codes' => $kpCodes,
+            'kp_code_list' => $kpCodes,
             'textbook_id' => $textbookId,
             'grade' => $grade,
-            'catalog_chapter_ids' => $catalogChapterIds,
             'paper_name' => $params['paper_name'] ?? ('智能组卷_' . now()->format('Ymd_His')),
             // 智能组卷:平衡的题型和难度配比
             'question_type_ratio' => [
@@ -804,7 +828,7 @@ class ExamTypeStrategy
             'total_questions' => $totalQuestions
         ]);
 
-        return $enhanced;
+        return $this->buildKnowledgePointAssembleParams($enhanced);
     }
 
     /**