StudentAnswerAnalysisService.php 13 KB

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