Ver código fonte

update 新增接口:修改手机,修改密码,忘记密码

宋天 3 anos atrás
pai
commit
8e0569ee54
19 arquivos alterados com 696 adições e 18 exclusões
  1. 7 0
      yudao-user-server/pom.xml
  2. 20 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java
  3. 35 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java
  4. 8 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java
  5. 16 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java
  6. 36 3
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java
  7. 20 3
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java
  8. 3 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java
  9. 3 1
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java
  10. 19 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java
  11. 76 3
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java
  12. 12 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java
  13. 43 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java
  14. 48 0
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java
  15. 30 0
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java
  16. 59 0
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java
  17. 52 8
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java
  18. 75 0
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java
  19. 134 0
      yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java

+ 7 - 0
yudao-user-server/pom.xml

@@ -91,6 +91,13 @@
             <scope>test</scope>
         </dependency>
 
+        <!--单元测试相关-->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <!-- 工具类相关 -->
 
     </dependencies>

+ 20 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java

@@ -5,6 +5,9 @@ import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
+import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
+import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
@@ -13,10 +16,12 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import javax.validation.Valid;
 
 import java.io.IOException;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.userserver.modules.member.enums.MbrErrorCodeConstants.FILE_IS_EMPTY;
 
@@ -30,6 +35,9 @@ public class SysUserProfileController {
     @Resource
     private MbrUserService userService;
 
+    @Resource
+    private SysSmsCodeService smsCodeService;
+
     @PutMapping("/update-nickname")
     @ApiOperation("修改用户昵称")
     @PreAuthenticated
@@ -56,5 +64,17 @@ public class SysUserProfileController {
         return success(userService.getUserInfo(getLoginUserId()));
     }
 
+
+    @PostMapping("/update-mobile")
+    @ApiOperation(value = "修改用户手机")
+    @PreAuthenticated
+    public CommonResult<Boolean> updateMobile(@RequestBody @Valid MbrUserUpdateMobileReqVO reqVO) {
+        // 校验验证码
+        smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP());
+
+        userService.updateMobile(getLoginUserId(), reqVO);
+        return success(true);
+    }
+
 }
 

+ 35 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.user.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+@ApiModel("修改手机 Request VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MbrUserUpdateMobileReqVO {
+
+    @ApiModelProperty(value = "手机验证码", required = true, example = "1024")
+    @NotEmpty(message = "手机验证码不能为空")
+    @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
+    @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
+    private String code;
+
+
+    @ApiModelProperty(value = "手机号",required = true,example = "15823654487")
+    @NotBlank(message = "手机号不能为空")
+    @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位")
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
+    private String mobile;
+
+}

+ 8 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.member.service.user;
 import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
 import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
 import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
 
 import java.io.InputStream;
 
@@ -69,4 +70,11 @@ public interface MbrUserService {
      */
     MbrUserInfoRespVO getUserInfo(Long userId);
 
+    /**
+     * 修改手机
+     * @param userId 用户id
+     * @param reqVO 请求实体
+     */
+    void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO);
+
 }

+ 16 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java

@@ -6,8 +6,10 @@ import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreServic
 import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
 import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
 import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
 import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
