recordId && !$this->paperId) { Notification::make() ->title('错误') ->body('缺少记录ID或试卷ID') ->danger() ->send(); $this->redirectRoute('filament.admin.pages.upload-exam-paper'); return; } // 根据记录类型选择不同的视图 if ($this->recordId) { // OCR记录使用紧凑布局 $this->view = 'filament.pages.exam-analysis-compact'; } elseif ($this->paperId) { // 系统生成卷子使用标准布局 $this->view = 'filament.pages.exam-analysis-standard'; } $this->loadAnalysisData(); } protected function loadAnalysisData() { try { // 处理OCR记录 if ($this->recordId) { $this->recordType = 'ocr'; $record = OCRRecord::with('student')->find($this->recordId); if (!$record) { Notification::make() ->title('错误') ->body('未找到指定的上传记录') ->danger() ->send(); $this->redirectRoute('filament.admin.pages.upload-exam-paper'); return; } $this->recordData = $record->toArray(); $this->studentInfo = $record->student ? $record->student->toArray() : []; // OCR记录:添加题目统计信息 $ocrQuestionsCount = OCRQuestionResult::where('ocr_record_id', $this->recordId)->count(); $this->recordData['total_questions'] = $ocrQuestionsCount; $this->recordData['questions'] = $this->getQuestions(); // 提前加载题目数据 // OCR记录如果已完成处理,加载分析数据 if ($record->status === 'completed' && $record->student_id) { $this->loadLearningAnalysis($record->student_id, $this->recordId); } else { $this->analysisData = []; } } // 处理系统生成卷子 elseif ($this->paperId) { $this->recordType = 'generated'; $paper = \App\Models\Paper::with('student')->find($this->paperId); if (!$paper) { Notification::make() ->title('错误') ->body('未找到指定的试卷') ->danger() ->send(); $this->redirectRoute('filament.admin.pages.upload-exam-paper'); return; } // 构造基础数据,视图会安全处理缺失字段 $this->recordData = [ 'id' => $paper->paper_id, 'paper_id' => $paper->paper_id, 'student_id' => $paper->student_id, 'paper_type' => 'system_generated', 'paper_name' => $paper->paper_name, 'status' => $paper->status, 'total_questions' => $paper->question_count, 'created_at' => $paper->created_at, 'analysis_id' => $paper->analysis_id, // AI分析记录ID ]; $this->studentInfo = $paper->student ? $paper->student->toArray() : []; // 获取试卷题目列表(包含题库API的详细数据) $this->recordData['questions'] = $this->getQuestions(); // 系统生成卷子也尝试加载学习分析数据 if ($paper->student_id) { $this->loadLearningAnalysis($paper->student_id, $this->paperId); } else { $this->analysisData = []; } } $this->loading = false; } catch (\Exception $e) { \Log::error('加载试卷分析数据失败', [ 'record_id' => $this->recordId, 'paper_id' => $this->paperId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); Notification::make() ->title('错误') ->body('加载分析数据失败:' . $e->getMessage()) ->danger() ->send(); $this->loading = false; } } /** * 加载学习分析数据 */ protected function loadLearningAnalysis($studentId, $identifier) { try { // 1. 尝试从数据库加载本次试卷的分析结果 $paperId = null; if ($this->recordType === 'ocr') { $paperId = $this->recordData['exam_id'] ?? null; } elseif ($this->recordType === 'generated') { $paperId = $this->recordData['paper_id'] ?? null; } if ($paperId) { // 直接从API调用获取分析结果,不查询本地数据库 \Log::info('跳过本地数据库查询,直接从API加载分析结果', [ 'paper_id' => $paperId, 'student_id' => $studentId ]); $this->loadLearningAnalysisFromAPI($studentId, $paperId); return; } // 2. 直接从API获取整体掌握度数据,不查询本地数据库 \Log::info('跳过知识点记录表查询,直接从API获取掌握度数据', [ 'student_id' => $studentId ]); $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null); } catch (\Exception $e) { \Log::warning('加载分析数据失败,回退到API调用', [ 'identifier' => $identifier, 'student_id' => $studentId, 'type' => $this->recordType, 'error' => $e->getMessage() ]); // 数据库查询失败时回退到API调用 $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null); } } /** * 从API加载学习分析数据(回退方案) */ protected function loadLearningAnalysisFromAPI($studentId, $paperId = null) { try { $learningService = app(\App\Services\LearningAnalyticsService::class); // 1. 加载本次试卷的分析结果 $analysisId = null; if ($this->recordType === 'ocr' && isset($this->recordData['analysis_id'])) { $analysisId = $this->recordData['analysis_id']; } elseif ($this->recordType === 'generated' && isset($this->recordData['analysis_id'])) { $analysisId = $this->recordData['analysis_id']; } if ($analysisId) { $paperAnalysisResponse = $learningService->getAnalysisResult($analysisId); if (!empty($paperAnalysisResponse) && isset($paperAnalysisResponse['data'])) { $this->paperAnalysisData = $paperAnalysisResponse['data']; // 将API的分析结果同步到题目数据 if (isset($this->paperAnalysisData['question_results'])) { $this->syncApiAnalysisToQuestions($this->paperAnalysisData['question_results']); } \Log::info('本次试卷分析结果已从API加载', [ 'analysis_id' => $analysisId, 'student_id' => $studentId, 'data_keys' => array_keys($this->paperAnalysisData), 'question_results_count' => count($this->paperAnalysisData['question_results'] ?? []) ]); } } // 2. 调用学习分析API获取整体掌握度数据 $masteryResponse = $learningService->getStudentMastery($studentId); // 转换为页面期望的格式 if (!empty($masteryResponse) && isset($masteryResponse['data'])) { $masteryList = $masteryResponse['data']; // 计算整体掌握度 $totalMastery = 0; $count = count($masteryList); $weakAreas = []; $knowledgePoints = []; foreach ($masteryList as $mastery) { $masteryLevel = $mastery['mastery_level'] ?? 0; $totalMastery += $masteryLevel; // 识别薄弱知识点(掌握度 < 0.6) if ($masteryLevel < 0.6) { $weakAreas[] = [ 'kp_code' => $mastery['kp_code'], 'mastery' => $masteryLevel ]; } // 构造知识点数据 $totalAttempts = $mastery['total_attempts'] ?? 0; $correctAttempts = $mastery['correct_attempts'] ?? 0; $accuracyRate = $totalAttempts > 0 ? $correctAttempts / $totalAttempts : 0; $knowledgePoints[] = [ 'kp_code' => $mastery['kp_code'], 'name' => $mastery['kp_code'], // TODO: 从知识图谱服务获取名称 'mastery' => $masteryLevel, 'mastery_level' => $masteryLevel, // 添加模板需要的字段 'total_attempts' => $totalAttempts, 'correct_attempts' => $correctAttempts, 'accuracy_rate' => $accuracyRate ]; } $overallMastery = $count > 0 ? $totalMastery / $count : 0; // 生成学习建议 $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints); // 只显示与当前试卷相关的知识点 $currentPaperKps = $this->getCurrentPaperKnowledgePoints(); $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code'); $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) { return in_array($kp['kp_code'], $currentPaperKpCodes); }); $this->analysisData = [ 'overall_mastery' => $overallMastery, 'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) { return in_array($weak['kp_code'], $currentPaperKpCodes); }), 'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点 'recommendations' => $recommendations, 'total_knowledge_points' => count($filteredKnowledgePoints), 'mastery_distribution' => $this->calculateMasteryDistribution($masteryList) ]; \Log::info('学习分析数据已从API加载', [ 'student_id' => $studentId, 'overall_mastery' => $overallMastery, 'knowledge_points_count' => count($knowledgePoints), 'filtered_knowledge_points_count' => count($filteredKnowledgePoints), 'current_paper_kp_codes' => $currentPaperKpCodes, 'weak_areas_count' => count($weakAreas) ]); } else { \Log::info('API返回数据为空', [ 'student_id' => $studentId, 'paper_id' => $paperId, 'type' => $this->recordType ]); $this->analysisData = []; } } catch (\Exception $apiError) { \Log::warning('API调用失败', [ 'student_id' => $studentId, 'paper_id' => $paperId, 'type' => $this->recordType, 'error' => $apiError->getMessage() ]); // API调用失败时设置空数组,避免页面报错 $this->analysisData = []; $this->paperAnalysisData = []; } } /** * 处理知识点记录数据 */ protected function processKnowledgePointRecords($knowledgePointRecords) { // 根据实际的knowledge_point_records表结构处理数据 $masteryList = []; $weakAreas = []; $knowledgePoints = []; $totalMastery = 0; $count = 0; foreach ($knowledgePointRecords as $record) { // knowledge_point_records表有不同的字段结构 $kpCode = $record->knowledge_point ?? ''; $masteryLevel = $record->mastery_after ?? $record->mastery_before ?? 0; if (!empty($kpCode)) { $masteryList[] = [ 'kp_code' => $kpCode, 'mastery_level' => $masteryLevel ]; $totalMastery += $masteryLevel; $count++; // 识别薄弱知识点(掌握度 < 0.6) if ($masteryLevel < 0.6) { $weakAreas[] = [ 'kp_code' => $kpCode, 'mastery' => $masteryLevel ]; } // 构造知识点数据 $knowledgePoints[] = [ 'kp_code' => $kpCode, 'name' => $kpCode, 'mastery' => $masteryLevel, 'mastery_level' => $masteryLevel, // 添加模板需要的字段 'total_attempts' => 1, // 默认值 'correct_attempts' => $masteryLevel > 0.5 ? 1 : 0, // 估算值 'accuracy_rate' => $masteryLevel ]; } } $overallMastery = $count > 0 ? $totalMastery / $count : 0; // 生成学习建议 $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints); // 只显示与当前试卷相关的知识点 $currentPaperKps = $this->getCurrentPaperKnowledgePoints(); $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code'); $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) { return in_array($kp['kp_code'], $currentPaperKpCodes); }); $this->analysisData = [ 'overall_mastery' => $overallMastery, 'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) { return in_array($weak['kp_code'], $currentPaperKpCodes); }), 'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点 'recommendations' => $recommendations, 'total_knowledge_points' => count($filteredKnowledgePoints), 'mastery_distribution' => $this->calculateMasteryDistribution($masteryList) ]; \Log::info('学习分析数据已从knowledge_point_records加载', [ 'student_id' => $knowledgePointRecords->first()->student_id ?? 'unknown', 'overall_mastery' => $overallMastery, 'knowledge_points_count' => count($knowledgePoints), 'filtered_knowledge_points_count' => count($filteredKnowledgePoints), 'current_paper_kp_codes' => $currentPaperKpCodes, 'weak_areas_count' => count($weakAreas) ]); } /** * 生成学习建议 */ protected function generateRecommendations($overallMastery, $weakAreas, $knowledgePoints) { $recommendations = []; if ($overallMastery >= 0.8) { $recommendations[] = '整体掌握情况良好,继续保持!可以尝试更有挑战性的题目。'; } elseif ($overallMastery >= 0.6) { $recommendations[] = '基础掌握较好,建议加强薄弱知识点的练习。'; } else { $recommendations[] = '需要系统复习基础知识,建议从简单题目开始逐步提升。'; } // 针对薄弱知识点的建议 if (count($weakAreas) > 0) { $weakKpCodes = array_slice(array_column($weakAreas, 'kp_code'), 0, 3); $recommendations[] = '重点加强以下知识点: ' . implode('、', $weakKpCodes); } // 根据答题次数给建议 $lowAttempts = array_filter($knowledgePoints, function($kp) { return ($kp['total_attempts'] ?? 0) < 5; }); if (count($lowAttempts) > 0) { $recommendations[] = '部分知识点练习次数较少,建议增加练习量以巩固掌握。'; } return $recommendations; } /** * 计算掌握度分布 */ protected function calculateMasteryDistribution($masteryList) { $distribution = [ 'high' => 0, // >= 0.7 'medium' => 0, // 0.4 - 0.7 'low' => 0 // < 0.4 ]; foreach ($masteryList as $mastery) { $level = $mastery['mastery_level'] ?? 0; if ($level >= 0.7) { $distribution['high']++; } elseif ($level >= 0.4) { $distribution['medium']++; } else { $distribution['low']++; } } return $distribution; } public function getPaperTypeLabel(): string { if ($this->recordType === 'ocr' && isset($this->recordData['paper_type'])) { return match($this->recordData['paper_type']) { 'unit_test' => '单元测试', 'midterm' => '期中考试', 'final' => '期末考试', 'homework' => '家庭作业', 'quiz' => '随堂测验', 'other' => '其他', default => '未分类', }; } return $this->recordData['paper_type'] ?? '未知'; } public function getStatusBadge(): string { $status = $this->recordData['status'] ?? 'unknown'; return match($status) { 'pending' => '待处理', 'processing' => '处理中', 'completed' => '已完成', 'failed' => '失败', default => '未知', }; } /** * 判断是否为 OCR 场景 */ public function isOcrRecord(): bool { return $this->recordType === 'ocr'; } /** * 获取OCR记录的题目数据 * 从OCRQuestionResult表加载并格式化为组件期望的格式 */ protected function getOcrQuestions(): array { $recordId = $this->recordId ?? null; if (!$recordId) { \Log::warning('OCR记录缺少recordId', ['recordId' => $recordId]); return []; } try { // 从OCRQuestionResult表加载题目数据 $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $recordId) ->orderBy('question_number') ->get(); \Log::info('加载OCR题目数据', [ 'record_id' => $recordId, 'questions_count' => $ocrQuestions->count() ]); if ($ocrQuestions->isEmpty()) { \Log::warning('OCR记录没有题目数据', ['record_id' => $recordId]); return []; } // 创建API分析结果的映射(如果有) $analysisMap = []; if (!empty($this->paperAnalysisData['question_results'])) { foreach ($this->paperAnalysisData['question_results'] as $result) { if (isset($result['question_id'])) { $analysisMap[$result['question_id']] = $result; } } } $questions = []; foreach ($ocrQuestions as $oq) { // 获取API分析结果(如果有) $aiAnalysis = null; if (isset($analysisMap[$oq->question_number])) { $analysis = $analysisMap[$oq->question_number]; $aiAnalysis = [ 'analysis' => $analysis['reason'] ?? '', 'mistake_type' => $analysis['mistake_type'] ?? '', 'mistake_category' => $analysis['mistake_category'] ?? '', 'suggestions' => [$analysis['suggestions'] ?? ''], 'correct_solution' => $analysis['correct_solution'] ?? '', ]; } elseif (!empty($oq->ai_feedback)) { // 如果没有API分析,使用OCR记录的AI反馈 $aiAnalysis = [ 'analysis' => $oq->ai_feedback, 'mistake_type' => '', 'mistake_category' => '', 'suggestions' => [$oq->ai_feedback], 'correct_solution' => '', ]; } // 学生答案:优先使用老师校准的答案(manual_answer),如果没有则使用OCR识别的答案 $ocrAnswer = trim($oq->student_answer ?? ''); $manualAnswer = trim($oq->manual_answer ?? ''); $studentAnswer = !empty($manualAnswer) ? $manualAnswer : $ocrAnswer; // 判断是否有答案(OCR识别或老师校准) $hasAnswer = !empty($studentAnswer); $displayAnswer = $hasAnswer ? ($studentAnswer ?: '空') : '未作答'; // 从AI分析结果中获取正确答案和判断 $correctAnswer = null; $isCorrect = false; $solution = null; if (isset($analysisMap[$oq->question_number])) { $analysis = $analysisMap[$oq->question_number]; $correctAnswer = $analysis['correct_answer'] ?? $analysis['correct_solution'] ?? null; $isCorrect = $analysis['is_correct'] ?? false; $solution = $analysis['correct_solution'] ?? null; } // 显示答案对比(如果有AI分析的正确答案) $answerComparison = null; if (!empty($correctAnswer) && $studentAnswer !== $correctAnswer && $hasAnswer) { $answerComparison = [ 'student' => $studentAnswer, 'correct' => $correctAnswer ]; } $questions[] = [ 'id' => $oq->id, 'question_number' => $oq->question_number, 'question_bank_id' => 'ocr_' . $oq->question_number, // OCR题目没有题库ID,用前缀标识 'question_type' => 'unknown', 'question_text' => $oq->question_text ?? '题目内容缺失', 'content' => $oq->question_text ?? '题目内容缺失', 'stem' => $oq->question_text ?? '题目内容缺失', 'answer' => $correctAnswer ?? '', // 正确答案(从AI分析获取) 'reference_answer' => $correctAnswer ?? '', 'solution' => $solution, // 解题步骤 'score_total' => $oq->score_total ?? null, // OCR题目的分数可能为空 'score_obtained' => $oq->score_obtained ?? null, // OCR题目的分数可能为空 'student_answer' => $displayAnswer, // 学生答案:未作答/空/实际答案(校准后) 'is_correct' => $isCorrect, 'kp_code' => $oq->kp_code ?? null, // OCR题目的知识点可能为空 'ai_analysis' => $aiAnalysis, 'answer_comparison' => $answerComparison, // 答案对比信息 ]; } \Log::info('OCR题目数据格式化完成', [ 'record_id' => $recordId, 'formatted_questions_count' => count($questions), 'has_ai_analysis_count' => count(array_filter($questions, fn($q) => !empty($q['ai_analysis']))) ]); return $questions; } catch (\Exception $e) { \Log::error('获取OCR题目数据失败', [ 'record_id' => $recordId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return []; } } /** * 获取题目列表(根据场景返回不同数据,包含AI分析解析) */ public function getQuestions(): array { // OCR记录:从OCRQuestionResult表加载题目数据 if ($this->recordType === 'ocr') { $questions = $this->getOcrQuestions(); // 丰富知识点信息 return $this->enrichQuestionsWithKnowledgePoints($questions); } // 系统生成卷子:从PaperQuestion表加载题目数据 $paperId = $this->recordData['paper_id'] ?? null; if (!$paperId) { return []; } try { // 直接查询题目数据 $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paperId) ->orderBy('question_number') ->get(); if ($paperQuestions->isEmpty()) { return []; } // 获取题库题目详情 $questionBankService = app(\App\Services\QuestionBankService::class); $questionBankIds = $paperQuestions->pluck('question_bank_id')->unique()->filter()->toArray(); $questionDetails = collect([]); \Log::info('准备调用题库服务', [ 'question_bank_ids' => $questionBankIds, 'ids_count' => count($questionBankIds) ]); if (!empty($questionBankIds)) { try { \Log::info('开始调用题库服务getQuestionsByIds'); $details = $questionBankService->getQuestionsByIds($questionBankIds); \Log::info('题库服务调用结果', [ 'response' => $details, 'has_data' => isset($details['data']), 'data_type' => gettype($details['data'] ?? null) ]); if (isset($details['data']) && is_array($details['data'])) { $questionDetails = collect($details['data'])->keyBy('id'); \Log::info('题库数据处理成功', [ 'details_count' => $questionDetails->count(), 'first_key' => $questionDetails->keys()->first() ]); } else { \Log::warning('题库服务返回数据格式不正确', ['details' => $details]); } } catch (\Exception $e) { \Log::error('获取题库数据失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); } } else { \Log::info('没有有效的题库ID'); } // 构建题目数据 - 合并paper_questions表和题库API数据 $questions = []; // 创建API分析结果的映射(如果有) $analysisMap = []; if (!empty($this->paperAnalysisData['question_results'])) { foreach ($this->paperAnalysisData['question_results'] as $result) { if (isset($result['question_id'])) { $analysisMap[$result['question_id']] = $result; } } } foreach ($paperQuestions as $pq) { // 从题库API获取详细数据 $detail = $questionDetails->get($pq->question_bank_id); \Log::info('构建题目数据', [ 'question_bank_id' => $pq->question_bank_id, 'has_detail' => $detail ? 'Yes' : 'No', 'detail_id' => $detail['id'] ?? 'null', 'stem_preview' => $detail ? substr($detail['stem'] ?? '', 50) : 'null', 'kp_code_from_db' => $pq->knowledge_point, 'kp_code_from_api' => $detail['kp_code'] ?? 'null', 'has_analysis' => isset($analysisMap[$pq->question_bank_id]) ? 'Yes' : 'No' ]); // 优先使用题库API返回的stem,如果没有则使用paper_questions中的question_text $questionText = $detail['stem'] ?? $detail['content'] ?? $pq->question_text ?? '题目内容缺失'; // 优先使用题库API返回的kp_code,如果没有则使用paper_questions中的knowledge_point $kpCode = $detail['kp_code'] ?? $pq->knowledge_point ?? 'N/A'; // 获取API分析结果(如果有) $aiAnalysis = null; if (isset($analysisMap[$pq->question_bank_id])) { $analysis = $analysisMap[$pq->question_bank_id]; $aiAnalysis = [ 'analysis' => $analysis['reason'] ?? '', 'mistake_type' => $analysis['mistake_type'] ?? '', 'mistake_category' => $analysis['mistake_category'] ?? '', 'suggestions' => [$analysis['suggestions'] ?? ''], 'correct_solution' => $analysis['correct_solution'] ?? '', ]; } // 获取解题步骤solution(从Question Bank或AI分析) $solution = $detail['solution'] ?? $detail['correct_solution'] ?? null; if (!$solution && isset($analysisMap[$pq->question_bank_id])) { $analysis = $analysisMap[$pq->question_bank_id]; $solution = $analysis['correct_solution'] ?? null; } $questions[] = [ 'id' => $pq->id, 'question_number' => $pq->question_number, 'question_bank_id' => $pq->question_bank_id, 'question_type' => $pq->question_type, 'question_text' => $questionText, 'content' => $questionText, 'stem' => $questionText, 'answer' => $detail['answer'] ?? '', 'reference_answer' => $detail['answer'] ?? '', 'solution' => $solution, // 解题步骤 'score_total' => $pq->score ?? 5, 'score_obtained' => $pq->score_obtained ?? 0, 'student_answer' => '老师已评分', // 隐藏学生答案,显示老师评分状态 'is_correct' => $pq->is_correct ?? false, 'kp_code' => $kpCode, 'ai_analysis' => $aiAnalysis, ]; } // 丰富知识点信息 return $this->enrichQuestionsWithKnowledgePoints($questions); } catch (\Exception $e) { \Log::error('获取题目列表失败', [ 'paper_id' => $paperId, 'error' => $e->getMessage() ]); return []; } } /** * 丰富题目数据,添加知识点详细信息 */ protected function enrichQuestionsWithKnowledgePoints(array $questions): array { // 收集所有知识点代码 $kpCodes = []; foreach ($questions as $question) { if (!empty($question['kp_code']) && $question['kp_code'] !== 'N/A') { $kpCodes[] = $question['kp_code']; } } if (empty($kpCodes)) { return $questions; } // 获取知识点详细信息 $knowledgeService = app(\App\Services\KnowledgeGraphService::class); $knowledgePointsList = $knowledgeService->listKnowledgePoints(1, 1000); $knowledgePoints = []; if (isset($knowledgePointsList['data']) && !empty($knowledgePointsList['data'])) { foreach ($knowledgePointsList['data'] as $kp) { $knowledgePoints[$kp['kp_code']] = $kp; } } // 获取技能点信息 $skillsList = []; foreach ($kpCodes as $kpCode) { $skills = $knowledgeService->getSkillsByKnowledgePoint($kpCode); if (!empty($skills)) { $skillsList[$kpCode] = $skills; } } // 丰富题目数据 foreach ($questions as &$question) { $kpCode = $question['kp_code'] ?? null; if ($kpCode && isset($knowledgePoints[$kpCode])) { $kp = $knowledgePoints[$kpCode]; $question['knowledge_point'] = [ 'code' => $kpCode, 'name' => $kp['cn_name'] ?? $kpCode, 'category' => $kp['category'] ?? '', 'phase' => $kp['phase'] ?? '', 'grade' => $kp['grade'] ?? '', 'description' => $kp['description'] ?? '', ]; // 添加技能点 if (isset($skillsList[$kpCode])) { $question['knowledge_point']['skills'] = $skillsList[$kpCode]; } } } return $questions; } /** * 重新处理OCR */ public function reprocessOCR() { if (!$this->recordId) { return; } try { $record = OCRRecord::find($this->recordId); if (!$record) { throw new \Exception('记录不存在'); } $ocrService = app(OCRService::class); $ocrService->reprocess($record); Notification::make() ->title('已提交重新处理') ->body('OCR识别任务已重新加入队列') ->success() ->send(); $this->loadAnalysisData(); // 刷新数据 } catch (\Exception $e) { Notification::make() ->title('操作失败') ->body($e->getMessage()) ->danger() ->send(); } } /** * 重新提交分析 */ public function reanalyze() { if (!$this->recordId) { return; } try { $record = OCRRecord::find($this->recordId); if (!$record) { throw new \Exception('记录不存在'); } // 获取当前的题目数据(包含可能的人工校准) $questions = OCRQuestionResult::where('ocr_record_id', $this->recordId) ->orderBy('question_number') ->get() ->map(function ($q) { return [ 'question_number' => $q->question_number, 'content' => $q->question_text, 'student_answer' => $q->student_answer, 'manual_answer' => $q->manual_answer, 'answer_verified' => $q->answer_verified, 'confidence' => $q->score_confidence, 'kp_code' => $q->kp_code, 'score_value' => $q->score_value, ]; })->toArray(); if (empty($questions)) { throw new \Exception('没有可分析的题目数据'); } // 构造分析请求数据 $analysisData = [ 'exam_id' => $record->exam_id, 'student_id' => $record->student_id, 'ocr_record_id' => $record->id, 'teacher_name' => auth()->user()->name ?? 'Teacher', 'analysis_type' => 'mastery', 'questions' => array_map(function($q) { // 优先使用人工校准的答案 $studentAnswer = $q['student_answer'] ?? ''; if (isset($q['manual_answer']) && !empty($q['manual_answer'])) { $studentAnswer = $q['manual_answer']; } return [ 'question_id' => $q['question_number'], 'question_number' => (string)$q['question_number'], 'kp_code' => $q['kp_code'] ?? null, 'score_value' => $q['score_value'] ?? 0, 'student_answer' => $studentAnswer, 'ocr_confidence' => $q['confidence'] ?? 0, 'question_text' => $q['content'] ?? '', 'teacher_validated' => $q['answer_verified'] ?? false, ]; }, $questions) ]; // 调用分析服务 $learningService = app(LearningAnalyticsService::class); $result = $learningService->submitOCRAnalysis($analysisData); if (isset($result['success']) && $result['success']) { $record->update([ 'ai_analyzed_at' => now(), 'ai_analysis_count' => ($record->ai_analysis_count ?? 0) + 1 ]); Notification::make() ->title('分析请求已提交') ->body('系统正在重新分析试卷,请稍后刷新查看结果') ->success() ->send(); $this->loadAnalysisData(); // 刷新数据 } else { throw new \Exception($result['message'] ?? '提交分析失败'); } } catch (\Exception $e) { Notification::make() ->title('操作失败') ->body($e->getMessage()) ->danger() ->send(); } } /** * 从当前试卷数据中提取知识点信息 */ protected function extractKnowledgePointsFromCurrentPaper(): array { $knowledgePoints = []; $questions = $this->getQuestions(); foreach ($questions as $question) { $kpCode = $question['kp_code'] ?? null; if ($kpCode && $kpCode !== 'N/A') { $isCorrect = $question['is_correct'] ?? false; if (!isset($knowledgePoints[$kpCode])) { $knowledgePoints[$kpCode] = [ 'kp_code' => $kpCode, 'name' => $kpCode, 'total_attempts' => 0, 'correct_attempts' => 0, 'mastery_level' => 0, 'accuracy_rate' => 0 ]; } $knowledgePoints[$kpCode]['total_attempts']++; if ($isCorrect) { $knowledgePoints[$kpCode]['correct_attempts']++; } } } // 计算准确率和掌握度 foreach ($knowledgePoints as &$kp) { if ($kp['total_attempts'] > 0) { $kp['accuracy_rate'] = $kp['correct_attempts'] / $kp['total_attempts']; $kp['mastery_level'] = $kp['accuracy_rate']; } } return array_values($knowledgePoints); } /** * 获取当前试卷的知识点掌握情况 */ protected function getCurrentPaperKnowledgePoints(): array { // 首先尝试从当前试卷数据中提取 $currentPaperKps = $this->extractKnowledgePointsFromCurrentPaper(); // 如果有历史分析数据,合并以提供更准确的掌握度 if (!empty($this->analysisData['knowledge_points'])) { $historicalKps = $this->analysisData['knowledge_points']; foreach ($currentPaperKps as &$currentKp) { $kpCode = $currentKp['kp_code']; // 查找历史数据 $historicalData = collect($historicalKps)->firstWhere('kp_code', $kpCode); if ($historicalData && isset($historicalData['mastery_level'])) { // 使用历史数据的掌握度,但保留试卷的实际表现 $currentKp['historical_mastery_level'] = $historicalData['mastery_level']; $currentKp['total_attempts'] = $historicalData['total_attempts'] ?? $currentKp['total_attempts']; $currentKp['correct_attempts'] = $historicalData['correct_attempts'] ?? $currentKp['correct_attempts']; $currentKp['accuracy_rate'] = $historicalData['accuracy_rate'] ?? $currentKp['accuracy_rate']; // 优先使用历史的掌握度,但考虑试卷表现做微调 $paperPerformance = $currentKp['accuracy_rate']; $historicalPerformance = $historicalData['mastery_level'] ?? 0; // 综合计算:70%历史 + 30%本试卷 $currentKp['mastery_level'] = ($historicalPerformance * 0.7 + $paperPerformance * 0.3); } } } return $currentPaperKps; } /** * 将API的分析结果同步到题目数据和数据库 */ protected function syncApiAnalysisToQuestions(array $questionResults) { try { \Log::info('开始同步API分析结果到题目数据', [ 'paper_id' => $this->paperId, 'question_results_count' => count($questionResults) ]); // 创建question_id到分析结果的映射 $analysisMap = []; foreach ($questionResults as $result) { $questionId = $result['question_id'] ?? null; if ($questionId) { $analysisMap[$questionId] = $result; } } // 获取当前试卷的题目 $paper = \App\Models\Paper::find($this->paperId); if ($paper) { $paperQuestions = $paper->questions()->get(); $updatedCount = 0; foreach ($paperQuestions as $pq) { // 查找对应的API分析结果 if (isset($analysisMap[$pq->question_bank_id])) { $analysis = $analysisMap[$pq->question_bank_id]; // 更新数据库字段 $pq->is_correct = $analysis['correct'] ?? false; $pq->score_obtained = $analysis['score'] ?? 0; $pq->save(); $updatedCount++; \Log::info('更新题目分析结果', [ 'question_bank_id' => $pq->question_bank_id, 'is_correct' => $pq->is_correct, 'score_obtained' => $pq->score_obtained ]); } } \Log::info('API分析结果同步完成', [ 'updated_count' => $updatedCount, 'total_questions' => $paperQuestions->count() ]); // 刷新recordData中的questions数据 $this->recordData['questions'] = $this->getQuestions(); } } catch (\Exception $e) { \Log::error('同步API分析结果失败', [ 'paper_id' => $this->paperId, 'error' => $e->getMessage() ]); } } /** * 提交OCR记录到AI分析API(与系统卷子使用统一接口) */ protected function submitOcrForAnalysis($record) { try { // 获取OCR题目结果(使用校准后的答案) $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $record->id) ->orderBy('question_number') ->get(); if ($ocrQuestions->isEmpty()) { \Log::warning('OCR记录没有题目,无法提交分析', ['record_id' => $record->id]); return; } // 构建答题数据(与系统卷子格式一致) $answers = []; foreach ($ocrQuestions as $oq) { // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案 $studentAnswer = !empty(trim($oq->manual_answer ?? '')) ? trim($oq->manual_answer) : trim($oq->student_answer ?? ''); $answers[] = [ 'question_bank_id' => 'ocr_q' . $oq->question_number, // OCR题目没有题库ID,生成临时ID 'question_text' => $oq->question_text ?? '', // 添加题目内容 'student_answer' => $studentAnswer, 'is_correct' => null, // 让AI分析判断 'score' => null, // OCR题目可能没有分数 'max_score' => $oq->score_total ?? null, 'kp_code' => $oq->kp_code ?? null, ]; } // 使用与系统卷子相同的接口提交 $submissionData = [ 'paper_id' => 'ocr_' . $record->id, // OCR记录ID作为paper_id 'answers' => $answers, ]; \Log::info('提交OCR数据到AI分析(统一接口)', [ 'record_id' => $record->id, 'student_id' => $record->student_id, 'question_count' => count($answers), 'api_endpoint' => '/api/v1/attempts/batch/student/' . $record->student_id ]); // 调用学习分析服务(与系统卷子使用相同的方法) $learningService = app(\App\Services\LearningAnalyticsService::class); $response = $learningService->submitBatchAttempts($record->student_id, $submissionData); if (!empty($response) && !isset($response['error'])) { // 从响应中获取analysis_id(如果API返回) $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time()); // 更新OCR记录的analysis_id $record->analysis_id = $analysisId; $record->save(); \Log::info('OCR分析提交成功(统一接口)', [ 'record_id' => $record->id, 'analysis_id' => $analysisId, 'response_keys' => array_keys($response) ]); // 更新recordData $this->recordData['analysis_id'] = $analysisId; } else { \Log::error('OCR分析提交失败', [ 'record_id' => $record->id, 'response' => $response ]); } } catch (\Exception $e) { \Log::error('提交OCR分析异常', [ 'record_id' => $record->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); } } }