ExamAnswerAnalysisService.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  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. // 1. 获取学案基准难度
  56. $examBaseDifficulty = $this->getExamBaseDifficulty($examData['paper_id'] ?? '');
  57. // 2. 保存答题记录到数据库
  58. $this->saveExamAnswerRecords($examData);
  59. // 3. 获取题目知识点映射
  60. $questionMappings = $this->getQuestionKnowledgeMappings($questions);
  61. // 4. 计算每个知识点的加权掌握度(传入学案基准难度)
  62. $knowledgeMasteryVector = $this->calculateKnowledgeMasteryVector($questions, $questionMappings, $examBaseDifficulty, $studentId);
  63. // 5. 更新学生掌握度
  64. $updatedMastery = $this->updateStudentMastery($studentId, $knowledgeMasteryVector);
  65. // 5. 生成题目维度分析
  66. $questionAnalysis = $this->analyzeQuestions($questions, $questionMappings);
  67. // 6. 生成知识点维度分析
  68. $knowledgePointAnalysis = $this->analyzeKnowledgePoints($knowledgeMasteryVector, $questionMappings);
  69. // 7. 生成整体掌握度总结
  70. $overallSummary = $this->generateOverallSummary($updatedMastery);
  71. // 8. 生成智能出卷推荐依据
  72. $smartQuizRecommendation = $this->generateSmartQuizRecommendation($updatedMastery);
  73. // 9. 保存分析结果
  74. $analysisResult = [
  75. 'paper_id' => $examData['paper_id'],
  76. 'student_id' => $studentId,
  77. 'timestamp' => now()->toISOString(),
  78. 'question_analysis' => $questionAnalysis,
  79. 'knowledge_point_analysis' => $knowledgePointAnalysis,
  80. 'overall_summary' => $overallSummary,
  81. 'smart_quiz_recommendation' => $smartQuizRecommendation,
  82. 'mastery_vector' => $updatedMastery,
  83. ];
  84. $this->saveAnalysisResult($studentId, $examData['paper_id'], $analysisResult);
  85. Log::info('考试答题分析完成', [
  86. 'student_id' => $studentId,
  87. 'paper_id' => $examData['paper_id'],
  88. 'analyzed_knowledge_points' => count($knowledgeMasteryVector)
  89. ]);
  90. return $analysisResult;
  91. }
  92. /**
  93. * 获取学案基准难度(映射为1-4级)
  94. */
  95. private function getExamBaseDifficulty(string $paperId): ?int
  96. {
  97. if (empty($paperId)) {
  98. return null;
  99. }
  100. try {
  101. // 从试卷表获取difficulty_category
  102. $paper = DB::table('papers')
  103. ->where('paper_id', $paperId)
  104. ->first();
  105. if (!$paper) {
  106. Log::warning('未找到试卷,尝试从缓存获取', ['paper_id' => $paperId]);
  107. return null;
  108. }
  109. $difficultyCategory = $paper->difficulty_category ?? '中等';
  110. // 映射为1-4级(筑基、提分、培优、竞赛)
  111. return match (strtolower($difficultyCategory)) {
  112. '筑基', 'easy', 'foundation', '1' => 1,
  113. '提分', 'medium', 'improvement', '2' => 2,
  114. '培优', 'hard', 'excellent', 'difficult', '3' => 3,
  115. '竞赛', 'competition', 'very hard', 'very difficult', '4' => 4,
  116. default => 2, // 默认提分
  117. };
  118. } catch (\Exception $e) {
  119. Log::warning('获取学案基准难度失败,使用默认2级', [
  120. 'paper_id' => $paperId,
  121. 'error' => $e->getMessage(),
  122. ]);
  123. return 2; // 默认中等难度
  124. }
  125. }
  126. /**
  127. * 获取题目知识点映射
  128. */
  129. private function getQuestionKnowledgeMappings(array $questions): array
  130. {
  131. $mappings = [];
  132. // 直接从题目数据中提取知识点信息(不再调用外部服务)
  133. foreach ($questions as $question) {
  134. $questionId = $question['question_id'] ?? null;
  135. if (!$questionId) continue;
  136. // 提取知识点信息(优先使用请求数据中的字段)
  137. $kpMapping = [];
  138. // 尝试多个可能的知识点字段
  139. $kpCode = $question['kp_code']
  140. ?? $question['knowledge_point']
  141. ?? $question['kp_code']
  142. ?? null;
  143. if (!empty($kpCode)) {
  144. $kpMapping[] = [
  145. 'kp_id' => $kpCode,
  146. 'kp_name' => $question['kp_name'] ?? $kpCode,
  147. 'weight' => 1.0
  148. ];
  149. } else {
  150. // 【修复】不允许使用默认知识点,必须明确指定
  151. Log::warning('ExamAnswerAnalysisService: 题目缺少知识点信息', [
  152. 'question_id' => $questionId,
  153. 'question' => $question
  154. ]);
  155. // 不创建默认映射,让后续处理明确报错
  156. continue;
  157. }
  158. $mappings[$questionId] = [
  159. 'question_id' => $questionId,
  160. 'kp_mapping' => $kpMapping
  161. ];
  162. }
  163. Log::info('题目知识点映射构建完成', [
  164. 'total_questions' => count($questions),
  165. 'mapped_questions' => count($mappings),
  166. ]);
  167. return $mappings;
  168. }
  169. /**
  170. * 计算知识点掌握度向量
  171. * 【修复】集成MasteryCalculator的BKT模型进行精确计算
  172. *
  173. * 核心算法说明:
  174. * 1. 从考试答题中提取每个知识点的答题记录
  175. * 2. 调用MasteryCalculator计算掌握度(包含:正确率、难度加权、时间效率、遗忘曲线)
  176. * 3. 返回包含掌握度、置信度、趋势等完整信息的向量
  177. */
  178. private function calculateKnowledgeMasteryVector(array $questions, array $questionMappings, ?int $examBaseDifficulty = null, ?string $studentId = null): array
  179. {
  180. // 按知识点聚合答题记录
  181. $knowledgeAttempts = [];
  182. foreach ($questions as $question) {
  183. $questionId = $question['question_id'];
  184. $score = floatval($question['score_obtained'] ?? 0);
  185. $maxScore = floatval($question['score'] ?? $score);
  186. $steps = $question['steps'] ?? [];
  187. $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
  188. $mapping = $questionMappings[$questionId] ?? null;
  189. if (!$mapping || !isset($mapping['kp_mapping'])) {
  190. continue;
  191. }
  192. // 构建答题记录(用于MasteryCalculator)
  193. $attemptRecord = [
  194. 'question_id' => $questionId,
  195. 'is_correct' => $isCorrect,
  196. 'partial_score' => $maxScore > 0 ? $score / $maxScore : 0,
  197. 'question_difficulty' => $question['difficulty'] ?? 0.6,
  198. 'attempt_time_seconds' => $question['time_spent'] ?? 120,
  199. 'completed_at' => now()->toISOString(),
  200. 'created_at' => now()->toISOString(),
  201. ];
  202. // 如果有步骤级分析,使用步骤分析
  203. if (!empty($steps)) {
  204. foreach ($steps as $step) {
  205. $kpId = $step['kp_id'];
  206. if (empty($kpId)) {
  207. Log::warning('ExamAnswerAnalysisService: 步骤缺少知识点ID', [
  208. 'question_id' => $questionId,
  209. 'step' => $step
  210. ]);
  211. continue;
  212. }
  213. if (!isset($knowledgeAttempts[$kpId])) {
  214. $knowledgeAttempts[$kpId] = [
  215. 'attempts' => [],
  216. 'step_details' => [],
  217. ];
  218. }
  219. // 每个步骤作为独立答题记录
  220. $stepAttempt = $attemptRecord;
  221. $stepAttempt['is_correct'] = $step['is_correct'];
  222. $stepAttempt['step_index'] = $step['step_index'];
  223. $knowledgeAttempts[$kpId]['attempts'][] = $stepAttempt;
  224. $knowledgeAttempts[$kpId]['step_details'][] = [
  225. 'question_id' => $questionId,
  226. 'step_index' => $step['step_index'],
  227. 'score' => $step['score'] ?? ($maxScore / count($steps)),
  228. 'is_correct' => $step['is_correct'],
  229. ];
  230. }
  231. } else {
  232. // 题目整体分析
  233. foreach ($mapping['kp_mapping'] as $kpMapping) {
  234. $kpId = $kpMapping['kp_id'];
  235. if (!isset($knowledgeAttempts[$kpId])) {
  236. $knowledgeAttempts[$kpId] = [
  237. 'attempts' => [],
  238. 'step_details' => [],
  239. ];
  240. }
  241. $knowledgeAttempts[$kpId]['attempts'][] = $attemptRecord;
  242. $knowledgeAttempts[$kpId]['step_details'][] = [
  243. 'question_id' => $questionId,
  244. 'score' => $score,
  245. 'max_score' => $maxScore,
  246. 'is_correct' => $isCorrect,
  247. ];
  248. }
  249. }
  250. }
  251. // 【核心】使用MasteryCalculator计算每个知识点的掌握度
  252. $masteryVector = [];
  253. foreach ($knowledgeAttempts as $kpId => $data) {
  254. $attempts = $data['attempts'];
  255. // 调用MasteryCalculator的核心算法(传入学案基准难度)
  256. // 该算法包含:正确率、难度加权、时间效率、技能熟练度、遗忘曲线衰减
  257. $masteryResult = $this->masteryCalculator->calculateMasteryLevel(
  258. $studentId ?? '', // 传递学生ID,用于保存掌握度到数据库
  259. $kpId,
  260. $attempts,
  261. $examBaseDifficulty
  262. );
  263. $masteryVector[$kpId] = [
  264. 'kp_id' => $kpId,
  265. 'mastery' => $masteryResult['mastery'],
  266. 'confidence' => $masteryResult['confidence'],
  267. 'trend' => $masteryResult['trend'],
  268. 'total_attempts' => $masteryResult['total_attempts'],
  269. 'correct_attempts' => $masteryResult['correct_attempts'],
  270. 'accuracy_rate' => $masteryResult['accuracy_rate'],
  271. 'step_details' => $data['step_details'],
  272. // 计算细节(用于调试和分析)
  273. 'calculation_details' => $masteryResult['details'] ?? [],
  274. ];
  275. }
  276. Log::info('知识点掌握度向量计算完成', [
  277. 'knowledge_points_count' => count($masteryVector),
  278. 'sample' => array_slice($masteryVector, 0, 3, true),
  279. ]);
  280. return $masteryVector;
  281. }
  282. /**
  283. * 更新学生掌握度(与历史数据合并)
  284. */
  285. private function updateStudentMastery(string $studentId, array $knowledgeMasteryVector): array
  286. {
  287. $updatedMastery = [];
  288. foreach ($knowledgeMasteryVector as $kpId => $data) {
  289. // 获取历史掌握度
  290. $historyMastery = DB::connection('mysql')
  291. ->table('student_knowledge_mastery')
  292. ->where('student_id', $studentId)
  293. ->where('kp_code', $kpId)
  294. ->first();
  295. $historyMasteryLevel = $historyMastery->mastery_level ?? 0.5;
  296. $historyWeight = $historyMastery->total_attempts ?? 0;
  297. $currentWeight = $data['total_attempts'] ?? 1;
  298. // 合并计算:历史权重 + 当前权重
  299. $newMastery = $historyWeight > 0
  300. ? ($historyWeight * $historyMasteryLevel + $currentWeight * $data['mastery'])
  301. / ($historyWeight + $currentWeight)
  302. : $data['mastery'];
  303. $newConfidence = $data['confidence'];
  304. // 保存到数据库
  305. DB::connection('mysql')
  306. ->table('student_knowledge_mastery')
  307. ->updateOrInsert(
  308. ['student_id' => $studentId, 'kp_code' => $kpId],
  309. [
  310. 'mastery_level' => $newMastery,
  311. 'confidence_level' => $newConfidence,
  312. 'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
  313. 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_attempts'] > 0),
  314. 'mastery_trend' => $this->determineMasteryTrend($historyMasteryLevel, $newMastery),
  315. 'last_mastery_update' => now(),
  316. 'updated_at' => now(),
  317. ]
  318. );
  319. $updatedMastery[$kpId] = [
  320. 'kp_id' => $kpId,
  321. 'current_mastery' => $newMastery,
  322. 'previous_mastery' => $historyMasteryLevel,
  323. 'confidence' => $newConfidence,
  324. 'change' => $newMastery - $historyMasteryLevel,
  325. 'weight' => $currentWeight
  326. ];
  327. }
  328. return $updatedMastery;
  329. }
  330. /**
  331. * 生成题目维度分析(包含AI分析和解题思路)
  332. */
  333. private function analyzeQuestions(array $questions, array $questionMappings): array
  334. {
  335. $analysis = [];
  336. foreach ($questions as $question) {
  337. $questionId = $question['question_id'];
  338. $score = floatval($question['score_obtained'] ?? 0);
  339. $maxScore = floatval($question['score'] ?? $score);
  340. $steps = $question['steps'] ?? [];
  341. $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
  342. $mapping = $questionMappings[$questionId] ?? ['kp_mapping' => []];
  343. if (empty($mapping['kp_mapping'])) {
  344. Log::warning('ExamAnswerAnalysisService: 题目无知识点映射', ['question_id' => $questionId]);
  345. continue;
  346. }
  347. $kpCode = $mapping['kp_mapping'][0]['kp_id'];
  348. // 步骤分析
  349. $stepAnalysis = [];
  350. if (!empty($steps)) {
  351. foreach ($steps as $step) {
  352. $kpId = $step['kp_id'];
  353. if (empty($kpId)) {
  354. Log::warning('ExamAnswerAnalysisService: 步骤缺少知识点ID', [
  355. 'question_id' => $questionId,
  356. 'step_index' => $step['step_index'] ?? 'unknown'
  357. ]);
  358. continue;
  359. }
  360. $stepAnalysis[] = [
  361. 'step_index' => $step['step_index'],
  362. 'is_correct' => $step['is_correct'],
  363. 'kp_id' => $kpId,
  364. 'description' => $step['description'] ?? ''
  365. ];
  366. }
  367. }
  368. // 知识点关联
  369. $knowledgePoints = array_map(function($kp) {
  370. return [
  371. 'kp_id' => $kp['kp_id'],
  372. 'kp_name' => $kp['kp_name'] ?? $kp['kp_id'],
  373. 'weight' => $kp['weight'] ?? 1.0
  374. ];
  375. }, $mapping['kp_mapping']);
  376. // 【集成】调用AI分析服务,获取解题思路和错误分析
  377. $aiAnalysis = $this->getQuestionAIAnalysis($question, $mapping);
  378. $analysis[] = [
  379. 'question_id' => $questionId,
  380. 'score_obtained' => $score,
  381. 'max_score' => $maxScore,
  382. 'accuracy_rate' => $maxScore > 0 ? $score / $maxScore : 0,
  383. 'is_correct' => $isCorrect,
  384. 'step_analysis' => $stepAnalysis,
  385. 'knowledge_points' => $knowledgePoints,
  386. 'performance_summary' => $this->generateQuestionPerformanceSummary($question, $stepAnalysis),
  387. // 【新增】解题思路和错误分析
  388. 'solution_process' => $aiAnalysis['solution_process'] ?? '',
  389. 'error_analysis' => $aiAnalysis['error_analysis'] ?? '',
  390. 'mistake_type' => $aiAnalysis['mistake_type'] ?? '',
  391. 'suggestions' => $aiAnalysis['suggestions'] ?? '',
  392. 'next_steps' => $aiAnalysis['next_steps'] ?? [],
  393. ];
  394. }
  395. return $analysis;
  396. }
  397. /**
  398. * 获取题目的AI分析(解题思路、错误分析)
  399. */
  400. private function getQuestionAIAnalysis(array $question, array $mapping): array
  401. {
  402. $isCorrect = $question['is_correct'] ?? false;
  403. $score = floatval($question['score_obtained'] ?? 0);
  404. $maxScore = floatval($question['score'] ?? 10);
  405. $kpCode = $mapping['kp_mapping'][0]['kp_id'] ?? null;
  406. if (empty($kpCode)) {
  407. Log::warning('ExamAnswerAnalysisService: getQuestionAIAnalysis缺少知识点ID', [
  408. 'question_id' => $question['question_id'] ?? 'unknown',
  409. 'mapping' => $mapping
  410. ]);
  411. $kpCode = 'UNKNOWN_KP';
  412. }
  413. // 调用LocalAIAnalysisService进行分析
  414. try {
  415. $analysisResult = $this->aiAnalysisService->analyzeAnswer([
  416. 'question_id' => $question['question_id'],
  417. 'question_text' => $question['question_text'] ?? '',
  418. 'student_answer' => $question['student_answer'] ?? '',
  419. 'correct_answer' => $question['correct_answer'] ?? '',
  420. 'score' => $score,
  421. 'max_score' => $maxScore,
  422. 'kp_code' => $kpCode,
  423. ]);
  424. $data = $analysisResult['data'] ?? [];
  425. // 根据正确性生成不同的解题思路
  426. if ($isCorrect) {
  427. return [
  428. 'solution_process' => $data['correct_solution'] ?? '该题作答正确,解题思路清晰',
  429. 'error_analysis' => '',
  430. 'mistake_type' => '',
  431. 'suggestions' => $data['suggestions'] ?? '继续保持良好的解题习惯',
  432. 'next_steps' => $data['next_steps'] ?? ['尝试更高难度的同类题目'],
  433. ];
  434. }
  435. // 错误题目:返回详细分析
  436. return [
  437. 'solution_process' => $data['correct_solution'] ?? '请参考标准解题步骤',
  438. 'error_analysis' => $data['reason'] ?? '解题过程中存在错误',
  439. 'mistake_type' => $data['mistake_type'] ?? '计算或理解错误',
  440. 'suggestions' => $data['suggestions'] ?? '建议针对薄弱知识点进行专项练习',
  441. 'next_steps' => $data['next_steps'] ?? ['复习相关知识点', '做同类型练习题'],
  442. ];
  443. } catch (\Exception $e) {
  444. Log::warning('AI分析失败,使用默认分析', [
  445. 'question_id' => $question['question_id'],
  446. 'error' => $e->getMessage(),
  447. ]);
  448. // 回退到基础分析
  449. return $this->getFallbackAnalysis($question, $isCorrect);
  450. }
  451. }
  452. /**
  453. * 回退分析(当AI分析失败时)
  454. */
  455. private function getFallbackAnalysis(array $question, bool $isCorrect): array
  456. {
  457. if ($isCorrect) {
  458. return [
  459. 'solution_process' => '该题作答正确',
  460. 'error_analysis' => '',
  461. 'mistake_type' => '',
  462. 'suggestions' => '继续保持',
  463. 'next_steps' => ['尝试更高难度的题目'],
  464. ];
  465. }
  466. $scoreRatio = floatval($question['score_obtained'] ?? 0) / max(floatval($question['score'] ?? 1), 1);
  467. return [
  468. 'solution_process' => '请参考标准答案和解题步骤',
  469. 'error_analysis' => $scoreRatio < 0.3 ? '知识点理解存在偏差' : '解题过程中出现错误',
  470. 'mistake_type' => $scoreRatio < 0.3 ? '概念错误' : '计算/步骤错误',
  471. 'suggestions' => '建议复习相关知识点,加强练习',
  472. 'next_steps' => ['复习基础概念', '做同类型练习题', '请教老师或同学'],
  473. ];
  474. }
  475. /**
  476. * 生成知识点维度分析
  477. */
  478. private function analyzeKnowledgePoints(array $knowledgeMasteryVector, array $questionMappings): array
  479. {
  480. $analysis = [];
  481. foreach ($knowledgeMasteryVector as $kpId => $data) {
  482. $analysis[] = [
  483. 'kp_id' => $kpId,
  484. 'mastery_level' => $data['mastery'],
  485. 'confidence_level' => $data['confidence'],
  486. 'performance_in_exam' => $this->evaluatePerformanceLevel($data['mastery']),
  487. 'evidence_count' => count($data['step_details']),
  488. 'step_evidence' => $data['step_details'],
  489. 'recommendation' => $this->generateKnowledgePointRecommendation($data)
  490. ];
  491. }
  492. return $analysis;
  493. }
  494. /**
  495. * 生成整体掌握度总结
  496. */
  497. private function generateOverallSummary(array $updatedMastery): array
  498. {
  499. $knowledgePoints = array_values($updatedMastery);
  500. if (empty($knowledgePoints)) {
  501. return [
  502. 'total_knowledge_points' => 0,
  503. 'average_mastery' => 0,
  504. 'mastery_distribution' => [
  505. 'mastered' => 0,
  506. 'good' => 0,
  507. 'weak' => 0
  508. ],
  509. 'top_strengths' => [],
  510. 'top_weaknesses' => []
  511. ];
  512. }
  513. // 计算平均掌握度
  514. $averageMastery = array_sum(array_column($knowledgePoints, 'current_mastery')) / count($knowledgePoints);
  515. // 掌握度分布
  516. $mastered = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] >= 0.85);
  517. $good = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] >= 0.70 && $kp['current_mastery'] < 0.85);
  518. $weak = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] < 0.70);
  519. // 排序找出优势和薄弱点
  520. usort($knowledgePoints, fn($a, $b) => $b['current_mastery'] <=> $a['current_mastery']);
  521. $topStrengths = array_slice($knowledgePoints, 0, 3);
  522. $topWeaknesses = array_slice(array_reverse($knowledgePoints), 0, 3);
  523. return [
  524. 'total_knowledge_points' => count($knowledgePoints),
  525. 'average_mastery' => round($averageMastery, 4),
  526. 'mastery_distribution' => [
  527. 'mastered' => count($mastered),
  528. 'good' => count($good),
  529. 'weak' => count($weak)
  530. ],
  531. 'top_strengths' => $topStrengths,
  532. 'top_weaknesses' => $topWeaknesses,
  533. 'overall_performance' => $this->evaluateOverallPerformance($averageMastery)
  534. ];
  535. }
  536. /**
  537. * 生成智能出卷推荐依据
  538. * 基于文档中的推荐优先级算法
  539. */
  540. private function generateSmartQuizRecommendation(array $updatedMastery): array
  541. {
  542. $recommendations = [];
  543. foreach ($updatedMastery as $kpId => $data) {
  544. $mastery = $data['current_mastery'];
  545. $confidence = $data['confidence'];
  546. $weight = $data['weight'];
  547. // 推荐优先级 = (1 - 掌握度) * 重要性 * 覆盖需求
  548. // 重要性可以根据知识点在中考/阶段考试中的权重,这里简化为1.0
  549. $importance = 1.0;
  550. // 覆盖需求:最近没考过或考得少,值大
  551. $coverageNeed = max(1.0, 1.5 - ($weight / 10));
  552. $priority = (1 - $mastery) * $importance * $coverageNeed;
  553. $recommendations[] = [
  554. 'kp_id' => $kpId,
  555. 'current_mastery' => $mastery,
  556. 'priority' => $priority,
  557. 'recommended_questions' => $this->calculateRecommendedQuestions($mastery),
  558. 'focus_type' => $this->determineFocusType($mastery)
  559. ];
  560. }
  561. // 按优先级排序
  562. usort($recommendations, fn($a, $b) => $b['priority'] <=> $a['priority']);
  563. // 控制难度节奏:40%巩固型 + 40%修补型 + 20%挑战型
  564. $totalRecommendations = count($recommendations);
  565. $consolidation = array_slice($recommendations, 0, intval($totalRecommendations * 0.4));
  566. $remediation = array_slice($recommendations, intval($totalRecommendations * 0.4), intval($totalRecommendations * 0.4));
  567. $challenge = array_slice($recommendations, intval($totalRecommendations * 0.8));
  568. return [
  569. 'priority_list' => $recommendations,
  570. 'quiz_structure' => [
  571. 'consolidation_type' => $consolidation,
  572. 'remediation_type' => $remediation,
  573. 'challenge_type' => $challenge
  574. ],
  575. 'total_recommended_questions' => array_sum(array_column($recommendations, 'recommended_questions'))
  576. ];
  577. }
  578. /**
  579. * 保存考试答题记录
  580. */
  581. private function saveExamAnswerRecords(array $examData): void
  582. {
  583. $studentId = $examData['student_id'];
  584. $examId = $examData['paper_id'];
  585. // 【修复】先清理该考试的所有答题记录(支持重复考试)
  586. DB::connection('mysql')->table('student_answer_questions')
  587. ->where('student_id', $studentId)
  588. ->where('exam_id', $examId)
  589. ->delete();
  590. DB::connection('mysql')->table('student_answer_steps')
  591. ->where('student_id', $studentId)
  592. ->where('exam_id', $examId)
  593. ->delete();
  594. foreach ($examData['questions'] as $question) {
  595. $questionId = $question['question_id'];
  596. $steps = $question['steps'] ?? [];
  597. // 保存步骤级记录
  598. if (!empty($steps)) {
  599. foreach ($steps as $step) {
  600. $kpId = $step['kp_id'] ?? null;
  601. if (empty($kpId)) {
  602. Log::warning('ExamAnswerAnalysisService: 步骤保存缺少知识点ID', [
  603. 'student_id' => $studentId,
  604. 'exam_id' => $examId,
  605. 'question_id' => $questionId,
  606. 'step_index' => $step['step_index'] ?? 'unknown'
  607. ]);
  608. continue;
  609. }
  610. DB::connection('mysql')->table('student_answer_steps')->insert([
  611. 'student_id' => $studentId,
  612. 'exam_id' => $examId,
  613. 'question_id' => $questionId,
  614. 'step_index' => $step['step_index'],
  615. 'kp_id' => $kpId,
  616. 'is_correct' => $step['is_correct'],
  617. 'step_score' => $step['score'] ?? 0,
  618. 'created_at' => now(),
  619. 'updated_at' => now(),
  620. ]);
  621. }
  622. } else {
  623. // 保存题目级记录
  624. try {
  625. DB::connection('mysql')->table('student_answer_questions')->insertOrIgnore([
  626. 'student_id' => $studentId,
  627. 'exam_id' => $examId,
  628. 'question_id' => $questionId,
  629. 'score_obtained' => $question['score_obtained'] ?? 0,
  630. 'max_score' => $question['score'] ?? 0,
  631. 'created_at' => now(),
  632. 'updated_at' => now(),
  633. ]);
  634. } catch (\Exception $e) {
  635. Log::warning('保存答题记录失败', [
  636. 'student_id' => $studentId,
  637. 'exam_id' => $examId,
  638. 'question_id' => $questionId,
  639. 'error' => $e->getMessage(),
  640. ]);
  641. }
  642. }
  643. }
  644. Log::info('答题记录保存完成', [
  645. 'student_id' => $studentId,
  646. 'exam_id' => $examId,
  647. 'question_count' => count($examData['questions']),
  648. ]);
  649. }
  650. /**
  651. * 保存分析结果并创建掌握度快照
  652. */
  653. private function saveAnalysisResult(string $studentId, string $paperId, array $result): void
  654. {
  655. // 【修复】支持重复分析:先删除旧的分析结果
  656. DB::connection('mysql')->table('exam_analysis_results')
  657. ->where('student_id', $studentId)
  658. ->where('paper_id', $paperId)
  659. ->delete();
  660. // 插入新的分析结果
  661. DB::connection('mysql')->table('exam_analysis_results')->insert([
  662. 'student_id' => $studentId,
  663. 'paper_id' => $paperId,
  664. 'analysis_data' => json_encode($result),
  665. 'created_at' => now(),
  666. 'updated_at' => now(),
  667. ]);
  668. Log::info('分析结果保存完成', [
  669. 'student_id' => $studentId,
  670. 'paper_id' => $paperId,
  671. 'data_size' => strlen(json_encode($result)),
  672. ]);
  673. // 【集成】创建知识点掌握度快照
  674. $this->createMasterySnapshot($studentId, $paperId, $result);
  675. // 【新增】异步生成学情分析PDF
  676. try {
  677. Log::info('开始异步生成学情分析PDF', [
  678. 'student_id' => $studentId,
  679. 'paper_id' => $paperId,
  680. ]);
  681. // 使用队列异步生成PDF,避免阻塞主流程
  682. dispatch(new \App\Jobs\GenerateAnalysisPdfJob($paperId, $studentId, null));
  683. Log::info('PDF生成任务已加入队列', [
  684. 'student_id' => $studentId,
  685. 'paper_id' => $paperId,
  686. ]);
  687. } catch (\Exception $e) {
  688. Log::error('PDF生成任务加入队列失败', [
  689. 'student_id' => $studentId,
  690. 'paper_id' => $paperId,
  691. 'error' => $e->getMessage(),
  692. ]);
  693. }
  694. }
  695. /**
  696. * 创建知识点掌握度快照
  697. * 【集成】使用LocalAIAnalysisService的快照功能
  698. *
  699. * 快照用途:
  700. * 1. 追踪学生掌握度变化趋势
  701. * 2. 生成学情报告时对比历史数据
  702. * 3. 为智能出卷提供决策依据
  703. */
  704. private function createMasterySnapshot(string $studentId, string $paperId, array $analysisResult): void
  705. {
  706. try {
  707. // 计算快照数据
  708. $masteryVector = $analysisResult['mastery_vector'] ?? [];
  709. $overallMastery = 0;
  710. $weakCount = 0;
  711. $strongCount = 0;
  712. foreach ($masteryVector as $kpData) {
  713. $mastery = $kpData['current_mastery'] ?? $kpData['mastery'] ?? 0;
  714. $overallMastery += $mastery;
  715. if ($mastery < 0.6) {
  716. $weakCount++;
  717. } elseif ($mastery >= 0.85) {
  718. $strongCount++;
  719. }
  720. }
  721. $kpCount = count($masteryVector);
  722. $overallMastery = $kpCount > 0 ? round($overallMastery / $kpCount, 4) : 0;
  723. // 生成快照ID
  724. $snapshotId = 'snap_' . $paperId . '_' . now()->format('YmdHis');
  725. // 保存到快照表
  726. DB::connection('mysql')->table('knowledge_point_mastery_snapshots')->insert([
  727. 'snapshot_id' => $snapshotId,
  728. 'student_id' => $studentId,
  729. 'paper_id' => $paperId,
  730. 'answer_record_id' => null,
  731. 'mastery_data' => json_encode($masteryVector),
  732. 'overall_mastery' => $overallMastery,
  733. 'weak_knowledge_points_count' => $weakCount,
  734. 'strong_knowledge_points_count' => $strongCount,
  735. 'snapshot_time' => now(),
  736. 'analysis_id' => null,
  737. 'created_at' => now(),
  738. 'updated_at' => now(),
  739. ]);
  740. Log::info('掌握度快照创建成功', [
  741. 'snapshot_id' => $snapshotId,
  742. 'student_id' => $studentId,
  743. 'paper_id' => $paperId,
  744. 'overall_mastery' => $overallMastery,
  745. 'weak_count' => $weakCount,
  746. 'strong_count' => $strongCount,
  747. ]);
  748. } catch (\Exception $e) {
  749. // 快照创建失败不影响主流程
  750. Log::warning('掌握度快照创建失败', [
  751. 'student_id' => $studentId,
  752. 'paper_id' => $paperId,
  753. 'error' => $e->getMessage(),
  754. ]);
  755. }
  756. }
  757. /**
  758. * 判断掌握度趋势
  759. */
  760. private function determineMasteryTrend(float $previous, float $current): string
  761. {
  762. $change = $current - $previous;
  763. if ($change > 0.1) {
  764. return 'improving';
  765. } elseif ($change < -0.1) {
  766. return 'declining';
  767. } else {
  768. return 'stable';
  769. }
  770. }
  771. /**
  772. * 评估表现水平
  773. */
  774. private function evaluatePerformanceLevel(float $mastery): string
  775. {
  776. if ($mastery >= 0.85) {
  777. return 'excellent';
  778. } elseif ($mastery >= 0.70) {
  779. return 'good';
  780. } elseif ($mastery >= 0.50) {
  781. return 'fair';
  782. } else {
  783. return 'poor';
  784. }
  785. }
  786. /**
  787. * 生成题目表现总结
  788. */
  789. private function generateQuestionPerformanceSummary(array $question, array $stepAnalysis): string
  790. {
  791. if (empty($stepAnalysis)) {
  792. return '整题作答';
  793. }
  794. $correctSteps = count(array_filter($stepAnalysis, fn($s) => $s['is_correct']));
  795. $totalSteps = count($stepAnalysis);
  796. if ($correctSteps === $totalSteps) {
  797. return '所有步骤正确';
  798. } elseif ($correctSteps > 0) {
  799. return "部分正确 ({$correctSteps}/{$totalSteps} 步骤正确)";
  800. } else {
  801. return '所有步骤错误';
  802. }
  803. }
  804. /**
  805. * 生成知识点建议
  806. */
  807. private function generateKnowledgePointRecommendation(array $data): string
  808. {
  809. $mastery = $data['mastery'];
  810. if ($mastery >= 0.85) {
  811. return '掌握良好,可安排综合练习';
  812. } elseif ($mastery >= 0.70) {
  813. return '基本掌握,建议加强练习';
  814. } elseif ($mastery >= 0.50) {
  815. return '需要重点练习,建议安排专项训练';
  816. } else {
  817. return '薄弱知识点,建议系统学习和大量练习';
  818. }
  819. }
  820. /**
  821. * 评估整体表现
  822. */
  823. private function evaluateOverallPerformance(float $averageMastery): string
  824. {
  825. if ($averageMastery >= 0.85) {
  826. return '优秀';
  827. } elseif ($averageMastery >= 0.70) {
  828. return '良好';
  829. } elseif ($averageMastery >= 0.50) {
  830. return '一般';
  831. } else {
  832. return '需加强';
  833. }
  834. }
  835. /**
  836. * 计算推荐题目数量
  837. */
  838. private function calculateRecommendedQuestions(float $mastery): int
  839. {
  840. if ($mastery >= 0.85) {
  841. return 1; // 巩固型:1题
  842. } elseif ($mastery >= 0.50) {
  843. return 2; // 修补型:2题
  844. } else {
  845. return 3; // 挑战型:3题
  846. }
  847. }
  848. /**
  849. * 确定重点类型
  850. */
  851. private function determineFocusType(float $mastery): string
  852. {
  853. if ($mastery >= 0.70 && $mastery < 0.85) {
  854. return 'consolidation'; // 巩固型
  855. } elseif ($mastery < 0.70) {
  856. return 'remediation'; // 修补型
  857. } else {
  858. return 'challenge'; // 挑战型
  859. }
  860. }
  861. }