✨ 新增手机号唯一匹配司机档案 ID 的解析入口

This commit is contained in:
2026-10-27 20:29:47 +08:00
parent 4b9e117c9e
commit 4a4b69024f
3 changed files with 168 additions and 0 deletions
@@ -111,4 +111,16 @@ public interface IDriverService extends BaseService<Driver> {
*/
List<DriverExcel> exportDriver(Wrapper<Driver> queryWrapper);
/**
* 按手机号解析司机档案 ID
* <p>
* 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,
* 0 条(承运商司机、未登录过小程序的司机)与多条(历史脏数据)都返回 null,不抛异常。
*
* @param tenantId 租户ID,为空时取当前登录租户
* @param mobile 手机号
* @return 司机档案ID;无法唯一确定时为 null
*/
Long resolveDriverIdByMobile(String tenantId, String mobile);
}
@@ -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<DriverMapper, Driver> 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<Driver> matchedDrivers = list(Wrappers.<Driver>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) {
@@ -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 解析入口的测试。
* <p>
* 归属主键是档案 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<Driver> 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);
}
}