From 543a6909020a318cf742bd3a31d7c5501164b721 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Sun, 18 Oct 2026 14:03:41 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8D=95=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=96=B0=E5=A2=9E=E5=8F=AF=E9=85=8D=E8=BD=BD=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E4=B8=8E=E6=8C=89=E4=B8=BB=E9=94=AE=E5=9B=9E=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/vo/WaybillVO.java | 9 + .../service/impl/WaybillServiceImpl.java | 7 + .../support/LoadableAdmissionSupport.java | 144 +++++++++++++++ .../impl/WaybillLoadableQueryTest.java | 173 ++++++++++++++++++ .../support/LoadableAdmissionSupportTest.java | 92 ++++++++++ 5 files changed, 425 insertions(+) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java index ebba5343..a2ecf024 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java @@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.Waybill; import java.io.Serial; +import java.util.List; /** * 运单管理视图实体类 @@ -78,4 +79,12 @@ public class WaybillVO extends Waybill { @Schema(description = "是否仅查询未配载运单") private Integer onlyUnassignedLoading; + @TableField(exist = false) + @Schema(description = "是否仅查询可配载运单") + private Integer loadableOnly; + + @TableField(exist = false) + @Schema(description = "运单主键集合") + private List ids; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 3b242698..14e60060 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -68,6 +68,7 @@ import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.support.LiveProcessConfigSupport; +import org.springblade.transport.support.LoadableAdmissionSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.WaybillWrapper; @@ -1735,6 +1736,12 @@ public class WaybillServiceImpl extends BaseServiceImpl if (Objects.equals(waybill.getOnlyUnassignedLoading(), 1)) { queryWrapper.and(wrapper -> wrapper.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, "")); } + if (Func.isNotEmpty(waybill.getIds())) { + queryWrapper.in(Waybill::getId, waybill.getIds()); + } + if (Objects.equals(waybill.getLoadableOnly(), 1)) { + LoadableAdmissionSupport.applyLoadableConditions(queryWrapper); + } if (Func.isNotEmpty(waybill.getBatchNo())) { queryWrapper.like(Waybill::getBatchNo, waybill.getBatchNo()); } 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 new file mode 100644 index 00000000..202c2644 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java @@ -0,0 +1,144 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springblade.transport.pojo.entity.Waybill; + +import java.util.List; +import java.util.Locale; + +/** + * 可配载准入判据:公路 + 待执行 + 未关联其它配载单 + 未拒单。 + *

+ * 候选列表取数与配载提交/编辑/重新派单的校验共用这一份定义。 + */ +public final class LoadableAdmissionSupport { + + /** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */ + public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING; + + /** + * 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。 + *

+ * 运输方式字典只有 road/railway/river/ocean/air,而库里存在「公路运输」「水路运输」这类中文取值, + * 因此词表写在代码里,不从字典取。 + */ + public static final List ROAD_TRANSPORT_KEYWORDS = List.of("公路", "道路", "汽车", "陆运", "road", "highway"); + + /** 公路运输方式的历史短码,按整体相等比对(按包含比对会误伤含 gl 的其它取值) */ + public static final List ROAD_TRANSPORT_CODES = List.of("gl"); + + private LoadableAdmissionSupport() { + } + + /** + * 是否公路运输。未知或为空一律按非公路处理。 + * + * @param transportType 运单的运输方式取值 + * @return true-公路 + */ + public static boolean isRoadTransport(String transportType) { + String value = text(transportType).toLowerCase(Locale.ROOT); + if (value.isEmpty()) { + return false; + } + if (ROAD_TRANSPORT_CODES.contains(value)) { + return true; + } + for (String keyword : ROAD_TRANSPORT_KEYWORDS) { + if (value.contains(keyword)) { + return true; + } + } + return false; + } + + /** + * 该运单此刻是否可被配载:要求未关联任何配载单。 + * + * @param waybill 运单 + * @return true-可配载 + */ + public static boolean isLoadable(Waybill waybill) { + return isLoadable(waybill, null); + } + + /** + * 该运单此刻是否可被配载。 + *

+ * ownLoadingNo 是正在编辑或重新派单的配载单号:运单已挂在本单上不算冲突, + * 否则编辑保存会把自己的子运单判为不可用。 + * + * @param waybill 运单 + * @param ownLoadingNo 本配载单号;新建时传 null + * @return true-可配载 + */ + public static boolean isLoadable(Waybill waybill, String ownLoadingNo) { + if (waybill == null) { + return false; + } + if (!isRoadTransport(waybill.getTransportType()) + || !LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus())) + || WaybillProcessSupport.isRejected(waybill.getDriverAcceptStatus())) { + return false; + } + String loadingNo = text(waybill.getLoadingNo()); + return loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo)); + } + + /** + * 把整条可配载判据加到运单查询上:公路 + 待执行 + 未配载 + 非拒单。 + * + * @param wrapper 运单列表查询条件 + */ + public static void applyLoadableConditions(LambdaQueryWrapper wrapper) { + applyRoadTransportCondition(wrapper); + wrapper.eq(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUS); + wrapper.and(nested -> nested.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, "")); + // 接单状态绝大多数为 NULL,而 NULL <> 'rejected' 不成立,只写不等于会把它们一起滤掉 + wrapper.and(nested -> nested.isNull(Waybill::getDriverAcceptStatus) + .or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); + } + + private static void applyRoadTransportCondition(LambdaQueryWrapper wrapper) { + wrapper.and(nested -> { + boolean started = false; + for (String keyword : ROAD_TRANSPORT_KEYWORDS) { + if (started) { + nested.or(); + } + nested.like(Waybill::getTransportType, keyword); + started = true; + } + for (String code : ROAD_TRANSPORT_CODES) { + nested.or().eq(Waybill::getTransportType, code); + } + }); + } + + private static String text(String value) { + return value == null ? "" : value.trim(); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java new file mode 100644 index 00000000..3b588be4 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java @@ -0,0 +1,173 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +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.AfterEach; +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.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.transport.mapper.WaybillMapper; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.WaybillVO; +import org.springblade.transport.support.LoadableAdmissionSupport; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 运单列表的可配载取数:开关一次带上整条判据,另支持按主键集合回查。 + *

