QuestionServiceApi.php 10 KB

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