QuestionServiceApi.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Http\Client\PendingRequest;
  4. use Illuminate\Http\Client\RequestException;
  5. use Illuminate\Support\Collection;
  6. use Illuminate\Support\Facades\Cache;
  7. use Illuminate\Support\Facades\Http;
  8. use Illuminate\Support\Facades\Log;
  9. class QuestionServiceApi
  10. {
  11. public function __construct(
  12. protected string $baseUrl = '',
  13. protected int $timeout = 10,
  14. protected int $cacheTtl = 300,
  15. ) {
  16. $this->baseUrl = rtrim($this->baseUrl ?: config('question_bank.api_base', 'http://localhost:5015'), '/');
  17. $this->timeout = (int) config('question_bank.timeout', 10);
  18. $this->cacheTtl = (int) config('question_bank.cache_ttl', 300);
  19. }
  20. /**
  21. * 获取所有题目(分页)
  22. */
  23. public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array
  24. {
  25. // 移除缓存,直接请求最新数据
  26. $query = array_filter([
  27. 'page' => $page,
  28. 'per_page' => $perPage,
  29. 'kp_code' => $filters['kp_code'] ?? null,
  30. 'difficulty' => $filters['difficulty'] ?? null,
  31. 'type' => $filters['type'] ?? null,
  32. 'skill' => $filters['skill'] ?? null,
  33. 'search' => $filters['search'] ?? null,
  34. ], fn ($value) => filled($value));
  35. // 使用更短的超时时间避免阻塞
  36. try {
  37. $response = Http::timeout(5) // 5秒超时
  38. ->get($this->baseUrl . '/questions', $query);
  39. if (!$response->successful()) {
  40. Log::warning('题目列表API调用失败', [
  41. 'status' => $response->status(),
  42. 'url' => $this->baseUrl . '/questions'
  43. ]);
  44. return [
  45. 'data' => [],
  46. 'meta' => [
  47. 'page' => $page,
  48. 'per_page' => $perPage,
  49. 'total' => 0,
  50. 'total_pages' => 0,
  51. ]
  52. ];
  53. }
  54. $response = $response->json();
  55. } catch (\Illuminate\Http\Client\ConnectionException $e) {
  56. Log::error('题目列表API连接超时', [
  57. 'error' => $e->getMessage()
  58. ]);
  59. // 返回空数据,避免页面卡死
  60. return [
  61. 'data' => [],
  62. 'meta' => [
  63. 'page' => $page,
  64. 'per_page' => $perPage,
  65. 'total' => 0,
  66. 'total_pages' => 0,
  67. ]
  68. ];
  69. }
  70. // 处理数学公式
  71. $data = $response['data'] ?? [];
  72. foreach ($data as &$question) {
  73. // 使用数学公式处理器处理题目数据
  74. $question = MathFormulaProcessor::processQuestionData($question);
  75. }
  76. return [
  77. 'data' => $data,
  78. 'meta' => $response['meta'] ?? [
  79. 'page' => $page,
  80. 'per_page' => $perPage,
  81. 'total' => is_array($response) ? count($response) : 0,
  82. 'total_pages' => 1,
  83. ],
  84. ];
  85. }
  86. /**
  87. * 获取题目统计信息
  88. */
  89. public function getStatistics(): array
  90. {
  91. // 移除缓存,直接请求最新数据
  92. try {
  93. $response = Http::timeout(5) // 5秒超时
  94. ->get($this->baseUrl . '/questions/statistics');
  95. if (!$response->successful()) {
  96. Log::warning('统计API调用失败', [
  97. 'status' => $response->status()
  98. ]);
  99. return [
  100. 'total' => 0,
  101. 'by_difficulty' => [],
  102. 'by_kp' => [],
  103. 'by_source' => [],
  104. ];
  105. }
  106. $response = $response->json();
  107. } catch (\Illuminate\Http\Client\ConnectionException $e) {
  108. Log::error('统计API连接超时', [
  109. 'error' => $e->getMessage()
  110. ]);
  111. return [
  112. 'total' => 0,
  113. 'by_difficulty' => [],
  114. 'by_kp' => [],
  115. 'by_source' => [],
  116. ];
  117. }
  118. return $response ?? [
  119. 'total' => 0,
  120. 'by_difficulty' => [],
  121. 'by_kp' => [],
  122. 'by_source' => [],
  123. ];
  124. }
  125. /**
  126. * 根据 kp_code 获取题目
  127. */
  128. public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array
  129. {
  130. $response = $this->request('GET', '/questions', [
  131. 'kp_code' => $kpCode,
  132. 'limit' => $limit,
  133. ]);
  134. // 处理数学公式
  135. if ($response && isset($response['data']) && is_array($response['data'])) {
  136. foreach ($response['data'] as &$question) {
  137. $question = MathFormulaProcessor::processQuestionData($question);
  138. }
  139. }
  140. return $response;
  141. }
  142. /**
  143. * 语义搜索题目
  144. */
  145. public function searchQuestions(string $query, int $limit = 20): array
  146. {
  147. $cacheKey = sprintf('question-search-%s-%d', md5($query), $limit);
  148. return Cache::remember(
  149. $cacheKey,
  150. now()->addSeconds($this->cacheTtl),
  151. function () use ($query, $limit): array {
  152. try {
  153. $response = $this->request('POST', '/questions/search', [
  154. 'query' => $query,
  155. 'limit' => $limit,
  156. ]);
  157. // 处理数学公式
  158. if ($response && is_array($response)) {
  159. $response = MathFormulaProcessor::processArray($response, [
  160. 'stem', 'content', 'question_text', 'answer', 'explanation'
  161. ]);
  162. }
  163. return $response ?? [];
  164. } catch (\Exception $e) {
  165. \Log::error('Question search failed: ' . $e->getMessage());
  166. return [];
  167. }
  168. }
  169. );
  170. }
  171. /**
  172. * 获取单个题目详情
  173. */
  174. public function getQuestionById(int $id): ?array
  175. {
  176. $cacheKey = "question-{$id}";
  177. return Cache::remember(
  178. $cacheKey,
  179. now()->addSeconds($this->cacheTtl),
  180. function () use ($id): ?array {
  181. try {
  182. $response = $this->request('GET', "/questions/{$id}");
  183. // 处理数学公式
  184. if ($response && is_array($response)) {
  185. $response = MathFormulaProcessor::processQuestionData($response);
  186. }
  187. return $response ?: null;
  188. } catch (\Exception $e) {
  189. \Log::error("Failed to get question {$id}: " . $e->getMessage());
  190. return null;
  191. }
  192. }
  193. );
  194. }
  195. /**
  196. * 创建题目(通过 AI 生成)
  197. */
  198. public function generateQuestions(array $params): array
  199. {
  200. try {
  201. $response = $this->request('POST', '/questions/generate', $params);
  202. return $response ?? [
  203. 'success' => false,
  204. 'message' => '生成失败',
  205. ];
  206. } catch (\Exception $e) {
  207. \Log::error('Question generation failed: ' . $e->getMessage());
  208. return [
  209. 'success' => false,
  210. 'message' => '生成失败:' . $e->getMessage(),
  211. ];
  212. }
  213. }
  214. /**
  215. * 导入题目(JSON 批量导入)
  216. */
  217. public function importQuestions(array $questions): array
  218. {
  219. try {
  220. $response = $this->request('POST', '/questions/import', [
  221. 'questions' => $questions,
  222. ]);
  223. return $response ?? [
  224. 'success' => false,
  225. 'message' => '导入失败',
  226. ];
  227. } catch (\Exception $e) {
  228. \Log::error('Question import failed: ' . $e->getMessage());
  229. return [
  230. 'success' => false,
  231. 'message' => '导入失败:' . $e->getMessage(),
  232. ];
  233. }
  234. }
  235. /**
  236. * 删除题目
  237. */
  238. public function deleteQuestion(int $id): bool
  239. {
  240. try {
  241. $response = $this->request('DELETE', "/questions/{$id}");
  242. return $response['success'] ?? false;
  243. } catch (\Exception $e) {
  244. \Log::error("Failed to delete question {$id}: " . $e->getMessage());
  245. return false;
  246. }
  247. }
  248. /**
  249. * 获取知识点选项(从知识图谱服务)
  250. */
  251. public function getKnowledgePointOptions(): array
  252. {
  253. // 使用缓存来避免重复请求
  254. $cacheKey = 'knowledge-point-options';
  255. return Cache::remember(
  256. $cacheKey,
  257. now()->addHours(1), // 缓存1小时
  258. function () {
  259. try {
  260. // 使用新的知识图谱API
  261. $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
  262. $response = Http::timeout(30) // 增加超时时间到30秒
  263. ->get($knowledgeApiBase . '/graph/export');
  264. if ($response->successful()) {
  265. $data = $response->json();
  266. $nodes = $data['nodes'] ?? [];
  267. // 转换为键值对格式
  268. $options = [];
  269. foreach ($nodes as $node) {
  270. $code = $node['kp_code'] ?? null;
  271. $name = $node['cn_name'] ?? null;
  272. if ($code && $name) {
  273. $options[$code] = $name;
  274. }
  275. }
  276. // 按代码排序
  277. ksort($options);
  278. \Log::info('成功获取知识点选项', ['count' => count($options)]);
  279. return $options;
  280. }
  281. \Log::warning('知识图谱API调用失败', [
  282. 'status' => $response->status(),
  283. 'url' => $knowledgeApiBase . '/graph/export'
  284. ]);
  285. } catch (\Exception $e) {
  286. \Log::error('Failed to get knowledge points: ' . $e->getMessage());
  287. }
  288. // 返回空数组作为fallback
  289. return [];
  290. }
  291. );
  292. }
  293. /**
  294. * 获取提示词模板列表
  295. */
  296. public function listPrompts(?string $type = null, ?string $active = null): array
  297. {
  298. $query = array_filter([
  299. 'type' => $type,
  300. 'active' => $active,
  301. ], fn ($value) => filled($value));
  302. try {
  303. $response = $this->request('GET', '/prompts', $query);
  304. return $response ?? [];
  305. } catch (\Exception $e) {
  306. \Log::error('Failed to get prompts: ' . $e->getMessage());
  307. return [];
  308. }
  309. }
  310. /**
  311. * 保存提示词模板
  312. */
  313. public function savePrompt(array $data): array
  314. {
  315. try {
  316. // 先尝试更新
  317. $response = $this->request('PUT', '/prompts/default', $data);
  318. // 如果更新失败(可能不存在),则尝试创建
  319. if (!isset($response['success']) || !$response['success']) {
  320. $response = $this->request('POST', '/prompts', $data);
  321. }
  322. return $response ?? [
  323. 'success' => true,
  324. 'message' => '提示词保存成功',
  325. ];
  326. } catch (\Exception $e) {
  327. \Log::error('Failed to save prompt: ' . $e->getMessage());
  328. return [
  329. 'success' => false,
  330. 'message' => '保存失败:' . $e->getMessage(),
  331. ];
  332. }
  333. }
  334. /**
  335. * @throws RequestException
  336. * @return mixed
  337. */
  338. protected function request(string $method, string $path, array $params = []): mixed
  339. {
  340. $response = $this->http()->send($method, ltrim($path, '/'), [
  341. 'query' => $method === 'GET' ? $params : null,
  342. 'json' => $method !== 'GET' ? $params : null,
  343. ]);
  344. return $response->throw()->json();
  345. }
  346. protected function http(): PendingRequest
  347. {
  348. return Http::baseUrl($this->baseUrl)
  349. ->acceptJson()
  350. ->timeout($this->timeout)
  351. ->retry(2, 200)
  352. ->withHeaders([
  353. 'User-Agent' => 'Laravel/' . (app()->version()),
  354. 'Accept' => 'application/json',
  355. ]);
  356. }
  357. }