🐛 后台改手机号同步登录账号与司机档案电话
- 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:
+7
@@ -103,6 +103,13 @@ public interface IMpUserService extends BaseService<MpUser> {
|
||||
*/
|
||||
boolean updateUser(MpUser user);
|
||||
|
||||
/**
|
||||
* 更新小程序用户,并在手机号变化时同步司机档案的 mobile
|
||||
* <p>
|
||||
* 仅供后台编辑与 App 内改号两个入口使用;档案同步失败不影响本次更新结果。
|
||||
*/
|
||||
boolean updateUserWithPhoneSync(MpUser user);
|
||||
|
||||
/**
|
||||
* 新增或修改(后台管理)
|
||||
*/
|
||||
|
||||
+44
-15
@@ -48,6 +48,8 @@ import org.springblade.system.pojo.entity.MpUserInfo;
|
||||
import org.springblade.system.pojo.enums.MpUserIdentity;
|
||||
import org.springblade.system.pojo.vo.MpUserDataScopeVO;
|
||||
import org.springblade.system.service.IMpUserService;
|
||||
import org.springblade.system.support.MpPhoneChangePlan;
|
||||
import org.springblade.system.support.MpUserPhoneChangeSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -89,6 +91,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
private static final int DATA_LEVEL_CURRENT_ONLY = 2;
|
||||
|
||||
private final MpUserDataScopeMapper mpUserDataScopeMapper;
|
||||
private final MpUserPhoneChangeSupport mpUserPhoneChangeSupport;
|
||||
|
||||
@Override
|
||||
public MpUser getByPhone(String tenantId, String phone) {
|
||||
@@ -309,6 +312,20 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateUser(MpUser user) {
|
||||
return doUpdateUser(user, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateUserWithPhoneSync(MpUser user) {
|
||||
return doUpdateUser(user, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param syncDriverPhone 是否在手机号变化后同步司机档案的 mobile
|
||||
* 仅在后台编辑与 App 内改号时开启,登录链路(feign 回调)必须为 false
|
||||
*/
|
||||
private boolean doUpdateUser(MpUser user, boolean syncDriverPhone) {
|
||||
if (user == null || Func.isEmpty(user.getId())) {
|
||||
throw new ServiceException("小程序用户主键不能为空");
|
||||
}
|
||||
@@ -330,6 +347,10 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
if (user.getDataScopeDeptIds() == null && Objects.equals(user.getDataScopeRange(), 3)) {
|
||||
user.setDataScopeDeptIds(fillScopes(dbUser).getDataScopeDeptIds());
|
||||
}
|
||||
// 改号判据只认库里的值:提交值可能是脱敏值、空串,也可能被 prepare 用手机号兜底
|
||||
String submittedAccount = trimToNull(user.getAccount());
|
||||
String oldPhone = trimToNull(dbUser.getPhone());
|
||||
String oldAccount = trimToNull(dbUser.getAccount());
|
||||
prepare(user);
|
||||
// 未改手机号(前端脱敏未提交 / 仍为脱敏值)时沿用库中原值,避免把 138****8000 写回
|
||||
if (Func.isBlank(user.getPhone()) || isMaskedPhone(user.getPhone())) {
|
||||
@@ -338,16 +359,30 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
if (Func.isBlank(user.getEmail()) || isMaskedEmail(user.getEmail())) {
|
||||
user.setEmail(dbUser.getEmail());
|
||||
}
|
||||
// 改号时账号由 plan 判定是否跟随;提交的账号为空时也用 plan 兜底,
|
||||
// 避免 prepare 拿回填前的脱敏手机号当账号
|
||||
MpPhoneChangePlan phoneChangePlan = MpPhoneChangePlan.of(oldPhone, oldAccount, user.getPhone(), submittedAccount);
|
||||
if (phoneChangePlan.isPhoneChanged() || Func.isBlank(submittedAccount)) {
|
||||
user.setAccount(phoneChangePlan.nextAccount());
|
||||
}
|
||||
validate(user);
|
||||
MpUser existed = getByPhone(user.getTenantId(), user.getPhone());
|
||||
if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) {
|
||||
throw new ServiceException("该手机号已存在小程序用户");
|
||||
}
|
||||
// account 只有普通索引,库不拦重,账号跟随手机号时在这里补一次唯一性校验
|
||||
if (phoneChangePlan.isAccountChanging()) {
|
||||
assertAccountAvailable(user.getTenantId(), user.getId(), user.getAccount());
|
||||
}
|
||||
// 后台编辑不改密码,避免空串覆盖
|
||||
user.setPassword(null);
|
||||
boolean updated = updateById(user) && submitScopes(user, rebuildScopes);
|
||||
if (updated) {
|
||||
MpUserCache.clear();
|
||||
// 同步放在本地写库之后:失败只记日志,不回滚账号更新
|
||||
if (syncDriverPhone && phoneChangePlan.isPhoneChanged()) {
|
||||
mpUserPhoneChangeSupport.syncDriverMobile(user.getTenantId(), user.getId(), oldPhone, user.getPhone());
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
@@ -356,11 +391,18 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submitOrUpdate(MpUser user) {
|
||||
if (user != null && Func.isNotEmpty(user.getId())) {
|
||||
return updateUser(user);
|
||||
return updateUserWithPhoneSync(user);
|
||||
}
|
||||
return submit(user);
|
||||
}
|
||||
|
||||
private void assertAccountAvailable(String tenantId, Long userId, String account) {
|
||||
MpUser existed = getByAccount(tenantId, account);
|
||||
if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(userId)) {
|
||||
throw new ServiceException("该账号已存在小程序用户");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeMpUser(List<Long> ids) {
|
||||
@@ -439,20 +481,7 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
|
||||
if (!tenantId.equals(user.getTenantId())) {
|
||||
throw new ServiceException("租户不匹配,无法绑定 openid");
|
||||
}
|
||||
// 目标账号已有另一个非空 openid 时不覆盖:一个微信换绑由管理员处理,
|
||||
// 登录路径直接放弃本次绑定
|
||||
if (Func.isNotBlank(user.getOpenid()) && !user.getOpenid().equals(openid)) {
|
||||
log.warn("小程序用户已绑定其他 openid,跳过绑定:租户={} 账号={} 已有openid={} 本次openid={}",
|
||||
tenantId, userId, user.getOpenid(), openid);
|
||||
return false;
|
||||
}
|
||||
MpUser byOpenId = getOne(Wrappers.<MpUser>lambdaQuery()
|
||||
.eq(MpUser::getTenantId, tenantId)
|
||||
.eq(MpUser::getOpenid, openid)
|
||||
.last("LIMIT 1"));
|
||||
if (byOpenId != null && !byOpenId.getId().equals(userId)) {
|
||||
throw new ServiceException("该微信已绑定其他小程序用户");
|
||||
}
|
||||
// openid 是该账号最近一次登录用的微信,不参与识别:同账号换微信、两个账号用同一微信都不拦截,直接覆盖
|
||||
MpUser patch = new MpUser();
|
||||
patch.setId(userId);
|
||||
patch.setOpenid(openid);
|
||||
|
||||
+2
-1
@@ -156,7 +156,8 @@ public class UserPhoneServiceImpl implements IUserPhoneService {
|
||||
} else {
|
||||
updateUser.setAccount(currentUser.getAccount());
|
||||
}
|
||||
boolean updated = mpUserService.updateUser(updateUser);
|
||||
// 走带档案同步的更新:档案归属该账号且档案手机号等于旧号时,档案 mobile 一并跟随
|
||||
boolean updated = mpUserService.updateUserWithPhoneSync(updateUser);
|
||||
if (!updated) {
|
||||
throw new ServiceException("手机号修改失败");
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.system.support;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 「改手机号时登录账号该不该跟着改」的判定
|
||||
* <p>
|
||||
* 纯 POJO:不读写库、不依赖 Spring 容器,判定全部以 trim 后比较。
|
||||
* 判据只有一条事实基准:库里的 account 是否等于旧手机号——自定义登录名不参与改号。
|
||||
* <p>
|
||||
* 另外识别前端回传的脱敏值(如 138****8000):含 * 的一律不落库。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class MpPhoneChangePlan {
|
||||
|
||||
private final String oldPhone;
|
||||
private final String oldAccount;
|
||||
private final String newPhone;
|
||||
private final String submittedAccount;
|
||||
|
||||
private MpPhoneChangePlan(String oldPhone, String oldAccount, String newPhone, String submittedAccount) {
|
||||
this.oldPhone = trimToNull(oldPhone);
|
||||
this.oldAccount = trimToNull(oldAccount);
|
||||
this.newPhone = trimToNull(newPhone);
|
||||
this.submittedAccount = trimToNull(submittedAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param oldPhone 库里的手机号(改号前的权威值)
|
||||
* @param oldAccount 库里的账号
|
||||
* @param newPhone 本次要落库的手机号(调用方已用库值回填脱敏/空值)
|
||||
* @param submittedAccount 调用方提交的账号(prepare 补默认值之前的原值)
|
||||
*/
|
||||
public static MpPhoneChangePlan of(String oldPhone, String oldAccount, String newPhone, String submittedAccount) {
|
||||
return new MpPhoneChangePlan(oldPhone, oldAccount, newPhone, submittedAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号是否真的变了:脱敏值回填后与库值相同即视为没变。
|
||||
* 含脱敏星号的值不可落库,也不当作新手机号(改号判据一律不成立)。
|
||||
*/
|
||||
public boolean isPhoneChanged() {
|
||||
if (oldPhone == null || newPhone == null || isMasked(oldPhone) || isMasked(newPhone)) {
|
||||
return false;
|
||||
}
|
||||
return !oldPhone.equals(newPhone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入参里是否含前端脱敏值(含 *),含则不可落库
|
||||
*/
|
||||
public boolean isMasked() {
|
||||
return isMasked(oldPhone) || isMasked(newPhone) || isMasked(oldAccount) || isMasked(submittedAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号是否该跟随手机号:账号当前就是旧号(或历史行为空)时跟随
|
||||
*/
|
||||
public boolean shouldSyncAccount() {
|
||||
return isPhoneChanged() && !isMasked() && (oldAccount == null || oldAccount.equals(oldPhone));
|
||||
}
|
||||
|
||||
/**
|
||||
* 本次更新应落库的账号
|
||||
* <p>
|
||||
* 管理员显式提交了另一个账号名时按提交值(前端在账号为空时会把手机号回填进 account,
|
||||
* 故「提交值等于旧号」视为未改账号)。否则账号等于旧号就跟随新号,自定义登录名保留。
|
||||
*/
|
||||
public String nextAccount() {
|
||||
if (!isPhoneChanged()) {
|
||||
return oldAccount == null ? newPhone : oldAccount;
|
||||
}
|
||||
if (submittedAccount != null && !submittedAccount.equals(oldPhone)
|
||||
&& !submittedAccount.equals(oldAccount) && !isMasked(submittedAccount)) {
|
||||
return submittedAccount;
|
||||
}
|
||||
if (oldAccount == null) {
|
||||
return newPhone;
|
||||
}
|
||||
if (oldAccount.equals(oldPhone)) {
|
||||
return newPhone;
|
||||
}
|
||||
return oldAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号是否会变化:变化时要补账号唯一性校验(account 只有普通索引,库不拦重)
|
||||
*/
|
||||
public boolean isAccountChanging() {
|
||||
return isPhoneChanged() && !Objects.equals(oldAccount, nextAccount());
|
||||
}
|
||||
|
||||
private static boolean isMasked(String value) {
|
||||
return value != null && value.contains("*");
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.system.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.feign.IDriverClient;
|
||||
import org.springblade.transport.pojo.dto.DriverSyncMobileDTO;
|
||||
import org.springblade.transport.pojo.vo.DriverSyncMobileVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 改号后同步司机档案手机号
|
||||
* <p>
|
||||
* 只在后台编辑与 App 内改号两个入口调用:登录链路上 transport 会回调本服务的 updateUser,
|
||||
* 若那里再回调用 transport,会形成 transport(持 driver 行锁) → system → transport 的嵌套更新,
|
||||
* 内层等同一行的锁直到超时。
|
||||
* <p>
|
||||
* 档案同步是账号更新之后的补偿动作:任何失败只记日志、不抛异常,账号更新照常提交。
|
||||
* 调用方据此能看出「账号改了、档案没改」。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MpUserPhoneChangeSupport {
|
||||
|
||||
private final IDriverClient driverClient;
|
||||
|
||||
/**
|
||||
* 按账号 id + 旧手机号同步司机档案的 mobile
|
||||
*
|
||||
* @return true 表示档案已同步(含本来就是新号的幂等情形)
|
||||
*/
|
||||
public boolean syncDriverMobile(String tenantId, Long userId, String oldPhone, String newPhone) {
|
||||
String tenant = Func.toStr(tenantId, "").trim();
|
||||
String oldMobile = Func.toStr(oldPhone, "").trim();
|
||||
String newMobile = Func.toStr(newPhone, "").trim();
|
||||
if (Func.isBlank(tenant) || Func.isEmpty(userId) || Func.isBlank(oldMobile) || Func.isBlank(newMobile)
|
||||
|| oldMobile.contains("*") || newMobile.contains("*") || oldMobile.equals(newMobile)) {
|
||||
return false;
|
||||
}
|
||||
DriverSyncMobileDTO param = new DriverSyncMobileDTO();
|
||||
param.setTenantId(tenant);
|
||||
param.setMpUserId(userId);
|
||||
param.setOldPhone(oldMobile);
|
||||
param.setNewPhone(newMobile);
|
||||
try {
|
||||
R<DriverSyncMobileVO> result = driverClient.syncMobile(param);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
log.error("小程序用户改号:账号已更新,司机档案同步调用失败。tenantId={} mpUserId={} oldPhone={} newPhone={} reason={}",
|
||||
tenant, userId, oldMobile, newMobile, result == null ? "无响应" : result.getMsg());
|
||||
return false;
|
||||
}
|
||||
DriverSyncMobileVO data = result.getData();
|
||||
if (data == null) {
|
||||
log.error("小程序用户改号:账号已更新,司机档案同步无返回数据。tenantId={} mpUserId={} oldPhone={} newPhone={}",
|
||||
tenant, userId, oldMobile, newMobile);
|
||||
return false;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(data.getSynced())) {
|
||||
log.warn("小程序用户改号:账号已更新,司机档案未同步。tenantId={} mpUserId={} oldPhone={} newPhone={} driverId={} reason={}",
|
||||
tenant, userId, oldMobile, newMobile, data.getDriverId(), data.getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
log.error("小程序用户改号:账号已更新,司机档案同步异常。tenantId={} mpUserId={} oldPhone={} newPhone={}",
|
||||
tenant, userId, oldMobile, newMobile, exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user