+import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.crypto.password.PasswordEncoder;
@@ -40,6 +42,9 @@ public class MbrUserServiceImpl implements MbrUserService {
     @Resource
     private PasswordEncoder passwordEncoder;
 
+    @Resource
+    private SysAuthService sysAuthService;
+
     @Override
     public MbrUserDO getUserByMobile(String mobile) {
         return userMapper.selectByMobile(mobile);
@@ -116,6 +121,17 @@ public class MbrUserServiceImpl implements MbrUserService {
         return userResp;
     }
 
+    @Override
+    public void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO) {
+        // 检测用户是否存在
+        MbrUserDO userDO = checkUserExists(userId);
+        // 检测手机与验证码是否匹配
+        sysAuthService.checkIfMobileMatchCodeAndDeleteCode(userDO.getMobile(),reqVO.getCode());
+        // 更新用户手机
+        userDO.setMobile(reqVO.getMobile());
+        userMapper.updateById(userDO);
+    }
+
     @VisibleForTesting
     public MbrUserDO checkUserExists(Long id) {
         if (id == null) {

+ 36 - 3
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java

@@ -3,7 +3,9 @@ package cn.iocoder.yudao.userserver.modules.system.controller.auth;
 import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
+import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
 import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
 import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
 import com.alibaba.fastjson.JSON;
@@ -56,16 +58,47 @@ public class SysAuthController {
     }
 
     @PostMapping("/send-sms-code")
-    @ApiOperation("发送手机验证码")
+    @ApiOperation(value = "发送手机验证码",notes = "不检测该手机号是否已被注册")
     public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) {
         smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP());
         return success(true);
     }
 
+    @PostMapping("/send-sms-new-code")
+    @ApiOperation(value = "发送手机验证码",notes = "检测该手机号是否已被注册,用于修改手机时使用")
+    public CommonResult<Boolean> sendSmsNewCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) {
+        smsCodeService.sendSmsNewCode(reqVO);
+        return success(true);
+    }
+
+    @GetMapping("/send-sms-code-login")
+    @ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用")
+    public CommonResult<Boolean> sendSmsCodeLogin() {
+        smsCodeService.sendSmsCodeLogin(getLoginUserId());
+        return success(true);
+    }
+
     @PostMapping("/reset-password")
     @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用")
-    public CommonResult<Boolean> resetPassword(@RequestBody @Valid MbrAuthResetPasswordReqVO reqVO) {
-        return null;
+    public CommonResult<Boolean> resetPassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.resetPasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) {
+        authService.resetPassword(reqVO);
+        return success(true);
+    }
+
+    @PostMapping("/update-password")
+    @ApiOperation(value = "修改用户密码",notes = "用户修改密码时使用")
+    @PreAuthenticated
+    public CommonResult<Boolean> updatePassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.updatePasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) {
+        authService.updatePassword(getLoginUserId(), reqVO);
+        return success(true);
+    }
+
+    @PostMapping("/check-sms-code")
+    @ApiOperation(value = "校验验证码是否正确")
+    @PreAuthenticated
+    public CommonResult<Boolean> checkSmsCode(@RequestBody @Valid SysAuthSmsLoginReqVO reqVO) {
+        smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHECK_CODE_BY_SMS.getScene(),reqVO.getCode(),getClientIP());
+        return success(true);
     }
 
     // ========== 社交登录相关 ==========

+ 20 - 3
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.hibernate.validator.constraints.Length;
 
+import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.Pattern;
 
@@ -18,15 +19,31 @@ import javax.validation.constraints.Pattern;
 @Builder
 public class MbrAuthResetPasswordReqVO {
 
+    /**
+     * 修改密码校验规则
+     */
+    public interface updatePasswordValidView {
+    }
+
+    /**
+     * 忘记密码校验规则
+     */
+    public interface resetPasswordValidView {
+    }
+
+    @ApiModelProperty(value = "用户旧密码", required = true, example = "123456")
+    @NotBlank(message = "旧密码不能为空",groups = updatePasswordValidView.class)
+    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
+    private String oldPassword;
+
     @ApiModelProperty(value = "新密码", required = true, example = "buzhidao")
-    @NotEmpty(message = "新密码不能为空")
+    @NotEmpty(message = "新密码不能为空",groups = {updatePasswordValidView.class,resetPasswordValidView.class})
     @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
     private String password;
 
     @ApiModelProperty(value = "手机验证码", required = true, example = "1024")
-    @NotEmpty(message = "手机验证码不能为空")
+    @NotEmpty(message = "手机验证码不能为空",groups = resetPasswordValidView.class)
     @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
     @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
     private String code;
-
 }

+ 3 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java

@@ -23,7 +23,10 @@ public interface SysErrorCodeConstants {
     ErrorCode USER_SMS_CODE_NOT_CORRECT = new ErrorCode(1005001003, "验证码不正确");
     ErrorCode USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1005001004, "超过每日短信发送数量");
     ErrorCode USER_SMS_CODE_SEND_TOO_FAST = new ErrorCode(1005001005, "短信发送过于频率");
+    ErrorCode USER_SMS_CODE_IS_EXISTS = new ErrorCode(1005001006, "手机号已被使用");
 
     // ========== 用户模块 1005002000 ==========
     ErrorCode USER_NOT_EXISTS = new ErrorCode(1005002001, "用户不存在");
