package com.cyksj.service.claude.impl; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.http.HttpException; 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.manage.views.GroupsRelationView; import com.cyksj.model.request.ClaudeCodeApiKeysReq; import com.cyksj.model.request.ClaudeCodeDelReq; import com.cyksj.model.response.ClaudeCodePointsHistoryResp; import com.cyksj.model.response.claudecode.ClaudeCodeResp; import com.cyksj.model.views.ClaudeCodeUserApiKeysView; import com.cyksj.model.views.ClaudeCodeUserInfoView; import com.cyksj.service.claude.ClaudeCodeService; import com.cyksj.service.user.UserBindRelationService; import com.ejlchina.searcher.BeanSearcher; import com.ejlchina.searcher.param.Operator; import com.ejlchina.searcher.util.MapUtils; import com.github.rholder.retry.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * 项目名: yhlxj11111111 * 文件名: ClaudeCodeServiceImpl * 创建者: JavaZou * 创建时间:2025/8/8 10:55 */ @Service @RequiredArgsConstructor @Slf4j public class ClaudeCodeServiceImpl implements ClaudeCodeService { private final UserBindRelationService userBindRelationService; private final GroupsRelationMapper relationMapper; private final BeanSearcher beanSearcher; private final ClaudeCodeUserRenewExpiryRecordMapper claudeCodeUserRenewExpiryRecordMapper; private final ClaudeCodeUserMapper claudeCodeUserMapper; private final GoodsDonSkuMapper skuMapper; private final GroupsMapper groupsMapper; private final OrderDonMapper orderDonMapper; private final static String CLAUDE_CODE_API_PREFIX = "https://relay01.yhlxj.com/"; public static final String CLAUDE_CODE_API_ADMIN_KEY = "claudecodeyhlxjclaude"; //连接超时时间 毫秒 public static final int CONNECT_MILLISECONDS = 10000; @Override public void generateOrUpdateClaudeCodeUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList)); GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId()); GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId()); Long thisCodeOriSkuId = relationSku.getId(); Long relationUserId = relation.getUserId(); //此时车票的套餐规格积分 Integer thisRelationCodePoints = null; ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1")); if (claudeCodeUser != null) { thisRelationCodePoints = claudeCodeUser.getCodePoints(); } Date expiryTime = relation.getExpiryTime(); Integer codePoints = sku.getCodePoints(); Integer codeCreditRecovery = sku.getCodeCreditRecovery(); Long skuId = sku.getId(); Boolean isUpgrade = false; Date renewExpiryTime = null; //若是续费升级 记录此时续费升级的过期时间 if (thisRelationCodePoints != null && thisRelationCodePoints <= codePoints && !ObjectUtil.equal(thisCodeOriSkuId, skuId)) { if (sku.getDays() != null) { renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays()); } else { renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths()); } isUpgrade = true; saveClaudeCodeRenewExpiryRecord(orderId, relationUserId, relationId, renewExpiryTime, skuId); } generateOrUpdateClaudeCodeUser(relationUserId, relationId, codePoints, codeCreditRecovery, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal()); try { //生成claude code或更新用户套餐 ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), sku.getSpecVal(), codePoints, codeCreditRecovery, expiryTime); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { //重试更新code userInfo数据 setClaudeCodeUserRetryUpdateInfo(relationId); return; } //清缓存 if (orderType == 2) { invalidateUserCache(relationUserId); } } catch (Exception e) { log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e)); //重试更新code userInfo数据 setClaudeCodeUserRetryUpdateInfo(relationId); } } private void generateOrUpdateClaudeCodeUser(Long relationUserId, Long relationId, Integer codePoints, Integer codeCreditRecovery, Date expiryTime, Boolean isUpgrade, Long orderId, Long renewSkuId, Date renewExpiryTime, String planName) { ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1")); if (claudeCodeUser == null) { claudeCodeUser = new ClaudeCodeUser(); } if (isUpgrade) { claudeCodeUser.setRenewOrderId(orderId); claudeCodeUser.setRenewSkuId(renewSkuId); claudeCodeUser.setRenewExpiryTime(renewExpiryTime); } claudeCodeUser.setUserId(relationUserId); claudeCodeUser.setPlanName(planName); claudeCodeUser.setRelationId(relationId); claudeCodeUser.setExpiryTime(expiryTime); claudeCodeUser.setCodePoints(codePoints); claudeCodeUser.setCodeCreditRecovery(codeCreditRecovery); if (claudeCodeUser.getId() == null) { try { claudeCodeUserMapper.insert(claudeCodeUser); } catch (DuplicateKeyException e) { log.info("插入claude code user 重复"); } } else claudeCodeUserMapper.updateById(claudeCodeUser); } @Override public ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception { Long userId = req.getUserId(); if (checkClaudeCodeUser(userId)) { Long claudeCodeUserId = getClaudeCodeUserId(userId); Assert.notNull(claudeCodeUserId, "您的claude code账号已过期"); String apiKeyUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", claudeCodeUserId); Map parmas = new HashMap<>(); parmas.put("name", req.getName()); Integer expiresDays = req.getExpiresDays(); if (expiresDays != null) { parmas.put("expires_days", expiresDays); } ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(apiKeyUrl, Jsons.toJson(parmas)); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { throw BusinessRuntimeException.getInstance("创建失败"); } return claudeCodeResp; } throw new BusinessRuntimeException("您还没有claudecode账号"); } @Override public void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception { Long userId = delReq.getUserId(); Long keyId = delReq.getKeyId(); if (checkClaudeCodeUser(userId)) { Long claudeCodeUserId = getClaudeCodeUserId(userId); String url = String.format(CLAUDE_CODE_API_PREFIX + "api/keys/%s/%s", claudeCodeUserId, keyId); HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(CONNECT_MILLISECONDS).execute(); String responseBody = execute.body(); ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { log.error("删除key失败:{}", claudeCodeResp.getMessage()); throw BusinessRuntimeException.getInstance("删除失败"); } } } @Override public List getUserApiKeys(long userId) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (claudeCodeUserId != null) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", claudeCodeUserId); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return Jsons.parseList(claudeCodeResp.getData(), ClaudeCodeUserApiKeysView.class); } return null; } @Override public ClaudeCodePointsHistoryResp getUserPointsDetail(Long userId, String type, Integer page, Integer limit) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (claudeCodeUserId != null) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/history?page=%s&limit=%s&type=%s", claudeCodeUserId, page, limit, type); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); ClaudeCodePointsHistoryResp historyResp = Jsons.parseObject(claudeCodeResp.getData(), ClaudeCodePointsHistoryResp.class); return historyResp; } return null; } @Override public ClaudeCodeResp getUserDashboard(Long userId) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (claudeCodeUserId != null) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/dashboard", claudeCodeUserId); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return claudeCodeResp; } return null; } @Override public ClaudeCodeResp getUserBalance(Long userId) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (claudeCodeUserId != null) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/balance", claudeCodeUserId); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return claudeCodeResp; } return null; } @Override public ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, Integer page, Integer limit, String order, String type) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (start == null) { DateTime now = DateTime.now(); start = DateUtil.offsetDay(now, -1).toString(); end = now.toString(); } if (claudeCodeUserId != null) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/analytics?start=%s&end=%s&tz=%s&page=%s&limit=%s&order=%s&type=%s", claudeCodeUserId, start, end, tz, page, limit, order, type); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return claudeCodeResp; } return null; } /** * 用户缓存失效 */ @Override public void invalidateUserCache(Long userId) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/cache/invalidate/user/%s", userId); try { executeClaudeCodePostApi(url, null); } catch (Exception e) { } } @Override public ClaudeCodeUserInfoView getClaudeCodeUserSubsInfo(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); //是否有该车票 GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder() .field(GroupsRelationView::getUserId, userIdList).op(Operator.InList) .field(GroupsRelationView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID) .field(GroupsRelationView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan) .build()); ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1")); //车票不存在 if (groupsRelationView == null) { //套餐是否存在 if (claudeCodeUser != null) { //清除用户套餐 if (delUserPackages(claudeCodeUser.getUserId())) { claudeCodeUserMapper.deleteById(claudeCodeUser.getId()); } } return null; } if (claudeCodeUser == null) { OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class) .eq(OrderDon::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID) .eq(OrderDon::getRelationId, groupsRelationView.getId()) .in(OrderDon::getUserId, userIdList) .notIn(OrderDon::getStatus, Constant.noOrderAllStatus) .orderByDesc(OrderDon::getId) .last("limit 1")); if (orderDon != null) { GoodsDonSku sku = skuMapper.selectById(orderDon.getSkuId()); generateOrUpdateClaudeCodeUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType()); claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1")); } } else { claudeCodeUser.setExpiryTime(groupsRelationView.getExpiryTime()); claudeCodeUserMapper.updateById(claudeCodeUser); } //是否需要重试更新 cladueCode userInfo信息 if (claudeCodeUser.getIsRetry()) { try { ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), claudeCodeUser.getExpiryTime()); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { claudeCodeUser.setIsRetry(Boolean.TRUE); claudeCodeUserMapper.updateById(claudeCodeUser); } else { claudeCodeUser.setIsRetry(Boolean.FALSE); claudeCodeUserMapper.updateById(claudeCodeUser); } } catch (Exception e) { } } return ClaudeCodeUserInfoView.builder() .planName(claudeCodeUser.getPlanName()) .relationId(claudeCodeUser.getRelationId()) .codePoints(claudeCodeUser.getCodePoints()) .codeCreditRecovery(claudeCodeUser.getCodeCreditRecovery()) .expiryTime(claudeCodeUser.getExpiryTime()) .renewSkuId(claudeCodeUser.getRenewSkuId()) .renewOrderId(claudeCodeUser.getRenewOrderId()) .renewExpiryTime(claudeCodeUser.getRenewExpiryTime()) .build(); } public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer codePoints, Integer codeCreditRecovery, Date expiryTime) throws Exception { Map parmas = new HashMap<>(); parmas.put("plan_name", planName.replaceAll(";", "")); parmas.put("credit_limit", codePoints); parmas.put("credit_recovery", codeCreditRecovery); parmas.put("expire_time", DateUtil.format(expiryTime,"yyyy-MM-dd HH:mm:ss")); String planUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId); ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(planUrl, Jsons.toJson(parmas)); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { log.info("用户claude code用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, claudeCodeResp.getMessage()); } return claudeCodeResp; } /** * 校验是否是claude code用户 * @param userId * @return */ @Override public Boolean checkClaudeCodeUser(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); Integer count = relationMapper.selectCountByGoodsId(userIdList, Constant.CLAUDE_CODE_GOODS_ID, DateTime.now()); return count > 0; } @Override public void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime) { try { ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), expiryTime); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { //重试标记 claudeCodeUser.setIsRetry(Boolean.TRUE); claudeCodeUserMapper.updateById(claudeCodeUser); return; } //刷新缓存 invalidateUserCache(claudeCodeUser.getUserId()); } catch (Exception e) { claudeCodeUser.setIsRetry(Boolean.TRUE); claudeCodeUserMapper.updateById(claudeCodeUser); } } @Override public Boolean delUserPackages(Long userId) { String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId); try { ClaudeCodeResp claudeCodeResp = executeClaudeCodeDeletedApi(url); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { log.error("删除用户:{} claude code套餐失败:{}", userId, claudeCodeResp.getMessage()); return Boolean.FALSE; } return Boolean.TRUE; } catch (Exception e) { log.error("删除用户:{} claude code套餐报错:{}", userId, StringUtil.getErrorText(e)); } return Boolean.FALSE; } @Override public void handleClaudeCodeUserPackageSku(GroupsRelation relation, GoodsDonSku sku, Long orderId) { Long groupsId = relation.getGroupsId(); GroupsTrips groupsTrips = groupsMapper.selectById(groupsId); GoodsDonSku oriRelationSku = skuMapper.selectById(groupsTrips.getSkuId()); Integer oriRelationSkuCodePoints = oriRelationSku.getCodePoints(); Integer presentCodePoints = sku.getCodePoints(); Long oriRelationSkuId = oriRelationSku.getId(); Long toUseSkuId = sku.getId(); //若是续费升级 记录此时续费升级的过期时间 Boolean isUpgrade = false; Date renewExpiryTime = null; if (oriRelationSkuCodePoints <= presentCodePoints && !ObjectUtil.equal(oriRelationSkuId, toUseSkuId)) { if (sku.getDays() != null) { renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays()); } else { renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths()); } isUpgrade = true; saveClaudeCodeRenewExpiryRecord(orderId, relation.getUserId(), relation.getId(), renewExpiryTime, sku.getId()); } } /** * 获取claude code用户id */ public Long getClaudeCodeUserId(Long userId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder() .field(GroupsRelationView::getUserId, userIdList).op(Operator.InList) .field(GroupsRelationView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID) .field(GroupsRelationView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan) .build()); if (groupsRelationView != null) { return groupsRelationView.getUserId(); } return null; } /** * claude code GET请求 */ public ClaudeCodeResp executeClaudeCodeGetApi(String url) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createGet(url) .header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY) .setConnectionTimeout(CONNECT_MILLISECONDS) .execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!Constant.SUCCESS.equals(resp.getMessage())) { log.info("claude code GET URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (HttpException e) { return null; } }); } catch (ExecutionException | RetryException e) { log.info("重试调用claude code GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("接口请求失败"); } } /** * claude code Post请求 */ public ClaudeCodeResp executeClaudeCodePostApi(String url, String body) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createPost(url) .header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY) .setConnectionTimeout(CONNECT_MILLISECONDS) .body(body) .execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!Constant.SUCCESS.equals(resp.getMessage())) { log.info("claude code POST URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (Exception e) { return null; } }); } catch (ExecutionException | RetryException e) { log.info("重试调用claude code POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("接口请求失败"); } } /** * claude code DELETE请求 */ public ClaudeCodeResp executeClaudeCodeDeletedApi(String url) { Retryer build = getApiRetryer(1, 3); try { return build.call(() -> { try { HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(CONNECT_MILLISECONDS).execute(); ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class); if (!Constant.SUCCESS.equals(resp.getMessage())) { log.info("claude code DELETE URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (HttpException e) { return null; } }); } catch (ExecutionException | RetryException e) { log.info("重试调用claude code DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("接口请求失败"); } } /** * 记录claude code 续费升级的过期时间 */ private void saveClaudeCodeRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Date expiryTime, Long skuId) { ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = new ClaudeCodeUserRenewExpiryRecord(); renewExpiryRecord.setOrderId(orderId); renewExpiryRecord.setUserId(relationUserId); renewExpiryRecord.setRelationId(relationId); renewExpiryRecord.setRenewSkuId(skuId); renewExpiryRecord.setExpiryTime(expiryTime); claudeCodeUserRenewExpiryRecordMapper.insert(renewExpiryRecord); } /** * claude code用户信息重试更新信息 */ public void setClaudeCodeUserRetryUpdateInfo(Long relationId) { ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1")); if (claudeCodeUser != null) { claudeCodeUser.setIsRetry(Boolean.TRUE); claudeCodeUserMapper.updateById(claudeCodeUser); } } public Retryer getApiRetryer(Integer sleepSecond, Integer attemptNum) { Retryer build = RetryerBuilder.newBuilder() .retryIfException() //运行时异常重试 .retryIfRuntimeException() //false重试 .retryIfResult(res -> res == null) //1s 间隔 .withWaitStrategy(WaitStrategies.fixedWait(sleepSecond, TimeUnit.SECONDS)) //停止策略 : 尝试请求3次 .withStopStrategy(StopStrategies.stopAfterAttempt(attemptNum)).build(); return build; } }