From 8ea8d48fff5425b564ee924ca3f18ab1f8c1c6b8 Mon Sep 17 00:00:00 2001
From: weicw <594098497@qq.com>
Date: Mon, 26 Oct 2026 14:52:44 +0800
Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?=
=?UTF-8?q?=E7=99=BB=E5=BD=95=E6=94=B9=E4=B8=BA=E5=8F=AA=E6=8C=89=E6=89=8B?=
=?UTF-8?q?=E6=9C=BA=E5=8F=B7=E8=AF=86=E5=88=AB=E8=B4=A6=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
微信 openid 改为每次登录覆盖写留痕,不再参与账号查找,删除 openid 查账号、改手机号与手机号占用提示。
---
.../auth/granter/WechatMiniTokenGranter.java | 6 +-
.../auth/support/MpLoginSupport.java | 129 +------
.../auth/support/MpLoginSupportTest.java | 342 +++++++++---------
3 files changed, 199 insertions(+), 278 deletions(-)
diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java
index 9d2bb75c..91eec71e 100644
--- a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java
+++ b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java
@@ -25,8 +25,8 @@ import org.springframework.stereotype.Component;
* 微信小程序手机号一键登录。
*
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
- * 流程:换 openid + 手机号 → 由 {@link MpLoginSupport} 找到或创建账号并补齐档案
- * → 按身份发令牌(不依赖系统角色)。
+ * 流程:换 openid + 手机号 → 由 {@link MpLoginSupport} 按手机号找到或创建账号、补齐司机档案、
+ * 把 openid 覆盖写为本次值留痕 → 按身份发令牌(不依赖系统角色)。
*/
@Slf4j
@Component
@@ -79,7 +79,7 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
throw new UserInvalidException("未获取到微信手机号");
}
- // 账号不存在时自动建号;建档与绑 openid 失败不阻断登录
+ // 账号不存在时自动建号;建档与写 openid 失败不阻断登录
OAuth2User user = mpLoginSupport.ensureLoginAccount(request.getTenantId(), phone, session.getOpenid());
if (!userService.validateUser(user)) {
throw new UserInvalidException("用户身份无效,无法登录");
diff --git a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
index da5442a4..9e8154bd 100644
--- a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
+++ b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java
@@ -44,12 +44,10 @@ import org.springblade.transport.pojo.dto.DriverEnsureProfileDTO;
import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
import org.springframework.stereotype.Component;
-import java.util.Objects;
-
/**
- * 小程序登录编排:按 openid / 手机号找到或创建账号,并补齐司机档案与微信绑定。
+ * 小程序登录编排:按手机号找到或创建账号,并补齐司机档案与微信 openid 留痕。
*
- * 只有「建号失败」会让登录失败;建档与绑 openid 失败只记 WARN,不影响本次登录。
+ * 只有「建号失败」会让登录失败;建档与写 openid 失败只记 WARN,不影响本次登录。
* 本类只供登录链路调用。
*
* @author Chill
@@ -64,16 +62,11 @@ public class MpLoginSupport {
*/
public static final String DETAIL_LOGIN_NOTICE = "loginNotice";
- /**
- * 换号撞键提示:openid 属于旧账号、新手机号已属于另一账号
- */
- private static final String NOTICE_PHONE_OCCUPIED = "该微信已绑定其他账号,请联系管理员";
-
private final IMpUserClient mpUserClient;
private final IDriverClient driverClient;
/**
- * 找到或创建本次登录使用的账号,并补齐档案与 openid 绑定。
+ * 找到或创建本次登录使用的账号,并补齐档案与 openid 留痕。
*
* 返回 {@link OAuth2User}:两条登录链路都消费 OAuth2User,而 MpUserInfo
* (identities / permissions / detail)只在 system 侧组装。
@@ -87,71 +80,21 @@ public class MpLoginSupport {
if (StringUtil.isBlank(phone)) {
throw new UserInvalidException("未获取到手机号,无法登录");
}
- MpUser account;
- String loginNotice = null;
- // openid 已属于另一个账号:本次不迁移、也不再尝试绑定(与建档冲突提示区分开)
- boolean openidOccupied = false;
- // ① openid 命中即复用:openid 是稳定标识,手机号会被运营商二次放号
- MpUser openidAccount = findByOpenid(tenantId, openid);
- if (openidAccount == null) {
- // ② 未按 openid 命中:按手机号查找或创建(登录路径专用,已存在不抛异常)
+ // ① 手机号是账号唯一识别凭据:按手机号命中即复用
+ MpUser account = findByPhone(tenantId, phone);
+ if (account == null) {
+ // ② 未命中:查找或创建(登录路径专用,已存在不抛异常)
account = ensureUser(tenantId, phone, openid);
- } else {
- account = openidAccount;
- // 账号 phone 经 feign 下发时是脱敏值(MpUser.phone 标了 @Sensitive),不能拿来比较。
- // 改按「该手机号是否已有账号」判断:命中自己是同一个人、命中别人是撞键、没命中就换号。
- MpUser phoneAccount = findByPhone(tenantId, phone);
- if (phoneAccount != null && !Objects.equals(phoneAccount.getId(), openidAccount.getId())) {
- // ①-撞键:落到手机号账号,不迁移 openid
- account = phoneAccount;
- loginNotice = NOTICE_PHONE_OCCUPIED;
- openidOccupied = true;
- log.warn("微信换号撞键,登录落到手机号账号:租户={} openid账号={} 手机号账号={}",
- tenantId, openidAccount.getId(), phoneAccount.getId());
- } else if (phoneAccount == null) {
- account = movePhone(openidAccount, phone);
- if (account == null) {
- // 更新手机号失败:并发时可能是手机号刚被别的账号占用,重查一次再决定落点
- MpUser concurrent = findByPhone(tenantId, phone);
- if (concurrent != null && !Objects.equals(concurrent.getId(), openidAccount.getId())) {
- account = concurrent;
- loginNotice = NOTICE_PHONE_OCCUPIED;
- openidOccupied = true;
- log.warn("更新手机号失败后命中手机号账号:租户={} openid账号={} 手机号账号={}",
- tenantId, openidAccount.getId(), concurrent.getId());
- } else {
- // 仍然用 openid 账号登录:手机号未能更新不影响身份识别,下次登录再试
- account = openidAccount;
- }
- }
- }
}
// ③ 身份含 driver 才建档;纯 admin 不建
- String profileNotice = ensureProfile(account, phone, tenantId);
- if (StringUtil.isBlank(loginNotice)) {
- loginNotice = profileNotice;
- }
- // ④ 微信登录补绑 openid;失败只 WARN
- bindOpenid(account, openid, openidOccupied);
+ String loginNotice = ensureProfile(account, phone, tenantId);
+ // ④ 微信登录把 openid 覆盖写为本次值留痕;失败只 WARN
+ bindOpenid(account, openid);
MpUserInfo userInfo = loadUserInfo(account);
attachNotice(userInfo, loginNotice);
return TokenUtil.convertMpUser(userInfo);
}
- private MpUser findByOpenid(String tenantId, String openid) {
- if (StringUtil.isBlank(openid)) {
- return null;
- }
- try {
- R result = mpUserClient.userByOpenid(tenantId, openid);
- return presentAccount(isSuccess(result) ? result.getData() : null);
- } catch (Exception exception) {
- // 查询失败按未命中处理,随后由 ensureUser 触达系统服务并兜住建号失败
- log.warn("按 openid 查询小程序账号失败:租户={} openid={}", tenantId, openid, exception);
- return null;
- }
- }
-
private MpUser findByPhone(String tenantId, String phone) {
try {
R result = mpUserClient.userByPhone(tenantId, phone);
@@ -199,37 +142,6 @@ public class MpLoginSupport {
}
}
- /**
- * 把 openid 账号的手机号换成新号
- *
- * 只 patch phone:account 是登录名,不随手机号变;status 必须带原值,
- * system 侧 updateUser 的 prepare 会把空 status 重置成启用。
- *
- * @return 更新成功后的账号;失败返回 null,不抛异常
- */
- private MpUser movePhone(MpUser account, String phone) {
- MpUser patch = new MpUser();
- patch.setId(account.getId());
- patch.setTenantId(account.getTenantId());
- patch.setPhone(phone);
- patch.setAccount(account.getAccount());
- patch.setIdentities(account.getIdentities());
- patch.setStatus(account.getStatus());
- try {
- R result = mpUserClient.updateUser(patch);
- if (isSuccess(result) && Boolean.TRUE.equals(result.getData())) {
- account.setPhone(phone);
- return account;
- }
- log.warn("更新小程序账号手机号失败:租户={} 账号={} 新手机号={} 返回={}",
- account.getTenantId(), account.getId(), phone, result == null ? null : result.getMsg());
- } catch (Exception exception) {
- log.warn("更新小程序账号手机号失败:租户={} 账号={} 新手机号={}",
- account.getTenantId(), account.getId(), phone, exception);
- }
- return null;
- }
-
/**
* ③ 为司机身份账号补齐档案;失败或冲突只记 WARN,不阻断登录
*
@@ -273,37 +185,26 @@ public class MpLoginSupport {
}
/**
- * ④ 微信登录补绑 openid;失败只 WARN,不阻断登录
+ * ④ 微信登录把 openid 覆盖写为本次值:「该账号最近一次登录用的微信」,不参与识别;失败只 WARN
*/
- private void bindOpenid(MpUser account, String openid, boolean openidOccupied) {
+ private void bindOpenid(MpUser account, String openid) {
if (account == null || StringUtil.isBlank(openid)) {
return;
}
// 空主键/空租户来自「查无账号」被反序列化成空对象,绑定请求会被系统侧判为缺参 400
if (account.getId() == null || StringUtil.isBlank(account.getTenantId())) {
- log.warn("小程序账号缺少主键或租户,跳过绑定 openid:租户={} 账号={} openid={}",
+ log.warn("小程序账号缺少主键或租户,跳过写入 openid:租户={} 账号={} openid={}",
account.getTenantId(), account.getId(), openid);
return;
}
- // 撞键场景 openid 已属于另一个账号,明确不迁移,也不再尝试绑定
- if (openidOccupied) {
- return;
- }
- if (StringUtil.isNotBlank(account.getOpenid())) {
- if (!openid.equals(account.getOpenid())) {
- log.warn("小程序账号已有其他 openid,本次不覆盖:租户={} 账号={} 已有={} 本次={}",
- account.getTenantId(), account.getId(), account.getOpenid(), openid);
- }
- return;
- }
try {
R result = mpUserClient.bindOpenId(account.getTenantId(), account.getId(), openid);
if (!isSuccess(result) || !Boolean.TRUE.equals(result.getData())) {
- log.warn("绑定微信 openid 失败(不阻断登录):租户={} 账号={} openid={} 返回={}",
+ log.warn("写入小程序账号 openid 失败(不阻断登录):租户={} 账号={} openid={} 返回={}",
account.getTenantId(), account.getId(), openid, result == null ? null : result.getMsg());
}
} catch (Exception exception) {
- log.warn("绑定微信 openid 失败(不阻断登录):租户={} 账号={} openid={}",
+ log.warn("写入小程序账号 openid 失败(不阻断登录):租户={} 账号={} openid={}",
account.getTenantId(), account.getId(), openid, exception);
}
}
diff --git a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
index 79284906..2deb88ca 100644
--- a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
+++ b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java
@@ -19,24 +19,23 @@ import org.springblade.transport.pojo.vo.DriverEnsureProfileVO;
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.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 登录编排测试。
*
- * 保住三条不变式:建号失败才阻断登录;建档失败放行;
- * openid 换号撞键时落到手机号账号且不迁移 openid。
+ * 手机号是账号唯一识别凭据:登录只按手机号找账号,openid 只做「最近一次登录用的微信」留痕;
+ * 登录路径不改手机号、不按 openid 找账号。只有建号失败才阻断登录。
*/
class MpLoginSupportTest {
@@ -44,6 +43,7 @@ class MpLoginSupportTest {
private static final String PHONE = "13800000001";
private static final String NEW_PHONE = "13800000002";
private static final String OPENID = "wx-openid-1";
+ private static final String OTHER_OPENID = "wx-openid-2";
private IMpUserClient mpUserClient;
private IDriverClient driverClient;
@@ -57,214 +57,234 @@ class MpLoginSupportTest {
}
@Test
- @DisplayName("新手机号微信登录:建号 + 建档,登录成功且无提示")
- void newPhoneCreatesAccountAndProfile() {
- // feign 下发时 phone 是脱敏值(MpUser.phone 标了 @Sensitive),建档必须仍用登录入参的真实号
- MpUser created = mpUser(11L, "138****0001", OPENID, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
- when(mpUserClient.userInfoById(11L)).thenReturn(R.data(userInfo(created, "driver")));
+ @DisplayName("手机号命中账号:登进该账号,openid 覆盖为本次值,手机号与登录名不动")
+ void phoneHitReusesAccountAndOverwritesOpenid() {
+ // feign 下发时 phone 是脱敏值(MpUser.phone 标了 @Sensitive),登录不改它
+ MpUser existing = mpUser(11L, "138****0001", OTHER_OPENID, "driver");
+ existing.setAccount("wangwu");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(11L)).thenReturn(R.data(userInfo(existing, "driver")));
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 11L, OPENID)).thenReturn(R.data(Boolean.TRUE));
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
assertNotNull(user);
assertEquals("11", user.getUserId());
+ assertEquals("wangwu", user.getAccount());
+ assertEquals("138****0001", user.getPhone());
+ assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE));
+ verify(mpUserClient).bindOpenId(TENANT_ID, 11L, OPENID);
+ // 不再按 openid 找账号,也不再改手机号
+ verify(mpUserClient, never()).userByOpenid(anyString(), anyString());
+ verify(mpUserClient, never()).updateUser(any(MpUser.class));
+ verify(mpUserClient, never()).ensureUser(any(MpUserEnsureDTO.class));
+ }
+
+ @Test
+ @DisplayName("同一手机号两次登录、openid 不同:落在同一账号,第二次覆盖第一次")
+ void repeatedLoginOverwritesOpenid() {
+ MpUser existing = mpUser(12L, PHONE, OPENID, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(12L)).thenReturn(R.data(userInfo(existing, "driver")));
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 12L, OPENID)).thenReturn(R.data(Boolean.TRUE));
+ when(mpUserClient.bindOpenId(TENANT_ID, 12L, OTHER_OPENID)).thenReturn(R.data(Boolean.TRUE));
+
+ OAuth2User first = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
+ OAuth2User second = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OTHER_OPENID);
+
+ assertEquals("12", first.getUserId());
+ assertEquals("12", second.getUserId(), "同一手机号始终落在同一账号");
+ verify(mpUserClient).bindOpenId(TENANT_ID, 12L, OPENID);
+ verify(mpUserClient).bindOpenId(TENANT_ID, 12L, OTHER_OPENID);
+ // 账号已存在,两次登录都不该重建
+ verify(mpUserClient, never()).ensureUser(any(MpUserEnsureDTO.class));
+ }
+
+ @Test
+ @DisplayName("openid 曾属于别的账号:仍按手机号登进本账号,无换号提示、不改手机号")
+ void openidHistoryOnOtherAccountDoesNotAffectLogin() {
+ MpUser phoneAccount = mpUser(21L, PHONE, null, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(phoneAccount));
+ when(mpUserClient.userInfoById(21L)).thenReturn(R.data(userInfo(phoneAccount, "driver")));
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 21L, OPENID)).thenReturn(R.data(Boolean.TRUE));
+
+ OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
+
+ assertEquals("21", user.getUserId());
+ assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE), "换号撞键提示已不存在");
+ verify(mpUserClient, never()).userByOpenid(anyString(), anyString());
+ verify(mpUserClient).bindOpenId(TENANT_ID, 21L, OPENID);
+ verify(mpUserClient, never()).updateUser(any(MpUser.class));
+ }
+
+ @Test
+ @DisplayName("新手机号微信登录:建号 + 建档 + openid 落库,登录成功且无提示")
+ void newPhoneCreatesAccountAndProfile() {
+ MpUser created = mpUser(31L, "138****0001", OPENID, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(null));
+ when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
+ when(mpUserClient.userInfoById(31L)).thenReturn(R.data(userInfo(created, "driver")));
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 31L, OPENID)).thenReturn(R.data(Boolean.TRUE));
+
+ OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
+
+ assertNotNull(user);
+ assertEquals("31", user.getUserId());
assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE));
ArgumentCaptor ensureCaptor = ArgumentCaptor.forClass(MpUserEnsureDTO.class);
verify(mpUserClient).ensureUser(ensureCaptor.capture());
assertEquals(TENANT_ID, ensureCaptor.getValue().getTenantId());
assertEquals(PHONE, ensureCaptor.getValue().getPhone());
assertEquals(OPENID, ensureCaptor.getValue().getOpenid());
+ // 建档必须用登录入参的真实手机号,不能用被脱敏的账号 phone
ArgumentCaptor profileCaptor = ArgumentCaptor.forClass(DriverEnsureProfileDTO.class);
verify(driverClient).ensureProfile(profileCaptor.capture());
assertEquals(TENANT_ID, profileCaptor.getValue().getTenantId());
- assertEquals(11L, profileCaptor.getValue().getMpUserId());
+ assertEquals(31L, profileCaptor.getValue().getMpUserId());
assertEquals(PHONE, profileCaptor.getValue().getPhone());
- }
-
- @Test
- @DisplayName("建档抛异常:登录仍然成功")
- void profileFailureDoesNotBlockLogin() {
- MpUser created = mpUser(12L, PHONE, OPENID, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
- when(mpUserClient.userInfoById(12L)).thenReturn(R.data(userInfo(created, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class)))
- .thenThrow(new RuntimeException("transport 不可用"));
-
- OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
-
- assertNotNull(user);
- assertEquals("12", user.getUserId());
- verify(driverClient).ensureProfile(any(DriverEnsureProfileDTO.class));
- }
-
- @Test
- @DisplayName("openid 绑定失败:登录仍然成功,不抛异常")
- void bindOpenidFailureDoesNotBlockLogin() {
- MpUser existing = mpUser(13L, PHONE, null, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(existing));
- when(mpUserClient.userInfoById(13L)).thenReturn(R.data(userInfo(existing, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
- when(mpUserClient.bindOpenId(TENANT_ID, 13L, OPENID)).thenReturn(R.fail("该微信已绑定其他小程序用户"));
-
- OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
-
- assertNotNull(user);
- assertEquals("13", user.getUserId());
- verify(mpUserClient).bindOpenId(TENANT_ID, 13L, OPENID);
- }
-
- @Test
- @DisplayName("打开号码来源:微信登录时给空 openid 账号补绑 openid")
- void bindsOpenidForPhoneAccount() {
- MpUser existing = mpUser(14L, PHONE, null, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(existing));
- when(mpUserClient.userInfoById(14L)).thenReturn(R.data(userInfo(existing, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
- when(mpUserClient.bindOpenId(TENANT_ID, 14L, OPENID)).thenReturn(R.data(Boolean.TRUE));
-
- mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
-
- verify(mpUserClient).bindOpenId(TENANT_ID, 14L, OPENID);
- }
-
- @Test
- @DisplayName("openid 命中旧账号但新手机号已被别的账号占用:落到手机号账号、不迁移 openid、有提示")
- void phoneOccupiedKeepsPhoneAccount() {
- MpUser openidAccount = mpUser(21L, "13900000000", OPENID, "driver");
- MpUser phoneAccount = mpUser(22L, NEW_PHONE, null, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(openidAccount));
- when(mpUserClient.userByPhone(TENANT_ID, NEW_PHONE)).thenReturn(R.data(phoneAccount));
- when(mpUserClient.userInfoById(22L)).thenReturn(R.data(userInfo(phoneAccount, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
-
- OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, NEW_PHONE, OPENID);
-
- assertNotNull(user);
- assertEquals("22", user.getUserId(), "应落在手机号账号上");
+ verify(mpUserClient).bindOpenId(TENANT_ID, 31L, OPENID);
verify(mpUserClient, never()).updateUser(any(MpUser.class));
- verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
- assertNotNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE), "应带上可读提示");
- assertTrue(String.valueOf(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE)).contains("该微信已绑定其他账号"));
}
@Test
- @DisplayName("openid 命中且手机号不同但新号空闲:更新账号手机号")
- void openidHitWithFreePhoneUpdatesPhone() {
- MpUser openidAccount = mpUser(31L, "139****0000", OPENID, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(openidAccount));
+ @DisplayName("同一微信换手机号登录:登进新号对应的账号,旧账号不被改动")
+ void sameOpenidNewPhoneLandsOnNewPhoneAccount() {
+ MpUser created = mpUser(41L, NEW_PHONE, OPENID, "driver");
when(mpUserClient.userByPhone(TENANT_ID, NEW_PHONE)).thenReturn(R.data(null));
- when(mpUserClient.updateUser(any(MpUser.class))).thenReturn(R.data(Boolean.TRUE));
- when(mpUserClient.userInfoById(31L)).thenReturn(R.data(userInfo(openidAccount, "driver")));
+ when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
+ when(mpUserClient.userInfoById(41L)).thenReturn(R.data(userInfo(created, "driver")));
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 41L, OPENID)).thenReturn(R.data(Boolean.TRUE));
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, NEW_PHONE, OPENID);
- assertEquals("31", user.getUserId());
- ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(MpUser.class);
- verify(mpUserClient).updateUser(updateCaptor.capture());
- assertEquals(31L, updateCaptor.getValue().getId());
- assertEquals(NEW_PHONE, updateCaptor.getValue().getPhone());
- verify(mpUserClient, never()).ensureUser(any(MpUserEnsureDTO.class));
- }
-
- @Test
- @DisplayName("openid 命中且该手机号就是本账号:不重复更新手机号")
- void openidHitSameAccountDoesNotUpdatePhone() {
- MpUser openidAccount = mpUser(32L, "138****0001", OPENID, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(openidAccount));
- when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(openidAccount));
- when(mpUserClient.userInfoById(32L)).thenReturn(R.data(userInfo(openidAccount, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
-
- OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
-
- assertEquals("32", user.getUserId());
- verify(mpUserClient, never()).updateUser(any(MpUser.class));
- verify(mpUserClient, never()).ensureUser(any(MpUserEnsureDTO.class));
- }
-
- @Test
- @DisplayName("纯 admin 身份:不建司机档案")
- void adminOnlyDoesNotCreateProfile() {
- MpUser admin = mpUser(41L, PHONE, OPENID, "admin");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(admin));
- when(mpUserClient.userInfoById(41L)).thenReturn(R.data(userInfo(admin, "admin")));
-
- OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
-
assertEquals("41", user.getUserId());
- verify(driverClient, never()).ensureProfile(any(DriverEnsureProfileDTO.class));
+ ArgumentCaptor ensureCaptor = ArgumentCaptor.forClass(MpUserEnsureDTO.class);
+ verify(mpUserClient).ensureUser(ensureCaptor.capture());
+ assertEquals(NEW_PHONE, ensureCaptor.getValue().getPhone());
+ // 旧账号既不改手机号也不改 openid
+ verify(mpUserClient, never()).userByOpenid(anyString(), anyString());
+ verify(mpUserClient, never()).updateUser(any(MpUser.class));
+ verify(mpUserClient).bindOpenId(TENANT_ID, 41L, OPENID);
}
@Test
- @DisplayName("建号失败是唯一阻断登录的条件")
- void accountCreationFailureBlocksLogin() {
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(null));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.fail("创建小程序用户失败"));
-
- UserInvalidException exception = assertThrows(UserInvalidException.class,
- () -> mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID));
- assertEquals("创建小程序用户失败", exception.getMessage());
- verify(driverClient, never()).ensureProfile(any(DriverEnsureProfileDTO.class));
- }
-
- @Test
- @DisplayName("短信登录(openid 为空)不做 openid 相关调用")
- void smsLoginSkipsOpenid() {
- MpUser created = mpUser(51L, PHONE, null, "driver");
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
- when(mpUserClient.userInfoById(51L)).thenReturn(R.data(userInfo(created, "driver")));
+ @DisplayName("短信登录:不读不写 openid,也不改手机号")
+ void smsLoginDoesNotTouchOpenid() {
+ MpUser existing = mpUser(51L, PHONE, OTHER_OPENID, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(51L)).thenReturn(R.data(userInfo(existing, "driver")));
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, null);
assertEquals("51", user.getUserId());
+ assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE));
verify(mpUserClient, never()).userByOpenid(anyString(), anyString());
verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
+ verify(mpUserClient, never()).updateUser(any(MpUser.class));
+ }
+
+ @Test
+ @DisplayName("短信登录:司机档案冲突仍下发 loginNotice")
+ void smsLoginReportsProfileConflict() {
+ MpUser existing = mpUser(52L, PHONE, null, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(52L)).thenReturn(R.data(userInfo(existing, "driver")));
+ DriverEnsureProfileVO conflict = new DriverEnsureProfileVO();
+ conflict.setMessage("手机号已被其他司机档案占用");
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(conflict));
+
+ OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, null);
+
+ assertEquals("手机号已被其他司机档案占用", user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE));
+ verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
}
@Test
- @DisplayName("远端查无账号返回空对象(id 为空):按未命中建号,并用真实主键补绑 openid")
- void emptyAccountFromRemoteIsTreatedAsMiss() {
- // 服务端 R.data(null) 落 HTTP 是 {},Feign 反序列化成 id 为空的 MpUser 而非 null
- MpUser created = mpUser(61L, PHONE, null, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser()));
- when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser()));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
- when(mpUserClient.bindOpenId(TENANT_ID, 61L, OPENID)).thenReturn(R.data(Boolean.TRUE));
- when(mpUserClient.userInfoById(61L)).thenReturn(R.data(userInfo(created, "driver")));
+ @DisplayName("写 openid 失败:登录仍然成功")
+ void openidWriteFailureDoesNotBlockLogin() {
+ MpUser existing = mpUser(53L, PHONE, null, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(53L)).thenReturn(R.data(userInfo(existing, "driver")));
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 53L, OPENID)).thenThrow(new RuntimeException("system 不可用"));
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
assertNotNull(user);
- assertEquals("61", user.getUserId());
- assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE), "不该出现换号撞键提示");
- verify(mpUserClient).ensureUser(any(MpUserEnsureDTO.class));
- verify(mpUserClient, never()).updateUser(any(MpUser.class));
- verify(mpUserClient).bindOpenId(TENANT_ID, 61L, OPENID);
+ assertEquals("53", user.getUserId());
+ verify(mpUserClient).bindOpenId(TENANT_ID, 53L, OPENID);
}
@Test
- @DisplayName("两个查询都返回空对象时:不落空账号、不发起带空主键的绑定")
- void emptyAccountDoesNotLeakNullIdentityIntoBind() {
- MpUser created = mpUser(62L, PHONE, OPENID, "driver");
- when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser()));
- when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser()));
- when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
- when(mpUserClient.userInfoById(62L)).thenReturn(R.data(userInfo(created, "driver")));
- when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ @DisplayName("建档抛异常:登录仍然成功")
+ void profileFailureDoesNotBlockLogin() {
+ MpUser existing = mpUser(54L, PHONE, OPENID, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(existing));
+ when(mpUserClient.userInfoById(54L)).thenReturn(R.data(userInfo(existing, "driver")));
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class)))
+ .thenThrow(new RuntimeException("transport 不可用"));
+ when(mpUserClient.bindOpenId(TENANT_ID, 54L, OPENID)).thenReturn(R.data(Boolean.TRUE));
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
- assertEquals("62", user.getUserId());
+ assertNotNull(user);
+ assertEquals("54", user.getUserId());
+ verify(driverClient).ensureProfile(any(DriverEnsureProfileDTO.class));
+ }
+
+ @Test
+ @DisplayName("建号失败是唯一阻断登录的条件")
+ void accountCreationFailureBlocksLogin() {
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(null));
+ when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.fail("创建小程序用户失败"));
+
+ UserInvalidException exception = assertThrows(UserInvalidException.class,
+ () -> mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID));
+
+ assertEquals("创建小程序用户失败", exception.getMessage());
verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
+ verify(driverClient, never()).ensureProfile(any(DriverEnsureProfileDTO.class));
+ }
+
+ @Test
+ @DisplayName("纯 admin 身份:不建司机档案")
+ void adminOnlyDoesNotCreateProfile() {
+ MpUser admin = mpUser(61L, PHONE, OPENID, "admin");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(admin));
+ when(mpUserClient.userInfoById(61L)).thenReturn(R.data(userInfo(admin, "admin")));
+ when(mpUserClient.bindOpenId(TENANT_ID, 61L, OPENID)).thenReturn(R.data(Boolean.TRUE));
+
+ OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
+
+ assertEquals("61", user.getUserId());
+ verify(driverClient, never()).ensureProfile(any(DriverEnsureProfileDTO.class));
+ }
+
+ @Test
+ @DisplayName("远端查无账号返回空对象(id 为空):按未命中建号,用真实主键写 openid")
+ void emptyAccountFromRemoteIsTreatedAsMiss() {
+ // 服务端 R.data(null) 落 HTTP 是 {},Feign 反序列化成 id 为空的 MpUser 而非 null
+ MpUser created = mpUser(71L, PHONE, OPENID, "driver");
+ when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser()));
+ when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created));
+ when(mpUserClient.userInfoById(71L)).thenReturn(R.data(userInfo(created, "driver")));
+ when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
+ when(mpUserClient.bindOpenId(TENANT_ID, 71L, OPENID)).thenReturn(R.data(Boolean.TRUE));
+
+ OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
+
+ assertNotNull(user);
+ assertEquals("71", user.getUserId());
+ assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE));
+ verify(mpUserClient).ensureUser(any(MpUserEnsureDTO.class));
+ verify(mpUserClient, never()).updateUser(any(MpUser.class));
+ verify(mpUserClient).bindOpenId(TENANT_ID, 71L, OPENID);
}
private MpUser mpUser(Long id, String phone, String openid, String identities) {