PaperSubmitAnalysisController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Models\Paper;
  5. use App\Models\PaperQuestion;
  6. use App\Services\ExamAnswerAnalysisService;
  7. use App\Services\MistakeBookService;
  8. use App\Services\QuestionBankService;
  9. use Illuminate\Http\JsonResponse;
  10. use Illuminate\Http\Request;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. use Illuminate\Support\Facades\Validator;
  14. /**
  15. * 试卷提交分析控制器
  16. *
  17. * 接收前端提交的试卷答题数据,进行学情分析并写入错题本
  18. */
  19. class PaperSubmitAnalysisController extends Controller
  20. {
  21. public function __construct(
  22. private readonly ExamAnswerAnalysisService $analysisService,
  23. private readonly MistakeBookService $mistakeBookService,
  24. private readonly QuestionBankService $questionBankService
  25. ) {}
  26. /**
  27. * 提交试卷答题数据进行分析
  28. *
  29. * POST /api/paper-submit-analysis
  30. *
  31. * 请求格式:
  32. * {
  33. * "paperId": "paper_661325736792",
  34. * "questions": [
  35. * {
  36. * "question_bank_id": 876,
  37. * "student_answer": "C",
  38. * "is_correct": [0], // 0=错, 1=对;简答题分步骤 [1,0,1]
  39. * "teacher_comment": null
  40. * }
  41. * ]
  42. * }
  43. */
  44. public function analyze(Request $request): JsonResponse
  45. {
  46. try {
  47. // 1. 验证请求数据
  48. $validator = Validator::make($request->all(), [
  49. 'paperId' => 'required|string|max:255',
  50. 'questions' => 'required|array|min:1',
  51. 'questions.*.question_bank_id' => 'required|integer',
  52. 'questions.*.student_answer' => 'nullable|string',
  53. 'questions.*.is_correct' => 'required|array|min:1',
  54. 'questions.*.is_correct.*' => 'integer|in:0,1',
  55. 'questions.*.teacher_comment' => 'nullable|string',
  56. ]);
  57. if ($validator->fails()) {
  58. return response()->json([
  59. 'success' => false,
  60. 'error' => '参数验证失败',
  61. 'details' => $validator->errors()
  62. ], 422);
  63. }
  64. $paperId = $request->input('paperId');
  65. $questionsData = $request->input('questions');
  66. Log::info('开始处理试卷提交分析', [
  67. 'paper_id' => $paperId,
  68. 'questions_count' => count($questionsData)
  69. ]);
  70. // 2. 通过 paperId 查询试卷获取 student_id
  71. $paper = Paper::where('paper_id', $paperId)->first();
  72. if (!$paper) {
  73. return response()->json([
  74. 'success' => false,
  75. 'error' => '试卷不存在',
  76. 'paper_id' => $paperId
  77. ], 404);
  78. }
  79. $studentId = $paper->student_id;
  80. if (!$studentId) {
  81. return response()->json([
  82. 'success' => false,
  83. 'error' => '试卷未关联学生',
  84. 'paper_id' => $paperId
  85. ], 400);
  86. }
  87. // 3. 获取题目详情并转换数据格式
  88. $transformedQuestions = $this->transformQuestionsData($questionsData, $paperId);
  89. // 4. 调用学情分析服务(可能失败,不影响错题本写入)
  90. $analysisResult = [];
  91. $analysisError = null;
  92. try {
  93. $examData = [
  94. 'exam_id' => $paperId,
  95. 'student_id' => $studentId,
  96. 'questions' => $transformedQuestions['questions'],
  97. ];
  98. $analysisResult = $this->analysisService->analyzeExamAnswers($examData);
  99. } catch (\Exception $e) {
  100. $analysisError = $e->getMessage();
  101. Log::warning('学情分析失败,继续处理错题本', [
  102. 'paper_id' => $paperId,
  103. 'error' => $analysisError
  104. ]);
  105. }
  106. // 5. 将错题写入错题本
  107. $mistakesAdded = $this->addMistakesToBook(
  108. $studentId,
  109. $paperId,
  110. $questionsData,
  111. $transformedQuestions['question_details']
  112. );
  113. // 6. 更新试卷状态为已完成
  114. $this->updatePaperStatus($paperId, $questionsData);
  115. Log::info('试卷提交分析完成', [
  116. 'paper_id' => $paperId,
  117. 'student_id' => $studentId,
  118. 'questions_analyzed' => count($transformedQuestions['questions']),
  119. 'mistakes_added' => $mistakesAdded
  120. ]);
  121. return response()->json([
  122. 'success' => true,
  123. 'data' => [
  124. 'paper_id' => $paperId,
  125. 'student_id' => $studentId,
  126. 'analysis_summary' => $analysisResult['overall_summary'] ?? null,
  127. 'knowledge_point_analysis' => $analysisResult['knowledge_point_analysis'] ?? [],
  128. 'smart_quiz_recommendation' => $analysisResult['smart_quiz_recommendation'] ?? null,
  129. 'mastery_vector' => $analysisResult['mastery_vector'] ?? [],
  130. 'mistakes_added' => $mistakesAdded,
  131. 'total_questions' => count($questionsData),
  132. 'correct_count' => $this->countCorrectQuestions($questionsData),
  133. 'incorrect_count' => $mistakesAdded,
  134. 'analysis_error' => $analysisError, // 如果学情分析失败,返回错误信息
  135. ],
  136. 'message' => "分析完成,新增 {$mistakesAdded} 条错题记录" . ($analysisError ? "(学情分析暂不可用)" : "")
  137. ]);
  138. } catch (\Exception $e) {
  139. Log::error('试卷提交分析失败', [
  140. 'error' => $e->getMessage(),
  141. 'trace' => $e->getTraceAsString(),
  142. 'request_data' => $request->all()
  143. ]);
  144. return response()->json([
  145. 'success' => false,
  146. 'error' => '分析失败:' . $e->getMessage()
  147. ], 500);
  148. }
  149. }
  150. /**
  151. * 转换前端数据格式为分析服务所需格式
  152. */
  153. private function transformQuestionsData(array $questionsData, string $paperId): array
  154. {
  155. $transformedQuestions = [];
  156. $questionDetails = [];
  157. $questionBankIds = array_column($questionsData, 'question_bank_id');
  158. // 批量获取题目详情
  159. $questionBankData = $this->fetchQuestionBankData($questionBankIds);
  160. // 获取试卷中题目的分数信息
  161. $paperQuestions = PaperQuestion::where('paper_id', $paperId)
  162. ->whereIn('question_bank_id', $questionBankIds)
  163. ->get()
  164. ->keyBy('question_bank_id');
  165. foreach ($questionsData as $index => $questionData) {
  166. $questionBankId = $questionData['question_bank_id'];
  167. $isCorrectArray = $questionData['is_correct'];
  168. $studentAnswer = $questionData['student_answer'] ?? null;
  169. $teacherComment = $questionData['teacher_comment'] ?? null;
  170. // 获取题目详情
  171. $qbData = $questionBankData[$questionBankId] ?? null;
  172. $paperQuestion = $paperQuestions->get($questionBankId);
  173. // 确定题目分数
  174. $maxScore = $paperQuestion->score ?? ($qbData['score'] ?? 5.0);
  175. $kpCode = $qbData['kp_code'] ?? ($paperQuestion->knowledge_point ?? 'K-GENERAL');
  176. $kpName = $qbData['kp_name'] ?? $kpCode;
  177. $questionType = $qbData['question_type'] ?? ($paperQuestion->question_type ?? 'unknown');
  178. // 保存详情供后续使用
  179. $questionDetails[$questionBankId] = [
  180. 'question_bank_id' => $questionBankId,
  181. 'stem' => $qbData['stem'] ?? ($paperQuestion->question_text ?? ''),
  182. 'answer' => $qbData['answer'] ?? ($paperQuestion->correct_answer ?? ''),
  183. 'solution' => $qbData['solution'] ?? ($paperQuestion->solution ?? ''),
  184. 'kp_code' => $kpCode,
  185. 'kp_name' => $kpName,
  186. 'question_type' => $questionType,
  187. 'max_score' => $maxScore,
  188. 'skills' => $qbData['skills'] ?? [],
  189. ];
  190. // 转换 is_correct 数组为 steps 格式
  191. $steps = [];
  192. $totalSteps = count($isCorrectArray);
  193. $scorePerStep = $maxScore / $totalSteps;
  194. $scoreObtained = 0;
  195. foreach ($isCorrectArray as $stepIndex => $isCorrect) {
  196. $stepCorrect = (bool) $isCorrect;
  197. if ($stepCorrect) {
  198. $scoreObtained += $scorePerStep;
  199. }
  200. $steps[] = [
  201. 'step_index' => $stepIndex + 1,
  202. 'is_correct' => $stepCorrect,
  203. 'kp_id' => $kpCode,
  204. 'score' => $scorePerStep,
  205. 'weight' => 1.0,
  206. ];
  207. }
  208. $transformedQuestions[] = [
  209. 'question_id' => (string) $questionBankId,
  210. 'score' => $maxScore,
  211. 'score_obtained' => round($scoreObtained, 2),
  212. 'steps' => $totalSteps > 1 ? $steps : [], // 单步骤不传steps
  213. ];
  214. }
  215. return [
  216. 'questions' => $transformedQuestions,
  217. 'question_details' => $questionDetails,
  218. ];
  219. }
  220. /**
  221. * 批量获取题库题目详情
  222. */
  223. private function fetchQuestionBankData(array $questionBankIds): array
  224. {
  225. try {
  226. $response = $this->questionBankService->getQuestionsByIds($questionBankIds);
  227. $questions = $response['data'] ?? $response;
  228. // 转换为以 id 为 key 的数组
  229. $result = [];
  230. foreach ($questions as $question) {
  231. $id = $question['id'] ?? $question['question_id'] ?? null;
  232. if ($id) {
  233. $result[$id] = $question;
  234. }
  235. }
  236. return $result;
  237. } catch (\Exception $e) {
  238. Log::warning('获取题库详情失败,使用空数据', [
  239. 'error' => $e->getMessage(),
  240. 'question_bank_ids' => $questionBankIds
  241. ]);
  242. return [];
  243. }
  244. }
  245. /**
  246. * 将错题写入错题本
  247. */
  248. private function addMistakesToBook(
  249. string $studentId,
  250. string $paperId,
  251. array $questionsData,
  252. array $questionDetails
  253. ): int {
  254. $mistakesAdded = 0;
  255. foreach ($questionsData as $questionData) {
  256. $isCorrectArray = $questionData['is_correct'];
  257. // 判断是否有错误(数组中存在0)
  258. $hasError = in_array(0, $isCorrectArray, true);
  259. if (!$hasError) {
  260. continue;
  261. }
  262. $questionBankId = $questionData['question_bank_id'];
  263. $detail = $questionDetails[$questionBankId] ?? [];
  264. try {
  265. $payload = [
  266. 'student_id' => $studentId,
  267. 'question_id' => $questionBankId,
  268. 'paper_id' => $paperId,
  269. 'my_answer' => $questionData['student_answer'] ?? '',
  270. 'correct_answer' => $detail['answer'] ?? '',
  271. 'question_text' => $detail['stem'] ?? '',
  272. 'knowledge_point' => $detail['kp_name'] ?? '',
  273. 'explanation' => $detail['solution'] ?? '',
  274. 'kp_ids' => [$detail['kp_code'] ?? 'K-GENERAL'],
  275. 'source' => "paper:{$paperId}",
  276. 'happened_at' => now()->toISOString(),
  277. ];
  278. $result = $this->mistakeBookService->createMistake($payload);
  279. // 如果不是重复记录,则计数
  280. if (!($result['duplicate'] ?? false)) {
  281. $mistakesAdded++;
  282. }
  283. Log::debug('错题写入成功', [
  284. 'student_id' => $studentId,
  285. 'question_bank_id' => $questionBankId,
  286. 'duplicate' => $result['duplicate'] ?? false
  287. ]);
  288. } catch (\Exception $e) {
  289. Log::error('写入错题本失败', [
  290. 'student_id' => $studentId,
  291. 'question_bank_id' => $questionBankId,
  292. 'error' => $e->getMessage()
  293. ]);
  294. }
  295. }
  296. return $mistakesAdded;
  297. }
  298. /**
  299. * 更新试卷状态和题目作答信息
  300. */
  301. private function updatePaperStatus(string $paperId, array $questionsData): void
  302. {
  303. try {
  304. // 更新试卷状态为已完成
  305. Paper::where('paper_id', $paperId)->update([
  306. 'status' => 'completed',
  307. 'completed_at' => now(),
  308. ]);
  309. // 更新每道题目的作答信息
  310. foreach ($questionsData as $questionData) {
  311. $questionBankId = $questionData['question_bank_id'];
  312. $isCorrectArray = $questionData['is_correct'];
  313. // 计算得分比例
  314. $correctCount = array_sum($isCorrectArray);
  315. $totalSteps = count($isCorrectArray);
  316. $scoreRatio = $totalSteps > 0 ? $correctCount / $totalSteps : 0;
  317. // 判断是否全对
  318. $isFullyCorrect = !in_array(0, $isCorrectArray, true);
  319. PaperQuestion::where('paper_id', $paperId)
  320. ->where('question_bank_id', $questionBankId)
  321. ->update([
  322. 'student_answer' => $questionData['student_answer'] ?? null,
  323. 'is_correct' => $isFullyCorrect,
  324. 'score_ratio' => $scoreRatio,
  325. 'score_obtained' => DB::raw("score * {$scoreRatio}"),
  326. 'teacher_comment' => $questionData['teacher_comment'] ?? null,
  327. 'graded_at' => now(),
  328. ]);
  329. }
  330. Log::info('试卷状态更新完成', ['paper_id' => $paperId]);
  331. } catch (\Exception $e) {
  332. Log::error('更新试卷状态失败', [
  333. 'paper_id' => $paperId,
  334. 'error' => $e->getMessage()
  335. ]);
  336. }
  337. }
  338. /**
  339. * 统计正确题目数量
  340. */
  341. private function countCorrectQuestions(array $questionsData): int
  342. {
  343. $correctCount = 0;
  344. foreach ($questionsData as $questionData) {
  345. $isCorrectArray = $questionData['is_correct'];
  346. // 全对才算正确
  347. if (!in_array(0, $isCorrectArray, true)) {
  348. $correctCount++;
  349. }
  350. }
  351. return $correctCount;
  352. }
  353. /**
  354. * 获取试卷分析结果
  355. *
  356. * GET /api/paper-submit-analysis/{paperId}
  357. */
  358. public function getResult(string $paperId): JsonResponse
  359. {
  360. try {
  361. $paper = Paper::where('paper_id', $paperId)->first();
  362. if (!$paper) {
  363. return response()->json([
  364. 'success' => false,
  365. 'error' => '试卷不存在'
  366. ], 404);
  367. }
  368. // 从数据库获取分析结果
  369. $result = DB::connection('mysql')
  370. ->table('exam_analysis_results')
  371. ->where('paper_id', $paperId)
  372. ->where('student_id', $paper->student_id)
  373. ->orderBy('created_at', 'desc')
  374. ->first();
  375. if (!$result) {
  376. return response()->json([
  377. 'success' => false,
  378. 'error' => '未找到分析结果,请先提交试卷进行分析'
  379. ], 404);
  380. }
  381. return response()->json([
  382. 'success' => true,
  383. 'data' => json_decode($result->analysis_data, true)
  384. ]);
  385. } catch (\Exception $e) {
  386. Log::error('获取试卷分析结果失败', [
  387. 'paper_id' => $paperId,
  388. 'error' => $e->getMessage()
  389. ]);
  390. return response()->json([
  391. 'success' => false,
  392. 'error' => '获取分析结果失败:' . $e->getMessage()
  393. ], 500);
  394. }
  395. }
  396. }