diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java new file mode 100644 index 00000000..29a2aef0 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java @@ -0,0 +1,272 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.transport.pojo.entity.ExceptionDisposal; +import org.springblade.transport.pojo.entity.LoadingManage; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.AdminWaybillCardVO; +import org.springblade.transport.pojo.vo.AdminWaybillDetailVO; +import org.springblade.transport.pojo.vo.TripStationVO; +import org.springblade.transport.pojo.vo.TripVO; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.IDriverWaybillService; +import org.springblade.transport.service.IExceptionDisposalService; +import org.springblade.transport.service.ILoadingManageService; +import org.springblade.transport.service.IRiskDisposalService; +import org.springblade.transport.service.ITripQueryService; +import org.springblade.transport.service.IWaybillService; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 调度端列表与详情的运次口径:一卡一运次、整趟车的站点与货物。 + *

+ * 卡片上的状态、站点与货物汇总都出自共享运次读服务,本类只做字段裁剪;这里钉住的是 + * 「运次字段有没有被摆到卡片与详情上」,以及取不到运次时按运单主键与配载单主键的回落路径。 + */ +class ManageTripListDetailTest { + + private static final String LOADING_NO = "PZ-20261017-001"; + private static final Long LOADING_ID = 2111350784070275074L; + private static final Long CHILD_A = 2111350784070275123L; + private static final Long CHILD_B = 2102780956493299713L; + + private ManageWaybillServiceImpl service; + private IWaybillService waybillService; + private IExceptionDisposalService exceptionDisposalService; + private ITripQueryService tripQueryService; + private ILoadingManageService loadingManageService; + private IDriverWaybillService driverWaybillService; + + @BeforeAll + static void initTableInfo() { + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Waybill.class); + TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class); + TableInfoHelper.initTableInfo(assistant, LoadingManage.class); + } + + @BeforeEach + void setUp() { + waybillService = mock(IWaybillService.class); + exceptionDisposalService = mock(IExceptionDisposalService.class); + tripQueryService = mock(ITripQueryService.class); + loadingManageService = mock(ILoadingManageService.class); + driverWaybillService = mock(IDriverWaybillService.class); + service = new ManageWaybillServiceImpl( + waybillService, + mock(IDriverService.class), + exceptionDisposalService, + mock(IRiskDisposalService.class), + driverWaybillService, + tripQueryService, + loadingManageService); + when(exceptionDisposalService.list(ArgumentMatchers.>any())) + .thenReturn(new ArrayList<>()); + when(waybillService.list(ArgumentMatchers.>any())).thenReturn(new ArrayList<>()); + } + + @Test + @DisplayName("列表一卡一运次:单号、组织类型、起经终、站点数、货物汇总都取运次字段") + void pageListShowsOneCardPerTrip() { + when(tripQueryService.page(any())).thenReturn(pageOf(1, trip())); + + IPage page = service.pageList(1, 10, null, null, null, null, null, null); + + assertEquals(1, page.getTotal()); + assertEquals(1, page.getRecords().size(), "四张子单只出一条卡"); + AdminWaybillCardVO card = page.getRecords().get(0); + assertEquals(String.valueOf(LOADING_ID), card.getTripId()); + assertEquals(LOADING_NO, card.getLoadingNo()); + assertEquals(LOADING_NO, card.getTripNo()); + assertEquals("load", card.getTransportType(), "有配载单即为公路配载"); + assertEquals(2, card.getStatus()); + assertEquals(4, card.getWaybillCount()); + assertEquals(3, card.getStationCount()); + assertEquals("南宁吴圩机场", card.getFromName()); + assertEquals("新福兴硅科技有限公司", card.getViaName()); + assertEquals("宾阳", card.getToName()); + assertEquals("石英砂A 131.84吨、技嘉4070显卡 30吨", card.getCargoSummary()); + assertEquals("161.84吨", card.getTotalWeight()); + assertEquals("2026-09-23 23:23:42", card.getCreateTime(), "卡片时间取运次排序键"); + assertEquals(Long.valueOf(CHILD_A), card.getId(), "旧 id 仍是运单主键,打卡订正等按它进入"); + assertEquals("YD-20260825-0006", card.getWaybillNo()); + assertFalse(card.getNeedReassign()); + } + + @Test + @DisplayName("详情按运次主键取:配载单号、组织类型、站点 ①②③ 与每站货物都带运单号") + void detailShowsWholeTripStations() { + when(tripQueryService.detail(any())).thenReturn(trip()); + + AdminWaybillDetailVO detail = service.detail(String.valueOf(LOADING_ID)); + + assertEquals(String.valueOf(LOADING_ID), detail.getTripId()); + assertEquals(LOADING_NO, detail.getLoadingNo()); + assertEquals(LOADING_NO, detail.getTripNo()); + assertEquals("load", detail.getTransportOrgType()); + assertEquals(2, detail.getStatus()); + assertEquals(3, detail.getStationCount()); + assertEquals(3, detail.getStations().size()); + assertEquals(3, detail.getRoutePoints().size(), "点位列与站点同源"); + assertEquals("161.84吨", detail.getTotalWeight()); + + TripStationVO first = detail.getStations().get(0); + assertEquals(1, first.getIndex()); + assertEquals("装货", first.getOperationTypeLabel()); + assertEquals("2102780956493299713", first.getCargos().get(0).getWaybillId(), "货物条目带所属子运单主键"); + assertEquals("YD-20260923-0008", first.getCargos().get(0).getWaybillNo()); + + TripStationVO second = detail.getStations().get(1); + assertEquals("装卸", second.getOperationTypeLabel()); + assertEquals(2, second.getCargos().size()); + assertEquals("YD-20260923-0008", second.getCargos().get(0).getWaybillNo(), "同一站可卸一张、装另一张"); + assertEquals("2131150784070275011", second.getCargos().get(1).getWaybillId()); + } + + @Test + @DisplayName("取不到运次时按运单主键与配载单主键回落:整单被拒的配载单仍能打开并重新派单") + void detailFallsBackWhenTripIsNotReachable() { + when(tripQueryService.detail(any())).thenThrow(new ServiceException("运次不存在或无权操作")); + when(waybillService.getById(CHILD_A)).thenReturn(null); + when(loadingManageService.getById(LOADING_ID)).thenReturn(loading()); + + AdminWaybillDetailVO detail = service.detail(String.valueOf(LOADING_ID)); + + assertEquals(String.valueOf(LOADING_ID), detail.getTripId()); + assertEquals(LOADING_NO, detail.getLoadingNo()); + assertEquals("load", detail.getTransportOrgType()); + assertTrue(detail.getNeedReassign(), "整单被拒时按配载单回落,标记待重新派单"); + assertEquals(3, detail.getStationCount(), "子单为空时站点退回配载单自己的发货/途经/到货"); + assertEquals("南宁吴圩机场", detail.getFromName()); + assertEquals("宾阳", detail.getToName()); + } + + @Test + @DisplayName("运次主键不是数字时直接拒绝,不去查库") + void detailRefusesUnparsableTripId() { + assertThrows(ServiceException.class, () -> service.detail("not-a-number")); + assertThrows(ServiceException.class, () -> service.detail(null)); + } + + // ------------------------------------------------------------ 夹具 + + private Page pageOf(long total, TripVO... trips) { + Page page = new Page<>(1, 10, total); + page.setRecords(List.of(trips)); + return page; + } + + /** + * 一趟 4 张子单的配载运次:站点与货物按本库真实样本同形(三站,第二站装卸) + */ + private TripVO trip() { + TripVO trip = new TripVO(); + trip.setId(String.valueOf(LOADING_ID)); + trip.setTripNo(LOADING_NO); + trip.setLoadingNo(LOADING_NO); + trip.setTransportOrgType("load"); + trip.setStatus(2); + trip.setRequireAccept(false); + trip.setWaybillCount(4); + trip.setDriverName("黄宇富"); + trip.setVehicleNo("桂A32523"); + trip.setDepartureName("南宁吴圩机场"); + trip.setTransitName("新福兴硅科技有限公司"); + trip.setArrivalName("宾阳"); + trip.setStationCount(3); + trip.setCargoSummary("石英砂A 131.84吨、技嘉4070显卡 30吨"); + trip.setTotalQuantity(new BigDecimal("161.84")); + trip.setQuantityUnit("吨"); + trip.setSortTime(at(1790177022000L)); + trip.setEstimatedStartDate(LocalDate.of(2026, 8, 25)); + trip.setEstimatedEndDate(LocalDate.of(2026, 9, 23)); + trip.setWaybills(List.of( + tripWaybill(CHILD_A, "YD-20260825-0006"), + tripWaybill(2111350784070275124L, "YD-20260825-0007"), + tripWaybill(2111350784070275125L, "YD-20260825-0008"), + tripWaybill(CHILD_B, "YD-20260923-0008"))); + trip.setStations(List.of( + station(1, "南宁吴圩机场", "load", "装货", cargo("2102780956493299713", "YD-20260923-0008", "技嘉4070显卡", "30")), + station(2, "武宣", "load_unload", "装卸", + cargo("2102780956493299713", "YD-20260923-0008", "技嘉4070显卡", "30"), + cargo("2131150784070275011", "YD-20260825-0006", "石英砂A", "42.10")), + station(3, "宾阳", "unload", "卸货", cargo("2131150784070275011", "YD-20260825-0006", "石英砂A", "42.10")))); + return trip; + } + + private TripVO.TripWaybillVO tripWaybill(Long id, String waybillNo) { + TripVO.TripWaybillVO child = new TripVO.TripWaybillVO(); + child.setId(String.valueOf(id)); + child.setWaybillNo(waybillNo); + child.setBusinessStatus("running"); + return child; + } + + private TripStationVO station(int index, String name, String operationType, String label, + TripStationVO.TripCargoVO... cargos) { + TripStationVO station = new TripStationVO(); + station.setIndex(index); + station.setName(name); + station.setAddress(name); + station.setOperationType(operationType); + station.setOperationTypeLabel(label); + station.setTags(new ArrayList<>()); + station.setCargos(List.of(cargos)); + return station; + } + + private TripStationVO.TripCargoVO cargo(String waybillId, String waybillNo, String cargoName, String quantity) { + TripStationVO.TripCargoVO cargo = new TripStationVO.TripCargoVO(); + cargo.setWaybillId(waybillId); + cargo.setWaybillNo(waybillNo); + cargo.setCargoName(cargoName); + cargo.setQuantity(new BigDecimal(quantity)); + cargo.setQuantityUnit("吨"); + return cargo; + } + + private LoadingManage loading() { + LoadingManage loading = new LoadingManage(); + loading.setId(LOADING_ID); + loading.setLoadingNo(LOADING_NO); + loading.setDriverName("黄宇富"); + loading.setDriverPhone("17587421208"); + loading.setVehicleNo("桂A32523"); + loading.setDepartureAddress("南宁吴圩机场"); + loading.setTransitAddress("武宣"); + loading.setArrivalAddress("宾阳"); + loading.setCargoName("石英砂A"); + return loading; + } + + private Date at(long millis) { + return new Date(millis); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java new file mode 100644 index 00000000..744a5548 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/TripReadServiceBoundaryTest.java @@ -0,0 +1,358 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.transport.pojo.entity.LoadingManage; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.TripQueryVO; +import org.springblade.transport.pojo.vo.TripVO; +import org.springblade.transport.service.ILoadingManageService; +import org.springblade.transport.service.IWaybillService; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 运次读服务的聚合边界:四档状态、排序键、空子单集合与主键下发形态。 + *

