Explorar o código

codex 抵扣 续费

zoujiajian hai 11 meses
pai
achega
fc0d45f82b
Modificáronse 16 ficheiros con 895 adicións e 15 borrados
  1. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/codex/CodexDeductRecordMapper.java
  2. 20 0
      netflix-dao/src/main/java/com/cyksj/mapper/codex/CodexUserRenewExpiryRecordMapper.java
  3. 23 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CodexDeductRecord.java
  4. 18 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CodexUser.java
  5. 27 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CodexUserRenewExpiryRecord.java
  6. 1 0
      netflix-dao/src/main/java/com/cyksj/model/entity/UserTicketClearedRecord.java
  7. 23 0
      netflix-dao/src/main/java/com/cyksj/model/request/CodexUpgradePayReq.java
  8. 23 0
      netflix-dao/src/main/java/com/cyksj/model/response/CodexDeductResp.java
  9. 32 0
      netflix-dao/src/main/resources/mapper/CodexUserRenewExpiryRecordMapper.xml
  10. 21 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java
  11. 20 0
      netflix-service/src/main/java/com/cyksj/service/codex/CodexService.java
  12. 476 2
      netflix-service/src/main/java/com/cyksj/service/codex/impl/CodexServiceImpl.java
  13. 77 0
      netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java
  14. 21 1
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
  15. 1 1
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
  16. 99 11
      netflix-web/src/main/java/com/cyksj/web/controller/codex/CodexController.java

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/codex/CodexDeductRecordMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.codex;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CodexDeductRecord;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexDeductRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 18:08
+ */
+public interface CodexDeductRecordMapper extends BaseMapper<CodexDeductRecord> {
+}

+ 20 - 0
netflix-dao/src/main/java/com/cyksj/mapper/codex/CodexUserRenewExpiryRecordMapper.java

@@ -0,0 +1,20 @@
+package com.cyksj.mapper.codex;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CodexUserRenewExpiryRecord;
+import com.cyksj.model.entity.OrderDon;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserRenewExpiryRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 17:12
+ */
+public interface CodexUserRenewExpiryRecordMapper extends BaseMapper<CodexUserRenewExpiryRecord> {
+	CodexUserRenewExpiryRecord getOtherRenewSku(@Param("orderId") Long orderId, @Param("relationId") Long relationId, @Param("userIds") List<Long> userIds);
+
+	List<OrderDon> getOtherRenewSkus(Long renewOrderId, Long relationId, List<Long> userIdList);
+}

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CodexDeductRecord.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexDeductRecord
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 18:07
+ */
+@Getter
+@Setter
+public class CodexDeductRecord extends BaseEntity{
+	private Long relationId;
+
+	private Long orderId;
+
+	/**
+	 * 抵扣状态 默认false
+	 */
+	private Boolean status;
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CodexUser.java

@@ -49,6 +49,24 @@ public class CodexUser extends BaseEntity {
      */
     private Date expiryTime;
 
+    /**
+     * 续费升级订单id
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Long renewOrderId;
+
+    /**
+     * 续费升级skuid
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Long renewSkuId;
+
+    /**
+     * 续费升级过期时间
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Date renewExpiryTime;
+
     /**
      * 是否重试更新
      */

+ 27 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CodexUserRenewExpiryRecord.java

@@ -0,0 +1,27 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserRenewExpiryRecord
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 17:11
+ */
+@Getter
+@Setter
+public class CodexUserRenewExpiryRecord extends BaseEntity{
+	private Long userId;
+
+	private Long relationId;
+
+	private Long renewSkuId;
+
+	private Long orderId;
+
+	/**
+	 * 是否失效 默认false
+	 */
+	private Boolean isValid;
+}

+ 1 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/UserTicketClearedRecord.java

@@ -67,6 +67,7 @@ public class UserTicketClearedRecord extends BaseEntity{
 		vip_refund("退款"),
 		ticket_replace("替换车票"),
 		vip_deduct("会员抵扣"),
+		codex_deduct("codex抵扣")
 		;
 		String desc;
 

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/request/CodexUpgradePayReq.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUpgradePayReq
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 17:54
+ */
+@Getter
+@Setter
+public class CodexUpgradePayReq {
+	private Long relationId;
+
+	private Long userId;
+
+	/**
+	 * 升级claude code规格id
+	 */
+	private Long skuId;
+}

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/response/CodexDeductResp.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.response;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexDeductResp
+ * 创建者: JavaZou
+ * 创建时间:2025/9/23 18:16
+ */
+@Getter
+@Setter
+public class CodexDeductResp {
+	private BigDecimal deductMoney;
+
+	/**
+	 * 是否可抵扣
+	 */
+	private Boolean isDeduct = Boolean.TRUE;
+}

+ 32 - 0
netflix-dao/src/main/resources/mapper/CodexUserRenewExpiryRecordMapper.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper">
+
+    <select id="getOtherRenewSku" resultType="com.cyksj.model.entity.CodexUserRenewExpiryRecord">
+        select orr.* from (select * from codex_user_renew_expiry_record where order_id != #{orderId}
+        and is_valid = 0
+        and user_id in
+        <foreach collection="userIds" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>) orr inner join order_don o on o.id = orr.order_id
+        and o.status not in ('close','noPayment','refund')
+        and o.order_type = 2 and o.relation_id = #{relationId}
+        inner join goods_don_sku sku on sku.id = o.sku_id
+        order by sku.months desc,sku.code_points desc
+        limit 1
+    </select>
+
+    <select id="getOtherRenewSkus" resultType="com.cyksj.model.entity.OrderDon">
+        select o.* from (select order_id from codex_user_renew_expiry_record where order_id != #{orderId}
+        and is_valid = 0
+        and user_id in
+        <foreach collection="userIds" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>) orr inner join order_don o on o.id = orr.order_id
+        and o.status not in ('close','noPayment','refund')
+        and o.order_type = 2 and o.relation_id = #{relationId}
+        inner join goods_don_sku sku on sku.id = o.sku_id
+        order by sku.months desc,sku.gpt_limit_num desc
+    </select>
+</mapper>

+ 21 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java

@@ -399,6 +399,7 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 			isPay = Boolean.TRUE;
 		}
 		if (codexUser != null) {
+			updateCodexUserIfTry(codexUser);
 			codexUserInfoView = CodexUserInfoView.builder().isPay(isPay).planName(codexUser.getPlanName()).openaiDailyLimit(codexUser.getOpenaiDailyLimit()).openaiQuota(codexUser.getOpenaiQuota()).relationId(codexUser.getRelationId()).build();
 		}
 
@@ -1072,4 +1073,24 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 			throw BusinessRuntimeException.getInstance("使用补充卡失败");
 		}
 	}
