OCRPaperAnalysisView.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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 Filament\Notifications\Notification;
  8. use Filament\Pages\Page;
  9. use Livewire\Attributes\Computed;
  10. use Livewire\Attributes\On;
  11. class OCRPaperAnalysisView extends Page
  12. {
  13. protected static ?string $title = '试卷答题分析';
  14. protected static ?string $slug = 'ocr-paper-analysis/{recordId}';
  15. // 不在导航中显示,通过链接直接访问
  16. protected string $view = 'filament.pages.ocr-paper-analysis';
  17. public static function shouldRegisterNavigation(): bool
  18. {
  19. return false; // 不显示在导航菜单中,通过链接直接访问
  20. }
  21. public string $recordId = '';
  22. public ?Paper $paper = null;
  23. public ?OCRRecord $ocrRecord = null;
  24. public array $matchedQuestions = [];
  25. public array $analysisResults = [];
  26. public bool $isAnalyzing = false;
  27. #[Computed]
  28. public function paperInfo(): array
  29. {
  30. if (!$this->paper) return [];
  31. return [
  32. 'paper_id' => $this->paper->paper_id,
  33. 'paper_name' => $this->paper->paper_name,
  34. 'student_id' => $this->paper->student_id,
  35. 'teacher_id' => $this->paper->teacher_id,
  36. 'total_questions' => $this->paper->total_questions,
  37. 'total_score' => $this->paper->total_score,
  38. 'status' => $this->paper->status,
  39. 'created_at' => $this->paper->created_at->format('Y-m-d H:i'),
  40. ];
  41. }
  42. #[Computed]
  43. public function studentInfo(): ?array
  44. {
  45. if (!$this->paper || !$this->paper->student_id) return null;
  46. $student = \App\Models\Student::find($this->paper->student_id);
  47. if (!$student) return null;
  48. return [
  49. 'student_id' => $student->student_id,
  50. 'name' => $student->name,
  51. 'grade' => $student->grade,
  52. 'class' => $student->class_name,
  53. ];
  54. }
  55. public function mount(string $recordId): void
  56. {
  57. $this->recordId = $recordId;
  58. $this->loadOCRRecord();
  59. if ($this->ocrRecord && $this->ocrRecord->analysis_id) {
  60. $this->loadPaper();
  61. if ($this->paper) {
  62. $this->matchQuestions();
  63. }
  64. }
  65. }
  66. /**
  67. * 加载OCR记录
  68. */
  69. private function loadOCRRecord(): void
  70. {
  71. $this->ocrRecord = OCRRecord::with(['student', 'questions'])
  72. ->where('id', $this->recordId)
  73. ->first();
  74. if (!$this->ocrRecord) {
  75. Notification::make()
  76. ->title('错误')
  77. ->body('OCR记录不存在')
  78. ->danger()
  79. ->send();
  80. return;
  81. }
  82. }
  83. /**
  84. * 加载关联的试卷
  85. */
  86. private function loadPaper(): void
  87. {
  88. $this->paper = Paper::where('paper_id', $this->ocrRecord->analysis_id)->first();
  89. }
  90. /**
  91. * 匹配OCR识别结果与系统试卷题目
  92. */
  93. private function matchQuestions(): void
  94. {
  95. if (!$this->ocrRecord || !$this->paper) return;
  96. // 获取系统试卷的题目(作为匹配基准)
  97. $paperQuestions = PaperQuestion::where('paper_id', $this->paper->paper_id)
  98. ->orderBy('question_number')
  99. ->get();
  100. // 获取OCR识别的题目
  101. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->ocrRecord->id)
  102. ->orderBy('question_number')
  103. ->get();
  104. // 如果没有OCR题目记录,尝试从原始数据恢复
  105. if ($ocrQuestions->isEmpty()) {
  106. $rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data')
  107. ->where('ocr_record_id', $this->ocrRecord->id)
  108. ->value('raw_response');
  109. if ($rawOcrData) {
  110. try {
  111. $rawOcrData = json_decode($rawOcrData, true);
  112. $parser = new \App\Services\OCRDataParser();
  113. $matchedResults = $parser->matchWithSystemPaper($rawOcrData, $paperQuestions);
  114. foreach ($matchedResults as $qNum => $result) {
  115. OCRQuestionResult::create([
  116. 'ocr_record_id' => $this->ocrRecord->id,
  117. 'question_number' => $qNum,
  118. 'question_text' => '系统题目 ' . $qNum, // 占位
  119. 'student_answer' => $result['student_answer'],
  120. 'score_confidence' => $result['confidence'],
  121. 'score_value' => 0,
  122. ]);
  123. }
  124. // 重新获取
  125. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $this->ocrRecord->id)
  126. ->orderBy('question_number')
  127. ->get();
  128. \Log::info('从原始数据恢复了OCR题目记录', ['count' => $ocrQuestions->count()]);
  129. } catch (\Exception $e) {
  130. \Log::error('从原始数据恢复OCR记录失败: ' . $e->getMessage());
  131. }
  132. }
  133. }
  134. $this->matchedQuestions = [];
  135. // 以系统试卷的题目为基准进行匹配
  136. foreach ($paperQuestions as $paperQuestion) {
  137. // 查找对应题号的OCR识别结果(可能有多个,取第一个有效的)
  138. $ocrQuestion = $ocrQuestions
  139. ->where('question_number', $paperQuestion->question_number)
  140. ->first();
  141. if ($ocrQuestion) {
  142. $this->matchedQuestions[] = [
  143. 'question_number' => $paperQuestion->question_number,
  144. 'ocr_id' => $ocrQuestion->id,
  145. 'paper_id' => $paperQuestion->id,
  146. 'question_text' => $paperQuestion->question_text ?: '系统题目',
  147. 'knowledge_point' => $paperQuestion->knowledge_point,
  148. 'question_type' => $paperQuestion->question_type,
  149. 'student_answer' => $ocrQuestion->student_answer,
  150. 'correct_answer' => $paperQuestion->correct_answer,
  151. 'full_score' => $paperQuestion->score,
  152. 'is_correct' => $ocrQuestion->is_correct,
  153. 'score' => $ocrQuestion->ai_score,
  154. 'ocr_confidence' => $ocrQuestion->score_confidence ?? null,
  155. 'bbox' => $ocrQuestion->student_answer_bbox,
  156. ];
  157. // 更新OCR记录,关联到题库
  158. if (!$ocrQuestion->question_bank_id) {
  159. $ocrQuestion->update([
  160. 'question_bank_id' => $paperQuestion->question_id,
  161. 'kp_code' => $paperQuestion->knowledge_point,
  162. ]);
  163. }
  164. } else {
  165. // 没有找到对应的OCR结果,但仍然显示系统题目
  166. $this->matchedQuestions[] = [
  167. 'question_number' => $paperQuestion->question_number,
  168. 'ocr_id' => null,
  169. 'paper_id' => $paperQuestion->id,
  170. 'question_text' => $paperQuestion->question_text ?: '系统题目',
  171. 'knowledge_point' => $paperQuestion->knowledge_point,
  172. 'question_type' => $paperQuestion->question_type,
  173. 'student_answer' => null,
  174. 'correct_answer' => $paperQuestion->correct_answer,
  175. 'full_score' => $paperQuestion->score,
  176. 'is_correct' => null,
  177. 'score' => null,
  178. 'ocr_confidence' => null,
  179. 'note' => 'OCR未识别到此题'
  180. ];
  181. }
  182. }
  183. // 记录匹配统计
  184. \Log::info('OCR题目匹配完成', [
  185. 'ocr_record_id' => $this->ocrRecord->id,
  186. 'paper_id' => $this->paper->paper_id,
  187. 'system_questions' => $paperQuestions->count(),
  188. 'ocr_questions_total' => $ocrQuestions->count(),
  189. 'matched_questions' => count($this->matchedQuestions),
  190. 'ocr_question_numbers' => $ocrQuestions->pluck('question_number')->unique()->values()->toArray(),
  191. ]);
  192. }
  193. /**
  194. * 执行答题分析
  195. */
  196. public function analyzeAnswers(): void
  197. {
  198. $this->isAnalyzing = true;
  199. try {
  200. $this->analysisResults = [];
  201. $totalScore = 0;
  202. $correctCount = 0;
  203. foreach ($this->matchedQuestions as &$matched) {
  204. $analysis = $this->analyzeAnswer(
  205. $matched['student_answer'],
  206. $matched['correct_answer'],
  207. $matched['question_type'],
  208. $matched['full_score']
  209. );
  210. $matched['is_correct'] = $analysis['is_correct'];
  211. $matched['score'] = $analysis['score'];
  212. $matched['analysis_details'] = $analysis['details'];
  213. $totalScore += $analysis['score'];
  214. if ($analysis['is_correct']) {
  215. $correctCount++;
  216. }
  217. // 更新数据库
  218. OCRQuestionResult::where('id', $matched['ocr_id'])
  219. ->update([
  220. 'ai_score' => $analysis['score'],
  221. 'is_correct' => $analysis['is_correct'],
  222. ]);
  223. $this->analysisResults[] = $analysis;
  224. }
  225. unset($matched);
  226. // 更新OCR记录状态
  227. $this->ocrRecord->update([
  228. 'ai_analyzed_at' => now(),
  229. ]);
  230. // 计算统计信息
  231. $stats = [
  232. 'total_questions' => count($this->matchedQuestions),
  233. 'correct_count' => $correctCount,
  234. 'incorrect_count' => count($this->matchedQuestions) - $correctCount,
  235. 'total_score' => $totalScore,
  236. 'full_score' => $this->paper->total_score,
  237. 'accuracy_rate' => round(($correctCount / count($this->matchedQuestions)) * 100, 2),
  238. 'score_rate' => round(($totalScore / $this->paper->total_score) * 100, 2),
  239. ];
  240. // 可选:发送到Learning Analytics进行深度分析
  241. $this->sendToLearningAnalytics($stats);
  242. Notification::make()
  243. ->title('分析完成')
  244. ->body(sprintf(
  245. '共%d道题,正确%d道,得分%d分(满分%d分)',
  246. $stats['total_questions'],
  247. $stats['correct_count'],
  248. $stats['total_score'],
  249. $stats['full_score']
  250. ))
  251. ->success()
  252. ->send();
  253. } catch (\Exception $e) {
  254. \Log::error('试卷答题分析失败: ' . $e->getMessage());
  255. Notification::make()
  256. ->title('分析失败')
  257. ->body($e->getMessage())
  258. ->danger()
  259. ->send();
  260. } finally {
  261. $this->isAnalyzing = false;
  262. }
  263. }
  264. /**
  265. * 分析单个答案
  266. */
  267. private function analyzeAnswer(?string $studentAnswer, ?string $correctAnswer, string $questionType, float $fullScore): array
  268. {
  269. $studentAnswer = trim($studentAnswer ?? '');
  270. $correctAnswer = trim($correctAnswer ?? '');
  271. // 空答案处理
  272. if (empty($studentAnswer)) {
  273. return [
  274. 'is_correct' => false,
  275. 'score' => 0,
  276. 'details' => '未作答'
  277. ];
  278. }
  279. $isCorrect = false;
  280. $score = 0;
  281. $details = '';
  282. // 根据题型进行不同的分析
  283. switch ($questionType) {
  284. case '选择题':
  285. case 'choice':
  286. $result = $this->analyzeChoiceAnswer($studentAnswer, $correctAnswer);
  287. break;
  288. case '填空题':
  289. case 'fill':
  290. $result = $this->analyzeFillAnswer($studentAnswer, $correctAnswer);
  291. break;
  292. case '解答题':
  293. case 'answer':
  294. $result = $this->analyzeAnswerAnswer($studentAnswer, $correctAnswer, $fullScore);
  295. break;
  296. default:
  297. $result = $this->analyzeGeneralAnswer($studentAnswer, $correctAnswer, $fullScore);
  298. }
  299. return $result;
  300. }
  301. /**
  302. * 分析选择题答案
  303. */
  304. private function analyzeChoiceAnswer(string $studentAnswer, string $correctAnswer): array
  305. {
  306. $studentAnswer = $this->normalizeChoiceAnswer($studentAnswer);
  307. $correctAnswer = $this->normalizeChoiceAnswer($correctAnswer);
  308. $isCorrect = $studentAnswer === $correctAnswer;
  309. return [
  310. 'is_correct' => $isCorrect,
  311. 'score' => $isCorrect ? $fullScore : 0,
  312. 'details' => $isCorrect ? '正确' : '错误'
  313. ];
  314. }
  315. /**
  316. * 分析填空题答案
  317. */
  318. private function analyzeFillAnswer(string $studentAnswer, string $correctAnswer): array
  319. {
  320. // 精确匹配
  321. if (strcasecmp($studentAnswer, $correctAnswer) === 0) {
  322. return [
  323. 'is_correct' => true,
  324. 'score' => $fullScore,
  325. 'details' => '完全正确'
  326. ];
  327. }
  328. // 去除空格后匹配
  329. if (strcasecmp(str_replace(' ', '', $studentAnswer), str_replace(' ', '', $correctAnswer)) === 0) {
  330. return [
  331. 'is_correct' => true,
  332. 'score' => $fullScore * 0.9, // 扣10%
  333. 'details' => '基本正确(多空格)'
  334. ];
  335. }
  336. // 数值比较
  337. if (is_numeric($studentAnswer) && is_numeric($correctAnswer)) {
  338. if (abs(floatval($studentAnswer) - floatval($correctAnswer)) < 0.001) {
  339. return [
  340. 'is_correct' => true,
  341. 'score' => $fullScore,
  342. 'details' => '数值正确'
  343. ];
  344. }
  345. }
  346. return [
  347. 'is_correct' => false,
  348. 'score' => 0,
  349. 'details' => '错误'
  350. ];
  351. }
  352. /**
  353. * 分析解答题答案(简化版,实际需要更复杂的评分逻辑)
  354. */
  355. private function analyzeAnswerAnswer(string $studentAnswer, string $correctAnswer, float $fullScore): array
  356. {
  357. // 简化处理:解答题需要人工评分或更复杂的AI评分
  358. // 这里仅做简单的文本相似度比较
  359. $similar = similar_text($studentAnswer, $correctAnswer, $percent);
  360. if ($percent > 80) {
  361. return [
  362. 'is_correct' => true,
  363. 'score' => $fullScore,
  364. 'details' => sprintf('相似度%.1f%%,建议人工复核', $percent)
  365. ];
  366. } elseif ($percent > 50) {
  367. return [
  368. 'is_correct' => false,
  369. 'score' => $fullScore * 0.5,
  370. 'details' => sprintf('部分正确(相似度%.1f%%)', $percent)
  371. ];
  372. } else {
  373. return [
  374. 'is_correct' => false,
  375. 'score' => 0,
  376. 'details' => sprintf('相似度%.1f%%', $percent)
  377. ];
  378. }
  379. }
  380. /**
  381. * 通用答案分析
  382. */
  383. private function analyzeGeneralAnswer(string $studentAnswer, string $correctAnswer, float $fullScore): array
  384. {
  385. $isCorrect = strcasecmp($studentAnswer, $correctAnswer) === 0;
  386. return [
  387. 'is_correct' => $isCorrect,
  388. 'score' => $isCorrect ? $fullScore : 0,
  389. 'details' => $isCorrect ? '正确' : '错误'
  390. ];
  391. }
  392. /**
  393. * 标准化选择题答案
  394. */
  395. private function normalizeChoiceAnswer(string $answer): string
  396. {
  397. // 处理各种格式:A, B, C, D 或 a, b, c, d 或 ①, ②, ③, ④ 或 1, 2, 3, 4
  398. $map = [
  399. '①' => 'a', '②' => 'b', '③' => 'c', '④' => 'd',
  400. '1' => 'a', '2' => 'b', '3' => 'c', '4' => 'd',
  401. 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd',
  402. ];
  403. $answer = trim($answer);
  404. return $map[$answer] ?? strtolower($answer);
  405. }
  406. /**
  407. * 发送分析结果到Learning Analytics(已本地化)
  408. */
  409. private function sendToLearningAnalytics(array $stats): void
  410. {
  411. try {
  412. // 本地保存分析结果,不再调用外部API
  413. \Illuminate\Support\Facades\DB::table('exam_analysis_results')
  414. ->updateOrInsert(
  415. [
  416. 'student_id' => $this->paper->student_id,
  417. 'exam_id' => $this->paper->paper_id,
  418. ],
  419. [
  420. 'analysis_data' => json_encode([
  421. 'analysis_type' => 'ocr_matching',
  422. 'stats' => $stats,
  423. 'detailed_results' => $this->matchedQuestions,
  424. 'timestamp' => now()->toISOString(),
  425. ]),
  426. 'created_at' => now(),
  427. 'updated_at' => now(),
  428. ]
  429. );
  430. \Log::info('分析结果已本地保存');
  431. } catch (\Exception $e) {
  432. \Log::error('保存分析结果失败: ' . $e->getMessage());
  433. }
  434. }
  435. /**
  436. * 导出报告
  437. */
  438. public function exportReport(): void
  439. {
  440. // 生成简单的文本报告
  441. $report = $this->generateTextReport();
  442. $filename = "试卷分析报告_" . date('Y-m-d_H-i-s') . ".txt";
  443. $filepath = storage_path("reports/" . $filename);
  444. // 确保目录存在
  445. if (!is_dir(dirname($filepath))) {
  446. mkdir(dirname($filepath), 0755, true);
  447. }
  448. file_put_contents($filepath, $report);
  449. // 提供下载链接
  450. Notification::make()
  451. ->title('报告导出成功')
  452. ->body('报告已保存到:' . $filename)
  453. ->success()
  454. ->send();
  455. }
  456. /**
  457. * 生成文本报告
  458. */
  459. private function generateTextReport(): string
  460. {
  461. $stats = $this->getAnalysisStats();
  462. $report = "";
  463. // 报告头部
  464. $report .= "=====================================\n";
  465. $report .= "试卷分析报告\n";
  466. $report .= "=====================================\n\n";
  467. $report .= "试卷名称:" . $this->paper->paper_name . "\n";
  468. $report .= "学生姓名:" . ($this->studentInfo()['name'] ?? '未知') . "\n";
  469. $report .= "班级:" . ($this->studentInfo()['class'] ?? '未知') . "\n";
  470. $report .= "分析时间:" . date('Y-m-d H:i:s') . "\n\n";
  471. // 统计信息
  472. $report .= "【统计信息】\n";
  473. $report .= "-------------------------\n";
  474. $report .= "题目总数:" . $stats['total'] . "题\n";
  475. $report .= "正确题数:" . $stats['correct'] . "题\n";
  476. $report .= "错误题数:" . $stats['incorrect'] . "题\n";
  477. $report .= "正确率:" . $stats['accuracy'] . "%\n";
  478. $report .= "得分:" . $stats['score'] . "分\n";
  479. $report .= "满分:" . $stats['full_score'] . "分\n";
  480. $report .= "得分率:" . $stats['score_rate'] . "%\n\n";
  481. // 详细题目分析
  482. $report .= "【题目详情】\n";
  483. $report .= "-------------------------\n";
  484. foreach ($this->matchedQuestions as $index => $question) {
  485. $report .= "\n题目" . ($index + 1) . ":\n";
  486. $report .= " 知识点:" . ($question['knowledge_point'] ?? '未知') . "\n";
  487. $report .= " 学生答案:" . ($question['student_answer'] ?: '未作答') . "\n";
  488. $report .= " 正确答案:" . ($question['correct_answer'] ?? '未知') . "\n";
  489. $report .= " 得分:" . ($question['score'] ?? 0) . "分\n";
  490. $report .= " 状态:" . ($question['is_correct'] ? '正确' : '错误') . "\n";
  491. if (isset($question['analysis_details'])) {
  492. $report .= " 说明:" . $question['analysis_details'] . "\n";
  493. }
  494. }
  495. return $report;
  496. }
  497. /**
  498. * 获取分析统计信息
  499. */
  500. public function getAnalysisStats(): array
  501. {
  502. if (empty($this->analysisResults)) return [];
  503. $correct = 0;
  504. $total = count($this->matchedQuestions);
  505. $score = 0;
  506. foreach ($this->matchedQuestions as $matched) {
  507. if ($matched['is_correct']) $correct++;
  508. $score += $matched['score'];
  509. }
  510. return [
  511. 'total' => $total,
  512. 'correct' => $correct,
  513. 'incorrect' => $total - $correct,
  514. 'accuracy' => $total > 0 ? round(($correct / $total) * 100, 2) : 0,
  515. 'score' => $score,
  516. 'full_score' => $this->paper->total_score,
  517. 'score_rate' => $this->paper->total_score > 0 ? round(($score / $this->paper->total_score) * 100, 2) : 0,
  518. ];
  519. }
  520. /**
  521. * 重新匹配题目
  522. */
  523. public function rematchQuestions(): void
  524. {
  525. try {
  526. $rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data')
  527. ->where('ocr_record_id', $this->ocrRecord->id)
  528. ->value('raw_response');
  529. if (!$rawOcrData) {
  530. Notification::make()->title('未找到原始OCR数据')->danger()->send();
  531. return;
  532. }
  533. $rawOcrData = json_decode($rawOcrData, true);
  534. $paperQuestions = PaperQuestion::where('paper_id', $this->paper->paper_id)
  535. ->orderBy('question_number')
  536. ->get();
  537. $ocrService = app(\App\Services\OCRService::class);
  538. $matchedResults = $ocrService->performEnhancedMatching($this->ocrRecord, $rawOcrData, $paperQuestions);
  539. // 更新现有记录
  540. foreach ($matchedResults as $result) {
  541. OCRQuestionResult::updateOrCreate(
  542. [
  543. 'ocr_record_id' => $this->ocrRecord->id,
  544. 'question_number' => $result['question_number'],
  545. ],
  546. [
  547. 'student_answer' => $result['student_answer'],
  548. 'score_confidence' => $result['confidence'],
  549. 'student_answer_bbox' => $result['student_answer_bbox'] ?? null,
  550. 'question_text' => '系统题目 ' . $result['question_number'], // 确保有值
  551. ]
  552. );
  553. }
  554. // 刷新页面数据
  555. $this->matchQuestions();
  556. Notification::make()->title('重新匹配完成')->success()->send();
  557. } catch (\Exception $e) {
  558. \Log::error('重新匹配失败: ' . $e->getMessage());
  559. Notification::make()->title('重新匹配失败')->body($e->getMessage())->danger()->send();
  560. }
  561. }
  562. /**
  563. * 提交AI分析
  564. */
  565. public function submitForAiAnalysis(): void
  566. {
  567. try {
  568. // 确保统计信息是最新的
  569. $stats = $this->getAnalysisStats();
  570. if (empty($stats)) {
  571. // 如果还没有分析过,先简单统计一下(或者强制先运行 analyzeAnswers)
  572. $this->analyzeAnswers();
  573. $stats = $this->getAnalysisStats();
  574. }
  575. $this->sendToLearningAnalytics($stats);
  576. Notification::make()->title('已提交AI分析请求')->success()->send();
  577. } catch (\Exception $e) {
  578. Notification::make()->title('提交失败')->body($e->getMessage())->danger()->send();
  579. }
  580. }
  581. /**
  582. * 判断是否已完成分析
  583. */
  584. public function hasAnalysis(): bool
  585. {
  586. return !empty($this->analysisResults) ||
  587. ($this->ocrRecord && $this->ocrRecord->ai_analyzed_at);
  588. }
  589. }