| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- <?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('mysql')
- ->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);
- }
- }
- }
|