Selaa lähdekoodia

feat: 统一订单列表外卖卡片改店铺名称并补充头像与商品件数(030 T021)

qmj 10 tuntia sitten
vanhempi
sitoutus
93b682d315

+ 45 - 19
ruoyi-admin/src/main/java/com/ruoyi/app/unifiedorder/UnifiedOrderQueryService.java

@@ -1,14 +1,15 @@
 package com.ruoyi.app.unifiedorder;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.app.unifiedorder.dto.UnifiedOrderListItem;
 import com.ruoyi.app.unifiedorder.dto.UnifiedOrderPageView;
-import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
-import com.ruoyi.system.mapper.InfoUserMapper;
 import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.PosStoreMapper;
 import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
 import org.springframework.stereotype.Service;
 
@@ -39,14 +40,14 @@ public class UnifiedOrderQueryService {
 
     private final PosOrderMapper posOrderMapper;
     private final FlashDeliveryOrderMapper flashOrderMapper;
-    private final InfoUserMapper infoUserMapper;
+    private final PosStoreMapper posStoreMapper;
 
     public UnifiedOrderQueryService(PosOrderMapper posOrderMapper,
                                     FlashDeliveryOrderMapper flashOrderMapper,
-                                    InfoUserMapper infoUserMapper) {
+                                    PosStoreMapper posStoreMapper) {
         this.posOrderMapper = posOrderMapper;
         this.flashOrderMapper = flashOrderMapper;
-        this.infoUserMapper = infoUserMapper;
+        this.posStoreMapper = posStoreMapper;
     }
 
     /** 查询统一列表:size 服务端钳制 1-50;cursor 为空表示首页。 */
@@ -74,11 +75,11 @@ public class UnifiedOrderQueryService {
         List<Row> displayed = hasMore ? rows.subList(0, limit) : rows;
 
         UnifiedOrderPageView view = new UnifiedOrderPageView();
-        Map<Long, String> storeNames = storeNamesOf(displayed);
+        Map<Long, PosStore> stores = storesOf(displayed);
         List<UnifiedOrderListItem> items = new ArrayList<>(displayed.size());
         for (Row row : displayed) {
             items.add(row.takeaway() != null
-                    ? takeawayItem(row.takeaway(), storeNames)
+                    ? takeawayItem(row.takeaway(), stores)
                     : flashItem(row.flash(), userId));
         }
         view.setList(items);
@@ -130,25 +131,26 @@ public class UnifiedOrderQueryService {
         return wrapper;
     }
 
-    private Map<Long, String> storeNamesOf(List<Row> displayed) {
-        Set<Long> shopIds = new HashSet<>();
+    /** 外卖条目按门店 ID 批量查 pos_store,一次回填店铺名称/头像。 */
+    private Map<Long, PosStore> storesOf(List<Row> displayed) {
+        Set<Long> mdIds = new HashSet<>();
         for (Row row : displayed) {
-            if (row.takeaway() != null && row.takeaway().getShId() != null) {
-                shopIds.add(row.takeaway().getShId());
+            if (row.takeaway() != null && row.takeaway().getMdId() != null) {
+                mdIds.add(row.takeaway().getMdId());
             }
         }
-        Map<Long, String> names = new HashMap<>();
-        if (!shopIds.isEmpty()) {
-            for (InfoUser user : infoUserMapper.selectBatchIds(shopIds)) {
-                if (user != null && user.getUserId() != null) {
-                    names.put(user.getUserId(), user.getNickName());
+        Map<Long, PosStore> stores = new HashMap<>();
+        if (!mdIds.isEmpty()) {
+            for (PosStore store : posStoreMapper.selectBatchIds(mdIds)) {
+                if (store != null && store.getId() != null) {
+                    stores.put(store.getId().longValue(), store);
                 }
             }
         }
-        return names;
+        return stores;
     }
 
-    private UnifiedOrderListItem takeawayItem(PosOrder order, Map<Long, String> storeNames) {
+    private UnifiedOrderListItem takeawayItem(PosOrder order, Map<Long, PosStore> stores) {
         UnifiedOrderListItem item = new UnifiedOrderListItem();
         item.setSourceType("takeaway");
         item.setRole("sender");
@@ -164,11 +166,35 @@ public class UnifiedOrderQueryService {
         item.setPayStatus(order.getPayStatus());
         item.setPayType(order.getPayType());
         item.setAfterSaleStatus(order.getAfterSaleStatus());
-        item.setStoreName(order.getShId() == null ? null : storeNames.get(order.getShId()));
+        // 店铺已删或 mdId 缺失时回退订单创建时的快照 posName/logo
+        PosStore store = order.getMdId() == null ? null : stores.get(order.getMdId());
+        item.setStoreName(store != null ? store.getPosName() : order.getPosName());
+        item.setStoreAvatar(store != null ? store.getLogo() : order.getLogo());
+        item.setGoodsCount(takeawayGoodsCount(order.getFood()));
         item.setCollectPayment(order.getCollectPayment());
         return item;
     }
 
+    /** 商品件数:food 快照数组各行 number 求和;food 为空或非法返回 null。 */
+    private Integer takeawayGoodsCount(String food) {
+        if (food == null || food.isBlank()) {
+            return null;
+        }
+        try {
+            JSONArray rows = JSONArray.parseArray(food);
+            int count = 0;
+            for (int i = 0; i < rows.size(); i++) {
+                Integer num = rows.getJSONObject(i).getInteger("number");
+                if (num != null) {
+                    count += num;
+                }
+            }
+            return count;
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
     private UnifiedOrderListItem flashItem(FlashDeliveryOrder order, Long userId) {
         UnifiedOrderListItem item = new UnifiedOrderListItem();
         item.setSourceType("flash");

+ 5 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/unifiedorder/dto/UnifiedOrderListItem.java

@@ -45,8 +45,12 @@ public class UnifiedOrderListItem {
     private String payType;
     /** 售后状态:0无售后,1申请中,2退款中,3已退款,4退款拒绝,5客服介入,6售后完成。 */
     private Long afterSaleStatus;
-    /** 店铺名称(按商家 ID 批量回填)。 */
+    /** 店铺名称(按门店 ID 查 pos_store.posName 批量回填;店铺已删时回退订单快照 posName)。 */
     private String storeName;
+    /** 店铺头像(pos_store.logo;店铺已删时回退订单快照 logo)。 */
+    private String storeAvatar;
+    /** 商品件数(订单 food 快照数组 number 求和;无商品数据为 null)。 */
+    private Integer goodsCount;
     /** 到付/现金支付标记。 */
     private String collectPayment;
 

+ 55 - 16
ruoyi-admin/src/test/java/com/ruoyi/app/unifiedorder/UnifiedOrderQueryServiceTest.java

@@ -6,12 +6,12 @@ import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.app.unifiedorder.dto.UnifiedOrderListItem;
 import com.ruoyi.app.unifiedorder.dto.UnifiedOrderPageView;
-import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
 import com.ruoyi.system.domain.flash.FlashDeliveryStatus;
-import com.ruoyi.system.mapper.InfoUserMapper;
 import com.ruoyi.system.mapper.PosOrderMapper;
+import com.ruoyi.system.mapper.PosStoreMapper;
 import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
 import org.apache.ibatis.builder.MapperBuilderAssistant;
 import org.junit.jupiter.api.BeforeAll;
@@ -43,9 +43,9 @@ class UnifiedOrderQueryServiceTest {
 
     private final PosOrderMapper posOrderMapper = mock(PosOrderMapper.class);
     private final FlashDeliveryOrderMapper flashOrderMapper = mock(FlashDeliveryOrderMapper.class);
-    private final InfoUserMapper infoUserMapper = mock(InfoUserMapper.class);
+    private final PosStoreMapper posStoreMapper = mock(PosStoreMapper.class);
     private final UnifiedOrderQueryService service =
-            new UnifiedOrderQueryService(posOrderMapper, flashOrderMapper, infoUserMapper);
+            new UnifiedOrderQueryService(posOrderMapper, flashOrderMapper, posStoreMapper);
 
     @BeforeAll
     static void initializeTableMetadata() {
@@ -183,22 +183,24 @@ class UnifiedOrderQueryServiceTest {
     }
 
     @Test
-    void takeawayItemsCarryStoreNameFromSingleBatchLookup() {
+    void takeawayItemsCarryStoreNameAndAvatarFromSingleBatchLookup() {
         PosOrder first = takeaway(21L, 3000L);
-        first.setShId(55L);
+        first.setMdId(55L);
         PosOrder second = takeaway(22L, 2000L);
-        second.setShId(56L);
+        second.setMdId(56L);
         PosOrder third = takeaway(23L, 1000L);
-        third.setShId(55L);
+        third.setMdId(55L);
         doReturn(pageOf(List.of(first, second, third))).when(posOrderMapper).selectPage(any(), any());
         doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
-        InfoUser shopA = new InfoUser();
-        shopA.setUserId(55L);
-        shopA.setNickName("店铺A");
-        InfoUser shopB = new InfoUser();
-        shopB.setUserId(56L);
-        shopB.setNickName("店铺B");
-        when(infoUserMapper.selectBatchIds(anyCollection())).thenReturn(List.of(shopA, shopB));
+        PosStore shopA = new PosStore();
+        shopA.setId(55);
+        shopA.setPosName("店铺A");
+        shopA.setLogo("http://img/a.png");
+        PosStore shopB = new PosStore();
+        shopB.setId(56);
+        shopB.setPosName("店铺B");
+        shopB.setLogo("http://img/b.png");
+        when(posStoreMapper.selectBatchIds(anyCollection())).thenReturn(List.of(shopA, shopB));
 
         UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
 
@@ -206,7 +208,44 @@ class UnifiedOrderQueryServiceTest {
         assertEquals("店铺A", view.getList().get(0).getStoreName());
         assertEquals("店铺B", view.getList().get(1).getStoreName());
         assertEquals("店铺A", view.getList().get(2).getStoreName());
-        verify(infoUserMapper, times(1)).selectBatchIds(anyCollection());
+        assertEquals("http://img/a.png", view.getList().get(0).getStoreAvatar());
+        assertEquals("http://img/b.png", view.getList().get(1).getStoreAvatar());
+        verify(posStoreMapper, times(1)).selectBatchIds(anyCollection());
+    }
+
+    @Test
+    void takeawayFallsBackToOrderSnapshotWhenStoreMissing() {
+        PosOrder order = takeaway(21L, 3000L);
+        order.setMdId(404L);
+        order.setPosName("快照店铺");
+        order.setLogo("http://img/snap.png");
+        doReturn(pageOf(List.of(order))).when(posOrderMapper).selectPage(any(), any());
+        doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
+        when(posStoreMapper.selectBatchIds(anyCollection())).thenReturn(List.of());
+
+        UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
+
+        assertEquals("快照店铺", view.getList().get(0).getStoreName());
+        assertEquals("http://img/snap.png", view.getList().get(0).getStoreAvatar());
+    }
+
+    @Test
+    void takeawayGoodsCountSumsFoodNumberFields() {
+        PosOrder order = takeaway(21L, 3000L);
+        order.setFood("[{\"name\":\"餐A\",\"number\":2},{\"name\":\"餐B\",\"number\":3}]");
+        PosOrder emptyFood = takeaway(22L, 2000L);
+        PosOrder brokenFood = takeaway(23L, 1000L);
+        brokenFood.setFood("not-json");
+        doReturn(pageOf(List.of(order, emptyFood, brokenFood)))
+                .when(posOrderMapper).selectPage(any(), any());
+        doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
+        when(posStoreMapper.selectBatchIds(anyCollection())).thenReturn(List.of());
+
+        UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
+
+        assertEquals(5, view.getList().get(0).getGoodsCount());
+        assertNull(view.getList().get(1).getGoodsCount());
+        assertNull(view.getList().get(2).getGoodsCount());
     }
 
     @Test

+ 1 - 1
specs/030-unified-order-list/contracts/api-contract.md

@@ -49,7 +49,7 @@ Query:
 
 ### takeaway 渲染块(字段值语义与现有外卖列表完全一致,渲染逻辑直接复用)
 
-`type`(0外送/1自取/2堂食)、`state`(0待处理/1已接单/2已出餐/3已完成/4已取消)、`deliveryStatus`(0待接/1已接/2配送中/3已送达)、`payStatus`(0未付/1已付/2已退)、`payType`(1到付/2OMG/3LINE/4现金/5余额/6转账)、`afterSaleStatus`(0-6)、`storeName`、`collectPayment`
+`type`(0外送/1自取/2堂食)、`state`(0待处理/1已接单/2已出餐/3已完成/4已取消)、`deliveryStatus`(0待接/1已接/2配送中/3已送达)、`payStatus`(0未付/1已付/2已退)、`payType`(1到付/2OMG/3LINE/4现金/5余额/6转账)、`afterSaleStatus`(0-6)、`storeName`(店铺名称,按门店查 pos_store,非商家昵称)、`storeAvatar`(店铺头像 logo)、`goodsCount`(商品件数,food 快照 number 求和)、`collectPayment`
 
 ### flash 渲染块(对齐现有闪送列表卡片)
 

+ 1 - 1
specs/030-unified-order-list/data-model.md

@@ -22,7 +22,7 @@
 
 ### 外卖渲染块(takeaway,前端复用现有外卖卡片)
 
-`type`(0外送/1自取/2堂食)、`state`(0-4)、`deliveryStatus`(0-3,仅外送)、`payStatus`(0/1/2)、`payType`(1-6)、`afterSaleStatus`(0-6)、`storeName`(批量回填商家昵称)、`collectPayment`(到付标记)——数值原值透传,文案由前端渲染。
+`type`(0外送/1自取/2堂食)、`state`(0-4)、`deliveryStatus`(0-3,仅外送)、`payStatus`(0/1/2)、`payType`(1-6)、`afterSaleStatus`(0-6)、`storeName`(按 mdId 批量查 pos_store.posName,店铺已删回退订单快照 posName)、`storeAvatar`(pos_store.logo,同回退)、`goodsCount`(food 快照数组 number 求和)、`collectPayment`(到付标记)——数值原值透传,文案由前端渲染。
 
 ### 闪送渲染块(flash,对齐 FlashDeliveryUserOrderListView 字段子集)
 

+ 6 - 0
specs/030-unified-order-list/tasks.md

@@ -103,6 +103,12 @@ mvn -pl ruoyi-admin -am test -Dtest='XxxTest' -Dsurefire.failIfNoSpecifiedTests=
 
 ---
 
+## Phase 7: 追加需求(2026-09-21)——外卖卡片店铺信息修正
+
+- [x] T021 外卖 `storeName` 修正为店铺名称:`UnifiedOrderQueryService` 批量回填由「shId 查 InfoUser 昵称」改为「mdId 查 PosStore.posName」,店铺已删/缺失时回退订单快照 posName/logo;新增 `storeAvatar`(PosStore.logo,同回退)、`goodsCount`(food 快照数组 number 求和,空/非法为 null);DTO/测试/api-contract/data-model 同步
+
+---
+
 ## Dependencies & Execution Order
 
 ### Phase Dependencies