| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- <?php
- namespace App\Filament\Pages;
- use App\Models\OCRRecord;
- use App\Models\OCRQuestionResult;
- use App\Jobs\ProcessOCRRecord;
- use Filament\Notifications\Notification;
- use Filament\Pages\Page;
- use Livewire\Attributes\Computed;
- class OCRRecordView extends Page
- {
- protected static ?string $title = 'OCR记录详情';
- protected static ?string $slug = 'ocr-record-view/{recordId}';
- protected string $view = 'filament.pages.ocr-record-view-new';
- public static function shouldRegisterNavigation(): bool
- {
- return false;
- }
- public string $recordId = '';
- public array $manualAnswers = [];
- public bool $hasAnalysisResults = false;
-
- // 新增:判卷相关
- public array $questionGrades = []; // 存储每道题的评分 [question_id => ['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],
- };
- }
- }
|