CodexServiceImpl.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. package com.cyksj.service.codex.impl;
  2. import cn.hutool.core.date.DateUtil;
  3. import cn.hutool.http.HttpResponse;
  4. import cn.hutool.http.HttpUtil;
  5. import cn.hutool.http.Method;
  6. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  7. import com.cyksj.common.constant.Constant;
  8. import com.cyksj.common.exception.BusinessRuntimeException;
  9. import com.cyksj.common.util.Jsons;
  10. import com.cyksj.common.util.StringUtil;
  11. import com.cyksj.mapper.*;
  12. import com.cyksj.model.entity.*;
  13. import com.cyksj.model.request.UpdateDailyLimitRequest;
  14. import com.cyksj.model.request.codex.CodexUserPackageReq;
  15. import com.cyksj.model.response.claudecode.ClaudeCodeResp;
  16. import com.cyksj.model.views.CodexUserInfoView;
  17. import com.cyksj.service.codex.CodexService;
  18. import com.cyksj.service.user.UserBindRelationService;
  19. import com.github.rholder.retry.Retryer;
  20. import com.github.rholder.retry.RetryerBuilder;
  21. import com.github.rholder.retry.StopStrategies;
  22. import com.github.rholder.retry.WaitStrategies;
  23. import lombok.RequiredArgsConstructor;
  24. import lombok.extern.slf4j.Slf4j;
  25. import org.springframework.dao.DuplicateKeyException;
  26. import org.springframework.stereotype.Service;
  27. import java.util.*;
  28. import java.util.concurrent.ExecutionException;
  29. import java.util.concurrent.TimeUnit;
  30. /**
  31. * 项目名: yhlxj11111111
  32. * 文件名: CodexServiceImpl
  33. * 创建者: Claude
  34. * 创建时间:2025/9/15
  35. */
  36. @Slf4j
  37. @Service
  38. @RequiredArgsConstructor
  39. public class CodexServiceImpl implements CodexService {
  40. private final CodexUserMapper codexUserMapper;
  41. private final ClaudeCodeUserMapper claudeCodeUserMapper;
  42. private final UserBindRelationService userBindRelationService;
  43. private final GoodsDonSkuMapper skuMapper;
  44. private final GroupsRelationMapper relationMapper;
  45. private final GroupsMapper groupsMapper;
  46. @Override
  47. public CodexUserInfoView getCodexUserInfo(Long userId) {
  48. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  49. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  50. // 查询Claude Code信息
  51. CodexUserInfoView.ClaudeCodeInfo claudeCodeInfo = getClaudeCodeInfo(userId);
  52. if (codexUser == null) {
  53. //是否有claude code
  54. if (claudeCodeInfo.getCodePoints() != null) {
  55. GoodsDonSku sku = null;
  56. if (claudeCodeInfo.getRenewSkuId() != null) {
  57. sku = skuMapper.selectById(claudeCodeInfo.getRenewSkuId());
  58. }
  59. //续费升级规格不存在 或者 不赠送codex
  60. if (sku == null || !sku.getIsCodex()) {
  61. GroupsRelation relation = relationMapper.selectById(claudeCodeInfo.getRelationId());
  62. GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
  63. sku = skuMapper.selectById(groupsTrips.getSkuId());
  64. }
  65. //sku 存在并且开启赠送codex
  66. if (sku != null && sku.getIsCodex()) {
  67. CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
  68. .userId(userId)
  69. .planName(sku.getCodexPlanName())
  70. .openaiDailyLimit(sku.getOpenaiDailyLimit())
  71. .openaiQuota(sku.getOpenaiQuota())
  72. .expiryTime(claudeCodeInfo.getExpiryTime())
  73. .build();
  74. createOrUpdateUserPackage(codexPackageReq);
  75. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  76. }
  77. }
  78. if (codexUser == null) {
  79. return null;
  80. }
  81. }
  82. return CodexUserInfoView.builder().userId(codexUser.getUserId()).openaiDailyLimit(codexUser.getOpenaiDailyLimit()).openaiQuota(codexUser.getOpenaiQuota()).relationId(codexUser.getRelationId()).claudeCodeInfo(claudeCodeInfo).build();
  83. }
  84. private CodexUserInfoView.ClaudeCodeInfo getClaudeCodeInfo(Long userId) {
  85. // 获取用户关联的ID列表
  86. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  87. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  88. if (claudeCodeUser == null) {
  89. return null;
  90. }
  91. return CodexUserInfoView.ClaudeCodeInfo.builder().planName(claudeCodeUser.getPlanName()).relationId(claudeCodeUser.getRelationId()).codePoints(claudeCodeUser.getCodePoints()).codeCreditRecovery(claudeCodeUser.getCodeCreditRecovery()).expiryTime(claudeCodeUser.getExpiryTime()).renewSkuId(claudeCodeUser.getRenewSkuId()).renewExpiryTime(claudeCodeUser.getRenewExpiryTime()).build();
  92. }
  93. @Override
  94. public void createOrUpdateUserPackage(CodexUserPackageReq req) {
  95. Long userId = req.getUserId();
  96. // 构建完整的套餐参数
  97. String planName = req.getPlanName();
  98. Date expiryTime = req.getExpiryTime();
  99. Integer openaiDailyLimit = req.getOpenaiDailyLimit();
  100. Integer openaiQuota = req.getOpenaiQuota();
  101. Long relationId = req.getRelationId();
  102. try {
  103. createOrUpdateCodexUserPackage(userId, planName, openaiDailyLimit, openaiQuota, expiryTime);
  104. } catch (Exception e) {
  105. setCodexUserRetryUpdateInfo(relationId);
  106. return;
  107. }
  108. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  109. CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser());
  110. existingCodexUser.setPlanName(planName);
  111. existingCodexUser.setRelationId(relationId);
  112. existingCodexUser.setUserId(userId);
  113. existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit);
  114. existingCodexUser.setOpenaiQuota(openaiQuota);
  115. existingCodexUser.setExpiryTime(expiryTime);
  116. if (existingCodexUser.getId() == null) {
  117. try {
  118. codexUserMapper.insert(existingCodexUser);
  119. } catch (DuplicateKeyException e) {
  120. }
  121. return;
  122. }
  123. codexUserMapper.updateById(existingCodexUser);
  124. }
  125. /**
  126. * 创建或更新Codex用户套餐
  127. */
  128. public ClaudeCodeResp createOrUpdateCodexUserPackage(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
  129. Map<String, Object> params = new HashMap<>();
  130. params.put("plan_name", planName.replaceAll(";", ""));
  131. params.put("openai_daily_limit", openaiDailyLimit);
  132. params.put("openai_quota", openaiQuota);
  133. params.put("end_date", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
  134. String planUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
  135. ClaudeCodeResp codexResp = executeCodexPostApi(planUrl, Jsons.toJson(params));
  136. if (!"success".equals(codexResp.getMessage())) {
  137. log.info("用户codex用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, codexResp.getMessage());
  138. throw BusinessRuntimeException.getInstance("调用codex 套餐接口失败");
  139. }
  140. return codexResp;
  141. }
  142. private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
  143. return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
  144. }
  145. /**
  146. * Codex POST请求 - 复制ClaudeCodeServiceImpl的实现模式
  147. */
  148. public ClaudeCodeResp executeCodexPostApi(String url, String body) {
  149. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  150. try {
  151. return build.call(() -> {
  152. try {
  153. HttpResponse execute = HttpUtil.createPost(url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).body(body).execute();
  154. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  155. if (!"success".equals(resp.getMessage())) {
  156. log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage());
  157. }
  158. return resp;
  159. } catch (Exception e) {
  160. log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  161. return null;
  162. }
  163. });
  164. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  165. log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  166. throw BusinessRuntimeException.getInstance("创建或更新codex user失败");
  167. }
  168. }
  169. /**
  170. * codex用户信息重试更新信息
  171. */
  172. public void setCodexUserRetryUpdateInfo(Long relationId) {
  173. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  174. if (codexUser != null) {
  175. codexUser.setIsRetry(Boolean.TRUE);
  176. codexUserMapper.updateById(codexUser);
  177. }
  178. }
  179. @Override
  180. public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) {
  181. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  182. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  183. if (codexUser == null) {
  184. return null;
  185. }
  186. // 调用Codex API获取实时使用统计
  187. String usageUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/usage?period=%s", userId, period);
  188. ClaudeCodeResp codexResp = executeCodexGetApi(usageUrl);
  189. return codexResp;
  190. }
  191. @Override
  192. public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) {
  193. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  194. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  195. if (codexUser == null) {
  196. throw new BusinessRuntimeException("用户未开通Codex服务");
  197. }
  198. Integer dailyLimit = request.getDailyLimit();
  199. Integer totalQuota = request.getTotalQuota();
  200. // 验证每日限额参数
  201. if (dailyLimit == null || dailyLimit <= 0) {
  202. throw new BusinessRuntimeException("每日限额必须大于0");
  203. }
  204. // 保存原始值用于回滚
  205. Integer originalDailyLimit = codexUser.getOpenaiDailyLimit();
  206. // 更新数据库中的每日限额
  207. codexUser.setOpenaiDailyLimit(dailyLimit);
  208. codexUserMapper.updateById(codexUser);
  209. try {
  210. // 调用Codex API更新每日限额
  211. String updateUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/limit", userId);
  212. Map<String, Object> params = new HashMap<>();
  213. params.put("daily_limit", dailyLimit);
  214. params.put("total_quota", totalQuota);
  215. ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params));
  216. if (!"success".equals(codexResp.getMessage())) {
  217. log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage());
  218. // API调用失败时回滚数据库更新
  219. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  220. codexUserMapper.updateById(codexUser);
  221. throw new BusinessRuntimeException("更新每日限额失败");
  222. }
  223. log.info("成功更新用户{}每日限额为{}", userId, dailyLimit);
  224. return codexResp;
  225. } catch (Exception e) {
  226. log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e));
  227. // 异常时回滚数据库更新
  228. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  229. codexUserMapper.updateById(codexUser);
  230. throw new BusinessRuntimeException("更新每日限额失败");
  231. }
  232. }
  233. @Override
  234. public ClaudeCodeResp getSystemMetrics(String period, String metric) {
  235. try {
  236. // 构建系统指标API URL
  237. String metricsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/system/metrics");
  238. // 添加查询参数
  239. List<String> queryParams = new ArrayList<>();
  240. if (period != null && !period.isEmpty()) {
  241. queryParams.add("period=" + period);
  242. }
  243. if (metric != null && !metric.isEmpty()) {
  244. queryParams.add("metric=" + metric);
  245. }
  246. if (!queryParams.isEmpty()) {
  247. metricsUrl += "?" + String.join("&", queryParams);
  248. }
  249. // 调用Codex API获取系统指标
  250. ClaudeCodeResp codexResp = executeCodexGetApi(metricsUrl);
  251. if (codexResp == null) {
  252. log.error("获取系统指标失败: API返回null");
  253. throw new BusinessRuntimeException("获取系统指标失败");
  254. }
  255. log.info("成功获取系统指标, period={}, metric={}", period, metric);
  256. return codexResp;
  257. } catch (Exception e) {
  258. log.error("获取系统指标异常, period={}, metric={}, error={}", period, metric, StringUtil.getErrorText(e));
  259. throw new BusinessRuntimeException("获取系统指标失败");
  260. }
  261. }
  262. @Override
  263. public ClaudeCodeResp getUserKeys(Long userId) {
  264. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  265. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  266. if (codexUser == null) {
  267. throw new BusinessRuntimeException("用户未开通Codex服务");
  268. }
  269. try {
  270. // 调用Codex API获取用户密钥列表
  271. String keysUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId);
  272. ClaudeCodeResp codexResp = executeCodexGetApi(keysUrl);
  273. if (codexResp == null) {
  274. log.error("获取用户{}密钥列表失败: API返回null", userId);
  275. throw new BusinessRuntimeException("获取用户密钥列表失败");
  276. }
  277. log.info("成功获取用户{}密钥列表", userId);
  278. return codexResp;
  279. } catch (Exception e) {
  280. log.error("获取用户{}密钥列表异常, error={}", userId, StringUtil.getErrorText(e));
  281. throw new BusinessRuntimeException("获取用户密钥列表失败");
  282. }
  283. }
  284. @Override
  285. public ClaudeCodeResp createUserKey(Long userId, String keyName) {
  286. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  287. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  288. if (codexUser == null) {
  289. throw new BusinessRuntimeException("用户未开通Codex服务");
  290. }
  291. try {
  292. // 调用Codex API创建用户密钥
  293. String createKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId);
  294. Map<String, Object> params = new HashMap<>();
  295. params.put("name", keyName);
  296. ClaudeCodeResp codexResp = executeCodexPostApi(createKeyUrl, Jsons.toJson(params));
  297. if (!"success".equals(codexResp.getMessage())) {
  298. log.error("创建用户{}密钥失败: {}", userId, codexResp.getMessage());
  299. throw new BusinessRuntimeException("创建用户密钥失败");
  300. }
  301. log.info("成功创建用户{}密钥, keyName={}", userId, keyName);
  302. return codexResp;
  303. } catch (Exception e) {
  304. log.error("创建用户{}密钥异常, keyName={}, error={}", userId, keyName, StringUtil.getErrorText(e));
  305. throw new BusinessRuntimeException("创建用户密钥失败");
  306. }
  307. }
  308. @Override
  309. public ClaudeCodeResp deleteUserKey(Long userId, String keyId) {
  310. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  311. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  312. if (codexUser == null) {
  313. throw new BusinessRuntimeException("用户未开通Codex服务");
  314. }
  315. try {
  316. // 调用Codex API删除用户密钥
  317. String deleteKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys/%s", userId, keyId);
  318. ClaudeCodeResp codexResp = executeCodexDeleteApi(deleteKeyUrl);
  319. if (!"success".equals(codexResp.getMessage())) {
  320. log.error("删除用户{}密钥{}失败: {}", userId, keyId, codexResp.getMessage());
  321. throw new BusinessRuntimeException("删除用户密钥失败");
  322. }
  323. log.info("成功删除用户{}密钥{}", userId, keyId);
  324. return codexResp;
  325. } catch (Exception e) {
  326. log.error("删除用户{}密钥{}异常, error={}", userId, keyId, StringUtil.getErrorText(e));
  327. throw new BusinessRuntimeException("删除用户密钥失败");
  328. }
  329. }
  330. @Override
  331. public ClaudeCodeResp getUserDashboard(Long userId) {
  332. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  333. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  334. if (codexUser == null) {
  335. throw new BusinessRuntimeException("用户未开通Codex服务");
  336. }
  337. try {
  338. // 调用Codex API获取用户仪表盘数据
  339. String dashboardUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/dashboard", userId);
  340. ClaudeCodeResp codexResp = executeCodexGetApi(dashboardUrl);
  341. if (codexResp == null) {
  342. log.error("获取用户{}仪表盘数据失败: API返回null", userId);
  343. throw new BusinessRuntimeException("获取用户仪表盘数据失败");
  344. }
  345. log.info("成功获取用户{}仪表盘数据", userId);
  346. return codexResp;
  347. } catch (Exception e) {
  348. log.error("获取用户{}仪表盘数据异常, error={}", userId, StringUtil.getErrorText(e));
  349. throw new BusinessRuntimeException("获取用户仪表盘数据失败");
  350. }
  351. }
  352. /**
  353. * Codex DELETE请求
  354. */
  355. public ClaudeCodeResp executeCodexDeleteApi(String url) {
  356. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  357. try {
  358. return build.call(() -> {
  359. try {
  360. HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url)
  361. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  362. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  363. .execute();
  364. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  365. if (!"success".equals(resp.getMessage())) {
  366. log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
  367. }
  368. return resp;
  369. } catch (Exception e) {
  370. log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  371. return null;
  372. }
  373. });
  374. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  375. log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  376. throw BusinessRuntimeException.getInstance("删除操作失败");
  377. }
  378. }
  379. /**
  380. * Codex GET请求
  381. */
  382. public ClaudeCodeResp executeCodexGetApi(String url) {
  383. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  384. try {
  385. return build.call(() -> {
  386. try {
  387. HttpResponse execute = HttpUtil.createGet(url)
  388. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  389. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  390. .execute();
  391. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  392. if (!"success".equals(resp.getMessage())) {
  393. log.error("codex GET URL:{}接口返回msg:{}", url, resp.getMessage());
  394. }
  395. return resp;
  396. } catch (Exception e) {
  397. log.error("codex GET请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  398. return null;
  399. }
  400. });
  401. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  402. log.error("重试调用codex GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  403. throw BusinessRuntimeException.getInstance("获取codex使用统计失败");
  404. }
  405. }
  406. }