+
+	/**
+	 * 更新重试codexUser info
+	 */
+	private void updateCodexUserIfTry(CodexUser codexUser) {
+		if (codexUser.getIsRetry()) {
+			try {
+				ClaudeCodeResp codeUserPackage = codexService.createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
+				if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+					codexUser.setIsRetry(Boolean.TRUE);
+					codexUserMapper.updateById(codexUser);
+				} else {
+					codexUser.setIsRetry(Boolean.FALSE);
+					codexUserMapper.updateById(codexUser);
+				}
+			} catch (Exception e) {
+
+			}
+		}
+	}
 }

+ 20 - 0
netflix-service/src/main/java/com/cyksj/service/codex/CodexService.java

@@ -1,10 +1,17 @@
 package com.cyksj.service.codex;
 
+import com.cyksj.model.entity.CodexUser;
+import com.cyksj.model.entity.GoodsDonSku;
+import com.cyksj.model.entity.OrderDon;
+import com.cyksj.model.request.CodexUpgradePayReq;
 import com.cyksj.model.request.UpdateDailyLimitRequest;
 import com.cyksj.model.request.codex.CodexUserPackageReq;
+import com.cyksj.model.response.CodexDeductResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.CodexUserInfoView;
 
+import java.util.Date;
+
 /**
  * 项目名: yhlxj11111111
  * 文件名: CodexService
@@ -49,4 +56,17 @@ public interface CodexService {
     ClaudeCodeResp getJavaUserAnalytics(Long userId, String start, String end, Integer page, Integer limit, String order);
 
     void delCodexUser(Long userId, Long relationId);
+
+    void createOrUpdateCodexUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType);
+
+    /**
+     * 调用第三方api 更新codexUser
+     */
+	void updateCodexUserPackageInfo(CodexUser codexUser, Date expiryTime, GoodsDonSku finalSku);
+
+    ClaudeCodeResp createOrUpdateCodexUserPackageApi(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception;
+
+    CodexDeductResp getDeductMoney(Long userId, Long relationId);
+
+    OrderDon deductSubmitOrder(CodexUpgradePayReq payReq);
 }

+ 476 - 2
netflix-service/src/main/java/com/cyksj/service/codex/impl/CodexServiceImpl.java

@@ -2,22 +2,35 @@ package com.cyksj.service.codex.impl;
 
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.HttpResponse;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.http.Method;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.constant.Constant;
 import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.snowflake.Sequence;
 import com.cyksj.common.util.Jsons;
 import com.cyksj.common.util.StringUtil;
+import com.cyksj.dto.RedisKey;
 import com.cyksj.mapper.*;
+import com.cyksj.mapper.codex.CodexDeductRecordMapper;
+import com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper;
 import com.cyksj.model.entity.*;
+import com.cyksj.model.manage.views.GroupsRelationView;
+import com.cyksj.model.request.CodexUpgradePayReq;
 import com.cyksj.model.request.UpdateDailyLimitRequest;
 import com.cyksj.model.request.codex.CodexUserPackageReq;
+import com.cyksj.model.response.CodexDeductResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.CodexUserInfoView;
+import com.cyksj.redis.RedisService;
 import com.cyksj.service.codex.CodexService;
