diff --git a/snailmall-user-service/src/main/java/com/njupt/swg/service/UserServiceImpl.java b/snailmall-user-service/src/main/java/com/njupt/swg/service/UserServiceImpl.java index e2cc35b..7136e92 100644 --- a/snailmall-user-service/src/main/java/com/njupt/swg/service/UserServiceImpl.java +++ b/snailmall-user-service/src/main/java/com/njupt/swg/service/UserServiceImpl.java @@ -26,34 +26,81 @@ import java.util.concurrent.TimeUnit; * @CONTACT 317758022@qq.com * @DESC 登陆错误 code=1,msg=xxx 登陆成功 code=0,data=UserRespVO */ +// @Service注解用于将当前类标记为Spring框架中的服务层组件,表明该类是一个业务逻辑处理类,会被Spring容器管理, +// 方便在其他组件(如控制器层)中通过依赖注入的方式进行调用,实现业务逻辑与其他层的解耦。 @Service +// @Slf4j注解是lombok提供的一个便捷方式,用于自动在类中生成一个名为log的日志记录器对象, +// 可以通过它方便地记录不同级别(如info、error等)的日志信息,便于在程序运行过程中对业务流程、异常情况等进行记录和跟踪,方便后续的调试与监控。 @Slf4j -public class UserServiceImpl implements IUserService{ +// 该类实现了IUserService接口,意味着它需要按照接口中定义的方法契约来实现具体的业务逻辑, +// 这样的设计符合面向接口编程的原则,使得代码结构更加清晰,便于替换不同的实现类(例如在测试环境或不同业务场景下使用不同的实现逻辑), +// 同时也方便其他代码依赖于接口进行调用,而不用关心具体的实现细节。 +public class UserServiceImpl implements IUserService { + + // 通过Spring的依赖注入机制,使用@Autowired注解自动装配一个UserMapper类型的实例。 + // UserMapper通常是一个MyBatis的Mapper接口,定义了与数据库中用户表进行交互的各种方法,比如查询用户信息、插入用户记录等操作, + // 在这里被注入后,就可以在业务逻辑方法中调用它的方法来实现与数据库的数据交互,以完成诸如登录验证、注册用户等业务功能。 @Autowired private UserMapper userMapper; + // 同样使用@Autowired注解注入CuratorFramework类型的实例,CuratorFramework是一个用于与Zookeeper进行交互的客户端框架相关的类, + // 在本代码中可能是用于实现分布式锁功能(结合后续代码逻辑来看),通过与Zookeeper的协作来保证在分布式环境下关键业务操作(如用户注册)的原子性和并发控制, + // 避免多个实例同时操作产生的数据不一致等问题。 @Autowired private CuratorFramework zkClient; + // 注入CommonCacheUtil类型的实例,CommonCacheUtil是一个用于缓存操作的工具类, + // 大概率是用于和缓存系统(如Redis)进行交互,实现对常用数据(比如用户登录状态信息、临时验证信息等)的缓存管理, + // 通过缓存数据可以减少对数据库的频繁访问,提高系统的整体性能和响应速度。 @Autowired private CommonCacheUtil commonCacheUtil; + /** + * 此方法实现了用户登录的业务逻辑,整体流程包含以下几个关键步骤: + * + * 步骤一:参数校验 + * 首先对传入的用户名和密码参数进行非空校验,通过StringUtils.isEmpty方法判断,如果用户名或密码为空字符串, + * 则抛出SnailmallException异常,并给出相应的提示信息“温馨提示:用户名或密码不能为空”,确保登录操作的基础参数完整。 + * + * 步骤二:用户名存在性校验 + * 在参数非空的基础上,调用userMapper的selectByUsername方法,根据传入的用户名去数据库中查询对应的用户记录, + * 该方法返回的结果是匹配该用户名的记录数量。如果查询到的记录数量为0,意味着数据库中不存在该用户名对应的用户, + * 此时抛出SnailmallException异常,提示“温馨提示:用户名不存在”,说明用户输入的用户名有误,无法进行后续登录验证。 + * + * 步骤三:密码校验 + * 当确认用户名存在后,将传入的密码通过MD5Util.MD5EncodeUtf8方法进行MD5加密处理, + * 然后使用加密后的密码和原始用户名再次调用userMapper的selectByUsernameAndPasswd方法去数据库中查询用户记录, + * 目的是验证用户输入的密码是否正确。如果查询结果为null,即没有找到匹配用户名和加密后密码的用户记录, + * 则抛出SnailmallException异常,提示“温馨提示:用户名或者密码不正确,请重试”,告知用户登录信息有误。 + * + * 步骤四:构建返回结果 + * 若前面的用户名和密码校验都通过,说明用户登录成功。此时创建一个UserResVO对象, + * 将从数据库查询到的用户记录(resultUser)中的相关信息(如用户ID、用户名、邮箱、电话、角色、问题、答案、创建时间等) + * 分别设置到UserResVO对象的对应属性中,其中更新时间设置为当前日期(new Date()), + * 最后通过ServerResponse.createBySuccess方法构建一个表示成功的ServerResponse对象,将提示信息“用户登陆成功”和封装好的UserResVO对象作为参数传入, + * 返回给调用者,通常这个调用者可能是控制器层,然后再将结果返回给前端客户端,告知用户登录成功以及返回相关的用户信息。 + * + * @param username 客户端传入的用户名,不能为空,用于在数据库中查找对应的用户记录进行登录验证。 + * @param password 客户端传入的用户密码,不能为空,需要与数据库中存储的密码进行比对验证用户身份。 + * @return 返回一个ServerResponse类型的对象,包含登录操作的结果状态(成功则为“用户登陆成功”提示信息)以及登录成功后的用户详细信息(封装在UserResVO对象中), + * 方便前端根据返回结果进行页面跳转、显示用户信息等后续操作。 + */ @Override - public ServerResponse login(String username,String password) { - //1.校验参数不能为空 - if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ + public ServerResponse login(String username, String password) { + // 1.校验参数不能为空 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { throw new SnailmallException("温馨提示:用户名或密码不能为空"); } - //2.根据用户名去取用户信息(本系统用户名不能重复) + // 2.根据用户名去取用户信息(本系统用户名不能重复) int resultCount = userMapper.selectByUsername(username); - if(resultCount == 0){ + if (resultCount == 0) { throw new SnailmallException("温馨提示:用户名不存在"); } - //3.走到这一步,说明存在该用户,下面就执行登陆校验 + // 3.走到这一步,说明存在该用户,下面就执行登陆校验 String md5Passwd = MD5Util.MD5EncodeUtf8(password); - User resultUser = userMapper.selectByUsernameAndPasswd(username,md5Passwd); - if (resultUser == null){ + User resultUser = userMapper.selectByUsernameAndPasswd(username, md5Passwd); + if (resultUser == null) { throw new SnailmallException("温馨提示:用户名或者密码不正确,请重试"); } - //4.走到这一步,说明用户名密码正确,应该返回成功 + // 4.走到这一步,说明用户名密码正确,应该返回成功 UserResVO userResVO = new UserResVO(); userResVO.setId(resultUser.getId()); userResVO.setUsername(resultUser.getUsername()); @@ -65,58 +112,96 @@ public class UserServiceImpl implements IUserService{ userResVO.setCreateTime(resultUser.getCreateTime()); userResVO.setUpdateTime(new Date()); - return ServerResponse.createBySuccess("用户登陆成功",userResVO); + return ServerResponse.createBySuccess("用户登陆成功", userResVO); } + /** + * 该方法用于处理用户注册的业务逻辑,整体逻辑较为复杂,主要涉及以下几个重要部分: + * + * 1. 参数校验 + * 首先对传入的User对象中的关键属性(用户名、邮箱、密码、问题、答案)进行非空校验, + * 通过StringUtils.isBlank方法判断,如果这些属性中有任何一个为空字符串,就抛出SnailmallException异常, + * 并提示“参数不能为空,请仔细填写”,确保用户注册时提供的必要信息完整,避免后续因数据缺失导致的问题。 + * + * 2. 分布式锁获取与处理 + * - 初始化分布式锁对象:创建一个InterProcessLock类型的锁对象(具体实现为InterProcessMutex,通过zkClient和指定的锁路径Constants.USER_REGISTER_DISTRIBUTE_LOCK_PATH来构造), + * 用于在分布式环境下对用户注册操作进行并发控制,避免多个线程或实例同时进行注册导致的数据冲突(比如用户名或邮箱重复注册等问题)。 + * - 尝试获取锁的循环:进入一个do-while循环,不断尝试获取分布式锁,通过调用lock.acquire方法,并设置超时时间为3000毫秒(以TimeUnit.MILLISECONDS为单位), + * 如果成功获取到锁,就执行以下操作: + * - 记录获取锁的日志信息:通过log.info方法记录当前正在注册的用户邮箱以及获取锁的线程名称,方便后续查看日志排查问题,了解锁的获取情况以及注册操作的执行线程。 + * - 用户名重复性校验:调用this.checkValid方法,传入用户名和Constants.USERNAME常量,校验用户名是否已经在数据库中存在, + * 如果返回的ServerResponse对象表示校验不成功(即用户名已存在),则直接将该响应对象返回,终止当前注册流程,告知客户端用户名重复不能注册。 + * - 邮箱重复性校验:同样调用this.checkValid方法,传入邮箱和Constants.EMAIL常量,校验邮箱是否已经存在于数据库中, + * 若返回结果表示校验不成功(即邮箱已存在),则返回该响应对象,结束注册流程,提示客户端邮箱已被使用不能注册。 + * - 插入用户数据:当用户名和邮箱的重复性校验都通过后,设置用户的角色为普通用户(通过Constants.Role.ROLE_CUSTOME常量指定), + * 并对用户密码进行MD5加密处理(使用MD5Util.MD5EncodeUtf8方法),然后调用userMapper的insert方法将用户信息插入到数据库中,完成用户注册的核心操作, + * 最后将循环控制变量retry设置为false,跳出循环,结束尝试获取锁和注册操作的循环过程。 + * 如果在尝试获取锁的过程中失败(即没有在超时时间内获取到锁),则通过log.info方法记录“【获取锁失败,继续尝试...】”的日志信息, + * 提示后续会继续尝试获取锁来进行注册操作(虽然代码中此处没有明确体现等待或延迟等具体的重试策略,但逻辑上会不断循环尝试获取锁)。 + * + * 3. 锁释放与异常处理 + * 在finally块中,无论是否成功获取锁以及是否完成注册操作,都会对锁进行释放操作,以避免锁资源一直被占用导致的死锁等问题。 + * 如果锁对象lock不为null,就调用lock.release方法释放锁,并通过log.info方法记录释放锁的日志信息,包括释放锁的用户邮箱和线程名称,方便查看锁的释放情况。 + * 如果在释放锁的过程中出现异常,通过e.printStackTrace方法打印异常堆栈信息,便于排查锁释放环节出现的问题,不过这种情况属于异常情况,正常情况下应该能顺利释放锁。 + * + * 4. 返回注册结果 + * 如果整个注册流程顺利完成(即没有因为参数问题、用户名或邮箱重复问题以及锁相关的异常等情况中断), + * 最后通过ServerResponse.createBySuccessMessage方法构建一个表示注册成功的ServerResponse对象,返回“注册成功”的提示信息给客户端,告知用户注册操作已成功完成。 + * + * @param user 包含用户注册信息的User对象,由客户端传入,其关键属性(用户名、邮箱、密码、问题、答案等)需满足非空校验要求, + * 用于在数据库中插入新的用户记录以及进行相关的业务逻辑判断(如重复性校验等)。 + * @return 返回一个ServerResponse对象,包含用户注册操作的结果状态(成功则返回“注册成功”提示信息)以及相应的提示信息, + * 方便客户端根据返回结果提示用户注册是否成功,并进行相应的后续操作(如跳转到登录页面等)。 + */ @Override public ServerResponse register(User user) { - //1.校验参数是否为空 - if(StringUtils.isBlank(user.getUsername()) || + // 1.校验参数是否为空 + if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getEmail()) || StringUtils.isBlank(user.getPassword()) || StringUtils.isBlank(user.getQuestion()) || - StringUtils.isBlank(user.getAnswer())){ + StringUtils.isBlank(user.getAnswer())) { throw new SnailmallException("参数不能为空,请仔细填写"); } - //---开启锁 + // ---开启锁 InterProcessLock lock = null; try { lock = new InterProcessMutex(zkClient, Constants.USER_REGISTER_DISTRIBUTE_LOCK_PATH); boolean retry = true; do { - if (lock.acquire(3000, TimeUnit.MILLISECONDS)){ - log.info(user.getEmail()+Thread.currentThread().getName()+"获取锁"); - //2.参数没问题的话,就校验一下名字是否已经存在 - ServerResponse response = this.checkValid(user.getUsername(),Constants.USERNAME); - if(!response.isSuccess()){ - //说明用户名已经重复了 + if (lock.acquire(3000, TimeUnit.MILLISECONDS)) { + log.info(user.getEmail() + Thread.currentThread().getName() + "获取锁"); + // 2.参数没问题的话,就校验一下名字是否已经存在 + ServerResponse response = this.checkValid(user.getUsername(), Constants.USERNAME); + if (!response.isSuccess()) { + // 说明用户名已经重复了 return response; } - //3.再校验一下邮箱是否存在 - response = this.checkValid(user.getEmail(),Constants.EMAIL); - if(!response.isSuccess()){ - //说明用户名已经重复了 + // 3.再校验一下邮箱是否存在 + response = this.checkValid(user.getEmail(), Constants.EMAIL); + if (!response.isSuccess()) { + // 说明用户名已经重复了 return response; } - //4.重复校验通过之后就可以塞入这条数据了 - user.setRole(Constants.Role.ROLE_CUSTOME);//普通用户 + // 4.重复校验通过之后就可以塞入这条数据了 + user.setRole(Constants.Role.ROLE_CUSTOME); // 普通用户 user.setPassword(MD5Util.MD5EncodeUtf8(user.getPassword())); userMapper.insert(user); - //跳出循环 + // 跳出循环 retry = false; } log.info("【获取锁失败,继续尝试...】"); - //可以适当休息一会 - }while (retry); - }catch (Exception e){ - log.error("【校验用户所填的用户名或者密码出现问题】",e); + // 可以适当休息一会 + } while (retry); + } catch (Exception e) { + log.error("【校验用户所填的用户名或者密码出现问题】", e); throw new SnailmallException("分布式锁校验出错"); - }finally { - //---释放锁 - if(lock != null){ + } finally { + // ---释放锁 + if (lock!= null) { try { lock.release(); - log.info(user.getEmail()+Thread.currentThread().getName()+"释放锁"); + log.info(user.getEmail() + Thread.currentThread().getName() + "释放锁"); } catch (Exception e) { e.printStackTrace(); } @@ -125,23 +210,50 @@ public class UserServiceImpl implements IUserService{ return ServerResponse.createBySuccessMessage("注册成功"); } + /** + * 此方法用于校验传入的字符串(代表用户名或邮箱)在数据库中的唯一性,具体逻辑如下: + * + * 1. 参数校验 + * 首先通过StringUtils.isBlank方法检查传入的参数str(待校验的用户名或邮箱)和type(用于表明str代表的是用户名还是邮箱的类型标识)是否为空字符串。 + * 如果其中任意一个参数为空,则抛出SnailmallException异常,并提示“参数有问题”,以此确保后续校验操作能基于有效的输入参数进行。 + * + * 2. 根据类型校验唯一性 + * 如果传入的type参数在忽略大小写的情况下与Constants.USERNAME相等,说明此次校验针对的是用户名的唯一性。 + * 此时调用userMapper的selectByUsername方法,传入str作为用户名去数据库中查询匹配的用户记录数量。 + * 若查询得到的结果数量大于0,意味着数据库中已经存在使用该用户名的用户,那么就通过ServerResponse.createByErrorMessage方法创建一个表示错误信息的ServerResponse对象, + * 返回“用户已经存在”的提示信息给调用者,告知该用户名已被占用,无法进行相应操作(比如注册新用户时使用该用户名)。 + * + * 若type参数与Constants.EMAIL相等,表明此次校验针对的是邮箱的唯一性。 + * 相应地,调用userMapper的selectByEmail方法,以str作为邮箱地址去数据库查询匹配的用户记录数量。 + * 当查询结果数量大于0时,说明该邮箱已被其他用户使用,同样通过ServerResponse.createByErrorMessage方法返回一个包含“邮箱已经存在”提示信息的ServerResponse对象给调用者, + * 提示该邮箱不能再用于当前操作(例如注册时使用该邮箱)。 + * + * 3. 返回校验成功结果 + * 如果经过上述针对用户名或邮箱的唯一性校验后,均未发现重复情况(即对应查询结果数量都不大于0), + * 则通过ServerResponse.createBySuccess方法创建一个表示成功的ServerResponse对象,返回“校验成功,用户名和邮箱都合法”的提示信息给调用者, + * 意味着传入的用户名或邮箱可以用于后续操作(如注册时可继续插入数据等)。 + * + * @param str 要校验的用户名或邮箱字符串,不能为空,其具体代表的含义由type参数确定,用于在数据库中查询验证其唯一性。 + * @param type 用于标识str代表的是用户名还是邮箱的类型参数,取值应为预定义的相关常量(如Constants类中定义的常量),不能为空,以此决定具体的校验逻辑走向。 + * @return 返回一个ServerResponse对象,包含了校验操作的结果状态(如用户名或邮箱已存在、校验成功等情况)以及相应的提示信息,方便调用者根据返回结果进行后续操作决策。 + */ @Override public ServerResponse checkValid(String str, String type) { //校验参数是否为空 - if(StringUtils.isBlank(str) || StringUtils.isBlank(type)){ + if (StringUtils.isBlank(str) || StringUtils.isBlank(type)) { throw new SnailmallException("参数有问题"); } - if(Constants.USERNAME.equalsIgnoreCase(type)){ + if (Constants.USERNAME.equalsIgnoreCase(type)) { //如果是username类型,那么就根据str为username去数据库查询 int resultCount = userMapper.selectByUsername(str); - if(resultCount > 0){ + if (resultCount > 0) { //说明数据库已经存在这个username的用户了,返回用户已存在 return ServerResponse.createByErrorMessage("用户已经存在"); } - }else if(Constants.EMAIL.equals(type)){ + } else if (Constants.EMAIL.equals(type)) { //如果是email类型,就根据str为email去数据库查询 int resultCount = userMapper.selectByEmail(str); - if(resultCount > 0){ + if (resultCount > 0) { //说明数据库已经存在这个email的用户了,返回用户已存在 return ServerResponse.createByErrorMessage("邮箱已经存在"); } @@ -149,120 +261,289 @@ public class UserServiceImpl implements IUserService{ return ServerResponse.createBySuccess("校验成功,用户名和邮箱都合法"); } + /** + * 该方法用于根据传入的用户名获取对应的找回密码问题,执行逻辑如下: + * + * 1. 参数校验 + * 通过StringUtils.isBlank方法检查传入的用户名参数是否为空字符串。 + * 如果为空,则通过ServerResponse.createByErrorMessage方法创建并返回一个包含“用户名不能为空”提示信息的ServerResponse对象给调用者, + * 告知调用者需要提供有效的用户名才能进行后续操作,避免因空用户名导致的异常情况。 + * + * 2. 查询用户信息 + * 若用户名参数非空,调用userMapper的getUserByUsername方法,传入用户名去数据库中查询对应的用户信息,将查询结果赋值给User类型的user对象。 + * 如果查询结果为null,意味着数据库中不存在该用户名对应的用户,此时通过ServerResponse.createByErrorMessage方法返回一个包含“用户不存在”提示信息的ServerResponse对象给调用者, + * 说明无法获取到对应的找回密码问题,因为用户本身不存在。 + * + * 3. 获取并校验问题 + * 若成功查询到用户信息(即user对象不为null),则获取该用户设置的找回密码问题,通过user.getQuestion方法获取对应的问题字符串。 + * 再次通过StringUtils.isBlank方法检查该问题字符串是否为空,如果为空,说明该用户没有设置对应的找回密码问题, + * 于是通过ServerResponse.createByErrorMessage方法返回一个包含“该用户没有设置对应的问题”提示信息的ServerResponse对象给调用者, + * 告知调用者无法获取到有效的问题用于找回密码操作。 + * + * 4. 返回问题成功获取结果 + * 如果经过上述步骤,成功获取到了非空的找回密码问题字符串,则通过ServerResponse.createBySuccess方法创建并返回一个包含该问题字符串的ServerResponse对象给调用者, + * 表示成功获取到了对应的找回密码问题,方便调用者(通常是前端相关代码)展示该问题给用户,以便用户进行后续的回答操作来找回密码。 + * + * @param username 要获取找回密码问题的用户名,不能为空,用于在数据库中查找对应的用户信息及设置的问题。 + * @return 返回一个ServerResponse对象,包含了获取问题操作的结果状态(如成功获取到问题、用户不存在、未设置问题等情况)以及相应的提示信息或获取到的问题内容, + * 方便调用者根据返回结果进行后续展示等操作。 + */ @Override public ServerResponse getQuestionByUsername(String username) { //1.校验参数 - if(StringUtils.isBlank(username)){ + if (StringUtils.isBlank(username)) { return ServerResponse.createByErrorMessage("用户名不能为空"); } //2.根据username去获取题目 User user = userMapper.getUserByUsername(username); - if(user == null){ + if (user == null) { return ServerResponse.createByErrorMessage("用户不存在"); } String question = user.getQuestion(); - if(StringUtils.isBlank(question)){ + if (StringUtils.isBlank(question)) { return ServerResponse.createByErrorMessage("该用户没有设置对应的问题"); } return ServerResponse.createBySuccess(question); } + /** + * 此方法用于校验用户输入的找回密码答案是否正确,并根据情况生成或获取相应的忘记密码token,具体逻辑如下: + * + * 1. 参数校验 + * 首先通过StringUtils.isBlank方法分别检查传入的用户名、问题和答案参数是否为空字符串。 + * 如果其中任意一个参数为空,则通过ServerResponse.createByErrorMessage方法创建并返回一个包含“参数有问题”提示信息的ServerResponse对象给调用者, + * 确保后续的校验及token相关操作基于完整有效的参数进行,避免因参数缺失导致的异常情况。 + * + * 2. 验证答案并处理token + * 在参数都非空的情况下,调用userMapper的getUserByUsernameQuestionAnswer方法,传入用户名、问题和答案去数据库中查询对应的用户信息, + * 将查询结果赋值给User类型的user对象。如果user对象不为null,说明数据库中存在匹配该用户名、问题和答案的用户,意味着用户输入的答案可能是正确的,接下来进行以下token相关操作: + * + * - 首先尝试从Redis缓存中获取对应的忘记密码token,通过调用commonCacheUtil的getCacheValue方法,传入由Constants.TOKEN_PREFIX和用户名拼接而成的字符串作为key去获取token值, + * 将获取到的token值赋值给forgetToken变量。 + * - 接着通过StringUtils.isNotBlank方法检查获取到的forgetToken是否为非空字符串,如果是,说明Redis中存在未过期的有效token, + * 则通过ServerResponse.createBySuccess方法创建并返回一个包含该有效token的ServerResponse对象给调用者, + * 此时就可以直接使用该token进行后续找回密码相关操作(例如验证等),无需重新生成token。 + * - 如果从Redis中获取到的forgetToken为空字符串,说明缓存中不存在对应的token或者token已过期,并且由于前面已经验证了用户输入的答案是正确的(通过查询到了对应的用户信息), + * 那么就需要重新生成一个token。通过UUID.randomUUID().toString方法生成一个唯一的字符串作为新的token, + * 然后调用commonCacheUtil的cacheNxExpire方法,将新生成的token以Constants.TOKEN_PREFIX和用户名拼接的字符串作为key,缓存到Redis中,并设置过期时间为60 * 60 * 12秒(12小时), + * 最后通过ServerResponse.createBySuccess方法创建并返回一个包含新生成token的ServerResponse对象给调用者,方便后续找回密码操作使用该新token。 + * + * 3. 返回答案错误结果 + * 如果经过数据库查询,发现没有匹配传入用户名、问题和答案的用户信息(即user对象为null),说明用户输入的答案有误, + * 则通过ServerResponse.createByErrorMessage方法创建并返回一个包含“问题答案有误”提示信息的ServerResponse对象给调用者,告知调用者答案不正确,无法进行后续找回密码操作。 + * + * @param username 要校验找回密码答案的用户名,不能为空,用于在数据库中查找对应的用户信息进行答案验证以及token相关操作。 + * @param question 找回密码时用户需要回答的问题,不能为空,与用户名和答案一起用于数据库查询验证。 + * @param answer 用户输入的找回密码答案,不能为空,用于验证是否与数据库中存储的对应用户的答案一致。 + * @return 返回一个ServerResponse对象,包含了校验答案及token操作的结果状态(如答案正确并获取到token、答案错误等情况)以及相应的提示信息或获取到的token值, + * 方便调用者根据返回结果进行后续找回密码等相关操作。 + */ @Override public ServerResponse checkAnswer(String username, String question, String answer) { //1.校验参数是否正确 - if(StringUtils.isBlank(username) || StringUtils.isBlank(question) || StringUtils.isBlank(answer)){ + if (StringUtils.isBlank(username) || StringUtils.isBlank(question) || StringUtils.isBlank(answer)) { return ServerResponse.createByErrorMessage("参数有问题"); } //2.参数没有问题之后,就可以去校验答案是否正确了 - User user = userMapper.getUserByUsernameQuestionAnswer(username,question,answer); - if(user != null){ + User user = userMapper.getUserByUsernameQuestionAnswer(username, question, answer); + if (user!= null) { //首先根据规则key去redis取,如果还有没有过期的key,就可以直接拿来用了,不用再重新生成 - String forgetToken = commonCacheUtil.getCacheValue(Constants.TOKEN_PREFIX+username); - if(StringUtils.isNotBlank(forgetToken)){ + String forgetToken = commonCacheUtil.getCacheValue(Constants.TOKEN_PREFIX + username); + if (StringUtils.isNotBlank(forgetToken)) { return ServerResponse.createBySuccess(forgetToken); } //取不到值,并且答案是对的,那么就重新生成一下吧! forgetToken = UUID.randomUUID().toString(); - commonCacheUtil.cacheNxExpire(Constants.TOKEN_PREFIX+username,forgetToken,60*60*12); + commonCacheUtil.cacheNxExpire(Constants.TOKEN_PREFIX + username, forgetToken, 60 * 60 * 12); return ServerResponse.createBySuccess(forgetToken); } return ServerResponse.createByErrorMessage("问题答案有误"); } + /** + * 该方法用于处理用户通过忘记密码流程重置密码的业务逻辑,主要步骤如下: + * + * 1. 参数校验 + * 通过StringUtils.isBlank方法分别检查传入的用户名、新密码和忘记密码token参数是否为空字符串。 + * 如果其中任意一个参数为空,则通过ServerResponse.createByErrorMessage方法创建并返回一个包含“参数有误,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 确保后续的密码重置操作基于完整有效的参数进行,避免因参数缺失导致的异常情况。 + * + * 2. 查询用户信息 + * 在参数都非空的情况下,调用userMapper的getUserByUsername方法,传入用户名去数据库中查询对应的用户信息,将查询结果赋值给User类型的user对象。 + * 如果查询结果为null,意味着数据库中不存在该用户名对应的用户,此时通过ServerResponse.createByErrorMessage方法返回一个包含“用户名不存在,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 说明无法进行密码重置操作,因为用户本身不存在。 + * + * 3. 验证token是否过期 + * 若成功查询到用户信息(即user对象不为null),接着从Redis缓存中获取对应的token,通过调用commonCacheUtil的getCacheValue方法,传入由Constants.TOKEN_PREFIX和用户名拼接而成的字符串作为key去获取token值, + * 将获取到的token值赋值给redisToken变量。 + * 如果获取到的redisToken为null,说明Redis中不存在对应的token或者token已过期,此时通过ServerResponse.createByErrorMessage方法返回一个包含“token已经过期,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 告知调用者由于token过期无法进行密码重置操作。 + * + * 4. 验证token一致性 + * 若成功获取到了非空的redisToken,通过StringUtils.equals方法检查前端传过来的forgetToken与从Redis中获取到的redisToken是否相等。 + * 如果两者不相等,说明token不一致,可能存在安全风险,此时通过ServerResponse.createByErrorMessage方法返回一个包含“token错误,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 提示调用者token不匹配,不能进行密码重置操作。 + * + * 5. 密码重复性校验 + * 若token验证通过,将传入的新密码通过MD5Util.MD5EncodeUtf8方法进行MD5加密处理,得到加密后的密码MD5Passwd。 + * 通过user.getPassword().equals方法检查加密后的新密码与数据库中存储的用户原密码是否相等,如果相等,说明新密码与原密码重复, + * 则通过ServerResponse.createByErrorMessage方法返回一个包含“不要使用重复密码!”提示信息的ServerResponse对象给调用者, + * 提示调用者不能使用重复密码进行重置操作,应更换新密码。 + * + * 6. 重置密码 + * 如果经过前面的各项验证都通过,说明可以进行密码重置操作。通过user.setPassword方法将用户对象的密码属性设置为加密后的新密码MD5Passwd, + * 然后调用userMapper的updateByPrimaryKeySelective方法,传入更新后的user对象,根据用户的主键(通常是用户ID)去数据库中更新对应的用户密码信息。 + * 该方法返回一个表示更新记录数量的整数result,如果result大于0,说明密码更新成功,通过ServerResponse.createBySuccessMessage方法创建并返回一个包含“修改密码成功”提示信息的ServerResponse对象给调用者, + * 告知调用者密码重置操作已成功完成。 + * + * 7. 返回密码重置失败结果 + * 如果密码更新操作没有成功(即result不大于0),则通过ServerResponse.createByErrorMessage方法返回一个包含“修改密码失败”提示信息的ServerResponse对象给调用者, + * 提示调用者密码重置操作出现问题,未成功完成。 + * + * @param username 要重置密码的用户名,不能为空,用于在数据库中查找对应的用户信息以及验证token等相关操作。 + * @param passwordNew 用户输入的新密码,不能为空,用于更新用户在数据库中的密码信息,且需要经过加密及重复性等校验。 + * @param forgetToken 用户在忘记密码流程中获取到的用于验证身份的token,不能为空,用于验证操作的合法性以及与Redis中缓存的token进行比对。 + * @return 返回一个ServerResponse对象,包含了重置密码操作的结果状态(如密码重置成功、token错误、密码重复等各种情况)以及相应的提示信息, + * 方便调用者根据返回结果了解密码重置操作是否成功,并进行相应的后续操作(如提示用户操作结果等)。 + */ + /** + * 此方法用于处理用户通过忘记密码流程重置密码的业务逻辑,具体步骤如下: + * + * 1. 参数校验 + * 首先使用StringUtils.isBlank方法对传入的用户名(username)、新密码(passwordNew)和忘记密码的token(forgetToken)这三个参数进行非空校验。 + * 如果其中任意一个参数为空字符串,意味着参数不完整,不符合密码重置的基本要求,此时通过ServerResponse.createByErrorMessage方法创建一个表示错误信息的ServerResponse对象, + * 返回“参数有误,修改密码操作失败”的提示信息给调用者,告知调用者参数存在问题,无法进行后续的密码重置操作。 + * + * 2. 查询用户信息 + * 在参数都通过非空校验后,调用userMapper的getUserByUsername方法,传入用户名作为参数去数据库中查询对应的用户信息,并将查询结果赋值给User类型的user对象。 + * 如果查询返回的user对象为null,说明数据库中不存在该用户名对应的用户,那么就无法进行密码重置操作, + * 通过ServerResponse.createByErrorMessage方法创建一个包含“用户名不存在,修改密码操作失败”提示信息的ServerResponse对象返回给调用者,告知调用者找不到对应的用户,重置密码操作不能继续。 + * + * 3. 验证token是否过期 + * 若成功查询到了用户信息(即user对象不为null),接着调用commonCacheUtil的getCacheValue方法,传入由Constants.TOKEN_PREFIX和用户名拼接而成的字符串作为key, + * 从Redis缓存中获取对应的token值,并将获取到的值赋给redisToken变量。 + * 如果redisToken为null,意味着在Redis中没有找到对应的token,可能是token已经过期或者不存在,此时通过ServerResponse.createByErrorMessage方法返回一个包含“token已经过期,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 提示调用者由于token问题无法进行密码重置操作,因为token在忘记密码流程中起着验证身份的关键作用,过期或不存在则无法确认操作合法性。 + * + * 4. 验证token一致性 + * 在成功从Redis获取到非空的redisToken后,使用StringUtils.equals方法比较前端传过来的forgetToken和从Redis中获取到的redisToken是否相等。 + * 如果两者不相等,说明token不一致,存在安全风险或者不符合操作流程要求,此时通过ServerResponse.createByErrorMessage方法返回一个包含“token错误,修改密码操作失败”提示信息的ServerResponse对象给调用者, + * 告知调用者token不匹配,不能继续进行密码重置操作,以此保证只有持有正确token的用户才能进行密码修改。 + * + * 5. 判断密码是否重复 + * 当token验证通过后,将传入的新密码passwordNew通过MD5Util.MD5EncodeUtf8方法进行MD5加密处理,得到加密后的密码MD5Passwd。 + * 然后使用user.getPassword().equals方法比较加密后的新密码MD5Passwd和数据库中存储的用户原密码(通过user对象获取)是否相等。 + * 如果两者相等,说明新密码与原密码重复,不符合密码修改的要求,此时通过ServerResponse.createByErrorMessage方法返回一个包含“不要使用重复密码!”提示信息的ServerResponse对象给调用者, + * 提示调用者不能使用重复密码进行重置操作,需要重新输入一个与原密码不同的新密码。 + * + * 6. 重置密码 + * 如果前面的各项验证(参数完整性、用户存在性、token有效性以及密码重复性校验)都通过,说明可以进行密码重置操作。 + * 首先通过user.setPassword方法将user对象的密码属性设置为加密后的新密码MD5Passwd,然后调用userMapper的updateByPrimaryKeySelective方法, + * 传入更新后的user对象,根据用户的主键(通常就是用户ID,此处通过前面查询用户时已确定对应的用户)去数据库中更新对应的用户密码信息。 + * 该方法会返回一个表示更新操作影响的记录数量的整数updateCount,如果updateCount大于0,说明密码更新成功, + * 通过ServerResponse.createBySuccessMessage方法创建并返回一个包含“修改密码成功”提示信息的ServerResponse对象给调用者,告知调用者密码重置操作已顺利完成。 + * + * 7. 返回密码重置失败结果 + * 如果密码更新操作没有成功(即updateCount不大于0),意味着在数据库更新密码时出现问题,可能是数据库连接异常、数据更新语句执行失败等原因, + * 此时通过ServerResponse.createByErrorMessage方法返回一个包含“修改密码失败”提示信息的ServerResponse对象给调用者,提示调用者密码重置操作没有达到预期效果,出现了故障。 + * + * @param username 要重置密码的用户名,由客户端传入,不能为空,用于在数据库中查找对应的用户信息以及后续与token相关的验证操作,是整个密码重置流程的关键标识之一。 + * @param passwordNew 用户输入的新密码,同样不能为空,是要更新到数据库中替换原密码的内容,且需要经过一系列校验(如重复性校验等)确保其合法性和安全性。 + * @param forgetToken 用户在忘记密码流程中获取到的用于验证身份的token,不能为空,用于和Redis缓存中的token进行比对以及确认本次密码重置操作的合法性。 + * @return 返回一个ServerResponse对象,包含了重置密码操作的结果状态(如密码重置成功、token错误、密码重复等各种情况)以及相应的提示信息, + * 方便调用者(如前端界面或者其他调用该服务的模块)根据返回结果知晓密码重置操作是否成功,并进行相应的后续处理(如提示用户操作结果、跳转到相应页面等)。 + */ @Override public ServerResponse forgetResetPasswd(String username, String passwordNew, String forgetToken) { //1.校验参数 - if(StringUtils.isBlank(username) || StringUtils.isBlank(passwordNew) || StringUtils.isBlank(forgetToken)){ + if (StringUtils.isBlank(username) || StringUtils.isBlank(passwordNew) || StringUtils.isBlank(forgetToken)) { return ServerResponse.createByErrorMessage("参数有误,修改密码操作失败"); } //2.根据username去获取用户 User user = userMapper.getUserByUsername(username); - if(user == null){ + if (user == null) { return ServerResponse.createByErrorMessage("用户名不存在,修改密码操作失败"); } //3.从redis中获取token,看是否超时 - String redisToken = commonCacheUtil.getCacheValue(Constants.TOKEN_PREFIX+username); - if(redisToken == null){ + String redisToken = commonCacheUtil.getCacheValue(Constants.TOKEN_PREFIX + username); + if (redisToken == null) { return ServerResponse.createByErrorMessage("token已经过期,修改密码操作失败"); } //4.看前端传过来的token与redis中取出来的token是否相等 - if(!StringUtils.equals(redisToken,forgetToken)){ + if (!StringUtils.equals(redisToken, forgetToken)) { return ServerResponse.createByErrorMessage("token错误,修改密码操作失败"); } //5.判断密码是否重复 String MD5Passwd = MD5Util.MD5EncodeUtf8(passwordNew); - if(user.getPassword().equals(MD5Passwd)){ + if (user.getPassword().equals(MD5Passwd)) { return ServerResponse.createByErrorMessage("不要使用重复密码!"); } //6.重置密码 user.setPassword(MD5Passwd); - int result = userMapper.updateByPrimaryKeySelective(user); - if(result > 0){ + int updateCount = userMapper.updateByPrimaryKeySelective(user); + if (updateCount > 0) { return ServerResponse.createBySuccessMessage("修改密码成功"); } return ServerResponse.createByErrorMessage("修改密码失败"); } - @Override - public ServerResponse resetPasswd(String passwordOld, String passwordNew, int userId) { - //1.校验参数 - if(StringUtils.isBlank(passwordOld) || StringUtils.isBlank(passwordNew)){ - return ServerResponse.createByErrorMessage("参数有误"); - } - User user = userMapper.selectByPrimaryKey(userId); - if (user == null){ - return ServerResponse.createByErrorMessage("无用户登陆"); - } - //2.校验老的密码 - String passwordOldMD5 = MD5Util.MD5EncodeUtf8(passwordOld); - if(!StringUtils.equals(passwordOldMD5,user.getPassword())){ - return ServerResponse.createByErrorMessage("老密码输错啦..."); - } - //3.重置新的密码 - user.setPassword(MD5Util.MD5EncodeUtf8(passwordNew)); - int updateCount = userMapper.updateByPrimaryKeySelective(user); - if(updateCount > 0){ - return ServerResponse.createBySuccessMessage("更新密码成功"); - } - return ServerResponse.createByErrorMessage("更新密码失败"); - } - + /** + * 该方法用于更新用户的信息,具体实现逻辑如下: + * + * 1. 获取当前登录用户 + * 首先调用userMapper的selectByPrimaryKey方法,传入用户ID(userId)作为参数去数据库中查询对应的用户信息,将查询结果赋值给User类型的user对象。 + * 如果查询返回的user对象为null,说明根据传入的用户ID在数据库中找不到对应的用户,可能是用户不存在或者传入的ID有误等原因, + * 此时通过ServerResponse.createByErrorMessage方法创建一个包含“获取当前登陆用户失败,请重新登陆”提示信息的ServerResponse对象返回给调用者, + * 告知调用者无法获取到要更新信息的用户,建议重新登录操作,以确保后续能正确获取到用户信息进行更新。 + * + * 2. 参数校验 + * 在成功获取到用户信息后,使用StringUtils.isBlank方法对传入的邮箱(email)、电话(phone)、问题(question)和答案(answer)这几个参数进行非空校验。 + * 如果这些参数中有任何一个为空字符串,意味着要更新的数据不完整,不符合更新用户信息的要求, + * 通过ServerResponse.createByErrorMessage方法创建一个包含“更新的数据不能存在空值!”提示信息的ServerResponse对象返回给调用者, + * 提示调用者需要提供完整的非空信息才能进行用户信息更新操作。 + * + * 3. 校验邮箱是否重复 + * 由于考虑到修改用户信息这个操作并发量不大,所以这里没有使用锁机制(与一些高并发场景下的操作有所区别)。 + * 接着调用userMapper的checkEmailValid方法,传入要更新的邮箱地址(email)和用户ID(userId)作为参数,去数据库中查询是否存在其他用户已经使用了该邮箱地址(排除当前要更新的这个用户本身)。 + * 如果查询返回的记录数量queryCount大于0,说明这个邮箱已经被其他用户占用了,不符合唯一性要求,此时不能使用该邮箱进行更新操作, + * 通过ServerResponse.createByErrorMessage方法创建一个包含“此邮箱已经被占用,换个试试~”提示信息的ServerResponse对象返回给调用者,告知调用者需要更换一个未被使用的邮箱地址。 + * + * 4. 构建更新用户对象并执行更新操作 + * 如果邮箱校验通过(即没有被其他用户占用),创建一个新的User对象updateUser,通过相应的set方法设置其ID为传入的用户ID(userId), + * 以及设置要更新的邮箱(email)、电话(phone)、问题(question)和答案(answer)等属性值,以此构建出一个包含了更新后信息的用户对象。 + * 然后调用userMapper的updateByPrimaryKeySelective方法,传入这个updateUser对象,根据用户的主键(userId)去数据库中更新对应的用户信息, + * 该方法会返回一个表示更新操作影响的记录数量的整数updateCount。 + * + * 5. 返回更新结果 + * 如果updateCount大于0,说明用户信息更新成功,通过ServerResponse.createBySuccessMessage方法创建并返回一个包含“更新信息成功”提示信息的ServerResponse对象给调用者, + * 告知调用者用户信息已成功更新,可以进行后续相关操作(如刷新页面展示新信息等)。 + * 如果updateCount不大于0,意味着在数据库更新用户信息时出现问题,可能是数据库连接异常、数据更新语句执行失败等原因, + * 通过ServerResponse.createByErrorMessage方法返回一个包含“更新用户信息失败”提示信息的ServerResponse对象给调用者,提示调用者用户信息更新操作没有达到预期效果,出现了故障。 + * + * @param email 要更新的用户邮箱地址,由客户端传入,不能为空(经过参数校验),且需要确保其在数据库中的唯一性(经过邮箱重复校验),用于更新用户在数据库中的邮箱信息。 + * @param phone 要更新的用户电话号码,同样不能为空,用于更新用户在数据库中的电话信息。 + * @param question 要更新的用户找回密码相关问题,不能为空,用于更新用户在数据库中设置的对应问题信息。 + * @param answer 要更新的用户找回密码相关问题的答案,不能为空,用于更新用户在数据库中设置的对应答案信息。 + * @param userId 用户的唯一标识,用于在数据库中准确查找对应的用户信息以及作为更新操作的主键依据,确保更新的是正确的用户记录。 + * @return 返回一个ServerResponse对象,包含了更新用户信息操作的结果状态(如更新成功、邮箱重复、更新失败等各种情况)以及相应的提示信息, + * 方便调用者根据返回结果知晓用户信息更新操作是否成功,并进行相应的后续处理(如提示用户操作结果、刷新页面等)。 + */ @Override public ServerResponse updateInfomation(String email, String phone, String question, String answer, Integer userId) { //1.获取当前登陆用户 User user = userMapper.selectByPrimaryKey(userId); - if (user == null){ + if (user == null) { return ServerResponse.createByErrorMessage("获取当前登陆用户失败,请重新登陆"); } //2.校验参数 - if(StringUtils.isBlank(email) || StringUtils.isBlank(phone) || StringUtils.isBlank(question) || StringUtils.isBlank(answer)){ + if (StringUtils.isBlank(email) || StringUtils.isBlank(phone) || StringUtils.isBlank(question) || StringUtils.isBlank(answer)) { return ServerResponse.createByErrorMessage("更新的数据不能存在空值!"); } //2.修改用户信息应该并发不大,所以不用加锁了,这里校验邮箱是否重复 - Integer queryCount = userMapper.checkEmailValid(email,userId); - if(queryCount > 0){ + Integer queryCount = userMapper.checkEmailValid(email, userId); + if (queryCount > 0) { //说明这个邮箱已经被其他用户占用了,所以不能使用 return ServerResponse.createByErrorMessage("此邮箱已经被占用,换个试试~"); } @@ -276,18 +557,32 @@ public class UserServiceImpl implements IUserService{ int updateCount = userMapper.updateByPrimaryKeySelective(updateUser); - if(updateCount > 0){ + if (updateCount > 0) { return ServerResponse.createBySuccessMessage("更新信息成功"); } return ServerResponse.createByErrorMessage("更新用户信息失败"); } + /** + * 此方法用于从数据库中根据用户ID获取用户的详细信息,并封装到UserResVO对象中返回,具体实现过程如下: + * + * 首先创建一个UserResVO类型的对象userResVO,用于封装要返回的用户信息。 + * 接着调用userMapper的selectByPrimaryKey方法,传入用户ID(userId)作为参数去数据库中查询对应的用户信息,将查询结果赋值给User类型的userDB对象。 + * 如果查询返回的userDB对象不为null,说明找到了对应的用户记录,此时将用户的相关信息(如用户ID、用户名、邮箱、角色、电话、问题、答案、创建时间、更新时间等) + * 通过userResVO对象的相应set方法分别设置到该对象的对应属性中,构建出一个包含完整用户信息的UserResVO对象。 + * 最后将这个封装好用户信息的userResVO对象返回给调用者,方便调用者(如前端界面或者其他业务逻辑层代码)获取用户详细信息进行后续的展示、处理等操作。 + * 如果查询返回的userDB对象为null,意味着没有找到对应的用户记录,此时直接返回一个空的UserResVO对象,调用者可以根据返回的对象情况进行相应的判断和处理。 + * + * @param userId 用户的唯一标识,用于在数据库中准确查找对应的用户信息,不能为空,是获取用户详细信息的关键依据。 + * @return 返回一个UserResVO对象,该对象封装了从数据库中查询到的对应用户ID的用户详细信息(如果用户存在),方便调用者进行后续相关操作, + * 若用户不存在则返回一个空的UserResVO对象,调用者可据此判断并做相应处理。 + */ @Override public UserResVO getUserInfoFromDB(Integer userId) { UserResVO userResVO = new UserResVO(); User userDB = userMapper.selectByPrimaryKey(userId); - if(userDB != null){ + if (userDB!= null) { userResVO.setId(userId); userResVO.setUsername(userDB.getUsername()); userResVO.setEmail(userDB.getEmail()); @@ -299,7 +594,4 @@ public class UserServiceImpl implements IUserService{ userResVO.setUpdateTime(userDB.getUpdateTime()); } return userResVO; - } - - -} + } \ No newline at end of file