✨ 运单查询新增可配载开关与按主键回查
This commit is contained in:
+9
@@ -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<Long> ids;
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -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<WaybillMapper, Waybill>
|
||||
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());
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 可配载准入判据:公路 + 待执行 + 未关联其它配载单 + 未拒单。
|
||||
* <p>
|
||||
* 候选列表取数与配载提交/编辑/重新派单的校验共用这一份定义。
|
||||
*/
|
||||
public final class LoadableAdmissionSupport {
|
||||
|
||||
/** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */
|
||||
public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING;
|
||||
|
||||
/**
|
||||
* 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。
|
||||
* <p>
|
||||
* 运输方式字典只有 road/railway/river/ocean/air,而库里存在「公路运输」「水路运输」这类中文取值,
|
||||
* 因此词表写在代码里,不从字典取。
|
||||
*/
|
||||
public static final List<String> ROAD_TRANSPORT_KEYWORDS = List.of("公路", "道路", "汽车", "陆运", "road", "highway");
|
||||
|
||||
/** 公路运输方式的历史短码,按整体相等比对(按包含比对会误伤含 gl 的其它取值) */
|
||||
public static final List<String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 该运单此刻是否可被配载。
|
||||
* <p>
|
||||
* 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<Waybill> 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<Waybill> 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();
|
||||
}
|
||||
|
||||
}
|
||||
+173
@@ -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;
|
||||
|
||||
/**
|
||||
* 运单列表的可配载取数:开关一次带上整条判据,另支持按主键集合回查。
|
||||
* <p>
|
||||
* 候选列表与「公路配载」按钮预检都走这次查询,所以断言下在生成的 SQL 片段上——
|
||||
* 开关不启用时默认列表不能被顺手改口径,那是全部运单入口共用的查询。
|
||||
*/
|
||||
class WaybillLoadableQueryTest {
|
||||
|
||||
private WaybillMapper waybillMapper;
|
||||
private WaybillServiceImpl service;
|
||||
private MockedStatic<AuthUtil> 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<Waybill>(1, 10));
|
||||
// 组织过滤只看当前用户;超级管理员不带组织条件,断言里就只剩本用例关心的条件
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
when(AuthUtil.getUserRole()).thenReturn("administrator");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
authUtil.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("开关开启:公路词表、待执行、未配载、非拒单一次全部进 wrapper")
|
||||
void loadableSwitchAppliesWholePredicate() {
|
||||
LambdaQueryWrapper<Waybill> wrapper = capturedQuery(loadableQuery());
|
||||
String sql = String.valueOf(wrapper.getTargetSql());
|
||||
Collection<Object> 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<Waybill> wrapper = capturedQuery(query);
|
||||
String sql = String.valueOf(wrapper.getTargetSql());
|
||||
Collection<Object> 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<Waybill> 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<Waybill> 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<Waybill> capturedQuery(WaybillVO query) {
|
||||
IPage<WaybillVO> page = service.selectWaybillPage(new Page<>(1, 10), query);
|
||||
assertTrue(page.getRecords().isEmpty());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<Waybill>> 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);
|
||||
}
|
||||
|
||||
}
|
||||
+92
@@ -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;
|
||||
|
||||
/**
|
||||
* 可配载准入判据:公路词表与单张运单的准入检查。
|
||||
* <p>
|
||||
* 判据是读侧候选列表与写侧提交/编辑/重新派单校验共用的同一份定义。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user