From ecb15ca7668d36903ca03855a7b5b74a2ece4a23 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Sun, 18 Oct 2026 15:52:07 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E9=85=8D=E8=BD=BD=E5=87=86=E5=85=A5?= =?UTF-8?q?=E7=9A=84=E6=9C=AC=E5=8D=95=E8=B1=81=E5=85=8D=E5=8F=AA=E5=AF=B9?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E5=8D=95=E7=94=9F=E6=95=88=EF=BC=8C=E5=90=88?= =?UTF-8?q?=E5=90=8C=E4=B8=8D=E5=B1=9E=E4=BA=8E=E6=9C=AC=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=82=B9=E5=90=8D=E8=BF=90=E5=8D=95=E4=B8=8E=E5=85=B6=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=E5=8D=95=E4=B8=80=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/LoadingManageServiceImpl.java | 106 +++++++++--------- .../support/LoadableAdmissionSupport.java | 52 ++++++--- .../impl/LoadingManageAdmissionTest.java | 69 +++++++++++- .../LoadingManageChildStatusWriteTest.java | 11 +- .../support/LoadableAdmissionSupportTest.java | 39 +++++-- 5 files changed, 188 insertions(+), 89 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 96dd0fcb..ed88df34 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -155,9 +155,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl oldWaybillIds = new ArrayList<>(); + boolean ownChildrenAdmitted = false; if (Func.isNotEmpty(loadingManage.getId())) { LoadingManage oldRecord = loadEditable(loadingManage.getId(), true); oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson()); + // 草稿只预占运单号、未过判据,草稿转正式不得沿用本单豁免 + ownChildrenAdmitted = !STATUS_DRAFT.equals(oldRecord.getBusinessStatus()); loadingManage.setLoadingNo(oldRecord.getLoadingNo()); loadingManage.setDeptId(oldRecord.getDeptId()); loadingManage.setDeptName(oldRecord.getDeptName()); @@ -168,10 +171,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), ownChildrenAdmitted); + validateCarrierContract(loadingManage, true, waybills); boolean result = saveOrUpdate(loadingManage); - writeWaybillsForDriver(loadingManage, oldWaybillIds); + writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills); return result; } @@ -417,10 +420,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), true); + validateCarrierContract(loadingManage, true, waybills); boolean result = updateById(loadingManage); - writeWaybillsForDriver(loadingManage, oldWaybillIds); + writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills); return result; } @@ -742,57 +745,46 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybillIds) { + private List validateWaybillsAvailable(LoadingManage loadingManage, List waybillIds, boolean ownChildrenAdmitted) { if (Func.isEmpty(waybillIds)) { throw new ServiceException("请选择待配载运单"); } - List waybills = waybillMapper.selectList(Wrappers.lambdaQuery() - .eq(Waybill::getIsDeleted, 0) - .in(Waybill::getId, waybillIds)); + List waybills = loadWaybills(waybillIds); if (waybills.size() != waybillIds.size()) { throw new ServiceException("待配载运单不存在或已删除"); } String ownLoadingNo = loadingManage.getLoadingNo(); for (Waybill waybill : waybills) { TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理"); - // 本单既有子运单在先前提交时已过准入判据,重新提交/编辑不再判: - // 不需接单项目的票提交后即为进行中,重复判会让本单再也保存不了 - if (LoadableAdmissionSupport.isOwnChild(waybill, ownLoadingNo)) { + if (ownChildrenAdmitted && LoadableAdmissionSupport.isOwnChild(waybill, ownLoadingNo)) { continue; } - if (!LoadableAdmissionSupport.isLoadable(waybill, ownLoadingNo)) { - throw new ServiceException(unloadableReason(waybill)); + LoadableAdmissionSupport.UnloadableReason reason = + LoadableAdmissionSupport.unloadableReason(waybill, ownLoadingNo); + if (reason != null) { + throw new ServiceException(unloadableMessage(waybill, reason)); } } validateSameProject(waybills); + return waybills; } /** - * 逐张准入失败的可读原因。判据本身在可配载支持类里,这里只把命中的那一条翻成文案。 - * - * @param waybill 未通过准入判据的运单 - * @return 含运单号与原因的提示 + * 逐张准入失败的可读原因:判据只给原因枚举,运单号与文案在这里拼。 */ - private String unloadableReason(Waybill waybill) { + private String unloadableMessage(Waybill waybill, LoadableAdmissionSupport.UnloadableReason reason) { String prefix = "运单" + waybill.getWaybillNo(); - if (Func.isNotEmpty(waybill.getLoadingNo())) { - return prefix + "已关联其他配载单"; - } - if (!LoadableAdmissionSupport.isRoadTransport(waybill.getTransportType())) { - return prefix + "运输方式不是公路,不可配载"; - } - if (!LoadableAdmissionSupport.LOADABLE_BUSINESS_STATUS.equals(waybill.getBusinessStatus())) { - return prefix + "当前状态不是待执行,不可配载"; - } - return prefix + "不可配载"; + return prefix + switch (reason) { + case NOT_ROAD -> "运输方式不是公路,不可配载"; + case NOT_PENDING -> "当前状态不是待执行,不可配载"; + case ON_OTHER_MANIFEST -> "已关联其他配载单"; + }; } /** * 一张配载单的全部子运单必须属于同一个项目,否则点名与其它子运单项目不同的运单。 *