+    ErrorCode USER_CODE_FAILED = new ErrorCode(1005002002, "验证码不匹配");
+    ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1005002003, "密码校验失败");
 }

+ 3 - 1
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java

@@ -17,7 +17,9 @@ public enum SysSmsSceneEnum implements IntArrayValuable {
 
     LOGIN_BY_SMS(1, "手机号登陆"),
     CHANGE_MOBILE_BY_SMS(2, "更换手机号"),
-            ;
+    FORGET_MOBILE_BY_SMS(3, "忘记密码"),
+    CHECK_CODE_BY_SMS(4, "审核验证码"),
+    ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSmsSceneEnum::getScene).toArray();
 

+ 19 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java

@@ -63,4 +63,23 @@ public interface SysAuthService extends SecurityAuthFrameworkService {
      */
     void socialBind(Long userId, @Valid MbrAuthSocialBindReqVO reqVO);
 
+    /**
+     * 修改用户密码
+     * @param userId 用户id
+     * @param userReqVO 用户请求实体类
+     */
+    void updatePassword(Long userId, MbrAuthResetPasswordReqVO userReqVO);
+
+    /**
+     * 忘记密码
+     * @param userReqVO 用户请求实体类
+     */
+    void resetPassword(MbrAuthResetPasswordReqVO userReqVO);
+
+    /**
+     * 检测手机与验证码是否匹配
+     * @param phone 手机号
+     * @param code 验证码
+     */
+    void checkIfMobileMatchCodeAndDeleteCode(String phone,String code);
 }

+ 76 - 3
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -15,15 +15,19 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
 import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
 import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
 import cn.iocoder.yudao.userserver.modules.system.convert.auth.SysAuthConvert;
 import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
 import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
 import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthUser;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.DisabledException;
