From 7d5fa9549456d453dafcd5df703f830d09a3714e Mon Sep 17 00:00:00 2001 From: gxwebsoft <170083662@qq.com> Date: Fri, 12 Dec 2025 14:18:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E6=89=BE?= =?UTF-8?q?=E5=9B=9E=E8=B4=A6=E5=8F=B7=E5=92=8C=E9=87=8D=E7=BD=AE=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增通过手机号查找账号接口 (/findAccountByPhone) - 新增重置密码接口 (/resetPassword),支持密码强度校验和事务处理 - 新增检查手机号是否已注册接口 (/checkPhoneRegistered) - 在UserMapper中添加根据手机号查询账号和统计数量的方法 - 在UserService中实现账号查找和密码重置相关业务逻辑 - 添加AccountInfoResult、CheckPhoneResult等返回结果类 - 添加FindAccountByPhoneParam、ResetPasswordParam等参数类 - 更新SecurityConfig,放行新增的三个公共接口 - 完善UserMapper.xml中的SQL映射,支持跨租户查询和更新操作 --- .../common/core/security/SecurityConfig.java | 3 + .../system/controller/MainController.java | 138 ++++++++++++++++++ .../common/system/mapper/UserMapper.java | 18 +++ .../common/system/mapper/xml/UserMapper.xml | 52 ++++++- .../system/param/FindAccountByPhoneParam.java | 34 +++++ .../system/param/ResetPasswordParam.java | 53 +++++++ .../system/result/AccountInfoResult.java | 40 +++++ .../system/result/CheckPhoneResult.java | 28 ++++ .../common/system/service/UserService.java | 26 ++++ .../system/service/impl/UserServiceImpl.java | 46 ++++++ 10 files changed, 434 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/common/system/param/FindAccountByPhoneParam.java create mode 100644 src/main/java/com/gxwebsoft/common/system/param/ResetPasswordParam.java create mode 100644 src/main/java/com/gxwebsoft/common/system/result/AccountInfoResult.java create mode 100644 src/main/java/com/gxwebsoft/common/system/result/CheckPhoneResult.java diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index 1bbb461..f005231 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -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/**", diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index 176bb9e..155c75e 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -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 userList = userService.findAccountsByPhone(param.getPhone()); + + if (userList == null || userList.isEmpty()) { + return fail("该手机号尚未注册任何账号"); + } + + // 转换为前端需要的格式 + List 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); + } + } diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java index 42081be..7e18c1d 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java +++ b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java @@ -72,4 +72,22 @@ public interface UserMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") User getByUserId(String userId); + /** + * 根据手机号查询所有账号(忽略租户隔离) + * + * @param phone 手机号 + * @return List + */ + @InterceptorIgnore(tenantLine = "true") + List selectAccountsByPhone(@Param("phone") String phone); + + /** + * 根据手机号统计账号数量(忽略租户隔离) + * + * @param phone 手机号 + * @return Integer + */ + @InterceptorIgnore(tenantLine = "true") + Integer countByPhone(@Param("phone") String phone); + } diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml index 00aad89..b1da35d 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml @@ -281,10 +281,32 @@ - + UPDATE sys_user - SET grade_id = #{param.gradeId} + + + grade_id = #{param.gradeId}, + + + password = #{param.password}, + + + nickname = #{param.nickname}, + + + avatar = #{param.avatar}, + + + phone = #{param.phone}, + + + email = #{param.email}, + + + status = #{param.status}, + + WHERE user_id = #{param.userId} @@ -327,15 +349,37 @@ - + + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/param/FindAccountByPhoneParam.java b/src/main/java/com/gxwebsoft/common/system/param/FindAccountByPhoneParam.java new file mode 100644 index 0000000..b5d6248 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/FindAccountByPhoneParam.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/param/ResetPasswordParam.java b/src/main/java/com/gxwebsoft/common/system/param/ResetPasswordParam.java new file mode 100644 index 0000000..0555a6d --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/ResetPasswordParam.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/result/AccountInfoResult.java b/src/main/java/com/gxwebsoft/common/system/result/AccountInfoResult.java new file mode 100644 index 0000000..14df17d --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/result/AccountInfoResult.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/result/CheckPhoneResult.java b/src/main/java/com/gxwebsoft/common/system/result/CheckPhoneResult.java new file mode 100644 index 0000000..b3519e3 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/result/CheckPhoneResult.java @@ -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; +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/UserService.java b/src/main/java/com/gxwebsoft/common/system/service/UserService.java index ad8a6f9..2b7cf31 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/UserService.java +++ b/src/main/java/com/gxwebsoft/common/system/service/UserService.java @@ -129,4 +129,30 @@ public interface UserService extends IService, UserDetailsService { List pageAll(UserParam param); User getByUserId(String userId); + + /** + * 根据手机号查询所有账号(忽略租户隔离) + * + * @param phone 手机号 + * @return List + */ + List 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); } diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java index cf57903..dd99a96 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java @@ -366,4 +366,50 @@ public class UserServiceImpl extends ServiceImpl implements Us .eq(Organization::getPark, param.getPark()) ); } + + @Override + public List 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; + } }