🐛 修复小程序重新授权因空账号判空失败报 500 且 openid 无法绑定
system 侧 R.data(null) 被空值序列化写成 {},Feign 侧反序列化出 id 为空的
MpUser 而非 null,被当作「已查到」:换号撞键分支误置 openidOccupied,导致
openid 永不绑定并提示「该微信已绑定其他账号」;手机号查不到账号时还会以
空主键、空租户调用 bindOpenId,系统侧判缺参 400,再被 Feign 降级包成
AssertionError(Error 而非 Exception)逃出 catch (Exception),登录返回 500。
- 新增 presentAccount():以主键判定是否命中,findByOpenid/findByPhone 统一走它
- bindOpenid 入口加守卫:缺主键或租户只记 WARN 后跳过,不再发空参数请求
- 补 2 条单测:空对象 data 视为未命中;不落空主键账号、不发空主键绑定
This commit is contained in:
@@ -144,7 +144,7 @@ public class MpLoginSupport {
|
||||
}
|
||||
try {
|
||||
R<MpUser> result = mpUserClient.userByOpenid(tenantId, openid);
|
||||
return isSuccess(result) ? result.getData() : null;
|
||||
return presentAccount(isSuccess(result) ? result.getData() : null);
|
||||
} catch (Exception exception) {
|
||||
// 查询失败按未命中处理,随后由 ensureUser 触达系统服务并兜住建号失败
|
||||
log.warn("按 openid 查询小程序账号失败:租户={} openid={}", tenantId, openid, exception);
|
||||
@@ -155,13 +155,25 @@ public class MpLoginSupport {
|
||||
private MpUser findByPhone(String tenantId, String phone) {
|
||||
try {
|
||||
R<MpUser> result = mpUserClient.userByPhone(tenantId, phone);
|
||||
return isSuccess(result) ? result.getData() : null;
|
||||
return presentAccount(isSuccess(result) ? result.getData() : null);
|
||||
} catch (Exception exception) {
|
||||
log.warn("按手机号查询小程序账号失败:租户={} 手机号={}", tenantId, phone, exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端「查无此账号」不等于客户端拿到 null
|
||||
* <p>
|
||||
* 服务端 R.data(null) 落 HTTP 时被 null 空值序列化写成 {}(BladeBeanSerializerModifier 对
|
||||
* Object 属性 null 输出空对象),Feign 侧于是反序列化出一个 id 为空的 MpUser 实例而不是 null。
|
||||
* 调用方只判 null,会把「没查到」当成「查到了空账号」,越过 ensureUser 建号并带着空主键去改库。
|
||||
* 故一律以主键判定是否命中。
|
||||
*/
|
||||
private static MpUser presentAccount(MpUser account) {
|
||||
return account == null || account.getId() == null ? null : account;
|
||||
}
|
||||
|
||||
/**
|
||||
* ② 查找或创建账号:登录路径上唯一允许让登录失败的动作
|
||||
*/
|
||||
@@ -267,6 +279,12 @@ public class MpLoginSupport {
|
||||
if (account == null || StringUtil.isBlank(openid)) {
|
||||
return;
|
||||
}
|
||||
// 空主键/空租户来自「查无账号」被反序列化成空对象,绑定请求会被系统侧判为缺参 400
|
||||
if (account.getId() == null || StringUtil.isBlank(account.getTenantId())) {
|
||||
log.warn("小程序账号缺少主键或租户,跳过绑定 openid:租户={} 账号={} openid={}",
|
||||
account.getTenantId(), account.getId(), openid);
|
||||
return;
|
||||
}
|
||||
// 撞键场景 openid 已属于另一个账号,明确不迁移,也不再尝试绑定
|
||||
if (openidOccupied) {
|
||||
return;
|
||||
|
||||
@@ -229,6 +229,44 @@ class MpLoginSupportTest {
|
||||
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")));
|
||||
when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult()));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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()));
|
||||
|
||||
OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID);
|
||||
|
||||
assertEquals("62", user.getUserId());
|
||||
verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private MpUser mpUser(Long id, String phone, String openid, String identities) {
|
||||
MpUser user = new MpUser();
|
||||
user.setId(id);
|
||||
|
||||
Reference in New Issue
Block a user