@@ -32,6 +36,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -40,6 +45,7 @@ import java.util.List;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*;
 
 /**
@@ -65,6 +71,13 @@ public class SysAuthServiceImpl implements SysAuthService {
     private SysUserSessionCoreService userSessionCoreService;
     @Resource
     private SysSocialService socialService;
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+    @Resource
+    private PasswordEncoder passwordEncoder;
+    @Resource
+    private MbrUserMapper userMapper;
+
     private static final UserTypeEnum userTypeEnum = UserTypeEnum.MEMBER;
 
     @Override
@@ -200,12 +213,12 @@ public class SysAuthServiceImpl implements SysAuthService {
         }
         reqDTO.setUsername(mobile);
         reqDTO.setUserAgent(ServletUtils.getUserAgent());
-        reqDTO.setUserIp(ServletUtils.getClientIP());
+        reqDTO.setUserIp(getClientIP());
         reqDTO.setResult(loginResult.getResult());
         loginLogCoreService.createLoginLog(reqDTO);
         // 更新最后登录时间
         if (user != null && Objects.equals(SysLoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
-            userService.updateUserLogin(user.getId(), ServletUtils.getClientIP());
+            userService.updateUserLogin(user.getId(), getClientIP());
         }
     }
 
@@ -266,6 +279,66 @@ public class SysAuthServiceImpl implements SysAuthService {
         this.createLogoutLog(loginUser.getId(), loginUser.getUsername());
     }
 
+    @Override
+    public void updatePassword(Long userId, MbrAuthResetPasswordReqVO reqVO) {
+        // 检验旧密码
+        MbrUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword());
+
+        // 更新用户密码
+        userDO.setPassword(passwordEncoder.encode(reqVO.getPassword()));
+        userMapper.updateById(userDO);
+    }
+
+    @Override
+    public void resetPassword(MbrAuthResetPasswordReqVO reqVO) {
+        // 根据验证码取出手机号,并查询用户
+        String mobile = stringRedisTemplate.opsForValue().get(reqVO.getCode());
+        MbrUserDO userDO = userMapper.selectByMobile(mobile);
+        if (userDO == null){
+            throw exception(USER_NOT_EXISTS);
+        }
+        // TODO @芋艿 这一步没必要检验验证码与手机是否匹配,因为是根据验证码去redis中查找手机号,然后根据手机号查询用户
+        //  也就是说 即便黑客以其他方式将验证码发送到自己手机上,最终还是会根据手机号查询用户然后进行重置密码的操作,不存在安全问题
+
+        // 校验验证码
+        smsCodeService.useSmsCode(userDO.getMobile(), SysSmsSceneEnum.FORGET_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP());
+
+        // 更新密码
+        userDO.setPassword(passwordEncoder.encode(reqVO.getPassword()));
+        userMapper.updateById(userDO);
+    }
+
+    @Override
+    public void checkIfMobileMatchCodeAndDeleteCode(String phone, String code) {
+        // 检验用户手机与验证码是否匹配
+        String mobile = stringRedisTemplate.opsForValue().get(code);
+        if (!phone.equals(mobile)){
+            throw exception(USER_CODE_FAILED);
+        }
+        // 销毁redis中此验证码
+        stringRedisTemplate.delete(code);
+    }
+
+    /**
+     * 校验旧密码
+     *
+     * @param id          用户 id
+     * @param oldPassword 旧密码
+     * @return MbrUserDO 用户实体
+     */
+    @VisibleForTesting
+    public MbrUserDO checkOldPassword(Long id, String oldPassword) {
+        MbrUserDO user = userMapper.selectById(id);
+        if (user == null) {
+            throw exception(USER_NOT_EXISTS);
+        }
+        // 参数:未加密密码,编码后的密码
+        if (!passwordEncoder.matches(oldPassword,user.getPassword())) {
+            throw exception(USER_PASSWORD_FAILED);
+        }
+        return user;
+    }
+
     private void createLogoutLog(Long userId, String username) {
         SysLoginLogCreateReqDTO reqDTO = new SysLoginLogCreateReqDTO();
         reqDTO.setLogType(SysLoginLogTypeEnum.LOGOUT_SELF.getType());
@@ -274,7 +347,7 @@ public class SysAuthServiceImpl implements SysAuthService {
         reqDTO.setUserType(userTypeEnum.getValue());
         reqDTO.setUsername(username);
         reqDTO.setUserAgent(ServletUtils.getUserAgent());
-        reqDTO.setUserIp(ServletUtils.getClientIP());
+        reqDTO.setUserIp(getClientIP());
         reqDTO.setResult(SysLoginResultEnum.SUCCESS.getResult());
         loginLogCoreService.createLoginLog(reqDTO);
     }

+ 12 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.userserver.modules.system.service.sms;
 
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
 import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
 
 /**
@@ -20,6 +21,12 @@ public interface SysSmsCodeService {
      */
     void sendSmsCode(@Mobile String mobile, Integer scene, String createIp);
 
+    /**
+     * 发送短信验证码,并检测手机号是否已被注册
+     * @param reqVO 请求实体
+     */
+    void sendSmsNewCode(SysAuthSendSmsReqVO reqVO);
+
     /**
      * 验证短信验证码,并进行使用
      * 如果正确,则将验证码标记成已使用
@@ -32,4 +39,9 @@ public interface SysSmsCodeService {
      */
     void useSmsCode(@Mobile String mobile, Integer scene, String code, String usedIp);
 
+    /**
+     * 根据用户id发送验证码
+     * @param userId 用户id
+     */
+    void sendSmsCodeLogin(Long userId);
 }

+ 43 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java

@@ -1,20 +1,27 @@
 package cn.iocoder.yudao.userserver.modules.system.service.sms.impl;
 
 import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
 import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsCoreService;
+import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
+import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
 import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO;
 import cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms.SysSmsCodeMapper;
+import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
 import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsTemplateCodeConstants;
 import cn.iocoder.yudao.userserver.modules.system.framework.sms.SmsCodeProperties;
 import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.util.Date;
+import java.util.concurrent.TimeUnit;
 
 import static cn.hutool.core.util.RandomUtil.randomInt;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*;
 
 /**
@@ -26,15 +33,26 @@ import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConst
 @Validated
 public class SysSmsCodeServiceImpl implements SysSmsCodeService {
 
+    /**
+     * 验证码 + 手机 在redis中存储的有效时间,单位:分钟
+     */
+    private static final Long CODE_TIME = 10L;
+
     @Resource
     private SmsCodeProperties smsCodeProperties;
 
     @Resource
     private SysSmsCodeMapper smsCodeMapper;
 