+import com.cyksj.service.groups.GroupsFuncService;
 import com.cyksj.service.user.UserBindRelationService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import com.github.rholder.retry.Retryer;
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
@@ -26,7 +39,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.*;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -54,6 +70,19 @@ public class CodexServiceImpl implements CodexService {
 
 	private final GroupsMapper groupsMapper;
 
+	private final CodexUserRenewExpiryRecordMapper codexUserRenewExpiryRecordMapper;
+
+	private final BeanSearcher beanSearcher;
+
+	private final OrderDonMapper orderDonMapper;
+
+	private final GroupsFuncService groupsFuncService;
+
+	private final RedisService redisService;
+
+	private final CodexDeductRecordMapper codexDeductRecordMapper;
+
+	private static final Sequence SEQUENCE = new Sequence(0);
 	@Override
 	public CodexUserInfoView getCodexUserInfo(Long userId) {
 		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
@@ -151,7 +180,8 @@ public class CodexServiceImpl implements CodexService {
 	/**
 	 * 创建或更新Codex用户套餐
 	 */
-	public ClaudeCodeResp createOrUpdateCodexUserPackage(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
+	@Override
+	public ClaudeCodeResp createOrUpdateCodexUserPackageApi(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
 		Map<String, Object> params = new HashMap<>();
 		params.put("plan_name", planName.replaceAll(";", ""));
 		params.put("openai_daily_limit", openaiDailyLimit);
@@ -169,13 +199,231 @@ public class CodexServiceImpl implements CodexService {
 		return codexResp;
 	}
 
+	@Override
+	public CodexDeductResp getDeductMoney(Long userId, Long relationId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexDeductResp codexDeductResp = new CodexDeductResp();
+		Integer selectCount = claudeCodeUserMapper.selectCount(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList));
+		if (selectCount > 0) {
+			codexDeductResp.setIsDeduct(Boolean.FALSE);
+			return codexDeductResp;
+		}
+		GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()
+				.field(GroupsRelationView::getId, relationId)
+				.field(GroupsRelationView::getGoodsId, Constant.CODEX_GOODS_ID)
+				.field(GroupsRelationView::getUserId, userIdList).op(Operator.InList)
+				.field(GroupsRelationView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
+				.build());
+		BigDecimal deductMoney;
+		if (groupsRelationView == null || groupsRelationView.getExpiryTime().before(DateTime.now())) {
+			deductMoney = BigDecimal.ZERO;
+		}
+		CodexUser codexUser = codexUserMapper.selectById(relationId);
+		deductMoney = getFinalDeductMoney(relationId, groupsRelationView.getSkuId(), codexUser.getRenewSkuId(), codexUser.getRenewOrderId(), codexUser.getRenewExpiryTime(), groupsRelationView.getStartTime(), groupsRelationView.getExpiryTime(), DateTime.now(), userIdList);
+		codexDeductResp.setDeductMoney(deductMoney);
+		return codexDeductResp;
+	}
+
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public OrderDon deductSubmitOrder(CodexUpgradePayReq payReq) {
+		Long userId = payReq.getUserId();
+		//codex 车票id
+		Long relationId = payReq.getRelationId();
+		CodexDeductResp codexDeductResp = getDeductMoney(userId, relationId);
+		if (!codexDeductResp.getIsDeduct()) {
+			throw BusinessRuntimeException.getInstance("你存在claude code 无法抵扣");
+		}
+		//抵扣金额
+		BigDecimal deductMoney = codexDeductResp.getDeductMoney();
+
+		GoodsDonSku sku = skuMapper.selectById(payReq.getSkuId());
+		if (sku == null || sku.getGoodsId() != Constant.CLAUDE_CODE_GOODS_ID || !sku.getIsCodex()) {
+			throw BusinessRuntimeException.getInstance("系统异常,请刷新页面重试");
+		}
+
+		int noPayCount = orderDonMapper.selectCount(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getGoodsId, sku.getGoodsId())
+				.eq(OrderDon::getStatus, OrderDon.Status.noPayment.toString()));
+		if (noPayCount > 0) {
+			throw new BusinessRuntimeException("您有未支付订单.");
+		}
+		String cacheKye = "codex_deduct_cache_key:" + relationId;
+		boolean b = redisService.setNx(cacheKye, relationId, 10L);
+		if (!b) {
+			throw BusinessRuntimeException.getInstance("系统繁忙,请刷新页面重试");
+		}
+
+		OrderDon orderDon;
+		try {
+			orderDon = new OrderDon();
+			orderDon.setGoodsId(sku.getGoodsId());
+			orderDon.setSkuId(sku.getId());
+			orderDon.setUserId(userId);
+			orderDon.setMoney(sku.getPrice().subtract(deductMoney));
+
+			/* 生成订单 */
+			String donNo = SEQUENCE.nextId().toString();
+			orderDon.setOrderNo(donNo);
+
+			//锁一个cc的车票id
+			GroupsRelation relation = getClaudeCodeRelation(userId, sku);
+			orderDon.setRelationId(relation.getId());
+			orderDonMapper.insert(orderDon);
+
+			//记录抵扣记录
+			CodexDeductRecord codexDeductRecord = new CodexDeductRecord();
+			codexDeductRecord.setOrderId(orderDon.getId());
+			codexDeductRecord.setRelationId(relationId);
+			codexDeductRecordMapper.insert(codexDeductRecord);
+		} finally {
+			if (redisService.hasKey(cacheKye)) {
+				redisService.del(cacheKye);
+			}
+		}
+		return orderDon;
+	}
+
+	private GroupsRelation getClaudeCodeRelation(Long userId, GoodsDonSku sku) {
+		//获取车票车位
+		GroupsRelation relation = getRelation(sku);
+		//锁定座位
+		setRelation(relation);
+		//校验座位
+		int update = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+				.set(GroupsRelation::getUserId, userId)
+				.eq(GroupsRelation::getUserId, 0)
+				.eq(GroupsRelation::getId, relation.getId()));
+		if (update == 0) {
+			throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
+		}
+		return relation;
+	}
+
+	private GroupsRelation getRelation(GoodsDonSku sku) {
+		GroupsRelation relation = null;
+		Integer maxNum = sku.getNum();
+		GroupsTrips groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.validity, maxNum, null, null);
+		if (groups == null) {
+			//待发车
+			groups = groupsMapper.selectTicketGroups(sku.getId(), GroupsTrips.Status.waiting, maxNum, null, null);
+		}
+		//新增空车队
+		if (groups == null) {
+			relation = groupsFuncService.createNewGroupsTrips(sku);
+		}
+		//获取车位
+		if (relation == null) {
+			//寻找快满编车队进行占座
+			relation = relationMapper.selectRandomOneRelationByGroupsId(groups.getId());
+			if (relation == null) {
+				throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
+			}
+		}
+		return relation;
+	}
+
+	public BigDecimal getFinalDeductMoney(Long relationId, Long relationSkuId, Long renewSkuId, Long renewOrderId, Date renewExpiryTime, Date relationStartTime, Date relationExpiryTime, DateTime now, List<Long> userIdList) {
+		BigDecimal deductMoney = BigDecimal.ZERO;
+		//一个月当30天算单价 往上取整
+		//续费规格
+		if (renewSkuId != null && renewExpiryTime.compareTo(now) > 0) {
+			OrderDon renewOrderDon = orderDonMapper.selectById(renewOrderId);
+			BigDecimal orderMoney = renewOrderDon.getMoney().add(Optional.ofNullable(renewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
+			GoodsDonSku presentSku = skuMapper.selectById(renewSkuId);
+			if (orderMoney.compareTo(BigDecimal.ZERO) == 0) {
+				orderMoney = presentSku.getPrice();
+			}
+			Long betweenDay = DateUtil.betweenDay(now, renewExpiryTime, false) + 1;
+			//当天购买的按规格原价抵扣
+			BigDecimal renewDeductMoney;
+			if (now.toDateStr().equals(DateUtil.format(renewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
+				renewDeductMoney = orderMoney;
+				deductMoney = deductMoney.add(renewDeductMoney);
+				if (presentSku.getDays() != null) {
+					relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -presentSku.getDays());
+				} else {
+					relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -presentSku.getMonths());
+				}
+			} else {
+				//此次续费规格单价
+				BigDecimal single = getPayOrderSingle(orderMoney, presentSku);
+				//该车票抵扣金额
+				renewDeductMoney = single.multiply(BigDecimal.valueOf(betweenDay));
+				log.info("codex车票id:{}目前续费升级规格可抵扣的金额为:{}", relationId, renewDeductMoney);
+				deductMoney = deductMoney.add(renewDeductMoney);
+				relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -betweenDay.intValue());
+			}
+			//上一个续费升级规格
+			List<OrderDon> orderDOns = codexUserRenewExpiryRecordMapper.getOtherRenewSkus(renewOrderId, relationId, userIdList);
+			OrderDon presentRenewOrderDon = renewOrderDon;
+			for (OrderDon thisRenewOrderDon : orderDOns) {
+				Long skuId = thisRenewOrderDon.getSkuId();
+				GoodsDonSku thisRenewSku = skuMapper.selectById(skuId);
+				BigDecimal thisOrderMoney = thisRenewOrderDon.getMoney().add(Optional.ofNullable(thisRenewOrderDon.getBalance()).orElse(BigDecimal.ZERO));
+				if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
+					thisOrderMoney = thisRenewSku.getPrice();
+				}
+				//当天购买的按规格原价抵扣
+				BigDecimal thisRenewDeductMoney = BigDecimal.ZERO;
+				if (now.toDateStr().equals(DateUtil.format(thisRenewOrderDon.getCreatedTime(), "yyyy-MM-dd"))) {
+					thisRenewDeductMoney = thisOrderMoney;
+					deductMoney = deductMoney.add(thisRenewDeductMoney);
+					if (thisRenewSku.getDays() != null) {
+						relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisRenewSku.getDays());
+					} else {
+						relationExpiryTime = DateUtil.offsetMonth(relationExpiryTime, -thisRenewSku.getMonths());
+					}
+				} else {
+					//此次续费订单过期时间
+					Date thisOrderExpiryTime;
+					if (thisRenewSku.getDays() != null) {
+						thisOrderExpiryTime = DateUtil.offsetDay(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getDays());
+					} else {
+						thisOrderExpiryTime = DateUtil.offsetMonth(thisRenewOrderDon.getCreatedTime(), thisRenewSku.getMonths());
+					}
+					//续费同规格不处理
+					if (!ObjectUtil.equal(thisRenewOrderDon.getSkuId(), presentRenewOrderDon.getSkuId())) {
+						//先扣除已使用的天数
+						//相差多少天
+						Long thisRenewBetDay = DateUtil.betweenDay(thisRenewOrderDon.getCreatedTime(), presentRenewOrderDon.getCreatedTime(), false);
+						//扣除已使用的
+						thisOrderExpiryTime = DateUtil.offsetDay(thisOrderExpiryTime, -thisRenewBetDay.intValue());
+					}
+					//此订单过期时间
+					if (thisOrderExpiryTime.compareTo(now) > 0) {
+						//距离过期还剩多少天
+						Long thisDeductBetweenDay = DateUtil.betweenDay(now, thisOrderExpiryTime, false) + 1;
+						if (thisDeductBetweenDay != 0) {
+							//此次续费规格单价
+							BigDecimal single = getPayOrderSingle(thisOrderMoney, thisRenewSku);
+							//该车票抵扣金额
+							thisRenewDeductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
+							deductMoney = deductMoney.add(thisRenewDeductMoney);
+						}
+						//目前级别续费订单
+						presentRenewOrderDon = thisRenewOrderDon;
+						relationExpiryTime = DateUtil.offsetDay(relationExpiryTime, -thisDeductBetweenDay.intValue());
+					}
+				}
+				log.info("codex车票id:{}目前次一级规格:{}可抵扣的金额为:{}", relationId, thisRenewSku.getSpecVal(), thisRenewDeductMoney);
+
+			}
+		}
+		if (relationExpiryTime.after(now)) {
+			//该车票抵扣金额
+			BigDecimal deductMoneyFromTicket = getDeductMoneyFromTicket(relationId, userIdList, relationSkuId, now, relationStartTime, relationExpiryTime);
+			deductMoney = deductMoney.add(deductMoneyFromTicket);
+		}
+		log.info("codex车票id:{}最终到期时间为:{}车票可返回 抵扣金额:{}", relationId, DateUtil.format(relationExpiryTime, "yyyy-MM-dd HH:mm:ss"), deductMoney);
+		return deductMoney;
+	}
 
 	private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
 		return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
 	}
 
 	/**
-	 * Codex POST请求 - 复制ClaudeCodeServiceImpl的实现模式
+	 * Codex POST请求
 	 */
 	public ClaudeCodeResp executeCodexPostApi(String url, String body) {
 		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
@@ -358,6 +606,113 @@ public class CodexServiceImpl implements CodexService {
 		}
 	}
 
