瀏覽代碼

修复骑手接单后商家外送新订单语音提醒

qmj 4 天之前
父節點
當前提交
e7f76ec741

+ 5 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java

@@ -258,17 +258,17 @@ public class PosOrderQsOprateController extends BaseController {
                                         }
                                     });
                                 }
-                                // 给商家推送(骑手已接单时)
+                                // 平台外送单在骑手接单后才作为商家新订单提醒;复用新单声音入口,只发一条。
                                 if (posOrder.getDeliveryStatus() != null && posOrder.getDeliveryStatus() == 1L) {
                                     String shTitle = "no.message.push.message";
-                                    String shContent = "no.message.push.rider.accepted.merchant";
-                                    String shBody = OrderPushBodyDto.getJson(String.valueOf(orst.getDdId()), String.valueOf(orst.getState()));
+                                    String shContent = "no.message.push.new.order";
+                                    String shBody = OrderPushBodyDto.getJson(String.valueOf(orst.getDdId()), String.valueOf(orst.getState()), 0);
                                     String shDdId = String.valueOf(orst.getDdId());
                                     AsyncManager.me().execute(new TimerTask() {
                                         @Override
                                         public void run() {
-                                            merchantNotificationRouter.sendStoreNotification(orst.getMdId(), orst.getShId(),
-                                                    shTitle, shContent, shBody, shDdId);
+                                            merchantNotificationRouter.sendStoreNewOrder(orst.getMdId(), orst.getShId(),
+                                                    shTitle, shContent, shBody, shDdId, orst.getType());
                                         }
                                     });
                                 }

+ 83 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/RiderAcceptanceMerchantNotificationTest.java

@@ -0,0 +1,83 @@
+package com.ruoyi.app.order;
+
+import com.alibaba.fastjson.JSON;
+import com.ruoyi.app.utils.event.PushEventService;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.service.IInfoUserService;
+import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.utils.JwtUtil;
+import com.ruoyi.system.utils.OrderLogHelper;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+class RiderAcceptanceMerchantNotificationTest {
+    @Test
+    void committedAcceptanceSendsExactlyOneMerchantNewDeliveryOrderEvenWithoutBuyerAccount() throws Exception {
+        var controller = new PosOrderQsOprateController();
+        var orders = mock(IPosOrderService.class);
+        var users = mock(IInfoUserService.class);
+        var router = mock(MerchantNotificationRouter.class);
+        var lifecycle = mock(OrderLifecycleService.class);
+        var redisson = mock(RedissonClient.class);
+        var lock = mock(RLock.class);
+        ReflectionTestUtils.setField(controller, "posOrderService", orders);
+        ReflectionTestUtils.setField(controller, "infoUserService", users);
+        ReflectionTestUtils.setField(controller, "merchantNotificationRouter", router);
+        ReflectionTestUtils.setField(controller, "orderLifecycleService", lifecycle);
+        ReflectionTestUtils.setField(controller, "redissonClient", redisson);
+        ReflectionTestUtils.setField(controller, "orderLogHelper", mock(OrderLogHelper.class));
+        ReflectionTestUtils.setField(controller, "pushEventService", mock(PushEventService.class));
+        PosOrder order = new PosOrder();
+        order.setId(3215L); order.setDdId("ORDER-NOTIFY-1"); order.setType(0L);
+        order.setMdId(195L); order.setShId(801L); order.setUserId(909L);
+        order.setState(0L); order.setDeliveryStatus(0L);
+        when(orders.getOne(any())).thenReturn(order);
+        when(orders.getById(3215L)).thenReturn(order);
+        InfoUser rider = new InfoUser(); rider.setNickName("rider");
+        when(users.getOne(any())).thenReturn(rider);
+        when(redisson.getLock(anyString())).thenReturn(lock);
+        when(lock.tryLock(20L, 10L, TimeUnit.SECONDS)).thenReturn(true);
+        try (var spring = mockStatic(SpringUtils.class);
+             var messages = mockStatic(MessageUtils.class, call -> call.getArgument(0))) {
+            spring.when(() -> SpringUtils.getBean("scheduledExecutorService"))
+                    .thenReturn(mock(ScheduledExecutorService.class));
+            try (var async = mockStatic(AsyncManager.class)) {
+                var manager = mock(AsyncManager.class);
+                async.when(AsyncManager::me).thenReturn(manager);
+                TransactionSynchronizationManager.initSynchronization();
+                try {
+                    controller.acceptOrder(JwtUtil.token("831", "rider"), 3215L);
+                    verifyNoInteractions(router, manager);
+                    TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit());
+                    var task = ArgumentCaptor.forClass(TimerTask.class);
+                    verify(manager).execute(task.capture());
+                    task.getValue().run();
+                    var body = ArgumentCaptor.forClass(String.class);
+                    verify(router).sendStoreNewOrder(eq(195L), eq(801L), eq("no.message.push.message"),
+                            eq("no.message.push.new.order"), body.capture(), eq("ORDER-NOTIFY-1"), eq(0L));
+                    assertEquals(0, JSON.parseObject(body.getValue()).getIntValue("type"));
+                    assertEquals("ORDER-NOTIFY-1", JSON.parseObject(body.getValue()).getString("ddId"));
+                    verifyNoMoreInteractions(router);
+                } finally {
+                    TransactionSynchronizationManager.clearSynchronization();
+                }
+            }
+        }
+    }
+}