* 查询条件表达不了跨行约束,只能整批取回后在内存里比。 - * - * @param waybills 本次要写入的子运单全集 */ private void validateSameProject(List waybills) { Long projectId = waybills.get(0).getProjectId(); @@ -807,6 +799,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills) { if (!Objects.equals(loadingManage.getCarrierType(), "承运商")) { loadingManage.setCarrierContractId(null); if (!Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) { @@ -825,9 +821,19 @@ public class LoadingManageServiceImpl extends BaseServiceImpl childWaybills = waybills == null + ? loadWaybills(waybillIds(loadingManage.getWaybillIdsJson())) + : waybills; // 合同自带项目与乙方:合同的项目不属于本单子运单的项目,等于换了项目却没换乙方 - if (!projectIdsByWaybills(loadingManage).contains(carrierContract.getProjectId())) { - throw new ServiceException("所选承运商合同不属于本项目"); + if (!projectIdsOf(childWaybills).contains(carrierContract.getProjectId())) { + String waybillNos = childWaybills.stream() + .filter(waybill -> !Objects.equals(waybill.getProjectId(), carrierContract.getProjectId())) + .map(Waybill::getWaybillNo) + .filter(Func::isNotEmpty) + .collect(Collectors.joining("、")); + throw new ServiceException(Func.isEmpty(waybillNos) + ? "所选承运商合同不属于本项目" + : "所选承运商合同不属于运单" + waybillNos + "的项目"); } loadingManage.setCarrierName(carrierContract.getPartyB()); } @@ -845,16 +851,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl projectIdsByWaybills(LoadingManage loadingManage) { - List waybillIds = waybillIds(loadingManage.getWaybillIdsJson()); + private List loadWaybills(List waybillIds) { if (Func.isEmpty(waybillIds)) { return List.of(); } return waybillMapper.selectList(Wrappers.lambdaQuery() - .select(Waybill::getProjectId) .eq(Waybill::getIsDeleted, 0) - .in(Waybill::getId, waybillIds)) - .stream().map(Waybill::getProjectId).filter(Objects::nonNull).distinct().toList(); + .in(Waybill::getId, waybillIds)); + } + + private List projectIdsOf(List waybills) { + return waybills.stream().map(Waybill::getProjectId).filter(Objects::nonNull).distinct().toList(); } private List availableCarrierContracts(List projectIds) { @@ -870,9 +877,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl * 不按项目过滤,才能把「合同不可用」与「合同不属于本项目」分成两种提示。 - * - * @param contractId 合同主键 - * @return 可用的合同;不存在或不可用返回 null + * 取不到可用的合同返回 null。 */ private ContractManage availableCarrierContractById(Long contractId) { if (Func.isEmpty(contractId)) { @@ -899,9 +904,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl * 推导按「接单事实已清、尚未接单」的口径取值,故清接单事实放在状态写入之后,两者口径一致。 */ - private void writeWaybillsForDriver(LoadingManage loadingManage, List oldWaybillIds) { - syncAssociatedWaybills(loadingManage, - deriveWaybillBusinessStatuses(waybillIds(loadingManage.getWaybillIdsJson())), oldWaybillIds); + private void writeWaybillsForDriver(LoadingManage loadingManage, List oldWaybillIds, List waybills) { + syncAssociatedWaybills(loadingManage, deriveWaybillBusinessStatuses(waybills), oldWaybillIds); // 接单事实不清,被拒的票对新司机不可见——司机端一切入口都排除已拒绝的运单 clearAssociatedWaybillAcceptRecords(loadingManage); } @@ -913,16 +917,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl deriveWaybillBusinessStatuses(List waybillIds) { + private Map deriveWaybillBusinessStatuses(List waybills) { Map businessStatusByWaybillId = new LinkedHashMap<>(); - if (Func.isEmpty(waybillIds)) { + if (Func.isEmpty(waybills)) { return businessStatusByWaybillId; } - List waybills = waybillMapper.selectList(Wrappers.lambdaQuery() - .select(Waybill::getId, Waybill::getProjectId, Waybill::getProcessJson) - .eq(Waybill::getIsDeleted, 0) - .in(Waybill::getId, waybillIds)); - // 一次查全所有项目的生效配置,不逐张回查 Map liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService, waybills.stream().map(Waybill::getProjectId).toList()); for (Waybill waybill : waybills) { @@ -943,9 +942,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl statusByWaybillId, List oldWaybillIds) { List newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); if (Func.isNotEmpty(oldWaybillIds)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java index 66f5d834..eb7d9a43 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java @@ -55,6 +55,23 @@ public final class LoadableAdmissionSupport { private LoadableAdmissionSupport() { } + /** + * 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。 + *

+ * 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。 + */ + public enum UnloadableReason { + + /** 运输方式不是公路 */ + NOT_ROAD, + + /** 业务状态不是待执行 */ + NOT_PENDING, + + /** 已关联其它配载单 */ + ON_OTHER_MANIFEST + } + /** * 是否公路运输。未知或为空一律按非公路处理。 * @@ -77,16 +94,6 @@ public final class LoadableAdmissionSupport { return false; } - /** - * 该运单此刻是否可被配载:要求未关联任何配载单。 - * - * @param waybill 运单 - * @return true-可配载 - */ - public static boolean isLoadable(Waybill waybill) { - return isLoadable(waybill, null); - } - /** * 该运单此刻是否可被配载。 *

@@ -98,15 +105,28 @@ public final class LoadableAdmissionSupport { * @return true-可配载 */ public static boolean isLoadable(Waybill waybill, String ownLoadingNo) { - if (waybill == null) { - return false; + return unloadableReason(waybill, ownLoadingNo) == null; + } + + /** + * 该运单此刻不可配载的原因。 + * + * @param waybill 运单 + * @param ownLoadingNo 本配载单号;新建时传 null + * @return 命中的第一条原因;可配载时返回 null + */ + public static UnloadableReason unloadableReason(Waybill waybill, String ownLoadingNo) { + if (waybill == null || !isRoadTransport(waybill.getTransportType())) { + return UnloadableReason.NOT_ROAD; } - if (!isRoadTransport(waybill.getTransportType()) - || !LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) { - return false; + if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) { + return UnloadableReason.NOT_PENDING; } String loadingNo = text(waybill.getLoadingNo()); - return loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo)); + if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) { + return null; + } + return UnloadableReason.ON_OTHER_MANIFEST; } /** diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java index aceee927..a8eccbcd 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageAdmissionTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; *

* 逐张准入用的是读侧候选列表的同一份判据;已挂在本单号上的子运单在先前提交时已过判据, * 重新提交不再重复判——不需接单项目的票提交后即为进行中,重复判会让本单再也保存不了。 + * 暂存只预占运单号、未过判据,故草稿转正式时本单号不构成豁免。 *

* 跨行判据(同项目、合同属于本项目)只对提交/编辑/重新派单生效,暂存草稿不受影响。 */ @@ -197,6 +198,60 @@ class LoadingManageAdmissionTest { assertDoesNotThrow(() -> service.reassign(payload)); } + @Test + @DisplayName("交叉判据同时命中时按 公路 → 待执行 → 已关联其它配载单 取第一条") + void firstFailingConditionWins() { + stubWaybills(waybill(1L, WAYBILL_NO_A, "railway", "pending", "PZ20261018-999", PROJECT_A)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.submit(submitPayload(List.of(1L)))); + + assertEquals("运单" + WAYBILL_NO_A + "运输方式不是公路,不可配载", exception.getMessage()); + } + + // ------------------------------------------------------------ 草稿预占 + + @Test + @DisplayName("暂存只预占运单号:草稿里那张进行中的票在草稿转正式时被拒") + void draftReservedRunningWaybillRejected() { + stubManifest("draft"); + stubWaybills(waybill(1L, WAYBILL_NO_A, "road", "running", OWN_LOADING_NO, PROJECT_A)); + stubContract(CONTRACT_ID, PROJECT_A); + + assertDoesNotThrow(() -> service.saveDraft(existingPayload(List.of(1L)))); + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.submit(existingPayload(List.of(1L)))); + + assertEquals("运单" + WAYBILL_NO_A + "当前状态不是待执行,不可配载", exception.getMessage()); + } + + @Test + @DisplayName("暂存只预占运单号:草稿里那张非公路的票在草稿转正式时被拒") + void draftReservedNonRoadWaybillRejected() { + stubManifest("draft"); + stubWaybills(waybill(1L, WAYBILL_NO_A, "railway", "pending", OWN_LOADING_NO, PROJECT_A)); + stubContract(CONTRACT_ID, PROJECT_A); + + assertDoesNotThrow(() -> service.saveDraft(existingPayload(List.of(1L)))); + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.submit(existingPayload(List.of(1L)))); + + assertEquals("运单" + WAYBILL_NO_A + "运输方式不是公路,不可配载", exception.getMessage()); + } + + @Test + @DisplayName("暂存预占的票本身合格时,草稿转正式照常通过") + void draftReservationStaysAdmissibleOnSubmit() { + stubManifest("draft"); + stubWaybills(waybill(1L, WAYBILL_NO_A, "road", "pending", OWN_LOADING_NO, PROJECT_A)); + stubContract(CONTRACT_ID, PROJECT_A); + + assertDoesNotThrow(() -> service.saveDraft(existingPayload(List.of(1L)))); + assertDoesNotThrow(() -> service.submit(existingPayload(List.of(1L)))); + } + // ------------------------------------------------------------ 跨行判据 @Test @@ -222,7 +277,7 @@ class LoadingManageAdmissionTest { ServiceException exception = assertThrows(ServiceException.class, () -> service.submit(submitPayload(List.of(1L)))); - assertEquals("所选承运商合同不属于本项目", exception.getMessage()); + assertEquals("所选承运商合同不属于运单" + WAYBILL_NO_A + "的项目", exception.getMessage()); } @Test @@ -296,16 +351,26 @@ class LoadingManageAdmissionTest { return loadingManage; } + private LoadingManage existingPayload(List waybillIdList) { + LoadingManage loadingManage = submitPayload(waybillIdList); + loadingManage.setId(OWN_LOADING_ID); + return loadingManage; + } + private static String waybillIdsJson(List waybillIdList) { return waybillIdList.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]")); } private void stubExistingManifest() { + stubManifest("pending"); + } + + private void stubManifest(String businessStatus) { LoadingManage existing = new LoadingManage(); existing.setId(OWN_LOADING_ID); existing.setLoadingNo(OWN_LOADING_NO); existing.setDeptId(DEPT_ID); - existing.setBusinessStatus("pending"); + existing.setBusinessStatus(businessStatus); when(loadingManageMapper.selectById(OWN_LOADING_ID)).thenReturn(existing); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java index 8ebcbee3..52ab19f9 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java @@ -166,9 +166,6 @@ class LoadingManageChildStatusWriteTest { @Test @DisplayName("跨项目的子单逐张按各自项目推导,互不牵连,配置只查一次") void deriveEachWaybillStatusFromItsOwnProject() throws Exception { - stubWaybills( - waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID), - waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID)); stubConfigs( config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON), config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON)); @@ -177,7 +174,9 @@ class LoadingManageChildStatusWriteTest { assertEquals(Map.of( NEED_ACCEPT_WAYBILL_ID, "pending", NO_ACCEPT_WAYBILL_ID, "running"), - deriveBusinessStatuses(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID)); + deriveBusinessStatuses( + waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID), + waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID))); verify(processConfigService, times(1)).list(ArgumentMatchers.>any()); } @@ -409,12 +408,12 @@ class LoadingManageChildStatusWriteTest { } /** 逐张推导的结果:提交/重新派单在准入阶段就拦下跨项目子单,跨项目推导只能直接测这个函数 */ - private Map deriveBusinessStatuses(Long... waybillIds) throws Exception { + private Map deriveBusinessStatuses(Waybill... waybills) throws Exception { Method method = LoadingManageServiceImpl.class .getDeclaredMethod("deriveWaybillBusinessStatuses", List.class); method.setAccessible(true); @SuppressWarnings("unchecked") - Map statusByWaybillId = (Map) method.invoke(service, List.of(waybillIds)); + Map statusByWaybillId = (Map) method.invoke(service, List.of(waybills)); return statusByWaybillId; } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java index 92d8382f..9b5c72ad 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java @@ -4,7 +4,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springblade.transport.pojo.entity.Waybill; +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; /** @@ -50,30 +52,47 @@ class LoadableAdmissionSupportTest { @Test @DisplayName("公路 + 待执行 + 未配载才可配载") void loadableOnlyWhenAllConditionsHold() { - assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"))); - assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null))); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null)); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null)); } @Test @DisplayName("已拒单的票照样可配载:接单事实由配载提交清除,不靠把拒单票挡在候选之外") void rejectedWaybillIsLoadable() { - assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "rejected"))); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "rejected"), null)); assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "rejected"), "PZ20260101-001")); } @Test @DisplayName("非公路、非待执行、已关联配载单各自单独即可拒绝") void eachConditionRejectsOnItsOwn() { - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending")), + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending"), null), "非公路不可配载"); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null)), + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null), "进行中不可配载:不需要接单的项目保存后即为进行中,那是过程配置的产物,不代表可配载"); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null))); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null))); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null))); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", "PZ20260101-001", null)), + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null), null)); + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null), null)); + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null)); + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", "PZ20260101-001", null), null), "已挂在其它配载单上的票不可再配载"); - assertFalse(LoadableAdmissionSupport.isLoadable(null)); + assertFalse(LoadableAdmissionSupport.isLoadable(null, null)); + } + + @Test + @DisplayName("不可配载原因按 公路 → 待执行 → 未关联其它配载单 取第一条") + void unloadableReasonFollowsPredicateOrder() { + assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD, + LoadableAdmissionSupport.unloadableReason( + waybill("railway", "running", "PZ20260101-002", null), "PZ20260101-001")); + assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_PENDING, + LoadableAdmissionSupport.unloadableReason( + waybill("road", "running", "PZ20260101-002", null), "PZ20260101-001")); + assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST, + LoadableAdmissionSupport.unloadableReason( + waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001")); + assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null)); + assertNull(LoadableAdmissionSupport.unloadableReason( + waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001")); } @Test