baseUrl = rtrim($this->baseUrl ?: config('knowledge.base_url'), '/'); $this->timeout = $this->timeout ?: (int) config('knowledge.timeout', 10); $this->cacheTtl = $this->cacheTtl ?: (int) config('knowledge.cache_ttl', 300); } /** * Fetch and cache all knowledge points by iterating through paginated API responses. * * @return Collection> */ public function listKnowledgePoints(int $perPage = 200, array $filters = []): Collection { $cacheKey = sprintf( 'knowledge-points-all-%d-%s', $perPage, md5(json_encode($filters)) ); return Cache::remember( $cacheKey, now()->addSeconds($this->cacheTtl), function () use ($perPage, $filters): Collection { $page = 1; $results = collect(); while (true) { $response = $this->paginateKnowledgePoints($page, $perPage, $filters); $chunk = collect($response['data'] ?? []); if ($chunk->isEmpty()) { break; } $results = $results->concat($chunk); $meta = $response['meta'] ?? []; $hasNext = $meta['has_next'] ?? ($page < ($meta['total_pages'] ?? $page)); if (! $hasNext) { break; } $page++; } return $results->values(); }, ); } /** * @return array{data: array>, meta: array} */ public function paginateKnowledgePoints(int $page = 1, int $perPage = 50, array $filters = []): array { $query = array_filter([ 'page' => $page, 'per_page' => $perPage, 'phase' => $filters['phase'] ?? null, 'category' => $filters['category'] ?? null, 'grade' => $filters['grade'] ?? null, 'search' => $filters['search'] ?? null, ], fn ($value) => filled($value)); $response = $this->request('GET', '/knowledge-points', $query); if (is_array($response) && array_key_exists('data', $response)) { return [ 'data' => $response['data'] ?? [], 'meta' => $response['meta'] ?? [], ]; } return [ 'data' => is_array($response) ? $response : [], 'meta' => [ 'page' => $page, 'per_page' => $perPage, 'total' => is_array($response) ? count($response) : 0, 'total_pages' => 1, 'has_next' => false, 'has_prev' => $page > 1, ], ]; } /** * @return array */ public function getKnowledgePointDetail(string $kpCode): array { return Cache::remember( key: "knowledge-point-detail-{$kpCode}", ttl: now()->addSeconds($this->cacheTtl), callback: fn () => $this->request('GET', "/graph/node/{$kpCode}"), ); } /** * 获取包含上下游节点的完整图谱数据 */ public function getFullGraphData(string $kpCode): array { $data = $this->getKnowledgePointDetail($kpCode); if (empty($data)) { return $data; } $data['child_nodes'] = $this->findChildNodes($kpCode); $data['parent_nodes'] = []; if (!empty($data['parents']) && is_array($data['parents'])) { foreach ($data['parents'] as $parentCode) { $parentDetail = $this->getKnowledgePointDetail($parentCode); if ($parentDetail) { $data['parent_nodes'][] = $parentDetail; } } } return $data; } /** * 反向查找子节点(以当前节点为父节点的节点) */ private function findChildNodes(string $kpCode): array { // 缓存键 $cacheKey = "knowledge-point-children-{$kpCode}"; return Cache::remember( key: $cacheKey, ttl: now()->addSeconds($this->cacheTtl), callback: function () use ($kpCode) { try { \Log::info("开始查找子节点: {$kpCode}"); // 获取所有知识点 $allPoints = $this->listKnowledgePoints(); \Log::info("获取到知识点数量: " . $allPoints->count()); // 查找以当前节点为父节点的知识点 $children = $allPoints->filter(function ($point) use ($kpCode) { return in_array($kpCode, $point['parents'] ?? []); })->values()->toArray(); \Log::info("找到子节点数量: " . count($children)); return $children; } catch (\Exception $e) { \Log::error("查找子节点失败: " . $e->getMessage()); \Log::error($e->getTraceAsString()); return []; } } ); } /** * @return Collection> */ public function listSkills(?string $kpCode = null, int $limit = 200): Collection { return Cache::remember( key: "knowledge-skills-{$kpCode}-{$limit}", ttl: now()->addSeconds($this->cacheTtl), callback: fn () => collect($this->request('GET', '/skills', array_filter([ 'kp_code' => $kpCode, 'limit' => $limit, ]))), ); } /** * Search knowledge points by keyword * * @return Collection> */ public function searchKnowledgePoints(string $keyword, int $limit = 50): Collection { // Ensure keyword is not empty $keyword = trim($keyword); if ($keyword === '') { return collect(); } // Use query params array for proper encoding (no cache during debugging) try { $response = $this->request('GET', 'knowledge-points/search', [ 'keyword' => $keyword, 'limit' => $limit, ]); } catch (\Exception $e) { // Log error but don't fail completely \Log::error('Search API error: ' . $e->getMessage()); return collect(); } return collect($response); } /** * @throws RequestException * @return mixed */ protected function request(string $method, string $path, array $params = []): mixed { $response = $this->http()->send($method, ltrim($path, '/'), [ 'query' => $params, ]); return $response->throw()->json(); } protected function http(): PendingRequest { return Http::baseUrl($this->baseUrl) ->acceptJson() ->timeout($this->timeout) ->retry(2, 200) ->withHeaders([ 'User-Agent' => 'Laravel/' . (app()->version()), 'Accept' => 'application/json', ]); } }