+	@Override
+	public void createOrUpdateCodexUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType) {
+		//新增或续费升级
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList));
+		GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+		GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId());
+		Long thisCodeOriSkuId = relationSku.getId();
+		Long relationUserId = relation.getUserId();
+		//此时车票的套餐规格积分
+		Integer thisRelationDayilyLimit = null;
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
+		if (codexUser != null) {
+			thisRelationDayilyLimit = codexUser.getOpenaiDailyLimit();
+		}
+		Date expiryTime = relation.getExpiryTime();
+		Integer openaiDailyLimit = sku.getOpenaiDailyLimit();
+		Integer openaiQuota = sku.getOpenaiQuota();
+		Long skuId = sku.getId();
+		Boolean isUpgrade = false;
+		Date renewExpiryTime = null;
+		//若是续费升级 记录此时续费升级的过期时间
+		if (!ObjectUtil.equal(thisCodeOriSkuId, skuId)) {
+			//非车票原规格id 续费升级记录
+			saveCodexRenewExpiryRecord(orderId, relationUserId, relationId, skuId);
+			//是否需要修改当前claude code user 套餐等级
+			if (thisRelationDayilyLimit != null && thisRelationDayilyLimit <= openaiDailyLimit) {
+				if (sku.getDays() != null) {
+					renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
+				} else {
+					renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
+				}
+				isUpgrade = true;
+			}
+		}
+		//更新db codexUser
+		generateOrUpdateCodexUser(relationUserId, relationId, openaiDailyLimit, openaiQuota, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal(), userIdList);
+
+		try {
+			//获取最新的claude code user
+			codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
+			//生成codex或更新用户套餐
+			ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(relation.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), codexUser.getExpiryTime());
+			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+				//重试更新code userInfo数据
+				setCodexUserRetryUpdateInfo(relationId);
+				return;
+			}
+		} catch (Exception e) {
+			log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e));
+			//重试更新code userInfo数据
+			setCodexUserRetryUpdateInfo(relationId);
+		}
+	}
+
+	@Override
+	public void updateCodexUserPackageInfo(CodexUser codexUser, Date expiryTime, GoodsDonSku finalSku) {
+		try {
+			ClaudeCodeResp codeUserPackage = createOrUpdateCodexUserPackageApi(codexUser.getUserId(), codexUser.getPlanName(), codexUser.getOpenaiDailyLimit(), codexUser.getOpenaiQuota(), expiryTime);
+			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+				//重试标记
+				codexUser.setIsRetry(Boolean.TRUE);
+				codexUserMapper.updateById(codexUser);
+				return;
+			}
+		} catch (Exception e) {
+			codexUser.setIsRetry(Boolean.TRUE);
+			codexUserMapper.updateById(codexUser);
+		}
+	}
+
+	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) {
+		CodexUser codexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"))).orElse(new CodexUser());
+		if (isUpgrade) {
+			codexUser.setRenewOrderId(orderId);
+			codexUser.setRenewSkuId(renewSkuId);
+			codexUser.setRenewExpiryTime(renewExpiryTime);
+			codexUser.setPlanName(planName);
+			codexUser.setOpenaiDailyLimit(openaiDailyLimit);
+			codexUser.setOpenaiQuota(openaiQuota);
+		}
+		codexUser.setUserId(relationUserId);
+		codexUser.setRelationId(relationId);
+		codexUser.setExpiryTime(expiryTime);
+		if (codexUser.getId() == null) {
+			codexUser.setPlanName(planName);
+			codexUser.setOpenaiDailyLimit(openaiDailyLimit);
+			codexUser.setOpenaiQuota(openaiQuota);
+		}
+		if (codexUser.getId() == null) {
+			try {
+				codexUserMapper.insert(codexUser);
+			} catch (DuplicateKeyException e) {
+				log.info("插入codex user 重复");
+			}
+		} else codexUserMapper.updateById(codexUser);
+	}
+
+	private void saveCodexRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Long skuId) {
+		CodexUserRenewExpiryRecord renewExpiryRecord = new CodexUserRenewExpiryRecord();
+		renewExpiryRecord.setOrderId(orderId);
+		renewExpiryRecord.setUserId(relationUserId);
+		renewExpiryRecord.setRelationId(relationId);
+		renewExpiryRecord.setRenewSkuId(skuId);
+		codexUserRenewExpiryRecordMapper.insert(renewExpiryRecord);
+	}
+
 	/**
 	 * Codex DELETE请求
 	 */
