Forráskód Böngészése

036 骑手组合列表补齐旧列表逐单信息:legacyOrder/store/user/juli

对照 /system/orderQsOprate/orderList 的列表项(PosOrder 全字段 + juli + store + user),
普通单卡与组卡 members[] 统一附带订单全量快照、门店对象、用户对象和骑手定位到订单
坐标的直线距离(同旧公式,未传定位为 null);本页普通单与组子单合并一次批量装配。
接入文档 6.1/6.3 同步字段说明并调整原待抢不透出联系人口径为与旧列表一致。
qmj 2 napja
szülő
commit
9f5e7a6a8e

+ 82 - 12
ruoyi-admin/src/main/java/com/ruoyi/app/order/MultiStoreCheckoutQueryService.java

@@ -8,11 +8,19 @@ import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.CheckoutCardRow;
 import com.ruoyi.system.domain.CheckoutMemberRow;
+import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.mapper.MultiStoreCheckoutQueryMapper;
+import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.service.IPosStoreService;
 import org.springframework.stereotype.Service;
 
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
@@ -31,20 +39,26 @@ public class MultiStoreCheckoutQueryService {
     private final MultiStoreCheckoutQueryMapper mapper;
     private final MultiStoreCheckoutRefundProjectionService refunds;
     private final IPosOrderService ordinaryOrders;
+    private final IPosStoreService stores;
+    private final IInfoUserService users;
 
     public MultiStoreCheckoutQueryService(MultiStoreCheckoutQueryMapper mapper,
                                           MultiStoreCheckoutRefundProjectionService refunds,
-                                          IPosOrderService ordinaryOrders) {
+                                          IPosOrderService ordinaryOrders,
+                                          IPosStoreService stores,
+                                          IInfoUserService users) {
         this.mapper = mapper;
         this.refunds = refunds;
         this.ordinaryOrders = ordinaryOrders;
+        this.stores = stores;
+        this.users = users;
     }
 
     public MultiStorePageView userCards(Long userId, int page, int size, String tab) {
         validate(userId, page, size, tab, USER_TABS);
         long total = mapper.countUserCards(userId, tab);
         List<CheckoutCardRow> rows = mapper.selectUserCards(userId, tab, (page - 1) * size, size);
-        return new MultiStorePageView(assemble(rows), total, page, size);
+        return new MultiStorePageView(assemble(rows, null, null), total, page, size);
     }
 
     public Map<String, Long> userCardCount(Long userId) {
@@ -58,7 +72,7 @@ public class MultiStoreCheckoutQueryService {
         if (batchId == null || userId == null) throw invalid();
         CheckoutCardRow row = mapper.selectBatchForUser(batchId, userId);
         if (row == null) throw invalid();
-        return assemble(List.of(row)).get(0);
+        return assemble(List.of(row), null, null).get(0);
     }
 
     public MultiStorePageView riderTasks(Long riderId, int page, int size, String tab,
@@ -68,7 +82,7 @@ public class MultiStoreCheckoutQueryService {
         long total = mapper.countRiderTasks(riderId, tab, longitude, latitude, limitMeters);
         List<CheckoutCardRow> rows = mapper.selectRiderTasks(riderId, tab, longitude, latitude,
                 limitMeters, (page - 1) * size, size);
-        return new MultiStorePageView(assemble(rows), total, page, size);
+        return new MultiStorePageView(assemble(rows, longitude, latitude), total, page, size);
     }
 
     public Map<String, Long> riderTaskCount(Long riderId, BigDecimal longitude, BigDecimal latitude,
@@ -85,10 +99,11 @@ public class MultiStoreCheckoutQueryService {
         if (groupId == null || riderId == null) throw invalid();
         CheckoutCardRow row = mapper.selectGroupForRider(groupId, riderId, limitMeters);
         if (row == null) throw invalid();
-        return assemble(List.of(row)).get(0);
+        return assemble(List.of(row), null, null).get(0);
     }
 
-    private List<MultiStoreCardView> assemble(List<CheckoutCardRow> rows) {
+    private List<MultiStoreCardView> assemble(List<CheckoutCardRow> rows, BigDecimal longitude,
+                                               BigDecimal latitude) {
         if (rows == null || rows.isEmpty()) return List.of();
         List<Long> batchIds = rows.stream().map(CheckoutCardRow::getBatchId).filter(Objects::nonNull).distinct().toList();
         Map<Long, List<CheckoutMemberRow>> members = new HashMap<>();
@@ -105,19 +120,74 @@ public class MultiStoreCheckoutQueryService {
         List<MultiStoreCardView> result = rows.stream().map(row -> assembleOne(row,
                 members.getOrDefault(row.getBatchId(), List.of()),
                 refundFacts.getOrDefault(row.getBatchId(), Map.of()))).toList();
-        List<Long> ordinaryIds = rows.stream().filter(row -> "ORDER".equals(row.getCardType()))
-                .map(CheckoutCardRow::getCardId).filter(Objects::nonNull).distinct().toList();
-        if (!ordinaryIds.isEmpty()) {
-            // 仅查询当前页普通单,复用原 PosOrder 快照字段,避免新组合接口丢失旧单展示内容。
+        List<Long> orderIds = new ArrayList<>();
+        for (CheckoutCardRow row : rows) {
+            if ("ORDER".equals(row.getCardType()) && row.getCardId() != null) orderIds.add(row.getCardId());
+        }
+        for (List<CheckoutMemberRow> batchMembers : members.values()) {
+            for (CheckoutMemberRow member : batchMembers) {
+                if (member.getOrderId() != null) orderIds.add(member.getOrderId());
+            }
+        }
+        if (!orderIds.isEmpty()) {
+            // 与旧骑手列表逐项对齐:本页普通单与组子单都补全量订单、门店、用户与直线距离。
             Map<Long, PosOrder> originals = new HashMap<>();
-            for (PosOrder order : ordinaryOrders.listByIds(ordinaryIds)) originals.put(order.getId(), order);
+            for (PosOrder order : ordinaryOrders.listByIds(orderIds.stream().distinct().toList())) {
+                originals.put(order.getId(), order);
+            }
+            Set<Long> storeIds = new HashSet<>();
+            Set<Long> userIds = new HashSet<>();
+            for (PosOrder order : originals.values()) {
+                if (order.getMdId() != null) storeIds.add(order.getMdId());
+                if (order.getUserId() != null) userIds.add(order.getUserId());
+            }
+            Map<Integer, PosStore> storeMap = storeIds.isEmpty() ? Map.of()
+                    : stores.listByIds(storeIds).stream().collect(Collectors.toMap(PosStore::getId, s -> s));
+            Map<Long, InfoUser> userMap = userIds.isEmpty() ? Map.of()
+                    : users.listByIds(userIds).stream().collect(Collectors.toMap(InfoUser::getUserId, s -> s));
             for (MultiStoreCardView view : result) {
-                if ("ORDER".equals(view.getCardType())) view.setLegacyOrder(originals.get(view.getOrderId()));
+                if ("ORDER".equals(view.getCardType())) {
+                    attachOrderContext(view.getOrderId(), originals, storeMap, userMap, longitude, latitude,
+                            view::setLegacyOrder, view::setStore, view::setUser, view::setJuli);
+                } else {
+                    for (MultiStoreMemberView member : view.getMembers()) {
+                        attachOrderContext(member.getOrderId(), originals, storeMap, userMap, longitude, latitude,
+                                member::setLegacyOrder, member::setStore, member::setUser, member::setJuli);
+                    }
+                }
             }
         }
         return result;
     }
 
+    private void attachOrderContext(Long orderId, Map<Long, PosOrder> originals,
+                                    Map<Integer, PosStore> storeMap, Map<Long, InfoUser> userMap,
+                                    BigDecimal longitude, BigDecimal latitude,
+                                    java.util.function.Consumer<PosOrder> orderSetter,
+                                    java.util.function.Consumer<PosStore> storeSetter,
+                                    java.util.function.Consumer<InfoUser> userSetter,
+                                    java.util.function.Consumer<Double> juliSetter) {
+        PosOrder order = originals.get(orderId);
+        orderSetter.accept(order);
+        storeSetter.accept(order == null || order.getMdId() == null ? null : storeMap.get(order.getMdId().intValue()));
+        userSetter.accept(order == null || order.getUserId() == null ? null : userMap.get(order.getUserId()));
+        juliSetter.accept(straightKm(order, longitude, latitude));
+    }
+
+    /** 旧骑手列表同款球面直线距离(公里、两位小数);订单或请求坐标缺失返回空。 */
+    private Double straightKm(PosOrder order, BigDecimal longitude, BigDecimal latitude) {
+        if (order == null || order.getLongitude() == null || order.getLatitude() == null
+                || longitude == null || latitude == null) return null;
+        double lat1 = Math.toRadians(order.getLatitude().doubleValue());
+        double lat2 = Math.toRadians(latitude.doubleValue());
+        double dlat = lat2 - lat1;
+        double dlng = Math.toRadians(longitude.doubleValue() - order.getLongitude().doubleValue());
+        double a = Math.sin(dlat / 2) * Math.sin(dlat / 2)
+                + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlng / 2) * Math.sin(dlng / 2);
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+        return Math.round(6371 * c * 100.0) / 100.0;
+    }
+
     private MultiStoreCardView assembleOne(CheckoutCardRow row, List<CheckoutMemberRow> members,
                                            Map<Long, MultiStoreRefundMemberView> refundFacts) {
         MultiStoreCardView view = new MultiStoreCardView();

+ 6 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/MultiStoreCardView.java

@@ -1,7 +1,9 @@
 package com.ruoyi.app.order.dto;
 
 import lombok.Data;
+import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -14,6 +16,10 @@ public class MultiStoreCardView {
     private Long orderId;
     /** 普通单沿用原骑手/用户列表的完整订单字段,不把旧单强行压缩为 036 子单摘要。 */
     private PosOrder legacyOrder;
+    /** 普通单卡与旧骑手列表逐项对齐的门店、用户与直线距离;组卡/批次卡无单一对应,保持空。 */
+    private PosStore store;
+    private InfoUser user;
+    private Double juli;
     private Long batchId;
     private Long groupId;
     private Long version;

+ 10 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/MultiStoreMemberView.java

@@ -1,9 +1,13 @@
 package com.ruoyi.app.order.dto;
 
 import lombok.Data;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
+
 import java.util.List;
 
-/** 组合卡逐店摘要;不承载待抢阶段的用户联系人或地址。 */
+/** 组合卡逐店摘要;骑手端按旧列表口径附带订单全量快照、门店、用户与直线距离。 */
 @Data
 public class MultiStoreMemberView {
     private Long orderId;
@@ -20,5 +24,10 @@ public class MultiStoreMemberView {
     private String refundStatus;
     private Integer codToCollectAmount;
     private Integer codToMerchantAmount;
+    /** 与旧骑手列表逐单展示对齐的附加信息;无对应数据时保持空。 */
+    private PosOrder legacyOrder;
+    private PosStore store;
+    private InfoUser user;
+    private Double juli;
     private List<String> allowedActions = List.of();
 }

+ 51 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/order/MultiStoreCheckoutQueryServiceTest.java

@@ -7,8 +7,12 @@ import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.CheckoutCardRow;
 import com.ruoyi.system.domain.CheckoutMemberRow;
 import com.ruoyi.system.mapper.MultiStoreCheckoutQueryMapper;
+import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.PosStore;
+import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
+import com.ruoyi.system.service.IPosStoreService;
 import org.junit.jupiter.api.Test;
 
 import java.time.LocalDateTime;
@@ -25,8 +29,10 @@ class MultiStoreCheckoutQueryServiceTest {
     private final MultiStoreCheckoutQueryMapper mapper = mock(MultiStoreCheckoutQueryMapper.class);
     private final MultiStoreCheckoutRefundProjectionService refunds = mock(MultiStoreCheckoutRefundProjectionService.class);
     private final IPosOrderService ordinaryOrders = mock(IPosOrderService.class);
+    private final IPosStoreService stores = mock(IPosStoreService.class);
+    private final IInfoUserService users = mock(IInfoUserService.class);
     private final MultiStoreCheckoutQueryService service =
-            new MultiStoreCheckoutQueryService(mapper, refunds, ordinaryOrders);
+            new MultiStoreCheckoutQueryService(mapper, refunds, ordinaryOrders, stores, users);
 
     @Test
     void userPageCountsBatchOnceAlongsideOrdinaryOrder() {
@@ -37,7 +43,7 @@ class MultiStoreCheckoutQueryServiceTest {
         PosOrder legacy = new PosOrder();
         legacy.setId(9L);
         legacy.setFood("legacy-snapshot");
-        when(ordinaryOrders.listByIds(List.of(9L))).thenReturn(List.of(legacy));
+        when(ordinaryOrders.listByIds(List.of(9L, 71L, 72L))).thenReturn(List.of(legacy));
         when(mapper.countUserCards(21L, "all")).thenReturn(2L);
         when(mapper.selectUserCards(21L, "all", 0, 10)).thenReturn(List.of(batch, order));
         when(mapper.selectMembersForBatches(List.of(7L))).thenReturn(List.of(
@@ -153,6 +159,49 @@ class MultiStoreCheckoutQueryServiceTest {
         assertEquals(null, result.records().get(0).getMembers().get(1).getCodToCollectAmount());
     }
 
+    @Test
+    void riderCardsCarryLegacyOrderStoreUserAndJuliLikeOldList() {
+        // 骑手定位与订单坐标一致,juli 恒为 0.0,避免测试复制距离公式。
+        BigDecimal riderLng = new BigDecimal("108.30");
+        BigDecimal riderLat = new BigDecimal("22.90");
+        CheckoutCardRow group = card("GROUP", 4L, "GROUP:4");
+        group.setBatchId(7L);
+        group.setGroupStatus("ACCEPTED");
+        group.setPrimaryTab("toPickup");
+        CheckoutCardRow ordinary = card("ORDER", 9L, "ORDER:9");
+        when(mapper.countRiderTasks(33L, "toPickup", riderLng, riderLat, 1000L)).thenReturn(2L);
+        when(mapper.selectRiderTasks(33L, "toPickup", riderLng, riderLat, 1000L, 0, 10))
+                .thenReturn(List.of(group, ordinary));
+        when(mapper.selectMembersForBatches(List.of(7L))).thenReturn(List.of(member(7L, 71L, 1L, "COD_CONFIRMED", 80, 0)));
+
+        PosOrder memberOrder = new PosOrder();
+        memberOrder.setId(71L); memberOrder.setMdId(5L); memberOrder.setUserId(21L);
+        memberOrder.setLongitude(riderLng); memberOrder.setLatitude(riderLat);
+        PosOrder ordinaryOrder = new PosOrder();
+        ordinaryOrder.setId(9L); ordinaryOrder.setMdId(5L); ordinaryOrder.setUserId(21L);
+        ordinaryOrder.setLongitude(riderLng); ordinaryOrder.setLatitude(riderLat);
+        when(ordinaryOrders.listByIds(List.of(9L, 71L))).thenReturn(List.of(memberOrder, ordinaryOrder));
+        PosStore store = new PosStore();
+        store.setId(5);
+        when(stores.listByIds(java.util.Set.of(5L))).thenReturn(java.util.List.of(store));
+        InfoUser user = new InfoUser();
+        user.setUserId(21L);
+        when(users.listByIds(java.util.Set.of(21L))).thenReturn(java.util.List.of(user));
+
+        MultiStorePageView result = service.riderTasks(33L, 1, 10, "toPickup", riderLng, riderLat, 1000L);
+
+        MultiStoreCardView groupCard = result.records().get(0);
+        assertEquals(71L, groupCard.getMembers().get(0).getLegacyOrder().getId());
+        assertEquals(5, groupCard.getMembers().get(0).getStore().getId());
+        assertEquals(21L, groupCard.getMembers().get(0).getUser().getUserId());
+        assertEquals(0.0, groupCard.getMembers().get(0).getJuli());
+        MultiStoreCardView orderCard = result.records().get(1);
+        assertEquals(9L, orderCard.getLegacyOrder().getId());
+        assertEquals(5, orderCard.getStore().getId());
+        assertEquals(21L, orderCard.getUser().getUserId());
+        assertEquals(0.0, orderCard.getJuli());
+    }
+
     @Test
     void invalidPaginationIsRejectedBeforeQuery() {
         try (org.mockito.MockedStatic<com.ruoyi.common.utils.MessageUtils> messages =

+ 10 - 2
specs/036-multi-store-checkout/frontend-app-integration.md

@@ -215,9 +215,12 @@ POST /system/multiStoreCheckout/quote
 | `stores[].items[].productId` | long | 是 | 商品 ID;只提交商品/规格 ID,客户端价格不作为计价依据 |
 | `stores[].items[].quantity` | int | 是 | 数量 |
 | `stores[].items[].specValueIds` | long[] | 是 | 规格 ID 列表,无规格传 `[]` |
+
 | `stores[].couponId` | long | 否 | 门店优惠券 ID,无传 `null` |
 | `stores[].forcePath` | string | 否 | 预留字段,正常流程传 `null` |
 
+建批后的子单 `food` 商品快照由服务端按 `specValueIds` 查数据库生成:有规格时同时返回 `ask`(规格名称数组,兼容原订单详情展示)和 `specValueIds`(规格值 ID 数组)。免费规格的 `otherPrice` 可以为 0,但 `ask` 仍应展示;不要只传客户端的 `ask` 文本代替规格 ID。此修复只影响新创建的版本 2 快照,不回写历史订单。
+
 响应 `data`:
 
 | 字段 | 类型 | 说明 |
@@ -410,7 +413,10 @@ GET /system/multiStoreCheckout/rider/tasks?page=1&size=10&tab=newTask&longitude=
 | `tab` | 否 | 默认 `newTask`,取值见 3.6 |
 | `longitude` / `latitude` | 否 | 骑手坐标。**只用于普通单排序兼容**,不能替代服务端位置;待抢组资格以服务端最近位置与首店距离为准 |
 
-响应 `data` 为分页结构,`records[]` 中 `GROUP` 卡一张 = 一组(角标与列表同口径,组只计一次)。待抢组只在批次已生效且达到原预约开放时点、组 `openedAt` 已写入后可见;新任务有有效坐标时按首店距离升序,其余按创建时间方向。最后成员就绪并提交后,服务端立即尝试开放即时/已到点批次;若失败,由原分钟任务重试,预约未到也由该任务到点开放。App 不依赖固定一分钟延迟,付款后可刷新列表,未出现时以服务端状态为准;对外 URL 保持不变。
+响应 `data` 为分页结构,`records[]` 中 `GROUP` 卡一张 = 一组(角标与列表同口径,组只计一次)。
+普通单卡与组卡 `members[]` 均附带与旧骑手列表逐单一致的信息:`legacyOrder`(订单全量快照)、
+`store`(门店对象)、`user`(用户对象)、`juli`(骑手定位到订单坐标的直线距离,公里两位小数;
+未传定位时为 null,组卡另有服务端口径的 `anchorDistanceMeters`)。待抢组只在批次已生效且达到原预约开放时点、组 `openedAt` 已写入后可见;新任务有有效坐标时按首店距离升序,其余按创建时间方向。最后成员就绪并提交后,服务端立即尝试开放即时/已到点批次;若失败,由原分钟任务重试,预约未到也由该任务到点开放。App 不依赖固定一分钟延迟,付款后可刷新列表,未出现时以服务端状态为准;对外 URL 保持不变。
 
 ### 6.2 各页签任务数(角标)
 
@@ -438,7 +444,9 @@ GET /system/multiStoreCheckout/groups/{groupId}
 - `allowedActions`:待抢时 `ACCEPT_GROUP`;本人已接组后 `members[]` 内出现 `PICKUP_MEMBER` / `DELIVER_MEMBER`。
 - `codToCollectAmount`:有效到付子单的待收总额。
 
-待抢组的成员摘要只含店 ID、店名、订单号及状态,**不透出**用户联系人与地址;已接单后按子单详情读取(见 6.5)。接单前如产品要求画完整多店路线,须后端另行提供脱敏路线数据,前端不要凭店名推测坐标。
+成员摘要(含待抢阶段)附带 `legacyOrder`/`store`/`user`/`juli`,与旧骑手列表逐单口径一致,
+可直接复用旧列表项渲染;已接单后逐店履约仍按子单详情读取(见 6.5)。如产品后续要求待抢阶段
+脱敏联系人与地址,须另行变更口径并同步两端。
 
 ### 6.4 整组接单