🐛 修复启用/停用司机与关联账号时清空驾驶车辆绑定

blade_transport_driver.driving_vehicle 标了 FieldStrategy.ALWAYS,更新时忽略
null 直接写。changeStatus 与同步小程序账号的 linkDriverUserId 都是只带少数
几列的 patch,没带这一列就会把司机的车辆绑定清成 NULL,司机端车辆列表随即
变成「暂无绑定车辆」,司机在界面上无法自行恢复。

两处改为按库中原值一并带上(与认证缓存、审核写回两处的既有做法一致),
并补 DriverServiceImplTest 四个用例:撤下修复后两条断言即变红。
This commit is contained in:
2026-10-15 21:02:41 +08:00
parent fe35b39f2c
commit 164ef5f8e2
2 changed files with 208 additions and 0 deletions
@@ -113,6 +113,8 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> 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<DriverMapper, Driver> 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。
* <p>
* 该列标了 {@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;
}
@@ -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;
/**
* 司机管理保存与状态变更测试。
* <p>
* 守住的是一条不变式:司机档案的行更新一律不得把「驾驶车辆」绑定写掉。
* 该列标了 {@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<Driver> 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> authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
assertTrue(service.submit(driver));
}
ArgumentCaptor<Driver> 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);
}
}