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) {
// 从 paper_analysis_results 表读取分析结果
$paperAnalysis = \DB::table('paper_analysis_results')
->where('paper_id', $paperId)
->first();
if ($paperAnalysis && !empty($paperAnalysis->analysis_data)) {
$this->paperAnalysisData = json_decode($paperAnalysis->analysis_data, true);
\Log::info('本次试卷分析结果已从数据库加载', [
'paper_id' => $paperId,
'student_id' => $studentId,
'data_keys' => array_keys($this->paperAnalysisData ?? [])
]);
} else {
// 如果数据库中没有分析结果,回退到API调用(临时解决方案)
\Log::info('数据库中未找到分析结果,回退到API调用', [
'paper_id' => $paperId,
'student_id' => $studentId
]);
$this->loadLearningAnalysisFromAPI($studentId, $paperId);
return;
}
}
// 2. 尝试从知识点记录表获取整体掌握度数据
$knowledgePointRecords = \DB::table('knowledge_point_records')
->where('student_id', $studentId)
->get();
// 处理知识点掌握度数据
if ($knowledgePointRecords->count() > 0) {
// 如果找到了知识点记录数据,使用它
$this->processKnowledgePointRecords($knowledgePointRecords);
} else {
// 如果没有找到知识点记录,回退到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;
if (isset($analysisMap[$oq->question_number])) {
$analysis = $analysisMap[$oq->question_number];
$correctAnswer = $analysis['correct_answer'] ?? $analysis['correct_solution'] ?? null;
$isCorrect = $analysis['is_correct'] ?? false;
}
// 显示答案对比(如果有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 ?? '',
'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') {
return $this->getOcrQuestions();
}
// 系统生成卷子:从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'] ?? '',
];
}
$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'] ?? '',
'score_total' => $pq->score ?? 5,
'score_obtained' => $pq->score_obtained ?? 0,
'student_answer' => $pq->student_answer ?? '未作答',
'is_correct' => $pq->is_correct ?? false,
'kp_code' => $kpCode,
'ai_analysis' => $aiAnalysis,
];
}
return $questions;
} catch (\Exception $e) {
\Log::error('获取题目列表失败', [
'paper_id' => $paperId,
'error' => $e->getMessage()
]);
return [];
}
}
/**
* 重新处理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()
]);
}
}
}