@@ -413,4 +768,123 @@ public class CodexServiceImpl implements CodexService {
 			throw BusinessRuntimeException.getInstance("获取codex使用统计失败");
 		}
 	}
+
+	/**
+	 * 获取订单单价
+	 */
+	public BigDecimal getPayOrderSingle(BigDecimal orderMoney, GoodsDonSku sku) {
+		//此次续费规格单价
+		BigDecimal single;
+		//区分天/月
+		if (sku.getDays() != null) {
+			single = orderMoney.divide(BigDecimal.valueOf(sku.getDays()), 0, RoundingMode.HALF_UP);
+		} else {
+			single = orderMoney.divide(BigDecimal.valueOf(sku.getMonths()).multiply(BigDecimal.valueOf(30)), 0, RoundingMode.HALF_UP);
+		}
+		return single;
+	}
+
+
+	/**
+	 * 获取车票可抵扣金额
+	 */
+	public BigDecimal getDeductMoneyFromTicket(Long relationId, List<Long> userIdList, Long skuId, DateTime now, Date relationStartTime, Date relationExpiryTime) {
+		List<OrderDon> orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
+				.eq(OrderDon::getRelationId, relationId)
+				.in(OrderDon::getUserId, userIdList)
+				.eq(OrderDon::getSkuId, skuId)
+				.notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
+				.orderByDesc(OrderDon::getId));
+		//部分退款
+		if (orderDonList.isEmpty()) {
+			orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
+					.eq(OrderDon::getRelationId, relationId)
+					.in(OrderDon::getUserId, userIdList)
+					.eq(OrderDon::getSkuId, skuId)
+					.notIn(OrderDon::getStatus, Constant.noOrderStatus)
+					.orderByDesc(OrderDon::getId));
+		}
+		//原车票订单总实付金额
+		BigDecimal totalOrderMoney = BigDecimal.ZERO;
+		//原车票订单总天数
+		Integer totalDays = 0;
+		for (OrderDon orderDon : orderDonList) {
+			GoodsDonSku sku = skuMapper.selectById(skuId);
+			BigDecimal thisOrderMoney = orderDon.getMoney().add(Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO)).subtract(Optional.ofNullable(orderDon.getRefundMoney()).orElse(BigDecimal.ZERO));
+			if (thisOrderMoney.compareTo(BigDecimal.ZERO) == 0) {
+				thisOrderMoney = sku.getPrice();
+			}
+			totalOrderMoney = totalOrderMoney.add(thisOrderMoney);
+			//总时间
+			if (sku.getDays() != null) {
+				totalDays += sku.getDays();
+			} else {
+				totalDays += sku.getMonths() * 30;
+			}
+			//原订单数
+			if (orderDonList.size() == 1) {
+				//此次订单过期时间
+				Date thisOrderExpiryTime;
+				if (sku.getDays() != null) {
+					thisOrderExpiryTime = DateUtil.offsetDay(orderDon.getCreatedTime(), sku.getDays());
+				} else {
+					thisOrderExpiryTime = DateUtil.offsetMonth(orderDon.getCreatedTime(), sku.getMonths());
+				}
+				//当天购买的按规格原价抵扣
+				if (now.toDateStr().equals(DateUtil.format(relationStartTime, "yyyy-MM-dd")) && (DateUtil.betweenDay(thisOrderExpiryTime, relationExpiryTime, true) == 0)) {
+					return thisOrderMoney;
+				}
+			}
+		}
+		Long thisDeductBetweenDay = DateUtil.betweenDay(now, relationExpiryTime, false) + 1;
+		BigDecimal single = totalOrderMoney.divide(BigDecimal.valueOf(totalDays), 0, RoundingMode.HALF_UP);
+		//该车票抵扣金额
+		BigDecimal deductMoney = single.multiply(BigDecimal.valueOf(thisDeductBetweenDay));
+		return deductMoney;
+	}
+
+	public void setRelation(GroupsRelation relation) {
+		Long relationId = relation.getId();
+		Long userId = relation.getUserId();
+		Long groupsId = relation.getGroupsId();
+		GroupsRelation.Status status = relation.getStatus();
+		String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
+		if (!redisService.setNx(relation_num_key, userId, 60 * 5L)) {
+			GroupsRelation dbRelation = relationMapper.selectById(relationId);
+			if (dbRelation.getUserId() != 0) {
+				log.info("=====>用户:{}未抢到座位:{}", userId, relationId);
+				throw new BusinessRuntimeException("系统繁忙,请重试...");
+			}
+		}
+		if (status == GroupsRelation.Status.outside) {
+			return;
+		}
+		int count = groupsMapper.decrAvailableNum(groupsId);
+		if (count == 0) {
+			log.error("车位异常 groupId:{}", groupsId);
+			groupsMapper.updateAvailableNum(groupsId);
+			//清除座位缓存key
+			delRelationKey(relationId, userId);
+			throw new BusinessRuntimeException("车位异常");
+		}
+	}
+
+	/**
+	 * 清除座位缓存key
+	 */
+	public void delRelationKey(Long relationId, Long userId) {
+		String relation_num_key = RedisKey.GROUPS_RELATION_NUM_KEY + relationId;
+		Object object = redisService.get(relation_num_key);
+		if (object != null) {
+			try {
+				Long keyUserId = Long.parseLong(object.toString());
+				//清除车票key
+				if (ObjectUtil.equal(userId, keyUserId)) {
+					redisService.del(relation_num_key);
+				}
+			} catch (Exception e) {
+				log.error("删除缓存key错误:{}", StringUtil.getErrorText(e));
+			}
+		}
+	}
 }

