🐛 后台改手机号同步登录账号与司机档案电话
- 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:
+195
-1
@@ -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 掉
|
||||
*/
|
||||
|
||||
+152
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+108
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+111
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user