| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- 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.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.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;
- 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 PosStoreMapper posStoreMapper = mock(PosStoreMapper.class);
- private final UnifiedOrderQueryService service =
- new UnifiedOrderQueryService(posOrderMapper, flashOrderMapper, posStoreMapper);
- @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
- @SuppressWarnings({"rawtypes", "unchecked"})
- void sourceFilterAddsPosOrderTypePredicate() {
- doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
- doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
- service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.TAKEAWAY, null, 10);
- service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.DINEIN, null, 10);
- service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.PICKUP, null, 10);
- service.query(USER_ID, UnifiedOrderTab.ALL, null, null, 10);
- ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper> captor =
- ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
- verify(posOrderMapper, times(4)).selectPage(any(), captor.capture());
- List<LambdaQueryWrapper<PosOrder>> wrappers = captor.getAllValues().stream()
- .map(wrapper -> (LambdaQueryWrapper<PosOrder>) wrapper)
- .toList();
- // takeaway 仅外送 type=0;dinein=堂食 type=2;pickup=自取 type=1
- assertTrue(wrappers.get(0).getSqlSegment().contains("type ="), wrappers.get(0).getSqlSegment());
- assertTrue(wrappers.get(0).getParamNameValuePairs().containsValue(0L),
- wrappers.get(0).getParamNameValuePairs().toString());
- assertTrue(wrappers.get(1).getSqlSegment().contains("type ="), wrappers.get(1).getSqlSegment());
- assertTrue(wrappers.get(1).getParamNameValuePairs().containsValue(2L),
- wrappers.get(1).getParamNameValuePairs().toString());
- assertTrue(wrappers.get(2).getSqlSegment().contains("type ="), wrappers.get(2).getSqlSegment());
- assertTrue(wrappers.get(2).getParamNameValuePairs().containsValue(1L),
- wrappers.get(2).getParamNameValuePairs().toString());
- // source=all 不限 type,堂食/自取单照常混排
- assertFalse(wrappers.get(3).getSqlSegment().contains("type ="), wrappers.get(3).getSqlSegment());
- }
- @Test
- void dineinAndPickupQueryOnlyPosOrderPathAndTagCursorWithOwnSource() {
- doReturn(pageOf(List.of(takeaway(21L, 3000L), takeaway(22L, 2000L))))
- .when(posOrderMapper).selectPage(any(), any());
- UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.DINEIN, null, 1);
- assertEquals(1, view.getList().size());
- // 卡片仍是外卖样式,堂食/自取由 type 字段渲染
- assertEquals("takeaway", view.getList().get(0).getSourceType());
- assertTrue(view.isHasMore());
- assertEquals("3000000_dinein_21", view.getNextCursor());
- verify(flashOrderMapper, never()).selectPage(any(), any());
- service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.PICKUP, null, 10);
- verify(posOrderMapper, times(2)).selectPage(any(), any());
- verify(flashOrderMapper, never()).selectPage(any(), any());
- }
- @Test
- @SuppressWarnings({"rawtypes", "unchecked"})
- void dineinCursorDrivesSameSecondIdDecisivePredicateOnNextScroll() {
- doReturn(pageOf(List.of())).when(posOrderMapper).selectPage(any(), any());
- service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.DINEIN,
- UnifiedCursor.parse("3000000_dinein_21"), 10);
- ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper> captor =
- ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
- verify(posOrderMapper).selectPage(any(), captor.capture());
- LambdaQueryWrapper<PosOrder> wrapper = (LambdaQueryWrapper<PosOrder>) captor.getValue();
- // 同源游标:同秒 id 决胜分支存在,保证翻页不丢同秒行
- String sql = wrapper.getSqlSegment();
- assertTrue(sql.contains(" OR "), sql);
- assertTrue(sql.contains("type ="), sql);
- }
- @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 takeawayItemsCarryStoreNameAndAvatarFromSingleBatchLookup() {
- PosOrder first = takeaway(21L, 3000L);
- first.setMdId(55L);
- PosOrder second = takeaway(22L, 2000L);
- second.setMdId(56L);
- PosOrder third = takeaway(23L, 1000L);
- third.setMdId(55L);
- doReturn(pageOf(List.of(first, second, third))).when(posOrderMapper).selectPage(any(), any());
- doReturn(pageOf(List.of())).when(flashOrderMapper).selectPage(any(), any());
- 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);
- 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());
- 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
- 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());
- }
- @Test
- void flashItemsCarryOrderVersion() {
- FlashDeliveryOrder order = flash(31L, 3000L);
- order.setVersion(7);
- doReturn(pageOf(List.of(order))).when(flashOrderMapper).selectPage(any(), any());
- UnifiedOrderPageView view = service.query(USER_ID, UnifiedOrderTab.ALL, OrderSource.FLASH, null, 10);
- assertEquals(7, view.getList().get(0).getOrderVersion());
- }
- // ---------- 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;
- }
- }
|