浏览代码

feat: 推送消息增加订单指针,消息列表反查订单回填商家名/地址(#671)

push_message 加 dd_id/push_type 两列,PushEventListener 入库时从 body JSON
解析订单指针(所有推送通道零调用方改动);getPushMessageList 分页后按类型
批量反查 pos_order(商家名+收货地址)与 flash_delivery_order(取件/收件地址)
回填展示字段。正文 ,NO: 后缀保留不动;SQL 迁移见 updatesql/sql.md 2026-09-24 段。
qmj 1 天之前
父节点
当前提交
d09a6c9a1b

+ 27 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/utils/event/PushEventListener.java

@@ -1,6 +1,8 @@
 package com.ruoyi.app.utils.event;
 
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.ruoyi.system.domain.PushMessage;
 import com.ruoyi.system.service.IPushMessageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -23,7 +25,32 @@ public class PushEventListener  implements ApplicationListener<PushEvent> {
         pushMessage.setBody(event.getBody());
         pushMessage.setUserId(event.getUserId());
         pushMessage.setTime(new java.util.Date());
+        applyOrderPointer(pushMessage);
         pushMessageService.insertPushMessage(pushMessage);
 
     }
+
+    /**
+     * 从推送 body JSON(OrderPushBodyDto 结构)解析订单指针 ddId/pushType 存列,
+     * 供消息列表反查订单展示信息;非订单类推送(body 非订单 JSON)解析失败时指针留空。
+     */
+    private void applyOrderPointer(PushMessage pushMessage) {
+        String body = pushMessage.getBody();
+        if (body == null || body.isEmpty()) {
+            return;
+        }
+        try {
+            JSONObject json = JSON.parseObject(body);
+            String ddId = json.getString("ddId");
+            Integer pushType = json.getInteger("pushType");
+            if (ddId != null && !ddId.isEmpty()) {
+                pushMessage.setDdId(ddId);
+            }
+            if (pushType != null) {
+                pushMessage.setPushType(pushType);
+            }
+        } catch (Exception ignored) {
+            // body 不是订单 JSON(如视频通话推送),不阻断入库
+        }
+    }
 }

+ 2 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/PushMessageController.java

@@ -48,6 +48,8 @@ public class PushMessageController extends BaseController
         LambdaQueryWrapper<PushMessage> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(PushMessage::getUserId,uid).orderByDesc(PushMessage::getTime);
         IPage<PushMessage> data = pushMessageService.page(page,queryWrapper);
+        // 按订单指针回填商家名/地址,供骑手消息卡片展示
+        pushMessageService.attachOrderInfo(data.getRecords());
         return success(data);
     }
 

+ 70 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/utils/event/PushEventListenerTest.java

