KnowledgeMasteryService.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Facades\Cache;
  6. /**
  7. * 知识点掌握情况服务
  8. *
  9. * 提供:
  10. * 1. 获取学生知识点掌握情况统计
  11. * 2. 获取知识点图谱数据(考试快照)
  12. * 3. 获取知识点图谱快照列表
  13. */
  14. class KnowledgeMasteryService
  15. {
  16. protected string $learningAnalyticsBase;
  17. protected string $knowledgeServiceBase;
  18. protected int $timeout;
  19. public function __construct(?string $learningAnalyticsBase = null, ?string $knowledgeServiceBase = null, ?int $timeout = null)
  20. {
  21. // 已迁移到本地,使用MasteryCalculator
  22. $this->learningAnalyticsBase = '';
  23. $this->knowledgeServiceBase = rtrim(
  24. $knowledgeServiceBase
  25. ?: config('services.knowledge_service.url', env('KNOWLEDGE_SERVICE_API_BASE', 'http://localhost:5011')),
  26. '/'
  27. );
  28. $this->timeout = 20;
  29. }
  30. /**
  31. * 获取学生知识点掌握情况统计
  32. *
  33. * @param string $studentId 学生ID
  34. * @return array
  35. */
  36. public function getStats(string $studentId): array
  37. {
  38. try {
  39. // 使用本地的MasteryCalculator
  40. $masteryCalculator = app(MasteryCalculator::class);
  41. $overview = $masteryCalculator->getStudentMasteryOverview($studentId);
  42. // 转换为兼容格式
  43. $body = [
  44. 'student_id' => $studentId,
  45. 'total_knowledge_points' => $overview['total_knowledge_points'],
  46. 'average_mastery' => $overview['average_mastery_level'],
  47. 'mastered_count' => $overview['mastered_knowledge_points'],
  48. 'good_count' => $overview['good_knowledge_points'],
  49. 'weak_count' => $overview['weak_knowledge_points'],
  50. 'details' => $overview['details'],
  51. ];
  52. // 丰富知识点名称(如果有knowledge_points表)
  53. $body = $this->enrichWithKnowledgePointNames($body);
  54. // 添加知识图谱总数统计
  55. $graphStats = $this->getKnowledgeGraphStats();
  56. $body['graph_total_knowledge_points'] = $graphStats['total'] ?? 0;
  57. Log::info('KnowledgeMasteryService::getStats (Local)', ['student_id' => $studentId]);
  58. return [
  59. 'success' => true,
  60. 'data' => $body,
  61. ];
  62. } catch (\Throwable $e) {
  63. Log::error('Knowledge mastery stats exception', [
  64. 'student_id' => $studentId,
  65. 'error' => $e->getMessage(),
  66. ]);
  67. return [
  68. 'success' => false,
  69. 'error' => '获取知识点掌握情况异常: ' . $e->getMessage(),
  70. ];
  71. }
  72. }
  73. /**
  74. * 丰富知识点名称
  75. */
  76. private function enrichWithKnowledgePointNames(array $data): array
  77. {
  78. if (empty($data['details'])) {
  79. return $data;
  80. }
  81. // 确保 details 是数组格式(处理 stdClass 对象)
  82. $details = $data['details'];
  83. if (!is_array($details)) {
  84. $details = [];
  85. } elseif (isset($details[0]) && is_object($details[0])) {
  86. // 将 stdClass 对象转换为关联数组
  87. $details = array_map(function ($item) {
  88. return (array) $item;
  89. }, $details);
  90. }
  91. // 收集所有kp_code
  92. $kpCodes = array_column($details, 'kp_code');
  93. if (empty($kpCodes)) {
  94. return $data;
  95. }
  96. // 批量获取知识点名称
  97. $kpNames = $this->getKnowledgePointNames($kpCodes);
  98. // 丰富details数据
  99. foreach ($details as &$detail) {
  100. $kpCode = $detail['kp_code'] ?? null;
  101. if ($kpCode && isset($kpNames[$kpCode])) {
  102. $detail['kp_name'] = $kpNames[$kpCode];
  103. } else {
  104. $detail['kp_name'] = $kpCode; // fallback to code
  105. }
  106. }
  107. // 更新原始数据
  108. $data['details'] = $details;
  109. return $data;
  110. }
  111. /**
  112. * 批量获取知识点名称
  113. */
  114. private function getKnowledgePointNames(array $kpCodes): array
  115. {
  116. $result = [];
  117. foreach ($kpCodes as $kpCode) {
  118. $name = $this->getKnowledgePointName($kpCode);
  119. if ($name) {
  120. $result[$kpCode] = $name;
  121. }
  122. }
  123. return $result;
  124. }
  125. /**
  126. * 获取单个知识点名称(带缓存)
  127. */
  128. private function getKnowledgePointName(string $kpCode): ?string
  129. {
  130. $cacheKey = "kp_name_{$kpCode}";
  131. return Cache::remember($cacheKey, 3600, function () use ($kpCode) {
  132. try {
  133. $response = Http::timeout(5)
  134. ->get($this->knowledgeServiceBase . '/knowledge-points/' . $kpCode);
  135. if ($response->successful()) {
  136. $data = $response->json();
  137. return $data['cn_name'] ?? $data['en_name'] ?? null;
  138. }
  139. } catch (\Throwable $e) {
  140. Log::debug('Failed to get knowledge point name', [
  141. 'kp_code' => $kpCode,
  142. 'error' => $e->getMessage(),
  143. ]);
  144. }
  145. return null;
  146. });
  147. }
  148. /**
  149. * 获取知识图谱统计信息(带缓存)
  150. */
  151. public function getKnowledgeGraphStats(): array
  152. {
  153. return Cache::remember('knowledge_graph_stats', 3600, function () {
  154. try {
  155. $response = Http::timeout(10)
  156. ->get($this->knowledgeServiceBase . '/knowledge-points/');
  157. if ($response->successful()) {
  158. $data = $response->json();
  159. $items = $data['data'] ?? $data ?? [];
  160. return [
  161. 'total' => count($items),
  162. 'updated_at' => now()->toISOString(),
  163. ];
  164. }
  165. } catch (\Throwable $e) {
  166. Log::error('Failed to get knowledge graph stats', [
  167. 'error' => $e->getMessage(),
  168. ]);
  169. }
  170. return ['total' => 0];
  171. });
  172. }
  173. /**
  174. * 获取学生知识点图谱数据
  175. *
  176. * @param string $studentId 学生ID
  177. * @param string|null $examId 考试ID(可选,不指定则返回最新快照)
  178. * @return array
  179. */
  180. public function getGraph(string $studentId, ?string $examId = null): array
  181. {
  182. try {
  183. // 使用本地的MasteryCalculator
  184. $masteryCalculator = app(MasteryCalculator::class);
  185. $overview = $masteryCalculator->getStudentMasteryOverview($studentId);
  186. // 转换为图谱格式
  187. $nodes = [];
  188. $edges = [];
  189. foreach ($overview['details'] as $detail) {
  190. $masteryLevel = floatval($detail->mastery_level ?? 0);
  191. $kpCode = $detail->kp_code;
  192. // 节点
  193. $nodes[] = [
  194. 'id' => $kpCode,
  195. 'label' => $kpCode,
  196. 'mastery' => $masteryLevel,
  197. 'mastery_level' => $this->getMasteryLevelLabel($masteryLevel),
  198. 'color' => $this->getMasteryColor($masteryLevel),
  199. 'size' => 20 + ($masteryLevel * 20),
  200. ];
  201. // 这里可以添加知识点之间的依赖关系边
  202. // 暂时为空,后续从knowledge_points表获取
  203. }
  204. $graphData = [
  205. 'nodes' => $nodes,
  206. 'edges' => $edges,
  207. 'statistics' => [
  208. 'total_nodes' => count($nodes),
  209. 'total_edges' => count($edges),
  210. 'average_mastery' => $overview['average_mastery_level'],
  211. ],
  212. ];
  213. Log::info('KnowledgeMasteryService::getGraph (Local)', ['student_id' => $studentId, 'exam_id' => $examId]);
  214. return [
  215. 'success' => true,
  216. 'data' => $graphData,
  217. ];
  218. } catch (\Throwable $e) {
  219. Log::error('Knowledge graph exception', [
  220. 'student_id' => $studentId,
  221. 'exam_id' => $examId,
  222. 'error' => $e->getMessage(),
  223. ]);
  224. return [
  225. 'success' => false,
  226. 'error' => '获取知识点图谱异常: ' . $e->getMessage(),
  227. ];
  228. }
  229. }
  230. /**
  231. * 获取掌握度等级标签
  232. */
  233. private function getMasteryLevelLabel(float $mastery): string
  234. {
  235. if ($mastery >= 0.85) return 'mastered';
  236. if ($mastery >= 0.70) return 'good';
  237. if ($mastery >= 0.50) return 'fair';
  238. return 'weak';
  239. }
  240. /**
  241. * 获取掌握度颜色
  242. */
  243. private function getMasteryColor(float $mastery): string
  244. {
  245. if ($mastery >= 0.85) return '#52c41a'; // 绿色 - 掌握
  246. if ($mastery >= 0.70) return '#1890ff'; // 蓝色 - 良好
  247. if ($mastery >= 0.50) return '#faad14'; // 橙色 - 一般
  248. return '#ff4d4f'; // 红色 - 薄弱
  249. }
  250. /**
  251. * 获取学生知识点图谱快照列表
  252. *
  253. * @param string $studentId 学生ID
  254. * @param int $limit 返回数量限制
  255. * @return array
  256. */
  257. public function getGraphSnapshots(string $studentId, int $limit = 10): array
  258. {
  259. try {
  260. // 从knowledge_point_mastery_snapshots表获取快照
  261. $snapshots = \DB::table('knowledge_point_mastery_snapshots')
  262. ->where('student_id', $studentId)
  263. ->orderBy('created_at', 'desc')
  264. ->limit($limit)
  265. ->get();
  266. $snapshotList = $snapshots->map(function ($snapshot) {
  267. // 解析掌握度数据
  268. $masteryData = json_decode($snapshot->mastery_data, true) ?: [];
  269. // 提取知识点信息
  270. $knowledgePoints = [];
  271. foreach ($masteryData as $kpData) {
  272. $knowledgePoints[] = [
  273. 'kp_code' => $kpData['kp_code'] ?? '',
  274. 'kp_name' => $kpData['kp_name'] ?? ($kpData['kp_code'] ?? ''),
  275. 'mastery_level' => floatval($kpData['mastery_level'] ?? 0),
  276. ];
  277. }
  278. return [
  279. 'snapshot_id' => $snapshot->snapshot_id,
  280. 'student_id' => $snapshot->student_id,
  281. 'overall_mastery' => floatval($snapshot->overall_mastery),
  282. 'weak_knowledge_points_count' => intval($snapshot->weak_knowledge_points_count),
  283. 'strong_knowledge_points_count' => intval($snapshot->strong_knowledge_points_count),
  284. 'knowledge_points' => $knowledgePoints,
  285. 'created_at' => $snapshot->created_at,
  286. 'snapshot_time' => $snapshot->snapshot_time,
  287. ];
  288. })->toArray();
  289. Log::info('KnowledgeMasteryService::getGraphSnapshots (Local)', ['student_id' => $studentId, 'limit' => $limit]);
  290. return [
  291. 'success' => true,
  292. 'data' => [
  293. 'snapshots' => $snapshotList,
  294. 'total' => count($snapshotList),
  295. ],
  296. ];
  297. } catch (\Throwable $e) {
  298. Log::error('Knowledge graph snapshots exception', [
  299. 'student_id' => $studentId,
  300. 'limit' => $limit,
  301. 'error' => $e->getMessage(),
  302. ]);
  303. return [
  304. 'success' => false,
  305. 'error' => '获取知识点图谱快照列表异常: ' . $e->getMessage(),
  306. ];
  307. }
  308. }
  309. /**
  310. * 获取学生知识点掌握摘要(简化版)
  311. *
  312. * @param string $studentId 学生ID
  313. * @return array
  314. */
  315. public function getSummary(string $studentId): array
  316. {
  317. $stats = $this->getStats($studentId);
  318. if (!$stats['success']) {
  319. return $stats;
  320. }
  321. $data = $stats['data'];
  322. return [
  323. 'success' => true,
  324. 'data' => [
  325. 'student_id' => $data['student_id'] ?? $studentId,
  326. 'total' => $data['total_knowledge_points'] ?? 0,
  327. 'mastered' => $data['mastered_knowledge_points'] ?? 0,
  328. 'unmastered' => $data['unmastered_knowledge_points'] ?? 0,
  329. 'mastery_rate' => $data['mastery_rate'] ?? 0.0,
  330. 'mastery_percentage' => round(($data['mastery_rate'] ?? 0) * 100, 1) . '%',
  331. 'graph_total' => $data['graph_total_knowledge_points'] ?? 0,
  332. ],
  333. ];
  334. }
  335. /**
  336. * 创建知识点掌握度快照
  337. *
  338. * @param string $studentId 学生ID
  339. * @param string $snapshotType 快照类型 (exam/report/manual/scheduled)
  340. * @param string|null $sourceId 来源ID
  341. * @param string|null $sourceName 来源名称
  342. * @param string|null $notes 备注
  343. * @return array
  344. */
  345. public function createSnapshot(
  346. string $studentId,
  347. string $snapshotType = 'report',
  348. ?string $sourceId = null,
  349. ?string $sourceName = null,
  350. ?string $notes = null
  351. ): array {
  352. try {
  353. // 使用LocalAIAnalysisService创建快照
  354. $localAI = app(LocalAIAnalysisService::class);
  355. // 获取当前掌握度数据
  356. $masteryCalculator = app(MasteryCalculator::class);
  357. $overview = $masteryCalculator->getStudentMasteryOverview($studentId);
  358. $snapshots = [];
  359. foreach ($overview['details'] as $detail) {
  360. $snapshotId = \DB::table('knowledge_point_mastery_snapshots')->insertGetId([
  361. 'student_id' => $studentId,
  362. 'kp_code' => $detail->kp_code,
  363. 'mastery_level' => $detail->mastery_level,
  364. 'confidence_level' => $detail->confidence_level,
  365. 'snapshot_type' => $snapshotType,
  366. 'source_id' => $sourceId,
  367. 'source_name' => $sourceName,
  368. 'notes' => $notes,
  369. 'created_at' => now(),
  370. 'updated_at' => now(),
  371. ]);
  372. $snapshots[] = [
  373. 'snapshot_id' => $snapshotId,
  374. 'kp_code' => $detail->kp_code,
  375. 'mastery_level' => $detail->mastery_level,
  376. ];
  377. }
  378. Log::info('KnowledgeMasteryService::createSnapshot (Local)', [
  379. 'student_id' => $studentId,
  380. 'snapshot_type' => $snapshotType,
  381. 'snapshot_count' => count($snapshots),
  382. ]);
  383. return [
  384. 'success' => true,
  385. 'data' => [
  386. 'snapshots' => $snapshots,
  387. 'total_snapshots' => count($snapshots),
  388. ],
  389. ];
  390. } catch (\Throwable $e) {
  391. Log::error('Create knowledge mastery snapshot exception', [
  392. 'student_id' => $studentId,
  393. 'error' => $e->getMessage(),
  394. ]);
  395. return [
  396. 'success' => false,
  397. 'error' => '创建知识点掌握度快照异常: ' . $e->getMessage(),
  398. ];
  399. }
  400. }
  401. }