From 164ef5f8e23a9f2f1da6b47111834bf9eec4771c Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Thu, 15 Oct 2026 21:02:41 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=90=AF=E7=94=A8/?= =?UTF-8?q?=E5=81=9C=E7=94=A8=E5=8F=B8=E6=9C=BA=E4=B8=8E=E5=85=B3=E8=81=94?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=97=B6=E6=B8=85=E7=A9=BA=E9=A9=BE=E9=A9=B6?= =?UTF-8?q?=E8=BD=A6=E8=BE=86=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blade_transport_driver.driving_vehicle 标了 FieldStrategy.ALWAYS,更新时忽略 null 直接写。changeStatus 与同步小程序账号的 linkDriverUserId 都是只带少数 几列的 patch,没带这一列就会把司机的车辆绑定清成 NULL,司机端车辆列表随即 变成「暂无绑定车辆」,司机在界面上无法自行恢复。 两处改为按库中原值一并带上(与认证缓存、审核写回两处的既有做法一致), 并补 DriverServiceImplTest 四个用例:撤下修复后两条断言即变红。 --- .../service/impl/DriverServiceImpl.java | 19 ++ .../service/impl/DriverServiceImplTest.java | 189 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java 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 97e7186b..825c3bc0 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 @@ -113,6 +113,8 @@ public class DriverServiceImpl extends BaseServiceImpl imp Driver driver = new Driver(); driver.setId(id); driver.setStatus(status); + // 只改 status 也必须带上 driving_vehicle 原值,理由见 carryDrivingVehicle + carryDrivingVehicle(id, driver); return updateById(driver); } @@ -446,9 +448,26 @@ public class DriverServiceImpl extends BaseServiceImpl imp Driver patch = new Driver(); patch.setId(driver.getId()); patch.setUserId(userId); + // 同上:这次只改 user_id,不带 driving_vehicle 原值同样会把车辆绑定清掉 + carryDrivingVehicle(driver.getId(), patch); updateById(patch); } + /** + * 把库里的 driving_vehicle 原值带进 patch。 + *

+ * 该列标了 {@code FieldStrategy.ALWAYS},更新时忽略 null 直接写:patch 里不带这一列, + * 一次只改别的列的更新就会把司机的车辆绑定清成 NULL,而司机端车辆列表正是按这一列 + * 的车牌匹配出来的(清空即「暂无绑定车辆」)。读不到该司机时保持原样, + * 让更新自己去失败。 + */ + private void carryDrivingVehicle(Long driverId, Driver patch) { + Driver current = getById(driverId); + if (current != null) { + patch.setDrivingVehicle(current.getDrivingVehicle()); + } + } + private Long defaultZero(Long value) { return value == null ? 0L : value; } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java new file mode 100644 index 00000000..878fa815 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java @@ -0,0 +1,189 @@ +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.mockito.MockedStatic; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.system.feign.IMpUserClient; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.pojo.entity.MpUser; +import org.springblade.transport.mapper.DriverMapper; +import org.springblade.transport.pojo.entity.Driver; + +import java.lang.reflect.Field; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 司机管理保存与状态变更测试。 + *