@@ -0,0 +1,70 @@
+package com.ruoyi.app.utils.event;
+
+import com.ruoyi.system.domain.PushMessage;
+import com.ruoyi.system.service.IPushMessageService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * push_message 订单指针字段:listener 入库时从 body JSON(OrderPushBodyDto 结构)
+ * 解析 ddId/pushType 存列;非订单类推送(body 非订单 JSON/为空)容错存 NULL,不影响入库。
+ */
+class PushEventListenerTest {
+
+    private final IPushMessageService pushMessageService = mock(IPushMessageService.class);
+    private final PushEventListener listener = new PushEventListener();
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(listener, "pushMessageService", pushMessageService);
+    }
+
+    @Test
+    void extractsDdIdAndPushTypeFromBodyJsonWhenInserting() {
+        listener.onApplicationEvent(new PushEvent(9L, "訊息", "您有新的外送訂單",
+                "{\"ddId\":\"991790045286244\",\"state\":\"0\",\"type\":-1,\"pushType\":1}"));
+
+        ArgumentCaptor<PushMessage> captor = ArgumentCaptor.forClass(PushMessage.class);
+        verify(pushMessageService).insertPushMessage(captor.capture());
+        assertEquals("991790045286244", captor.getValue().getDdId());
+        assertEquals(Integer.valueOf(1), captor.getValue().getPushType());
+    }
+
+    @Test
+    void extractsFlashOrderNoAsDdIdWithPushTypeThree() {
+        listener.onApplicationEvent(new PushEvent(9L, "訊息", "您有新的快送訂單",
+                "{\"ddId\":\"F20260924001\",\"state\":\"WAITING_ACCEPTANCE\",\"type\":0,\"pushType\":3}"));
+
+        ArgumentCaptor<PushMessage> captor = ArgumentCaptor.forClass(PushMessage.class);
+        verify(pushMessageService).insertPushMessage(captor.capture());
+        assertEquals("F20260924001", captor.getValue().getDdId());
+        assertEquals(Integer.valueOf(3), captor.getValue().getPushType());
+    }
+
+    @Test
+    void keepsNullPointersWhenBodyIsNotOrderJson() {
+        listener.onApplicationEvent(new PushEvent(9L, "視頻通話", "來電提醒", "not-a-json"));
+
+        ArgumentCaptor<PushMessage> captor = ArgumentCaptor.forClass(PushMessage.class);
+        verify(pushMessageService).insertPushMessage(captor.capture());
+        assertNull(captor.getValue().getDdId());
+        assertNull(captor.getValue().getPushType());
+    }
+
+    @Test
+    void keepsNullPointersWhenBodyIsNull() {
+        listener.onApplicationEvent(new PushEvent(9L, "訊息", "系統通知", null));
+
+        ArgumentCaptor<PushMessage> captor = ArgumentCaptor.forClass(PushMessage.class);
+        verify(pushMessageService).insertPushMessage(captor.capture());
+        assertNull(captor.getValue().getDdId());
+        assertNull(captor.getValue().getPushType());
+    }
+}

+ 15 - 0
ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/PushMessageControllerTest.java

@@ -23,6 +23,7 @@ import org.springframework.context.support.StaticMessageSource;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -88,6 +89,20 @@ class PushMessageControllerTest {
         verify(pushMessageService, never()).update(any(Wrapper.class));
     }
 
