From 4a4b69024fc637d9fc09fb5c77c12cc3887aa03c Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 27 Oct 2026 20:29:47 +0800 Subject: [PATCH 1/5] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E5=8F=B7=E5=94=AF=E4=B8=80=E5=8C=B9=E9=85=8D=E5=8F=B8?= =?UTF-8?q?=E6=9C=BA=E6=A1=A3=E6=A1=88=20ID=20=E7=9A=84=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/service/IDriverService.java | 12 ++ .../service/impl/DriverServiceImpl.java | 26 ++++ .../service/impl/DriverResolveMobileTest.java | 130 ++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java index 73924e3e..d2962801 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverService.java @@ -111,4 +111,16 @@ public interface IDriverService extends BaseService { */ List exportDriver(Wrapper queryWrapper); + /** + * 按手机号解析司机档案 ID + *

+ * 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认, + * 0 条(承运商司机、未登录过小程序的司机)与多条(历史脏数据)都返回 null,不抛异常。 + * + * @param tenantId 租户ID,为空时取当前登录租户 + * @param mobile 手机号 + * @return 司机档案ID;无法唯一确定时为 null + */ + Long resolveDriverIdByMobile(String tenantId, String mobile); + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java index 2d4dc598..fa3a1c80 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java @@ -25,6 +25,7 @@ */ package org.springblade.transport.service.impl; +import cn.hutool.core.util.DesensitizedUtil; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -171,6 +172,31 @@ public class DriverServiceImpl extends BaseServiceImpl imp return bindExistingProfile(existing.getId(), mpUserId); } + @Override + public Long resolveDriverIdByMobile(String tenantId, String mobile) { + String resolvedTenantId = trimToNull(tenantId); + if (resolvedTenantId == null) { + resolvedTenantId = trimToNull(AuthUtil.getTenantId()); + } + String mobileSnapshot = trimToNull(mobile); + // 脱敏手机号(如 138****8000)与空号都不是可用业务数据,查不到也不该查 + if (resolvedTenantId == null || mobileSnapshot == null || mobileSnapshot.contains("*")) { + return null; + } + // 手机号会被二次放号,同租户内可能残留多条历史档案:只有唯一命中才认,不猜 + List matchedDrivers = list(Wrappers.lambdaQuery() + .select(Driver::getId) + .eq(Driver::getTenantId, resolvedTenantId) + .eq(Driver::getMobile, mobileSnapshot)); + if (matchedDrivers.size() != 1) { + log.warn("司机档案解析未唯一命中,driver_id 留空。租户={} 手机号={} 命中={} 条", + resolvedTenantId, DesensitizedUtil.mobilePhone(mobileSnapshot), matchedDrivers.size()); + return null; + } + Driver matchedDriver = matchedDrivers.get(0); + return matchedDriver == null ? null : matchedDriver.getId(); + } + @Override @Transactional(rollbackFor = Exception.class) public DriverSyncMobileVO syncMobile(String tenantId, Long mpUserId, String oldPhone, String newPhone) { diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java new file mode 100644 index 00000000..01db1969 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverResolveMobileTest.java @@ -0,0 +1,130 @@ +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.springblade.system.feign.IMpUserClient; +import org.springblade.system.feign.ISysClient; +import org.springblade.transport.mapper.DriverMapper; +import org.springblade.transport.pojo.entity.Driver; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +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.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 手机号 -> 司机档案 ID 解析入口的测试。 + *

+ * 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,0 条与多条都不认。 + * 解析结果是"能不能写归属"的判断,不是业务校验,因此任何入参都只返回 null,不抛异常。 + */ +class DriverResolveMobileTest { + + private static final String TENANT_ID = "000000"; + private static final String MOBILE = "13800138000"; + + private DriverMapper driverMapper; + private DriverServiceImpl service; + + @BeforeAll + static void initTableInfo() { + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Driver.class); + } + + @BeforeEach + void setUp() throws Exception { + driverMapper = mock(DriverMapper.class); + service = new DriverServiceImpl(mock(IMpUserClient.class), mock(ISysClient.class)); + inject("baseMapper", driverMapper); + } + + @Test + @DisplayName("租户内唯一命中:返回档案 ID") + void returnsProfileIdWhenUnique() { + when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L))); + + assertEquals(9001L, service.resolveDriverIdByMobile(TENANT_ID, MOBILE)); + } + + @Test + @DisplayName("租户内没有档案:返回 null") + void returnsNullWhenAbsent() { + when(driverMapper.selectList(any())).thenReturn(Collections.emptyList()); + + assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE)); + } + + @Test + @DisplayName("租户内多条档案(历史同号):返回 null,不猜") + void returnsNullWhenMultiple() { + List profiles = new ArrayList<>(); + profiles.add(profile(9001L)); + profiles.add(profile(9002L)); + when(driverMapper.selectList(any())).thenReturn(profiles); + + assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE)); + } + + @Test + @DisplayName("手机号为空或脱敏串:返回 null,且不查库") + void returnsNullWithoutQueryForBlankOrMaskedMobile() { + assertNull(service.resolveDriverIdByMobile(TENANT_ID, null)); + assertNull(service.resolveDriverIdByMobile(TENANT_ID, " ")); + assertNull(service.resolveDriverIdByMobile(TENANT_ID, "138****8000")); + verify(driverMapper, never()).selectList(any()); + } + + @Test + @DisplayName("租户为空:取当前登录租户,仍不抛异常") + void fallsBackToCurrentTenant() { + when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L))); + + Long driverId = null; + try (var ignored = org.mockito.Mockito.mockStatic(org.springblade.core.secure.utils.AuthUtil.class)) { + ignored.when(org.springblade.core.secure.utils.AuthUtil::getTenantId).thenReturn(TENANT_ID); + driverId = service.resolveDriverIdByMobile(null, MOBILE); + } + assertEquals(9001L, driverId); + } + + private Driver profile(Long id) { + Driver profile = new Driver(); + profile.setId(id); + profile.setIsDeleted(0); + profile.setTenantId(TENANT_ID); + profile.setMobile(MOBILE); + return profile; + } + + 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); + } + +} 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 2/5] =?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); + } + +} From 625fbc5f042e0242497d9f6e2ea3eb8b4959ca1d Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 27 Oct 2026 20:30:13 +0800 Subject: [PATCH 3/5] =?UTF-8?q?:card=5Ffile=5Fbox:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=85=8D=E8=BD=BD=E5=8D=95=E5=8F=B8=E6=9C=BA=20ID=20=E5=88=97?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E5=8D=95=E9=85=8D=E8=BD=BD=E5=8D=95=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=8F=B8=E6=9C=BA=E7=B4=A2=E5=BC=95=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blade_loading_manage_20260804.sql | 4 +- ...lade_loading_manage_driver_id_20261028.sql | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 doc/sql/transport/blade_loading_manage_driver_id_20261028.sql diff --git a/doc/sql/transport/blade_loading_manage_20260804.sql b/doc/sql/transport/blade_loading_manage_20260804.sql index ffadac6b..4d10b9f4 100644 --- a/doc/sql/transport/blade_loading_manage_20260804.sql +++ b/doc/sql/transport/blade_loading_manage_20260804.sql @@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( `trailer_vehicle_no` varchar(100) DEFAULT NULL COMMENT '挂车车牌号', `driver_name` varchar(100) DEFAULT NULL COMMENT '司机', `driver_phone` varchar(50) DEFAULT NULL COMMENT '联系电话', + `driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID', `escort_name` varchar(100) DEFAULT NULL COMMENT '押运人', `escort_phone` varchar(50) DEFAULT NULL COMMENT '押运人手机号', `carrier_type` varchar(50) DEFAULT NULL COMMENT '承运类型', @@ -50,7 +51,8 @@ CREATE TABLE IF NOT EXISTS `blade_loading_manage` ( UNIQUE KEY `uk_loading_manage_no` (`loading_no`) USING BTREE, KEY `idx_loading_manage_carrier_contract_id` (`carrier_contract_id`) USING BTREE, KEY `idx_loading_manage_dept` (`dept_id`) USING BTREE, - KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE + KEY `idx_loading_manage_create_time` (`create_time`) USING BTREE, + KEY `idx_loading_manage_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配载管理'; INSERT IGNORE INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES diff --git a/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql b/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql new file mode 100644 index 00000000..1e828a9c --- /dev/null +++ b/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql @@ -0,0 +1,80 @@ +-- ============================================================ +-- blade_loading_manage_driver_id_20261028.sql +-- ============================================================ +-- 运单/配载单写入司机档案 ID,把「归属」与「手机号快照」解耦: +-- blade_loading_manage.driver_id 配载单当前司机的司机档案 ID(blade_transport_driver.id) +-- blade_waybill.driver_id 已有列(本脚本只补索引),派单/配载/导入时写入 +-- 归属判据:driver_id > 0 才算有主;0、-1 与 NULL 一律等同「无主」(承运商司机、未登录过小程序的司机)。 +-- 历史数据不回填:手机号二次放号后按号反查会张冠李戴,旧单统一按无主处理,等下一次派单/配载再写入。 +-- 本脚本只做结构升级,不改任何业务数据;以下结构变更可重复执行。 + +-- ------------------------------------------------------------ +-- 0. 存档 +-- ------------------------------------------------------------ +-- 本脚本无数据订正,无需备份表。 + +-- ------------------------------------------------------------ +-- 1. 结构变更 +-- ------------------------------------------------------------ +-- ① blade_loading_manage.driver_id +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_loading_manage` ADD COLUMN `driver_id` bigint(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''司机ID'' AFTER `driver_phone`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_loading_manage' + AND COLUMN_NAME = 'driver_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- ② blade_loading_manage 按租户 + 司机归属检索(司机端/归属巡检) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_loading_manage` ADD KEY `idx_loading_manage_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_loading_manage' + AND INDEX_NAME = 'idx_loading_manage_tenant_driver_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- ③ blade_waybill 按租户 + 司机归属检索(列已存在,只补索引) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_waybill` ADD KEY `idx_waybill_tenant_driver_id` (`tenant_id`, `driver_id`) USING BTREE', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_waybill' + AND INDEX_NAME = 'idx_waybill_tenant_driver_id' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- ------------------------------------------------------------ +-- 2. 校验 +-- ------------------------------------------------------------ +-- 列存在(期望 1 行) +-- SELECT COUNT(*) FROM information_schema.COLUMNS +-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_loading_manage' AND COLUMN_NAME = 'driver_id'; +-- 索引存在(期望 2 行) +-- SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS +-- WHERE TABLE_SCHEMA = DATABASE() +-- AND INDEX_NAME IN ('idx_loading_manage_tenant_driver_id', 'idx_waybill_tenant_driver_id') +-- ORDER BY TABLE_NAME, SEQ_IN_INDEX; +-- 归属分布(历史单 driver_id 应为空) +-- SELECT COUNT(*) AS total, SUM(driver_id IS NOT NULL AND driver_id > 0) AS owned FROM blade_waybill; + +-- ------------------------------------------------------------ +-- 3. 回退 +-- ------------------------------------------------------------ +-- ALTER TABLE `blade_waybill` DROP INDEX `idx_waybill_tenant_driver_id`; +-- ALTER TABLE `blade_loading_manage` DROP INDEX `idx_loading_manage_tenant_driver_id`; +-- ALTER TABLE `blade_loading_manage` DROP COLUMN `driver_id`; From 9daf7e5be4e2d608a08e7610aeae6273aa5ea942 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 27 Oct 2026 20:30:13 +0800 Subject: [PATCH 4/5] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=8F=B8=E6=9C=BA=E6=94=B9=E5=8F=B7=E4=B8=8D=E5=9B=9E=E5=86=99?= =?UTF-8?q?=E5=8D=95=E6=8D=AE=E5=BF=AB=E7=85=A7=E7=9A=84=E5=AE=88=E5=8D=AB?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/service/impl/DriverSyncMobileTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java index 35568abb..16b484b9 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverSyncMobileTest.java @@ -105,6 +105,17 @@ class DriverSyncMobileTest { verify(driverMapper, never()).updateById(any(Driver.class)); } + @Test + @DisplayName("改号链路不持有运单/配载服务:换号只改档案,不回写单据上的手机号快照") + void phoneSyncDoesNotDependOnWaybillServices() { + for (Field field : DriverServiceImpl.class.getDeclaredFields()) { + String typeName = field.getType().getSimpleName(); + assertFalse(typeName.contains("Waybill") || typeName.contains("LoadingManage"), + "改号链路引入了 " + typeName + ":换号不得顺手回写运单/配载单的司机快照," + + "否则司机档案与历史单据的归属会一起漂移"); + } + } + @Test @DisplayName("档案 mobile 与账号旧号不一致:不动档案,返回可读原因(不抛异常)") void skipsWhenProfileMobileDiffersFromOldPhone() { From 4b489c00411fc0bdea3ad576d33cb2821ea682f1 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Tue, 27 Oct 2026 20:35:49 +0800 Subject: [PATCH 5/5] =?UTF-8?q?:card=5Ffile=5Fbox:=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E9=85=8D=E8=BD=BD=E5=8D=95=E5=8F=B8=E6=9C=BA=20ID=20=E5=88=97?= =?UTF-8?q?=E4=B8=8A=E6=97=A0=E6=84=8F=E4=B9=89=E7=9A=84=20COLLATE=20?= =?UTF-8?q?=E5=AD=90=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/sql/transport/blade_loading_manage_driver_id_20261028.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql b/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql index 1e828a9c..73b762ba 100644 --- a/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql +++ b/doc/sql/transport/blade_loading_manage_driver_id_20261028.sql @@ -19,7 +19,7 @@ -- ① blade_loading_manage.driver_id SET @ddl_sql = ( SELECT IF(COUNT(*) = 0, - 'ALTER TABLE `blade_loading_manage` ADD COLUMN `driver_id` bigint(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''司机ID'' AFTER `driver_phone`', + 'ALTER TABLE `blade_loading_manage` ADD COLUMN `driver_id` bigint(20) DEFAULT NULL COMMENT ''司机ID'' AFTER `driver_phone`', 'SELECT 1') FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()