KnowledgeMasteryService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. $this->learningAnalyticsBase = rtrim(
  22. $learningAnalyticsBase
  23. ?: config('services.learning_analytics.url', env('LEARNING_ANALYTICS_API_BASE', 'http://localhost:5016')),
  24. '/'
  25. );
  26. $this->knowledgeServiceBase = rtrim(
  27. $knowledgeServiceBase
  28. ?: config('services.knowledge_service.url', env('KNOWLEDGE_SERVICE_API_BASE', 'http://localhost:5011')),
  29. '/'
  30. );
  31. $this->timeout = $timeout ?? (int) config('services.learning_analytics.timeout', 20);
  32. }
  33. /**
  34. * 获取学生知识点掌握情况统计
  35. *
  36. * @param string $studentId 学生ID
  37. * @return array
  38. */
  39. public function getStats(string $studentId): array
  40. {
  41. try {
  42. $response = Http::timeout($this->timeout)
  43. ->get($this->learningAnalyticsBase . '/api/knowledge-mastery/stats/' . $studentId);
  44. if ($response->successful()) {
  45. $body = $response->json();
  46. // 丰富知识点名称
  47. $body = $this->enrichWithKnowledgePointNames($body);
  48. // 添加知识图谱总数统计
  49. $graphStats = $this->getKnowledgeGraphStats();
  50. $body['graph_total_knowledge_points'] = $graphStats['total'] ?? 0;
  51. Log::info('KnowledgeMasteryService::getStats', ['student_id' => $studentId]);
  52. return [
  53. 'success' => true,
  54. 'data' => $body,
  55. ];
  56. }
  57. Log::warning('Knowledge mastery stats request failed', [
  58. 'student_id' => $studentId,
  59. 'status' => $response->status(),
  60. 'body' => $response->body(),
  61. ]);
  62. return [
  63. 'success' => false,
  64. 'error' => '获取知识点掌握情况失败: ' . $response->status(),
  65. ];
  66. } catch (\Throwable $e) {
  67. Log::error('Knowledge mastery stats exception', [
  68. 'student_id' => $studentId,
  69. 'error' => $e->getMessage(),
  70. ]);
  71. return [
  72. 'success' => false,
  73. 'error' => '获取知识点掌握情况异常: ' . $e->getMessage(),
  74. ];
  75. }
  76. }
  77. /**
  78. * 丰富知识点名称
  79. */
  80. private function enrichWithKnowledgePointNames(array $data): array
  81. {
  82. if (empty($data['details'])) {
  83. return $data;
  84. }
  85. // 收集所有kp_code
  86. $kpCodes = array_column($data['details'], 'kp_code');
  87. if (empty($kpCodes)) {
  88. return $data;
  89. }
  90. // 批量获取知识点名称
  91. $kpNames = $this->getKnowledgePointNames($kpCodes);
  92. // 丰富details数据
  93. foreach ($data['details'] as &$detail) {
  94. $kpCode = $detail['kp_code'] ?? null;
  95. if ($kpCode && isset($kpNames[$kpCode])) {
  96. $detail['kp_name'] = $kpNames[$kpCode];
  97. } else {
  98. $detail['kp_name'] = $kpCode; // fallback to code
  99. }
  100. }
  101. return $data;
  102. }
  103. /**
  104. * 批量获取知识点名称
  105. */
  106. private function getKnowledgePointNames(array $kpCodes): array
  107. {
  108. $result = [];
  109. foreach ($kpCodes as $kpCode) {
  110. $name = $this->getKnowledgePointName($kpCode);
  111. if ($name) {
  112. $result[$kpCode] = $name;
  113. }
  114. }
  115. return $result;
  116. }
  117. /**
  118. * 获取单个知识点名称(带缓存)
  119. */
  120. private function getKnowledgePointName(string $kpCode): ?string
  121. {
  122. $cacheKey = "kp_name_{$kpCode}";
  123. return Cache::remember($cacheKey, 3600, function () use ($kpCode) {
  124. try {
  125. $response = Http::timeout(5)
  126. ->get($this->knowledgeServiceBase . '/knowledge-points/' . $kpCode);
  127. if ($response->successful()) {
  128. $data = $response->json();
  129. return $data['cn_name'] ?? $data['en_name'] ?? null;
  130. }
  131. } catch (\Throwable $e) {
  132. Log::debug('Failed to get knowledge point name', [
  133. 'kp_code' => $kpCode,
  134. 'error' => $e->getMessage(),
  135. ]);
  136. }
  137. return null;
  138. });
  139. }
  140. /**
  141. * 获取知识图谱统计信息(带缓存)
  142. */
  143. public function getKnowledgeGraphStats(): array
  144. {
  145. return Cache::remember('knowledge_graph_stats', 3600, function () {
  146. try {
  147. $response = Http::timeout(10)
  148. ->get($this->knowledgeServiceBase . '/knowledge-points/');
  149. if ($response->successful()) {
  150. $data = $response->json();
  151. $items = $data['data'] ?? $data ?? [];
  152. return [
  153. 'total' => count($items),
  154. 'updated_at' => now()->toISOString(),
  155. ];
  156. }
  157. } catch (\Throwable $e) {
  158. Log::error('Failed to get knowledge graph stats', [
  159. 'error' => $e->getMessage(),
  160. ]);
  161. }
  162. return ['total' => 0];
  163. });
  164. }
  165. /**
  166. * 获取学生知识点图谱数据
  167. *
  168. * @param string $studentId 学生ID
  169. * @param string|null $examId 考试ID(可选,不指定则返回最新快照)
  170. * @return array
  171. */
  172. public function getGraph(string $studentId, ?string $examId = null): array
  173. {
  174. try {
  175. $query = array_filter(['exam_id' => $examId], fn($v) => filled($v));
  176. $response = Http::timeout($this->timeout)
  177. ->get($this->learningAnalyticsBase . '/api/knowledge-mastery/graph/' . $studentId, $query);
  178. if ($response->successful()) {
  179. $body = $response->json();
  180. Log::info('KnowledgeMasteryService::getGraph', ['student_id' => $studentId, 'exam_id' => $examId]);
  181. return [
  182. 'success' => true,
  183. 'data' => $body,
  184. ];
  185. }
  186. Log::warning('Knowledge graph request failed', [
  187. 'student_id' => $studentId,
  188. 'exam_id' => $examId,
  189. 'status' => $response->status(),
  190. 'body' => $response->body(),
  191. ]);
  192. return [
  193. 'success' => false,
  194. 'error' => '获取知识点图谱失败: ' . $response->status(),
  195. ];
  196. } catch (\Throwable $e) {
  197. Log::error('Knowledge graph exception', [
  198. 'student_id' => $studentId,
  199. 'exam_id' => $examId,
  200. 'error' => $e->getMessage(),
  201. ]);
  202. return [
  203. 'success' => false,
  204. 'error' => '获取知识点图谱异常: ' . $e->getMessage(),
  205. ];
  206. }
  207. }
  208. /**
  209. * 获取学生知识点图谱快照列表
  210. *
  211. * @param string $studentId 学生ID
  212. * @param int $limit 返回数量限制
  213. * @return array
  214. */
  215. public function getGraphSnapshots(string $studentId, int $limit = 10): array
  216. {
  217. try {
  218. $response = Http::timeout($this->timeout)
  219. ->get($this->learningAnalyticsBase . '/api/knowledge-mastery/graph/snapshots/' . $studentId, [
  220. 'limit' => $limit,
  221. ]);
  222. if ($response->successful()) {
  223. $body = $response->json();
  224. Log::info('KnowledgeMasteryService::getGraphSnapshots', ['student_id' => $studentId, 'limit' => $limit]);
  225. return [
  226. 'success' => true,
  227. 'data' => $body,
  228. ];
  229. }
  230. Log::warning('Knowledge graph snapshots request failed', [
  231. 'student_id' => $studentId,
  232. 'limit' => $limit,
  233. 'status' => $response->status(),
  234. 'body' => $response->body(),
  235. ]);
  236. return [
  237. 'success' => false,
  238. 'error' => '获取知识点图谱快照列表失败: ' . $response->status(),
  239. ];
  240. } catch (\Throwable $e) {
  241. Log::error('Knowledge graph snapshots exception', [
  242. 'student_id' => $studentId,
  243. 'limit' => $limit,
  244. 'error' => $e->getMessage(),
  245. ]);
  246. return [
  247. 'success' => false,
  248. 'error' => '获取知识点图谱快照列表异常: ' . $e->getMessage(),
  249. ];
  250. }
  251. }
  252. /**
  253. * 获取学生知识点掌握摘要(简化版)
  254. *
  255. * @param string $studentId 学生ID
  256. * @return array
  257. */
  258. public function getSummary(string $studentId): array
  259. {
  260. $stats = $this->getStats($studentId);
  261. if (!$stats['success']) {
  262. return $stats;
  263. }
  264. $data = $stats['data'];
  265. return [
  266. 'success' => true,
  267. 'data' => [
  268. 'student_id' => $data['student_id'] ?? $studentId,
  269. 'total' => $data['total_knowledge_points'] ?? 0,
  270. 'mastered' => $data['mastered_knowledge_points'] ?? 0,
  271. 'unmastered' => $data['unmastered_knowledge_points'] ?? 0,
  272. 'mastery_rate' => $data['mastery_rate'] ?? 0.0,
  273. 'mastery_percentage' => round(($data['mastery_rate'] ?? 0) * 100, 1) . '%',
  274. 'graph_total' => $data['graph_total_knowledge_points'] ?? 0,
  275. ],
  276. ];
  277. }
  278. /**
  279. * 创建知识点掌握度快照
  280. *
  281. * @param string $studentId 学生ID
  282. * @param string $snapshotType 快照类型 (exam/report/manual/scheduled)
  283. * @param string|null $sourceId 来源ID
  284. * @param string|null $sourceName 来源名称
  285. * @param string|null $notes 备注
  286. * @return array
  287. */
  288. public function createSnapshot(
  289. string $studentId,
  290. string $snapshotType = 'report',
  291. ?string $sourceId = null,
  292. ?string $sourceName = null,
  293. ?string $notes = null
  294. ): array {
  295. try {
  296. $response = Http::timeout($this->timeout)
  297. ->post($this->learningAnalyticsBase . '/api/knowledge-mastery/snapshot/' . $studentId, [
  298. 'snapshot_type' => $snapshotType,
  299. 'source_id' => $sourceId,
  300. 'source_name' => $sourceName,
  301. 'notes' => $notes,
  302. ]);
  303. if ($response->successful()) {
  304. $body = $response->json();
  305. Log::info('KnowledgeMasteryService::createSnapshot', [
  306. 'student_id' => $studentId,
  307. 'snapshot_type' => $snapshotType,
  308. 'snapshot_id' => $body['data']['snapshot_id'] ?? null,
  309. ]);
  310. return [
  311. 'success' => true,
  312. 'data' => $body['data'] ?? $body,
  313. ];
  314. }
  315. Log::warning('Create knowledge mastery snapshot failed', [
  316. 'student_id' => $studentId,
  317. 'status' => $response->status(),
  318. 'body' => $response->body(),
  319. ]);
  320. return [
  321. 'success' => false,
  322. 'error' => '创建知识点掌握度快照失败: ' . $response->status(),
  323. ];
  324. } catch (\Throwable $e) {
  325. Log::error('Create knowledge mastery snapshot exception', [
  326. 'student_id' => $studentId,
  327. 'error' => $e->getMessage(),
  328. ]);
  329. return [
  330. 'success' => false,
  331. 'error' => '创建知识点掌握度快照异常: ' . $e->getMessage(),
  332. ];
  333. }
  334. }
  335. }