+ 77 - 0
netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java

@@ -7,9 +7,11 @@ import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.constant.Constant;
 import com.cyksj.mapper.*;
+import com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper;
 import com.cyksj.model.entity.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.codex.CodexService;
 import com.cyksj.service.mange.MirrorUserCleatService;
 import com.cyksj.service.user.UserBindRelationService;
 import lombok.RequiredArgsConstructor;
@@ -68,6 +70,10 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	private final CodexUserMapper codexUserMapper;
 
+	private final CodexUserRenewExpiryRecordMapper codexUserRenewExpiryRecordMapper;
+
+	private final CodexService codexService;
+
 	/**
 	 * 删除镜像User
 	 */
@@ -139,6 +145,77 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 		Long goodsId = orderDon.getGoodsId();
 		Long orderId = orderDon.getId();
 		List<Long> userIdList = userBindRelationService.getRelationUserIdList(orderDon.getUserId(), null);
+		//codex user
+		if (goodsId == Constant.CODEX_GOODS_ID) {
+			CodexUserRenewExpiryRecord renewExpiryRecord = codexUserRenewExpiryRecordMapper.selectOne(Wrappers.lambdaQuery(CodexUserRenewExpiryRecord.class).eq(CodexUserRenewExpiryRecord::getOrderId, orderId).eq(CodexUserRenewExpiryRecord::getIsValid, Boolean.FALSE).last("limit 1"));
+			if (renewExpiryRecord != null) {
+				//将续费有效置为过期
+				renewExpiryRecord.setIsValid(true);
+				codexUserRenewExpiryRecordMapper.updateById(renewExpiryRecord);
+			}
+			CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class)
+					.eq(CodexUser::getRelationId, orderDon.getRelationId())
+					.last("limit 1"));
+			if (codexUser == null) {
+				return;
+			}
+			Long presentRenewSkuId = codexUser.getRenewSkuId();
+			if (presentRenewSkuId == null) {
+				return;
+			}
+			GroupsRelation relation = groupsRelationMapper.selectById(orderDon.getRelationId());
+			GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+			GoodsDonSku finalSku = skuMapper.selectById(groupsTrips.getSkuId());
+			Date renewExpiryTime = null;
+			//当前续费订单id
+			Long presentUserCodePackageOrderId = codexUser.getRenewOrderId();
+			codexUser.setRenewSkuId(null);
+			codexUser.setRenewOrderId(null);
+			if (renewExpiryRecord != null) {
+				//当前续费
+				OrderDon presentRenewOrderDon = orderDonMapper.selectById(presentUserCodePackageOrderId);
+				//是否存在其他续费升级
+				CodexUserRenewExpiryRecord otherRenewRecord = codexUserRenewExpiryRecordMapper.getOtherRenewSku(orderDon.getId(), relation.getId(), userIdList);
+				GoodsDonSku otherRenewSku = null;
+				if (otherRenewRecord != null) {
+					otherRenewSku = skuMapper.selectById(otherRenewRecord.getRenewSkuId());
+				}
+				//续费升级
+				if (otherRenewSku != null) {
+					finalSku = otherRenewSku;
+					if (otherRenewSku.getDays() != null && otherRenewSku.getDays() > 0) {
+						renewExpiryTime = DateUtil.offsetDay(DateTime.now(), otherRenewSku.getDays());
+					} else {
+						renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), otherRenewSku.getMonths());
+					}
+					//相差多少天
+					Long betDay = 0l;
+					if (presentRenewOrderDon != null && presentRenewOrderDon.getCreatedTime().compareTo(otherRenewRecord.getCreatedTime()) > 0) {
+						betDay = DateUtil.betweenDay(otherRenewRecord.getCreatedTime(), presentRenewOrderDon.getCreatedTime(), false);
+					}
+					renewExpiryTime = DateUtil.offsetDay(renewExpiryTime, -betDay.intValue());
+
+					codexUser.setRenewOrderId(otherRenewRecord.getOrderId());
+					codexUser.setRenewSkuId(otherRenewRecord.getRenewSkuId());
+				}
+			}
+			//非当前套餐续费订单id 不修改claudeCode
+			if (!ObjectUtil.equal(presentUserCodePackageOrderId, orderId)) {
+				return;
+			}
+			//还原镜像user
+			codexUser.setOpenaiDailyLimit(finalSku.getOpenaiDailyLimit());
+			codexUser.setOpenaiQuota(finalSku.getOpenaiQuota());
+			codexUser.setRenewExpiryTime(renewExpiryTime);
+			codexUser.setPlanName(finalSku.getSpecVal());
+			codexUserMapper.updateById(codexUser);
+			log.info("还原镜像codex user套餐成功");
+			//调用api更新codex用户信息
+			Date expiryTime = relation.getExpiryTime();
+			codexService.updateCodexUserPackageInfo(codexUser, expiryTime, finalSku);
+			return;
+		}
+
 		//claude code user
 		if (goodsId == Constant.CLAUDE_CODE_GOODS_ID) {
 			ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = claudeCodeUserRenewExpiryRecordMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUserRenewExpiryRecord.class).eq(ClaudeCodeUserRenewExpiryRecord::getOrderId, orderId).eq(ClaudeCodeUserRenewExpiryRecord::getIsValid, Boolean.FALSE).last("limit 1"));

