OCRRecordView.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Models\OCRRecord;
  4. use App\Models\OCRQuestionResult;
  5. use App\Jobs\ProcessOCRRecord;
  6. use Filament\Notifications\Notification;
  7. use Filament\Pages\Page;
  8. use Livewire\Attributes\Computed;
  9. class OCRRecordView extends Page
  10. {
  11. protected static ?string $title = 'OCR记录详情';
  12. protected static ?string $slug = 'ocr-record-view/{recordId}';
  13. protected string $view = 'filament.pages.ocr-record-view-new';
  14. public static function shouldRegisterNavigation(): bool
  15. {
  16. return false;
  17. }
  18. public string $recordId = '';
  19. public array $manualAnswers = [];
  20. public bool $hasAnalysisResults = false;
  21. // 新增:判卷相关
  22. public array $questionGrades = []; // 存储每道题的评分 [question_id => ['score' => x, 'is_correct' => true/false]]
  23. #[Computed]
  24. public function record(): ?OCRRecord
  25. {
  26. return OCRRecord::with(['student', 'questions'])->find($this->recordId);
  27. }
  28. public function mount(string $recordId): void
  29. {
  30. $this->recordId = $recordId;
  31. $record = $this->record();
  32. if ($record) {
  33. // Fix stuck processing status: if status is processing but we have questions, it's actually completed
  34. if ($record->status === 'processing' && $record->questions()->count() > 0) {
  35. $record->update([
  36. 'status' => 'completed',
  37. 'processed_at' => $record->processed_at ?? now(),
  38. 'total_questions' => $record->questions()->count(),
  39. 'processed_questions' => $record->questions()->count(),
  40. ]);
  41. // Refresh record to get updated status
  42. $record = $this->record();
  43. }
  44. foreach ($record->questions as $question) {
  45. if ($question->manual_answer) {
  46. $this->manualAnswers[$question->id] = $question->manual_answer;
  47. }
  48. // 加载已有的评分
  49. if ($question->ai_score !== null || $question->score_value !== null) {
  50. $this->questionGrades[$question->id] = [
  51. 'score' => $question->ai_score ?? $question->score_value,
  52. 'is_correct' => $question->is_correct,
  53. ];
  54. }
  55. }
  56. // 检查是否已有AI分析结果
  57. $this->checkAnalysisResults($record);
  58. }
  59. }
  60. /**
  61. * Check if record already has AI analysis results
  62. */
  63. private function checkAnalysisResults(OCRRecord $record): void
  64. {
  65. // Only consider analyzed if ai_analyzed_at is set AND we have scores
  66. $this->hasAnalysisResults = $record->ai_analyzed_at && $record->questions()
  67. ->whereNotNull('ai_score')
  68. ->exists();
  69. }
  70. /**
  71. * Submit all questions for AI analysis.
  72. * Updates manual answers in batch, then sends data to LearningAnalytics using unified interface.
  73. */
  74. public function submitForAnalysis(): void
  75. {
  76. $record = $this->record();
  77. if (! $record) {
  78. Notification::make()
  79. ->title('记录不存在')
  80. ->danger()
  81. ->send();
  82. return;
  83. }
  84. $updatedCount = 0;
  85. foreach ($record->questions as $question) {
  86. $manualAnswer = $this->manualAnswers[$question->id] ?? null;
  87. if ($manualAnswer && trim($manualAnswer) !== '') {
  88. $question->update([
  89. 'manual_answer' => trim($manualAnswer),
  90. 'answer_verified' => true,
  91. ]);
  92. $updatedCount++;
  93. }
  94. }
  95. // 使用统一接口提交分析
  96. try {
  97. $learningService = app(\App\Services\LearningAnalyticsService::class);
  98. // 准备答题数据(与系统卷子格式一致)
  99. $answers = [];
  100. foreach ($record->questions as $question) {
  101. // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案
  102. $studentAnswer = !empty(trim($question->manual_answer ?? ''))
  103. ? trim($question->manual_answer)
  104. : trim($question->student_answer ?? '');
  105. $answers[] = [
  106. 'question_bank_id' => 'ocr_q' . $question->question_number,
  107. 'question_text' => $question->question_text ?? '',
  108. 'student_answer' => $studentAnswer,
  109. 'is_correct' => null, // 让AI判断
  110. 'score' => null,
  111. 'max_score' => $question->score_total ?? null,
  112. 'kp_code' => $question->kp_code ?? null,
  113. ];
  114. }
  115. // 提交到统一接口
  116. $submissionData = [
  117. 'paper_id' => 'ocr_' . $record->id,
  118. 'answers' => $answers,
  119. ];
  120. \Log::info('OCRRecordView提交分析(统一接口)', [
  121. 'record_id' => $record->id,
  122. 'student_id' => $record->student_id,
  123. 'question_count' => count($answers)
  124. ]);
  125. $response = $learningService->submitBatchAttempts($record->student_id, $submissionData);
  126. if (!empty($response) && !isset($response['error'])) {
  127. // 从响应中获取analysis_id
  128. $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time());
  129. // 更新OCR记录的analysis_id和状态
  130. $record->update([
  131. 'analysis_id' => $analysisId,
  132. 'ai_analyzed_at' => now(),
  133. 'ai_analysis_count' => count($answers),
  134. ]);
  135. \Log::info('OCR分析提交成功', [
  136. 'record_id' => $record->id,
  137. 'analysis_id' => $analysisId
  138. ]);
  139. // 重新检查分析结果状态
  140. $this->checkAnalysisResults($record);
  141. Notification::make()
  142. ->title('分析完成')
  143. ->body("已更新 {$updatedCount} 道题的答案,已提交 " . count($answers) . " 道题目进行AI分析")
  144. ->success()
  145. ->send();
  146. // 跳转到分析页面
  147. $this->redirect("/admin/exam-analysis?recordId={$record->id}");
  148. } else {
  149. \Log::error('OCR分析提交失败', [
  150. 'record_id' => $record->id,
  151. 'response' => $response
  152. ]);
  153. Notification::make()
  154. ->title('分析失败')
  155. ->body('提交AI分析失败:' . ($response['message'] ?? '未知错误'))
  156. ->danger()
  157. ->send();
  158. }
  159. } catch (\Exception $e) {
  160. \Log::error('提交OCR分析异常', [
  161. 'record_id' => $record->id,
  162. 'error' => $e->getMessage(),
  163. 'trace' => $e->getTraceAsString()
  164. ]);
  165. Notification::make()
  166. ->title('分析失败')
  167. ->body('提交分析时发生异常:' . $e->getMessage())
  168. ->danger()
  169. ->send();
  170. }
  171. }
  172. public function startRecognition(): void
  173. {
  174. $record = $this->record();
  175. if (! $record) {
  176. Notification::make()
  177. ->title('记录不存在')
  178. ->danger()
  179. ->send();
  180. return;
  181. }
  182. if ($record->status === 'processing') {
  183. Notification::make()
  184. ->title('正在处理中')
  185. ->warning()
  186. ->send();
  187. return;
  188. }
  189. ProcessOCRRecord::dispatch($record);
  190. $record->update(['status' => 'processing']);
  191. Notification::make()
  192. ->title('开始识别')
  193. ->body('OCR识别任务已启动,请稍后刷新查看结果')
  194. ->success()
  195. ->send();
  196. }
  197. public function getStatusBadgeConfig(string $status): array
  198. {
  199. return match ($status) {
  200. 'pending' => ['class' => 'badge-warning', 'text' => '待处理'],
  201. 'processing' => ['class' => 'badge-info', 'text' => '处理中'],
  202. 'completed' => ['class' => 'badge-success', 'text' => '已完成'],
  203. 'failed' => ['class' => 'badge-error', 'text' => '失败'],
  204. default => ['class' => 'badge-ghost', 'text' => $status],
  205. };
  206. }
  207. }