KnowledgeMasteryService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. // 收集所有kp_code
  82. $kpCodes = array_column($data['details'], 'kp_code');
  83. if (empty($kpCodes)) {
  84. return $data;
  85. }
  86. // 批量获取知识点名称
  87. $kpNames = $this->getKnowledgePointNames($kpCodes);
  88. // 丰富details数据
  89. foreach ($data['details'] as &$detail) {
  90. $kpCode = $detail['kp_code'] ?? null;
  91. if ($kpCode && isset($kpNames[$kpCode])) {
  92. $detail['kp_name'] = $kpNames[$kpCode];
  93. } else {
  94. $detail['kp_name'] = $kpCode; // fallback to code
  95. }
  96. }
  97. return $data;
  98. }
  99. /**
  100. * 批量获取知识点名称
  101. */
  102. private function getKnowledgePointNames(array $kpCodes): array
  103. {
  104. $result = [];
  105. foreach ($kpCodes as $kpCode) {
  106. $name = $this->getKnowledgePointName($kpCode);
  107. if ($name) {
  108. $result[$kpCode] = $name;
  109. }
  110. }
  111. return $result;
  112. }
  113. /**
  114. * 获取单个知识点名称(带缓存)
  115. */
  116. private function getKnowledgePointName(string $kpCode): ?string
  117. {
  118. $cacheKey = "kp_name_{$kpCode}";
  119. return Cache::remember($cacheKey, 3600, function () use ($kpCode) {
  120. try {
  121. $response = Http::timeout(5)
  122. ->get($this->knowledgeServiceBase . '/knowledge-points/' . $kpCode);
  123. if ($response->successful()) {
  124. $data = $response->json();
  125. return $data['cn_name'] ?? $data['en_name'] ?? null;
  126. }
  127. } catch (\Throwable $e) {
  128. Log::debug('Failed to get knowledge point name', [
  129. 'kp_code' => $kpCode,
  130. 'error' => $e->getMessage(),
  131. ]);
  132. }
  133. return null;
  134. });
  135. }
  136. /**
  137. * 获取知识图谱统计信息(带缓存)
  138. */
  139. public function getKnowledgeGraphStats(): array
  140. {
  141. return Cache::remember('knowledge_graph_stats', 3600, function () {
  142. try {
  143. $response = Http::timeout(10)
  144. ->get($this->knowledgeServiceBase . '/knowledge-points/');
  145. if ($response->successful()) {
  146. $data = $response->json();
  147. $items = $data['data'] ?? $data ?? [];
  148. return [
  149. 'total' => count($items),
  150. 'updated_at' => now()->toISOString(),
  151. ];
  152. }
  153. } catch (\Throwable $e) {
  154. Log::error('Failed to get knowledge graph stats', [
  155. 'error' => $e->getMessage(),
  156. ]);
  157. }
  158. return ['total' => 0];
  159. });
  160. }
  161. /**
  162. * 获取学生知识点图谱数据
  163. *
  164. * @param string $studentId 学生ID
  165. * @param string|null $examId 考试ID(可选,不指定则返回最新快照)
  166. * @return array
  167. */
  168. public function getGraph(string $studentId, ?string $examId = null): array
  169. {
  170. try {
  171. // 使用本地的MasteryCalculator
  172. $masteryCalculator = app(MasteryCalculator::class);
  173. $overview = $masteryCalculator->getStudentMasteryOverview($studentId);
  174. // 转换为图谱格式
  175. $nodes = [];
  176. $edges = [];
  177. foreach ($overview['details'] as $detail) {
  178. $masteryLevel = floatval($detail->mastery_level ?? 0);
  179. $kpCode = $detail->kp_code;
  180. // 节点
  181. $nodes[] = [
  182. 'id' => $kpCode,
  183. 'label' => $kpCode,
  184. 'mastery' => $masteryLevel,
  185. 'mastery_level' => $this->getMasteryLevelLabel($masteryLevel),
  186. 'color' => $this->getMasteryColor($masteryLevel),
  187. 'size' => 20 + ($masteryLevel * 20),
  188. ];
  189. // 这里可以添加知识点之间的依赖关系边
  190. // 暂时为空,后续从knowledge_points表获取
  191. }
  192. $graphData = [
  193. 'nodes' => $nodes,
  194. 'edges' => $edges,
  195. 'statistics' => [
  196. 'total_nodes' => count($nodes),
  197. 'total_edges' => count($edges),
  198. 'average_mastery' => $overview['average_mastery_level'],
  199. ],
  200. ];
  201. Log::info('KnowledgeMasteryService::getGraph (Local)', ['student_id' => $studentId, 'exam_id' => $examId]);
  202. return [
  203. 'success' => true,
  204. 'data' => $graphData,
  205. ];
  206. } catch (\Throwable $e) {
  207. Log::error('Knowledge graph exception', [
  208. 'student_id' => $studentId,
  209. 'exam_id' => $examId,
  210. 'error' => $e->getMessage(),
  211. ]);
  212. return [
  213. 'success' => false,
  214. 'error' => '获取知识点图谱异常: ' . $e->getMessage(),
  215. ];
  216. }
  217. }
  218. /**
  219. * 获取掌握度等级标签
  220. */
  221. private function getMasteryLevelLabel(float $mastery): string
  222. {
  223. if ($mastery >= 0.85) return 'mastered';
  224. if ($mastery >= 0.70) return 'good';
  225. if ($mastery >= 0.50) return 'fair';
  226. return 'weak';
  227. }
  228. /**
  229. * 获取掌握度颜色
  230. */
  231. private function getMasteryColor(float $mastery): string
  232. {
  233. if ($mastery >= 0.85) return '#52c41a'; // 绿色 - 掌握
  234. if ($mastery >= 0.70) return '#1890ff'; // 蓝色 - 良好
  235. if ($mastery >= 0.50) return '#faad14'; // 橙色 - 一般
  236. return '#ff4d4f'; // 红色 - 薄弱
  237. }
  238. /**
  239. * 获取学生知识点图谱快照列表
  240. *
  241. * @param string $studentId 学生ID
  242. * @param int $limit 返回数量限制
  243. * @return array
  244. */
  245. public function getGraphSnapshots(string $studentId, int $limit = 10): array
  246. {
  247. try {
  248. // 从knowledge_point_mastery_snapshots表获取快照
  249. $snapshots = \DB::table('knowledge_point_mastery_snapshots')
  250. ->where('student_id', $studentId)
  251. ->orderBy('created_at', 'desc')
  252. ->limit($limit)
  253. ->get();
  254. $snapshotList = $snapshots->map(function ($snapshot) {
  255. return [
  256. 'snapshot_id' => $snapshot->id,
  257. 'student_id' => $snapshot->student_id,
  258. 'kp_code' => $snapshot->kp_code,
  259. 'mastery_level' => floatval($snapshot->mastery_level),
  260. 'snapshot_type' => $snapshot->snapshot_type,
  261. 'source_id' => $snapshot->source_id,
  262. 'source_name' => $snapshot->source_name,
  263. 'notes' => $snapshot->notes,
  264. 'created_at' => $snapshot->created_at,
  265. ];
  266. })->toArray();
  267. Log::info('KnowledgeMasteryService::getGraphSnapshots (Local)', ['student_id' => $studentId, 'limit' => $limit]);
  268. return [
  269. 'success' => true,
  270. 'data' => [
  271. 'snapshots' => $snapshotList,
  272. 'total' => count($snapshotList),
  273. ],
  274. ];
  275. } catch (\Throwable $e) {
  276. Log::error('Knowledge graph snapshots exception', [
  277. 'student_id' => $studentId,
  278. 'limit' => $limit,
  279. 'error' => $e->getMessage(),
  280. ]);
  281. return [
  282. 'success' => false,
  283. 'error' => '获取知识点图谱快照列表异常: ' . $e->getMessage(),
  284. ];
  285. }
  286. }
  287. /**
  288. * 获取学生知识点掌握摘要(简化版)
  289. *
  290. * @param string $studentId 学生ID
  291. * @return array
  292. */
  293. public function getSummary(string $studentId): array
  294. {
  295. $stats = $this->getStats($studentId);
  296. if (!$stats['success']) {
  297. return $stats;
  298. }
  299. $data = $stats['data'];
  300. return [
  301. 'success' => true,
  302. 'data' => [
  303. 'student_id' => $data['student_id'] ?? $studentId,
  304. 'total' => $data['total_knowledge_points'] ?? 0,
  305. 'mastered' => $data['mastered_knowledge_points'] ?? 0,
  306. 'unmastered' => $data['unmastered_knowledge_points'] ?? 0,
  307. 'mastery_rate' => $data['mastery_rate'] ?? 0.0,
  308. 'mastery_percentage' => round(($data['mastery_rate'] ?? 0) * 100, 1) . '%',
  309. 'graph_total' => $data['graph_total_knowledge_points'] ?? 0,
  310. ],
  311. ];
  312. }
  313. /**
  314. * 创建知识点掌握度快照
  315. *
  316. * @param string $studentId 学生ID
  317. * @param string $snapshotType 快照类型 (exam/report/manual/scheduled)
  318. * @param string|null $sourceId 来源ID
  319. * @param string|null $sourceName 来源名称
  320. * @param string|null $notes 备注
  321. * @return array
  322. */
  323. public function createSnapshot(
  324. string $studentId,
  325. string $snapshotType = 'report',
  326. ?string $sourceId = null,
  327. ?string $sourceName = null,
  328. ?string $notes = null
  329. ): array {
  330. try {
  331. // 使用LocalAIAnalysisService创建快照
  332. $localAI = app(LocalAIAnalysisService::class);
  333. // 获取当前掌握度数据
  334. $masteryCalculator = app(MasteryCalculator::class);
  335. $overview = $masteryCalculator->getStudentMasteryOverview($studentId);
  336. $snapshots = [];
  337. foreach ($overview['details'] as $detail) {
  338. $snapshotId = \DB::table('knowledge_point_mastery_snapshots')->insertGetId([
  339. 'student_id' => $studentId,
  340. 'kp_code' => $detail->kp_code,
  341. 'mastery_level' => $detail->mastery_level,
  342. 'confidence_level' => $detail->confidence_level,
  343. 'snapshot_type' => $snapshotType,
  344. 'source_id' => $sourceId,
  345. 'source_name' => $sourceName,
  346. 'notes' => $notes,
  347. 'created_at' => now(),
  348. 'updated_at' => now(),
  349. ]);
  350. $snapshots[] = [
  351. 'snapshot_id' => $snapshotId,
  352. 'kp_code' => $detail->kp_code,
  353. 'mastery_level' => $detail->mastery_level,
  354. ];
  355. }
  356. Log::info('KnowledgeMasteryService::createSnapshot (Local)', [
  357. 'student_id' => $studentId,
  358. 'snapshot_type' => $snapshotType,
  359. 'snapshot_count' => count($snapshots),
  360. ]);
  361. return [
  362. 'success' => true,
  363. 'data' => [
  364. 'snapshots' => $snapshots,
  365. 'total_snapshots' => count($snapshots),
  366. ],
  367. ];
  368. } catch (\Throwable $e) {
  369. Log::error('Create knowledge mastery snapshot exception', [
  370. 'student_id' => $studentId,
  371. 'error' => $e->getMessage(),
  372. ]);
  373. return [
  374. 'success' => false,
  375. 'error' => '创建知识点掌握度快照异常: ' . $e->getMessage(),
  376. ];
  377. }
  378. }
  379. }