Explorar el Código

在生成user_reservation_order记录时,如果order_cost_type是免费的则生成verification_code,不生成verification_url

zhangchen hace 1 mes
padre
commit
2160ff19b6

+ 6 - 2
alien-store/src/main/java/shop/alien/store/service/RefundRecordAsyncService.java

@@ -1,11 +1,15 @@
 package shop.alien.store.service;
 
+import shop.alien.entity.store.MerchantPaymentOrder;
 import shop.alien.entity.store.RefundRecord;
 
 /**
- * 退款记录异步落库,避免阻塞退款接口响应
+ * 退款相关异步落库:更新支付单 + 保存退款记录,避免阻塞退款接口响应
  */
 public interface RefundRecordAsyncService {
 
-    void saveRefundRecordAsync(RefundRecord record);
+    /**
+     * 异步执行:先更新支付单状态,再保存退款记录(顺序保证)
+     */
+    void completeRefundAsync(MerchantPaymentOrder paymentOrder, RefundRecord record);
 }

+ 14 - 5
alien-store/src/main/java/shop/alien/store/service/impl/RefundRecordAsyncServiceImpl.java

@@ -4,28 +4,37 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+import shop.alien.entity.store.MerchantPaymentOrder;
 import shop.alien.entity.store.RefundRecord;
+import shop.alien.store.service.MerchantPaymentOrderService;
 import shop.alien.store.service.RefundRecordAsyncService;
 import shop.alien.store.service.RefundRecordService;
 
 /**
- * 退款记录异步落库,避免阻塞退款接口响应
+ * 退款相关异步落库:更新支付单 + 保存退款记录
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class RefundRecordAsyncServiceImpl implements RefundRecordAsyncService {
 
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordService refundRecordService;
 
     @Async("taskExecutor")
     @Override
-    public void saveRefundRecordAsync(RefundRecord record) {
+    public void completeRefundAsync(MerchantPaymentOrder paymentOrder, RefundRecord record) {
+        String outTradeNo = paymentOrder != null ? paymentOrder.getOutTradeNo() : (record != null ? record.getOutTradeNo() : null);
         try {
-            refundRecordService.save(record);
-            log.debug("退款记录异步保存成功 outTradeNo={}", record.getOutTradeNo());
+            if (paymentOrder != null) {
+                merchantPaymentOrderService.updateById(paymentOrder);
+            }
+            if (record != null) {
+                refundRecordService.save(record);
+            }
+            log.debug("退款异步落库成功 outTradeNo={}", outTradeNo);
         } catch (Exception e) {
-            log.error("退款记录异步保存失败 outTradeNo={}", record != null ? record.getOutTradeNo() : null, e);
+            log.error("退款异步落库失败 outTradeNo={}", outTradeNo, e);
         }
     }
 }

+ 5 - 0
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -25,6 +25,7 @@ import shop.alien.store.vo.ReservationOrderPageVo;
 import shop.alien.mapper.UserReservationMapper;
 import shop.alien.mapper.UserReservationTableMapper;
 import shop.alien.store.service.*;
+import shop.alien.util.common.UniqueRandomNumGenerator;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
@@ -127,6 +128,10 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
                         storeBookingSettingsService.getByStoreId(reservation.getStoreId()).getReservation()) ? 0 : 1);
         order.setIsMerchantReservation(0);
         fillOrderFromStoreSettings(order, reservation);
+        // 免费预订:生成核销码,不生成核销二维码 URL(付费订单在支付成功时再生成)
+        if (order.getOrderCostType() != null && order.getOrderCostType() == 0) {
+            order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
+        }
         return order;
     }
 

+ 15 - 2
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java

@@ -58,6 +58,10 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
     private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:alipay:prepay:order:";
     /** 预支付缓存过期时间(秒),与支付宝预支付单 15 分钟一致 */
     private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    /** 防抖 key 前缀:同一订单在有效期内仅允许一次创建预支付 */
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:alipay:prepay:debounce:order:";
+    /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
 
     private final StorePaymentConfigService storePaymentConfigService;
     private final UserReservationOrderService userReservationOrderService;
@@ -130,6 +134,14 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             }
         }
 
