| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Collection;
- /**
- * 考试答题分析服务(步骤级分析)
- * 基于卷子分析思考文档的思路实现
- *
- * 核心流程:
- * 1. 接收卷子ID和每道题的对错、简答题的分步骤对错
- * 2. 将原子信息映射到知识点/技能
- * 3. 计算知识点掌握度向量
- * 4. 生成详细分析报告
- * 5. 提供智能出卷推荐依据
- */
- class ExamAnswerAnalysisService
- {
- public function __construct(
- private readonly MasteryCalculator $masteryCalculator,
- private readonly KnowledgeMasteryService $knowledgeMasteryService,
- private readonly LocalAIAnalysisService $aiAnalysisService,
- private readonly QuestionBankService $questionBankService
- ) {}
- /**
- * 分析考试答题数据
- *
- * @param array $examData 考试数据
- * [
- * 'exam_id' => 'exam_001',
- * 'student_id' => 'student_001',
- * 'questions' => [
- * [
- * 'question_id' => 'Q1',
- * 'score' => 5,
- * 'score_obtained' => 5,
- * 'steps' => [
- * ['step_index' => 1, 'is_correct' => true, 'kp_id' => 'K-SQRT-SIMPLE'],
- * ['step_index' => 2, 'is_correct' => true, 'kp_id' => 'K-NUM-ADD-SUB']
- * ]
- * ]
- * ]
- * ]
- *
- * @return array 分析结果
- */
- public function analyzeExamAnswers(array $examData): array
- {
- Log::info('开始分析考试答题', [
- 'exam_id' => $examData['exam_id'] ?? 'unknown',
- 'student_id' => $examData['student_id'] ?? 'unknown',
- 'question_count' => count($examData['questions'] ?? [])
- ]);
- $studentId = $examData['student_id'];
- $questions = $examData['questions'] ?? [];
- // 1. 保存答题记录到数据库
- $this->saveExamAnswerRecords($examData);
- // 2. 获取题目知识点映射
- $questionMappings = $this->getQuestionKnowledgeMappings($questions);
- // 3. 计算每个知识点的加权掌握度
- $knowledgeMasteryVector = $this->calculateKnowledgeMasteryVector($questions, $questionMappings);
- // 4. 更新学生掌握度
- $updatedMastery = $this->updateStudentMastery($studentId, $knowledgeMasteryVector);
- // 5. 生成题目维度分析
- $questionAnalysis = $this->analyzeQuestions($questions, $questionMappings);
- // 6. 生成知识点维度分析
- $knowledgePointAnalysis = $this->analyzeKnowledgePoints($knowledgeMasteryVector, $questionMappings);
- // 7. 生成整体掌握度总结
- $overallSummary = $this->generateOverallSummary($updatedMastery);
- // 8. 生成智能出卷推荐依据
- $smartQuizRecommendation = $this->generateSmartQuizRecommendation($updatedMastery);
- // 9. 保存分析结果
- $analysisResult = [
- 'exam_id' => $examData['exam_id'],
- 'student_id' => $studentId,
- 'timestamp' => now()->toISOString(),
- 'question_analysis' => $questionAnalysis,
- 'knowledge_point_analysis' => $knowledgePointAnalysis,
- 'overall_summary' => $overallSummary,
- 'smart_quiz_recommendation' => $smartQuizRecommendation,
- 'mastery_vector' => $updatedMastery,
- ];
- $this->saveAnalysisResult($studentId, $examData['exam_id'], $analysisResult);
- Log::info('考试答题分析完成', [
- 'student_id' => $studentId,
- 'exam_id' => $examData['exam_id'],
- 'analyzed_knowledge_points' => count($knowledgeMasteryVector)
- ]);
- return $analysisResult;
- }
- /**
- * 获取题目知识点映射
- */
- private function getQuestionKnowledgeMappings(array $questions): array
- {
- $mappings = [];
- $questionIds = array_column($questions, 'question_id');
- // 从题库获取题目知识点映射
- try {
- $response = $this->questionBankService->getQuestionsByIds($questionIds);
- $questionsData = $response['data'] ?? $response;
- foreach ($questionsData as $questionData) {
- $questionId = $questionData['id'] ?? $questionData['question_id'];
- if (!$questionId) continue;
- // 提取知识点信息
- $kpMapping = [];
- if (!empty($questionData['kp_code'])) {
- $kpMapping[] = [
- 'kp_id' => $questionData['kp_code'],
- 'kp_name' => $questionData['kp_name'] ?? $questionData['kp_code'],
- 'weight' => 1.0
- ];
- } else {
- $kpMapping[] = [
- 'kp_id' => 'K-GENERAL',
- 'kp_name' => '综合',
- 'weight' => 1.0
- ];
- }
- $mappings[$questionId] = [
- 'question_id' => $questionId,
- 'kp_mapping' => $kpMapping
- ];
- }
- } catch (\Exception $e) {
- Log::warning('获取题目知识点映射失败,使用默认映射', [
- 'error' => $e->getMessage(),
- 'question_ids' => $questionIds
- ]);
- // 使用默认映射:每道题至少映射到一个知识点
- foreach ($questions as $question) {
- $mappings[$question['question_id']] = [
- 'question_id' => $question['question_id'],
- 'kp_mapping' => [
- ['kp_id' => 'K-GENERAL', 'kp_name' => '综合', 'weight' => 1.0]
- ]
- ];
- }
- }
- return $mappings;
- }
- /**
- * 计算知识点掌握度向量
- * 基于文档中的简单实用更新公式
- */
- private function calculateKnowledgeMasteryVector(array $questions, array $questionMappings): array
- {
- $knowledgeScores = [];
- foreach ($questions as $question) {
- $questionId = $question['question_id'];
- $score = floatval($question['score_obtained'] ?? 0);
- $maxScore = floatval($question['score'] ?? $score);
- $steps = $question['steps'] ?? [];
- $mapping = $questionMappings[$questionId] ?? null;
- if (!$mapping || !isset($mapping['kp_mapping'])) {
- continue;
- }
- // 如果有步骤级分析,使用步骤分析
- if (!empty($steps)) {
- foreach ($steps as $step) {
- $kpId = $step['kp_id'] ?? 'K-GENERAL';
- $stepScore = floatval($step['score'] ?? ($maxScore / count($steps)));
- $stepWeight = floatval($step['weight'] ?? 1.0);
- if (!isset($knowledgeScores[$kpId])) {
- $knowledgeScores[$kpId] = [
- 'total_weight' => 0,
- 'correct_weight' => 0,
- 'step_details' => []
- ];
- }
- $knowledgeScores[$kpId]['total_weight'] += $stepScore * $stepWeight;
- if ($step['is_correct']) {
- $knowledgeScores[$kpId]['correct_weight'] += $stepScore * $stepWeight;
- }
- $knowledgeScores[$kpId]['step_details'][] = [
- 'question_id' => $questionId,
- 'step_index' => $step['step_index'],
- 'score' => $stepScore,
- 'is_correct' => $step['is_correct']
- ];
- }
- } else {
- // 没有步骤级分析,使用题目整体分析
- foreach ($mapping['kp_mapping'] as $kpMapping) {
- $kpId = $kpMapping['kp_id'];
- $weight = floatval($kpMapping['weight'] ?? 1.0);
- $kpMaxScore = $maxScore * $weight;
- if (!isset($knowledgeScores[$kpId])) {
- $knowledgeScores[$kpId] = [
- 'total_weight' => 0,
- 'correct_weight' => 0,
- 'step_details' => []
- ];
- }
- $knowledgeScores[$kpId]['total_weight'] += $kpMaxScore;
- if ($score > 0) {
- $knowledgeScores[$kpId]['correct_weight'] += $score * $weight;
- }
- }
- }
- }
- // 计算掌握度
- $masteryVector = [];
- foreach ($knowledgeScores as $kpId => $data) {
- $mastery = $data['total_weight'] > 0
- ? $data['correct_weight'] / $data['total_weight']
- : 0;
- // 置信度校正:考得越多,评价越稳定
- $confidence = 1 - exp(-$data['total_weight'] / 5);
- $masteryVector[$kpId] = [
- 'kp_id' => $kpId,
- 'mastery' => $mastery,
- 'confidence' => $confidence,
- 'total_weight' => $data['total_weight'],
- 'correct_weight' => $data['correct_weight'],
- 'step_details' => $data['step_details'],
- ];
- }
- return $masteryVector;
- }
- /**
- * 更新学生掌握度(与历史数据合并)
- */
- private function updateStudentMastery(string $studentId, array $knowledgeMasteryVector): array
- {
- $updatedMastery = [];
- foreach ($knowledgeMasteryVector as $kpId => $data) {
- // 获取历史掌握度
- $historyMastery = DB::connection('mysql')
- ->table('student_knowledge_mastery')
- ->where('student_id', $studentId)
- ->where('kp_code', $kpId)
- ->first();
- $historyMasteryLevel = $historyMastery->mastery_level ?? 0.5;
- $historyWeight = $historyMastery->total_attempts ?? 0;
- $currentWeight = $data['total_weight'];
- // 合并计算:历史权重 + 当前权重
- $newMastery = $historyWeight > 0
- ? ($historyWeight * $historyMasteryLevel + $currentWeight * $data['mastery'])
- / ($historyWeight + $currentWeight)
- : $data['mastery'];
- $newConfidence = $data['confidence'];
- // 保存到数据库
- DB::connection('mysql')
- ->table('student_knowledge_mastery')
- ->updateOrInsert(
- ['student_id' => $studentId, 'kp_code' => $kpId],
- [
- 'mastery_level' => $newMastery,
- 'confidence_level' => $newConfidence,
- 'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
- 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_weight'] > 0),
- 'mastery_trend' => $this->determineMasteryTrend($historyMasteryLevel, $newMastery),
- 'last_mastery_update' => now(),
- 'updated_at' => now(),
- ]
- );
- $updatedMastery[$kpId] = [
- 'kp_id' => $kpId,
- 'current_mastery' => $newMastery,
- 'previous_mastery' => $historyMasteryLevel,
- 'confidence' => $newConfidence,
- 'change' => $newMastery - $historyMasteryLevel,
- 'weight' => $currentWeight
- ];
- }
- return $updatedMastery;
- }
- /**
- * 生成题目维度分析
- */
- private function analyzeQuestions(array $questions, array $questionMappings): array
- {
- $analysis = [];
- foreach ($questions as $question) {
- $questionId = $question['question_id'];
- $score = floatval($question['score_obtained'] ?? 0);
- $maxScore = floatval($question['score'] ?? $score);
- $steps = $question['steps'] ?? [];
- $mapping = $questionMappings[$questionId] ?? ['kp_mapping' => []];
- // 步骤分析
- $stepAnalysis = [];
- if (!empty($steps)) {
- foreach ($steps as $step) {
- $kpId = $step['kp_id'] ?? 'K-GENERAL';
- $stepAnalysis[] = [
- 'step_index' => $step['step_index'],
- 'is_correct' => $step['is_correct'],
- 'kp_id' => $kpId,
- 'description' => $step['description'] ?? ''
- ];
- }
- }
- // 知识点关联
- $knowledgePoints = array_map(function($kp) {
- return [
- 'kp_id' => $kp['kp_id'],
- 'kp_name' => $kp['kp_name'] ?? $kp['kp_id'],
- 'weight' => $kp['weight'] ?? 1.0
- ];
- }, $mapping['kp_mapping']);
- $analysis[] = [
- 'question_id' => $questionId,
- 'score_obtained' => $score,
- 'max_score' => $maxScore,
- 'accuracy_rate' => $maxScore > 0 ? $score / $maxScore : 0,
- 'step_analysis' => $stepAnalysis,
- 'knowledge_points' => $knowledgePoints,
- 'performance_summary' => $this->generateQuestionPerformanceSummary($question, $stepAnalysis)
- ];
- }
- return $analysis;
- }
- /**
- * 生成知识点维度分析
- */
- private function analyzeKnowledgePoints(array $knowledgeMasteryVector, array $questionMappings): array
- {
- $analysis = [];
- foreach ($knowledgeMasteryVector as $kpId => $data) {
- $analysis[] = [
- 'kp_id' => $kpId,
- 'mastery_level' => $data['mastery'],
- 'confidence_level' => $data['confidence'],
- 'performance_in_exam' => $this->evaluatePerformanceLevel($data['mastery']),
- 'evidence_count' => count($data['step_details']),
- 'step_evidence' => $data['step_details'],
- 'recommendation' => $this->generateKnowledgePointRecommendation($data)
- ];
- }
- return $analysis;
- }
- /**
- * 生成整体掌握度总结
- */
- private function generateOverallSummary(array $updatedMastery): array
- {
- $knowledgePoints = array_values($updatedMastery);
- if (empty($knowledgePoints)) {
- return [
- 'total_knowledge_points' => 0,
- 'average_mastery' => 0,
- 'mastery_distribution' => [
- 'mastered' => 0,
- 'good' => 0,
- 'weak' => 0
- ],
- 'top_strengths' => [],
- 'top_weaknesses' => []
- ];
- }
- // 计算平均掌握度
- $averageMastery = array_sum(array_column($knowledgePoints, 'current_mastery')) / count($knowledgePoints);
- // 掌握度分布
- $mastered = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] >= 0.85);
- $good = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] >= 0.70 && $kp['current_mastery'] < 0.85);
- $weak = array_filter($knowledgePoints, fn($kp) => $kp['current_mastery'] < 0.70);
- // 排序找出优势和薄弱点
- usort($knowledgePoints, fn($a, $b) => $b['current_mastery'] <=> $a['current_mastery']);
- $topStrengths = array_slice($knowledgePoints, 0, 3);
- $topWeaknesses = array_slice(array_reverse($knowledgePoints), 0, 3);
- return [
- 'total_knowledge_points' => count($knowledgePoints),
- 'average_mastery' => round($averageMastery, 4),
- 'mastery_distribution' => [
- 'mastered' => count($mastered),
- 'good' => count($good),
- 'weak' => count($weak)
- ],
- 'top_strengths' => $topStrengths,
- 'top_weaknesses' => $topWeaknesses,
- 'overall_performance' => $this->evaluateOverallPerformance($averageMastery)
- ];
- }
- /**
- * 生成智能出卷推荐依据
- * 基于文档中的推荐优先级算法
- */
- private function generateSmartQuizRecommendation(array $updatedMastery): array
- {
- $recommendations = [];
- foreach ($updatedMastery as $kpId => $data) {
- $mastery = $data['current_mastery'];
- $confidence = $data['confidence'];
- $weight = $data['weight'];
- // 推荐优先级 = (1 - 掌握度) * 重要性 * 覆盖需求
- // 重要性可以根据知识点在中考/阶段考试中的权重,这里简化为1.0
- $importance = 1.0;
- // 覆盖需求:最近没考过或考得少,值大
- $coverageNeed = max(1.0, 1.5 - ($weight / 10));
- $priority = (1 - $mastery) * $importance * $coverageNeed;
- $recommendations[] = [
- 'kp_id' => $kpId,
- 'current_mastery' => $mastery,
- 'priority' => $priority,
- 'recommended_questions' => $this->calculateRecommendedQuestions($mastery),
- 'focus_type' => $this->determineFocusType($mastery)
- ];
- }
- // 按优先级排序
- usort($recommendations, fn($a, $b) => $b['priority'] <=> $a['priority']);
- // 控制难度节奏:40%巩固型 + 40%修补型 + 20%挑战型
- $totalRecommendations = count($recommendations);
- $consolidation = array_slice($recommendations, 0, intval($totalRecommendations * 0.4));
- $remediation = array_slice($recommendations, intval($totalRecommendations * 0.4), intval($totalRecommendations * 0.4));
- $challenge = array_slice($recommendations, intval($totalRecommendations * 0.8));
- return [
- 'priority_list' => $recommendations,
- 'quiz_structure' => [
- 'consolidation_type' => $consolidation,
- 'remediation_type' => $remediation,
- 'challenge_type' => $challenge
- ],
- 'total_recommended_questions' => array_sum(array_column($recommendations, 'recommended_questions'))
- ];
- }
- /**
- * 保存考试答题记录
- */
- private function saveExamAnswerRecords(array $examData): void
- {
- $studentId = $examData['student_id'];
- $examId = $examData['exam_id'];
- foreach ($examData['questions'] as $question) {
- $questionId = $question['question_id'];
- $steps = $question['steps'] ?? [];
- // 保存步骤级记录
- if (!empty($steps)) {
- foreach ($steps as $step) {
- DB::connection('mysql')->table('student_answer_steps')->insert([
- 'student_id' => $studentId,
- 'exam_id' => $examId,
- 'question_id' => $questionId,
- 'step_index' => $step['step_index'],
- 'kp_id' => $step['kp_id'] ?? 'K-GENERAL',
- 'is_correct' => $step['is_correct'],
- 'step_score' => $step['score'] ?? 0,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- } else {
- // 保存题目级记录
- DB::connection('mysql')->table('student_answer_questions')->insert([
- 'student_id' => $studentId,
- 'exam_id' => $examId,
- 'question_id' => $questionId,
- 'score_obtained' => $question['score_obtained'] ?? 0,
- 'max_score' => $question['score'] ?? 0,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- }
- }
- /**
- * 保存分析结果
- */
- private function saveAnalysisResult(string $studentId, string $examId, array $result): void
- {
- DB::connection('mysql')->table('exam_analysis_results')->insert([
- 'student_id' => $studentId,
- 'exam_id' => $examId,
- 'analysis_data' => json_encode($result),
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- /**
- * 判断掌握度趋势
- */
- private function determineMasteryTrend(float $previous, float $current): string
- {
- $change = $current - $previous;
- if ($change > 0.1) {
- return 'improving';
- } elseif ($change < -0.1) {
- return 'declining';
- } else {
- return 'stable';
- }
- }
- /**
- * 评估表现水平
- */
- private function evaluatePerformanceLevel(float $mastery): string
- {
- if ($mastery >= 0.85) {
- return 'excellent';
- } elseif ($mastery >= 0.70) {
- return 'good';
- } elseif ($mastery >= 0.50) {
- return 'fair';
- } else {
- return 'poor';
- }
- }
- /**
- * 生成题目表现总结
- */
- private function generateQuestionPerformanceSummary(array $question, array $stepAnalysis): string
- {
- if (empty($stepAnalysis)) {
- return '整题作答';
- }
- $correctSteps = count(array_filter($stepAnalysis, fn($s) => $s['is_correct']));
- $totalSteps = count($stepAnalysis);
- if ($correctSteps === $totalSteps) {
- return '所有步骤正确';
- } elseif ($correctSteps > 0) {
- return "部分正确 ({$correctSteps}/{$totalSteps} 步骤正确)";
- } else {
- return '所有步骤错误';
- }
- }
- /**
- * 生成知识点建议
- */
- private function generateKnowledgePointRecommendation(array $data): string
- {
- $mastery = $data['mastery'];
- if ($mastery >= 0.85) {
- return '掌握良好,可安排综合练习';
- } elseif ($mastery >= 0.70) {
- return '基本掌握,建议加强练习';
- } elseif ($mastery >= 0.50) {
- return '需要重点练习,建议安排专项训练';
- } else {
- return '薄弱知识点,建议系统学习和大量练习';
- }
- }
- /**
- * 评估整体表现
- */
- private function evaluateOverallPerformance(float $averageMastery): string
- {
- if ($averageMastery >= 0.85) {
- return '优秀';
- } elseif ($averageMastery >= 0.70) {
- return '良好';
- } elseif ($averageMastery >= 0.50) {
- return '一般';
- } else {
- return '需加强';
- }
- }
- /**
- * 计算推荐题目数量
- */
- private function calculateRecommendedQuestions(float $mastery): int
- {
- if ($mastery >= 0.85) {
- return 1; // 巩固型:1题
- } elseif ($mastery >= 0.50) {
- return 2; // 修补型:2题
- } else {
- return 3; // 挑战型:3题
- }
- }
- /**
- * 确定重点类型
- */
- private function determineFocusType(float $mastery): string
- {
- if ($mastery >= 0.70 && $mastery < 0.85) {
- return 'consolidation'; // 巩固型
- } elseif ($mastery < 0.70) {
- return 'remediation'; // 修补型
- } else {
- return 'challenge'; // 挑战型
- }
- }
- }
|