OCRRecordViewEnhanced.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Models\OCRRecord;
  4. use App\Models\OCRQuestionResult;
  5. use App\Models\Paper;
  6. use App\Models\PaperQuestion;
  7. use App\Jobs\ProcessOCRRecord;
  8. use Filament\Notifications\Notification;
  9. use Filament\Pages\Page;
  10. use Livewire\Attributes\Computed;
  11. use Livewire\Attributes\On;
  12. class OCRRecordViewEnhanced extends Page
  13. {
  14. protected static ?string $title = 'OCR记录详情';
  15. protected static ?string $slug = 'ocr-record-view/{recordId}';
  16. protected string $view = 'filament.pages.ocr-record-view-enhanced';
  17. public static function shouldRegisterNavigation(): bool
  18. {
  19. return false;
  20. }
  21. public string $recordId = '';
  22. public array $manualAnswers = [];
  23. public bool $hasAnalysisResults = false;
  24. public array $questionGrades = [];
  25. public bool $isGenerating = false;
  26. public ?string $generationTaskId = null;
  27. public array $questionGenerationStatus = [];
  28. // 新增:判断是否为系统生成的试卷
  29. public ?Paper $associatedPaper = null;
  30. public bool $isSystemGeneratedPaper = false;
  31. #[Computed]
  32. public function record(): ?OCRRecord
  33. {
  34. return OCRRecord::with(['student', 'questions'])->find($this->recordId);
  35. }
  36. public function mount(string $recordId): void
  37. {
  38. $this->recordId = $recordId;
  39. $record = $this->record();
  40. if ($record) {
  41. // 检查是否有关联的系统生成试卷
  42. if ($record->analysis_id) {
  43. $this->associatedPaper = Paper::where('paper_id', $record->analysis_id)->first();
  44. if ($this->associatedPaper && $this->associatedPaper->paper_type === 'auto_generated') {
  45. $this->isSystemGeneratedPaper = true;
  46. $this->initializeForSystemPaper();
  47. }
  48. }
  49. // 修复卡在处理状态的问题
  50. if ($record->status === 'processing' && $record->questions()->count() > 0) {
  51. $record->update([
  52. 'status' => 'completed',
  53. 'processed_at' => $record->processed_at ?? now(),
  54. 'total_questions' => $record->questions()->count(),
  55. 'processed_questions' => $record->questions()->count(),
  56. ]);
  57. $record = $this->record();
  58. }
  59. // 对于非系统生成的试卷,加载常规初始化
  60. if (!$this->isSystemGeneratedPaper) {
  61. $this->initializeForUploadedPaper($record);
  62. }
  63. $this->checkAnalysisResults($record);
  64. }
  65. }
  66. /**
  67. * 初始化系统生成试卷的处理逻辑
  68. */
  69. private function initializeForSystemPaper(): void
  70. {
  71. if (!$this->associatedPaper) return;
  72. // 获取系统生成试卷的题目
  73. $paperQuestions = PaperQuestion::where('paper_id', $this->associatedPaper->paper_id)
  74. ->orderBy('question_number')
  75. ->get();
  76. // 为OCR识别结果匹配系统试卷的题目
  77. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->recordId)
  78. ->orderBy('question_number')
  79. ->get();
  80. foreach ($ocrQuestions as $ocrQuestion) {
  81. // 尝试匹配相同题号的系统题目
  82. $matchedPaperQuestion = $paperQuestions->firstWhere('question_number', $ocrQuestion->question_number);
  83. if ($matchedPaperQuestion) {
  84. // 更新OCR记录,关联到系统题库
  85. $ocrQuestion->update([
  86. 'question_bank_id' => $matchedPaperQuestion->question_id,
  87. 'kp_code' => $matchedPaperQuestion->knowledge_point,
  88. 'ai_score' => null, // 等待评分
  89. ]);
  90. // 预设正确答案(来自系统试卷)
  91. $this->manualAnswers[$ocrQuestion->id] = $matchedPaperQuestion->correct_answer;
  92. }
  93. // 加载已有的评分
  94. if ($ocrQuestion->ai_score !== null || $ocrQuestion->score_value !== null) {
  95. $this->questionGrades[$ocrQuestion->id] = [
  96. 'score' => $ocrQuestion->ai_score ?? $ocrQuestion->score_value,
  97. 'is_correct' => $ocrQuestion->is_correct,
  98. 'student_answer' => $ocrQuestion->student_answer,
  99. ];
  100. }
  101. }
  102. }
  103. /**
  104. * 初始化上传试卷的处理逻辑(原逻辑)
  105. */
  106. private function initializeForUploadedPaper(OCRRecord $record): void
  107. {
  108. foreach ($record->questions as $question) {
  109. if ($question->manual_answer) {
  110. $this->manualAnswers[$question->id] = $question->manual_answer;
  111. }
  112. if ($question->ai_score !== null || $question->score_value !== null) {
  113. $this->questionGrades[$question->id] = [
  114. 'score' => $question->ai_score ?? $question->score_value,
  115. 'is_correct' => $question->is_correct,
  116. ];
  117. }
  118. $this->questionGenerationStatus[$question->id] = $question->generation_status ?? 'pending';
  119. if ($question->generation_status === 'generating' && $question->generation_task_id) {
  120. $this->isGenerating = true;
  121. $this->generationTaskId = $question->generation_task_id;
  122. }
  123. }
  124. }
  125. /**
  126. * 检查分析结果
  127. */
  128. private function checkAnalysisResults(OCRRecord $record): void
  129. {
  130. $this->hasAnalysisResults = $record->ai_analyzed_at && $record->questions()
  131. ->whereNotNull('ai_score')
  132. ->exists();
  133. }
  134. /**
  135. * 判断是否可以提交分析
  136. */
  137. public function canSubmitAnalysis(): bool
  138. {
  139. $record = $this->record();
  140. if (!$record) return false;
  141. // 对于系统生成的试卷,题目已自动匹配,无需检查question_bank_id
  142. if ($this->isSystemGeneratedPaper) {
  143. return true;
  144. }
  145. // 对于上传的试卷,需要检查是否所有题目都已关联题库
  146. return !$record->questions()->whereNull('question_bank_id')->exists();
  147. }
  148. /**
  149. * 提交分析
  150. */
  151. public function submitForAnalysis(): void
  152. {
  153. $record = $this->record();
  154. if (!$record) {
  155. Notification::make()->title('记录不存在')->danger()->send();
  156. return;
  157. }
  158. // 对于系统生成的试卷,直接进行评分分析
  159. if ($this->isSystemGeneratedPaper) {
  160. $this->analyzeSystemPaperAnswers();
  161. return;
  162. }
  163. // 原有的上传试卷分析逻辑
  164. if (!$this->canSubmitAnalysis()) {
  165. Notification::make()
  166. ->title('请先生成题库题目')
  167. ->body('分析前需要确保所有题目都已在题库中创建并关联')
  168. ->warning()
  169. ->send();
  170. return;
  171. }
  172. // ... 原有的提交分析逻辑 ...
  173. Notification::make()->title('分析提交成功')->success()->send();
  174. }
  175. /**
  176. * 分析系统生成试卷的答案
  177. */
  178. private function analyzeSystemPaperAnswers(): void
  179. {
  180. try {
  181. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->recordId)->get();
  182. $analysisResults = [];
  183. // 1. 尝试获取原始OCR数据并进行增强匹配
  184. $rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data')
  185. ->where('ocr_record_id', $this->recordId)
  186. ->value('raw_response');
  187. $enhancedAnswers = [];
  188. if ($rawOcrData) {
  189. $rawOcrData = json_decode($rawOcrData, true);
  190. $paperQuestions = PaperQuestion::where('paper_id', $this->associatedPaper->paper_id)
  191. ->orderBy('question_number')
  192. ->get();
  193. $parser = new \App\Services\OCRDataParser();
  194. $enhancedAnswers = $parser->matchWithSystemPaper($rawOcrData, $paperQuestions);
  195. \Log::info('增强匹配结果', ['record_id' => $this->recordId, 'matches' => $enhancedAnswers]);
  196. }
  197. foreach ($ocrQuestions as $ocrQuestion) {
  198. $paperQuestion = PaperQuestion::where('paper_id', $this->associatedPaper->paper_id)
  199. ->where('question_number', $ocrQuestion->question_number)
  200. ->first();
  201. if ($paperQuestion) {
  202. // 优先使用增强匹配的答案
  203. $studentAnswer = $ocrQuestion->student_answer;
  204. if (isset($enhancedAnswers[$ocrQuestion->question_number])) {
  205. $enhancedAnswer = $enhancedAnswers[$ocrQuestion->question_number]['student_answer'];
  206. if (!empty($enhancedAnswer)) {
  207. $studentAnswer = $enhancedAnswer;
  208. // 更新OCR记录中的答案,以便用户看到的是真实的提取内容
  209. $ocrQuestion->update(['student_answer' => $studentAnswer]);
  210. }
  211. }
  212. // 比较学生答案和正确答案
  213. $isCorrect = $this->compareAnswers(
  214. $studentAnswer,
  215. $paperQuestion->correct_answer
  216. );
  217. $analysisResults[] = [
  218. 'ocr_question_id' => $ocrQuestion->id,
  219. 'paper_question_id' => $paperQuestion->id,
  220. 'is_correct' => $isCorrect,
  221. 'student_answer' => $studentAnswer,
  222. 'correct_answer' => $paperQuestion->correct_answer,
  223. 'score' => $isCorrect ? $paperQuestion->score : 0,
  224. ];
  225. // 更新OCR记录
  226. $ocrQuestion->update([
  227. 'ai_score' => $isCorrect ? $paperQuestion->score : 0,
  228. 'is_correct' => $isCorrect,
  229. ]);
  230. }
  231. }
  232. // 更新OCR记录状态
  233. $record = $this->record();
  234. $record->update([
  235. 'ai_analyzed_at' => now(),
  236. ]);
  237. // 可选:调用Learning Analytics API进行深度分析
  238. $this->sendToLearningAnalytics($analysisResults);
  239. Notification::make()
  240. ->title('试卷分析完成')
  241. ->body('系统生成试卷的答题分析已完成')
  242. ->success()
  243. ->send();
  244. } catch (\Exception $e) {
  245. \Log::error('系统试卷分析失败: ' . $e->getMessage());
  246. Notification::make()
  247. ->title('分析失败')
  248. ->body('分析过程中出现错误:' . $e->getMessage())
  249. ->danger()
  250. ->send();
  251. }
  252. }
  253. /**
  254. * 比较答案(支持多种答案格式)
  255. */
  256. private function compareAnswers(string $studentAnswer, string $correctAnswer): bool
  257. {
  258. // 标准化答案格式
  259. $studentAnswer = trim(strtolower($studentAnswer));
  260. $correctAnswer = trim(strtolower($correctAnswer));
  261. // 处理选择题(A, B, C, D 或 ①, ②, ③, ④)
  262. if (preg_match('/^[a-d①②③④]$/', $studentAnswer) && preg_match('/^[a-d①②③④]$/', $correctAnswer)) {
  263. return $this->normalizeChoiceAnswer($studentAnswer) === $this->normalizeChoiceAnswer($correctAnswer);
  264. }
  265. // 处理数字答案
  266. if (is_numeric($studentAnswer) && is_numeric($correctAnswer)) {
  267. return abs(floatval($studentAnswer) - floatval($correctAnswer)) < 0.001;
  268. }
  269. // 文本答案直接比较
  270. return $studentAnswer === $correctAnswer;
  271. }
  272. /**
  273. * 标准化选择题答案格式
  274. */
  275. private function normalizeChoiceAnswer(string $answer): string
  276. {
  277. $map = [
  278. '①' => 'a', '②' => 'b', '③' => 'c', '④' => 'd',
  279. '1' => 'a', '2' => 'b', '3' => 'c', '4' => 'd'
  280. ];
  281. return $map[$answer] ?? $answer;
  282. }
  283. /**
  284. * 发送分析结果到Learning Analytics
  285. */
  286. private function sendToLearningAnalytics(array $analysisResults): void
  287. {
  288. // 实现发送逻辑...
  289. }
  290. /**
  291. * 获取试卷类型描述
  292. */
  293. public function getPaperTypeDescription(): string
  294. {
  295. if ($this->isSystemGeneratedPaper) {
  296. return '系统生成的智能试卷 - 题目已自动匹配,可直接进行答题分析';
  297. }
  298. return '上传的试卷 - 需要先生成题库题目后再进行分析';
  299. }
  300. /**
  301. * 获取匹配的题目数量
  302. */
  303. public function getMatchedQuestionsCount(): int
  304. {
  305. if ($this->isSystemGeneratedPaper) {
  306. return OCRQuestionResult::where('ocr_record_id', $this->recordId)
  307. ->whereNotNull('question_bank_id')
  308. ->count();
  309. }
  310. return 0;
  311. }
  312. }