Эх сурвалжийг харах

feat: unify intelligent exam default to 10 questions

yemeishu 1 сар өмнө
parent
commit
21c1a94543

+ 6 - 7
app/Filament/Pages/IntelligentExamGeneration.php

@@ -34,7 +34,7 @@ class IntelligentExamGeneration extends Page
     public ?string $paperDescription = '';
     public ?string $selectedGrade = '初中'; // 初中/七年级/八年级/九年级
     public ?string $difficultyCategory = '基础'; // 基础/进阶/竞赛
-    public int $totalQuestions = 20;
+    public int $totalQuestions = 10;
     public int $totalScore = 100;
     public bool $includeAnswer = false; // 是否生成答案(默认不生成参考答案)
 
@@ -83,6 +83,8 @@ class IntelligentExamGeneration extends Page
         // 初始化用户角色检查
         $this->initializeUserRole();
 
+        $this->totalQuestions = (int) config('question_bank.default_total_questions');
+
         // 如果是老师,自动选择当前老师
         if ($this->isTeacher) {
             $teacherId = $this->getCurrentTeacherId();
@@ -561,16 +563,13 @@ class IntelligentExamGeneration extends Page
         \Illuminate\Support\Facades\Log::info('generateExam called with studentId=' . ($this->selectedStudentId ?? 'null'));
         $this->validate([
             // 'paperName' => 'required|string|max:255', // 已移除必填
-            'totalQuestions' => 'required|integer|min:6|max:100',
+            'totalQuestions' => 'required|integer|in:'.(int) config('question_bank.default_total_questions'),
             'selectedTeacherId' => 'nullable|string', // 可选老师
             'selectedStudentId' => 'nullable|string', // 可选学生
         ]);
 
-        // 确保题目数量至少6题
-        if ($this->totalQuestions < 6) {
-            \Illuminate\Support\Facades\Log::warning('题目数量少于6题,已自动调整为6题', ['original' => $this->totalQuestions]);
-            $this->totalQuestions = 6;
-        }
+        // 管理后台与 API 保持一致:题量固定为配置值(默认10题)
+        $this->totalQuestions = (int) config('question_bank.default_total_questions');
 
         if (empty($this->selectedTeacherId) || empty($this->selectedStudentId)) {
             Notification::make()

+ 10 - 54
app/Http/Controllers/Api/IntelligentExamController.php

@@ -67,7 +67,6 @@ class IntelligentExamController extends Controller
             'grade' => 'required|integer|min:1|max:12',  // 支持小学1-6、初中7-9、高中10-12
             'student_name' => 'required|string|max:50',
             'teacher_name' => 'required|string|max:50',
-            'total_questions' => 'nullable|integer|min:1|max:100',
             'difficulty_category' => 'nullable|integer|in:0,1,2,3,4',
             'kp_codes' => 'nullable|array',
             'kp_codes.*' => 'string',
@@ -126,11 +125,8 @@ class IntelligentExamController extends Controller
 
         $data = $validator->validated();
         $assembleType = (int) ($data['assemble_type'] ?? 4);
-        if (in_array($assembleType, [1, 8])) {
-            $data['total_questions'] = 10;
-        } else {
-            $data['total_questions'] = $data['total_questions'] ?? 20;
-        }
+        // API 固定题量:含追练(assemble_type=5),一律 default_total_questions,不使用请求题量参数
+        $data['total_questions'] = (int) config('question_bank.default_total_questions');
         $this->ensureStudentTeacherRelation($data);
 
         // 【修改】使用series_id、semester_code和grade获取textbook_id
@@ -152,7 +148,7 @@ class IntelligentExamController extends Controller
         $mistakeIds = $data['mistake_ids'] ?? [];
         $mistakeQuestionIds = $data['mistake_question_ids'] ?? [];
         $paperIds = $data['paper_ids'] ?? [];
-        $assembleType = $data['assemble_type'] ?? 4; // 默认为通用类型(4)
+        $assembleType = (int) ($data['assemble_type'] ?? 4); // 默认为通用类型(4)
 
         try {
             $questions = [];
@@ -249,32 +245,16 @@ class IntelligentExamController extends Controller
                 ], 400);
             }
 