+    @Test
+    void listEndpointEnrichesPageRecordsWithOrderInfo() {
+        Page<PushMessage> page = new Page<>(1, 10);
+        List<PushMessage> records = new ArrayList<>();
+        records.add(new PushMessage());
+        page.setRecords(records);
+        when(pushMessageService.page(any(Page.class), any(Wrapper.class))).thenReturn(page);
+
+        controller.getPushMessageList(JwtUtil.token("9", "user"), 1, 10);
+
+        // 分页查完后按订单指针回填商家名/地址,仍返回同一分页对象
+        verify(pushMessageService).attachOrderInfo(records);
+    }
+
     @Test
     void unreadCountEndpointCountsOnlyUnreadForTokenUser() {
         when(pushMessageService.count(any(Wrapper.class))).thenReturn(5L);

+ 14 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PushMessage.java

@@ -56,6 +56,20 @@ public class PushMessage
     /** 是否已读(0未读 1已读) */
     private Integer isRead;
 
+    /** 关联订单号(外卖ddId/闪送orderNo) */
+    private String ddId;
+
+    /** 推送业务类型 1外卖 2充值 3闪送 */
+    private Integer pushType;
+
+    /** 以下为列表展示回填字段,不落库 */
+    private transient String shopName;
+    private transient String shAddress;
+    private transient String pickupAddress;
+    private transient String pickupAddressDetail;
+    private transient String deliveryAddress;
+    private transient String deliveryAddressDetail;
+
     public void setId(Long id)
     {
         this.id = id;

+ 7 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPushMessageService.java

@@ -59,4 +59,11 @@ public interface IPushMessageService extends IService<PushMessage>
      * @return 结果
      */
     public int deletePushMessageById(Long id);
+
+    /**
+     * 按订单指针回填展示信息:外卖消息取商家名+收货地址,闪送消息取取件/收件地址
+     *
+     * @param messages 分页查出的当页消息
+     */
+    public void attachOrderInfo(List<PushMessage> messages);
 }

+ 97 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PushMessageServiceImpl.java

@@ -1,12 +1,24 @@
 package com.ruoyi.system.service.impl;
 
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.mapper.PushMessageMapper;
+import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
+import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PushMessage;
+import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
 import com.ruoyi.system.service.IPushMessageService;
 
 /**
@@ -21,6 +33,12 @@ public class PushMessageServiceImpl extends ServiceImpl<BaseMapper<PushMessage>,
     @Autowired
     private PushMessageMapper pushMessageMapper;
 
+    @Autowired
+    private PosOrderMapper posOrderMapper;
+
+    @Autowired
+    private FlashDeliveryOrderMapper flashDeliveryOrderMapper;
+
     /**
      * 查询推送消息
      *
@@ -92,4 +110,83 @@ public class PushMessageServiceImpl extends ServiceImpl<BaseMapper<PushMessage>,
     {
         return pushMessageMapper.deletePushMessageById(id);
     }
+
+    /**
+     * 按订单指针回填展示信息:外卖消息取商家名+收货地址,闪送消息取取件/收件地址
+     *
+     * @param messages 分页查出的当页消息
+     */
+    @Override
+    public void attachOrderInfo(List<PushMessage> messages)
+    {
+        if (messages == null || messages.isEmpty())
+        {
+            return;
+        }
+        Set<String> foodDdIds = new LinkedHashSet<>();
+        Set<String> flashOrderNos = new LinkedHashSet<>();
+        collectOrderPointers(messages, foodDdIds, flashOrderNos);
+        Map<String, PosOrder> foodOrders = foodDdIds.isEmpty() ? Collections.emptyMap()
+                : posOrderMapper.selectList(new LambdaQueryWrapper<PosOrder>().in(PosOrder::getDdId, foodDdIds))
+                .stream().filter(order -> order.getDdId() != null)
+                .collect(Collectors.toMap(PosOrder::getDdId, Function.identity(), (first, second) -> first));
+        Map<String, FlashDeliveryOrder> flashOrders = flashOrderNos.isEmpty() ? Collections.emptyMap()
+                : flashDeliveryOrderMapper.selectList(new LambdaQueryWrapper<FlashDeliveryOrder>().in(FlashDeliveryOrder::getOrderNo, flashOrderNos))
+                .stream().filter(order -> order.getOrderNo() != null)
+                .collect(Collectors.toMap(FlashDeliveryOrder::getOrderNo, Function.identity(), (first, second) -> first));
+        fillOrderInfo(messages, foodOrders, flashOrders);
+    }
+
+    /** 收集当页消息的订单指针:push_type=1 归入外卖单号,push_type=3 归入闪送单号。 */
+    private void collectOrderPointers(List<PushMessage> messages, Set<String> foodDdIds, Set<String> flashOrderNos)
+    {
+        for (PushMessage message : messages)
+        {
+            if (message == null || StringUtils.isEmpty(message.getDdId()))
+            {
+                continue;
+            }
+            if (Integer.valueOf(1).equals(message.getPushType()))
+            {
+                foodDdIds.add(message.getDdId());
+            }
+            else if (Integer.valueOf(3).equals(message.getPushType()))
+            {
+                flashOrderNos.add(message.getDdId());
+            }
+        }
+    }
+
+    /** 按指针把商家名/地址写回消息;订单已删或不存在的消息保持原样。 */
+    private void fillOrderInfo(List<PushMessage> messages, Map<String, PosOrder> foodOrders,
+                               Map<String, FlashDeliveryOrder> flashOrders)
+    {
+        for (PushMessage message : messages)
+        {
+            if (message == null || StringUtils.isEmpty(message.getDdId()))
+            {
+                continue;
+            }
+            if (Integer.valueOf(1).equals(message.getPushType()))
+            {
+                PosOrder order = foodOrders.get(message.getDdId());
+                if (order != null)
+                {
+                    message.setShopName(order.getPosName());
+                    message.setShAddress(order.getShAddress());
+                }
+            }
+            else if (Integer.valueOf(3).equals(message.getPushType()))
+            {
+                FlashDeliveryOrder order = flashOrders.get(message.getDdId());
+                if (order != null)
+                {
+                    message.setPickupAddress(order.getPickupAddress());
+                    message.setPickupAddressDetail(order.getPickupAddressDetail());
+                    message.setDeliveryAddress(order.getDeliveryAddress());
+                    message.setDeliveryAddressDetail(order.getDeliveryAddressDetail());
+                }
+            }
+        }
+    }
 }