+ 21 - 1
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -46,6 +46,7 @@ import com.cyksj.mapper.*;
 import com.cyksj.mapper.channel.ChannelPopularizeMapper;
 import com.cyksj.mapper.channel.ShopConfigMapper;
 import com.cyksj.mapper.channel.UserPayEquipmentAvailableBenefitsMapper;
+import com.cyksj.mapper.codex.CodexDeductRecordMapper;
 import com.cyksj.mapper.corp.CorpInteractiveUserOrderRelationMapper;
 import com.cyksj.mapper.corp.CorpUserMapper;
 import com.cyksj.mapper.corp.CorpUserTagMapper;
@@ -81,6 +82,7 @@ import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.codex.CodexService;
 import com.cyksj.service.coin.UserGalaxyCoinService;
 import com.cyksj.service.corp.WxCorpOps;
 import com.cyksj.service.coupon.CouponFontService;
@@ -395,7 +397,9 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 	private final UserTicketClearedRecordMapper userTicketClearedRecordMapper;
 
-	private final CodexUserMapper codexUserMapper;
+	private final CodexService codexService;
+
+	private final CodexDeductRecordMapper codexDeductRecordMapper;
 
 	private final List<String> noChekGoodsTemp = List.of("Apple One", "Youtube", "Spotify");
 
@@ -3061,6 +3065,17 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 			return;
 		}
 
+		//codex 抵扣
+		CodexDeductRecord codexDeductRecord = codexDeductRecordMapper.selectOne(Wrappers.lambdaQuery(CodexDeductRecord.class).eq(CodexDeductRecord::getOrderId, orderDon.getId()).last("limit 1"));
+		if (codexDeductRecord != null) {
+			//codex 抵扣车票id
+			Long relationId = codexDeductRecord.getRelationId();
+			//清除codex 抵扣的车票
+			groupRelationClearService.clearTicket(groupsRelationMapper.selectById(relationId), UserTicketClearedRecord.Source.codex_deduct);
+			codexDeductRecord.setStatus(Boolean.TRUE);
+			codexDeductRecordMapper.updateById(codexDeductRecord);
+		}
+
 		if (orderDon.getIsNoLogin() != null && orderDon.getIsNoLogin()) {
 			orderDon.setStatus(OrderDon.Status.hasPayment);
 		}
@@ -3261,6 +3276,11 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 			//生成所需的claude code用户信息
 			claudeCodeService.generateOrUpdateClaudeCodeUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
 		}