+    @Resource
+    private MbrUserService mbrUserService;
+
     @Resource
     private SysSmsCoreService smsCoreService;
 
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
     @Override
     public void sendSmsCode(String mobile, Integer scene, String createIp) {
         // 创建验证码
@@ -42,6 +60,21 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService {
         // 发送验证码
         smsCoreService.sendSingleSmsToMember(mobile, null, SysSmsTemplateCodeConstants.USER_SMS_LOGIN,
                 MapUtil.of("code", code));
+
+        // 存储手机号与验证码到redis,用于标记
+        stringRedisTemplate.opsForValue().set(code,mobile,CODE_TIME, TimeUnit.MINUTES);
+    }
+
+    @Override
+    public void sendSmsNewCode(SysAuthSendSmsReqVO reqVO) {
+        // 检测手机号是否已被使用
+        MbrUserDO userByMobile = mbrUserService.getUserByMobile(reqVO.getMobile());
+        if (userByMobile != null){
+            throw exception(USER_SMS_CODE_IS_EXISTS);
+        }
+
+        // 发送短信
+        this.sendSmsCode(reqVO.getMobile(),reqVO.getScene(),getClientIP());
     }
 
     private String createSmsCode(String mobile, Integer scene, String ip) {
@@ -91,4 +124,14 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService {
                 .used(true).usedTime(new Date()).usedIp(usedIp).build());
     }
 
+    @Override
+    public void sendSmsCodeLogin(Long userId) {
+        MbrUserDO user = mbrUserService.getUser(userId);
+        if (user == null){
+            throw exception(USER_NOT_EXISTS);
+        }
+        // 发送验证码
+        this.sendSmsCode(user.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), getClientIP());
+    }
+
 }

+ 48 - 0
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.userserver;
+
+import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
+import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import cn.iocoder.yudao.userserver.config.RedisTestConfiguration;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+/**
+ * 依赖内存 DB + Redis 的单元测试
+ *
+ * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
+ *
+ * @author 芋道源码
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+public class BaseDbAndRedisUnitTest {
+
+    @Import({
+            // DB 配置类
+            YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
+            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
+            // MyBatis 配置类
+            YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+            // Redis 配置类
+            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动高配置类
+    })
+    public static class Application {
+    }
+
+}

+ 30 - 0
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.userserver.config;
+
+import com.github.fppt.jedismock.RedisServer;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.io.IOException;
+
+@Configuration(proxyBeanMethods = false)
+@Lazy(false) // 禁止延迟加载
+@EnableConfigurationProperties(RedisProperties.class)
+public class RedisTestConfiguration {
+
+    /**
+     * 创建模拟的 Redis Server 服务器
+     */
+    @Bean
+    public RedisServer redisServer(RedisProperties properties) throws IOException {
+        RedisServer redisServer = new RedisServer(properties.getPort());
+        // TODO 芋艿:一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
+        try {
+            redisServer.start();
+        } catch (Exception ignore) {}
+        return redisServer;
+    }
+
+}

+ 59 - 0
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.userserver.modules.member.controller;
+
+import cn.iocoder.yudao.userserver.modules.member.controller.user.SysUserProfileController;
+import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * {@link SysUserProfileController} 的单元测试类
+ *
+ * @author 宋天
+ */
+public class SysUserProfileControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private SysUserProfileController sysUserProfileController;
+
+    @Mock
+    private MbrUserService userService;
+
+    @Mock
+    private SysSmsCodeService smsCodeService;
+
+    @Before
+    public void setup() {
+        // 初始化
+        MockitoAnnotations.openMocks(this);
+
+        // 构建mvc环境
+        mockMvc = MockMvcBuilders.standaloneSetup(sysUserProfileController).build();
+    }
+
+    @Test
+    public void testUpdateMobile_success() throws Exception {
+        //模拟接口调用
+        this.mockMvc.perform(post("/system/user/profile/update-mobile")
+                        .contentType(MediaType.APPLICATION_JSON_VALUE)
+                        .content("{\"mobile\":\"15819844280\",\"code\":\"123456\"}}"))
+                .andExpect(status().isOk())
+                .andDo(MockMvcResultHandlers.print());
+
+    }
+
+
+}

