ClaudeCodeServiceImpl.java 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282
  1. package com.cyksj.service.claude.impl;
  2. import cn.hutool.core.date.DateTime;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.lang.Assert;
  5. import cn.hutool.core.util.ObjectUtil;
  6. import cn.hutool.http.HttpException;
  7. import cn.hutool.http.HttpResponse;
  8. import cn.hutool.http.HttpUtil;
  9. import cn.hutool.http.Method;
  10. import cn.hutool.json.JSONObject;
  11. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  12. import com.cyksj.common.constant.Constant;
  13. import com.cyksj.common.exception.BusinessRuntimeException;
  14. import com.cyksj.common.util.Jsons;
  15. import com.cyksj.common.util.StringUtil;
  16. import com.cyksj.mapper.*;
  17. import com.cyksj.mapper.claudecode.*;
  18. import com.cyksj.mapper.manage.distribute.DistributePointsExchangeMoneyMapper;
  19. import com.cyksj.mapper.manage.distribute.DistributeWaitingSendPointsMapper;
  20. import com.cyksj.model.dto.ClaudeCodeDeductDto;
  21. import com.cyksj.model.entity.*;
  22. import com.cyksj.model.manage.views.GroupsRelationView;
  23. import com.cyksj.model.request.ClaudeCodeApiKeysReq;
  24. import com.cyksj.model.request.ClaudeCodeCreditCardUseReq;
  25. import com.cyksj.model.request.ClaudeCodeDelReq;
  26. import com.cyksj.model.request.CreditResetReq;
  27. import com.cyksj.model.request.codex.CodexUserPackageReq;
  28. import com.cyksj.model.response.ClaudeCodeRewardDetailsResp;
  29. import com.cyksj.model.response.CreditResetResp;
  30. import com.cyksj.model.response.TimeRateConfigsResp;
  31. import com.cyksj.model.response.claudecode.ClaudeCodeResp;
  32. import com.cyksj.model.views.*;
  33. import com.cyksj.redis.RedisService;
  34. import com.cyksj.service.api.ClaudeCodexApiService;
  35. import com.cyksj.service.claude.ClaudeCodeService;
  36. import com.cyksj.service.codex.CodexService;
  37. import com.cyksj.service.user.UserBindRelationService;
  38. import com.ejlchina.searcher.BeanSearcher;
  39. import com.ejlchina.searcher.SearchResult;
  40. import com.ejlchina.searcher.param.Operator;
  41. import com.ejlchina.searcher.util.MapUtils;
  42. import com.github.rholder.retry.*;
  43. import lombok.RequiredArgsConstructor;
  44. import lombok.extern.slf4j.Slf4j;
  45. import org.springframework.dao.DuplicateKeyException;
  46. import org.springframework.stereotype.Service;
  47. import org.springframework.transaction.annotation.Transactional;
  48. import java.math.BigDecimal;
  49. import java.math.RoundingMode;
  50. import java.util.*;
  51. import java.util.concurrent.ExecutionException;
  52. import java.util.concurrent.TimeUnit;
  53. /**
  54. * 项目名: yhlxj11111111
  55. * 文件名: ClaudeCodeServiceImpl
  56. * 创建者: JavaZou
  57. * 创建时间:2025/8/8 10:55
  58. */
  59. @Service
  60. @RequiredArgsConstructor
  61. @Slf4j
  62. public class ClaudeCodeServiceImpl implements ClaudeCodeService {
  63. private final UserBindRelationService userBindRelationService;
  64. private final GroupsRelationMapper relationMapper;
  65. private final BeanSearcher beanSearcher;
  66. private final ClaudeCodeUserRenewExpiryRecordMapper claudeCodeUserRenewExpiryRecordMapper;
  67. private final ClaudeCodeUserMapper claudeCodeUserMapper;
  68. private final GoodsDonSkuMapper skuMapper;
  69. private final GroupsMapper groupsMapper;
  70. private final OrderDonMapper orderDonMapper;
  71. private final ClaudeCodeUserResetRecordMapper claudeCodeUserResetRecordMapper;
  72. private final ClaudeCodeUserCreditCardRecordMapper claudeCodeUserCreditCardRecordMapper;
  73. private final ClaudeCodeCreditCardMapper claudeCodeCreditCardMapper;
  74. private final UserMapper userMapper;
  75. private final DistributeWaitingSendPointsMapper distributeWaitingSendPointsMapper;
  76. private final DistributePointsExchangeMoneyMapper distributePointsExchangeMoneyMapper;
  77. private final GoodsDonMapper goodsDonMapper;
  78. private final ClaudeCodeDayResetRecordMapper claudeCodeDayResetRecordMapper;
  79. private final RedisService redisService;
  80. private final CodexService codexService;
  81. private final CodexUserMapper codexUserMapper;
  82. private final ClaudeCodexApiService claudeCodexApiService;
  83. private final ClaudeCodeDeductOrderRelateMapper claudeCodeDeductOrderRelateMapper;
  84. @Override
  85. public void generateOrUpdateClaudeCodeUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType) {
  86. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  87. GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList));
  88. GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
  89. GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId());
  90. Long thisCodeOriSkuId = relationSku.getId();
  91. Long relationUserId = relation.getUserId();
  92. //此时车票的套餐规格积分
  93. Integer thisRelationCodeDayilyLimit = null;
  94. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
  95. if (claudeCodeUser != null) {
  96. thisRelationCodeDayilyLimit = claudeCodeUser.getClaudeDailyLimit();
  97. }
  98. Date expiryTime = relation.getExpiryTime();
  99. Integer claudeDailyLimit = sku.getClaudeDailyLimit();
  100. Integer claudeQuota = sku.getClaudeQuota();
  101. Long skuId = sku.getId();
  102. Boolean isUpgrade = false;
  103. Date renewExpiryTime = null;
  104. //若是续费升级 记录此时续费升级的过期时间
  105. if (!ObjectUtil.equal(thisCodeOriSkuId, skuId)) {
  106. //非车票原规格id 续费升级记录
  107. saveClaudeCodeRenewExpiryRecord(orderId, relationUserId, relationId, skuId);
  108. //是否需要修改当前claude code user 套餐等级
  109. if (thisRelationCodeDayilyLimit != null && thisRelationCodeDayilyLimit <= claudeDailyLimit) {
  110. if (sku.getDays() != null) {
  111. renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
  112. } else {
  113. renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
  114. }
  115. isUpgrade = true;
  116. }
  117. }
  118. generateOrUpdateClaudeCodeUser(relationUserId, relationId, claudeDailyLimit, claudeQuota, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal(), userIdList);
  119. try {
  120. //获取最新的claude code user
  121. claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
  122. //生成claude code或更新用户套餐
  123. ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), claudeCodeUser.getExpiryTime(), claudeCodeUser.getRelationId(), sku);
  124. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  125. //重试更新code userInfo数据
  126. setClaudeCodeUserRetryUpdateInfo(relationId);
  127. return;
  128. }
  129. //清缓存
  130. if (orderType != null && orderType == 2) {
  131. invalidateUserCache(relationUserId);
  132. }
  133. } catch (Exception e) {
  134. log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e));
  135. //重试更新code userInfo数据
  136. setClaudeCodeUserRetryUpdateInfo(relationId);
  137. }
  138. }
  139. private void generateOrUpdateClaudeCodeUser(Long relationUserId, Long relationId, Integer claudeDailyLimit, Integer claudeQuota, Date expiryTime, Boolean isUpgrade, Long orderId, Long renewSkuId, Date renewExpiryTime, String planName, List<Long> userIdList) {
  140. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
  141. if (claudeCodeUser == null) {
  142. claudeCodeUser = new ClaudeCodeUser();
  143. }
  144. if (isUpgrade) {
  145. claudeCodeUser.setRenewOrderId(orderId);
  146. claudeCodeUser.setRenewSkuId(renewSkuId);
  147. claudeCodeUser.setRenewExpiryTime(renewExpiryTime);
  148. claudeCodeUser.setPlanName(planName);
  149. claudeCodeUser.setClaudeDailyLimit(claudeDailyLimit);
  150. claudeCodeUser.setClaudeQuota(claudeQuota);
  151. }
  152. claudeCodeUser.setUserId(relationUserId);
  153. claudeCodeUser.setRelationId(relationId);
  154. claudeCodeUser.setExpiryTime(expiryTime);
  155. if (claudeCodeUser.getId() == null) {
  156. claudeCodeUser.setPlanName(planName);
  157. claudeCodeUser.setClaudeDailyLimit(claudeDailyLimit);
  158. claudeCodeUser.setClaudeQuota(claudeQuota);
  159. //可重置次数
  160. Integer resetCount = claudeCodeUserResetRecordMapper.getUserResetCountByUserIds(userIdList);
  161. if (resetCount != null && resetCount > 0) {
  162. claudeCodeUser.setResetCount(resetCount);
  163. }
  164. }
  165. if (claudeCodeUser.getId() == null) {
  166. try {
  167. claudeCodeUserMapper.insert(claudeCodeUser);
  168. } catch (DuplicateKeyException e) {
  169. log.info("插入claude code user 重复");
  170. }
  171. } else claudeCodeUserMapper.updateById(claudeCodeUser);
  172. }
  173. @Override
  174. public ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
  175. Long userId = req.getUserId();
  176. if (checkClaudeCodeUser(userId)) {
  177. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  178. Assert.notNull(claudeCodeUserId, "您的claude code账号已过期");
  179. req.setUserId(claudeCodeUserId);
  180. return claudeCodexApiService.createApiKeys(req);
  181. }
  182. throw new BusinessRuntimeException("您还没有claudecode账号");
  183. }
  184. @Override
  185. public void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception {
  186. Long userId = delReq.getUserId();
  187. Long keyId = delReq.getKeyId();
  188. if (checkClaudeCodeUser(userId)) {
  189. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  190. claudeCodexApiService.deleteApiKeysById(claudeCodeUserId, keyId);
  191. }
  192. }
  193. @Override
  194. public List<ClaudeCodeUserApiKeysView> getUserApiKeys(long userId) throws Exception {
  195. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  196. if (claudeCodeUserId != null) {
  197. return claudeCodexApiService.getUserApiKeys(claudeCodeUserId);
  198. }
  199. return null;
  200. }
  201. @Override
  202. public ClaudeCodeResp getUserDashboard(Long userId) throws Exception {
  203. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  204. if (claudeCodeUserId != null) {
  205. String url = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/claude/users/%s/dashboard", claudeCodeUserId);
  206. ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
  207. return claudeCodeResp;
  208. }
  209. return null;
  210. }
  211. @Override
  212. public ClaudeCodeResp getUserBalance(Long userId) throws Exception {
  213. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  214. if (claudeCodeUserId != null) {
  215. String url = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/balance", claudeCodeUserId);
  216. ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
  217. return claudeCodeResp;
  218. }
  219. return null;
  220. }
  221. @Override
  222. public ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, Integer page, Integer limit, String order, String type) throws Exception {
  223. Long claudeCodeUserId = getClaudeCodeUserId(userId);
  224. if (start == null) {
  225. DateTime now = DateTime.now();
  226. start = DateUtil.offsetDay(now, -1).toString();
  227. end = now.toString();
  228. }
  229. if (claudeCodeUserId != null) {
  230. 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);
  231. ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
  232. return claudeCodeResp;
  233. }
  234. return null;
  235. }
  236. /**
  237. * 用户缓存失效
  238. */
  239. @Override
  240. public void invalidateUserCache(Long userId) {
  241. String url = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/cache/invalidate/user/%s", userId);
  242. try {
  243. executeClaudeCodePostApi(url, null);
  244. } catch (Exception e) {
  245. }
  246. }
  247. @Override
  248. public ClaudeCodeUserInfoView getClaudeCodeUserSubsInfo(Long userId) {
  249. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  250. //是否有该车票
  251. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  252. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  253. .field(GroupsRelationMirrorView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  254. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  255. .build());
  256. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  257. CodexUser codexUser = null;
  258. CodexUserInfoView codexUserInfoView = null;
  259. //车票不存在
  260. if (groupsRelationMirrorView == null) {
  261. //套餐是否存在
  262. if (claudeCodeUser != null) {
  263. //清除用户套餐
  264. if (delUserPackages(claudeCodeUser.getUserId())) {
  265. claudeCodeUserMapper.deleteById(claudeCodeUser.getId());
  266. //codex
  267. codexUserMapper.delete(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, claudeCodeUser.getRelationId()).in(CodexUser::getUserId, userIdList));
  268. }
  269. }
  270. return null;
  271. }
  272. GoodsDonSku sku = null;
  273. if (claudeCodeUser == null) {
  274. OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
  275. .eq(OrderDon::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  276. .eq(OrderDon::getRelationId, groupsRelationMirrorView.getId())
  277. .in(OrderDon::getUserId, userIdList)
  278. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  279. .orderByDesc(OrderDon::getId)
  280. .last("limit 1"));
  281. if (orderDon != null) {
  282. sku = skuMapper.selectById(orderDon.getSkuId());
  283. generateOrUpdateClaudeCodeUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
  284. claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  285. } else {
  286. //试用车票
  287. Long skuId = groupsRelationMirrorView.getSkuId();
  288. sku = skuMapper.selectById(skuId);
  289. generateOrUpdateClaudeCodeUserInfo(null, groupsRelationMirrorView.getId(), groupsRelationMirrorView.getUserId(), sku, null);
  290. claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  291. }
  292. }
  293. //续费升级
  294. if (claudeCodeUser.getRenewSkuId() != null) {
  295. sku = skuMapper.selectById(claudeCodeUser.getRenewSkuId());
  296. }
  297. if (sku == null) {
  298. sku = skuMapper.selectById(groupsRelationMirrorView.getSkuId());
  299. }
  300. if (claudeCodeUser != null) {
  301. claudeCodeUser.setPlanName(sku.getSpecVal());
  302. claudeCodeUser.setExpiryTime(groupsRelationMirrorView.getExpiryTime());
  303. claudeCodeUserMapper.updateById(claudeCodeUser);
  304. }
  305. //是否需要重试更新 cladueCode userInfo信息
  306. if (claudeCodeUser.getIsRetry()) {
  307. try {
  308. ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), claudeCodeUser.getExpiryTime(), claudeCodeUser.getRelationId(), sku);
  309. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  310. claudeCodeUser.setIsRetry(Boolean.TRUE);
  311. claudeCodeUserMapper.updateById(claudeCodeUser);
  312. } else {
  313. claudeCodeUser.setIsRetry(Boolean.FALSE);
  314. claudeCodeUserMapper.updateById(claudeCodeUser);
  315. }
  316. } catch (Exception e) {
  317. }
  318. }
  319. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, claudeCodeUser.getRelationId()).in(CodexUser::getUserId, userIdList).last("limit 1"));
  320. if (codexUser == null) {
  321. //是否赠送codex
  322. if (sku.getIsCodex()) {
  323. CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
  324. .userId(claudeCodeUser.getUserId())
  325. .relationId(claudeCodeUser.getRelationId())
  326. .planName(sku.getCodexPlanName())
  327. .openaiDailyLimit(sku.getOpenaiDailyLimit())
  328. .openaiQuota(sku.getOpenaiQuota())
  329. .expiryTime(claudeCodeUser.getExpiryTime()).build();
  330. codexService.createOrUpdateUserPackage(codexPackageReq);
  331. }
  332. }
  333. return ClaudeCodeUserInfoView.builder()
  334. .planName(claudeCodeUser.getPlanName())
  335. .relationId(claudeCodeUser.getRelationId())
  336. .claudeDailyLimit(claudeCodeUser.getClaudeDailyLimit())
  337. .claudeQuota(claudeCodeUser.getClaudeQuota())
  338. .expiryTime(claudeCodeUser.getExpiryTime())
  339. .skuId(groupsRelationMirrorView.getSkuId())
  340. .renewSkuId(claudeCodeUser.getRenewSkuId())
  341. .renewOrderId(claudeCodeUser.getRenewOrderId())
  342. .renewExpiryTime(claudeCodeUser.getRenewExpiryTime())
  343. .highestUsage(sku.getHighestUsage())
  344. .build();
  345. }
  346. public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer claudeDailyLimit, Integer claudeQuota, Date expiryTime, Long relationId, GoodsDonSku sku) throws Exception {
  347. Map<String, Object> parmas = new HashMap<>();
  348. parmas.put("plan_name", planName.replaceAll(";", ""));
  349. parmas.put("claude_daily_limit", claudeDailyLimit);
  350. parmas.put("claude_quota", claudeQuota);
  351. //服务类型
  352. parmas.put("service_type", "claude");
  353. if (sku != null && sku.getIsCodex()) {
  354. parmas.put("openai_daily_limit", sku.getOpenaiDailyLimit());
  355. parmas.put("openai_quota", sku.getOpenaiQuota());
  356. //组合
  357. parmas.put("service_type", "combined");
  358. } else {
  359. parmas.put("openai_daily_limit", 0);
  360. parmas.put("openai_quota", 0);
  361. //单独 claude类型
  362. parmas.put("service_type", "claude");
  363. }
  364. parmas.put("expire_time", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
  365. String planUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
  366. ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(planUrl, Jsons.toJson(parmas));
  367. if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
  368. log.info("用户claude code用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, claudeCodeResp.getMessage());
  369. } else {
  370. //是否赠送codex
  371. if (sku != null && sku.getIsCodex()) {
  372. CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
  373. .userId(userId)
  374. .relationId(relationId)
  375. .planName(sku.getCodexPlanName())
  376. .openaiDailyLimit(sku.getOpenaiDailyLimit())
  377. .openaiQuota(sku.getOpenaiQuota())
  378. .expiryTime(expiryTime).build();
  379. codexService.createOrUpdateUserPackage(codexPackageReq);
  380. } else {
  381. codexService.delCodexUser(userId, relationId);
  382. }
  383. }
  384. return claudeCodeResp;
  385. }
  386. /**
  387. * 校验是否是claude code用户
  388. * @param userId
  389. * @return
  390. */
  391. @Override
  392. public Boolean checkClaudeCodeUser(Long userId) {
  393. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  394. Integer count = relationMapper.selectCountByGoodsId(userIdList, Constant.CLAUDE_CODE_GOODS_ID, DateTime.now());
  395. return count > 0;
  396. }
  397. @Override
  398. public void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime, GoodsDonSku sku) {
  399. try {
  400. ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getClaudeDailyLimit(), claudeCodeUser.getClaudeQuota(), expiryTime, claudeCodeUser.getRelationId(), sku);
  401. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  402. //重试标记
  403. claudeCodeUser.setIsRetry(Boolean.TRUE);
  404. claudeCodeUserMapper.updateById(claudeCodeUser);
  405. return;
  406. }
  407. //刷新缓存
  408. invalidateUserCache(claudeCodeUser.getUserId());
  409. } catch (Exception e) {
  410. claudeCodeUser.setIsRetry(Boolean.TRUE);
  411. claudeCodeUserMapper.updateById(claudeCodeUser);
  412. }
  413. }
  414. @Override
  415. public Boolean delUserPackages(Long userId) {
  416. String url = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan?service_type=claude", userId);
  417. try {
  418. ClaudeCodeResp claudeCodeResp = executeClaudeCodeDeletedApi(url);
  419. if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
  420. log.error("删除用户:{} claude code套餐失败:{}", userId, claudeCodeResp.getMessage());
  421. return Boolean.FALSE;
  422. }
  423. return Boolean.TRUE;
  424. } catch (Exception e) {
  425. log.error("删除用户:{} claude code套餐报错:{}", userId, StringUtil.getErrorText(e));
  426. }
  427. return Boolean.FALSE;
  428. }
  429. @Override
  430. public void handleClaudeCodeUserPackageSku(GroupsRelation relation, GoodsDonSku sku, Long orderId) {
  431. generateOrUpdateClaudeCodeUserInfo(orderId, relation.getId(), relation.getUserId(), sku, 2);
  432. }
  433. @Override
  434. public TimeRateConfigsResp getAllConfigs() {
  435. try {
  436. String url = Constant.CLAUDE_CODE_API_PREFIX + "api/time-multiplier/configs";
  437. ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
  438. return Jsons.parseObject(claudeCodeResp.getData(), TimeRateConfigsResp.class);
  439. } catch (Exception e) {
  440. log.error("获取时间倍率配置列表失败", e);
  441. throw BusinessRuntimeException.getInstance("获取时间倍率配置列表失败");
  442. }
  443. }
  444. @Override
  445. public CreditResetResp resetUserCredits(Long userId, CreditResetReq req) throws Exception {
  446. if (userId <= 0) {
  447. throw BusinessRuntimeException.getInstance("用户ID无效");
  448. }
  449. String url = Constant.CLAUDE_CODE_API_PREFIX + "api/users/" + userId + "/credits/reset";
  450. String body = req != null ? Jsons.toJson(req) : "{}";
  451. ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(url, body);
  452. if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
  453. throw BusinessRuntimeException.getInstance("重置次数失败");
  454. }
  455. return Jsons.parseObject(claudeCodeResp.getData(), CreditResetResp.class);
  456. }
  457. /**
  458. * 验证时间范围
  459. */
  460. private void validateTimeRange(Integer startHour, Integer endHour) {
  461. if (startHour == null || endHour == null) {
  462. throw BusinessRuntimeException.getInstance("开始和结束小时不能为空");
  463. }
  464. if (startHour < 0 || startHour > 23) {
  465. throw BusinessRuntimeException.getInstance("开始小时必须在0-23之间");
  466. }
  467. if (endHour < 0 || endHour > 23) {
  468. throw BusinessRuntimeException.getInstance("结束小时必须在0-23之间");
  469. }
  470. }
  471. /**
  472. * 获取claude code用户id
  473. */
  474. public Long getClaudeCodeUserId(Long userId) {
  475. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  476. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  477. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  478. .field(GroupsRelationMirrorView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  479. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  480. .build());
  481. if (groupsRelationMirrorView != null) {
  482. return groupsRelationMirrorView.getUserId();
  483. }
  484. return null;
  485. }
  486. /**
  487. * claude code GET请求
  488. */
  489. public ClaudeCodeResp executeClaudeCodeGetApi(String url) {
  490. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  491. try {
  492. return build.call(() -> {
  493. try {
  494. HttpResponse execute = HttpUtil.createGet(url)
  495. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  496. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  497. .execute();
  498. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  499. if (!Constant.SUCCESS.equals(resp.getMessage())) {
  500. log.error("claude code GET URL:{}接口返回msg:{}", url, resp.getMessage());
  501. }
  502. return resp;
  503. } catch (HttpException e) {
  504. return null;
  505. }
  506. });
  507. } catch (ExecutionException | RetryException e) {
  508. log.error("重试调用claude code GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  509. throw BusinessRuntimeException.getInstance("接口请求失败");
  510. }
  511. }
  512. /**
  513. * claude code Post请求
  514. */
  515. public ClaudeCodeResp executeClaudeCodePostApi(String url, String body) {
  516. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  517. try {
  518. return build.call(() -> {
  519. try {
  520. HttpResponse execute = HttpUtil.createPost(url)
  521. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  522. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  523. .body(body)
  524. .execute();
  525. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  526. if (!Constant.SUCCESS.equals(resp.getMessage())) {
  527. log.error("claude code POST URL:{}接口返回msg:{}", url, resp.getMessage());
  528. }
  529. return resp;
  530. } catch (Exception e) {
  531. return null;
  532. }
  533. });
  534. } catch (ExecutionException | RetryException e) {
  535. log.info("重试调用claude code POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  536. throw BusinessRuntimeException.getInstance("接口请求失败");
  537. }
  538. }
  539. /**
  540. * claude code DELETE请求
  541. */
  542. public ClaudeCodeResp executeClaudeCodeDeletedApi(String url) {
  543. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  544. try {
  545. return build.call(() -> {
  546. try {
  547. HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).execute();
  548. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  549. if (!Constant.SUCCESS.equals(resp.getMessage())) {
  550. log.error("claude code DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
  551. }
  552. return resp;
  553. } catch (HttpException e) {
  554. return null;
  555. }
  556. });
  557. } catch (ExecutionException | RetryException e) {
  558. log.error("重试调用claude code DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  559. throw BusinessRuntimeException.getInstance("接口请求失败");
  560. }
  561. }
  562. /**
  563. * 记录claude code 续费升级的过期时间
  564. */
  565. private void saveClaudeCodeRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Long skuId) {
  566. ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = new ClaudeCodeUserRenewExpiryRecord();
  567. renewExpiryRecord.setOrderId(orderId);
  568. renewExpiryRecord.setUserId(relationUserId);
  569. renewExpiryRecord.setRelationId(relationId);
  570. renewExpiryRecord.setRenewSkuId(skuId);
  571. claudeCodeUserRenewExpiryRecordMapper.insert(renewExpiryRecord);
  572. }
  573. /**
  574. * claude code、codex user用户信息重试更新信息
  575. */
  576. public void setClaudeCodeUserRetryUpdateInfo(Long relationId) {
  577. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
  578. if (claudeCodeUser != null) {
  579. claudeCodeUser.setIsRetry(Boolean.TRUE);
  580. claudeCodeUserMapper.updateById(claudeCodeUser);
  581. }
  582. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  583. if (codexUser != null) {
  584. codexUser.setIsRetry(Boolean.TRUE);
  585. codexUserMapper.updateById(codexUser);
  586. }
  587. }
  588. public Retryer getApiRetryer(Integer sleepSecond, Integer attemptNum) {
  589. Retryer build = RetryerBuilder.newBuilder()
  590. .retryIfException()
  591. //运行时异常重试
  592. .retryIfRuntimeException()
  593. //false重试
  594. .retryIfResult(res -> res == null)
  595. //1s 间隔
  596. .withWaitStrategy(WaitStrategies.fixedWait(sleepSecond, TimeUnit.SECONDS))
  597. //停止策略 : 尝试请求3次
  598. .withStopStrategy(StopStrategies.stopAfterAttempt(attemptNum)).build();
  599. return build;
  600. }
  601. @Override
  602. @Transactional(rollbackFor = Throwable.class)
  603. public void useResetCount(Long userId) throws Exception {
  604. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  605. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(
  606. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  607. .in(ClaudeCodeUser::getUserId, userIdList)
  608. .last("limit 1")
  609. );
  610. if (claudeCodeUser == null) {
  611. throw new BusinessRuntimeException("用户未开通Claude Code服务");
  612. }
  613. Integer currentResetCount = claudeCodeUser.getResetCount();
  614. if (currentResetCount == null || currentResetCount <= 0) {
  615. throw BusinessRuntimeException.getInstance("可用重置次数为0");
  616. }
  617. claudeCodeUser.setResetCount(currentResetCount - 1);
  618. int updateResult = claudeCodeUserMapper.update(claudeCodeUser,
  619. Wrappers.lambdaUpdate(ClaudeCodeUser.class)
  620. .eq(ClaudeCodeUser::getId, claudeCodeUser.getId())
  621. .eq(ClaudeCodeUser::getResetCount, currentResetCount)
  622. );
  623. if (updateResult > 0) {
  624. log.info("用户{}成功使用重置次数,剩余次数:{}", userId, currentResetCount - 1);
  625. //重置用户次数
  626. CreditResetReq creditResetReq = new CreditResetReq();
  627. creditResetReq.setDescription("用户使用重置次数");
  628. //调用API重置用户次数
  629. resetUserCredits(claudeCodeUser.getUserId(), creditResetReq);
  630. ClaudeCodeUserResetRecord resetRecord = new ClaudeCodeUserResetRecord();
  631. resetRecord.setUserId(userId);
  632. resetRecord.setNum(-1);
  633. resetRecord.setResetReason("使用重置次数");
  634. claudeCodeUserResetRecordMapper.insert(resetRecord);
  635. } else {
  636. throw BusinessRuntimeException.getInstance("使用重置次数失败");
  637. }
  638. }
  639. @Override
  640. @Transactional(rollbackFor = Throwable.class)
  641. public void useCreditCard(ClaudeCodeCreditCardUseReq req) throws Exception {
  642. Long userId = req.getUserId();
  643. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  644. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(
  645. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  646. .in(ClaudeCodeUser::getUserId, userIdList)
  647. .last("limit 1")
  648. );
  649. if (claudeCodeUser == null) {
  650. throw new BusinessRuntimeException("用户未开通Claude Code服务");
  651. }
  652. ClaudeCodeCreditCard creditCard = claudeCodeCreditCardMapper.selectOne(
  653. Wrappers.lambdaQuery(ClaudeCodeCreditCard.class)
  654. .eq(ClaudeCodeCreditCard::getStatus, 0)
  655. .in(ClaudeCodeCreditCard::getUserId, userIdList)
  656. .last("limit 1")
  657. );
  658. if (creditCard == null) {
  659. throw new BusinessRuntimeException("无可用补充卡");
  660. }
  661. if (creditCard.getStatus() != 0) {
  662. if (creditCard.getStatus() == 1) {
  663. throw new BusinessRuntimeException("补充卡已被使用");
  664. } else if (creditCard.getStatus() == 2) {
  665. throw new BusinessRuntimeException("补充卡已过期");
  666. } else {
  667. throw new BusinessRuntimeException("补充卡状态异常");
  668. }
  669. }
  670. String cardNumber = creditCard.getCardNumber();
  671. if (creditCard.getExpiryTime() != null && creditCard.getExpiryTime().before(new Date())) {
  672. creditCard.setStatus(2);
  673. claudeCodeCreditCardMapper.updateById(creditCard);
  674. throw new BusinessRuntimeException("补充卡已过期");
  675. }
  676. ClaudeCodeResp dashboard = getUserDashboard(userId);
  677. JSONObject data = Jsons.parseObject(dashboard.getData(), JSONObject.class);
  678. BigDecimal daily_remaining = data.getBigDecimal("daily_remaining");
  679. BigDecimal daily_total = BigDecimal.valueOf(claudeCodeUser.getClaudeDailyLimit());
  680. //超出积分 异常
  681. if (daily_remaining.add(BigDecimal.valueOf(creditCard.getCreditAmount())).compareTo(daily_total) > 0) {
  682. throw BusinessRuntimeException.getInstance("目前使用只能补充到{0},建议您消耗更多额度后再补充使用", daily_total);
  683. }
  684. BigDecimal beforeCodePoints = daily_remaining;
  685. BigDecimal addCodePoints = BigDecimal.valueOf(creditCard.getCreditAmount());
  686. BigDecimal afterCodePoints = beforeCodePoints.add(addCodePoints);
  687. creditCard.setStatus(1);
  688. creditCard.setUsedUserId(userId);
  689. creditCard.setUsedTime(new Date());
  690. int cardUpdateResult = claudeCodeCreditCardMapper.update(creditCard,
  691. Wrappers.lambdaUpdate(ClaudeCodeCreditCard.class)
  692. .eq(ClaudeCodeCreditCard::getId, creditCard.getId())
  693. .eq(ClaudeCodeCreditCard::getStatus, 0)
  694. );
  695. if (cardUpdateResult > 0) {
  696. ClaudeCodeUserCreditCardRecord creditCardRecord = new ClaudeCodeUserCreditCardRecord();
  697. creditCardRecord.setUserId(userId);
  698. creditCardRecord.setBeforeCodePoints(beforeCodePoints);
  699. creditCardRecord.setAddCodePoints(addCodePoints);
  700. creditCardRecord.setAfterCodePoints(afterCodePoints);
  701. creditCardRecord.setCardNumber(cardNumber);
  702. //使用一次
  703. creditCardRecord.setNum(-1);
  704. creditCardRecord.setUseReason("使用补充卡");
  705. claudeCodeUserCreditCardRecordMapper.insert(creditCardRecord);
  706. log.info("用户{}成功使用补充卡{},增加{}预算", userId, cardNumber, addCodePoints);
  707. //调用API调整用户积分
  708. adjClaudeCodeUserPoints(claudeCodeUser.getUserId(), creditCard.getCreditAmount(), "adjustment", "用户使用补充卡");
  709. } else {
  710. throw new BusinessRuntimeException("补充卡使用失败,请重试");
  711. }
  712. }
  713. @Override
  714. public SearchResult<ClaudeCodeUserResetRecordView> getUserResetRecords(Long userId, Integer page, Integer limit) {
  715. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  716. return beanSearcher.search(ClaudeCodeUserResetRecordView.class, MapUtils.builder()
  717. .field(ClaudeCodeUserResetRecordView::getUserId, userIdList).op(Operator.InList)
  718. .page(page, limit)
  719. .orderBy(ClaudeCodeUserResetRecordView::getCreatedTime).desc()
  720. .build());
  721. }
  722. @Override
  723. public SearchResult<ClaudeCodeUserCreditCardRecordView> getUserCreditCardRecords(Long userId, Integer page, Integer limit) {
  724. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  725. return beanSearcher.search(ClaudeCodeUserCreditCardRecordView.class, MapUtils.builder()
  726. .field(ClaudeCodeUserCreditCardRecordView::getUserId, userIdList).op(Operator.InList)
  727. .page(page, limit)
  728. .orderBy(ClaudeCodeUserCreditCardRecordView::getCreatedTime).desc()
  729. .build());
  730. }
  731. @Override
  732. public ClaudeCodeRewardDetailsResp getUserRewardDetails(Long userId, Boolean general) {
  733. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  734. ClaudeCodeRewardDetailsResp response = new ClaudeCodeRewardDetailsResp();
  735. // 获取用户Claude Code信息
  736. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(
  737. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  738. .in(ClaudeCodeUser::getUserId, userIdList)
  739. .last("limit 1")
  740. );
  741. // 设置可重置次数
  742. if (claudeCodeUser != null) {
  743. Integer resetCount = claudeCodeUser.getResetCount();
  744. response.setResetCount(resetCount != null ? resetCount : 0);
  745. } else {
  746. response.setResetCount(0);
  747. }
  748. // 获取可用补充卡数量
  749. Integer availableCreditCards = claudeCodeCreditCardMapper.selectCount(
  750. Wrappers.lambdaQuery(ClaudeCodeCreditCard.class)
  751. .eq(ClaudeCodeCreditCard::getStatus, 0)
  752. .in(ClaudeCodeCreditCard::getUserId, userIdList)
  753. .isNull(ClaudeCodeCreditCard::getUsedUserId)
  754. );
  755. response.setAvailableCreditCards(availableCreditCards != null ? availableCreditCards : 0);
  756. //是否是普通推广
  757. if (general != null && general) {
  758. //提现门槛
  759. DistributePointsExchangeMoney distributePointsExchangeMoney = distributePointsExchangeMoneyMapper.selectOne(Wrappers.lambdaQuery(DistributePointsExchangeMoney.class).last("limit 1"));
  760. response.setThresholdMoney(BigDecimal.valueOf(distributePointsExchangeMoney.getMinPoints()).divide(BigDecimal.valueOf(distributePointsExchangeMoney.getPoints())).multiply(distributePointsExchangeMoney.getMoney()));
  761. // 获取分销相关数据
  762. try {
  763. User user = userMapper.selectById(userId);
  764. List<DistributeWaitingSendPoints> distributeWaitingSendPoints = distributeWaitingSendPointsMapper.selectList(Wrappers.lambdaQuery(DistributeWaitingSendPoints.class).eq(DistributeWaitingSendPoints::getSendStatus, DistributeWaitingSendPoints.Status.waiting).eq(DistributeWaitingSendPoints::getSharedId, userId).select(DistributeWaitingSendPoints::getPoints));
  765. BigDecimal waitingPoints = BigDecimal.ZERO;
  766. if (!distributeWaitingSendPoints.isEmpty()) {
  767. Optional<BigDecimal> points = distributeWaitingSendPoints.stream().map(DistributeWaitingSendPoints::getPoints).reduce(BigDecimal::add);
  768. if (points.isPresent()) {
  769. waitingPoints = points.get();
  770. }
  771. }
  772. GoodsDon goodsDon = goodsDonMapper.selectById(Constant.CLAUDE_CODE_GOODS_ID);
  773. //cc 分销平台比例
  774. if (goodsDon != null) {
  775. response.setRate(goodsDon.getRate());
  776. }
  777. if (user.getPoints().compareTo(BigDecimal.ZERO) <= 0 && waitingPoints.compareTo(BigDecimal.ZERO) <= 0) {
  778. response.setGotMoney(BigDecimal.ZERO);
  779. response.setWaitingMoney(BigDecimal.ZERO);
  780. } else {
  781. BigDecimal points = BigDecimal.valueOf(distributePointsExchangeMoney.getPoints());
  782. BigDecimal money = distributePointsExchangeMoney.getMoney();
  783. BigDecimal oneHundred = BigDecimal.valueOf(100);
  784. if (user.getPoints().compareTo(BigDecimal.ZERO) >= 0) {
  785. response.setGotMoney(user.getPoints().divide(points).multiply(money).divide(oneHundred, 2, RoundingMode.HALF_DOWN));
  786. }
  787. if (user.getPoints().compareTo(BigDecimal.ZERO) >= 0) {
  788. response.setWaitingMoney(waitingPoints.divide(points).multiply(money).divide(oneHundred, 2, RoundingMode.DOWN));
  789. }
  790. }
  791. } catch (Exception e) {
  792. log.warn("获取用户{}分销数据异常", userId, e);
  793. // 设置默认值
  794. response.setGotMoney(BigDecimal.ZERO);
  795. response.setWaitingMoney(BigDecimal.ZERO);
  796. }
  797. }
  798. return response;
  799. }
  800. @Override
  801. public void sendCreditCard(Long orderId, Long userId, Integer points, String cardNumber, String remark) {
  802. //暂时不送积分卡
  803. if (true) {
  804. return;
  805. }
  806. ClaudeCodeCreditCard creditCard = new ClaudeCodeCreditCard();
  807. creditCard.setUserId(userId);
  808. creditCard.setOrderId(orderId);
  809. creditCard.setCardNumber(cardNumber);
  810. creditCard.setCreditAmount(points);
  811. creditCard.setRemark(remark);
  812. claudeCodeCreditCardMapper.insert(creditCard);
  813. //补充卡明细
  814. ClaudeCodeUserCreditCardRecord creditCardRecord = new ClaudeCodeUserCreditCardRecord();
  815. creditCardRecord.setUserId(userId);
  816. creditCardRecord.setCardNumber(cardNumber);
  817. creditCardRecord.setUseReason(remark);
  818. creditCardRecord.setNum(1);
  819. creditCardRecord.setAddCodePoints(BigDecimal.valueOf(points));
  820. claudeCodeUserCreditCardRecordMapper.insert(creditCardRecord);
  821. }
  822. @Override
  823. public void adjUserRestNum(Long orderId, Long userId, Integer num, String remark) {
  824. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  825. // 获取用户Claude Code信息
  826. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(
  827. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  828. .in(ClaudeCodeUser::getUserId, userIdList)
  829. .last("limit 1")
  830. );
  831. if (claudeCodeUser == null) {
  832. log.info("claude code user不存在");
  833. return;
  834. }
  835. Integer resetCount = claudeCodeUser.getResetCount();
  836. claudeCodeUser.setResetCount(resetCount + num);
  837. if (claudeCodeUser.getResetCount() < 0) {
  838. return;
  839. }
  840. int update = 0;
  841. while (update == 0) {
  842. update = claudeCodeUserMapper.update(claudeCodeUser, Wrappers.lambdaUpdate(ClaudeCodeUser.class).eq(ClaudeCodeUser::getUserId, claudeCodeUser.getUserId()).eq(ClaudeCodeUser::getResetCount, resetCount));
  843. if (update == 0) {
  844. claudeCodeUser = claudeCodeUserMapper.selectOne(
  845. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  846. .in(ClaudeCodeUser::getUserId, userIdList)
  847. .last("limit 1")
  848. );
  849. if (claudeCodeUser == null) {
  850. break;
  851. }
  852. resetCount = claudeCodeUser.getResetCount();
  853. claudeCodeUser.setResetCount(resetCount + num);
  854. if (claudeCodeUser.getResetCount() < 0) {
  855. break;
  856. }
  857. }
  858. //记录获取重置次数记录
  859. ClaudeCodeUserResetRecord resetRecord = new ClaudeCodeUserResetRecord();
  860. resetRecord.setOrderId(orderId);
  861. resetRecord.setUserId(userId);
  862. resetRecord.setResetReason(remark);
  863. resetRecord.setNum(num);
  864. claudeCodeUserResetRecordMapper.insert(resetRecord);
  865. }
  866. }
  867. @Override
  868. @Transactional(rollbackFor = Throwable.class)
  869. public void useDayResetCount(Long userId) throws Exception {
  870. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  871. // 获取用户Claude Code信息
  872. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(
  873. Wrappers.lambdaQuery(ClaudeCodeUser.class)
  874. .in(ClaudeCodeUser::getUserId, userIdList)
  875. .last("limit 1")
  876. );
  877. if (claudeCodeUser == null) {
  878. log.info("claude code user不存在");
  879. return;
  880. }
  881. //非标准版、专业版 不给使用
  882. if (claudeCodeUser.getClaudeDailyLimit() < 5000) {
  883. return;
  884. }
  885. Integer selectCount = claudeCodeDayResetRecordMapper.selectCount(Wrappers.lambdaQuery(ClaudeCodeDayResetRecord.class).in(ClaudeCodeDayResetRecord::getUserId, userIdList).gt(ClaudeCodeDayResetRecord::getCreatedTime, DateUtil.beginOfDay(DateTime.now())));
  886. //今日使用为0
  887. if (selectCount == 0) {
  888. String idxKey = "cc:uid:" + userId;
  889. try {
  890. if (redisService.setNx(idxKey, userId, 5l)) {
  891. //重置用户次数
  892. CreditResetReq creditResetReq = new CreditResetReq();
  893. creditResetReq.setDescription("用户使用每日重置");
  894. //调用API重置用户次数
  895. resetUserCredits(claudeCodeUser.getUserId(), creditResetReq);
  896. ClaudeCodeDayResetRecord claudeCodeDayResetRecord = new ClaudeCodeDayResetRecord();
  897. claudeCodeDayResetRecord.setUserId(userId);
  898. claudeCodeDayResetRecord.setRelationId(claudeCodeUser.getRelationId());
  899. claudeCodeDayResetRecordMapper.insert(claudeCodeDayResetRecord);
  900. }
  901. } finally {
  902. if (redisService.hasKey(idxKey)) {
  903. redisService.del(idxKey);
  904. }
  905. }
  906. }
  907. }
  908. @Override
  909. public Boolean getUserSubs(long userId) {
  910. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  911. //是否有该车票
  912. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  913. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  914. .field(GroupsRelationMirrorView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  915. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  916. .build());
  917. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  918. //车票不存在
  919. if (groupsRelationMirrorView == null) {
  920. //套餐是否存在
  921. if (claudeCodeUser != null) {
  922. //清除用户套餐
  923. if (delUserPackages(claudeCodeUser.getUserId())) {
  924. claudeCodeUserMapper.deleteById(claudeCodeUser.getId());
  925. }
  926. }
  927. return false;
  928. }
  929. return true;
  930. }
  931. @Override
  932. public ClaudeCodeDeductDto getDeductSkuMoney(Long userId, Long relationId, Long skuId) {
  933. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  934. GroupsRelationView relationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()
  935. .field(GroupsRelationView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  936. .field(GroupsRelationView::getId, relationId)
  937. .field(GroupsRelationView::getUserId, userIdList).op(Operator.InList)
  938. .field(GroupsRelationView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  939. .build());
  940. ClaudeCodeDeductDto claudeCodeDeductDto = new ClaudeCodeDeductDto();
  941. if (relationView == null) {
  942. claudeCodeDeductDto.setDeductMoney(BigDecimal.ZERO);
  943. return claudeCodeDeductDto;
  944. }
  945. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
  946. GoodsDonSku upgradeSku = skuMapper.selectById(skuId);
  947. if (upgradeSku.getGoodsId() != Constant.CLAUDE_CODE_GOODS_ID) {
  948. claudeCodeDeductDto.setDeductMoney(BigDecimal.ZERO);
  949. return claudeCodeDeductDto;
  950. }
  951. GoodsDonSku relationPresentSku = null;
  952. Long renewSkuId = claudeCodeUser.getRenewSkuId();
  953. if (renewSkuId != null) {
  954. relationPresentSku = skuMapper.selectById(renewSkuId);
  955. }
  956. if (relationPresentSku == null) {
  957. relationPresentSku = skuMapper.selectById(relationView.getSkuId());
  958. }
  959. if (upgradeSku.getClaudeQuota().intValue() < relationPresentSku.getClaudeQuota().intValue()) {
  960. claudeCodeDeductDto.setDeductMoney(BigDecimal.ZERO);
  961. return claudeCodeDeductDto;
  962. }
  963. claudeCodeDeductDto = getDeductMoney(relationId, relationView.getSkuId(), claudeCodeUser.getRenewSkuId(), claudeCodeUser.getRenewOrderId(), claudeCodeUser.getRenewExpiryTime(), relationView.getStartTime(), relationView.getExpiryTime(), DateTime.now(), userIdList, upgradeSku);
  964. return claudeCodeDeductDto;
  965. }
  966. @Override
  967. public ClaudeCodeDeductDto getDeductMoney(Long relationId, Long relationSkuId, Long renewSkuId, Long renewOrderId, Date renewExpiryTime, Date relationStartTime, Date relationExpiryTime, DateTime now, List<Long> userIdList, GoodsDonSku upgradeSku) {
  968. BigDecimal deductMoney = BigDecimal.ZERO;
  969. //一个月当30天算单价 往上取整
  970. //续费规格
  971. ClaudeCodeDeductDto claudeCodeDeductDto = new ClaudeCodeDeductDto();
  972. if (renewSkuId != null && renewExpiryTime.compareTo(now) > 0) {
  973. OrderDon renewOrderDon = orderDonMapper.selectById(renewOrderId);
  974. BigDecimal orderMoney = renewOrderDon.getMoney().add(Optional.ofNullable(renewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
  975. GoodsDonSku presentSku = skuMapper.selectById(renewSkuId);
  976. if (orderMoney.compareTo(BigDecimal.ZERO) == 0) {
  977. //存在升级后的0元订单 即兑换码订单 不让抵扣
  978. claudeCodeDeductDto.setDeductMoney(BigDecimal.ZERO);
  979. return claudeCodeDeductDto;
  980. } else {
  981. Long betweenDay = DateUtil.betweenDay(now, renewExpiryTime, false) + 1;
  982. //当天购买的按规格原价抵扣
  983. BigDecimal renewDeductMoney;
  984. if (now.toDateStr().equals(DateUtil.format(renewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  985. renewDeductMoney = orderMoney;
  986. deductMoney = deductMoney.add(renewDeductMoney);
  987. if (presentSku.getDays() != null) {
  988. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -presentSku.getDays());
  989. } else {
  990. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -presentSku.getMonths());
  991. }
  992. } else {
  993. //此次续费规格单价
  994. BigDecimal single = getPayOrderSingle(orderMoney, presentSku);
  995. //该车票抵扣金额
  996. renewDeductMoney = single.multiply(BigDecimal.valueOf(betweenDay));
  997. log.info("车票id:{}目前续费升级规格可抵扣的金额为:{}", relationId, renewDeductMoney);
  998. deductMoney = deductMoney.add(renewDeductMoney);
  999. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -betweenDay.intValue());
  1000. }
  1001. }
  1002. //上一个续费升级规格
  1003. List<OrderDon> orderDOns = claudeCodeUserRenewExpiryRecordMapper.getOtherRenewSkuOrders(renewOrderId, relationId, userIdList);
  1004. OrderDon presentRenewOrderDon = renewOrderDon;
  1005. for (OrderDon thisRenewOrderDon : orderDOns) {
  1006. Long skuId = thisRenewOrderDon.getSkuId();
  1007. GoodsDonSku thisRenewSku = skuMapper.selectById(skuId);
  1008. BigDecimal thisOrderMoney = thisRenewOrderDon.getMoney().add(Optional.ofNullable(thisRenewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
  1009. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  1010. //存在升级后的0元订单 即兑换码订单 不让抵扣
  1011. claudeCodeDeductDto.setDeductMoney(BigDecimal.ZERO);
  1012. return claudeCodeDeductDto;
  1013. }
  1014. //当天购买的按规格原价抵扣
  1015. BigDecimal thisRenewDeductMoney = BigDecimal.ZERO;
  1016. if (now.toDateStr().equals(DateUtil.format(thisRenewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  1017. thisRenewDeductMoney = thisOrderMoney;
  1018. deductMoney = deductMoney.add(thisRenewDeductMoney);
  1019. if (thisRenewSku.getDays() != null) {
  1020. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisRenewSku.getDays());
  1021. } else {
  1022. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -thisRenewSku.getMonths());
  1023. }
  1024. } else {
  1025. //此次续费订单过期时间
  1026. Date thisOrderExpiryTime;
  1027. if (thisRenewSku.getDays() != null) {
  1028. thisOrderExpiryTime = DateUtil.offsetDay(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getDays());
  1029. } else {
  1030. thisOrderExpiryTime = DateUtil.offsetMonth(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getMonths());
  1031. }
  1032. //续费同规格不处理
  1033. if (!ObjectUtil.equal(thisRenewOrderDon.getSkuId(), presentRenewOrderDon.getSkuId())) {
  1034. //先扣除已使用的天数
  1035. //相差多少天
  1036. Long thisRenewBetDay = DateUtil.betweenDay(thisRenewOrderDon.getCreatedTime(), presentRenewOrderDon.getCreatedTime(), false);
  1037. //扣除已使用的
  1038. thisOrderExpiryTime = DateUtil.offsetDay(thisOrderExpiryTime, -thisRenewBetDay.intValue());
  1039. }
  1040. //此订单过期时间
  1041. if (thisOrderExpiryTime.compareTo(now) > 0) {
  1042. //距离过期还剩多少天
  1043. Long thisDeductBetweenDay = DateUtil.betweenDay(now, thisOrderExpiryTime, false) + 1;
  1044. if (thisDeductBetweenDay != 0) {
  1045. //此次续费规格单价
  1046. BigDecimal single = getPayOrderSingle(thisOrderMoney, thisRenewSku);
  1047. //该车票抵扣金额
  1048. thisRenewDeductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
  1049. deductMoney = deductMoney.add(thisRenewDeductMoney);
  1050. }
  1051. //目前级别续费订单
  1052. presentRenewOrderDon = thisRenewOrderDon;
  1053. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisDeductBetweenDay.intValue());
  1054. }
  1055. }
  1056. log.info("车票id:{}目前次一级规格:{}可抵扣的金额为:{}", relationId, thisRenewSku.getSpecVal(), thisRenewDeductMoney);
  1057. }
  1058. }
  1059. if (relationExpiryTime.after(now)) {
  1060. //该车票抵扣金额
  1061. BigDecimal deductMoneyFromTicket = getDeductMoneyFromTicket(relationId, userIdList, relationSkuId, now, relationStartTime, relationExpiryTime);
  1062. deductMoney = deductMoney.add(deductMoneyFromTicket);
  1063. }
  1064. log.info("车票id:{}最终到期时间为:{}车票可返回 抵扣金额:{}", relationId, DateUtil.format(relationExpiryTime, "yyyy-MM-dd HH:mm:ss"), deductMoney);
  1065. //超出可抵扣的金额 直接返回
  1066. if (deductMoney.compareTo(upgradeSku.getPrice()) >= 0) {
  1067. BigDecimal overMoney = deductMoney.subtract(upgradeSku.getPrice());
  1068. //超过抵扣金额可换延长会员多少天
  1069. //每天单价
  1070. BigDecimal daySingle = upgradeSku.getPrice().divide(BigDecimal.valueOf(upgradeSku.getMonths())).divide(BigDecimal.valueOf(30), 0, RoundingMode.DOWN);
  1071. //超出部分可换取多少天 低于每天单价为0天
  1072. BigDecimal extendsDays = overMoney.divide(daySingle, 0, RoundingMode.UP);
  1073. claudeCodeDeductDto.setExtendsDays(extendsDays.intValue());
  1074. }
  1075. claudeCodeDeductDto.setDeductMoney(deductMoney);
  1076. return claudeCodeDeductDto;
  1077. }
  1078. private void adjClaudeCodeUserPoints(Long userId, Integer points, String type, String description) throws Exception {
  1079. String apiKeyUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/claude/users/%s/limit", userId);
  1080. Map<String, Object> parmas = new HashMap<>();
  1081. parmas.put("amount", points);
  1082. parmas.put("type", type);
  1083. parmas.put("description", description);
  1084. ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(apiKeyUrl, Jsons.toJson(parmas));
  1085. if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
  1086. throw BusinessRuntimeException.getInstance("使用补充卡失败");
  1087. }
  1088. }
  1089. /**
  1090. * 获取订单单价
  1091. */
  1092. public BigDecimal getPayOrderSingle(BigDecimal orderMoney, GoodsDonSku sku) {
  1093. //此次续费规格单价
  1094. BigDecimal single;
  1095. //区分天/月
  1096. if (sku.getDays() != null) {
  1097. single = orderMoney.divide(BigDecimal.valueOf(sku.getDays()), 0, RoundingMode.HALF_UP);
  1098. } else {
  1099. single = orderMoney.divide(BigDecimal.valueOf(sku.getMonths()).multiply(BigDecimal.valueOf(30)), 0, RoundingMode.HALF_UP);
  1100. }
  1101. return single;
  1102. }
  1103. /**
  1104. * 获取车票可抵扣金额
  1105. */
  1106. public BigDecimal getDeductMoneyFromTicket(Long relationId, List<Long> userIdList, Long skuId, DateTime now, Date relationStartTime, Date relationExpiryTime) {
  1107. List<OrderDon> orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
  1108. .eq(OrderDon::getRelationId, relationId)
  1109. .in(OrderDon::getUserId, userIdList)
  1110. .eq(OrderDon::getSkuId, skuId)
  1111. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  1112. .orderByDesc(OrderDon::getId));
  1113. //部分退款
  1114. if (orderDonList.isEmpty()) {
  1115. orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
  1116. .eq(OrderDon::getRelationId, relationId)
  1117. .in(OrderDon::getUserId, userIdList)
  1118. .eq(OrderDon::getSkuId, skuId)
  1119. .notIn(OrderDon::getStatus, Constant.noOrderStatus)
  1120. .orderByDesc(OrderDon::getId));
  1121. }
  1122. //未有订单 返回抵扣金额0
  1123. if (orderDonList.isEmpty()) {
  1124. return BigDecimal.ZERO;
  1125. }
  1126. //原车票订单总实付金额
  1127. BigDecimal totalOrderMoney = BigDecimal.ZERO;
  1128. //原车票订单总天数
  1129. Integer totalDays = 0;
  1130. for (OrderDon orderDon : orderDonList) {
  1131. GoodsDonSku sku = skuMapper.selectById(skuId);
  1132. BigDecimal thisOrderMoney = orderDon.getMoney().add(Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO)).subtract(Optional.ofNullable(orderDon.getRefundMoney()).orElse(BigDecimal.ZERO));
  1133. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  1134. if (sku.getDays() != null) {
  1135. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -sku.getDays());
  1136. } else {
  1137. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -sku.getMonths());
  1138. }
  1139. continue;
  1140. }
  1141. totalOrderMoney = totalOrderMoney.add(thisOrderMoney);
  1142. //总时间
  1143. if (sku.getDays() != null) {
  1144. totalDays += sku.getDays();
  1145. } else {
  1146. totalDays += sku.getMonths() * 30;
  1147. }
  1148. ClaudeCodeDeductOrderRelate claudeCodeDeductOrderRelate = claudeCodeDeductOrderRelateMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeDeductOrderRelate.class).eq(ClaudeCodeDeductOrderRelate::getOrderId, orderDon.getId()).last("limit 1"));
  1149. if (claudeCodeDeductOrderRelate != null) {
  1150. totalOrderMoney = totalOrderMoney.add(claudeCodeDeductOrderRelate.getDeductMoney());
  1151. }
  1152. //原订单数
  1153. if (orderDonList.size() == 1) {
  1154. //此次订单过期时间
  1155. Date thisOrderExpiryTime;
  1156. if (sku.getDays() != null) {
  1157. thisOrderExpiryTime = DateUtil.offsetDay(orderDon.getCreatedTime(), sku.getDays());
  1158. } else {
  1159. thisOrderExpiryTime = DateUtil.offsetMonth(orderDon.getCreatedTime(), sku.getMonths());
  1160. }
  1161. //当天购买的按规格原价抵扣
  1162. if (now.toDateStr().equals(DateUtil.format(relationStartTime, "yyyy-MM-dd")) && (DateUtil.betweenDay(thisOrderExpiryTime, relationExpiryTime, true) == 0)) {
  1163. return totalOrderMoney;
  1164. }
  1165. }
  1166. }
  1167. //总天数为0 不抵扣
  1168. if (totalDays == 0) {
  1169. return BigDecimal.ZERO;
  1170. }
  1171. Long thisDeductBetweenDay = DateUtil.betweenDay(now, relationExpiryTime, false) + 1;
  1172. BigDecimal single = totalOrderMoney.divide(BigDecimal.valueOf(totalDays), 0, RoundingMode.HALF_UP);
  1173. //该车票抵扣金额
  1174. BigDecimal deductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
  1175. if (deductMoney.compareTo(totalOrderMoney) > 0) {
  1176. deductMoney = totalOrderMoney;
  1177. }
  1178. return deductMoney;
  1179. }
  1180. }