|
@@ -1172,14 +1172,38 @@ class ExamAnswerAnalysisService
|
|
|
$analysis = [];
|
|
$analysis = [];
|
|
|
|
|
|
|
|
foreach ($knowledgeMasteryVector as $kpId => $data) {
|
|
foreach ($knowledgeMasteryVector as $kpId => $data) {
|
|
|
|
|
+ $totalAttempts = intval($data['total_attempts'] ?? 0);
|
|
|
|
|
+ $correctAttempts = intval($data['correct_attempts'] ?? 0);
|
|
|
|
|
+ $examAccuracyRatio = $totalAttempts > 0 ? ($correctAttempts / $totalAttempts) : null;
|
|
|
|
|
+ $historicalMastery = floatval($data['mastery'] ?? 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 收敛规则(最小影响):
|
|
|
|
|
+ // 仅调整“报告展示口径”,不改 student_knowledge_mastery 的历史累计值。
|
|
|
|
|
+ // 当次有错题时,采用“历史掌握度 + 当次正确率”的证据加权融合:
|
|
|
|
|
+ // display = (historical*PRIOR_WEIGHT + examAccuracy*evidenceWeight) / (PRIOR_WEIGHT + evidenceWeight)
|
|
|
|
|
+ // 这样避免一次异常把 100% 直接打到 0%,同时保证有错会下调。
|
|
|
|
|
+ $displayMastery = $historicalMastery;
|
|
|
|
|
+ if ($examAccuracyRatio !== null && $correctAttempts < $totalAttempts) {
|
|
|
|
|
+ $priorWeight = 8.0; // 历史先验权重(固定,保证历史连续性)
|
|
|
|
|
+ $evidenceWeight = max(1.0, min(10.0, floatval($totalAttempts))); // 当次证据权重(随作答量提升)
|
|
|
|
|
+ $displayMastery = (
|
|
|
|
|
+ ($historicalMastery * $priorWeight) + ($examAccuracyRatio * $evidenceWeight)
|
|
|
|
|
+ ) / ($priorWeight + $evidenceWeight);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $displayData = $data;
|
|
|
|
|
+ $displayData['mastery'] = round($displayMastery, 4);
|
|
|
|
|
+
|
|
|
// 【公司要求】移除置信度,只保留核心掌握度分析
|
|
// 【公司要求】移除置信度,只保留核心掌握度分析
|
|
|
$analysis[] = [
|
|
$analysis[] = [
|
|
|
'kp_id' => $kpId,
|
|
'kp_id' => $kpId,
|
|
|
- 'mastery_level' => $data['mastery'],
|
|
|
|
|
- 'performance_in_exam' => $this->evaluatePerformanceLevel($data['mastery']),
|
|
|
|
|
|
|
+ 'mastery_level' => round($displayMastery, 4),
|
|
|
|
|
+ 'historical_mastery_level' => round($historicalMastery, 4),
|
|
|
|
|
+ 'exam_accuracy_rate' => $examAccuracyRatio !== null ? round($examAccuracyRatio, 4) : null,
|
|
|
|
|
+ 'performance_in_exam' => $this->evaluatePerformanceLevel($displayMastery),
|
|
|
'evidence_count' => count($data['step_details']),
|
|
'evidence_count' => count($data['step_details']),
|
|
|
'step_evidence' => $data['step_details'],
|
|
'step_evidence' => $data['step_details'],
|
|
|
- 'recommendation' => $this->generateKnowledgePointRecommendation($data),
|
|
|
|
|
|
|
+ 'recommendation' => $this->generateKnowledgePointRecommendation($displayData),
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1612,51 +1636,21 @@ class ExamAnswerAnalysisService
|
|
|
|
|
|
|
|
$now = now();
|
|
$now = now();
|
|
|
$updated = 0;
|
|
$updated = 0;
|
|
|
-
|
|
|
|
|
- $questionIds = [];
|
|
|
|
|
- foreach ($examData['questions'] as $question) {
|
|
|
|
|
- $questionId = $question['question_id'] ?? $question['question_bank_id'] ?? null;
|
|
|
|
|
- if (!empty($questionId)) {
|
|
|
|
|
- $questionIds[] = (string) $questionId;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- $questionIds = array_values(array_unique($questionIds));
|
|
|
|
|
- if (empty($questionIds)) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $paperQuestionRows = DB::connection('mysql')->table('paper_questions')
|
|
|
|
|
- ->where('paper_id', $paperId)
|
|
|
|
|
- ->where(function ($q) use ($questionIds) {
|
|
|
|
|
- $q->whereIn('question_bank_id', $questionIds)
|
|
|
|
|
- ->orWhereIn('question_id', $questionIds);
|
|
|
|
|
- })
|
|
|
|
|
- ->get(['id', 'question_bank_id', 'question_id']);
|
|
|
|
|
-
|
|
|
|
|
- $rowIdsByQuestionId = [];
|
|
|
|
|
- foreach ($paperQuestionRows as $row) {
|
|
|
|
|
- if (!empty($row->question_bank_id)) {
|
|
|
|
|
- $rowIdsByQuestionId[(string) $row->question_bank_id][] = (int) $row->id;
|
|
|
|
|
- }
|
|
|
|
|
- if (!empty($row->question_id)) {
|
|
|
|
|
- $rowIdsByQuestionId[(string) $row->question_id][] = (int) $row->id;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $updateRowsById = [];
|
|
|
|
|
|
|
+ $processedQuestionIds = [];
|
|
|
foreach ($examData['questions'] as $question) {
|
|
foreach ($examData['questions'] as $question) {
|
|
|
$questionId = $question['question_id'] ?? $question['question_bank_id'] ?? null;
|
|
$questionId = $question['question_id'] ?? $question['question_bank_id'] ?? null;
|
|
|
if (empty($questionId)) {
|
|
if (empty($questionId)) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
$questionId = (string) $questionId;
|
|
$questionId = (string) $questionId;
|
|
|
- $targetRowIds = $rowIdsByQuestionId[$questionId] ?? [];
|
|
|
|
|
- if (empty($targetRowIds)) {
|
|
|
|
|
|
|
+ // 同一题只更新一次,避免双字段命中导致重复更新
|
|
|
|
|
+ if (isset($processedQuestionIds[$questionId])) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
+ $processedQuestionIds[$questionId] = true;
|
|
|
|
|
|
|
|
$isCorrectArray = $question['is_correct'] ?? [];
|
|
$isCorrectArray = $question['is_correct'] ?? [];
|
|
|
- if (!is_array($isCorrectArray)) {
|
|
|
|
|
|
|
+ if (! is_array($isCorrectArray)) {
|
|
|
$isCorrectArray = [$isCorrectArray ? 1 : 0];
|
|
$isCorrectArray = [$isCorrectArray ? 1 : 0];
|
|
|
}
|
|
}
|
|
|
$totalSteps = count($isCorrectArray);
|
|
$totalSteps = count($isCorrectArray);
|
|
@@ -1665,63 +1659,35 @@ class ExamAnswerAnalysisService
|
|
|
$isFullyCorrect = $totalSteps > 0 ? ($correctSteps === $totalSteps ? 1 : 0) : null;
|
|
$isFullyCorrect = $totalSteps > 0 ? ($correctSteps === $totalSteps ? 1 : 0) : null;
|
|
|
$scoreObtained = $question['score_obtained'] ?? null;
|
|
$scoreObtained = $question['score_obtained'] ?? null;
|
|
|
|
|
|
|
|
- foreach ($targetRowIds as $rowId) {
|
|
|
|
|
- $payload = [
|
|
|
|
|
- 'id' => $rowId,
|
|
|
|
|
- 'graded_at' => $now,
|
|
|
|
|
- ];
|
|
|
|
|
- if ($isFullyCorrect !== null) {
|
|
|
|
|
- $payload['is_correct'] = $isFullyCorrect;
|
|
|
|
|
- }
|
|
|
|
|
- if ($scoreRatio !== null) {
|
|
|
|
|
- $payload['score_ratio'] = $scoreRatio;
|
|
|
|
|
- }
|
|
|
|
|
- if ($scoreObtained !== null) {
|
|
|
|
|
- $payload['score_obtained'] = $scoreObtained;
|
|
|
|
|
- }
|
|
|
|
|
- if (array_key_exists('student_answer', $question)) {
|
|
|
|
|
- $payload['student_answer'] = $question['student_answer'];
|
|
|
|
|
- }
|
|
|
|
|
- if (array_key_exists('teacher_comment', $question)) {
|
|
|
|
|
- $payload['teacher_comment'] = $question['teacher_comment'];
|
|
|
|
|
- }
|
|
|
|
|
- $updateRowsById[$rowId] = $payload;
|
|
|
|
|
|
|
+ $payload = [
|
|
|
|
|
+ 'graded_at' => $now,
|
|
|
|
|
+ ];
|
|
|
|
|
+ if ($isFullyCorrect !== null) {
|
|
|
|
|
+ $payload['is_correct'] = $isFullyCorrect;
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!empty($updateRowsById)) {
|
|
|
|
|
- $updateRows = array_values($updateRowsById);
|
|
|
|
|
- $fields = ['graded_at', 'is_correct', 'score_ratio', 'score_obtained', 'student_answer', 'teacher_comment'];
|
|
|
|
|
- $cases = [];
|
|
|
|
|
- $bindings = [];
|
|
|
|
|
-
|
|
|
|
|
- foreach ($fields as $field) {
|
|
|
|
|
- $caseSql = "`{$field}` = CASE `id`";
|
|
|
|
|
- $hasValue = false;
|
|
|
|
|
- foreach ($updateRows as $row) {
|
|
|
|
|
- if (!array_key_exists($field, $row)) {
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- $hasValue = true;
|
|
|
|
|
- $caseSql .= ' WHEN ? THEN ?';
|
|
|
|
|
- $bindings[] = $row['id'];
|
|
|
|
|
- $bindings[] = $row[$field];
|
|
|
|
|
- }
|
|
|
|
|
- $caseSql .= " ELSE `{$field}` END";
|
|
|
|
|
- if ($hasValue) {
|
|
|
|
|
- $cases[] = $caseSql;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if ($scoreRatio !== null) {
|
|
|
|
|
+ $payload['score_ratio'] = $scoreRatio;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if (!empty($cases)) {
|
|
|
|
|
- $ids = array_column($updateRows, 'id');
|
|
|
|
|
- $idPlaceholders = implode(',', array_fill(0, count($ids), '?'));
|
|
|
|
|
- $sql = 'UPDATE `paper_questions` SET '.implode(', ', $cases)." WHERE `id` IN ({$idPlaceholders})";
|
|
|
|
|
- foreach ($ids as $id) {
|
|
|
|
|
- $bindings[] = $id;
|
|
|
|
|
- }
|
|
|
|
|
- $updated = DB::connection('mysql')->update($sql, $bindings);
|
|
|
|
|
|
|
+ if ($scoreObtained !== null) {
|
|
|
|
|
+ $payload['score_obtained'] = $scoreObtained;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (array_key_exists('student_answer', $question)) {
|
|
|
|
|
+ $payload['student_answer'] = $question['student_answer'];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (array_key_exists('teacher_comment', $question)) {
|
|
|
|
|
+ $payload['teacher_comment'] = $question['teacher_comment'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关键:不再依赖 paper_questions.id(部分库该字段为空),改为业务键匹配更新
|
|
|
|
|
+ $affected = DB::connection('mysql')->table('paper_questions')
|
|
|
|
|
+ ->where('paper_id', $paperId)
|
|
|
|
|
+ ->where(function ($q) use ($questionId) {
|
|
|
|
|
+ $q->where('question_bank_id', $questionId)
|
|
|
|
|
+ ->orWhere('question_id', $questionId);
|
|
|
|
|
+ })
|
|
|
|
|
+ ->update($payload);
|
|
|
|
|
+
|
|
|
|
|
+ $updated += $affected;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if ($updated > 0) {
|
|
if ($updated > 0) {
|
|
@@ -1737,6 +1703,7 @@ class ExamAnswerAnalysisService
|
|
|
Log::info('paper_questions 判分同步完成', [
|
|
Log::info('paper_questions 判分同步完成', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'input_question_count' => count($examData['questions']),
|
|
'input_question_count' => count($examData['questions']),
|
|
|
|
|
+ 'processed_question_count' => count($processedQuestionIds),
|
|
|
'updated_rows' => $updated,
|
|
'updated_rows' => $updated,
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|