QuestionServiceApi.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. class QuestionServiceApi
  9. {
  10. public function __construct(
  11. protected string $baseUrl = '',
  12. protected int $timeout = 10,
  13. protected int $cacheTtl = 300,
  14. ) {
  15. $this->baseUrl = rtrim($this->baseUrl ?: config('question_bank.api_base', 'http://localhost:5015'), '/');
  16. $this->timeout = (int) config('question_bank.timeout', 10);
  17. $this->cacheTtl = (int) config('question_bank.cache_ttl', 300);
  18. }
  19. /**
  20. * 获取所有题目(分页)
  21. */
  22. public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array
  23. {
  24. $cacheKey = sprintf(
  25. 'questions-list-%d-%d-%s',
  26. $page,
  27. $perPage,
  28. md5(json_encode($filters))
  29. );
  30. return Cache::remember(
  31. $cacheKey,
  32. now()->addSeconds($this->cacheTtl),
  33. function () use ($page, $perPage, $filters): array {
  34. $query = array_filter([
  35. 'page' => $page,
  36. 'per_page' => $perPage,
  37. 'kp_code' => $filters['kp_code'] ?? null,
  38. 'difficulty' => $filters['difficulty'] ?? null,
  39. 'skill' => $filters['skill'] ?? null,
  40. 'search' => $filters['search'] ?? null,
  41. ], fn ($value) => filled($value));
  42. $response = $this->request('GET', '/questions', $query);
  43. return [
  44. 'data' => $response['data'] ?? [],
  45. 'meta' => $response['meta'] ?? [
  46. 'page' => $page,
  47. 'per_page' => $perPage,
  48. 'total' => is_array($response) ? count($response) : 0,
  49. 'total_pages' => 1,
  50. ],
  51. ];
  52. }
  53. );
  54. }
  55. /**
  56. * 获取题目统计信息
  57. */
  58. public function getStatistics(): array
  59. {
  60. $cacheKey = 'question-statistics';
  61. return Cache::remember(
  62. $cacheKey,
  63. now()->addSeconds($this->cacheTtl),
  64. function (): array {
  65. $response = $this->request('GET', '/questions/statistics');
  66. return $response ?? [
  67. 'total' => 0,
  68. 'by_difficulty' => [],
  69. 'by_kp' => [],
  70. 'by_source' => [],
  71. ];
  72. }
  73. );
  74. }
  75. /**
  76. * 根据 kp_code 获取题目
  77. */
  78. public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array
  79. {
  80. return $this->request('GET', '/questions', [
  81. 'kp_code' => $kpCode,
  82. 'limit' => $limit,
  83. ]);
  84. }
  85. /**
  86. * 语义搜索题目
  87. */
  88. public function searchQuestions(string $query, int $limit = 20): array
  89. {
  90. $cacheKey = sprintf('question-search-%s-%d', md5($query), $limit);
  91. return Cache::remember(
  92. $cacheKey,
  93. now()->addSeconds($this->cacheTtl),
  94. function () use ($query, $limit): array {
  95. try {
  96. $response = $this->request('POST', '/questions/search', [
  97. 'query' => $query,
  98. 'limit' => $limit,
  99. ]);
  100. return $response ?? [];
  101. } catch (\Exception $e) {
  102. \Log::error('Question search failed: ' . $e->getMessage());
  103. return [];
  104. }
  105. }
  106. );
  107. }
  108. /**
  109. * 获取单个题目详情
  110. */
  111. public function getQuestionById(int $id): ?array
  112. {
  113. $cacheKey = "question-{$id}";
  114. return Cache::remember(
  115. $cacheKey,
  116. now()->addSeconds($this->cacheTtl),
  117. function () use ($id): ?array {
  118. try {
  119. $response = $this->request('GET', "/questions/{$id}");
  120. return $response ?: null;
  121. } catch (\Exception $e) {
  122. \Log::error("Failed to get question {$id}: " . $e->getMessage());
  123. return null;
  124. }
  125. }
  126. );
  127. }
  128. /**
  129. * 创建题目(通过 AI 生成)
  130. */
  131. public function generateQuestions(array $params): array
  132. {
  133. try {
  134. $response = $this->request('POST', '/questions/generate', $params);
  135. return $response ?? [
  136. 'success' => false,
  137. 'message' => '生成失败',
  138. ];
  139. } catch (\Exception $e) {
  140. \Log::error('Question generation failed: ' . $e->getMessage());
  141. return [
  142. 'success' => false,
  143. 'message' => '生成失败:' . $e->getMessage(),
  144. ];
  145. }
  146. }
  147. /**
  148. * 导入题目(JSON 批量导入)
  149. */
  150. public function importQuestions(array $questions): array
  151. {
  152. try {
  153. $response = $this->request('POST', '/questions/import', [
  154. 'questions' => $questions,
  155. ]);
  156. return $response ?? [
  157. 'success' => false,
  158. 'message' => '导入失败',
  159. ];
  160. } catch (\Exception $e) {
  161. \Log::error('Question import failed: ' . $e->getMessage());
  162. return [
  163. 'success' => false,
  164. 'message' => '导入失败:' . $e->getMessage(),
  165. ];
  166. }
  167. }
  168. /**
  169. * 删除题目
  170. */
  171. public function deleteQuestion(int $id): bool
  172. {
  173. try {
  174. $response = $this->request('DELETE', "/questions/{$id}");
  175. return $response['success'] ?? false;
  176. } catch (\Exception $e) {
  177. \Log::error("Failed to delete question {$id}: " . $e->getMessage());
  178. return false;
  179. }
  180. }
  181. /**
  182. * 获取知识点选项(从知识图谱服务)
  183. */
  184. public function getKnowledgePointOptions(): array
  185. {
  186. try {
  187. $knowledgeService = app(KnowledgeServiceApi::class);
  188. $points = $knowledgeService->listKnowledgePoints(limit: 1000);
  189. return $points->pluck('cn_name', 'kp_code')
  190. ->sort()
  191. ->all();
  192. } catch (\Exception $e) {
  193. \Log::error('Failed to get knowledge points: ' . $e->getMessage());
  194. return [];
  195. }
  196. }
  197. /**
  198. * 获取提示词模板列表
  199. */
  200. public function listPrompts(?string $type = null, ?string $active = null): array
  201. {
  202. $cacheKey = sprintf(
  203. 'prompts-list-%s-%s',
  204. $type ?: 'all',
  205. $active ?: 'all'
  206. );
  207. return Cache::remember(
  208. $cacheKey,
  209. now()->addSeconds($this->cacheTtl),
  210. function () use ($type, $active): array {
  211. $query = array_filter([
  212. 'type' => $type,
  213. 'active' => $active,
  214. ], fn ($value) => filled($value));
  215. try {
  216. $response = $this->request('GET', '/prompts', $query);
  217. return $response ?? [];
  218. } catch (\Exception $e) {
  219. \Log::error('Failed to get prompts: ' . $e->getMessage());
  220. return [];
  221. }
  222. }
  223. );
  224. }
  225. /**
  226. * @throws RequestException
  227. * @return mixed
  228. */
  229. protected function request(string $method, string $path, array $params = []): mixed
  230. {
  231. $response = $this->http()->send($method, ltrim($path, '/'), [
  232. 'query' => $method === 'GET' ? $params : null,
  233. 'json' => $method !== 'GET' ? $params : null,
  234. ]);
  235. return $response->throw()->json();
  236. }
  237. protected function http(): PendingRequest
  238. {
  239. return Http::baseUrl($this->baseUrl)
  240. ->acceptJson()
  241. ->timeout($this->timeout)
  242. ->retry(2, 200)
  243. ->withHeaders([
  244. 'User-Agent' => 'Laravel/' . (app()->version()),
  245. 'Accept' => 'application/json',
  246. ]);
  247. }
  248. }