Просмотр исходного кода

feat(push): add user payment and cancellation scenes

qmj 1 неделя назад
Родитель
Сommit
1f3ba28db4

+ 4 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentNotifyService.java

@@ -6,6 +6,7 @@ import com.ruoyi.app.order.MerchantNotificationRouter;
 import com.ruoyi.app.order.dto.OrderPushBodyDto;
 import com.ruoyi.app.push.BusinessPushService;
 import com.ruoyi.app.push.RecipientApp;
+import com.ruoyi.app.push.UserNotificationScene;
 import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.utils.LocaleUtils;
@@ -232,7 +233,9 @@ public class OmgPaymentNotifyService {
                 Locale userLocale = LocaleUtils.getUserLocale(user.getUserId());
                 String title = MessageUtils.message("no.message.push.message", userLocale);
                 String content = MessageUtils.message("no.message.push.payment.success", userLocale);
-                String body = OrderPushBodyDto.getJson(order.getDdId(), String.valueOf(order.getState()), 0);
+                // 保留支付通知 type=0,不改变核销/幂等,仅增加前台用户语音场景。
+                String body = OrderPushBodyDto.getUserJson(order.getDdId(), String.valueOf(order.getState()),
+                        0, 1, UserNotificationScene.PAYMENT_SUCCEEDED);
                 pushEventService.PublisherEvent(user.getUserId(), title, content, body);
                 if (businessPushService != null) {
                     businessPushService.send(RecipientApp.USER, user.getUserId(), user.getCid(), title, content, body);

+ 18 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/order/DeliveryOrderNotificationService.java

@@ -3,6 +3,9 @@ package com.ruoyi.app.order;
 import com.ruoyi.app.order.dto.OrderPushBodyDto;
 import com.ruoyi.app.push.BusinessPushProperties;
 import com.ruoyi.app.push.NativeRiderBatchPushService;
+import com.ruoyi.app.push.UserNotificationScene;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.framework.manager.AsyncManager;
@@ -24,6 +27,7 @@ import java.util.TimerTask;
 /** 外送订单开放接单及取消后的推送。 */
 @Service
 public class DeliveryOrderNotificationService {
+    private static final Logger log = LoggerFactory.getLogger(DeliveryOrderNotificationService.class);
     private static final int RIDER_NOTIFICATION_LIMIT = 20;
 
     private final RiderPositionMapper riderPositionMapper;
@@ -167,8 +171,11 @@ public class DeliveryOrderNotificationService {
                     PayPush.shPushHandleLocal(push, pushEventService, recipient.getUserId(), recipient.getCid(),
                             "no.message.push.message", "no.message.push.order.cancelled", body, "", ddId);
                 } else {
+                    // 只有原链路实际通知的用户带场景,取消操作者和非用户通知范围不变。
+                    String userBody = OrderPushBodyDto.getUserJson(ddId, "4", 0, 1,
+                            UserNotificationScene.ORDER_CANCELLED);
                     PayPush.userPushHandleLocal(push, pushEventService, recipient.getUserId(), recipient.getCid(),
-                            "no.message.push.message", "no.message.push.order.cancelled", body, "", ddId);
+                            "no.message.push.message", "no.message.push.order.cancelled", userBody, "", ddId);
                 }
             }
         });
@@ -206,14 +213,22 @@ public class DeliveryOrderNotificationService {
     }
 
     private static void runAfterCommit(Runnable action) {
+        // 已提交的取消/开放接单不能被通知准备或调度失败反向影响。
+        Runnable safeAction = () -> {
+            try {
+                action.run();
+            } catch (Exception exception) {
+                log.error("外送订单通知准备失败", exception);
+            }
+        };
         if (!TransactionSynchronizationManager.isSynchronizationActive()) {
-            action.run();
+            safeAction.run();
             return;
         }
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             @Override
             public void afterCommit() {
-                action.run();
+                safeAction.run();
             }
         });
     }

+ 6 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/pay/LinePayOrderNotificationService.java

@@ -5,6 +5,7 @@ import com.ruoyi.app.order.DeliveryOrderNotificationService;
 import com.ruoyi.app.order.MerchantNotificationRouter;
 import com.ruoyi.app.push.BusinessPushService;
 import com.ruoyi.app.push.RecipientApp;
+import com.ruoyi.app.push.UserNotificationScene;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.utils.LocaleUtils;
 import com.ruoyi.common.utils.MessageUtils;
