|
@@ -0,0 +1,962 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Filament\Pages;
|
|
|
|
|
+
|
|
|
|
|
+use App\Services\KnowledgeGraphService;
|
|
|
|
|
+use App\Services\LearningAnalyticsService;
|
|
|
|
|
+use App\Services\QuestionBankService;
|
|
|
|
|
+use BackedEnum;
|
|
|
|
|
+use Filament\Notifications\Notification;
|
|
|
|
|
+use Filament\Pages\Page;
|
|
|
|
|
+use UnitEnum;
|
|
|
|
|
+use Livewire\Attributes\Computed;
|
|
|
|
|
+use Livewire\Attributes\On;
|
|
|
|
|
+use Livewire\Component;
|
|
|
|
|
+use Illuminate\Support\Facades\Cache; // Add Cache import
|
|
|
|
|
+
|
|
|
|
|
+class IntelligentExamGeneration extends Page
|
|
|
|
|
+{
|
|
|
|
|
+ protected static ?string $title = '智能出卷';
|
|
|
|
|
+ protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-duplicate';
|
|
|
|
|
+ protected static ?string $navigationLabel = '智能出卷';
|
|
|
|
|
+ protected static string|UnitEnum|null $navigationGroup = '题库系统';
|
|
|
|
|
+ protected static ?int $navigationSort = 3;
|
|
|
|
|
+
|
|
|
|
|
+ protected string $view = 'filament.pages.intelligent-exam-generation-simple';
|
|
|
|
|
+
|
|
|
|
|
+ // 基本配置
|
|
|
|
|
+ public ?string $paperName = '';
|
|
|
|
|
+ public ?string $paperDescription = '';
|
|
|
|
|
+ public ?string $difficultyCategory = '基础'; // 基础/进阶/竞赛
|
|
|
|
|
+ public int $totalQuestions = 20;
|
|
|
|
|
+ public int $totalScore = 100;
|
|
|
|
|
+
|
|
|
|
|
+ // 知识点和技能点选择
|
|
|
|
|
+ public array $selectedKpCodes = [];
|
|
|
|
|
+ public array $selectedSkills = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 题型配比
|
|
|
|
|
+ public array $questionTypeRatio = [
|
|
|
|
|
+ '选择题' => 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(cache: false)]
|
|
|
|
|
+ public function knowledgePoints(): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $result = app(KnowledgeGraphService::class)->listKnowledgePoints(1, 1000);
|
|
|
|
|
+ return $result['data'] ?? [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[Computed(cache: false)]
|
|
|
|
|
+ public function teachers(): array
|
|
|
|
|
+ {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 首先获取teachers表中的老师
|
|
|
|
|
+ $teachers = \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'
|
|
|
|
|
+ )
|
|
|
|
|
+ ->orderBy('t.name')
|
|
|
|
|
+ ->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 {
|
|
|
|
|
+ return \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.name')
|
|
|
|
|
+ ->orderBy('s.name')
|
|
|
|
|
+ ->get()
|
|
|
|
|
+ ->all();
|
|
|
|
|
+ } 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) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return app(LearningAnalyticsService::class)->getStudentWeaknesses($this->selectedStudentId);
|
|
|
|
|
+ } 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 updatedSelectedStudentId($value)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($this->filterByStudentWeakness && $value) {
|
|
|
|
|
+ // 根据学生薄弱点自动选择知识点
|
|
|
|
|
+ $weaknesses = $this->studentWeaknesses;
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($weaknesses)) {
|
|
|
|
|
+ Notification::make()
|
|
|
|
|
+ ->title('提示')
|
|
|
|
|
+ ->body('该学生暂无薄弱点数据,将随机生成题目或根据年级推荐')
|
|
|
|
|
+ ->warning()
|
|
|
|
|
+ ->send();
|
|
|
|
|
+
|
|
|
|
|
+ // 保持选中状态,但不自动勾选知识点,或者可以选择取消勾选
|
|
|
|
|
+ // $this->filterByStudentWeakness = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $this->selectedKpCodes = array_slice(array_column($weaknesses, 'kp_code'), 0, 5);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function generateExam()
|
|
|
|
|
+ {
|
|
|
|
|
+ \Illuminate\Support\Facades\Log::info('generateExam called with studentId=' . $this->selectedStudentId);
|
|
|
|
|
+ $this->validate([
|
|
|
|
|
+ // 'paperName' => 'required|string|max:255', // 已移除必填
|
|
|
|
|
+ 'totalQuestions' => 'required|integer|min:6|max:100',
|
|
|
|
|
+ 'selectedStudentId' => 'required', // 必选学生
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ // 确保题目数量至少6题
|
|
|
|
|
+ if ($this->totalQuestions < 6) {
|
|
|
|
|
+ \Illuminate\Support\Facades\Log::warning('题目数量少于6题,已自动调整为6题', ['original' => $this->totalQuestions]);
|
|
|
|
|
+ $this->totalQuestions = 6;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 自动生成试卷名称
|
|
|
|
|
+ if (empty($this->paperName)) {
|
|
|
|
|
+ $studentName = '学生' . $this->selectedStudentId;
|
|
|
|
|
+ // 尝试从 students 列表中获取真实姓名
|
|
|
|
|
+ foreach ($this->students as $student) {
|
|
|
|
|
+ if (is_array($student)) {
|
|
|
|
|
+ $sId = $student['student_id'] ?? '';
|
|
|
|
|
+ if ($sId == $this->selectedStudentId) {
|
|
|
|
|
+ $studentName = $student['name'] ?? $studentName;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ } elseif (is_object($student)) {
|
|
|
|
|
+ if ($student->student_id == $this->selectedStudentId) {
|
|
|
|
|
+ $studentName = $student->name ?? $studentName;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $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. 限制试卷题目数量为用户要求的数量
|
|
|
|
|
+ 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)
|
|
|
|
|
+ {
|
|
|
|
|
+ $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 {
|
|
|
|
|
+ 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. 根据题型配比计算每种题型应选择的题目数量
|
|
|
|
|
+ // 先确保每种题型至少有1题(如果题目数量>=3)
|
|
|
|
|
+ $selectedQuestions = [];
|
|
|
|
|
+ $totalSelected = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 优先保证每种题型至少一题(适用于总题目数>=3的情况)
|
|
|
|
|
+ if ($targetCount >= 3) {
|
|
|
|
|
+ foreach (['choice', 'fill', 'answer'] as $typeKey) {
|
|
|
|
|
+ if (!empty($difficultyFilteredQuestions[$typeKey])) {
|
|
|
|
|
+ // 随机选择1道该题型的题目
|
|
|
|
|
+ $randomIndex = array_rand($difficultyFilteredQuestions[$typeKey]);
|
|
|
|
|
+ $selectedQuestions[] = $difficultyFilteredQuestions[$typeKey][$randomIndex];
|
|
|
|
|
+ $totalSelected++;
|
|
|
|
|
+
|
|
|
|
|
+ \Illuminate\Support\Facades\Log::info("保证题型最少题目: {$typeKey}", [
|
|
|
|
|
+ 'selected_index' => $randomIndex
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果某题型没有题目,标记为缺失
|
|
|
|
|
+ \Illuminate\Support\Facades\Log::warning("题型缺失: {$typeKey},需要从其他题型补充");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果题目数量不足3题,则跳过最少保证
|
|
|
|
|
+ // 根据题型配比计算每种题型应选择的题目数量
|
|
|
|
|
+ foreach ($questionTypeRatio as $type => $ratio) {
|
|
|
|
|
+ $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer');
|
|
|
|
|
+ $countForType = floor($targetCount * $ratio / 100);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果总题目数>=3,已经为每种题型分配了1题,需要从剩余数量中扣除
|
|
|
|
|
+ if ($targetCount >= 3 && $totalSelected > 0) {
|
|
|
|
|
+ // 重新计算:确保至少1题 + 按比例分配的额外题目
|
|
|
|
|
+ $baseCount = 1; // 最少1题
|
|
|
|
|
+ $extraCount = floor(($targetCount - 3) * $ratio / 100); // 剩余题目按比例分配
|
|
|
|
|
+ $countForType = $baseCount + $extraCount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($countForType > 0 && !empty($difficultyFilteredQuestions[$typeKey])) {
|
|
|
|
|
+ // 按难度排序后选择该题型的一部分题目
|
|
|
|
|
+ $availableCount = count($difficultyFilteredQuestions[$typeKey]);
|
|
|
|
|
+ $takeCount = min($countForType, $availableCount, $targetCount - $totalSelected);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果该题型已经在最少保证中分配过,需要排除已分配的题目
|
|
|
|
|
+ if ($targetCount >= 3 && isset($selectedQuestions)) {
|
|
|
|
|
+ // 重新获取题目,排除已选择的
|
|
|
|
|
+ $availableQuestions = $difficultyFilteredQuestions[$typeKey];
|
|
|
|
|
+ $takeCount = min($takeCount, $availableCount, $targetCount - $totalSelected);
|
|
|
|
|
+ if ($takeCount > 0) {
|
|
|
|
|
+ $selectedFromType = array_rand(array_flip(array_keys($availableQuestions)), $takeCount);
|
|
|
|
|
+ if (!is_array($selectedFromType)) {
|
|
|
|
|
+ $selectedFromType = [$selectedFromType];
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($selectedFromType as $index) {
|
|
|
|
|
+ $selectedQuestions[] = $availableQuestions[$index];
|
|
|
|
|
+ }
|
|
|
|
|
+ $totalSelected += $takeCount;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 正常分配
|
|
|
|
|
+ if ($takeCount > 0) {
|
|
|
|
|
+ $selectedFromType = array_rand(array_flip(array_keys($difficultyFilteredQuestions[$typeKey])), $takeCount);
|
|
|
|
|
+ if (!is_array($selectedFromType)) {
|
|
|
|
|
+ $selectedFromType = [$selectedFromType];
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($selectedFromType as $index) {
|
|
|
|
|
+ $selectedQuestions[] = $difficultyFilteredQuestions[$typeKey][$index];
|
|
|
|
|
+ }
|
|
|
|
|
+ $totalSelected += $takeCount;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ \Illuminate\Support\Facades\Log::info("{$type}题型筛选结果", [
|
|
|
|
|
+ 'available' => $availableCount,
|
|
|
|
|
+ 'take' => $takeCount,
|
|
|
|
|
+ 'ratio' => $ratio,
|
|
|
|
|
+ 'type' => $typeKey
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 如果还有空缺,随机补充其他题型
|
|
|
|
|
+ while ($totalSelected < $targetCount && count($selectedQuestions) < count($questions)) {
|
|
|
|
|
+ $randomQuestion = $questions[array_rand($questions)];
|
|
|
|
|
+ if (!in_array($randomQuestion, $selectedQuestions)) {
|
|
|
|
|
+ $selectedQuestions[] = $randomQuestion;
|
|
|
|
|
+ $totalSelected++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ \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
|
|
|
|
|
+ {
|
|
|
|
|
+ $tags = $question['tags'] ?? '';
|
|
|
|
|
+ $stem = $question['stem'] ?? '';
|
|
|
|
|
+
|
|
|
|
|
+ // 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. 根据题干内容判断 - 选择题(有括号的或包含选项A.B.C.D.)
|
|
|
|
|
+ if (is_string($stem)) {
|
|
|
|
|
+ // 检查全角括号
|
|
|
|
|
+ if (strpos($stem, '()') !== false) {
|
|
|
|
|
+ return 'choice';
|
|
|
|
|
+ }
|
|
|
|
|
+ // 检查半角括号
|
|
|
|
|
+ if (strpos($stem, '()') !== false) {
|
|
|
|
|
+ return 'choice';
|
|
|
|
|
+ }
|
|
|
|
|
+ // 检查选项格式 A. B. C. D.(支持跨行匹配)
|
|
|
|
|
+ if (preg_match('/[A-D]\.\s/m', $stem)) {
|
|
|
|
|
+ return 'choice';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 根据题干内容判断 - 填空题(有下划线)
|
|
|
|
|
+ if (is_string($stem) && (strpos($stem, '____') !== false || strpos($stem, '______') !== false)) {
|
|
|
|
|
+ return 'fill';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 根据题干长度和内容判断(启发式)
|
|
|
|
|
+ if (is_string($stem)) {
|
|
|
|
|
+ $shortQuestions = ['下列', '判断', '选择', '计算', '求'];
|
|
|
|
|
+ $isShort = false;
|
|
|
|
|
+ foreach ($shortQuestions as $keyword) {
|
|
|
|
|
+ if (strpos($stem, $keyword) !== false) {
|
|
|
|
|
+ $isShort = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 短题目通常是选择题或填空题
|
|
|
|
|
+ if ($isShort && mb_strlen($stem) < 100) {
|
|
|
|
|
+ return 'choice';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 有证明、解答等关键词的是解答题
|
|
|
|
|
+ if (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,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+}
|