+ 52 - 8
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java

@@ -2,28 +2,32 @@ package cn.iocoder.yudao.userserver.modules.member.service;
 
 import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreService;
 import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
-import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest;
 import cn.iocoder.yudao.userserver.BaseDbUnitTest;
 import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
+import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
 import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
 import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl;
+import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
+import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
+import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.impl.SysSmsCodeServiceImpl;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
-import org.springframework.http.MediaType;
-import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.util.Assert;
 
 import javax.annotation.Resource;
 import java.io.*;
 import java.util.function.Consumer;
 
-import static cn.hutool.core.util.RandomUtil.randomBytes;
+import static cn.hutool.core.util.RandomUtil.*;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static cn.hutool.core.util.RandomUtil.randomEle;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
 import static org.mockito.Mockito.*;
@@ -32,21 +36,30 @@ import static org.mockito.Mockito.*;
  *
  * @author 宋天
  */
-@Import(MbrUserServiceImpl.class)
-public class MbrUserServiceImplTest extends BaseDbUnitTest {
+@Import({MbrUserServiceImpl.class, YudaoRedisAutoConfiguration.class})
+public class MbrUserServiceImplTest extends BaseDbAndRedisUnitTest {
 
     @Resource
     private MbrUserServiceImpl mbrUserService;
 
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
     @Resource
     private MbrUserMapper userMapper;
 
+    @MockBean
+    private SysAuthServiceImpl authService;
+
     @MockBean
     private InfFileCoreService fileCoreService;
 
     @MockBean
     private PasswordEncoder passwordEncoder;
 
+    @MockBean
+    private SysSmsCodeService sysSmsCodeService;
+
     @Test
     public void testUpdateNickName_success(){
         // mock 数据
@@ -94,6 +107,37 @@ public class MbrUserServiceImplTest extends BaseDbUnitTest {
         assertEquals(avatar, str);
     }
 
+    @Test
+    public void updateMobile_success(){
+        // mock数据
+        String oldMobile = randomNumbers(11);
+        MbrUserDO userDO = randomMbrUserDO();
+        userDO.setMobile(oldMobile);
+        userMapper.insert(userDO);
+
+        // 验证旧手机
+        sysSmsCodeService.sendSmsCodeLogin(userDO.getId());
+
+        // 验证旧手机验证码是否正确
+        sysSmsCodeService.useSmsCode(oldMobile,SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(),"123","1.1.1.1");
+
+        // 验证新手机
+        SysAuthSendSmsReqVO smsReqVO = new SysAuthSendSmsReqVO();
+        smsReqVO.setMobile(oldMobile);
+        smsReqVO.setScene(SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene());
+        sysSmsCodeService.sendSmsNewCode(smsReqVO);
+
+        // 更新手机号
+        String newMobile = randomNumbers(11);
+        String code = randomNumbers(4);
+        MbrUserUpdateMobileReqVO reqVO = new MbrUserUpdateMobileReqVO();
+        reqVO.setMobile(newMobile);
+        reqVO.setCode(code);
+        mbrUserService.updateMobile(userDO.getId(),reqVO);
+
+        assertEquals(mbrUserService.getUser(userDO.getId()).getMobile(),newMobile);
+    }
+
     // ========== 随机对象 ==========
 
     @SafeVarargs

+ 75 - 0
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java

@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.userserver.modules.system.controller;
+
+import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService;
+import cn.iocoder.yudao.userserver.modules.system.controller.auth.SysAuthController;
+import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * {@link SysAuthController} 的单元测试类
+ *
+ * @author 宋天
+ */
+public class SysAuthControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private SysAuthController sysAuthController;
+
+    @Mock
+    private SysAuthService authService;
+    @Mock
+    private SysSmsCodeService smsCodeService;
+    @Mock
+    private SysSocialService socialService;
+
+
+    @Before
+    public void setup() {
+        // 初始化
+        MockitoAnnotations.openMocks(this);
+
+        // 构建mvc环境
+        mockMvc = MockMvcBuilders.standaloneSetup(sysAuthController).build();
+    }
+
+    @Test
+    public void testResetPassword_success() throws Exception {
+        //模拟接口调用
+        this.mockMvc.perform(post("/reset-password")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"password\":\"1123\",\"code\":\"123456\"}}"))
+                .andExpect(status().isOk())
+                .andDo(MockMvcResultHandlers.print());
+
+    }
+
+    @Test
+    public void testUpdatePassword_success() throws Exception {
+        //模拟接口调用
+        this.mockMvc.perform(post("/update-password")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"password\":\"1123\",\"code\":\"123456\",\"oldPassword\":\"1123\"}}"))
+                .andExpect(status().isOk())
+                .andDo(MockMvcResultHandlers.print());
+
+    }
+
+
+
+}