@@ -63,8 +64,11 @@ public class LinePayOrderNotificationService {
                 Locale userLocale = LocaleUtils.getUserLocale(user.getUserId());
                 String title = MessageUtils.message("no.message.push.message", userLocale);
                 String content = MessageUtils.message("no.message.push.payment.success", userLocale);
-                pushEventService.PublisherEvent(user.getUserId(), title, content, body);
-                businessPushService.send(RecipientApp.USER, user.getUserId(), user.getCid(), title, content, body);
+                // 用户场景不写入商家新单 body;入库和外发使用同一完整 JSON。
+                String userBody = OrderPushBodyDto.getUserJson(ddId, String.valueOf(order.getState()),
+                        0, 1, UserNotificationScene.PAYMENT_SUCCEEDED);
+                pushEventService.PublisherEvent(user.getUserId(), title, content, userBody);
+                businessPushService.send(RecipientApp.USER, user.getUserId(), user.getCid(), title, content, userBody);
             }
             if (Long.valueOf(0L).equals(order.getType())) {
                 deliveryOrderNotificationService.notifyOrderAvailable(order);

+ 6 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentNotifyServiceTest.java

@@ -201,8 +201,12 @@ class OmgPaymentNotifyServiceTest {
 
         assertTrue(service.process(signedRequest(1, 0, 100)));
 
-        verify(pushEvents).PublisherEvent(eq(9L), anyString(), anyString(), anyString());
-        verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), anyString(), anyString(), anyString());
+        var userBody = org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(pushEvents).PublisherEvent(eq(9L), anyString(), anyString(), userBody.capture());
+        verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), anyString(), anyString(), eq(userBody.getValue()));
+        var json = com.alibaba.fastjson.JSON.parseObject(userBody.getValue());
+        assertEquals("PAYMENT_SUCCEEDED", json.getString("eventScene"));
+        assertEquals(0, json.getIntValue("type"));
     }
 
     @Test

+ 59 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/DeliveryOrderNotificationServiceTest.java

