baseUrl = ''; $this->timeout = (int) config('question_bank.timeout', 10); $this->cacheTtl = (int) config('question_bank.cache_ttl', 300); $this->mode = 'local'; } /** * 获取所有题目(分页) */ public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array { if ($this->useLocal()) { return $this->local()->listQuestions($page, $perPage, $filters); } // 移除缓存,直接请求最新数据 $query = array_filter([ 'page' => $page, 'per_page' => $perPage, 'kp_code' => $filters['kp_code'] ?? null, 'difficulty' => $filters['difficulty'] ?? null, 'type' => $filters['type'] ?? null, 'skill' => $filters['skill'] ?? null, 'search' => $filters['search'] ?? null, ], fn ($value) => filled($value)); // 使用更短的超时时间避免阻塞 try { $response = Http::timeout(5) // 5秒超时 ->get($this->baseUrl . '/questions', $query); if (!$response->successful()) { Log::warning('题目列表API调用失败', [ 'status' => $response->status(), 'url' => $this->baseUrl . '/questions' ]); return [ 'data' => [], 'meta' => [ 'page' => $page, 'per_page' => $perPage, 'total' => 0, 'total_pages' => 0, ] ]; } $response = $response->json(); } catch (\Illuminate\Http\Client\ConnectionException $e) { Log::error('题目列表API连接超时', [ 'error' => $e->getMessage() ]); // 返回空数据,避免页面卡死 return [ 'data' => [], 'meta' => [ 'page' => $page, 'per_page' => $perPage, 'total' => 0, 'total_pages' => 0, ] ]; } // 处理数学公式 $data = $response['data'] ?? []; foreach ($data as &$question) { // 使用数学公式处理器处理题目数据 $question = MathFormulaProcessor::processQuestionData($question); } return [ 'data' => $data, 'meta' => $response['meta'] ?? [ 'page' => $page, 'per_page' => $perPage, 'total' => is_array($response) ? count($response) : 0, 'total_pages' => 1, ], ]; } /** * 获取技能点名称映射 */ public function getSkillNameMapping(?string $kpCode = null): array { if ($this->useLocal()) { return $this->local()->getSkillNameMapping($kpCode); } // 如果没有指定知识点,返回空映射 if (empty($kpCode)) { return []; } try { // 从知识服务API获取知识点详情,包括技能点 $baseUrl = config('knowledge.base_url', 'http://localhost:5011'); $url = rtrim($baseUrl, '/') . '/graph/node/' . $kpCode; $response = Http::timeout(5) ->get($url); if (!$response->successful()) { Log::warning('获取知识点详情失败', [ 'kp_code' => $kpCode, 'status' => $response->status(), 'url' => $url ]); return []; } $data = $response->json(); $mapping = []; // 从响应中提取技能点信息 $skills = $data['skills'] ?? []; foreach ($skills as $skill) { $code = $skill['skill_code'] ?? ''; $name = $skill['skill_name'] ?? $skill['cn_name'] ?? $code; if (!empty($code)) { $mapping[$code] = $name; } } Log::info('成功获取技能点映射', [ 'kp_code' => $kpCode, 'skill_count' => count($mapping) ]); return $mapping; } catch (\Exception $e) { Log::warning('获取技能点名称映射失败', [ 'kp_code' => $kpCode, 'error' => $e->getMessage() ]); return []; } } /** * 获取题目统计信息 */ public function getStatistics(array $filters = []): array { if ($this->useLocal()) { return $this->local()->getStatistics($filters); } // 移除缓存,直接请求最新数据 try { $query = array_filter([ 'kp_code' => $filters['kp_code'] ?? null, 'difficulty' => $filters['difficulty'] ?? null, 'type' => $filters['type'] ?? null, 'skill' => $filters['skill'] ?? null, 'search' => $filters['search'] ?? null, ], fn ($value) => filled($value)); $response = Http::timeout(5) // 5秒超时 ->get($this->baseUrl . '/questions/statistics', $query); if (!$response->successful()) { Log::warning('统计API调用失败', [ 'status' => $response->status(), 'filters' => $filters ]); return [ 'total' => 0, 'by_difficulty' => [], 'by_type' => [], 'by_kp' => [], 'by_source' => [], ]; } $response = $response->json(); } catch (\Illuminate\Http\Client\ConnectionException $e) { Log::error('统计API连接超时', [ 'error' => $e->getMessage(), 'filters' => $filters ]); return [ 'total' => 0, 'by_difficulty' => [], 'by_type' => [], 'by_kp' => [], 'by_source' => [], ]; } return $response ?? [ 'total' => 0, 'by_difficulty' => [], 'by_type' => [], 'by_source' => [], ]; } /** * 根据知识点获取题型统计 */ public function getQuestionTypeStatisticsByKpCode(string $kpCode): array { if ($this->useLocal()) { $response = $this->local()->getQuestionsByKpCode($kpCode, 1000); $questions = $response['data'] ?? []; $typeStats = []; foreach ($questions as $question) { $type = $question['type'] ?? 'CALCULATION'; $typeStats[$type] = ($typeStats[$type] ?? 0) + 1; } arsort($typeStats); return $typeStats; } try { // 获取该知识点下的所有题目 $response = Http::timeout(5) ->get($this->baseUrl . '/questions', ['kp_code' => $kpCode, 'per_page' => 1000]); if (!$response->successful()) { return []; } $questions = $response['data'] ?? []; $typeStats = []; // 直接使用 question_type 字段统计,而不是猜测 foreach ($questions as $question) { $type = $question['type'] ?? 'CALCULATION'; if (!isset($typeStats[$type])) { $typeStats[$type] = 0; } $typeStats[$type]++; } // 按数量排序 arsort($typeStats); return $typeStats; } catch (\Exception $e) { Log::error('获取题型统计失败', [ 'kp_code' => $kpCode, 'error' => $e->getMessage() ]); return []; } } /** * 根据 kp_code 获取题目 */ public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array { if ($this->useLocal()) { return $this->local()->getQuestionsByKpCode($kpCode, $limit); } $response = $this->request('GET', '/questions', [ 'kp_code' => $kpCode, 'limit' => $limit, ]); // 处理数学公式 if ($response && isset($response['data']) && is_array($response['data'])) { foreach ($response['data'] as &$question) { $question = MathFormulaProcessor::processQuestionData($question); } } return $response; } /** * 语义搜索题目 */ public function searchQuestions(string $query, int $limit = 20): array { if ($this->useLocal()) { return $this->local()->searchQuestions($query, $limit); } $cacheKey = sprintf('question-search-%s-%d', md5($query), $limit); return Cache::remember( $cacheKey, now()->addSeconds($this->cacheTtl), function () use ($query, $limit): array { try { $response = $this->request('POST', '/questions/search', [ 'query' => $query, 'limit' => $limit, ]); // 处理数学公式 if ($response && is_array($response)) { $response = MathFormulaProcessor::processArray($response, [ 'stem', 'content', 'question_text', 'answer', 'explanation' ]); } return $response ?? []; } catch (\Exception $e) { \Log::error('Question search failed: ' . $e->getMessage()); return []; } } ); } /** * 获取单个题目详情 */ public function getQuestionById(int $id): ?array { if ($this->useLocal()) { return $this->local()->getQuestionById($id); } $cacheKey = "question-{$id}"; return Cache::remember( $cacheKey, now()->addSeconds($this->cacheTtl), function () use ($id): ?array { try { $response = $this->request('GET', "/questions/{$id}"); // 处理数学公式 if ($response && is_array($response)) { $response = MathFormulaProcessor::processQuestionData($response); } return $response ?: null; } catch (\Exception $e) { \Log::error("Failed to get question {$id}: " . $e->getMessage()); return null; } } ); } /** * 获取题目详情(通过 question_id) */ public function getQuestionDetail(string $questionId): array { if ($this->useLocal()) { $question = null; if (is_numeric($questionId)) { $question = $this->local()->getQuestionById((int) $questionId); } if (!$question) { $question = $this->local()->getQuestionByCode($questionId); } return ['data' => $question]; } try { // 先通过ID获取题目信息(使用批量接口) $batchResponse = Http::timeout(5) ->get($this->baseUrl . '/questions/batch', [ 'ids' => $questionId ]); if (!$batchResponse->successful()) { Log::warning('批量获取题目详情失败', [ 'question_id' => $questionId, 'status' => $batchResponse->status() ]); return ['data' => null]; } $batchData = $batchResponse->json(); $questions = $batchData['data'] ?? []; if (empty($questions)) { Log::warning('题目不存在', [ 'question_id' => $questionId ]); return ['data' => null]; } $question = $questions[0]; // 处理数学公式 $question = MathFormulaProcessor::processQuestionData($question); return ['data' => $question]; } catch (\Exception $e) { Log::error('获取题目详情异常', [ 'question_id' => $questionId, 'error' => $e->getMessage() ]); return ['data' => null]; } } /** * 创建题目(通过 AI 生成) */ public function generateQuestions(array $params): array { if ($this->useLocal()) { return $this->local()->generateQuestions($params); } try { $response = $this->request('POST', '/questions/generate', $params); return $response ?? [ 'success' => false, 'message' => '生成失败', ]; } catch (\Exception $e) { \Log::error('Question generation failed: ' . $e->getMessage()); return [ 'success' => false, 'message' => '生成失败:' . $e->getMessage(), ]; } } /** * 导入题目(JSON 批量导入) */ public function importQuestions(array $questions): array { if ($this->useLocal()) { return $this->local()->importQuestions($questions); } try { $response = $this->request('POST', '/questions/import', [ 'questions' => $questions, ]); return $response ?? [ 'success' => false, 'message' => '导入失败', ]; } catch (\Exception $e) { \Log::error('Question import failed: ' . $e->getMessage()); return [ 'success' => false, 'message' => '导入失败:' . $e->getMessage(), ]; } } /** * 删除题目 */ public function deleteQuestion(int $id): bool { if ($this->useLocal()) { return $this->local()->deleteQuestionById($id); } try { $response = $this->request('DELETE', "/questions/{$id}"); return $response['success'] ?? false; } catch (\Exception $e) { \Log::error("Failed to delete question {$id}: " . $e->getMessage()); return false; } } /** * 获取知识点选项(从知识图谱服务) */ public function getKnowledgePointOptions(): array { if ($this->useLocal()) { return $this->local()->getKnowledgePointOptions(); } // 使用缓存来避免重复请求 $cacheKey = 'knowledge-point-options'; return Cache::remember( $cacheKey, now()->addHours(1), // 缓存1小时 function () { try { // 使用新的知识图谱API $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011'); $response = Http::timeout(30) // 增加超时时间到30秒 ->get($knowledgeApiBase . '/graph/export'); if ($response->successful()) { $data = $response->json(); $nodes = $data['nodes'] ?? []; // 转换为键值对格式 $options = []; foreach ($nodes as $node) { $code = $node['kp_code'] ?? null; $name = $node['cn_name'] ?? null; if ($code && $name) { $options[$code] = $name; } } // 按代码排序 ksort($options); \Log::info('成功获取知识点选项', ['count' => count($options)]); return $options; } \Log::warning('知识图谱API调用失败', [ 'status' => $response->status(), 'url' => $knowledgeApiBase . '/graph/export' ]); } catch (\Exception $e) { \Log::error('Failed to get knowledge points: ' . $e->getMessage()); } // 返回空数组作为fallback return []; } ); } /** * 获取提示词模板列表 */ public function listPrompts(?string $type = null, ?string $active = null): array { return app(PromptService::class)->listPrompts($type, $active); } /** * 保存提示词模板 */ public function savePrompt(array $data): array { return app(PromptService::class)->savePrompt($data); } /** * @throws RequestException * @return mixed */ protected function request(string $method, string $path, array $params = []): mixed { $response = $this->http()->send($method, ltrim($path, '/'), [ 'query' => $method === 'GET' ? $params : null, 'json' => $method !== 'GET' ? $params : null, ]); 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', ]); } protected function useLocal(): bool { return true; } protected function local(): QuestionLocalService { return app(QuestionLocalService::class); } }