StudentProgressService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Collection;
  6. class StudentProgressService
  7. {
  8. /**
  9. * 计算学生学习进度(总体掌握度)
  10. * 公式:所有子知识点掌握度累加值 / 所有子知识点掌握度的累加值
  11. *
  12. * @param string $studentId
  13. * @return array
  14. */
  15. public function calculateLearningProgress(string $studentId): array
  16. {
  17. try {
  18. // 1. 获取所有父知识点代码(需要排除的)
  19. $parentKpCodes = $this->getParentKnowledgePointCodes();
  20. // 2. 获取学生所有知识点的掌握度数据(合并两个表)
  21. $allMasteryData = $this->getMergedMasteryData($studentId);
  22. // 3. 过滤掉父知识点,只保留子知识点
  23. $childMasteryData = $allMasteryData->filter(function ($item) use ($parentKpCodes) {
  24. return !in_array($item['kp_code'], $parentKpCodes);
  25. });
  26. if ($childMasteryData->isEmpty()) {
  27. return [
  28. 'success' => false,
  29. 'error' => '该学生没有子知识点的掌握度数据'
  30. ];
  31. }
  32. // 4. 计算总体掌握度
  33. $totalMasterySum = $childMasteryData->sum('mastery_level');
  34. $masteryCount = $childMasteryData->count();
  35. $overallMastery = $masteryCount > 0 ? $totalMasterySum / $masteryCount : 0.0;
  36. // 5. 统计信息
  37. $statistics = [
  38. 'total_child_kps' => $masteryCount,
  39. 'average_mastery' => round($overallMastery, 4),
  40. 'max_mastery' => round($childMasteryData->max('mastery_level'), 4),
  41. 'min_mastery' => round($childMasteryData->min('mastery_level'), 4),
  42. 'data_source' => $this->getDataSourceInfo($allMasteryData),
  43. 'parent_kp_excluded' => count($parentKpCodes),
  44. 'mastery_distribution' => $this->getMasteryDistribution($childMasteryData)
  45. ];
  46. $result = [
  47. 'student_id' => $studentId,
  48. 'overall_mastery' => round($overallMastery, 4),
  49. 'child_knowledge_points' => $childMasteryData->values()->toArray(),
  50. 'statistics' => $statistics,
  51. 'calculated_at' => now()->toISOString()
  52. ];
  53. Log::info('学生学习进度计算成功', [
  54. 'student_id' => $studentId,
  55. 'overall_mastery' => $overallMastery,
  56. 'child_kp_count' => $masteryCount,
  57. 'parent_kp_excluded' => count($parentKpCodes)
  58. ]);
  59. return [
  60. 'success' => true,
  61. 'data' => $result
  62. ];
  63. } catch (\Exception $e) {
  64. Log::error('计算学生学习进度失败', [
  65. 'student_id' => $studentId,
  66. 'error' => $e->getMessage()
  67. ]);
  68. return [
  69. 'success' => false,
  70. 'error' => '计算学习进度时发生错误: ' . $e->getMessage()
  71. ];
  72. }
  73. }
  74. /**
  75. * 获取学生知识点掌握度详情
  76. *
  77. * @param string $studentId
  78. * @return array
  79. */
  80. public function getKnowledgePointDetails(string $studentId): array
  81. {
  82. try {
  83. // 获取合并的掌握度数据
  84. $masteryData = $this->getMergedMasteryData($studentId);
  85. // 获取父知识点列表
  86. $parentKpCodes = $this->getParentKnowledgePointCodes();
  87. // 分类数据
  88. $childData = [];
  89. $parentData = [];
  90. foreach ($masteryData as $item) {
  91. if (in_array($item['kp_code'], $parentKpCodes)) {
  92. $parentData[] = $item;
  93. } else {
  94. $childData[] = $item;
  95. }
  96. }
  97. // 获取知识点详细信息
  98. $knowledgePointDetails = $this->getKnowledgePointDetailsByCodes(
  99. array_column($masteryData->toArray(), 'kp_code')
  100. );
  101. // 合并知识点信息
  102. $enhancedData = [];
  103. foreach ($masteryData as $item) {
  104. $kpCode = $item['kp_code'];
  105. $detail = $knowledgePointDetails[$kpCode] ?? null;
  106. $enhancedData[] = array_merge($item, [
  107. 'knowledge_point_name' => $detail['name'] ?? '未知知识点',
  108. 'subject' => $detail['subject'] ?? null,
  109. 'grade' => $detail['grade'] ?? null,
  110. 'is_parent' => in_array($kpCode, $parentKpCodes),
  111. 'parent_kp_code' => $detail['parent_kp_code'] ?? null
  112. ]);
  113. }
  114. return [
  115. 'student_id' => $studentId,
  116. 'mastery_data' => $enhancedData,
  117. 'summary' => [
  118. 'total_kps' => count($enhancedData),
  119. 'child_kps' => count($childData),
  120. 'parent_kps' => count($parentData),
  121. 'overall_child_mastery' => count($childData) > 0 ?
  122. round(array_sum(array_column($childData, 'mastery_level')) / count($childData), 4) : 0.0
  123. ]
  124. ];
  125. } catch (\Exception $e) {
  126. Log::error('获取学生知识点掌握度详情失败', [
  127. 'student_id' => $studentId,
  128. 'error' => $e->getMessage()
  129. ]);
  130. throw $e;
  131. }
  132. }
  133. /**
  134. * 批量计算学生学习进度
  135. *
  136. * @param array $studentIds
  137. * @return array
  138. */
  139. public function batchCalculateLearningProgress(array $studentIds): array
  140. {
  141. $results = [];
  142. foreach ($studentIds as $studentId) {
  143. $result = $this->calculateLearningProgress($studentId);
  144. $results[] = [
  145. 'student_id' => $studentId,
  146. 'success' => $result['success'],
  147. 'data' => $result['success'] ? $result['data'] : null,
  148. 'error' => $result['success'] ? null : $result['error']
  149. ];
  150. }
  151. return $results;
  152. }
  153. /**
  154. * 获取所有父知识点代码
  155. *
  156. * @return array
  157. */
  158. private function getParentKnowledgePointCodes(): array
  159. {
  160. try {
  161. $parentCodes = DB::connection('remote_mysql')
  162. ->table('knowledge_points')
  163. ->whereNotNull('parent_kp_code')
  164. ->distinct()
  165. ->pluck('parent_kp_code')
  166. ->toArray();
  167. return array_filter($parentCodes);
  168. } catch (\Exception $e) {
  169. Log::error('获取父知识点代码失败', ['error' => $e->getMessage()]);
  170. return [];
  171. }
  172. }
  173. /**
  174. * 获取合并的学生掌握度数据(从两个表)
  175. *
  176. * @param string $studentId
  177. * @return Collection
  178. */
  179. private function getMergedMasteryData(string $studentId): Collection
  180. {
  181. $mergedData = [];
  182. try {
  183. // 从 student_knowledge_mastery 表获取数据
  184. $detailedData = DB::connection('remote_mysql')
  185. ->table('student_knowledge_mastery')
  186. ->where('student_id', $studentId)
  187. ->select([
  188. 'kp_code',
  189. 'mastery_level',
  190. 'total_attempts',
  191. 'correct_attempts',
  192. 'updated_at'
  193. ])
  194. ->get()
  195. ->toArray();
  196. foreach ($detailedData as $item) {
  197. $mergedData[$item->kp_code] = [
  198. 'kp_code' => $item->kp_code,
  199. 'mastery_level' => (float) $item->mastery_level,
  200. 'total_attempts' => $item->total_attempts,
  201. 'correct_attempts' => $item->correct_attempts,
  202. 'source_table' => 'student_knowledge_mastery',
  203. 'updated_at' => $item->updated_at
  204. ];
  205. }
  206. } catch (\Exception $e) {
  207. Log::warning('从 student_knowledge_mastery 表获取数据失败', [
  208. 'student_id' => $studentId,
  209. 'error' => $e->getMessage()
  210. ]);
  211. }
  212. try {
  213. // 从 student_mastery 表获取数据(补充或覆盖)
  214. $simpleData = DB::connection('remote_mysql')
  215. ->table('student_mastery')
  216. ->where('student_id', $studentId)
  217. ->select([
  218. 'kp as kp_code',
  219. 'mastery',
  220. 'attempts as total_attempts',
  221. 'correct as correct_attempts',
  222. 'updated_at'
  223. ])
  224. ->get()
  225. ->toArray();
  226. foreach ($simpleData as $item) {
  227. $kpCode = $item->kp_code;
  228. $masteryLevel = (float) $item->mastery;
  229. // 如果已存在,优先使用 mastery_level 更高的数据
  230. if (isset($mergedData[$kpCode])) {
  231. if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
  232. $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
  233. $mergedData[$kpCode]['source_table'] = 'student_mastery (updated)';
  234. }
  235. } else {
  236. $mergedData[$kpCode] = [
  237. 'kp_code' => $kpCode,
  238. 'mastery_level' => $masteryLevel,
  239. 'total_attempts' => $item->total_attempts ?? 0,
  240. 'correct_attempts' => $item->correct_attempts ?? 0,
  241. 'source_table' => 'student_mastery',
  242. 'updated_at' => $item->updated_at ?? null
  243. ];
  244. }
  245. }
  246. } catch (\Exception $e) {
  247. Log::warning('从 student_mastery 表获取数据失败', [
  248. 'student_id' => $studentId,
  249. 'error' => $e->getMessage()
  250. ]);
  251. }
  252. return collect($mergedData)->values();
  253. }
  254. /**
  255. * 获取知识点详细信息
  256. *
  257. * @param array $kpCodes
  258. * @return array
  259. */
  260. private function getKnowledgePointDetailsByCodes(array $kpCodes): array
  261. {
  262. if (empty($kpCodes)) {
  263. return [];
  264. }
  265. try {
  266. $details = DB::connection('remote_mysql')
  267. ->table('knowledge_points')
  268. ->whereIn('kp_code', $kpCodes)
  269. ->select(['kp_code', 'name', 'subject', 'grade', 'parent_kp_code'])
  270. ->get()
  271. ->keyBy('kp_code')
  272. ->toArray();
  273. return $details;
  274. } catch (\Exception $e) {
  275. Log::warning('获取知识点详细信息失败', [
  276. 'kp_codes' => $kpCodes,
  277. 'error' => $e->getMessage()
  278. ]);
  279. return [];
  280. }
  281. }
  282. /**
  283. * 获取数据源信息
  284. *
  285. * @param Collection $masteryData
  286. * @return string
  287. */
  288. private function getDataSourceInfo(Collection $masteryData): string
  289. {
  290. $sourceTables = $masteryData->pluck('source_table')->unique()->toArray();
  291. if (count($sourceTables) === 1) {
  292. return $sourceTables[0];
  293. }
  294. return 'merged (' . implode(', ', $sourceTables) . ')';
  295. }
  296. /**
  297. * 获取掌握度分布
  298. *
  299. * @param Collection $masteryData
  300. * @return array
  301. */
  302. private function getMasteryDistribution(Collection $masteryData): array
  303. {
  304. $distribution = [
  305. 'excellent' => 0, // >= 0.9
  306. 'good' => 0, // 0.7 - 0.89
  307. 'fair' => 0, // 0.5 - 0.69
  308. 'poor' => 0, // < 0.5
  309. 'unknown' => 0 // 无数据
  310. ];
  311. foreach ($masteryData as $item) {
  312. $mastery = $item['mastery_level'];
  313. if ($mastery >= 0.9) {
  314. $distribution['excellent']++;
  315. } elseif ($mastery >= 0.7) {
  316. $distribution['good']++;
  317. } elseif ($mastery >= 0.5) {
  318. $distribution['fair']++;
  319. } elseif ($mastery > 0) {
  320. $distribution['poor']++;
  321. } else {
  322. $distribution['unknown']++;
  323. }
  324. }
  325. return $distribution;
  326. }
  327. }