+ 2 - 0
specs/035-user-notification-voice/merchant-new-order-native-sound.md

@@ -2,6 +2,8 @@
 
 本次只调整服务端商家 FCM/APNs 原生推送;不改 uni 推送和消息中心,不新增第二条通知,也不提供音频下载。App 已有三个本地 WAV 文件。
 
+平台骑手配送的普通外送单:骑手接单事务提交后,原“骑手已接单”通知改为“有新订单”,`type=0`,原生场景 `NEW_ORDER_DELIVERY`。沿用下表外送声音和渠道;不在支付时提前通知,不另加第二条推送。此处 type=-1改0是对接单通知分类的明确修正;自配送、自取、堂食和多店通知时机不变。
+
 | 订单 `type` | 原生 payload `eventScene` | APNs `aps.sound` / FCM `sound` 数据 | Android 通知渠道 ID |
 |---|---|---|---|
 | 0 外送 | `NEW_ORDER_DELIVERY` | `new_order_delivery.wav` | `merchant_voice_new_order_delivery_v1` |

+ 2 - 0
specs/035-user-notification-voice/plan.md

@@ -2,6 +2,8 @@
 
 ## 2026-10-29 商家原生新单语音计划
 
+追加:用骑手接单控制器事务夹具复现接单后仍调用普通通知、type=-1的问题;仅将此分支改走新单入口,文案用已有国际化新订单 key。验证事务提交前不发送、提交后只排队一次、无买方账号也通知商家、payload type=0;与路由/声音/FCM载荷测试一起回归。只提交本次修复文件,不混入工作区原有小票接口修改。
+
 在各新单发送点显式传 `PosOrder.type`,由 `MerchantNotificationRouter` 只为原生外发 body 附加场景;入库/uni 保留原 body。`NativePushSoundResolver` 仅接受商家三个新单场景,沿现有 FCM/APNs sound 通道输出本地 WAV 文件名和 Android 渠道 ID。定向验证三类映射、uni 隔离、非新单兼容以及现有新单发送测试。
 
 

+ 2 - 0
specs/035-user-notification-voice/spec.md

@@ -2,6 +2,8 @@
 
 ## 2026-10-29 商家原生新单语音追加
 
+平台骑手配送的普通外送单,在骑手接单事务提交后向商家发送一条“有新订单”通知,body 使用既有 `type=0` 新单标识;原生通道通过 `sendStoreNewOrder` 附加 `NEW_ORDER_DELIVERY` 及对应声音。替换原“骑手已接单”普通通知,不额外发送第二条。不因支付成功提前通知;自配送、自取、堂食及多店通知时机保持原流程。
+
 仅商家 App 的 FCM/APNs 新订单通知按订单 `type` 选声音:0 外送 `new_order_delivery.wav`、1 自取 `new_order_pickup.wav`、2 堂食 `new_order_dine_in.wav`。原生业务 payload 增加 `eventScene=NEW_ORDER_DELIVERY|NEW_ORDER_PICKUP|NEW_ORDER_DINE_IN`;uni 推送与消息中心 body、非新单通知、用户/骑手声音保持原样。App 已有对应 WAV;Android 需预建相应 `merchant_voice_<文件名去扩展名>_v1` 通知渠道,iOS 将 WAV 打入主包。详见 [merchant-new-order-native-sound.md](merchant-new-order-native-sound.md)。本节覆盖下方历史“商家新单铃声不改”的范围限制。
 
 

+ 4 - 0
specs/035-user-notification-voice/tasks.md

@@ -2,6 +2,10 @@
 
 ## 2026-10-29 商家原生新单语音任务
 
+- [x] 追加复现:骑手接单事务提交后仍发送 type=-1 的普通商家通知,不能选择外送新单声音。
+- [x] 改为一条“有新订单”通知,type=0,复用 NEW_ORDER_DELIVERY 原生声音入口;不改用户通知、支付时机及原有接收账号路由。
+- [x] 骑手接单通知、商家路由、声音与FCM定向回归及package:JDK21共73项,0失败/错误/跳过;先复现普通通知调用与期望新单入口不符,再修改通过。未部署、未发真实FCM、未修改App或数据库;真机播放仍待验证。
+
 - [x] 核对三类 `PosOrder.type` 及新单发送入口。
 - [x] 原生商家新单附加场景并映射本地 WAV;uni 与消息中心保留原 body。
 - [x] 补充映射/隔离单测和 App 接入说明。