package com.cyksj.service.codex.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.http.Method; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.cyksj.common.constant.Constant; import com.cyksj.common.exception.BusinessRuntimeException; import com.cyksj.common.util.Jsons; import com.cyksj.common.util.StringUtil; import com.cyksj.mapper.*; import com.cyksj.model.entity.*; import com.cyksj.model.request.UpdateDailyLimitRequest; import com.cyksj.model.request.codex.CodexUserPackageReq; import com.cyksj.model.response.claudecode.ClaudeCodeResp; import com.cyksj.model.views.CodexUserInfoView; import com.cyksj.service.codex.CodexService; import com.cyksj.service.user.UserBindRelationService; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; import com.github.rholder.retry.WaitStrategies; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * 项目名: yhlxj11111111 * 文件名: CodexServiceImpl * 创建者: Claude * 创建时间:2025/9/15 */ @Slf4j @Service @RequiredArgsConstructor public class CodexServiceImpl implements CodexService { private final CodexUserMapper codexUserMapper; private final ClaudeCodeUserMapper claudeCodeUserMapper; private final UserBindRelationService userBindRelationService; private final GoodsDonSkuMapper skuMapper; private final GroupsRelationMapper relationMapper; private final GroupsMapper groupsMapper; @Override public CodexUserInfoView getCodexUserInfo(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); // 查询Claude Code信息 CodexUserInfoView.ClaudeCodeInfo claudeCodeInfo = getClaudeCodeInfo(userId); if (codexUser == null) { //是否有claude code if (claudeCodeInfo.getCodePoints() != null) { GoodsDonSku sku = null; if (claudeCodeInfo.getRenewSkuId() != null) { sku = skuMapper.selectById(claudeCodeInfo.getRenewSkuId()); } //续费升级规格不存在 或者 不赠送codex if (sku == null || !sku.getIsCodex()) { GroupsRelation relation = relationMapper.selectById(claudeCodeInfo.getRelationId()); GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId()); sku = skuMapper.selectById(groupsTrips.getSkuId()); } //sku 存在并且开启赠送codex if (sku != null && sku.getIsCodex()) { CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder() .userId(userId) .planName(sku.getCodexPlanName()) .openaiDailyLimit(sku.getOpenaiDailyLimit()) .openaiQuota(sku.getOpenaiQuota()) .expiryTime(claudeCodeInfo.getExpiryTime()) .build(); createOrUpdateUserPackage(codexPackageReq); codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); } } if (codexUser == null) { return null; } } return CodexUserInfoView.builder().userId(codexUser.getUserId()).openaiDailyLimit(codexUser.getOpenaiDailyLimit()).openaiQuota(codexUser.getOpenaiQuota()).relationId(codexUser.getRelationId()).claudeCodeInfo(claudeCodeInfo).build(); } private CodexUserInfoView.ClaudeCodeInfo getClaudeCodeInfo(Long userId) { // 获取用户关联的ID列表 List userIdList = userBindRelationService.getRelationUserIdList(userId, null); ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1")); if (claudeCodeUser == null) { return null; } 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(); } @Override public void createOrUpdateUserPackage(CodexUserPackageReq req) { Long userId = req.getUserId(); // 构建完整的套餐参数 String planName = req.getPlanName(); Date expiryTime = req.getExpiryTime(); Integer openaiDailyLimit = req.getOpenaiDailyLimit(); Integer openaiQuota = req.getOpenaiQuota(); Long relationId = req.getRelationId(); try { createOrUpdateCodexUserPackage(userId, planName, openaiDailyLimit, openaiQuota, expiryTime); } catch (Exception e) { setCodexUserRetryUpdateInfo(relationId); return; } List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser()); existingCodexUser.setPlanName(planName); existingCodexUser.setRelationId(relationId); existingCodexUser.setUserId(userId); existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit); existingCodexUser.setOpenaiQuota(openaiQuota); existingCodexUser.setExpiryTime(expiryTime); if (existingCodexUser.getId() == null) { try { codexUserMapper.insert(existingCodexUser); } catch (DuplicateKeyException e) { } return; } codexUserMapper.updateById(existingCodexUser); } /** * 创建或更新Codex用户套餐 */ public ClaudeCodeResp createOrUpdateCodexUserPackage(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception { Map params = new HashMap<>(); params.put("plan_name", planName.replaceAll(";", "")); params.put("openai_daily_limit", openaiDailyLimit); params.put("openai_quota", openaiQuota); params.put("end_date", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss")); String planUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId); ClaudeCodeResp codexResp = executeCodexPostApi(planUrl, Jsons.toJson(params)); if (!"success".equals(codexResp.getMessage())) { log.info("用户codex用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, codexResp.getMessage()); throw BusinessRuntimeException.getInstance("调用codex 套餐接口失败"); } return codexResp; } private Retryer getApiRetryer(int wait, int stop) { return RetryerBuilder.newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build(); } /** * Codex POST请求 - 复制ClaudeCodeServiceImpl的实现模式 */ public ClaudeCodeResp executeCodexPostApi(String url, String body) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createPost(url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).body(body).execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!"success".equals(resp.getMessage())) { log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (Exception e) { log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e)); return null; } }); } catch (ExecutionException | com.github.rholder.retry.RetryException e) { log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("创建或更新codex user失败"); } } /** * codex用户信息重试更新信息 */ public void setCodexUserRetryUpdateInfo(Long relationId) { CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1")); if (codexUser != null) { codexUser.setIsRetry(Boolean.TRUE); codexUserMapper.updateById(codexUser); } } @Override public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { return null; } // 调用Codex API获取实时使用统计 String usageUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/usage?period=%s", userId, period); ClaudeCodeResp codexResp = executeCodexGetApi(usageUrl); return codexResp; } @Override public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { throw new BusinessRuntimeException("用户未开通Codex服务"); } Integer dailyLimit = request.getDailyLimit(); Integer totalQuota = request.getTotalQuota(); // 验证每日限额参数 if (dailyLimit == null || dailyLimit <= 0) { throw new BusinessRuntimeException("每日限额必须大于0"); } // 保存原始值用于回滚 Integer originalDailyLimit = codexUser.getOpenaiDailyLimit(); // 更新数据库中的每日限额 codexUser.setOpenaiDailyLimit(dailyLimit); codexUserMapper.updateById(codexUser); try { // 调用Codex API更新每日限额 String updateUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/limit", userId); Map params = new HashMap<>(); params.put("daily_limit", dailyLimit); params.put("total_quota", totalQuota); ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params)); if (!"success".equals(codexResp.getMessage())) { log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage()); // API调用失败时回滚数据库更新 codexUser.setOpenaiDailyLimit(originalDailyLimit); codexUserMapper.updateById(codexUser); throw new BusinessRuntimeException("更新每日限额失败"); } log.info("成功更新用户{}每日限额为{}", userId, dailyLimit); return codexResp; } catch (Exception e) { log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e)); // 异常时回滚数据库更新 codexUser.setOpenaiDailyLimit(originalDailyLimit); codexUserMapper.updateById(codexUser); throw new BusinessRuntimeException("更新每日限额失败"); } } @Override public ClaudeCodeResp getSystemMetrics(String period, String metric) { try { // 构建系统指标API URL String metricsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/system/metrics"); // 添加查询参数 List queryParams = new ArrayList<>(); if (period != null && !period.isEmpty()) { queryParams.add("period=" + period); } if (metric != null && !metric.isEmpty()) { queryParams.add("metric=" + metric); } if (!queryParams.isEmpty()) { metricsUrl += "?" + String.join("&", queryParams); } // 调用Codex API获取系统指标 ClaudeCodeResp codexResp = executeCodexGetApi(metricsUrl); if (codexResp == null) { log.error("获取系统指标失败: API返回null"); throw new BusinessRuntimeException("获取系统指标失败"); } log.info("成功获取系统指标, period={}, metric={}", period, metric); return codexResp; } catch (Exception e) { log.error("获取系统指标异常, period={}, metric={}, error={}", period, metric, StringUtil.getErrorText(e)); throw new BusinessRuntimeException("获取系统指标失败"); } } @Override public ClaudeCodeResp getUserKeys(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { throw new BusinessRuntimeException("用户未开通Codex服务"); } try { // 调用Codex API获取用户密钥列表 String keysUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId); ClaudeCodeResp codexResp = executeCodexGetApi(keysUrl); if (codexResp == null) { log.error("获取用户{}密钥列表失败: API返回null", userId); throw new BusinessRuntimeException("获取用户密钥列表失败"); } log.info("成功获取用户{}密钥列表", userId); return codexResp; } catch (Exception e) { log.error("获取用户{}密钥列表异常, error={}", userId, StringUtil.getErrorText(e)); throw new BusinessRuntimeException("获取用户密钥列表失败"); } } @Override public ClaudeCodeResp createUserKey(Long userId, String keyName) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { throw new BusinessRuntimeException("用户未开通Codex服务"); } try { // 调用Codex API创建用户密钥 String createKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId); Map params = new HashMap<>(); params.put("name", keyName); ClaudeCodeResp codexResp = executeCodexPostApi(createKeyUrl, Jsons.toJson(params)); if (!"success".equals(codexResp.getMessage())) { log.error("创建用户{}密钥失败: {}", userId, codexResp.getMessage()); throw new BusinessRuntimeException("创建用户密钥失败"); } log.info("成功创建用户{}密钥, keyName={}", userId, keyName); return codexResp; } catch (Exception e) { log.error("创建用户{}密钥异常, keyName={}, error={}", userId, keyName, StringUtil.getErrorText(e)); throw new BusinessRuntimeException("创建用户密钥失败"); } } @Override public ClaudeCodeResp deleteUserKey(Long userId, String keyId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { throw new BusinessRuntimeException("用户未开通Codex服务"); } try { // 调用Codex API删除用户密钥 String deleteKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys/%s", userId, keyId); ClaudeCodeResp codexResp = executeCodexDeleteApi(deleteKeyUrl); if (!"success".equals(codexResp.getMessage())) { log.error("删除用户{}密钥{}失败: {}", userId, keyId, codexResp.getMessage()); throw new BusinessRuntimeException("删除用户密钥失败"); } log.info("成功删除用户{}密钥{}", userId, keyId); return codexResp; } catch (Exception e) { log.error("删除用户{}密钥{}异常, error={}", userId, keyId, StringUtil.getErrorText(e)); throw new BusinessRuntimeException("删除用户密钥失败"); } } @Override public ClaudeCodeResp getUserDashboard(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { throw new BusinessRuntimeException("用户未开通Codex服务"); } try { // 调用Codex API获取用户仪表盘数据 String dashboardUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/dashboard", userId); ClaudeCodeResp codexResp = executeCodexGetApi(dashboardUrl); if (codexResp == null) { log.error("获取用户{}仪表盘数据失败: API返回null", userId); throw new BusinessRuntimeException("获取用户仪表盘数据失败"); } log.info("成功获取用户{}仪表盘数据", userId); return codexResp; } catch (Exception e) { log.error("获取用户{}仪表盘数据异常, error={}", userId, StringUtil.getErrorText(e)); throw new BusinessRuntimeException("获取用户仪表盘数据失败"); } } /** * Codex DELETE请求 */ public ClaudeCodeResp executeCodexDeleteApi(String url) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url) .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY) .setConnectionTimeout(Constant.CONNECT_MILLISECONDS) .execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!"success".equals(resp.getMessage())) { log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (Exception e) { log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e)); return null; } }); } catch (ExecutionException | com.github.rholder.retry.RetryException e) { log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("删除操作失败"); } } /** * Codex GET请求 */ public ClaudeCodeResp executeCodexGetApi(String url) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createGet(url) .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY) .setConnectionTimeout(Constant.CONNECT_MILLISECONDS) .execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!"success".equals(resp.getMessage())) { log.error("codex GET URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (Exception e) { log.error("codex GET请求异常: url={}, error={}", url, StringUtil.getErrorText(e)); return null; } }); } catch (ExecutionException | com.github.rholder.retry.RetryException e) { log.error("重试调用codex GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("获取codex使用统计失败"); } } }