+        // 防抖:同一订单在有效期内只允许一次创建预支付,防止重复调用
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + orderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            log.warn("商户支付宝预支付防抖拦截,orderId={}", orderId);
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
         // 未命中缓存:先按订单ID+支付类型将原支付单逻辑删除,再生成新预支付(仅删除同 pay_type)
         int deleted = merchantPaymentOrderService.logicDeleteByOrderIdAndPayType(orderId, PaymentEnum.ALIPAY.getType());
         if (deleted > 0) {
@@ -327,7 +339,6 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
             paymentOrder.setPayStatus(3);
             paymentOrder.setUpdatedTime(now);
-            merchantPaymentOrderService.updateById(paymentOrder);
 
             order.setOrderStatus(7);
             order.setPaymentStatus(2);
@@ -349,8 +360,10 @@ public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrateg
             record.setStoreId(storeId);
             record.setUserId(order.getUserId());
             record.setCreatedTime(now);
+            record.setCreatedUserId(order.getUserId());
+            record.setUpdatedUserId(order.getUserId());
             record.setDeleteFlag(0);
-            refundRecordAsyncService.saveRefundRecordAsync(record);
+            refundRecordAsyncService.completeRefundAsync(paymentOrder, record);
 
             log.info("商户预订订单退款成功,outTradeNo={}", outTradeNo);
             return R.data("退款成功");

+ 61 - 6
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

@@ -50,6 +50,10 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     private static final String GETMETHOD = "GET";
     private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:wechat:prepay:order:";
     private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    /** 防抖 key 前缀:同一订单在有效期内仅允许一次创建预支付 */
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "merchant:wechat:prepay:debounce:order:";
+    /** 防抖有效期(秒),期内重复调用返回请勿重复提交 */
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
 
     @Value("${payment.wechatPay.host:https://api.mch.weixin.qq.com}")
     private String wechatPayApiHost;
@@ -121,6 +125,14 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             }
         }
 
+        // 防抖:同一订单在有效期内只允许一次创建预支付,防止重复调用
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + orderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            log.warn("商户微信预支付防抖拦截,orderId={}", orderId);
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
         int deleted = merchantPaymentOrderService.logicDeleteByOrderIdAndPayType(orderId, PaymentEnum.WECHAT_PAY.getType());
         if (deleted > 0) {
             log.info("未命中缓存,已逻辑删除该订单下微信支付单 {} 条,orderId={}", deleted, orderId);
@@ -199,19 +211,24 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
 
     @Override
     public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        log.info("queryPayStatus 开始 storeId={} outTradeNo={}", storeId, outTradeNo);
         if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=参数为空", storeId, outTradeNo);
             return R.fail("门店ID和商户订单号不能为空");
         }
         StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
         if (config == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=未配置支付参数", storeId, outTradeNo);
             return R.fail("该门店未配置支付参数");
         }
         MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
         if (paymentOrder == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=支付单不存在", storeId, outTradeNo);
             return R.fail("支付单不存在");
         }
         UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
         if (order == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=预订订单不存在 orderId={}", storeId, outTradeNo, paymentOrder.getOrderId());
             return R.fail("预订订单不存在");
         }
         try {
@@ -223,8 +240,10 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             req.mchid = config.getWechatMchId();
             WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = searchOrderRun(config, privateKey, wechatPayPublicKey, req);
             if (response == null) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=微信返回为空", storeId, outTradeNo);
                 return R.fail("查询失败");
             }