-            // 错题本类型不需要限制题目数量,由错题数量决定
-            if ($assembleType === 5) {
-                // 错题本:使用所有错题,不限制数量
-                Log::info('错题本类型,使用所有错题', [
-                    'assemble_type' => $assembleType,
-                    'question_count' => count($questions),
-                ]);
-            } else {
-                // 其他类型:限制题目数量
-                $totalQuestions = min($data['total_questions'], count($questions));
-                $questions = array_slice($questions, 0, $totalQuestions);
-            }
+            // 含追练在内:统一截断为 default_total_questions(已写入 $data['total_questions'])
+            $totalQuestions = min($data['total_questions'], count($questions));
+            $questions = array_slice($questions, 0, $totalQuestions);
 
             // 每个题型内按难度升序排序(由易到难),并重排题号
             $questions = $this->sortQuestionsWithinTypeByDifficulty($questions);
 
-            // 调整题目分值
-            if (($data['total_questions'] ?? 20) == 20) {
-                // 20题:沿用动态凑整算法,目标总分100
-                $targetTotalScore = $data['total_score'] ?? 100.0;
-                $questions = $this->adjustQuestionScores($questions, $targetTotalScore);
-            } else {
-                // 非20题:固定题型分值(选择5、填空5、解答10)
-                $questions = $this->applyFixedScores($questions);
-                $targetTotalScore = array_sum(array_column($questions, 'score'));
-            }
+            // 调整题目分值:统一按目标总分凑整(默认100),不再按20题特殊分支处理
+            $targetTotalScore = (float) ($data['total_score'] ?? 100.0);
+            $questions = $this->adjustQuestionScores($questions, $targetTotalScore);
 
             // 计算总分
             $totalScore = array_sum(array_column($questions, 'score'));
@@ -540,11 +520,7 @@ class IntelligentExamController extends Controller
      */
     private function normalizePayload(array $payload): array
     {
-        // 处理 question_count 参数:转换为 total_questions
-        if (isset($payload['question_count']) && ! isset($payload['total_questions'])) {
-            $payload['total_questions'] = $payload['question_count'];
-            unset($payload['question_count']);
-        }
+        unset($payload['total_questions'], $payload['question_count']);
 
         // 将student_id转换为字符串(支持数字和字符串输入)
         if (isset($payload['student_id'])) {
@@ -884,26 +860,6 @@ class IntelligentExamController extends Controller
         };
     }
 
