ClaudeCodeServiceImpl.java 52 KB

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