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 cn.hutool.json.JSONObject; 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.mapper.claudecode.ClaudeCodeCreditCardMapper; import com.cyksj.mapper.claudecode.ClaudeCodeDayResetRecordMapper; import com.cyksj.mapper.claudecode.ClaudeCodeUserCreditCardRecordMapper; import com.cyksj.mapper.claudecode.ClaudeCodeUserResetRecordMapper; import com.cyksj.mapper.manage.distribute.DistributePointsExchangeMoneyMapper; import com.cyksj.mapper.manage.distribute.DistributeWaitingSendPointsMapper; import com.cyksj.model.entity.*; import com.cyksj.model.manage.views.GroupsRelationView; import com.cyksj.model.request.ClaudeCodeApiKeysReq; import com.cyksj.model.request.ClaudeCodeCreditCardUseReq; import com.cyksj.model.request.ClaudeCodeDelReq; import com.cyksj.model.request.CreditResetReq; import com.cyksj.model.request.codex.CodexUserPackageReq; import com.cyksj.model.response.ClaudeCodeRewardDetailsResp; import com.cyksj.model.response.CreditResetResp; import com.cyksj.model.response.TimeRateConfigsResp; import com.cyksj.model.response.claudecode.ClaudeCodeResp; import com.cyksj.model.views.*; import com.cyksj.redis.RedisService; import com.cyksj.service.claude.ClaudeCodeService; import com.cyksj.service.codex.CodexService; import com.cyksj.service.user.UserBindRelationService; import com.ejlchina.searcher.BeanSearcher; import com.ejlchina.searcher.SearchResult; 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 org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; 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 ClaudeCodeUserResetRecordMapper claudeCodeUserResetRecordMapper; private final ClaudeCodeUserCreditCardRecordMapper claudeCodeUserCreditCardRecordMapper; private final ClaudeCodeCreditCardMapper claudeCodeCreditCardMapper; private final UserMapper userMapper; private final DistributeWaitingSendPointsMapper distributeWaitingSendPointsMapper; private final DistributePointsExchangeMoneyMapper distributePointsExchangeMoneyMapper; private final GoodsDonMapper goodsDonMapper; private final ClaudeCodeDayResetRecordMapper claudeCodeDayResetRecordMapper; private final RedisService redisService; private final CodexService codexService; private final CodexUserMapper codexUserMapper; @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 thisRelationCodeDayilyLimit = null; ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1")); if (claudeCodeUser != null) { thisRelationCodeDayilyLimit = claudeCodeUser.getClaudeDailyLimit(); } Date expiryTime = relation.getExpiryTime(); Integer claudeDailyLimit = sku.getClaudeDailyLimit(); Integer claudeQuota = sku.getClaudeQuota(); Long skuId = sku.getId(); Boolean isUpgrade = false; Date renewExpiryTime = null; //若是续费升级 记录此时续费升级的过期时间 if (!ObjectUtil.equal(thisCodeOriSkuId, skuId)) { //非车票原规格id 续费升级记录 saveClaudeCodeRenewExpiryRecord(orderId, relationUserId, relationId, skuId); //是否需要修改当前claude code user 套餐等级 if (thisRelationCodeDayilyLimit != null && thisRelationCodeDayilyLimit <= claudeDailyLimit) { if (sku.getDays() != null) { renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays()); } else { renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths()); } isUpgrade = true; } } generateOrUpdateClaudeCodeUser(relationUserId, relationId, claudeDailyLimit, claudeQuota, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal(), userIdList); try { //获取最新的claude code user claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1")); //生成claude code或更新用户套餐 ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), claudeCodeUser.getExpiryTime(), claudeCodeUser.getRelationId(), sku); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { //重试更新code userInfo数据 setClaudeCodeUserRetryUpdateInfo(relationId); return; } //清缓存 if (orderType != null && 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 claudeDailyLimit, Integer claudeQuota, Date expiryTime, Boolean isUpgrade, Long orderId, Long renewSkuId, Date renewExpiryTime, String planName, List userIdList) { 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.setPlanName(planName); claudeCodeUser.setClaudeDailyLimit(claudeDailyLimit); claudeCodeUser.setClaudeQuota(claudeQuota); } claudeCodeUser.setUserId(relationUserId); claudeCodeUser.setRelationId(relationId); claudeCodeUser.setExpiryTime(expiryTime); if (claudeCodeUser.getId() == null) { claudeCodeUser.setPlanName(planName); claudeCodeUser.setClaudeDailyLimit(claudeDailyLimit); claudeCodeUser.setClaudeQuota(claudeQuota); //可重置次数 Integer resetCount = claudeCodeUserResetRecordMapper.getUserResetCountByUserIds(userIdList); if (resetCount != null && resetCount > 0) { claudeCodeUser.setResetCount(resetCount); } } 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(Constant.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(Constant.CLAUDE_CODE_API_PREFIX + "api/keys/%s/%s", claudeCodeUserId, keyId); HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.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(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", claudeCodeUserId); ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return Jsons.parseList(claudeCodeResp.getData(), ClaudeCodeUserApiKeysView.class); } return null; } @Override public ClaudeCodeResp getUserDashboard(Long userId) throws Exception { Long claudeCodeUserId = getClaudeCodeUserId(userId); if (claudeCodeUserId != null) { String url = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/claude/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(Constant.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(Constant.CLAUDE_CODE_API_PREFIX + "api/claude/users/%s/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(Constant.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")); CodexUser codexUser = null; CodexUserInfoView codexUserInfoView = null; //车票不存在 if (groupsRelationView == null) { //套餐是否存在 if (claudeCodeUser != null) { //清除用户套餐 if (delUserPackages(claudeCodeUser.getUserId())) { claudeCodeUserMapper.deleteById(claudeCodeUser.getId()); //codex codexUserMapper.delete(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, claudeCodeUser.getRelationId()).in(CodexUser::getUserId, userIdList)); } } return null; } GoodsDonSku sku = 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) { 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 { //试用车票 Long skuId = groupsRelationView.getSkuId(); sku = skuMapper.selectById(skuId); generateOrUpdateClaudeCodeUserInfo(null, groupsRelationView.getId(), groupsRelationView.getUserId(), sku, null); claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1")); } } else { claudeCodeUser.setExpiryTime(groupsRelationView.getExpiryTime()); claudeCodeUserMapper.updateById(claudeCodeUser); } //续费升级 if (claudeCodeUser.getRenewSkuId() != null) { sku = skuMapper.selectById(claudeCodeUser.getRenewSkuId()); } if (sku == null) { sku = skuMapper.selectById(groupsRelationView.getSkuId()); } //是否需要重试更新 cladueCode userInfo信息 if (claudeCodeUser.getIsRetry()) { try { ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), claudeCodeUser.getExpiryTime(), claudeCodeUser.getRelationId(), sku); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { claudeCodeUser.setIsRetry(Boolean.TRUE); claudeCodeUserMapper.updateById(claudeCodeUser); } else { claudeCodeUser.setIsRetry(Boolean.FALSE); claudeCodeUserMapper.updateById(claudeCodeUser); } } catch (Exception e) { } } codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, claudeCodeUser.getRelationId()).in(CodexUser::getUserId, userIdList).last("limit 1")); if (codexUser == null) { //是否赠送codex if (sku.getIsCodex()) { CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder() .userId(claudeCodeUser.getUserId()) .relationId(claudeCodeUser.getRelationId()) .planName(sku.getCodexPlanName()) .openaiDailyLimit(sku.getOpenaiDailyLimit()) .openaiQuota(sku.getOpenaiQuota()) .expiryTime(claudeCodeUser.getExpiryTime()).build(); codexService.createOrUpdateUserPackage(codexPackageReq); } } return ClaudeCodeUserInfoView.builder() .planName(claudeCodeUser.getPlanName()) .relationId(claudeCodeUser.getRelationId()) .claudeDailyLimit(claudeCodeUser.getClaudeDailyLimit()) .claudeQuota(claudeCodeUser.getClaudeQuota()) .expiryTime(claudeCodeUser.getExpiryTime()) .renewSkuId(Optional.ofNullable(claudeCodeUser.getRenewSkuId()).orElse(groupsRelationView.getSkuId())) .renewOrderId(claudeCodeUser.getRenewOrderId()) .renewExpiryTime(claudeCodeUser.getRenewExpiryTime()) .highestUsage(sku.getHighestUsage()) .build(); } public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer claudeDailyLimit, Integer claudeQuota, Date expiryTime, Long relationId, GoodsDonSku sku) throws Exception { Map parmas = new HashMap<>(); parmas.put("plan_name", planName.replaceAll(";", "")); parmas.put("claude_daily_limit", claudeDailyLimit); parmas.put("claude_quota", claudeQuota); //服务类型 parmas.put("service_type", "claude"); if (sku.getIsCodex()) { parmas.put("openai_daily_limit", sku.getOpenaiDailyLimit()); parmas.put("openai_quota", sku.getOpenaiQuota()); //组合 parmas.put("service_type", "combined"); } else { parmas.put("openai_daily_limit", 0); parmas.put("openai_quota", 0); //单独 claude类型 parmas.put("service_type", "claude"); } parmas.put("expire_time", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss")); String planUrl = String.format(Constant.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()); } else { //是否赠送codex if (sku.getIsCodex()) { CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder() .userId(userId) .relationId(relationId) .planName(sku.getCodexPlanName()) .openaiDailyLimit(sku.getOpenaiDailyLimit()) .openaiQuota(sku.getOpenaiQuota()) .expiryTime(expiryTime).build(); codexService.createOrUpdateUserPackage(codexPackageReq); } else { codexService.delCodexUser(userId, relationId); } } 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, GoodsDonSku sku) { try { ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), expiryTime, claudeCodeUser.getRelationId(), sku); 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(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan?service_type=claude", 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) { generateOrUpdateClaudeCodeUserInfo(orderId, relation.getId(), relation.getUserId(), sku, 2); } @Override public TimeRateConfigsResp getAllConfigs() { try { String url = Constant.CLAUDE_CODE_API_PREFIX + "api/time-multiplier/configs"; ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url); return Jsons.parseObject(claudeCodeResp.getData(), TimeRateConfigsResp.class); } catch (Exception e) { log.error("获取时间倍率配置列表失败", e); throw BusinessRuntimeException.getInstance("获取时间倍率配置列表失败"); } } @Override public CreditResetResp resetUserCredits(Long userId, CreditResetReq req) throws Exception { if (userId <= 0) { throw BusinessRuntimeException.getInstance("用户ID无效"); } String url = Constant.CLAUDE_CODE_API_PREFIX + "api/users/" + userId + "/credits/reset"; String body = req != null ? Jsons.toJson(req) : "{}"; ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(url, body); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { throw BusinessRuntimeException.getInstance("重置次数失败"); } return Jsons.parseObject(claudeCodeResp.getData(), CreditResetResp.class); } /** * 验证时间范围 */ private void validateTimeRange(Integer startHour, Integer endHour) { if (startHour == null || endHour == null) { throw BusinessRuntimeException.getInstance("开始和结束小时不能为空"); } if (startHour < 0 || startHour > 23) { throw BusinessRuntimeException.getInstance("开始小时必须在0-23之间"); } if (endHour < 0 || endHour > 23) { throw BusinessRuntimeException.getInstance("结束小时必须在0-23之间"); } } /** * 获取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(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 (!Constant.SUCCESS.equals(resp.getMessage())) { log.error("claude code GET URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (HttpException e) { return null; } }); } catch (ExecutionException | RetryException e) { log.error("重试调用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(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 (!Constant.SUCCESS.equals(resp.getMessage())) { log.error("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(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 (!Constant.SUCCESS.equals(resp.getMessage())) { log.error("claude code DELETE URL:{}接口返回msg:{}", url, resp.getMessage()); } return resp; } catch (HttpException e) { return null; } }); } catch (ExecutionException | RetryException e) { log.error("重试调用claude code DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e)); throw BusinessRuntimeException.getInstance("接口请求失败"); } } /** * 记录claude code 续费升级的过期时间 */ private void saveClaudeCodeRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Long skuId) { ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = new ClaudeCodeUserRenewExpiryRecord(); renewExpiryRecord.setOrderId(orderId); renewExpiryRecord.setUserId(relationUserId); renewExpiryRecord.setRelationId(relationId); renewExpiryRecord.setRenewSkuId(skuId); claudeCodeUserRenewExpiryRecordMapper.insert(renewExpiryRecord); } /** * claude code、codex user用户信息重试更新信息 */ 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); } 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); } } 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; } @Override @Transactional(rollbackFor = Throwable.class) public void useResetCount(Long userId) throws Exception { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); if (claudeCodeUser == null) { throw new BusinessRuntimeException("用户未开通Claude Code服务"); } Integer currentResetCount = claudeCodeUser.getResetCount(); if (currentResetCount == null || currentResetCount <= 0) { throw BusinessRuntimeException.getInstance("可用重置次数为0"); } claudeCodeUser.setResetCount(currentResetCount - 1); int updateResult = claudeCodeUserMapper.update(claudeCodeUser, Wrappers.lambdaUpdate(ClaudeCodeUser.class) .eq(ClaudeCodeUser::getId, claudeCodeUser.getId()) .eq(ClaudeCodeUser::getResetCount, currentResetCount) ); if (updateResult > 0) { log.info("用户{}成功使用重置次数,剩余次数:{}", userId, currentResetCount - 1); //重置用户次数 CreditResetReq creditResetReq = new CreditResetReq(); creditResetReq.setDescription("用户使用重置次数"); //调用API重置用户次数 resetUserCredits(claudeCodeUser.getUserId(), creditResetReq); ClaudeCodeUserResetRecord resetRecord = new ClaudeCodeUserResetRecord(); resetRecord.setUserId(userId); resetRecord.setNum(-1); resetRecord.setResetReason("使用重置次数"); claudeCodeUserResetRecordMapper.insert(resetRecord); } else { throw BusinessRuntimeException.getInstance("使用重置次数失败"); } } @Override @Transactional(rollbackFor = Throwable.class) public void useCreditCard(ClaudeCodeCreditCardUseReq req) throws Exception { Long userId = req.getUserId(); List userIdList = userBindRelationService.getRelationUserIdList(userId, null); ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); if (claudeCodeUser == null) { throw new BusinessRuntimeException("用户未开通Claude Code服务"); } ClaudeCodeCreditCard creditCard = claudeCodeCreditCardMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeCreditCard.class) .eq(ClaudeCodeCreditCard::getStatus, 0) .in(ClaudeCodeCreditCard::getUserId, userIdList) .last("limit 1") ); if (creditCard == null) { throw new BusinessRuntimeException("无可用补充卡"); } if (creditCard.getStatus() != 0) { if (creditCard.getStatus() == 1) { throw new BusinessRuntimeException("补充卡已被使用"); } else if (creditCard.getStatus() == 2) { throw new BusinessRuntimeException("补充卡已过期"); } else { throw new BusinessRuntimeException("补充卡状态异常"); } } String cardNumber = creditCard.getCardNumber(); if (creditCard.getExpiryTime() != null && creditCard.getExpiryTime().before(new Date())) { creditCard.setStatus(2); claudeCodeCreditCardMapper.updateById(creditCard); throw new BusinessRuntimeException("补充卡已过期"); } ClaudeCodeResp userBalance = getUserBalance(userId); JSONObject data = Jsons.parseObject(userBalance.getData(), JSONObject.class); Integer availableCredits = data.getInt("available_credits"); Integer creditLimit = data.getInt("credit_limit"); //超出积分 异常 if (availableCredits + creditCard.getCreditAmount() > creditLimit) { throw BusinessRuntimeException.getInstance("目前使用只能补充到{0},建议您消耗更多积分后再兑换使用", creditLimit); } Integer beforeCodePoints = availableCredits; Integer addCodePoints = creditCard.getCreditAmount(); Integer afterCodePoints = beforeCodePoints + addCodePoints; creditCard.setStatus(1); creditCard.setUsedUserId(userId); creditCard.setUsedTime(new Date()); int cardUpdateResult = claudeCodeCreditCardMapper.update(creditCard, Wrappers.lambdaUpdate(ClaudeCodeCreditCard.class) .eq(ClaudeCodeCreditCard::getId, creditCard.getId()) .eq(ClaudeCodeCreditCard::getStatus, 0) ); if (cardUpdateResult > 0) { ClaudeCodeUserCreditCardRecord creditCardRecord = new ClaudeCodeUserCreditCardRecord(); creditCardRecord.setUserId(userId); creditCardRecord.setBeforeCodePoints(beforeCodePoints); creditCardRecord.setAddCodePoints(addCodePoints); creditCardRecord.setAfterCodePoints(afterCodePoints); creditCardRecord.setCardNumber(cardNumber); //使用一次 creditCardRecord.setNum(-1); creditCardRecord.setUseReason("使用补充卡"); claudeCodeUserCreditCardRecordMapper.insert(creditCardRecord); log.info("用户{}成功使用补充卡{},增加{}预算", userId, cardNumber, addCodePoints); //调用API调整用户积分 adjClaudeCodeUserPoints(claudeCodeUser.getUserId(), creditCard.getCreditAmount(), "adjustment", "用户使用补充卡"); } else { throw new BusinessRuntimeException("补充卡使用失败,请重试"); } } @Override public SearchResult getUserResetRecords(Long userId, Integer page, Integer limit) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); return beanSearcher.search(ClaudeCodeUserResetRecordView.class, MapUtils.builder() .field(ClaudeCodeUserResetRecordView::getUserId, userIdList).op(Operator.InList) .page(page, limit) .orderBy(ClaudeCodeUserResetRecordView::getCreatedTime).desc() .build()); } @Override public SearchResult getUserCreditCardRecords(Long userId, Integer page, Integer limit) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); return beanSearcher.search(ClaudeCodeUserCreditCardRecordView.class, MapUtils.builder() .field(ClaudeCodeUserCreditCardRecordView::getUserId, userIdList).op(Operator.InList) .page(page, limit) .orderBy(ClaudeCodeUserCreditCardRecordView::getCreatedTime).desc() .build()); } @Override public ClaudeCodeRewardDetailsResp getUserRewardDetails(Long userId, Boolean general) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); ClaudeCodeRewardDetailsResp response = new ClaudeCodeRewardDetailsResp(); // 获取用户Claude Code信息 ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); // 设置可重置次数 if (claudeCodeUser != null) { Integer resetCount = claudeCodeUser.getResetCount(); response.setResetCount(resetCount != null ? resetCount : 0); } else { response.setResetCount(0); } // 获取可用补充卡数量 Integer availableCreditCards = claudeCodeCreditCardMapper.selectCount( Wrappers.lambdaQuery(ClaudeCodeCreditCard.class) .eq(ClaudeCodeCreditCard::getStatus, 0) .in(ClaudeCodeCreditCard::getUserId, userIdList) .isNull(ClaudeCodeCreditCard::getUsedUserId) ); response.setAvailableCreditCards(availableCreditCards != null ? availableCreditCards : 0); //是否是普通推广 if (general != null && general) { //提现门槛 DistributePointsExchangeMoney distributePointsExchangeMoney = distributePointsExchangeMoneyMapper.selectOne(Wrappers.lambdaQuery(DistributePointsExchangeMoney.class).last("limit 1")); response.setThresholdMoney(BigDecimal.valueOf(distributePointsExchangeMoney.getMinPoints()).divide(BigDecimal.valueOf(distributePointsExchangeMoney.getPoints())).multiply(distributePointsExchangeMoney.getMoney())); // 获取分销相关数据 try { User user = userMapper.selectById(userId); List distributeWaitingSendPoints = distributeWaitingSendPointsMapper.selectList(Wrappers.lambdaQuery(DistributeWaitingSendPoints.class).eq(DistributeWaitingSendPoints::getSendStatus, DistributeWaitingSendPoints.Status.waiting).eq(DistributeWaitingSendPoints::getSharedId, userId).select(DistributeWaitingSendPoints::getPoints)); BigDecimal waitingPoints = BigDecimal.ZERO; if (!distributeWaitingSendPoints.isEmpty()) { Optional points = distributeWaitingSendPoints.stream().map(DistributeWaitingSendPoints::getPoints).reduce(BigDecimal::add); if (points.isPresent()) { waitingPoints = points.get(); } } GoodsDon goodsDon = goodsDonMapper.selectById(Constant.CLAUDE_CODE_GOODS_ID); //cc 分销平台比例 if (goodsDon != null) { response.setRate(goodsDon.getRate()); } if (user.getPoints().compareTo(BigDecimal.ZERO) <= 0 && waitingPoints.compareTo(BigDecimal.ZERO) <= 0) { response.setGotMoney(BigDecimal.ZERO); response.setWaitingMoney(BigDecimal.ZERO); } else { BigDecimal points = BigDecimal.valueOf(distributePointsExchangeMoney.getPoints()); BigDecimal money = distributePointsExchangeMoney.getMoney(); BigDecimal oneHundred = BigDecimal.valueOf(100); if (user.getPoints().compareTo(BigDecimal.ZERO) >= 0) { response.setGotMoney(user.getPoints().divide(points).multiply(money).divide(oneHundred, 2, RoundingMode.HALF_DOWN)); } if (user.getPoints().compareTo(BigDecimal.ZERO) >= 0) { response.setWaitingMoney(waitingPoints.divide(points).multiply(money).divide(oneHundred, 2, RoundingMode.DOWN)); } } } catch (Exception e) { log.warn("获取用户{}分销数据异常", userId, e); // 设置默认值 response.setGotMoney(BigDecimal.ZERO); response.setWaitingMoney(BigDecimal.ZERO); } } return response; } @Override public void sendCreditCard(Long orderId, Long userId, Integer points, String cardNumber, String remark) { ClaudeCodeCreditCard creditCard = new ClaudeCodeCreditCard(); creditCard.setUserId(userId); creditCard.setOrderId(orderId); creditCard.setCardNumber(cardNumber); creditCard.setCreditAmount(points); creditCard.setRemark(remark); claudeCodeCreditCardMapper.insert(creditCard); //补充卡明细 ClaudeCodeUserCreditCardRecord creditCardRecord = new ClaudeCodeUserCreditCardRecord(); creditCardRecord.setUserId(userId); creditCardRecord.setCardNumber(cardNumber); creditCardRecord.setUseReason(remark); creditCardRecord.setNum(1); creditCardRecord.setAddCodePoints(points); claudeCodeUserCreditCardRecordMapper.insert(creditCardRecord); } @Override public void adjUserRestNum(Long orderId, Long userId, Integer num, String remark) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); // 获取用户Claude Code信息 ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); if (claudeCodeUser == null) { log.info("claude code user不存在"); return; } Integer resetCount = claudeCodeUser.getResetCount(); claudeCodeUser.setResetCount(resetCount + num); if (claudeCodeUser.getResetCount() < 0) { return; } int update = 0; while (update == 0) { update = claudeCodeUserMapper.update(claudeCodeUser, Wrappers.lambdaUpdate(ClaudeCodeUser.class).eq(ClaudeCodeUser::getUserId, claudeCodeUser.getUserId()).eq(ClaudeCodeUser::getResetCount, resetCount)); if (update == 0) { claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); if (claudeCodeUser == null) { break; } resetCount = claudeCodeUser.getResetCount(); claudeCodeUser.setResetCount(resetCount + num); if (claudeCodeUser.getResetCount() < 0) { break; } } //记录获取重置次数记录 ClaudeCodeUserResetRecord resetRecord = new ClaudeCodeUserResetRecord(); resetRecord.setOrderId(orderId); resetRecord.setUserId(userId); resetRecord.setResetReason(remark); resetRecord.setNum(num); claudeCodeUserResetRecordMapper.insert(resetRecord); } } @Override @Transactional(rollbackFor = Throwable.class) public void useDayResetCount(Long userId) throws Exception { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); // 获取用户Claude Code信息 ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne( Wrappers.lambdaQuery(ClaudeCodeUser.class) .in(ClaudeCodeUser::getUserId, userIdList) .last("limit 1") ); if (claudeCodeUser == null) { log.info("claude code user不存在"); return; } //非标准版、专业版 不给使用 if (claudeCodeUser.getClaudeDailyLimit() < 5000) { return; } Integer selectCount = claudeCodeDayResetRecordMapper.selectCount(Wrappers.lambdaQuery(ClaudeCodeDayResetRecord.class).in(ClaudeCodeDayResetRecord::getUserId, userIdList).gt(ClaudeCodeDayResetRecord::getCreatedTime, DateUtil.beginOfDay(DateTime.now()))); //今日使用为0 if (selectCount == 0) { String idxKey = "cc:uid:" + userId; try { if (redisService.setNx(idxKey, userId, 5l)) { //重置用户次数 CreditResetReq creditResetReq = new CreditResetReq(); creditResetReq.setDescription("用户使用每日重置"); //调用API重置用户次数 resetUserCredits(claudeCodeUser.getUserId(), creditResetReq); ClaudeCodeDayResetRecord claudeCodeDayResetRecord = new ClaudeCodeDayResetRecord(); claudeCodeDayResetRecord.setUserId(userId); claudeCodeDayResetRecord.setRelationId(claudeCodeUser.getRelationId()); claudeCodeDayResetRecordMapper.insert(claudeCodeDayResetRecord); } } finally { if (redisService.hasKey(idxKey)) { redisService.del(idxKey); } } } } @Override public Boolean getUserSubs(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 false; } return true; } private void adjClaudeCodeUserPoints(Long userId, Integer points, String type, String description) throws Exception { String apiKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/adjust", userId); Map parmas = new HashMap<>(); parmas.put("amount", points); parmas.put("type", type); parmas.put("description", description); ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(apiKeyUrl, Jsons.toJson(parmas)); if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) { throw BusinessRuntimeException.getInstance("使用补充卡失败"); } } /** * 更新重试codexUser info */ private void updateCodexUserIfTry(CodexUser codexUser) { if (codexUser.getIsRetry()) { try { ClaudeCodeResp codeUserPackage = codexService.createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime()); if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) { codexUser.setIsRetry(Boolean.TRUE); codexUserMapper.updateById(codexUser); } else { codexUser.setIsRetry(Boolean.FALSE); codexUserMapper.updateById(codexUser); } } catch (Exception e) { } } } }