@@ -24,6 +24,65 @@ import static org.mockito.Mockito.when;
 
 class DeliveryOrderNotificationServiceTest {
 
+    @Test
+    void cancellationSceneIsUserOnlyAndOnlySentAfterCommit() {
+        IInfoUserService users = mock(IInfoUserService.class);
+        DeliveryOrderNotificationService service = new DeliveryOrderNotificationService(
+                mock(RiderPositionMapper.class), mock(PushEventService.class), users, null);
+        PosOrder order = availableOrder(); order.setUserId(201L); order.setQsId(301L); order.setShId(101L);
+        java.util.List<InfoUser> recipients = new java.util.ArrayList<>();
+        for (long id : new long[]{201, 301, 101}) {
+            InfoUser recipient = new InfoUser(); recipient.setUserId(id); recipients.add(recipient);
+        }
+        when(users.listByIds(org.mockito.ArgumentMatchers.anyCollection())).thenReturn(recipients);
+        java.util.Map<String, com.alibaba.fastjson.JSONObject> bodies = new java.util.HashMap<>();
+        try (var spring = org.mockito.Mockito.mockStatic(com.ruoyi.common.utils.spring.SpringUtils.class)) {
+            spring.when(() -> com.ruoyi.common.utils.spring.SpringUtils.getBean("scheduledExecutorService"))
+                    .thenReturn(mock(java.util.concurrent.ScheduledExecutorService.class));
+            com.ruoyi.framework.manager.AsyncManager manager = mock(com.ruoyi.framework.manager.AsyncManager.class);
+            org.mockito.Mockito.doAnswer(call -> { ((java.util.TimerTask) call.getArgument(0)).run(); return null; })
+                    .when(manager).execute(org.mockito.ArgumentMatchers.any(java.util.TimerTask.class));
+            try (var async = org.mockito.Mockito.mockStatic(com.ruoyi.framework.manager.AsyncManager.class);
+                 var push = org.mockito.Mockito.mockStatic(com.ruoyi.app.utils.PayPush.class)) {
+                async.when(com.ruoyi.framework.manager.AsyncManager::me).thenReturn(manager);
+                push.when(() -> com.ruoyi.app.utils.PayPush.userPushHandleLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                        org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.anyString(),
+                        org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()))
+                        .thenAnswer(call -> { bodies.put("user", com.alibaba.fastjson.JSON.parseObject(call.getArgument(6, String.class))); return null; });
+                push.when(() -> com.ruoyi.app.utils.PayPush.qsPushHandleLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                        org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.anyString(),
+                        org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()))
+                        .thenAnswer(call -> { bodies.put("rider", com.alibaba.fastjson.JSON.parseObject(call.getArgument(6, String.class))); return null; });
+                push.when(() -> com.ruoyi.app.utils.PayPush.shPushHandleLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                        org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.anyString(),
+                        org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()))
+                        .thenAnswer(call -> { bodies.put("merchant", com.alibaba.fastjson.JSON.parseObject(call.getArgument(6, String.class))); return null; });
+                org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization();
+                try {
+                    service.notifyOrderCancelled(order, null);
+                    org.junit.jupiter.api.Assertions.assertTrue(bodies.isEmpty());
+                    org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit());
+                    org.junit.jupiter.api.Assertions.assertEquals("ORDER_CANCELLED", bodies.get("user").getString("eventScene"));
+                    org.junit.jupiter.api.Assertions.assertEquals(0, bodies.get("user").getIntValue("type"));
+                    org.junit.jupiter.api.Assertions.assertFalse(bodies.get("rider").containsKey("eventScene"));
+                    org.junit.jupiter.api.Assertions.assertFalse(bodies.get("merchant").containsKey("eventScene"));
+                } finally {
+                    org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization();
+                }
+            }
+        }
+    }
+
+    @Test
+    void cancellationPreparationFailureDoesNotEscapeBusinessOperation() {
+        IInfoUserService users = mock(IInfoUserService.class);
+        DeliveryOrderNotificationService service = new DeliveryOrderNotificationService(
+                mock(RiderPositionMapper.class), mock(PushEventService.class), users, null);
+        PosOrder order = availableOrder(); order.setQsId(301L); order.setUserId(201L);
+        when(users.listByIds(org.mockito.ArgumentMatchers.anyCollection())).thenThrow(new IllegalStateException("failed notification lookup"));
+        org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> service.notifyOrderCancelled(order, null));
+    }
+
     @Test
     void notifiesNearbyAvailableRidersOnlyAfterDeliveryOrderIsPaid() {
         RiderPositionMapper positions = mock(RiderPositionMapper.class);

+ 48 - 3
ruoyi-admin/src/test/java/com/ruoyi/app/pay/LinePayOrderNotificationServiceTest.java

@@ -24,6 +24,45 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 class LinePayOrderNotificationServiceTest {
+    @Test
+    void paymentNotificationWaitsForCommitAndRollbackDoesNotNotify() {
+        IInfoUserService users = mock(IInfoUserService.class);
+        PushEventService events = mock(PushEventService.class);
+        BusinessPushService push = mock(BusinessPushService.class);
+        LinePayOrderNotificationService service = new LinePayOrderNotificationService(users, events,
+                mock(OrderLogHelper.class), mock(DeliveryOrderNotificationService.class), mock(MerchantNotificationRouter.class), push);
+        InfoUser recipient = new InfoUser(); recipient.setUserId(9L); // Neither CID nor native token.
+        when(users.getById(9L)).thenReturn(recipient);
+        PosOrder order = new PosOrder(); order.setDdId("DD-1"); order.setUserId(9L); order.setState(0L); order.setType(1L);
+        try (var locale = mockStatic(LocaleUtils.class); var messages = mockStatic(MessageUtils.class)) {
+            locale.when(() -> LocaleUtils.getUserLocale(9L)).thenReturn(Locale.US);
+            messages.when(() -> MessageUtils.message(anyString(), eq(Locale.US))).thenAnswer(call -> call.getArgument(0));
+            org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization();
+            try {
+                service.paymentCaptured(order);
+                org.mockito.Mockito.verifyNoInteractions(events, push);
+                org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCompletion(1));
+                org.mockito.Mockito.verifyNoInteractions(events, push);
+            } finally {
+                org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization();
+            }
+            org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization();
+            try {
+                // Delivery failure is isolated even though the payment transaction has already succeeded.
+                org.mockito.Mockito.doThrow(new IllegalStateException("external channel unavailable")).when(push)
+                        .send(eq(RecipientApp.USER), eq(9L), eq(null), anyString(), anyString(), anyString());
+                service.paymentCaptured(order);
+                org.junit.jupiter.api.Assertions.assertDoesNotThrow(() ->
+                        org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit()));
+                var body = org.mockito.ArgumentCaptor.forClass(String.class);
+                verify(events).PublisherEvent(eq(9L), anyString(), anyString(), body.capture());
+                org.junit.jupiter.api.Assertions.assertEquals("PAYMENT_SUCCEEDED", com.alibaba.fastjson.JSON.parseObject(body.getValue()).getString("eventScene"));
+            } finally {
+                org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization();
+            }
+        }
+    }
+
     @Test
     void paymentSuccessWithNoCidStillRoutesNativeEligibleUserOnlyOnce() {
         IInfoUserService users = mock(IInfoUserService.class);
@@ -55,8 +94,14 @@ class LinePayOrderNotificationServiceTest {
             service.paymentCaptured(order);
         }
 
-        verify(events).PublisherEvent(eq(9L), eq("title"), eq("paid"), anyString());
-        verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), eq("title"), eq("paid"), anyString());
-        verify(merchants).sendStoreNotification(eq(null), eq(null), anyString(), anyString(), anyString(), eq("DD-1"));
+        var userBody = org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(events).PublisherEvent(eq(9L), eq("title"), eq("paid"), userBody.capture());
+        verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), eq("title"), eq("paid"), eq(userBody.getValue()));
+        var json = com.alibaba.fastjson.JSON.parseObject(userBody.getValue());
+        org.junit.jupiter.api.Assertions.assertEquals("PAYMENT_SUCCEEDED", json.getString("eventScene"));
+        org.junit.jupiter.api.Assertions.assertEquals(0, json.getIntValue("type"));
+        var merchantBody = org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(merchants).sendStoreNotification(eq(null), eq(null), anyString(), anyString(), merchantBody.capture(), eq("DD-1"));
+        org.junit.jupiter.api.Assertions.assertFalse(com.alibaba.fastjson.JSON.parseObject(merchantBody.getValue()).containsKey("eventScene"));
     }
 }