UploadExamPaper.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Jobs\ProcessOCRRecord;
  4. use App\Models\OCRRecord;
  5. use App\Models\Student;
  6. use App\Models\Teacher;
  7. use BackedEnum;
  8. use Filament\Notifications\Notification;
  9. use Filament\Pages\Page;
  10. use Livewire\WithFileUploads;
  11. use Livewire\Attributes\Computed;
  12. use Livewire\Attributes\On;
  13. use Illuminate\Support\Facades\Storage;
  14. use UnitEnum;
  15. class UploadExamPaper extends Page
  16. {
  17. use WithFileUploads;
  18. protected static ?string $title = '上传考试卷子';
  19. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-up-tray';
  20. protected static ?string $navigationLabel = '上传考试卷子';
  21. protected static string|UnitEnum|null $navigationGroup = '操作';
  22. protected static ?int $navigationSort = 2;
  23. protected static ?string $slug = 'upload-exam-paper';
  24. protected string $view = 'filament.pages.upload-exam-paper';
  25. public ?string $teacherId = null;
  26. public ?string $studentId = null;
  27. public $uploadedImage = null;
  28. public bool $isUploading = false;
  29. public ?string $paperType = null; // 试卷类型:unit_test, midterm, final, homework, quiz, other
  30. // 新增:模式选择
  31. public string $mode = 'upload'; // 'upload' 或 'select_paper'
  32. public ?string $selectedPaperId = null;
  33. public array $questionGrades = []; // 存储每道题的评分
  34. public function mount()
  35. {
  36. $this->teacherId = null;
  37. $this->studentId = null;
  38. $this->uploadedImage = null;
  39. $this->paperType = null;
  40. $this->mode = 'upload';
  41. $this->selectedPaperId = null;
  42. $this->questionGrades = [];
  43. }
  44. #[Computed]
  45. public function teachers(): array
  46. {
  47. try {
  48. $teachers = Teacher::query()
  49. ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id')
  50. ->select(
  51. 'teachers.teacher_id',
  52. 'teachers.name',
  53. 'teachers.subject',
  54. 'u.username',
  55. 'u.email'
  56. )
  57. ->orderBy('teachers.name')
  58. ->get();
  59. // 检查是否有学生没有对应的老师记录
  60. $teacherIds = $teachers->pluck('teacher_id')->toArray();
  61. $missingTeacherIds = Student::query()
  62. ->distinct()
  63. ->whereNotIn('teacher_id', $teacherIds)
  64. ->pluck('teacher_id')
  65. ->toArray();
  66. $teachersArray = $teachers->all();
  67. if (!empty($missingTeacherIds)) {
  68. foreach ($missingTeacherIds as $missingId) {
  69. $teachersArray[] = (object) [
  70. 'teacher_id' => $missingId,
  71. 'name' => '未知老师 (' . $missingId . ')',
  72. 'subject' => '未知',
  73. 'username' => null,
  74. 'email' => null
  75. ];
  76. }
  77. usort($teachersArray, function($a, $b) {
  78. return strcmp($a->name, $b->name);
  79. });
  80. }
  81. return $teachersArray;
  82. } catch (\Exception $e) {
  83. \Illuminate\Support\Facades\Log::error('加载老师列表失败', [
  84. 'error' => $e->getMessage()
  85. ]);
  86. return [];
  87. }
  88. }
  89. #[Computed]
  90. public function students(): array
  91. {
  92. if (empty($this->teacherId)) {
  93. return [];
  94. }
  95. try {
  96. return Student::query()
  97. ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
  98. ->where('students.teacher_id', $this->teacherId)
  99. ->select(
  100. 'students.student_id',
  101. 'students.name',
  102. 'students.grade',
  103. 'students.class_name',
  104. 'u.username',
  105. 'u.email'
  106. )
  107. ->orderBy('students.grade')
  108. ->orderBy('students.class_name')
  109. ->orderBy('students.name')
  110. ->get()
  111. ->all();
  112. } catch (\Exception $e) {
  113. \Illuminate\Support\Facades\Log::error('加载学生列表失败', [
  114. 'teacher_id' => $this->teacherId,
  115. 'error' => $e->getMessage()
  116. ]);
  117. return [];
  118. }
  119. }
  120. #[Computed]
  121. public function recentRecords(): array
  122. {
  123. // 1. 获取OCR记录(图片上传)
  124. $ocrQuery = OCRRecord::with('student')->latest();
  125. // 如果选择了学生,则筛选该学生的记录
  126. if (!empty($this->studentId)) {
  127. $ocrQuery->where('user_id', $this->studentId);
  128. }
  129. $ocrRecords = $ocrQuery->take(5)
  130. ->get()
  131. ->map(function($record) {
  132. return [
  133. 'type' => 'ocr_upload',
  134. 'id' => $record->id,
  135. 'record_id' => $record->id,
  136. 'paper_id' => null,
  137. 'student_id' => $record->user_id,
  138. 'student_name' => $record->student?->name ?? $record->user_id,
  139. 'paper_type' => $record->paper_type_label,
  140. 'paper_name' => $record->image_filename ?: '未命名图片',
  141. 'status' => $record->status,
  142. 'total_questions' => $record->total_questions,
  143. 'processed_questions' => $record->processed_questions ?? 0,
  144. 'created_at' => $record->created_at->format('Y-m-d H:i'),
  145. 'is_completed' => $record->status === 'completed',
  146. ];
  147. })->toArray();
  148. // 2. 获取所有Paper记录(包括草稿和已评分)
  149. $paperQuery = \App\Models\Paper::with(['createdByUser'])->latest();
  150. // 如果选择了学生,则筛选该学生的记录
  151. if (!empty($this->studentId)) {
  152. $paperQuery->where('created_by', $this->studentId);
  153. }
  154. $allPapers = $paperQuery->take(5)
  155. ->get()
  156. ->map(function($paper) {
  157. $type = $paper->status === 'completed' ? 'graded_paper' : 'generated';
  158. $paperType = $paper->status === 'completed' ? '已评分试卷' : '系统生成试卷';
  159. $iconColor = $paper->status === 'completed' ? 'text-green-500' : 'text-blue-500';
  160. return [
  161. 'type' => $type,
  162. 'id' => $paper->id,
  163. 'record_id' => null,
  164. 'paper_id' => $paper->id,
  165. 'student_id' => $paper->created_by,
  166. 'student_name' => $paper->createdByUser?->full_name ?? $paper->created_by,
  167. 'paper_type' => $paperType,
  168. 'paper_name' => $paper->title ?? '未命名试卷',
  169. 'status' => $paper->difficulty_level,
  170. 'total_questions' => 0, // papers表没有question_count字段
  171. 'created_at' => $paper->created_at->format('Y-m-d H:i'),
  172. 'is_completed' => $paper->difficulty_level !== null,
  173. 'icon_color' => $iconColor,
  174. ];
  175. })->toArray();
  176. // 3. 合并并按时间排序
  177. $allRecords = array_merge($ocrRecords, $allPapers);
  178. usort($allRecords, function($a, $b) {
  179. return strcmp($b['created_at'], $a['created_at']);
  180. });
  181. return array_slice($allRecords, 0, 10);
  182. }
  183. /**
  184. * 获取学生的试卷列表
  185. */
  186. #[Computed]
  187. public function studentPapers(): array
  188. {
  189. if (empty($this->studentId)) {
  190. return [];
  191. }
  192. try {
  193. return \App\Models\Paper::where('created_by', $this->studentId)
  194. ->withCount('questions') // 添加题目计数
  195. ->orderBy('created_at', 'desc')
  196. ->take(20)
  197. ->get()
  198. ->map(function($paper) {
  199. return [
  200. 'paper_id' => $paper->id,
  201. 'paper_name' => $paper->title ?? '未命名试卷',
  202. 'total_questions' => $paper->questions_count ?? 0,
  203. 'total_score' => $paper->total_score ?? 0,
  204. 'created_at' => $paper->created_at->format('Y-m-d H:i'),
  205. ];
  206. })
  207. ->toArray();
  208. } catch (\Exception $e) {
  209. \Log::error('获取学生试卷列表失败', [
  210. 'student_id' => $this->studentId,
  211. 'error' => $e->getMessage()
  212. ]);
  213. return [];
  214. }
  215. }
  216. #[Computed]
  217. public function paperTypes(): array
  218. {
  219. return [
  220. '' => '请选择试卷形式',
  221. 'unit_test' => '单元测试',
  222. 'midterm' => '期中考试',
  223. 'final' => '期末考试',
  224. 'homework' => '家庭作业',
  225. 'quiz' => '随堂测验',
  226. 'other' => '其他',
  227. ];
  228. }
  229. /**
  230. * 获取选中试卷的题目列表
  231. */
  232. #[Computed]
  233. public function selectedPaperQuestions(): array
  234. {
  235. if (empty($this->selectedPaperId)) {
  236. return [];
  237. }
  238. try {
  239. // 首先检查试卷是否存在
  240. $paper = \App\Models\Paper::find($this->selectedPaperId);
  241. if (!$paper) {
  242. \Log::warning('未找到指定试卷', ['paper_id' => $this->selectedPaperId]);
  243. return [];
  244. }
  245. // 使用关联关系查询题目
  246. $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) {
  247. $query->orderBy('question_number');
  248. }])->find($this->selectedPaperId);
  249. $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]);
  250. // 处理数据不一致的情况:如果题目为空但试卷显示有题目
  251. if ($questions->isEmpty() && ($paper->question_count ?? 0) > 0) {
  252. \Log::warning('试卷显示有题目但实际题目数据缺失', [
  253. 'paper_id' => $this->selectedPaperId,
  254. 'expected_questions' => $paper->question_count,
  255. 'actual_questions' => 0
  256. ]);
  257. // 返回占位题目,让用户知道有数据缺失
  258. return [
  259. [
  260. 'id' => 'missing_data',
  261. 'question_number' => 1,
  262. 'question_bank_id' => null,
  263. 'question_type' => 'info',
  264. 'content' => "⚠️ 数据异常:试卷显示应有 {$paper->question_count} 道题目,但未找到题目数据。这通常是试卷创建过程中断导致的。请联系管理员或重新创建试卷。",
  265. 'answer' => '',
  266. 'score' => 0,
  267. 'is_missing_data' => true
  268. ]
  269. ];
  270. }
  271. if ($questions->isEmpty()) {
  272. \Log::info('试卷确实没有题目', ['paper_id' => $this->selectedPaperId]);
  273. return [
  274. [
  275. 'id' => 'no_questions',
  276. 'question_number' => 1,
  277. 'question_bank_id' => null,
  278. 'question_type' => 'info',
  279. 'content' => '该试卷暂无题目数据',
  280. 'answer' => '',
  281. 'score' => 0,
  282. 'is_empty' => true
  283. ]
  284. ];
  285. }
  286. // 获取题目详情
  287. $questionBankService = app(\App\Services\QuestionBankService::class);
  288. $questionIds = $questions->pluck('question_bank_id')->filter()->unique()->toArray();
  289. if (empty($questionIds)) {
  290. \Log::info('题目没有关联题库ID', ['paper_id' => $this->selectedPaperId]);
  291. // 返回基本的题目信息,不包含题库详情
  292. return $questions->map(function($q) {
  293. return [
  294. 'id' => $q->id,
  295. 'question_number' => $q->question_number,
  296. 'question_bank_id' => $q->question_bank_id,
  297. 'question_type' => $q->question_type,
  298. 'content' => '题目内容未关联到题库',
  299. 'answer' => '',
  300. 'score' => $q->score ?? 5,
  301. ];
  302. })->toArray();
  303. }
  304. $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
  305. $questionDetails = collect($questionsResponse['data'] ?? [])->keyBy('id');
  306. return $questions->map(function($q) use ($questionDetails) {
  307. $detail = $questionDetails->get($q->question_bank_id);
  308. return [
  309. 'id' => $q->id,
  310. 'question_number' => $q->question_number,
  311. 'question_bank_id' => $q->question_bank_id,
  312. 'question_type' => $q->question_type,
  313. 'content' => $detail['stem'] ?? '题目内容缺失',
  314. 'answer' => $detail['answer'] ?? '',
  315. 'score' => $q->score ?? 5,
  316. 'kp_code' => $q->knowledge_point, // 从本地数据库获取知识点代码
  317. ];
  318. })->toArray();
  319. } catch (\Exception $e) {
  320. \Log::error('获取试卷题目失败', [
  321. 'paper_id' => $this->selectedPaperId,
  322. 'error' => $e->getMessage(),
  323. 'trace' => $e->getTraceAsString()
  324. ]);
  325. return [
  326. [
  327. 'id' => 'error',
  328. 'question_number' => 1,
  329. 'question_bank_id' => null,
  330. 'question_type' => 'error',
  331. 'content' => '获取题目数据时发生错误:' . $e->getMessage(),
  332. 'answer' => '',
  333. 'score' => 0,
  334. 'is_error' => true
  335. ]
  336. ];
  337. }
  338. }
  339. public function updatedTeacherId($value): void
  340. {
  341. // 当教师选择变化时,清空之前选择的学生
  342. $this->studentId = null;
  343. $this->selectedPaperId = null;
  344. $this->questionGrades = [];
  345. }
  346. public function updatedStudentId($value): void
  347. {
  348. // 当学生选择变化时,清空已选试卷
  349. $this->selectedPaperId = null;
  350. $this->questionGrades = [];
  351. }
  352. public function updatedMode($value): void
  353. {
  354. // 切换模式时重置相关字段
  355. $this->uploadedImage = null;
  356. $this->selectedPaperId = null;
  357. $this->questionGrades = [];
  358. }
  359. public function submitUpload(): void
  360. {
  361. if (!$this->teacherId) {
  362. Notification::make()
  363. ->title('请选择老师')
  364. ->danger()
  365. ->send();
  366. return;
  367. }
  368. if (!$this->studentId) {
  369. Notification::make()
  370. ->title('请选择学生')
  371. ->danger()
  372. ->send();
  373. return;
  374. }
  375. if (!$this->uploadedImage) {
  376. Notification::make()
  377. ->title('请上传图片')
  378. ->danger()
  379. ->send();
  380. return;
  381. }
  382. $this->isUploading = true;
  383. try {
  384. // 保存图片
  385. $path = $this->uploadedImage->store('ocr-uploads', 'public');
  386. $filename = basename($path);
  387. // 创建OCR记录
  388. $record = OCRRecord::create([
  389. 'student_id' => $this->studentId,
  390. 'image_path' => $path,
  391. 'image_filename' => $filename,
  392. 'paper_type' => $this->paperType,
  393. 'status' => 'pending',
  394. 'total_questions' => 0,
  395. 'processed_questions' => 0,
  396. ]);
  397. // 立即更新状态为处理中,提供更好的用户体验
  398. $record->update(['status' => 'processing']);
  399. // 自动触发OCR处理
  400. ProcessOCRRecord::dispatch($record->id);
  401. // 重置表单
  402. $this->teacherId = null;
  403. $this->studentId = null;
  404. $this->uploadedImage = null;
  405. $this->paperType = null;
  406. Notification::make()
  407. ->title('上传成功')
  408. ->body("卷子已上传并开始OCR处理,正在跳转到校准页面...")
  409. ->success()
  410. ->send();
  411. // 跳转到OCR记录详情页面进行校准和提交分析
  412. $this->redirect("/admin/ocr-record-view/{$record->id}");
  413. } catch (\Exception $e) {
  414. Notification::make()
  415. ->title('上传失败')
  416. ->body($e->getMessage())
  417. ->danger()
  418. ->send();
  419. } finally {
  420. $this->isUploading = false;
  421. }
  422. }
  423. #[On('teacherChanged')]
  424. public function updateTeacherId($teacherId)
  425. {
  426. $this->teacherId = $teacherId;
  427. $this->studentId = null;
  428. }
  429. #[On('studentChanged')]
  430. public function updateStudentId($teacherId, $studentId)
  431. {
  432. $this->studentId = $studentId;
  433. }
  434. public function removeImage(): void
  435. {
  436. $this->uploadedImage = null;
  437. }
  438. /**
  439. * 提交手动评分
  440. */
  441. public function submitManualGrading(): void
  442. {
  443. if (!$this->selectedPaperId) {
  444. Notification::make()
  445. ->title('请选择试卷')
  446. ->danger()
  447. ->send();
  448. return;
  449. }
  450. if (empty($this->questionGrades)) {
  451. Notification::make()
  452. ->title('请至少为一道题目评分')
  453. ->danger()
  454. ->send();
  455. return;
  456. }
  457. try {
  458. // 准备数据发送到 LearningAnalytics
  459. $analyticsData = [];
  460. // 获取题目详情以便查找kp_code
  461. $questionsMap = collect($this->selectedPaperQuestions)->keyBy('id');
  462. // 收集需要从API补充信息的题目ID
  463. $missingKpCodeQuestionIds = [];
  464. foreach ($this->questionGrades as $questionId => $grade) {
  465. $question = $questionsMap->get($questionId);
  466. if (!$question) {
  467. continue;
  468. }
  469. // 优先使用本地存储的 kp_code
  470. if (empty($question['kp_code'])) {
  471. $missingKpCodeQuestionIds[] = $questionId;
  472. }
  473. }
  474. // 如果有缺失 kp_code 的题目,尝试从 API 获取
  475. $apiDetailsMap = collect([]);
  476. if (!empty($missingKpCodeQuestionIds)) {
  477. $questionBankIds = collect($missingKpCodeQuestionIds)
  478. ->map(fn($qId) => $questionsMap->get($qId)['question_bank_id'] ?? null)
  479. ->filter()
  480. ->toArray();
  481. if (!empty($questionBankIds)) {
  482. $questionBankService = app(\App\Services\QuestionBankService::class);
  483. $questionsDetails = $questionBankService->getQuestionsByIds($questionBankIds);
  484. $apiDetailsMap = collect($questionsDetails['data'] ?? [])->keyBy('id');
  485. }
  486. }
  487. foreach ($this->questionGrades as $questionId => $grade) {
  488. $question = $questionsMap->get($questionId);
  489. if (!$question) {
  490. continue;
  491. }
  492. $kpCode = $question['kp_code'];
  493. // 如果本地没有,尝试从API结果中获取
  494. if (empty($kpCode)) {
  495. $detail = $apiDetailsMap->get($question['question_bank_id']);
  496. $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
  497. }
  498. $analyticsData[] = [
  499. 'question_bank_id' => $question['question_bank_id'],
  500. 'student_answer' => $grade['student_answer'] ?? '',
  501. 'is_correct' => $grade['is_correct'] ?? null,
  502. 'score' => $grade['score'] ?? null,
  503. 'max_score' => $question['score'],
  504. 'kp_code' => $kpCode, // 添加 kp_code
  505. ];
  506. }
  507. // 调用 LearningAnalytics 服务
  508. $learningAnalyticsService = app(\App\Services\LearningAnalyticsService::class);
  509. // 步骤0: 保存学生答案到本地数据库 (重要:确保数据持久化)
  510. foreach ($this->questionGrades as $questionId => $grade) {
  511. \App\Models\PaperQuestion::where('id', $questionId)->update([
  512. 'student_answer' => $grade['student_answer'] ?? '',
  513. 'is_correct' => $grade['is_correct'] ?? false,
  514. 'score_obtained' => $grade['score'] ?? 0,
  515. ]);
  516. }
  517. \Log::info('学生答案已保存到数据库', [
  518. 'student_id' => $this->studentId,
  519. 'paper_id' => $this->selectedPaperId,
  520. 'updated_count' => count($this->questionGrades)
  521. ]);
  522. // 步骤1: 保存答题记录到 LearningAnalytics
  523. \Log::info('准备调用submitBatchAttempts API', [
  524. 'student_id' => $this->studentId,
  525. 'paper_id' => $this->selectedPaperId,
  526. 'analytics_data_sample' => array_slice($analyticsData, 0, 2) // 记录前2题的数据作为样本
  527. ]);
  528. $result = $learningAnalyticsService->submitBatchAttempts($this->studentId, [
  529. 'paper_id' => $this->selectedPaperId,
  530. 'answers' => $analyticsData,
  531. ]);
  532. // 检查API返回结果
  533. if (is_array($result) && isset($result['error']) && $result['error']) {
  534. throw new \Exception($result['message'] ?? 'API调用失败');
  535. }
  536. if ($result === null || (is_array($result) && empty($result))) {
  537. throw new \Exception('API返回空数据');
  538. }
  539. \Log::info('答题记录已保存到学习分析服务', [
  540. 'student_id' => $this->studentId,
  541. 'paper_id' => $this->selectedPaperId,
  542. 'count' => count($analyticsData)
  543. ]);
  544. // 步骤2: 触发 AI 分析(包含掌握度更新和学习报告生成)
  545. try {
  546. $paper = \App\Models\Paper::find($this->selectedPaperId);
  547. // 构造 AI 分析请求数据
  548. $analysisQuestions = [];
  549. foreach ($this->questionGrades as $questionId => $grade) {
  550. $question = $questionsMap->get($questionId);
  551. if (!$question) {
  552. continue;
  553. }
  554. $kpCode = $question['kp_code'];
  555. if (empty($kpCode)) {
  556. $detail = $apiDetailsMap->get($question['question_bank_id']);
  557. $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
  558. }
  559. $analysisQuestions[] = [
  560. 'question_id' => $question['question_bank_id'],
  561. 'question_number' => (string)$question['question_number'],
  562. 'question_text' => $question['content'] ?? '',
  563. 'student_answer' => $grade['student_answer'] ?? '',
  564. 'correct_answer' => $question['answer'] ?? '',
  565. 'kp_code' => $kpCode,
  566. 'score_value' => $grade['score'] ?? 0,
  567. 'max_score' => $question['score'],
  568. 'is_correct' => $grade['is_correct'] ?? false,
  569. 'teacher_validated' => true, // 手动评分即为教师验证
  570. 'ocr_confidence' => 1.0, // 手动评分置信度为1
  571. ];
  572. }
  573. $analysisData = [
  574. 'exam_id' => $this->selectedPaperId,
  575. 'student_id' => $this->studentId,
  576. 'ocr_record_id' => 0, // 系统生成卷子没有OCR记录ID
  577. 'paper_id' => $this->selectedPaperId,
  578. 'teacher_name' => auth()->user()->name ?? 'Teacher',
  579. 'analysis_type' => 'mastery',
  580. 'questions' => $analysisQuestions,
  581. ];
  582. // 调用统一的 AI 分析接口
  583. \Log::info('准备调用submitOCRAnalysis API', [
  584. 'paper_id' => $this->selectedPaperId,
  585. 'student_id' => $this->studentId,
  586. 'analysis_data_sample' => [
  587. 'question_count' => count($analysisQuestions),
  588. 'first_question' => $analysisQuestions[0] ?? null
  589. ]
  590. ]);
  591. $analysisResult = $learningAnalyticsService->submitOCRAnalysis($analysisData);
  592. \Log::info('AI分析已触发', [
  593. 'paper_id' => $this->selectedPaperId,
  594. 'student_id' => $this->studentId,
  595. 'analysis_result_keys' => is_array($analysisResult) ? array_keys($analysisResult) : 'not_array',
  596. 'analysis_result' => $analysisResult
  597. ]);
  598. // 保存 analysis_id 到 Paper 表
  599. if (isset($analysisResult['analysis_id'])) {
  600. \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
  601. 'analysis_id' => $analysisResult['analysis_id'],
  602. ]);
  603. \Log::info('已保存 analysis_id', [
  604. 'paper_id' => $this->selectedPaperId,
  605. 'analysis_id' => $analysisResult['analysis_id']
  606. ]);
  607. }
  608. } catch (\Exception $analysisError) {
  609. // AI 分析失败不影响主流程
  610. \Log::warning('触发AI分析失败', [
  611. 'paper_id' => $this->selectedPaperId,
  612. 'error' => $analysisError->getMessage()
  613. ]);
  614. }
  615. // 更新Paper表状态为已完成评分
  616. \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
  617. 'status' => 'completed',
  618. 'completed_at' => now(),
  619. ]);
  620. Notification::make()
  621. ->title('提交成功')
  622. ->body('评分已提交,AI分析正在进行中')
  623. ->success()
  624. ->send();
  625. // 刷新最近记录列表
  626. unset($this->recentRecords);
  627. // 重置表单
  628. $this->selectedPaperId = null;
  629. $this->questionGrades = [];
  630. } catch (\Exception $e) {
  631. \Log::error('提交手动评分失败', [
  632. 'error' => $e->getMessage(),
  633. 'student_id' => $this->studentId,
  634. 'paper_id' => $this->selectedPaperId,
  635. ]);
  636. Notification::make()
  637. ->title('提交失败')
  638. ->body($e->getMessage())
  639. ->danger()
  640. ->send();
  641. }
  642. }
  643. }