🐛 后台改手机号同步登录账号与司机档案电话

- transport 新增 feign /feign/client/driver/sync-mobile:按 user_id+tenant_id 找档案,
  档案 mobile 等于账号旧号时只改 mobile 一列(带 driving_vehicle 原值),已是新号幂等成功,
  找不到或判据不满足返回 synced=false 与原因
- blade-system updateUserWithPhoneSync:账号等于旧号时跟随新号、补账号唯一性校验,
  本地写库后调 feign 同步档案;同步失败只记日志,不回滚账号更新。登录链路仍走 updateUser
- App 内改号改调 updateUserWithPhoneSync
- bindOpenId 改为覆盖写:不再拦截已绑定其他 openid 或 openid 属于其它账号
This commit is contained in:
2026-10-26 15:19:33 +08:00
parent ddf0d7b2cf
commit 5cc5dbb002
18 changed files with 1433 additions and 17 deletions
@@ -19,6 +19,7 @@ import org.springblade.system.mapper.MpUserMapper;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserDataScope;
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
import org.springblade.system.support.MpUserPhoneChangeSupport;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -60,10 +61,15 @@ class MpUserServiceImplTest {
private static final Long SCOPE_DEPT_B = 1002L;
private static final String TENANT_ID = "000000";
private static final String PHONE = "15975348524";
private static final String NEW_PHONE = "13800138000";
private static final String CUSTOM_ACCOUNT = "driver-001";
private static final String LEGACY_ACCOUNT = "15577122728";
private static final Long OTHER_USER_ID = 7002L;
private static final String DEPT_ID = "1123598817739906";
private MpUserMapper mpUserMapper;
private MpUserDataScopeMapper mpUserDataScopeMapper;
private MpUserPhoneChangeSupport phoneChangeSupport;
private MpUserServiceImpl service;
@BeforeAll
@@ -80,7 +86,8 @@ class MpUserServiceImplTest {
void setUp() throws Exception {
mpUserMapper = mock(MpUserMapper.class);
mpUserDataScopeMapper = mock(MpUserDataScopeMapper.class);
service = new MpUserServiceImpl(mpUserDataScopeMapper);
phoneChangeSupport = mock(MpUserPhoneChangeSupport.class);
service = new MpUserServiceImpl(mpUserDataScopeMapper, phoneChangeSupport);
inject("baseMapper", mpUserMapper);
}
@@ -317,8 +324,195 @@ class MpUserServiceImplTest {
assertTrue(scope.isAllDept());
}
// ------------------------------------------------------------ 改号三处一致(票 05 场景 6)
@Test
@DisplayName("后台改号(account 就是旧号):手机号、登录账号、司机档案电话三处一致")
void updateUserWithPhoneSyncChangesPhoneAccountAndProfile() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
MpUser user = phoneChangeUser(NEW_PHONE);
// 前端编辑页会把库里账号原样回传;回传值仍等于旧号时不当作自定义登录名
user.setAccount(PHONE);
assertTrue(updateUserWithPhoneSync(user));
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserMapper).updateById(captor.capture());
assertEquals(NEW_PHONE, captor.getValue().getPhone());
assertEquals(NEW_PHONE, captor.getValue().getAccount(), "账号等于旧号时跟随新号,否则新号 + 原密码登不进去");
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
@Test
@DisplayName("后台改号(自定义登录名):账号原样保留,档案电话照常同步")
void updateUserWithPhoneSyncKeepsCustomAccount() {
MpUser dbUser = dbUser();
dbUser.setAccount(CUSTOM_ACCOUNT);
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser);
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
MpUser user = phoneChangeUser(NEW_PHONE);
user.setAccount(CUSTOM_ACCOUNT);
assertTrue(updateUserWithPhoneSync(user));
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserMapper).updateById(captor.capture());
assertEquals(NEW_PHONE, captor.getValue().getPhone());
assertEquals(CUSTOM_ACCOUNT, captor.getValue().getAccount(), "自定义登录名不参与改号");
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
@Test
@DisplayName("存量反例 account(15577122728) 与 phone 不同:改号只动手机号与档案,账号保持原值")
void updateUserWithPhoneSyncKeepsLegacyAccount() {
MpUser dbUser = dbUser();
dbUser.setAccount(LEGACY_ACCOUNT);
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser);
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
MpUser user = phoneChangeUser(NEW_PHONE);
user.setAccount(LEGACY_ACCOUNT);
assertTrue(updateUserWithPhoneSync(user));
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserMapper).updateById(captor.capture());
assertEquals(LEGACY_ACCOUNT, captor.getValue().getAccount(), "只有 account 等于旧号时才跟随");
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
@Test
@DisplayName("登录等非改号入口(updateUser):手机号变了也不调档案同步,避免 transport↔system 回环")
void updateUserDoesNotSyncDriverProfile() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
assertTrue(updateUser(phoneChangeUser(NEW_PHONE)));
verify(phoneChangeSupport, never()).syncDriverMobile(any(), any(), any(), any());
}
@Test
@DisplayName("手机号回传脱敏值:视为没改号,账号不动也不调档案同步")
void maskedPhoneIsTreatedAsUnchanged() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
assertTrue(updateUserWithPhoneSync(phoneChangeUser("138****8000")));
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserMapper).updateById(captor.capture());
assertEquals(PHONE, captor.getValue().getPhone());
assertEquals(PHONE, captor.getValue().getAccount());
verify(phoneChangeSupport, never()).syncDriverMobile(any(), any(), any(), any());
}
@Test
@DisplayName("历史行没有 account:改号时账号写成新号,与 prepare 的默认值同口径")
void emptyAccountFollowsNewPhone() {
MpUser dbUser = dbUser();
dbUser.setAccount(null);
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser);
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
MpUser user = phoneChangeUser(NEW_PHONE);
user.setAccount(null);
assertTrue(updateUserWithPhoneSync(user));
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserMapper).updateById(captor.capture());
assertEquals(NEW_PHONE, captor.getValue().getAccount());
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
@Test
@DisplayName("新手机号已属于别的账号:拒绝且不调档案同步")
void rejectsPhoneOccupiedByAnotherUser() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.selectOne(any(), anyBoolean())).thenReturn(otherUser());
MpUser user = phoneChangeUser(NEW_PHONE);
ServiceException exception = assertThrows(ServiceException.class, () -> updateUserWithPhoneSync(user));
assertEquals("该手机号已存在小程序用户", exception.getMessage());
verify(mpUserMapper, never()).updateById(any(MpUser.class));
verify(phoneChangeSupport, never()).syncDriverMobile(any(), any(), any(), any());
}
@Test
@DisplayName("账号跟随新号后与别的账号的登录名撞车:拒绝(account 只有普通索引,库不拦重)")
void rejectsAccountOccupiedByAnotherUser() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
// 第一次查手机号命中为空,第二次查账号命中别人
when(mpUserMapper.selectOne(any(), anyBoolean())).thenReturn(null, otherUser());
MpUser user = phoneChangeUser(NEW_PHONE);
ServiceException exception = assertThrows(ServiceException.class, () -> updateUserWithPhoneSync(user));
assertEquals("该账号已存在小程序用户", exception.getMessage());
verify(mpUserMapper, never()).updateById(any(MpUser.class));
verify(phoneChangeSupport, never()).syncDriverMobile(any(), any(), any(), any());
}
@Test
@DisplayName("档案同步失败:账号更新照常成功,不抛异常不回滚")
void profileSyncFailureDoesNotFailUpdate() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
when(phoneChangeSupport.syncDriverMobile(any(), any(), any(), any())).thenReturn(false);
MpUser user = phoneChangeUser(NEW_PHONE);
assertTrue(updateUserWithPhoneSync(user));
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
@Test
@DisplayName("后台保存入口(submitOrUpdate):已有账号走带档案同步的更新")
void submitOrUpdateSyncsProfileForExistingUser() {
when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
try (MockedStatic<MpUserCache> ignored = mockStatic(MpUserCache.class)) {
assertTrue(service.submitOrUpdate(phoneChangeUser(NEW_PHONE)));
}
verify(phoneChangeSupport).syncDriverMobile(TENANT_ID, USER_ID, PHONE, NEW_PHONE);
}
// ------------------------------------------------------------ 夹具
/**
* 改号入口(带司机档案同步)成功后会清缓存;缓存实现依赖 Spring 容器,无容器时静态 mock 掉
*/
private boolean updateUserWithPhoneSync(MpUser user) {
try (MockedStatic<MpUserCache> ignored = mockStatic(MpUserCache.class)) {
return service.updateUserWithPhoneSync(user);
}
}
private MpUser phoneChangeUser(String phone) {
MpUser user = new MpUser();
user.setId(USER_ID);
user.setTenantId(TENANT_ID);
user.setPhone(phone);
user.setIdentities("driver");
return user;
}
private MpUser otherUser() {
MpUser user = new MpUser();
user.setId(OTHER_USER_ID);
user.setTenantId(TENANT_ID);
user.setPhone(PHONE);
user.setAccount(PHONE);
return user;
}
/**
* updateUser 成功后会清缓存;缓存实现依赖 Spring 容器,无容器时静态 mock 掉
*/
@@ -0,0 +1,152 @@
package org.springblade.system.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.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.resource.feign.ISmsClient;
import org.springblade.system.cache.MpUserCache;
import org.springblade.system.pojo.dto.PhoneChangeDTO;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.service.IMpUserService;
import org.springblade.system.utils.MpUserAuth;
import static org.junit.jupiter.api.Assertions.assertEquals;
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;
/**
* App 内改号(票 05 场景 6 的小程序侧入口)。
* <p>
* 只验证「交给 updateUser 之前」的三处一致性判据:手机号是新号、账号是否跟随取库里的旧号、
* 以及必须走带司机档案同步的更新入口(否则档案电话会停在旧号)。
* 登录态与缓存均为静态工具,无 Spring 容器时需要 mock。
*/
class UserPhoneServiceImplTest {
private static final Long USER_ID = 7001L;
private static final String TENANT_ID = "000000";
private static final String OLD_PHONE = "15975348524";
private static final String NEW_PHONE = "13800138000";
private static final String CUSTOM_ACCOUNT = "driver-001";
private static final String VERIFIED_KEY = "blade:mp-user:phone:change:verified:" + USER_ID;
private IMpUserService mpUserService;
private ISmsClient smsClient;
private BladeRedis bladeRedis;
private UserPhoneServiceImpl service;
@BeforeEach
void setUp() {
mpUserService = mock(IMpUserService.class);
smsClient = mock(ISmsClient.class);
bladeRedis = mock(BladeRedis.class);
service = new UserPhoneServiceImpl(mpUserService, smsClient, bladeRedis);
}
@Test
@DisplayName("App 改号(account 就是旧号):手机号与账号都改,并走带档案同步的更新入口")
void changePhoneFollowsAccountAndSyncsDriverProfile() {
when(bladeRedis.get(VERIFIED_KEY)).thenReturn("1");
when(mpUserService.getById(USER_ID)).thenReturn(currentUser(OLD_PHONE));
when(mpUserService.count(any())).thenReturn(0L);
when(smsClient.validateMessage(any(), any(), any(), any(), any())).thenReturn(R.data(true));
when(mpUserService.updateUserWithPhoneSync(any())).thenReturn(true);
assertTrue(changePhone());
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserService).updateUserWithPhoneSync(captor.capture());
assertEquals(NEW_PHONE, captor.getValue().getPhone());
assertEquals(NEW_PHONE, captor.getValue().getAccount(), "账号等于旧号时跟随新号,否则短信登录用不了");
verify(mpUserService, never()).updateUser(any());
}
@Test
@DisplayName("App 改号(自定义登录名):账号不动,仍走带档案同步的更新入口")
void changePhoneKeepsCustomAccount() {
when(bladeRedis.get(VERIFIED_KEY)).thenReturn("1");
when(mpUserService.getById(USER_ID)).thenReturn(currentUser(CUSTOM_ACCOUNT));
when(mpUserService.count(any())).thenReturn(0L);
when(smsClient.validateMessage(any(), any(), any(), any(), any())).thenReturn(R.data(true));
when(mpUserService.updateUserWithPhoneSync(any())).thenReturn(true);
assertTrue(changePhone());
ArgumentCaptor<MpUser> captor = ArgumentCaptor.forClass(MpUser.class);
verify(mpUserService).updateUserWithPhoneSync(captor.capture());
assertEquals(NEW_PHONE, captor.getValue().getPhone());
assertEquals(CUSTOM_ACCOUNT, captor.getValue().getAccount());
verify(mpUserService, times(1)).count(any());
}
@Test
@DisplayName("未完成原手机号验证:拒绝,不调更新")
void rejectsWithoutOldPhoneVerification() {
when(bladeRedis.get(VERIFIED_KEY)).thenReturn(null);
ServiceException exception = assertThrows(ServiceException.class, this::changePhone);
assertEquals("请先完成原手机号验证", exception.getMessage());
verify(mpUserService, never()).updateUserWithPhoneSync(any());
}
@Test
@DisplayName("新手机号与当前号相同:拒绝,不调更新")
void rejectsSamePhone() {
PhoneChangeDTO change = change();
change.setNewPhone(OLD_PHONE);
when(bladeRedis.get(VERIFIED_KEY)).thenReturn("1");
when(mpUserService.getById(USER_ID)).thenReturn(currentUser(OLD_PHONE));
ServiceException exception = assertThrows(ServiceException.class, () -> changePhone(change));
assertEquals("新手机号不可与当前手机号相同", exception.getMessage());
verify(mpUserService, never()).updateUserWithPhoneSync(any());
}
private boolean changePhone() {
return changePhone(change());
}
private boolean changePhone(PhoneChangeDTO change) {
try (MockedStatic<MpUserCache> ignoredCache = mockStatic(MpUserCache.class);
MockedStatic<AuthUtil> ignoredAuth = mockStatic(AuthUtil.class);
MockedStatic<MpUserAuth> ignoredMpUserAuth = mockStatic(MpUserAuth.class)) {
ignoredAuth.when(AuthUtil::getUserId).thenReturn(USER_ID);
ignoredAuth.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
ignoredMpUserAuth.when(MpUserAuth::isMpUser).thenReturn(true);
return service.changePhone(change);
}
}
private PhoneChangeDTO change() {
PhoneChangeDTO change = new PhoneChangeDTO();
change.setId("sms-1");
change.setCode("123456");
change.setNewPhone(NEW_PHONE);
return change;
}
private MpUser currentUser(String account) {
MpUser user = new MpUser();
user.setId(USER_ID);
user.setTenantId(TENANT_ID);
user.setPhone(OLD_PHONE);
user.setAccount(account);
user.setIdentities("driver");
user.setStatus(1);
return user;
}
}
@@ -0,0 +1,108 @@
package org.springblade.system.support;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 「后台 / App 改号时账号该不该跟着改」的判据测试。
* <p>
* 纯函数,不起 Spring 容器、不连库。判据基准只有一条:库里的 account 是否等于旧手机号。
* 存量数据反例(account=15577122728 / phone=15577122721)在最后两条用例里。
*/
class MpPhoneChangePlanTest {
private static final String OLD_PHONE = "15975348524";
private static final String NEW_PHONE = "13800138000";
private static final String LEGACY_ACCOUNT = "15577122728";
private static final String CUSTOM_ACCOUNT = "driver-001";
@Test
@DisplayName("账号就是旧号:手机号变化时账号跟随新号")
void accountFollowsPhoneWhenAccountWasOldPhone() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, OLD_PHONE, NEW_PHONE, OLD_PHONE);
assertTrue(plan.isPhoneChanged());
assertTrue(plan.shouldSyncAccount());
assertEquals(NEW_PHONE, plan.nextAccount());
assertTrue(plan.isAccountChanging());
}
@Test
@DisplayName("自定义登录名:手机号变化时账号不动")
void customAccountStaysWhenPhoneChanges() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, CUSTOM_ACCOUNT, NEW_PHONE, CUSTOM_ACCOUNT);
assertTrue(plan.isPhoneChanged());
assertFalse(plan.shouldSyncAccount());
assertEquals(CUSTOM_ACCOUNT, plan.nextAccount());
assertFalse(plan.isAccountChanging());
}
@Test
@DisplayName("存量反例 account(15577122728) ≠ phone(15577122721):改号时账号不动")
void legacyAccountDifferentFromPhoneStays() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, LEGACY_ACCOUNT, NEW_PHONE, LEGACY_ACCOUNT);
assertFalse(plan.shouldSyncAccount());
assertEquals(LEGACY_ACCOUNT, plan.nextAccount());
assertFalse(plan.isAccountChanging());
}
@Test
@DisplayName("手机号没变(脱敏值回填后仍是库值):不动账号")
void keepsAccountWhenPhoneUnchanged() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, OLD_PHONE, OLD_PHONE, OLD_PHONE);
assertFalse(plan.isPhoneChanged());
assertFalse(plan.shouldSyncAccount());
assertFalse(plan.isAccountChanging());
assertEquals(OLD_PHONE, plan.nextAccount());
}
@Test
@DisplayName("手机号或提交的账号含脱敏星号:不落库、不跟随")
void maskedValueNeverTouchesAccount() {
MpPhoneChangePlan maskedPhone = MpPhoneChangePlan.of(OLD_PHONE, OLD_PHONE, "138****8000", OLD_PHONE);
assertFalse(maskedPhone.isPhoneChanged());
assertFalse(maskedPhone.shouldSyncAccount());
assertEquals(OLD_PHONE, maskedPhone.nextAccount());
MpPhoneChangePlan maskedAccount = MpPhoneChangePlan.of(OLD_PHONE, CUSTOM_ACCOUNT, NEW_PHONE, "dri***001");
assertTrue(maskedAccount.isMasked());
assertEquals(CUSTOM_ACCOUNT, maskedAccount.nextAccount());
}
@Test
@DisplayName("管理员显式提交了另一个账号名:按提交值落库")
void explicitAccountRenameWins() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, OLD_PHONE, NEW_PHONE, "admin-002");
assertEquals("admin-002", plan.nextAccount());
assertTrue(plan.isAccountChanging());
}
@Test
@DisplayName("没有账号(历史行):改号时账号写成新号,与 prepare 的默认值同口径")
void emptyAccountFollowsPhone() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(OLD_PHONE, null, NEW_PHONE, null);
assertTrue(plan.shouldSyncAccount());
assertEquals(NEW_PHONE, plan.nextAccount());
assertTrue(plan.isAccountChanging());
}
@Test
@DisplayName("比较前 trim:前后空格不影响「账号就是旧号」的判定")
void trimsBeforeComparing() {
MpPhoneChangePlan plan = MpPhoneChangePlan.of(" " + OLD_PHONE + " ", OLD_PHONE + " ", " " + NEW_PHONE, OLD_PHONE);
assertTrue(plan.isPhoneChanged());
assertTrue(plan.shouldSyncAccount());
assertEquals(NEW_PHONE, plan.nextAccount());
}
}
@@ -0,0 +1,111 @@
package org.springblade.system.support;
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.core.tool.api.R;
import org.springblade.transport.feign.IDriverClient;
import org.springblade.transport.pojo.dto.DriverSyncMobileDTO;
import org.springblade.transport.pojo.vo.DriverSyncMobileVO;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 改号后同步司机档案手机号的出口测试。
* <p>
* 这里只测「调用与失败处理」:feign 结果无论成功、判据不满足、RPC 失败还是抛异常,
* 都不能让调用方的事务失败(账号更新已经落库,档案同步是补偿动作)。
*/
class MpUserPhoneChangeSupportTest {
private static final String TENANT_ID = "000000";
private static final Long USER_ID = 7001L;
private static final Long DRIVER_ID = 9001L;
private static final String OLD_PHONE = "15975348524";
private static final String NEW_PHONE = "13800138000";
private IDriverClient driverClient;
private MpUserPhoneChangeSupport support;
@BeforeEach
void setUp() {
driverClient = mock(IDriverClient.class);
support = new MpUserPhoneChangeSupport(driverClient);
}
@Test
@DisplayName("档案同步成功:返回 true,入参是租户 + 账号 id + 旧新手机号")
void passesAccountIdentityAndPhonesToDriverClient() {
when(driverClient.syncMobile(any())).thenReturn(R.data(DriverSyncMobileVO.synced(DRIVER_ID)));
assertTrue(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE));
ArgumentCaptor<DriverSyncMobileDTO> captor = ArgumentCaptor.forClass(DriverSyncMobileDTO.class);
verify(driverClient).syncMobile(captor.capture());
assertEquals(TENANT_ID, captor.getValue().getTenantId());
assertEquals(USER_ID, captor.getValue().getMpUserId());
assertEquals(OLD_PHONE, captor.getValue().getOldPhone());
assertEquals(NEW_PHONE, captor.getValue().getNewPhone());
}
@Test
@DisplayName("档案不属于该账号或手机号对不上:synced=false 只记日志,返回 false 不抛")
void returnsFalseWhenDriverProfileNotSynced() {
when(driverClient.syncMobile(any()))
.thenReturn(R.data(DriverSyncMobileVO.notSynced(null, "未找到该账号的司机档案")));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE));
}
@Test
@DisplayName("RPC 返回失败:返回 false 不抛")
void returnsFalseWhenRpcFails() {
when(driverClient.syncMobile(any())).thenReturn(R.fail("服务不可用"));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE));
}
@Test
@DisplayName("RPC 成功但没有数据或 synced 为空:按未同步处理")
void returnsFalseWhenDataOrSyncedMissing() {
when(driverClient.syncMobile(any())).thenReturn(R.data(null));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE));
DriverSyncMobileVO empty = new DriverSyncMobileVO();
empty.setDriverId(DRIVER_ID);
when(driverClient.syncMobile(any())).thenReturn(R.data(empty));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE));
}
@Test
@DisplayName("RPC 抛异常(超时 / 连不上):吞掉并返回 false,账号更新不回滚")
void swallowsExceptionAndReturnsFalse() {
when(driverClient.syncMobile(any())).thenThrow(new IllegalStateException("connect timed out"));
assertFalse(assertDoesNotThrow(
() -> support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, NEW_PHONE)));
}
@Test
@DisplayName("入参不全或新旧号相同:不发起 RPC")
void skipsRpcForIncompleteArguments() {
assertFalse(support.syncDriverMobile(TENANT_ID, null, OLD_PHONE, NEW_PHONE));
assertFalse(support.syncDriverMobile(null, USER_ID, OLD_PHONE, NEW_PHONE));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, null, NEW_PHONE));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, " "));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, OLD_PHONE, OLD_PHONE));
assertFalse(support.syncDriverMobile(TENANT_ID, USER_ID, "159****8524", NEW_PHONE));
verify(driverClient, never()).syncMobile(any());
}
}