Browse Source

feat(charge): 实现用户资金操作分布式锁防止并发冲突

- 在启动充电流程中新增获取分布式锁,防止用户资金操作冲突
- 频道方启动充电跳过用户资金锁,保持原有流程
- 定义用户资金操作锁常量及过期时间,统一锁资源管理
- 退款流程改用用户资金操作锁,防止退款与充电启动并发
- 退款时加入充电中订单状态校验,避免退款与充电状态冲突
- 充电订单设置车牌号,完善订单信息记录
wzq 3 days ago
parent
commit
efaa184af2

+ 18 - 12
src/main/java/com/zsElectric/boot/business/service/WFTOrderService.java

@@ -76,12 +76,18 @@ public class WFTOrderService {
     /**
      * 退款分布式锁key前缀
      */
-    private static final String REFUND_LOCK_KEY = "lock:refund:user:";
+    /**
+     * 用户资金操作统一锁key(充电启动、退款互斥)
+     */
+    public static final String USER_FUND_LOCK_KEY = "lock:user:fund:";
 
     /**
      * 退款锁过期时间(秒)
      */
-    private static final long REFUND_LOCK_EXPIRE = 60;
+    /**
+     * 用户资金操作锁过期时间(秒)
+     */
+    public static final long USER_FUND_LOCK_EXPIRE = 60;
 
 
     /**
@@ -802,17 +808,17 @@ public class WFTOrderService {
      */
     @Transactional(rollbackFor = Exception.class)
     public String refundOrder(Long userId, Integer type) throws Exception {
-        // 获取分布式锁,防止同一用户并发退款
-        String lockKey = REFUND_LOCK_KEY + userId;
+        // 获取用户资金操作统一锁,防止退款与充电启动并发
+        String lockKey = USER_FUND_LOCK_KEY + userId;
         RLock lock = redissonClient.getLock(lockKey);
 
         boolean locked = false;
         try {
-            // 尝试获取锁,等待0秒,锁过期时间60秒
-            locked = lock.tryLock(0, REFUND_LOCK_EXPIRE, TimeUnit.SECONDS);
+            // 尝试获取锁,等待0秒(不等待),锁过期时间60秒
+            locked = lock.tryLock(0, USER_FUND_LOCK_EXPIRE, TimeUnit.SECONDS);
             if (!locked) {
-                log.warn("用户:{}退款操作正在进行中,请勿重复提交", userId);
-                throw new BusinessException("退款操作正在进行中,请勿重复提交");
+                log.warn("用户:{}资金操作正在进行中,请勿重复提交", userId);
+                throw new BusinessException("操作正在进行中,请勿重复提交");
             }
 
             return doRefundOrder(userId, type);
@@ -855,14 +861,14 @@ public class WFTOrderService {
             throw new BusinessException("无法进行退款操作,请联系客服处理!");
         }
 
-        // 查询是否存在正在充电的订单
+        // 查询是否存在正在充电的订单(包含启动中、充电中、结算中,防止充电启动过程中并发退款绕过校验)
         List<ChargeOrderInfo> chargeOrderInfoList = chargeOrderInfoMapper.selectList(Wrappers.<ChargeOrderInfo>lambdaQuery()
                 .eq(ChargeOrderInfo::getUserId, userId)
-                .in(ChargeOrderInfo::getStatus, SystemConstants.STATUS_ONE, SystemConstants.STATUS_TWO)
+                .in(ChargeOrderInfo::getStatus, SystemConstants.STATUS_ZERO, SystemConstants.STATUS_ONE, SystemConstants.STATUS_TWO)
         );
         if (CollUtil.isNotEmpty(chargeOrderInfoList)) {
-            log.info("当前用户存在正在充电订单,无法进行退款操作!");
-            throw new BusinessException("当前用户存在正在充电订单,无法进行退款操作!");
+            log.info("当前用户存在正在进行中的充电订单,无法进行退款操作!");
+            throw new BusinessException("当前用户存在正在进行中的充电订单,无法进行退款操作!");
         }
 
         for (UserOrderInfo userOrderInfo : userOrderInfoList) {

+ 55 - 11
src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java

@@ -64,6 +64,12 @@ import com.zsElectric.boot.common.util.DateUtils;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.JsonNode;
 import org.springframework.transaction.annotation.Transactional;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import java.util.concurrent.TimeUnit;
+
+import static com.zsElectric.boot.business.service.WFTOrderService.USER_FUND_LOCK_KEY;
+import static com.zsElectric.boot.business.service.WFTOrderService.USER_FUND_LOCK_EXPIRE;
 
 /**
  * 充电订单信息服务实现类
@@ -106,6 +112,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
     private final ThirdPartyApiLogMapper thirdPartyApiLogMapper;
     private final ObjectMapper objectMapper;
     private final DictItemService dictItemService;
+    private final RedissonClient redissonClient;
 
     //充电订单号前缀
     private final String ORDER_NO_PREFIX = "CD";
@@ -196,21 +203,55 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
         log.info("启动充电开始,用户ID:{},设备认证流水号:{},充电桩编号:{}", SecurityUtils.getUserId(), formData.getEquipAuthSeq(), formData.getEquipmentId());
 
-        try {
-            AppChargeVO appInvokeChargeVO = new AppChargeVO();
-
-            //渠道方启动充电
-            if (Objects.equals(formData.getOrderType(), SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)) {
-
-                log.info("渠道方启动充电开始,用户ID:{},设备认证流水号:{},充电桩编号:{}", SecurityUtils.getUserId(), formData.getEquipAuthSeq(), formData.getEquipmentId());
+        // 渠道方启动充电不需要用户资金锁
+        if (Objects.equals(formData.getOrderType(), SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)) {
+            log.info("渠道方启动充电开始,用户ID:{},设备认证流水号:{},充电桩编号:{}", SecurityUtils.getUserId(), formData.getEquipAuthSeq(), formData.getEquipmentId());
+            try {
+                AppChargeVO appInvokeChargeVO = new AppChargeVO();
                 String orderNo = channelInvokeCharge(formData);
                 appInvokeChargeVO.setChargeOrderNo(orderNo);
                 return appInvokeChargeVO;
+            } catch (Exception e) {
+                log.error("渠道方启动充电失败", e);
+                throw new BusinessException("启动充电失败 !" + e.getMessage());
+            }
+        }
+
+        // 必要校验
+        Long userId = SecurityUtils.getUserId();
+        Assert.isTrue(userId != null, "用户ID不能为空");
+
+        // 获取用户资金操作统一锁,防止充电启动与退款并发
+        String lockKey = USER_FUND_LOCK_KEY + userId;
+        RLock lock = redissonClient.getLock(lockKey);
+
+        boolean locked = false;
+        try {
+            // 尝试获取锁,等待3秒,锁过期时间60秒
+            locked = lock.tryLock(3, USER_FUND_LOCK_EXPIRE, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("用户:{}资金操作正在进行中,无法启动充电", userId);
+                throw new BusinessException("操作正在进行中,请稍后重试");
+            }
+
+            return doInvokeCharge(formData, userId);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new BusinessException("启动充电失败,请稍后重试");
+        } finally {
+            // 释放锁
+            if (locked && lock.isHeldByCurrentThread()) {
+                lock.unlock();
             }
+        }
+    }
 
-            //必要校验
-            Long userId = SecurityUtils.getUserId();
-            Assert.isTrue(userId != null, "用户ID不能为空");
+    /**
+     * 执行启动充电逻辑(内部方法)
+     */
+    private AppChargeVO doInvokeCharge(AppInvokeChargeForm formData, Long userId) {
+        try {
+            AppChargeVO appInvokeChargeVO = new AppChargeVO();
 
             AppUserInfoVO userInfo = userInfoMapper.getAppletUserInfo(userId);
             Assert.isTrue(userInfo != null, "用户信息不存在");
@@ -371,7 +412,10 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
         chargeOrderInfo.setPreAmt(formData.getChannelPreAmt());
         //渠道方订单设置运营商ID
         chargeOrderInfo.setOperatorId(formData.getOperatorId());
-
+        //车牌号
+        if(ObjectUtil.isNotEmpty(formData.getPlateNum())) {
+            chargeOrderInfo.setPlateNum(formData.getPlateNum());
+        }
         //启动充电
         StartChargingRequestDTO requestDTO = new StartChargingRequestDTO();
         requestDTO