40, // 百分比,支持 4:2:4 或 5:2:3 '填空题' => 20, '解答题' => 40, ]; // 难度配比 public array $difficultyRatio = [ '基础' => 50, // 百分比 '中等' => 35, '拔高' => 15, ]; // 难度多选(空数组表示随机难度) public array $selectedDifficultyLevels = []; // 教师和学生相关 public ?string $selectedTeacherId = null; public ?string $selectedStudentId = null; public bool $filterByStudentWeakness = false; // 状态 public bool $isGenerating = false; public array $generatedQuestions = []; public ?string $generatedPaperId = null; #[Computed] public function canGenerate(): bool { return !$this->isGenerating && $this->totalQuestions >= 6 && !empty($this->selectedTeacherId) && !empty($this->selectedStudentId); // 注意:不强制要求选择知识点,没有选择时将按年级或随机生成 } public function mount(): void { // 初始化用户角色检查 $this->initializeUserRole(); // 如果是老师,自动选择当前老师 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $this->selectedTeacherId = $teacherId; } } else { // 管理员模式:从 URL 参数获取 $this->selectedTeacherId = request()->query('teacher_id', $this->selectedTeacherId); } $this->selectedStudentId = request()->query('student_id', $this->selectedStudentId); // 如果只传了 student_id,补全 teacher_id 以便学生下拉加载 if ($this->selectedStudentId && !$this->selectedTeacherId) { $student = Student::find($this->selectedStudentId); if ($student && $student->teacher_id) { $this->selectedTeacherId = (string) $student->teacher_id; } } // 注意:不初始化 selectedDifficultyLevels,保持为空数组表示随机 // 如果用户不选择任何难度,系统将随机生成题目 } #[Computed(cache: false)] public function knowledgePoints(): array { try { $result = app(KnowledgeGraphService::class)->listKnowledgePoints(1, 1000); $knowledgePoints = $result['data'] ?? []; \Illuminate\Support\Facades\Log::info('知识点列表获取成功', [ 'count' => count($knowledgePoints), 'first_item' => !empty($knowledgePoints) ? $knowledgePoints[0] : null ]); return $knowledgePoints; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('获取知识点列表失败', [ 'error' => $e->getMessage() ]); return []; } } #[Computed(cache: false)] public function skills(): array { if (empty($this->selectedKpCodes)) { return []; } $allSkills = []; foreach ($this->selectedKpCodes as $kpCode) { $kpSkills = app(KnowledgeGraphService::class)->getSkillsByKnowledgePoint($kpCode); $allSkills = array_merge($allSkills, $kpSkills); } return $allSkills; } /** * 获取当前选择的教师名称 */ public function getSelectedTeacherName(): string { if (empty($this->selectedTeacherId)) { return '未选择'; } try { $teacher = \App\Models\Teacher::query() ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id') ->where('teachers.teacher_id', $this->selectedTeacherId) ->select( 'teachers.name', 'teachers.subject', 'u.username' ) ->first(); if ($teacher) { $name = trim($teacher->name ?? $this->selectedTeacherId); $subject = $teacher->subject ? " ({$teacher->subject})" : ''; $username = $teacher->username ? " [{$teacher->username}]" : ''; return "{$name}{$subject}{$username}"; } return $this->selectedTeacherId; } catch (\Exception $e) { return $this->selectedTeacherId; } } /** * 获取当前选择的学生名称 */ public function getSelectedStudentName(): string { if (empty($this->selectedStudentId) || empty($this->selectedTeacherId)) { return '未选择'; } try { $student = \App\Models\Student::query() ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id') ->where('students.student_id', $this->selectedStudentId) ->where('students.teacher_id', $this->selectedTeacherId) ->select( 'students.name', 'students.grade', 'students.class_name', 'u.username' ) ->first(); if ($student) { $name = trim($student->name ?? $this->selectedStudentId); $gradeClass = trim("{$student->grade} - {$student->class_name}"); $username = $student->username ? " [{$student->username}]" : ''; return "{$name} ({$gradeClass}){$username}"; } return $this->selectedStudentId; } catch (\Exception $e) { return $this->selectedStudentId; } } #[Computed(cache: false)] public function teachers(): array { try { $query = \App\Models\Teacher::query()->from('teachers as t') ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id') ->select( 't.teacher_id', 't.name', 't.subject', 'u.username', 'u.email' ); // 如果是老师,只返回自己 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $query->where('t.teacher_id', $teacherId); } } $teachers = $query->orderBy('t.name')->get(); // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目 $teacherIds = $teachers->pluck('teacher_id')->toArray(); $missingTeacherIds = \App\Models\Student::query()->from('students as s') ->distinct() ->whereNotIn('s.teacher_id', $teacherIds) ->pluck('teacher_id') ->toArray(); // 转换 Collection 为数组以便合并和排序 $teachersArray = $teachers->all(); if (!empty($missingTeacherIds)) { foreach ($missingTeacherIds as $missingId) { $teachersArray[] = (object) [ 'teacher_id' => $missingId, 'name' => '未知老师 (' . $missingId . ')', 'subject' => '未知', 'username' => null, 'email' => null ]; } // 重新排序 usort($teachersArray, function($a, $b) { return strcmp($a->name, $b->name); }); return $teachersArray; } return $teachersArray; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('加载老师列表失败', [ 'error' => $e->getMessage() ]); return []; } } #[Computed(cache: false)] public function students(): array { if (empty($this->selectedTeacherId)) { return []; } try { $students = \App\Models\Student::query()->from('students as s') ->leftJoin('users as u', 's.student_id', '=', 'u.user_id') ->where('s.teacher_id', $this->selectedTeacherId) ->select( 's.student_id', 's.name', 's.grade', 's.class_name', 'u.username', 'u.email' ) ->orderBy('s.grade') ->orderBy('s.class_name') ->orderBy('s.name') ->get() ->all(); \Illuminate\Support\Facades\Log::info('智能出题页面加载学生列表', [ 'teacher_id' => $this->selectedTeacherId, 'student_count' => count($students) ]); return $students; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('加载学生列表失败', [ 'teacher_id' => $this->selectedTeacherId, 'error' => $e->getMessage() ]); return []; } } #[Computed(cache: false)] public function studentWeaknesses(): array { if (!$this->selectedStudentId || !$this->filterByStudentWeakness) { \Illuminate\Support\Facades\Log::info('学生薄弱点未加载', [ 'student_id' => $this->selectedStudentId, 'filter_enabled' => $this->filterByStudentWeakness ]); return []; } try { // 使用本地MasteryCalculator替代LearningAnalyticsService $masteryCalculator = app(MasteryCalculator::class); $overview = $masteryCalculator->getStudentMasteryOverview($this->selectedStudentId); // 从概览中提取薄弱点(掌握度 < 0.5) $weaknesses = []; foreach ($overview['details'] ?? [] as $detail) { $masteryLevel = floatval($detail->mastery_level ?? 0); if ($masteryLevel < 0.5) { $weaknesses[] = [ 'kp_code' => $detail->kp_code, 'mastery' => $masteryLevel ]; } } \Illuminate\Support\Facades\Log::info('获取学生薄弱点成功(本地服务)', [ 'student_id' => $this->selectedStudentId, 'weakness_count' => count($weaknesses), 'weaknesses' => $weaknesses ]); return $weaknesses; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('获取学生薄弱点失败', [ 'student_id' => $this->selectedStudentId, 'error' => $e->getMessage() ]); return []; } } /** * 检查学生是否有薄弱知识点(用于 UI 判断) */ #[Computed(cache: false)] public function hasStudentWeaknesses(): bool { if (!$this->selectedStudentId) { return false; } try { // 使用本地MasteryCalculator替代LearningAnalyticsService $masteryCalculator = app(MasteryCalculator::class); $overview = $masteryCalculator->getStudentMasteryOverview($this->selectedStudentId); // 检查是否有薄弱点(掌握度 < 0.5) foreach ($overview['details'] ?? [] as $detail) { $masteryLevel = floatval($detail->mastery_level ?? 0); if ($masteryLevel < 0.5) { return true; } } return false; } catch (\Exception $e) { return false; } } public function updatedSelectedTeacherId($value) { // 当教师选择变化时,清空之前选择的学生 $this->selectedStudentId = null; $this->filterByStudentWeakness = false; } /** * 监听薄弱点筛选开关变化 * 确保不会重置学生选择 */ public function updatedFilterByStudentWeakness($value) { \Illuminate\Support\Facades\Log::info('薄弱点筛选开关变化', [ 'enabled' => $value, 'student_id' => $this->selectedStudentId, 'teacher_id' => $this->selectedTeacherId ]); // 不需要做任何事情,只是记录日志 // 学生ID和教师ID保持不变 } /** * 全选所有薄弱知识点 */ public function selectAllWeaknesses(): void { $weaknesses = $this->studentWeaknesses; if (empty($weaknesses)) { Notification::make() ->title('提示') ->body('暂无薄弱知识点数据') ->warning() ->send(); return; } // 获取所有薄弱知识点的代码 $kpCodes = array_column($weaknesses, 'kp_code'); // 合并到已选择的知识点中(去重) $this->selectedKpCodes = array_unique(array_merge($this->selectedKpCodes, $kpCodes)); Notification::make() ->title('成功') ->body('已全选 ' . count($kpCodes) . ' 个薄弱知识点') ->success() ->send(); \Illuminate\Support\Facades\Log::info('全选薄弱知识点', [ 'student_id' => $this->selectedStudentId, 'selected_kp_codes' => $kpCodes, 'total_selected' => count($this->selectedKpCodes) ]); } /** * 去重题目:基于ID和内容相似度去重 */ protected function deduplicateQuestions(array $questions): array { $uniqueQuestions = []; $seenIds = []; $seenStems = []; foreach ($questions as $question) { $id = $question['id'] ?? $question['question_id'] ?? null; $stem = $question['stem'] ?? $question['content'] ?? $question['question_text'] ?? ''; $difficulty = $question['difficulty'] ?? 0.5; $kpCode = $question['kp_code'] ?? ''; // 规范化题干:移除多余空白和换行 $normalizedStem = preg_replace('/\s+/', ' ', trim($stem)); $normalizedStem = preg_replace('/^\d+\.\s*/', '', $normalizedStem); // 移除题号 // 创建唯一键:ID + 题干哈希 + 知识点 $key = ($id ? "id:{$id}" : "stem:" . md5($normalizedStem)) . "kp:{$kpCode}"; // 如果是选择题,进一步检查选项是否相同 if ($this->determineQuestionType($question) === 'choice') { $options = $question['options'] ?? []; if (!empty($options) && is_array($options)) { $optionsKey = md5(implode('|', $options)); $key .= "opt:{$optionsKey}"; } } // 检查是否已存在 if (isset($seenIds[$key]) || in_array($normalizedStem, $seenStems)) { \Illuminate\Support\Facades\Log::debug("跳过重复题目", [ 'id' => $id, 'stem_preview' => mb_substr($normalizedStem, 0, 50) ]); continue; } // 记录并保留 $seenIds[$key] = true; $seenStems[] = $normalizedStem; $uniqueQuestions[] = $question; } \Illuminate\Support\Facades\Log::info("题目去重完成", [ 'original_count' => count($questions), 'unique_count' => count($uniqueQuestions), 'removed_count' => count($questions) - count($uniqueQuestions) ]); return array_values($uniqueQuestions); } /** * 清空所有选择的知识点 */ public function clearSelection(): void { $this->selectedKpCodes = []; Notification::make() ->title('成功') ->body('已清空所有选择的知识点') ->info() ->send(); \Illuminate\Support\Facades\Log::info('清空知识点选择', [ 'student_id' => $this->selectedStudentId ]); } public function updatedSelectedStudentId($value) { // 选择学生后,不要清空知识点选择,保持步骤3的选择有效 // $this->selectedKpCodes = []; // 注释掉这行,避免清空用户选择 // 如果启用了薄弱点筛选,加载但不自动勾选 if ($this->filterByStudentWeakness && $value) { $weaknesses = $this->studentWeaknesses; if (empty($weaknesses)) { Notification::make() ->title('提示') ->body('该学生暂无薄弱点数据,您可以在步骤3中手动选择知识点') ->warning() ->send(); } else { Notification::make() ->title('提示') ->body('已加载' . count($weaknesses) . '个薄弱知识点,您可以在下方或步骤3中选择要练习的知识点') ->info() ->send(); } } } /** * 监听知识点选择变化 */ public function updatedSelectedKpCodes($value) { // 记录调试信息 \Illuminate\Support\Facades\Log::info('selectedKpCodes updated', [ 'count' => count($this->selectedKpCodes), 'codes' => $this->selectedKpCodes, 'type' => gettype($value), 'value' => $value ]); } public function generateExam() { \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', 'selectedTeacherId' => 'nullable|string', // 可选老师 'selectedStudentId' => 'nullable|string', // 可选学生 ]); // 确保题目数量至少6题 if ($this->totalQuestions < 6) { \Illuminate\Support\Facades\Log::warning('题目数量少于6题,已自动调整为6题', ['original' => $this->totalQuestions]); $this->totalQuestions = 6; } if (empty($this->selectedTeacherId) || empty($this->selectedStudentId)) { Notification::make() ->title('请选择教师与学生') ->body('请选择出卷对应的教师与学生后再生成试卷,确保个性化规则生效。') ->danger() ->send(); return; } // 若未选知识点,允许随机(后台会按年级或薄弱点补全) // 自动生成试卷名称 - 使用 paper_id 作为唯一标识 if (empty($this->paperName)) { // 先生成一个临时名称,保存后会更新为真实的 paper_id $this->paperName = 'paper_' . time() . '_' . bin2hex(random_bytes(4)); } $this->isGenerating = true; try { // 使用本地QuestionBankService进行智能出卷(generateIntelligentExam是LearningAnalytics的特定方法) // TODO: 需要实现本地的智能出卷功能,暂时跳过 \Illuminate\Support\Facades\Log::warning('跳过LearningAnalytics的generateIntelligentExam调用', [ 'student_id' => $this->selectedStudentId, 'reason' => '功能已迁移到本地KnowledgeMasteryService,但generateIntelligentExam尚未实现' ]); // 模拟成功结果,返回空题目列表 $questions = []; $result = [ 'success' => true, 'message' => '使用本地题库服务生成', 'questions' => $questions ]; \Illuminate\Support\Facades\Log::info('智能出卷使用本地服务', [ 'success' => $result['success'], 'message' => $result['message'], 'questions_count' => count($questions), 'target_count' => $this->totalQuestions ]); if (count($questions) < $this->totalQuestions) { // 题库不足时,批量生成题目(使用题库的多AI模型并行生成功能) $neededCount = $this->totalQuestions - count($questions); // 生成比需求更多的题目,储备起来供后续使用 $generateCount = max($neededCount, 20); // 至少生成20道题作为储备 \Illuminate\Support\Facades\Log::info("题库题目不足,需要补充 {$neededCount} 道题,准备批量生成 {$generateCount} 道题", [ 'current_count' => count($questions), 'needed' => $neededCount, 'will_generate' => $generateCount ]); // 只生成一次,生成足够多的题目(题库服务支持多AI模型并行) $this->batchGenerateQuestions($generateCount); // 重新从题库获取题目 $questionBankService = app(QuestionBankService::class); $params = [ 'kp_codes' => implode(',', $this->selectedKpCodes), 'limit' => $this->totalQuestions * 2 // 获取更多题目用于筛选 ]; if (!empty($this->selectedSkills)) { $params['skills'] = implode(',', $this->selectedSkills); } if ($this->selectedStudentId) { $params['exclude_student_questions'] = $this->selectedStudentId; } $newResponse = $questionBankService->filterQuestions($params); // 合并题目并去重,只保留有解题思路的题目 if (!empty($newResponse['data'])) { $existingIds = array_column($questions, 'id'); foreach ($newResponse['data'] as $newQ) { // 只添加有解题思路的题目 $sol = $newQ['solution'] ?? ''; if (is_array($sol)) $sol = json_encode($sol, JSON_UNESCAPED_UNICODE); if (!in_array($newQ['id'], $existingIds) && !empty(trim($sol))) { $questions[] = $newQ; } } } \Illuminate\Support\Facades\Log::info("批量生成完成,当前题库题目数量: " . count($questions), [ 'generated_count' => $generateCount, 'total_in_bank' => count($questions) ]); } // 2. 最终去重:确保没有重复题目 $questions = $this->deduplicateQuestions($questions); \Illuminate\Support\Facades\Log::info("去重后题目数量: " . count($questions)); // 3. 限制试卷题目数量为用户要求的数量 if (count($questions) > $this->totalQuestions) { // 根据题型配比和难度配比对题目进行筛选和排序 // 注意:如果用户未选择难度(selectedDifficultyLevels为空),则传入null表示随机难度 $effectiveDifficultyCategory = !empty($this->selectedDifficultyLevels) ? $this->difficultyCategory : null; $questions = $this->selectBestQuestions( $questions, $this->totalQuestions, $effectiveDifficultyCategory, $this->totalScore, $this->questionTypeRatio ); $difficultyLog = $effectiveDifficultyCategory ?? '随机'; \Illuminate\Support\Facades\Log::info("从 " . count($questions) . " 道题中筛选出 " . count($questions) . " 道题,难度: {$difficultyLog}, 总分: {$this->totalScore}"); } // 3. 检查题型完整性(至少保证每种题型都有题目) $checkResult = $this->ensureQuestionTypeCompleteness($questions, $this->totalQuestions); if ($checkResult['missing_types']) { \Illuminate\Support\Facades\Log::warning("检测到缺失题型,将自动生成", [ 'missing_types' => $checkResult['missing_types'], 'current_count' => $checkResult['current_count'] ]); // 批量生成缺失题型的题目 $this->batchGenerateMissingTypes($checkResult['missing_types']); // 重新获取题目 $questionBankService = app(QuestionBankService::class); $params = [ 'kp_codes' => implode(',', $this->selectedKpCodes), 'limit' => $this->totalQuestions * 2 ]; if (!empty($this->selectedSkills)) { $params['skills'] = implode(',', $this->selectedSkills); } if ($this->selectedStudentId) { $params['exclude_student_questions'] = $this->selectedStudentId; } $newResponse = $questionBankService->filterQuestions($params); if (!empty($newResponse['data'])) { // 只保留有解题思路的题目 $questionsWithSolution = array_filter($newResponse['data'], function($q) { $sol = $q['solution'] ?? ''; if (is_array($sol)) $sol = json_encode($sol, JSON_UNESCAPED_UNICODE); return !empty(trim($sol)); }); $questions = array_merge($questions, array_values($questionsWithSolution)); } // 再次筛选 $effectiveDifficultyCategory = !empty($this->selectedDifficultyLevels) ? $this->difficultyCategory : null; $questions = $this->selectBestQuestions( $questions, $this->totalQuestions, $effectiveDifficultyCategory, $this->totalScore, $this->questionTypeRatio ); } // 2. 最终过滤:确保所有题目都有解题思路 $questions = array_filter($questions, function($q) { $solution = $q['solution'] ?? ''; // 处理 solution 可能是数组的情况 if (is_array($solution)) { $solution = json_encode($solution, JSON_UNESCAPED_UNICODE); } return !empty(trim($solution)); }); $questions = array_values($questions); // 重新索引数组 \Illuminate\Support\Facades\Log::info('最终过滤后题目数量', [ 'total_questions' => count($questions), 'filtered_message' => '已过滤掉没有解题思路的题目' ]); // 3. 为题目添加类型信息(如果缺失) foreach ($questions as &$question) { if (!isset($question['question_type'])) { $question['question_type'] = $this->determineQuestionType($question); \Illuminate\Support\Facades\Log::debug('为题目添加类型', [ 'question_id' => $question['id'] ?? '', 'added_type' => $question['question_type'] ]); } } unset($question); // 释放引用 // 3. 为题目分配分数(根据题型配比和总分) $questions = $this->assignScoresToQuestions($questions, $this->totalScore, $this->questionTypeRatio); // 4. 生成试卷数据 $examData = [ 'paper_name' => $this->paperName, 'paper_description' => $this->paperDescription, 'difficulty_category' => $this->difficultyCategory, 'questions' => $questions, 'total_score' => $this->totalScore, 'total_questions' => count($questions), 'student_id' => $this->selectedStudentId, 'teacher_id' => $this->selectedTeacherId, ]; // 4. 保存到数据库 $questionBankService = app(QuestionBankService::class); $paperId = $questionBankService->saveExamToDatabase($examData); // 如果保存返回 null,使用默认占位 ID,防止 UI 不显示 if (empty($paperId)) { $paperId = 'demo_' . $this->selectedStudentId . '_' . now()->format('YmdHis'); } // 使用 paper_id 作为试卷名称 $this->paperName = $paperId; \Illuminate\Support\Facades\Log::info('Generated paper ID: ' . $paperId); $this->generatedPaperId = $paperId; // 将生成的试卷数据缓存,以便 PDF 预览时使用(缓存 1 小时) \Illuminate\Support\Facades\Log::info('缓存试卷数据', [ 'paper_id' => $paperId, 'question_count' => count($questions), 'question_types' => array_column($questions, 'question_type') ]); Cache::put('generated_exam_' . $paperId, $examData, now()->addHour()); $this->generatedQuestions = $questions; $stats = $result['stats'] ?? []; $message = "已生成包含 " . count($questions) . " 道题的试卷"; if (!empty($stats['weakness_targeted'])) { $message .= ",其中针对薄弱点 " . $stats['weakness_targeted'] . " 题"; } Notification::make() ->title('试卷生成成功') ->body($message) ->success() ->send(); } catch (\Exception $e) { // 记录错误,生成失败时不创建 paper_id,避免生成无效的 PDF 链接 \Illuminate\Support\Facades\Log::error('生成试卷失败', ['error' => $e->getMessage()]); $this->generatedPaperId = null; // 不设置 paper_id,避免显示无效链接 $this->generatedQuestions = []; Notification::make() ->title('试卷生成失败') ->body('错误: ' . $e->getMessage() . "\n\n请检查题目数量、知识点选择或网络连接后重试") ->danger() ->send(); } finally { $this->isGenerating = false; } } /** * 批量生成题目(使用题库的多AI模型并行功能) */ protected function batchGenerateQuestions(int $count) { // 增加PHP脚本执行时间到120秒,给足够时间启动异步任务和等待完成 set_time_limit(120); $questionBankService = app(QuestionBankService::class); $generatedTasks = []; // 只生成一次,使用所有选中的知识点 $allKpCodes = $this->selectedKpCodes; if (empty($allKpCodes)) { // 如果没有选中知识点,使用默认知识点 $allKpCodes = ['R01']; // 默认知识点 } // 对每个知识点生成题目 foreach ($allKpCodes as $kpCode) { \Illuminate\Support\Facades\Log::info("为知识点 {$kpCode} 生成题目", [ 'count' => $count, 'skills' => $this->selectedSkills ]); $result = $questionBankService->generateIntelligentQuestions([ 'kp_code' => $kpCode, 'skills' => $this->selectedSkills, 'count' => $count, 'difficulty_distribution' => $this->difficultyRatio, ]); if ($result['success'] && isset($result['task_id'])) { $generatedTasks[] = [ 'task_id' => $result['task_id'], 'kp_code' => $kpCode ]; \Illuminate\Support\Facades\Log::info("已启动生成任务: {$result['task_id']} for {$kpCode}"); } else { \Illuminate\Support\Facades\Log::warning("生成任务启动失败", [ 'kp_code' => $kpCode, 'result' => $result ]); } } // 等待所有任务完成(最多等待60秒) if (!empty($generatedTasks)) { $maxWaitTime = 60; // 增加最大等待时间 $startTime = time(); \Illuminate\Support\Facades\Log::info("等待 {$maxWaitTime} 秒,所有生成任务完成", [ 'tasks' => array_column($generatedTasks, 'task_id') ]); while (time() - $startTime < $maxWaitTime) { $allCompleted = true; $completedTasks = []; $runningTasks = []; foreach ($generatedTasks as $task) { $taskStatus = $questionBankService->getTaskStatus($task['task_id']); if (!$taskStatus) { $allCompleted = false; $runningTasks[] = $task['task_id']; continue; } $status = $taskStatus['status'] ?? ''; if ($status === 'completed') { $completedTasks[] = $task['task_id']; } elseif ($status === 'failed') { \Illuminate\Support\Facades\Log::error("生成任务失败", [ 'task_id' => $task['task_id'], 'error' => $taskStatus['error'] ?? '未知错误' ]); // 任务失败继续等待其他任务 } else { $allCompleted = false; $runningTasks[] = $task['task_id']; } } if ($allCompleted) { \Illuminate\Support\Facades\Log::info('所有AI生成任务已完成', [ 'completed' => $completedTasks, 'tasks' => $generatedTasks ]); break; } // 每10秒输出一次进度 $elapsed = time() - $startTime; if ($elapsed % 10 < 2) { \Illuminate\Support\Facades\Log::info("生成进度", [ 'elapsed' => $elapsed, 'completed' => count($completedTasks), 'running' => count($runningTasks), 'total' => count($generatedTasks) ]); } // 等待3秒后重试 sleep(3); } $waitTime = time() - $startTime; \Illuminate\Support\Facades\Log::info('AI生成任务等待完成', [ 'wait_time' => $waitTime, 'tasks' => $generatedTasks ]); } } /** * 根据题型配比和难度配比,从大量题目中筛选出最佳题目 */ protected function selectBestQuestions( array $questions, int $targetCount, ?string $difficultyCategory, // null 表示随机难度 float $totalScore, array $questionTypeRatio ): array { // 去重并过滤:确保输入题目列表没有重复ID,且都有解题思路 $uniqueQuestions = []; foreach ($questions as $q) { $id = $q['id'] ?? $q['question_id'] ?? null; // 只保留有解题思路且不重复的题目 $sol = $q['solution'] ?? ''; if (is_array($sol)) $sol = json_encode($sol, JSON_UNESCAPED_UNICODE); if ($id && !isset($uniqueQuestions[$id]) && !empty(trim($sol))) { $uniqueQuestions[$id] = $q; } } $questions = array_values($uniqueQuestions); if (count($questions) <= $targetCount) { return $questions; } \Illuminate\Support\Facades\Log::info("开始筛选题目", [ 'total_available' => count($questions), 'target_count' => $targetCount, 'difficulty_category' => $difficultyCategory, 'total_score' => $totalScore, 'type_ratio' => $questionTypeRatio ]); // 1. 按题型分类题目 $categorizedQuestions = [ 'choice' => [], // 选择题 'fill' => [], // 填空题 'answer' => [], // 解答题 ]; // 统计所有题型的原始类型分布 $rawTypeStats = ['choice' => 0, 'fill' => 0, 'answer' => 0]; foreach ($questions as $question) { $type = $this->determineQuestionType($question); if (!isset($categorizedQuestions[$type])) { $type = 'answer'; } $categorizedQuestions[$type][] = $question; // 统计原始类型 $rawType = $question['question_type'] ?? $question['type'] ?? 'unknown'; if (strtolower($rawType) === 'choice') $rawTypeStats['choice']++; if (strtolower($rawType) === 'fill') $rawTypeStats['fill']++; if (strtolower($rawType) === 'answer') $rawTypeStats['answer']++; // 记录选择题的详细信息(用于调试) if ($type === 'choice') { \Illuminate\Support\Facades\Log::debug('题目被分类为选择题', [ 'question_id' => $question['id'] ?? '', 'question_type' => $question['question_type'] ?? '', 'type' => $question['type'] ?? '', 'stem_preview' => mb_substr($question['stem'] ?? '', 0, 50) ]); } } \Illuminate\Support\Facades\Log::info("题目类型分类统计", [ 'total_questions' => count($questions), 'raw_type_stats' => $rawTypeStats, // 题库中题目的原始类型分布 'final_type_stats' => [ 'choice' => count($categorizedQuestions['choice']), 'fill' => count($categorizedQuestions['fill']), 'answer' => count($categorizedQuestions['answer']) ] ]); // 2. 根据难度分类筛选题目 // 如果难度分类为null(用户未选择),则不过滤,保留所有题目(随机难度) if ($difficultyCategory === null) { \Illuminate\Support\Facades\Log::info("用户未选择难度,将随机生成所有难度的题目"); $difficultyFilteredQuestions = $categorizedQuestions; } else { $difficultyFilteredQuestions = $this->filterByDifficulty($categorizedQuestions, $difficultyCategory); } // 3. 根据题型配比计算每种题型应选择的题目数量 $selectedQuestions = []; $selectedIds = []; // 用于追踪已选题目ID $typeTargets = $this->computeTypeTargets($targetCount, $questionTypeRatio); // 检查每种题型的可用数量 $availableCounts = [ 'choice' => count($difficultyFilteredQuestions['choice'] ?? []), 'fill' => count($difficultyFilteredQuestions['fill'] ?? []), 'answer' => count($difficultyFilteredQuestions['answer'] ?? []), ]; \Illuminate\Support\Facades\Log::info("各题型可用数量", [ 'available' => $availableCounts, 'targets' => $typeTargets ]); // 优先保证每种题型至少一题(适用于总题目数>=3的情况) if ($targetCount >= 3) { foreach (['choice', 'fill', 'answer'] as $typeKey) { if (!empty($difficultyFilteredQuestions[$typeKey])) { // 随机选择1道该题型的题目 $randomIndex = array_rand($difficultyFilteredQuestions[$typeKey]); $q = $difficultyFilteredQuestions[$typeKey][$randomIndex]; $id = $q['id'] ?? $q['question_id']; if (!in_array($id, $selectedIds)) { $selectedQuestions[] = $q; $selectedIds[] = $id; } \Illuminate\Support\Facades\Log::info("保证题型最少题目: {$typeKey}", [ 'selected_index' => $randomIndex ]); } else { \Illuminate\Support\Facades\Log::warning("题型缺失: {$typeKey},需要从其他题型补充"); } } } // 根据题型配比计算每种题型应选择的题目数量 foreach ($typeTargets as $typeKey => $targetTypeCount) { if ($targetTypeCount <= 0) continue; $availableQuestions = $difficultyFilteredQuestions[$typeKey] ?? []; // 过滤掉已选的 $availableQuestions = array_filter($availableQuestions, function($q) use ($selectedIds) { $id = $q['id'] ?? $q['question_id']; return !in_array($id, $selectedIds); }); // 还需要选多少:目标数量 - 已选该类型的数量 $currentTypeCount = 0; foreach ($selectedQuestions as $sq) { if ($this->determineQuestionType($sq) === $typeKey) { $currentTypeCount++; } } $needToSelect = $targetTypeCount - $currentTypeCount; if ($needToSelect > 0) { $availableCount = count($availableQuestions); // 如果该题型可用数量不足目标数量,从其他题型补充 if ($availableCount < $needToSelect) { \Illuminate\Support\Facades\Log::warning("题型 {$typeKey} 数量不足", [ 'available' => $availableCount, 'target' => $needToSelect, 'will_supplement' => true ]); // 先选完该题型的所有可用题目 if ($availableCount > 0) { foreach ($availableQuestions as $q) { $selectedQuestions[] = $q; $selectedIds[] = $q['id'] ?? $q['question_id']; } } // 剩余题目从其他题型补充 $remainingNeed = $needToSelect - $availableCount; $otherTypes = ['choice', 'fill', 'answer']; $otherTypes = array_filter($otherTypes, fn($t) => $t !== $typeKey); foreach ($otherTypes as $otherType) { if ($remainingNeed <= 0) break; $otherQuestions = $difficultyFilteredQuestions[$otherType] ?? []; // 过滤掉已选的 $otherQuestions = array_filter($otherQuestions, function($q) use ($selectedIds) { $id = $q['id'] ?? $q['question_id']; return !in_array($id, $selectedIds); }); if (!empty($otherQuestions)) { $takeCount = min($remainingNeed, count($otherQuestions)); $randomKeys = array_rand($otherQuestions, $takeCount); if (!is_array($randomKeys)) { $randomKeys = [$randomKeys]; } foreach ($randomKeys as $key) { $q = $otherQuestions[$key]; $selectedQuestions[] = $q; $selectedIds[] = $q['id'] ?? $q['question_id']; } $remainingNeed -= $takeCount; } } \Illuminate\Support\Facades\Log::info("题型 {$typeKey} 补充完成", [ 'original_need' => $needToSelect, 'available' => $availableCount, 'supplemented' => $needToSelect - $remainingNeed ]); } else { // 该题型数量足够,正常选择 $takeCount = min($needToSelect, $availableCount, $targetCount - count($selectedQuestions)); if ($takeCount > 0) { $randomKeys = array_rand($availableQuestions, $takeCount); if (!is_array($randomKeys)) { $randomKeys = [$randomKeys]; } foreach ($randomKeys as $key) { $q = $availableQuestions[$key]; $selectedQuestions[] = $q; $selectedIds[] = $q['id'] ?? $q['question_id']; } } } } } // 4. 如果还有空缺,随机补充其他题型 if (count($selectedQuestions) < $targetCount) { // 从所有题目中过滤掉已选的 $remainingQuestions = array_filter($questions, function($q) use ($selectedIds) { $id = $q['id'] ?? $q['question_id']; return !in_array($id, $selectedIds); }); if (!empty($remainingQuestions)) { $needed = $targetCount - count($selectedQuestions); $take = min($needed, count($remainingQuestions)); $randomKeys = array_rand($remainingQuestions, $take); if (!is_array($randomKeys)) { $randomKeys = [$randomKeys]; } foreach ($randomKeys as $key) { $selectedQuestions[] = $remainingQuestions[$key]; } } } // 5. 打乱题目顺序 shuffle($selectedQuestions); $finalQuestions = array_slice($selectedQuestions, 0, $targetCount); \Illuminate\Support\Facades\Log::info("题目筛选完成", [ 'selected_count' => count($finalQuestions), 'difficulty_category' => $difficultyCategory, 'total_score' => $totalScore ]); return $finalQuestions; } /** * 根据比例与总题数计算各题型目标数量,四舍五入并校正总数 */ protected function computeTypeTargets(int $targetCount, array $questionTypeRatio): array { $map = [ '选择题' => 'choice', '填空题' => 'fill', '解答题' => 'answer', ]; $targets = ['choice' => 0, 'fill' => 0, 'answer' => 0]; // 初步分配 foreach ($questionTypeRatio as $label => $ratio) { $key = $map[$label] ?? null; if (!$key) { continue; } $count = (int) round($targetCount * ($ratio / 100)); if ($ratio > 0 && $count < 1) { $count = 1; // 有比例就至少 1 道 } $targets[$key] = $count; } $currentSum = array_sum($targets); if ($currentSum === 0) { $targets['answer'] = $targetCount; return $targets; } // 总数超出则递减最多的类型(>1 时才减) while ($currentSum > $targetCount) { arsort($targets); foreach ($targets as $k => $v) { if ($v > 1) { $targets[$k]--; $currentSum--; break; } } } // 总数不足则按比例最高的类型补 if ($currentSum < $targetCount) { $ratioByKey = [ 'choice' => $questionTypeRatio['选择题'] ?? 0, 'fill' => $questionTypeRatio['填空题'] ?? 0, 'answer' => $questionTypeRatio['解答题'] ?? 0, ]; while ($currentSum < $targetCount) { arsort($ratioByKey); $key = array_key_first($ratioByKey); $targets[$key]++; $currentSum++; } } return $targets; } /** * 检查题型完整性,确保每种题型至少有一题 */ protected function ensureQuestionTypeCompleteness(array $questions, int $targetCount): array { $result = [ 'missing_types' => [], 'current_count' => count($questions), 'has_choice' => false, 'has_fill' => false, 'has_answer' => false, ]; // 统计各题型数量 $choiceCount = $fillCount = $answerCount = 0; foreach ($questions as $q) { $type = $this->determineQuestionType($q); if ($type === 'choice') { $choiceCount++; $result['has_choice'] = true; } elseif ($type === 'fill') { $fillCount++; $result['has_fill'] = true; } elseif ($type === 'answer') { $answerCount++; $result['has_answer'] = true; } } // 如果题目数量>=3,确保每种题型至少1题 if ($targetCount >= 3) { if (!$result['has_choice']) { $result['missing_types'][] = 'choice'; } if (!$result['has_fill']) { $result['missing_types'][] = 'fill'; } if (!$result['has_answer']) { $result['missing_types'][] = 'answer'; } } \Illuminate\Support\Facades\Log::info("题型完整性检查", [ 'choice_count' => $choiceCount, 'fill_count' => $fillCount, 'answer_count' => $answerCount, 'missing_types' => $result['missing_types'] ]); return $result; } /** * 批量生成缺失题型的题目 */ protected function batchGenerateMissingTypes(array $missingTypes): void { if (empty($missingTypes)) { return; } // 增加PHP脚本执行时间到120秒,给足够时间启动异步任务 set_time_limit(120); \Illuminate\Support\Facades\Log::info("开始生成缺失题型题目", ['missing_types' => $missingTypes]); // 为每个缺失题型生成3-5道题 foreach ($missingTypes as $type) { $generateCount = 5; // 每个缺失题型生成5道题 \Illuminate\Support\Facades\Log::info("为缺失题型 {$type} 生成 {$generateCount} 道题"); foreach ($this->selectedKpCodes as $kpCode) { $questionBankService = app(QuestionBankService::class); // 根据题型设置特定的技能点 $skills = $this->selectedSkills; if ($type === 'choice') { $skills[] = '选择题专项练习'; } elseif ($type === 'fill') { $skills[] = '填空题专项练习'; } elseif ($type === 'answer') { $skills[] = '解答题专项练习'; } $result = $questionBankService->generateIntelligentQuestions([ 'kp_code' => $kpCode, 'skills' => $skills, 'count' => $generateCount, 'difficulty_distribution' => $this->difficultyRatio, ]); if ($result['success'] && isset($result['task_id'])) { \Illuminate\Support\Facades\Log::info("已启动生成任务", [ 'type' => $type, 'task_id' => $result['task_id'], 'kp_code' => $kpCode ]); } } } // 缺失题型生成任务已启动,由于题目生成是异步的, // 将在预览时动态获取最新生成的题目 \Illuminate\Support\Facades\Log::info("缺失题型生成任务已启动,将在预览时动态获取"); } /** * 根据难度分类筛选题目 */ protected function filterByDifficulty(array $categorizedQuestions, string $difficultyCategory): array { $filtered = []; $difficultyRanges = [ '基础' => [0, 0.6], // 扩大基础难度上限,从0.4提升到0.6 '中等' => [0.4, 0.8], // 扩大中等难度范围 '拔高' => [0.6, 1.0] ]; $targetRange = $difficultyRanges[$difficultyCategory] ?? [0, 1.0]; foreach ($categorizedQuestions as $type => $questions) { $filtered[$type] = []; foreach ($questions as $question) { $difficulty = floatval($question['difficulty'] ?? 0.5); if ($difficulty >= $targetRange[0] && $difficulty <= $targetRange[1]) { $filtered[$type][] = $question; } // 不再保留越界题目,严格按难度范围筛选 } } \Illuminate\Support\Facades\Log::info("难度筛选结果", [ 'difficulty_category' => $difficultyCategory, 'difficulty_range' => $targetRange, 'choice_count' => count($filtered['choice']), 'fill_count' => count($filtered['fill']), 'answer_count' => count($filtered['answer']) ]); return $filtered; } /** * 根据题目标签或内容判断题型 */ protected function determineQuestionType(array $question): string { // 优先根据题目内容判断(而不是数据库字段) $stem = $question['stem'] ?? $question['content'] ?? ''; // 处理 stem 可能是数组的情况 if (is_array($stem)) { $stem = json_encode($stem, JSON_UNESCAPED_UNICODE); } $tags = $question['tags'] ?? ''; // 处理 tags 可能是数组的情况 if (is_array($tags)) { $tags = json_encode($tags, JSON_UNESCAPED_UNICODE); } $skills = $question['skills'] ?? []; // 1. 根据题干内容判断 - 选择题特征:必须包含 A. B. C. D. 选项(至少2个) if (is_string($stem)) { // 选择题特征:必须包含 A. B. C. D. 四个选项(至少2个) $hasOptionA = preg_match('/\bA\s*[\.\、\:]/', $stem) || preg_match('/\(A\)/', $stem) || preg_match('/^A[\.\s]/', $stem); $hasOptionB = preg_match('/\bB\s*[\.\、\:]/', $stem) || preg_match('/\(B\)/', $stem) || preg_match('/^B[\.\s]/', $stem); $hasOptionC = preg_match('/\bC\s*[\.\、\:]/', $stem) || preg_match('/\(C\)/', $stem) || preg_match('/^C[\.\s]/', $stem); $hasOptionD = preg_match('/\bD\s*[\.\、\:]/', $stem) || preg_match('/\(D\)/', $stem) || preg_match('/^D[\.\s]/', $stem); $hasOptionE = preg_match('/\bE\s*[\.\、\:]/', $stem) || preg_match('/\(E\)/', $stem) || preg_match('/^E[\.\s]/', $stem); // 至少有2个选项就认为是选择题(降低阈值) $optionCount = ($hasOptionA ? 1 : 0) + ($hasOptionB ? 1 : 0) + ($hasOptionC ? 1 : 0) + ($hasOptionD ? 1 : 0) + ($hasOptionE ? 1 : 0); if ($optionCount >= 2) { return 'choice'; } // 检查是否有"( )"或"( )"括号,这通常是选择题的标志 if (preg_match('/(\s*)|\(\s*\)/', $stem) && (strpos($stem, 'A.') !== false || strpos($stem, 'B.') !== false || strpos($stem, 'C.') !== false || strpos($stem, 'D.') !== false)) { return 'choice'; } } // 2. 根据技能点判断 if (is_array($skills) && !empty($skills)) { // 过滤非字符串元素,避免 implode 报错 $skillsFiltered = array_filter($skills, 'is_string'); if (!empty($skillsFiltered)) { $skillsStr = implode(',', $skillsFiltered); if (strpos($skillsStr, '选择题') !== false) return 'choice'; if (strpos($skillsStr, '填空题') !== false) return 'fill'; if (strpos($skillsStr, '解答题') !== false) return 'answer'; } } // 3. 根据题目已有类型字段判断(作为后备) if (!empty($question['question_type']) && is_string($question['question_type'])) { $type = strtolower(trim($question['question_type'])); if (in_array($type, ['choice', '选择题', 'choice question'])) return 'choice'; if (in_array($type, ['fill', '填空题', 'fill in the blank'])) return 'fill'; if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) return 'answer'; } if (!empty($question['type']) && is_string($question['type'])) { $type = strtolower(trim($question['type'])); if (in_array($type, ['choice', '选择题', 'choice question'])) return 'choice'; if (in_array($type, ['fill', '填空题', 'fill in the blank'])) return 'fill'; if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) return 'answer'; } // 4. 根据标签判断 if (is_string($tags)) { if (strpos($tags, '选择') !== false || strpos($tags, '选择题') !== false) { return 'choice'; } if (strpos($tags, '填空') !== false || strpos($tags, '填空题') !== false) { return 'fill'; } if (strpos($tags, '解答') !== false || strpos($tags, '简答') !== false || strpos($tags, '证明') !== false) { return 'answer'; } } // 5. 填空题特征:连续下划线或明显的填空括号 if (is_string($stem)) { // 检查填空题特征:连续下划线 if (preg_match('/_{3,}/', $stem) || strpos($stem, '____') !== false) { return 'fill'; } // 空括号填空 if (preg_match('/(\s*)/', $stem) || preg_match('/\(\s*\)/', $stem)) { return 'fill'; } } // 6. 根据题干内容关键词判断 if (is_string($stem)) { // 有证明、解答、计算、求证等关键词的是解答题 if (preg_match('/(证明|求证|解方程|计算:|求解|推导|说明理由)/', $stem)) { return 'answer'; } } // 默认是解答题(更安全的默认值) return 'answer'; } /** * 为题目分配分数,确保总分等于设定值 */ protected function assignScoresToQuestions(array $questions, float $totalScore, array $questionTypeRatio): array { if (empty($questions)) { return $questions; } // 按题型分组统计 $typeStats = [ 'choice' => ['count' => 0, 'indices' => []], 'fill' => ['count' => 0, 'indices' => []], 'answer' => ['count' => 0, 'indices' => []], ]; foreach ($questions as $index => $question) { $type = $this->determineQuestionType($question); if (!isset($typeStats[$type])) { $type = 'answer'; } $typeStats[$type]['count']++; $typeStats[$type]['indices'][] = $index; } \Illuminate\Support\Facades\Log::info("分数分配统计", [ 'total_score' => $totalScore, 'choice_count' => $typeStats['choice']['count'], 'fill_count' => $typeStats['fill']['count'], 'answer_count' => $typeStats['answer']['count'], 'type_ratio' => $questionTypeRatio ]); // 根据题型配比计算每种题型的分数比例 $choiceRatio = ($questionTypeRatio['选择题'] ?? 40) / 100; $fillRatio = ($questionTypeRatio['填空题'] ?? 30) / 100; $answerRatio = ($questionTypeRatio['解答题'] ?? 30) / 100; // 计算每种题型的总分 $choiceTotalScore = $totalScore * $choiceRatio; $fillTotalScore = $totalScore * $fillRatio; $answerTotalScore = $totalScore * $answerRatio; // 计算每道题的分数 $choiceScore = $typeStats['choice']['count'] > 0 ? round($choiceTotalScore / $typeStats['choice']['count'], 0) : 0; $fillScore = $typeStats['fill']['count'] > 0 ? round($fillTotalScore / $typeStats['fill']['count'], 0) : 0; $answerScore = $typeStats['answer']['count'] > 0 ? round($answerTotalScore / $typeStats['answer']['count'], 0) : 0; // 确保每道题至少有分数 $choiceScore = max($choiceScore, 2); $fillScore = max($fillScore, 3); $answerScore = max($answerScore, 5); // 分配分数 $assignedTotal = 0; foreach ($questions as $index => &$question) { // 题目类型应该在之前确定并存储,避免重复调用 $type = $question['question_type'] ?? $this->determineQuestionType($question); if ($type === 'choice') { $question['score'] = $choiceScore; } elseif ($type === 'fill') { $question['score'] = $fillScore; } else { $question['score'] = $answerScore; } $assignedTotal += $question['score']; } unset($question); // 调整分数以确保总分正确 $diff = $totalScore - $assignedTotal; if ($diff != 0 && !empty($questions)) { // 优先调整解答题的分数 $adjustIndices = !empty($typeStats['answer']['indices']) ? $typeStats['answer']['indices'] : (!empty($typeStats['fill']['indices']) ? $typeStats['fill']['indices'] : $typeStats['choice']['indices']); if (!empty($adjustIndices)) { $adjustPerQuestion = intval($diff / count($adjustIndices)); $remainder = $diff % count($adjustIndices); foreach ($adjustIndices as $i => $idx) { $adjustment = $adjustPerQuestion + ($i < abs($remainder) ? ($remainder > 0 ? 1 : -1) : 0); $questions[$idx]['score'] = max(1, $questions[$idx]['score'] + $adjustment); } } } // 最终验证 $scores = array_column($questions, 'score'); // 确保所有分数都是数字 $scores = array_map(function($s) { return is_numeric($s) ? (float)$s : 0; }, $scores); $finalTotal = array_sum($scores); \Illuminate\Support\Facades\Log::info("分数分配完成", [ 'target_score' => $totalScore, 'final_total' => $finalTotal, 'choice_score_each' => $choiceScore, 'fill_score_each' => $fillScore, 'answer_score_each' => $answerScore ]); return $questions; } /** * 快捷设置题型配比方案 */ public function applyRatioPreset(string $preset): void { if ($preset === '4-2-4') { $this->questionTypeRatio = [ '选择题' => 40, '填空题' => 20, '解答题' => 40, ]; } elseif ($preset === '5-2-3') { $this->questionTypeRatio = [ '选择题' => 50, '填空题' => 20, '解答题' => 30, ]; } } /** * 保留旧方法以兼容(但不再使用) */ protected function autoGenerateQuestions(int $count) { // 调用新的批量生成方法 $this->batchGenerateQuestions($count); } public function exportToPdf() { if (!$this->generatedPaperId) { Notification::make() ->title('错误') ->body('请先生成试卷') ->danger() ->send(); return; } // 调用PDF导出API return redirect()->route('filament.admin.auth.intelligent-exam.pdf', [ 'paper_id' => $this->generatedPaperId, 'answer' => 'false' ]); } public function resetForm() { $this->reset([ 'paperName', 'paperDescription', 'selectedGrade', 'selectedKpCodes', 'selectedSkills', 'selectedTeacherId', 'selectedStudentId', 'filterByStudentWeakness', 'generatedQuestions', 'generatedPaperId' ]); $this->questionTypeRatio = [ '选择题' => 40, '填空题' => 30, '解答题' => 30, ]; $this->difficultyRatio = [ '基础' => 50, '中等' => 35, '拔高' => 15, ]; } /** * 监听TeacherStudentSelector组件的老师变化事件 */ #[On('teacherChanged')] public function onTeacherChanged(string $teacherId): void { \Illuminate\Support\Facades\Log::info('智能出题页面收到教师变更事件', [ 'teacher_id' => $teacherId ]); $this->selectedTeacherId = $teacherId; // 清空学生选择和相关的筛选 $this->selectedStudentId = null; $this->filterByStudentWeakness = false; } /** * 监听TeacherStudentSelector组件的学生变化事件 */ #[On('studentChanged')] public function onStudentChanged(string $teacherId, string $studentId): void { \Illuminate\Support\Facades\Log::info('智能出题页面收到学生变更事件', [ 'teacher_id' => $teacherId, 'student_id' => $studentId ]); $this->selectedTeacherId = $teacherId; $this->selectedStudentId = $studentId; // ✅ 如果有学生选择,自动启用学生薄弱点筛选(但暂不勾选知识点) if ($studentId) { $this->filterByStudentWeakness = true; \Illuminate\Support\Facades\Log::info('已自动启用薄弱点筛选', [ 'student_id' => $studentId, 'filter_enabled' => $this->filterByStudentWeakness ]); // 不立即触发,让用户自己选择 } else { // 如果清空了学生选择,也清空薄弱点筛选 $this->filterByStudentWeakness = false; } } }