CodexServiceImpl.java 50 KB

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