+ 9 - 1
ruoyi-system/src/main/resources/mapper/system/PushMessageMapper.xml

@@ -12,10 +12,12 @@
         <result property="body"    column="body"    />
         <result property="time"    column="time"    />
         <result property="isRead"    column="is_read"    />
+        <result property="ddId"    column="dd_id"    />
+        <result property="pushType"    column="push_type"    />
     </resultMap>
 
     <sql id="selectPushMessageVo">
-        select id, user_id, title, content, body, time, is_read from push_message
+        select id, user_id, title, content, body, time, is_read, dd_id, push_type from push_message
     </sql>
 
     <select id="selectPushMessageList" parameterType="PushMessage" resultMap="PushMessageResult">
@@ -44,6 +46,8 @@
             <if test="body != null">body,</if>
             <if test="time != null">time,</if>
             <if test="isRead != null">is_read,</if>
+            <if test="ddId != null">dd_id,</if>
+            <if test="pushType != null">push_type,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null">#{userId},</if>
@@ -52,6 +56,8 @@
             <if test="body != null">#{body},</if>
             <if test="time != null">#{time},</if>
             <if test="isRead != null">#{isRead},</if>
+            <if test="ddId != null">#{ddId},</if>
+            <if test="pushType != null">#{pushType},</if>
         </trim>
     </insert>
 
@@ -64,6 +70,8 @@
             <if test="body != null">body = #{body},</if>
             <if test="time != null">time = #{time},</if>
             <if test="isRead != null">is_read = #{isRead},</if>
+            <if test="ddId != null">dd_id = #{ddId},</if>
+            <if test="pushType != null">push_type = #{pushType},</if>
         </trim>
         where id = #{id}
     </update>

+ 147 - 0
ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PushMessageServiceImplTest.java

