Просмотр исходного кода

fix(exam): 指定章节摸底支持重复与节点类型兜底

为章节摸底透传 chapter_id_list,在指定范围内按章节顺序选取有题章节并允许重复摸底;同时新增 section/subsection 到 chapter 的自动映射,降低前端传参层级不一致导致的失败风险。

Made-with: Cursor
yemeishu 1 месяц назад
Родитель
Сommit
341f6d4ee3
2 измененных файлов с 122 добавлено и 7 удалено
  1. 114 5
      app/Services/DiagnosticChapterService.php
  2. 8 2
      app/Services/ExamTypeStrategy.php

+ 114 - 5
app/Services/DiagnosticChapterService.php

@@ -307,14 +307,31 @@ class DiagnosticChapterService
     }
 
     /**
-     * 获取第一个未摸底的章节
-     * 用于章节摸底流程
+     * 获取章节摸底目标章节
+     * - 传入 targetChapterIds: 仅在指定章节范围内按顺序找“有题知识点”章节(允许重复摸底)
+     * - 未传 targetChapterIds: 保持原逻辑,找第一个未摸底章节
      */
-    public function getFirstUndiagnosedChapter(int $textbookId, int $studentId): ?array
+    public function getFirstUndiagnosedChapter(int $textbookId, int $studentId, ?array $targetChapterIds = null): ?array
     {
-        $chapters = TextbookCatalog::query()
+        $query = TextbookCatalog::query()
             ->where('textbook_id', $textbookId)
-            ->where('node_type', 'chapter')
+            ->where('node_type', 'chapter');
+
+        $targetChapterIds = is_array($targetChapterIds)
+            ? array_values(array_unique(array_filter(array_map('intval', $targetChapterIds), fn ($id) => $id > 0)))
+            : [];
+
+        // 兜底:允许传入 section/subsection 节点,自动映射到所属 chapter 节点
+        if (!empty($targetChapterIds)) {
+            $targetChapterIds = $this->normalizeToChapterIds($textbookId, $targetChapterIds);
+        }
+
+        $useTargetChapters = !empty($targetChapterIds);
+        if ($useTargetChapters) {
+            $query->whereIn('id', $targetChapterIds);
+        }
+
+        $chapters = $query
             ->orderBy('sort_order')
             ->orderBy('display_no')
             ->orderBy('id')
@@ -324,6 +341,42 @@ class DiagnosticChapterService
             return null;
         }
 
+        // 用户指定章节:按顺序找“有题知识点”的章节,不判断是否已摸底(允许重复摸底)
+        if ($useTargetChapters) {
+            foreach ($chapters as $chapter) {
+                $chapterData = $this->getChapterKnowledgePointsSimple($chapter->id);
+                $kpCodesWithQuestions = $this->filterKpCodesWithQuestions($chapterData['kp_codes']);
+
+                if (empty($kpCodesWithQuestions)) {
+                    continue;
+                }
+
+                Log::info('DiagnosticChapterService: 指定章节摸底命中章节(允许重复)', [
+                    'textbook_id' => $textbookId,
+                    'student_id' => $studentId,
+                    'target_chapter_ids' => $targetChapterIds,
+                    'chapter_id' => $chapter->id,
+                    'chapter_name' => $chapter->name ?? '',
+                    'kp_count' => count($kpCodesWithQuestions),
+                ]);
+
+                return [
+                    'chapter_id' => $chapter->id,
+                    'chapter_name' => $chapter->name ?? '',
+                    'section_ids' => $chapterData['section_ids'],
+                    'kp_codes' => $kpCodesWithQuestions,
+                ];
+            }
+
+            Log::warning('DiagnosticChapterService: 指定章节均无可用题目知识点', [
+                'textbook_id' => $textbookId,
+                'student_id' => $studentId,
+                'target_chapter_ids' => $targetChapterIds,
+            ]);
+
+            return null;
+        }
+
         foreach ($chapters as $chapter) {
             // 检查是否已摸底
             if (!$this->hasChapterDiagnostic($studentId, $chapter->id)) {
@@ -374,6 +427,62 @@ class DiagnosticChapterService
         ];
     }
 
+    /**
+     * 将传入节点ID(chapter/section/subsection)统一映射为所属 chapter ID 列表。
+     * 保持传入顺序去重,忽略不属于当前教材的节点。
+     *
+     * @param array<int> $nodeIds
+     * @return array<int>
+     */
+    private function normalizeToChapterIds(int $textbookId, array $nodeIds): array
+    {
+        if (empty($nodeIds)) {
+            return [];
+        }
+
+        $chapterIds = [];
+        $seen = [];
+
+        foreach ($nodeIds as $nodeId) {
+            $currentId = (int) $nodeId;
+            if ($currentId <= 0) {
+                continue;
+            }
+
+            $guard = 0;
+            while ($currentId > 0 && $guard++ < 10) {
+                $node = TextbookCatalog::query()
+                    ->where('id', $currentId)
+                    ->where('textbook_id', $textbookId)
+                    ->first(['id', 'parent_id', 'node_type']);
+
+                if (!$node) {
+                    break;
+                }
+
+                if ($node->node_type === 'chapter') {
+                    if (!isset($seen[$node->id])) {
+                        $seen[$node->id] = true;
+                        $chapterIds[] = (int) $node->id;
+                    }
+                    break;
+                }
+
+                $currentId = (int) ($node->parent_id ?? 0);
+            }
+        }
+
+        if (count($chapterIds) !== count($nodeIds)) {
+            Log::info('DiagnosticChapterService: 章节参数已自动映射到chapter节点', [
+                'textbook_id' => $textbookId,
+                'input_ids' => $nodeIds,
+                'resolved_chapter_ids' => $chapterIds,
+            ]);
+        }
+
+        return $chapterIds;
+    }
+
     /**
      * 获取当前应该学习的章节(第一个有未达标知识点的章节)
      * 用于智能组卷流程

+ 8 - 2
app/Services/ExamTypeStrategy.php

@@ -2029,8 +2029,14 @@ class ExamTypeStrategy
             return $this->buildGeneralParams($params);
         }
 
-        // 找第一个未摸底的章节
-        $chapterInfo = $diagnosticService->getFirstUndiagnosedChapter((int) $textbookId, $studentId);
+        // 找章节摸底目标:
+        // - 传了 chapter_id_list:按指定章节顺序找有题章节(允许重复摸底)
+        // - 未传:走默认未摸底优先逻辑
+        $chapterInfo = $diagnosticService->getFirstUndiagnosedChapter(
+            (int) $textbookId,
+            $studentId,
+            $params['chapter_id_list'] ?? null
+        );
 
         if (empty($chapterInfo) || empty($chapterInfo['kp_codes'])) {
             Log::warning('ExamTypeStrategy: 章节摸底未找到有效章节', [