QuestionServiceApi.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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. // 仅做基础清理
  44. $data = $response['data'] ?? [];
  45. foreach ($data as &$question) {
  46. if (isset($question['stem'])) {
  47. // 只移除HTML标签,不做其他处理
  48. $question['stem'] = strip_tags($question['stem']);
  49. }
  50. }
  51. return [
  52. 'data' => $data,
  53. 'meta' => $response['meta'] ?? [
  54. 'page' => $page,
  55. 'per_page' => $perPage,
  56. 'total' => is_array($response) ? count($response) : 0,
  57. 'total_pages' => 1,
  58. ],
  59. ];
  60. }
  61. );
  62. }
  63. /**
  64. * 获取题目统计信息
  65. */
  66. public function getStatistics(): array
  67. {
  68. $cacheKey = 'question-statistics';
  69. return Cache::remember(
  70. $cacheKey,
  71. now()->addSeconds($this->cacheTtl),
  72. function (): array {
  73. $response = $this->request('GET', '/questions/statistics');
  74. return $response ?? [
  75. 'total' => 0,
  76. 'by_difficulty' => [],
  77. 'by_kp' => [],
  78. 'by_source' => [],
  79. ];
  80. }
  81. );
  82. }
  83. /**
  84. * 根据 kp_code 获取题目
  85. */
  86. public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array
  87. {
  88. return $this->request('GET', '/questions', [
  89. 'kp_code' => $kpCode,
  90. 'limit' => $limit,
  91. ]);
  92. }
  93. /**
  94. * 语义搜索题目
  95. */
  96. public function searchQuestions(string $query, int $limit = 20): array
  97. {
  98. $cacheKey = sprintf('question-search-%s-%d', md5($query), $limit);
  99. return Cache::remember(
  100. $cacheKey,
  101. now()->addSeconds($this->cacheTtl),
  102. function () use ($query, $limit): array {
  103. try {
  104. $response = $this->request('POST', '/questions/search', [
  105. 'query' => $query,
  106. 'limit' => $limit,
  107. ]);
  108. return $response ?? [];
  109. } catch (\Exception $e) {
  110. \Log::error('Question search failed: ' . $e->getMessage());
  111. return [];
  112. }
  113. }
  114. );
  115. }
  116. /**
  117. * 获取单个题目详情
  118. */
  119. public function getQuestionById(int $id): ?array
  120. {
  121. $cacheKey = "question-{$id}";
  122. return Cache::remember(
  123. $cacheKey,
  124. now()->addSeconds($this->cacheTtl),
  125. function () use ($id): ?array {
  126. try {
  127. $response = $this->request('GET', "/questions/{$id}");
  128. return $response ?: null;
  129. } catch (\Exception $e) {
  130. \Log::error("Failed to get question {$id}: " . $e->getMessage());
  131. return null;
  132. }
  133. }
  134. );
  135. }
  136. /**
  137. * 创建题目(通过 AI 生成)
  138. */
  139. public function generateQuestions(array $params): array
  140. {
  141. try {
  142. $response = $this->request('POST', '/questions/generate', $params);
  143. return $response ?? [
  144. 'success' => false,
  145. 'message' => '生成失败',
  146. ];
  147. } catch (\Exception $e) {
  148. \Log::error('Question generation failed: ' . $e->getMessage());
  149. return [
  150. 'success' => false,
  151. 'message' => '生成失败:' . $e->getMessage(),
  152. ];
  153. }
  154. }
  155. /**
  156. * 导入题目(JSON 批量导入)
  157. */
  158. public function importQuestions(array $questions): array
  159. {
  160. try {
  161. $response = $this->request('POST', '/questions/import', [
  162. 'questions' => $questions,
  163. ]);
  164. return $response ?? [
  165. 'success' => false,
  166. 'message' => '导入失败',
  167. ];
  168. } catch (\Exception $e) {
  169. \Log::error('Question import failed: ' . $e->getMessage());
  170. return [
  171. 'success' => false,
  172. 'message' => '导入失败:' . $e->getMessage(),
  173. ];
  174. }
  175. }
  176. /**
  177. * 删除题目
  178. */
  179. public function deleteQuestion(int $id): bool
  180. {
  181. try {
  182. $response = $this->request('DELETE', "/questions/{$id}");
  183. return $response['success'] ?? false;
  184. } catch (\Exception $e) {
  185. \Log::error("Failed to delete question {$id}: " . $e->getMessage());
  186. return false;
  187. }
  188. }
  189. /**
  190. * 获取知识点选项(从知识图谱服务)
  191. */
  192. public function getKnowledgePointOptions(): array
  193. {
  194. try {
  195. $knowledgeService = app(KnowledgeGraphService::class);
  196. $points = $knowledgeService->listKnowledgePoints(1, 100);
  197. // 转换为键值对格式
  198. $options = [];
  199. foreach ($points as $point) {
  200. $code = $point['code'];
  201. $name = $point['name'];
  202. $options[$code] = $name;
  203. }
  204. // 按名称排序
  205. asort($options);
  206. return $options;
  207. } catch (\Exception $e) {
  208. \Log::error('Failed to get knowledge points: ' . $e->getMessage());
  209. return [];
  210. }
  211. }
  212. /**
  213. * 获取提示词模板列表
  214. */
  215. public function listPrompts(?string $type = null, ?string $active = null): array
  216. {
  217. $cacheKey = sprintf(
  218. 'prompts-list-%s-%s',
  219. $type ?: 'all',
  220. $active ?: 'all'
  221. );
  222. return Cache::remember(
  223. $cacheKey,
  224. now()->addSeconds($this->cacheTtl),
  225. function () use ($type, $active): array {
  226. $query = array_filter([
  227. 'type' => $type,
  228. 'active' => $active,
  229. ], fn ($value) => filled($value));
  230. try {
  231. $response = $this->request('GET', '/prompts', $query);
  232. return $response ?? [];
  233. } catch (\Exception $e) {
  234. \Log::error('Failed to get prompts: ' . $e->getMessage());
  235. return [];
  236. }
  237. }
  238. );
  239. }
  240. /**
  241. * 保存提示词模板
  242. */
  243. public function savePrompt(array $data): array
  244. {
  245. try {
  246. // 先尝试更新
  247. $response = $this->request('PUT', '/prompts/default', $data);
  248. // 如果更新失败(可能不存在),则尝试创建
  249. if (!isset($response['success']) || !$response['success']) {
  250. $response = $this->request('POST', '/prompts', $data);
  251. }
  252. // 清除提示词缓存
  253. Cache::forget('prompts-list-all-all');
  254. return $response ?? [
  255. 'success' => true,
  256. 'message' => '提示词保存成功',
  257. ];
  258. } catch (\Exception $e) {
  259. \Log::error('Failed to save prompt: ' . $e->getMessage());
  260. return [
  261. 'success' => false,
  262. 'message' => '保存失败:' . $e->getMessage(),
  263. ];
  264. }
  265. }
  266. /**
  267. * @throws RequestException
  268. * @return mixed
  269. */
  270. protected function request(string $method, string $path, array $params = []): mixed
  271. {
  272. $response = $this->http()->send($method, ltrim($path, '/'), [
  273. 'query' => $method === 'GET' ? $params : null,
  274. 'json' => $method !== 'GET' ? $params : null,
  275. ]);
  276. return $response->throw()->json();
  277. }
  278. protected function http(): PendingRequest
  279. {
  280. return Http::baseUrl($this->baseUrl)
  281. ->acceptJson()
  282. ->timeout($this->timeout)
  283. ->retry(2, 200)
  284. ->withHeaders([
  285. 'User-Agent' => 'Laravel/' . (app()->version()),
  286. 'Accept' => 'application/json',
  287. ]);
  288. }
  289. }