ExamAnalysis.php 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Models\OCRRecord;
  4. use App\Models\OCRQuestionResult;
  5. use App\Services\LearningAnalyticsService;
  6. use App\Services\OCRService;
  7. use App\Services\ChatGPTAnalysisService;
  8. use BackedEnum;
  9. use Filament\Notifications\Notification;
  10. use Filament\Pages\Page;
  11. use Livewire\Attributes\Url;
  12. use UnitEnum;
  13. class ExamAnalysis extends Page
  14. {
  15. protected static ?string $title = '试卷分析详情';
  16. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
  17. protected static ?string $navigationLabel = '试卷分析';
  18. protected static string|UnitEnum|null $navigationGroup = '管理';
  19. protected static ?int $navigationSort = 15;
  20. #[Url]
  21. public ?string $recordId = null; // OCR记录ID
  22. #[Url]
  23. public ?string $paperId = null; // 系统生成卷子ID
  24. public array $recordData = [];
  25. public array $analysisData = []; // 整体掌握度分析数据
  26. public array $paperAnalysisData = []; // 本次试卷的分析结果
  27. public array $studentInfo = [];
  28. public bool $loading = true;
  29. public string $recordType = ''; // 'ocr' 或 'generated'
  30. // ChatGPT识别相关
  31. public ?string $imageUrl = null; // 试卷图片URL
  32. public bool $useChatGPT = false; // 是否使用ChatGPT识别
  33. public bool $isAnalyzing = false; // 是否正在分析
  34. public array $chatGPTResult = []; // ChatGPT分析结果
  35. public function mount()
  36. {
  37. // 允许使用 recordId(OCR记录)或 paperId(系统生成卷子)
  38. if (!$this->recordId && !$this->paperId) {
  39. Notification::make()
  40. ->title('错误')
  41. ->body('缺少记录ID或试卷ID')
  42. ->danger()
  43. ->send();
  44. $this->redirectRoute('filament.admin.pages.upload-exam-paper');
  45. return;
  46. }
  47. // 根据记录类型选择不同的视图
  48. if ($this->recordId) {
  49. // OCR记录使用紧凑布局
  50. $this->view = 'filament.pages.exam-analysis-compact';
  51. } elseif ($this->paperId) {
  52. // 系统生成卷子使用标准布局
  53. $this->view = 'filament.pages.exam-analysis-standard';
  54. }
  55. $this->loadAnalysisData();
  56. }
  57. protected function loadAnalysisData()
  58. {
  59. try {
  60. // 处理OCR记录
  61. if ($this->recordId) {
  62. $this->recordType = 'ocr';
  63. $record = OCRRecord::with('student')->find($this->recordId);
  64. if (!$record) {
  65. Notification::make()
  66. ->title('错误')
  67. ->body('未找到指定的上传记录')
  68. ->danger()
  69. ->send();
  70. $this->redirectRoute('filament.admin.pages.upload-exam-paper');
  71. return;
  72. }
  73. $this->recordData = $record->toArray();
  74. $this->studentInfo = $record->student ? $record->student->toArray() : [];
  75. // OCR记录:添加题目统计信息
  76. $ocrQuestionsCount = OCRQuestionResult::where('ocr_record_id', $this->recordId)->count();
  77. $this->recordData['total_questions'] = $ocrQuestionsCount;
  78. $this->recordData['questions'] = $this->getQuestions(); // 提前加载题目数据
  79. // OCR记录如果已完成处理,加载分析数据
  80. if ($record->status === 'completed' && $record->student_id) {
  81. $this->loadLearningAnalysis($record->student_id, $this->recordId);
  82. } else {
  83. $this->analysisData = [];
  84. }
  85. }
  86. // 处理系统生成卷子
  87. elseif ($this->paperId) {
  88. $this->recordType = 'generated';
  89. $paper = \App\Models\Paper::with('student')->find($this->paperId);
  90. if (!$paper) {
  91. Notification::make()
  92. ->title('错误')
  93. ->body('未找到指定的试卷')
  94. ->danger()
  95. ->send();
  96. $this->redirectRoute('filament.admin.pages.upload-exam-paper');
  97. return;
  98. }
  99. // 构造基础数据,视图会安全处理缺失字段
  100. $this->recordData = [
  101. 'id' => $paper->paper_id,
  102. 'paper_id' => $paper->paper_id,
  103. 'student_id' => $paper->student_id,
  104. 'paper_type' => 'system_generated',
  105. 'paper_name' => $paper->paper_name,
  106. 'status' => $paper->status,
  107. 'total_questions' => $paper->question_count,
  108. 'created_at' => $paper->created_at,
  109. 'analysis_id' => $paper->analysis_id, // AI分析记录ID
  110. ];
  111. $this->studentInfo = $paper->student ? $paper->student->toArray() : [];
  112. // 获取试卷题目列表(包含题库API的详细数据)
  113. $this->recordData['questions'] = $this->getQuestions();
  114. // 系统生成卷子也尝试加载学习分析数据
  115. if ($paper->student_id) {
  116. $this->loadLearningAnalysis($paper->student_id, $this->paperId);
  117. } else {
  118. $this->analysisData = [];
  119. }
  120. }
  121. $this->loading = false;
  122. } catch (\Exception $e) {
  123. \Log::error('加载试卷分析数据失败', [
  124. 'record_id' => $this->recordId,
  125. 'paper_id' => $this->paperId,
  126. 'error' => $e->getMessage(),
  127. 'trace' => $e->getTraceAsString()
  128. ]);
  129. Notification::make()
  130. ->title('错误')
  131. ->body('加载分析数据失败:' . $e->getMessage())
  132. ->danger()
  133. ->send();
  134. $this->loading = false;
  135. }
  136. }
  137. /**
  138. * 加载学习分析数据
  139. */
  140. protected function loadLearningAnalysis($studentId, $identifier)
  141. {
  142. try {
  143. // 1. 尝试从数据库加载本次试卷的分析结果
  144. $paperId = null;
  145. if ($this->recordType === 'ocr') {
  146. $paperId = $this->recordData['exam_id'] ?? null;
  147. } elseif ($this->recordType === 'generated') {
  148. $paperId = $this->recordData['paper_id'] ?? null;
  149. }
  150. if ($paperId) {
  151. // 直接从API调用获取分析结果,不查询本地数据库
  152. \Log::info('跳过本地数据库查询,直接从API加载分析结果', [
  153. 'paper_id' => $paperId,
  154. 'student_id' => $studentId
  155. ]);
  156. $this->loadLearningAnalysisFromAPI($studentId, $paperId);
  157. return;
  158. }
  159. // 2. 直接从API获取整体掌握度数据,不查询本地数据库
  160. \Log::info('跳过知识点记录表查询,直接从API获取掌握度数据', [
  161. 'student_id' => $studentId
  162. ]);
  163. $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null);
  164. } catch (\Exception $e) {
  165. \Log::warning('加载分析数据失败,回退到API调用', [
  166. 'identifier' => $identifier,
  167. 'student_id' => $studentId,
  168. 'type' => $this->recordType,
  169. 'error' => $e->getMessage()
  170. ]);
  171. // 数据库查询失败时回退到API调用
  172. $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null);
  173. }
  174. }
  175. /**
  176. * 从API加载学习分析数据(回退方案)
  177. */
  178. protected function loadLearningAnalysisFromAPI($studentId, $paperId = null)
  179. {
  180. try {
  181. $learningService = app(\App\Services\LearningAnalyticsService::class);
  182. // 1. 加载本次试卷的分析结果
  183. $analysisId = null;
  184. if ($this->recordType === 'ocr' && isset($this->recordData['analysis_id'])) {
  185. $analysisId = $this->recordData['analysis_id'];
  186. } elseif ($this->recordType === 'generated' && isset($this->recordData['analysis_id'])) {
  187. $analysisId = $this->recordData['analysis_id'];
  188. }
  189. if ($analysisId) {
  190. $paperAnalysisResponse = $learningService->getAnalysisResult($analysisId);
  191. if (!empty($paperAnalysisResponse) && isset($paperAnalysisResponse['data'])) {
  192. $this->paperAnalysisData = $paperAnalysisResponse['data'];
  193. // 将API的分析结果同步到题目数据
  194. if (isset($this->paperAnalysisData['question_results'])) {
  195. $this->syncApiAnalysisToQuestions($this->paperAnalysisData['question_results']);
  196. }
  197. \Log::info('本次试卷分析结果已从API加载', [
  198. 'analysis_id' => $analysisId,
  199. 'student_id' => $studentId,
  200. 'data_keys' => array_keys($this->paperAnalysisData),
  201. 'question_results_count' => count($this->paperAnalysisData['question_results'] ?? [])
  202. ]);
  203. }
  204. }
  205. // 2. 调用学习分析API获取整体掌握度数据
  206. $masteryResponse = $learningService->getStudentMastery($studentId);
  207. // 转换为页面期望的格式
  208. if (!empty($masteryResponse) && isset($masteryResponse['data'])) {
  209. $masteryList = $masteryResponse['data'];
  210. // 计算整体掌握度
  211. $totalMastery = 0;
  212. $count = count($masteryList);
  213. $weakAreas = [];
  214. $knowledgePoints = [];
  215. foreach ($masteryList as $mastery) {
  216. $masteryLevel = $mastery['mastery_level'] ?? 0;
  217. $totalMastery += $masteryLevel;
  218. // 识别薄弱知识点(掌握度 < 0.6)
  219. if ($masteryLevel < 0.6) {
  220. $weakAreas[] = [
  221. 'kp_code' => $mastery['kp_code'],
  222. 'mastery' => $masteryLevel
  223. ];
  224. }
  225. // 构造知识点数据
  226. $totalAttempts = $mastery['total_attempts'] ?? 0;
  227. $correctAttempts = $mastery['correct_attempts'] ?? 0;
  228. $accuracyRate = $totalAttempts > 0 ? $correctAttempts / $totalAttempts : 0;
  229. $knowledgePoints[] = [
  230. 'kp_code' => $mastery['kp_code'],
  231. 'name' => $mastery['kp_code'], // TODO: 从知识图谱服务获取名称
  232. 'mastery' => $masteryLevel,
  233. 'mastery_level' => $masteryLevel, // 添加模板需要的字段
  234. 'total_attempts' => $totalAttempts,
  235. 'correct_attempts' => $correctAttempts,
  236. 'accuracy_rate' => $accuracyRate
  237. ];
  238. }
  239. $overallMastery = $count > 0 ? $totalMastery / $count : 0;
  240. // 生成学习建议
  241. $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints);
  242. // 只显示与当前试卷相关的知识点
  243. $currentPaperKps = $this->getCurrentPaperKnowledgePoints();
  244. $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code');
  245. $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) {
  246. return in_array($kp['kp_code'], $currentPaperKpCodes);
  247. });
  248. $this->analysisData = [
  249. 'overall_mastery' => $overallMastery,
  250. 'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) {
  251. return in_array($weak['kp_code'], $currentPaperKpCodes);
  252. }),
  253. 'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点
  254. 'recommendations' => $recommendations,
  255. 'total_knowledge_points' => count($filteredKnowledgePoints),
  256. 'mastery_distribution' => $this->calculateMasteryDistribution($masteryList)
  257. ];
  258. \Log::info('学习分析数据已从API加载', [
  259. 'student_id' => $studentId,
  260. 'overall_mastery' => $overallMastery,
  261. 'knowledge_points_count' => count($knowledgePoints),
  262. 'filtered_knowledge_points_count' => count($filteredKnowledgePoints),
  263. 'current_paper_kp_codes' => $currentPaperKpCodes,
  264. 'weak_areas_count' => count($weakAreas)
  265. ]);
  266. } else {
  267. \Log::info('API返回数据为空', [
  268. 'student_id' => $studentId,
  269. 'paper_id' => $paperId,
  270. 'type' => $this->recordType
  271. ]);
  272. $this->analysisData = [];
  273. }
  274. } catch (\Exception $apiError) {
  275. \Log::warning('API调用失败', [
  276. 'student_id' => $studentId,
  277. 'paper_id' => $paperId,
  278. 'type' => $this->recordType,
  279. 'error' => $apiError->getMessage()
  280. ]);
  281. // API调用失败时设置空数组,避免页面报错
  282. $this->analysisData = [];
  283. $this->paperAnalysisData = [];
  284. }
  285. }
  286. /**
  287. * 处理知识点记录数据
  288. */
  289. protected function processKnowledgePointRecords($knowledgePointRecords)
  290. {
  291. // 根据实际的knowledge_point_records表结构处理数据
  292. $masteryList = [];
  293. $weakAreas = [];
  294. $knowledgePoints = [];
  295. $totalMastery = 0;
  296. $count = 0;
  297. foreach ($knowledgePointRecords as $record) {
  298. // knowledge_point_records表有不同的字段结构
  299. $kpCode = $record->knowledge_point ?? '';
  300. $masteryLevel = $record->mastery_after ?? $record->mastery_before ?? 0;
  301. if (!empty($kpCode)) {
  302. $masteryList[] = [
  303. 'kp_code' => $kpCode,
  304. 'mastery_level' => $masteryLevel
  305. ];
  306. $totalMastery += $masteryLevel;
  307. $count++;
  308. // 识别薄弱知识点(掌握度 < 0.6)
  309. if ($masteryLevel < 0.6) {
  310. $weakAreas[] = [
  311. 'kp_code' => $kpCode,
  312. 'mastery' => $masteryLevel
  313. ];
  314. }
  315. // 构造知识点数据
  316. $knowledgePoints[] = [
  317. 'kp_code' => $kpCode,
  318. 'name' => $kpCode,
  319. 'mastery' => $masteryLevel,
  320. 'mastery_level' => $masteryLevel, // 添加模板需要的字段
  321. 'total_attempts' => 1, // 默认值
  322. 'correct_attempts' => $masteryLevel > 0.5 ? 1 : 0, // 估算值
  323. 'accuracy_rate' => $masteryLevel
  324. ];
  325. }
  326. }
  327. $overallMastery = $count > 0 ? $totalMastery / $count : 0;
  328. // 生成学习建议
  329. $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints);
  330. // 只显示与当前试卷相关的知识点
  331. $currentPaperKps = $this->getCurrentPaperKnowledgePoints();
  332. $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code');
  333. $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) {
  334. return in_array($kp['kp_code'], $currentPaperKpCodes);
  335. });
  336. $this->analysisData = [
  337. 'overall_mastery' => $overallMastery,
  338. 'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) {
  339. return in_array($weak['kp_code'], $currentPaperKpCodes);
  340. }),
  341. 'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点
  342. 'recommendations' => $recommendations,
  343. 'total_knowledge_points' => count($filteredKnowledgePoints),
  344. 'mastery_distribution' => $this->calculateMasteryDistribution($masteryList)
  345. ];
  346. \Log::info('学习分析数据已从knowledge_point_records加载', [
  347. 'student_id' => $knowledgePointRecords->first()->student_id ?? 'unknown',
  348. 'overall_mastery' => $overallMastery,
  349. 'knowledge_points_count' => count($knowledgePoints),
  350. 'filtered_knowledge_points_count' => count($filteredKnowledgePoints),
  351. 'current_paper_kp_codes' => $currentPaperKpCodes,
  352. 'weak_areas_count' => count($weakAreas)
  353. ]);
  354. }
  355. /**
  356. * 生成学习建议
  357. */
  358. protected function generateRecommendations($overallMastery, $weakAreas, $knowledgePoints)
  359. {
  360. $recommendations = [];
  361. if ($overallMastery >= 0.8) {
  362. $recommendations[] = '整体掌握情况良好,继续保持!可以尝试更有挑战性的题目。';
  363. } elseif ($overallMastery >= 0.6) {
  364. $recommendations[] = '基础掌握较好,建议加强薄弱知识点的练习。';
  365. } else {
  366. $recommendations[] = '需要系统复习基础知识,建议从简单题目开始逐步提升。';
  367. }
  368. // 针对薄弱知识点的建议
  369. if (count($weakAreas) > 0) {
  370. $weakKpCodes = array_slice(array_column($weakAreas, 'kp_code'), 0, 3);
  371. $recommendations[] = '重点加强以下知识点: ' . implode('、', $weakKpCodes);
  372. }
  373. // 根据答题次数给建议
  374. $lowAttempts = array_filter($knowledgePoints, function($kp) {
  375. return ($kp['total_attempts'] ?? 0) < 5;
  376. });
  377. if (count($lowAttempts) > 0) {
  378. $recommendations[] = '部分知识点练习次数较少,建议增加练习量以巩固掌握。';
  379. }
  380. return $recommendations;
  381. }
  382. /**
  383. * 计算掌握度分布
  384. */
  385. protected function calculateMasteryDistribution($masteryList)
  386. {
  387. $distribution = [
  388. 'high' => 0, // >= 0.7
  389. 'medium' => 0, // 0.4 - 0.7
  390. 'low' => 0 // < 0.4
  391. ];
  392. foreach ($masteryList as $mastery) {
  393. $level = $mastery['mastery_level'] ?? 0;
  394. if ($level >= 0.7) {
  395. $distribution['high']++;
  396. } elseif ($level >= 0.4) {
  397. $distribution['medium']++;
  398. } else {
  399. $distribution['low']++;
  400. }
  401. }
  402. return $distribution;
  403. }
  404. public function getPaperTypeLabel(): string
  405. {
  406. if ($this->recordType === 'ocr' && isset($this->recordData['paper_type'])) {
  407. return match($this->recordData['paper_type']) {
  408. 'unit_test' => '单元测试',
  409. 'midterm' => '期中考试',
  410. 'final' => '期末考试',
  411. 'homework' => '家庭作业',
  412. 'quiz' => '随堂测验',
  413. 'other' => '其他',
  414. default => '未分类',
  415. };
  416. }
  417. return $this->recordData['paper_type'] ?? '未知';
  418. }
  419. public function getStatusBadge(): string
  420. {
  421. $status = $this->recordData['status'] ?? 'unknown';
  422. return match($status) {
  423. 'pending' => '<span class="badge badge-ghost">待处理</span>',
  424. 'processing' => '<span class="badge badge-info gap-2"><span class="loading loading-spinner loading-xs"></span>处理中</span>',
  425. 'completed' => '<span class="badge badge-success">已完成</span>',
  426. 'failed' => '<span class="badge badge-error">失败</span>',
  427. default => '<span class="badge badge-ghost">未知</span>',
  428. };
  429. }
  430. /**
  431. * 判断是否为 OCR 场景
  432. */
  433. public function isOcrRecord(): bool
  434. {
  435. return $this->recordType === 'ocr';
  436. }
  437. /**
  438. * 获取OCR记录的题目数据
  439. * 从OCRQuestionResult表加载并格式化为组件期望的格式
  440. */
  441. protected function getOcrQuestions(): array
  442. {
  443. $recordId = $this->recordId ?? null;
  444. if (!$recordId) {
  445. \Log::warning('OCR记录缺少recordId', ['recordId' => $recordId]);
  446. return [];
  447. }
  448. try {
  449. // 从OCRQuestionResult表加载题目数据
  450. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $recordId)
  451. ->orderBy('question_number')
  452. ->get();
  453. \Log::info('加载OCR题目数据', [
  454. 'record_id' => $recordId,
  455. 'questions_count' => $ocrQuestions->count()
  456. ]);
  457. if ($ocrQuestions->isEmpty()) {
  458. \Log::warning('OCR记录没有题目数据', ['record_id' => $recordId]);
  459. return [];
  460. }
  461. // 创建API分析结果的映射(如果有)
  462. $analysisMap = [];
  463. if (!empty($this->paperAnalysisData['question_results'])) {
  464. foreach ($this->paperAnalysisData['question_results'] as $result) {
  465. if (isset($result['question_id'])) {
  466. $analysisMap[$result['question_id']] = $result;
  467. }
  468. }
  469. }
  470. $questions = [];
  471. foreach ($ocrQuestions as $oq) {
  472. // 获取API分析结果(如果有)
  473. $aiAnalysis = null;
  474. if (isset($analysisMap[$oq->question_number])) {
  475. $analysis = $analysisMap[$oq->question_number];
  476. $aiAnalysis = [
  477. 'analysis' => $analysis['reason'] ?? '',
  478. 'mistake_type' => $analysis['mistake_type'] ?? '',
  479. 'mistake_category' => $analysis['mistake_category'] ?? '',
  480. 'suggestions' => [$analysis['suggestions'] ?? ''],
  481. 'correct_solution' => $analysis['correct_solution'] ?? '',
  482. ];
  483. } elseif (!empty($oq->ai_feedback)) {
  484. // 如果没有API分析,使用OCR记录的AI反馈
  485. $aiAnalysis = [
  486. 'analysis' => $oq->ai_feedback,
  487. 'mistake_type' => '',
  488. 'mistake_category' => '',
  489. 'suggestions' => [$oq->ai_feedback],
  490. 'correct_solution' => '',
  491. ];
  492. }
  493. // 学生答案:优先使用老师校准的答案(manual_answer),如果没有则使用OCR识别的答案
  494. $ocrAnswer = trim($oq->student_answer ?? '');
  495. $manualAnswer = trim($oq->manual_answer ?? '');
  496. $studentAnswer = !empty($manualAnswer) ? $manualAnswer : $ocrAnswer;
  497. // 判断是否有答案(OCR识别或老师校准)
  498. $hasAnswer = !empty($studentAnswer);
  499. $displayAnswer = $hasAnswer ? ($studentAnswer ?: '空') : '未作答';
  500. // 从AI分析结果中获取正确答案和判断
  501. $correctAnswer = null;
  502. $isCorrect = false;
  503. $solution = null;
  504. if (isset($analysisMap[$oq->question_number])) {
  505. $analysis = $analysisMap[$oq->question_number];
  506. $correctAnswer = $analysis['correct_answer'] ?? $analysis['correct_solution'] ?? null;
  507. $isCorrect = $analysis['is_correct'] ?? false;
  508. $solution = $analysis['correct_solution'] ?? null;
  509. }
  510. // 显示答案对比(如果有AI分析的正确答案)
  511. $answerComparison = null;
  512. if (!empty($correctAnswer) && $studentAnswer !== $correctAnswer && $hasAnswer) {
  513. $answerComparison = [
  514. 'student' => $studentAnswer,
  515. 'correct' => $correctAnswer
  516. ];
  517. }
  518. $questions[] = [
  519. 'id' => $oq->id,
  520. 'question_number' => $oq->question_number,
  521. 'question_bank_id' => 'ocr_' . $oq->question_number, // OCR题目没有题库ID,用前缀标识
  522. 'question_type' => 'unknown',
  523. 'question_text' => $oq->question_text ?? '题目内容缺失',
  524. 'content' => $oq->question_text ?? '题目内容缺失',
  525. 'stem' => $oq->question_text ?? '题目内容缺失',
  526. 'answer' => $correctAnswer ?? '', // 正确答案(从AI分析获取)
  527. 'reference_answer' => $correctAnswer ?? '',
  528. 'solution' => $solution, // 解题步骤
  529. 'score_total' => $oq->score_total ?? null, // OCR题目的分数可能为空
  530. 'score_obtained' => $oq->score_obtained ?? null, // OCR题目的分数可能为空
  531. 'student_answer' => $displayAnswer, // 学生答案:未作答/空/实际答案(校准后)
  532. 'is_correct' => $isCorrect,
  533. 'kp_code' => $oq->kp_code ?? null, // OCR题目的知识点可能为空
  534. 'ai_analysis' => $aiAnalysis,
  535. 'answer_comparison' => $answerComparison, // 答案对比信息
  536. ];
  537. }
  538. \Log::info('OCR题目数据格式化完成', [
  539. 'record_id' => $recordId,
  540. 'formatted_questions_count' => count($questions),
  541. 'has_ai_analysis_count' => count(array_filter($questions, fn($q) => !empty($q['ai_analysis'])))
  542. ]);
  543. return $questions;
  544. } catch (\Exception $e) {
  545. \Log::error('获取OCR题目数据失败', [
  546. 'record_id' => $recordId,
  547. 'error' => $e->getMessage(),
  548. 'trace' => $e->getTraceAsString()
  549. ]);
  550. return [];
  551. }
  552. }
  553. /**
  554. * 获取题目列表(根据场景返回不同数据,包含AI分析解析)
  555. */
  556. public function getQuestions(): array
  557. {
  558. // OCR记录:从OCRQuestionResult表加载题目数据
  559. if ($this->recordType === 'ocr') {
  560. $questions = $this->getOcrQuestions();
  561. // 丰富知识点信息
  562. return $this->enrichQuestionsWithKnowledgePoints($questions);
  563. }
  564. // 系统生成卷子:从PaperQuestion表加载题目数据
  565. $paperId = $this->recordData['paper_id'] ?? null;
  566. if (!$paperId) {
  567. return [];
  568. }
  569. try {
  570. // 直接查询题目数据
  571. $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paperId)
  572. ->orderBy('question_number')
  573. ->get();
  574. if ($paperQuestions->isEmpty()) {
  575. return [];
  576. }
  577. // 获取题库题目详情
  578. $questionBankService = app(\App\Services\QuestionBankService::class);
  579. $questionBankIds = $paperQuestions->pluck('question_bank_id')->unique()->filter()->toArray();
  580. $questionDetails = collect([]);
  581. \Log::info('准备调用题库服务', [
  582. 'question_bank_ids' => $questionBankIds,
  583. 'ids_count' => count($questionBankIds)
  584. ]);
  585. if (!empty($questionBankIds)) {
  586. try {
  587. \Log::info('开始调用题库服务getQuestionsByIds');
  588. $details = $questionBankService->getQuestionsByIds($questionBankIds);
  589. \Log::info('题库服务调用结果', [
  590. 'response' => $details,
  591. 'has_data' => isset($details['data']),
  592. 'data_type' => gettype($details['data'] ?? null)
  593. ]);
  594. if (isset($details['data']) && is_array($details['data'])) {
  595. $questionDetails = collect($details['data'])->keyBy('id');
  596. \Log::info('题库数据处理成功', [
  597. 'details_count' => $questionDetails->count(),
  598. 'first_key' => $questionDetails->keys()->first()
  599. ]);
  600. } else {
  601. \Log::warning('题库服务返回数据格式不正确', ['details' => $details]);
  602. }
  603. } catch (\Exception $e) {
  604. \Log::error('获取题库数据失败', [
  605. 'error' => $e->getMessage(),
  606. 'trace' => $e->getTraceAsString()
  607. ]);
  608. }
  609. } else {
  610. \Log::info('没有有效的题库ID');
  611. }
  612. // 构建题目数据 - 合并paper_questions表和题库API数据
  613. $questions = [];
  614. // 创建API分析结果的映射(如果有)
  615. $analysisMap = [];
  616. if (!empty($this->paperAnalysisData['question_results'])) {
  617. foreach ($this->paperAnalysisData['question_results'] as $result) {
  618. if (isset($result['question_id'])) {
  619. $analysisMap[$result['question_id']] = $result;
  620. }
  621. }
  622. }
  623. foreach ($paperQuestions as $pq) {
  624. // 从题库API获取详细数据
  625. $detail = $questionDetails->get($pq->question_bank_id);
  626. \Log::info('构建题目数据', [
  627. 'question_bank_id' => $pq->question_bank_id,
  628. 'has_detail' => $detail ? 'Yes' : 'No',
  629. 'detail_id' => $detail['id'] ?? 'null',
  630. 'stem_preview' => $detail ? substr($detail['stem'] ?? '', 50) : 'null',
  631. 'kp_code_from_db' => $pq->knowledge_point,
  632. 'kp_code_from_api' => $detail['kp_code'] ?? 'null',
  633. 'has_analysis' => isset($analysisMap[$pq->question_bank_id]) ? 'Yes' : 'No'
  634. ]);
  635. // 优先使用题库API返回的stem,如果没有则使用paper_questions中的question_text
  636. $questionText = $detail['stem'] ?? $detail['content'] ?? $pq->question_text ?? '题目内容缺失';
  637. // 优先使用题库API返回的kp_code,如果没有则使用paper_questions中的knowledge_point
  638. $kpCode = $detail['kp_code'] ?? $pq->knowledge_point ?? 'N/A';
  639. // 获取API分析结果(如果有)
  640. $aiAnalysis = null;
  641. if (isset($analysisMap[$pq->question_bank_id])) {
  642. $analysis = $analysisMap[$pq->question_bank_id];
  643. $aiAnalysis = [
  644. 'analysis' => $analysis['reason'] ?? '',
  645. 'mistake_type' => $analysis['mistake_type'] ?? '',
  646. 'mistake_category' => $analysis['mistake_category'] ?? '',
  647. 'suggestions' => [$analysis['suggestions'] ?? ''],
  648. 'correct_solution' => $analysis['correct_solution'] ?? '',
  649. ];
  650. }
  651. // 获取解题步骤solution(从Question Bank或AI分析)
  652. $solution = $detail['solution'] ?? $detail['correct_solution'] ?? null;
  653. if (!$solution && isset($analysisMap[$pq->question_bank_id])) {
  654. $analysis = $analysisMap[$pq->question_bank_id];
  655. $solution = $analysis['correct_solution'] ?? null;
  656. }
  657. $questions[] = [
  658. 'id' => $pq->id,
  659. 'question_number' => $pq->question_number,
  660. 'question_bank_id' => $pq->question_bank_id,
  661. 'question_type' => $pq->question_type,
  662. 'question_text' => $questionText,
  663. 'content' => $questionText,
  664. 'stem' => $questionText,
  665. 'answer' => $detail['answer'] ?? '',
  666. 'reference_answer' => $detail['answer'] ?? '',
  667. 'solution' => $solution, // 解题步骤
  668. 'score_total' => $pq->score ?? 5,
  669. 'score_obtained' => $pq->score_obtained ?? 0,
  670. 'student_answer' => '老师已评分', // 隐藏学生答案,显示老师评分状态
  671. 'is_correct' => $pq->is_correct ?? false,
  672. 'kp_code' => $kpCode,
  673. 'ai_analysis' => $aiAnalysis,
  674. ];
  675. }
  676. // 丰富知识点信息
  677. return $this->enrichQuestionsWithKnowledgePoints($questions);
  678. } catch (\Exception $e) {
  679. \Log::error('获取题目列表失败', [
  680. 'paper_id' => $paperId,
  681. 'error' => $e->getMessage()
  682. ]);
  683. return [];
  684. }
  685. }
  686. /**
  687. * 丰富题目数据,添加知识点详细信息
  688. */
  689. protected function enrichQuestionsWithKnowledgePoints(array $questions): array
  690. {
  691. // 收集所有知识点代码
  692. $kpCodes = [];
  693. foreach ($questions as $question) {
  694. if (!empty($question['kp_code']) && $question['kp_code'] !== 'N/A') {
  695. $kpCodes[] = $question['kp_code'];
  696. }
  697. }
  698. if (empty($kpCodes)) {
  699. return $questions;
  700. }
  701. // 获取知识点详细信息
  702. $knowledgeService = app(\App\Services\KnowledgeGraphService::class);
  703. $knowledgePointsList = $knowledgeService->listKnowledgePoints(1, 1000);
  704. $knowledgePoints = [];
  705. if (isset($knowledgePointsList['data']) && !empty($knowledgePointsList['data'])) {
  706. foreach ($knowledgePointsList['data'] as $kp) {
  707. $knowledgePoints[$kp['kp_code']] = $kp;
  708. }
  709. }
  710. // 获取技能点信息
  711. $skillsList = [];
  712. foreach ($kpCodes as $kpCode) {
  713. $skills = $knowledgeService->getSkillsByKnowledgePoint($kpCode);
  714. if (!empty($skills)) {
  715. $skillsList[$kpCode] = $skills;
  716. }
  717. }
  718. // 丰富题目数据
  719. foreach ($questions as &$question) {
  720. $kpCode = $question['kp_code'] ?? null;
  721. if ($kpCode && isset($knowledgePoints[$kpCode])) {
  722. $kp = $knowledgePoints[$kpCode];
  723. $question['knowledge_point'] = [
  724. 'code' => $kpCode,
  725. 'name' => $kp['cn_name'] ?? $kpCode,
  726. 'category' => $kp['category'] ?? '',
  727. 'phase' => $kp['phase'] ?? '',
  728. 'grade' => $kp['grade'] ?? '',
  729. 'description' => $kp['description'] ?? '',
  730. ];
  731. // 添加技能点
  732. if (isset($skillsList[$kpCode])) {
  733. $question['knowledge_point']['skills'] = $skillsList[$kpCode];
  734. }
  735. }
  736. }
  737. return $questions;
  738. }
  739. /**
  740. * 重新处理OCR
  741. */
  742. public function reprocessOCR()
  743. {
  744. if (!$this->recordId) {
  745. return;
  746. }
  747. try {
  748. $record = OCRRecord::find($this->recordId);
  749. if (!$record) {
  750. throw new \Exception('记录不存在');
  751. }
  752. $ocrService = app(OCRService::class);
  753. $ocrService->reprocess($record);
  754. Notification::make()
  755. ->title('已提交重新处理')
  756. ->body('OCR识别任务已重新加入队列')
  757. ->success()
  758. ->send();
  759. $this->loadAnalysisData(); // 刷新数据
  760. } catch (\Exception $e) {
  761. Notification::make()
  762. ->title('操作失败')
  763. ->body($e->getMessage())
  764. ->danger()
  765. ->send();
  766. }
  767. }
  768. /**
  769. * 重新提交分析
  770. */
  771. public function reanalyze()
  772. {
  773. if (!$this->recordId) {
  774. return;
  775. }
  776. try {
  777. $record = OCRRecord::find($this->recordId);
  778. if (!$record) {
  779. throw new \Exception('记录不存在');
  780. }
  781. // 获取当前的题目数据(包含可能的人工校准)
  782. $questions = OCRQuestionResult::where('ocr_record_id', $this->recordId)
  783. ->orderBy('question_number')
  784. ->get()
  785. ->map(function ($q) {
  786. return [
  787. 'question_number' => $q->question_number,
  788. 'content' => $q->question_text,
  789. 'student_answer' => $q->student_answer,
  790. 'manual_answer' => $q->manual_answer,
  791. 'answer_verified' => $q->answer_verified,
  792. 'confidence' => $q->score_confidence,
  793. 'kp_code' => $q->kp_code,
  794. 'score_value' => $q->score_value,
  795. ];
  796. })->toArray();
  797. if (empty($questions)) {
  798. throw new \Exception('没有可分析的题目数据');
  799. }
  800. // 构造分析请求数据
  801. $analysisData = [
  802. 'exam_id' => $record->exam_id,
  803. 'student_id' => $record->student_id,
  804. 'ocr_record_id' => $record->id,
  805. 'teacher_name' => auth()->user()->name ?? 'Teacher',
  806. 'analysis_type' => 'mastery',
  807. 'questions' => array_map(function($q) {
  808. // 优先使用人工校准的答案
  809. $studentAnswer = $q['student_answer'] ?? '';
  810. if (isset($q['manual_answer']) && !empty($q['manual_answer'])) {
  811. $studentAnswer = $q['manual_answer'];
  812. }
  813. return [
  814. 'question_id' => $q['question_number'],
  815. 'question_number' => (string)$q['question_number'],
  816. 'kp_code' => $q['kp_code'] ?? null,
  817. 'score_value' => $q['score_value'] ?? 0,
  818. 'student_answer' => $studentAnswer,
  819. 'ocr_confidence' => $q['confidence'] ?? 0,
  820. 'question_text' => $q['content'] ?? '',
  821. 'teacher_validated' => $q['answer_verified'] ?? false,
  822. ];
  823. }, $questions)
  824. ];
  825. // 调用分析服务
  826. $learningService = app(LearningAnalyticsService::class);
  827. $result = $learningService->submitOCRAnalysis($analysisData);
  828. if (isset($result['success']) && $result['success']) {
  829. $record->update([
  830. 'ai_analyzed_at' => now(),
  831. 'ai_analysis_count' => ($record->ai_analysis_count ?? 0) + 1
  832. ]);
  833. Notification::make()
  834. ->title('分析请求已提交')
  835. ->body('系统正在重新分析试卷,请稍后刷新查看结果')
  836. ->success()
  837. ->send();
  838. $this->loadAnalysisData(); // 刷新数据
  839. } else {
  840. throw new \Exception($result['message'] ?? '提交分析失败');
  841. }
  842. } catch (\Exception $e) {
  843. Notification::make()
  844. ->title('操作失败')
  845. ->body($e->getMessage())
  846. ->danger()
  847. ->send();
  848. }
  849. }
  850. /**
  851. * 从当前试卷数据中提取知识点信息
  852. */
  853. protected function extractKnowledgePointsFromCurrentPaper(): array
  854. {
  855. $knowledgePoints = [];
  856. $questions = $this->getQuestions();
  857. foreach ($questions as $question) {
  858. $kpCode = $question['kp_code'] ?? null;
  859. if ($kpCode && $kpCode !== 'N/A') {
  860. $isCorrect = $question['is_correct'] ?? false;
  861. if (!isset($knowledgePoints[$kpCode])) {
  862. $knowledgePoints[$kpCode] = [
  863. 'kp_code' => $kpCode,
  864. 'name' => $kpCode,
  865. 'total_attempts' => 0,
  866. 'correct_attempts' => 0,
  867. 'mastery_level' => 0,
  868. 'accuracy_rate' => 0
  869. ];
  870. }
  871. $knowledgePoints[$kpCode]['total_attempts']++;
  872. if ($isCorrect) {
  873. $knowledgePoints[$kpCode]['correct_attempts']++;
  874. }
  875. }
  876. }
  877. // 计算准确率和掌握度
  878. foreach ($knowledgePoints as &$kp) {
  879. if ($kp['total_attempts'] > 0) {
  880. $kp['accuracy_rate'] = $kp['correct_attempts'] / $kp['total_attempts'];
  881. $kp['mastery_level'] = $kp['accuracy_rate'];
  882. }
  883. }
  884. return array_values($knowledgePoints);
  885. }
  886. /**
  887. * 获取当前试卷的知识点掌握情况
  888. */
  889. protected function getCurrentPaperKnowledgePoints(): array
  890. {
  891. // 首先尝试从当前试卷数据中提取
  892. $currentPaperKps = $this->extractKnowledgePointsFromCurrentPaper();
  893. // 如果有历史分析数据,合并以提供更准确的掌握度
  894. if (!empty($this->analysisData['knowledge_points'])) {
  895. $historicalKps = $this->analysisData['knowledge_points'];
  896. foreach ($currentPaperKps as &$currentKp) {
  897. $kpCode = $currentKp['kp_code'];
  898. // 查找历史数据
  899. $historicalData = collect($historicalKps)->firstWhere('kp_code', $kpCode);
  900. if ($historicalData && isset($historicalData['mastery_level'])) {
  901. // 使用历史数据的掌握度,但保留试卷的实际表现
  902. $currentKp['historical_mastery_level'] = $historicalData['mastery_level'];
  903. $currentKp['total_attempts'] = $historicalData['total_attempts'] ?? $currentKp['total_attempts'];
  904. $currentKp['correct_attempts'] = $historicalData['correct_attempts'] ?? $currentKp['correct_attempts'];
  905. $currentKp['accuracy_rate'] = $historicalData['accuracy_rate'] ?? $currentKp['accuracy_rate'];
  906. // 优先使用历史的掌握度,但考虑试卷表现做微调
  907. $paperPerformance = $currentKp['accuracy_rate'];
  908. $historicalPerformance = $historicalData['mastery_level'] ?? 0;
  909. // 综合计算:70%历史 + 30%本试卷
  910. $currentKp['mastery_level'] = ($historicalPerformance * 0.7 + $paperPerformance * 0.3);
  911. }
  912. }
  913. }
  914. return $currentPaperKps;
  915. }
  916. /**
  917. * 将API的分析结果同步到题目数据和数据库
  918. */
  919. protected function syncApiAnalysisToQuestions(array $questionResults)
  920. {
  921. try {
  922. \Log::info('开始同步API分析结果到题目数据', [
  923. 'paper_id' => $this->paperId,
  924. 'question_results_count' => count($questionResults)
  925. ]);
  926. // 创建question_id到分析结果的映射
  927. $analysisMap = [];
  928. foreach ($questionResults as $result) {
  929. $questionId = $result['question_id'] ?? null;
  930. if ($questionId) {
  931. $analysisMap[$questionId] = $result;
  932. }
  933. }
  934. // 获取当前试卷的题目
  935. $paper = \App\Models\Paper::find($this->paperId);
  936. if ($paper) {
  937. $paperQuestions = $paper->questions()->get();
  938. $updatedCount = 0;
  939. foreach ($paperQuestions as $pq) {
  940. // 查找对应的API分析结果
  941. if (isset($analysisMap[$pq->question_bank_id])) {
  942. $analysis = $analysisMap[$pq->question_bank_id];
  943. // 更新数据库字段
  944. $pq->is_correct = $analysis['correct'] ?? false;
  945. $pq->score_obtained = $analysis['score'] ?? 0;
  946. $pq->save();
  947. $updatedCount++;
  948. \Log::info('更新题目分析结果', [
  949. 'question_bank_id' => $pq->question_bank_id,
  950. 'is_correct' => $pq->is_correct,
  951. 'score_obtained' => $pq->score_obtained
  952. ]);
  953. }
  954. }
  955. \Log::info('API分析结果同步完成', [
  956. 'updated_count' => $updatedCount,
  957. 'total_questions' => $paperQuestions->count()
  958. ]);
  959. // 刷新recordData中的questions数据
  960. $this->recordData['questions'] = $this->getQuestions();
  961. }
  962. } catch (\Exception $e) {
  963. \Log::error('同步API分析结果失败', [
  964. 'paper_id' => $this->paperId,
  965. 'error' => $e->getMessage()
  966. ]);
  967. }
  968. }
  969. /**
  970. * 提交OCR记录到AI分析API(与系统卷子使用统一接口)
  971. */
  972. protected function submitOcrForAnalysis($record)
  973. {
  974. try {
  975. // 获取OCR题目结果(使用校准后的答案)
  976. $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $record->id)
  977. ->orderBy('question_number')
  978. ->get();
  979. if ($ocrQuestions->isEmpty()) {
  980. \Log::warning('OCR记录没有题目,无法提交分析', ['record_id' => $record->id]);
  981. return;
  982. }
  983. // 构建答题数据(与系统卷子格式一致)
  984. $answers = [];
  985. foreach ($ocrQuestions as $oq) {
  986. // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案
  987. $studentAnswer = !empty(trim($oq->manual_answer ?? ''))
  988. ? trim($oq->manual_answer)
  989. : trim($oq->student_answer ?? '');
  990. $answers[] = [
  991. 'question_bank_id' => 'ocr_q' . $oq->question_number, // OCR题目没有题库ID,生成临时ID
  992. 'question_text' => $oq->question_text ?? '', // 添加题目内容
  993. 'student_answer' => $studentAnswer,
  994. 'is_correct' => null, // 让AI分析判断
  995. 'score' => null, // OCR题目可能没有分数
  996. 'max_score' => $oq->score_total ?? null,
  997. 'kp_code' => $oq->kp_code ?? null,
  998. ];
  999. }
  1000. // 使用与系统卷子相同的接口提交
  1001. $submissionData = [
  1002. 'paper_id' => 'ocr_' . $record->id, // OCR记录ID作为paper_id
  1003. 'answers' => $answers,
  1004. ];
  1005. \Log::info('提交OCR数据到AI分析(统一接口)', [
  1006. 'record_id' => $record->id,
  1007. 'student_id' => $record->student_id,
  1008. 'question_count' => count($answers),
  1009. 'api_endpoint' => '/api/v1/attempts/batch/student/' . $record->student_id
  1010. ]);
  1011. // 调用学习分析服务(与系统卷子使用相同的方法)
  1012. $learningService = app(\App\Services\LearningAnalyticsService::class);
  1013. $response = $learningService->submitBatchAttempts($record->student_id, $submissionData);
  1014. if (!empty($response) && !isset($response['error'])) {
  1015. // 从响应中获取analysis_id(如果API返回)
  1016. $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time());
  1017. // 更新OCR记录的analysis_id
  1018. $record->analysis_id = $analysisId;
  1019. $record->save();
  1020. \Log::info('OCR分析提交成功(统一接口)', [
  1021. 'record_id' => $record->id,
  1022. 'analysis_id' => $analysisId,
  1023. 'response_keys' => array_keys($response)
  1024. ]);
  1025. // 更新recordData
  1026. $this->recordData['analysis_id'] = $analysisId;
  1027. } else {
  1028. \Log::error('OCR分析提交失败', [
  1029. 'record_id' => $record->id,
  1030. 'response' => $response
  1031. ]);
  1032. }
  1033. } catch (\Exception $e) {
  1034. \Log::error('提交OCR分析异常', [
  1035. 'record_id' => $record->id,
  1036. 'error' => $e->getMessage(),
  1037. 'trace' => $e->getTraceAsString()
  1038. ]);
  1039. }
  1040. }
  1041. /**
  1042. * 使用ChatGPT分析试卷图片
  1043. */
  1044. public function analyzeWithChatGPT(): void
  1045. {
  1046. $this->validate([
  1047. 'imageUrl' => 'required|url',
  1048. ]);
  1049. if (empty($this->imageUrl)) {
  1050. Notification::make()
  1051. ->title('请先上传试卷图片')
  1052. ->danger()
  1053. ->send();
  1054. return;
  1055. }
  1056. $this->isAnalyzing = true;
  1057. $this->chatGPTResult = [];
  1058. try {
  1059. // 确定要分析的试卷ID
  1060. $targetPaperId = null;
  1061. if ($this->recordId) {
  1062. // OCR记录,使用记录ID作为paper_id
  1063. $targetPaperId = 'ocr_' . $this->recordId;
  1064. } elseif ($this->paperId) {
  1065. // 系统生成卷子
  1066. $targetPaperId = $this->paperId;
  1067. }
  1068. if (!$targetPaperId) {
  1069. throw new \Exception('未找到有效的试卷ID');
  1070. }
  1071. $chatGPTService = app(ChatGPTAnalysisService::class);
  1072. $result = $chatGPTService->analyzeExamPaper($targetPaperId, $this->imageUrl);
  1073. if ($result['success']) {
  1074. $this->chatGPTResult = $result['data'];
  1075. // 保存分析结果到数据库
  1076. $saved = $chatGPTService->saveAnalysisResult($targetPaperId, $result['data']);
  1077. // 同时提交到学习分析服务,更新掌握度
  1078. $this->submitToLearningAnalysis($result['data']);
  1079. if ($saved) {
  1080. Notification::make()
  1081. ->title('ChatGPT分析完成')
  1082. ->body('已成功分析 ' . count($result['data']['questions'] ?? []) . ' 道题目')
  1083. ->success()
  1084. ->send();
  1085. // 刷新分析数据
  1086. $this->loadAnalysisData();
  1087. } else {
  1088. Notification::make()
  1089. ->title('分析完成但保存失败')
  1090. ->body('请手动刷新查看结果')
  1091. ->warning()
  1092. ->send();
  1093. }
  1094. } else {
  1095. throw new \Exception($result['error'] ?? '未知错误');
  1096. }
  1097. } catch (\Exception $e) {
  1098. \Log::error('ChatGPT分析失败', [
  1099. 'paper_id' => $targetPaperId ?? 'unknown',
  1100. 'error' => $e->getMessage()
  1101. ]);
  1102. Notification::make()
  1103. ->title('ChatGPT分析失败')
  1104. ->body($e->getMessage())
  1105. ->danger()
  1106. ->send();
  1107. } finally {
  1108. $this->isAnalyzing = false;
  1109. }
  1110. }
  1111. /**
  1112. * 将ChatGPT分析结果提交到学习分析服务
  1113. */
  1114. private function submitToLearningAnalysis(array $analysisData): void
  1115. {
  1116. try {
  1117. if (!isset($analysisData['questions'])) {
  1118. return;
  1119. }
  1120. $studentId = $this->studentInfo['student_id'] ?? null;
  1121. if (!$studentId) {
  1122. \Log::warning('未找到学生ID,跳过学习分析提交');
  1123. return;
  1124. }
  1125. // 转换ChatGPT结果为学习分析服务期望的格式
  1126. $answers = [];
  1127. foreach ($analysisData['questions'] as $question) {
  1128. // 获取知识点名称(ChatGPT返回的是name,不是id)
  1129. $kpCode = null;
  1130. if (isset($question['knowledge_points']) && !empty($question['knowledge_points'])) {
  1131. $kpCode = $question['knowledge_points'][0]['name'] ?? $question['knowledge_points'][0]['id'] ?? null;
  1132. }
  1133. $answers[] = [
  1134. 'question_id' => $question['q'] ?? null,
  1135. 'student_answer' => $question['student_answer'] ?? null,
  1136. 'correct_answer' => $question['correct_answer'] ?? null,
  1137. 'is_correct' => $question['is_correct'] ?? false,
  1138. 'score' => $question['is_correct'] ? 1 : 0,
  1139. 'max_score' => 1,
  1140. 'kp_code' => $kpCode,
  1141. ];
  1142. }
  1143. $submissionData = [
  1144. 'paper_id' => $this->paperId ?? ('ocr_' . $this->recordId),
  1145. 'answers' => $answers,
  1146. ];
  1147. \Log::info('提交ChatGPT分析结果到学习分析服务', [
  1148. 'student_id' => $studentId,
  1149. 'paper_id' => $submissionData['paper_id'],
  1150. 'question_count' => count($answers),
  1151. 'sample_kp_code' => $answers[0]['kp_code'] ?? 'N/A'
  1152. ]);
  1153. // 调用学习分析服务
  1154. $learningService = app(\App\Services\LearningAnalyticsService::class);
  1155. $response = $learningService->submitBatchAttempts($studentId, $submissionData);
  1156. \Log::info('ChatGPT分析结果已提交到学习分析服务', [
  1157. 'student_id' => $studentId,
  1158. 'paper_id' => $submissionData['paper_id'],
  1159. 'question_count' => count($answers),
  1160. 'response_success' => !isset($response['error'])
  1161. ]);
  1162. } catch (\Exception $e) {
  1163. \Log::error('提交ChatGPT分析结果到学习分析服务失败', [
  1164. 'error' => $e->getMessage(),
  1165. 'trace' => $e->getTraceAsString()
  1166. ]);
  1167. }
  1168. }
  1169. /**
  1170. * 切换识别模式
  1171. */
  1172. public function updatedUseChatGPT($value): void
  1173. {
  1174. if ($value) {
  1175. Notification::make()
  1176. ->title('ChatGPT识别模式')
  1177. ->body('将使用ChatGPT进行试卷智能分析,无需OCR识别,可直接分析图片中的学生答案')
  1178. ->info()
  1179. ->send();
  1180. }
  1181. }
  1182. /**
  1183. * 重置ChatGPT分析表单
  1184. */
  1185. public function resetChatGPTForm(): void
  1186. {
  1187. $this->reset(['imageUrl', 'useChatGPT', 'chatGPTResult']);
  1188. $this->isAnalyzing = false;
  1189. }
  1190. }