StudentAnswerAnalysisService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <?php
  2. namespace App\Services;
  3. use App\Models\StudentExercise;
  4. use App\Models\MistakeRecord;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Log;
  7. use Illuminate\Support\Str;
  8. /**
  9. * 学生作答分析服务
  10. * 负责处理学生作答结果的保存、分析和掌握度更新
  11. */
  12. class StudentAnswerAnalysisService
  13. {
  14. public function __construct(
  15. private readonly LocalAIAnalysisService $aiAnalysisService,
  16. private readonly ExamAnswerAnalysisService $examAnswerAnalysisService
  17. ) {}
  18. /**
  19. * 保存作答记录
  20. */
  21. public function saveAnswerRecord(array $data): array
  22. {
  23. // 生成唯一记录ID
  24. $recordId = 'ans_' . Str::uuid()->toString();
  25. // 计算统计数据
  26. $answers = $data['answers'];
  27. $correctCount = 0;
  28. $wrongCount = 0;
  29. $totalScore = 0;
  30. $obtainedScore = 0;
  31. foreach ($answers as $answer) {
  32. $score = (float) ($answer['score'] ?? 0);
  33. $maxScore = (float) ($answer['max_score'] ?? $score);
  34. $totalScore += $maxScore;
  35. $obtainedScore += $score;
  36. if ($answer['is_correct']) {
  37. $correctCount++;
  38. } else {
  39. $wrongCount++;
  40. }
  41. }
  42. $accuracyRate = ($correctCount + $wrongCount) > 0
  43. ? round($correctCount / ($correctCount + $wrongCount), 4)
  44. : 0;
  45. // 保存到 student_exercises 表
  46. foreach ($answers as $answer) {
  47. StudentExercise::create([
  48. 'student_id' => $data['student_id'],
  49. 'question_id' => $answer['question_id'],
  50. 'question_content' => json_encode([
  51. 'question_id' => $answer['question_id'],
  52. 'question_number' => $answer['question_number'] ?? null,
  53. 'paper_id' => $data['paper_id'],
  54. ]),
  55. 'student_answer' => $answer['student_answer'] ?? '',
  56. 'correct_answer' => $answer['correct_answer'] ?? '',
  57. 'is_correct' => $answer['is_correct'],
  58. 'submission_status' => 'completed',
  59. 'kp_code' => $answer['knowledge_point'] ?? null,
  60. 'difficulty_level' => 0.5, // 默认难度
  61. 'time_spent_seconds' => 0, // 默认耗时
  62. 'created_at' => $data['answer_time'] ?? now(),
  63. 'updated_at' => now(),
  64. ]);
  65. // 保存错题记录
  66. if (!$answer['is_correct']) {
  67. $this->saveMistakeRecord($data, $answer);
  68. }
  69. }
  70. return [
  71. 'record_id' => $recordId,
  72. 'paper_id' => $data['paper_id'],
  73. 'student_id' => $data['student_id'],
  74. 'total_score' => $totalScore,
  75. 'obtained_score' => $obtainedScore,
  76. 'accuracy_rate' => $accuracyRate,
  77. 'correct_count' => $correctCount,
  78. 'wrong_count' => $wrongCount,
  79. 'total_questions' => count($answers),
  80. ];
  81. }
  82. /**
  83. * 保存错题记录
  84. */
  85. private function saveMistakeRecord(array $data, array $answer): void
  86. {
  87. try {
  88. // 检查是否已存在相同的错题记录
  89. $existing = MistakeRecord::where('student_id', $data['student_id'])
  90. ->where('question_id', $answer['question_id'])
  91. ->first();
  92. if ($existing) {
  93. // 更新现有记录
  94. $existing->increment('review_count');
  95. $existing->update([
  96. 'student_answer' => $answer['student_answer'] ?? '',
  97. 'correct_answer' => $answer['correct_answer'] ?? '',
  98. 'updated_at' => now(),
  99. ]);
  100. } else {
  101. // 创建新记录
  102. MistakeRecord::create([
  103. 'student_id' => $data['student_id'],
  104. 'question_id' => $answer['question_id'],
  105. 'source' => MistakeRecord::SOURCE_EXAM,
  106. 'question_text' => json_encode([
  107. 'question_number' => $answer['question_number'] ?? null,
  108. 'paper_id' => $data['paper_id'],
  109. 'question_type' => $answer['question_type'] ?? null,
  110. ]),
  111. 'student_answer' => $answer['student_answer'] ?? '',
  112. 'correct_answer' => $answer['correct_answer'] ?? '',
  113. 'knowledge_point' => $answer['knowledge_point'] ?? null,
  114. 'error_type' => $this->guessErrorType($answer),
  115. 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING,
  116. 'review_count' => 0,
  117. 'force_review' => false,
  118. 'is_favorite' => false,
  119. 'in_retry_list' => false,
  120. 'difficulty' => 0.5,
  121. 'mastery_level' => 0.0,
  122. ]);
  123. }
  124. } catch (\Exception $e) {
  125. Log::warning('保存错题记录失败', [
  126. 'student_id' => $data['student_id'],
  127. 'question_id' => $answer['question_id'],
  128. 'error' => $e->getMessage(),
  129. ]);
  130. }
  131. }
  132. /**
  133. * 猜测错误类型
  134. */
  135. private function guessErrorType(array $answer): string
  136. {
  137. // 根据题型和答案特征猜测错误类型
  138. $questionType = $answer['question_type'] ?? '';
  139. if ($questionType === 'choice') {
  140. return MistakeRecord::ERROR_TYPE_CARELESS;
  141. }
  142. if ($questionType === 'fill' || $questionType === 'answer') {
  143. // 检查是否部分正确
  144. if (isset($answer['step_scores']) && is_array($answer['step_scores'])) {
  145. $totalSteps = count($answer['step_scores']);
  146. $correctSteps = array_sum($answer['step_scores']);
  147. if ($correctSteps > 0 && $correctSteps < $totalSteps) {
  148. return MistakeRecord::ERROR_TYPE_PROCEDURE ?? MistakeRecord::ERROR_TYPE_CALCULATION;
  149. }
  150. }
  151. }
  152. return MistakeRecord::ERROR_TYPE_OTHER;
  153. }
  154. /**
  155. * 保存分析结果
  156. */
  157. public function saveAnalysisResults(array $answerRecord, array $analysisData, array $questionAnalyses): void
  158. {
  159. try {
  160. // 生成分析ID
  161. $analysisId = 'analysis_' . Str::uuid()->toString();
  162. // 保存分析记录到 PostgreSQL
  163. DB::connection('pgsql')->table('answer_analysis_records')->insert([
  164. 'analysis_id' => $analysisId,
  165. 'exam_id' => $answerRecord['paper_id'],
  166. 'student_id' => $answerRecord['student_id'],
  167. 'ocr_record_id' => 0, // 如果是系统试卷,没有OCR记录
  168. 'status' => 'completed',
  169. 'analysis_results' => json_encode($analysisData),
  170. 'completed_at' => now(),
  171. 'created_at' => now(),
  172. 'updated_at' => now(),
  173. ]);
  174. // 获取分析记录的ID
  175. $analysisRecordId = DB::connection('pgsql')
  176. ->table('answer_analysis_records')
  177. ->where('analysis_id', $analysisId)
  178. ->value('id');
  179. // 保存每道题的分析结果
  180. foreach ($questionAnalyses as $questionAnalysis) {
  181. DB::connection('pgsql')->table('question_analysis_results')->insert([
  182. 'analysis_record_id' => $analysisRecordId,
  183. 'question_id' => $questionAnalysis['question_id'],
  184. 'question_number' => $questionAnalysis['question_number'] ?? null,
  185. 'kp_code' => $questionAnalysis['kp_code'] ?? null,
  186. 'student_answer' => $questionAnalysis['student_answer'] ?? '',
  187. 'correct_answer' => $questionAnalysis['correct_answer'] ?? '',
  188. 'is_correct' => $questionAnalysis['is_correct'] ?? false,
  189. 'score_obtained' => $questionAnalysis['score_obtained'] ?? 0,
  190. 'max_score' => $questionAnalysis['max_score'] ?? 0,
  191. 'ai_analysis' => $questionAnalysis['ai_analysis'] ?? null,
  192. 'learning_suggestions' => json_encode($questionAnalysis['suggestions'] ?? []),
  193. 'created_at' => now(),
  194. 'updated_at' => now(),
  195. ]);
  196. // 更新掌握度
  197. if (!empty($questionAnalysis['kp_code'])) {
  198. $this->updateMasteryForQuestion(
  199. $answerRecord['student_id'],
  200. $questionAnalysis['kp_code'],
  201. $questionAnalysis['is_correct'],
  202. $questionAnalysis['difficulty'] ?? 0.5
  203. );
  204. }
  205. }
  206. Log::info('分析结果已保存', [
  207. 'student_id' => $answerRecord['student_id'],
  208. 'paper_id' => $answerRecord['paper_id'],
  209. 'analysis_id' => $analysisId,
  210. 'question_count' => count($questionAnalyses),
  211. ]);
  212. } catch (\Exception $e) {
  213. Log::error('保存分析结果失败', [
  214. 'student_id' => $answerRecord['student_id'],
  215. 'paper_id' => $answerRecord['paper_id'],
  216. 'error' => $e->getMessage(),
  217. ]);
  218. }
  219. }
  220. /**
  221. * 为单个题目更新掌握度
  222. */
  223. private function updateMasteryForQuestion(string $studentId, string $kpCode, bool $isCorrect, float $difficulty): void
  224. {
  225. try {
  226. // 获取当前掌握度
  227. $currentMastery = 0.5; // 默认值
  228. $existingMastery = DB::connection('pgsql')
  229. ->table('student_knowledge_mastery')
  230. ->where('student_id', $studentId)
  231. ->where('kp_code', $kpCode)
  232. ->first();
  233. if ($existingMastery) {
  234. $currentMastery = (float) $existingMastery->mastery_level;
  235. }
  236. // 使用AI分析服务更新掌握度
  237. $result = $this->aiAnalysisService->updateMastery(
  238. $studentId,
  239. $kpCode,
  240. $currentMastery,
  241. $isCorrect,
  242. $difficulty
  243. );
  244. Log::debug('掌握度已更新', [
  245. 'student_id' => $studentId,
  246. 'kp_code' => $kpCode,
  247. 'old_mastery' => $result['old_mastery'],
  248. 'new_mastery' => $result['new_mastery'],
  249. 'change' => $result['change'],
  250. ]);
  251. } catch (\Exception $e) {
  252. Log::warning('更新掌握度失败', [
  253. 'student_id' => $studentId,
  254. 'kp_code' => $kpCode,
  255. 'error' => $e->getMessage(),
  256. ]);
  257. }
  258. }
  259. /**
  260. * 创建掌握度快照
  261. */
  262. public function createMasterySnapshot(string $studentId, ?string $paperId = null, ?string $answerRecordId = null): ?array
  263. {
  264. // 使用AI分析服务创建快照
  265. return $this->aiAnalysisService->createMasterySnapshot($studentId, $paperId, $answerRecordId);
  266. }
  267. /**
  268. * 获取学生的学习历史
  269. */
  270. public function getStudentLearningHistory(string $studentId, int $limit = 10): array
  271. {
  272. try {
  273. $exercises = StudentExercise::where('student_id', $studentId)
  274. ->orderBy('created_at', 'desc')
  275. ->limit($limit)
  276. ->get()
  277. ->toArray();
  278. $mistakes = MistakeRecord::where('student_id', $studentId)
  279. ->orderBy('created_at', 'desc')
  280. ->limit($limit)
  281. ->get()
  282. ->toArray();
  283. // 使用AI分析服务获取掌握度数据
  284. $masteryData = $this->aiAnalysisService->getStudentMastery($studentId);
  285. // 获取掌握度快照历史
  286. $snapshots = DB::connection('pgsql')
  287. ->table('knowledge_point_mastery_snapshots')
  288. ->where('student_id', $studentId)
  289. ->orderBy('snapshot_time', 'desc')
  290. ->limit($limit)
  291. ->get()
  292. ->toArray();
  293. return [
  294. 'exercises' => $exercises,
  295. 'mistakes' => $mistakes,
  296. 'mastery_data' => $masteryData['data'] ?? [],
  297. 'mastery_snapshots' => $snapshots,
  298. 'summary' => [
  299. 'total_exercises' => StudentExercise::where('student_id', $studentId)->count(),
  300. 'total_mistakes' => MistakeRecord::where('student_id', $studentId)->count(),
  301. 'mastery_snapshots_count' => count($snapshots),
  302. 'total_mastery_items' => count($masteryData['data'] ?? []),
  303. ],
  304. ];
  305. } catch (\Exception $e) {
  306. Log::error('获取学习历史失败', [
  307. 'student_id' => $studentId,
  308. 'error' => $e->getMessage(),
  309. ]);
  310. return [];
  311. }
  312. }
  313. /**
  314. * 使用增强版步骤级分析算法
  315. *
  316. * 这是基于《卷子分析思考.md》思路的增强版分析方法
  317. * 支持步骤级分析、知识点映射和智能出卷推荐
  318. *
  319. * @param array $examData 考试数据
  320. * @return array 分析结果
  321. */
  322. public function analyzeWithEnhancedSteps(array $examData): array
  323. {
  324. Log::info('StudentAnswerAnalysisService: 开始增强版步骤级分析', [
  325. 'exam_id' => $examData['exam_id'] ?? 'unknown',
  326. 'student_id' => $examData['student_id'] ?? 'unknown',
  327. 'has_steps' => !empty(array_filter($examData['questions'] ?? [], fn($q) => !empty($q['steps'])))
  328. ]);
  329. try {
  330. // 使用增强的分析算法
  331. $result = $this->examAnswerAnalysisService->analyzeExamAnswers($examData);
  332. Log::info('StudentAnswerAnalysisService: 增强版步骤级分析完成', [
  333. 'exam_id' => $examData['exam_id'],
  334. 'student_id' => $examData['student_id'],
  335. 'analyzed_knowledge_points' => count($result['knowledge_point_analysis'] ?? [])
  336. ]);
  337. return $result;
  338. } catch (\Exception $e) {
  339. Log::error('StudentAnswerAnalysisService: 增强版步骤级分析失败', [
  340. 'exam_id' => $examData['exam_id'] ?? 'unknown',
  341. 'student_id' => $examData['student_id'] ?? 'unknown',
  342. 'error' => $e->getMessage()
  343. ]);
  344. throw new \Exception('增强版步骤级分析失败:' . $e->getMessage());
  345. }
  346. }
  347. }