| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601 |
- <?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;
- use Illuminate\Support\Facades\Log;
- class QuestionServiceApi
- {
- protected string $mode;
- public function __construct(
- protected string $baseUrl = '',
- protected int $timeout = 10,
- protected int $cacheTtl = 300,
- ) {
- $this->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);
- }
- }
|