|
|
@@ -91,6 +91,84 @@ class MasteryCalculator
|
|
|
return $masteryData;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 批量计算并落库知识点掌握度(性能优化:批量读历史 + 批量upsert)
|
|
|
+ *
|
|
|
+ * @param string $studentId
|
|
|
+ * @param array<string, array{attempts: array}> $knowledgeAttempts
|
|
|
+ * @param int $examBaseDifficulty
|
|
|
+ * @return array<string, array>
|
|
|
+ */
|
|
|
+ public function calculateMasteryLevelsBatch(string $studentId, array $knowledgeAttempts, int $examBaseDifficulty): array
|
|
|
+ {
|
|
|
+ if (empty($knowledgeAttempts)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $kpCodes = array_keys($knowledgeAttempts);
|
|
|
+ $historyRows = DB::table('student_knowledge_mastery')
|
|
|
+ ->where('student_id', $studentId)
|
|
|
+ ->whereIn('kp_code', $kpCodes)
|
|
|
+ ->get(['kp_code', 'mastery_level', 'total_attempts', 'correct_attempts'])
|
|
|
+ ->keyBy('kp_code');
|
|
|
+
|
|
|
+ $now = now();
|
|
|
+ $upsertRows = [];
|
|
|
+ $results = [];
|
|
|
+
|
|
|
+ foreach ($knowledgeAttempts as $kpCode => $data) {
|
|
|
+ $attempts = $data['attempts'] ?? [];
|
|
|
+ if (empty($attempts)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $historyMastery = $historyRows->get($kpCode);
|
|
|
+ $oldMastery = (float) ($historyMastery->mastery_level ?? 0.0);
|
|
|
+ $historyTotalAttempts = (int) ($historyMastery->total_attempts ?? 0);
|
|
|
+ $historyCorrectAttempts = (int) ($historyMastery->correct_attempts ?? 0);
|
|
|
+
|
|
|
+ $computed = $this->computeMasteryChange($attempts, $examBaseDifficulty, false);
|
|
|
+ $newMastery = max(0.0, min(1.0, $oldMastery + $computed['total_change']));
|
|
|
+ $newMastery = round($newMastery, 4);
|
|
|
+
|
|
|
+ $upsertRows[] = [
|
|
|
+ 'student_id' => $studentId,
|
|
|
+ 'kp_code' => $kpCode,
|
|
|
+ 'mastery_level' => $newMastery,
|
|
|
+ 'direct_mastery_level' => $newMastery,
|
|
|
+ 'confidence_level' => 0.0,
|
|
|
+ 'total_attempts' => $historyTotalAttempts + $computed['total_attempts'],
|
|
|
+ 'correct_attempts' => $historyCorrectAttempts + $computed['correct_attempts'],
|
|
|
+ 'mastery_trend' => 'stable',
|
|
|
+ 'last_mastery_update' => $now,
|
|
|
+ 'updated_at' => $now,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $results[$kpCode] = [
|
|
|
+ 'mastery' => $newMastery,
|
|
|
+ 'total_attempts' => $computed['total_attempts'],
|
|
|
+ 'correct_attempts' => $computed['correct_attempts'],
|
|
|
+ 'accuracy_rate' => $computed['accuracy_rate'],
|
|
|
+ 'old_mastery' => $oldMastery,
|
|
|
+ 'change' => round($computed['total_change'], 4),
|
|
|
+ 'details' => [
|
|
|
+ 'exam_base_difficulty' => $examBaseDifficulty,
|
|
|
+ 'total_change' => round($computed['total_change'], 4),
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($upsertRows)) {
|
|
|
+ DB::table('student_knowledge_mastery')->upsert(
|
|
|
+ $upsertRows,
|
|
|
+ ['student_id', 'kp_code'],
|
|
|
+ ['mastery_level', 'direct_mastery_level', 'confidence_level', 'total_attempts', 'correct_attempts', 'mastery_trend', 'last_mastery_update', 'updated_at']
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return $results;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取难度等级名称
|
|
|
*/
|
|
|
@@ -119,54 +197,10 @@ class MasteryCalculator
|
|
|
|
|
|
$oldMastery = $historyMastery->mastery_level ?? 0.0; // 默认 0.0
|
|
|
|
|
|
- // 统计正确和错误次数
|
|
|
- $totalAttempts = count($attempts);
|
|
|
- $correctAttempts = 0;
|
|
|
- $incorrectAttempts = 0;
|
|
|
- foreach ($attempts as $attempt) {
|
|
|
- if (boolval($attempt['is_correct'] ?? false)) {
|
|
|
- $correctAttempts++;
|
|
|
- } else {
|
|
|
- $incorrectAttempts++;
|
|
|
- }
|
|
|
- }
|
|
|
- $accuracyRate = $totalAttempts > 0 ? ($correctAttempts / $totalAttempts) : 0.0;
|
|
|
-
|
|
|
- // 计算每次答题的权重变化
|
|
|
- $totalChange = 0.0;
|
|
|
-
|
|
|
- foreach ($attempts as $attempt) {
|
|
|
- $isCorrect = boolval($attempt['is_correct'] ?? false);
|
|
|
- $questionDifficulty = floatval($attempt['question_difficulty'] ?? 0.6);
|
|
|
-
|
|
|
- // 难度映射:将0.0-1.0的浮点数难度映射为 1-4 等级
|
|
|
- $questionLevel = $this->mapDifficultyToLevel($questionDifficulty);
|
|
|
-
|
|
|
- // 根据难度关系计算权重变化
|
|
|
- $change = $this->calculateWeightByDifficultyRelation($questionLevel, $examBaseDifficulty, $isCorrect);
|
|
|
-
|
|
|
- // 0基础保护:当次正确率偏低时,限制正向增益,避免“错很多但掌握度持续贴近100%”
|
|
|
- if (
|
|
|
- $examBaseDifficulty === 0
|
|
|
- && $accuracyRate < self::BASE_ZERO_GAIN_GUARD_CORRECT_RATE
|
|
|
- && $change > 0
|
|
|
- ) {
|
|
|
- $change = round($change * 0.5, 4);
|
|
|
- }
|
|
|
-
|
|
|
- $totalChange += $change;
|
|
|
-
|
|
|
- Log::debug('掌握度变化计算', [
|
|
|
- 'question_id' => $attempt['question_id'] ?? '',
|
|
|
- 'question_difficulty' => $questionDifficulty,
|
|
|
- 'question_level' => $questionLevel,
|
|
|
- 'exam_base_difficulty' => $examBaseDifficulty,
|
|
|
- 'is_correct' => $isCorrect,
|
|
|
- 'change' => $change,
|
|
|
- 'running_total' => $totalChange,
|
|
|
- 'accuracy_rate' => round($accuracyRate, 4),
|
|
|
- ]);
|
|
|
- }
|
|
|
+ $computed = $this->computeMasteryChange($attempts, $examBaseDifficulty, config('app.debug'));
|
|
|
+ $totalAttempts = $computed['total_attempts'];
|
|
|
+ $correctAttempts = $computed['correct_attempts'];
|
|
|
+ $totalChange = $computed['total_change'];
|
|
|
|
|
|
// 【公司要求】数值更新:newMastery = oldMastery + change
|
|
|
$newMastery = $oldMastery + $totalChange;
|
|
|
@@ -196,7 +230,7 @@ class MasteryCalculator
|
|
|
'mastery' => round($newMastery, 4),
|
|
|
'total_attempts' => $totalAttempts,
|
|
|
'correct_attempts' => $correctAttempts,
|
|
|
- 'accuracy_rate' => round(($correctAttempts / $totalAttempts) * 100, 2),
|
|
|
+ 'accuracy_rate' => $computed['accuracy_rate'],
|
|
|
'old_mastery' => $oldMastery,
|
|
|
'change' => round($totalChange, 4),
|
|
|
'details' => [
|
|
|
@@ -206,6 +240,64 @@ class MasteryCalculator
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 计算一次掌握度变化,不落库。
|
|
|
+ *
|
|
|
+ * @param array $attempts
|
|
|
+ * @param int $examBaseDifficulty
|
|
|
+ * @param bool $enableDebug
|
|
|
+ * @return array{total_attempts:int,correct_attempts:int,accuracy_rate:float,total_change:float}
|
|
|
+ */
|
|
|
+ private function computeMasteryChange(array $attempts, int $examBaseDifficulty, bool $enableDebug = false): array
|
|
|
+ {
|
|
|
+ $totalAttempts = count($attempts);
|
|
|
+ $correctAttempts = 0;
|
|
|
+ foreach ($attempts as $attempt) {
|
|
|
+ if (boolval($attempt['is_correct'] ?? false)) {
|
|
|
+ $correctAttempts++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $accuracyRateRatio = $totalAttempts > 0 ? ($correctAttempts / $totalAttempts) : 0.0;
|
|
|
+
|
|
|
+ $totalChange = 0.0;
|
|
|
+ foreach ($attempts as $attempt) {
|
|
|
+ $isCorrect = boolval($attempt['is_correct'] ?? false);
|
|
|
+ $questionDifficulty = floatval($attempt['question_difficulty'] ?? 0.6);
|
|
|
+ $questionLevel = $this->mapDifficultyToLevel($questionDifficulty);
|
|
|
+ $change = $this->calculateWeightByDifficultyRelation($questionLevel, $examBaseDifficulty, $isCorrect);
|
|
|
+
|
|
|
+ if (
|
|
|
+ $examBaseDifficulty === 0
|
|
|
+ && $accuracyRateRatio < self::BASE_ZERO_GAIN_GUARD_CORRECT_RATE
|
|
|
+ && $change > 0
|
|
|
+ ) {
|
|
|
+ $change = round($change * 0.5, 4);
|
|
|
+ }
|
|
|
+
|
|
|
+ $totalChange += $change;
|
|
|
+
|
|
|
+ if ($enableDebug) {
|
|
|
+ Log::debug('掌握度变化计算', [
|
|
|
+ 'question_id' => $attempt['question_id'] ?? '',
|
|
|
+ 'question_difficulty' => $questionDifficulty,
|
|
|
+ 'question_level' => $questionLevel,
|
|
|
+ 'exam_base_difficulty' => $examBaseDifficulty,
|
|
|
+ 'is_correct' => $isCorrect,
|
|
|
+ 'change' => $change,
|
|
|
+ 'running_total' => $totalChange,
|
|
|
+ 'accuracy_rate' => round($accuracyRateRatio, 4),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'total_attempts' => $totalAttempts,
|
|
|
+ 'correct_attempts' => $correctAttempts,
|
|
|
+ 'accuracy_rate' => $totalAttempts > 0 ? round(($correctAttempts / $totalAttempts) * 100, 2) : 0.0,
|
|
|
+ 'total_change' => $totalChange,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 【公司要求】难度映射:将题目中0.0-1.0的浮点数难度映射为0-4等级
|
|
|
*
|