['score' => x, 'is_correct' => true/false]] public bool $isGenerating = false; public ?string $generationTaskId = null; public array $questionGenerationStatus = []; #[Computed] public function record(): ?OCRRecord { return OCRRecord::with(['student', 'questions'])->find($this->recordId); } public function mount(string $recordId): void { $this->recordId = $recordId; $record = $this->record(); if ($record) { // Fix stuck processing status: if status is processing but we have questions, it's actually completed if ($record->status === 'processing' && $record->questions()->count() > 0) { $record->update([ 'status' => 'completed', 'processed_at' => $record->processed_at ?? now(), 'total_questions' => $record->questions()->count(), 'processed_questions' => $record->questions()->count(), ]); // Refresh record to get updated status $record = $this->record(); } foreach ($record->questions as $question) { if ($question->manual_answer) { $this->manualAnswers[$question->id] = $question->manual_answer; } // 加载已有的评分 if ($question->ai_score !== null || $question->score_value !== null) { $this->questionGrades[$question->id] = [ 'score' => $question->ai_score ?? $question->score_value, 'is_correct' => $question->is_correct, ]; } // 初始化生成状态 $this->questionGenerationStatus[$question->id] = $question->generation_status ?? 'pending'; if ($question->generation_status === 'generating' && $question->generation_task_id) { $this->isGenerating = true; $this->generationTaskId = $question->generation_task_id; } } // 检查是否已有AI分析结果 $this->checkAnalysisResults($record); } } /** * Check if record already has AI analysis results */ private function checkAnalysisResults(OCRRecord $record): void { // Only consider analyzed if ai_analyzed_at is set AND we have scores $this->hasAnalysisResults = $record->ai_analyzed_at && $record->questions() ->whereNotNull('ai_score') ->exists(); } /** * 生成题库题目 */ public function generateQuestionBankQuestions(): void { $record = $this->record(); if (!$record) return; $questionsToGenerate = []; foreach ($record->questions as $question) { // 只有未关联题库的题目才需要生成 if (!$question->question_bank_id) { $questionsToGenerate[] = [ 'id' => $question->question_number, // 使用id字段匹配ocr_question_number 'content' => $question->question_text, // 可以传递更多字段,如知识点等 'student_answer' => $question->student_answer, 'kp_code' => $question->kp_code, ]; } } if (empty($questionsToGenerate)) { Notification::make() ->title('无需生成') ->body('所有题目已关联题库') ->success() ->send(); return; } try { $service = app(\App\Services\QuestionBankService::class); // 使用异步API,让系统自动生成回调URL $response = $service->generateQuestionsFromOcrAsync( $questionsToGenerate, $record->student->grade ?? '高一', // 假设有年级字段 '数学', // 默认科目 $record->id, // OCR记录ID,用于关联 null, // 让系统自动生成回调URL 'api.ocr.callback' // 回调路由名称 ); if ($response['status'] === 'processing' && isset($response['task_id'])) { $this->isGenerating = true; $this->generationTaskId = $response['task_id']; // 更新数据库状态为生成中 foreach ($record->questions as $question) { if (!$question->question_bank_id) { $question->update([ 'generation_status' => 'generating', 'generation_task_id' => $this->generationTaskId, 'generation_error' => null ]); $this->questionGenerationStatus[$question->id] = 'generating'; } } Notification::make() ->title('开始生成') ->body('已提交题库题目生成任务,系统将在后台处理并自动关联结果...') ->success() ->send(); } else { throw new \Exception($response['message'] ?? '未知错误'); } } catch (\Exception $e) { Notification::make() ->title('生成失败') ->body($e->getMessage()) ->danger() ->send(); } } /** * 检查生成状态 (被轮询调用) */ public function checkGenerationStatus(): void { if (!$this->isGenerating || !$this->generationTaskId) { return; } try { $service = app(\App\Services\QuestionBankService::class); $status = $service->checkGenerationTaskStatus($this->generationTaskId); if (($status['status'] ?? '') === 'completed') { $this->isGenerating = false; $results = $status['results'] ?? []; // 更新题目关联 $record = $this->record(); foreach ($record->questions as $question) { // 在结果中查找对应题目(假设通过 question_number 匹配) $result = collect($results)->firstWhere('question_number', $question->question_number); if ($result && isset($result['question_id'])) { $question->update([ 'question_bank_id' => $result['question_id'], 'generation_status' => 'completed', 'generation_error' => null ]); $this->questionGenerationStatus[$question->id] = 'completed'; } elseif ($question->generation_status === 'generating') { // 如果任务完成了但没找到这个题的结果,标记为失败 $question->update([ 'generation_status' => 'failed', 'generation_error' => '生成结果中未找到对应题目' ]); $this->questionGenerationStatus[$question->id] = 'failed'; } } Notification::make() ->title('生成完成') ->body('题库题目已生成并关联') ->success() ->send(); } elseif (($status['status'] ?? '') === 'failed') { $this->isGenerating = false; $record = $this->record(); foreach ($record->questions as $question) { if ($question->generation_status === 'generating') { $question->update([ 'generation_status' => 'failed', 'generation_error' => $status['message'] ?? '任务执行失败' ]); $this->questionGenerationStatus[$question->id] = 'failed'; } } Notification::make() ->title('生成失败') ->body($status['message'] ?? '任务执行失败') ->danger() ->send(); } } catch (\Exception $e) { \Log::error('检查生成状态失败: ' . $e->getMessage()); } } /** * 检查是否可以提交分析 */ public function canSubmitAnalysis(): bool { $record = $this->record(); if (!$record) return false; // 检查是否有题目未关联题库ID // 排除那些可能不需要生成的(如果有的话),这里假设所有OCR题目都需要进题库 return !$record->questions()->whereNull('question_bank_id')->exists(); } /** * Submit all questions for AI analysis. * Updates manual answers in batch, then sends data to LearningAnalytics using unified interface. */ public function submitForAnalysis(): void { // 1. 检查是否所有题目都已关联题库 if (!$this->canSubmitAnalysis()) { Notification::make() ->title('请先生成题库题目') ->body('分析前需要确保所有题目都已在题库中创建并关联') ->warning() ->send(); return; } $record = $this->record(); if (! $record) { Notification::make() ->title('记录不存在') ->danger() ->send(); return; } $updatedCount = 0; foreach ($record->questions as $question) { $manualAnswer = $this->manualAnswers[$question->id] ?? null; if ($manualAnswer && trim($manualAnswer) !== '') { $question->update([ 'manual_answer' => trim($manualAnswer), 'answer_verified' => true, ]); $updatedCount++; } } // 使用统一接口提交分析 try { $learningService = app(\App\Services\LearningAnalyticsService::class); // 准备答题数据(与系统卷子格式一致) $answers = []; foreach ($record->questions as $question) { // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案 $studentAnswer = !empty(trim($question->manual_answer ?? '')) ? trim($question->manual_answer) : trim($question->student_answer ?? ''); $answers[] = [ 'question_bank_id' => $question->question_bank_id, // 使用真实的题库ID 'question_text' => $question->question_text ?? '', 'student_answer' => $studentAnswer, 'is_correct' => null, // 让AI判断 'score' => null, 'max_score' => $question->score_total ?? null, 'kp_code' => $question->kp_code ?? null, ]; } // 提交到统一接口 $submissionData = [ 'paper_id' => 'ocr_' . $record->id, 'answers' => $answers, ]; \Log::info('OCRRecordView提交分析(统一接口)', [ 'record_id' => $record->id, 'student_id' => $record->student_id, 'question_count' => count($answers) ]); $response = $learningService->submitBatchAttempts($record->student_id, $submissionData); if (!empty($response) && !isset($response['error'])) { // 从响应中获取analysis_id $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time()); // 更新OCR记录的analysis_id和状态 $record->update([ 'analysis_id' => $analysisId, 'ai_analyzed_at' => now(), 'ai_analysis_count' => count($answers), ]); \Log::info('OCR分析提交成功', [ 'record_id' => $record->id, 'analysis_id' => $analysisId ]); // 重新检查分析结果状态 $this->checkAnalysisResults($record); Notification::make() ->title('分析完成') ->body("已更新 {$updatedCount} 道题的答案,已提交 " . count($answers) . " 道题目进行AI分析") ->success() ->send(); // 跳转到分析页面 $this->redirect("/admin/exam-analysis?recordId={$record->id}"); } else { \Log::error('OCR分析提交失败', [ 'record_id' => $record->id, 'response' => $response ]); Notification::make() ->title('分析失败') ->body('提交AI分析失败:' . ($response['message'] ?? '未知错误')) ->danger() ->send(); } } catch (\Exception $e) { \Log::error('提交OCR分析异常', [ 'record_id' => $record->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); Notification::make() ->title('分析失败') ->body('提交分析时发生异常:' . $e->getMessage()) ->danger() ->send(); } } public function startRecognition(): void { $record = $this->record(); if (! $record) { Notification::make() ->title('记录不存在') ->danger() ->send(); return; } if ($record->status === 'processing') { Notification::make() ->title('正在处理中') ->warning() ->send(); return; } ProcessOCRRecord::dispatch($record); $record->update(['status' => 'processing']); Notification::make() ->title('开始识别') ->body('OCR识别任务已启动,请稍后刷新查看结果') ->success() ->send(); } public function getStatusBadgeConfig(string $status): array { return match ($status) { 'pending' => ['class' => 'badge-warning', 'text' => '待处理'], 'processing' => ['class' => 'badge-info', 'text' => '处理中'], 'completed' => ['class' => 'badge-success', 'text' => '已完成'], 'failed' => ['class' => 'badge-error', 'text' => '失败'], default => ['class' => 'badge-ghost', 'text' => $status], }; } }