StudentKnowledgeController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Models\StudentKnowledgeMastery;
  5. use App\Models\KnowledgePoint;
  6. use Illuminate\Http\JsonResponse;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Log;
  10. class StudentKnowledgeController extends Controller
  11. {
  12. /**
  13. * 获取学生知识点掌握详情
  14. *
  15. * @param string $studentId
  16. * @param Request $request
  17. * @return JsonResponse
  18. */
  19. public function getKnowledgePointsDetail(string $studentId, Request $request): JsonResponse
  20. {
  21. try {
  22. // 验证学生是否存在
  23. $student = \App\Models\Student::where('student_id', $studentId)->first();
  24. if (!$student) {
  25. return response()->json([
  26. 'success' => false,
  27. 'message' => '学生不存在',
  28. ], 404);
  29. }
  30. // 获取查询参数
  31. $level = $request->query('level'); // 筛选特定层级的知识点
  32. $sort = $request->query('sort', 'mastery_asc'); // 排序方式
  33. // 构建查询(解决字符集不匹配问题)
  34. // 【修复】显式选择mastery_level和mastery_change字段,避免访问器转换
  35. $query = StudentKnowledgeMastery::forStudent($studentId)
  36. ->with('knowledgePoint') // 预加载知识点信息
  37. ->select([
  38. 'student_knowledge_mastery.*',
  39. 'knowledge_points.name as kp_name',
  40. 'knowledge_points.parent_kp_code',
  41. // 显式选择原始字段,避免访问器
  42. DB::raw('CAST(student_knowledge_mastery.mastery_level AS DECIMAL(10,4)) as mastery_level_raw'),
  43. DB::raw('CAST(student_knowledge_mastery.mastery_change AS DECIMAL(10,4)) as mastery_change_raw'),
  44. ])
  45. ->join('knowledge_points', 'student_knowledge_mastery.kp_code', '=', DB::raw('CAST(knowledge_points.kp_code AS CHAR)'));
  46. // 按层级筛选
  47. if ($level) {
  48. if ($level === 'top') {
  49. // 只查询顶级知识点(没有父级)
  50. $query->whereNull('knowledge_points.parent_kp_code');
  51. } elseif ($level === 'leaf') {
  52. // 只查询叶子知识点(没有子级)
  53. $query->whereNotIn('knowledge_points.kp_code', function ($q) {
  54. $q->select('parent_kp_code')
  55. ->from('knowledge_points')
  56. ->whereNotNull('parent_kp_code');
  57. });
  58. }
  59. }
  60. // 排序
  61. switch ($sort) {
  62. case 'mastery_desc':
  63. $query->orderBy('mastery_level', 'desc');
  64. break;
  65. case 'name_asc':
  66. $query->orderBy('kp_name', 'asc');
  67. break;
  68. case 'name_desc':
  69. $query->orderBy('kp_name', 'desc');
  70. break;
  71. case 'attempts_desc':
  72. $query->orderBy('total_attempts', 'desc');
  73. break;
  74. case 'mastery_asc':
  75. default:
  76. $query->orderBy('mastery_level', 'asc');
  77. break;
  78. }
  79. $masteryRecords = $query->get();
  80. // 构建返回数据
  81. $knowledgePoints = [];
  82. foreach ($masteryRecords as $record) {
  83. // 计算正确率
  84. $correctRate = $record->total_attempts > 0
  85. ? round(($record->correct_attempts / $record->total_attempts) * 100, 2) / 100
  86. : 0.0;
  87. // 计算稳定度(基于近期的变化趋势)
  88. $stability = $this->calculateStability($record);
  89. // 获取层级信息
  90. $level = $record->knowledgePoint->parent_kp_code ? '子级' : '顶级';
  91. // 获取父子关系
  92. $parentName = null;
  93. if ($record->knowledgePoint->parent_kp_code) {
  94. $parentKp = KnowledgePoint::where('kp_code', $record->knowledgePoint->parent_kp_code)->first();
  95. $parentName = $parentKp?->name ?? $record->knowledgePoint->parent_kp_code;
  96. }
  97. // 【修复】使用CAST后的原始字段,避免访问器转换
  98. $rawMasteryLevel = $record->mastery_level_raw;
  99. $rawMasteryChange = $record->mastery_change_raw;
  100. $masteryLevel = is_numeric($rawMasteryLevel) ? (float)$rawMasteryLevel : 0.0;
  101. $masteryChange = is_numeric($rawMasteryChange) ? (float)$rawMasteryChange : 0.0;
  102. $knowledgePoints[] = [
  103. 'kp_code' => $record->kp_code,
  104. 'knowledge_point' => $record->kp_name ?: $record->kp_code, // 使用名称,如果没有名称则使用代码
  105. 'mastery' => round($masteryLevel, 4),
  106. 'stability' => $stability,
  107. 'level' => $level,
  108. 'parent_kp_code' => $record->knowledgePoint->parent_kp_code,
  109. 'parent_kp_name' => $parentName,
  110. 'last_updated' => $record->last_mastery_update?->toISOString(),
  111. 'practice_count' => (int)$record->total_attempts,
  112. 'correct_rate' => $correctRate,
  113. 'mastery_change' => $masteryChange,
  114. 'mastery_trend' => $record->mastery_trend,
  115. 'trend_label' => $record->trend_label ?? '稳定',
  116. ];
  117. }
  118. // 【新功能】获取父节点掌握度
  119. $parentMasteryLevels = $this->getParentMasteryLevels($studentId, $knowledgePoints);
  120. Log::info('获取学生知识点掌握详情', [
  121. 'student_id' => $studentId,
  122. 'count' => count($knowledgePoints),
  123. 'parent_count' => count($parentMasteryLevels),
  124. 'level' => $level,
  125. 'sort' => $sort,
  126. ]);
  127. return response()->json([
  128. 'success' => true,
  129. 'data' => [
  130. 'student_id' => $studentId,
  131. 'student_name' => $student->name,
  132. 'knowledge_points' => array_values($knowledgePoints), // 重新索引
  133. 'parent_mastery_levels' => $parentMasteryLevels, // 新增:父节点掌握度
  134. 'total_count' => count($knowledgePoints),
  135. ],
  136. ]);
  137. } catch (\Exception $e) {
  138. Log::error('获取学生知识点掌握详情失败', [
  139. 'student_id' => $studentId,
  140. 'error' => $e->getMessage(),
  141. 'trace' => $e->getTraceAsString(),
  142. ]);
  143. return response()->json([
  144. 'success' => false,
  145. 'message' => '获取失败:' . $e->getMessage(),
  146. ], 500);
  147. }
  148. }
  149. /**
  150. * 计算稳定度
  151. */
  152. private function calculateStability(StudentKnowledgeMastery $record): float
  153. {
  154. // 如果数据不足,返回默认值
  155. if ($record->total_attempts < 3) {
  156. return 0.5;
  157. }
  158. // 基于掌握度变化计算稳定度
  159. // 变化越小,稳定度越高
  160. $changeAbs = abs($record->mastery_change ?? 0);
  161. // 稳定度 = 1 - (变化幅度 * 10),范围0-1
  162. $stability = max(0, min(1, 1 - ($changeAbs * 10)));
  163. return round($stability, 4);
  164. }
  165. /**
  166. * 获取学生知识点层级关系
  167. *
  168. * @param string $studentId
  169. * @return JsonResponse
  170. */
  171. public function getKnowledgeHierarchy(string $studentId): JsonResponse
  172. {
  173. try {
  174. // 获取学生的所有知识点
  175. $masteryRecords = StudentKnowledgeMastery::forStudent($studentId)
  176. ->with('knowledgePoint')
  177. ->get();
  178. // 构建父子关系
  179. $hierarchy = [];
  180. $processed = [];
  181. foreach ($masteryRecords as $record) {
  182. $kpCode = $record->kp_code;
  183. // 避免重复处理
  184. if (isset($processed[$kpCode])) {
  185. continue;
  186. }
  187. $kp = $record->knowledgePoint;
  188. if (!$kp) {
  189. continue;
  190. }
  191. $processed[$kpCode] = true;
  192. // 如果是顶级知识点
  193. if (!$kp->parent_kp_code) {
  194. $hierarchy[] = [
  195. 'kp_code' => $kpCode,
  196. 'kp_name' => $kp->name ?: $kpCode,
  197. 'mastery' => round($record->mastery_level, 4),
  198. 'level' => 'top',
  199. 'children' => $this->getChildKnowledgePoints($masteryRecords, $kpCode),
  200. ];
  201. }
  202. }
  203. // 如果没有顶级知识点,尝试构建其他层级的结构
  204. if (empty($hierarchy)) {
  205. foreach ($masteryRecords as $record) {
  206. $kpCode = $record->kp_code;
  207. $kp = $record->knowledgePoint;
  208. if (!$kp) {
  209. continue;
  210. }
  211. $hierarchy[] = [
  212. 'kp_code' => $kpCode,
  213. 'kp_name' => $kp->name ?: $kpCode,
  214. 'mastery' => round($record->mastery_level, 4),
  215. 'level' => $kp->parent_kp_code ? 'child' : 'top',
  216. 'parent_kp_code' => $kp->parent_kp_code,
  217. ];
  218. }
  219. }
  220. return response()->json([
  221. 'success' => true,
  222. 'data' => [
  223. 'student_id' => $studentId,
  224. 'hierarchy' => $hierarchy,
  225. 'total_count' => count($hierarchy),
  226. ],
  227. ]);
  228. } catch (\Exception $e) {
  229. Log::error('获取学生知识点层级关系失败', [
  230. 'student_id' => $studentId,
  231. 'error' => $e->getMessage(),
  232. ]);
  233. return response()->json([
  234. 'success' => false,
  235. 'message' => '获取失败:' . $e->getMessage(),
  236. ], 500);
  237. }
  238. }
  239. /**
  240. * 获取子知识点
  241. */
  242. private function getChildKnowledgePoints($masteryRecords, string $parentKpCode): array
  243. {
  244. $children = [];
  245. foreach ($masteryRecords as $record) {
  246. $kp = $record->knowledgePoint;
  247. if ($kp && $kp->parent_kp_code === $parentKpCode) {
  248. $children[] = [
  249. 'kp_code' => $kp->kp_code,
  250. 'kp_name' => $kp->name ?: $kp->kp_code,
  251. 'mastery' => round($record->mastery_level, 4),
  252. 'level' => 'child',
  253. 'children' => $this->getChildKnowledgePoints($masteryRecords, $kp->kp_code),
  254. ];
  255. }
  256. }
  257. return $children;
  258. }
  259. /**
  260. * 【新功能】获取父节点掌握度
  261. */
  262. private function getParentMasteryLevels(string $studentId, array $knowledgePoints): array
  263. {
  264. try {
  265. $parentMasteryLevels = [];
  266. // 收集所有父节点代码
  267. $parentKpCodes = [];
  268. foreach ($knowledgePoints as $kp) {
  269. if (!empty($kp['parent_kp_code'])) {
  270. $parentKpCodes[$kp['parent_kp_code']] = true;
  271. }
  272. }
  273. if (empty($parentKpCodes)) {
  274. return $parentMasteryLevels;
  275. }
  276. // 查询父节点的掌握度
  277. $parentRecords = StudentKnowledgeMastery::forStudent($studentId)
  278. ->whereIn('kp_code', array_keys($parentKpCodes))
  279. ->get();
  280. foreach ($parentRecords as $record) {
  281. // 使用CAST后的原始字段
  282. $rawMasteryLevel = $record->getAttributeValue('mastery_level');
  283. $rawMasteryChange = $record->getAttributeValue('mastery_change');
  284. $masteryLevel = is_numeric($rawMasteryLevel) ? (float)$rawMasteryLevel : 0.0;
  285. $masteryChange = is_numeric($rawMasteryChange) ? (float)$rawMasteryChange : 0.0;
  286. // 获取父节点的父节点
  287. $kp = $record->knowledgePoint;
  288. $grandParentKpCode = $kp?->parent_kp_code;
  289. $parentMasteryLevels[] = [
  290. 'kp_code' => $record->kp_code,
  291. 'knowledge_point' => $kp?->name ?: $record->kp_code,
  292. 'mastery' => round($masteryLevel, 4),
  293. 'mastery_change' => $masteryChange,
  294. 'mastery_trend' => $record->mastery_trend,
  295. 'trend_label' => $record->trend_label ?? '稳定',
  296. 'level' => '父级',
  297. 'parent_kp_code' => $grandParentKpCode,
  298. 'practice_count' => (int)$record->total_attempts,
  299. 'correct_rate' => $record->total_attempts > 0
  300. ? round(($record->correct_attempts / $record->total_attempts), 4)
  301. : 0.0,
  302. 'is_calculated' => ($record->mastery_trend === 'calculated'), // 标记为计算得出
  303. 'last_updated' => $record->last_mastery_update?->toISOString(),
  304. ];
  305. }
  306. return $parentMasteryLevels;
  307. } catch (\Exception $e) {
  308. Log::error('获取父节点掌握度失败', [
  309. 'student_id' => $studentId,
  310. 'error' => $e->getMessage(),
  311. ]);
  312. return [];
  313. }
  314. }
  315. }