QuestionServiceApi.php 11 KB

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