|
|
@@ -0,0 +1,333 @@
|
|
|
+package com.ruoyi.app.unifiedorder;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+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.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.flash.FlashDeliveryOrderMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+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.ArgumentMatchers.anyCollection;
|
|
|
+import static org.mockito.Mockito.doReturn;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/** 统一订单列表服务:两路查询、归并排序、翻页完整性、hasMore、tab 跳过闪送路。 */
|
|
|
+class UnifiedOrderQueryServiceTest {
|
|
|
+
|
|
|
+ private static final Long USER_ID = 9L;
|
|
|
+
|
|
|
+ private final PosOrderMapper posOrderMapper = mock(PosOrderMapper.class);
|
|
|
+ private final FlashDeliveryOrderMapper flashOrderMapper = mock(FlashDeliveryOrderMapper.class);
|
|
|
+ private final InfoUserMapper infoUserMapper = mock(InfoUserMapper.class);
|
|
|
+ private final UnifiedOrderQueryService service =
|
|
|
+ new UnifiedOrderQueryService(posOrderMapper, flashOrderMapper, infoUserMapper);
|
|
|
+
|
|
|
+ @BeforeAll
|
|
|
+ static void initializeTableMetadata() {
|
|
|
+ TableInfoHelper.initTableInfo(
|
|
|
+ new MapperBuilderAssistant(new MybatisConfiguration(), ""), PosOrder.class);
|
|
|
+ TableInfoHelper.initTableInfo(
|
|
|
+ new MapperBuilderAssistant(new MybatisConfiguration(), ""), FlashDeliveryOrder.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void twoPathsMergeInDisplayOrderAndReturnExactlySize() {
|
|
|
+ doReturn(pageOf(List.of(
|
|
|
+ takeaway(21L, 3000L), takeaway(22L, 2000L), takeaway(23L, 1000L))))
|
|
|
+ .when(posOrderMapper).selectPage(any(), any());
|
|
|
+ doReturn(pageOf(List.of(
|
|
|
+ flash(31L, 3000L), flash(32L, 2500L))))
|
|
|
+ .when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 3);
|
|
|
+
|
|
|
+ assertEquals(3, view.getList().size());
|
|
|
+ // 同秒并列:flash 排在 takeaway 前;整体时间非递增
|
|
|
+ assertEquals(List.of("flash", "takeaway", "flash"),
|
|
|
+ view.getList().stream().map(UnifiedOrderListItem::getSourceType).toList());
|
|
|
+ assertEquals(List.of(31L, 21L, 32L),
|
|
|
+ view.getList().stream().map(UnifiedOrderListItem::getOrderId).toList());
|
|
|
+ long previous = Long.MAX_VALUE;
|
|
|
+ for (UnifiedOrderListItem item : view.getList()) {
|
|
|
+ assertTrue(item.getCreateTime() <= previous, "createTime 非递增");
|
|
|
+ previous = item.getCreateTime();
|
|
|
+ assertEquals("completed", item.getUnifiedStatus());
|
|
|
+ }
|
|
|
+ assertTrue(view.isHasMore());
|
|
|
+ assertEquals("2500000_flash_32", view.getNextCursor());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void eachPathQueriesSizePlusOneOnFirstPageWithoutCount() {
|
|
|
+ doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 10);
|
|
|
+
|
|
|
+ ArgumentCaptor<Page> posPage = ArgumentCaptor.forClass(Page.class);
|
|
|
+ verify(posOrderMapper).selectPage(posPage.capture(), any());
|
|
|
+ assertEquals(1L, posPage.getValue().getCurrent());
|
|
|
+ assertEquals(11L, posPage.getValue().getSize());
|
|
|
+ assertFalse(posPage.getValue().searchCount());
|
|
|
+
|
|
|
+ ArgumentCaptor<Page> flashPage = ArgumentCaptor.forClass(Page.class);
|
|
|
+ verify(flashOrderMapper).selectPage(flashPage.capture(), any());
|
|
|
+ assertEquals(11L, flashPage.getValue().getSize());
|
|
|
+ assertFalse(flashPage.getValue().searchCount());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void scrollingThroughEntireSetYieldsEveryOrderExactlyOnce() {
|
|
|
+ // 样本全集(含同秒跨源并列、同源同秒 id 决胜)
|
|
|
+ List<FlashDeliveryOrder> flashAll = List.of(
|
|
|
+ flash(31L, 3000L), flash(32L, 2000L), flash(34L, 1000L), flash(33L, 1000L));
|
|
|
+ List<PosOrder> takeawayAll = List.of(
|
|
|
+ takeaway(21L, 3000L), takeaway(22L, 2000L), takeaway(23L, 1000L));
|
|
|
+ int size = 3;
|
|
|
+
|
|
|
+ List<String> collected = new ArrayList<>();
|
|
|
+ UnifiedCursor cursor = null;
|
|
|
+ for (int round = 0; round < 10; round++) {
|
|
|
+ doReturn(pageOf(afterCursor(flashAll, OrderSource.FLASH, cursor, size + 1)))
|
|
|
+ .when(flashOrderMapper).selectPage(any(), any());
|
|
|
+ doReturn(pageOf(afterCursor(takeawayAll, OrderSource.TAKEAWAY, cursor, size + 1)))
|
|
|
+ .when(posOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, null, cursor, size);
|
|
|
+ view.getList().forEach(item -> collected.add(item.getSourceType() + ":" + item.getOrderId()));
|
|
|
+ if (!view.isHasMore()) {
|
|
|
+ assertNull(view.getNextCursor());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ cursor = UnifiedCursor.parse(view.getNextCursor());
|
|
|
+ }
|
|
|
+ // 展示全序:3000F31 3000T21 2000F32 2000T22 1000F34 1000F33 1000T23
|
|
|
+ assertEquals(List.of("flash:31", "takeaway:21", "flash:32",
|
|
|
+ "takeaway:22", "flash:34", "flash:33", "takeaway:23"), collected);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void sizeIsClampedBetweenOneAndFifty() {
|
|
|
+ doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 0);
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 999);
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 10);
|
|
|
+
|
|
|
+ ArgumentCaptor<Page> pages = ArgumentCaptor.forClass(Page.class);
|
|
|
+ verify(posOrderMapper, times(3)).selectPage(pages.capture(), any());
|
|
|
+ assertEquals(List.of(2L, 51L, 11L),
|
|
|
+ pages.getAllValues().stream().map(Page::getSize).toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void singleSourceFilterQueriesOnlyThatMapper() {
|
|
|
+ doReturn(pageOf(List.of(flash(31L, 3000L)))).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
|
|
|
+ verify(posOrderMapper, never()).selectPage(any(), any());
|
|
|
+ verify(flashOrderMapper, times(1)).selectPage(any(), any());
|
|
|
+
|
|
|
+ doReturn(pageOf(List.of(takeaway(21L, 3000L)))).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
|
|
|
+ verify(flashOrderMapper, times(1)).selectPage(any(), any());
|
|
|
+ verify(posOrderMapper, times(1)).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void unpaidAndRefundTabsNeverQueryFlashPath() {
|
|
|
+ doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.UNPAID, null, null, 10);
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.REFUND, null, null, 10);
|
|
|
+ verify(flashOrderMapper, never()).selectPage(any(), any());
|
|
|
+ verify(posOrderMapper, times(2)).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void emptyResultReportsNoMoreAndNoCursor() {
|
|
|
+ doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 10);
|
|
|
+
|
|
|
+ assertTrue(view.getList().isEmpty());
|
|
|
+ assertFalse(view.isHasMore());
|
|
|
+ assertNull(view.getNextCursor());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void takeawayItemsCarryStoreNameFromSingleBatchLookup() {
|
|
|
+ PosOrder first = takeaway(21L, 3000L);
|
|
|
+ first.setShId(55L);
|
|
|
+ PosOrder second = takeaway(22L, 2000L);
|
|
|
+ second.setShId(56L);
|
|
|
+ PosOrder third = takeaway(23L, 1000L);
|
|
|
+ third.setShId(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));
|
|
|
+
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
|
|
|
+
|
|
|
+ assertEquals(3, view.getList().size());
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refundWithFlashSourceQueriesNothingAtAll() {
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.REFUND, OrderSource.FLASH, null, 10);
|
|
|
+ assertTrue(view.getList().isEmpty());
|
|
|
+ assertFalse(view.isHasMore());
|
|
|
+ assertNull(view.getNextCursor());
|
|
|
+ verify(posOrderMapper, never()).selectPage(any(), any());
|
|
|
+ verify(flashOrderMapper, never()).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void activeOrUnpaidWithTakeawaySourceQueriesOnlyTakeawayPath() {
|
|
|
+ doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ACTIVE, OrderSource.TAKEAWAY, null, 10);
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.UNPAID, OrderSource.TAKEAWAY, null, 10);
|
|
|
+ verify(posOrderMapper, times(2)).selectPage(any(), any());
|
|
|
+ verify(flashOrderMapper, never()).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void flashPathQueriesBothSenderAndReceiverParticipation() {
|
|
|
+ doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+ service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
|
|
|
+
|
|
|
+ ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper> captor =
|
|
|
+ ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
|
|
+ verify(flashOrderMapper).selectPage(any(), captor.capture());
|
|
|
+ LambdaQueryWrapper<FlashDeliveryOrder> wrapper =
|
|
|
+ (LambdaQueryWrapper<FlashDeliveryOrder>) captor.getValue();
|
|
|
+ String sql = wrapper.getSqlSegment();
|
|
|
+ assertTrue(sql.contains("user_id ="), sql);
|
|
|
+ assertTrue(sql.contains("receiver_user_id ="), sql);
|
|
|
+ assertTrue(sql.contains(" OR "), sql);
|
|
|
+ assertEquals(2, java.util.Collections.frequency(wrapper.getParamNameValuePairs().values(), USER_ID),
|
|
|
+ wrapper.getParamNameValuePairs().toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void receivedFlashOrderCarriesRolePerViewer() {
|
|
|
+ FlashDeliveryOrder order = flash(31L, 3000L);
|
|
|
+ doReturn(pageOf(List.of(order))).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ // 收件人 B=77 视角
|
|
|
+ UnifiedOrderPageView receiverView = service.query(77L, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
|
|
|
+ assertEquals(1, receiverView.getList().size());
|
|
|
+ assertEquals("receiver", receiverView.getList().get(0).getRole());
|
|
|
+
|
|
|
+ // 寄件人 A=9 视角
|
|
|
+ UnifiedOrderPageView senderView = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
|
|
|
+ assertEquals(1, senderView.getList().size());
|
|
|
+ assertEquals("sender", senderView.getList().get(0).getRole());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void selfSentFlashOrderKeepsSenderRole() {
|
|
|
+ FlashDeliveryOrder order = flash(31L, 3000L);
|
|
|
+ order.setReceiverUserId(USER_ID);
|
|
|
+ doReturn(pageOf(List.of(order))).when(flashOrderMapper).selectPage(any(), any());
|
|
|
+
|
|
|
+ UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
|
|
|
+ assertEquals(1, view.getList().size());
|
|
|
+ assertEquals("sender", view.getList().get(0).getRole());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- helpers ----------
|
|
|
+
|
|
|
+ private static PosOrder takeaway(long id, long timeSec) {
|
|
|
+ PosOrder order = new PosOrder();
|
|
|
+ order.setId(id);
|
|
|
+ order.setDdId("DD" + id);
|
|
|
+ order.setUserId(USER_ID);
|
|
|
+ order.setCretim(new Date(timeSec * 1000));
|
|
|
+ order.setState(3L);
|
|
|
+ order.setType(0L);
|
|
|
+ order.setPayStatus(1L);
|
|
|
+ order.setPayType("2");
|
|
|
+ order.setAfterSaleStatus(0L);
|
|
|
+ order.setAmount(100);
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static FlashDeliveryOrder flash(long id, long timeSec) {
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setId(id);
|
|
|
+ order.setOrderNo("FD" + id);
|
|
|
+ order.setUserId(USER_ID);
|
|
|
+ order.setCreateTime(new Date(timeSec * 1000));
|
|
|
+ order.setStatus(FlashDeliveryStatus.COMPLETED);
|
|
|
+ order.setAmount(500L);
|
|
|
+ order.setCurrency("VND");
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <T> Page<T> pageOf(List<T> records) {
|
|
|
+ Page<T> page = new Page<>(1, Math.max(records.size(), 1), false);
|
|
|
+ page.setRecords(new ArrayList<>(records));
|
|
|
+ return page;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 模拟 keyset:返回该源中展示序严格晚于游标的前 limit 行。 */
|
|
|
+ private static <T> List<T> afterCursor(List<T> sourceRows, OrderSource source, UnifiedCursor cursor, int limit) {
|
|
|
+ List<T> result = new ArrayList<>();
|
|
|
+ for (T row : sourceRows) {
|
|
|
+ UnifiedCursor key;
|
|
|
+ if (source == OrderSource.TAKEAWAY) {
|
|
|
+ PosOrder order = (PosOrder) row;
|
|
|
+ key = UnifiedCursor.of(order.getCretim().getTime(), source, order.getId());
|
|
|
+ } else {
|
|
|
+ FlashDeliveryOrder order = (FlashDeliveryOrder) row;
|
|
|
+ key = UnifiedCursor.of(order.getCreateTime().getTime(), source, order.getId());
|
|
|
+ }
|
|
|
+ if (cursor == null || UnifiedCursor.DISPLAY_ORDER.compare(key, cursor) > 0) {
|
|
|
+ result.add(row);
|
|
|
+ if (result.size() >= limit) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|