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 = []; } #[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')->latest(); // 如果选择了学生,则筛选该学生的记录 if (!empty($this->studentId)) { $ocrQuery->where('user_id', $this->studentId); } $ocrRecords = $ocrQuery->take(5) ->get() ->map(function($record) { return [ 'type' => 'ocr_upload', 'id' => $record->id, 'record_id' => $record->id, 'paper_id' => null, 'student_id' => $record->user_id, 'student_name' => $record->student?->name ?? $record->user_id, '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'])->latest(); // 如果选择了学生,则筛选该学生的记录 if (!empty($this->studentId)) { $paperQuery->where('student_id', $this->studentId); } $allPapers = $paperQuery->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'; return [ 'type' => $type, 'id' => $paper->paper_id, 'record_id' => null, 'paper_id' => $paper->paper_id, 'student_id' => $paper->student_id, 'student_name' => $paper->student?->name ?? $paper->student_id, '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; } if (!$this->uploadedImage) { Notification::make() ->title('请上传图片') ->danger() ->send(); return; } $this->isUploading = true; try { // 保存图片 $path = $this->uploadedImage->store('ocr-uploads', 'public'); $filename = basename($path); // 创建OCR记录 $record = OCRRecord::create([ 'user_id' => $this->studentId, 'file_path' => $path, 'paper_title' => $filename, 'paper_type' => $this->paperType, 'status' => 'pending', 'total_questions' => 0, ]); // 立即更新状态为处理中,提供更好的用户体验 $record->update(['status' => 'processing']); // 自动触发OCR处理 ProcessOCRRecord::dispatch($record->id); // 重置表单 $this->teacherId = null; $this->studentId = null; $this->uploadedImage = null; $this->paperType = null; Notification::make() ->title('上传成功') ->body("卷子已上传并开始OCR处理,正在跳转到校准页面...") ->success() ->send(); // 跳转到OCR记录详情页面进行校准和提交分析 $this->redirect("/admin/ocr-record-view/{$record->id}"); } catch (\Exception $e) { Notification::make() ->title('上传失败') ->body($e->getMessage()) ->danger() ->send(); } finally { $this->isUploading = 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; } 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; } $analyticsData[] = [ 'question_bank_id' => $question['question_bank_id'], 'student_answer' => $grade['student_answer'] ?? '', 'is_correct' => $grade['is_correct'] ?? null, 'score' => $grade['score'] ?? null, 'max_score' => $question['score'], 'kp_code' => $kpCode, // 添加 kp_code ]; } // 调用 LearningAnalytics 服务 $learningAnalyticsService = app(\App\Services\LearningAnalyticsService::class); // 步骤0: 保存学生答案到本地数据库 (重要:确保数据持久化) foreach ($this->questionGrades as $questionId => $grade) { \App\Models\PaperQuestion::where('id', $questionId)->update([ 'student_answer' => $grade['student_answer'] ?? '', 'is_correct' => $grade['is_correct'] ?? false, 'score_obtained' => $grade['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(); } } }