@@ -0,0 +1,147 @@
+package com.ruoyi.system.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PushMessage;
+import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
+import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 消息列表订单信息回填:外卖消息(push_type=1)批量反查 pos_order 取商家名+收货地址;
+ * 闪送消息(push_type=3)反查 flash_delivery_order 取取件/收件地址;
+ * 无订单指针、非订单类型或订单已删时不回填,也不报错。
+ */
+class PushMessageServiceImplTest {
+
+    private final PosOrderMapper posOrderMapper = mock(PosOrderMapper.class);
+    private final FlashDeliveryOrderMapper flashDeliveryOrderMapper = mock(FlashDeliveryOrderMapper.class);
+    private final PushMessageServiceImpl service = new PushMessageServiceImpl();
+
+    @BeforeAll
+    static void initTableInfo() {
+        // 纯单测下初始化 MP 实体元数据,否则 LambdaWrapper 解析列名时无缓存可用
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, PosOrder.class);
+        TableInfoHelper.initTableInfo(assistant, FlashDeliveryOrder.class);
+    }
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(service, "posOrderMapper", posOrderMapper);
+        ReflectionTestUtils.setField(service, "flashDeliveryOrderMapper", flashDeliveryOrderMapper);
+    }
+
+    @Test
+    void foodOrderMessageGetsShopNameAndDeliveryAddress() {
+        PushMessage message = message("991790045286244", 1);
+        PosOrder order = new PosOrder();
+        order.setDdId("991790045286244");
+        order.setPosName("阿寶早餐店");
+        order.setShAddress("台北市大安區仁愛路一段1號");
+        when(posOrderMapper.selectList(any(Wrapper.class))).thenReturn(List.of(order));
+
+        service.attachOrderInfo(List.of(message));
+
+        assertEquals("阿寶早餐店", message.getShopName());
+        assertEquals("台北市大安區仁愛路一段1號", message.getShAddress());
+        verify(flashDeliveryOrderMapper, never()).selectList(any(Wrapper.class));
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Wrapper<PosOrder>> captor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(posOrderMapper).selectList(captor.capture());
+        // 反查必须按订单号过滤且不逐条查询(in 参数由 MP 惰性注册,断言以 sqlSegment 为准)
+        LambdaQueryWrapper<PosOrder> queryWrapper = (LambdaQueryWrapper<PosOrder>) captor.getValue();
+        String sqlSegment = queryWrapper.getSqlSegment();
+        assertTrue(sqlSegment.contains("dd_id"));
+        assertTrue(sqlSegment.contains("IN"));
+    }
+
+    @Test
+    void flashOrderMessageGetsPickupAndDeliveryAddress() {
+        PushMessage message = message("F20260924001", 3);
+        FlashDeliveryOrder order = new FlashDeliveryOrder();
+        order.setOrderNo("F20260924001");
+        order.setPickupAddress("台中市西屯區文華路100號");
+        order.setPickupAddressDetail("3樓之2");
+        order.setDeliveryAddress("台中市北區三民路三段129號");
+        order.setDeliveryAddressDetail("B1");
+        when(flashDeliveryOrderMapper.selectList(any(Wrapper.class))).thenReturn(List.of(order));
+
+        service.attachOrderInfo(List.of(message));
+
+        assertEquals("台中市西屯區文華路100號", message.getPickupAddress());
+        assertEquals("3樓之2", message.getPickupAddressDetail());
+        assertEquals("台中市北區三民路三段129號", message.getDeliveryAddress());
+        assertEquals("B1", message.getDeliveryAddressDetail());
+        verify(posOrderMapper, never()).selectList(any(Wrapper.class));
+    }
+
+    @Test
+    void messageWithoutDdIdStaysUntouched() {
+        PushMessage message = message(null, 1);
+
+        service.attachOrderInfo(List.of(message));
+
+        verify(posOrderMapper, never()).selectList(any(Wrapper.class));
+        verify(flashDeliveryOrderMapper, never()).selectList(any(Wrapper.class));
+        assertNull(message.getShopName());
+    }
+
+    @Test
+    void nonOrderPushTypeStaysUntouched() {
+        PushMessage message = message("R20260924001", 2);
+
+        service.attachOrderInfo(List.of(message));
+
+        verify(posOrderMapper, never()).selectList(any(Wrapper.class));
+        verify(flashDeliveryOrderMapper, never()).selectList(any(Wrapper.class));
+        assertNull(message.getShopName());
+    }
+
+    @Test
+    void missingOrderLeavesMessageUntouched() {
+        PushMessage message = message("991790045286244", 1);
+        when(posOrderMapper.selectList(any(Wrapper.class))).thenReturn(Collections.emptyList());
+
+        service.attachOrderInfo(List.of(message));
+
+        assertNull(message.getShopName());
+        assertNull(message.getShAddress());
+    }
+
+    @Test
+    void emptyListTouchesNothing() {
+        service.attachOrderInfo(Collections.emptyList());
+
+        verify(posOrderMapper, never()).selectList(any(Wrapper.class));
+        verify(flashDeliveryOrderMapper, never()).selectList(any(Wrapper.class));
+    }
+
+    private PushMessage message(String ddId, Integer pushType) {
+        PushMessage message = new PushMessage();
+        message.setDdId(ddId);
+        message.setPushType(pushType);
+        return message;
+    }
+}

+ 6 - 0
updatesql/sql.md

@@ -1652,3 +1652,9 @@ ALTER TABLE push_message ADD COLUMN is_read TINYINT NOT NULL DEFAULT 0 COMMENT '
 -- 历史消息统一置为已读,避免上线当天全员角标爆量
 UPDATE push_message SET is_read = 1;
 ```
+
+```sql
+-- 2026-09-24 消息列表反查订单展示信息(Bug #671 骑手消息显示商家名+地址/闪送取收件地址)
+ALTER TABLE push_message ADD COLUMN dd_id VARCHAR(32) DEFAULT NULL COMMENT '关联订单号(外卖ddId/闪送orderNo)';
+ALTER TABLE push_message ADD COLUMN push_type TINYINT DEFAULT NULL COMMENT '推送业务类型 1外卖 2充值 3闪送';
+```