40, // 百分比 '填空题' => 30, '解答题' => 30, ]; // 难度配比 public array $difficultyRatio = [ '基础' => 50, // 百分比 '中等' => 35, '拔高' => 15, ]; // 教师和学生相关 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) && count($this->selectedKpCodes) > 0; } 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; } } } #[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 { $weaknesses = app(LearningAnalyticsService::class)->getStudentWeaknesses($this->selectedStudentId); \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 []; } } public function updatedSelectedTeacherId($value) { // 当教师选择变化时,清空之前选择的学生 $this->selectedStudentId = null; } /** * 全选所有薄弱知识点 */ 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; } if (empty($this->selectedKpCodes)) { Notification::make() ->title('请选择知识点') ->body('请至少选择 1 个知识点后再生成试卷。可勾选学生薄弱点或手动选择知识点。') ->danger() ->send(); return; } // 自动生成试卷名称 if (empty($this->paperName)) { $studentName = '学生' . ($this->selectedStudentId ?? '未选择'); // 如果有选择学生,尝试从数据库获取真实姓名 if ($this->selectedStudentId) { try { $student = \App\Models\Student::where('student_id', $this->selectedStudentId)->first(); if ($student && $student->name) { $studentName = $student->name; } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::warning('获取学生姓名失败', [ 'student_id' => $this->selectedStudentId, 'error' => $e->getMessage() ]); } } $this->paperName = $studentName . '_' . now()->format('Ymd_His') . '_智能试卷'; } $this->isGenerating = true; try { // 使用LearningAnalyticsService进行智能出卷 $learningAnalyticsService = app(LearningAnalyticsService::class); // 准备出卷参数 $examParams = [ 'student_id' => $this->selectedStudentId, 'total_questions' => $this->totalQuestions, 'kp_codes' => $this->selectedKpCodes, 'skills' => $this->selectedSkills, 'question_type_ratio' => $this->questionTypeRatio, 'difficulty_ratio' => $this->difficultyRatio, ]; // 调用智能出卷API $result = $learningAnalyticsService->generateIntelligentExam($examParams); if (!$result['success']) { throw new \Exception($result['message']); } $questions = $result['questions']; 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) { if (!in_array($newQ['id'], $existingIds)) { $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) { // 根据题型配比和难度配比对题目进行筛选和排序 $questions = $this->selectBestQuestions( $questions, $this->totalQuestions, $this->difficultyCategory, $this->totalScore, $this->questionTypeRatio ); \Illuminate\Support\Facades\Log::info("从 " . count($questions) . " 道题中筛选出 " . count($questions) . " 道题,难度分类: {$this->difficultyCategory}, 总分: {$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'])) { $questions = array_merge($questions, $newResponse['data']); } // 再次筛选 $questions = $this->selectBestQuestions( $questions, $this->totalQuestions, $this->difficultyCategory, $this->totalScore, $this->questionTypeRatio ); } // 2. 为题目添加类型信息(如果缺失) 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. 生成试卷数据 $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'); } \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) { // 记录错误并提供回退的试卷 ID,防止 UI 无显示 \Illuminate\Support\Facades\Log::error('生成试卷失败', ['error' => $e->getMessage()]); $fallbackId = 'demo_' . $this->selectedStudentId . '_' . now()->format('YmdHis'); $this->generatedPaperId = $fallbackId; $this->generatedQuestions = []; Notification::make() ->title('试卷生成失败,使用默认试卷') ->body('错误: ' . $e->getMessage() . "\n已生成默认试卷 ID: $fallbackId") ->warning() ->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, float $totalScore, array $questionTypeRatio ): array { // 去重:确保输入题目列表没有重复ID $uniqueQuestions = []; foreach ($questions as $q) { $id = $q['id'] ?? $q['question_id'] ?? null; if ($id && !isset($uniqueQuestions[$id])) { $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' => [], // 解答题 ]; foreach ($questions as $question) { $type = $this->determineQuestionType($question); if (!isset($categorizedQuestions[$type])) { $type = 'answer'; } $categorizedQuestions[$type][] = $question; } // 2. 根据难度分类筛选题目 $difficultyFilteredQuestions = $this->filterByDifficulty($categorizedQuestions, $difficultyCategory); // 3. 根据题型配比计算每种题型应选择的题目数量 $selectedQuestions = []; $selectedIds = []; // 用于追踪已选题目ID // 优先保证每种题型至少一题(适用于总题目数>=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 ($questionTypeRatio as $type => $ratio) { $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer'); // 计算该类型目标数量 $targetTypeCount = floor($targetCount * $ratio / 100); // 调整目标数量:如果总数>=3,需要考虑已经选了的题目 // 简单起见,我们计算总共需要的数量,然后减去已经选了的数量 // 但这里为了保证比例,我们还是尽量多选 if ($targetTypeCount <= 0) continue; if (!empty($difficultyFilteredQuestions[$typeKey])) { $availableQuestions = $difficultyFilteredQuestions[$typeKey]; // 过滤掉已选的 $availableQuestions = array_filter($availableQuestions, function($q) use ($selectedIds) { $id = $q['id'] ?? $q['question_id']; return !in_array($id, $selectedIds); }); // 如果没有可用题目了,跳过 if (empty($availableQuestions)) continue; $availableCount = count($availableQuestions); // 还需要选多少:目标数量 - 已选该类型的数量 $currentTypeCount = 0; foreach ($selectedQuestions as $sq) { if ($this->determineQuestionType($sq) === $typeKey) { $currentTypeCount++; } } $needToSelect = $targetTypeCount - $currentTypeCount; if ($needToSelect > 0) { $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 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.4], '中等' => [0.3, 0.7], '拔高' => [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; } else { // 保留部分越界题目(如果该难度题目不足) if (count($filtered[$type]) < 2) { $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 { // 0. 如果题目已有明确类型,直接返回 if (!empty($question['type'])) { if ($question['type'] === 'choice' || $question['type'] === '选择题') return 'choice'; if ($question['type'] === 'fill' || $question['type'] === '填空题') return 'fill'; if ($question['type'] === 'answer' || $question['type'] === '解答题') return 'answer'; } $tags = $question['tags'] ?? ''; $stem = $question['stem'] ?? $question['content'] ?? ''; // 1. 根据标签判断 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'; } } // 2. 根据题干内容判断 - 填空题优先(有下划线) // 填空题特征:连续的下划线,或者括号中明显是填空的(通常不会有选项) if (is_string($stem)) { // 检查填空题特征:连续下划线 if (strpos($stem, '____') !== false || strpos($stem, '______') !== false) { return 'fill'; } } // 3. 根据题干内容判断 - 选择题 // 选择题特征:必须包含选项 A. B. C. D. if (is_string($stem)) { // 检查选项格式 A. B. C. D.(支持跨行匹配) // 更严格的正则:A. 后面跟内容,或者 (A) 后面跟内容 if (preg_match('/[A-D]\s*\./', $stem) || preg_match('/\([A-D]\)/', $stem)) { // 再次确认是否包含多个选项,防止误判 if (preg_match('/A\./', $stem) && preg_match('/B\./', $stem)) { return 'choice'; } } // 如果只有括号但没有选项,可能是填空题 // 比如 "计算:(1) ... (2) ..." 这种是解答题 // "若 x > 0,则 x + 1 ( )" 这种可能是填空也可能是选择,取决于是否有选项 // 这里我们保守一点,如果没有选项特征,就不认为是选择题 } // 4. 再次检查填空题特征(括号填空) if (is_string($stem)) { // 只有括号且没有选项,通常是填空 if ((strpos($stem, '()') !== false || strpos($stem, '()') !== false) && !preg_match('/[A-D]\./', $stem)) { return 'fill'; } } // 5. 根据题干长度和内容判断(启发式) if (is_string($stem)) { // 有证明、解答、计算、求证等关键词的是解答题 if (strpos($stem, '证明') !== false || strpos($stem, '求证') !== false || strpos($stem, '解方程') !== false || strpos($stem, '计算:') !== false) { return 'answer'; } } // 默认是解答题 return 'answer'; } /** * 保留旧方法以兼容(但不再使用) */ 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 ]); } public function resetForm() { $this->reset([ 'paperName', 'paperDescription', '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; } } }