+ * 断言都从服务出口读({@code page} / {@code detail}),不直接调判定函数:随卡片下发的 + * {@code status} 与 {@code requireAccept} 是两个字段,只验静态判定看不出服务有没有把子单真身取回来—— + * 库层分组行只投影主键、分组键与排序键,business_status 是 NULL。 + */ +class TripReadServiceBoundaryTest { + + private static final String PHONE = "17587421208"; + private static final String LOADING_NO = "PZ-20261017-001"; + /** 19 位雪花号,按数字下发会被双精度取整 */ + private static final Long LOADING_ID = 2111350784070275074L; + private static final Long CHILD_A = 2111350784070275123L; + private static final Long CHILD_B = 2102780956493299713L; + private static final String ACCEPT_YES = "[{\"key\":\"accept\",\"name\":\"接单\",\"enabled\":true,\"confirmMode\":\"yes\"}]"; + private static final String ACCEPT_NO = "[{\"key\":\"accept\",\"name\":\"接单\",\"enabled\":true,\"confirmMode\":\"no_confirm_accept\"}]"; + + private IWaybillService waybillService; + private ILoadingManageService loadingManageService; + private TripQueryServiceImpl service; + + @BeforeAll + static void initTableInfo() { + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Waybill.class); + TableInfoHelper.initTableInfo(assistant, LoadingManage.class); + } + + @BeforeEach + void setUp() { + waybillService = mock(IWaybillService.class); + loadingManageService = mock(ILoadingManageService.class); + service = new TripQueryServiceImpl(waybillService, loadingManageService); + lenient().when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of()); + lenient().when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of()); + } + + @Test + @DisplayName("子单全部完成 → 运次已完成(2)") + void allChildrenCompleted() { + stubLoadedTrip( + child(CHILD_A, "completed", null, ACCEPT_NO, at(1787657033000L)), + child(CHILD_B, "completed", "accepted", ACCEPT_NO, at(1787671422000L))); + + TripVO trip = onlyTrip(); + + assertEquals(2, trip.getStatus(), "终态且含已完成为已完成档"); + assertEquals(2, trip.getWaybillCount()); + assertEquals("accepted", trip.getAcceptStatus(), "任一子单已接单即整单已接单"); + } + + @Test + @DisplayName("子单全部取消 → 运次已取消(3),不是运输中") + void allChildrenCancelled() { + stubLoadedTrip( + child(CHILD_A, "cancelled", null, ACCEPT_NO, at(1787657033000L)), + child(CHILD_B, "cancelled", null, ACCEPT_NO, at(1787671422000L))); + + TripVO trip = onlyTrip(); + + assertEquals(3, trip.getStatus(), "已取消子单必须进运次,否则全取消会被算成运输中"); + } + + @Test + @DisplayName("混合终态且含 completed → 运次已完成(2),不看取消的那张") + void mixedTerminalWithCompleted() { + stubLoadedTrip( + child(CHILD_A, "completed", null, ACCEPT_NO, at(1787657033000L)), + child(CHILD_B, "cancelled", null, ACCEPT_NO, at(1787671422000L))); + + TripVO trip = onlyTrip(); + + assertEquals(2, trip.getStatus()); + } + + @Test + @DisplayName("需接单且未接单 → 待接单(0)") + void requireAcceptWithoutAcceptance() { + Waybill required = child(CHILD_A, "pending", "pending", ACCEPT_YES, at(1787657033000L)); + required.setProjectId(1L); + required.setProjectName("MK项目测试-1"); + stubLoadedTrip(required); + + TripVO trip = onlyTrip(); + + assertEquals(Boolean.TRUE, trip.getRequireAccept()); + assertEquals("pending", trip.getAcceptStatus()); + assertEquals(0, trip.getStatus()); + } + + @Test + @DisplayName("不需接单 → 运输中(1)") + void withoutAcceptRequirement() { + Waybill notRequired = child(CHILD_A, "pending", null, ACCEPT_NO, at(1787657033000L)); + notRequired.setProjectId(2L); + notRequired.setProjectName("武宣建筑原料运输项目"); + stubLoadedTrip(notRequired); + + TripVO trip = onlyTrip(); + + assertEquals(Boolean.FALSE, trip.getRequireAccept()); + assertEquals(1, trip.getStatus()); + } + + @Test + @DisplayName("跨项目子单的接单需求按 OR 合并:一张要接单整趟车都要接单") + void crossProjectRequirementMergesWithOr() { + Waybill required = child(CHILD_A, "running", null, ACCEPT_YES, at(1787657033000L)); + required.setProjectId(1L); + required.setProjectName("MK项目测试-1"); + Waybill notRequired = child(CHILD_B, "running", null, ACCEPT_NO, at(1787671422000L)); + notRequired.setProjectId(2L); + notRequired.setProjectName("武宣建筑原料运输项目"); + stubLoadedTrip(required, notRequired); + + TripVO trip = onlyTrip(); + + assertEquals(Boolean.TRUE, trip.getRequireAccept()); + assertEquals(0, trip.getStatus()); + assertEquals(Boolean.TRUE, trip.getWaybills().get(0).getRequireAccept()); + assertEquals(Boolean.FALSE, trip.getWaybills().get(1).getRequireAccept()); + } + + @Test + @DisplayName("跨项目但都没接单节点 → 整趟车不需要接单") + void crossProjectWithoutAnyRequirement() { + Waybill first = child(CHILD_A, "running", null, null, at(1787657033000L)); + first.setProjectId(1L); + Waybill second = child(CHILD_B, "running", null, ACCEPT_NO, at(1787671422000L)); + second.setProjectId(2L); + stubLoadedTrip(first, second); + + TripVO trip = onlyTrip(); + + assertEquals(Boolean.FALSE, trip.getRequireAccept()); + assertEquals(1, trip.getStatus()); + } + + @Test + @DisplayName("排序键取子单 create_time 的最大值,不取第一张也不取最后一张") + void sortTimeIsMaxChildCreateTime() { + Date earliest = at(1787657033000L); + Date latest = at(1787671422000L); + Date middle = at(1787663000000L); + stubLoadedTrip( + child(CHILD_A, "running", null, ACCEPT_NO, middle), + child(2111350784070275999L, "running", null, ACCEPT_NO, latest), + child(CHILD_B, "running", null, ACCEPT_NO, earliest)); + + TripVO trip = onlyTrip(); + + assertEquals(latest, trip.getSortTime()); + assertEquals(3, trip.getWaybillCount()); + assertEquals(earliest, trip.getWaybills().get(0).getCreateTime(), "子单按创建时间升序摆"); + } + + @Test + @DisplayName("子单集合为空不抛异常:列表不给这张卡,详情直接拒绝而不是 NPE") + void emptyChildrenDoNotThrow() { + stubLoadedTrip(); + + IPage page = service.page(TripQueryVO.forDriver(PHONE)); + + assertTrue(page.getRecords().isEmpty(), "配载单在、可见子单为空时不给没有子单的运次"); + + when(loadingManageService.getById(LOADING_ID)).thenReturn(loading()); + assertThrows(ServiceException.class, () -> service.detail(detailQuery()), + "子单全被拒单的配载运次对司机不可见"); + } + + @Test + @DisplayName("子单状态为空时不抛异常:读不出状态按没走完兜底,不崩也不误判成终态") + void nullBusinessStatusDoesNotThrow() { + Waybill blank = new Waybill(); + blank.setId(CHILD_A); + blank.setWaybillNo("YD-20260920-0003"); + blank.setLoadingNo(LOADING_NO); + blank.setBusinessStatus(null); + stubLoadedTrip(blank); + + TripVO trip = onlyTrip(); + + assertEquals(1, trip.getStatus(), "读不出状态按没走完处理,不是终态也不抛异常"); + assertEquals(1, trip.getWaybillCount()); + } + + @Test + @DisplayName("未配载运次按主键回查真身:分组行没有状态列,拿它当子单会让接单需求恒为 false") + void loneTripReadsStateFromFetchedWaybill() { + // 段一给的行只有主键、分组键与排序键,真身要靠段二回查 + Waybill row = new Waybill(); + row.setId(CHILD_A); + row.setCreateTime(at(1787657033000L)); + stubPage(1, row); + Waybill bare = new Waybill(); + bare.setId(CHILD_A); + bare.setWaybillNo("YD-20260920-0003"); + bare.setBusinessStatus("pending"); + bare.setDriverAcceptStatus("pending"); + bare.setProcessJson(ACCEPT_YES); + bare.setDriverPhone(PHONE); + when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of(bare)); + + TripVO trip = onlyTrip(); + + assertEquals("2111350784070275123", trip.getId()); + assertNull(trip.getLoadingNo()); + assertEquals(19, trip.getId().length(), "19 位雪花号的末位不被取整"); + assertEquals(Boolean.TRUE, trip.getRequireAccept(), "接单需求取自回查到的真身"); + assertEquals(0, trip.getStatus()); + assertEquals(1, trip.getWaybillCount()); + } + + @Test + @DisplayName("运次主键按字符串下发:19 位雪花号的末位不被双精度取整") + void tripIdIsSentAsString() { + stubLoadedTrip(child(CHILD_A, "running", null, ACCEPT_NO, at(1787657033000L))); + + TripVO trip = onlyTrip(); + + assertNotNull(trip.getId()); + assertEquals("2111350784070275074", trip.getId()); + assertEquals(19, trip.getId().length()); + assertEquals("2111350784070275123", trip.getWaybills().get(0).getId()); + assertEquals(19, trip.getWaybills().get(0).getId().length()); + assertTrue(trip.getId() instanceof String); + } + + @Test + @DisplayName("货物与合计重量按整趟车算:同名同单位合并,单位不一致时合计留空") + void cargoSummarySpansWholeTrip() { + Waybill first = child(CHILD_A, "running", null, ACCEPT_NO, at(1787657033000L)); + first.setCargoName("石英砂A"); + first.setQuantity(new BigDecimal("42.10")); + first.setQuantityUnit("吨"); + Waybill second = child(CHILD_B, "running", null, ACCEPT_NO, at(1787671422000L)); + second.setCargoName("石英砂A"); + second.setQuantity(new BigDecimal("47.57")); + second.setQuantityUnit("吨"); + stubLoadedTrip(first, second); + + TripVO trip = onlyTrip(); + + assertEquals("石英砂A 89.67吨", trip.getCargoSummary()); + assertEquals(0, new BigDecimal("89.67").compareTo(trip.getTotalQuantity())); + assertEquals("吨", trip.getQuantityUnit()); + } + + // ------------------------------------------------------------ 夹具 + + @SuppressWarnings("unchecked") + private static Wrapper anyWaybillWrapper() { + return ArgumentMatchers.>any(); + } + + @SuppressWarnings("unchecked") + private static Wrapper anyLoadingWrapper() { + return ArgumentMatchers.>any(); + } + + /** 配载运次:段一给一行分组行,段二给配载单与这一组的子单 */ + private void stubLoadedTrip(Waybill... children) { + Waybill row = new Waybill(); + row.setId(children.length == 0 ? CHILD_A : children[0].getId()); + row.setLoadingNo(LOADING_NO); + stubPage(1, row); + when(loadingManageService.list(anyLoadingWrapper())).thenReturn(List.of(loading())); + when(waybillService.list(anyWaybillWrapper())).thenReturn(List.of(children)); + } + + private void stubPage(long total, Waybill... rows) { + when(waybillService.page(any(), any())).thenAnswer(invocation -> { + Page page = invocation.getArgument(0); + page.setRecords(List.of(rows)); + page.setTotal(total); + return page; + }); + } + + private TripVO onlyTrip() { + IPage page = service.page(TripQueryVO.forDriver(PHONE)); + assertEquals(1, page.getRecords().size(), "本页应只有一个运次"); + return page.getRecords().get(0); + } + + private TripQueryVO detailQuery() { + TripQueryVO query = TripQueryVO.forDriver(PHONE); + query.setTripId(String.valueOf(LOADING_ID)); + return query; + } + + private LoadingManage loading() { + LoadingManage loading = new LoadingManage(); + loading.setId(LOADING_ID); + loading.setLoadingNo(LOADING_NO); + loading.setDriverPhone(PHONE); + loading.setDriverName("黄宇富"); + loading.setVehicleNo("桂A32523"); + loading.setProjectName("武宣建筑原料运输项目"); + return loading; + } + + private Waybill child(Long id, String businessStatus, String acceptStatus, String processJson, Date createTime) { + Waybill waybill = new Waybill(); + waybill.setId(id); + waybill.setWaybillNo("YD-" + id); + waybill.setLoadingNo(LOADING_NO); + waybill.setBusinessStatus(businessStatus); + waybill.setDriverAcceptStatus(acceptStatus); + waybill.setDriverPhone(PHONE); + waybill.setProcessJson(processJson); + waybill.setCreateTime(createTime); + return waybill; + } + + private Date at(long millis) { + return new Date(millis); + } + +}