StudentAnswerAnalysisService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. namespace App\Services;
  3. use App\Models\MistakeRecord;
  4. use Illuminate\Support\Facades\DB;
  5. use Illuminate\Support\Facades\Log;
  6. use Illuminate\Support\Str;
  7. /**
  8. * 学生作答分析服务
  9. * 负责处理学生作答结果的保存、分析和掌握度更新
  10. */
  11. class StudentAnswerAnalysisService
  12. {
  13. public function __construct(
  14. private readonly LocalAIAnalysisService $aiAnalysisService,
  15. private readonly ExamAnswerAnalysisService $examAnswerAnalysisService
  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. $missingCount = 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. // 缺题不计入对错统计
  35. if ($answer['is_missing'] ?? false) {
  36. $missingCount++;
  37. continue;
  38. }
  39. $totalScore += $maxScore;
  40. $obtainedScore += $score;
  41. if ($answer['is_correct']) {
  42. $correctCount++;
  43. } else {
  44. $wrongCount++;
  45. }
  46. }
  47. $accuracyRate = ($correctCount + $wrongCount) > 0
  48. ? round($correctCount / ($correctCount + $wrongCount), 4)
  49. : 0;
  50. // 只返回统计信息,不进行数据库操作(避免阻塞)
  51. // 错题记录将在后台任务中异步处理
  52. Log::info('作答记录已保存', [
  53. 'record_id' => $recordId,
  54. 'paper_id' => $data['paper_id'],
  55. 'student_id' => $data['student_id'],
  56. 'total_questions' => count($answers),
  57. 'correct_count' => $correctCount,
  58. 'wrong_count' => $wrongCount,
  59. 'accuracy_rate' => $accuracyRate,
  60. ]);
  61. return [
  62. 'record_id' => $recordId,
  63. 'paper_id' => $data['paper_id'],
  64. 'student_id' => $data['student_id'],
  65. 'total_score' => $totalScore,
  66. 'obtained_score' => $obtainedScore,
  67. 'accuracy_rate' => $accuracyRate,
  68. 'correct_count' => $correctCount,
  69. 'wrong_count' => $wrongCount,
  70. 'missing_count' => $missingCount,
  71. 'total_questions' => count($answers),
  72. ];
  73. }
  74. /**
  75. * 保存错题记录
  76. * 优化功能:
  77. * 1. 相同题目不重复出现在错题本中
  78. * 2. 记录错误次数和作答次数
  79. * 3. 增加错误类型分析
  80. * 4. 记录知识点掌握度变化
  81. */
  82. private function saveMistakeRecord(array $data, array $answer): void
  83. {
  84. try {
  85. // 检查是否已存在相同的错题记录
  86. $existing = MistakeRecord::where('student_id', $data['student_id'])
  87. ->where('question_id', $answer['question_id'])
  88. ->first();
  89. if ($existing) {
  90. // 更新现有记录(不重复创建)
  91. $oldReviewCount = $existing->review_count;
  92. // 递增错误次数和作答次数
  93. $existing->increment('review_count');
  94. // 记录每次错误的时间戳
  95. $errorHistory = json_decode($existing->remark ?? '[]', true) ?: [];
  96. $errorHistory[] = [
  97. 'timestamp' => now()->toISOString(),
  98. 'paper_id' => $data['paper_id'],
  99. 'student_answer' => $answer['student_answer'] ?? '',
  100. 'correct_answer' => $answer['correct_answer'] ?? '',
  101. 'error_type' => $this->guessErrorType($answer),
  102. 'is_missing' => $answer['is_missing'] ?? false,
  103. ];
  104. // 更新记录
  105. $existing->update([
  106. 'student_answer' => $answer['student_answer'] ?? '',
  107. 'correct_answer' => $answer['correct_answer'] ?? '',
  108. 'knowledge_point' => $answer['knowledge_point'] ?? $existing->knowledge_point,
  109. 'error_type' => $this->guessErrorType($answer),
  110. 'remark' => json_encode($errorHistory),
  111. 'updated_at' => now(),
  112. // 如果是缺题,更新为待复习状态
  113. 'review_status' => ($answer['is_missing'] ?? false)
  114. ? MistakeRecord::REVIEW_STATUS_PENDING
  115. : $existing->review_status,
  116. ]);
  117. Log::info('错题记录已更新', [
  118. 'student_id' => $data['student_id'],
  119. 'question_id' => $answer['question_id'],
  120. 'old_review_count' => $oldReviewCount,
  121. 'new_review_count' => $existing->review_count,
  122. 'total_errors' => count($errorHistory),
  123. ]);
  124. } else {
  125. // 创建新记录
  126. $errorHistory = [[
  127. 'timestamp' => now()->toISOString(),
  128. 'paper_id' => $data['paper_id'],
  129. 'student_answer' => $answer['student_answer'] ?? '',
  130. 'correct_answer' => $answer['correct_answer'] ?? '',
  131. 'error_type' => $this->guessErrorType($answer),
  132. 'is_missing' => $answer['is_missing'] ?? false,
  133. ]];
  134. $mistakeRecord = MistakeRecord::create([
  135. 'student_id' => $data['student_id'],
  136. 'question_id' => $answer['question_id'],
  137. 'paper_id' => $data['paper_id'] ?? null,
  138. 'source' => MistakeRecord::SOURCE_EXAM,
  139. 'question_text' => json_encode([
  140. 'question_number' => $answer['question_number'] ?? null,
  141. 'question_type' => $answer['question_type'] ?? null,
  142. 'is_missing' => $answer['is_missing'] ?? false,
  143. ]),
  144. 'student_answer' => $answer['student_answer'] ?? '',
  145. 'correct_answer' => $answer['correct_answer'] ?? '',
  146. 'knowledge_point' => $answer['knowledge_point'] ?? null,
  147. 'error_type' => $this->guessErrorType($answer),
  148. 'review_status' => ($answer['is_missing'] ?? false)
  149. ? MistakeRecord::REVIEW_STATUS_PENDING
  150. : MistakeRecord::REVIEW_STATUS_PENDING,
  151. 'review_count' => 1, // 第一次错误
  152. 'force_review' => false,
  153. 'is_favorite' => false,
  154. 'in_retry_list' => false,
  155. 'difficulty' => 0.5,
  156. 'mastery_level' => 0.0,
  157. 'remark' => json_encode($errorHistory),
  158. ]);
  159. Log::info('新错题记录已创建', [
  160. 'student_id' => $data['student_id'],
  161. 'question_id' => $answer['question_id'],
  162. 'mistake_record_id' => $mistakeRecord->id,
  163. 'error_type' => $mistakeRecord->error_type,
  164. ]);
  165. }
  166. } catch (\Exception $e) {
  167. Log::error('保存错题记录失败', [
  168. 'student_id' => $data['student_id'],
  169. 'question_id' => $answer['question_id'],
  170. 'error' => $e->getMessage(),
  171. 'trace' => $e->getTraceAsString(),
  172. ]);
  173. }
  174. }
  175. /**
  176. * 猜测错误类型
  177. */
  178. private function guessErrorType(array $answer): string
  179. {
  180. // 根据题型和答案特征猜测错误类型
  181. $questionType = $answer['question_type'] ?? '';
  182. if ($questionType === 'choice') {
  183. return MistakeRecord::ERROR_TYPE_CARELESS;
  184. }
  185. if ($questionType === 'fill' || $questionType === 'answer') {
  186. // 检查是否部分正确
  187. if (isset($answer['step_scores']) && is_array($answer['step_scores'])) {
  188. $totalSteps = count($answer['step_scores']);
  189. $correctSteps = array_sum($answer['step_scores']);
  190. if ($correctSteps > 0 && $correctSteps < $totalSteps) {
  191. return MistakeRecord::ERROR_TYPE_PROCEDURE ?? MistakeRecord::ERROR_TYPE_CALCULATION;
  192. }
  193. }
  194. }
  195. return MistakeRecord::ERROR_TYPE_OTHER;
  196. }
  197. /**
  198. * 保存分析结果
  199. * 简化版:只保存错题记录,不依赖其他表
  200. */
  201. public function saveAnalysisResults(array $answerRecord, array $analysisData, array $questionAnalyses): void
  202. {
  203. try {
  204. // 只更新错题记录的掌握度(如果题目有错误)
  205. foreach ($questionAnalyses as $questionAnalysis) {
  206. // 更新掌握度(如果题目有知识点且答错了)
  207. if (!empty($questionAnalysis['kp_code']) && !($questionAnalysis['is_correct'] ?? true)) {
  208. $this->updateMasteryForQuestion(
  209. $answerRecord['student_id'],
  210. $questionAnalysis['kp_code'],
  211. $questionAnalysis['is_correct'],
  212. $questionAnalysis['difficulty'] ?? 0.5
  213. );
  214. }
  215. }
  216. Log::info('分析结果已保存', [
  217. 'student_id' => $answerRecord['student_id'],
  218. 'paper_id' => $answerRecord['paper_id'],
  219. 'question_count' => count($questionAnalyses),
  220. ]);
  221. } catch (\Exception $e) {
  222. Log::error('保存分析结果失败', [
  223. 'student_id' => $answerRecord['student_id'],
  224. 'paper_id' => $answerRecord['paper_id'],
  225. 'error' => $e->getMessage(),
  226. ]);
  227. }
  228. }
  229. /**
  230. * 为单个题目更新掌握度
  231. */
  232. private function updateMasteryForQuestion(string $studentId, string $kpCode, bool $isCorrect, float $difficulty): void
  233. {
  234. try {
  235. // 使用AI分析服务更新掌握度(如果表存在)
  236. $result = $this->aiAnalysisService->updateMastery(
  237. $studentId,
  238. $kpCode,
  239. 0.5, // 默认掌握度
  240. $isCorrect,
  241. $difficulty
  242. );
  243. Log::info('掌握度已更新', [
  244. 'student_id' => $studentId,
  245. 'kp_code' => $kpCode,
  246. 'is_correct' => $isCorrect,
  247. 'new_mastery' => $result['new_mastery'] ?? 'N/A',
  248. 'change' => $result['change'] ?? 'N/A',
  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. // 获取错题记录历史(作为学习历史的主要数据)
  273. $mistakes = MistakeRecord::where('student_id', $studentId)
  274. ->orderBy('created_at', 'desc')
  275. ->limit($limit)
  276. ->get()
  277. ->toArray();
  278. // 使用AI分析服务获取掌握度数据(如果表存在)
  279. $masteryData = ['data' => []];
  280. try {
  281. $masteryData = $this->aiAnalysisService->getStudentMastery($studentId);
  282. } catch (\Exception $e) {
  283. Log::warning('获取掌握度数据失败', ['student_id' => $studentId, 'error' => $e->getMessage()]);
  284. }
  285. return [
  286. 'mistakes' => $mistakes,
  287. 'mastery_data' => $masteryData['data'] ?? [],
  288. 'summary' => [
  289. 'total_mistakes' => MistakeRecord::where('student_id', $studentId)->count(),
  290. 'total_mastery_items' => count($masteryData['data'] ?? []),
  291. ],
  292. ];
  293. } catch (\Exception $e) {
  294. Log::error('获取学习历史失败', [
  295. 'student_id' => $studentId,
  296. 'error' => $e->getMessage(),
  297. ]);
  298. return [];
  299. }
  300. }
  301. /**
  302. * 使用增强版步骤级分析算法
  303. *
  304. * 这是基于《卷子分析思考.md》思路的增强版分析方法
  305. * 支持步骤级分析、知识点映射和智能出卷推荐
  306. *
  307. * @param array $examData 考试数据
  308. * @return array 分析结果
  309. */
  310. public function analyzeWithEnhancedSteps(array $examData): array
  311. {
  312. Log::info('StudentAnswerAnalysisService: 开始增强版步骤级分析', [
  313. 'exam_id' => $examData['exam_id'] ?? 'unknown',
  314. 'student_id' => $examData['student_id'] ?? 'unknown',
  315. 'has_steps' => !empty(array_filter($examData['questions'] ?? [], fn($q) => !empty($q['steps'])))
  316. ]);
  317. try {
  318. // 使用增强的分析算法
  319. $result = $this->examAnswerAnalysisService->analyzeExamAnswers($examData);
  320. Log::info('StudentAnswerAnalysisService: 增强版步骤级分析完成', [
  321. 'exam_id' => $examData['exam_id'],
  322. 'student_id' => $examData['student_id'],
  323. 'analyzed_knowledge_points' => count($result['knowledge_point_analysis'] ?? [])
  324. ]);
  325. return $result;
  326. } catch (\Exception $e) {
  327. Log::error('StudentAnswerAnalysisService: 增强版步骤级分析失败', [
  328. 'exam_id' => $examData['exam_id'] ?? 'unknown',
  329. 'student_id' => $examData['student_id'] ?? 'unknown',
  330. 'error' => $e->getMessage()
  331. ]);
  332. throw new \Exception('增强版步骤级分析失败:' . $e->getMessage());
  333. }
  334. }
  335. }