StudentProgressService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Facades\Cache;
  6. class StudentProgressService
  7. {
  8. /**
  9. * 获取单个学生的学习进度
  10. */
  11. public function getProgress(string $studentId): array
  12. {
  13. Log::info('获取学生学习进度', ['student_id' => $studentId]);
  14. // 获取知识图谱结构
  15. $graphData = $this->getKnowledgeGraphStructure();
  16. $leafKpCodes = $graphData['leafKpCodes'];
  17. $leafKpCodesSet = $graphData['leafKpCodesSet'];
  18. $kpCodes = $graphData['kpCodes'];
  19. $maxChildScore = count($leafKpCodes) * 1.0;
  20. // 获取学生掌握度数据
  21. $mergedData = $this->getStudentMasteryData($studentId);
  22. // 如果没有数据,返回空结构(和批量接口行为一致)
  23. if (empty($mergedData)) {
  24. return [
  25. 'success' => true,
  26. 'data' => [
  27. 'student_id' => $studentId,
  28. 'learning_progress' => 0,
  29. 'learning_progress_percentage' => 0,
  30. 'mastered_child_count' => 0,
  31. 'total_child_count' => count($leafKpCodes),
  32. 'child_mastery_sum' => 0,
  33. 'has_data' => false,
  34. 'calculated_at' => now()->toISOString()
  35. ],
  36. 'message' => '该学生暂无学习数据'
  37. ];
  38. }
  39. // 筛选叶子节点
  40. $childMasteryData = collect($mergedData)->filter(function ($item) use ($leafKpCodesSet) {
  41. return isset($leafKpCodesSet[$item['kp_code']]);
  42. });
  43. // 如果没有子知识点数据,也返回空结构
  44. if ($childMasteryData->isEmpty()) {
  45. return [
  46. 'success' => true,
  47. 'data' => [
  48. 'student_id' => $studentId,
  49. 'learning_progress' => 0,
  50. 'learning_progress_percentage' => 0,
  51. 'mastered_child_count' => 0,
  52. 'total_child_count' => count($leafKpCodes),
  53. 'child_mastery_sum' => 0,
  54. 'has_data' => false,
  55. 'calculated_at' => now()->toISOString()
  56. ],
  57. 'message' => '该学生暂无子知识点学习数据'
  58. ];
  59. }
  60. // 计算学习进度
  61. $totalChildMasterySum = $childMasteryData->sum('mastery_level');
  62. $learningProgress = $maxChildScore > 0 ? ($totalChildMasterySum / $maxChildScore) : 0.0;
  63. // 统计信息
  64. $statistics = [
  65. 'total_knowledge_points' => count($kpCodes),
  66. 'child_knowledge_points' => count($leafKpCodes),
  67. 'student_mastered_child_count' => $childMasteryData->count(),
  68. 'student_mastered_child_percentage' => round(($childMasteryData->count() / count($leafKpCodes)) * 100, 2),
  69. 'child_mastery_sum' => round($totalChildMasterySum, 4),
  70. 'max_child_score' => round($maxChildScore, 4),
  71. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  72. 'data_source' => implode(', ', collect($mergedData)->pluck('source_table')->unique()->toArray()),
  73. 'child_mastery_max' => round($childMasteryData->max('mastery_level'), 4),
  74. 'child_mastery_min' => round($childMasteryData->min('mastery_level'), 4),
  75. 'child_mastery_avg' => round($childMasteryData->avg('mastery_level'), 4),
  76. ];
  77. Log::info('学生学习进度计算成功', [
  78. 'student_id' => $studentId,
  79. 'learning_progress' => $learningProgress,
  80. ]);
  81. return [
  82. 'success' => true,
  83. 'data' => [
  84. 'student_id' => $studentId,
  85. 'learning_progress' => round($learningProgress, 6),
  86. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  87. 'mastered_child_count' => $childMasteryData->count(),
  88. 'total_child_count' => count($leafKpCodes),
  89. 'child_mastery_sum' => round($totalChildMasterySum, 4),
  90. 'has_data' => true,
  91. 'child_knowledge_points' => $childMasteryData->values()->toArray(),
  92. 'statistics' => $statistics,
  93. 'calculated_at' => now()->toISOString()
  94. ],
  95. 'message' => '学习进度计算成功'
  96. ];
  97. }
  98. /**
  99. * 批量获取学生学习进度
  100. */
  101. public function getBatchProgress(array $studentIds): array
  102. {
  103. if (empty($studentIds)) {
  104. return [
  105. 'success' => false,
  106. 'error' => 'student_ids 不能为空'
  107. ];
  108. }
  109. if (count($studentIds) > 100) {
  110. return [
  111. 'success' => false,
  112. 'error' => '单次最多查询 100 个学生'
  113. ];
  114. }
  115. Log::info('批量获取学生学习进度', ['count' => count($studentIds)]);
  116. // 获取知识图谱结构(所有学生共用)
  117. $graphData = $this->getKnowledgeGraphStructure();
  118. $leafKpCodes = $graphData['leafKpCodes'];
  119. $leafKpCodesSet = $graphData['leafKpCodesSet'];
  120. $maxChildScore = count($leafKpCodes) * 1.0;
  121. // 批量获取掌握度数据
  122. $batchData = $this->getBatchStudentMasteryData($studentIds);
  123. $detailedData = $batchData['detailed'];
  124. $simpleData = $batchData['simple'];
  125. // 为每个学生计算学习进度
  126. $results = [];
  127. foreach ($studentIds as $studentId) {
  128. $studentId = (string) $studentId;
  129. $mergedData = [];
  130. // 从 student_knowledge_mastery 获取
  131. if (isset($detailedData[$studentId])) {
  132. foreach ($detailedData[$studentId] as $item) {
  133. $mergedData[$item->kp_code] = [
  134. 'kp_code' => $item->kp_code,
  135. 'mastery_level' => (float) $item->mastery_level,
  136. ];
  137. }
  138. }
  139. // 从 student_mastery 补充或更新
  140. if (isset($simpleData[$studentId])) {
  141. foreach ($simpleData[$studentId] as $item) {
  142. $kpCode = $item->kp_code;
  143. $masteryLevel = (float) $item->mastery;
  144. if (isset($mergedData[$kpCode])) {
  145. if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
  146. $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
  147. }
  148. } else {
  149. $mergedData[$kpCode] = [
  150. 'kp_code' => $kpCode,
  151. 'mastery_level' => $masteryLevel,
  152. ];
  153. }
  154. }
  155. }
  156. // 计算学习进度
  157. if (empty($mergedData)) {
  158. $results[$studentId] = [
  159. 'student_id' => $studentId,
  160. 'learning_progress' => 0,
  161. 'learning_progress_percentage' => 0,
  162. 'mastered_child_count' => 0,
  163. 'total_child_count' => count($leafKpCodes),
  164. 'has_data' => false,
  165. ];
  166. continue;
  167. }
  168. // 筛选叶子节点并计算
  169. $childMasterySum = 0;
  170. $masteredChildCount = 0;
  171. foreach ($mergedData as $item) {
  172. if (isset($leafKpCodesSet[$item['kp_code']])) {
  173. $childMasterySum += $item['mastery_level'];
  174. $masteredChildCount++;
  175. }
  176. }
  177. $learningProgress = $maxChildScore > 0 ? ($childMasterySum / $maxChildScore) : 0.0;
  178. $results[$studentId] = [
  179. 'student_id' => $studentId,
  180. 'learning_progress' => round($learningProgress, 6),
  181. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  182. 'mastered_child_count' => $masteredChildCount,
  183. 'total_child_count' => count($leafKpCodes),
  184. 'child_mastery_sum' => round($childMasterySum, 4),
  185. 'has_data' => true,
  186. ];
  187. }
  188. Log::info('批量学习进度计算完成', ['count' => count($results)]);
  189. return [
  190. 'success' => true,
  191. 'data' => $results,
  192. 'meta' => [
  193. 'total_students' => count($studentIds),
  194. 'total_child_knowledge_points' => count($leafKpCodes),
  195. 'calculated_at' => now()->toISOString(),
  196. ]
  197. ];
  198. }
  199. /**
  200. * 获取知识图谱结构(缓存 5 分钟)
  201. */
  202. private function getKnowledgeGraphStructure(): array
  203. {
  204. return Cache::remember('knowledge_graph_structure', 300, function () {
  205. $allKps = DB::connection('remote_mysql')
  206. ->table('knowledge_points')
  207. ->select(['kp_code', 'parent_kp_code'])
  208. ->get();
  209. $kpCodes = $allKps->pluck('kp_code')->toArray();
  210. $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray();
  211. $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes));
  212. return [
  213. 'kpCodes' => $kpCodes,
  214. 'leafKpCodes' => $leafKpCodes,
  215. 'leafKpCodesSet' => array_flip($leafKpCodes),
  216. ];
  217. });
  218. }
  219. /**
  220. * 获取单个学生的掌握度数据
  221. */
  222. private function getStudentMasteryData(string $studentId): array
  223. {
  224. $mergedData = [];
  225. try {
  226. $detailedData = DB::connection('remote_mysql')
  227. ->table('student_knowledge_mastery')
  228. ->where('student_id', $studentId)
  229. ->select(['kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at'])
  230. ->get();
  231. foreach ($detailedData as $item) {
  232. $mergedData[$item->kp_code] = [
  233. 'kp_code' => $item->kp_code,
  234. 'mastery_level' => (float) $item->mastery_level,
  235. 'total_attempts' => $item->total_attempts,
  236. 'correct_attempts' => $item->correct_attempts,
  237. 'source_table' => 'student_knowledge_mastery',
  238. 'updated_at' => $item->updated_at
  239. ];
  240. }
  241. } catch (\Exception $e) {
  242. Log::warning('从 student_knowledge_mastery 获取数据失败', ['error' => $e->getMessage()]);
  243. }
  244. try {
  245. $simpleData = DB::connection('remote_mysql')
  246. ->table('student_mastery')
  247. ->where('student_id', $studentId)
  248. ->select(['kp as kp_code', 'mastery', 'attempts as total_attempts', 'correct as correct_attempts', 'updated_at'])
  249. ->get();
  250. foreach ($simpleData as $item) {
  251. $kpCode = $item->kp_code;
  252. $masteryLevel = (float) $item->mastery;
  253. if (isset($mergedData[$kpCode])) {
  254. if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
  255. $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
  256. $mergedData[$kpCode]['source_table'] = 'student_mastery (updated)';
  257. }
  258. } else {
  259. $mergedData[$kpCode] = [
  260. 'kp_code' => $kpCode,
  261. 'mastery_level' => $masteryLevel,
  262. 'total_attempts' => $item->total_attempts ?? 0,
  263. 'correct_attempts' => $item->correct_attempts ?? 0,
  264. 'source_table' => 'student_mastery',
  265. 'updated_at' => $item->updated_at ?? null
  266. ];
  267. }
  268. }
  269. } catch (\Exception $e) {
  270. Log::warning('从 student_mastery 获取数据失败', ['error' => $e->getMessage()]);
  271. }
  272. return $mergedData;
  273. }
  274. /**
  275. * 批量获取学生掌握度数据
  276. */
  277. private function getBatchStudentMasteryData(array $studentIds): array
  278. {
  279. $detailedData = DB::connection('remote_mysql')
  280. ->table('student_knowledge_mastery')
  281. ->whereIn('student_id', $studentIds)
  282. ->select(['student_id', 'kp_code', 'mastery_level'])
  283. ->get()
  284. ->groupBy('student_id');
  285. $simpleData = DB::connection('remote_mysql')
  286. ->table('student_mastery')
  287. ->whereIn('student_id', $studentIds)
  288. ->select(['student_id', 'kp as kp_code', 'mastery'])
  289. ->get()
  290. ->groupBy('student_id');
  291. return [
  292. 'detailed' => $detailedData->toArray(),
  293. 'simple' => $simpleData->toArray(),
  294. ];
  295. }
  296. }