From 50ad99370c382337ea35def6eaabd0bc02e42007 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 27 Oct 2026 20:30:07 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8D=95=E4=B8=8E?= =?UTF-8?q?=E9=85=8D=E8=BD=BD=E5=8D=95=E5=9C=A8=E6=B4=BE=E5=8D=95=E3=80=81?= =?UTF-8?q?=E9=85=8D=E8=BD=BD=E3=80=81=E5=AF=BC=E5=85=A5=E6=97=B6=E5=86=99?= =?UTF-8?q?=E5=85=A5=E5=8F=B8=E6=9C=BA=E6=A1=A3=E6=A1=88=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/pojo/entity/LoadingManage.java | 3 + .../impl/LoadingManageServiceImpl.java | 15 ++ .../service/impl/MasterOrderServiceImpl.java | 12 +- .../impl/WaybillImportBatchServiceImpl.java | 28 ++- .../service/impl/WaybillServiceImpl.java | 14 +- .../impl/LoadingManageAdmissionTest.java | 2 + .../LoadingManageChildStatusWriteTest.java | 161 +++++++++++++++- .../service/impl/MasterOrderDriverIdTest.java | 120 ++++++++++++ .../service/impl/MasterOrderProgressTest.java | 4 +- .../impl/WaybillImportDriverIdTest.java | 121 ++++++++++++ .../impl/WaybillReassignDriverIdTest.java | 173 ++++++++++++++++++ 11 files changed, 640 insertions(+), 13 deletions(-) create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDriverIdTest.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillReassignDriverIdTest.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java index e037a882..1a1283de 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java @@ -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; 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 0777be99..13b6252e 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 @@ -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 selectLoadingManagePage(IPage page, LoadingManageVO loadingManage) { IPage entityPage = page(page, buildQuery(loadingManage)); @@ -319,6 +323,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl0 才算有主,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 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")); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java index 61604120..f78533ab 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -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 implements IWaybillImportBatchService { @@ -101,6 +104,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl 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 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 row, WaybillImportBatch batch, Long carrierContractId, boolean draft) { if (row == null) throw new ServiceException("运单数据不正确"); Waybill waybill = new Waybill(); 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 bc263482..aa3d3290 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 @@ -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 @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 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); 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 a8eccbcd..b71ce064 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 @@ -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)); } 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 10dab615..6087f36b 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 @@ -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; @@ -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.>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 captor = ArgumentCaptor.forClass(LoadingManage.class); + verify(service, atLeastOnce()).save(captor.capture()); + return captor.getValue(); + } + + private LoadingManage capturedLoading() { ArgumentCaptor 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 params, List columns) { + private static final java.util.regex.Pattern PARAM_REF = + java.util.regex.Pattern.compile("#\\{ew\\.paramNameValuePairs\\.([^}]+)}"); + @SuppressWarnings("unchecked") static CapturedUpdate of(Wrapper wrapper) { LambdaUpdateWrapper update = (LambdaUpdateWrapper) wrapper; @@ -478,8 +609,20 @@ class LoadingManageChildStatusWriteTest { return null; } + /** 只认 WHERE 段用到的参数:SET 段的 driver_id 也是 Long,混进来会当成运单 id */ List whereIds() { - return params.values().stream().filter(Long.class::isInstance).map(Long.class::cast).toList(); + Set setParamKeys = new java.util.HashSet<>(); + Matcher matcher = PARAM_REF.matcher(sqlSet); + while (matcher.find()) { + setParamKeys.add(matcher.group(1)); + } + List ids = new java.util.ArrayList<>(); + for (Map.Entry entry : params.entrySet()) { + if (entry.getValue() instanceof Long longValue && !setParamKeys.contains(entry.getKey())) { + ids.add(longValue); + } + } + return ids; } } } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java new file mode 100644 index 00000000..8872dd01 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderDriverIdTest.java @@ -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 的测试。 + *

+ * 派单请求可能来自调度(带档案 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 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 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 dispatch() { + Map 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 captor = ArgumentCaptor.forClass(Waybill.class); + verify(waybillService).submit(captor.capture()); + return captor.getValue(); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java index f7d77747..aa52fec8 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/MasterOrderProgressTest.java @@ -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()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDriverIdTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDriverIdTest.java new file mode 100644 index 00000000..d9581932 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDriverIdTest.java @@ -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 的测试。 + *

+ * 导入模板可能带档案 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; + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillReassignDriverIdTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillReassignDriverIdTest.java new file mode 100644 index 00000000..6ce47414 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillReassignDriverIdTest.java @@ -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 的测试。 + *

+ * 派单是司机归属的唯一入口:调用方带了档案 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 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); + } + +}