+ * 守住的是一条不变式:司机档案的行更新一律不得把「驾驶车辆」绑定写掉。 + * 该列标了 {@code FieldStrategy.ALWAYS},patch 里不带它就写成 NULL, + * 司机端车辆列表随即变成「暂无绑定车辆」,而司机在界面上无法自行恢复。 + */ +class DriverServiceImplTest { + + private static final Long DRIVER_ID = 9001L; + private static final Long USER_ID = 7001L; + private static final String TENANT_ID = "000000"; + private static final String PLATE = "桂A32523"; + private static final String MOBILE = "15975348524"; + + private DriverMapper driverMapper; + private IMpUserClient mpUserClient; + private ISysClient sysClient; + private DriverServiceImpl service; + + @BeforeAll + static void initTableInfo() { + // saveOrUpdate 需要 MyBatis-Plus 的实体元数据缓存 + 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); + mpUserClient = mock(IMpUserClient.class); + sysClient = mock(ISysClient.class); + service = new DriverServiceImpl(mpUserClient, sysClient); + inject("baseMapper", driverMapper); + } + + // ------------------------------------------------------------ 启用 / 停用 + + @Test + @DisplayName("启用/停用司机:patch 必须带上驾驶车辆原值,否则这次更新就把车辆绑定清成 NULL") + void changeStatusKeepsDrivingVehicle() { + when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver(PLATE)); + when(driverMapper.updateById(any(Driver.class))).thenReturn(1); + + assertTrue(service.changeStatus(DRIVER_ID, 2)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper).updateById(captor.capture()); + assertEquals(2, captor.getValue().getStatus()); + assertEquals(PLATE, captor.getValue().getDrivingVehicle(), + "只改 status 的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉"); + } + + @Test + @DisplayName("启用/停用司机:状态取值不合法时拒绝,且不写库") + void changeStatusRejectsInvalidStatus() { + assertThrows(ServiceException.class, () -> service.changeStatus(DRIVER_ID, 9)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 保存司机 + + @Test + @DisplayName("保存司机:关联小程序账号的那次 patch 也必须带上驾驶车辆原值") + void submitKeepsDrivingVehicleWhenLinkingMpUser() { + Driver driver = driver(PLATE); + // 提交前校验按身份证号与主键各查一次 + when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(driver); + when(driverMapper.selectByIdCardNoIncludingDeleted(driver.getIdCardNo())).thenReturn(null); + // 完整对象更新:saveOrUpdate 判存在走更新 + when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver); + when(driverMapper.updateById(any(Driver.class))).thenReturn(1); + // 先查不到小程序用户 → 建号 → 回查拿到新建用户 + MpUser created = new MpUser(); + created.setId(USER_ID); + when(mpUserClient.userByPhone(TENANT_ID, MOBILE)).thenReturn(null, R.data(created)); + when(mpUserClient.saveUser(any(MpUser.class))).thenReturn(R.data(true)); + + try (MockedStatic authUtil = mockStatic(AuthUtil.class)) { + authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID); + assertTrue(service.submit(driver)); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper, times(2)).updateById(captor.capture()); + // captor 存的是引用:完整对象更新带走的是 driver 本身,而它随后会被就地写上 userId, + // 所以只能用对象身份把「只带少数几列的 patch」摘出来,不能按 userId 认 + Driver linkPatch = captor.getAllValues().stream() + .filter(patch -> patch != driver) + .findFirst() + .orElseThrow(() -> new AssertionError("没有发出关联小程序账号的那次 patch")); + assertEquals(USER_ID, linkPatch.getUserId()); + assertEquals(PLATE, linkPatch.getDrivingVehicle(), + "关联账号的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉"); + } + + @Test + @DisplayName("保存司机:司机不存在时拒绝,不写库") + void submitRejectsUnknownDriver() { + Driver driver = driver(PLATE); + when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.submit(driver)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 夹具 + + private Driver driver(String drivingVehicle) { + Driver driver = new Driver(); + driver.setId(DRIVER_ID); + driver.setIsDeleted(0); + driver.setDriverName("张三"); + driver.setIdCardNo("110101199001011234"); + driver.setGender("男"); + driver.setPosts("司机"); + driver.setDrivingType("A2"); + driver.setDrivingLicenseNo("110101199001011234"); + driver.setDrivingLicenseStartDate(LocalDate.of(2020, 1, 1)); + driver.setDrivingLicenseLongTerm(1); + driver.setQualificationType("道路运输"); + driver.setQualificationNo("Q123456"); + driver.setQualificationLongTerm(1); + driver.setDriverType("自有"); + driver.setMobile(MOBILE); + driver.setOrganizationName("测试组织"); + driver.setEmergencyContactName("李四"); + driver.setEmergencyContactMobile("13800000000"); + driver.setIdCardFront("https://oss/id-front.jpg"); + driver.setIdCardBack("https://oss/id-back.jpg"); + driver.setDrivingLicenseFront("https://oss/dl-front.jpg"); + driver.setDrivingLicenseBack("https://oss/dl-back.jpg"); + driver.setDrivingVehicle(drivingVehicle); + return driver; + } + + 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); + } + +}