paper) return []; return [ 'paper_id' => $this->paper->paper_id, 'paper_name' => $this->paper->paper_name, 'student_id' => $this->paper->student_id, 'teacher_id' => $this->paper->teacher_id, 'total_questions' => $this->paper->total_questions, 'total_score' => $this->paper->total_score, 'status' => $this->paper->status, 'created_at' => $this->paper->created_at->format('Y-m-d H:i'), ]; } #[Computed] public function studentInfo(): ?array { if (!$this->paper || !$this->paper->student_id) return null; $student = \App\Models\Student::find($this->paper->student_id); if (!$student) return null; return [ 'student_id' => $student->student_id, 'name' => $student->name, 'grade' => $student->grade, 'class' => $student->class_name, ]; } public function mount(string $recordId): void { $this->recordId = $recordId; $this->loadOCRRecord(); if ($this->ocrRecord && $this->ocrRecord->analysis_id) { $this->loadPaper(); if ($this->paper) { $this->matchQuestions(); } } } /** * 加载OCR记录 */ private function loadOCRRecord(): void { $this->ocrRecord = OCRRecord::with(['student', 'questions']) ->where('id', $this->recordId) ->first(); if (!$this->ocrRecord) { Notification::make() ->title('错误') ->body('OCR记录不存在') ->danger() ->send(); return; } } /** * 加载关联的试卷 */ private function loadPaper(): void { $this->paper = Paper::where('paper_id', $this->ocrRecord->analysis_id)->first(); } /** * 匹配OCR识别结果与系统试卷题目 */ private function matchQuestions(): void { if (!$this->ocrRecord || !$this->paper) return; // 获取系统试卷的题目(作为匹配基准) $paperQuestions = PaperQuestion::where('paper_id', $this->paper->paper_id) ->orderBy('question_number') ->get(); // 获取OCR识别的题目 $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->ocrRecord->id) ->orderBy('question_number') ->get(); // 如果没有OCR题目记录,尝试从原始数据恢复 if ($ocrQuestions->isEmpty()) { $rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data') ->where('ocr_record_id', $this->ocrRecord->id) ->value('raw_response'); if ($rawOcrData) { try { $rawOcrData = json_decode($rawOcrData, true); $parser = new \App\Services\OCRDataParser(); $matchedResults = $parser->matchWithSystemPaper($rawOcrData, $paperQuestions); foreach ($matchedResults as $qNum => $result) { OCRQuestionResult::create([ 'ocr_record_id' => $this->ocrRecord->id, 'question_number' => $qNum, 'question_text' => '系统题目 ' . $qNum, // 占位 'student_answer' => $result['student_answer'], 'score_confidence' => $result['confidence'], 'score_value' => 0, ]); } // 重新获取 $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->ocrRecord->id) ->orderBy('question_number') ->get(); \Log::info('从原始数据恢复了OCR题目记录', ['count' => $ocrQuestions->count()]); } catch (\Exception $e) { \Log::error('从原始数据恢复OCR记录失败: ' . $e->getMessage()); } } } $this->matchedQuestions = []; // 以系统试卷的题目为基准进行匹配 foreach ($paperQuestions as $paperQuestion) { // 查找对应题号的OCR识别结果(可能有多个,取第一个有效的) $ocrQuestion = $ocrQuestions ->where('question_number', $paperQuestion->question_number) ->first(); if ($ocrQuestion) { $this->matchedQuestions[] = [ 'question_number' => $paperQuestion->question_number, 'ocr_id' => $ocrQuestion->id, 'paper_id' => $paperQuestion->id, 'question_text' => $paperQuestion->question_text ?: '系统题目', 'knowledge_point' => $paperQuestion->knowledge_point, 'question_type' => $paperQuestion->question_type, 'student_answer' => $ocrQuestion->student_answer, 'correct_answer' => $paperQuestion->correct_answer, 'full_score' => $paperQuestion->score, 'is_correct' => $ocrQuestion->is_correct, 'score' => $ocrQuestion->ai_score, 'ocr_confidence' => $ocrQuestion->score_confidence ?? null, 'bbox' => $ocrQuestion->student_answer_bbox, ]; // 更新OCR记录,关联到题库 if (!$ocrQuestion->question_bank_id) { $ocrQuestion->update([ 'question_bank_id' => $paperQuestion->question_id, 'kp_code' => $paperQuestion->knowledge_point, ]); } } else { // 没有找到对应的OCR结果,但仍然显示系统题目 $this->matchedQuestions[] = [ 'question_number' => $paperQuestion->question_number, 'ocr_id' => null, 'paper_id' => $paperQuestion->id, 'question_text' => $paperQuestion->question_text ?: '系统题目', 'knowledge_point' => $paperQuestion->knowledge_point, 'question_type' => $paperQuestion->question_type, 'student_answer' => null, 'correct_answer' => $paperQuestion->correct_answer, 'full_score' => $paperQuestion->score, 'is_correct' => null, 'score' => null, 'ocr_confidence' => null, 'note' => 'OCR未识别到此题' ]; } } // 记录匹配统计 \Log::info('OCR题目匹配完成', [ 'ocr_record_id' => $this->ocrRecord->id, 'paper_id' => $this->paper->paper_id, 'system_questions' => $paperQuestions->count(), 'ocr_questions_total' => $ocrQuestions->count(), 'matched_questions' => count($this->matchedQuestions), 'ocr_question_numbers' => $ocrQuestions->pluck('question_number')->unique()->values()->toArray(), ]); } /** * 执行答题分析 */ public function analyzeAnswers(): void { $this->isAnalyzing = true; try { $this->analysisResults = []; $totalScore = 0; $correctCount = 0; foreach ($this->matchedQuestions as &$matched) { $analysis = $this->analyzeAnswer( $matched['student_answer'], $matched['correct_answer'], $matched['question_type'], $matched['full_score'] ); $matched['is_correct'] = $analysis['is_correct']; $matched['score'] = $analysis['score']; $matched['analysis_details'] = $analysis['details']; $totalScore += $analysis['score']; if ($analysis['is_correct']) { $correctCount++; } // 更新数据库 OCRQuestionResult::where('id', $matched['ocr_id']) ->update([ 'ai_score' => $analysis['score'], 'is_correct' => $analysis['is_correct'], ]); $this->analysisResults[] = $analysis; } unset($matched); // 更新OCR记录状态 $this->ocrRecord->update([ 'ai_analyzed_at' => now(), ]); // 计算统计信息 $stats = [ 'total_questions' => count($this->matchedQuestions), 'correct_count' => $correctCount, 'incorrect_count' => count($this->matchedQuestions) - $correctCount, 'total_score' => $totalScore, 'full_score' => $this->paper->total_score, 'accuracy_rate' => round(($correctCount / count($this->matchedQuestions)) * 100, 2), 'score_rate' => round(($totalScore / $this->paper->total_score) * 100, 2), ]; // 可选:发送到Learning Analytics进行深度分析 $this->sendToLearningAnalytics($stats); Notification::make() ->title('分析完成') ->body(sprintf( '共%d道题,正确%d道,得分%d分(满分%d分)', $stats['total_questions'], $stats['correct_count'], $stats['total_score'], $stats['full_score'] )) ->success() ->send(); } catch (\Exception $e) { \Log::error('试卷答题分析失败: ' . $e->getMessage()); Notification::make() ->title('分析失败') ->body($e->getMessage()) ->danger() ->send(); } finally { $this->isAnalyzing = false; } } /** * 分析单个答案 */ private function analyzeAnswer(?string $studentAnswer, ?string $correctAnswer, string $questionType, float $fullScore): array { $studentAnswer = trim($studentAnswer ?? ''); $correctAnswer = trim($correctAnswer ?? ''); // 空答案处理 if (empty($studentAnswer)) { return [ 'is_correct' => false, 'score' => 0, 'details' => '未作答' ]; } $isCorrect = false; $score = 0; $details = ''; // 根据题型进行不同的分析 switch ($questionType) { case '选择题': case 'choice': $result = $this->analyzeChoiceAnswer($studentAnswer, $correctAnswer); break; case '填空题': case 'fill': $result = $this->analyzeFillAnswer($studentAnswer, $correctAnswer); break; case '解答题': case 'answer': $result = $this->analyzeAnswerAnswer($studentAnswer, $correctAnswer, $fullScore); break; default: $result = $this->analyzeGeneralAnswer($studentAnswer, $correctAnswer, $fullScore); } return $result; } /** * 分析选择题答案 */ private function analyzeChoiceAnswer(string $studentAnswer, string $correctAnswer): array { $studentAnswer = $this->normalizeChoiceAnswer($studentAnswer); $correctAnswer = $this->normalizeChoiceAnswer($correctAnswer); $isCorrect = $studentAnswer === $correctAnswer; return [ 'is_correct' => $isCorrect, 'score' => $isCorrect ? $fullScore : 0, 'details' => $isCorrect ? '正确' : '错误' ]; } /** * 分析填空题答案 */ private function analyzeFillAnswer(string $studentAnswer, string $correctAnswer): array { // 精确匹配 if (strcasecmp($studentAnswer, $correctAnswer) === 0) { return [ 'is_correct' => true, 'score' => $fullScore, 'details' => '完全正确' ]; } // 去除空格后匹配 if (strcasecmp(str_replace(' ', '', $studentAnswer), str_replace(' ', '', $correctAnswer)) === 0) { return [ 'is_correct' => true, 'score' => $fullScore * 0.9, // 扣10% 'details' => '基本正确(多空格)' ]; } // 数值比较 if (is_numeric($studentAnswer) && is_numeric($correctAnswer)) { if (abs(floatval($studentAnswer) - floatval($correctAnswer)) < 0.001) { return [ 'is_correct' => true, 'score' => $fullScore, 'details' => '数值正确' ]; } } return [ 'is_correct' => false, 'score' => 0, 'details' => '错误' ]; } /** * 分析解答题答案(简化版,实际需要更复杂的评分逻辑) */ private function analyzeAnswerAnswer(string $studentAnswer, string $correctAnswer, float $fullScore): array { // 简化处理:解答题需要人工评分或更复杂的AI评分 // 这里仅做简单的文本相似度比较 $similar = similar_text($studentAnswer, $correctAnswer, $percent); if ($percent > 80) { return [ 'is_correct' => true, 'score' => $fullScore, 'details' => sprintf('相似度%.1f%%,建议人工复核', $percent) ]; } elseif ($percent > 50) { return [ 'is_correct' => false, 'score' => $fullScore * 0.5, 'details' => sprintf('部分正确(相似度%.1f%%)', $percent) ]; } else { return [ 'is_correct' => false, 'score' => 0, 'details' => sprintf('相似度%.1f%%', $percent) ]; } } /** * 通用答案分析 */ private function analyzeGeneralAnswer(string $studentAnswer, string $correctAnswer, float $fullScore): array { $isCorrect = strcasecmp($studentAnswer, $correctAnswer) === 0; return [ 'is_correct' => $isCorrect, 'score' => $isCorrect ? $fullScore : 0, 'details' => $isCorrect ? '正确' : '错误' ]; } /** * 标准化选择题答案 */ private function normalizeChoiceAnswer(string $answer): string { // 处理各种格式:A, B, C, D 或 a, b, c, d 或 ①, ②, ③, ④ 或 1, 2, 3, 4 $map = [ '①' => 'a', '②' => 'b', '③' => 'c', '④' => 'd', '1' => 'a', '2' => 'b', '3' => 'c', '4' => 'd', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', ]; $answer = trim($answer); return $map[$answer] ?? strtolower($answer); } /** * 发送分析结果到Learning Analytics */ private function sendToLearningAnalytics(array $stats): void { try { $client = new \GuzzleHttp\Client(); $response = $client->post('http://localhost:5016/api/student/exam-analysis', [ 'json' => [ 'student_id' => $this->paper->student_id, 'paper_id' => $this->paper->paper_id, 'analysis_type' => 'ocr_matching', 'stats' => $stats, 'detailed_results' => $this->matchedQuestions, 'timestamp' => now()->toISOString(), ] ]); if ($response->getStatusCode() === 200) { \Log::info('分析结果已发送到Learning Analytics'); } } catch (\Exception $e) { \Log::error('发送分析结果到Learning Analytics失败: ' . $e->getMessage()); } } /** * 导出报告 */ public function exportReport(): void { // 生成简单的文本报告 $report = $this->generateTextReport(); $filename = "试卷分析报告_" . date('Y-m-d_H-i-s') . ".txt"; $filepath = storage_path("reports/" . $filename); // 确保目录存在 if (!is_dir(dirname($filepath))) { mkdir(dirname($filepath), 0755, true); } file_put_contents($filepath, $report); // 提供下载链接 Notification::make() ->title('报告导出成功') ->body('报告已保存到:' . $filename) ->success() ->send(); } /** * 生成文本报告 */ private function generateTextReport(): string { $stats = $this->getAnalysisStats(); $report = ""; // 报告头部 $report .= "=====================================\n"; $report .= "试卷分析报告\n"; $report .= "=====================================\n\n"; $report .= "试卷名称:" . $this->paper->paper_name . "\n"; $report .= "学生姓名:" . ($this->studentInfo()['name'] ?? '未知') . "\n"; $report .= "班级:" . ($this->studentInfo()['class'] ?? '未知') . "\n"; $report .= "分析时间:" . date('Y-m-d H:i:s') . "\n\n"; // 统计信息 $report .= "【统计信息】\n"; $report .= "-------------------------\n"; $report .= "题目总数:" . $stats['total'] . "题\n"; $report .= "正确题数:" . $stats['correct'] . "题\n"; $report .= "错误题数:" . $stats['incorrect'] . "题\n"; $report .= "正确率:" . $stats['accuracy'] . "%\n"; $report .= "得分:" . $stats['score'] . "分\n"; $report .= "满分:" . $stats['full_score'] . "分\n"; $report .= "得分率:" . $stats['score_rate'] . "%\n\n"; // 详细题目分析 $report .= "【题目详情】\n"; $report .= "-------------------------\n"; foreach ($this->matchedQuestions as $index => $question) { $report .= "\n题目" . ($index + 1) . ":\n"; $report .= " 知识点:" . ($question['knowledge_point'] ?? '未知') . "\n"; $report .= " 学生答案:" . ($question['student_answer'] ?: '未作答') . "\n"; $report .= " 正确答案:" . ($question['correct_answer'] ?? '未知') . "\n"; $report .= " 得分:" . ($question['score'] ?? 0) . "分\n"; $report .= " 状态:" . ($question['is_correct'] ? '正确' : '错误') . "\n"; if (isset($question['analysis_details'])) { $report .= " 说明:" . $question['analysis_details'] . "\n"; } } return $report; } /** * 获取分析统计信息 */ public function getAnalysisStats(): array { if (empty($this->analysisResults)) return []; $correct = 0; $total = count($this->matchedQuestions); $score = 0; foreach ($this->matchedQuestions as $matched) { if ($matched['is_correct']) $correct++; $score += $matched['score']; } return [ 'total' => $total, 'correct' => $correct, 'incorrect' => $total - $correct, 'accuracy' => $total > 0 ? round(($correct / $total) * 100, 2) : 0, 'score' => $score, 'full_score' => $this->paper->total_score, 'score_rate' => $this->paper->total_score > 0 ? round(($score / $this->paper->total_score) * 100, 2) : 0, ]; } /** * 重新匹配题目 */ public function rematchQuestions(): void { try { $rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data') ->where('ocr_record_id', $this->ocrRecord->id) ->value('raw_response'); if (!$rawOcrData) { Notification::make()->title('未找到原始OCR数据')->danger()->send(); return; } $rawOcrData = json_decode($rawOcrData, true); $paperQuestions = PaperQuestion::where('paper_id', $this->paper->paper_id) ->orderBy('question_number') ->get(); $ocrService = app(\App\Services\OCRService::class); $matchedResults = $ocrService->performEnhancedMatching($this->ocrRecord, $rawOcrData, $paperQuestions); // 更新现有记录 foreach ($matchedResults as $result) { OCRQuestionResult::updateOrCreate( [ 'ocr_record_id' => $this->ocrRecord->id, 'question_number' => $result['question_number'], ], [ 'student_answer' => $result['student_answer'], 'score_confidence' => $result['confidence'], 'student_answer_bbox' => $result['student_answer_bbox'] ?? null, 'question_text' => '系统题目 ' . $result['question_number'], // 确保有值 ] ); } // 刷新页面数据 $this->matchQuestions(); Notification::make()->title('重新匹配完成')->success()->send(); } catch (\Exception $e) { \Log::error('重新匹配失败: ' . $e->getMessage()); Notification::make()->title('重新匹配失败')->body($e->getMessage())->danger()->send(); } } /** * 提交AI分析 */ public function submitForAiAnalysis(): void { try { // 确保统计信息是最新的 $stats = $this->getAnalysisStats(); if (empty($stats)) { // 如果还没有分析过,先简单统计一下(或者强制先运行 analyzeAnswers) $this->analyzeAnswers(); $stats = $this->getAnalysisStats(); } $this->sendToLearningAnalytics($stats); Notification::make()->title('已提交AI分析请求')->success()->send(); } catch (\Exception $e) { Notification::make()->title('提交失败')->body($e->getMessage())->danger()->send(); } } /** * 判断是否已完成分析 */ public function hasAnalysis(): bool { return !empty($this->analysisResults) || ($this->ocrRecord && $this->ocrRecord->ai_analyzed_at); } }