Jelajahi Sumber

fix: /pay/switch 组内切换走 retry 路径,修复 PAYMENT_ATTEMPT_EXISTS 死锁

线上事故:订单 5(Apple Pay)→2(信用卡) 切换,payType 已回写但 OMG create
发现旧激活尝试直接拒绝(OmgPaymentBusinessException: PAYMENT_ATTEMPT_EXISTS),
且该异常非 ServiceException 落入 catch-all 显示通用失败文案,用户重试同方式
仍死锁。

- createPayment OMG 路径改走 OmgPaymentRetryService.retry:先向网关核实旧
  尝试未付再替换重建(PAID 拒绝防双付;无尝试/已失败回落 create)
- 控制器补 OmgPaymentBusinessException 翻译:按 getMessageKey 输出渠道
  业务文案 + 错误码,不再落通用文案误导排障
- 测试:OMG 路径用例改断言 retry 路径(含组内切换场景);8/8 通过
- 已知基线红(createRejectsUnsupportedPayType)与本次无关
qmj 3 hari lalu
induk
melakukan
6eb29ca198

+ 6 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/pay/PayMethodSwitchController.java

@@ -48,6 +48,12 @@ public class PayMethodSwitchController {
         } catch (ServiceException exception) {
         } catch (ServiceException exception) {
             // 业务拒绝(订单不可支付/方式非法/闸门限制)携带国际化文案原样返回
             // 业务拒绝(订单不可支付/方式非法/闸门限制)携带国际化文案原样返回
             return AjaxResult.error(exception.getMessage());
             return AjaxResult.error(exception.getMessage());
+        } catch (com.ruoyi.app.omgpay.OmgPaymentBusinessException exception) {
+            // 渠道业务拒绝(如已有激活尝试)按 OMG 既有约定翻译,不再落通用文案误导排障
+            logger.warn("pay switch rejected by channel orderId={}, code={}",
+                    request == null ? null : request.getOrderId(), exception.getCode());
+            return AjaxResult.error(MessageUtils.message(exception.getMessageKey()),
+                    new com.ruoyi.app.omgpay.dto.OmgPaymentErrorResponse(exception.getCode().name()));
         } catch (Exception exception) {
         } catch (Exception exception) {
             // 兜底前必须留痕:真实异常(网关调用/NPE/SQL)只有这里能看到,不打日志等于盲修
             // 兜底前必须留痕:真实异常(网关调用/NPE/SQL)只有这里能看到,不打日志等于盲修
             logger.error("pay switch failed orderId={}",
             logger.error("pay switch failed orderId={}",

+ 8 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/pay/PayMethodSwitchService.java

@@ -2,6 +2,7 @@ package com.ruoyi.app.pay;
 
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.ruoyi.app.omgpay.OmgPaymentCreateService;
 import com.ruoyi.app.omgpay.OmgPaymentCreateService;
+import com.ruoyi.app.omgpay.OmgPaymentRetryService;
 import com.ruoyi.app.omgpay.OmgPaymentMethod;
 import com.ruoyi.app.omgpay.OmgPaymentMethod;
 import com.ruoyi.app.order.OrderLifecycleService;
 import com.ruoyi.app.order.OrderLifecycleService;
 import com.ruoyi.app.pay.dto.PayMethodSwitchRequest;
 import com.ruoyi.app.pay.dto.PayMethodSwitchRequest;
@@ -40,18 +41,18 @@ public class PayMethodSwitchService {
     private static final String PAY_TYPE_LINE_PAY = "3";
     private static final String PAY_TYPE_LINE_PAY = "3";
 
 
     private final IPosOrderService orderService;
     private final IPosOrderService orderService;
-    private final OmgPaymentCreateService omgPaymentCreateService;
+    private final OmgPaymentRetryService omgPaymentRetryService;
     private final LinePayService linePayService;
     private final LinePayService linePayService;
     private final OrderLogHelper orderLogHelper;
     private final OrderLogHelper orderLogHelper;
     private final IInfoUserService infoUserService;
     private final IInfoUserService infoUserService;
 
 
     public PayMethodSwitchService(IPosOrderService orderService,
     public PayMethodSwitchService(IPosOrderService orderService,
-                                  OmgPaymentCreateService omgPaymentCreateService,
+                                  OmgPaymentRetryService omgPaymentRetryService,
                                   LinePayService linePayService,
                                   LinePayService linePayService,
                                   OrderLogHelper orderLogHelper,
                                   OrderLogHelper orderLogHelper,
                                   IInfoUserService infoUserService) {
                                   IInfoUserService infoUserService) {
         this.orderService = orderService;
         this.orderService = orderService;
-        this.omgPaymentCreateService = omgPaymentCreateService;
+        this.omgPaymentRetryService = omgPaymentRetryService;
         this.linePayService = linePayService;
         this.linePayService = linePayService;
         this.orderLogHelper = orderLogHelper;
         this.orderLogHelper = orderLogHelper;
         this.infoUserService = infoUserService;
         this.infoUserService = infoUserService;
@@ -90,10 +91,12 @@ public class PayMethodSwitchService {
     /** 按新方式路由到对应渠道拉起支付;到付/现金无支付环节返回 null。 */
     /** 按新方式路由到对应渠道拉起支付;到付/现金无支付环节返回 null。 */
     private Object createPayment(Long userId, String orderId, String method) {
     private Object createPayment(Long userId, String orderId, String method) {
         if (OrderLifecycleService.isCardWalletPayType(method)) {
         if (OrderLifecycleService.isCardWalletPayType(method)) {
-            // 信用卡(2)/Apple Pay(5) 同组同商(023),都走 OMG 创建;payType 已回写,组内校验自然通过
+            // 信用卡(2)/Apple Pay(5) 同组同商(023),都走 OMG;走 retry 路径而非裸 create:
+            // 组内切换(如 5->2)时旧尝试仍激活,retry 会先向网关核实未付再替换重建,
+            // 避免 PAYMENT_ATTEMPT_EXISTS 拒绝(payType 已回写,组内校验自然通过)
             OmgPaymentMethod omgMethod = OrderLifecycleService.PAY_TYPE_APPLE_PAY.equals(method)
             OmgPaymentMethod omgMethod = OrderLifecycleService.PAY_TYPE_APPLE_PAY.equals(method)
                     ? OmgPaymentMethod.APPLE_PAY : OmgPaymentMethod.CREDIT;
                     ? OmgPaymentMethod.APPLE_PAY : OmgPaymentMethod.CREDIT;
-            return omgPaymentCreateService.create(userId, orderId, omgMethod).response();
+            return omgPaymentRetryService.retry(userId, orderId, omgMethod).response();
         }
         }
         if (PAY_TYPE_LINE_PAY.equals(method)) {
         if (PAY_TYPE_LINE_PAY.equals(method)) {
             return linePayService.create(userId, orderId);
             return linePayService.create(userId, orderId);

+ 19 - 17
ruoyi-admin/src/test/java/com/ruoyi/app/pay/PayMethodSwitchServiceTest.java

@@ -3,6 +3,7 @@ package com.ruoyi.app.pay;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.ruoyi.app.omgpay.OmgPaymentCreateOutcome;
 import com.ruoyi.app.omgpay.OmgPaymentCreateOutcome;
 import com.ruoyi.app.omgpay.OmgPaymentCreateService;
 import com.ruoyi.app.omgpay.OmgPaymentCreateService;
+import com.ruoyi.app.omgpay.OmgPaymentRetryService;
 import com.ruoyi.app.omgpay.OmgPaymentMethod;
 import com.ruoyi.app.omgpay.OmgPaymentMethod;
 import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
 import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
 import com.ruoyi.app.pay.dto.PayMethodSwitchRequest;
 import com.ruoyi.app.pay.dto.PayMethodSwitchRequest;
@@ -90,7 +91,7 @@ class PayMethodSwitchServiceTest {
     @Test
     @Test
     void switchLinePayOrderToOfflineUpdatesPayTypeWritesLogAndSkipsPayment() {
     void switchLinePayOrderToOfflineUpdatesPayTypeWritesLogAndSkipsPayment() {
         IPosOrderService orderService = mock(IPosOrderService.class);
         IPosOrderService orderService = mock(IPosOrderService.class);
-        OmgPaymentCreateService omgPaymentCreateService = mock(OmgPaymentCreateService.class);
+        OmgPaymentRetryService omgPaymentRetryService = mock(OmgPaymentRetryService.class);
         LinePayService linePayService = mock(LinePayService.class);
         LinePayService linePayService = mock(LinePayService.class);
         OrderLogHelper orderLogHelper = mock(OrderLogHelper.class);
         OrderLogHelper orderLogHelper = mock(OrderLogHelper.class);
         IInfoUserService infoUserService = mock(IInfoUserService.class);
         IInfoUserService infoUserService = mock(IInfoUserService.class);
@@ -102,7 +103,7 @@ class PayMethodSwitchServiceTest {
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.updateById(any(PosOrder.class))).thenReturn(true);
         when(orderService.updateById(any(PosOrder.class))).thenReturn(true);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                omgPaymentCreateService, linePayService, orderLogHelper, infoUserService);
+                omgPaymentRetryService, linePayService, orderLogHelper, infoUserService);
 
 
         AjaxResult result = service.switchMethod(7L, request("dd-1", "1"));
         AjaxResult result = service.switchMethod(7L, request("dd-1", "1"));
 
 
@@ -122,7 +123,7 @@ class PayMethodSwitchServiceTest {
         order.setType(0L);
         order.setType(0L);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                mock(OmgPaymentCreateService.class), mock(LinePayService.class),
+                mock(OmgPaymentRetryService.class), mock(LinePayService.class),
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
 
 
         ServiceException exception = assertThrows(ServiceException.class,
         ServiceException exception = assertThrows(ServiceException.class,
@@ -135,22 +136,23 @@ class PayMethodSwitchServiceTest {
     @SuppressWarnings({"unchecked", "rawtypes"})
     @SuppressWarnings({"unchecked", "rawtypes"})
     void switchToCreditCardRewritesPayTypeBeforeOmgCreate() {
     void switchToCreditCardRewritesPayTypeBeforeOmgCreate() {
         IPosOrderService orderService = mock(IPosOrderService.class);
         IPosOrderService orderService = mock(IPosOrderService.class);
-        OmgPaymentCreateService omgPaymentCreateService = mock(OmgPaymentCreateService.class);
-        when(omgPaymentCreateService.create(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT)))
+        OmgPaymentRetryService omgPaymentRetryService = mock(OmgPaymentRetryService.class);
+        when(omgPaymentRetryService.retry(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT)))
                 .thenReturn(new OmgPaymentCreateOutcome(mock(OmgCreatePaymentResponse.class), 1L, "dd-1", 7L, 5L, 90, "abcde***fghi"));
                 .thenReturn(new OmgPaymentCreateOutcome(mock(OmgCreatePaymentResponse.class), 1L, "dd-1", 7L, 5L, 90, "abcde***fghi"));
         PosOrder order = baseOrder();
         PosOrder order = baseOrder();
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.updateById(any(PosOrder.class))).thenReturn(true);
         when(orderService.updateById(any(PosOrder.class))).thenReturn(true);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                omgPaymentCreateService, mock(LinePayService.class), mock(OrderLogHelper.class),
+                omgPaymentRetryService, mock(LinePayService.class), mock(OrderLogHelper.class),
                 mock(IInfoUserService.class));
                 mock(IInfoUserService.class));
 
 
         AjaxResult result = service.switchMethod(7L, request("dd-1", "2"));
         AjaxResult result = service.switchMethod(7L, request("dd-1", "2"));
 
 
-        // 顺序约束:必须先回写 payType 再调 OMG 创建,否则创建校验按旧 payType 拒绝
-        InOrder inOrder = inOrder(orderService, omgPaymentCreateService);
+        // 顺序约束:先回写 payType 再拉起渠道;走 retry 路径:旧 Apple Pay 尝试仍激活时
+        // 替换重建而非 PAYMENT_ATTEMPT_EXISTS 死锁(组内 5->2 切换线上事故场景)
+        InOrder inOrder = inOrder(orderService, omgPaymentRetryService);
         inOrder.verify(orderService).updateById(any(PosOrder.class));
         inOrder.verify(orderService).updateById(any(PosOrder.class));
-        inOrder.verify(omgPaymentCreateService).create(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT));
+        inOrder.verify(omgPaymentRetryService).retry(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT));
         Map<?, ?> data = (Map<?, ?>) result.get("data");
         Map<?, ?> data = (Map<?, ?>) result.get("data");
         assertEquals("2", data.get("payType"));
         assertEquals("2", data.get("payType"));
         assertNotNull(data.get("payParams"), "在线方式应带渠道支付参数");
         assertNotNull(data.get("payParams"), "在线方式应带渠道支付参数");
@@ -159,20 +161,20 @@ class PayMethodSwitchServiceTest {
     @Test
     @Test
     void sameMethodSkipsRewriteButStillDelegates() {
     void sameMethodSkipsRewriteButStillDelegates() {
         IPosOrderService orderService = mock(IPosOrderService.class);
         IPosOrderService orderService = mock(IPosOrderService.class);
-        OmgPaymentCreateService omgPaymentCreateService = mock(OmgPaymentCreateService.class);
-        when(omgPaymentCreateService.create(any(), anyString(), any(OmgPaymentMethod.class)))
+        OmgPaymentRetryService omgPaymentRetryService = mock(OmgPaymentRetryService.class);
+        when(omgPaymentRetryService.retry(any(), anyString(), any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentCreateOutcome(mock(OmgCreatePaymentResponse.class), 1L, "dd-1", 7L, 5L, 90, "abcde***fghi"));
                 .thenReturn(new OmgPaymentCreateOutcome(mock(OmgCreatePaymentResponse.class), 1L, "dd-1", 7L, 5L, 90, "abcde***fghi"));
         PosOrder order = baseOrder();
         PosOrder order = baseOrder();
         order.setPayType("2");
         order.setPayType("2");
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                omgPaymentCreateService, mock(LinePayService.class), mock(OrderLogHelper.class),
+                omgPaymentRetryService, mock(LinePayService.class), mock(OrderLogHelper.class),
                 mock(IInfoUserService.class));
                 mock(IInfoUserService.class));
 
 
         service.switchMethod(7L, request("dd-1", "2"));
         service.switchMethod(7L, request("dd-1", "2"));
 
 
         verify(orderService, never()).updateById(any(PosOrder.class));
         verify(orderService, never()).updateById(any(PosOrder.class));
-        verify(omgPaymentCreateService, times(1)).create(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT));
+        verify(omgPaymentRetryService, times(1)).retry(eq(7L), eq("dd-1"), eq(OmgPaymentMethod.CREDIT));
     }
     }
 
 
     @Test
     @Test
@@ -182,7 +184,7 @@ class PayMethodSwitchServiceTest {
         order.setPayStatus(1L);
         order.setPayStatus(1L);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                mock(OmgPaymentCreateService.class), mock(LinePayService.class),
+                mock(OmgPaymentRetryService.class), mock(LinePayService.class),
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
 
 
         ServiceException exception = assertThrows(ServiceException.class,
         ServiceException exception = assertThrows(ServiceException.class,
@@ -194,7 +196,7 @@ class PayMethodSwitchServiceTest {
     void unknownMethodIsRejectedBeforeOrderLookup() {
     void unknownMethodIsRejectedBeforeOrderLookup() {
         IPosOrderService orderService = mock(IPosOrderService.class);
         IPosOrderService orderService = mock(IPosOrderService.class);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                mock(OmgPaymentCreateService.class), mock(LinePayService.class),
+                mock(OmgPaymentRetryService.class), mock(LinePayService.class),
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
 
 
         ServiceException exception = assertThrows(ServiceException.class,
         ServiceException exception = assertThrows(ServiceException.class,
@@ -210,7 +212,7 @@ class PayMethodSwitchServiceTest {
         order.setUserId(8L);
         order.setUserId(8L);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                mock(OmgPaymentCreateService.class), mock(LinePayService.class),
+                mock(OmgPaymentRetryService.class), mock(LinePayService.class),
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
 
 
         ServiceException exception = assertThrows(ServiceException.class,
         ServiceException exception = assertThrows(ServiceException.class,
@@ -225,7 +227,7 @@ class PayMethodSwitchServiceTest {
         order.setParentDdId("dd-parent");
         order.setParentDdId("dd-parent");
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
         PayMethodSwitchService service = new PayMethodSwitchService(orderService,
-                mock(OmgPaymentCreateService.class), mock(LinePayService.class),
+                mock(OmgPaymentRetryService.class), mock(LinePayService.class),
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
                 mock(OrderLogHelper.class), mock(IInfoUserService.class));
 
 
         ServiceException exception = assertThrows(ServiceException.class,
         ServiceException exception = assertThrows(ServiceException.class,