all(), [ 'paper_id' => 'required|string|max:255', 'questions' => 'required|array|min:1', 'questions.*.question_id' => 'required|integer', // 前端使用 question_id 'questions.*.student_answer' => 'nullable|string', 'questions.*.is_correct' => 'required|array|min:1', 'questions.*.is_correct.*' => 'integer|in:0,1', 'questions.*.teacher_comment' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'error' => '参数验证失败', 'details' => $validator->errors() ], 422); } $paperId = $request->input('paper_id'); $questionsData = $request->input('questions'); Log::info('开始处理试卷提交分析', [ 'paper_id' => $paperId, 'questions_count' => count($questionsData) ]); // 2. 通过 paperId 查询试卷获取 student_id $paper = Paper::where('paper_id', $paperId)->first(); if (!$paper) { return response()->json([ 'success' => false, 'error' => '试卷不存在', 'paper_id' => $paperId ], 404); } $studentId = $paper->student_id; if (!$studentId) { return response()->json([ 'success' => false, 'error' => '试卷未关联学生', 'paper_id' => $paperId ], 400); } // 3. 转换前端数据:将 question_id 转换为 question_bank_id $questionsData = $this->convertQuestionIds($questionsData); // 4. 获取题目详情并转换数据格式 $transformedQuestions = $this->transformQuestionsData($questionsData, $paperId); // 4. 调用学情分析服务(可能失败,不影响错题本写入) $analysisResult = []; $analysisError = null; try { $examData = [ 'paper_id' => $paperId, 'student_id' => $studentId, 'questions' => $transformedQuestions['questions'], ]; $analysisResult = $this->analysisService->analyzeExamAnswers($examData); } catch (\Exception $e) { $analysisError = $e->getMessage(); Log::warning('学情分析失败,继续处理错题本', [ 'paper_id' => $paperId, 'error' => $analysisError ]); } // 5. 将错题写入错题本 $mistakesAdded = $this->addMistakesToBook( $studentId, $paperId, $questionsData, $transformedQuestions['question_details'] ); // 6. 更新试卷状态为已完成 $this->updatePaperStatus($paperId, $questionsData); Log::info('试卷提交分析完成', [ 'paper_id' => $paperId, 'student_id' => $studentId, 'questions_analyzed' => count($transformedQuestions['questions']), 'mistakes_added' => $mistakesAdded ]); return response()->json([ 'success' => true, 'data' => [ 'paper_id' => $paperId, 'student_id' => $studentId, 'analysis_summary' => $analysisResult['overall_summary'] ?? null, 'knowledge_point_analysis' => $analysisResult['knowledge_point_analysis'] ?? [], 'smart_quiz_recommendation' => $analysisResult['smart_quiz_recommendation'] ?? null, 'mastery_vector' => $analysisResult['mastery_vector'] ?? [], 'mistakes_added' => $mistakesAdded, 'total_questions' => count($questionsData), 'correct_count' => $this->countCorrectQuestions($questionsData), 'incorrect_count' => $mistakesAdded, 'analysis_error' => $analysisError, // 如果学情分析失败,返回错误信息 ], 'message' => "分析完成,新增 {$mistakesAdded} 条错题记录" . ($analysisError ? "(学情分析暂不可用)" : "") ]); } catch (\Exception $e) { Log::error('试卷提交分析失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'request_data' => $request->all() ]); return response()->json([ 'success' => false, 'error' => '分析失败:' . $e->getMessage() ], 500); } } /** * 转换前端数据格式为分析服务所需格式 * 【增强】处理未提交的题目,标记为"回答正确" */ private function transformQuestionsData(array $questionsData, string $paperId): array { $transformedQuestions = []; $questionDetails = []; // 获取试卷中的所有题目 $allPaperQuestions = PaperQuestion::where('paper_id', $paperId)->get(); $allQuestionBankIds = $allPaperQuestions->pluck('question_bank_id')->toArray(); // 区分已提交和未提交的题目 $submittedIds = array_column($questionsData, 'question_bank_id'); // 转换后使用 question_bank_id $notSubmittedIds = array_diff($allQuestionBankIds, $submittedIds); Log::info('题目提交情况分析', [ 'paper_id' => $paperId, 'total_questions' => count($allQuestionBankIds), 'submitted_count' => count($submittedIds), 'not_submitted_count' => count($notSubmittedIds), 'submitted_ids' => $submittedIds, 'not_submitted_ids' => $notSubmittedIds, ]); // 批量获取所有题目的详情(包括未提交的) // 【优化】使用新的通用方法直接从题库关联知识点 $allQuestionBankData = []; foreach ($allQuestionBankIds as $questionBankId) { $questionData = $this->questionBankService->getQuestionKnowledgePoint($questionBankId); $allQuestionBankData[$questionBankId] = $questionData; // 调试日志 Log::info('获取题目知识点', [ 'question_bank_id' => $questionBankId, 'kp_code' => $questionData['kp_code'] ?? null, 'kp_name' => $questionData['kp_name'] ?? null, ]); } // 获取所有题目的分数信息 $allPaperQuestionsCollection = PaperQuestion::where('paper_id', $paperId) ->get() ->keyBy('question_bank_id'); // 处理已提交的题目 foreach ($questionsData as $index => $questionData) { $questionBankId = $questionData['question_bank_id']; $isCorrectArray = $questionData['is_correct']; $studentAnswer = $questionData['student_answer'] ?? null; $teacherComment = $questionData['teacher_comment'] ?? null; // 获取题目详情 $qbData = $allQuestionBankData[$questionBankId] ?? null; $paperQuestion = $allPaperQuestionsCollection->get($questionBankId); // 确定题目分数 $maxScore = $paperQuestion->score ?? 5.0; $kpCode = $qbData['kp_code'] ?? null; if (!$kpCode) { throw new \Exception("题目 {$questionBankId} 缺少知识点代码"); } $kpName = $qbData['kp_name'] ?? $kpCode; $questionType = $qbData['question_type'] ?? 'unknown'; // 保存详情供后续使用 $questionDetails[$questionBankId] = [ 'question_bank_id' => $questionBankId, 'stem' => $qbData['question_content'] ?? ($paperQuestion->question_text ?? ''), 'answer' => $qbData['question_answer'] ?? ($paperQuestion->correct_answer ?? ''), 'solution' => $paperQuestion->solution ?? '', 'kp_code' => $kpCode, 'kp_name' => $kpName, 'question_type' => $questionType, 'max_score' => $maxScore, 'skills' => [], 'is_submitted' => true, ]; // 转换 is_correct 数组为 steps 格式 $steps = []; $totalSteps = count($isCorrectArray); $scorePerStep = $maxScore / $totalSteps; $scoreObtained = 0; foreach ($isCorrectArray as $stepIndex => $isCorrect) { $stepCorrect = (bool) $isCorrect; if ($stepCorrect) { $scoreObtained += $scorePerStep; } $steps[] = [ 'step_index' => $stepIndex + 1, 'is_correct' => $stepCorrect, 'kp_id' => $kpCode, 'score' => $scorePerStep, 'weight' => 1.0, ]; } $transformedQuestions[] = [ 'question_id' => (string) $questionBankId, 'score' => $maxScore, 'score_obtained' => round($scoreObtained, 2), 'steps' => $totalSteps > 1 ? $steps : [], // 单步骤不传steps 'kp_code' => $kpCode, // 【修复】添加知识点代码,供ExamAnswerAnalysisService使用 'kp_name' => $kpName, // 【修复】添加知识点名称 ]; } // 【新增】处理未提交的题目:标记为"回答正确" foreach ($notSubmittedIds as $questionBankId) { // 获取题目详情 $qbData = $allQuestionBankData[$questionBankId] ?? null; $paperQuestion = $allPaperQuestionsCollection->get($questionBankId); // 确定题目分数 $maxScore = $paperQuestion->score ?? 5.0; $kpCode = $qbData['kp_code'] ?? null; if (!$kpCode) { throw new \Exception("题目 {$questionBankId} 缺少知识点代码"); } $kpName = $qbData['kp_name'] ?? $kpCode; $questionType = $qbData['question_type'] ?? 'unknown'; // 保存详情供后续使用 $questionDetails[$questionBankId] = [ 'question_bank_id' => $questionBankId, 'stem' => $qbData['question_content'] ?? ($paperQuestion->question_text ?? ''), 'answer' => $qbData['question_answer'] ?? ($paperQuestion->correct_answer ?? ''), 'solution' => $paperQuestion->solution ?? '', 'kp_code' => $kpCode, 'kp_name' => $kpName, 'question_type' => $questionType, 'max_score' => $maxScore, 'skills' => [], 'is_submitted' => false, // 未提交标记 ]; // 未提交的题目:标记为完全正确(说明学生已经掌握,不需要作答) $transformedQuestions[] = [ 'question_id' => (string) $questionBankId, 'score' => $maxScore, 'score_obtained' => $maxScore, // 获得满分 'steps' => [], // 不需要步骤分析 'is_correct' => true, // 标记为正确 'is_submitted' => false, // 标记为未提交 'kp_code' => $kpCode, // 【修复】添加知识点代码,供ExamAnswerAnalysisService使用 'kp_name' => $kpName, // 【修复】添加知识点名称 ]; Log::info('未提交题目处理', [ 'question_bank_id' => $questionBankId, 'kp_code' => $kpCode, 'max_score' => $maxScore, 'reason' => '未作答视为已掌握', ]); } Log::info('题目数据转换完成', [ 'paper_id' => $paperId, 'total_transformed' => count($transformedQuestions), 'submitted_count' => count($submittedIds), 'not_submitted_count' => count($notSubmittedIds), ]); return [ 'questions' => $transformedQuestions, 'question_details' => $questionDetails, ]; } /** * 批量获取题库题目详情 */ /** * 将错题写入错题本 * 【正确逻辑】只处理已提交的题目,未提交的题目默认正确,不写入错题本 */ private function addMistakesToBook( string $studentId, string $paperId, array $questionsData, array $questionDetails ): int { $mistakesAdded = 0; // 只有已提交的题目才处理错题本 foreach ($questionsData as $questionData) { $isCorrectArray = $questionData['is_correct']; // 判断是否有错误(数组中存在0) $hasError = in_array(0, $isCorrectArray, true); if (!$hasError) { continue; // 全对,跳过 } $questionBankId = $questionData['question_bank_id']; $detail = $questionDetails[$questionBankId] ?? []; $mistakesAdded += $this->createMistakeRecord($studentId, $paperId, $questionData, $detail); } // 【说明】未提交的题目不写入错题本,因为默认正确(已掌握) Log::info('错题本写入完成', [ 'paper_id' => $paperId, 'submitted_questions' => count($questionsData), 'mistakes_added' => $mistakesAdded, 'note' => '未提交题目默认正确,未写入错题本', ]); return $mistakesAdded; } /** * 创建单个错题记录 */ private function createMistakeRecord( string $studentId, string $paperId, array $questionData, array $detail ): int { try { $payload = [ 'student_id' => $studentId, 'question_id' => $questionData['question_bank_id'], 'paper_id' => $paperId, 'my_answer' => $questionData['student_answer'] ?? '', 'correct_answer' => $detail['answer'] ?? '', 'question_text' => $detail['stem'] ?? '', 'knowledge_point' => $detail['kp_name'] ?? '', 'explanation' => $detail['solution'] ?? '', 'kp_ids' => [$detail['kp_code']], 'source' => "paper:{$paperId}", 'happened_at' => now()->toISOString(), ]; $result = $this->mistakeBookService->createMistake($payload); // 如果不是重复记录,则计数 if (!($result['duplicate'] ?? false)) { Log::debug('错题写入成功', [ 'student_id' => $studentId, 'question_bank_id' => $questionData['question_bank_id'], 'duplicate' => $result['duplicate'] ?? false ]); return 1; } return 0; } catch (\Exception $e) { Log::error('写入错题本失败', [ 'student_id' => $studentId, 'question_bank_id' => $questionData['question_bank_id'], 'error' => $e->getMessage() ]); return 0; } } /** * 更新试卷状态和题目作答信息 * 【正确逻辑】未提交题目视为正确(已掌握),已提交错误题目标记为错误 */ private function updatePaperStatus(string $paperId, array $questionsData): void { try { // 更新试卷状态为已完成 Paper::where('paper_id', $paperId)->update([ 'status' => 'completed', 'completed_at' => now(), ]); // 获取试卷中的所有题目(包括未提交的) $allPaperQuestions = PaperQuestion::where('paper_id', $paperId)->get(); $allQuestionIds = $allPaperQuestions->pluck('question_bank_id')->toArray(); // 区分已提交和未提交的题目 $submittedIds = array_column($questionsData, 'question_bank_id'); $notSubmittedIds = array_diff($allQuestionIds, $submittedIds); // 更新已提交题目的作答信息 foreach ($questionsData as $questionData) { $questionBankId = $questionData['question_bank_id']; $isCorrectArray = $questionData['is_correct']; // 计算得分比例 $correctCount = array_sum($isCorrectArray); $totalSteps = count($isCorrectArray); $scoreRatio = $totalSteps > 0 ? $correctCount / $totalSteps : 0; // 判断是否全对 $isFullyCorrect = !in_array(0, $isCorrectArray, true); PaperQuestion::where('paper_id', $paperId) ->where('question_bank_id', $questionBankId) ->update([ 'student_answer' => $questionData['student_answer'] ?? null, 'is_correct' => $isFullyCorrect, 'score_ratio' => $scoreRatio, 'score_obtained' => DB::raw("score * {$scoreRatio}"), 'teacher_comment' => $questionData['teacher_comment'] ?? null, 'graded_at' => now(), ]); } // 【修正】更新未提交题目的状态:视为正确(已掌握) foreach ($notSubmittedIds as $questionBankId) { // 未提交题目:视为正确(已掌握),获得满分 PaperQuestion::where('paper_id', $paperId) ->where('question_bank_id', $questionBankId) ->update([ 'student_answer' => null, 'is_correct' => true, // 视为正确 'score_ratio' => 1.0, // 获得满分 'score_obtained' => DB::raw('score'), // 获得满分 'teacher_comment' => '未作答(已掌握)', 'graded_at' => now(), ]); Log::debug('未提交题目状态更新', [ 'paper_id' => $paperId, 'question_bank_id' => $questionBankId, 'is_correct' => true, 'score_ratio' => 1.0, 'note' => '未提交题目视为正确(已掌握)', ]); } Log::info('试卷状态更新完成', [ 'paper_id' => $paperId, 'total_questions' => count($allQuestionIds), 'submitted_count' => count($submittedIds), 'not_submitted_count' => count($notSubmittedIds), 'note' => '未提交题目视为正确(已掌握)', ]); } catch (\Exception $e) { Log::error('更新试卷状态失败', [ 'paper_id' => $paperId, 'error' => $e->getMessage() ]); } } /** * 统计正确题目数量 */ private function countCorrectQuestions(array $questionsData): int { $correctCount = 0; foreach ($questionsData as $questionData) { $isCorrectArray = $questionData['is_correct']; // 全对才算正确 if (!in_array(0, $isCorrectArray, true)) { $correctCount++; } } return $correctCount; } /** * 获取试卷分析结果 * * GET /api/paper-submit-analysis/{paperId} */ public function getResult(string $paperId): JsonResponse { try { $paper = Paper::where('paper_id', $paperId)->first(); if (!$paper) { return response()->json([ 'success' => false, 'error' => '试卷不存在' ], 404); } // 从数据库获取分析结果 $result = DB::connection('mysql') ->table('exam_analysis_results') ->where('paper_id', $paperId) ->where('student_id', $paper->student_id) ->orderBy('created_at', 'desc') ->first(); if (!$result) { return response()->json([ 'success' => false, 'error' => '未找到分析结果,请先提交试卷进行分析' ], 404); } return response()->json([ 'success' => true, 'data' => json_decode($result->analysis_data, true) ]); } catch (\Exception $e) { Log::error('获取试卷分析结果失败', [ 'paper_id' => $paperId, 'error' => $e->getMessage() ]); return response()->json([ 'success' => false, 'error' => '获取分析结果失败:' . $e->getMessage() ], 500); } } /** * 转换前端 question_id 为后端 question_bank_id * 前端使用 question_id,后端转换为 question_bank_id 进行处理 */ private function convertQuestionIds(array $questionsData): array { $converted = []; foreach ($questionsData as $question) { // 将 question_id 转换为 question_bank_id if (isset($question['question_id'])) { $question['question_bank_id'] = $question['question_id']; unset($question['question_id']); } $converted[] = $question; } Log::info('转换 question_id 为 question_bank_id', [ 'original_count' => count($questionsData), 'converted_count' => count($converted), 'sample_conversion' => !empty($converted) ? [ 'from' => $questionsData[0]['question_id'] ?? null, 'to' => $converted[0]['question_bank_id'] ?? null ] : null ]); return $converted; } }