|
|
@@ -22,8 +22,7 @@ class ExamAnswerAnalysisService
|
|
|
public function __construct(
|
|
|
private readonly MasteryCalculator $masteryCalculator,
|
|
|
private readonly KnowledgeMasteryService $knowledgeMasteryService,
|
|
|
- private readonly LocalAIAnalysisService $aiAnalysisService,
|
|
|
- private readonly QuestionBankService $questionBankService
|
|
|
+ private readonly LocalAIAnalysisService $aiAnalysisService
|
|
|
) {}
|
|
|
|
|
|
/**
|
|
|
@@ -31,7 +30,7 @@ class ExamAnswerAnalysisService
|
|
|
*
|
|
|
* @param array $examData 考试数据
|
|
|
* [
|
|
|
- * 'exam_id' => 'exam_001',
|
|
|
+ * 'paper_id' => 'exam_001',
|
|
|
* 'student_id' => 'student_001',
|
|
|
* 'questions' => [
|
|
|
* [
|
|
|
@@ -51,7 +50,7 @@ class ExamAnswerAnalysisService
|
|
|
public function analyzeExamAnswers(array $examData): array
|
|
|
{
|
|
|
Log::info('开始分析考试答题', [
|
|
|
- 'exam_id' => $examData['exam_id'] ?? 'unknown',
|
|
|
+ 'paper_id' => $examData['paper_id'] ?? 'unknown',
|
|
|
'student_id' => $examData['student_id'] ?? 'unknown',
|
|
|
'question_count' => count($examData['questions'] ?? [])
|
|
|
]);
|
|
|
@@ -85,7 +84,7 @@ class ExamAnswerAnalysisService
|
|
|
|
|
|
// 9. 保存分析结果
|
|
|
$analysisResult = [
|
|
|
- 'exam_id' => $examData['exam_id'],
|
|
|
+ 'paper_id' => $examData['paper_id'],
|
|
|
'student_id' => $studentId,
|
|
|
'timestamp' => now()->toISOString(),
|
|
|
'question_analysis' => $questionAnalysis,
|
|
|
@@ -95,11 +94,11 @@ class ExamAnswerAnalysisService
|
|
|
'mastery_vector' => $updatedMastery,
|
|
|
];
|
|
|
|
|
|
- $this->saveAnalysisResult($studentId, $examData['exam_id'], $analysisResult);
|
|
|
+ $this->saveAnalysisResult($studentId, $examData['paper_id'], $analysisResult);
|
|
|
|
|
|
Log::info('考试答题分析完成', [
|
|
|
'student_id' => $studentId,
|
|
|
- 'exam_id' => $examData['exam_id'],
|
|
|
+ 'paper_id' => $examData['paper_id'],
|
|
|
'analyzed_knowledge_points' => count($knowledgeMasteryVector)
|
|
|
]);
|
|
|
|
|
|
@@ -112,147 +111,167 @@ class ExamAnswerAnalysisService
|
|
|
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
|
|
|
+ // 直接从题目数据中提取知识点信息(不再调用外部服务)
|
|
|
+ foreach ($questions as $question) {
|
|
|
+ $questionId = $question['question_id'] ?? null;
|
|
|
+ if (!$questionId) continue;
|
|
|
+
|
|
|
+ // 提取知识点信息(优先使用请求数据中的字段)
|
|
|
+ $kpMapping = [];
|
|
|
+
|
|
|
+ // 尝试多个可能的知识点字段
|
|
|
+ $kpCode = $question['kp_code']
|
|
|
+ ?? $question['knowledge_point']
|
|
|
+ ?? $question['kp_code']
|
|
|
+ ?? null;
|
|
|
+
|
|
|
+ if (!empty($kpCode)) {
|
|
|
+ $kpMapping[] = [
|
|
|
+ 'kp_id' => $kpCode,
|
|
|
+ 'kp_name' => $question['kp_name'] ?? $kpCode,
|
|
|
+ 'weight' => 1.0
|
|
|
];
|
|
|
- }
|
|
|
- } 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]
|
|
|
- ]
|
|
|
+ } else {
|
|
|
+ // 如果没有知识点信息,使用默认的综合知识点
|
|
|
+ $kpMapping[] = [
|
|
|
+ 'kp_id' => 'K-GENERAL',
|
|
|
+ 'kp_name' => '综合',
|
|
|
+ 'weight' => 1.0
|
|
|
];
|
|
|
}
|
|
|
+
|
|
|
+ $mappings[$questionId] = [
|
|
|
+ 'question_id' => $questionId,
|
|
|
+ 'kp_mapping' => $kpMapping
|
|
|
+ ];
|
|
|
}
|
|
|
|
|
|
+ Log::info('题目知识点映射构建完成', [
|
|
|
+ 'total_questions' => count($questions),
|
|
|
+ 'mapped_questions' => count($mappings),
|
|
|
+ ]);
|
|
|
+
|
|
|
return $mappings;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 计算知识点掌握度向量
|
|
|
- * 基于文档中的简单实用更新公式
|
|
|
+ * 【修复】集成MasteryCalculator的BKT模型进行精确计算
|
|
|
+ *
|
|
|
+ * 核心算法说明:
|
|
|
+ * 1. 从考试答题中提取每个知识点的答题记录
|
|
|
+ * 2. 调用MasteryCalculator计算掌握度(包含:正确率、难度加权、时间效率、遗忘曲线)
|
|
|
+ * 3. 返回包含掌握度、置信度、趋势等完整信息的向量
|
|
|
*/
|
|
|
private function calculateKnowledgeMasteryVector(array $questions, array $questionMappings): array
|
|
|
{
|
|
|
- $knowledgeScores = [];
|
|
|
+ // 按知识点聚合答题记录
|
|
|
+ $knowledgeAttempts = [];
|
|
|
|
|
|
foreach ($questions as $question) {
|
|
|
$questionId = $question['question_id'];
|
|
|
$score = floatval($question['score_obtained'] ?? 0);
|
|
|
$maxScore = floatval($question['score'] ?? $score);
|
|
|
$steps = $question['steps'] ?? [];
|
|
|
+ $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
|
|
|
|
|
|
$mapping = $questionMappings[$questionId] ?? null;
|
|
|
if (!$mapping || !isset($mapping['kp_mapping'])) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
+ // 构建答题记录(用于MasteryCalculator)
|
|
|
+ $attemptRecord = [
|
|
|
+ 'question_id' => $questionId,
|
|
|
+ 'is_correct' => $isCorrect,
|
|
|
+ 'partial_score' => $maxScore > 0 ? $score / $maxScore : 0,
|
|
|
+ 'question_difficulty' => $question['difficulty'] ?? 0.6,
|
|
|
+ 'attempt_time_seconds' => $question['time_spent'] ?? 120,
|
|
|
+ 'completed_at' => now()->toISOString(),
|
|
|
+ 'created_at' => now()->toISOString(),
|
|
|
+ ];
|
|
|
+
|
|
|
// 如果有步骤级分析,使用步骤分析
|
|
|
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' => []
|
|
|
+
|
|
|
+ if (!isset($knowledgeAttempts[$kpId])) {
|
|
|
+ $knowledgeAttempts[$kpId] = [
|
|
|
+ 'attempts' => [],
|
|
|
+ 'step_details' => [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- $knowledgeScores[$kpId]['total_weight'] += $stepScore * $stepWeight;
|
|
|
- if ($step['is_correct']) {
|
|
|
- $knowledgeScores[$kpId]['correct_weight'] += $stepScore * $stepWeight;
|
|
|
- }
|
|
|
+ // 每个步骤作为独立答题记录
|
|
|
+ $stepAttempt = $attemptRecord;
|
|
|
+ $stepAttempt['is_correct'] = $step['is_correct'];
|
|
|
+ $stepAttempt['step_index'] = $step['step_index'];
|
|
|
|
|
|
- $knowledgeScores[$kpId]['step_details'][] = [
|
|
|
+ $knowledgeAttempts[$kpId]['attempts'][] = $stepAttempt;
|
|
|
+ $knowledgeAttempts[$kpId]['step_details'][] = [
|
|
|
'question_id' => $questionId,
|
|
|
'step_index' => $step['step_index'],
|
|
|
- 'score' => $stepScore,
|
|
|
- 'is_correct' => $step['is_correct']
|
|
|
+ 'score' => $step['score'] ?? ($maxScore / count($steps)),
|
|
|
+ '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' => []
|
|
|
+
|
|
|
+ if (!isset($knowledgeAttempts[$kpId])) {
|
|
|
+ $knowledgeAttempts[$kpId] = [
|
|
|
+ 'attempts' => [],
|
|
|
+ 'step_details' => [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- $knowledgeScores[$kpId]['total_weight'] += $kpMaxScore;
|
|
|
- if ($score > 0) {
|
|
|
- $knowledgeScores[$kpId]['correct_weight'] += $score * $weight;
|
|
|
- }
|
|
|
+ $knowledgeAttempts[$kpId]['attempts'][] = $attemptRecord;
|
|
|
+ $knowledgeAttempts[$kpId]['step_details'][] = [
|
|
|
+ 'question_id' => $questionId,
|
|
|
+ 'score' => $score,
|
|
|
+ 'max_score' => $maxScore,
|
|
|
+ 'is_correct' => $isCorrect,
|
|
|
+ ];
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 计算掌握度
|
|
|
+ // 【核心】使用MasteryCalculator计算每个知识点的掌握度
|
|
|
$masteryVector = [];
|
|
|
- foreach ($knowledgeScores as $kpId => $data) {
|
|
|
- $mastery = $data['total_weight'] > 0
|
|
|
- ? $data['correct_weight'] / $data['total_weight']
|
|
|
- : 0;
|
|
|
+ foreach ($knowledgeAttempts as $kpId => $data) {
|
|
|
+ $attempts = $data['attempts'];
|
|
|
|
|
|
- // 置信度校正:考得越多,评价越稳定
|
|
|
- $confidence = 1 - exp(-$data['total_weight'] / 5);
|
|
|
+ // 调用MasteryCalculator的核心算法
|
|
|
+ // 该算法包含:正确率、难度加权、时间效率、技能熟练度、遗忘曲线衰减
|
|
|
+ $masteryResult = $this->masteryCalculator->calculateMasteryLevel(
|
|
|
+ '', // studentId在此不需要,因为直接传入attempts
|
|
|
+ $kpId,
|
|
|
+ $attempts
|
|
|
+ );
|
|
|
|
|
|
$masteryVector[$kpId] = [
|
|
|
'kp_id' => $kpId,
|
|
|
- 'mastery' => $mastery,
|
|
|
- 'confidence' => $confidence,
|
|
|
- 'total_weight' => $data['total_weight'],
|
|
|
- 'correct_weight' => $data['correct_weight'],
|
|
|
+ 'mastery' => $masteryResult['mastery'],
|
|
|
+ 'confidence' => $masteryResult['confidence'],
|
|
|
+ 'trend' => $masteryResult['trend'],
|
|
|
+ 'total_attempts' => $masteryResult['total_attempts'],
|
|
|
+ 'correct_attempts' => $masteryResult['correct_attempts'],
|
|
|
+ 'accuracy_rate' => $masteryResult['accuracy_rate'],
|
|
|
'step_details' => $data['step_details'],
|
|
|
+ // 计算细节(用于调试和分析)
|
|
|
+ 'calculation_details' => $masteryResult['details'] ?? [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ Log::info('知识点掌握度向量计算完成', [
|
|
|
+ 'knowledge_points_count' => count($masteryVector),
|
|
|
+ 'sample' => array_slice($masteryVector, 0, 3, true),
|
|
|
+ ]);
|
|
|
+
|
|
|
return $masteryVector;
|
|
|
}
|
|
|
|
|
|
@@ -273,7 +292,7 @@ class ExamAnswerAnalysisService
|
|
|
|
|
|
$historyMasteryLevel = $historyMastery->mastery_level ?? 0.5;
|
|
|
$historyWeight = $historyMastery->total_attempts ?? 0;
|
|
|
- $currentWeight = $data['total_weight'];
|
|
|
+ $currentWeight = $data['total_attempts'] ?? 1;
|
|
|
|
|
|
// 合并计算:历史权重 + 当前权重
|
|
|
$newMastery = $historyWeight > 0
|
|
|
@@ -292,7 +311,7 @@ class ExamAnswerAnalysisService
|
|
|
'mastery_level' => $newMastery,
|
|
|
'confidence_level' => $newConfidence,
|
|
|
'total_attempts' => ($historyMastery->total_attempts ?? 0) + 1,
|
|
|
- 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_weight'] > 0),
|
|
|
+ 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + intval($data['correct_attempts'] > 0),
|
|
|
'mastery_trend' => $this->determineMasteryTrend($historyMasteryLevel, $newMastery),
|
|
|
'last_mastery_update' => now(),
|
|
|
'updated_at' => now(),
|
|
|
@@ -313,7 +332,7 @@ class ExamAnswerAnalysisService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成题目维度分析
|
|
|
+ * 生成题目维度分析(包含AI分析和解题思路)
|
|
|
*/
|
|
|
private function analyzeQuestions(array $questions, array $questionMappings): array
|
|
|
{
|
|
|
@@ -324,8 +343,10 @@ class ExamAnswerAnalysisService
|
|
|
$score = floatval($question['score_obtained'] ?? 0);
|
|
|
$maxScore = floatval($question['score'] ?? $score);
|
|
|
$steps = $question['steps'] ?? [];
|
|
|
+ $isCorrect = $question['is_correct'] ?? ($score >= $maxScore);
|
|
|
|
|
|
$mapping = $questionMappings[$questionId] ?? ['kp_mapping' => []];
|
|
|
+ $kpCode = $mapping['kp_mapping'][0]['kp_id'] ?? 'K-GENERAL';
|
|
|
|
|
|
// 步骤分析
|
|
|
$stepAnalysis = [];
|
|
|
@@ -350,20 +371,111 @@ class ExamAnswerAnalysisService
|
|
|
];
|
|
|
}, $mapping['kp_mapping']);
|
|
|
|
|
|
+ // 【集成】调用AI分析服务,获取解题思路和错误分析
|
|
|
+ $aiAnalysis = $this->getQuestionAIAnalysis($question, $mapping);
|
|
|
+
|
|
|
$analysis[] = [
|
|
|
'question_id' => $questionId,
|
|
|
'score_obtained' => $score,
|
|
|
'max_score' => $maxScore,
|
|
|
'accuracy_rate' => $maxScore > 0 ? $score / $maxScore : 0,
|
|
|
+ 'is_correct' => $isCorrect,
|
|
|
'step_analysis' => $stepAnalysis,
|
|
|
'knowledge_points' => $knowledgePoints,
|
|
|
- 'performance_summary' => $this->generateQuestionPerformanceSummary($question, $stepAnalysis)
|
|
|
+ 'performance_summary' => $this->generateQuestionPerformanceSummary($question, $stepAnalysis),
|
|
|
+ // 【新增】解题思路和错误分析
|
|
|
+ 'solution_process' => $aiAnalysis['solution_process'] ?? '',
|
|
|
+ 'error_analysis' => $aiAnalysis['error_analysis'] ?? '',
|
|
|
+ 'mistake_type' => $aiAnalysis['mistake_type'] ?? '',
|
|
|
+ 'suggestions' => $aiAnalysis['suggestions'] ?? '',
|
|
|
+ 'next_steps' => $aiAnalysis['next_steps'] ?? [],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
return $analysis;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取题目的AI分析(解题思路、错误分析)
|
|
|
+ */
|
|
|
+ private function getQuestionAIAnalysis(array $question, array $mapping): array
|
|
|
+ {
|
|
|
+ $isCorrect = $question['is_correct'] ?? false;
|
|
|
+ $score = floatval($question['score_obtained'] ?? 0);
|
|
|
+ $maxScore = floatval($question['score'] ?? 10);
|
|
|
+ $kpCode = $mapping['kp_mapping'][0]['kp_id'] ?? 'K-GENERAL';
|
|
|
+
|
|
|
+ // 调用LocalAIAnalysisService进行分析
|
|
|
+ try {
|
|
|
+ $analysisResult = $this->aiAnalysisService->analyzeAnswer([
|
|
|
+ 'question_id' => $question['question_id'],
|
|
|
+ 'question_text' => $question['question_text'] ?? '',
|
|
|
+ 'student_answer' => $question['student_answer'] ?? '',
|
|
|
+ 'correct_answer' => $question['correct_answer'] ?? '',
|
|
|
+ 'score' => $score,
|
|
|
+ 'max_score' => $maxScore,
|
|
|
+ 'kp_code' => $kpCode,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $data = $analysisResult['data'] ?? [];
|
|
|
+
|
|
|
+ // 根据正确性生成不同的解题思路
|
|
|
+ if ($isCorrect) {
|
|
|
+ return [
|
|
|
+ 'solution_process' => $data['correct_solution'] ?? '该题作答正确,解题思路清晰',
|
|
|
+ 'error_analysis' => '',
|
|
|
+ 'mistake_type' => '',
|
|
|
+ 'suggestions' => $data['suggestions'] ?? '继续保持良好的解题习惯',
|
|
|
+ 'next_steps' => $data['next_steps'] ?? ['尝试更高难度的同类题目'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 错误题目:返回详细分析
|
|
|
+ return [
|
|
|
+ 'solution_process' => $data['correct_solution'] ?? '请参考标准解题步骤',
|
|
|
+ 'error_analysis' => $data['reason'] ?? '解题过程中存在错误',
|
|
|
+ 'mistake_type' => $data['mistake_type'] ?? '计算或理解错误',
|
|
|
+ 'suggestions' => $data['suggestions'] ?? '建议针对薄弱知识点进行专项练习',
|
|
|
+ 'next_steps' => $data['next_steps'] ?? ['复习相关知识点', '做同类型练习题'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::warning('AI分析失败,使用默认分析', [
|
|
|
+ 'question_id' => $question['question_id'],
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 回退到基础分析
|
|
|
+ return $this->getFallbackAnalysis($question, $isCorrect);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 回退分析(当AI分析失败时)
|
|
|
+ */
|
|
|
+ private function getFallbackAnalysis(array $question, bool $isCorrect): array
|
|
|
+ {
|
|
|
+ if ($isCorrect) {
|
|
|
+ return [
|
|
|
+ 'solution_process' => '该题作答正确',
|
|
|
+ 'error_analysis' => '',
|
|
|
+ 'mistake_type' => '',
|
|
|
+ 'suggestions' => '继续保持',
|
|
|
+ 'next_steps' => ['尝试更高难度的题目'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $scoreRatio = floatval($question['score_obtained'] ?? 0) / max(floatval($question['score'] ?? 1), 1);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'solution_process' => '请参考标准答案和解题步骤',
|
|
|
+ 'error_analysis' => $scoreRatio < 0.3 ? '知识点理解存在偏差' : '解题过程中出现错误',
|
|
|
+ 'mistake_type' => $scoreRatio < 0.3 ? '概念错误' : '计算/步骤错误',
|
|
|
+ 'suggestions' => '建议复习相关知识点,加强练习',
|
|
|
+ 'next_steps' => ['复习基础概念', '做同类型练习题', '请教老师或同学'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成知识点维度分析
|
|
|
*/
|
|
|
@@ -491,7 +603,18 @@ class ExamAnswerAnalysisService
|
|
|
private function saveExamAnswerRecords(array $examData): void
|
|
|
{
|
|
|
$studentId = $examData['student_id'];
|
|
|
- $examId = $examData['exam_id'];
|
|
|
+ $examId = $examData['paper_id'];
|
|
|
+
|
|
|
+ // 【修复】先清理该考试的所有答题记录(支持重复考试)
|
|
|
+ DB::connection('mysql')->table('student_answer_questions')
|
|
|
+ ->where('student_id', $studentId)
|
|
|
+ ->where('exam_id', $examId)
|
|
|
+ ->delete();
|
|
|
+
|
|
|
+ DB::connection('mysql')->table('student_answer_steps')
|
|
|
+ ->where('student_id', $studentId)
|
|
|
+ ->where('exam_id', $examId)
|
|
|
+ ->delete();
|
|
|
|
|
|
foreach ($examData['questions'] as $question) {
|
|
|
$questionId = $question['question_id'];
|
|
|
@@ -525,20 +648,134 @@ class ExamAnswerAnalysisService
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ Log::info('答题记录保存完成', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'exam_id' => $examId,
|
|
|
+ 'question_count' => count($examData['questions']),
|
|
|
+ ]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 保存分析结果
|
|
|
+ * 保存分析结果并创建掌握度快照
|
|
|
*/
|
|
|
- private function saveAnalysisResult(string $studentId, string $examId, array $result): void
|
|
|
+ private function saveAnalysisResult(string $studentId, string $paperId, array $result): void
|
|
|
{
|
|
|
+ // 【修复】支持重复分析:先删除旧的分析结果
|
|
|
+ DB::connection('mysql')->table('exam_analysis_results')
|
|
|
+ ->where('student_id', $studentId)
|
|
|
+ ->where('paper_id', $paperId)
|
|
|
+ ->delete();
|
|
|
+
|
|
|
+ // 插入新的分析结果
|
|
|
DB::connection('mysql')->table('exam_analysis_results')->insert([
|
|
|
'student_id' => $studentId,
|
|
|
- 'exam_id' => $examId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
'analysis_data' => json_encode($result),
|
|
|
'created_at' => now(),
|
|
|
'updated_at' => now(),
|
|
|
]);
|
|
|
+
|
|
|
+ Log::info('分析结果保存完成', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'data_size' => strlen(json_encode($result)),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 【集成】创建知识点掌握度快照
|
|
|
+ $this->createMasterySnapshot($studentId, $paperId, $result);
|
|
|
+
|
|
|
+ // 【新增】异步生成学情分析PDF
|
|
|
+ try {
|
|
|
+ Log::info('开始异步生成学情分析PDF', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 使用队列异步生成PDF,避免阻塞主流程
|
|
|
+ dispatch(new \App\Jobs\GenerateAnalysisPdfJob($paperId, $studentId, null));
|
|
|
+
|
|
|
+ Log::info('PDF生成任务已加入队列', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ ]);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('PDF生成任务加入队列失败', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建知识点掌握度快照
|
|
|
+ * 【集成】使用LocalAIAnalysisService的快照功能
|
|
|
+ *
|
|
|
+ * 快照用途:
|
|
|
+ * 1. 追踪学生掌握度变化趋势
|
|
|
+ * 2. 生成学情报告时对比历史数据
|
|
|
+ * 3. 为智能出卷提供决策依据
|
|
|
+ */
|
|
|
+ private function createMasterySnapshot(string $studentId, string $paperId, array $analysisResult): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 计算快照数据
|
|
|
+ $masteryVector = $analysisResult['mastery_vector'] ?? [];
|
|
|
+ $overallMastery = 0;
|
|
|
+ $weakCount = 0;
|
|
|
+ $strongCount = 0;
|
|
|
+
|
|
|
+ foreach ($masteryVector as $kpData) {
|
|
|
+ $mastery = $kpData['current_mastery'] ?? $kpData['mastery'] ?? 0;
|
|
|
+ $overallMastery += $mastery;
|
|
|
+
|
|
|
+ if ($mastery < 0.6) {
|
|
|
+ $weakCount++;
|
|
|
+ } elseif ($mastery >= 0.85) {
|
|
|
+ $strongCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpCount = count($masteryVector);
|
|
|
+ $overallMastery = $kpCount > 0 ? round($overallMastery / $kpCount, 4) : 0;
|
|
|
+
|
|
|
+ // 生成快照ID
|
|
|
+ $snapshotId = 'snap_' . $paperId . '_' . now()->format('YmdHis');
|
|
|
+
|
|
|
+ // 保存到快照表
|
|
|
+ DB::connection('mysql')->table('knowledge_point_mastery_snapshots')->insert([
|
|
|
+ 'snapshot_id' => $snapshotId,
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'answer_record_id' => null,
|
|
|
+ 'mastery_data' => json_encode($masteryVector),
|
|
|
+ 'overall_mastery' => $overallMastery,
|
|
|
+ 'weak_knowledge_points_count' => $weakCount,
|
|
|
+ 'strong_knowledge_points_count' => $strongCount,
|
|
|
+ 'snapshot_time' => now(),
|
|
|
+ 'analysis_id' => null,
|
|
|
+ 'created_at' => now(),
|
|
|
+ 'updated_at' => now(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ Log::info('掌握度快照创建成功', [
|
|
|
+ 'snapshot_id' => $snapshotId,
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'overall_mastery' => $overallMastery,
|
|
|
+ 'weak_count' => $weakCount,
|
|
|
+ 'strong_count' => $strongCount,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // 快照创建失败不影响主流程
|
|
|
+ Log::warning('掌握度快照创建失败', [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'paper_id' => $paperId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|