+ 134 - 0
yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java

@@ -0,0 +1,134 @@
+package cn.iocoder.yudao.userserver.modules.system.service;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
+import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.yudao.coreservice.modules.system.service.auth.SysUserSessionCoreService;
+import cn.iocoder.yudao.coreservice.modules.system.service.logger.SysLoginLogCoreService;
+import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest;
+import cn.iocoder.yudao.userserver.BaseDbUnitTest;
+import cn.iocoder.yudao.userserver.config.RedisTestConfiguration;
+import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
+import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
+import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl;
+import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthResetPasswordReqVO;
+import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
+import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl;
+import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import javax.annotation.Resource;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import static cn.hutool.core.util.RandomUtil.randomEle;
+import static cn.hutool.core.util.RandomUtil.randomNumbers;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+
+/**
+ * {@link SysAuthService} 的单元测试类
+ *
+ * @author 宋天
+ */
+@Import({SysAuthServiceImpl.class, YudaoRedisAutoConfiguration.class})
+public class SysAuthServiceTest extends BaseDbAndRedisUnitTest {
+
+    @MockBean
+    private AuthenticationManager authenticationManager;
+    @MockBean
+    private MbrUserService userService;
+    @MockBean
+    private SysSmsCodeService smsCodeService;
+    @MockBean
+    private SysLoginLogCoreService loginLogCoreService;
+    @MockBean
+    private SysUserSessionCoreService userSessionCoreService;
+    @MockBean
+    private SysSocialService socialService;
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+    @MockBean
+    private PasswordEncoder passwordEncoder;
+    @Resource
+    private MbrUserMapper mbrUserMapper;
+    @Resource
+    private SysAuthServiceImpl authService;
+
+
+    @Test
+    public void testUpdatePassword_success(){
+        // 准备参数
+        MbrUserDO userDO = randomMbrUserDO();
+        mbrUserMapper.insert(userDO);
+
+        // 新密码
+        String newPassword = randomString();
+
+        // 请求实体
+        MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO();
+        reqVO.setOldPassword(userDO.getPassword());
+        reqVO.setPassword(newPassword);
+
+        // 测试桩
+        // 这两个相等是为了返回ture这个结果
+        when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true);
+        when(passwordEncoder.encode(newPassword)).thenReturn(newPassword);
+
+        // 更新用户密码
+        authService.updatePassword(userDO.getId(),reqVO);
+        assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),newPassword);
+    }
+
+    @Test
+    public void testResetPassword_success(){
+        // 准备参数
+        MbrUserDO userDO = randomMbrUserDO();
+        mbrUserMapper.insert(userDO);
+
+        // 随机密码
+        String password = randomNumbers(11);
+        // 随机验证码
+        String code = randomNumbers(4);
+
+        MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO();
+        reqVO.setPassword(password);
+        reqVO.setCode(code);
+
+        // 放入code+手机号
+        stringRedisTemplate.opsForValue().set(code,userDO.getMobile(),10, TimeUnit.MINUTES);
+
+        // mock
+        when(passwordEncoder.encode(password)).thenReturn(password);
+
+        // 更新用户密码
+        authService.resetPassword(reqVO);
+        assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),password);
+    }
+
+
+    // ========== 随机对象 ==========
+
+    @SafeVarargs
+    private static MbrUserDO randomMbrUserDO(Consumer<MbrUserDO>... consumers) {
+        Consumer<MbrUserDO> consumer = (o) -> {
+            o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
+            o.setPassword(randomString());
+        };
+        return randomPojo(MbrUserDO.class, ArrayUtils.append(consumer, consumers));
+    }
+
+
+}