+		//购买codex
+		if (orderDon.getGoodsId() == Constant.CODEX_GOODS_ID) {
+			codexService.createOrUpdateCodexUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
+		}
+
 		//GPT代充 回调兜底
 		if (orderDon.getGoodsId() == Constant.GPT_RECHARGE_GOODS_ID) {
 			GroupsRelation relation = groupsRelationMapper.selectById(orderDon.getRelationId());

+ 1 - 1
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -2158,7 +2158,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private BigDecimal getRenewDiscount(Long goodsId, Long skuId, Boolean isMirror, Integer skuNum) {
 		//镜像或者独立规格
-		if (goodsId == Constant.CLAUDE_CODE_GOODS_ID || isMirror || (skuNum != null && skuNum == 1)) {
+		if (goodsId == Constant.CODEX_GOODS_ID || goodsId == Constant.CLAUDE_CODE_GOODS_ID || isMirror || (skuNum != null && skuNum == 1)) {
 			RenewUpgradePackage renewUpgradePackage = renewUpgradePackageMapper.selectOne(Wrappers.lambdaQuery(RenewUpgradePackage.class)
 					.eq(RenewUpgradePackage::getGoodsId, goodsId)
 					.eq(RenewUpgradePackage::getStatus, 1)

+ 99 - 11
netflix-web/src/main/java/com/cyksj/web/controller/codex/CodexController.java

@@ -1,17 +1,40 @@
 package com.cyksj.web.controller.codex;
 
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.constant.Constant;
+import com.cyksj.common.util.Jsons;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.CodexUserMapper;
+import com.cyksj.mapper.GroupsMapper;
+import com.cyksj.mapper.GroupsRelationMapper;
+import com.cyksj.model.entity.CodexUser;
+import com.cyksj.model.entity.GroupsRelation;
+import com.cyksj.model.entity.GroupsTrips;
+import com.cyksj.model.entity.OrderDon;
+import com.cyksj.model.request.CodexUpgradePayReq;
 import com.cyksj.model.request.UpdateDailyLimitRequest;
+import com.cyksj.model.response.CodexDeductResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
-import com.cyksj.model.views.CodexUserInfoView;
+import com.cyksj.model.views.ClaudeCodePackageView;
+import com.cyksj.model.views.GoodsDonSkuView;
+import com.cyksj.model.views.RenewUpgradePackageFrontView;
 import com.cyksj.service.codex.CodexService;
+import com.cyksj.service.user.UserBindRelationService;
 import com.cyksj.web.util.StpUserUtil;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
+ * server/codex
  * server/codex
  * 项目名: yhlxj11111111
  * 文件名: CodexController
@@ -25,16 +48,16 @@ import org.springframework.web.bind.annotation.*;
 public class CodexController {
     
     private final CodexService codexService;
-    
-    /**
-     * 获取用户Codex信息
-     */
-    @GetMapping("/info")
-    public Result<CodexUserInfoView> getCodexUserInfo() {
-        long userId = StpUserUtil.getLoginIdAsLong();
-        CodexUserInfoView codexUserInfoView = codexService.getCodexUserInfo(userId);
-        return GatewayResponse.SUCCESS.newBuilder().toResult(codexUserInfoView);
-    }
+
+    private final UserBindRelationService userBindRelationService;
+
+    private final CodexUserMapper codexUserMapper;
+
+    private final GroupsRelationMapper relationMapper;
+
+    private final GroupsMapper groupsMapper;
+
+    private final BeanSearcher beanSearcher;
 
     /**
      * 获取用户OpenAI使用统计
@@ -77,4 +100,69 @@ public class CodexController {
         ClaudeCodeResp analytics = codexService.getJavaUserAnalytics(userId, start, end, page, limit, order);
         return analytics;
     }
+
+    /**
+     * 抵扣车票金额
+     */
+    @GetMapping("/get/deduct/money")
+    public Result<CodexDeductResp> getDeductMoney(Long relationId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        CodexDeductResp codexDeductResp = codexService.getDeductMoney(userId, relationId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(codexDeductResp);
+    }
+
+    /**
+     * codex 抵扣升级 预下单
+     */
+    @PostMapping("/deduct/submit/order")
+    public Result<OrderDon> deductSubmitOrder(CodexUpgradePayReq payReq) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        payReq.setUserId(userId);
+        OrderDon orderDon = codexService.deductSubmitOrder(payReq);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(orderDon);
+    }
+
+    /**
+     * codex续费升级 可选套餐
+     */
+    @GetMapping("/get/packages")
+    public Result<ClaudeCodePackageView> getClaudeCodePackages(String dsCode) {
+        Long userId = StpUserUtil.getUserIdAfterLogin();
+        ClaudeCodePackageView claudeCodePackageView = new ClaudeCodePackageView();
+        if (userId != null) {
+            List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+            CodexUser user = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class)
+                    .in(CodexUser::getUserId, userIdList)
+                    .last("limit 1"));
+            if (user != null) {
+                Integer openaiDailyLimit = user.getOpenaiDailyLimit();
+                GroupsRelation relation = relationMapper.selectById(user.getRelationId());
+                GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+                Long skuId = groupsTrips.getSkuId();
+                List<RenewUpgradePackageFrontView> renewUpgradePackageFrontViews = beanSearcher.searchAll(RenewUpgradePackageFrontView.class, MapUtils.builder().field(RenewUpgradePackageFrontView::getSkuId, skuId).build());
+                renewUpgradePackageFrontViews.forEach(renew -> {
+                    try {
+                        List<Long> upgradeSkuIds = Jsons.parseList(renew.getUpgradeSkuIds(), Long.class);
+                        if (user.getRenewSkuId() != null) {
+                            upgradeSkuIds.remove(user.getRenewSkuId());
+                        }
+                        List<RenewUpgradePackageFrontView.RenewSkuView> renewSkuViews = new ArrayList<>();
+                        if (CollUtil.isNotEmpty(upgradeSkuIds)) {
+                            renewSkuViews = beanSearcher.searchAll(RenewUpgradePackageFrontView.RenewSkuView.class, MapUtils.builder()
+                                    //高套餐
+                                    .field(RenewUpgradePackageFrontView.RenewSkuView::getOpenaiDailyLimit, openaiDailyLimit).op(Operator.GreaterThan)
+                                    .field(RenewUpgradePackageFrontView.RenewSkuView::getSkuId, upgradeSkuIds).op(Operator.InList)
+                                    .build());
+                        }
+                        renew.setRenewSkus(renewSkuViews);
+                    } catch (Exception e) {
+                    }
+                });
+                claudeCodePackageView.setUpgrades(renewUpgradePackageFrontViews);
+            }
+        }
+        List<GoodsDonSkuView> goodsDonSkuViews = beanSearcher.searchAll(GoodsDonSkuView.class, MapUtils.builder().field(GoodsDonSkuView::getGoodsId, Constant.CODEX_GOODS_ID).field(GoodsDonSkuView::getGoodsDonStatus, Boolean.TRUE).field(GoodsDonSkuView::getGoodsDonSkuStatus, Boolean.TRUE).field(GoodsDonSkuView::getUserOwns).op(Operator.IsNull).build());
+        claudeCodePackageView.setSkus(goodsDonSkuViews);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodePackageView);
+    }
 }