KnowledgeGraphService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. class KnowledgeGraphService
  6. {
  7. protected string $baseUrl;
  8. public function __construct()
  9. {
  10. // 从配置文件读取base_url,如果没有配置则使用容器服务名
  11. $this->baseUrl = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', env('KNOWLEDGE_API_BASE_URL', 'http://localhost:5011')));
  12. $this->baseUrl = rtrim($this->baseUrl, '/');
  13. }
  14. /**
  15. * 获取知识点列表
  16. */
  17. public function listKnowledgePoints(int $page = 1, int $perPage = 100): array
  18. {
  19. try {
  20. $response = Http::timeout(10)
  21. ->withHeaders(['Accept' => 'application/json'])
  22. ->get($this->baseUrl . '/knowledge-points/', [
  23. 'page' => $page,
  24. 'per_page' => $perPage
  25. ]);
  26. if ($response->successful()) {
  27. $data = $response->json();
  28. $points = $data['data'] ?? $data ?? [];
  29. // 格式化知识点数据
  30. $formattedPoints = array_map(function($kp) {
  31. return [
  32. 'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
  33. 'kp_code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
  34. 'cn_name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
  35. 'category' => $kp['category'] ?? '数学',
  36. 'phase' => $kp['phase'] ?? '',
  37. 'grade' => $kp['grade'] ?? null,
  38. 'importance' => $kp['importance'] ?? 0,
  39. 'description' => $kp['description'] ?? '',
  40. ];
  41. }, $points);
  42. // 返回标准格式:{data: [...], meta: {...}}
  43. return [
  44. 'data' => $formattedPoints,
  45. 'meta' => $data['meta'] ?? []
  46. ];
  47. }
  48. Log::warning('知识图谱API调用失败', [
  49. 'status' => $response->status(),
  50. 'url' => $this->baseUrl . '/knowledge-points',
  51. 'response' => $response->json()
  52. ]);
  53. } catch (\Exception $e) {
  54. Log::error('获取知识点列表失败', [
  55. 'error' => $e->getMessage()
  56. ]);
  57. }
  58. // 返回备用数据
  59. $fallback = $this->getFallbackKnowledgePoints();
  60. return [
  61. 'data' => $fallback,
  62. 'meta' => ['total' => count($fallback)]
  63. ];
  64. }
  65. /**
  66. * 根据知识点代码获取技能列表
  67. */
  68. public function getSkillsByKnowledgePoint(string $kpCode): array
  69. {
  70. try {
  71. $response = Http::timeout(10)
  72. ->withHeaders(['Accept' => 'application/json'])
  73. ->get($this->baseUrl . "/graph/node/{$kpCode}");
  74. if ($response->successful()) {
  75. $data = $response->json();
  76. $skills = $data['skills'] ?? [];
  77. return array_map(function($skill) {
  78. return [
  79. 'code' => $skill['skill_code'] ?? '',
  80. 'name' => $skill['skill_name'] ?? '',
  81. 'weight' => $skill['weight'] ?? 0,
  82. ];
  83. }, $skills);
  84. }
  85. Log::warning('获取技能列表失败', [
  86. 'status' => $response->status(),
  87. 'kp_code' => $kpCode
  88. ]);
  89. } catch (\Exception $e) {
  90. Log::error('获取技能列表异常', [
  91. 'kp_code' => $kpCode,
  92. 'error' => $e->getMessage()
  93. ]);
  94. }
  95. return [];
  96. }
  97. /**
  98. * 获取所有技能列表
  99. */
  100. public function listSkills(int $page = 1, int $perPage = 50): array
  101. {
  102. // ... (existing code)
  103. try {
  104. $response = Http::timeout(10)
  105. ->get($this->baseUrl . '/skills/', [
  106. 'page' => $page,
  107. 'per_page' => $perPage
  108. ]);
  109. if ($response->successful()) {
  110. $data = $response->json();
  111. $skills = $data['data'] ?? $data ?? [];
  112. // 格式化技能数据
  113. return array_map(function($skill) {
  114. return [
  115. 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
  116. 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
  117. 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
  118. 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
  119. ];
  120. }, $skills);
  121. }
  122. Log::warning('技能API调用失败', [
  123. 'status' => $response->status()
  124. ]);
  125. } catch (\Exception $e) {
  126. Log::error('获取技能列表失败', [
  127. 'error' => $e->getMessage()
  128. ]);
  129. }
  130. // 返回备用数据
  131. return $this->getFallbackSkills();
  132. }
  133. /**
  134. * 获取关联关系列表
  135. */
  136. public function listRelations(int $page = 1, int $perPage = 100): array
  137. {
  138. try {
  139. $response = Http::timeout(10)
  140. ->get($this->baseUrl . '/graph/relations', [
  141. 'page' => $page,
  142. 'per_page' => $perPage
  143. ]);
  144. if ($response->successful()) {
  145. $data = $response->json();
  146. $relations = $data['data'] ?? [];
  147. return array_map(function($rel) {
  148. return [
  149. 'source_kp' => $rel['source'] ?? $rel['source_kp'] ?? '',
  150. 'target_kp' => $rel['target'] ?? $rel['target_kp'] ?? '',
  151. 'relation_type' => $rel['relation_type'] ?? 'PREREQUISITE',
  152. 'relation_direction' => $rel['relation_direction'] ?? 'DOWNSTREAM',
  153. 'weight' => $rel['weight'] ?? 0,
  154. 'description' => $rel['description'] ?? '',
  155. ];
  156. }, $relations);
  157. }
  158. Log::warning('获取关联关系列表失败', [
  159. 'status' => $response->status()
  160. ]);
  161. } catch (\Exception $e) {
  162. Log::error('获取关联关系列表异常', [
  163. 'error' => $e->getMessage()
  164. ]);
  165. }
  166. return [];
  167. }
  168. /**
  169. * 导出完整图谱数据 (用于可视化)
  170. */
  171. public function exportGraph(): array
  172. {
  173. try {
  174. $response = Http::timeout(30)
  175. ->get($this->baseUrl . '/graph/export');
  176. if ($response->successful()) {
  177. return $response->json();
  178. }
  179. Log::warning('导出图谱数据失败', [
  180. 'status' => $response->status()
  181. ]);
  182. } catch (\Exception $e) {
  183. Log::error('导出图谱数据异常', [
  184. 'error' => $e->getMessage()
  185. ]);
  186. }
  187. return ['nodes' => [], 'edges' => []];
  188. }
  189. /**
  190. * 检查服务健康状态
  191. */
  192. public function checkHealth(): bool
  193. {
  194. try {
  195. $response = Http::timeout(5)
  196. ->get($this->baseUrl . '/health');
  197. return $response->successful();
  198. } catch (\Exception $e) {
  199. Log::error('知识图谱服务健康检查失败', [
  200. 'error' => $e->getMessage()
  201. ]);
  202. return false;
  203. }
  204. }
  205. /**
  206. * 获取备用知识点数据
  207. */
  208. private function getFallbackKnowledgePoints(): array
  209. {
  210. return [
  211. ['id' => 'kp_1', 'kp_code' => 'KP1001', 'cn_name' => '因式分解基础', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  212. ['id' => 'kp_2', 'kp_code' => 'KP1002', 'cn_name' => '提取公因式', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  213. ['id' => 'kp_3', 'kp_code' => 'KP1003', 'cn_name' => '平方差公式', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  214. ['id' => 'kp_4', 'kp_code' => 'KP1004', 'cn_name' => '完全平方公式', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  215. ['id' => 'kp_5', 'kp_code' => 'KP1005', 'cn_name' => '分组分解法', 'category' => '数学', 'phase' => '初中', 'importance' => 4, 'description' => ''],
  216. ['id' => 'kp_6', 'kp_code' => 'KP1006', 'cn_name' => '十字相乘法', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  217. ['id' => 'kp_7', 'kp_code' => 'KP1007', 'cn_name' => '有理数运算', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  218. ['id' => 'kp_8', 'kp_code' => 'KP1008', 'cn_name' => '一元二次方程', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  219. ['id' => 'kp_9', 'kp_code' => 'KP1009', 'cn_name' => '不等式', 'category' => '数学', 'phase' => '初中', 'importance' => 4, 'description' => ''],
  220. ['id' => 'kp_10', 'kp_code' => 'KP1010', 'cn_name' => '函数基础', 'category' => '数学', 'phase' => '初中', 'importance' => 5, 'description' => ''],
  221. ];
  222. }
  223. /**
  224. * 获取备用技能数据
  225. */
  226. private function getFallbackSkills(): array
  227. {
  228. return [
  229. ['id' => 'sk_1', 'code' => 'SK001', 'name' => '计算能力', 'category' => '基础技能'],
  230. ['id' => 'sk_2', 'code' => 'SK002', 'name' => '逻辑推理', 'category' => '思维技能'],
  231. ['id' => 'sk_3', 'code' => 'SK003', 'name' => '模式识别', 'category' => '认知技能'],
  232. ['id' => 'sk_4', 'code' => 'SK004', 'name' => '代数运算', 'category' => '专业技能'],
  233. ['id' => 'sk_5', 'code' => 'SK005', 'name' => '解题能力', 'category' => '专业技能'],
  234. ['id' => 'sk_6', 'code' => 'SK006', 'name' => '分析能力', 'category' => '思维技能'],
  235. ['id' => 'sk_7', 'code' => 'SK007', 'name' => '抽象思维', 'category' => '高级技能'],
  236. ['id' => 'sk_8', 'code' => 'SK008', 'name' => '创新思维', 'category' => '高级技能'],
  237. ];
  238. }
  239. /**
  240. * 导入知识图谱数据
  241. */
  242. public function importGraph(array $treeData, array $edgesData): bool
  243. {
  244. try {
  245. $response = Http::timeout(60) // 导入可能耗时较长
  246. ->post($this->baseUrl . '/import/graph', [
  247. 'tree_data' => $treeData,
  248. 'edges_data' => $edgesData
  249. ]);
  250. if ($response->successful()) {
  251. $result = $response->json();
  252. Log::info('知识图谱导入成功', [
  253. 'response' => $result
  254. ]);
  255. return true;
  256. }
  257. $errorBody = $response->body();
  258. Log::error('知识图谱导入失败', [
  259. 'status' => $response->status(),
  260. 'body' => $errorBody
  261. ]);
  262. // 尝试解析错误信息
  263. try {
  264. $errorData = json_decode($errorBody, true);
  265. if (isset($errorData['detail'])) {
  266. throw new \Exception('API错误: ' . $errorData['detail']);
  267. }
  268. } catch (\Exception $e) {
  269. // 忽略JSON解析错误,使用原始错误信息
  270. }
  271. throw new \Exception("API请求失败 (HTTP {$response->status()}): {$errorBody}");
  272. } catch (\Exception $e) {
  273. Log::error('知识图谱导入异常', [
  274. 'error' => $e->getMessage(),
  275. 'trace' => $e->getTraceAsString()
  276. ]);
  277. // 重新抛出异常,让上层处理
  278. throw $e;
  279. }
  280. }
  281. /**
  282. * 获取单个知识点详情
  283. */
  284. public function getKnowledgePoint(string $code): ?array
  285. {
  286. try {
  287. $response = Http::timeout(10)
  288. ->get($this->baseUrl . "/knowledge-points/{$code}");
  289. if ($response->successful()) {
  290. return $response->json();
  291. }
  292. } catch (\Exception $e) {
  293. Log::error('获取知识点详情失败', ['error' => $e->getMessage()]);
  294. }
  295. return null;
  296. }
  297. /**
  298. * 创建知识点
  299. */
  300. public function createKnowledgePoint(array $data): bool
  301. {
  302. try {
  303. $response = Http::timeout(10)
  304. ->post($this->baseUrl . '/knowledge-points/', $data);
  305. return $response->successful();
  306. } catch (\Exception $e) {
  307. Log::error('创建知识点失败', ['error' => $e->getMessage()]);
  308. return false;
  309. }
  310. }
  311. /**
  312. * 更新知识点
  313. */
  314. public function updateKnowledgePoint(string $code, array $data): bool
  315. {
  316. try {
  317. $response = Http::timeout(10)
  318. ->patch($this->baseUrl . "/knowledge-points/{$code}", $data);
  319. return $response->successful();
  320. } catch (\Exception $e) {
  321. Log::error('更新知识点失败', ['error' => $e->getMessage()]);
  322. return false;
  323. }
  324. }
  325. /**
  326. * 删除知识点
  327. */
  328. public function deleteKnowledgePoint(string $code): bool
  329. {
  330. try {
  331. $response = Http::timeout(10)
  332. ->delete($this->baseUrl . "/knowledge-points/{$code}");
  333. return $response->successful();
  334. } catch (\Exception $e) {
  335. Log::error('删除知识点失败', ['error' => $e->getMessage()]);
  336. return false;
  337. }
  338. }
  339. /**
  340. * 获取学生的知识点掌握度数据
  341. */
  342. public function getStudentMastery($studentId)
  343. {
  344. try {
  345. // 这里应该调用LearningAnalytics API
  346. $response = Http::timeout(10)
  347. ->get("http://localhost:5010/api/mastery/{$studentId}");
  348. if ($response->successful()) {
  349. return $response->json();
  350. }
  351. } catch (\Exception $e) {
  352. Log::warning('获取学生掌握度数据失败', [
  353. 'student_id' => $studentId,
  354. 'error' => $e->getMessage(),
  355. ]);
  356. }
  357. // 返回模拟数据
  358. return [
  359. 'masteries' => [
  360. ['kp_code' => 'R01', 'mastery_level' => 0.85, 'confidence_level' => 0.8],
  361. ['kp_code' => 'R02', 'mastery_level' => 0.72, 'confidence_level' => 0.75],
  362. ['kp_code' => 'R03', 'mastery_level' => 0.65, 'confidence_level' => 0.7],
  363. ['kp_code' => 'R04', 'mastery_level' => 0.45, 'confidence_level' => 0.65],
  364. ['kp_code' => 'R05', 'mastery_level' => 0.30, 'confidence_level' => 0.6],
  365. ]
  366. ];
  367. }
  368. /**
  369. * 获取学生掌握度统计信息
  370. */
  371. public function getStudentStatistics($studentId)
  372. {
  373. try {
  374. $response = Http::timeout(10)
  375. ->get("http://localhost:5010/api/mastery/{$studentId}/statistics");
  376. if ($response->successful()) {
  377. return $response->json();
  378. }
  379. } catch (\Exception $e) {
  380. Log::warning('获取学生统计信息失败', [
  381. 'student_id' => $studentId,
  382. 'error' => $e->getMessage(),
  383. ]);
  384. }
  385. // 返回模拟数据
  386. return [
  387. 'total_knowledge_points' => 5,
  388. 'average_mastery' => 0.594,
  389. 'high_mastery_count' => 1,
  390. 'medium_mastery_count' => 2,
  391. 'low_mastery_count' => 2,
  392. ];
  393. }
  394. /**
  395. * 获取掌握度颜色
  396. */
  397. public function getMasteryColor($mastery)
  398. {
  399. if ($mastery >= 0.8) return '#10b981'; // 绿色
  400. if ($mastery >= 0.6) return '#3b82f6'; // 蓝色
  401. if ($mastery >= 0.4) return '#f59e0b'; // 黄色
  402. if ($mastery >= 0.2) return '#f97316'; // 橙色
  403. return '#ef4444'; // 红色
  404. }
  405. }