-    /**
-     * 非20题时使用固定题型分值
-     * 选择题:5分,填空题:5分,解答题:10分
-     */
-    private function applyFixedScores(array $questions): array
-    {
-        foreach ($questions as &$question) {
-            $type = $this->normalizeQuestionType($question['question_type'] ?? 'answer');
-            $question['score'] = match ($type) {
-                'choice' => 5,
-                'fill' => 5,
-                'answer' => 10,
-                default => 5,
-            };
-        }
-        unset($question);
-
-        return $questions;
-    }
-
     /**
      * 计算试卷总分并调整各题目分值,确保总分接近目标分数
      * 符合中国中学卷子标准:

+ 1 - 2
app/Services/ApiDocumentation.php

@@ -267,7 +267,7 @@ class ApiDocumentation
             'api/intelligent-exams' => [
                 'POST' => [
                     'summary' => '智能出卷',
-                    'description' => '根据学生情况智能生成个性化试卷,返回 PDF 和判卷地址',
+                    'description' => '根据学生情况智能生成个性化试卷,返回 PDF 和判卷地址(题量固定为系统配置,默认10题)',
                     'params' => [
                         'body' => [
                             ['name' => 'student_id', 'type' => 'integer', 'required' => true, 'description' => '学生 ID(数字)'],
@@ -277,7 +277,6 @@ class ApiDocumentation
                             ['name' => 'grade', 'type' => 'string', 'required' => true, 'description' => '年级(7/8/9)'],
                             ['name' => 'knowledge_points', 'type' => 'array', 'required' => false, 'description' => '指定知识点列表'],
                             ['name' => 'difficulty', 'type' => 'string', 'required' => false, 'description' => '难度偏好:easy/medium/hard/adaptive'],
-                            ['name' => 'question_count', 'type' => 'integer', 'required' => false, 'default' => '20', 'description' => '题目数量'],
                         ],
                     ],
                     'response' => [

+ 20 - 15
app/Services/ExamTypeStrategy.php

@@ -28,6 +28,11 @@ class ExamTypeStrategy
         $this->difficultyDistributionService = $difficultyDistributionService ?? app(DifficultyDistributionService::class);
     }
 
+    private static function defaultTotalQuestions(): int
+    {
+        return (int) config('question_bank.default_total_questions');
+    }
+
     /**
      * 根据组卷类型构建参数
      * assembleType: 0-章节摸底, 1-智能组卷, 2-知识点组卷, 3-教材组卷, 4-通用, 5-错题本, 8-智能组卷(新), 9-原摸底
@@ -108,7 +113,7 @@ class ExamTypeStrategy
         $assembleType = (int) ($params['assemble_type'] ?? 4);
 
         $difficultyCategory = (int) ($params['difficulty_category'] ?? 1);
-        $totalQuestions = (int) ($params['total_questions'] ?? 20);
+        $totalQuestions = (int) ($params['total_questions'] ?? self::defaultTotalQuestions());
 
         Log::debug('ExamTypeStrategy: 应用难度系数分布', [
             'difficulty_category' => $difficultyCategory,
@@ -209,7 +214,7 @@ class ExamTypeStrategy
 
         $textbookId = $params['textbook_id'] ?? null;
         $grade = $params['grade'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
         $endCatalogId = $params['end_catalog_id'] ?? null; // 截止章节ID
 
         if (!$textbookId) {
@@ -284,7 +289,7 @@ class ExamTypeStrategy
         $diagnosticService = app(DiagnosticChapterService::class);
         $textbookId = $params['textbook_id'] ?? null;
         $grade = $params['grade'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
 
         Log::info('ExamTypeStrategy: 新摸底使用textbook_id', [
             'textbook_id' => $textbookId,
@@ -403,11 +408,7 @@ class ExamTypeStrategy
 
         // 组装增强参数(复用知识点组卷逻辑)
         $questionCount = count($paperQuestionIds);
-        $maxQuestions = 50; // 错题本最大题目数限制
-            $targetQuestions = (int) ($maxTotalQuestions ?? $questionCount);
-            $targetQuestions = min($targetQuestions, $maxQuestions);
-
-        // 如果题量超过最大值,按上限截取
+        $maxQuestions = 50; // 源侧题量上限,避免一次拉过多
         if ($questionCount > $maxQuestions) {
             Log::warning('ExamTypeStrategy: 错题数量超过最大值限制,已截取', [
                 'question_count' => $questionCount,
@@ -417,11 +418,15 @@ class ExamTypeStrategy
             $questionCount = $maxQuestions;
         }
 
+        $requested = (int) ($params['total_questions'] ?? self::defaultTotalQuestions());
+        $fromPaper = (int) ($maxTotalQuestions ?? $questionCount);
+        $targetQuestions = min($requested, $fromPaper, $maxQuestions);
+
         $enhanced = array_merge($params, [
             'kp_code_list' => array_values($paperKnowledgePoints),
             'paper_ids' => $paperIds,
             'paper_name' => $params['paper_name'] ?? ('追练_' . now()->format('Ymd_His')),
-            'total_questions' => $targetQuestions, // 题目数量由卷子题目规模/参数决定
+            'total_questions' => $targetQuestions, // 与 API/默认题量对齐,不超过源卷子可支撑题量
             'total_score' => $maxTotalScore ?? ($params['total_score'] ?? null),
             'difficulty_category' => $difficultyCategory,
             'is_mistake_exam' => true,
@@ -663,7 +668,7 @@ class ExamTypeStrategy
         Log::info('ExamTypeStrategy: 构建按知识点组卷参数', $params);
 
         $studentId = $params['student_id'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
         $knowledgePointsOptions = $params['knowledge_points_options'] ?? [];
         $weaknessThreshold = $knowledgePointsOptions['weakness_threshold'] ?? 0.7;
         $focusWeaknesses = $knowledgePointsOptions['focus_weaknesses'] ?? true;
@@ -846,7 +851,7 @@ class ExamTypeStrategy
 
         $textbookId = $params['textbook_id'] ?? null;
         $grade = $params['grade'] ?? null; // 年级信息
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
         $studentId = (int) ($params['student_id'] ?? 0);
 
         if (!$textbookId) {
@@ -961,7 +966,7 @@ class ExamTypeStrategy
         }
         $kpCodeList = array_values(array_unique(array_filter($kpCodeList)));
         $studentId = $params['student_id'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
         $assembleType = (int) ($params['assemble_type'] ?? 4);
 
         if (empty($kpCodeList)) {
@@ -1145,7 +1150,7 @@ class ExamTypeStrategy
 
         $chapterIdList = $params['chapter_id_list'] ?? [];
         $studentId = $params['student_id'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
 
         // 【优化】如果用户没有指定章节,自动从教材所有有题目的章节中选择
         if (empty($chapterIdList) && !empty($params['textbook_id'])) {
@@ -2012,7 +2017,7 @@ class ExamTypeStrategy
         $textbookId = $params['textbook_id'] ?? null;
         $studentId = (int) ($params['student_id'] ?? 0);
         $grade = $params['grade'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
 
         Log::info('ExamTypeStrategy: 构建章节摸底参数', [
             'textbook_id' => $textbookId,
@@ -2072,7 +2077,7 @@ class ExamTypeStrategy
         $textbookId = $params['textbook_id'] ?? null;
         $studentId = (int) ($params['student_id'] ?? 0);
         $grade = $params['grade'] ?? null;
-        $totalQuestions = $params['total_questions'] ?? 20;
+        $totalQuestions = $params['total_questions'] ?? self::defaultTotalQuestions();
 
         Log::info('ExamTypeStrategy: 构建按知识点顺序学习参数', [
             'textbook_id' => $textbookId,

+ 3 - 4
app/Services/LearningAnalyticsService.php

@@ -1269,7 +1269,7 @@ class LearningAnalyticsService
 
             $studentId = $params['student_id'] ?? null;
             $grade = $params['grade'] ?? null; // 用户选择的年级
-            $totalQuestions = $params['total_questions'] ?? 20;
+            $totalQuestions = $params['total_questions'] ?? (int) config('question_bank.default_total_questions');
 
             // 【修复】参数映射:支持 kp_codes 和 kp_code_list 两种参数名
             $kpCodes = $params['kp_codes'] ?? $params['kp_code_list'] ?? [];
@@ -1520,9 +1520,8 @@ class LearningAnalyticsService
                 ];
             }
 
-            // 3. 根据掌握度对题目进行筛选和排序
-            // 错题本类型:使用所有错题,但不超过最大值限制
-            $targetQuestionCount = $strictMistakeBook ? min(count($allQuestions), $maxQuestions) : $totalQuestions;
+            // 3. 根据掌握度对题目进行筛选和排序(含追练:题量与 total_questions / default_total_questions 一致,不再按 50 拉满)
+            $targetQuestionCount = min(count($allQuestions), $totalQuestions);
 
             $startTime = microtime(true);
             $selectedQuestions = $this->selectQuestionsByMastery(

+ 1 - 3
app/Support/JudgeCardTemplateBuilder.php

@@ -4,8 +4,6 @@ namespace App\Support;
 
 class JudgeCardTemplateBuilder
 {
-    private const TOTAL_QUESTIONS = 20;
-
     public function build(): array
     {
         $template = config('exam.judge_card_template', []);
@@ -37,7 +35,7 @@ class JudgeCardTemplateBuilder
             'label_width' => (int) data_get($template, 'layout.label_width', 180),
             'label_to_box_gap' => (int) data_get($template, 'layout.label_to_box_gap', 16),
         ];
-        $questions = self::TOTAL_QUESTIONS;
+        $questions = max(1, (int) config('question_bank.default_total_questions'));
         $questionBoxCounts = $this->buildQuestionBoxCountsByRule($questions);
 
         return [

+ 5 - 0
config/question_bank.php

@@ -13,4 +13,9 @@ return [
 
     /** 知识点题量统计:教材章节顺序用哪一册(textbooks.semester,默认 2=下学期) */
     'kp_stats_semester' => (int) env('KP_STATS_SEMESTER', 2),
+
+    /**
+     * 智能组卷默认题量(单一配置源:业务代码请用 config('question_bank.default_total_questions'),勿硬编码题数)
+     */
+    'default_total_questions' => (int) env('DEFAULT_TOTAL_QUESTIONS', 10),
 ];