CodexServiceImpl.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 != null && 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(claudeCodeInfo.getUserId())
  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().planName(codexUser.getPlanName()).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().userId(claudeCodeUser.getUserId()).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. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  103. CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser());
  104. existingCodexUser.setPlanName(planName);
  105. existingCodexUser.setRelationId(relationId);
  106. existingCodexUser.setUserId(userId);
  107. existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit);
  108. existingCodexUser.setOpenaiQuota(openaiQuota);
  109. existingCodexUser.setExpiryTime(expiryTime);
  110. if (existingCodexUser.getId() == null) {
  111. try {
  112. codexUserMapper.insert(existingCodexUser);
  113. } catch (DuplicateKeyException e) {
  114. }
  115. return;
  116. }
  117. codexUserMapper.updateById(existingCodexUser);
  118. }
  119. /**
  120. * 创建或更新Codex用户套餐
  121. */
  122. public ClaudeCodeResp createOrUpdateCodexUserPackage(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
  123. Map<String, Object> params = new HashMap<>();
  124. params.put("plan_name", planName.replaceAll(";", ""));
  125. params.put("openai_daily_limit", openaiDailyLimit);
  126. params.put("openai_quota", openaiQuota);
  127. params.put("end_date", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
  128. String planUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
  129. ClaudeCodeResp codexResp = executeCodexPostApi(planUrl, Jsons.toJson(params));
  130. if (!"success".equals(codexResp.getMessage())) {
  131. log.info("用户codex用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, codexResp.getMessage());
  132. throw BusinessRuntimeException.getInstance("调用codex 套餐接口失败");
  133. }
  134. return codexResp;
  135. }
  136. private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
  137. return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
  138. }
  139. /**
  140. * Codex POST请求 - 复制ClaudeCodeServiceImpl的实现模式
  141. */
  142. public ClaudeCodeResp executeCodexPostApi(String url, String body) {
  143. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  144. try {
  145. return build.call(() -> {
  146. try {
  147. HttpResponse execute = HttpUtil.createPost(url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).body(body).execute();
  148. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  149. if (!"success".equals(resp.getMessage())) {
  150. log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage());
  151. }
  152. return resp;
  153. } catch (Exception e) {
  154. log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  155. return null;
  156. }
  157. });
  158. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  159. log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  160. throw BusinessRuntimeException.getInstance("创建或更新codex user失败");
  161. }
  162. }
  163. /**
  164. * codex用户信息重试更新信息
  165. */
  166. public void setCodexUserRetryUpdateInfo(Long relationId) {
  167. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  168. if (codexUser != null) {
  169. codexUser.setIsRetry(Boolean.TRUE);
  170. codexUserMapper.updateById(codexUser);
  171. }
  172. }
  173. @Override
  174. public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) {
  175. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  176. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  177. if (codexUser == null) {
  178. return null;
  179. }
  180. // 调用Codex API获取实时使用统计
  181. String usageUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/usage?period=%s", codexUser.getUserId(), period);
  182. ClaudeCodeResp codexResp = executeCodexGetApi(usageUrl);
  183. return codexResp;
  184. }
  185. @Override
  186. public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) {
  187. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  188. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  189. if (codexUser == null) {
  190. throw new BusinessRuntimeException("用户未开通Codex服务");
  191. }
  192. Integer dailyLimit = request.getDailyLimit();
  193. Integer totalQuota = request.getTotalQuota();
  194. // 验证每日限额参数
  195. if (dailyLimit == null || dailyLimit <= 0) {
  196. throw new BusinessRuntimeException("每日限额必须大于0");
  197. }
  198. // 保存原始值用于回滚
  199. Integer originalDailyLimit = codexUser.getOpenaiDailyLimit();
  200. // 更新数据库中的每日限额
  201. codexUser.setOpenaiDailyLimit(dailyLimit);
  202. codexUserMapper.updateById(codexUser);
  203. try {
  204. // 调用Codex API更新每日限额
  205. String updateUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/limit", codexUser.getUserId());
  206. Map<String, Object> params = new HashMap<>();
  207. params.put("daily_limit", dailyLimit);
  208. params.put("total_quota", totalQuota);
  209. ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params));
  210. if (!"success".equals(codexResp.getMessage())) {
  211. log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage());
  212. // API调用失败时回滚数据库更新
  213. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  214. codexUserMapper.updateById(codexUser);
  215. throw new BusinessRuntimeException("更新每日限额失败");
  216. }
  217. log.info("成功更新用户{}每日限额为{}", userId, dailyLimit);
  218. return codexResp;
  219. } catch (Exception e) {
  220. log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e));
  221. // 异常时回滚数据库更新
  222. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  223. codexUserMapper.updateById(codexUser);
  224. throw new BusinessRuntimeException("更新每日限额失败");
  225. }
  226. }
  227. @Override
  228. public ClaudeCodeResp getSystemMetrics(String period, String metric) {
  229. try {
  230. // 构建系统指标API URL
  231. String metricsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/system/metrics");
  232. // 添加查询参数
  233. List<String> queryParams = new ArrayList<>();
  234. if (period != null && !period.isEmpty()) {
  235. queryParams.add("period=" + period);
  236. }
  237. if (metric != null && !metric.isEmpty()) {
  238. queryParams.add("metric=" + metric);
  239. }
  240. if (!queryParams.isEmpty()) {
  241. metricsUrl += "?" + String.join("&", queryParams);
  242. }
  243. // 调用Codex API获取系统指标
  244. ClaudeCodeResp codexResp = executeCodexGetApi(metricsUrl);
  245. if (codexResp == null) {
  246. log.error("获取系统指标失败: API返回null");
  247. throw new BusinessRuntimeException("获取系统指标失败");
  248. }
  249. log.info("成功获取系统指标, period={}, metric={}", period, metric);
  250. return codexResp;
  251. } catch (Exception e) {
  252. log.error("获取系统指标异常, period={}, metric={}, error={}", period, metric, StringUtil.getErrorText(e));
  253. throw new BusinessRuntimeException("获取系统指标失败");
  254. }
  255. }
  256. @Override
  257. public ClaudeCodeResp getJavaUserDashboard(Long userId) {
  258. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  259. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  260. if (codexUser == null) {
  261. return null;
  262. }
  263. // 调用Codex API获取用户控制台数据
  264. String dashboardUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/dashboard", codexUser.getUserId());
  265. ClaudeCodeResp codexResp = executeCodexGetApi(dashboardUrl);
  266. return codexResp;
  267. }
  268. @Override
  269. public ClaudeCodeResp getJavaUserAnalytics(Long userId) {
  270. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  271. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  272. if (codexUser == null) {
  273. return null;
  274. }
  275. // 调用Codex API获取用户分析数据
  276. String analyticsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/analytics", codexUser.getUserId());
  277. ClaudeCodeResp codexResp = executeCodexGetApi(analyticsUrl);
  278. return codexResp;
  279. }
  280. /**
  281. * Codex DELETE请求
  282. */
  283. public ClaudeCodeResp executeCodexDeleteApi(String url) {
  284. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  285. try {
  286. return build.call(() -> {
  287. try {
  288. HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url)
  289. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  290. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  291. .execute();
  292. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  293. if (!"success".equals(resp.getMessage())) {
  294. log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
  295. }
  296. return resp;
  297. } catch (Exception e) {
  298. log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  299. return null;
  300. }
  301. });
  302. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  303. log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  304. throw BusinessRuntimeException.getInstance("删除操作失败");
  305. }
  306. }
  307. /**
  308. * Codex GET请求
  309. */
  310. public ClaudeCodeResp executeCodexGetApi(String url) {
  311. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  312. try {
  313. return build.call(() -> {
  314. try {
  315. HttpResponse execute = HttpUtil.createGet(url)
  316. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  317. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  318. .execute();
  319. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  320. if (!"success".equals(resp.getMessage())) {
  321. log.error("codex GET URL:{}接口返回msg:{}", url, resp.getMessage());
  322. }
  323. return resp;
  324. } catch (Exception e) {
  325. log.error("codex GET请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  326. return null;
  327. }
  328. });
  329. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  330. log.error("重试调用codex GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  331. throw BusinessRuntimeException.getInstance("获取codex使用统计失败");
  332. }
  333. }
  334. }