CodexServiceImpl.java 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. package com.cyksj.service.codex.impl;
  2. import cn.hutool.core.collection.CollUtil;
  3. import cn.hutool.core.date.DateTime;
  4. import cn.hutool.core.date.DateUtil;
  5. import cn.hutool.core.lang.Assert;
  6. import cn.hutool.core.util.ObjectUtil;
  7. import cn.hutool.core.util.StrUtil;
  8. import cn.hutool.http.HttpResponse;
  9. import cn.hutool.http.HttpUtil;
  10. import cn.hutool.http.Method;
  11. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  12. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  13. import com.cyksj.common.constant.Constant;
  14. import com.cyksj.common.exception.BusinessRuntimeException;
  15. import com.cyksj.common.snowflake.Sequence;
  16. import com.cyksj.common.util.Jsons;
  17. import com.cyksj.common.util.StringUtil;
  18. import com.cyksj.dto.RedisKey;
  19. import com.cyksj.mapper.*;
  20. import com.cyksj.mapper.codex.CodexDeductRecordMapper;
  21. import com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper;
  22. import com.cyksj.mapper.manage.coupon.CouponMapper;
  23. import com.cyksj.mapper.manage.coupon.CouponSkuMapper;
  24. import com.cyksj.mapper.manage.coupon.CouponUserMapper;
  25. import com.cyksj.mapper.manage.distribute.UserDistributeMapper;
  26. import com.cyksj.mapper.manage.distribute.UserDistributeSharedMapper;
  27. import com.cyksj.mapper.renew.RenewUpgradePackageMapper;
  28. import com.cyksj.model.dto.CouponPopularizeDto;
  29. import com.cyksj.model.entity.*;
  30. import com.cyksj.model.request.ClaudeCodeApiKeysReq;
  31. import com.cyksj.model.request.ClaudeCodeDelReq;
  32. import com.cyksj.model.request.UpdateDailyLimitRequest;
  33. import com.cyksj.model.request.codex.CodexUserPackageReq;
  34. import com.cyksj.model.response.CodexDeductResp;
  35. import com.cyksj.model.response.claudecode.ClaudeCodeResp;
  36. import com.cyksj.model.views.*;
  37. import com.cyksj.redis.RedisService;
  38. import com.cyksj.service.api.ClaudeCodexApiService;
  39. import com.cyksj.service.codex.CodexService;
  40. import com.cyksj.service.coupon.CouponFontService;
  41. import com.cyksj.service.groups.GroupsFuncService;
  42. import com.cyksj.service.user.UserBindRelationService;
  43. import com.ejlchina.searcher.BeanSearcher;
  44. import com.ejlchina.searcher.param.Operator;
  45. import com.ejlchina.searcher.util.MapBuilder;
  46. import com.ejlchina.searcher.util.MapUtils;
  47. import com.github.rholder.retry.Retryer;
  48. import com.github.rholder.retry.RetryerBuilder;
  49. import com.github.rholder.retry.StopStrategies;
  50. import com.github.rholder.retry.WaitStrategies;
  51. import lombok.RequiredArgsConstructor;
  52. import lombok.extern.slf4j.Slf4j;
  53. import org.springframework.dao.DuplicateKeyException;
  54. import org.springframework.stereotype.Service;
  55. import java.math.BigDecimal;
  56. import java.math.RoundingMode;
  57. import java.util.*;
  58. import java.util.concurrent.ExecutionException;
  59. import java.util.concurrent.TimeUnit;
  60. import java.util.stream.Collectors;
  61. /**
  62. * 项目名: yhlxj11111111
  63. * 文件名: CodexServiceImpl
  64. * 创建者: Claude
  65. * 创建时间:2025/9/15
  66. */
  67. @Slf4j
  68. @Service
  69. @RequiredArgsConstructor
  70. public class CodexServiceImpl implements CodexService {
  71. private final CodexUserMapper codexUserMapper;
  72. private final ClaudeCodeUserMapper claudeCodeUserMapper;
  73. private final UserBindRelationService userBindRelationService;
  74. private final GoodsDonSkuMapper skuMapper;
  75. private final GoodsDonMapper goodsDonMapper;
  76. private final CouponMapper couponMapper;
  77. private final GroupsRelationMapper relationMapper;
  78. private final GroupsMapper groupsMapper;
  79. private final CodexUserRenewExpiryRecordMapper codexUserRenewExpiryRecordMapper;
  80. private final BeanSearcher beanSearcher;
  81. private final OrderDonMapper orderDonMapper;
  82. private final GroupsFuncService groupsFuncService;
  83. private final RedisService redisService;
  84. private final CodexDeductRecordMapper codexDeductRecordMapper;
  85. private final CouponSkuMapper couponSkuMapper;
  86. private final CouponUserMapper couponUserMapper;
  87. private final UserDistributeMapper userDistributeMapper;
  88. private final UserDistributeSharedMapper userDistributeSharedMapper;
  89. private final UserMapper userMapper;
  90. private final CouponFontService couponFontService;
  91. private final OrderDonGalaxyCoinRecordMapper orderDonGalaxyCoinRecordMapper;
  92. private final RenewUpgradePackageMapper renewUpgradePackageMapper;
  93. private static final Sequence SEQUENCE = new Sequence(0);
  94. private final ClaudeCodexApiService claudeCodexApiService;
  95. @Override
  96. public void createOrUpdateUserPackage(CodexUserPackageReq req) {
  97. Long userId = req.getUserId();
  98. // 构建完整的套餐参数
  99. String planName = req.getPlanName();
  100. Date expiryTime = req.getExpiryTime();
  101. Integer openaiDailyLimit = req.getOpenaiDailyLimit();
  102. Integer openaiQuota = req.getOpenaiQuota();
  103. Long relationId = req.getRelationId();
  104. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  105. CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser());
  106. existingCodexUser.setPlanName(planName);
  107. existingCodexUser.setRelationId(relationId);
  108. existingCodexUser.setUserId(userId);
  109. existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit);
  110. existingCodexUser.setOpenaiQuota(openaiQuota);
  111. existingCodexUser.setExpiryTime(expiryTime);
  112. if (existingCodexUser.getId() == null) {
  113. try {
  114. codexUserMapper.insert(existingCodexUser);
  115. } catch (DuplicateKeyException e) {
  116. }
  117. return;
  118. }
  119. codexUserMapper.updateById(existingCodexUser);
  120. }
  121. /**
  122. * 创建或更新Codex用户套餐
  123. */
  124. @Override
  125. public ClaudeCodeResp createOrUpdateCodexUserPackageApi(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
  126. return claudeCodexApiService.createOrUpdateCodexUserPackageApi(userId, planName, openaiDailyLimit, openaiQuota, expiryTime);
  127. }
  128. @Override
  129. public CodexDeductResp getDeductMoney(Long userId, Long relationId, Long upgradeSkuId) {
  130. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  131. CodexDeductResp codexDeductResp = new CodexDeductResp();
  132. List<Long> sendCodexSkuIds = skuMapper.selectList(Wrappers.lambdaQuery(GoodsDonSku.class).eq(GoodsDonSku::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID).eq(GoodsDonSku::getIsCodex, Boolean.TRUE)).stream().map(GoodsDonSku::getId).collect(Collectors.toList());
  133. if (CollUtil.isNotEmpty(sendCodexSkuIds)) {
  134. GroupsRelationMirrorView ccRelationView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  135. .field(GroupsRelationMirrorView::getSkuId, sendCodexSkuIds).op(Operator.InList)
  136. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  137. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  138. .field(GroupsRelationMirrorView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  139. .build());
  140. if (ccRelationView != null) {
  141. codexDeductResp.setIsDeduct(Boolean.FALSE);
  142. return codexDeductResp;
  143. }
  144. }
  145. GoodsDonSku upgradeSku = skuMapper.selectById(upgradeSkuId);
  146. if (upgradeSku.getGoodsId() != Constant.CODEX_GOODS_ID) {
  147. codexDeductResp.setDeductMoney(BigDecimal.ZERO);
  148. return codexDeductResp;
  149. }
  150. MapBuilder builder = MapUtils.builder();
  151. if (relationId != null) {
  152. builder.field(GroupsRelationMirrorView::getId, relationId);
  153. }
  154. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, builder
  155. .field(GroupsRelationMirrorView::getGoodsId, Constant.CODEX_GOODS_ID)
  156. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  157. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  158. .build());
  159. if (groupsRelationMirrorView == null || groupsRelationMirrorView.getExpiryTime().before(DateTime.now())) {
  160. codexDeductResp.setDeductMoney(BigDecimal.ZERO);
  161. } else {
  162. relationId = groupsRelationMirrorView.getId();
  163. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  164. Assert.notNull(codexUser, "系统异常,请联系客服");
  165. codexDeductResp = getFinalDeductMoney(relationId, groupsRelationMirrorView.getSkuId(), codexUser.getRenewSkuId(), codexUser.getRenewOrderId(), codexUser.getRenewExpiryTime(), groupsRelationMirrorView.getStartTime(), groupsRelationMirrorView.getExpiryTime(), DateTime.now(), userIdList, upgradeSku);
  166. }
  167. return codexDeductResp;
  168. }
  169. @Override
  170. public Boolean getUserSubs(Long userId) {
  171. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  172. //是否有该车票
  173. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  174. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  175. .field(GroupsRelationMirrorView::getGoodsId, Constant.CODEX_GOODS_ID)
  176. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  177. .build());
  178. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  179. //车票不存在
  180. if (groupsRelationMirrorView == null) {
  181. //套餐是否存在
  182. if (codexUser != null) {
  183. //清除用户套餐
  184. if (delUserPackages(codexUser.getUserId())) {
  185. codexUserMapper.deleteById(codexUser.getId());
  186. }
  187. }
  188. return false;
  189. }
  190. return true;
  191. }
  192. @Override
  193. public CodexUserInfoView getUserSubsInfo(long userId) {
  194. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  195. //是否有该车票
  196. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  197. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  198. .field(GroupsRelationMirrorView::getGoodsId, Constant.CODEX_GOODS_ID)
  199. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  200. .build());
  201. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  202. //车票不存在
  203. if (groupsRelationMirrorView == null) {
  204. //套餐是否存在
  205. if (codexUser != null) {
  206. //清除用户套餐
  207. if (delUserPackages(codexUser.getUserId())) {
  208. codexUserMapper.deleteById(codexUser.getId());
  209. }
  210. }
  211. return null;
  212. }
  213. GoodsDonSku sku = null;
  214. if (codexUser == null) {
  215. OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
  216. .eq(OrderDon::getGoodsId, Constant.CODEX_GOODS_ID)
  217. .eq(OrderDon::getRelationId, groupsRelationMirrorView.getId())
  218. .in(OrderDon::getUserId, userIdList)
  219. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  220. .orderByDesc(OrderDon::getId)
  221. .last("limit 1"));
  222. if (orderDon != null) {
  223. sku = skuMapper.selectById(orderDon.getSkuId());
  224. createOrUpdateCodexUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
  225. } else {
  226. //试用车票
  227. Long skuId = groupsRelationMirrorView.getSkuId();
  228. sku = skuMapper.selectById(skuId);
  229. createOrUpdateCodexUserInfo(null, groupsRelationMirrorView.getId(), groupsRelationMirrorView.getUserId(), sku, null);
  230. }
  231. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  232. }
  233. //续费升级
  234. if (codexUser.getRenewSkuId() != null) {
  235. sku = skuMapper.selectById(codexUser.getRenewSkuId());
  236. }
  237. if (sku == null) {
  238. sku = skuMapper.selectById(groupsRelationMirrorView.getSkuId());
  239. }
  240. if (codexUser != null) {
  241. codexUser.setPlanName(sku.getSpecVal());
  242. codexUser.setExpiryTime(groupsRelationMirrorView.getExpiryTime());
  243. codexUserMapper.updateById(codexUser);
  244. }
  245. //是否需要重试更新 cladueCode userInfo信息
  246. if (codexUser.getIsRetry()) {
  247. try {
  248. ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
  249. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  250. codexUser.setIsRetry(Boolean.TRUE);
  251. codexUserMapper.updateById(codexUser);
  252. } else {
  253. codexUser.setIsRetry(Boolean.FALSE);
  254. codexUserMapper.updateById(codexUser);
  255. }
  256. } catch (Exception e) {
  257. }
  258. }
  259. return CodexUserInfoView.builder()
  260. .isPay(codexUser.getIsPay())
  261. .planName(codexUser.getPlanName())
  262. .relationId(codexUser.getRelationId())
  263. .openaiDailyLimit(codexUser.getOpenaiDailyLimit())
  264. .openaiQuota(codexUser.getOpenaiQuota())
  265. .expiryTime(codexUser.getExpiryTime())
  266. .renewSkuId(Optional.ofNullable(codexUser.getRenewSkuId()).orElse(groupsRelationMirrorView.getSkuId()))
  267. .renewOrderId(codexUser.getRenewOrderId())
  268. .renewExpiryTime(codexUser.getRenewExpiryTime())
  269. .build();
  270. }
  271. @Override
  272. public ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
  273. Long userId = req.getUserId();
  274. if (checkCodexUser(userId)) {
  275. Long codexUserId = getCodexUserId(userId);
  276. Assert.notNull(codexUserId, "您的codex账号已过期");
  277. req.setUserId(codexUserId);
  278. ClaudeCodeResp claudeCodeResp = claudeCodexApiService.createApiKeys(req);
  279. return claudeCodeResp;
  280. }
  281. throw new BusinessRuntimeException("您还没有codex账号");
  282. }
  283. @Override
  284. public void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception {
  285. Long userId = delReq.getUserId();
  286. Long keyId = delReq.getKeyId();
  287. if (checkCodexUser(userId)) {
  288. Long codexUserId = getCodexUserId(userId);
  289. claudeCodexApiService.deleteApiKeysById(codexUserId, keyId);
  290. }
  291. }
  292. @Override
  293. public List<ClaudeCodeUserApiKeysView> getUserApiKeys(long userId) throws Exception {
  294. Long codexUserId = getCodexUserId(userId);
  295. if (codexUserId != null) {
  296. List<ClaudeCodeUserApiKeysView> keysViews = claudeCodexApiService.getUserApiKeys(codexUserId);
  297. return keysViews;
  298. }
  299. return null;
  300. }
  301. @Override
  302. public GroupsRelation getClaudeCodeRelation(Long userId, GoodsDonSku sku) {
  303. //获取车票车位
  304. GroupsRelation relation = getRelation(sku);
  305. //锁定座位
  306. setRelation(relation);
  307. //校验座位
  308. int update = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
  309. .set(GroupsRelation::getUserId, userId)
  310. .eq(GroupsRelation::getUserId, 0)
  311. .eq(GroupsRelation::getId, relation.getId()));
  312. if (update == 0) {
  313. throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
  314. }
  315. relation.setUserId(userId);
  316. //重新置为锁定
  317. relation.setStatus(GroupsRelation.Status.locking);
  318. relationMapper.updateById(relation);
  319. return relation;
  320. }
  321. private GroupsRelation getRelation(GoodsDonSku sku) {
  322. GroupsRelation relation = null;
  323. Integer maxNum = sku.getNum();
  324. GroupsTrips groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.validity, maxNum, null, null);
  325. if (groups == null) {
  326. //待发车
  327. groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.waiting, maxNum, null, null);
  328. }
  329. //新增空车队
  330. if (groups == null) {
  331. relation = groupsFuncService.createNewGroupsTrips(sku);
  332. }
  333. //获取车位
  334. if (relation == null) {
  335. //寻找快满编车队进行占座
  336. relation = relationMapper.selectRandomOneRelationByGroupsId(groups.getId());
  337. if (relation == null) {
  338. throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
  339. }
  340. }
  341. return relation;
  342. }
  343. public CodexDeductResp getFinalDeductMoney(Long relationId, Long relationSkuId, Long renewSkuId, Long renewOrderId, Date renewExpiryTime, Date relationStartTime, Date relationExpiryTime, DateTime now, List<Long> userIdList, GoodsDonSku upgradeSku) {
  344. BigDecimal deductMoney = BigDecimal.ZERO;
  345. //一个月当30天算单价 往上取整
  346. //续费规格
  347. if (renewSkuId != null && renewExpiryTime.compareTo(now) > 0) {
  348. OrderDon renewOrderDon = orderDonMapper.selectById(renewOrderId);
  349. BigDecimal orderMoney = renewOrderDon.getMoney().add(Optional.ofNullable(renewOrderDon.getBalance()).orElse(BigDecimal.ZERO)).add(Optional.ofNullable(renewOrderDon.getCouponMoney()).orElse(BigDecimal.ZERO));
  350. OrderDonGalaxyCoinRecord orderDonGalaxyCoinRecord = orderDonGalaxyCoinRecordMapper.selectOne(Wrappers.lambdaQuery(OrderDonGalaxyCoinRecord.class).eq(OrderDonGalaxyCoinRecord::getOrderId, renewOrderDon.getId()).last("limit 1"));
  351. if (orderDonGalaxyCoinRecord != null) {
  352. orderMoney = orderMoney.add(orderDonGalaxyCoinRecord.getDeductMoney());
  353. }
  354. // 续费升级订单的实际价值还包含上一次升级使用的 Codex 抵扣。
  355. // 若不回补这部分金额,连续升级时抵扣金额会逐级变少。
  356. CodexDeductRecord renewCodexDeductRecord = codexDeductRecordMapper.selectOne(Wrappers.lambdaQuery(CodexDeductRecord.class)
  357. .eq(CodexDeductRecord::getOrderId, renewOrderDon.getId()).last("limit 1"));
  358. if (renewCodexDeductRecord != null) {
  359. orderMoney = orderMoney.add(Optional.ofNullable(renewCodexDeductRecord.getDeductMoney()).orElse(BigDecimal.ZERO));
  360. }
  361. GoodsDonSku presentSku = skuMapper.selectById(renewSkuId);
  362. if (orderMoney.compareTo(BigDecimal.ZERO) == 0) {
  363. orderMoney = presentSku.getPrice();
  364. }
  365. //当天购买的按规格原价抵扣
  366. BigDecimal renewDeductMoney;
  367. if (now.toDateStr().equals(DateUtil.format(renewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  368. renewDeductMoney = orderMoney;
  369. deductMoney = deductMoney.add(renewDeductMoney);
  370. if (presentSku.getDays() != null) {
  371. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -presentSku.getDays());
  372. } else {
  373. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -presentSku.getMonths());
  374. }
  375. } else {
  376. long skuDays = presentSku.getDays() != null ? presentSku.getDays() : presentSku.getMonths() * 30L;
  377. // renewSkuId 存在即为续费升级:升级套餐立即优先使用,原套餐剩余时间暂停。
  378. // 只按续费升级自身的 renewExpiryTime 计算剩余价值。
  379. long renewRemainDays = Math.max(0L, Math.min(skuDays, DateUtil.betweenDay(now, renewExpiryTime, false)));
  380. //此次续费规格单价
  381. BigDecimal single = getPayOrderSingle(orderMoney, presentSku);
  382. //该车票抵扣金额
  383. renewDeductMoney = single.multiply(BigDecimal.valueOf(renewRemainDays));
  384. log.info("codex车票id:{}目前续费升级规格可抵扣的金额为:{}", relationId, renewDeductMoney);
  385. deductMoney = deductMoney.add(renewDeductMoney);
  386. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -(int) renewRemainDays);
  387. }
  388. //上一个续费升级规格
  389. List<OrderDon> orderDOns = codexUserRenewExpiryRecordMapper.getOtherRenewSkus(renewOrderId, relationId, userIdList);
  390. OrderDon presentRenewOrderDon = renewOrderDon;
  391. for (OrderDon thisRenewOrderDon : orderDOns) {
  392. Long skuId = thisRenewOrderDon.getSkuId();
  393. GoodsDonSku thisRenewSku = skuMapper.selectById(skuId);
  394. BigDecimal thisOrderMoney = thisRenewOrderDon.getMoney().add(Optional.ofNullable(thisRenewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
  395. CodexDeductRecord thisRenewCodexDeductRecord = codexDeductRecordMapper.selectOne(Wrappers.lambdaQuery(CodexDeductRecord.class)
  396. .eq(CodexDeductRecord::getOrderId, thisRenewOrderDon.getId()).last("limit 1"));
  397. if (thisRenewCodexDeductRecord != null) {
  398. thisOrderMoney = thisOrderMoney.add(Optional.ofNullable(thisRenewCodexDeductRecord.getDeductMoney()).orElse(BigDecimal.ZERO));
  399. }
  400. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  401. thisOrderMoney = thisRenewSku.getPrice();
  402. }
  403. //当天购买的按规格原价抵扣
  404. BigDecimal thisRenewDeductMoney = BigDecimal.ZERO;
  405. if (now.toDateStr().equals(DateUtil.format(thisRenewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
  406. thisRenewDeductMoney = thisOrderMoney;
  407. deductMoney = deductMoney.add(thisRenewDeductMoney);
  408. if (thisRenewSku.getDays() != null) {
  409. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisRenewSku.getDays());
  410. } else {
  411. relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -thisRenewSku.getMonths());
  412. }
  413. } else {
  414. //此次续费订单过期时间
  415. Date thisOrderExpiryTime;
  416. if (thisRenewSku.getDays() != null) {
  417. thisOrderExpiryTime = DateUtil.offsetDay(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getDays());
  418. } else {
  419. thisOrderExpiryTime = DateUtil.offsetMonth(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getMonths());
  420. }
  421. //续费同规格不处理
  422. if (!ObjectUtil.equal(thisRenewOrderDon.getSkuId(), presentRenewOrderDon.getSkuId())) {
  423. //先扣除已使用的天数
  424. //相差多少天
  425. Long thisRenewBetDay = DateUtil.betweenDay(thisRenewOrderDon.getCreatedTime(), presentRenewOrderDon.getCreatedTime(), false);
  426. //扣除已使用的
  427. thisOrderExpiryTime = DateUtil.offsetDay(thisOrderExpiryTime, -thisRenewBetDay.intValue());
  428. }
  429. //此订单过期时间
  430. if (thisOrderExpiryTime.compareTo(now) > 0) {
  431. //距离过期还剩多少天
  432. Long thisDeductBetweenDay = DateUtil.betweenDay(now, thisOrderExpiryTime, false) + 1;
  433. if (thisDeductBetweenDay != 0) {
  434. //此次续费规格单价
  435. BigDecimal single = getPayOrderSingle(thisOrderMoney, thisRenewSku);
  436. //该车票抵扣金额
  437. thisRenewDeductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
  438. deductMoney = deductMoney.add(thisRenewDeductMoney);
  439. }
  440. //目前级别续费订单
  441. presentRenewOrderDon = thisRenewOrderDon;
  442. relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisDeductBetweenDay.intValue());
  443. }
  444. }
  445. log.info("codex车票id:{}目前次一级规格:{}可抵扣的金额为:{}", relationId, thisRenewSku.getSpecVal(), thisRenewDeductMoney);
  446. }
  447. }
  448. CodexDeductResp codexDeductResp = new CodexDeductResp();
  449. if (relationExpiryTime.after(now)) {
  450. //该车票抵扣金额
  451. BigDecimal deductMoneyFromTicket = getDeductMoneyFromTicket(relationId, userIdList, relationSkuId, now, relationStartTime, relationExpiryTime);
  452. deductMoney = deductMoney.add(deductMoneyFromTicket);
  453. }
  454. Long presentSkuId = renewSkuId != null && renewSkuId > 0 ? renewSkuId : relationSkuId;
  455. RenewUpgradePackage renewUpgradePackage = renewUpgradePackageMapper.selectOne(Wrappers.lambdaQuery(RenewUpgradePackage.class)
  456. .eq(RenewUpgradePackage::getGoodsId, Constant.CODEX_GOODS_ID)
  457. .eq(RenewUpgradePackage::getSkuId, presentSkuId)
  458. .apply("JSON_CONTAINS(upgrade_sku_ids,'\"" + upgradeSku.getId() + "\"')")
  459. .eq(RenewUpgradePackage::getStatus, 1)
  460. .eq(RenewUpgradePackage::getDeleted, 1)
  461. .orderByAsc(RenewUpgradePackage::getDiscount)
  462. .last("limit 1"));
  463. BigDecimal skuPrice = upgradeSku.getPrice();
  464. if (renewUpgradePackage != null && renewUpgradePackage.getDiscount() != null) {
  465. skuPrice = skuPrice.multiply(renewUpgradePackage.getDiscount());
  466. }
  467. //超出可抵扣的金额 直接返回
  468. if (deductMoney.compareTo(skuPrice) >= 0) {
  469. BigDecimal overMoney = deductMoney.subtract(skuPrice);
  470. //超过抵扣金额可换延长会员多少天
  471. //每天单价
  472. BigDecimal daySingle = skuPrice.divide(BigDecimal.valueOf(upgradeSku.getMonths())).divide(BigDecimal.valueOf(30), 0, RoundingMode.DOWN);
  473. //超出部分可换取多少天 低于每天单价为0天
  474. BigDecimal extendsDays = overMoney.divide(daySingle, 0, RoundingMode.UP);
  475. codexDeductResp.setExtendsDays(extendsDays.intValue());
  476. }
  477. log.info("codex车票id:{}最终到期时间为:{}车票可返回 抵扣金额:{}", relationId, DateUtil.format(relationExpiryTime, "yyyy-MM-dd HH:mm:ss"), deductMoney);
  478. codexDeductResp.setDeductMoney(deductMoney);
  479. return codexDeductResp;
  480. }
  481. private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
  482. return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
  483. }
  484. /**
  485. * Codex POST请求
  486. */
  487. public ClaudeCodeResp executeCodexPostApi(String url, String body) {
  488. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  489. try {
  490. return build.call(() -> {
  491. try {
  492. HttpResponse execute = HttpUtil.createPost(url).header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(Constant.CONNECT_MILLISECONDS).body(body).execute();
  493. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  494. if (!"success".equals(resp.getMessage())) {
  495. log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage());
  496. }
  497. return resp;
  498. } catch (Exception e) {
  499. log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  500. return null;
  501. }
  502. });
  503. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  504. log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  505. throw BusinessRuntimeException.getInstance("创建或更新codex user失败");
  506. }
  507. }
  508. /**
  509. * codex用户信息重试更新信息
  510. */
  511. public void setCodexUserRetryUpdateInfo(Long relationId) {
  512. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  513. if (codexUser != null) {
  514. codexUser.setIsRetry(Boolean.TRUE);
  515. codexUserMapper.updateById(codexUser);
  516. }
  517. }
  518. @Override
  519. public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) {
  520. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  521. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  522. if (codexUser == null) {
  523. return null;
  524. }
  525. return claudeCodexApiService.getCodexUserOpenAIUsage(codexUser.getUserId(), period);
  526. }
  527. @Override
  528. public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) {
  529. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  530. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  531. if (codexUser == null) {
  532. throw new BusinessRuntimeException("用户未开通Codex服务");
  533. }
  534. Integer dailyLimit = request.getDailyLimit();
  535. Integer totalQuota = request.getTotalQuota();
  536. // 验证每日限额参数
  537. if (dailyLimit == null || dailyLimit <= 0) {
  538. throw new BusinessRuntimeException("每日限额必须大于0");
  539. }
  540. // 保存原始值用于回滚
  541. Integer originalDailyLimit = codexUser.getOpenaiDailyLimit();
  542. // 更新数据库中的每日限额
  543. codexUser.setOpenaiDailyLimit(dailyLimit);
  544. codexUserMapper.updateById(codexUser);
  545. try {
  546. // 调用Codex API更新每日限额
  547. String updateUrl = String.format(Constant.CLAUDE_CODE_API_PREFIX + "api/users/%s/limit", codexUser.getUserId());
  548. Map<String, Object> params = new HashMap<>();
  549. params.put("daily_limit", dailyLimit);
  550. params.put("total_quota", totalQuota);
  551. ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params));
  552. if (!"success".equals(codexResp.getMessage())) {
  553. log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage());
  554. // API调用失败时回滚数据库更新
  555. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  556. codexUserMapper.updateById(codexUser);
  557. throw new BusinessRuntimeException("更新每日限额失败");
  558. }
  559. log.info("成功更新用户{}每日限额为{}", userId, dailyLimit);
  560. return codexResp;
  561. } catch (Exception e) {
  562. log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e));
  563. // 异常时回滚数据库更新
  564. codexUser.setOpenaiDailyLimit(originalDailyLimit);
  565. codexUserMapper.updateById(codexUser);
  566. throw new BusinessRuntimeException("更新每日限额失败");
  567. }
  568. }
  569. @Override
  570. public ClaudeCodeResp getJavaUserDashboard(Long userId) {
  571. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  572. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  573. if (codexUser == null) {
  574. return null;
  575. }
  576. ClaudeCodeResp codexUserDashboard = claudeCodexApiService.getCodexUserDashboard(codexUser.getUserId());
  577. if (codexUserDashboard != null && codexUserDashboard.getMessage() != null && codexUserDashboard.getMessage().contains("套餐失败")) {
  578. try {
  579. createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
  580. } catch (Exception e) {
  581. log.error("用户id:{}仪表盘获取套餐失败,重新加载用户套餐", codexUser.getUserId());
  582. }
  583. }
  584. return codexUserDashboard;
  585. }
  586. @Override
  587. public ClaudeCodeResp getJavaUserAnalytics(Long userId, String start, String end, Integer page, Integer limit, String order) {
  588. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  589. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
  590. if (codexUser == null) {
  591. return null;
  592. }
  593. if (start == null) {
  594. DateTime now = DateTime.now();
  595. start = DateUtil.offsetDay(now, -1).toString();
  596. end = now.toString();
  597. }
  598. return claudeCodexApiService.getCodexUserAnalytics(codexUser.getUserId(), start, end, page, limit, order);
  599. }
  600. @Override
  601. public void delCodexUser(Long userId, Long relationId) {
  602. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  603. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  604. if (codexUser != null) {
  605. codexUserMapper.deleteById(codexUser.getId());
  606. }
  607. }
  608. @Override
  609. public void createOrUpdateCodexUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType) {
  610. //新增或续费升级
  611. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  612. GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList));
  613. GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
  614. GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId());
  615. Long thisCodeOriSkuId = relationSku.getId();
  616. Long relationUserId = relation.getUserId();
  617. //此时车票的套餐规格积分
  618. Integer thisRelationDayilyLimit = null;
  619. CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  620. if (codexUser != null) {
  621. thisRelationDayilyLimit = codexUser.getOpenaiDailyLimit();
  622. }
  623. Date expiryTime = relation.getExpiryTime();
  624. Integer openaiDailyLimit = sku.getOpenaiDailyLimit();
  625. Integer openaiQuota = sku.getOpenaiQuota();
  626. Long skuId = sku.getId();
  627. Boolean isUpgrade = false;
  628. Date renewExpiryTime = null;
  629. //若是续费升级 记录此时续费升级的过期时间
  630. if (!ObjectUtil.equal(thisCodeOriSkuId, skuId)) {
  631. //非车票原规格id 续费升级记录
  632. saveCodexRenewExpiryRecord(orderId, relationUserId, relationId, skuId);
  633. //是否需要修改当前claude code user 套餐等级
  634. if (thisRelationDayilyLimit != null && thisRelationDayilyLimit <= openaiDailyLimit) {
  635. if (sku.getDays() != null) {
  636. renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
  637. } else {
  638. renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
  639. }
  640. isUpgrade = true;
  641. }
  642. }
  643. //更新db codexUser
  644. generateOrUpdateCodexUser(relationUserId, relationId, openaiDailyLimit, openaiQuota, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal(), userIdList, Boolean.TRUE);
  645. try {
  646. //获取最新的claude code user
  647. codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
  648. //生成codex或更新用户套餐
  649. ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(relation.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
  650. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  651. //重试更新code userInfo数据
  652. setCodexUserRetryUpdateInfo(relationId);
  653. return;
  654. }
  655. } catch (Exception e) {
  656. log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e));
  657. //重试更新code userInfo数据
  658. setCodexUserRetryUpdateInfo(relationId);
  659. }
  660. }
  661. @Override
  662. public void updateCodexUserPackageInfo(CodexUser codexUser, Date expiryTime, GoodsDonSku finalSku) {
  663. try {
  664. ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), expiryTime);
  665. if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
  666. //重试标记
  667. codexUser.setIsRetry(Boolean.TRUE);
  668. codexUserMapper.updateById(codexUser);
  669. return;
  670. }
  671. } catch (Exception e) {
  672. codexUser.setIsRetry(Boolean.TRUE);
  673. codexUserMapper.updateById(codexUser);
  674. }
  675. }
  676. 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, Boolean isPay) {
  677. CodexUser codexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"))).orElse(new CodexUser());
  678. if (isUpgrade) {
  679. codexUser.setRenewOrderId(orderId);
  680. codexUser.setRenewSkuId(renewSkuId);
  681. codexUser.setRenewExpiryTime(renewExpiryTime);
  682. codexUser.setPlanName(planName);
  683. codexUser.setOpenaiDailyLimit(openaiDailyLimit);
  684. codexUser.setOpenaiQuota(openaiQuota);
  685. }
  686. codexUser.setIsPay(isPay);
  687. codexUser.setUserId(relationUserId);
  688. codexUser.setRelationId(relationId);
  689. codexUser.setExpiryTime(expiryTime);
  690. if (codexUser.getId() == null) {
  691. codexUser.setPlanName(planName);
  692. codexUser.setOpenaiDailyLimit(openaiDailyLimit);
  693. codexUser.setOpenaiQuota(openaiQuota);
  694. }
  695. if (codexUser.getId() == null) {
  696. try {
  697. codexUserMapper.insert(codexUser);
  698. } catch (DuplicateKeyException e) {
  699. log.info("插入codex user 重复");
  700. }
  701. } else codexUserMapper.updateById(codexUser);
  702. }
  703. private void saveCodexRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Long skuId) {
  704. CodexUserRenewExpiryRecord renewExpiryRecord = new CodexUserRenewExpiryRecord();
  705. renewExpiryRecord.setOrderId(orderId);
  706. renewExpiryRecord.setUserId(relationUserId);
  707. renewExpiryRecord.setRelationId(relationId);
  708. renewExpiryRecord.setRenewSkuId(skuId);
  709. codexUserRenewExpiryRecordMapper.insert(renewExpiryRecord);
  710. }
  711. /**
  712. * Codex DELETE请求
  713. */
  714. public ClaudeCodeResp executeCodexDeleteApi(String url) {
  715. Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
  716. try {
  717. return build.call(() -> {
  718. try {
  719. HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url)
  720. .header(Constant.CLAUDE_CODE_HEARD_KEY, Constant.CLAUDE_CODE_API_ADMIN_KEY)
  721. .setConnectionTimeout(Constant.CONNECT_MILLISECONDS)
  722. .execute();
  723. ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
  724. if (!"success".equals(resp.getMessage())) {
  725. log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
  726. }
  727. return resp;
  728. } catch (Exception e) {
  729. log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
  730. return null;
  731. }
  732. });
  733. } catch (ExecutionException | com.github.rholder.retry.RetryException e) {
  734. log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
  735. throw BusinessRuntimeException.getInstance("删除操作失败");
  736. }
  737. }
  738. /**
  739. * 获取订单单价
  740. */
  741. public BigDecimal getPayOrderSingle(BigDecimal orderMoney, GoodsDonSku sku) {
  742. //此次续费规格单价
  743. BigDecimal single;
  744. //区分天/月
  745. if (sku.getDays() != null) {
  746. single = orderMoney.divide(BigDecimal.valueOf(sku.getDays()), 0, RoundingMode.HALF_UP);
  747. } else {
  748. single = orderMoney.divide(BigDecimal.valueOf(sku.getMonths()).multiply(BigDecimal.valueOf(30)), 0, RoundingMode.HALF_UP);
  749. }
  750. return single;
  751. }
  752. /**
  753. * 获取车票可抵扣金额
  754. */
  755. public BigDecimal getDeductMoneyFromTicket(Long relationId, List<Long> userIdList, Long skuId, DateTime now, Date relationStartTime, Date relationExpiryTime) {
  756. // 抵扣升级成功后,新订单已经承接旧车票的剩余价值,作为新的初始订单。
  757. // 后续只计算该订单及之后的普通续费,避免再次累计抵扣升级前的旧订单。
  758. OrderDon oriOrderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
  759. .eq(OrderDon::getRelationId, relationId)
  760. .in(OrderDon::getUserId, userIdList)
  761. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  762. .apply("exists (select 1 from codex_deduct_record d where d.order_id = order_don.id and d.status = 1)")
  763. .orderByDesc(OrderDon::getId).last("limit 1"));
  764. if (oriOrderDon == null) {
  765. oriOrderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
  766. .eq(OrderDon::getRelationId, relationId)
  767. .in(OrderDon::getUserId, userIdList)
  768. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  769. .eq(OrderDon::getOrderType, 1)
  770. .orderByDesc(OrderDon::getId).last("limit 1"));
  771. }
  772. LambdaQueryWrapper<OrderDon> queryWrapper = Wrappers.lambdaQuery(OrderDon.class);
  773. Long remainDays = DateUtil.betweenDay(now, relationExpiryTime, false) + 1;
  774. int deductMonth = Math.max(1, BigDecimal.valueOf(remainDays).divide(BigDecimal.valueOf(30), 0, RoundingMode.UP).intValue());
  775. Date deductStartTime = DateUtil.offsetMonth(now, -deductMonth);
  776. Date cutoffTime = deductStartTime;
  777. if (oriOrderDon != null) {
  778. // 原套餐可能在续费升级期间暂停,订单虽早但剩余价值仍有效。
  779. cutoffTime = oriOrderDon.getCreatedTime();
  780. queryWrapper.ge(OrderDon::getCreatedTime, cutoffTime);
  781. } else {
  782. queryWrapper.ge(OrderDon::getCreatedTime, deductStartTime);
  783. }
  784. List<OrderDon> orderDonList = orderDonMapper.selectList(queryWrapper
  785. .eq(OrderDon::getRelationId, relationId)
  786. .in(OrderDon::getUserId, userIdList)
  787. .eq(OrderDon::getSkuId, skuId)
  788. .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
  789. .orderByDesc(OrderDon::getId));
  790. //部分退款
  791. if (orderDonList.isEmpty()) {
  792. orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
  793. .ge(OrderDon::getCreatedTime, cutoffTime)
  794. .eq(OrderDon::getRelationId, relationId)
  795. .in(OrderDon::getUserId, userIdList)
  796. .eq(OrderDon::getSkuId, skuId)
  797. .notIn(OrderDon::getStatus, Constant.noOrderStatus)
  798. .orderByDesc(OrderDon::getId));
  799. }
  800. // 多笔订单的有效期是依次追加的,必须从最终到期时间向前逐笔划分,
  801. // 不能把不同价格订单合并后用平均日价折算。
  802. BigDecimal deductMoney = BigDecimal.ZERO;
  803. Date segmentEndTime = relationExpiryTime;
  804. for (OrderDon orderDon : orderDonList) {
  805. GoodsDonSku sku = skuMapper.selectById(orderDon.getSkuId());
  806. long orderDays = sku.getDays() != null ? sku.getDays() : sku.getMonths() * 30L;
  807. Date segmentStartTime = DateUtil.offsetDay(segmentEndTime, -(int) orderDays);
  808. // 当前订单段已经全部使用,排在它前面的订单段也必然已使用完。
  809. if (!segmentEndTime.after(now)) {
  810. break;
  811. }
  812. BigDecimal thisOrderMoney = orderDon.getMoney().add(Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO)).subtract(Optional.ofNullable(orderDon.getRefundMoney()).orElse(BigDecimal.ZERO)).add(Optional.ofNullable(orderDon.getCouponMoney()).orElse(BigDecimal.ZERO));
  813. OrderDonGalaxyCoinRecord orderDonGalaxyCoinRecord = orderDonGalaxyCoinRecordMapper.selectOne(Wrappers.lambdaQuery(OrderDonGalaxyCoinRecord.class).eq(OrderDonGalaxyCoinRecord::getOrderId, orderDon.getId()).last("limit 1"));
  814. if (orderDonGalaxyCoinRecord != null) {
  815. thisOrderMoney = thisOrderMoney.add(orderDonGalaxyCoinRecord.getDeductMoney());
  816. }
  817. CodexDeductRecord codexDeductRecord = codexDeductRecordMapper.selectOne(Wrappers.lambdaQuery(CodexDeductRecord.class).eq(CodexDeductRecord::getOrderId, orderDon.getId()).last("limit 1"));
  818. if (codexDeductRecord != null) {
  819. thisOrderMoney = thisOrderMoney.add(Optional.ofNullable(codexDeductRecord.getDeductMoney()).orElse(BigDecimal.ZERO));
  820. }
  821. if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
  822. thisOrderMoney = sku.getPrice();
  823. }
  824. if (!now.after(segmentStartTime)) {
  825. // 该订单追加的时段尚未开始,完整价值参与抵扣。
  826. deductMoney = deductMoney.add(thisOrderMoney);
  827. } else {
  828. long remainingDays = Math.min(orderDays, DateUtil.betweenDay(now, segmentEndTime, false) + 1);
  829. BigDecimal single = thisOrderMoney.divide(BigDecimal.valueOf(orderDays), 2, RoundingMode.HALF_UP);
  830. deductMoney = deductMoney.add(single.multiply(BigDecimal.valueOf(remainingDays)));
  831. }
  832. segmentEndTime = segmentStartTime;
  833. }
  834. return deductMoney;
  835. }
  836. public void setRelation(GroupsRelation relation) {
  837. Long relationId = relation.getId();
  838. Long userId = relation.getUserId();
  839. Long groupsId = relation.getGroupsId();
  840. GroupsRelation.Status status = relation.getStatus();
  841. String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
  842. if (!redisService.setNx(relation_num_key, userId, 60 * 5L)) {
  843. GroupsRelation dbRelation = relationMapper.selectById(relationId);
  844. if (dbRelation.getUserId() != 0) {
  845. log.info("=====>用户:{}未抢到座位:{}", userId, relationId);
  846. throw new BusinessRuntimeException("系统繁忙,请重试...");
  847. }
  848. }
  849. if (status == GroupsRelation.Status.outside) {
  850. return;
  851. }
  852. int count = groupsMapper.decrAvailableNum(groupsId);
  853. if (count == 0) {
  854. log.error("车位异常 groupId:{}", groupsId);
  855. groupsMapper.updateAvailableNum(groupsId);
  856. //清除座位缓存key
  857. delRelationKey(relationId, userId);
  858. throw new BusinessRuntimeException("车位异常");
  859. }
  860. }
  861. /**
  862. * 清除座位缓存key
  863. */
  864. public void delRelationKey(Long relationId, Long userId) {
  865. String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
  866. Object object = redisService.get(relation_num_key);
  867. if (object != null) {
  868. try {
  869. Long keyUserId = Long.parseLong(object.toString());
  870. //清除车票key
  871. if (ObjectUtil.equal(userId, keyUserId)) {
  872. redisService.del(relation_num_key);
  873. }
  874. } catch (Exception e) {
  875. log.error("删除缓存key错误:{}", StringUtil.getErrorText(e));
  876. }
  877. }
  878. }
  879. public CouponPopularizeDto checkCouponStatus(Long skuId, String couponExCode, Long couponId, Long userId, Boolean isRenew) throws Exception {
  880. log.info("开始校验兑换码:{},优惠券id:{}状态", couponExCode, couponId);
  881. //实物还是虚物商品
  882. GoodsDonSku goodsDonSku = skuMapper.selectById(skuId);
  883. GoodsDon goodsDon = goodsDonMapper.selectById(goodsDonSku.getGoodsId());
  884. if (StrUtil.isNotBlank(couponExCode) && couponId != null) {
  885. throw BusinessRuntimeException.getInstance("优惠券与优惠码不可同时使用..");
  886. }
  887. if (StrUtil.isNotBlank(couponExCode)) {
  888. CouponPopularizeDto exists = couponMapper.checkExCode(couponExCode);
  889. if (exists == null) {
  890. throw BusinessRuntimeException.getInstance("该优惠码不存在");
  891. }
  892. //推广码
  893. if (exists.getIsGeneral() != null) {
  894. if (exists.getIsGeneral() && userId.equals(exists.getUserId())) {
  895. throw BusinessRuntimeException.getInstance("不可以使用自己的优惠码");
  896. }
  897. if (!exists.getIsGeneral()) {
  898. UserDistribute userDistribute = userDistributeMapper.selectById(exists.getDistributeId());
  899. if (userDistribute != null && userDistribute.getUserId().equals(userId)) {
  900. throw BusinessRuntimeException.getInstance("不可以使用自己的优惠码");
  901. }
  902. }
  903. //是否有上下级关系
  904. if (exists.getIsGeneral()) {
  905. UserDistributeShared userDistributeShared = userDistributeSharedMapper.selectOne(Wrappers.lambdaQuery(UserDistributeShared.class)
  906. .eq(UserDistributeShared::getUserId, userId).last("limit 1"));
  907. if (userDistributeShared == null) {
  908. //是否是 当天注册新用户 且下过单
  909. User user = userMapper.selectById(userId);
  910. if (user.getCreatedTime().before(DateUtil.beginOfDay(DateTime.now()))) {
  911. throw BusinessRuntimeException.getInstance("该优惠码只适用于新用户");
  912. }
  913. CouponNewUserView couponNewUserView = beanSearcher.searchFirst(CouponNewUserView.class, MapUtils.builder().field(CouponNewUserView::getUserId, userId).op(Operator.Equal).build());
  914. if (couponNewUserView != null)
  915. throw BusinessRuntimeException.getInstance("该优惠码只适用于新用户");
  916. }
  917. if (userDistributeShared != null && !userDistributeShared.getSharedId().equals(exists.getUserId())) {
  918. throw BusinessRuntimeException.getInstance("您已有推广,无法使用其他人推广码");
  919. }
  920. }
  921. }
  922. checkCouponGoods(goodsDon, skuId, exists.getCouponId(), true);
  923. //检验是优惠券使用范围
  924. if (isRenew && exists.getUseScope() == Coupon.UseScope.orders) {
  925. throw BusinessRuntimeException.getInstance("该优惠码续费时不可使用");
  926. }
  927. if (!isRenew && exists.getUseScope() == Coupon.UseScope.renew) {
  928. throw BusinessRuntimeException.getInstance("该优惠码下单时不可使用");
  929. }
  930. if (exists.getScope() == Coupon.Scope.newUser) {
  931. CouponNewUserView couponNewUserView = beanSearcher.searchFirst(CouponNewUserView.class, MapUtils.builder().field(CouponNewUserView::getUserId, userId).op(Operator.Equal).build());
  932. if (couponNewUserView != null) {
  933. throw BusinessRuntimeException.getInstance("该新用户优惠码有且仅可使用一次");
  934. }
  935. }
  936. if (exists.getIsValidTime() && exists.getValidEndTime().before(DateTime.now())) {
  937. throw BusinessRuntimeException.getInstance("该优惠码已失效");
  938. }
  939. if (exists.getIsValidTime() && exists.getValidStartTime().after(DateTime.now())) {
  940. throw BusinessRuntimeException.getInstance("该优惠码待生效");
  941. }
  942. return exists;
  943. } else if (couponId != null) {
  944. CouponUserView couponUserView = beanSearcher.searchFirst(CouponUserView.class, MapUtils.builder()
  945. .field(CouponUserView::getCouponId, couponId).op(Operator.Equal)
  946. .field(CouponUserView::getUserId, userId).op(Operator.Equal)
  947. .build());
  948. if (couponUserView == null) {
  949. throw BusinessRuntimeException.getInstance("该优惠券不存在");
  950. }
  951. if (couponUserView.getCouponUserStatus() == CouponUser.Status.used) {
  952. throw BusinessRuntimeException.getInstance("该优惠券已使用");
  953. }
  954. couponFontService.setCouponUserStatus(couponUserView, DateTime.now());
  955. if (couponUserView.getCouponUserStatus() != CouponUser.Status.unused) {
  956. throw BusinessRuntimeException.getInstance(couponUserView.getReason());
  957. }
  958. checkCouponGoods(goodsDon, skuId, couponUserView.getCouponId(), false);
  959. //检验是优惠券使用范围
  960. if (isRenew && couponUserView.getUseScope() == Coupon.UseScope.orders) {
  961. throw BusinessRuntimeException.getInstance("该优惠券续费时不可使用");
  962. }
  963. if (!isRenew && couponUserView.getUseScope() == Coupon.UseScope.renew) {
  964. throw BusinessRuntimeException.getInstance("该优惠券下单时不可使用");
  965. }
  966. }
  967. return null;
  968. }
  969. private CouponUser couponStatusRecord(String couponExCode, Long couponId, Long userId, CouponPopularizeDto exists) {
  970. log.info("兑换码:{},优惠券id:{},更新其优惠券状态", couponExCode, couponId);
  971. CouponUser couponUser = null;
  972. if (StrUtil.isNotBlank(couponExCode)) {
  973. if (exists == null) {
  974. throw BusinessRuntimeException.getInstance("该优惠码不存在");
  975. }
  976. couponUser = new CouponUser();
  977. couponUser.setCouponId(exists.getCouponId());
  978. if (exists.getPopularizeId() != null) {
  979. couponUser.setCouponId(exists.getPopularizeCouponId());
  980. couponUser.setPopularizeId(exists.getPopularizeId());
  981. }
  982. //续费优惠码可重复使用
  983. if (exists.getUseScope() != Coupon.UseScope.renew) {
  984. //非渠道推广 推广优惠码只可使用一次
  985. if ((exists.getPopularizeId() != null && exists.getIsGeneral()) || exists.getPopularizeId() == null) {
  986. CouponUser received = couponUserMapper.selectOne(Wrappers.lambdaQuery(CouponUser.class).eq(CouponUser::getUserId, userId).eq(CouponUser::getCouponId, couponUser.getCouponId()).last("limit 1"));
  987. if (received != null && received.getStatus() != CouponUser.Status.unused) {
  988. throw BusinessRuntimeException.getInstance("您已使用该优惠码所对应的优惠券");
  989. }
  990. couponUser.setUserId(userId);
  991. couponUser.setStatus(CouponUser.Status.used);
  992. couponUser.setChannel(CouponUser.Channel.exCode);
  993. couponFontService.updateCouponUserValidTime(couponUser.getCouponId(), couponUser);
  994. try {
  995. couponUserMapper.insert(couponUser);
  996. } catch (DuplicateKeyException e) {
  997. log.info("用户已领取该{}优惠券", couponUser.getCouponId());
  998. throw BusinessRuntimeException.getInstance("您已领取该优惠码对应的优惠券,请使用优惠券");
  999. }
  1000. }
  1001. }
  1002. } else {
  1003. couponUser = couponUserMapper.selectOne(Wrappers.lambdaQuery(CouponUser.class).eq(CouponUser::getUserId, userId).eq(CouponUser::getCouponId, couponId).last("limit 1"));
  1004. if (couponUser == null) {
  1005. throw BusinessRuntimeException.getInstance("该优惠券不存在");
  1006. }
  1007. couponUser.setStatus(CouponUser.Status.used);
  1008. couponUserMapper.updateById(couponUser);
  1009. }
  1010. return couponUser;
  1011. }
  1012. public void checkCouponGoods(GoodsDon goodsDon, Long skuId, Long couponId, Boolean isCouponCode) throws Exception {
  1013. if (goodsDon.getType() == 1) {
  1014. CouponSku couponSku = couponSkuMapper.selectOne(Wrappers.lambdaQuery(CouponSku.class)
  1015. .eq(CouponSku::getSkuId, skuId)
  1016. .eq(CouponSku::getCouponId, couponId)
  1017. .select(CouponSku::getId).last("limit 1"));
  1018. if (couponSku == null) {
  1019. throw BusinessRuntimeException.getInstance(String.format("该%s不适用于该规格", isCouponCode ? "优惠码" : "优惠券"));
  1020. }
  1021. } else {
  1022. //实物、潮玩 优惠码优惠券 针对平台
  1023. Long gid = goodsDon.getId();
  1024. CouponSku couponSku = couponSkuMapper.selectOne(Wrappers.lambdaQuery(CouponSku.class)
  1025. .eq(CouponSku::getGoodsId, gid)
  1026. .eq(CouponSku::getCouponId, couponId)
  1027. .select(CouponSku::getId).last("limit 1"));
  1028. if (couponSku == null) {
  1029. throw BusinessRuntimeException.getInstance(String.format("该%s不可用于该商品", isCouponCode ? "优惠码" : "优惠券"));
  1030. }
  1031. }
  1032. }
  1033. /**
  1034. * 使用优惠营销 订单金额抵扣
  1035. */
  1036. public void deductOrderMoney(OrderDon orderDon, CouponUser couponUser) throws Exception {
  1037. BigDecimal first_money = orderDon.getMoney();
  1038. //存在优惠券
  1039. if (couponUser != null) {
  1040. Long couponId = couponUser.getCouponId();
  1041. Coupon coupon = couponMapper.getCouponById(couponId);
  1042. BigDecimal couponMoney = null;
  1043. BigDecimal payMoney = orderDon.getMoney();
  1044. BigDecimal needDeductMoney = payMoney;
  1045. if (orderDon.getOriMoney() == null) {
  1046. orderDon.setOriMoney(payMoney);
  1047. }
  1048. if (orderDon.getIsDp() != null && orderDon.getIsDp()) {
  1049. needDeductMoney = needDeductMoney.subtract(orderDon.getDpMoney());
  1050. }
  1051. if (coupon.getType() == Coupon.Type.discount) {
  1052. couponMoney = needDeductMoney.multiply(BigDecimal.valueOf(1).subtract(coupon.getDiscount()));
  1053. //需支付金额
  1054. orderDon.setMoney(payMoney.subtract(couponMoney));
  1055. orderDon.setOriMoney(orderDon.getOriMoney().multiply(coupon.getDiscount()));
  1056. //记录折扣 用于分销
  1057. couponUser.setDiscount(coupon.getDiscount());
  1058. couponUserMapper.updateById(couponUser);
  1059. } else if (coupon.getType() == Coupon.Type.reduce) {
  1060. BigDecimal subtractMoney = payMoney.subtract(coupon.getReduceMoney());
  1061. orderDon.setMoney(subtractMoney);
  1062. couponMoney = coupon.getReduceMoney();
  1063. orderDon.setOriMoney(orderDon.getOriMoney().subtract(coupon.getReduceMoney()));
  1064. }
  1065. if (couponMoney != null && couponMoney.compareTo(first_money) >= 0) {
  1066. throw BusinessRuntimeException.getInstance("支付金额不能小于优惠金额");
  1067. }
  1068. orderDon.setCouponMoney(couponMoney);
  1069. orderDon.setCouponUserId(couponUser.getId());
  1070. orderDon.setCouponId(couponUser.getCouponId());
  1071. }
  1072. }
  1073. @Override
  1074. public boolean delUserPackages(Long userId) {
  1075. return claudeCodexApiService.delCodexUserPackages(userId);
  1076. }
  1077. @Override
  1078. public void handleClaudeCodeUserPackageSku(GroupsRelation relation, GoodsDonSku sku, Long orderId) {
  1079. //更新套餐
  1080. createOrUpdateCodexUserInfo(orderId, relation.getId(), relation.getUserId(), sku, 2);
  1081. }
  1082. /**
  1083. * 校验是否是codex用户
  1084. * @param userId
  1085. * @return
  1086. */
  1087. public Boolean checkCodexUser(Long userId) {
  1088. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  1089. Integer count = relationMapper.selectCountByGoodsId(userIdList, Constant.CODEX_GOODS_ID, DateTime.now());
  1090. return count > 0;
  1091. }
  1092. /**
  1093. * 获取codex用户id
  1094. */
  1095. public Long getCodexUserId(Long userId) {
  1096. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  1097. GroupsRelationMirrorView groupsRelationMirrorView = beanSearcher.searchFirst(GroupsRelationMirrorView.class, MapUtils.builder()
  1098. .field(GroupsRelationMirrorView::getUserId, userIdList).op(Operator.InList)
  1099. .field(GroupsRelationMirrorView::getGoodsId, Constant.CODEX_GOODS_ID)
  1100. .field(GroupsRelationMirrorView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
  1101. .build());
  1102. if (groupsRelationMirrorView != null) {
  1103. return groupsRelationMirrorView.getUserId();
  1104. }
  1105. return null;
  1106. }
  1107. }