✨ 运单与配载单在派单、配载、导入时写入司机档案 ID

This commit is contained in:
2026-10-27 20:30:07 +08:00
parent 4a4b69024f
commit 50ad99370c
11 changed files with 640 additions and 13 deletions
@@ -68,6 +68,9 @@ public class LoadingManage extends TenantEntity {
@Schema(description = "联系电话")
private String driverPhone;
@Schema(description = "司机ID")
private Long driverId;
@Schema(description = "押运人")
private String escortName;
@@ -29,6 +29,7 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
@@ -89,6 +90,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
@Resource
private IProcessConfigService processConfigService;
@Resource
private IDriverService driverService;
@Override
public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) {
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
@@ -319,6 +323,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setCargoName(first.getCargoName());
loadingManage.setVehicleNo(first.getVehicleNo());
loadingManage.setTrailerVehicleNo(first.getTrailerVehicleNo());
loadingManage.setDriverId(first.getDriverId());
loadingManage.setDriverName(first.getDriverName());
loadingManage.setDriverPhone(first.getDriverPhone());
loadingManage.setEscortName(first.getEscortName());
@@ -505,6 +510,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
target.setBusinessStatus(STATUS_DRAFT);
target.setLoadingNo(nextCode());
prepare(target);
// 源单已有归属就沿用(手机号可能已被改过);没有才用刚解析出的值
target.setDriverId(source.getDriverId() != null ? source.getDriverId() : target.getDriverId());
validate(target, true);
save(target);
return detail(target.getId());
@@ -749,6 +756,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setTrailerVehicleNo(TransportBusinessSupport.trimToNull(loadingManage.getTrailerVehicleNo()));
loadingManage.setDriverName(TransportBusinessSupport.trimToNull(loadingManage.getDriverName()));
loadingManage.setDriverPhone(TransportBusinessSupport.trimToNull(loadingManage.getDriverPhone()));
// 归属主键是档案 ID,手机号只是号码快照:每次保存都按手机号重解析,
// 避免改派后残留旧归属;解析不到(承运商司机)留空,不阻断保存
Long resolvedDriverId = driverService.resolveDriverIdByMobile(
loadingManage.getTenantId(), loadingManage.getDriverPhone());
// driver_id 只有 >0 才算有主,0 / -1 都是「无主」的哨兵写法,落库统一为 NULL
loadingManage.setDriverId(resolvedDriverId != null && resolvedDriverId > 0 ? resolvedDriverId : null);
loadingManage.setEscortName(TransportBusinessSupport.trimToNull(loadingManage.getEscortName()));
loadingManage.setEscortPhone(TransportBusinessSupport.trimToNull(loadingManage.getEscortPhone()));
loadingManage.setCarrierType(TransportBusinessSupport.trimToNull(loadingManage.getCarrierType()));
@@ -1150,6 +1163,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
.set(Waybill::getDriverId, loadingManage.getDriverId())
.set(Waybill::getDriverName, loadingManage.getDriverName())
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
@@ -1264,6 +1278,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
.set(Waybill::getDriverId, loadingManage.getDriverId())
.set(Waybill::getDriverName, loadingManage.getDriverName())
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
@@ -24,6 +24,7 @@ import org.springblade.transport.pojo.vo.MasterOrderCarrierVO;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IMasterOrderService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
@@ -58,13 +59,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
private final ITransportPlanService transportPlanService;
private final IProjectApplyService projectApplyService;
private final IContractManageService contractManageService;
private final IDriverService driverService;
public MasterOrderServiceImpl(IWaybillService waybillService, ITransportPlanService transportPlanService, IProjectApplyService projectApplyService,
IContractManageService contractManageService) {
IContractManageService contractManageService, IDriverService driverService) {
this.waybillService = waybillService;
this.transportPlanService = transportPlanService;
this.projectApplyService = projectApplyService;
this.contractManageService = contractManageService;
this.driverService = driverService;
}
@Override
@@ -263,6 +266,13 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
waybill.setTransportType(string(dispatch, "transportType")); waybill.setCarrierType(string(dispatch, "carrierType"));
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId")); waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
waybill.setDriverPhone(string(dispatch, "driverPhone")); waybill.setVehicleNo(string(dispatch, "vehicleNo")); waybill.setTrailerVehicleNo(string(dispatch, "trailerVehicleNo"));
// 归属主键是档案 ID:调度带了就用它,否则按手机号解析;承运商司机解析为空也照常派单
Long driverId = longValue(dispatch, "driverId");
if (driverId == null || driverId <= 0) {
Long resolvedDriverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), waybill.getDriverPhone());
driverId = resolvedDriverId != null && resolvedDriverId > 0 ? resolvedDriverId : null;
}
waybill.setDriverId(driverId);
waybill.setCaptainName(string(dispatch, "captainName")); waybill.setCabinNo(string(dispatch, "cabinNo")); waybill.setContainerNo(string(dispatch, "containerNo"));
waybill.setEscortName(string(dispatch, "escortName")); waybill.setEscortPhone(string(dispatch, "escortPhone")); waybill.setMileage(nullableDecimal(dispatch, "mileage"));
waybill.setDepartureName(string(dispatch, "departureName")); waybill.setDepartureAddress(string(dispatch, "departureAddress"));
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
@@ -41,6 +42,7 @@ import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ILoadingManageService;
@@ -68,6 +70,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** 运单批次服务实现。 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
@@ -101,6 +104,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
private final IReceivablePayableDetailService receivablePayableDetailService;
private final ILoadingManageService loadingManageService;
private final ISysClient sysClient;
private final IDriverService driverService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -207,7 +211,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
List<Waybill> rowWaybills = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
try {
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
Waybill builtWaybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
fillDriverOwnership(builtWaybill, index + 1);
rowWaybills.add(builtWaybill);
} catch (Exception exception) {
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
}
@@ -485,6 +491,26 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return id == null || id <= 0L ? null : id;
}
/**
* 补齐导入运单的司机档案 ID:行内有(老系统导出)就用,否则按手机号解析。
* 解析不到不阻断导入,只留行级 WARN:承运商司机与未登录过小程序的司机本来就没有档案。
*/
private void fillDriverOwnership(Waybill waybill, int rowNo) {
Long driverId = waybill.getDriverId();
if (driverId != null && driverId > 0) {
return;
}
Long resolvedDriverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), waybill.getDriverPhone());
if (resolvedDriverId != null && resolvedDriverId > 0) {
waybill.setDriverId(resolvedDriverId);
return;
}
// driver_id 只有 >0 才算有主,0 / -1 都按无主落空
waybill.setDriverId(null);
log.warn("第{}行 运单 {} 的司机手机号未唯一匹配司机档案,driver_id 留空(不阻断导入)",
rowNo, waybill.getWaybillNo());
}
private Waybill buildWaybill(Map<String, Object> row, WaybillImportBatch batch, Long carrierContractId, boolean draft) {
if (row == null) throw new ServiceException("运单数据不正确");
Waybill waybill = new Waybill();
@@ -61,6 +61,7 @@ import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IProjectApplyService;
@@ -110,6 +111,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
@jakarta.annotation.Resource
private ILoadingManageService loadingManageService;
@jakarta.annotation.Resource
private IDriverService driverService;
@jakarta.annotation.Resource
private IProcessConfigService processConfigService;
@@ -1610,7 +1614,15 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateRequired(driverPhone, "手机号不能为空");
TransportBusinessSupport.validateRequired(vehicleNo, "车牌号不能为空");
waybill.setDriverId(request.getDriverId());
// 归属主键是档案 ID,手机号只是派单时的号码快照:调用方带了档案 ID 就用它,
// 否则按手机号解析;承运商司机、未建档司机解析为空,派单照旧成功,只留 WARN
Long driverId = request.getDriverId();
if (driverId == null || driverId <= 0) {
Long resolvedDriverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), driverPhone);
// driver_id 只有 >0 才算有主,0 / -1 都是「无主」的哨兵写法,落库统一为 NULL
driverId = resolvedDriverId != null && resolvedDriverId > 0 ? resolvedDriverId : null;
}
waybill.setDriverId(driverId);
waybill.setDriverName(driverName);
waybill.setDriverPhone(driverPhone);
waybill.setVehicleNo(vehicleNo);
@@ -19,6 +19,7 @@ import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import java.lang.reflect.Field;
import java.util.List;
@@ -81,6 +82,7 @@ class LoadingManageAdmissionTest {
inject("baseMapper", loadingManageMapper);
inject("waybillMapper", waybillMapper);
inject("contractManageService", contractManageService);
inject("driverService", mock(IDriverService.class));
authUtil = mockStatic(AuthUtil.class);
authUtil.when(AuthUtil::getDeptId).thenReturn(String.valueOf(DEPT_ID));
}
@@ -15,16 +15,19 @@ import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.transport.mapper.LoadingManageMapper;
import org.springblade.transport.mapper.WaybillMapper;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.ProcessConfig;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@@ -35,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -60,6 +64,9 @@ class LoadingManageChildStatusWriteTest {
private static final String LOADING_NO = "PZ-20261018-001";
private static final Long LOADING_ID = 1800000000000000001L;
private static final String DRIVER_PHONE = "13800138000";
private static final String TENANT_ID = "000000";
private static final Long RESOLVED_DRIVER_ID = 9321L;
private static final Long SOURCE_LOADING_ID = 1800000000000000002L;
private static final Long NEED_ACCEPT_WAYBILL_ID = 3101L;
private static final Long NO_ACCEPT_WAYBILL_ID = 3102L;
@@ -80,6 +87,7 @@ class LoadingManageChildStatusWriteTest {
"carrier_type",
"carrier_contract_id",
"carrier_name",
"driver_id",
"driver_name",
"driver_phone",
"vehicle_no",
@@ -104,6 +112,8 @@ class LoadingManageChildStatusWriteTest {
private IProcessConfigService processConfigService;
private IContractManageService contractManageService;
private IReceivablePayableDetailService receivablePayableDetailService;
private IDriverService driverService;
private LoadingManageMapper loadingManageMapper;
private LoadingManageServiceImpl service;
private MockedStatic<AuthUtil> authUtil;
@@ -115,6 +125,7 @@ class LoadingManageChildStatusWriteTest {
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
TableInfoHelper.initTableInfo(assistant, ProcessConfig.class);
TableInfoHelper.initTableInfo(assistant, LoadingManage.class);
}
@BeforeEach
@@ -123,14 +134,19 @@ class LoadingManageChildStatusWriteTest {
processConfigService = mock(IProcessConfigService.class);
contractManageService = mock(IContractManageService.class);
receivablePayableDetailService = mock(IReceivablePayableDetailService.class);
driverService = mock(IDriverService.class);
loadingManageMapper = mock(LoadingManageMapper.class);
service = spy(new LoadingManageServiceImpl());
inject(service, "waybillMapper", waybillMapper);
inject(service, "processConfigService", processConfigService);
inject(service, "contractManageService", contractManageService);
inject(service, "receivablePayableDetailService", receivablePayableDetailService);
inject(service, "driverService", driverService);
inject(service, "baseMapper", loadingManageMapper);
doReturnTrue(service);
authUtil = mockStatic(AuthUtil.class);
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
}
@AfterEach
@@ -309,6 +325,75 @@ class LoadingManageChildStatusWriteTest {
verify(processConfigService, never()).list(ArgumentMatchers.<Wrapper<ProcessConfig>>any());
}
@Test
@DisplayName("提交配载单:按手机号解析到档案 ID 时,配载单与子单一起记住归属")
void submitWritesResolvedDriverId() {
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
assertEquals(RESOLVED_DRIVER_ID, capturedLoading().getDriverId());
assertEquals(RESOLVED_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"),
"子单必须与配载单写同一个归属,否则司机端按归属查不到这张票");
}
@Test
@DisplayName("解析不到档案(承运商司机):提交照常成功,归属留空")
void submitAllowsUnresolvedDriverId() {
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
assertNull(capturedLoading().getDriverId());
assertNull(statusSyncUpdates().get(0).setValue("driver_id"));
}
@Test
@DisplayName("进行中编辑保留的子单:任务字段更新也带上司机归属,改派后不留旧值")
void runningEditCarriesDriverIdToKeptWaybills() {
LoadingManage record = runningRecord();
stubLoadingRecord(record);
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
CapturedUpdate taskUpdate = waybillUpdates().stream()
.filter(update -> update.columns().contains("driver_name"))
.findFirst()
.orElseThrow();
assertEquals(List.of(NEED_ACCEPT_WAYBILL_ID), taskUpdate.whereIds());
assertEquals(RESOLVED_DRIVER_ID, taskUpdate.setValue("driver_id"));
}
@Test
@DisplayName("复制配载单:沿用源单的司机归属,不按手机号重解析覆盖")
void copyKeepsSourceDriverId() {
LoadingManage source = copySource();
source.setDriverId(555L);
stubCopyDependencies(source);
service.copy(SOURCE_LOADING_ID);
assertEquals(555L, capturedSaved().getDriverId());
}
@Test
@DisplayName("复制配载单:源单没有归属时,用手机号解析出的档案 ID")
void copyFallsBackToResolvedDriverId() {
LoadingManage source = copySource();
stubCopyDependencies(source);
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
service.copy(SOURCE_LOADING_ID);
assertEquals(RESOLVED_DRIVER_ID, capturedSaved().getDriverId());
}
// ------------------------------------------------------------ 夹具
private LoadingManage loadingForm(Long... waybillIds) {
@@ -375,10 +460,46 @@ class LoadingManageChildStatusWriteTest {
return waybill;
}
private String capturedLoadingStatus() {
private LoadingManage runningRecord() {
LoadingManage record = loadingRecord("running", NEED_ACCEPT_WAYBILL_ID);
record.setVehicleNo("桂A12345");
record.setDriverName("张三");
record.setDriverPhone(DRIVER_PHONE);
return record;
}
private LoadingManage copySource() {
LoadingManage source = loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID);
source.setLoadingSubNos("YD-" + NEED_ACCEPT_WAYBILL_ID);
source.setTransportType("公路运输");
source.setVehicleNo("桂A12345");
source.setDriverName("张三");
source.setDriverPhone(DRIVER_PHONE);
source.setDepartureAddress("南宁");
source.setArrivalAddress("广州");
return source;
}
private void stubCopyDependencies(LoadingManage source) {
source.setId(SOURCE_LOADING_ID);
doReturn(source).when(service).getById(SOURCE_LOADING_ID);
doReturn(null).when(service).detail(any());
}
private LoadingManage capturedSaved() {
ArgumentCaptor<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).save(captor.capture());
return captor.getValue();
}
private LoadingManage capturedLoading() {
ArgumentCaptor<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
return captor.getValue().getBusinessStatus();
return captor.getValue();
}
private String capturedLoadingStatus() {
return capturedLoading().getBusinessStatus();
}
private void stubWaybills(Waybill... waybills) {
@@ -438,18 +559,28 @@ class LoadingManageChildStatusWriteTest {
}
private static void inject(LoadingManageServiceImpl target, String fieldName, Object value) {
try {
Field field = LoadingManageServiceImpl.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (ReflectiveOperationException exception) {
throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
Class<?> type = LoadingManageServiceImpl.class;
while (type != null) {
try {
Field field = type.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
return;
} catch (NoSuchFieldException ignored) {
type = type.getSuperclass();
} catch (ReflectiveOperationException exception) {
throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
}
}
throw new IllegalStateException("注入测试替身失败:" + fieldName);
}
/** 一条被捕获的批量更新:SET 的列 → 值、WHERE 的运单 id */
private record CapturedUpdate(String sqlSet, String whereSql, Map<String, Object> params, List<String> columns) {
private static final java.util.regex.Pattern PARAM_REF =
java.util.regex.Pattern.compile("#\\{ew\\.paramNameValuePairs\\.([^}]+)}");
@SuppressWarnings("unchecked")
static CapturedUpdate of(Wrapper<Waybill> wrapper) {
LambdaUpdateWrapper<Waybill> update = (LambdaUpdateWrapper<Waybill>) wrapper;
@@ -478,8 +609,20 @@ class LoadingManageChildStatusWriteTest {
return null;
}
/** 只认 WHERE 段用到的参数:SET 段的 driver_id 也是 Long,混进来会当成运单 id */
List<Long> whereIds() {
return params.values().stream().filter(Long.class::isInstance).map(Long.class::cast).toList();
Set<String> setParamKeys = new java.util.HashSet<>();
Matcher matcher = PARAM_REF.matcher(sqlSet);
while (matcher.find()) {
setParamKeys.add(matcher.group(1));
}
List<Long> ids = new java.util.ArrayList<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() instanceof Long longValue && !setParamKeys.contains(entry.getKey())) {
ids.add(longValue);
}
}
return ids;
}
}
}
@@ -0,0 +1,120 @@
package org.springblade.transport.service.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 多联总单派单建运单时写司机档案 ID 的测试。
* <p>
* 派单请求可能来自调度(带档案 ID),也可能只填了姓名手机号(手工派单、承运商司机),
* 后者按手机号解析;解析不到也不能挡住派单。
*/
class MasterOrderDriverIdTest {
private static final String DRIVER_PHONE = "13800138000";
private static final Long RESOLVED_DRIVER_ID = 9321L;
private IWaybillService waybillService;
private IDriverService driverService;
private MasterOrderServiceImpl service;
@BeforeEach
void setUp() {
waybillService = mock(IWaybillService.class);
driverService = mock(IDriverService.class);
service = new MasterOrderServiceImpl(waybillService, mock(ITransportPlanService.class),
mock(IProjectApplyService.class), mock(IContractManageService.class), driverService);
when(waybillService.submit(any(Waybill.class))).thenReturn(true);
}
@Test
@DisplayName("调度带了档案 ID:按它写,不再按手机号解析")
void keepsExplicitDriverId() throws Exception {
Map<String, Object> dispatch = dispatch();
dispatch.put("driverId", 7001L);
assertTrue(createWaybill(dispatch));
assertEquals(7001L, submittedWaybill().getDriverId());
verify(driverService, never()).resolveDriverIdByMobile(any(), any());
}
@Test
@DisplayName("只填了手机号:派单时解析出档案 ID 写入运单")
void resolvesDriverIdFromPhone() throws Exception {
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
assertTrue(createWaybill(dispatch()));
assertEquals(RESOLVED_DRIVER_ID, submittedWaybill().getDriverId());
}
@Test
@DisplayName("解析不到档案(承运商司机):运单照常提交,归属留空")
void allowsDispatchWithoutProfile() throws Exception {
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
assertTrue(createWaybill(dispatch()));
assertNull(submittedWaybill().getDriverId());
verify(waybillService).submit(any(Waybill.class));
}
// ------------------------------------------------------------ 夹具
private boolean createWaybill(Map<String, Object> dispatch) throws Exception {
MasterOrder masterOrder = new MasterOrder();
masterOrder.setId(1L);
masterOrder.setMasterNo("ZD-20261027-001");
masterOrder.setProjectId(2101L);
masterOrder.setProjectName("测试项目");
Method method = MasterOrderServiceImpl.class
.getDeclaredMethod("createWaybill", MasterOrder.class, List.class);
method.setAccessible(true);
return (boolean) method.invoke(service, masterOrder, List.of(dispatch));
}
private Map<String, Object> dispatch() {
Map<String, Object> dispatch = new LinkedHashMap<>();
dispatch.put("documentType", "运单");
dispatch.put("transportType", "公路运输");
dispatch.put("carrierType", "自运");
dispatch.put("driverName", "张三");
dispatch.put("driverPhone", DRIVER_PHONE);
dispatch.put("vehicleNo", "桂A12345");
dispatch.put("quantity", 1);
dispatch.put("unitPrice", 100);
return dispatch;
}
private Waybill submittedWaybill() {
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
verify(waybillService).submit(captor.capture());
return captor.getValue();
}
}
@@ -14,6 +14,7 @@ import org.springblade.transport.pojo.entity.TransportPlan;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
@@ -67,7 +68,8 @@ class MasterOrderProgressTest {
waybillService,
transportPlanService,
mock(IProjectApplyService.class),
mock(IContractManageService.class));
mock(IContractManageService.class),
mock(IDriverService.class));
inject("baseMapper", masterOrderMapper);
when(transportPlanService.list(anyWrapper())).thenReturn(List.of());
@@ -0,0 +1,121 @@
package org.springblade.transport.service.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springblade.system.feign.ISysClient;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 批量导入运单时写司机档案 ID 的测试。
* <p>
* 导入模板可能带档案 ID(老系统导出),也可能只有司机姓名手机号。
* 解析不到档案时只留行级 WARN,该行照常导入(承运商司机本来就没有小程序档案)。
*/
class WaybillImportDriverIdTest {
private static final String DRIVER_PHONE = "13800138000";
private static final Long RESOLVED_DRIVER_ID = 9321L;
private IDriverService driverService;
private WaybillImportBatchServiceImpl service;
@BeforeEach
void setUp() {
driverService = mock(IDriverService.class);
service = new WaybillImportBatchServiceImpl(
mock(IWaybillService.class),
mock(ICustomerArchiveService.class),
mock(IProjectApplyService.class),
mock(ITransportPlanService.class),
mock(IReceivablePayableDetailService.class),
mock(ILoadingManageService.class),
mock(ISysClient.class),
driverService);
}
@Test
@DisplayName("行内带了档案 ID:直接用,不再按手机号解析")
void keepsDriverIdFromRow() throws Exception {
Waybill waybill = importedWaybill(7001L);
fillDriverOwnership(waybill, 3);
assertEquals(7001L, waybill.getDriverId());
verify(driverService, never()).resolveDriverIdByMobile(any(), any());
}
@Test
@DisplayName("行内只有手机号:按手机号解析出档案 ID 写入")
void resolvesDriverIdFromRowPhone() throws Exception {
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
Waybill waybill = importedWaybill(null);
fillDriverOwnership(waybill, 3);
assertEquals(RESOLVED_DRIVER_ID, waybill.getDriverId());
}
@Test
@DisplayName("解析不到档案:该行照常导入,归属留空(只留行级 WARN)")
void keepsRowWithoutResolvedProfile() throws Exception {
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
Waybill waybill = importedWaybill(null);
assertDoesNotThrow(() -> fillDriverOwnership(waybill, 3));
assertNull(waybill.getDriverId());
}
@Test
@DisplayName("解析器返回 0(无主哨兵):按无主落空,不把 0 写进库")
void normalizesSentinelToNull() throws Exception {
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(0L);
Waybill waybill = importedWaybill(0L);
fillDriverOwnership(waybill, 3);
assertNull(waybill.getDriverId());
}
// ------------------------------------------------------------ 夹具
private void fillDriverOwnership(Waybill waybill, int rowNo) throws Exception {
Method method = WaybillImportBatchServiceImpl.class.getDeclaredMethod("fillDriverOwnership", Waybill.class, int.class);
method.setAccessible(true);
try {
method.invoke(service, waybill, rowNo);
} catch (java.lang.reflect.InvocationTargetException exception) {
throw (Exception) exception.getCause();
}
}
private Waybill importedWaybill(Long driverId) {
Waybill waybill = new Waybill();
waybill.setWaybillNo("YD-IMPORT-1");
waybill.setDriverId(driverId);
waybill.setDriverName("张三");
waybill.setDriverPhone(DRIVER_PHONE);
return waybill;
}
}
@@ -0,0 +1,173 @@
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
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.ArgumentCaptor;
import org.springblade.transport.mapper.WaybillMapper;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProcessConfigService;
import java.lang.reflect.Field;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 重新派单写司机档案 ID 的测试。
* <p>
* 派单是司机归属的唯一入口:调用方带了档案 ID(>0)就按它写,
* 只带姓名手机号(承运商司机、手工派单)就按手机号解析;解析不到也不能挡住派单。
*/
class WaybillReassignDriverIdTest {
private static final Long WAYBILL_ID = 1900000000000000001L;
private static final String TENANT_ID = "000000";
private static final String DRIVER_PHONE = "13800138000";
private static final Long RESOLVED_DRIVER_ID = 9321L;
private WaybillMapper waybillMapper;
private IDriverService driverService;
private WaybillServiceImpl service;
@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);
driverService = mock(IDriverService.class);
service = new WaybillServiceImpl();
inject("baseMapper", waybillMapper);
inject("driverService", driverService);
inject("processConfigService", processConfigService());
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(pendingWaybill());
when(waybillMapper.updateById(any(Waybill.class))).thenReturn(1);
}
@Test
@DisplayName("调用方带了档案 ID:按它写,不再按手机号解析")
void keepsExplicitDriverId() {
Waybill request = reassignRequest(7721L);
assertTrue(service.reassignWithoutDeptCheck(request));
assertEquals(7721L, capturedWaybill().getDriverId());
verify(driverService, never()).resolveDriverIdByMobile(any(), anyString());
}
@Test
@DisplayName("调用方只带手机号:按手机号解析出档案 ID 写入")
void resolvesDriverIdFromPhone() {
when(driverService.resolveDriverIdByMobile(TENANT_ID, DRIVER_PHONE)).thenReturn(RESOLVED_DRIVER_ID);
assertTrue(service.reassignWithoutDeptCheck(reassignRequest(null)));
assertEquals(RESOLVED_DRIVER_ID, capturedWaybill().getDriverId());
}
@Test
@DisplayName("档案 ID 为 0 或 -1(无主哨兵):按手机号重新解析")
void resolvesWhenDriverIdIsSentinel() throws Exception {
when(driverService.resolveDriverIdByMobile(TENANT_ID, DRIVER_PHONE)).thenReturn(RESOLVED_DRIVER_ID);
assertTrue(service.reassignWithoutDeptCheck(reassignRequest(0L)));
assertEquals(RESOLVED_DRIVER_ID, capturedWaybill().getDriverId());
setUpAgain();
assertTrue(service.reassignWithoutDeptCheck(reassignRequest(-1L)));
assertEquals(RESOLVED_DRIVER_ID, capturedWaybill().getDriverId());
}
@Test
@DisplayName("解析不到档案(承运商司机):派单照常成功,归属留空")
void allowsReassignWithoutResolvedProfile() {
when(driverService.resolveDriverIdByMobile(TENANT_ID, DRIVER_PHONE)).thenReturn(null);
assertTrue(service.reassignWithoutDeptCheck(reassignRequest(null)));
assertNull(capturedWaybill().getDriverId());
}
// ------------------------------------------------------------ 夹具
private void setUpAgain() throws Exception {
waybillMapper = mock(WaybillMapper.class);
service = new WaybillServiceImpl();
inject("baseMapper", waybillMapper);
inject("driverService", driverService);
inject("processConfigService", processConfigService());
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(pendingWaybill());
when(waybillMapper.updateById(any(Waybill.class))).thenReturn(1);
}
private IProcessConfigService processConfigService() {
IProcessConfigService processConfigService = mock(IProcessConfigService.class);
when(processConfigService.listByIds(any())).thenReturn(Collections.emptyList());
return processConfigService;
}
private Waybill pendingWaybill() {
Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID);
waybill.setIsDeleted(0);
waybill.setTenantId(TENANT_ID);
waybill.setBusinessStatus("pending");
waybill.setTransportType("road");
waybill.setDriverName("张三");
waybill.setDriverPhone("13900139000");
waybill.setVehicleNo("桂A12345");
return waybill;
}
private Waybill reassignRequest(Long driverId) {
Waybill request = new Waybill();
request.setId(WAYBILL_ID);
request.setDriverId(driverId);
request.setDriverName("李四");
request.setDriverPhone(DRIVER_PHONE);
request.setVehicleNo("桂A88888");
return request;
}
private Waybill capturedWaybill() {
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
verify(waybillMapper, org.mockito.Mockito.atLeastOnce()).updateById(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);
}
}