UploadExamPaper.php 28 KB

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