CodexServiceImpl.java 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. package com.cyksj.service.codex.impl;
  2. import cn.hutool.core.date.DateTime;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.util.ObjectUtil;
  5. import cn.hutool.http.HttpResponse;
  6. import cn.hutool.http.HttpUtil;
  7. import cn.hutool.http.Method;
  8. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  9. import com.cyksj.common.constant.Constant;
  10. import com.cyksj.common.exception.BusinessRuntimeException;
  11. import com.cyksj.common.snowflake.Sequence;
  12. import com.cyksj.common.util.Jsons;
  13. import com.cyksj.common.util.StringUtil;
  14. import com.cyksj.dto.RedisKey;
  15. import com.cyksj.mapper.*;
  16. import com.cyksj.mapper.codex.CodexDeductRecordMapper;
  17. import com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper;
  18. import com.cyksj.model.entity.*;
  19. import com.cyksj.model.manage.views.GroupsRelationView;
  20. import com.cyksj.model.request.CodexUpgradePayReq;
  21. import com.cyksj.model.request.UpdateDailyLimitRequest;
  22. import com.cyksj.model.request.codex.CodexUserPackageReq;
  23. import com.cyksj.model.response.CodexDeductResp;
  24. import com.cyksj.model.response.claudecode.ClaudeCodeResp;
  25. import com.cyksj.model.views.CodexUserInfoView;
  26. import com.cyksj.redis.RedisService;
  27. import com.cyksj.service.codex.CodexService;
  28. import com.cyksj.service.groups.GroupsFuncService;
  29. import com.cyksj.service.user.UserBindRelationService;
  30. import com.ejlchina.searcher.BeanSearcher;
  31. import com.ejlchina.searcher.param.Operator;
  32. import com.ejlchina.searcher.util.MapUtils;
  33. import com.github.rholder.retry.Retryer;
  34. import com.github.rholder.retry.RetryerBuilder;
  35. import com.github.rholder.retry.StopStrategies;
  36. import com.github.rholder.retry.WaitStrategies;
  37. import lombok.RequiredArgsConstructor;
  38. import lombok.extern.slf4j.Slf4j;
  39. import org.springframework.dao.DuplicateKeyException;
  40. import org.springframework.stereotype.Service;
  41. import org.springframework.transaction.annotation.Transactional;
  42. import java.math.BigDecimal;
  43. import java.math.RoundingMode;
  44. import java.util.*;
  45. import java.util.concurrent.ExecutionException;
  46. import java.util.concurrent.TimeUnit;
  47. /**
  48. * 项目名: yhlxj11111111
  49. * 文件名: CodexServiceImpl
  50. * 创建者: Claude
  51. * 创建时间:2025/9/15
  52. */
  53. @Slf4j
  54. @Service
  55. @RequiredArgsConstructor
  56. public class CodexServiceImpl implements CodexService {
  57. private final CodexUserMapper codexUserMapper;
  58. private final ClaudeCodeUserMapper claudeCodeUserMapper;
  59. private final UserBindRelationService userBindRelationService;
  60. private final GoodsDonSkuMapper skuMapper;
  61. private final GroupsRelationMapper relationMapper;
  62. private final GroupsMapper groupsMapper;
  63. private final CodexUserRenewExpiryRecordMapper codexUserRenewExpiryRecordMapper;
  64. private final BeanSearcher beanSearcher;
  65. private final OrderDonMapper orderDonMapper;
  66. private final GroupsFuncService groupsFuncService;
  67. private final RedisService redisService;
  68. private final CodexDeductRecordMapper codexDeductRecordMapper;
  69. private static final Sequence SEQUENCE = new Sequence(0);
  70. @Override
  71. public CodexUserInfoView getCodexUserInfo(Long userId) {
  72. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  73. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  74. // 查询Claude Code信息
  75. CodexUserInfoView.ClaudeCodeInfo claudeCodeInfo = getClaudeCodeInfo(userId);
  76. if (codexUser == null) {
  77. //是否有claude code
  78. if (claudeCodeInfo != null && claudeCodeInfo.getClaudeDailyLimit() != null) {
  79. GoodsDonSku sku = null;
  80. if (claudeCodeInfo.getRenewSkuId() != null) {
  81. sku = skuMapper.selectById(claudeCodeInfo.getRenewSkuId());
  82. }
  83. //续费升级规格不存在 或者 不赠送codex
  84. if (sku == null || !sku.getIsCodex()) {
  85. GroupsRelation relation = relationMapper.selectById(claudeCodeInfo.getRelationId());
  86. GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
  87. sku = skuMapper.selectById(groupsTrips.getSkuId());
  88. }
  89. //sku 存在并且开启赠送codex
  90. if (sku != null && sku.getIsCodex()) {
  91. CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
  92. .userId(claudeCodeInfo.getUserId())
  93. .planName(sku.getCodexPlanName())
  94. .openaiDailyLimit(sku.getOpenaiDailyLimit())
  95. .openaiQuota(sku.getOpenaiQuota())
  96. .expiryTime(claudeCodeInfo.getExpiryTime())
  97. .build();
  98. createOrUpdateUserPackage(codexPackageReq);
  99. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  100. }
  101. }
  102. if (codexUser == null) {
  103. return null;
  104. }
  105. }
  106. return CodexUserInfoView.builder().planName(codexUser.getPlanName()).openaiDailyLimit(codexUser.getOpenaiDailyLimit()).openaiQuota(codexUser.getOpenaiQuota()).relationId(codexUser.getRelationId()).claudeCodeInfo(claudeCodeInfo).build();
  107. }
  108. private CodexUserInfoView.ClaudeCodeInfo getClaudeCodeInfo(Long userId) {
  109. // 获取用户关联的ID列表
  110. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  111. ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
  112. if (claudeCodeUser == null) {
  113. return null;
  114. }
  115. return CodexUserInfoView.ClaudeCodeInfo.builder()
  116. .userId(claudeCodeUser.getUserId())
  117. .planName(claudeCodeUser.getPlanName())
  118. .relationId(claudeCodeUser.getRelationId())
  119. .claudeDailyLimit(claudeCodeUser.getClaudeDailyLimit())
  120. .claudeQuota(claudeCodeUser.getClaudeQuota())
  121. .expiryTime(claudeCodeUser.getExpiryTime())
  122. .renewSkuId(claudeCodeUser.getRenewSkuId())
  123. .renewExpiryTime(claudeCodeUser.getRenewExpiryTime()).build();
  124. }
  125. @Override
  126. public void createOrUpdateUserPackage(CodexUserPackageReq req) {
  127. Long userId = req.getUserId();
  128. // 构建完整的套餐参数
  129. String planName = req.getPlanName();
  130. Date expiryTime = req.getExpiryTime();
  131. Integer openaiDailyLimit = req.getOpenaiDailyLimit();
  132. Integer openaiQuota = req.getOpenaiQuota();
  133. Long relationId = req.getRelationId();
  134. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  135. CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser());
  136. existingCodexUser.setPlanName(planName);
  137. existingCodexUser.setRelationId(relationId);
  138. existingCodexUser.setUserId(userId);
  139. existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit);
  140. existingCodexUser.setOpenaiQuota(openaiQuota);
  141. existingCodexUser.setExpiryTime(expiryTime);
  142. if (existingCodexUser.getId() == null) {
  143. try {
  144. codexUserMapper.insert(existingCodexUser);
  145. } catch (DuplicateKeyException e) {
  146. }
  147. return;
  148. }
  149. codexUserMapper.updateById(existingCodexUser);
  150. }
  151. /**
  152. * 创建或更新Codex用户套餐
  153. */
  154. @Override
  155. public ClaudeCodeResp createOrUpdateCodexUserPackageApi(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
  156. Map<String, Object> params = new HashMap<>();
  157. params.put("plan_name", planName.replaceAll(";", ""));
  158. params.put("openai_daily_limit", openaiDailyLimit);
  159. params.put("openai_quota", openaiQuota);
  160. params.put("expire_time", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
  161. String planUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
  162. ClaudeCodeResp codexResp = executeCodexPostApi(planUrl, Jsons.toJson(params));
  163. if (!"success".equals(codexResp.getMessage())) {
  164. log.info("用户codex用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, codexResp.getMessage());
  165. throw BusinessRuntimeException.getInstance("调用codex 套餐接口失败");
  166. }
  167. return codexResp;
  168. }
  169. @Override
  170. public CodexDeductResp getDeductMoney(Long userId, Long relationId) {
  171. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  172. CodexDeductResp codexDeductResp = new CodexDeductResp();
  173. Integer selectCount = claudeCodeUserMapper.selectCount(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList));
  174. if (selectCount > 0) {
  175. codexDeductResp.setIsDeduct(Boolean.FALSE);
  176. return codexDeductResp;
  177. }
  178. GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()
  179. .field(GroupsRelationView::getId, relationId)
  180. .field(GroupsRelationView::getGoodsId, Constant.CODEX_GOODS_ID)
  181. .field(GroupsRelationView::getUserId, userIdList).op(Operator.InList)
  182. .field(GroupsRelationView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  183. .build());
  184. BigDecimal deductMoney;
  185. if (groupsRelationView == null || groupsRelationView.getExpiryTime().before(DateTime.now())) {
  186. deductMoney = BigDecimal.ZERO;
  187. }
  188. CodexUser codexUser = codexUserMapper.selectById(relationId);
  189. deductMoney = getFinalDeductMoney(relationId, groupsRelationView.getSkuId(), codexUser.getRenewSkuId(), codexUser.getRenewOrderId(), codexUser.getRenewExpiryTime(), groupsRelationView.getStartTime(), groupsRelationView.getExpiryTime(), DateTime.now(), userIdList);
  190. codexDeductResp.setDeductMoney(deductMoney);
  191. return codexDeductResp;
  192. }
  193. @Override
  194. @Transactional(rollbackFor = Throwable.class)
  195. public OrderDon deductSubmitOrder(CodexUpgradePayReq payReq) {
  196. Long userId = payReq.getUserId();
  197. //codex 车票id
  198. Long relationId = payReq.getRelationId();
  199. CodexDeductResp codexDeductResp = getDeductMoney(userId, relationId);
  200. if (!codexDeductResp.getIsDeduct()) {
  201. throw BusinessRuntimeException.getInstance("你存在claude code 无法抵扣");
  202. }
  203. //抵扣金额
  204. BigDecimal deductMoney = codexDeductResp.getDeductMoney();
  205. GoodsDonSku sku = skuMapper.selectById(payReq.getSkuId());
  206. if (sku == null || sku.getGoodsId() != Constant.CLAUDE_CODE_GOODS_ID || !sku.getIsCodex()) {
  207. throw BusinessRuntimeException.getInstance("系统异常,请刷新页面重试");
  208. }
  209. int noPayCount = orderDonMapper.selectCount(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getGoodsId, sku.getGoodsId())
  210. .eq(OrderDon::getStatus, OrderDon.Status.noPayment.toString()));
  211. if (noPayCount > 0) {
  212. throw new BusinessRuntimeException("您有未支付订单.");
  213. }
  214. String cacheKye = "codex_deduct_cache_key:" + relationId;
  215. boolean b = redisService.setNx(cacheKye, relationId, 10L);
  216. if (!b) {
  217. throw BusinessRuntimeException.getInstance("系统繁忙,请刷新页面重试");
  218. }
  219. OrderDon orderDon;
  220. try {
  221. orderDon = new OrderDon();
  222. orderDon.setGoodsId(sku.getGoodsId());
  223. orderDon.setSkuId(sku.getId());
  224. orderDon.setUserId(userId);
  225. orderDon.setMoney(sku.getPrice().subtract(deductMoney));
  226. /* 生成订单 */
  227. String donNo = SEQUENCE.nextId().toString();
  228. orderDon.setOrderNo(donNo);
  229. //锁一个cc的车票id
  230. GroupsRelation relation = getClaudeCodeRelation(userId, sku);
  231. orderDon.setRelationId(relation.getId());
  232. orderDonMapper.insert(orderDon);
  233. //记录抵扣记录
  234. CodexDeductRecord codexDeductRecord = new CodexDeductRecord();
  235. codexDeductRecord.setOrderId(orderDon.getId());
  236. codexDeductRecord.setRelationId(relationId);
  237. codexDeductRecordMapper.insert(codexDeductRecord);
  238. } finally {
  239. if (redisService.hasKey(cacheKye)) {
  240. redisService.del(cacheKye);
  241. }
  242. }
  243. return orderDon;
  244. }
  245. private GroupsRelation getClaudeCodeRelation(Long userId, GoodsDonSku sku) {
  246. //获取车票车位
  247. GroupsRelation relation = getRelation(sku);
  248. //锁定座位
  249. setRelation(relation);
  250. //校验座位
  251. int update = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
  252. .set(GroupsRelation::getUserId, userId)
  253. .eq(GroupsRelation::getUserId, 0)
  254. .eq(GroupsRelation::getId, relation.getId()));
  255. if (update == 0) {
  256. throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
  257. }
  258. return relation;
  259. }
  260. private GroupsRelation getRelation(GoodsDonSku sku) {
  261. GroupsRelation relation = null;
  262. Integer maxNum = sku.getNum();
  263. GroupsTrips groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.validity, maxNum, null, null);
  264. if (groups == null) {
  265. //待发车
  266. groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.waiting, maxNum, null, null);
  267. }
  268. //新增空车队
  269. if (groups == null) {
  270. relation = groupsFuncService.createNewGroupsTrips(sku);
  271. }
  272. //获取车位
  273. if (relation == null) {
  274. //寻找快满编车队进行占座
  275. relation = relationMapper.selectRandomOneRelationByGroupsId(groups.getId());
  276. if (relation == null) {
  277. throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
  278. }
  279. }
  280. return relation;
  281. }
  282. public BigDecimal getFinalDeductMoney(Long relationId, Long relationSkuId, Long renewSkuId, Long renewOrderId, Date renewExpiryTime, Date relationStartTime, Date relationExpiryTime, DateTime now, List<Long> userIdList) {
  283. BigDecimal deductMoney = BigDecimal.ZERO;
  284. //一个月当30天算单价 往上取整
  285. //续费规格
  286. if (renewSkuId != null && renewExpiryTime.compareTo(now) > 0) {
  287. OrderDon renewOrderDon = orderDonMapper.selectById(renewOrderId);
  288. BigDecimal orderMoney = renewOrderDon.getMoney().add(Optional.ofNullable(renewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
  289. GoodsDonSku presentSku = skuMapper.selectById(renewSkuId);
  290. if (orderMoney.compareTo(BigDecimal.ZERO) == 0) {
  291. orderMoney = presentSku.getPrice();
  292. }
  293. Long betweenDay = DateUtil.betweenDay(now, renewExpiryTime, false) + 1;
  294. //当天购买的按规格原价抵扣
  295. BigDecimal renewDeductMoney;
  296. if (now.toDateStr().equals(DateUtil.format(renewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  297. renewDeductMoney = orderMoney;
  298. deductMoney = deductMoney.add(renewDeductMoney);
  299. if (presentSku.getDays() != null) {
  300. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -presentSku.getDays());
  301. } else {
  302. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -presentSku.getMonths());
  303. }
  304. } else {
  305. //此次续费规格单价
  306. BigDecimal single = getPayOrderSingle(orderMoney, presentSku);
  307. //该车票抵扣金额
  308. renewDeductMoney = single.multiply(BigDecimal.valueOf(betweenDay));
  309. log.info("codex车票id:{}目前续费升级规格可抵扣的金额为:{}", relationId, renewDeductMoney);
  310. deductMoney = deductMoney.add(renewDeductMoney);
  311. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -betweenDay.intValue());
  312. }
  313. //上一个续费升级规格
  314. List<OrderDon> orderDOns = codexUserRenewExpiryRecordMapper.getOtherRenewSkus(renewOrderId, relationId, userIdList);
  315. OrderDon presentRenewOrderDon = renewOrderDon;
  316. for (OrderDon thisRenewOrderDon : orderDOns) {
  317. Long skuId = thisRenewOrderDon.getSkuId();
  318. GoodsDonSku thisRenewSku = skuMapper.selectById(skuId);
  319. BigDecimal thisOrderMoney = thisRenewOrderDon.getMoney().add(Optional.ofNullable(thisRenewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
  320. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  321. thisOrderMoney = thisRenewSku.getPrice();
  322. }
  323. //当天购买的按规格原价抵扣
  324. BigDecimal thisRenewDeductMoney = BigDecimal.ZERO;
  325. if (now.toDateStr().equals(DateUtil.format(thisRenewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  326. thisRenewDeductMoney = thisOrderMoney;
  327. deductMoney = deductMoney.add(thisRenewDeductMoney);
  328. if (thisRenewSku.getDays() != null) {
  329. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisRenewSku.getDays());
  330. } else {
  331. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -thisRenewSku.getMonths());
  332. }
  333. } else {
  334. //此次续费订单过期时间
  335. Date thisOrderExpiryTime;
  336. if (thisRenewSku.getDays() != null) {
  337. thisOrderExpiryTime = DateUtil.offsetDay(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getDays());
  338. } else {
  339. thisOrderExpiryTime = DateUtil.offsetMonth(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getMonths());
  340. }
  341. //续费同规格不处理
  342. if (!ObjectUtil.equal(thisRenewOrderDon.getSkuId(), presentRenewOrderDon.getSkuId())) {
  343. //先扣除已使用的天数
  344. //相差多少天
  345. Long thisRenewBetDay = DateUtil.betweenDay(thisRenewOrderDon.getCreatedTime(), presentRenewOrderDon.getCreatedTime(), false);
  346. //扣除已使用的
  347. thisOrderExpiryTime = DateUtil.offsetDay(thisOrderExpiryTime, -thisRenewBetDay.intValue());
  348. }
  349. //此订单过期时间
  350. if (thisOrderExpiryTime.compareTo(now) > 0) {
  351. //距离过期还剩多少天
  352. Long thisDeductBetweenDay = DateUtil.betweenDay(now, thisOrderExpiryTime, false) + 1;
  353. if (thisDeductBetweenDay != 0) {
  354. //此次续费规格单价
  355. BigDecimal single = getPayOrderSingle(thisOrderMoney, thisRenewSku);
  356. //该车票抵扣金额
  357. thisRenewDeductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
  358. deductMoney = deductMoney.add(thisRenewDeductMoney);
  359. }
  360. //目前级别续费订单
  361. presentRenewOrderDon = thisRenewOrderDon;
  362. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisDeductBetweenDay.intValue());
  363. }
  364. }
  365. log.info("codex车票id:{}目前次一级规格:{}可抵扣的金额为:{}", relationId, thisRenewSku.getSpecVal(), thisRenewDeductMoney);
  366. }
  367. }
  368. if (relationExpiryTime.after(now)) {
  369. //该车票抵扣金额
  370. BigDecimal deductMoneyFromTicket = getDeductMoneyFromTicket(relationId, userIdList, relationSkuId, now, relationStartTime, relationExpiryTime);
  371. deductMoney = deductMoney.add(deductMoneyFromTicket);
  372. }
  373. log.info("codex车票id:{}最终到期时间为:{}车票可返回 抵扣金额:{}", relationId, DateUtil.format(relationExpiryTime, "yyyy-MM-dd HH:mm:ss"), deductMoney);
  374. return deductMoney;
  375. }
  376. private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
  377. return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
  378. }
  379. /**
  380. * Codex POST请求
  381. */
  382. public ClaudeCodeResp executeCodexPostApi(String url, String body) {
  383. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  384. try {
  385. return build.call(() -> {
  386. try {
  387. HttpResponse execute = HttpUtil.createPost(url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).body(body).execute();
  388. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  389. if (!"success".equals(resp.getMessage())) {
  390. log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage());
  391. }
  392. return resp;
  393. } catch (Exception e) {
  394. log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  395. return null;
  396. }
  397. });
  398. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  399. log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  400. throw BusinessRuntimeException.getInstance("创建或更新codex user失败");
  401. }
  402. }
  403. /**
  404. * codex用户信息重试更新信息
  405. */
  406. public void setCodexUserRetryUpdateInfo(Long relationId) {
  407. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  408. if (codexUser != null) {
  409. codexUser.setIsRetry(Boolean.TRUE);
  410. codexUserMapper.updateById(codexUser);
  411. }
  412. }
  413. @Override
  414. public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) {
  415. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  416. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  417. if (codexUser == null) {
  418. return null;
  419. }
  420. // 调用Codex API获取实时使用统计
  421. String usageUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/usage?period=%s", codexUser.getUserId(), period);
  422. ClaudeCodeResp codexResp = executeCodexGetApi(usageUrl);
  423. return codexResp;
  424. }
  425. @Override
  426. public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) {
  427. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  428. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  429. if (codexUser == null) {
  430. throw new BusinessRuntimeException("用户未开通Codex服务");
  431. }
  432. Integer dailyLimit = request.getDailyLimit();
  433. Integer totalQuota = request.getTotalQuota();
  434. // 验证每日限额参数
  435. if (dailyLimit == null || dailyLimit <= 0) {
  436. throw new BusinessRuntimeException("每日限额必须大于0");
  437. }
  438. // 保存原始值用于回滚
  439. Integer originalDailyLimit = codexUser.getOpenaiDailyLimit();
  440. // 更新数据库中的每日限额
  441. codexUser.setOpenaiDailyLimit(dailyLimit);
  442. codexUserMapper.updateById(codexUser);
  443. try {
  444. // 调用Codex API更新每日限额
  445. String updateUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/limit", codexUser.getUserId());
  446. Map<String, Object> params = new HashMap<>();
  447. params.put("daily_limit", dailyLimit);
  448. params.put("total_quota", totalQuota);
  449. ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params));
  450. if (!"success".equals(codexResp.getMessage())) {
  451. log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage());
  452. // API调用失败时回滚数据库更新
  453. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  454. codexUserMapper.updateById(codexUser);
  455. throw new BusinessRuntimeException("更新每日限额失败");
  456. }
  457. log.info("成功更新用户{}每日限额为{}", userId, dailyLimit);
  458. return codexResp;
  459. } catch (Exception e) {
  460. log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e));
  461. // 异常时回滚数据库更新
  462. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  463. codexUserMapper.updateById(codexUser);
  464. throw new BusinessRuntimeException("更新每日限额失败");
  465. }
  466. }
  467. @Override
  468. public ClaudeCodeResp getSystemMetrics(String period, String metric) {
  469. try {
  470. // 构建系统指标API URL
  471. String metricsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/system/metrics");
  472. // 添加查询参数
  473. List<String> queryParams = new ArrayList<>();
  474. if (period != null && !period.isEmpty()) {
  475. queryParams.add("period=" + period);
  476. }
  477. if (metric != null && !metric.isEmpty()) {
  478. queryParams.add("metric=" + metric);
  479. }
  480. if (!queryParams.isEmpty()) {
  481. metricsUrl += "?" + String.join("&", queryParams);
  482. }
  483. // 调用Codex API获取系统指标
  484. ClaudeCodeResp codexResp = executeCodexGetApi(metricsUrl);
  485. if (codexResp == null) {
  486. log.error("获取系统指标失败: API返回null");
  487. throw new BusinessRuntimeException("获取系统指标失败");
  488. }
  489. log.info("成功获取系统指标, period={}, metric={}", period, metric);
  490. return codexResp;
  491. } catch (Exception e) {
  492. log.error("获取系统指标异常, period={}, metric={}, error={}", period, metric, StringUtil.getErrorText(e));
  493. throw new BusinessRuntimeException("获取系统指标失败");
  494. }
  495. }
  496. @Override
  497. public ClaudeCodeResp getJavaUserDashboard(Long userId) {
  498. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  499. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  500. if (codexUser == null) {
  501. return null;
  502. }
  503. // 调用Codex API获取用户控制台数据
  504. String dashboardUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/dashboard", codexUser.getUserId());
  505. ClaudeCodeResp codexResp = executeCodexGetApi(dashboardUrl);
  506. return codexResp;
  507. }
  508. @Override
  509. public ClaudeCodeResp getJavaUserAnalytics(Long userId, String start, String end, Integer page, Integer limit, String order) {
  510. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  511. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  512. if (codexUser == null) {
  513. return null;
  514. }
  515. if (start == null) {
  516. DateTime now = DateTime.now();
  517. start = DateUtil.offsetDay(now, -1).toString();
  518. end = now.toString();
  519. }
  520. // 调用Codex API获取用户分析数据
  521. String analyticsUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/openai/users/%s/analytics?start=%s&end=%s&page=%s&limit=%s&order=%s", codexUser.getUserId(), start, end, page, limit, order);
  522. ClaudeCodeResp codexResp = executeCodexGetApi(analyticsUrl);
  523. return codexResp;
  524. }
  525. @Override
  526. public void delCodexUser(Long userId, Long relationId) {
  527. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  528. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  529. if (codexUser != null) {
  530. codexUserMapper.deleteById(codexUser.getId());
  531. }
  532. }
  533. @Override
  534. public void createOrUpdateCodexUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType) {
  535. //新增或续费升级
  536. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  537. GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList));
  538. GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
  539. GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId());
  540. Long thisCodeOriSkuId = relationSku.getId();
  541. Long relationUserId = relation.getUserId();
  542. //此时车票的套餐规格积分
  543. Integer thisRelationDayilyLimit = null;
  544. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  545. if (codexUser != null) {
  546. thisRelationDayilyLimit = codexUser.getOpenaiDailyLimit();
  547. }
  548. Date expiryTime = relation.getExpiryTime();
  549. Integer openaiDailyLimit = sku.getOpenaiDailyLimit();
  550. Integer openaiQuota = sku.getOpenaiQuota();
  551. Long skuId = sku.getId();
  552. Boolean isUpgrade = false;
  553. Date renewExpiryTime = null;
  554. //若是续费升级 记录此时续费升级的过期时间
  555. if (!ObjectUtil.equal(thisCodeOriSkuId, skuId)) {
  556. //非车票原规格id 续费升级记录
  557. saveCodexRenewExpiryRecord(orderId, relationUserId, relationId, skuId);
  558. //是否需要修改当前claude code user 套餐等级
  559. if (thisRelationDayilyLimit != null && thisRelationDayilyLimit <= openaiDailyLimit) {
  560. if (sku.getDays() != null) {
  561. renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
  562. } else {
  563. renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
  564. }
  565. isUpgrade = true;
  566. }
  567. }
  568. //更新db codexUser
  569. generateOrUpdateCodexUser(relationUserId, relationId, openaiDailyLimit, openaiQuota, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal(), userIdList);
  570. try {
  571. //获取最新的claude code user
  572. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  573. //生成codex或更新用户套餐
  574. ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(relation.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
  575. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  576. //重试更新code userInfo数据
  577. setCodexUserRetryUpdateInfo(relationId);
  578. return;
  579. }
  580. } catch (Exception e) {
  581. log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e));
  582. //重试更新code userInfo数据
  583. setCodexUserRetryUpdateInfo(relationId);
  584. }
  585. }
  586. @Override
  587. public void updateCodexUserPackageInfo(CodexUser codexUser, Date expiryTime, GoodsDonSku finalSku) {
  588. try {
  589. ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), expiryTime);
  590. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  591. //重试标记
  592. codexUser.setIsRetry(Boolean.TRUE);
  593. codexUserMapper.updateById(codexUser);
  594. return;
  595. }
  596. } catch (Exception e) {
  597. codexUser.setIsRetry(Boolean.TRUE);
  598. codexUserMapper.updateById(codexUser);
  599. }
  600. }
  601. private void generateOrUpdateCodexUser(Long relationUserId, Long relationId, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime, Boolean isUpgrade, Long orderId, Long renewSkuId, Date renewExpiryTime, String planName, List<Long> userIdList) {
  602. CodexUser codexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"))).orElse(new CodexUser());
  603. if (isUpgrade) {
  604. codexUser.setRenewOrderId(orderId);
  605. codexUser.setRenewSkuId(renewSkuId);
  606. codexUser.setRenewExpiryTime(renewExpiryTime);
  607. codexUser.setPlanName(planName);
  608. codexUser.setOpenaiDailyLimit(openaiDailyLimit);
  609. codexUser.setOpenaiQuota(openaiQuota);
  610. }
  611. codexUser.setUserId(relationUserId);
  612. codexUser.setRelationId(relationId);
  613. codexUser.setExpiryTime(expiryTime);
  614. if (codexUser.getId() == null) {
  615. codexUser.setPlanName(planName);
  616. codexUser.setOpenaiDailyLimit(openaiDailyLimit);
  617. codexUser.setOpenaiQuota(openaiQuota);
  618. }
  619. if (codexUser.getId() == null) {
  620. try {
  621. codexUserMapper.insert(codexUser);
  622. } catch (DuplicateKeyException e) {
  623. log.info("插入codex user 重复");
  624. }
  625. } else codexUserMapper.updateById(codexUser);
  626. }
  627. private void saveCodexRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Long skuId) {
  628. CodexUserRenewExpiryRecord renewExpiryRecord = new CodexUserRenewExpiryRecord();
  629. renewExpiryRecord.setOrderId(orderId);
  630. renewExpiryRecord.setUserId(relationUserId);
  631. renewExpiryRecord.setRelationId(relationId);
  632. renewExpiryRecord.setRenewSkuId(skuId);
  633. codexUserRenewExpiryRecordMapper.insert(renewExpiryRecord);
  634. }
  635. /**
  636. * Codex DELETE请求
  637. */
  638. public ClaudeCodeResp executeCodexDeleteApi(String url) {
  639. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  640. try {
  641. return build.call(() -> {
  642. try {
  643. HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url)
  644. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  645. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  646. .execute();
  647. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  648. if (!"success".equals(resp.getMessage())) {
  649. log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
  650. }
  651. return resp;
  652. } catch (Exception e) {
  653. log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  654. return null;
  655. }
  656. });
  657. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  658. log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  659. throw BusinessRuntimeException.getInstance("删除操作失败");
  660. }
  661. }
  662. /**
  663. * Codex GET请求
  664. */
  665. public ClaudeCodeResp executeCodexGetApi(String url) {
  666. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  667. try {
  668. return build.call(() -> {
  669. try {
  670. HttpResponse execute = HttpUtil.createGet(url)
  671. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  672. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  673. .execute();
  674. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  675. if (!"success".equals(resp.getMessage())) {
  676. log.error("codex GET URL:{}接口返回msg:{}", url, resp.getMessage());
  677. }
  678. return resp;
  679. } catch (Exception e) {
  680. log.error("codex GET请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  681. return null;
  682. }
  683. });
  684. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  685. log.error("重试调用codex GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  686. throw BusinessRuntimeException.getInstance("获取codex使用统计失败");
  687. }
  688. }
  689. /**
  690. * 获取订单单价
  691. */
  692. public BigDecimal getPayOrderSingle(BigDecimal orderMoney, GoodsDonSku sku) {
  693. //此次续费规格单价
  694. BigDecimal single;
  695. //区分天/月
  696. if (sku.getDays() != null) {
  697. single = orderMoney.divide(BigDecimal.valueOf(sku.getDays()), 0, RoundingMode.HALF_UP);
  698. } else {
  699. single = orderMoney.divide(BigDecimal.valueOf(sku.getMonths()).multiply(BigDecimal.valueOf(30)), 0, RoundingMode.HALF_UP);
  700. }
  701. return single;
  702. }
  703. /**
  704. * 获取车票可抵扣金额
  705. */
  706. public BigDecimal getDeductMoneyFromTicket(Long relationId, List<Long> userIdList, Long skuId, DateTime now, Date relationStartTime, Date relationExpiryTime) {
  707. List<OrderDon> orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
  708. .eq(OrderDon::getRelationId, relationId)
  709. .in(OrderDon::getUserId, userIdList)
  710. .eq(OrderDon::getSkuId, skuId)
  711. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  712. .orderByDesc(OrderDon::getId));
  713. //部分退款
  714. if (orderDonList.isEmpty()) {
  715. orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
  716. .eq(OrderDon::getRelationId, relationId)
  717. .in(OrderDon::getUserId, userIdList)
  718. .eq(OrderDon::getSkuId, skuId)
  719. .notIn(OrderDon::getStatus, Constant.noOrderStatus)
  720. .orderByDesc(OrderDon::getId));
  721. }
  722. //原车票订单总实付金额
  723. BigDecimal totalOrderMoney = BigDecimal.ZERO;
  724. //原车票订单总天数
  725. Integer totalDays = 0;
  726. for (OrderDon orderDon : orderDonList) {
  727. GoodsDonSku sku = skuMapper.selectById(skuId);
  728. BigDecimal thisOrderMoney = orderDon.getMoney().add(Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO)).subtract(Optional.ofNullable(orderDon.getRefundMoney()).orElse(BigDecimal.ZERO));
  729. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  730. thisOrderMoney = sku.getPrice();
  731. }
  732. totalOrderMoney = totalOrderMoney.add(thisOrderMoney);
  733. //总时间
  734. if (sku.getDays() != null) {
  735. totalDays += sku.getDays();
  736. } else {
  737. totalDays += sku.getMonths() * 30;
  738. }
  739. //原订单数
  740. if (orderDonList.size() == 1) {
  741. //此次订单过期时间
  742. Date thisOrderExpiryTime;
  743. if (sku.getDays() != null) {
  744. thisOrderExpiryTime = DateUtil.offsetDay(orderDon.getCreatedTime(), sku.getDays());
  745. } else {
  746. thisOrderExpiryTime = DateUtil.offsetMonth(orderDon.getCreatedTime(), sku.getMonths());
  747. }
  748. //当天购买的按规格原价抵扣
  749. if (now.toDateStr().equals(DateUtil.format(relationStartTime, "yyyy-MM-dd")) && (DateUtil.betweenDay(thisOrderExpiryTime, relationExpiryTime, true) == 0)) {
  750. return thisOrderMoney;
  751. }
  752. }
  753. }
  754. Long thisDeductBetweenDay = DateUtil.betweenDay(now, relationExpiryTime, false) + 1;
  755. BigDecimal single = totalOrderMoney.divide(BigDecimal.valueOf(totalDays), 0, RoundingMode.HALF_UP);
  756. //该车票抵扣金额
  757. BigDecimal deductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
  758. return deductMoney;
  759. }
  760. public void setRelation(GroupsRelation relation) {
  761. Long relationId = relation.getId();
  762. Long userId = relation.getUserId();
  763. Long groupsId = relation.getGroupsId();
  764. GroupsRelation.Status status = relation.getStatus();
  765. String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
  766. if (!redisService.setNx(relation_num_key, userId, 60 * 5L)) {
  767. GroupsRelation dbRelation = relationMapper.selectById(relationId);
  768. if (dbRelation.getUserId() != 0) {
  769. log.info("=====>用户:{}未抢到座位:{}", userId, relationId);
  770. throw new BusinessRuntimeException("系统繁忙,请重试...");
  771. }
  772. }
  773. if (status == GroupsRelation.Status.outside) {
  774. return;
  775. }
  776. int count = groupsMapper.decrAvailableNum(groupsId);
  777. if (count == 0) {
  778. log.error("车位异常 groupId:{}", groupsId);
  779. groupsMapper.updateAvailableNum(groupsId);
  780. //清除座位缓存key
  781. delRelationKey(relationId, userId);
  782. throw new BusinessRuntimeException("车位异常");
  783. }
  784. }
  785. /**
  786. * 清除座位缓存key
  787. */
  788. public void delRelationKey(Long relationId, Long userId) {
  789. String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
  790. Object object = redisService.get(relation_num_key);
  791. if (object != null) {
  792. try {
  793. Long keyUserId = Long.parseLong(object.toString());
  794. //清除车票key
  795. if (ObjectUtil.equal(userId, keyUserId)) {
  796. redisService.del(relation_num_key);
  797. }
  798. } catch (Exception e) {
  799. log.error("删除缓存key错误:{}", StringUtil.getErrorText(e));
  800. }
  801. }
  802. }
  803. }