| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- package com.ruoyi.app.flashdelivery.service;
- import com.ruoyi.app.flashdelivery.dto.*;
- import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
- import com.ruoyi.app.flashdelivery.route.RouteDistance;
- import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
- import com.ruoyi.app.order.RiderDeliveryLockService;
- import com.ruoyi.common.core.domain.entity.SysDictData;
- import com.ruoyi.common.exception.ServiceException;
- import com.ruoyi.common.utils.DictUtils;
- import com.ruoyi.system.domain.InfoUser;
- import com.ruoyi.system.domain.flash.*;
- import com.ruoyi.system.mapper.InfoUserMapper;
- import com.ruoyi.system.mapper.flash.*;
- import org.junit.jupiter.api.*;
- import org.mockito.MockedStatic;
- import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
- import org.springframework.beans.factory.support.DefaultListableBeanFactory;
- import org.springframework.context.support.StaticMessageSource;
- import org.springframework.test.util.ReflectionTestUtils;
- import com.ruoyi.common.utils.spring.SpringUtils;
- import java.math.BigDecimal;
- import java.util.*;
- import static org.junit.jupiter.api.Assertions.*;
- import static org.mockito.ArgumentMatchers.*;
- import static org.mockito.Mockito.*;
- /** v2 多点取送:统一 stops 数组、订单级物品、逐站送达与取消门禁。 */
- class FlashDeliveryV2Test {
- private static ConfigurableListableBeanFactory original;
- @BeforeAll static void messages() {
- original = (ConfigurableListableBeanFactory) ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
- DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
- StaticMessageSource source = new StaticMessageSource(); source.setUseCodeAsDefaultMessage(true);
- factory.registerSingleton("messageSource", source); new SpringUtils().postProcessBeanFactory(factory);
- }
- @AfterAll static void restoreMessages() { new SpringUtils().postProcessBeanFactory(original); }
- private static void multiStopDicts(MockedStatic<DictUtils> dict) {
- dict.when(() -> DictUtils.getDictCache("flash_pickup_stop_limit")).thenReturn(List.of(dictionary("3")));
- dict.when(() -> DictUtils.getDictCache("flash_show_multi_pickup")).thenReturn(List.of(dictionary("1")));
- dict.when(() -> DictUtils.getDictCache("flash_delivery_stop_limit")).thenReturn(List.of(dictionary("3")));
- dict.when(() -> DictUtils.getDictCache("flash_show_multi_dropoff")).thenReturn(List.of(dictionary("1")));
- }
- @Test void v2CreateSavesUnifiedStopsWithOrderLevelItemAndPerStopReceivers() {
- Fixture f = new Fixture();
- try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
- multiStopDicts(dict);
- FlashDeliveryV2CreateRequest request = v2Create();
- FlashDeliveryQuoteView quote = f.service.quoteV2(request);
- request.setPricingId(quote.getPricingId()); request.setPricingVersion(quote.getPricingVersion());
- request.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee()); request.setQuotedDistanceFee(quote.getDistanceFee());
- request.setQuotedUrgentFee(quote.getUrgentFee()); request.setQuotedAmount(quote.getAmount());
- f.service.createV2(7L, request);
- var saved = argThatStops(f);
- assertEquals(List.of("PICKUP", "PICKUP", "DELIVERY", "DELIVERY"),
- saved.stream().map(FlashDeliveryOrderStop::getStopType).toList());
- assertEquals(List.of(0, 1, 2, 3), saved.stream().map(FlashDeliveryOrderStop::getStopOrder).toList());
- assertEquals(List.of("取月餅", "取牛奶", "送管理室", "送前台"),
- saved.stream().map(FlashDeliveryOrderStop::getRemark).toList());
- // 按站收件人:两个收货站按各自电话分别固化;取货站不固化。
- assertEquals(Arrays.asList(null, null, 31L, 32L), saved.stream().map(FlashDeliveryOrderStop::getReceiverUserId).toList());
- // 启用收货码时各收货站独立生成。
- assertNotNull(saved.get(2).getDeliveryPinCode());
- assertNotNull(saved.get(3).getDeliveryPinCode());
- assertNotEquals(saved.get(2).getDeliveryPinCode(), saved.get(3).getDeliveryPinCode());
- var order = argThatOrder(f);
- assertEquals(2, order.getDeliveryStopCount());
- assertEquals(0, order.getDeliveredStopCount());
- // 物品主体整单一次,落在订单级摘要字段。
- assertEquals("FOOD_INGREDIENTS", order.getPackageType());
- assertEquals(8, order.getQuantity());
- }
- }
- @Test void v2RejectsDeliveryBeforePickupLimitAndDisabledSwitch() {
- Fixture f = new Fixture();
- try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
- multiStopDicts(dict);
- FlashDeliveryV2CreateRequest wrongOrder = v2Create();
- wrongOrder.getStops().add(0, stop("DELIVERY", "0911111112", "台北市松山區B街2號", "d1"));
- assertThrows(ServiceException.class, () -> f.service.createV2(7L, wrongOrder));
- FlashDeliveryV2CreateRequest tooMany = v2Create();
- for (int i = 0; i < 2; i++) tooMany.getStops().add(stop("DELIVERY", "091111111" + (4 + i), "台北市中山區C街" + i + "號", "d" + i));
- assertThrows(ServiceException.class, () -> f.service.createV2(7L, tooMany));
- verify(f.orders, never()).insert(any(FlashDeliveryOrder.class));
- dict.when(() -> DictUtils.getDictCache("flash_show_multi_dropoff")).thenReturn(List.of(dictionary("0")));
- assertThrows(ServiceException.class, () -> f.service.quoteV2(v2Create()));
- }
- }
- @Test void v2QuoteRoutesThroughAllStopsInUserOrder() {
- Fixture f = new Fixture();
- try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
- multiStopDicts(dict);
- FlashDeliveryV2QuoteRequest request = v2Create();
- FlashDeliveryQuoteView view = f.service.quoteV2(request);
- assertEquals(5000, view.getDistanceMeters());
- var points = f.capturedPoints;
- assertEquals(4, points.size());
- verify(f.routes).calculate(anyList(), eq(1));
- }
- }
- @Test void perStopDeliveryAdvancesOrderOnlyAfterLastStopAndBlocksCancel() {
- Fixture f = new Fixture();
- f.order.setStatus("PICKED_UP"); f.order.setPinRequired(false);
- f.rows.forEach(row -> { if ("PICKUP".equals(row.getStopType())) row.setStatus("PICKED_UP"); });
- when(f.orders.updateDeliveryFulfilment(any())).thenReturn(1);
- FlashDeliveryDeliverRequest deliver = new FlashDeliveryDeliverRequest();
- deliver.setImageUrls(List.of("/profile/d1.jpg"));
- deliver.setStopId(13L);
- f.service.deliverAtStop(8L, 10L, 13L, deliver);
- assertEquals("DELIVERED", f.rows.get(2).getStatus());
- assertEquals(1, f.order.getDeliveredStopCount());
- assertEquals("PICKED_UP", f.order.getStatus());
- // 部分送达后用户与平台都不能自助取消。
- FlashDeliveryReasonRequest cancel = new FlashDeliveryReasonRequest(); cancel.setReason("changed mind");
- assertThrows(ServiceException.class, () -> f.service.userCancel(7L, 10L, cancel));
- verify(f.orders, never()).cancel(any(), any(), any(), any(), any(), any());
- // 同站重试幂等:不重复写凭证、不重复推进。
- f.service.deliverAtStop(8L, 10L, 13L, deliver);
- verify(f.images, times(1)).insert(any(FlashDeliveryOrderImage.class));
- // 末站完成后整单推进一次。
- deliver.setStopId(14L);
- f.service.deliverAtStop(8L, 10L, 14L, deliver);
- assertEquals("DELIVERED", f.order.getStatus());
- assertEquals(2, f.order.getDeliveredStopCount());
- assertEquals(f.rows.get(3).getDeliveredAt(), f.order.getDeliveredAt());
- verify(f.notifications, times(1)).notifyDelivered(any());
- }
- @Test void perStopPinIsValidatedAgainstThatStopOnly() {
- Fixture f = new Fixture();
- f.order.setStatus("PICKED_UP"); f.order.setPinRequired(true); f.order.setDeliveryPinCode("0000");
- f.rows.forEach(row -> { if ("PICKUP".equals(row.getStopType())) row.setStatus("PICKED_UP"); });
- f.rows.get(2).setDeliveryPinCode("1111");
- f.rows.get(3).setDeliveryPinCode("2222");
- when(f.orders.updateDeliveryFulfilment(any())).thenReturn(1);
- FlashDeliveryDeliverRequest deliver = new FlashDeliveryDeliverRequest();
- deliver.setImageUrls(List.of("/profile/d1.jpg"));
- deliver.setStopId(13L);
- deliver.setPinCode("2222");
- assertThrows(ServiceException.class, () -> f.service.deliverAtStop(8L, 10L, 13L, deliver));
- verify(f.images, never()).insert(any(FlashDeliveryOrderImage.class));
- deliver.setPinCode("1111");
- f.service.deliverAtStop(8L, 10L, 13L, deliver);
- assertEquals("DELIVERED", f.rows.get(2).getStatus());
- }
- private List<FlashDeliveryOrderStop> argThatStops(Fixture f) {
- var captor = org.mockito.ArgumentCaptor.forClass(FlashDeliveryOrderStop.class);
- verify(f.stops, atLeast(4)).insert(captor.capture());
- return captor.getAllValues();
- }
- private FlashDeliveryOrder argThatOrder(Fixture f) {
- var captor = org.mockito.ArgumentCaptor.forClass(FlashDeliveryOrder.class);
- verify(f.orders).insert(captor.capture());
- return captor.getValue();
- }
- private static FlashDeliveryV2StopRequest stop(String type, String phone, String address, String remark) {
- FlashDeliveryV2StopRequest stop = new FlashDeliveryV2StopRequest();
- stop.setType(type); stop.setName("聯絡人"); stop.setPhone(phone);
- stop.setAddress(address); stop.setAddressDetail("3樓"); stop.setRemark(remark);
- int seq = Math.abs(address.hashCode()) % 100;
- stop.setLatitude(BigDecimal.valueOf(25.03 + seq * .0001));
- stop.setLongitude(BigDecimal.valueOf(121.56 + seq * .0001));
- return stop;
- }
- private static FlashDeliveryV2CreateRequest v2Create() {
- FlashDeliveryV2CreateRequest request = new FlashDeliveryV2CreateRequest();
- request.setServiceType("HELP_SEND"); request.setDeliveryType("NORMAL"); request.setVehicleType(1);
- request.setTipAmount(0L); request.setPackageType("FOOD_INGREDIENTS"); request.setQuantity(8);
- request.setClientRequestId("v2-" + System.nanoTime());
- List<FlashDeliveryV2StopRequest> stops = new ArrayList<>(List.of(
- stop("PICKUP", "0911111110", "台北市大安區信義路三段56號", "取月餅"),
- stop("PICKUP", "0911111111", "台北市信義區松高路11號", "取牛奶"),
- stop("DELIVERY", "0911111112", "台北市松山區南京東路五段1號", "送管理室"),
- stop("DELIVERY", "0911111113", "台北市中山區中山北路二段1號", "送前台")));
- request.setStops(stops);
- return request;
- }
- private static SysDictData dictionary(String value) { SysDictData d = new SysDictData(); d.setDictValue(value); return d; }
- private static class Fixture {
- final FlashDeliveryOrderMapper orders = mock(FlashDeliveryOrderMapper.class);
- final FlashDeliveryOrderStopMapper stops = mock(FlashDeliveryOrderStopMapper.class);
- final FlashDeliveryTargetAdjustmentMapper adjustments = mock(FlashDeliveryTargetAdjustmentMapper.class);
- final FlashDeliveryOrderImageMapper images = mock(FlashDeliveryOrderImageMapper.class);
- final FlashDeliveryOrderLogMapper logs = mock(FlashDeliveryOrderLogMapper.class);
- final FlashDeliveryPricingMapper prices = mock(FlashDeliveryPricingMapper.class);
- final InfoUserMapper users = mock(InfoUserMapper.class);
- final FlashDeliveryRouteService routes = mock(FlashDeliveryRouteService.class);
- final FlashDeliveryNotificationService notifications = mock(FlashDeliveryNotificationService.class);
- List<com.ruoyi.app.flashdelivery.route.GeoPoint> capturedPoints = new ArrayList<>();
- final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(orders, prices, images, logs, users, mock(com.ruoyi.system.mapper.InfoBankCardMapper.class),
- routes, new FlashDeliveryPricingCalculator(), mock(RiderDeliveryLockService.class), mock(RiderDeliveryAcceptanceService.class),
- stops, adjustments, mock(com.ruoyi.system.service.PaymentMethodGateService.class),
- notifications, mock(com.ruoyi.system.mapper.UserBillingMapper.class));
- final FlashDeliveryOrder order = new FlashDeliveryOrder();
- final List<FlashDeliveryOrderStop> rows = new ArrayList<>(List.of(
- stop(11L, 0, "PICKUP", "0911111110"), stop(12L, 1, "PICKUP", "0911111111"),
- stop(13L, 2, "DELIVERY", "0911111112"), stop(14L, 3, "DELIVERY", "0911111113")));
- Fixture() {
- order.setId(10L); order.setUserId(7L); order.setRiderId(8L); order.setStatus("ACCEPTED");
- order.setVersion(0); order.setPickupStopCount(2); order.setPickedUpStopCount(0);
- order.setDeliveryStopCount(2); order.setDeliveredStopCount(0);
- order.setAmount(100L); order.setDeliveryMode("NOW"); order.setIsDisplay(true); order.setPayType("4");
- InfoUser rider = new InfoUser(); rider.setUserId(8L); rider.setUserType("2"); rider.setDeliveryType("FLASH");
- when(users.selectInfoUserByUserId(anyLong())).thenReturn(rider);
- when(orders.selectById(10L)).thenReturn(order); when(orders.selectByIdForUpdate(10L)).thenReturn(order);
- when(orders.selectByUserRequestId(anyLong(), anyString())).thenReturn(null);
- when(orders.insert(any(FlashDeliveryOrder.class))).thenAnswer(i -> { ((FlashDeliveryOrder) i.getArgument(0)).setId(10L); return 1; });
- when(stops.selectList(any())).thenReturn(rows);
- when(stops.insert(any(FlashDeliveryOrderStop.class))).thenReturn(1);
- when(stops.updateById(any(FlashDeliveryOrderStop.class))).thenReturn(1);
- when(images.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
- when(orders.updateFulfilment(any())).thenReturn(1);
- when(orders.updateDeliveryFulfilment(any())).thenReturn(1);
- when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111112")).thenReturn(List.of(31L));
- when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111113")).thenReturn(List.of(32L));
- when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111110")).thenReturn(List.of());
- when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111111")).thenReturn(List.of());
- FlashDeliveryPricing price = new FlashDeliveryPricing(); price.setId(1L); price.setConfigVersion(1);
- price.setStartingDistance(BigDecimal.valueOf(3)); price.setStartingFare(80L); price.setDistance(BigDecimal.ONE);
- price.setFreight(10L); price.setUrgentRate(BigDecimal.ZERO); price.setMinimumUrgentFee(0L);
- when(prices.selectAtTime(anyString(), anyInt())).thenReturn(List.of(price));
- when(routes.calculate(anyList(), anyInt())).thenAnswer(i -> {
- capturedPoints.addAll(i.getArgument(0));
- return new RouteDistance(5000, 600, "ROUTE");
- });
- when(routes.calculate(any(com.ruoyi.app.flashdelivery.route.GeoPoint.class),
- any(com.ruoyi.app.flashdelivery.route.GeoPoint.class), anyInt()))
- .thenReturn(new RouteDistance(5000, 600, "ROUTE"));
- }
- }
- private static FlashDeliveryOrderStop stop(Long id, int index, String type, String phone) {
- FlashDeliveryOrderStop s = new FlashDeliveryOrderStop(); s.setId(id); s.setOrderId(10L); s.setStopOrder(index);
- s.setStopType(type); s.setStatus("PENDING"); s.setName("name"); s.setPhone(phone);
- s.setAddress("address " + index); s.setLatitude(BigDecimal.valueOf(25.03 + index * .001)); s.setLongitude(BigDecimal.valueOf(121.56));
- return s;
- }
- }
|