+ * 候选列表与「公路配载」按钮预检都走这次查询,所以断言下在生成的 SQL 片段上—— + * 开关不启用时默认列表不能被顺手改口径,那是全部运单入口共用的查询。 + */ +class WaybillLoadableQueryTest { + + private WaybillMapper waybillMapper; + private WaybillServiceImpl service; + private MockedStatic authUtil; + + @BeforeAll + static void initTableInfo() { + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Waybill.class); + } + + @BeforeEach + void setUp() throws Exception { + waybillMapper = mock(WaybillMapper.class); + service = new WaybillServiceImpl(); + inject("baseMapper", waybillMapper); + when(waybillMapper.selectPage(any(), any())).thenReturn(new Page(1, 10)); + // 组织过滤只看当前用户;超级管理员不带组织条件,断言里就只剩本用例关心的条件 + authUtil = mockStatic(AuthUtil.class); + when(AuthUtil.getUserRole()).thenReturn("administrator"); + } + + @AfterEach + void tearDown() { + authUtil.close(); + } + + @Test + @DisplayName("开关开启:公路词表、待执行、未配载、非拒单一次全部进 wrapper") + void loadableSwitchAppliesWholePredicate() { + LambdaQueryWrapper wrapper = capturedQuery(loadableQuery()); + String sql = String.valueOf(wrapper.getTargetSql()); + Collection params = wrapper.getParamNameValuePairs().values(); + + assertTrue(sql.contains("transport_type"), "公路判定要落在运单自己的运输方式上:" + sql); + for (String keyword : LoadableAdmissionSupport.ROAD_TRANSPORT_KEYWORDS) { + assertTrue(params.contains("%" + keyword + "%"), + "公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params); + } + assertTrue(sql.contains("business_status =") && params.contains("pending"), + "只放行待执行:" + sql + " " + params); + assertFalse(params.contains("running"), + "进行中一律不可配载,不能沿用「待执行与进行中都可配载」的旧口径:" + params); + assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="), + "未配载要同时覆盖 NULL 与空串:" + sql); + assertTrue(sql.contains("driver_accept_status <>") && params.contains("rejected"), + "接单状态为已拒绝的要排除:" + sql); + assertTrue(sql.contains("driver_accept_status IS NULL"), + "接单状态大多是 NULL,只写不等于 rejected 会把它们一起滤掉:" + sql); + } + + @Test + @DisplayName("开关开启 + 主键集合:收窄到所选运单,用于按钮预检") + void loadableSwitchNarrowsBySelectedIds() { + WaybillVO query = loadableQuery(); + query.setIds(List.of(11L, 22L)); + + LambdaQueryWrapper wrapper = capturedQuery(query); + String sql = String.valueOf(wrapper.getTargetSql()); + Collection params = wrapper.getParamNameValuePairs().values(); + + assertTrue(sql.contains("id IN"), "按主键回查:" + sql); + assertTrue(params.contains(11L) && params.contains(22L), "所选运单主键要进条件:" + params); + assertTrue(sql.contains("transport_type"), "回查仍要带整条判据:" + sql); + } + + @Test + @DisplayName("只传主键集合:按主键收窄,不附带可配载判据") + void idsNarrowWithoutTheSwitch() { + WaybillVO query = new WaybillVO(); + query.setAllDept(1); + query.setIds(List.of(33L)); + + LambdaQueryWrapper wrapper = capturedQuery(query); + String sql = String.valueOf(wrapper.getTargetSql()); + + assertTrue(sql.contains("id IN"), "按主键回查:" + sql); + assertTrue(wrapper.getParamNameValuePairs().values().contains(33L), sql); + assertFalse(sql.contains("transport_type"), "没开可配载开关就不加公路条件:" + sql); + } + + @Test + @DisplayName("开关未开启:运单管理默认列表条件与展示口径不变") + void withoutSwitchTheDefaultListIsUntouched() { + WaybillVO query = new WaybillVO(); + query.setAllDept(1); + + LambdaQueryWrapper wrapper = capturedQuery(query); + String sql = String.valueOf(wrapper.getTargetSql()); + + assertFalse(sql.contains("transport_type"), sql); + assertFalse(sql.contains("business_status"), sql); + assertFalse(sql.contains("loading_no"), sql); + assertFalse(sql.contains("driver_accept_status"), sql); + } + + // ------------------------------------------------------------ 夹具 + + private WaybillVO loadableQuery() { + WaybillVO query = new WaybillVO(); + query.setAllDept(1); + query.setLoadableOnly(1); + return query; + } + + private LambdaQueryWrapper capturedQuery(WaybillVO query) { + IPage page = service.selectWaybillPage(new Page<>(1, 10), query); + assertTrue(page.getRecords().isEmpty()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(waybillMapper).selectPage(any(), captor.capture()); + return captor.getValue(); + } + + private void inject(String field, Object value) throws Exception { + Class type = service.getClass(); + while (type != null) { + try { + Field declared = type.getDeclaredField(field); + declared.setAccessible(true); + declared.set(service, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new IllegalStateException("找不到字段:" + field); + } + +} 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 new file mode 100644 index 00000000..e73bd74e --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java @@ -0,0 +1,92 @@ +package org.springblade.transport.support; + +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 可配载准入判据:公路词表与单张运单的准入检查。 + *

+ * 判据是读侧候选列表与写侧提交/编辑/重新派单校验共用的同一份定义。 + */ +class LoadableAdmissionSupportTest { + + // ------------------------------------------------------------ 公路词表 + + @Test + @DisplayName("公路:road 与中文同义写法都命中") + void roadVocabularyHits() { + assertTrue(LoadableAdmissionSupport.isRoadTransport("road")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("ROAD")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("公路运输")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("公路")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("道路运输")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("汽车运输")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("陆运")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("highway")); + assertTrue(LoadableAdmissionSupport.isRoadTransport("gl")); + } + + @Test + @DisplayName("非公路与空值一律不命中——未知运输方式按非公路拒绝,不能放行") + void nonRoadVocabularyMisses() { + assertFalse(LoadableAdmissionSupport.isRoadTransport("railway")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("water")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("air")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("river")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("水路运输")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("铁路运输")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("航空运输")); + assertFalse(LoadableAdmissionSupport.isRoadTransport("")); + assertFalse(LoadableAdmissionSupport.isRoadTransport(" ")); + assertFalse(LoadableAdmissionSupport.isRoadTransport(null)); + } + + // ------------------------------------------------------------ 单张运单准入 + + @Test + @DisplayName("公路 + 待执行 + 未配载 + 未拒单才可配载") + void loadableOnlyWhenAllConditionsHold() { + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"))); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null))); + } + + @Test + @DisplayName("非公路、非待执行、已关联配载单、已拒单各自单独即可拒绝") + void eachConditionRejectsOnItsOwn() { + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending")), + "非公路不可配载"); + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", 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", "pending", null, "rejected")), + "已拒单的票不可配载"); + assertFalse(LoadableAdmissionSupport.isLoadable(null)); + } + + @Test + @DisplayName("编辑/重新派单:loadingNo 等于本配载单号时不算冲突,其它配载单号仍拒绝") + void ownLoadingNoIsNotAConflict() { + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001")); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, null), "PZ20260101-001")); + assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001"), + "挂在别的配载单上时即使正在编辑本单也不能合进来"); + } + + private Waybill waybill(String transportType, String businessStatus, String loadingNo, String driverAcceptStatus) { + Waybill waybill = new Waybill(); + waybill.setTransportType(transportType); + waybill.setBusinessStatus(businessStatus); + waybill.setLoadingNo(loadingNo); + waybill.setDriverAcceptStatus(driverAcceptStatus); + return waybill; + } + +}