FlashDeliveryV2Test.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package com.ruoyi.app.flashdelivery.service;
  2. import com.ruoyi.app.flashdelivery.dto.*;
  3. import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
  4. import com.ruoyi.app.flashdelivery.route.RouteDistance;
  5. import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
  6. import com.ruoyi.app.order.RiderDeliveryLockService;
  7. import com.ruoyi.common.core.domain.entity.SysDictData;
  8. import com.ruoyi.common.exception.ServiceException;
  9. import com.ruoyi.common.utils.DictUtils;
  10. import com.ruoyi.system.domain.InfoUser;
  11. import com.ruoyi.system.domain.flash.*;
  12. import com.ruoyi.system.mapper.InfoUserMapper;
  13. import com.ruoyi.system.mapper.flash.*;
  14. import org.junit.jupiter.api.*;
  15. import org.mockito.MockedStatic;
  16. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  17. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  18. import org.springframework.context.support.StaticMessageSource;
  19. import org.springframework.test.util.ReflectionTestUtils;
  20. import com.ruoyi.common.utils.spring.SpringUtils;
  21. import java.math.BigDecimal;
  22. import java.util.*;
  23. import static org.junit.jupiter.api.Assertions.*;
  24. import static org.mockito.ArgumentMatchers.*;
  25. import static org.mockito.Mockito.*;
  26. /** v2 多点取送:统一 stops 数组、订单级物品、逐站送达与取消门禁。 */
  27. class FlashDeliveryV2Test {
  28. private static ConfigurableListableBeanFactory original;
  29. @BeforeAll static void messages() {
  30. original = (ConfigurableListableBeanFactory) ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  31. DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
  32. StaticMessageSource source = new StaticMessageSource(); source.setUseCodeAsDefaultMessage(true);
  33. factory.registerSingleton("messageSource", source); new SpringUtils().postProcessBeanFactory(factory);
  34. }
  35. @AfterAll static void restoreMessages() { new SpringUtils().postProcessBeanFactory(original); }
  36. private static void multiStopDicts(MockedStatic<DictUtils> dict) {
  37. dict.when(() -> DictUtils.getDictCache("flash_pickup_stop_limit")).thenReturn(List.of(dictionary("3")));
  38. dict.when(() -> DictUtils.getDictCache("flash_show_multi_pickup")).thenReturn(List.of(dictionary("1")));
  39. dict.when(() -> DictUtils.getDictCache("flash_delivery_stop_limit")).thenReturn(List.of(dictionary("3")));
  40. dict.when(() -> DictUtils.getDictCache("flash_show_multi_dropoff")).thenReturn(List.of(dictionary("1")));
  41. }
  42. @Test void v2CreateSavesUnifiedStopsWithOrderLevelItemAndPerStopReceivers() {
  43. Fixture f = new Fixture();
  44. try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
  45. multiStopDicts(dict);
  46. FlashDeliveryV2CreateRequest request = v2Create();
  47. FlashDeliveryQuoteView quote = f.service.quoteV2(request);
  48. request.setPricingId(quote.getPricingId()); request.setPricingVersion(quote.getPricingVersion());
  49. request.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee()); request.setQuotedDistanceFee(quote.getDistanceFee());
  50. request.setQuotedUrgentFee(quote.getUrgentFee()); request.setQuotedAmount(quote.getAmount());
  51. f.service.createV2(7L, request);
  52. var saved = argThatStops(f);
  53. assertEquals(List.of("PICKUP", "PICKUP", "DELIVERY", "DELIVERY"),
  54. saved.stream().map(FlashDeliveryOrderStop::getStopType).toList());
  55. assertEquals(List.of(0, 1, 2, 3), saved.stream().map(FlashDeliveryOrderStop::getStopOrder).toList());
  56. assertEquals(List.of("取月餅", "取牛奶", "送管理室", "送前台"),
  57. saved.stream().map(FlashDeliveryOrderStop::getRemark).toList());
  58. // 按站收件人:两个收货站按各自电话分别固化;取货站不固化。
  59. assertEquals(Arrays.asList(null, null, 31L, 32L), saved.stream().map(FlashDeliveryOrderStop::getReceiverUserId).toList());
  60. // 启用收货码时各收货站独立生成。
  61. assertNotNull(saved.get(2).getDeliveryPinCode());
  62. assertNotNull(saved.get(3).getDeliveryPinCode());
  63. assertNotEquals(saved.get(2).getDeliveryPinCode(), saved.get(3).getDeliveryPinCode());
  64. var order = argThatOrder(f);
  65. assertEquals(2, order.getDeliveryStopCount());
  66. assertEquals(0, order.getDeliveredStopCount());
  67. // 物品主体整单一次,落在订单级摘要字段。
  68. assertEquals("FOOD_INGREDIENTS", order.getPackageType());
  69. assertEquals(8, order.getQuantity());
  70. }
  71. }
  72. @Test void v2RejectsDeliveryBeforePickupLimitAndDisabledSwitch() {
  73. Fixture f = new Fixture();
  74. try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
  75. multiStopDicts(dict);
  76. FlashDeliveryV2CreateRequest wrongOrder = v2Create();
  77. wrongOrder.getStops().add(0, stop("DELIVERY", "0911111112", "台北市松山區B街2號", "d1"));
  78. assertThrows(ServiceException.class, () -> f.service.createV2(7L, wrongOrder));
  79. FlashDeliveryV2CreateRequest tooMany = v2Create();
  80. for (int i = 0; i < 2; i++) tooMany.getStops().add(stop("DELIVERY", "091111111" + (4 + i), "台北市中山區C街" + i + "號", "d" + i));
  81. assertThrows(ServiceException.class, () -> f.service.createV2(7L, tooMany));
  82. verify(f.orders, never()).insert(any(FlashDeliveryOrder.class));
  83. dict.when(() -> DictUtils.getDictCache("flash_show_multi_dropoff")).thenReturn(List.of(dictionary("0")));
  84. assertThrows(ServiceException.class, () -> f.service.quoteV2(v2Create()));
  85. }
  86. }
  87. @Test void v2QuoteRoutesThroughAllStopsInUserOrder() {
  88. Fixture f = new Fixture();
  89. try (MockedStatic<DictUtils> dict = mockStatic(DictUtils.class)) {
  90. multiStopDicts(dict);
  91. FlashDeliveryV2QuoteRequest request = v2Create();
  92. FlashDeliveryQuoteView view = f.service.quoteV2(request);
  93. assertEquals(5000, view.getDistanceMeters());
  94. var points = f.capturedPoints;
  95. assertEquals(4, points.size());
  96. verify(f.routes).calculate(anyList(), eq(1));
  97. }
  98. }
  99. @Test void perStopDeliveryAdvancesOrderOnlyAfterLastStopAndBlocksCancel() {
  100. Fixture f = new Fixture();
  101. f.order.setStatus("PICKED_UP"); f.order.setPinRequired(false);
  102. f.rows.forEach(row -> { if ("PICKUP".equals(row.getStopType())) row.setStatus("PICKED_UP"); });
  103. when(f.orders.updateDeliveryFulfilment(any())).thenReturn(1);
  104. FlashDeliveryDeliverRequest deliver = new FlashDeliveryDeliverRequest();
  105. deliver.setImageUrls(List.of("/profile/d1.jpg"));
  106. deliver.setStopId(13L);
  107. f.service.deliverAtStop(8L, 10L, 13L, deliver);
  108. assertEquals("DELIVERED", f.rows.get(2).getStatus());
  109. assertEquals(1, f.order.getDeliveredStopCount());
  110. assertEquals("PICKED_UP", f.order.getStatus());
  111. // 部分送达后用户与平台都不能自助取消。
  112. FlashDeliveryReasonRequest cancel = new FlashDeliveryReasonRequest(); cancel.setReason("changed mind");
  113. assertThrows(ServiceException.class, () -> f.service.userCancel(7L, 10L, cancel));
  114. verify(f.orders, never()).cancel(any(), any(), any(), any(), any(), any());
  115. // 同站重试幂等:不重复写凭证、不重复推进。
  116. f.service.deliverAtStop(8L, 10L, 13L, deliver);
  117. verify(f.images, times(1)).insert(any(FlashDeliveryOrderImage.class));
  118. // 末站完成后整单推进一次。
  119. deliver.setStopId(14L);
  120. f.service.deliverAtStop(8L, 10L, 14L, deliver);
  121. assertEquals("DELIVERED", f.order.getStatus());
  122. assertEquals(2, f.order.getDeliveredStopCount());
  123. assertEquals(f.rows.get(3).getDeliveredAt(), f.order.getDeliveredAt());
  124. verify(f.notifications, times(1)).notifyDelivered(any());
  125. }
  126. @Test void perStopPinIsValidatedAgainstThatStopOnly() {
  127. Fixture f = new Fixture();
  128. f.order.setStatus("PICKED_UP"); f.order.setPinRequired(true); f.order.setDeliveryPinCode("0000");
  129. f.rows.forEach(row -> { if ("PICKUP".equals(row.getStopType())) row.setStatus("PICKED_UP"); });
  130. f.rows.get(2).setDeliveryPinCode("1111");
  131. f.rows.get(3).setDeliveryPinCode("2222");
  132. when(f.orders.updateDeliveryFulfilment(any())).thenReturn(1);
  133. FlashDeliveryDeliverRequest deliver = new FlashDeliveryDeliverRequest();
  134. deliver.setImageUrls(List.of("/profile/d1.jpg"));
  135. deliver.setStopId(13L);
  136. deliver.setPinCode("2222");
  137. assertThrows(ServiceException.class, () -> f.service.deliverAtStop(8L, 10L, 13L, deliver));
  138. verify(f.images, never()).insert(any(FlashDeliveryOrderImage.class));
  139. deliver.setPinCode("1111");
  140. f.service.deliverAtStop(8L, 10L, 13L, deliver);
  141. assertEquals("DELIVERED", f.rows.get(2).getStatus());
  142. }
  143. private List<FlashDeliveryOrderStop> argThatStops(Fixture f) {
  144. var captor = org.mockito.ArgumentCaptor.forClass(FlashDeliveryOrderStop.class);
  145. verify(f.stops, atLeast(4)).insert(captor.capture());
  146. return captor.getAllValues();
  147. }
  148. private FlashDeliveryOrder argThatOrder(Fixture f) {
  149. var captor = org.mockito.ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  150. verify(f.orders).insert(captor.capture());
  151. return captor.getValue();
  152. }
  153. private static FlashDeliveryV2StopRequest stop(String type, String phone, String address, String remark) {
  154. FlashDeliveryV2StopRequest stop = new FlashDeliveryV2StopRequest();
  155. stop.setType(type); stop.setName("聯絡人"); stop.setPhone(phone);
  156. stop.setAddress(address); stop.setAddressDetail("3樓"); stop.setRemark(remark);
  157. int seq = Math.abs(address.hashCode()) % 100;
  158. stop.setLatitude(BigDecimal.valueOf(25.03 + seq * .0001));
  159. stop.setLongitude(BigDecimal.valueOf(121.56 + seq * .0001));
  160. return stop;
  161. }
  162. private static FlashDeliveryV2CreateRequest v2Create() {
  163. FlashDeliveryV2CreateRequest request = new FlashDeliveryV2CreateRequest();
  164. request.setServiceType("HELP_SEND"); request.setDeliveryType("NORMAL"); request.setVehicleType(1);
  165. request.setTipAmount(0L); request.setPackageType("FOOD_INGREDIENTS"); request.setQuantity(8);
  166. request.setClientRequestId("v2-" + System.nanoTime());
  167. List<FlashDeliveryV2StopRequest> stops = new ArrayList<>(List.of(
  168. stop("PICKUP", "0911111110", "台北市大安區信義路三段56號", "取月餅"),
  169. stop("PICKUP", "0911111111", "台北市信義區松高路11號", "取牛奶"),
  170. stop("DELIVERY", "0911111112", "台北市松山區南京東路五段1號", "送管理室"),
  171. stop("DELIVERY", "0911111113", "台北市中山區中山北路二段1號", "送前台")));
  172. request.setStops(stops);
  173. return request;
  174. }
  175. private static SysDictData dictionary(String value) { SysDictData d = new SysDictData(); d.setDictValue(value); return d; }
  176. private static class Fixture {
  177. final FlashDeliveryOrderMapper orders = mock(FlashDeliveryOrderMapper.class);
  178. final FlashDeliveryOrderStopMapper stops = mock(FlashDeliveryOrderStopMapper.class);
  179. final FlashDeliveryTargetAdjustmentMapper adjustments = mock(FlashDeliveryTargetAdjustmentMapper.class);
  180. final FlashDeliveryOrderImageMapper images = mock(FlashDeliveryOrderImageMapper.class);
  181. final FlashDeliveryOrderLogMapper logs = mock(FlashDeliveryOrderLogMapper.class);
  182. final FlashDeliveryPricingMapper prices = mock(FlashDeliveryPricingMapper.class);
  183. final InfoUserMapper users = mock(InfoUserMapper.class);
  184. final FlashDeliveryRouteService routes = mock(FlashDeliveryRouteService.class);
  185. final FlashDeliveryNotificationService notifications = mock(FlashDeliveryNotificationService.class);
  186. List<com.ruoyi.app.flashdelivery.route.GeoPoint> capturedPoints = new ArrayList<>();
  187. final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(orders, prices, images, logs, users, mock(com.ruoyi.system.mapper.InfoBankCardMapper.class),
  188. routes, new FlashDeliveryPricingCalculator(), mock(RiderDeliveryLockService.class), mock(RiderDeliveryAcceptanceService.class),
  189. stops, adjustments, mock(com.ruoyi.system.service.PaymentMethodGateService.class),
  190. notifications, mock(com.ruoyi.system.mapper.UserBillingMapper.class));
  191. final FlashDeliveryOrder order = new FlashDeliveryOrder();
  192. final List<FlashDeliveryOrderStop> rows = new ArrayList<>(List.of(
  193. stop(11L, 0, "PICKUP", "0911111110"), stop(12L, 1, "PICKUP", "0911111111"),
  194. stop(13L, 2, "DELIVERY", "0911111112"), stop(14L, 3, "DELIVERY", "0911111113")));
  195. Fixture() {
  196. order.setId(10L); order.setUserId(7L); order.setRiderId(8L); order.setStatus("ACCEPTED");
  197. order.setVersion(0); order.setPickupStopCount(2); order.setPickedUpStopCount(0);
  198. order.setDeliveryStopCount(2); order.setDeliveredStopCount(0);
  199. order.setAmount(100L); order.setDeliveryMode("NOW"); order.setIsDisplay(true); order.setPayType("4");
  200. InfoUser rider = new InfoUser(); rider.setUserId(8L); rider.setUserType("2"); rider.setDeliveryType("FLASH");
  201. when(users.selectInfoUserByUserId(anyLong())).thenReturn(rider);
  202. when(orders.selectById(10L)).thenReturn(order); when(orders.selectByIdForUpdate(10L)).thenReturn(order);
  203. when(orders.selectByUserRequestId(anyLong(), anyString())).thenReturn(null);
  204. when(orders.insert(any(FlashDeliveryOrder.class))).thenAnswer(i -> { ((FlashDeliveryOrder) i.getArgument(0)).setId(10L); return 1; });
  205. when(stops.selectList(any())).thenReturn(rows);
  206. when(stops.insert(any(FlashDeliveryOrderStop.class))).thenReturn(1);
  207. when(stops.updateById(any(FlashDeliveryOrderStop.class))).thenReturn(1);
  208. when(images.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
  209. when(orders.updateFulfilment(any())).thenReturn(1);
  210. when(orders.updateDeliveryFulfilment(any())).thenReturn(1);
  211. when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111112")).thenReturn(List.of(31L));
  212. when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111113")).thenReturn(List.of(32L));
  213. when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111110")).thenReturn(List.of());
  214. when(users.selectOrdinaryUserIdsByNormalizedPhone("0911111111")).thenReturn(List.of());
  215. FlashDeliveryPricing price = new FlashDeliveryPricing(); price.setId(1L); price.setConfigVersion(1);
  216. price.setStartingDistance(BigDecimal.valueOf(3)); price.setStartingFare(80L); price.setDistance(BigDecimal.ONE);
  217. price.setFreight(10L); price.setUrgentRate(BigDecimal.ZERO); price.setMinimumUrgentFee(0L);
  218. when(prices.selectAtTime(anyString(), anyInt())).thenReturn(List.of(price));
  219. when(routes.calculate(anyList(), anyInt())).thenAnswer(i -> {
  220. capturedPoints.addAll(i.getArgument(0));
  221. return new RouteDistance(5000, 600, "ROUTE");
  222. });
  223. when(routes.calculate(any(com.ruoyi.app.flashdelivery.route.GeoPoint.class),
  224. any(com.ruoyi.app.flashdelivery.route.GeoPoint.class), anyInt()))
  225. .thenReturn(new RouteDistance(5000, 600, "ROUTE"));
  226. }
  227. }
  228. private static FlashDeliveryOrderStop stop(Long id, int index, String type, String phone) {
  229. FlashDeliveryOrderStop s = new FlashDeliveryOrderStop(); s.setId(id); s.setOrderId(10L); s.setStopOrder(index);
  230. s.setStopType(type); s.setStatus("PENDING"); s.setName("name"); s.setPhone(phone);
  231. s.setAddress("address " + index); s.setLatitude(BigDecimal.valueOf(25.03 + index * .001)); s.setLongitude(BigDecimal.valueOf(121.56));
  232. return s;
  233. }
  234. }