✅ 补充司机档案自动建档与绑定回归测试

This commit is contained in:
2026-10-23 15:48:57 +08:00
parent 49be9c6d52
commit 9f849f55be
@@ -0,0 +1,255 @@
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.system.feign.IMpUserClient;
import org.springblade.system.feign.ISysClient;
import org.springblade.transport.mapper.DriverMapper;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
import org.springframework.dao.DuplicateKeyException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
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.verify;
import static org.mockito.Mockito.when;
/**
* 登录自动建档与绑定已有档案的测试。
* <p>
* 守住三条不变式:按 user_id 幂等重入不重复建档、同号冲突一律不绑定也不新建、
* 绑定那次 patch 必须带上 driving_vehicle 原值(该列 {@code FieldStrategy.ALWAYS}
* 会把缺失的列写成 NULL,司机端的车辆绑定会整块消失)。
*/
class DriverEnsureProfileTest {
private static final Long MP_USER_ID = 7001L;
private static final Long OTHER_USER_ID = 7002L;
private static final Long DRIVER_ID = 9001L;
private static final Long NEW_DRIVER_ID = 9002L;
private static final String TENANT_ID = "000000";
private static final String MOBILE = "15975348524";
private static final String PLATE = "桂A32523";
private DriverMapper driverMapper;
private DriverServiceImpl service;
@BeforeAll
static void initTableInfo() {
// save 走 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);
IMpUserClient mpUserClient = mock(IMpUserClient.class);
ISysClient sysClient = mock(ISysClient.class);
service = new DriverServiceImpl(mpUserClient, sysClient);
inject("baseMapper", driverMapper);
}
// ------------------------------------------------------------ 新建占位档案
@Test
@DisplayName("无账号档案、无同号档案:按票 01 的占位值新建,created=true")
void createsPlaceholderProfile() {
// 第一次按 user_id 查、第二次按手机号查,都没有
when(driverMapper.selectList(any())).thenReturn(Collections.emptyList(), Collections.emptyList());
when(driverMapper.insert(any(Driver.class))).thenAnswer(invocation -> {
Driver inserted = invocation.getArgument(0);
inserted.setId(NEW_DRIVER_ID);
return 1;
});
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertTrue(result.getCreated());
assertFalse(result.getBoundExisting());
assertNull(result.getMessage());
assertEquals(NEW_DRIVER_ID, result.getDriverId());
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
verify(driverMapper).insert(captor.capture());
Driver saved = captor.getValue();
assertEquals(TENANT_ID, saved.getTenantId(), "tenant_id 必须显式写入且非空");
assertEquals(MP_USER_ID, saved.getUserId());
assertEquals(MOBILE, saved.getMobile());
assertEquals("司机8524", saved.getDriverName());
assertNull(saved.getIdCardNo(), "身份证号留 NULL 才是「未采集」,也是补全判据");
assertEquals("", saved.getDrivingType());
assertEquals("", saved.getDriverType());
assertEquals("", saved.getOrganizationName());
assertEquals("", saved.getEmergencyContactName());
assertEquals("", saved.getEmergencyContactMobile());
assertEquals(1, saved.getStatus());
assertEquals("login_auto", saved.getProfileSource());
assertNotNull(saved.getAccountBoundTime());
verify(driverMapper, never()).updateById(any(Driver.class));
}
// ------------------------------------------------------------ 绑定已有档案
@Test
@DisplayName("命中同号未绑定的档案:绑定并留痕,patch 必须带上 driving_vehicle 原值")
void bindsExistingProfileAndKeepsDrivingVehicle() {
Driver existing = driver(DRIVER_ID, null, PLATE);
when(driverMapper.selectList(any())).thenReturn(Collections.emptyList(), List.of(existing));
when(driverMapper.selectById(DRIVER_ID)).thenReturn(existing);
when(driverMapper.updateById(any(Driver.class))).thenReturn(1);
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertFalse(result.getCreated());
assertTrue(result.getBoundExisting());
assertNull(result.getMessage());
assertEquals(DRIVER_ID, result.getDriverId());
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
verify(driverMapper).updateById(captor.capture());
Driver patch = captor.getValue();
assertEquals(MP_USER_ID, patch.getUserId());
assertEquals("login_bound", patch.getProfileSource());
assertNotNull(patch.getAccountBoundTime());
assertEquals(PLATE, patch.getDrivingVehicle(),
"绑定 patch 不带 driving_vehicle 会把司机的车辆绑定写掉");
assertNull(patch.getMobile(), "绑定不得覆盖档案上的手机号");
verify(driverMapper, never()).insert(any(Driver.class));
}
// ------------------------------------------------------------ 幂等重入
@Test
@DisplayName("按 user_id 命中:直接返回该档案,不新建、不改手机号")
void returnsExistingProfileForRepeatLogin() {
when(driverMapper.selectList(any())).thenReturn(List.of(driver(DRIVER_ID, MP_USER_ID, PLATE)));
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertFalse(result.getCreated());
assertFalse(result.getBoundExisting());
assertNull(result.getMessage());
assertEquals(DRIVER_ID, result.getDriverId());
verify(driverMapper, never()).insert(any(Driver.class));
verify(driverMapper, never()).updateById(any(Driver.class));
}
@Test
@DisplayName("并发重复建档撞唯一键:重查后按已存在的档案返回,不把异常抛给登录方")
void toleratesConcurrentInsert() {
Driver concurrent = driver(DRIVER_ID, MP_USER_ID, PLATE);
when(driverMapper.selectList(any())).thenReturn(
Collections.emptyList(),
Collections.emptyList(),
List.of(concurrent));
when(driverMapper.insert(any(Driver.class))).thenThrow(new DuplicateKeyException("uk_transport_driver_user"));
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertFalse(result.getCreated());
assertFalse(result.getBoundExisting());
assertEquals(DRIVER_ID, result.getDriverId());
}
// ------------------------------------------------------------ 冲突
@Test
@DisplayName("同手机号多条档案:拒绝绑定也不新建,返回可读原因")
void rejectsMultipleProfilesWithSameMobile() {
when(driverMapper.selectList(any())).thenReturn(
Collections.emptyList(),
List.of(driver(DRIVER_ID, null, PLATE), driver(9003L, null, "")));
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertFalse(result.getCreated());
assertFalse(result.getBoundExisting());
assertNull(result.getDriverId());
assertEquals("该手机号存在多条司机档案,请联系管理员核对", result.getMessage());
verify(driverMapper, never()).insert(any(Driver.class));
verify(driverMapper, never()).updateById(any(Driver.class));
}
@Test
@DisplayName("同手机号档案已属于其他账号:不覆盖,返回可读原因")
void rejectsMobileOwnedByAnotherAccount() {
when(driverMapper.selectList(any())).thenReturn(
Collections.emptyList(),
List.of(driver(DRIVER_ID, OTHER_USER_ID, PLATE)));
DriverEnsureProfileVO result = service.ensureProfile(TENANT_ID, MP_USER_ID, MOBILE);
assertFalse(result.getCreated());
assertFalse(result.getBoundExisting());
assertNull(result.getDriverId());
assertEquals("该手机号已被其他账号占用,请联系管理员", result.getMessage());
verify(driverMapper, never()).insert(any(Driver.class));
verify(driverMapper, never()).updateById(any(Driver.class));
}
// ------------------------------------------------------------ 入参兜底
@Test
@DisplayName("租户为空且当前上下文也没有租户:拒绝写库")
void rejectsBlankTenant() {
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getTenantId).thenReturn(null);
assertThrows(ServiceException.class, () -> service.ensureProfile(null, MP_USER_ID, MOBILE));
}
verify(driverMapper, never()).insert(any(Driver.class));
}
// ------------------------------------------------------------ 夹具
private Driver driver(Long id, Long userId, String drivingVehicle) {
Driver driver = new Driver();
driver.setId(id);
driver.setIsDeleted(0);
driver.setTenantId(TENANT_ID);
driver.setUserId(userId);
driver.setMobile(MOBILE);
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);
}
}