| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- <?php
- namespace App\Services;
- use Illuminate\Http\Client\PendingRequest;
- use Illuminate\Http\Client\RequestException;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\Http;
- class KnowledgeServiceApi
- {
- public function __construct(
- protected string $baseUrl = '',
- protected int $timeout = 10,
- protected int $cacheTtl = 300,
- ) {
- $this->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<int, array<string, mixed>>
- */
- 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<int, array<string, mixed>>, meta: array<string, mixed>}
- */
- 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<string, mixed>
- */
- 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['parent_nodes'] = [];
- $data['parent_details'] = [];
- if (!empty($data['parents']) && is_array($data['parents'])) {
- foreach ($data['parents'] as $parentCode) {
- $parentDetail = $this->getKnowledgePointDetail($parentCode);
- if ($parentDetail) {
- $data['parent_nodes'][] = $parentDetail;
- $data['parent_details'][] = [
- 'kp_code' => $parentDetail['kp_code'] ?? $parentCode,
- 'cn_name' => $parentDetail['cn_name'] ?? $parentCode,
- ];
- }
- }
- }
- // 获取子节点(通过反向查询)
- $data['child_nodes'] = $this->findChildNodes($kpCode);
- 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<int, array<string, mixed>>
- */
- 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<int, array<string, mixed>>
- */
- 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',
- ]);
- }
- }
|