ExamAnswerAnalysisService.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Collection;
  6. /**
  7. * 考试答题分析服务(步骤级分析)
  8. * 基于卷子分析思考文档的思路实现
  9. *
  10. * 核心流程:
  11. * 1. 接收卷子ID和每道题的对错、简答题的分步骤对错
  12. * 2. 将原子信息映射到知识点/技能
  13. * 3. 计算知识点掌握度向量
  14. * 4. 生成详细分析报告
  15. * 5. 提供智能出卷推荐依据
  16. */
  17. class ExamAnswerAnalysisService
  18. {
  19. public function __construct(
  20. private readonly MasteryCalculator $masteryCalculator,
  21. private readonly KnowledgeMasteryService $knowledgeMasteryService,
  22. private readonly LocalAIAnalysisService $aiAnalysisService
  23. ) {}
  24. /**
  25. * 分析考试答题数据
  26. *
  27. * @param array $examData 考试数据
  28. * [
  29. * 'paper_id' => 'exam_001',
  30. * 'student_id' => 'student_001',
  31. * 'questions' => [
  32. * [
  33. * 'question_id' => 'Q1',
  34. * 'score' => 5,
  35. * 'score_obtained' => 5,
  36. * 'steps' => [
  37. * ['step_index' => 1, 'is_correct' => true, 'kp_id' => 'K-SQRT-SIMPLE'],
  38. * ['step_index' => 2, 'is_correct' => true, 'kp_id' => 'K-NUM-ADD-SUB']
  39. * ]
  40. * ]
  41. * ]
  42. * ]
  43. *
  44. * @return array 分析结果
  45. */
  46. public function analyzeExamAnswers(array $examData): array
  47. {
  48. Log::info('开始分析考试答题', [
  49. 'paper_id' => $examData['paper_id'] ?? 'unknown',
  50. 'student_id' => $examData['student_id'] ?? 'unknown',
  51. 'question_count' => count($examData['questions'] ?? [])
  52. ]);
  53. $studentId = $examData['student_id'];
  54. $questions = $examData['questions'] ?? [];
  55. // 0. 自动计算分数(如果用户未提供)
  56. $questions = $this->autoCalculateScores($questions);
  57. // 1. 获取学案基准难度
  58. $examBaseDifficulty = $this->getExamBaseDifficulty($examData['paper_id'] ?? '');
  59. // 2. 保存答题记录到数据库
  60. $this->saveExamAnswerRecords($examData);
  61. // 3. 获取题目知识点映射
  62. $questionMappings = $this->getQuestionKnowledgeMappings($questions);
  63. // 4. 计算每个知识点的加权掌握度(传入学案基准难度)
  64. $knowledgeMasteryVector = $this->calculateKnowledgeMasteryVector($questions, $questionMappings, $examBaseDifficulty, $studentId);
  65. // 5. 更新学生掌握度
  66. $updatedMastery = $this->updateStudentMastery($studentId, $knowledgeMasteryVector);
  67. // 5. 生成题目维度分析
  68. $questionAnalysis = $this->analyzeQuestions($questions, $questionMappings);
  69. // 6. 生成知识点维度分析
  70. $knowledgePointAnalysis = $this->analyzeKnowledgePoints($knowledgeMasteryVector, $questionMappings);
  71. // 7. 生成整体掌握度总结
  72. $overallSummary = $this->generateOverallSummary($updatedMastery);
  73. // 8. 生成智能出卷推荐依据
  74. $smartQuizRecommendation = $this->generateSmartQuizRecommendation($updatedMastery);
  75. // 9. 保存分析结果
  76. $analysisResult = [
  77. 'paper_id' => $examData['paper_id'],
  78. 'student_id' => $studentId,
  79. 'timestamp' => now()->toISOString(),
  80. 'question_analysis' => $questionAnalysis,
  81. 'knowledge_point_analysis' => $knowledgePointAnalysis,
  82. 'overall_summary' => $overallSummary,
  83. 'smart_quiz_recommendation' => $smartQuizRecommendation,
  84. 'mastery_vector' => $updatedMastery,
  85. ];
  86. $this->saveAnalysisResult($studentId, $examData['paper_id'], $analysisResult);
  87. Log::info('考试答题分析完成', [
  88. 'student_id' => $studentId,
  89. 'paper_id' => $examData['paper_id'],
  90. 'analyzed_knowledge_points' => count($knowledgeMasteryVector)
  91. ]);
  92. return $analysisResult;
  93. }
  94. /**
  95. * 获取学案基准难度(映射为1-4级)
  96. */
  97. private function getExamBaseDifficulty(string $paperId): ?int
  98. {
  99. if (empty($paperId)) {
  100. return null;
  101. }
  102. try {
  103. // 从试卷表获取difficulty_category
  104. $paper = DB::table('papers')
  105. ->where('paper_id', $paperId)
  106. ->first();
  107. if (!$paper) {
  108. Log::warning('未找到试卷,尝试从缓存获取', ['paper_id' => $paperId]);
  109. return null;
  110. }
  111. $difficultyCategory = $paper->difficulty_category ?? '中等';
  112. // 映射为1-4级(筑基、提分、培优、竞赛)
  113. return match (strtolower($difficultyCategory)) {
  114. '筑基', 'easy', 'foundation', '1' => 1,
  115. '提分', 'medium', 'improvement', '2' => 2,
  116. '培优', 'hard', 'excellent', 'difficult', '3' => 3,
  117. '竞赛', 'competition', 'very hard', 'very difficult', '4' => 4,
  118. default => 2, // 默认提分
  119. };
  120. } catch (\Exception $e) {
  121. Log::warning('获取学案基准难度失败,使用默认2级', [
  122. 'paper_id' => $paperId,
  123. 'error' => $e->getMessage(),
  124. ]);
  125. return 2; // 默认中等难度
  126. }
  127. }
  128. /**
  129. * 获取题目知识点映射
  130. */
  131. private function getQuestionKnowledgeMappings(array $questions): array
  132. {
  133. $mappings = [];
  134. $failedQuestions = [];
  135. // 直接从题目数据中提取知识点信息(不再调用外部服务)
  136. foreach ($questions as $question) {
  137. $questionId = $question['question_id'] ?? null;
  138. if (!$questionId) continue;
  139. // 提取知识点信息(优先使用请求数据中的字段)
  140. $kpMapping = [];
  141. // 尝试多个可能的知识点字段
  142. $kpCode = $question['kp_code']
  143. ?? $question['knowledge_point']
  144. ?? $question['kp_code']
  145. ?? null;
  146. // 如果请求中没有知识点信息,从数据库自动获取
  147. if (empty($kpCode)) {
  148. $dbKpInfo = $this->getQuestionKnowledgePointFromDb($questionId);
  149. if ($dbKpInfo) {
  150. $kpMapping[] = [
  151. 'kp_id' => $dbKpInfo['kp_id'],
  152. 'kp_name' => $dbKpInfo['kp_name'],
  153. 'weight' => 1.0
  154. ];
  155. Log::debug('从数据库获取题目知识点', [
  156. 'question_id' => $questionId,
  157. 'kp_id' => $dbKpInfo['kp_id']
  158. ]);
  159. }
  160. } else {
  161. $kpMapping[] = [
  162. 'kp_id' => $kpCode,
  163. 'kp_name' => $question['kp_name'] ?? $kpCode,
  164. 'weight' => 1.0
  165. ];
  166. }
  167. // 如果仍然没有知识点信息,跳过该题目
  168. if (empty($kpMapping)) {
  169. $failedQuestions[] = $questionId;
  170. Log::warning('跳过无法获取知识点的题目', [
  171. 'question_id' => $questionId
  172. ]);
  173. continue;
  174. }
  175. $mappings[$questionId] = [
  176. 'question_id' => $questionId,
  177. 'kp_mapping' => $kpMapping
  178. ];
  179. }
  180. if (!empty($failedQuestions)) {
  181. Log::warning('部分题目无法获取知识点信息', [
  182. 'failed_questions' => $failedQuestions,
  183. 'total_questions' => count($questions),
  184. 'mapped_questions' => count($mappings)
  185. ]);
  186. }
  187. Log::info('题目知识点映射构建完成', [
  188. 'total_questions' => count($questions),
  189. 'mapped_questions' => count($mappings),
  190. 'failed_questions' => count($failedQuestions)
  191. ]);
  192. return $mappings;
  193. }
  194. /**
  195. * 从数据库获取题目的知识点信息
  196. */
  197. private function getQuestionKnowledgePointFromDb(string $questionId): ?array
  198. {
  199. try {
  200. // 优先从 MySQL 的 questions 表获取
  201. $question = DB::connection('mysql')
  202. ->table('questions')
  203. ->where('id', $questionId)
  204. ->orWhere('question_code', $questionId)
  205. ->first();
  206. $kpCode = null;
  207. if ($question) {
  208. // 尝试从 kp_code 或 knowledge_point 字段获取
  209. $kpCode = $question->kp_code
  210. ?? $question->knowledge_point
  211. ?? $question->knowledge_points
  212. ?? null;
  213. }
  214. // 如果questions表中没有,从knowledge_points表查找
  215. if (!$kpCode) {
  216. $kpCode = $questionId; // 假设question_id就是kp_code
  217. }
  218. // 从 knowledge_points 表获取知识点详细信息(包含中文名称)
  219. $kpInfo = DB::connection('mysql')
  220. ->table('knowledge_points')
  221. ->where('kp_code', $kpCode)
  222. ->first();
  223. if ($kpInfo) {
  224. Log::debug('从knowledge_points表获取知识点', [
  225. 'question_id' => $questionId,
  226. 'kp_code' => $kpCode,
  227. 'kp_name' => $kpInfo->name
  228. ]);
  229. return [
  230. 'kp_id' => $kpCode,
  231. 'kp_name' => $kpInfo->name
  232. ];
  233. }
  234. // 备选:从 QuestionBank API 获取知识点
  235. $questionBankInfo = $this->getQuestionFromQuestionBank($questionId);
  236. if ($questionBankInfo && !empty($questionBankInfo['knowledge_points'])) {
  237. $apiKpCode = $questionBankInfo['knowledge_points'][0]['code'] ?? null;
  238. if ($apiKpCode) {
  239. // 尝试从 knowledge_points 表获取中文名称
  240. $apiKpInfo = DB::connection('mysql')
  241. ->table('knowledge_points')
  242. ->where('kp_code', $apiKpCode)
  243. ->first();
  244. return [
  245. 'kp_id' => $apiKpCode,
  246. 'kp_name' => $apiKpInfo->name ?? ($questionBankInfo['knowledge_points'][0]['name'] ?? $apiKpCode)
  247. ];
  248. }
  249. }
  250. // 最后备选:使用kpCode作为名称
  251. if ($kpCode) {
  252. Log::warning('未找到知识点中文名称,使用编码作为名称', [
  253. 'question_id' => $questionId,
  254. 'kp_code' => $kpCode
  255. ]);
  256. return [
  257. 'kp_id' => $kpCode,
  258. 'kp_name' => $kpCode
  259. ];
  260. }
  261. return null;
  262. } catch (\Exception $e) {
  263. Log::warning('从数据库获取题目知识点失败', [
  264. 'question_id' => $questionId,
  265. 'error' => $e->getMessage()
  266. ]);
  267. return null;
  268. }
  269. }
  270. /**
  271. * 从 QuestionBank API 获取题目信息
  272. */
  273. private function getQuestionFromQuestionBank(string $questionId): ?array
  274. {
  275. try {
  276. $baseUrl = config('services.question_bank_api.base_url', 'http://question-bank-api:5015');
  277. $response = Http::timeout(10)->get($baseUrl . '/questions/' . $questionId);
  278. if ($response->successful()) {
  279. $data = $response->json();
  280. return $data['data'] ?? null;
  281. }
  282. return null;
  283. } catch (\Exception $e) {
  284. Log::warning('从QuestionBank获取题目信息失败', [
  285. 'question_id' => $questionId,
  286. 'error' => $e->getMessage()
  287. ]);
  288. return null;
  289. }
  290. }
  291. /**
  292. * 计算知识点掌握度向量
  293. * 【修复】集成MasteryCalculator的BKT模型进行精确计算
  294. *
  295. * 核心算法说明:
  296. * 1. 从考试答题中提取每个知识点的答题记录
  297. * 2. 调用MasteryCalculator计算掌握度(包含:正确率、难度加权、时间效率、遗忘曲线)
  298. * 3. 返回包含掌握度、置信度、趋势等完整信息的向量
  299. */
  300. private function calculateKnowledgeMasteryVector(array $questions, array $questionMappings, ?int $examBaseDifficulty = null, ?string $studentId = null): array
  301. {
  302. // 按知识点聚合答题记录
  303. $knowledgeAttempts = [];
  304. foreach ($questions as $question) {
  305. $questionId = $question['question_id'];
  306. $score = floatval($question['score_obtained'] ?? 0);
  307. $maxScore = floatval($question['score'] ?? $score);
  308. $steps = $question['steps'] ?? [];
  309. $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
  310. $mapping = $questionMappings[$questionId] ?? null;
  311. if (!$mapping || !isset($mapping['kp_mapping'])) {
  312. continue;
  313. }
  314. // 构建答题记录(用于MasteryCalculator)
  315. $attemptRecord = [
  316. 'question_id' => $questionId,
  317. 'is_correct' => $isCorrect,
  318. 'partial_score' => $maxScore > 0 ? $score / $maxScore : 0,
  319. 'question_difficulty' => $question['difficulty'] ?? 0.6,
  320. 'attempt_time_seconds' => $question['time_spent'] ?? 120,
  321. 'completed_at' => now()->toISOString(),
  322. 'created_at' => now()->toISOString(),
  323. ];
  324. // 如果有步骤级分析,使用步骤分析
  325. if (!empty($steps)) {
  326. foreach ($steps as $step) {
  327. $kpId = $step['kp_id'];
  328. if (empty($kpId)) {
  329. Log::warning('ExamAnswerAnalysisService: 步骤缺少知识点ID', [
  330. 'question_id' => $questionId,
  331. 'step' => $step
  332. ]);
  333. continue;
  334. }
  335. if (!isset($knowledgeAttempts[$kpId])) {
  336. $knowledgeAttempts[$kpId] = [
  337. 'attempts' => [],
  338. 'step_details' => [],
  339. ];
  340. }
  341. // 每个步骤作为独立答题记录
  342. $stepAttempt = $attemptRecord;
  343. $stepAttempt['is_correct'] = $step['is_correct'];
  344. $stepAttempt['step_index'] = $step['step_index'];
  345. $knowledgeAttempts[$kpId]['attempts'][] = $stepAttempt;
  346. $knowledgeAttempts[$kpId]['step_details'][] = [
  347. 'question_id' => $questionId,
  348. 'step_index' => $step['step_index'],
  349. 'score' => $step['score'] ?? ($maxScore / count($steps)),
  350. 'is_correct' => $step['is_correct'],
  351. ];
  352. }
  353. } else {
  354. // 题目整体分析
  355. foreach ($mapping['kp_mapping'] as $kpMapping) {
  356. $kpId = $kpMapping['kp_id'];
  357. if (!isset($knowledgeAttempts[$kpId])) {
  358. $knowledgeAttempts[$kpId] = [
  359. 'attempts' => [],
  360. 'step_details' => [],
  361. ];
  362. }
  363. $knowledgeAttempts[$kpId]['attempts'][] = $attemptRecord;
  364. $knowledgeAttempts[$kpId]['step_details'][] = [
  365. 'question_id' => $questionId,
  366. 'score' => $score,
  367. 'max_score' => $maxScore,
  368. 'is_correct' => $isCorrect,
  369. ];
  370. }
  371. }
  372. }
  373. // 【核心】使用MasteryCalculator计算每个知识点的掌握度
  374. $masteryVector = [];
  375. foreach ($knowledgeAttempts as $kpId => $data) {
  376. $attempts = $data['attempts'];
  377. // 如果没有学案基准难度,使用默认值2(提分)
  378. $baseDifficulty = $examBaseDifficulty ?? 2;
  379. // 调用MasteryCalculator的核心算法(传入学案基准难度)
  380. // 该算法包含:正确率、难度加权、时间效率、技能熟练度、遗忘曲线衰减
  381. $masteryResult = $this->masteryCalculator->calculateMasteryLevel(
  382. $studentId ?? '', // 传递学生ID,用于保存掌握度到数据库
  383. $kpId,
  384. $attempts,
  385. $baseDifficulty
  386. );
  387. $masteryVector[$kpId] = [
  388. 'kp_id' => $kpId,
  389. 'mastery' => $masteryResult['mastery'],
  390. 'confidence' => $masteryResult['confidence'],
  391. 'trend' => $masteryResult['trend'],
  392. 'total_attempts' => $masteryResult['total_attempts'],
  393. 'correct_attempts' => $masteryResult['correct_attempts'],
  394. 'accuracy_rate' => $masteryResult['accuracy_rate'],
  395. 'step_details' => $data['step_details'],
  396. // 计算细节(用于调试和分析)
  397. 'calculation_details' => $masteryResult['details'] ?? [],
  398. ];
  399. }
  400. Log::info('知识点掌握度向量计算完成', [
  401. 'knowledge_points_count' => count($masteryVector),
  402. 'sample' => array_slice($masteryVector, 0, 3, true),
  403. ]);
  404. return $masteryVector;
  405. }
  406. /**
  407. * 更新学生掌握度(与历史数据合并)
  408. */
  409. private function updateStudentMastery(string $studentId, array $knowledgeMasteryVector): array
  410. {
  411. $updatedMastery = [];
  412. foreach ($knowledgeMasteryVector as $kpId => $data) {
  413. // 获取历史掌握度
  414. $historyMastery = DB::connection('mysql')
  415. ->table('student_knowledge_mastery')
  416. ->where('student_id', $studentId)
  417. ->where('kp_code', $kpId)
  418. ->first();
  419. $historyMasteryLevel = $historyMastery->mastery_level ?? 0.5;
  420. $historyWeight = $historyMastery->total_attempts ?? 0;
  421. $currentWeight = $data['total_attempts'] ?? 1;
  422. // 合并计算:历史权重 + 当前权重
  423. $newMastery = $historyWeight > 0
  424. ? ($historyWeight * $historyMasteryLevel + $currentWeight * $data['mastery'])
  425. / ($historyWeight + $currentWeight)
  426. : $data['mastery'];
  427. $newConfidence = $data['confidence'];
  428. // 保存到数据库
  429. DB::connection('mysql')
  430. ->table('student_knowledge_mastery')
  431. ->updateOrInsert(
  432. ['student_id' => $studentId, 'kp_code' => $kpId],
  433. [
  434. 'mastery_level' => $newMastery,
  435. 'confidence_level' => $newConfidence,
  436. 'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
  437. 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_attempts'] > 0),
  438. 'mastery_trend' => $this->determineMasteryTrend($historyMasteryLevel, $newMastery),
  439. 'last_mastery_update' => now(),
  440. 'updated_at' => now(),
  441. ]
  442. );
  443. $updatedMastery[$kpId] = [
  444. 'kp_id' => $kpId,
  445. 'current_mastery' => $newMastery,
  446. 'previous_mastery' => $historyMasteryLevel,
  447. 'confidence' => $newConfidence,
  448. 'change' => $newMastery - $historyMasteryLevel,
  449. 'weight' => $currentWeight,
  450. 'is_parent' => false
  451. ];
  452. }
  453. // 【修复】计算并更新父节点掌握度,同时添加到返回数组中
  454. $parentMasteryData = $this->updateParentMasteryLevels($studentId, array_keys($knowledgeMasteryVector));
  455. // 合并父节点数据到返回数组
  456. $updatedMastery = array_merge($updatedMastery, $parentMasteryData);
  457. return $updatedMastery;
  458. }
  459. /**
  460. * 计算并更新父节点掌握度
  461. */
  462. private function updateParentMasteryLevels(string $studentId, array $childKpCodes): array
  463. {
  464. $parentMasteryData = [];
  465. try {
  466. // 获取所有子节点的父节点
  467. $parentKpCodes = DB::connection('mysql')
  468. ->table('knowledge_points')
  469. ->whereIn('kp_code', $childKpCodes)
  470. ->whereNotNull('parent_kp_code')
  471. ->distinct()
  472. ->pluck('parent_kp_code')
  473. ->toArray();
  474. foreach ($parentKpCodes as $parentKpCode) {
  475. // 使用MasteryCalculator计算父节点掌握度
  476. $parentMastery = $this->masteryCalculator->calculateParentMastery($studentId, $parentKpCode);
  477. // 获取父节点历史数据
  478. $historyParentMastery = DB::connection('mysql')
  479. ->table('student_knowledge_mastery')
  480. ->where('student_id', $studentId)
  481. ->where('kp_code', $parentKpCode)
  482. ->first();
  483. $previousMastery = $historyParentMastery->mastery_level ?? 0.5;
  484. // 保存父节点掌握度到数据库
  485. DB::connection('mysql')
  486. ->table('student_knowledge_mastery')
  487. ->updateOrInsert(
  488. ['student_id' => $studentId, 'kp_code' => $parentKpCode],
  489. [
  490. 'mastery_level' => $parentMastery,
  491. 'confidence_level' => 0.8, // 父节点置信度默认为0.8
  492. 'total_attempts' => 1, // 父节点不记录答题次数
  493. 'correct_attempts' => 0, // 父节点不记录正确次数
  494. 'mastery_trend' => 'calculated', // 标记为计算得出
  495. 'last_mastery_update' => now(),
  496. 'updated_at' => now(),
  497. ]
  498. );
  499. // 添加到返回数组中
  500. $parentMasteryData[$parentKpCode] = [
  501. 'kp_id' => $parentKpCode,
  502. 'current_mastery' => $parentMastery,
  503. 'previous_mastery' => $previousMastery,
  504. 'confidence' => 0.8,
  505. 'change' => $parentMastery - $previousMastery,
  506. 'weight' => 1,
  507. 'is_parent' => true
  508. ];
  509. Log::debug('父节点掌握度已更新', [
  510. 'student_id' => $studentId,
  511. 'parent_kp_code' => $parentKpCode,
  512. 'parent_mastery' => $parentMastery
  513. ]);
  514. }
  515. if (!empty($parentKpCodes)) {
  516. Log::info('父节点掌握度计算完成', [
  517. 'student_id' => $studentId,
  518. 'child_count' => count($childKpCodes),
  519. 'parent_count' => count($parentKpCodes),
  520. 'parent_kp_codes' => $parentKpCodes
  521. ]);
  522. }
  523. } catch (\Exception $e) {
  524. Log::warning('计算父节点掌握度失败', [
  525. 'student_id' => $studentId,
  526. 'child_kp_codes' => $childKpCodes,
  527. 'error' => $e->getMessage()
  528. ]);
  529. }
  530. return $parentMasteryData;
  531. }
  532. /**
  533. * 生成题目维度分析(包含AI分析和解题思路)
  534. */
  535. private function analyzeQuestions(array $questions, array $questionMappings): array
  536. {
  537. $analysis = [];
  538. foreach ($questions as $question) {
  539. $questionId = $question['question_id'];
  540. $score = floatval($question['score_obtained'] ?? 0);
  541. $maxScore = floatval($question['score'] ?? $score);
  542. $steps = $question['steps'] ?? [];
  543. $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
  544. $mapping = $questionMappings[$questionId] ?? ['kp_mapping' => []];
  545. if (empty($mapping['kp_mapping'])) {
  546. Log::warning('ExamAnswerAnalysisService: 题目无知识点映射', ['question_id' => $questionId]);
  547. continue;
  548. }
  549. $kpCode = $mapping['kp_mapping'][0]['kp_id'];
  550. // 步骤分析
  551. $stepAnalysis = [];
  552. if (!empty($steps)) {
  553. foreach ($steps as $step) {
  554. $kpId = $step['kp_id'];
  555. if (empty($kpId)) {
  556. Log::warning('ExamAnswerAnalysisService: 步骤缺少知识点ID', [
  557. 'question_id' => $questionId,
  558. 'step_index' => $step['step_index'] ?? 'unknown'
  559. ]);
  560. continue;
  561. }
  562. $stepAnalysis[] = [
  563. 'step_index' => $step['step_index'],
  564. 'is_correct' => $step['is_correct'],
  565. 'kp_id' => $kpId,
  566. 'description' => $step['description'] ?? ''
  567. ];
  568. }
  569. }
  570. // 知识点关联
  571. $knowledgePoints = array_map(function($kp) {
  572. return [
  573. 'kp_id' => $kp['kp_id'],
  574. 'kp_name' => $kp['kp_name'] ?? $kp['kp_id'],
  575. 'weight' => $kp['weight'] ?? 1.0
  576. ];
  577. }, $mapping['kp_mapping']);
  578. // 【集成】调用AI分析服务,获取解题思路和错误分析
  579. $aiAnalysis = $this->getQuestionAIAnalysis($question, $mapping);
  580. $analysis[] = [
  581. 'question_id' => $questionId,
  582. 'score_obtained' => $score,
  583. 'max_score' => $maxScore,
  584. 'accuracy_rate' => $maxScore > 0 ? $score / $maxScore : 0,
  585. 'is_correct' => $isCorrect,
  586. 'step_analysis' => $stepAnalysis,
  587. 'knowledge_points' => $knowledgePoints,
  588. 'performance_summary' => $this->generateQuestionPerformanceSummary($question, $stepAnalysis),
  589. // 【新增】解题思路和错误分析
  590. 'solution_process' => $aiAnalysis['solution_process'] ?? '',
  591. 'error_analysis' => $aiAnalysis['error_analysis'] ?? '',
  592. 'mistake_type' => $aiAnalysis['mistake_type'] ?? '',
  593. 'suggestions' => $aiAnalysis['suggestions'] ?? '',
  594. 'next_steps' => $aiAnalysis['next_steps'] ?? [],
  595. ];
  596. }
  597. return $analysis;
  598. }
  599. /**
  600. * 获取题目的AI分析(解题思路、错误分析)
  601. */
  602. private function getQuestionAIAnalysis(array $question, array $mapping): array
  603. {
  604. $isCorrect = $question['is_correct'] ?? false;
  605. $score = floatval($question['score_obtained'] ?? 0);
  606. $maxScore = floatval($question['score'] ?? 10);
  607. $kpCode = $mapping['kp_mapping'][0]['kp_id'] ?? null;
  608. if (empty($kpCode)) {
  609. Log::warning('ExamAnswerAnalysisService: getQuestionAIAnalysis缺少知识点ID', [
  610. 'question_id' => $question['question_id'] ?? 'unknown',
  611. 'mapping' => $mapping
  612. ]);
  613. $kpCode = 'UNKNOWN_KP';
  614. }
  615. // 调用LocalAIAnalysisService进行分析
  616. try {
  617. $analysisResult = $this->aiAnalysisService->analyzeAnswer([
  618. 'question_id' => $question['question_id'],
  619. 'question_text' => $question['question_text'] ?? '',
  620. 'student_answer' => $question['student_answer'] ?? '',
  621. 'correct_answer' => $question['correct_answer'] ?? '',
  622. 'score' => $score,
  623. 'max_score' => $maxScore,
  624. 'kp_code' => $kpCode,
  625. ]);
  626. $data = $analysisResult['data'] ?? [];
  627. // 根据正确性生成不同的解题思路
  628. if ($isCorrect) {
  629. return [
  630. 'solution_process' => $data['correct_solution'] ?? '该题作答正确,解题思路清晰',
  631. 'error_analysis' => '',
  632. 'mistake_type' => '',
  633. 'suggestions' => $data['suggestions'] ?? '继续保持良好的解题习惯',
  634. 'next_steps' => $data['next_steps'] ?? ['尝试更高难度的同类题目'],
  635. ];
  636. }
  637. // 错误题目:返回详细分析
  638. return [
  639. 'solution_process' => $data['correct_solution'] ?? '请参考标准解题步骤',
  640. 'error_analysis' => $data['reason'] ?? '解题过程中存在错误',
  641. 'mistake_type' => $data['mistake_type'] ?? '计算或理解错误',
  642. 'suggestions' => $data['suggestions'] ?? '建议针对薄弱知识点进行专项练习',
  643. 'next_steps' => $data['next_steps'] ?? ['复习相关知识点', '做同类型练习题'],
  644. ];
  645. } catch (\Exception $e) {
  646. Log::warning('AI分析失败,使用默认分析', [
  647. 'question_id' => $question['question_id'],
  648. 'error' => $e->getMessage(),
  649. ]);
  650. // 回退到基础分析
  651. return $this->getFallbackAnalysis($question, $isCorrect);
  652. }
  653. }
  654. /**
  655. * 回退分析(当AI分析失败时)
  656. */
  657. private function getFallbackAnalysis(array $question, bool $isCorrect): array
  658. {
  659. if ($isCorrect) {
  660. return [
  661. 'solution_process' => '该题作答正确',
  662. 'error_analysis' => '',
  663. 'mistake_type' => '',
  664. 'suggestions' => '继续保持',
  665. 'next_steps' => ['尝试更高难度的题目'],
  666. ];
  667. }
  668. $scoreRatio = floatval($question['score_obtained'] ?? 0) / max(floatval($question['score'] ?? 1), 1);
  669. return [
  670. 'solution_process' => '请参考标准答案和解题步骤',
  671. 'error_analysis' => $scoreRatio < 0.3 ? '知识点理解存在偏差' : '解题过程中出现错误',
  672. 'mistake_type' => $scoreRatio < 0.3 ? '概念错误' : '计算/步骤错误',
  673. 'suggestions' => '建议复习相关知识点,加强练习',
  674. 'next_steps' => ['复习基础概念', '做同类型练习题', '请教老师或同学'],
  675. ];
  676. }
  677. /**
  678. * 生成知识点维度分析
  679. */
  680. private function analyzeKnowledgePoints(array $knowledgeMasteryVector, array $questionMappings): array
  681. {
  682. $analysis = [];
  683. foreach ($knowledgeMasteryVector as $kpId => $data) {
  684. $analysis[] = [
  685. 'kp_id' => $kpId,
  686. 'mastery_level' => $data['mastery'],
  687. 'confidence_level' => $data['confidence'],
  688. 'performance_in_exam' => $this->evaluatePerformanceLevel($data['mastery']),
  689. 'evidence_count' => count($data['step_details']),
  690. 'step_evidence' => $data['step_details'],
  691. 'recommendation' => $this->generateKnowledgePointRecommendation($data)
  692. ];
  693. }
  694. return $analysis;
  695. }
  696. /**
  697. * 生成整体掌握度总结
  698. */
  699. private function generateOverallSummary(array $updatedMastery): array
  700. {
  701. $knowledgePoints = array_values($updatedMastery);
  702. if (empty($knowledgePoints)) {
  703. return [
  704. 'total_knowledge_points' => 0,
  705. 'average_mastery' => 0,
  706. 'mastery_distribution' => [
  707. 'mastered' => 0,
  708. 'good' => 0,
  709. 'weak' => 0
  710. ],
  711. 'top_strengths' => [],
  712. 'top_weaknesses' => []
  713. ];
  714. }
  715. // 计算平均掌握度(只计算子节点,不包括父节点)
  716. $childNodes = array_filter($knowledgePoints, fn($kp) => !($kp['is_parent'] ?? false));
  717. $totalChildNodes = count($childNodes);
  718. if ($totalChildNodes === 0) {
  719. // 如果没有子节点,只计算所有节点
  720. $averageMastery = array_sum(array_column($knowledgePoints, 'current_mastery')) / count($knowledgePoints);
  721. } else {
  722. // 计算所有节点的平均掌握度(包括父节点)
  723. $allMastery = array_column($knowledgePoints, 'current_mastery');
  724. $averageMastery = array_sum($allMastery) / count($allMastery);
  725. }
  726. // 掌握度分布(只统计子节点)
  727. $mastered = array_filter($childNodes, fn($kp) => $kp['current_mastery'] >= 0.85);
  728. $good = array_filter($childNodes, fn($kp) => $kp['current_mastery'] >= 0.70 && $kp['current_mastery'] < 0.85);
  729. $weak = array_filter($childNodes, fn($kp) => $kp['current_mastery'] < 0.70);
  730. // 排序找出优势和薄弱点(只包括子节点)
  731. usort($childNodes, fn($a, $b) => $b['current_mastery'] <=> $a['current_mastery']);
  732. $topStrengths = array_slice($childNodes, 0, 3);
  733. $topWeaknesses = array_slice(array_reverse($childNodes), 0, 3);
  734. // 统计父子节点数量
  735. $childCount = count(array_filter($knowledgePoints, fn($kp) => !($kp['is_parent'] ?? false)));
  736. $parentCount = count(array_filter($knowledgePoints, fn($kp) => $kp['is_parent'] ?? false));
  737. return [
  738. 'total_knowledge_points' => $totalChildNodes, // 只统计子节点数量
  739. 'child_nodes_count' => $childCount,
  740. 'parent_nodes_count' => $parentCount,
  741. 'average_mastery' => round($averageMastery, 4),
  742. 'mastery_distribution' => [
  743. 'mastered' => count($mastered),
  744. 'good' => count($good),
  745. 'weak' => count($weak)
  746. ],
  747. 'top_strengths' => $topStrengths,
  748. 'top_weaknesses' => $topWeaknesses,
  749. 'overall_performance' => $this->evaluateOverallPerformance($averageMastery)
  750. ];
  751. }
  752. /**
  753. * 生成智能出卷推荐依据
  754. * 基于文档中的推荐优先级算法
  755. */
  756. private function generateSmartQuizRecommendation(array $updatedMastery): array
  757. {
  758. $recommendations = [];
  759. // 只对子节点生成推荐,忽略父节点
  760. foreach ($updatedMastery as $kpId => $data) {
  761. // 跳过父节点
  762. if ($data['is_parent'] ?? false) {
  763. continue;
  764. }
  765. $mastery = $data['current_mastery'];
  766. $confidence = $data['confidence'];
  767. $weight = $data['weight'];
  768. // 推荐优先级 = (1 - 掌握度) * 重要性 * 覆盖需求
  769. // 重要性可以根据知识点在中考/阶段考试中的权重,这里简化为1.0
  770. $importance = 1.0;
  771. // 覆盖需求:最近没考过或考得少,值大
  772. $coverageNeed = max(1.0, 1.5 - ($weight / 10));
  773. $priority = (1 - $mastery) * $importance * $coverageNeed;
  774. $recommendations[] = [
  775. 'kp_id' => $kpId,
  776. 'current_mastery' => $mastery,
  777. 'priority' => $priority,
  778. 'recommended_questions' => $this->calculateRecommendedQuestions($mastery),
  779. 'focus_type' => $this->determineFocusType($mastery)
  780. ];
  781. }
  782. // 按优先级排序
  783. usort($recommendations, fn($a, $b) => $b['priority'] <=> $a['priority']);
  784. // 控制难度节奏:40%巩固型 + 40%修补型 + 20%挑战型
  785. $totalRecommendations = count($recommendations);
  786. $consolidation = array_slice($recommendations, 0, intval($totalRecommendations * 0.4));
  787. $remediation = array_slice($recommendations, intval($totalRecommendations * 0.4), intval($totalRecommendations * 0.4));
  788. $challenge = array_slice($recommendations, intval($totalRecommendations * 0.8));
  789. return [
  790. 'priority_list' => $recommendations,
  791. 'quiz_structure' => [
  792. 'consolidation_type' => $consolidation,
  793. 'remediation_type' => $remediation,
  794. 'challenge_type' => $challenge
  795. ],
  796. 'total_recommended_questions' => array_sum(array_column($recommendations, 'recommended_questions'))
  797. ];
  798. }
  799. /**
  800. * 保存考试答题记录
  801. */
  802. private function saveExamAnswerRecords(array $examData): void
  803. {
  804. $studentId = $examData['student_id'];
  805. $examId = $examData['paper_id'];
  806. // 【修复】先清理该考试的所有答题记录(支持重复考试)
  807. DB::connection('mysql')->table('student_answer_questions')
  808. ->where('student_id', $studentId)
  809. ->where('exam_id', $examId)
  810. ->delete();
  811. DB::connection('mysql')->table('student_answer_steps')
  812. ->where('student_id', $studentId)
  813. ->where('exam_id', $examId)
  814. ->delete();
  815. foreach ($examData['questions'] as $question) {
  816. $questionId = $question['question_id'];
  817. $steps = $question['steps'] ?? [];
  818. // 保存步骤级记录
  819. if (!empty($steps)) {
  820. foreach ($steps as $step) {
  821. $kpId = $step['kp_id'] ?? null;
  822. if (empty($kpId)) {
  823. Log::warning('ExamAnswerAnalysisService: 步骤保存缺少知识点ID', [
  824. 'student_id' => $studentId,
  825. 'exam_id' => $examId,
  826. 'question_id' => $questionId,
  827. 'step_index' => $step['step_index'] ?? 'unknown'
  828. ]);
  829. continue;
  830. }
  831. DB::connection('mysql')->table('student_answer_steps')->insert([
  832. 'student_id' => $studentId,
  833. 'exam_id' => $examId,
  834. 'question_id' => $questionId,
  835. 'step_index' => $step['step_index'],
  836. 'kp_id' => $kpId,
  837. 'is_correct' => $step['is_correct'],
  838. 'step_score' => $step['score'] ?? 0,
  839. 'created_at' => now(),
  840. 'updated_at' => now(),
  841. ]);
  842. }
  843. } else {
  844. // 保存题目级记录
  845. try {
  846. DB::connection('mysql')->table('student_answer_questions')->insertOrIgnore([
  847. 'student_id' => $studentId,
  848. 'exam_id' => $examId,
  849. 'question_id' => $questionId,
  850. 'score_obtained' => $question['score_obtained'] ?? 0,
  851. 'max_score' => $question['score'] ?? 0,
  852. 'created_at' => now(),
  853. 'updated_at' => now(),
  854. ]);
  855. } catch (\Exception $e) {
  856. Log::warning('保存答题记录失败', [
  857. 'student_id' => $studentId,
  858. 'exam_id' => $examId,
  859. 'question_id' => $questionId,
  860. 'error' => $e->getMessage(),
  861. ]);
  862. }
  863. }
  864. }
  865. Log::info('答题记录保存完成', [
  866. 'student_id' => $studentId,
  867. 'exam_id' => $examId,
  868. 'question_count' => count($examData['questions']),
  869. ]);
  870. }
  871. /**
  872. * 保存分析结果并创建掌握度快照
  873. */
  874. private function saveAnalysisResult(string $studentId, string $paperId, array $result): void
  875. {
  876. // 【修复】支持重复分析:先删除旧的分析结果
  877. DB::connection('mysql')->table('exam_analysis_results')
  878. ->where('student_id', $studentId)
  879. ->where('paper_id', $paperId)
  880. ->delete();
  881. // 插入新的分析结果
  882. DB::connection('mysql')->table('exam_analysis_results')->insert([
  883. 'student_id' => $studentId,
  884. 'paper_id' => $paperId,
  885. 'analysis_data' => json_encode($result),
  886. 'created_at' => now(),
  887. 'updated_at' => now(),
  888. ]);
  889. Log::info('分析结果保存完成', [
  890. 'student_id' => $studentId,
  891. 'paper_id' => $paperId,
  892. 'data_size' => strlen(json_encode($result)),
  893. ]);
  894. // 【集成】创建知识点掌握度快照
  895. $this->createMasterySnapshot($studentId, $paperId, $result);
  896. // 【新增】异步生成学情分析PDF
  897. try {
  898. Log::info('开始异步生成学情分析PDF', [
  899. 'student_id' => $studentId,
  900. 'paper_id' => $paperId,
  901. ]);
  902. // 使用队列异步生成PDF,避免阻塞主流程
  903. dispatch(new \App\Jobs\GenerateAnalysisPdfJob($paperId, $studentId, null));
  904. Log::info('PDF生成任务已加入队列', [
  905. 'student_id' => $studentId,
  906. 'paper_id' => $paperId,
  907. ]);
  908. } catch (\Exception $e) {
  909. Log::error('PDF生成任务加入队列失败', [
  910. 'student_id' => $studentId,
  911. 'paper_id' => $paperId,
  912. 'error' => $e->getMessage(),
  913. ]);
  914. }
  915. }
  916. /**
  917. * 创建知识点掌握度快照
  918. * 【集成】使用LocalAIAnalysisService的快照功能
  919. *
  920. * 快照用途:
  921. * 1. 追踪学生掌握度变化趋势
  922. * 2. 生成学情报告时对比历史数据
  923. * 3. 为智能出卷提供决策依据
  924. */
  925. private function createMasterySnapshot(string $studentId, string $paperId, array $analysisResult): void
  926. {
  927. try {
  928. // 计算快照数据
  929. $masteryVector = $analysisResult['mastery_vector'] ?? [];
  930. $overallMastery = 0;
  931. $weakCount = 0;
  932. $strongCount = 0;
  933. foreach ($masteryVector as $kpData) {
  934. $mastery = $kpData['current_mastery'] ?? $kpData['mastery'] ?? 0;
  935. $overallMastery += $mastery;
  936. if ($mastery < 0.6) {
  937. $weakCount++;
  938. } elseif ($mastery >= 0.85) {
  939. $strongCount++;
  940. }
  941. }
  942. $kpCount = count($masteryVector);
  943. $overallMastery = $kpCount > 0 ? round($overallMastery / $kpCount, 4) : 0;
  944. // 生成快照ID
  945. $snapshotId = 'snap_' . $paperId . '_' . now()->format('YmdHis');
  946. // 保存到快照表
  947. DB::connection('mysql')->table('knowledge_point_mastery_snapshots')->insert([
  948. 'snapshot_id' => $snapshotId,
  949. 'student_id' => $studentId,
  950. 'paper_id' => $paperId,
  951. 'answer_record_id' => null,
  952. 'mastery_data' => json_encode($masteryVector),
  953. 'overall_mastery' => $overallMastery,
  954. 'weak_knowledge_points_count' => $weakCount,
  955. 'strong_knowledge_points_count' => $strongCount,
  956. 'snapshot_time' => now(),
  957. 'analysis_id' => null,
  958. 'created_at' => now(),
  959. 'updated_at' => now(),
  960. ]);
  961. Log::info('掌握度快照创建成功', [
  962. 'snapshot_id' => $snapshotId,
  963. 'student_id' => $studentId,
  964. 'paper_id' => $paperId,
  965. 'overall_mastery' => $overallMastery,
  966. 'weak_count' => $weakCount,
  967. 'strong_count' => $strongCount,
  968. ]);
  969. } catch (\Exception $e) {
  970. // 快照创建失败不影响主流程
  971. Log::warning('掌握度快照创建失败', [
  972. 'student_id' => $studentId,
  973. 'paper_id' => $paperId,
  974. 'error' => $e->getMessage(),
  975. ]);
  976. }
  977. }
  978. /**
  979. * 判断掌握度趋势
  980. */
  981. private function determineMasteryTrend(float $previous, float $current): string
  982. {
  983. $change = $current - $previous;
  984. if ($change > 0.1) {
  985. return 'improving';
  986. } elseif ($change < -0.1) {
  987. return 'declining';
  988. } else {
  989. return 'stable';
  990. }
  991. }
  992. /**
  993. * 评估表现水平
  994. */
  995. private function evaluatePerformanceLevel(float $mastery): string
  996. {
  997. if ($mastery >= 0.85) {
  998. return 'excellent';
  999. } elseif ($mastery >= 0.70) {
  1000. return 'good';
  1001. } elseif ($mastery >= 0.50) {
  1002. return 'fair';
  1003. } else {
  1004. return 'poor';
  1005. }
  1006. }
  1007. /**
  1008. * 生成题目表现总结
  1009. */
  1010. private function generateQuestionPerformanceSummary(array $question, array $stepAnalysis): string
  1011. {
  1012. if (empty($stepAnalysis)) {
  1013. return '整题作答';
  1014. }
  1015. $correctSteps = count(array_filter($stepAnalysis, fn($s) => $s['is_correct']));
  1016. $totalSteps = count($stepAnalysis);
  1017. if ($correctSteps === $totalSteps) {
  1018. return '所有步骤正确';
  1019. } elseif ($correctSteps > 0) {
  1020. return "部分正确 ({$correctSteps}/{$totalSteps} 步骤正确)";
  1021. } else {
  1022. return '所有步骤错误';
  1023. }
  1024. }
  1025. /**
  1026. * 生成知识点建议
  1027. */
  1028. private function generateKnowledgePointRecommendation(array $data): string
  1029. {
  1030. $mastery = $data['mastery'];
  1031. if ($mastery >= 0.85) {
  1032. return '掌握良好,可安排综合练习';
  1033. } elseif ($mastery >= 0.70) {
  1034. return '基本掌握,建议加强练习';
  1035. } elseif ($mastery >= 0.50) {
  1036. return '需要重点练习,建议安排专项训练';
  1037. } else {
  1038. return '薄弱知识点,建议系统学习和大量练习';
  1039. }
  1040. }
  1041. /**
  1042. * 评估整体表现
  1043. */
  1044. private function evaluateOverallPerformance(float $averageMastery): string
  1045. {
  1046. if ($averageMastery >= 0.85) {
  1047. return '优秀';
  1048. } elseif ($averageMastery >= 0.70) {
  1049. return '良好';
  1050. } elseif ($averageMastery >= 0.50) {
  1051. return '一般';
  1052. } else {
  1053. return '需加强';
  1054. }
  1055. }
  1056. /**
  1057. * 计算推荐题目数量
  1058. */
  1059. private function calculateRecommendedQuestions(float $mastery): int
  1060. {
  1061. if ($mastery >= 0.85) {
  1062. return 1; // 巩固型:1题
  1063. } elseif ($mastery >= 0.50) {
  1064. return 2; // 修补型:2题
  1065. } else {
  1066. return 3; // 挑战型:3题
  1067. }
  1068. }
  1069. /**
  1070. * 确定重点类型
  1071. */
  1072. private function determineFocusType(float $mastery): string
  1073. {
  1074. if ($mastery >= 0.70 && $mastery < 0.85) {
  1075. return 'consolidation'; // 巩固型
  1076. } elseif ($mastery < 0.70) {
  1077. return 'remediation'; // 修补型
  1078. } else {
  1079. return 'challenge'; // 挑战型
  1080. }
  1081. }
  1082. /**
  1083. * 自动计算题目分数
  1084. * 如果用户未提供 score 和 score_obtained,则根据 is_correct 字段自动计算
  1085. *
  1086. * @param array $questions 题目列表
  1087. * @return array 处理后的题目列表
  1088. */
  1089. private function autoCalculateScores(array $questions): array
  1090. {
  1091. foreach ($questions as &$question) {
  1092. // 如果用户没有提供 score,尝试从数据库获取或使用默认值
  1093. if (!isset($question['score'])) {
  1094. $question['score'] = $this->getQuestionDefaultScore($question['question_id'] ?? '');
  1095. }
  1096. // 如果用户没有提供 score_obtained,根据 is_correct 计算
  1097. if (!isset($question['score_obtained'])) {
  1098. $question['score_obtained'] = $this->calculateScoreObtained(
  1099. $question['score'] ?? 0,
  1100. $question['is_correct'] ?? []
  1101. );
  1102. }
  1103. Log::debug('自动计算分数', [
  1104. 'question_id' => $question['question_id'],
  1105. 'default_score' => $question['score'],
  1106. 'score_obtained' => $question['score_obtained'],
  1107. 'is_correct' => $question['is_correct'] ?? []
  1108. ]);
  1109. }
  1110. return $questions;
  1111. }
  1112. /**
  1113. * 获取题目默认分数
  1114. */
  1115. private function getQuestionDefaultScore(string $questionId): float
  1116. {
  1117. if (empty($questionId)) {
  1118. return 2.0; // 默认分数
  1119. }
  1120. try {
  1121. // 尝试从题库获取题目分数
  1122. $question = DB::connection('mysql')
  1123. ->table('questions')
  1124. ->where('id', $questionId)
  1125. ->orWhere('question_code', $questionId)
  1126. ->first();
  1127. if ($question && isset($question->score)) {
  1128. return (float) $question->score;
  1129. }
  1130. // 如果没有找到,根据题目ID生成一个合理的默认分数
  1131. // 这里可以根据需要调整默认分数逻辑
  1132. return 2.0;
  1133. } catch (\Exception $e) {
  1134. Log::warning('获取题目默认分数失败,使用默认值', [
  1135. 'question_id' => $questionId,
  1136. 'error' => $e->getMessage()
  1137. ]);
  1138. return 2.0;
  1139. }
  1140. }
  1141. /**
  1142. * 根据 is_correct 数组计算得分
  1143. *
  1144. * @param float $totalScore 总分
  1145. * @param array $isCorrect 正确性数组 [0, 1, 1] 表示第1题错误,第2、3题正确
  1146. * @return float 得分
  1147. */
  1148. private function calculateScoreObtained(float $totalScore, array $isCorrect): float
  1149. {
  1150. if (empty($isCorrect)) {
  1151. return 0.0;
  1152. }
  1153. $correctCount = array_sum($isCorrect);
  1154. $totalCount = count($isCorrect);
  1155. if ($totalCount === 0) {
  1156. return 0.0;
  1157. }
  1158. // 按正确率计算得分
  1159. $scoreRatio = $correctCount / $totalCount;
  1160. return round($totalScore * $scoreRatio, 2);
  1161. }
  1162. }