+            log.info("queryPayStatus 微信查单结果 storeId={} outTradeNo={} tradeState={} tradeStateDesc={}", storeId, outTradeNo, response.tradeState, response.tradeStateDesc);
             if ("SUCCESS".equals(response.tradeState)) {
                 Date now = new Date();
                 paymentOrder.setPayStatus(1);
@@ -255,20 +274,26 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
                     }
                 }
                 reservationOrderPaymentTimeoutService.cancelReservationOrderPaymentTimeout(order.getOrderSn());
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=success tradeState=SUCCESS orderId={}", storeId, outTradeNo, order.getId());
                 return R.success("支付成功");
             }
             if ("CLOSED".equals(response.tradeState)) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=交易已关闭", storeId, outTradeNo);
                 return R.fail("交易已关闭");
             }
             if ("NOTPAY".equals(response.tradeState) || "USERPAYING".equals(response.tradeState)) {
+                log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=等待用户付款 tradeState={}", storeId, outTradeNo, response.tradeState);
                 return R.fail("等待用户付款");
             }
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=其他状态 tradeState={} tradeStateDesc={}", storeId, outTradeNo, response.tradeState, response.tradeStateDesc);
             return R.fail("订单状态:" + (response.tradeStateDesc != null ? response.tradeStateDesc : response.tradeState));
         } catch (WXPayUtility.ApiException e) {
-            log.error("查询商户微信订单状态异常,outTradeNo={}", outTradeNo, e);
+            log.error("queryPayStatus 异常 storeId={} outTradeNo={} ApiException statusCode={} body={}", storeId, outTradeNo, e.getStatusCode(), e.getBody(), e);
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=ApiException", storeId, outTradeNo);
             return R.fail("查询异常:" + e.getMessage());
         } catch (Exception e) {
-            log.error("商户微信查询支付状态异常,storeId={}", storeId, e);
+            log.error("queryPayStatus 异常 storeId={} outTradeNo={}", storeId, outTradeNo, e);
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=Exception", storeId, outTradeNo);
             return R.fail("查询失败:" + e.getMessage());
         }
     }
@@ -311,9 +336,37 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             request.amount.total = order.getDepositAmount().multiply(new BigDecimal(100)).longValue();
             request.amount.currency = "CNY";
 
-            WeChatPaymentStrategyImpl.Refund response = refundRun(config, privateKey, wechatPayPublicKey, request);
+            WeChatPaymentStrategyImpl.Refund response = null;
+            WXPayUtility.ApiException lastApiException = null;
+            int maxAttempts = 3;
+            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+                try {
+                    response = refundRun(config, privateKey, wechatPayPublicKey, request);
+                    break;
+                } catch (WXPayUtility.ApiException e) {
+                    lastApiException = e;
+                    if (e.getStatusCode() == 429 && attempt < maxAttempts) {
+                        long delayMs = 2000L * attempt;
+                        log.warn("微信退款 429 限频,{}ms 后重试,第 {}/{} 次,outTradeNo={}", delayMs, attempt, maxAttempts, outTradeNo);
+                        try {
+                            Thread.sleep(delayMs);
+                        } catch (InterruptedException ie) {
+                            Thread.currentThread().interrupt();
+                            return R.fail("操作过于频繁,请稍后再试");
+                        }
+                    } else {
+                        if (e.getStatusCode() == 429) {
+                            break;
+                        }
+                        throw e;
+                    }
+                }
+            }
             if (response == null) {
-                return R.fail("退款失败");
+                String msg = lastApiException != null && lastApiException.getStatusCode() == 429
+                        ? (StringUtils.isNotBlank(lastApiException.getErrorMessage()) ? lastApiException.getErrorMessage() : "操作过于频繁,请稍后再试")
+                        : "退款失败";
+                return R.fail(msg);
             }
             String status = response.status != null ? response.status.name() : "";
             if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status)) {
@@ -324,7 +377,6 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
             paymentOrder.setPayStatus(3);
             paymentOrder.setUpdatedTime(now);
-            merchantPaymentOrderService.updateById(paymentOrder);
 
             order.setOrderStatus(7);
             order.setPaymentStatus(2);
@@ -350,12 +402,15 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             record.setCreatedUserId(order.getUserId());
             record.setUpdatedUserId(order.getUserId());
             record.setDeleteFlag(0);
-            refundRecordAsyncService.saveRefundRecordAsync(record);
+            refundRecordAsyncService.completeRefundAsync(paymentOrder, record);
 
             log.info("商户预订订单微信退款成功,outTradeNo={}", outTradeNo);
             return R.data("退款成功");
         } catch (WXPayUtility.ApiException e) {
             log.error("商户预订订单微信退款异常,outTradeNo={}", outTradeNo, e);
+            if (e.getStatusCode() == 429) {
+                return R.fail(StringUtils.isNotBlank(e.getErrorMessage()) ? e.getErrorMessage() : "操作过于频繁,请稍后再试");
+            }
             return R.fail("退款失败:" + e.getMessage());
         } catch (Exception e) {
             log.error("商户微信退款异常,storeId={}", storeId, e);