2
0

2 Commits 59044113b8 ... cd7af78a42

Autor SHA1 Nachricht Datum
  skyline cd7af78a42 chore: 联系我们增加工作时间提示;充电状态定时任务补充注释 vor 1 Monat
  skyline 4cfa120f4e fix: 退款申请防重复提交,避免并发创建多条退款记录 vor 1 Monat

+ 10 - 1
charge-front/src/pages-user/wallet-refund/wallet-refund.vue

@@ -58,7 +58,7 @@
     </view>
     <style-bottom-view>
       <view class="pl-40 pr-40 pb-30 pt-30" style="background-color: #fff">
-        <style-button type="primary" @click="submit">提交申请</style-button>
+        <style-button type="primary" :disabled="submitting" @click="submit">提交申请</style-button>
       </view>
     </style-bottom-view>
     <style-result
@@ -94,6 +94,7 @@ const reasons = ref([
   "其他原因",
 ]);
 const success = ref(false);
+const submitting = ref(false);
 onLoad(() => {
   if (getApp<any>().globalData.user) {
     user.value = getApp<any>().globalData.user;
@@ -113,8 +114,13 @@ const inputReasonText = (e: any) => {
   reasonText.value = e.detail.value;
 };
 const submit = () => {
+  if (submitting.value) {
+    return;
+  }
+  submitting.value = true;
   uni.showLoading({
     title: "提交中",
+    mask: true,
   });
   let r = "";
   if (reason.value >= 0) {
@@ -138,6 +144,9 @@ const submit = () => {
         content: err.errMsg,
         showCancel: false
       })
+    })
+    .finally(() => {
+      submitting.value = false;
     });
 };
 const close = () => {

+ 1 - 1
charge-front/src/pages/user/user.vue

@@ -124,7 +124,7 @@ const menu = ref([
     icon: "/static/images/user/3.png",
   },
   {
-    title: "联系我们",
+    title: "联系我们(工作时间7:00-24:00)",
     path: "",
     icon: "/static/images/user/4.png",
   },

+ 5 - 0
entity/src/main/java/com/kym/entity/common/RedisKeys.java

@@ -45,4 +45,9 @@ public interface RedisKeys {
      * 站点白名单用户列表(站点ID -> 用户ID集合)
      */
     String STATION_WHITELIST_USERS = "STATION_WHITELIST_USERS:";
+
+    /**
+     * 用户退款申请分布式锁
+     */
+    String REFUND_LOCK = "REFUND_LOCK:";
 }

+ 1 - 0
miniapp/src/main/java/com/kym/miniapp/jobs/EquipmentChargeStatusJob.java

@@ -61,6 +61,7 @@ public class EquipmentChargeStatusJob {
         log.info("设备充电状态信息(订单信息)更新定时任务启动...");
         var orders = redisTemplate.opsForHash().values(RedisKeys.CHARGE_ORDER_EQUIP_CHARGE_STATUS);
         if (CommUtil.isNotEmptyAndNull(orders)) {
+            // 实际已结束订单
             var orderList = new ArrayList<ChargeOrder>();
             orders.forEach(order -> {
                 var chargeOrder = JSONObject.parseObject(order.toString(), ChargeOrder.class);

+ 39 - 12
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -14,6 +14,7 @@ import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.CommUtil;
 import com.kym.common.utils.LambadaTools;
 import com.kym.common.utils.OrderUtils;
+import com.kym.entity.common.RedisKeys;
 import com.kym.entity.miniapp.Account;
 import com.kym.entity.miniapp.*;
 import com.kym.service.admin.ActivityService;
@@ -46,6 +47,7 @@ import lombok.SneakyThrows;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
@@ -62,6 +64,7 @@ import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -101,11 +104,14 @@ public class WxPayServiceImpl implements WxPayService {
 
     private final UserRechargeRightsService userRechargeRightsService;
 
+    private final StringRedisTemplate stringRedisTemplate;
+
 
     public WxPayServiceImpl(WxPayConfig conf, WalletDetailService walletDetailService,
                             PayLogService payLogService, AccountService accountService, ChargeOrderService chargeOrderService,
                             RefundLogService refundLogService,
-                            PlatformApiService platformApiService, ActivityService activityService, UserRechargeRightsService userRechargeRightsService) {
+                            PlatformApiService platformApiService, ActivityService activityService, UserRechargeRightsService userRechargeRightsService,
+                            StringRedisTemplate stringRedisTemplate) {
         this.conf = conf;
         this.walletDetailService = walletDetailService;
         this.payLogService = payLogService;
@@ -115,6 +121,7 @@ public class WxPayServiceImpl implements WxPayService {
         this.platformApiService = platformApiService;
         this.activityService = activityService;
         this.userRechargeRightsService = userRechargeRightsService;
+        this.stringRedisTemplate = stringRedisTemplate;
     }
 
     /**
@@ -367,19 +374,36 @@ public class WxPayServiceImpl implements WxPayService {
     @Transactional(rollbackFor = Exception.class)
     public void applyWxRefund(String reason) {
         var userId = StpUtil.getLoginIdAsLong();
-        var chargeOrder = chargeOrderService.getChargingOrderByUserId(userId);
-        if (chargeOrder != null) {
-            throw new BusinessException("存在未完结的订单,请等待所有订单完结之后重试");
-        }
 
-        var account = accountService.getAccountByUserId(userId);
-        if (account.getBalance() <= 0) {
-            throw new BusinessException("账户余额不足,无需退款");
-        }
-        // 校验余额大于优惠金额
-        if (account.getBalance() <= account.getDiscountAmount()) {
-            throw new BusinessException("不可退金额高于钱包余额,不支持退款");
+        // 防重复提交:Redis分布式锁,同一用户10秒内只能提交一次退款申请
+        String lockKey = RedisKeys.REFUND_LOCK + userId;
+        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(locked)) {
+            throw new BusinessException("退款申请正在处理中,请勿重复提交");
         }
+        try {
+            // 防重复提交:检查是否已有处理中的退款申请
+            var existingRefund = refundLogService.lambdaQuery()
+                    .eq(RefundLog::getUserId, userId)
+                    .in(RefundLog::getStatus, RefundLog.STATUS_退款已申请, RefundLog.STATUS_退款处理中)
+                    .one();
+            if (existingRefund != null) {
+                throw new BusinessException("您已有退款申请正在处理中,请勿重复提交");
+            }
+
+            var chargeOrder = chargeOrderService.getChargingOrderByUserId(userId);
+            if (chargeOrder != null) {
+                throw new BusinessException("存在未完结的订单,请等待所有订单完结之后重试");
+            }
+
+            var account = accountService.getAccountByUserId(userId);
+            if (account.getBalance() <= 0) {
+                throw new BusinessException("账户余额不足,无需退款");
+            }
+            // 校验余额大于优惠金额
+            if (account.getBalance() <= account.getDiscountAmount()) {
+                throw new BusinessException("不可退金额高于钱包余额,不支持退款");
+            }
 
         // 将余额转移至冻结余额
         accountService.lambdaUpdate().setSql(" frozen_amount = (frozen_amount + balance) ,balance = 0").eq(Account::getUserId, userId).update();
@@ -467,6 +491,9 @@ public class WxPayServiceImpl implements WxPayService {
             LOGGER.error("退款异常:userId:{},退款金额:{},payLogs:{}", userId, originalRefundAmount, payLogs);
             throw new BusinessException("退款异常");
         }
+        } finally {
+            stringRedisTemplate.delete(lockKey);
+        }
     }