initializeUserRole(); // 如果是老师,自动选择当前老师 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $this->teacherId = $teacherId; } } else { $this->teacherId = null; } $this->studentId = null; $this->uploadedImage = null; $this->paperType = null; $this->mode = 'upload'; $this->selectedPaperId = null; $this->questionGrades = []; } public function form(Forms\Form $form): Forms\Form { return $form ->statePath('data') ->schema([ Forms\Components\FileUpload::make('image') ->label('上传试卷图片') ->image() ->multiple() ->directory('exam-papers') ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/jpg']) ->helperText('支持PNG、JPG、JPEG格式,可同时上传多张图片') ->maxFiles(10) ->required(), Forms\Components\TextInput::make('paper_name') ->label('试卷名称') ->required() ->placeholder('例如:数学期末考试'), Forms\Components\Select::make('class') ->label('班级') ->options([ 'ClassA' => '三年级一班', 'ClassB' => '三年级二班', 'ClassC' => '四年级一班', 'ClassD' => '四年级二班', 'ClassE' => '五年级一班', 'ClassF' => '五年级二班', 'ClassG' => '六年级一班', 'ClassH' => '六年级二班', ]) ->required(), Forms\Components\TextInput::make('student_name') ->label('学生姓名') ->required() ->placeholder('请输入学生姓名'), Forms\Components\Select::make('paper_type') ->label('试卷类型') ->options([ 'quiz' => '课堂测验', 'midterm' => '期中考试', 'final' => '期末考试', 'homework' => '家庭作业', ]) ->default('quiz') ->required(), Forms\Components\TextInput::make('paper_subject') ->label('科目') ->default('数学') ->required(), ]); } #[Computed] public function teachers(): array { try { $query = Teacher::query() ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id') ->select( 'teachers.teacher_id', 'teachers.name', 'teachers.subject', 'u.username', 'u.email' ); // 如果是老师,只返回自己 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $query->where('teachers.teacher_id', $teacherId); } } $teachers = $query->orderBy('teachers.name')->get(); // 检查是否有学生没有对应的老师记录 $teacherIds = $teachers->pluck('teacher_id')->toArray(); $missingTeacherIds = Student::query() ->distinct() ->whereNotIn('teacher_id', $teacherIds) ->pluck('teacher_id') ->toArray(); $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; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('加载老师列表失败', [ 'error' => $e->getMessage() ]); return []; } } #[Computed] public function students(): array { if (empty($this->teacherId)) { return []; } try { return Student::query() ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id') ->where('students.teacher_id', $this->teacherId) ->select( 'students.student_id', 'students.name', 'students.grade', 'students.class_name', 'u.username', 'u.email' ) ->orderBy('students.grade') ->orderBy('students.class_name') ->orderBy('students.name') ->get() ->all(); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('加载学生列表失败', [ 'teacher_id' => $this->teacherId, 'error' => $e->getMessage() ]); return []; } } #[Computed] public function recentRecords(): array { // 1. 获取OCR记录(图片上传) $ocrQuery = OCRRecord::with('student'); // 如果选择了学生,则筛选该学生的记录 if (!empty($this->studentId)) { $ocrQuery->where('user_id', $this->studentId); } $ocrRecords = $ocrQuery->latest()->take(5)->get() ->map(function($record) { $studentName = $record->student?->name ?: ('学生ID: ' . $record->user_id); return [ 'type' => 'ocr_upload', 'id' => $record->id, 'record_id' => $record->id, 'paper_id' => null, 'student_id' => $record->user_id, 'student_name' => $studentName, 'paper_type' => $record->paper_type_label, 'paper_name' => $record->image_filename ?: '未命名图片', 'status' => $record->status, 'total_questions' => $record->total_questions, 'processed_questions' => $record->processed_questions ?? 0, 'created_at' => $record->created_at->format('Y-m-d H:i'), 'is_completed' => $record->status === 'completed', ]; })->toArray(); // 2. 获取所有Paper记录(包括草稿和已评分) $paperQuery = \App\Models\Paper::with('student'); // 如果选择了学生,则筛选该学生的记录 if (!empty($this->studentId)) { $paperQuery->where('student_id', $this->studentId); } $allPapers = $paperQuery->latest()->take(5)->get() ->map(function($paper) { $type = $paper->status === 'completed' ? 'graded_paper' : 'generated'; $paperType = $paper->status === 'completed' ? '已评分试卷' : '系统生成试卷'; $iconColor = $paper->status === 'completed' ? 'text-green-500' : 'text-blue-500'; $studentName = $paper->student?->name ?: ('学生ID: ' . $paper->student_id); return [ 'type' => $type, 'id' => $paper->paper_id, 'record_id' => null, 'paper_id' => $paper->paper_id, 'student_id' => $paper->student_id, 'student_name' => $studentName, 'paper_type' => $paperType, 'paper_name' => $paper->paper_name ?? '未命名试卷', 'status' => $paper->difficulty_category, 'total_questions' => $paper->question_count ?? 0, 'created_at' => $paper->created_at->format('Y-m-d H:i'), 'is_completed' => $paper->status === 'completed', 'icon_color' => $iconColor, ]; })->toArray(); // 3. 合并并按时间排序 $allRecords = array_merge($ocrRecords, $allPapers); usort($allRecords, function($a, $b) { return strcmp($b['created_at'], $a['created_at']); }); return array_slice($allRecords, 0, 10); } /** * 获取学生的试卷列表 */ #[Computed] public function studentPapers(): array { if (empty($this->studentId)) { return []; } try { // 使用 Student 关联查询试卷 $student = \App\Models\Student::find($this->studentId); if (!$student) { \Log::warning('未找到指定学生', ['student_id' => $this->studentId]); return []; } return $student->papers() ->withCount('questions') // 添加题目计数 ->orderBy('created_at', 'desc') ->take(20) ->get() ->map(function($paper) { return [ 'paper_id' => $paper->paper_id, // 使用 paper_id 而不是 id 'paper_name' => $paper->paper_name ?? '未命名试卷', 'total_questions' => $paper->questions_count ?? 0, 'total_score' => $paper->total_score ?? 0, 'created_at' => $paper->created_at->format('Y-m-d H:i'), ]; }) ->toArray(); } catch (\Exception $e) { \Log::error('获取学生试卷列表失败', [ 'student_id' => $this->studentId, 'error' => $e->getMessage() ]); return []; } } #[Computed] public function paperTypes(): array { return [ '' => '请选择试卷形式', 'unit_test' => '单元测试', 'midterm' => '期中考试', 'final' => '期末考试', 'homework' => '家庭作业', 'quiz' => '随堂测验', 'other' => '其他', ]; } /** * 获取选中试卷的题目列表 */ #[Computed] public function selectedPaperQuestions(): array { if (empty($this->selectedPaperId)) { return []; } try { // 首先检查试卷是否存在 $paper = \App\Models\Paper::where('paper_id', $this->selectedPaperId)->first(); if (!$paper) { \Log::warning('未找到指定试卷', ['paper_id' => $this->selectedPaperId]); return []; } // 使用关联关系查询题目 $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) { $query->orderBy('question_number'); }])->where('paper_id', $this->selectedPaperId)->first(); $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]); // 处理数据不一致的情况:如果题目为空但试卷显示有题目 if ($questions->isEmpty() && ($paper->question_count ?? 0) > 0) { \Log::warning('试卷显示有题目但实际题目数据缺失', [ 'paper_id' => $this->selectedPaperId, 'expected_questions' => $paper->question_count, 'actual_questions' => 0 ]); // 返回占位题目,让用户知道有数据缺失 return [ [ 'id' => 'missing_data', 'question_number' => 1, 'question_bank_id' => null, 'question_type' => 'info', 'content' => "⚠️ 数据异常:试卷显示应有 {$paper->question_count} 道题目,但未找到题目数据。这通常是试卷创建过程中断导致的。请联系管理员或重新创建试卷。", 'answer' => '', 'score' => 0, 'is_missing_data' => true ] ]; } if ($questions->isEmpty()) { \Log::info('试卷确实没有题目', ['paper_id' => $this->selectedPaperId]); return [ [ 'id' => 'no_questions', 'question_number' => 1, 'question_bank_id' => null, 'question_type' => 'info', 'content' => '该试卷暂无题目数据', 'answer' => '', 'score' => 0, 'is_empty' => true ] ]; } // 获取题目详情 $questionBankService = app(\App\Services\QuestionBankService::class); $questionIds = $questions->pluck('question_bank_id')->filter()->unique()->toArray(); if (empty($questionIds)) { \Log::info('题目没有关联题库ID', ['paper_id' => $this->selectedPaperId]); // 返回基本的题目信息,不包含题库详情 return $questions->map(function($q) { return [ 'id' => $q->id, 'question_number' => $q->question_number, 'question_bank_id' => $q->question_bank_id, 'question_type' => $q->question_type, 'content' => '题目内容未关联到题库', 'answer' => '', 'score' => $q->score ?? 5, ]; })->toArray(); } $questionsResponse = $questionBankService->getQuestionsByIds($questionIds); $questionDetails = collect($questionsResponse['data'] ?? [])->keyBy('id'); return $questions->map(function($q) use ($questionDetails) { $detail = $questionDetails->get($q->question_bank_id); return [ 'id' => $q->id, 'question_number' => $q->question_number, 'question_bank_id' => $q->question_bank_id, 'question_type' => $q->question_type, 'content' => $detail['stem'] ?? '题目内容缺失', 'answer' => $detail['answer'] ?? '', 'score' => $q->score ?? 5, 'kp_code' => $q->knowledge_point, // 从本地数据库获取知识点代码 ]; })->toArray(); } catch (\Exception $e) { \Log::error('获取试卷题目失败', [ 'paper_id' => $this->selectedPaperId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ [ 'id' => 'error', 'question_number' => 1, 'question_bank_id' => null, 'question_type' => 'error', 'content' => '获取题目数据时发生错误:' . $e->getMessage(), 'answer' => '', 'score' => 0, 'is_error' => true ] ]; } } public function updatedTeacherId($value): void { // 当教师选择变化时,清空之前选择的学生 $this->studentId = null; $this->selectedPaperId = null; $this->questionGrades = []; } public function updatedStudentId($value): void { // 当学生选择变化时,清空已选试卷 $this->selectedPaperId = null; $this->questionGrades = []; } public function updatedMode($value): void { // 切换模式时重置相关字段 $this->uploadedImage = null; $this->selectedPaperId = null; $this->questionGrades = []; } public function submitUpload(): void { if (!$this->teacherId) { Notification::make() ->title('请选择老师') ->danger() ->send(); return; } if (!$this->studentId) { Notification::make() ->title('请选择学生') ->danger() ->send(); return; } // 获取表单数据 $formData = $this->data; if (empty($formData['image'])) { Notification::make() ->title('请上传图片') ->danger() ->send(); return; } if (empty($formData['paper_name'])) { Notification::make() ->title('请填写试卷名称') ->danger() ->send(); return; } if (empty($formData['class'])) { Notification::make() ->title('请选择班级') ->danger() ->send(); return; } if (empty($formData['student_name'])) { Notification::make() ->title('请填写学生姓名') ->danger() ->send(); return; } $this->isUploading = true; try { // 处理图片(可能是单张或多张) $images = $formData['image']; if (!is_array($images)) { $images = [$images]; } $paths = []; foreach ($images as $image) { if ($image) { $paths[] = storage_path('app/public/' . $image); } } if (empty($paths)) { throw new \Exception('图片保存失败'); } $paperId = 'paper_' . time() . '_' . substr(md5(uniqid()), 0, 8); // AI分析服务调用 $response = \Http::timeout(300) ->post('http://localhost:5016/analyze-exam', [ 'paper_id' => $paperId, 'paper_name' => $formData['paper_name'], 'student_name' => $formData['student_name'], 'class_name' => $formData['class'], 'paper_type' => $formData['paper_type'], 'subject' => $formData['paper_subject'], 'image_files' => $paths, ]); if ($response->successful()) { $result = $response->json(); $this->saveAnalysisResult($result, $paperId); $this->analysisResult = $result; Notification::make() ->title('分析完成') ->success() ->send(); } else { $this->analysisError = '分析服务响应失败: ' . $response->status(); Notification::make() ->title('分析失败') ->body($this->analysisError) ->error() ->send(); } // 重置表单 $this->teacherId = null; $this->studentId = null; $this->uploadedImage = null; $this->paperType = null; } catch (\Exception $e) { Notification::make() ->title('上传失败') ->body($e->getMessage()) ->danger() ->send(); } finally { $this->isUploading = false; $this->analyzing = false; } } #[On('teacherChanged')] public function updateTeacherId($teacherId) { $this->teacherId = $teacherId; $this->studentId = null; } #[On('studentChanged')] public function updateStudentId($teacherId, $studentId) { $this->studentId = $studentId; } public function removeImage(): void { $this->uploadedImage = null; } /** * 提交手动评分 */ public function submitManualGrading(): void { if (!$this->selectedPaperId) { Notification::make() ->title('请选择试卷') ->danger() ->send(); return; } // 将 gradingData 转换为 questionGrades 格式 $this->convertGradingDataToQuestionGrades(); if (empty($this->questionGrades)) { Notification::make() ->title('请至少为一道题目评分') ->danger() ->send(); return; } try { // 准备数据发送到 LearningAnalytics $analyticsData = []; // 获取题目详情以便查找kp_code $questionsMap = collect($this->selectedPaperQuestions)->keyBy('id'); // 收集需要从API补充信息的题目ID $missingKpCodeQuestionIds = []; foreach ($this->questionGrades as $questionId => $grade) { $question = $questionsMap->get($questionId); if (!$question) { continue; } // 优先使用本地存储的 kp_code if (empty($question['kp_code'])) { $missingKpCodeQuestionIds[] = $questionId; } } // 如果有缺失 kp_code 的题目,尝试从 API 获取 $apiDetailsMap = collect([]); if (!empty($missingKpCodeQuestionIds)) { $questionBankIds = collect($missingKpCodeQuestionIds) ->map(fn($qId) => $questionsMap->get($qId)['question_bank_id'] ?? null) ->filter() ->toArray(); if (!empty($questionBankIds)) { $questionBankService = app(\App\Services\QuestionBankService::class); $questionsDetails = $questionBankService->getQuestionsByIds($questionBankIds); $apiDetailsMap = collect($questionsDetails['data'] ?? [])->keyBy('id'); } } foreach ($this->questionGrades as $questionId => $grade) { $question = $questionsMap->get($questionId); if (!$question) { continue; } $kpCode = $question['kp_code']; // 如果本地没有,尝试从API结果中获取 if (empty($kpCode)) { $detail = $apiDetailsMap->get($question['question_bank_id']); $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null; } // 确保 is_correct 有值(如果为 null,设置为 false) $isCorrect = $grade['is_correct']; if ($isCorrect === null) { $isCorrect = false; } $analyticsData[] = [ 'question_bank_id' => $question['question_bank_id'], 'student_answer' => $grade['student_answer'] ?? '', 'is_correct' => $isCorrect, 'score' => $grade['score'] ?? 0, 'max_score' => $question['score'] ?? 0, 'kp_code' => $kpCode, 'ip_address' => '127.0.0.1', // 提供默认IP地址,避免PostgreSQL inet类型错误 'device_type' => 'web', // 提供默认设备类型 'feedback_provided' => false, // 提供默认反馈状态 ]; } // 调用 LearningAnalytics 服务 $learningAnalyticsService = app(\App\Services\LearningAnalyticsService::class); // 步骤0: 保存学生答案到本地数据库 (重要:确保数据持久化) foreach ($this->questionGrades as $questionId => $grade) { // 确保 is_correct 是布尔值(转换字符串 'true'/'false' 为布尔值) $isCorrect = $grade['is_correct']; if ($isCorrect === 'true' || $isCorrect === true) { $isCorrect = true; } elseif ($isCorrect === 'false' || $isCorrect === false) { $isCorrect = false; } // 确保 score_obtained 是数字 $score = $grade['score']; if ($score !== null) { $score = is_numeric($score) ? (float)$score : 0; } // **关键修复**:确保 is_correct 和 score 的一致性 // 如果分数为满分或大于0,视为正确 if ($score > 0) { $isCorrect = true; } elseif ($score === 0) { $isCorrect = false; } \App\Models\PaperQuestion::where('id', $questionId)->update([ 'student_answer' => $grade['student_answer'] ?? '', 'is_correct' => $isCorrect, 'score_obtained' => $score ?? 0, ]); } \Log::info('学生答案已保存到数据库', [ 'student_id' => $this->studentId, 'paper_id' => $this->selectedPaperId, 'updated_count' => count($this->questionGrades) ]); // 步骤1: 保存答题记录到 LearningAnalytics \Log::info('准备调用submitBatchAttempts API', [ 'student_id' => $this->studentId, 'paper_id' => $this->selectedPaperId, 'analytics_data_sample' => array_slice($analyticsData, 0, 2) // 记录前2题的数据作为样本 ]); $result = $learningAnalyticsService->submitBatchAttempts($this->studentId, [ 'paper_id' => $this->selectedPaperId, 'answers' => $analyticsData, ]); // 检查API返回结果 if (is_array($result) && isset($result['error']) && $result['error']) { throw new \Exception($result['message'] ?? 'API调用失败'); } if ($result === null || (is_array($result) && empty($result))) { throw new \Exception('API返回空数据'); } \Log::info('答题记录已保存到学习分析服务', [ 'student_id' => $this->studentId, 'paper_id' => $this->selectedPaperId, 'count' => count($analyticsData) ]); // 步骤2: 触发 AI 分析(包含掌握度更新和学习报告生成) try { $paper = \App\Models\Paper::find($this->selectedPaperId); // 构造 AI 分析请求数据 $analysisQuestions = []; foreach ($this->questionGrades as $questionId => $grade) { $question = $questionsMap->get($questionId); if (!$question) { continue; } $kpCode = $question['kp_code']; if (empty($kpCode)) { $detail = $apiDetailsMap->get($question['question_bank_id']); $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null; } $analysisQuestions[] = [ 'question_id' => $question['question_bank_id'], 'question_number' => (string)$question['question_number'], 'question_text' => $question['content'] ?? '', 'student_answer' => $grade['student_answer'] ?? '', 'correct_answer' => $question['answer'] ?? '', 'kp_code' => $kpCode, 'score_value' => $grade['score'] ?? 0, 'max_score' => $question['score'], 'is_correct' => $grade['is_correct'] ?? false, 'teacher_validated' => true, // 手动评分即为教师验证 'ocr_confidence' => 1.0, // 手动评分置信度为1 ]; } $analysisData = [ 'exam_id' => $this->selectedPaperId, 'student_id' => $this->studentId, 'ocr_record_id' => 0, // 系统生成卷子没有OCR记录ID 'paper_id' => $this->selectedPaperId, 'teacher_name' => auth()->user()->name ?? 'Teacher', 'analysis_type' => 'mastery', 'questions' => $analysisQuestions, ]; // 调用统一的 AI 分析接口 \Log::info('准备调用submitOCRAnalysis API', [ 'paper_id' => $this->selectedPaperId, 'student_id' => $this->studentId, 'analysis_data_sample' => [ 'question_count' => count($analysisQuestions), 'first_question' => $analysisQuestions[0] ?? null ] ]); $analysisResult = $learningAnalyticsService->submitOCRAnalysis($analysisData); \Log::info('AI分析已触发', [ 'paper_id' => $this->selectedPaperId, 'student_id' => $this->studentId, 'analysis_result_keys' => is_array($analysisResult) ? array_keys($analysisResult) : 'not_array', 'analysis_result' => $analysisResult ]); // 保存 analysis_id 到 Paper 表 if (isset($analysisResult['analysis_id'])) { \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([ 'analysis_id' => $analysisResult['analysis_id'], ]); \Log::info('已保存 analysis_id', [ 'paper_id' => $this->selectedPaperId, 'analysis_id' => $analysisResult['analysis_id'] ]); } } catch (\Exception $analysisError) { // AI 分析失败不影响主流程 \Log::warning('触发AI分析失败', [ 'paper_id' => $this->selectedPaperId, 'error' => $analysisError->getMessage() ]); } // 更新Paper表状态为已完成评分 \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([ 'status' => 'completed', 'completed_at' => now(), ]); Notification::make() ->title('提交成功') ->body('评分已提交,AI分析正在进行中') ->success() ->send(); // 刷新最近记录列表 unset($this->recentRecords); // 重置表单 $this->selectedPaperId = null; $this->questionGrades = []; } catch (\Exception $e) { \Log::error('提交手动评分失败', [ 'error' => $e->getMessage(), 'student_id' => $this->studentId, 'paper_id' => $this->selectedPaperId, ]); Notification::make() ->title('提交失败') ->body($e->getMessage()) ->danger() ->send(); } } /** * 将 gradingData 转换为 questionGrades 格式 * gradingData: 索引数组 [{is_correct: bool, score: float}] * questionGrades: 题目ID为键的数组 [questionId => {is_correct: bool, score: float, student_answer: string}] */ private function convertGradingDataToQuestionGrades(): void { $this->questionGrades = []; // 遍历 questions 数组(包含题目信息) foreach ($this->questions as $index => $question) { // 获取对应索引的 gradingData $grading = $this->gradingData[$index] ?? null; // 只有当 grading 不为空且有评分数据时才添加 if ($grading && ( $grading['is_correct'] !== null || ($grading['score'] ?? null) !== null )) { $questionId = $question['id']; // 处理 is_correct 值(字符串 'true'/'false' 或布尔值) $isCorrect = $grading['is_correct']; if ($isCorrect === 'true') { $isCorrect = true; } elseif ($isCorrect === 'false') { $isCorrect = false; } // 处理 score 值 $score = $grading['score']; if ($score !== null && $score !== '') { $score = is_numeric($score) ? (float)$score : null; } // **关键修复**:根据题型处理缺失的字段 if ($question['question_type'] === 'choice') { // 选择题:只有 is_correct,需要自动计算分数 if ($isCorrect === true) { $score = $question['score'] ?? 0; // 正确给满分 } elseif ($isCorrect === false) { $score = 0; // 错误给0分 } } else { // 填空/解答题:只有 score,需要自动计算 is_correct if ($score !== null) { $isCorrect = ($score >= ($question['score'] ?? 0)); // 得分>=满分视为正确 } } // 获取学生答案(优先使用 gradingData 中的值,如果没有则使用题目中的值) $studentAnswer = $grading['student_answer'] ?? $question['student_answer'] ?? ''; // 对于选择题,如果学生答案为空,基于评分推断 if (empty($studentAnswer) && $question['question_type'] === 'choice') { if ($isCorrect === true) { // 如果选"正确",学生答案就是正确答案 $studentAnswer = $question['correct_answer'] ?? '正确答案'; } elseif ($isCorrect === false) { // 如果选"错误",学生答案可以为空或者设置为特殊标记 $studentAnswer = '错误答案'; } } // 转换格式 $this->questionGrades[$questionId] = [ 'is_correct' => $isCorrect, 'score' => $score, 'student_answer' => $studentAnswer, ]; } } \Log::info('转换评分数据', [ 'grading_data_count' => count(array_filter($this->gradingData ?? [])), 'question_grades_count' => count($this->questionGrades), 'questions_count' => count($this->questions ?? []), 'sample_question_grades' => array_slice($this->questionGrades, 0, 2, true), ]); } #[Computed] public function gradingProgress(): string { $gradedCount = count(array_filter($this->gradingData ?? [])); $totalCount = count($this->questions ?? []); return "已评分:{$gradedCount}/{$totalCount}题"; } public function startAnalysis(): void { $this->analyzing = true; $this->analysisError = null; try { $this->submitUpload(); } catch (\Exception $e) { $this->analysisError = $e->getMessage(); $this->analyzing = false; } } public function saveGrading(): void { $this->submitManualGrading(); } public function updatedSelectedPaperId($value): void { if (empty($value)) { $this->questions = []; $this->gradingData = []; $this->showGrading = false; return; } // 加载试卷信息和题目 $this->loadPaperForGrading($value); } public function loadPaperForGrading($paperId): void { try { $paper = \App\Models\Paper::where('paper_id', $paperId)->first(); if (!$paper) { Notification::make() ->title('试卷不存在') ->danger() ->send(); return; } // 设置试卷信息 $this->paperName = $paper->paper_name; $this->paperClass = $paper->difficulty_category ?? '未设置'; $this->paperStudent = $paper->student_id; $this->paperDate = $paper->created_at->format('Y-m-d H:i'); // 加载题目 $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) { $query->orderBy('question_number'); }])->where('paper_id', $paperId)->first(); $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]); // 如果没有正确答案,先尝试从题库API获取 $apiDetailsMap = new \Illuminate\Support\Collection(); if (!$questions->isEmpty()) { $questionBankIds = $questions->where('question_bank_id', '!=', null)->pluck('question_bank_id')->unique()->toArray(); if (!empty($questionBankIds)) { try { $questionBankService = app(\App\Services\QuestionBankService::class); $apiResponse = $questionBankService->getQuestionsByIds($questionBankIds); if (!empty($apiResponse['data'])) { foreach ($apiResponse['data'] as $detail) { $apiDetailsMap->put($detail['id'], $detail); } \Log::info('成功从题库API获取题目详情', [ 'count' => count($apiResponse['data']), 'ids' => array_keys($apiResponse['data']) ]); } } catch (\Exception $e) { \Log::warning('获取题库详情失败', ['error' => $e->getMessage()]); } } } if ($questions->isEmpty()) { $this->questions = [ [ 'id' => 'no_questions', 'question_number' => 1, 'question_type' => 'info', 'content' => '该试卷暂无题目数据', 'answer' => '', 'score' => 0, 'is_empty' => true ] ]; } else { $this->questions = $questions->map(function($question, $index) use ($apiDetailsMap) { // 从 API 获取正确答案(优先使用 API 数据) $correctAnswer = $question->correct_answer; if (empty($correctAnswer) && $question->question_bank_id && $apiDetailsMap->has($question->question_bank_id)) { $detail = $apiDetailsMap->get($question->question_bank_id); $correctAnswer = $detail['answer'] ?? $detail['correct_answer'] ?? ''; } return [ 'id' => $question->id, 'question_number' => $question->question_number, 'question_type' => $question->question_type, 'question_text' => $question->question_text, 'content' => $question->question_text, 'options' => json_decode($question->options, true) ?: [], 'answer' => $correctAnswer, 'correct_answer' => $correctAnswer, 'student_answer' => '', // 学生答案暂不显示,等后续完善 'score' => $question->score, 'max_score' => $question->score, 'question_bank_id' => $question->question_bank_id, 'is_empty' => false ]; })->toArray(); } // 初始化评分数据 $this->gradingData = array_fill(0, count($this->questions), ['score' => null, 'is_correct' => null, 'comment' => '']); $this->showGrading = true; } catch (\Exception $e) { \Log::error('加载试卷题目失败', [ 'paper_id' => $paperId, 'error' => $e->getMessage() ]); Notification::make() ->title('加载失败') ->body($e->getMessage()) ->danger() ->send(); } } private function saveAnalysisResult(array $result, string $paperId): void { try { \DB::beginTransaction(); // 保存试卷基本信息 $examPaper = \App\Models\Paper::create([ 'paper_id' => $paperId, 'paper_name' => $result['paper_name'] ?? '未命名试卷', 'student_id' => $this->studentId, 'teacher_id' => $this->teacherId, 'paper_type' => $result['paper_type'] ?? 'quiz', 'question_count' => count($result['questions'] ?? []), 'total_score' => $result['total_score'] ?? 0, 'status' => 'completed', ]); // 保存题目信息 foreach ($result['questions'] ?? [] as $index => $questionData) { \App\Models\PaperQuestion::create([ 'paper_id' => $paperId, 'question_number' => $index + 1, 'question_text' => $questionData['question_text'] ?? '', 'question_type' => $questionData['question_type'] ?? 'choice', 'options' => json_encode($questionData['options'] ?? []), 'correct_answer' => $questionData['correct_answer'] ?? '', 'score' => $questionData['score'] ?? 1, ]); } \DB::commit(); } catch (\Exception $e) { \DB::rollBack(); \Log::error('保存分析结果失败: ' . $e->getMessage()); } } /** * 查看记录详情 - 使用页面跳转 */ public function getViewRecordUrl(string $type, string $paperId, string $recordId, string $studentId): string { // 返回ExamAnalysis详情页面URL if (in_array($type, ['graded_paper', 'generated'])) { // 系统生成或已评分试卷,使用paperId return '/admin/exam-analysis?paperId=' . $paperId . '&studentId=' . $studentId; } elseif ($type === 'ocr_upload') { // OCR上传记录,也跳转到详情页 return '/admin/exam-analysis?recordId=' . $recordId . '&studentId=' . $studentId; } return '#'; } }