initializeUserRole(); // 如果是老师,自动选择当前老师 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $this->teacherId = $teacherId; } } else { $this->teacherId = null; } $this->studentId = null; $this->mode = 'select_paper'; $this->selectedPaperId = null; $this->questionGrades = []; } #[Computed] public function teachers(): array { return app(ExamPaperService::class)->getTeachers( $this->isTeacher ? $this->getCurrentTeacherId() : null ); } #[Computed] public function students(): array { return app(ExamPaperService::class)->getStudents($this->teacherId); } #[Computed] public function recentRecords(): array { return app(ExamPaperService::class)->getRecentRecords($this->studentId); } #[Computed] public function studentPapers(): array { return app(ExamPaperService::class)->getStudentPapers($this->studentId); } #[Computed] public function selectedPaperQuestions(): array { return app(ExamPaperService::class)->getPaperQuestions($this->selectedPaperId); } 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->selectedPaperId = null; $this->questionGrades = []; } /** * 处理评分完成事件 */ #[On('gradingComplete')] public function handleGradingComplete(): void { // 提交完成后跳转到详情页 if ($this->selectedPaperId && $this->studentId) { $url = $this->getViewRecordUrl('graded_paper', $this->selectedPaperId, '', $this->studentId); $this->redirect($url, navigate: true); } } /** * 处理来自子组件的评分提交事件 */ #[On('submitManualGrading')] public function handleSubmitFromParent(array $questionGrades, array $gradingData, array $questions): void { // 从子组件接收数据 $this->questionGrades = $questionGrades; $this->gradingData = $gradingData; $this->questions = $questions; \Log::info('UploadExamPaper: 接收到子组件提交的评分数据', [ 'questionGrades_count' => count($questionGrades), 'questions_count' => count($questions) ]); // 调用原有的提交方法 $this->submitManualGrading(); // 通知用户提交成功 Notification::make() ->title('评分提交成功') ->success() ->send(); // 触发事件处理跳转 $this->dispatch('gradingComplete'); } #[On('teacherChanged')] public function updateTeacherId($teacherId) { $this->teacherId = $teacherId; $this->studentId = null; } #[On('studentChanged')] public function updateStudentId($teacherId, $studentId) { $this->studentId = $studentId; } /** * 提交手动评分 */ public function submitManualGrading(): void { if (!$this->selectedPaperId) { Notification::make() ->title('请选择试卷') ->danger() ->send(); return; } // 将 gradingData 转换为 questionGrades 格式 // 注意:这里假设子组件已经传递了处理好的 questionGrades,或者我们在这里再次处理 // 如果子组件传递了 questionGrades,我们直接使用它。 // 如果没有(比如直接在父组件调用),我们需要 convertGradingDataToQuestionGrades。 // 但目前逻辑是子组件调用 handleSubmitFromParent 传递数据。 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) { // 从数据库获取当前题目的记录(包含满分) $paperQuestion = \App\Models\PaperQuestion::where('id', $questionId)->first(); if (!$paperQuestion) { \Log::warning('未找到题目记录', ['question_id' => $questionId]); continue; } // 确保 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 的一致性 // score 优先级高于 is_correct,根据得分比例动态计算 $maxScore = $paperQuestion->score ?? 0; if ($maxScore > 0) { $scoreRatio = $score / $maxScore; // 只有达到满分才算完全正确 if ($scoreRatio >= 1.0) { $isCorrect = true; } elseif ($scoreRatio > 0) { $isCorrect = false; // 部分得分不算完全正确 } else { $isCorrect = false; } } \Log::info('保存评分数据', [ 'question_id' => $questionId, 'max_score' => $maxScore, 'score_obtained' => $score, 'is_correct' => $isCorrect, 'score_ratio' => $maxScore > 0 ? ($score / $maxScore) : 0 ]); \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 { // 构造 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; } // 计算满分 $maxScore = floatval($question['score'] ?? 0); $scoreValue = floatval($grade['score'] ?? 0); $isCorrect = $maxScore > 0 ? ($scoreValue >= $maxScore) : ($grade['is_correct'] ?? false); $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' => $scoreValue, 'max_score' => $maxScore, 'is_correct' => $isCorrect, '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(); } } /** * 查看记录详情 - 使用页面跳转 */ 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 '#'; } }