|
@@ -52,10 +52,9 @@ class MasteryCalculator
|
|
|
'kp_code' => $kpCode,
|
|
'kp_code' => $kpCode,
|
|
|
'attempts_provided' => $attempts !== null,
|
|
'attempts_provided' => $attempts !== null,
|
|
|
]);
|
|
]);
|
|
|
|
|
+ // 【公司要求】返回值:只返回核心掌握度数据
|
|
|
return [
|
|
return [
|
|
|
'mastery' => 0.0,
|
|
'mastery' => 0.0,
|
|
|
- 'confidence' => 0.0,
|
|
|
|
|
- 'trend' => 'insufficient',
|
|
|
|
|
'total_attempts' => 0,
|
|
'total_attempts' => 0,
|
|
|
'correct_attempts' => 0,
|
|
'correct_attempts' => 0,
|
|
|
'accuracy_rate' => 0.0,
|
|
'accuracy_rate' => 0.0,
|
|
@@ -74,6 +73,7 @@ class MasteryCalculator
|
|
|
|
|
|
|
|
$masteryData = $this->calculateMasteryWithExamDifficulty($studentId, $kpCode, $attempts, $examBaseDifficulty);
|
|
$masteryData = $this->calculateMasteryWithExamDifficulty($studentId, $kpCode, $attempts, $examBaseDifficulty);
|
|
|
|
|
|
|
|
|
|
+ // 【公司要求】日志输出:只记录核心掌握度数据
|
|
|
Log::info('掌握度计算完成', [
|
|
Log::info('掌握度计算完成', [
|
|
|
'student_id' => $studentId,
|
|
'student_id' => $studentId,
|
|
|
'kp_code' => $kpCode,
|
|
'kp_code' => $kpCode,
|
|
@@ -82,8 +82,6 @@ class MasteryCalculator
|
|
|
'total_attempts' => count($attempts),
|
|
'total_attempts' => count($attempts),
|
|
|
'correct_attempts' => $masteryData['correct_attempts'],
|
|
'correct_attempts' => $masteryData['correct_attempts'],
|
|
|
'final_mastery' => $masteryData['mastery'],
|
|
'final_mastery' => $masteryData['mastery'],
|
|
|
- 'confidence' => $masteryData['confidence'],
|
|
|
|
|
- 'trend' => $masteryData['trend'],
|
|
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return $masteryData;
|
|
return $masteryData;
|
|
@@ -114,7 +112,7 @@ class MasteryCalculator
|
|
|
->where('kp_code', $kpCode)
|
|
->where('kp_code', $kpCode)
|
|
|
->first();
|
|
->first();
|
|
|
|
|
|
|
|
- $oldMastery = $historyMastery->mastery_level ?? 0.5; // 默认0.5
|
|
|
|
|
|
|
+ $oldMastery = $historyMastery->mastery_level ?? 0.0; // 默认 0.0
|
|
|
|
|
|
|
|
// 统计正确和错误次数
|
|
// 统计正确和错误次数
|
|
|
$totalAttempts = count($attempts);
|
|
$totalAttempts = count($attempts);
|
|
@@ -128,7 +126,7 @@ class MasteryCalculator
|
|
|
$isCorrect = boolval($attempt['is_correct'] ?? false);
|
|
$isCorrect = boolval($attempt['is_correct'] ?? false);
|
|
|
$questionDifficulty = floatval($attempt['question_difficulty'] ?? 0.6);
|
|
$questionDifficulty = floatval($attempt['question_difficulty'] ?? 0.6);
|
|
|
|
|
|
|
|
- // 难度映射:将0.0-1.0的浮点数难度映射为1-4等级
|
|
|
|
|
|
|
+ // 难度映射:将0.0-1.0的浮点数难度映射为 1-4 等级
|
|
|
$questionLevel = $this->mapDifficultyToLevel($questionDifficulty);
|
|
$questionLevel = $this->mapDifficultyToLevel($questionDifficulty);
|
|
|
|
|
|
|
|
// 根据难度关系计算权重变化
|
|
// 根据难度关系计算权重变化
|
|
@@ -153,75 +151,83 @@ class MasteryCalculator
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 计算平均变化(避免单次考试影响过大)
|
|
|
|
|
- $averageChange = $totalAttempts > 0 ? $totalChange / $totalAttempts : 0.0;
|
|
|
|
|
|
|
+ // 【公司要求】数值更新:newMastery = oldMastery + change
|
|
|
|
|
+ $newMastery = $oldMastery + $totalChange;
|
|
|
|
|
|
|
|
- // 应用权重调整(避免单次考试变化过大)
|
|
|
|
|
- $weightedChange = $averageChange * min($totalAttempts / 10.0, 1.0); // 最多10次考试达到满权重
|
|
|
|
|
-
|
|
|
|
|
- // 新掌握度 = 旧掌握度 + 加权变化
|
|
|
|
|
- $newMastery = $oldMastery + $weightedChange;
|
|
|
|
|
-
|
|
|
|
|
- // 边界限制:0.0 ~ 1.0
|
|
|
|
|
|
|
+ // 【公司要求】边界限制:0.0 ~ 1.0
|
|
|
$newMastery = max(0.0, min(1.0, $newMastery));
|
|
$newMastery = max(0.0, min(1.0, $newMastery));
|
|
|
|
|
|
|
|
- // 计算置信度(基于答题次数)
|
|
|
|
|
- $confidence = $this->calculateConfidence($attempts);
|
|
|
|
|
-
|
|
|
|
|
- // 判断趋势
|
|
|
|
|
- $trend = $this->determineTrend($attempts);
|
|
|
|
|
-
|
|
|
|
|
- // 保存到数据库
|
|
|
|
|
|
|
+ // 【公司要求】保存到数据库(只保存核心掌握度数据)
|
|
|
DB::table('student_knowledge_mastery')
|
|
DB::table('student_knowledge_mastery')
|
|
|
->updateOrInsert(
|
|
->updateOrInsert(
|
|
|
['student_id' => $studentId, 'kp_code' => $kpCode],
|
|
['student_id' => $studentId, 'kp_code' => $kpCode],
|
|
|
[
|
|
[
|
|
|
'mastery_level' => $newMastery,
|
|
'mastery_level' => $newMastery,
|
|
|
- 'confidence_level' => $confidence,
|
|
|
|
|
|
|
+ 'confidence_level' => 0.0, // 不再计算置信度
|
|
|
'total_attempts' => ($historyMastery->total_attempts ?? 0) + $totalAttempts,
|
|
'total_attempts' => ($historyMastery->total_attempts ?? 0) + $totalAttempts,
|
|
|
'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + $correctAttempts,
|
|
'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + $correctAttempts,
|
|
|
- 'mastery_trend' => $trend,
|
|
|
|
|
|
|
+ 'mastery_trend' => 'stable', // 不再判断趋势,统一设为stable
|
|
|
'last_mastery_update' => now(),
|
|
'last_mastery_update' => now(),
|
|
|
'updated_at' => now(),
|
|
'updated_at' => now(),
|
|
|
]
|
|
]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ // 【公司要求】返回值:只返回核心掌握度数据
|
|
|
return [
|
|
return [
|
|
|
'mastery' => round($newMastery, 4),
|
|
'mastery' => round($newMastery, 4),
|
|
|
- 'confidence' => round($confidence, 4),
|
|
|
|
|
- 'trend' => $trend,
|
|
|
|
|
'total_attempts' => $totalAttempts,
|
|
'total_attempts' => $totalAttempts,
|
|
|
'correct_attempts' => $correctAttempts,
|
|
'correct_attempts' => $correctAttempts,
|
|
|
'accuracy_rate' => round(($correctAttempts / $totalAttempts) * 100, 2),
|
|
'accuracy_rate' => round(($correctAttempts / $totalAttempts) * 100, 2),
|
|
|
'old_mastery' => $oldMastery,
|
|
'old_mastery' => $oldMastery,
|
|
|
- 'change' => round($weightedChange, 4),
|
|
|
|
|
|
|
+ 'change' => round($totalChange, 4),
|
|
|
'details' => [
|
|
'details' => [
|
|
|
'exam_base_difficulty' => $examBaseDifficulty,
|
|
'exam_base_difficulty' => $examBaseDifficulty,
|
|
|
- 'total_change' => round($totalChange, 4),
|
|
|
|
|
- 'average_change' => round($averageChange, 4),
|
|
|
|
|
- 'weighted_change' => round($weightedChange, 4),
|
|
|
|
|
|
|
+ 'total_change' => round($totalChange, 4)
|
|
|
],
|
|
],
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 难度映射:将0.0-1.0的浮点数难度映射为1-4等级
|
|
|
|
|
|
|
+ * 【公司要求】难度映射:将题目中0.0-1.0的浮点数难度映射为1-4等级
|
|
|
|
|
+ *
|
|
|
|
|
+ * 公司要求:
|
|
|
|
|
+ * 0.0 ~ 0.25 -> 1级(筑基)
|
|
|
|
|
+ * 0.25 ~ 0.5 -> 2级(提分)
|
|
|
|
|
+ * 0.5 ~ 0.75 -> 3级(培优)
|
|
|
|
|
+ * 0.75 ~ 1.0 -> 4级(竞赛)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param float $difficulty 题目难度(0.0-1.0浮点数)
|
|
|
|
|
+ * @return int 难度等级(1-4级)
|
|
|
*/
|
|
*/
|
|
|
private function mapDifficultyToLevel(float $difficulty): int
|
|
private function mapDifficultyToLevel(float $difficulty): int
|
|
|
{
|
|
{
|
|
|
if ($difficulty >= 0.0 && $difficulty < 0.25) {
|
|
if ($difficulty >= 0.0 && $difficulty < 0.25) {
|
|
|
- return 1; // 1级
|
|
|
|
|
|
|
+ return 1; // 1级(筑基)
|
|
|
} elseif ($difficulty >= 0.25 && $difficulty < 0.5) {
|
|
} elseif ($difficulty >= 0.25 && $difficulty < 0.5) {
|
|
|
- return 2; // 2级
|
|
|
|
|
|
|
+ return 2; // 2级(提分)
|
|
|
} elseif ($difficulty >= 0.5 && $difficulty < 0.75) {
|
|
} elseif ($difficulty >= 0.5 && $difficulty < 0.75) {
|
|
|
- return 3; // 3级
|
|
|
|
|
|
|
+ return 3; // 3级(培优)
|
|
|
} else {
|
|
} else {
|
|
|
- return 4; // 4级
|
|
|
|
|
|
|
+ return 4; // 4级(竞赛)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据难度关系计算权重变化
|
|
|
|
|
|
|
+ * 【公司要求】根据难度关系计算权重变化
|
|
|
|
|
+ *
|
|
|
|
|
+ * 公司要求的三种难度关系及权重:
|
|
|
|
|
+ * 1. 越级(Question > {学案基准难度}):对 +0.15 / 错 -0.05
|
|
|
|
|
+ * 2. 适应(Question = {学案基准难度}):对 +0.10 / 错 -0.10
|
|
|
|
|
+ * 3. 降级(Question < {学案基准难度}):对 +0.05 / 错 -0.15
|
|
|
|
|
+ *
|
|
|
|
|
+ * 学案基准难度获取:
|
|
|
|
|
+ * - 来源:试卷表(papers.difficulty_category)
|
|
|
|
|
+ * - 映射:筑基→1级,提分→2级,培优→3级,竞赛→4级
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param int $questionLevel 题目难度等级(1-4)
|
|
|
|
|
+ * @param int $examBaseDifficulty 学案基准难度(1-4)
|
|
|
|
|
+ * @param bool $isCorrect 答题是否正确
|
|
|
|
|
+ * @return float 权重变化值
|
|
|
*/
|
|
*/
|
|
|
private function calculateWeightByDifficultyRelation(int $questionLevel, int $examBaseDifficulty, bool $isCorrect): float
|
|
private function calculateWeightByDifficultyRelation(int $questionLevel, int $examBaseDifficulty, bool $isCorrect): float
|
|
|
{
|
|
{
|
|
@@ -237,77 +243,6 @@ class MasteryCalculator
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 计算置信度
|
|
|
|
|
- */
|
|
|
|
|
- private function calculateConfidence(array $attempts): float
|
|
|
|
|
- {
|
|
|
|
|
- if (empty($attempts)) {
|
|
|
|
|
- return 0.0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $totalAttempts = count($attempts);
|
|
|
|
|
-
|
|
|
|
|
- // 基于答题次数的置信度:答题越多,置信度越高
|
|
|
|
|
- // 5次以下线性增长,5次以上增长放缓
|
|
|
|
|
- if ($totalAttempts < 5) {
|
|
|
|
|
- $baseConfidence = $totalAttempts / 5.0;
|
|
|
|
|
- } else {
|
|
|
|
|
- $baseConfidence = 0.5 + (1.0 - 0.5) * (1 - exp(-($totalAttempts - 5) / 10));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算正确率
|
|
|
|
|
- $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct']));
|
|
|
|
|
- $accuracyRate = $correctAttempts / $totalAttempts;
|
|
|
|
|
-
|
|
|
|
|
- // 正确率也影响置信度
|
|
|
|
|
- $accuracyFactor = 0.5 + $accuracyRate * 0.5;
|
|
|
|
|
-
|
|
|
|
|
- // 综合置信度
|
|
|
|
|
- $confidence = $baseConfidence * $accuracyFactor;
|
|
|
|
|
-
|
|
|
|
|
- return min($confidence, 1.0);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 判断学习趋势
|
|
|
|
|
- */
|
|
|
|
|
- private function determineTrend(array $attempts): string
|
|
|
|
|
- {
|
|
|
|
|
- if (count($attempts) < 3) {
|
|
|
|
|
- return 'insufficient'; // 数据不足
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 按时间排序
|
|
|
|
|
- usort($attempts, function($a, $b) {
|
|
|
|
|
- $timeA = strtotime($a['completed_at'] ?? $a['created_at'] ?? 0);
|
|
|
|
|
- $timeB = strtotime($b['completed_at'] ?? $b['created_at'] ?? 0);
|
|
|
|
|
- return $timeA <=> $timeB;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 分为前后两部分
|
|
|
|
|
- $midPoint = intdiv(count($attempts), 2);
|
|
|
|
|
- $firstHalf = array_slice($attempts, 0, $midPoint);
|
|
|
|
|
- $secondHalf = array_slice($attempts, $midPoint);
|
|
|
|
|
-
|
|
|
|
|
- // 计算前半部分和后半部分的正确率
|
|
|
|
|
- $firstHalfCorrect = count(array_filter($firstHalf, fn($a) => $a['is_correct']));
|
|
|
|
|
- $firstHalfAccuracy = $firstHalfCorrect / count($firstHalf);
|
|
|
|
|
-
|
|
|
|
|
- $secondHalfCorrect = count(array_filter($secondHalf, fn($a) => $a['is_correct']));
|
|
|
|
|
- $secondHalfAccuracy = $secondHalfCorrect / count($secondHalf);
|
|
|
|
|
-
|
|
|
|
|
- $improvement = $secondHalfAccuracy - $firstHalfAccuracy;
|
|
|
|
|
-
|
|
|
|
|
- if ($improvement > 0.1) {
|
|
|
|
|
- return 'improving'; // 提升
|
|
|
|
|
- } elseif ($improvement < -0.1) {
|
|
|
|
|
- return 'declining'; // 下降
|
|
|
|
|
- } else {
|
|
|
|
|
- return 'stable'; // 稳定
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 获取学生的答题记录
|
|
* 获取学生的答题记录
|
|
|
*/
|
|
*/
|
|
@@ -323,6 +258,18 @@ class MasteryCalculator
|
|
|
->get();
|
|
->get();
|
|
|
|
|
|
|
|
foreach ($stepAttempts as $step) {
|
|
foreach ($stepAttempts as $step) {
|
|
|
|
|
+ // 【关键修复】从题目表查询题目难度
|
|
|
|
|
+ $questionDifficulty = 0.6; // 默认难度
|
|
|
|
|
+ if ($step->question_id) {
|
|
|
|
|
+ $question = DB::table('questions')
|
|
|
|
|
+ ->where('id', $step->question_id)
|
|
|
|
|
+ ->orWhere('question_code', $step->question_id)
|
|
|
|
|
+ ->first();
|
|
|
|
|
+ if ($question && $question->difficulty !== null) {
|
|
|
|
|
+ $questionDifficulty = floatval($question->difficulty);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
$attempts[] = [
|
|
$attempts[] = [
|
|
|
'student_id' => $step->student_id,
|
|
'student_id' => $step->student_id,
|
|
|
'paper_id' => $step->exam_id,
|
|
'paper_id' => $step->exam_id,
|
|
@@ -331,6 +278,7 @@ class MasteryCalculator
|
|
|
'is_correct' => (bool) $step->is_correct,
|
|
'is_correct' => (bool) $step->is_correct,
|
|
|
'score_obtained' => $step->step_score,
|
|
'score_obtained' => $step->step_score,
|
|
|
'max_score' => $step->step_score, // 步骤分数本身就是满分
|
|
'max_score' => $step->step_score, // 步骤分数本身就是满分
|
|
|
|
|
+ 'question_difficulty' => $questionDifficulty, // 【新增】题目难度
|
|
|
'created_at' => $step->created_at,
|
|
'created_at' => $step->created_at,
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
@@ -343,6 +291,18 @@ class MasteryCalculator
|
|
|
->get();
|
|
->get();
|
|
|
|
|
|
|
|
foreach ($questionAttempts as $question) {
|
|
foreach ($questionAttempts as $question) {
|
|
|
|
|
+ // 【关键修复】从题目表查询题目难度
|
|
|
|
|
+ $questionDifficulty = 0.6; // 默认难度
|
|
|
|
|
+ if ($question->question_id) {
|
|
|
|
|
+ $questionInfo = DB::table('questions')
|
|
|
|
|
+ ->where('id', $question->question_id)
|
|
|
|
|
+ ->orWhere('question_code', $question->question_id)
|
|
|
|
|
+ ->first();
|
|
|
|
|
+ if ($questionInfo && $questionInfo->difficulty !== null) {
|
|
|
|
|
+ $questionDifficulty = floatval($questionInfo->difficulty);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
$attempts[] = [
|
|
$attempts[] = [
|
|
|
'student_id' => $question->student_id,
|
|
'student_id' => $question->student_id,
|
|
|
'paper_id' => $question->exam_id,
|
|
'paper_id' => $question->exam_id,
|
|
@@ -351,6 +311,7 @@ class MasteryCalculator
|
|
|
'is_correct' => ($question->score_obtained ?? 0) > 0,
|
|
'is_correct' => ($question->score_obtained ?? 0) > 0,
|
|
|
'score_obtained' => $question->score_obtained ?? 0,
|
|
'score_obtained' => $question->score_obtained ?? 0,
|
|
|
'max_score' => $question->max_score ?? 0,
|
|
'max_score' => $question->max_score ?? 0,
|
|
|
|
|
+ 'question_difficulty' => $questionDifficulty, // 【新增】题目难度
|
|
|
'created_at' => $question->created_at,
|
|
'created_at' => $question->created_at,
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
@@ -471,12 +432,61 @@ class MasteryCalculator
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 【新功能】计算父节点掌握度(子节点平均值)
|
|
|
|
|
|
|
+ * 【公司要求】计算父节点掌握度(所有子节点算术平均值)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 公司要求:
|
|
|
|
|
+ * 1. 查询结构:通过 knowledge_points 表获取知识点的层级关系(parent_kp_code)
|
|
|
|
|
+ * 2. 平均值计算:如果一个知识点是父节点,它的掌握度不再从数据库直接读,
|
|
|
|
|
+ * 而是实时计算其下所有子节点掌握度的算术平均数
|
|
|
|
|
+ * 3. 多级递归:支持多级结构,能够从最底层逐级向上求平均
|
|
|
|
|
+ *
|
|
|
|
|
+ * 递归计算流程:
|
|
|
|
|
+ * - 从最底层叶子节点开始,逐级向上计算每个父节点的掌握度
|
|
|
|
|
+ * - 父节点掌握度 = 所有直接子节点掌握度的算术平均数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $studentId 学生ID
|
|
|
|
|
+ * @param string $parentKpCode 父节点编码
|
|
|
|
|
+ * @param int $maxDepth 最大递归深度(默认10级)
|
|
|
|
|
+ * @return float 父节点掌握度(0.0-1.0)
|
|
|
|
|
+ */
|
|
|
|
|
+ public function calculateParentMastery(string $studentId, string $parentKpCode, int $maxDepth = 3): float
|
|
|
|
|
+ {
|
|
|
|
|
+ return $this->calculateParentMasteryRecursive($studentId, $parentKpCode, 1, $maxDepth);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 【公司要求】递归计算父节点掌握度(多级结构支持)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 多级递归计算流程:
|
|
|
|
|
+ * 1. 获取父节点的所有直接子节点
|
|
|
|
|
+ * 2. 对每个子节点:
|
|
|
|
|
+ * - 如果子节点也是父节点,递归计算其掌握度
|
|
|
|
|
+ * - 如果子节点是叶子节点,从 student_knowledge_mastery 表读取掌握度
|
|
|
|
|
+ * 3. 计算所有子节点掌握度的算术平均数
|
|
|
|
|
+ * 4. 返回父节点掌握度
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param string $studentId 学生ID
|
|
|
|
|
+ * @param string $parentKpCode 父节点编码
|
|
|
|
|
+ * @param int $currentDepth 当前递归深度
|
|
|
|
|
+ * @param int $maxDepth 最大递归深度(防止无限递归)
|
|
|
|
|
+ * @return float 父节点掌握度
|
|
|
*/
|
|
*/
|
|
|
- public function calculateParentMastery(string $studentId, string $parentKpCode): float
|
|
|
|
|
|
|
+ private function calculateParentMasteryRecursive(string $studentId, string $parentKpCode, int $currentDepth, int $maxDepth): float
|
|
|
{
|
|
{
|
|
|
try {
|
|
try {
|
|
|
- // 获取所有子节点
|
|
|
|
|
|
|
+ // 【公司要求】防止无限递归
|
|
|
|
|
+ if ($currentDepth > $maxDepth) {
|
|
|
|
|
+ Log::warning('父节点掌握度计算达到最大递归深度', [
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'parent_kp_code' => $parentKpCode,
|
|
|
|
|
+ 'current_depth' => $currentDepth,
|
|
|
|
|
+ 'max_depth' => $maxDepth
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 【公司要求】查询结构:通过 knowledge_points 表获取知识点的层级关系(parent_kp_code)
|
|
|
|
|
+ // 获取所有直接子节点
|
|
|
$childKps = DB::table('knowledge_points')
|
|
$childKps = DB::table('knowledge_points')
|
|
|
->where('parent_kp_code', $parentKpCode)
|
|
->where('parent_kp_code', $parentKpCode)
|
|
|
->pluck('kp_code')
|
|
->pluck('kp_code')
|
|
@@ -484,44 +494,66 @@ class MasteryCalculator
|
|
|
|
|
|
|
|
if (empty($childKps)) {
|
|
if (empty($childKps)) {
|
|
|
// 如果没有子节点,返回0
|
|
// 如果没有子节点,返回0
|
|
|
|
|
+ Log::debug('父节点没有子节点,返回0', [
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'parent_kp_code' => $parentKpCode
|
|
|
|
|
+ ]);
|
|
|
return 0.0;
|
|
return 0.0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 获取所有子节点的掌握度
|
|
|
|
|
|
|
+ // 【公司要求】多级递归:支持多级结构,能够从最底层逐级向上求平均
|
|
|
|
|
+ // 获取所有子节点的掌握度(递归计算)
|
|
|
$masteryLevels = [];
|
|
$masteryLevels = [];
|
|
|
foreach ($childKps as $childKpCode) {
|
|
foreach ($childKps as $childKpCode) {
|
|
|
- $mastery = DB::table('student_knowledge_mastery')
|
|
|
|
|
- ->where('student_id', $studentId)
|
|
|
|
|
- ->where('kp_code', $childKpCode)
|
|
|
|
|
- ->value('mastery_level');
|
|
|
|
|
-
|
|
|
|
|
- if ($mastery !== null) {
|
|
|
|
|
- $masteryLevels[] = floatval($mastery);
|
|
|
|
|
|
|
+ // 【公司要求】多级递归:递归计算子节点掌握度
|
|
|
|
|
+ $childMastery = $this->calculateParentMasteryRecursive($studentId, $childKpCode, $currentDepth + 1, $maxDepth);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果子节点没有子节点,则从student_knowledge_mastery表读取
|
|
|
|
|
+ if ($childMastery == 0.0) {
|
|
|
|
|
+ $mastery = DB::table('student_knowledge_mastery')
|
|
|
|
|
+ ->where('student_id', $studentId)
|
|
|
|
|
+ ->where('kp_code', $childKpCode)
|
|
|
|
|
+ ->value('mastery_level');
|
|
|
|
|
+
|
|
|
|
|
+ if ($mastery !== null) {
|
|
|
|
|
+ $masteryLevels[] = floatval($mastery);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 子节点有子节点,使用递归计算的结果
|
|
|
|
|
+ $masteryLevels[] = $childMastery;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (empty($masteryLevels)) {
|
|
if (empty($masteryLevels)) {
|
|
|
|
|
+ Log::warning('父节点所有子节点都没有掌握度数据', [
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'parent_kp_code' => $parentKpCode,
|
|
|
|
|
+ 'child_count' => count($childKps)
|
|
|
|
|
+ ]);
|
|
|
return 0.0;
|
|
return 0.0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 计算算术平均数
|
|
|
|
|
|
|
+ // 【公司要求】平均值计算:父节点掌握度 = 所有子节点掌握度的算术平均数
|
|
|
$averageMastery = array_sum($masteryLevels) / count($masteryLevels);
|
|
$averageMastery = array_sum($masteryLevels) / count($masteryLevels);
|
|
|
|
|
+ $result = round($averageMastery, 4);
|
|
|
|
|
|
|
|
- Log::info('父节点掌握度计算', [
|
|
|
|
|
|
|
+ Log::info('父节点掌握度计算完成', [
|
|
|
'student_id' => $studentId,
|
|
'student_id' => $studentId,
|
|
|
'parent_kp_code' => $parentKpCode,
|
|
'parent_kp_code' => $parentKpCode,
|
|
|
'child_count' => count($childKps),
|
|
'child_count' => count($childKps),
|
|
|
'mastery_count' => count($masteryLevels),
|
|
'mastery_count' => count($masteryLevels),
|
|
|
- 'average_mastery' => round($averageMastery, 4),
|
|
|
|
|
|
|
+ 'average_mastery' => $result,
|
|
|
'child_masteries' => $masteryLevels,
|
|
'child_masteries' => $masteryLevels,
|
|
|
|
|
+ 'current_depth' => $currentDepth
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
- return round($averageMastery, 4);
|
|
|
|
|
|
|
+ return $result;
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
} catch (\Exception $e) {
|
|
|
Log::error('计算父节点掌握度失败', [
|
|
Log::error('计算父节点掌握度失败', [
|
|
|
'student_id' => $studentId,
|
|
'student_id' => $studentId,
|
|
|
'parent_kp_code' => $parentKpCode,
|
|
'parent_kp_code' => $parentKpCode,
|
|
|
|
|
+ 'current_depth' => $currentDepth,
|
|
|
'error' => $e->getMessage(),
|
|
'error' => $e->getMessage(),
|
|
|
]);
|
|
]);
|
|
|
return 0.0;
|
|
return 0.0;
|