ExamAnalysis.php 54 KB

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