fix(auth): 修复邮箱登录SQL别名错误并支持邮箱找回密码

- UserMapper.selectAccountsByEmail 修正 FROM 别名 a 与 WHERE 中 u.is_super_admin 不匹配导致的 BadSqlGrammarException
- 去掉邮箱查询的管理员限定(邮箱已全局唯一,邮箱登录不再限管理员)
- 重置密码接口支持邮箱+邮箱验证码找回方式
This commit is contained in:
2026-07-24 20:28:31 +08:00
parent 14f49e43dd
commit 3ffd3255ee
3 changed files with 70 additions and 35 deletions

View File

@@ -1489,17 +1489,20 @@ public class MainController extends BaseController {
/** /**
* 重置密码(找回密码功能) * 重置密码(找回密码功能)
* 支持两种验证方式:手机号+短信验证码 / 邮箱+邮箱验证码
* 支持两种模式: * 支持两种模式:
* 1. 传 userId + tenantId精确重置指定账号密码 * 1. 传 userId + tenantId精确重置指定账号密码
* 2. 不传 userId通过手机号查找所有正常账号统一重置密码忘记密码场景 * 2. 不传 userId通过手机号/邮箱查找所有正常账号,统一重置密码(忘记密码场景)
*/ */
@Operation(summary = "重置密码") @Operation(summary = "重置密码")
@PostMapping("/resetPassword") @PostMapping("/resetPassword")
@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE) @Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
public ApiResult<?> resetPassword(@RequestBody ResetPasswordParam param) { public ApiResult<?> resetPassword(@RequestBody ResetPasswordParam param) {
// 验证手机 // 判断找回方式:邮箱 or 手机
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) { boolean byEmail = StrUtil.isNotBlank(param.getEmail());
return fail("请输入有效的手机号码"); boolean byPhone = StrUtil.isNotBlank(param.getPhone());
if (!byEmail && !byPhone) {
return fail("请提供手机号或邮箱");
} }
// 验证两次密码是否一致 // 验证两次密码是否一致
@@ -1512,21 +1515,42 @@ public class MainController extends BaseController {
return fail("密码必须至少8位且包含字母和数字"); return fail("密码必须至少8位且包含字母和数字");
} }
// 验证短信验证码 // ===== 验证码校验 =====
String key = "code:" + param.getPhone(); String verifyKey;
String cachedCode = redisUtil.get(key); String inputCode;
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS); if (byEmail) {
if (!CommonUtil.isValidEmail(param.getEmail())) {
if (!param.getSmsCode().equals(cachedCode) && !param.getSmsCode().equals(devCode)) { return fail("请输入有效的邮箱地址");
return fail("短信验证码不正确"); }
if (StrUtil.isBlank(param.getEmailCode())) {
return fail("邮箱验证码不能为空");
}
verifyKey = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
inputCode = param.getEmailCode();
} else {
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
return fail("请输入有效的手机号码");
}
if (StrUtil.isBlank(param.getSmsCode())) {
return fail("短信验证码不能为空");
}
verifyKey = "code:" + param.getPhone();
inputCode = param.getSmsCode();
} }
// ===== 模式判断userId 为空时按手机号查所有用户统一重置 ===== String cachedCode = redisUtil.get(verifyKey);
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
if (!inputCode.equals(cachedCode) && !inputCode.equals(devCode)) {
return fail(byEmail ? "邮箱验证码不正确" : "短信验证码不正确");
}
// ===== 模式判断userId 为空时按手机号/邮箱查所有用户统一重置 =====
if (StrUtil.isBlank(param.getUserId())) { if (StrUtil.isBlank(param.getUserId())) {
// 通过手机号查找所有用户(跨租户,忽略租户隔离) List<User> userList = byEmail
List<User> userList = userMapper.selectAccountsByPhone(param.getPhone()); ? userMapper.selectAccountsByEmail(param.getEmail())
: userMapper.selectAccountsByPhone(param.getPhone());
if (userList == null || userList.isEmpty()) { if (userList == null || userList.isEmpty()) {
return fail("该手机号未注册"); return fail(byEmail ? "该邮箱未注册" : "该手机号未注册");
} }
// 过滤掉被冻结的账号 // 过滤掉被冻结的账号
List<User> activeUsers = userList.stream() List<User> activeUsers = userList.stream()
@@ -1535,8 +1559,9 @@ public class MainController extends BaseController {
if (activeUsers.isEmpty()) { if (activeUsers.isEmpty()) {
return fail("账号已被冻结,请联系管理员"); return fail("账号已被冻结,请联系管理员");
} }
// 统一重置所有同手机号的正常账号密码 // 统一重置所有正常账号密码
String encodedPassword = userService.encodePassword(param.getNewPassword()); String encodedPassword = userService.encodePassword(param.getNewPassword());
String methodDesc = byEmail ? "通过邮箱" : "通过手机号";
for (User u : activeUsers) { for (User u : activeUsers) {
User update = new User(); User update = new User();
update.setUserId(u.getUserId()); update.setUserId(u.getUserId());
@@ -1547,24 +1572,29 @@ public class MainController extends BaseController {
record.setUsername(u.getUsername()); record.setUsername(u.getUsername());
record.setNickname(u.getNickname()); record.setNickname(u.getNickname());
record.setLoginType(5); record.setLoginType(5);
record.setComments("密码重置成功(通过手机号"); record.setComments("密码重置成功(" + methodDesc + "");
record.setTenantId(u.getTenantId()); record.setTenantId(u.getTenantId());
loginRecordService.save(record); loginRecordService.save(record);
} }
redisUtil.delete(key); redisUtil.delete(verifyKey);
return success("密码重置成功"); return success("密码重置成功");
} }
// ===== 模式1传了 userId精确重置 ===== // ===== 模式1传了 userId精确重置 =====
// 验证用户是否存在且手机号匹配
User user = userService.getByUserId(param.getUserId()); User user = userService.getByUserId(param.getUserId());
if (user == null) { if (user == null) {
return fail("用户不存在"); return fail("用户不存在");
} }
if (byEmail) {
if (!param.getEmail().equals(user.getEmail())) {
return fail("邮箱与账号不匹配");
}
} else {
if (!param.getPhone().equals(user.getPhone())) { if (!param.getPhone().equals(user.getPhone())) {
return fail("手机号与账号不匹配"); return fail("手机号与账号不匹配");
} }
}
if (param.getTenantId() != null && !param.getTenantId().equals(user.getTenantId())) { if (param.getTenantId() != null && !param.getTenantId().equals(user.getTenantId())) {
return fail("租户信息不匹配"); return fail("租户信息不匹配");
@@ -1574,18 +1604,14 @@ public class MainController extends BaseController {
boolean success = userService.resetUserPassword(param.getUserId(), user.getTenantId(), param.getNewPassword()); boolean success = userService.resetUserPassword(param.getUserId(), user.getTenantId(), param.getNewPassword());
if (success) { if (success) {
// 密码重置成功后删除验证码 redisUtil.delete(verifyKey);
redisUtil.delete(key);
// 记录登录日志(密码重置)
LoginRecord record = new LoginRecord(); LoginRecord record = new LoginRecord();
record.setUsername(user.getUsername()); record.setUsername(user.getUsername());
record.setNickname(user.getNickname()); record.setNickname(user.getNickname());
record.setLoginType(5); // 5表示密码重置需要在LoginRecord中定义常量 record.setLoginType(5);
record.setComments("密码重置成功"); record.setComments("密码重置成功");
record.setTenantId(user.getTenantId()); record.setTenantId(user.getTenantId());
loginRecordService.save(record); loginRecordService.save(record);
return success("密码重置成功"); return success("密码重置成功");
} else { } else {
return fail("密码重置失败,请稍后重试"); return fail("密码重置失败,请稍后重试");

View File

@@ -384,15 +384,20 @@
<!-- 根据邮箱查询所有账号(忽略租户隔离,用于跨租户邮箱登录) --> <!-- 根据邮箱查询所有账号(忽略租户隔离,用于跨租户邮箱登录) -->
<select id="selectAccountsByEmail" resultType="com.gxwebsoft.common.system.entity.User"> <select id="selectAccountsByEmail" resultType="com.gxwebsoft.common.system.entity.User">
SELECT a.*, SELECT a.user_id,
a.tenant_id,
a.username,
a.nickname,
a.avatar,
a.status,
a.password,
a.create_time,
t.tenant_name t.tenant_name
FROM sys_user a FROM sys_user a
LEFT JOIN sys_tenant t ON a.tenant_id = t.tenant_id LEFT JOIN sys_tenant t ON a.tenant_id = t.tenant_id
WHERE a.deleted = 0 WHERE a.deleted = 0
AND a.email = #{email} AND a.email = #{email}
AND (u.is_super_admin = 1 OR u.is_admin = 1)
ORDER BY a.create_time DESC ORDER BY a.create_time DESC
LIMIT 1
</select> </select>
<!-- 根据手机号统计账号数量(忽略租户隔离) --> <!-- 根据手机号统计账号数量(忽略租户隔离) -->

View File

@@ -21,14 +21,18 @@ import java.io.Serializable;
public class ResetPasswordParam implements Serializable { public class ResetPasswordParam implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@NotBlank(message = "手机号不能为空") @Schema(description = "手机号码(手机找回时必填)")
@Schema(description = "手机号码", required = true)
private String phone; private String phone;
@NotBlank(message = "短信验证码不能为空") @Schema(description = "短信验证码(手机找回时必填)")
@Schema(description = "短信验证码", required = true)
private String smsCode; private String smsCode;
@Schema(description = "邮箱(邮箱找回时必填)")
private String email;
@Schema(description = "邮箱验证码(邮箱找回时必填)")
private String emailCode;
@Schema(description = "用户ID未登录忘记密码场景可不传后端按手机号查用户") @Schema(description = "用户ID未登录忘记密码场景可不传后端按手机号查用户")
private String userId; private String userId;