feat(user): 添加找回账号和重置密码功能
- 新增通过手机号查找账号接口 (/findAccountByPhone) - 新增重置密码接口 (/resetPassword),支持密码强度校验和事务处理 - 新增检查手机号是否已注册接口 (/checkPhoneRegistered) - 在UserMapper中添加根据手机号查询账号和统计数量的方法 - 在UserService中实现账号查找和密码重置相关业务逻辑 - 添加AccountInfoResult、CheckPhoneResult等返回结果类 - 添加FindAccountByPhoneParam、ResetPasswordParam等参数类 - 更新SecurityConfig,放行新增的三个公共接口 - 完善UserMapper.xml中的SQL映射,支持跨租户查询和更新操作
This commit is contained in:
@@ -43,6 +43,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
"/api/loginByUserId",
|
||||
"/api/register",
|
||||
"/api/superAdminRegister",
|
||||
"/api/findAccountByPhone",
|
||||
"/api/resetPassword",
|
||||
"/api/checkPhoneRegistered",
|
||||
"/api/existence",
|
||||
"/api/oss/upload",
|
||||
"/druid/**",
|
||||
|
||||
@@ -33,10 +33,14 @@ import com.gxwebsoft.common.system.entity.*;
|
||||
import com.gxwebsoft.common.system.mapper.CompanyMapper;
|
||||
import com.gxwebsoft.common.system.param.LoginParam;
|
||||
import com.gxwebsoft.common.system.param.SmsCaptchaParam;
|
||||
import com.gxwebsoft.common.system.param.FindAccountByPhoneParam;
|
||||
import com.gxwebsoft.common.system.param.ResetPasswordParam;
|
||||
import com.gxwebsoft.common.system.param.UpdatePasswordParam;
|
||||
import com.gxwebsoft.common.system.param.UserParam;
|
||||
import com.gxwebsoft.common.system.result.CaptchaResult;
|
||||
import com.gxwebsoft.common.system.result.LoginResult;
|
||||
import com.gxwebsoft.common.system.result.AccountInfoResult;
|
||||
import com.gxwebsoft.common.system.result.CheckPhoneResult;
|
||||
import com.gxwebsoft.common.system.service.*;
|
||||
import com.gxwebsoft.common.system.util.EmailTemplateUtil;
|
||||
import com.wf.captcha.SpecCaptcha;
|
||||
@@ -869,4 +873,138 @@ public class MainController extends BaseController {
|
||||
return fail("注册失败",null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过手机号查找账号(找回账号功能)
|
||||
*/
|
||||
@Operation(summary = "通过手机号查找账号")
|
||||
@PostMapping("/findAccountByPhone")
|
||||
public ApiResult<?> findAccountByPhone(@RequestBody FindAccountByPhoneParam param) {
|
||||
// 验证手机号
|
||||
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||
return fail("请输入有效的手机号码");
|
||||
}
|
||||
|
||||
// 验证短信验证码
|
||||
String key = "code:" + param.getPhone();
|
||||
String cachedCode = redisUtil.get(key);
|
||||
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||
|
||||
if (!param.getSmsCode().equals(cachedCode) && !param.getSmsCode().equals(devCode)) {
|
||||
return fail("短信验证码不正确");
|
||||
}
|
||||
|
||||
// 查询该手机号下的所有账号
|
||||
List<User> userList = userService.findAccountsByPhone(param.getPhone());
|
||||
|
||||
if (userList == null || userList.isEmpty()) {
|
||||
return fail("该手机号尚未注册任何账号");
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
List<AccountInfoResult> resultList = new java.util.ArrayList<>();
|
||||
for (User user : userList) {
|
||||
AccountInfoResult result = new AccountInfoResult();
|
||||
result.setUserId(String.valueOf(user.getUserId()));
|
||||
result.setTenantId(user.getTenantId());
|
||||
result.setTenantName(user.getTenantName());
|
||||
result.setAvatar(user.getAvatar());
|
||||
result.setCreateTime(user.getCreateTime() != null ? user.getCreateTime().toString() : "");
|
||||
result.setUsername(user.getUsername());
|
||||
resultList.add(result);
|
||||
}
|
||||
|
||||
// 验证码使用后删除(可选,根据业务需求决定是否删除)
|
||||
// redisUtil.del(key);
|
||||
|
||||
return success(resultList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码(找回密码功能)
|
||||
*/
|
||||
@Operation(summary = "重置密码")
|
||||
@PostMapping("/resetPassword")
|
||||
@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
|
||||
public ApiResult<?> resetPassword(@RequestBody ResetPasswordParam param) {
|
||||
// 验证手机号
|
||||
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||
return fail("请输入有效的手机号码");
|
||||
}
|
||||
|
||||
// 验证两次密码是否一致
|
||||
if (!param.getNewPassword().equals(param.getConfirmPassword())) {
|
||||
return fail("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 验证密码强度(前端已验证,后端再次验证)
|
||||
if (!param.getNewPassword().matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$")) {
|
||||
return fail("密码必须至少8位,且包含字母和数字");
|
||||
}
|
||||
|
||||
// 验证短信验证码
|
||||
String key = "code:" + param.getPhone();
|
||||
String cachedCode = redisUtil.get(key);
|
||||
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||
|
||||
if (!param.getSmsCode().equals(cachedCode) && !param.getSmsCode().equals(devCode)) {
|
||||
return fail("短信验证码不正确");
|
||||
}
|
||||
|
||||
// 验证用户是否存在且手机号匹配
|
||||
User user = userService.getByUserId(param.getUserId());
|
||||
if (user == null) {
|
||||
return fail("用户不存在");
|
||||
}
|
||||
|
||||
if (!param.getPhone().equals(user.getPhone())) {
|
||||
return fail("手机号与账号不匹配");
|
||||
}
|
||||
|
||||
if (!param.getTenantId().equals(user.getTenantId())) {
|
||||
return fail("租户信息不匹配");
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
boolean success = userService.resetUserPassword(param.getUserId(), param.getTenantId(), param.getNewPassword());
|
||||
|
||||
if (success) {
|
||||
// 密码重置成功后删除验证码
|
||||
redisUtil.delete(key);
|
||||
|
||||
// 记录登录日志(密码重置)
|
||||
LoginRecord record = new LoginRecord();
|
||||
record.setUsername(user.getUsername());
|
||||
record.setNickname(user.getNickname());
|
||||
record.setLoginType(5); // 5表示密码重置(需要在LoginRecord中定义常量)
|
||||
record.setComments("密码重置成功");
|
||||
record.setTenantId(user.getTenantId());
|
||||
loginRecordService.save(record);
|
||||
|
||||
return success("密码重置成功");
|
||||
} else {
|
||||
return fail("密码重置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号是否已注册(可选接口)
|
||||
*/
|
||||
@Operation(summary = "检查手机号是否已注册")
|
||||
@GetMapping("/checkPhoneRegistered")
|
||||
public ApiResult<?> checkPhoneRegistered(@RequestParam("phone") String phone) {
|
||||
// 验证手机号
|
||||
if (!CommonUtil.isValidPhoneNumber(phone)) {
|
||||
return fail("请输入有效的手机号码");
|
||||
}
|
||||
|
||||
// 统计该手机号注册的账号数量
|
||||
Integer accountCount = userService.countAccountsByPhone(phone);
|
||||
|
||||
CheckPhoneResult result = new CheckPhoneResult();
|
||||
result.setIsRegistered(accountCount > 0);
|
||||
result.setAccountCount(accountCount);
|
||||
|
||||
return success(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,4 +72,22 @@ public interface UserMapper extends BaseMapper<User> {
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
User getByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据手机号查询所有账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return List<User>
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
List<User> selectAccountsByPhone(@Param("phone") String phone);
|
||||
|
||||
/**
|
||||
* 根据手机号统计账号数量(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return Integer
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
Integer countByPhone(@Param("phone") String phone);
|
||||
|
||||
}
|
||||
|
||||
@@ -281,10 +281,32 @@
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- 更新用户信息 -->
|
||||
<!-- 更新用户信息(忽略租户隔离) -->
|
||||
<update id="updateByUserId">
|
||||
UPDATE sys_user
|
||||
SET grade_id = #{param.gradeId}
|
||||
<set>
|
||||
<if test="param.gradeId != null">
|
||||
grade_id = #{param.gradeId},
|
||||
</if>
|
||||
<if test="param.password != null">
|
||||
password = #{param.password},
|
||||
</if>
|
||||
<if test="param.nickname != null">
|
||||
nickname = #{param.nickname},
|
||||
</if>
|
||||
<if test="param.avatar != null">
|
||||
avatar = #{param.avatar},
|
||||
</if>
|
||||
<if test="param.phone != null">
|
||||
phone = #{param.phone},
|
||||
</if>
|
||||
<if test="param.email != null">
|
||||
email = #{param.email},
|
||||
</if>
|
||||
<if test="param.status != null">
|
||||
status = #{param.status},
|
||||
</if>
|
||||
</set>
|
||||
WHERE user_id = #{param.userId}
|
||||
</update>
|
||||
|
||||
@@ -327,15 +349,37 @@
|
||||
</select>
|
||||
|
||||
|
||||
<!-- 查询超级管理员 -->
|
||||
<!-- 根据用户ID查询(忽略租户隔离) -->
|
||||
<select id="getByUserId" resultType="com.gxwebsoft.common.system.entity.User">
|
||||
SELECT a.*
|
||||
FROM sys_user a
|
||||
<where>
|
||||
AND a.user_id = #{userId}
|
||||
AND a.is_admin = 1
|
||||
AND a.deleted = 0
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- 根据手机号查询所有账号(忽略租户隔离,用于找回密码) -->
|
||||
<select id="selectAccountsByPhone" resultType="com.gxwebsoft.common.system.entity.User">
|
||||
SELECT a.user_id,
|
||||
a.tenant_id,
|
||||
a.username,
|
||||
a.avatar,
|
||||
a.create_time,
|
||||
t.tenant_name
|
||||
FROM sys_user a
|
||||
LEFT JOIN sys_tenant t ON a.tenant_id = t.tenant_id
|
||||
WHERE a.deleted = 0
|
||||
AND a.phone = #{phone}
|
||||
ORDER BY a.create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 根据手机号统计账号数量(忽略租户隔离) -->
|
||||
<select id="countByPhone" resultType="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM sys_user
|
||||
WHERE deleted = 0
|
||||
AND phone = #{phone}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.gxwebsoft.common.system.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通过手机号查找账号参数
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2025-12-12
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "通过手机号查找账号参数")
|
||||
public class FindAccountByPhoneParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Schema(description = "手机号码", required = true)
|
||||
private String phone;
|
||||
|
||||
@NotBlank(message = "短信验证码不能为空")
|
||||
@Schema(description = "短信验证码", required = true)
|
||||
private String smsCode;
|
||||
|
||||
@NotNull(message = "模板ID不能为空")
|
||||
@Schema(description = "短信模板ID", required = true)
|
||||
private Integer templateId;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.gxwebsoft.common.system.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 重置密码参数
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2025-12-12
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "重置密码参数")
|
||||
public class ResetPasswordParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Schema(description = "手机号码", required = true)
|
||||
private String phone;
|
||||
|
||||
@NotBlank(message = "短信验证码不能为空")
|
||||
@Schema(description = "短信验证码", required = true)
|
||||
private String smsCode;
|
||||
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID", required = true)
|
||||
private String userId;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
@Schema(description = "租户ID", required = true)
|
||||
private Integer tenantId;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$",
|
||||
message = "密码必须至少8位,且包含字母和数字")
|
||||
@Schema(description = "新密码(至少8位,包含字母和数字)", required = true)
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
@Schema(description = "确认密码", required = true)
|
||||
private String confirmPassword;
|
||||
|
||||
@NotNull(message = "模板ID不能为空")
|
||||
@Schema(description = "短信模板ID", required = true)
|
||||
private Integer templateId;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gxwebsoft.common.system.result;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 账号信息返回结果
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2025-12-12
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "账号信息返回结果")
|
||||
public class AccountInfoResult implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "租户名称")
|
||||
private String tenantName;
|
||||
|
||||
@Schema(description = "用户头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private String createTime;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.gxwebsoft.common.system.result;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 检查手机号返回结果
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2025-12-12
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检查手机号返回结果")
|
||||
public class CheckPhoneResult implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "是否已注册")
|
||||
private Boolean isRegistered;
|
||||
|
||||
@Schema(description = "账号数量")
|
||||
private Integer accountCount;
|
||||
}
|
||||
@@ -129,4 +129,30 @@ public interface UserService extends IService<User>, UserDetailsService {
|
||||
List<User> pageAll(UserParam param);
|
||||
|
||||
User getByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据手机号查询所有账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return List<User>
|
||||
*/
|
||||
List<User> findAccountsByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 根据手机号统计账号数量
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return Integer
|
||||
*/
|
||||
Integer countAccountsByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param newPassword 新密码(明文)
|
||||
* @return boolean
|
||||
*/
|
||||
boolean resetUserPassword(String userId, Integer tenantId, String newPassword);
|
||||
}
|
||||
|
||||
@@ -366,4 +366,50 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
.eq(Organization::getPark, param.getPark())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> findAccountsByPhone(String phone) {
|
||||
if (StrUtil.isBlank(phone)) {
|
||||
return null;
|
||||
}
|
||||
return baseMapper.selectAccountsByPhone(phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer countAccountsByPhone(String phone) {
|
||||
if (StrUtil.isBlank(phone)) {
|
||||
return 0;
|
||||
}
|
||||
return baseMapper.countByPhone(phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
|
||||
public boolean resetUserPassword(String userId, Integer tenantId, String newPassword) {
|
||||
if (StrUtil.isBlank(userId) || tenantId == null || StrUtil.isBlank(newPassword)) {
|
||||
throw new BusinessException("参数不能为空");
|
||||
}
|
||||
|
||||
// 先查询用户是否存在(忽略租户隔离)
|
||||
User existingUser = baseMapper.getByUserId(userId);
|
||||
if (existingUser == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 验证租户ID是否匹配
|
||||
if (!tenantId.equals(existingUser.getTenantId())) {
|
||||
throw new BusinessException("租户信息不匹配");
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
String encodedPassword = encodePassword(newPassword);
|
||||
|
||||
// 使用跨租户更新方法
|
||||
User updateUser = new User();
|
||||
updateUser.setUserId(Integer.parseInt(userId));
|
||||
updateUser.setPassword(encodedPassword);
|
||||
baseMapper.updateByUserId(updateUser);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user