|
|
@@ -0,0 +1,452 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Controllers\Api;
|
|
|
+
|
|
|
+use App\Http\Controllers\Controller;
|
|
|
+use App\Models\Paper;
|
|
|
+use App\Models\PaperQuestion;
|
|
|
+use App\Services\ExamAnswerAnalysisService;
|
|
|
+use App\Services\MistakeBookService;
|
|
|
+use App\Services\QuestionBankService;
|
|
|
+use Illuminate\Http\JsonResponse;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Illuminate\Support\Facades\Validator;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 试卷提交分析控制器
|
|
|
+ *
|
|
|
+ * 接收前端提交的试卷答题数据,进行学情分析并写入错题本
|
|
|
+ */
|
|
|
+class PaperSubmitAnalysisController extends Controller
|
|
|
+{
|
|
|
+ public function __construct(
|
|
|
+ private readonly ExamAnswerAnalysisService $analysisService,
|
|
|
+ private readonly MistakeBookService $mistakeBookService,
|
|
|
+ private readonly QuestionBankService $questionBankService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交试卷答题数据进行分析
|
|
|
+ *
|
|
|
+ * POST /api/paper-submit-analysis
|
|
|
+ *
|
|
|
+ * 请求格式:
|
|
|
+ * {
|
|
|
+ * "paperId": "paper_661325736792",
|
|
|
+ * "questions": [
|
|
|
+ * {
|
|
|
+ * "question_bank_id": 876,
|
|
|
+ * "student_answer": "C",
|
|
|
+ * "is_correct": [0], // 0=错, 1=对;简答题分步骤 [1,0,1]
|
|
|
+ * "teacher_comment": null
|
|
|
+ * }
|
|
|
+ * ]
|
|
|
+ * }
|
|
|
+ */
|
|
|
+ public function analyze(Request $request): JsonResponse
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 1. 验证请求数据
|
|
|
+ $validator = Validator::make($request->all(), [
|
|
|
+ 'paperId' => 'required|string|max:255',
|
|
|
+ 'questions' => 'required|array|min:1',
|
|
|
+ 'questions.*.question_bank_id' => 'required|integer',
|
|
|
+ '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('paperId');
|
|
|
+ $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. 获取题目详情并转换数据格式
|
|
|
+ $transformedQuestions = $this->transformQuestionsData($questionsData, $paperId);
|
|
|
+
|
|
|
+ // 4. 调用学情分析服务(可能失败,不影响错题本写入)
|
|
|
+ $analysisResult = [];
|
|
|
+ $analysisError = null;
|
|
|
+ try {
|
|
|
+ $examData = [
|
|
|
+ 'exam_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 = [];
|
|
|
+ $questionBankIds = array_column($questionsData, 'question_bank_id');
|
|
|
+
|
|
|
+ // 批量获取题目详情
|
|
|
+ $questionBankData = $this->fetchQuestionBankData($questionBankIds);
|
|
|
+
|
|
|
+ // 获取试卷中题目的分数信息
|
|
|
+ $paperQuestions = PaperQuestion::where('paper_id', $paperId)
|
|
|
+ ->whereIn('question_bank_id', $questionBankIds)
|
|
|
+ ->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 = $questionBankData[$questionBankId] ?? null;
|
|
|
+ $paperQuestion = $paperQuestions->get($questionBankId);
|
|
|
+
|
|
|
+ // 确定题目分数
|
|
|
+ $maxScore = $paperQuestion->score ?? ($qbData['score'] ?? 5.0);
|
|
|
+ $kpCode = $qbData['kp_code'] ?? ($paperQuestion->knowledge_point ?? 'K-GENERAL');
|
|
|
+ $kpName = $qbData['kp_name'] ?? $kpCode;
|
|
|
+ $questionType = $qbData['question_type'] ?? ($paperQuestion->question_type ?? 'unknown');
|
|
|
+
|
|
|
+ // 保存详情供后续使用
|
|
|
+ $questionDetails[$questionBankId] = [
|
|
|
+ 'question_bank_id' => $questionBankId,
|
|
|
+ 'stem' => $qbData['stem'] ?? ($paperQuestion->question_text ?? ''),
|
|
|
+ 'answer' => $qbData['answer'] ?? ($paperQuestion->correct_answer ?? ''),
|
|
|
+ 'solution' => $qbData['solution'] ?? ($paperQuestion->solution ?? ''),
|
|
|
+ 'kp_code' => $kpCode,
|
|
|
+ 'kp_name' => $kpName,
|
|
|
+ 'question_type' => $questionType,
|
|
|
+ 'max_score' => $maxScore,
|
|
|
+ 'skills' => $qbData['skills'] ?? [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 转换 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
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'questions' => $transformedQuestions,
|
|
|
+ 'question_details' => $questionDetails,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量获取题库题目详情
|
|
|
+ */
|
|
|
+ private function fetchQuestionBankData(array $questionBankIds): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $response = $this->questionBankService->getQuestionsByIds($questionBankIds);
|
|
|
+ $questions = $response['data'] ?? $response;
|
|
|
+
|
|
|
+ // 转换为以 id 为 key 的数组
|
|
|
+ $result = [];
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $id = $question['id'] ?? $question['question_id'] ?? null;
|
|
|
+ if ($id) {
|
|
|
+ $result[$id] = $question;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $result;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::warning('获取题库详情失败,使用空数据', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'question_bank_ids' => $questionBankIds
|
|
|
+ ]);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将错题写入错题本
|
|
|
+ */
|
|
|
+ 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] ?? [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ $payload = [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'question_id' => $questionBankId,
|
|
|
+ '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'] ?? 'K-GENERAL'],
|
|
|
+ 'source' => "paper:{$paperId}",
|
|
|
+ 'happened_at' => now()->toISOString(),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $result = $this->mistakeBookService->createMistake($payload);
|
|
|
+
|
|
|
+ // 如果不是重复记录,则计数
|
|
|
+ if (!($result['duplicate'] ?? false)) {
|
|
|
+ $mistakesAdded++;
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::debug('错题写入成功', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'question_bank_id' => $questionBankId,
|
|
|
+ 'duplicate' => $result['duplicate'] ?? false
|
|
|
+ ]);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('写入错题本失败', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'question_bank_id' => $questionBankId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $mistakesAdded;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新试卷状态和题目作答信息
|
|
|
+ */
|
|
|
+ private function updatePaperStatus(string $paperId, array $questionsData): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 更新试卷状态为已完成
|
|
|
+ Paper::where('paper_id', $paperId)->update([
|
|
|
+ 'status' => 'completed',
|
|
|
+ 'completed_at' => now(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 更新每道题目的作答信息
|
|
|
+ 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(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('试卷状态更新完成', ['paper_id' => $paperId]);
|
|
|
+
|
|
|
+ } 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('pgsql')
|
|
|
+ ->table('exam_analysis_results')
|
|
|
+ ->where('exam_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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|