StudentKnowledgeMastery.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <?php
  2. namespace App\Models;
  3. use Illuminate\Database\Eloquent\Factories\HasFactory;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Database\Eloquent\Relations\BelongsTo;
  6. class StudentKnowledgeMastery extends Model
  7. {
  8. use HasFactory;
  9. protected $table = 'student_knowledge_mastery';
  10. protected $fillable = [
  11. 'student_id',
  12. 'kp_code',
  13. 'mastery_level',
  14. 'confidence_level',
  15. 'total_attempts',
  16. 'correct_attempts',
  17. 'incorrect_attempts',
  18. 'partial_attempts',
  19. 'avg_time_seconds',
  20. 'fastest_time',
  21. 'slowest_time',
  22. 'attempts_easy',
  23. 'attempts_medium',
  24. 'attempts_hard',
  25. 'correct_easy',
  26. 'correct_medium',
  27. 'correct_hard',
  28. 'first_attempt_at',
  29. 'last_attempt_at',
  30. 'last_mastery_update',
  31. 'mastery_trend',
  32. 'mastery_change',
  33. 'calculation_version',
  34. 'notes',
  35. ];
  36. protected $casts = [
  37. 'mastery_level' => 'decimal:4',
  38. 'confidence_level' => 'decimal:4',
  39. 'mastery_change' => 'decimal:4',
  40. 'avg_time_seconds' => 'decimal:2',
  41. 'first_attempt_at' => 'datetime',
  42. 'last_attempt_at' => 'datetime',
  43. 'last_mastery_update' => 'datetime',
  44. 'created_at' => 'datetime',
  45. 'updated_at' => 'datetime',
  46. ];
  47. /**
  48. * 关联学生
  49. */
  50. public function student(): BelongsTo
  51. {
  52. return $this->belongsTo(Student::class, 'student_id', 'student_id');
  53. }
  54. /**
  55. * 关联知识点
  56. */
  57. public function knowledgePoint(): BelongsTo
  58. {
  59. return $this->belongsTo(KnowledgePoint::class, 'kp_code', 'kp_code');
  60. }
  61. /**
  62. * 作用域:按学生筛选
  63. */
  64. public function scopeForStudent($query, string $studentId)
  65. {
  66. return $query->where('student_id', $studentId);
  67. }
  68. /**
  69. * 作用域:按知识点筛选
  70. */
  71. public function scopeForKnowledgePoint($query, string $kpCode)
  72. {
  73. return $query->where('kp_code', $kpCode);
  74. }
  75. /**
  76. * 作用域:薄弱点(掌握度低于阈值)
  77. */
  78. public function scopeWeaknesses($query, float $threshold = 0.7)
  79. {
  80. return $query->where('mastery_level', '<', $threshold);
  81. }
  82. /**
  83. * 作用域:按掌握度排序
  84. */
  85. public function scopeOrderByMastery($query, string $direction = 'asc')
  86. {
  87. return $query->orderBy('mastery_level', $direction);
  88. }
  89. /**
  90. * 获取薄弱点列表
  91. */
  92. public static function getWeaknesses(string $studentId, float $threshold = 0.7, int $limit = 20): array
  93. {
  94. return self::forStudent($studentId)
  95. ->weaknesses($threshold)
  96. ->orderByMastery('asc')
  97. ->limit($limit)
  98. ->get()
  99. ->toArray();
  100. }
  101. public static function allAtLeast(int $studentId, array $kpCodes, float $threshold): bool
  102. {
  103. if (empty($kpCodes)) {
  104. return false;
  105. }
  106. // 使用 DB::table 避免 Eloquent accessor 把数字转成文字标签
  107. $levels = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery')
  108. ->where('student_id', $studentId)
  109. ->whereIn('kp_code', $kpCodes)
  110. ->pluck('mastery_level', 'kp_code')
  111. ->toArray();
  112. foreach ($kpCodes as $kpCode) {
  113. $level = isset($levels[$kpCode]) ? (float) $levels[$kpCode] : 0.0;
  114. if ($level < $threshold) {
  115. return false;
  116. }
  117. }
  118. return true;
  119. }
  120. /**
  121. * 判断所有知识点是否达标(跳过没有题目的知识点)
  122. * 用于章节摸底后的知识点学习流程
  123. *
  124. * @param int $studentId 学生ID
  125. * @param array $kpCodes 知识点编码列表
  126. * @param float $threshold 达标阈值(默认0.9)
  127. * @return bool 是否全部达标
  128. */
  129. public static function allAtLeastSkipNoQuestions(int $studentId, array $kpCodes, float $threshold = 0.9): bool
  130. {
  131. if (empty($kpCodes)) {
  132. return true; // 没有知识点,视为达标
  133. }
  134. // 获取掌握度(使用 DB::table 避免 Eloquent accessor 把数字转成文字标签)
  135. $levels = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery')
  136. ->where('student_id', $studentId)
  137. ->whereIn('kp_code', $kpCodes)
  138. ->pluck('mastery_level', 'kp_code')
  139. ->toArray();
  140. // 获取有题目的知识点
  141. $kpCodesWithQuestions = \App\Models\Question::query()
  142. ->whereIn('kp_code', $kpCodes)
  143. ->distinct()
  144. ->pluck('kp_code')
  145. ->toArray();
  146. $hasAnyKpWithQuestions = false;
  147. foreach ($kpCodes as $kpCode) {
  148. // 跳过没有题目的知识点
  149. if (!in_array($kpCode, $kpCodesWithQuestions)) {
  150. continue;
  151. }
  152. $hasAnyKpWithQuestions = true;
  153. $level = isset($levels[$kpCode]) ? (float) $levels[$kpCode] : 0.0;
  154. if ($level < $threshold) {
  155. return false;
  156. }
  157. }
  158. // 如果没有任何有题的知识点,视为达标
  159. return $hasAnyKpWithQuestions ? true : true;
  160. }
  161. /**
  162. * 获取第一个未达标的知识点(跳过没有题目的知识点)
  163. *
  164. * @param int $studentId 学生ID
  165. * @param array $kpCodes 知识点编码列表(按顺序)
  166. * @param float $threshold 达标阈值(默认0.9)
  167. * @return string|null 第一个未达标的知识点编码,如果全部达标返回null
  168. */
  169. public static function getFirstUnmasteredKpCode(int $studentId, array $kpCodes, float $threshold = 0.9): ?string
  170. {
  171. if (empty($kpCodes)) {
  172. return null;
  173. }
  174. // 获取掌握度(使用 DB::table 避免 Eloquent accessor 把数字转成文字标签)
  175. $levels = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery')
  176. ->where('student_id', $studentId)
  177. ->whereIn('kp_code', $kpCodes)
  178. ->pluck('mastery_level', 'kp_code')
  179. ->toArray();
  180. // 获取有题目的知识点
  181. $kpCodesWithQuestions = \App\Models\Question::query()
  182. ->whereIn('kp_code', $kpCodes)
  183. ->distinct()
  184. ->pluck('kp_code')
  185. ->toArray();
  186. foreach ($kpCodes as $kpCode) {
  187. // 跳过没有题目的知识点
  188. if (!in_array($kpCode, $kpCodesWithQuestions)) {
  189. continue;
  190. }
  191. $level = isset($levels[$kpCode]) ? (float) $levels[$kpCode] : 0.0;
  192. if ($level < $threshold) {
  193. return $kpCode;
  194. }
  195. }
  196. return null; // 全部达标
  197. }
  198. /**
  199. * 计算掌握度等级
  200. */
  201. public function getMasteryLevelAttribute($value): string
  202. {
  203. if ($value >= 0.85) {
  204. return '优秀';
  205. } elseif ($value >= 0.70) {
  206. return '良好';
  207. } elseif ($value >= 0.50) {
  208. return '及格';
  209. } else {
  210. return '薄弱';
  211. }
  212. }
  213. /**
  214. * 获取趋势标签
  215. */
  216. public function getTrendLabelAttribute(): string
  217. {
  218. return match ($this->mastery_trend) {
  219. 'improving' => '上升',
  220. 'declining' => '下降',
  221. 'stable' => '稳定',
  222. 'insufficient' => '数据不足',
  223. default => '未知',
  224. };
  225. }
  226. /**
  227. * 计算成功率
  228. */
  229. public function getSuccessRateAttribute(): float
  230. {
  231. if ($this->total_attempts <= 0) {
  232. return 0.0;
  233. }
  234. return round(($this->correct_attempts / $this->total_attempts) * 100, 2);
  235. }
  236. }