SpingBoot的项目实战--模拟电商【2.登录】
🥳🥳Welcome Huihui's Code World ! !🥳🥳
接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧
目录
🥳🥳Welcome Huihui's Code World ! !🥳🥳
一.功能需求
二.代码编写
1.登录功能的完成
2.全局异常的处理
3.登录密码的两次加密
(1)为什么要加密两次
(2)整体的加密流程
(3)具体流程的代码实现
①前端加密
②加密之后传到后端
③后端拿取用户信息
④后端加密
⑤登录测试
4.前后端表单验证
(1)引入依赖
(2)实体类/Vo类加上注解
(3)Controller层加注解
(4)自定义JSR 303注解
①编写自定义注解类
②编写自定义注解的注解解析类
③配置注解解析类
④在实体类中使用自定义注解
5.登录状态的更换
一.功能需求
①完成用户登录功能
②用户的各种错误操作都需要给出相应的错误提示,而不是抛出异常【全局异常处理】
③用户登录的密码的两次加密
表单数据➡后端【加密】
后端数据➡数据库【加密】
④用户输入表单时,前端【表单验证】和后端【JSR303】都需要有相对应的验证
⑤用户登录成功之后,需要在首页显示出登录的用户的昵称【Redis+Cookie】
二.代码编写
1.登录功能的完成
package com.wh.easyshop.service.impl;import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.vo.UserVo; import org.springframework.stereotype.Service;import javax.swing.*;/*** <p>* 用户信息表 服务实现类* </p>** @author wh* @since 2023-12-27*/ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {/*** 登录* @param userVo* @return*/public JsonResponseBody<?> login (UserVo userVo){//如果传过来的电话号码(登录名)为空,那么提示用户相应的信息if(userVo.getMobile()==null){return JsonResponseBody.other(JsonResponseStatus.LOGIN_MOBILE_INFO);}//如果传过来的密码为空,那么提示用户相应的信息if(userVo.getPassword()==null){return JsonResponseBody.other(JsonResponseStatus.LOGIN_PASSWORD_INFO);}User user = getOne(new QueryWrapper<User>().lambda()//判断用户名以及密码是否一致.eq(User::getId, userVo.getMobile()).eq(User::getPassword, userVo.getPassword()));//如果内容匹配不成功,那么提示用户相应的信息if(user==null){return JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS);}//如果带来的信息都一致,则提示成功return JsonResponseBody.success();} }其中用到的响应类
package com.wh.easyshop.resp;import lombok.Data;@Data public class JsonResponseBody<T> {private Integer code;private String msg;private T data;private Long total;private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data) {this.code = jsonResponseStatus.getCode();this.msg = jsonResponseStatus.getMsg();this.data = data;}private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data, Long total) {this.code = jsonResponseStatus.getCode();this.msg = jsonResponseStatus.getMsg();this.data = data;this.total = total;}public static <T> JsonResponseBody<T> success() {return new JsonResponseBody<T>(JsonResponseStatus.OK, null);}public static <T> JsonResponseBody<T> success(T data) {return new JsonResponseBody<T>(JsonResponseStatus.OK, data);}public static <T> JsonResponseBody<T> success(T data, Long total) {return new JsonResponseBody<T>(JsonResponseStatus.OK, data, total);}public static <T> JsonResponseBody<T> unknown() {return new JsonResponseBody<T>(JsonResponseStatus.UN_KNOWN, null);}public static <T> JsonResponseBody<T> other(JsonResponseStatus jsonResponseStatus) {return new JsonResponseBody<T>(jsonResponseStatus, null);}}package com.wh.easyshop.resp;import lombok.Getter;@Getter public enum JsonResponseStatus {OK(200, "OK"),UN_KNOWN(500, "未知错误"),LOGIN_MOBILE_INFO(5001, "未携带手机号或手机号格式有误"),LOGIN_PASSWORD_INFO(5002, "未携带密码或不满足格式"),LOGIN_NO_EQUALS(5003, "登录信息不一致"),LOGIN_MOBILE_NOT_FOUND(5004, "登录手机号未找到"),;private final Integer code;private final String msg;JsonResponseStatus(Integer code, String msg) {this.code = code;this.msg = msg;}public String getName(){return this.name();}}我这里为了规范,还建了一个vo类,而且也考虑到后面需要做验证,为了不污染实体类与数据库的连接,还是需要建一个vo类的
VO类:
数据传输:VO类可以用于封装客户端和服务器之间的数据传输,例如RESTful API的请求和响应对象。
数据库实体映射:VO类可以用于将数据库表的记录映射为Java对象,并进行数据的读取和存储操作。
领域模型中的值对象:在领域驱动设计(DDD)中,VO类可以用于表示领域模型中的值对象,如金额、日期范围等。
总之,VO类主要用于封装和传递数据,以提高代码的可读性、可维护性和可测试性。它们通常是不可变的,并且只包含属性和访问方法
package com.wh.easyshop.vo;import lombok.Data;@Data public class UserVo {private String mobile;private String password;}但是上面的这个登录功能的代码只是很粗浅的,我们还需要将它进行升级
2.全局异常的处理
一个用户在输入自己的信息进行登录的时候,很可能会有一些非常规操作,一般这个时候,它会有一些错误以及异常抛出,我们可以使用全局异常进行处理
全局异常:
方便错误排查和日志记录:全局异常处理可以捕捉并记录异常信息,方便开发人员进行错误排查和系统故障分析。
提供友好的用户体验:通过合理的异常处理,可以向用户提供友好的错误提示,帮助他们理解发生的问题,并提供相应的解决方案
我先把原先的代码进行了修改,将前面有错误提示信息的地方,都换成异常【自定义异常】给它抛出
自定义异常
package com.wh.easyshop.exception;import com.wh.easyshop.resp.JsonResponseStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor;@EqualsAndHashCode(callSuper = true) @AllArgsConstructor @NoArgsConstructor @Data public class BusinessException extends RuntimeException {private JsonResponseStatus jsonResponseStatus;}
但是这个异常抛出了,我们需要一个类来处理它--全局异常处理类,编写全局异常处理类,需要 用到一个注解@
RestControllerAdvice@RestControllerAdvice:
@RestControllerAdvice是 Spring 框架中的一个注解,用于定义全局异常处理器(Global Exception Handler)。在 Spring MVC 中,当应用程序抛出异常时,可以使用
@ExceptionHandler注解来处理该异常。但是,如果在多个控制器中都有相同的异常处理逻辑,那么需要在每个控制器中都编写相同的代码,这样会导致代码冗余和可维护性差。
@RestControllerAdvice的作用就是解决这个问题,它结合了@ControllerAdvice和@ResponseBody两个注解的功能,用于全局处理控制器抛出的异常,并返回相应的错误信息。使用
@RestControllerAdvice注解的类可以包含多个被@ExceptionHandler注解修饰的方法,每个方法用于处理不同类型的异常。当应用程序抛出异常时,Spring 框架会根据异常的类型自动调用对应的异常处理方法。
@RestControllerAdvice类中的异常处理方法可以包含自定义的逻辑,比如记录日志、返回特定的错误信息等。通常,异常处理方法会返回一个包含错误信息的响应实体,供客户端进行处理。总之,
@RestControllerAdvice注解用于定义全局异常处理器,通过集中处理控制器抛出的异常,提高代码的可维护性和复用性。它可以在一个类中定义多个异常处理方法,根据异常类型自动调用相应的方法,并返回相应的错误信息。【其实简而言之,就是当controller抛出异常的时候,不会往外面抛了,这个注解相当于是@Controller的增强类,把@Controller给包裹起来了】全局异常的编写
package com.wh.easyshop.exception;import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Objects;@RestControllerAdvice // 声明这是一个全局异常处理器类 @Slf4j // 使用log4j进行日志记录 public class GlobalExceptionHandler {// 处理业务异常@ExceptionHandler(BusinessException.class)public JsonResponseBody<?> exceptionBusinessException(BusinessException e) {JsonResponseStatus status = e.getJsonResponseStatus(); // 获取异常的状态信息log.info(status.getMsg()); // 记录日志return JsonResponseBody.other(status); // 返回状态信息}// 处理其他类型的异常@ExceptionHandler(Throwable.class)public JsonResponseBody<?> exceptionThrowable(Throwable e) {log.info(e.getMessage()); // 记录日志return JsonResponseBody.other(JsonResponseStatus.UN_KNOWN); // 返回未知状态信息}}3.登录密码的两次加密
(1)为什么要加密两次
第一次加密防止前端传递数据时被截取
第二次加密防止数据库泄露
(2)整体的加密流程
MD5(MD5(pass明文+固定salt)+随机salt)
第一次固定salt写死在前端
第二次加密采用随机的salt 并将每次生成的salt保存在数据库中(3)具体流程的代码实现
①前端加密
对用户输入的密码进行md5加密(固定的salt)
引入md5的js
<script src="http://www.gongjuji.net/Content/files/jquery.md5.js" type="text/javascript"></script>使用MD5加密
<script>$("#login").click(()=>{let mobile=$("#mobile").val()let password=$("#password").val()password=$.md5(password)$.post(' ${springMacroRequestContext.contextPath}/user/login',{mobile,password},resp=>{},"json")})</script>但是我们知道MD5它的加密方式是不可逆的,也很容易被解析,所以我们可以自己加盐进去,在前后都加上字符
②加密之后传到后端
将加密后的密码传递到后端
package com.wh.easyshop.controller;import com.sun.corba.se.spi.orb.ParserImplBase; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** <p>* 用户信息表 前端控制器* </p>** @author wh* @since 2023-12-27*/ @RestController @RequestMapping("/user") public class UserController {@Autowiredprivate IUserService userService;@RequestMapping("/login")public JsonResponseBody<?> login(UserVo userVo){return userService.login(userVo);}}③后端拿取用户信息
使用用户id取出用户信息
④后端加密
后端对前端传过来的加密后的密码在进行md5加密(取出盐),然后与数据库中存储的密码进行对比
package com.wh.easyshop.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.wh.easyshop.exception.BusinessException; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.util.MD5Utils; import com.wh.easyshop.vo.UserVo; import org.springframework.stereotype.Service;import javax.swing.*;/*** <p>* 用户信息表 服务实现类* </p>** @author wh* @since 2023-12-27*/ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {/*** 登录* @param userVo* @return*/@Overridepublic JsonResponseBody<?> login (UserVo userVo){//通过用户名拿到用户的信息User user = getOne(new QueryWrapper<User>().lambda().eq(User::getId, userVo.getMobile()), false);//如果内容匹配不成功,那么提示用户相应的信息if(user==null){throw new BusinessException(JsonResponseStatus.LOGIN_MOBILE_NOT_FOUND);}//把数据库的盐值与前端的密码都拿出来,再加密一次【随机盐值】String secret = MD5Utils.formPassToDbPass(userVo.getPassword(), user.getSalt());//将数据库里面的密码与上面二次加密之后的密码进行比较,如果不一致就提示相应信息if(!user.getPassword().equals(secret)){throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);}//如果带来的信息都一致,则提示成功return JsonResponseBody.success();}}⑤登录测试
🔺登录与注册之间的逻辑大概也是差不多的,再这里小编没有做注册了,但是没有做注册,我们的数据库中就没有数据,所以我们要使用debug将数据手动的加到数据库中,不然这个登录就永远都是失败的
把盐值拿到也放到数据库中,这里我用的是固定的盐值,大家也可以用时间戳等一些随机的不会重复的数字
package com.wh.easyshop.util;import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils;import java.nio.charset.StandardCharsets; import java.util.UUID;@Component public class MD5Utils {//加密盐,与前端一致private static final String salt = "f1g2h3j4";public static String md5(String src) {return DigestUtils.md5DigestAsHex(src.getBytes(StandardCharsets.UTF_8));}public static String createSalt() {return UUID.randomUUID().toString().replace("-", "");}/*** 将前端的明文密码通过MD5加密方式加密成后端服务所需密码,混淆固定盐salt,安全性更可靠*/public static String inputPassToFormPass(String inputPass) {String str = salt.charAt(1) + String.valueOf(salt.charAt(5)) + inputPass + salt.charAt(0) + salt.charAt(3);return md5(str);}/*** 将后端密文密码+随机salt生成数据库的密码,混淆固定盐salt,安全性更可靠*/public static String formPassToDbPass(String formPass, String salt) {String str = salt.charAt(7) + String.valueOf(salt.charAt(9)) + formPass + salt.charAt(1) + salt.charAt(5);return md5(str);}public static void main(String[] args) {String formPass = inputPassToFormPass("123456");System.out.println("前端加密密码:" + formPass);String salt = createSalt();System.out.println("后端加密随机盐:" + salt);String dbPass = formPassToDbPass(formPass, salt);System.out.println("后端加密密码:" + dbPass);}}
4.前后端表单验证
我们为了规范用户的输入,常常会在前端的表单中进行验证,如果用户输入不规范,会在页面中显示出相应的提示,去引导用户输入规范的数据,但我们也可能会遇到这样的一种情况:有的用户不使用我们的表单进行提交,而是使用API测试工具,那么就有可能输入不规范的数据,如果这个时候我们再后端没有进行验证的话,不规范的数据便会进入的数据库中。为了避免这种情况,我们也需要在后端进行验证。这里我们使用的是JSR 303验证框架。
JSR 303:
全称为 Java Specification Request 303,是Java平台上的一个规范,用于定义面向对象的验证框架,也被称为Bean Validation(Bean验证)。
JSR 303提供了一套标准的注解,开发人员可以通过在Java类的字段、方法或者参数上添加这些注解来定义验证规则。这些注解用于描述字段的数据类型、长度、非空约束、正则表达式等验证条件。在运行时,验证框架会根据这些注解自动进行验证,并将验证结果返回给开发人员。【如果我们不使用这个框架进行验证的话,那么我们就需要在调用业务方法之前,手动的去定义每一个属性的规范】
使用JSR 303验证框架的步骤:
(1)引入依赖
<!--jsr303--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>(2)实体类/Vo类加上注解
关于JSR 303,小编之前也写了一篇文章,有兴趣的可以戳进去看看
package com.wh.easyshop.vo;import lombok.Data;import javax.validation.constraints.NotBlank;@Data public class UserVo {@NotBlankprivate String mobile;@NotBlankprivate String password;}(3)Controller层加注解
package com.wh.easyshop.controller;import com.sun.corba.se.spi.orb.ParserImplBase; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;/*** <p>* 用户信息表 前端控制器* </p>** @author wh* @since 2023-12-27*/ @RestController @RequestMapping("/user") public class UserController {@Autowiredprivate IUserService userService;@RequestMapping("/login")@ResponseBodypublic JsonResponseBody<?> login(@Valid UserVo userVo){return userService.login(userVo);}}这时候就代表已经将校验开启了,如果用户在表单/API测试工具不输入值,都会返回一个未知错误(全局异常类)
但这样返回的是一个未知错误,对于我们开发人员来说,是不好的,所以我们在全局异常中,再写一个处理绑定异常的方法
package com.wh.easyshop.exception;import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Objects;@RestControllerAdvice // 声明这是一个全局异常处理器类 @Slf4j // 使用log4j进行日志记录 public class GlobalExceptionHandler {// 处理业务异常@ExceptionHandler(BusinessException.class)public JsonResponseBody<?> exceptionBusinessException(BusinessException e) {JsonResponseStatus status = e.getJsonResponseStatus(); // 获取异常的状态信息log.info(status.getMsg()); // 记录日志return JsonResponseBody.other(status); // 返回状态信息}// 处理其他类型的异常@ExceptionHandler(Throwable.class)public JsonResponseBody<?> exceptionThrowable(Throwable e) {log.info(e.getMessage()); // 记录日志return JsonResponseBody.other(JsonResponseStatus.UN_KNOWN); // 返回未知状态信息}// 处理绑定异常@ExceptionHandler(BindException.class)public JsonResponseBody<?> exceptionThrowable(BindException e) {log.info(e.getMessage()); // 记录日志return JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS); // 返回未知状态信息}}那么这时候,就不再是未知错误了,而是我们写好的一个错误枚举中的错误信息
我们在编写项目的过程中,也可能遇到一些特殊的需求,可能JSR 303中原有的注解不能够完成这些特殊的需求,那么我们便可以自己定义JSR 303的注解
(4)自定义JSR 303注解
①编写自定义注解类
package com.wh.easyshop.util;import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/*** @author是辉辉啦* @create 2023-12-31-11:08*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Constraint(validatedBy = {MatchExprConstraintValidator.class}) public @interface MatchExpr {/*** 自定义的部分* @return*/boolean require() default true;//是否必填String expr() default "";//填的内容是什么(正则)/*** 使用jsr 303必须要的部分* @return*/String message() default "{javax.validation.constraints.NotBlank.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}②编写自定义注解的注解解析类
package com.wh.easyshop.util;import com.baomidou.mybatisplus.core.toolkit.StringUtils; import lombok.Data;import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Map;/*** MatchExpr的注解解析类*/ @Data public class MatchExprConstraintValidator implements ConstraintValidator<MatchExpr, String> {// 是否必须匹配private boolean require;// 正则表达式private String expr;@Overridepublic void initialize(MatchExpr matchExpr) {// 初始化时获取注解中的参数值expr = matchExpr.expr();require = matchExpr.require();}@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {// 如果不需要匹配,直接返回trueif (!require) return true;// 如果值为空,根据require的值返回false或trueif (StringUtils.isEmpty(value)) return false;// 使用正则表达式进行匹配,返回匹配结果return value.matches(expr);}}③配置注解解析类
@Constraint(validatedBy = {MatchExprConstraintValidator.class})
④在实体类中使用自定义注解
package com.wh.easyshop.vo;import com.wh.easyshop.util.MatchExpr; import lombok.Data;import javax.validation.constraints.NotBlank;/*** 用户视图对象*/ @Data public class UserVo {/*** 手机号码,使用正则表达式进行验证* @param mobile 手机号码*/@MatchExpr(require = true,expr = "(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}")private String mobile;/*** 密码,不能为空* @param password 密码*/@NotBlankprivate String password; }
如果此时输入的是正确的手机号
那么就不会进入断点,不会抛出那个绑定异常
出现的错误也只是手机号未找到
在项目中,为了方便编码以及后续的优化修复,通常是不会在代码中出现自定义的变量,所以这里我们编写一个常量类,把我们自定义的变量放入其中进行统一管理
package com.wh.easyshop.util;/*** 常量类*/ public abstract class Constants { //手机号正则匹配public static final String EXPR_MOBILE = "(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}"; //密码规则匹配public static final String EXPR_PASSWORD = "[a-zA-Z0-9]{32}";}修改前的代码
修改后的代码
5.登录状态的更换
用户没有登录时,在首页的登录按钮那里就是登录,如果用户已经登录了,那么便显示当前用户的昵称,这里是将用户的信息存储到redis中,
但又因为这个项目是单体项目,所以我们得想办法让用户能够获取到redis中的用户信息,这里我们又两种方式,可以用session以及cookie。但是session是存在服务器的,如果我们将用户的信息存入到session中,那么会让服务器承受过大的压力,所以这里我选择使用cookie来存放。
但这里还存在一个问题:我们是直接将redis中的所有的信息都直接放大cookie中吗?如果是这样的话,那么我们都不必要使用redis了,我们应该是需要哪个用户的信息,就拿到的特殊的标识去redis中取对应用户的信息。说到特殊标识,我们应该想到的是用户id,但如果使用用户id来拿,又会非常的不安全【id通常为主键,可以通过id做许多其他的操作】,所以我们可以手动生成一个唯一标识,这里我是用的雪花id,通过雪花id去拿到redis中的用户信息。
package com.wh.easyshop.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.github.yitter.idgen.YitIdHelper; import com.sun.deploy.net.HttpResponse; import com.wh.easyshop.exception.BusinessException; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IRedisService; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.util.Constants; import com.wh.easyshop.util.CookieUtils; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpRequest; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.swing.*; import java.util.Date;/*** <p>* 用户信息表 服务实现类* </p>** @author wh* @since 2023-12-27*/ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Autowiredprivate RedisTemplate redisTemplate;@AutowiredIRedisService redisService;/*** 登录* @param userVo* @return*/@Overridepublic JsonResponseBody<?> login (UserVo userVo, HttpServletRequest request, HttpServletResponse response){//通过用户名拿到用户的信息User user = getOne(new QueryWrapper<User>().lambda().eq(User::getId, userVo.getMobile()), false);//如果内容匹配不成功,那么提示用户相应的信息if(user==null){throw new BusinessException(JsonResponseStatus.LOGIN_MOBILE_NOT_FOUND);}//把数据库的盐值与前端的密码都拿出来,再加密一次【随机盐值】 // String timestamp = System.currentTimeMillis()+"";//时间戳String secret =user.getSalt()+userVo.getPassword();//将数据库密码与时间戳拼接起来String s = DigestUtils.md5DigestAsHex((secret).getBytes());//将数据库里面的密码与上面二次加密之后的密码进行比较,如果不一致就提示相应信息if(!user.getPassword().equals(s)){throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);}//将个人信息放入到redis中【到时候可以根据这个token去拿到用户的信息】String token=YitIdHelper.nextId()+"";redisService.setUserToRedis(token,user);//把token中的内容存到cookie中去传给用户CookieUtils.setCookie(request,response,"UserToken",token,7200);//令牌【redis】CookieUtils.setCookie(request,response,"nickname",user.getNickname(),7200);//用户昵称//如果带来的信息都一致,则提示成功return JsonResponseBody.success();}}这里使用redis还需要导入pom依赖
<!-- Redis 相关依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.5.6</version></dependency>使用了雪花id也要导入依赖呢
<!--雪花ID--><dependency><groupId>com.github.yitter</groupId><artifactId>yitter-idgenerator</artifactId><version>1.0.6</version></dependency>其中使用了cookie进行存储,我这里也写了一个工具类【方便对cookie进行操作】
package com.wh.easyshop.util;import lombok.extern.slf4j.Slf4j;import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder;@Slf4j public class CookieUtils {/*** @Description: 得到Cookie的值, 不编码*/public static String getCookieValue(HttpServletRequest request, String cookieName) {return getCookieValue(request, cookieName, false);}/*** @Description: 得到Cookie的值*/public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {Cookie[] cookieList = request.getCookies();if (cookieList == null || cookieName == null) {return null;}String retValue = null;try {for (int i = 0; i < cookieList.length; i++) {if (cookieList[i].getName().equals(cookieName)) {if (isDecoder) {retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");} else {retValue = cookieList[i].getValue();}break;}}} catch (UnsupportedEncodingException e) {e.printStackTrace();}return retValue;}/*** @Description: 得到Cookie的值*/public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {Cookie[] cookieList = request.getCookies();if (cookieList == null || cookieName == null) {return null;}String retValue = null;try {for (int i = 0; i < cookieList.length; i++) {if (cookieList[i].getName().equals(cookieName)) {retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);break;}}} catch (UnsupportedEncodingException e) {e.printStackTrace();}return retValue;}/*** @Description: 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码*/public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,String cookieValue) {setCookie(request, response, cookieName, cookieValue, -1);}/*** @param request* @param response* @param cookieName* @param cookieValue* @param cookieMaxage* @Description: 设置Cookie的值 在指定时间内生效,但不编码*/public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,String cookieValue, int cookieMaxage) {setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);}/*** @Description: 设置Cookie的值 不设置生效时间,但编码* 在服务器被创建,返回给客户端,并且保存客户端* 如果设置了SETMAXAGE(int seconds),会把cookie保存在客户端的硬盘中* 如果没有设置,会默认把cookie保存在浏览器的内存中* 一旦设置setPath():只能通过设置的路径才能获取到当前的cookie信息*/public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,String cookieValue, boolean isEncode) {setCookie(request, response, cookieName, cookieValue, -1, isEncode);}/*** @Description: 设置Cookie的值 在指定时间内生效, 编码参数*/public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,String cookieValue, int cookieMaxage, boolean isEncode) {doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);}/*** @Description: 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)*/public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,String cookieValue, int cookieMaxage, String encodeString) {doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);}/*** @Description: 删除Cookie带cookie域名*/public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,String cookieName) {doSetCookie(request, response, cookieName, null, -1, false);}/*** @Description: 设置Cookie的值,并使其在指定时间内生效*/private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {try {if (cookieValue == null) {cookieValue = "";} else if (isEncode) {cookieValue = URLEncoder.encode(cookieValue, "utf-8");}Cookie cookie = new Cookie(cookieName, cookieValue);if (cookieMaxage > 0)cookie.setMaxAge(cookieMaxage);if (null != request) {// 设置域名的cookieString domainName = getDomainName(request);log.info("========== domainName: {} ==========", domainName);if (!"localhost".equals(domainName)) {cookie.setDomain(domainName);}}cookie.setPath("/");response.addCookie(cookie);} catch (Exception e) {e.printStackTrace();}}/*** @Description: 设置Cookie的值,并使其在指定时间内生效*/private static void doSetCookie(HttpServletRequest request, HttpServletResponse response,String cookieName, String cookieValue, int cookieMaxage, String encodeString) {try {if (cookieValue == null) {cookieValue = "";} else {cookieValue = URLEncoder.encode(cookieValue, encodeString);}Cookie cookie = new Cookie(cookieName, cookieValue);if (cookieMaxage > 0)cookie.setMaxAge(cookieMaxage);if (null != request) {// 设置域名的cookieString domainName = getDomainName(request);log.info("========== domainName: {} ==========", domainName);if (!"localhost".equals(domainName)) {cookie.setDomain(domainName);}}cookie.setPath("/");response.addCookie(cookie);} catch (Exception e) {e.printStackTrace();}}/*** @Description: 得到cookie的域名*/private static String getDomainName(HttpServletRequest request) {String domainName = null;String serverName = request.getRequestURL().toString();if (serverName == null || serverName.equals("")) {domainName = "";} else {serverName = serverName.toLowerCase();serverName = serverName.substring(7);final int end = serverName.indexOf("/");serverName = serverName.substring(0, end);if (serverName.indexOf(":") > 0) {String[] ary = serverName.split("\\:");serverName = ary[0];}final String[] domains = serverName.split("\\.");int len = domains.length;if (len > 3 && !isIp(serverName)) {// www.xxx.com.cndomainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];} else if (len <= 3 && len > 1) {// xxx.com or xxx.cndomainName = "." + domains[len - 2] + "." + domains[len - 1];} else {domainName = serverName;}}return domainName;}public static String trimSpaces(String IP) {//去掉IP字符串前后所有的空格while (IP.startsWith(" ")) {IP = IP.substring(1, IP.length()).trim();}while (IP.endsWith(" ")) {IP = IP.substring(0, IP.length() - 1).trim();}return IP;}public static boolean isIp(String IP) {//判断是否是一个IPboolean b = false;IP = trimSpaces(IP);if (IP.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) {String s[] = IP.split("\\.");if (Integer.parseInt(s[0]) < 255)if (Integer.parseInt(s[1]) < 255)if (Integer.parseInt(s[2]) < 255)if (Integer.parseInt(s[3]) < 255)b = true;}return b;}}还有我们将数据存入到redis中的时候,通常会带有很多的前缀和后缀,为了便于我们操作,我们可以把其中的前后缀都给去除
package com.wh.easyshop.util;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration // 声明这是一个配置类 public class RedisConfig {@Bean // 声明这是一个Spring Bean,用于创建RedisTemplate实例public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {// 创建RedisTemplate实例RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// 设置键的序列化器为StringRedisSerializerredisTemplate.setKeySerializer(new StringRedisSerializer());// 设置值的序列化器为GenericJackson2JsonRedisSerializerredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置哈希表键的序列化器为StringRedisSerializerredisTemplate.setHashKeySerializer(new StringRedisSerializer());// 设置哈希表值的序列化器为GenericJackson2JsonRedisSerializerredisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置连接工厂redisTemplate.setConnectionFactory(connectionFactory);// 初始化RedisTemplateredisTemplate.afterPropertiesSet();// 返回RedisTemplate实例return redisTemplate;}}在编码的时候,发现那一段把用户的信息保存到redis中的代码可能会在许多地方用到【并且也考虑到后续要将用户信息拿出】,所以将方法都封装起来了,方面以后的调用以及修改
package com.wh.easyshop.service;import com.wh.easyshop.model.User;public interface IRedisService {/*** 将登陆User对象保存到Redis中,并以Token为键*/void setUserToRedis(String token, User user);/*** 根据token令牌获取redis中存储的user对象*/User getUserByToken(String token);}package com.wh.easyshop.service;import com.wh.easyshop.model.User; import com.wh.easyshop.util.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class RedisServiceImpl implements IRedisService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void setUserToRedis(String token, User user) {redisTemplate.opsForValue().set(Constants.REDIS_USER_PREFIX + token, user, 7200, TimeUnit.SECONDS);}@Overridepublic User getUserByToken(String token) {return (User) redisTemplate.opsForValue().get(Constants.REDIS_USER_PREFIX + token);}}其中也用到了常量类
接着就是前端的内容显示了
效果
代码
<script type="text/javascript" src="js/jquery-1.12.4.min.js"></script> <script>$(function(){let nickname=getCookie("nickname");if(null!=nickname&&''!=nickname&&undefined!=nickname) {//设置昵称$('#nickname').text("您好,"+nickname);//隐藏登录注册按钮$('p.fl>span:eq(1)').css("display","none");//显示昵称和退出按钮$('p.fl>span:eq(0)').css("display","block");}else{//隐藏昵称$('#nickname').text("");//显示登录注册按钮$('p.fl>span:eq(1)').css("display","block");//隐藏昵称和退出按钮$('p.fl>span:eq(0)').css("display","none");}});function getCookie(cname) {var name = cname + "=";var decodedCookie = decodeURIComponent(document.cookie);var ca = decodedCookie.split(';');for(var i = 0; i <ca.length; i++) {var c = ca[i];while (c.charAt(0) == ' ') {c = c.substring(1);}if (c.indexOf(name) == 0) {return c.substring(name.length, c.length);}}return "";} </script> <div class="head"><div class="wrapper clearfix"><div class="clearfix" id="top"><h1 class="fl"><a href="${ctx}/"><img src="img/logo.png"/></a></h1><div class="fr clearfix" id="top1"><p class="fl"><span><span id="nickname"></span><a href="${ctx}/user/userLogout">退出</a></span><span style="display: none"><a href="${ctx}/page/login.html" id="login">登录</a><a href="${ctx}/page/reg.html" id="reg">注册</a></span></p><form action="#" method="get" class="fl"><input type="text" placeholder="热门搜索:干花花瓶" /><input type="button" /></form><div class="btn fl clearfix"><a href="${ctx}/page/mygxin.html"><img src="img/grzx.png"/></a><a href="#" class="er1"><img src="img/ewm.png"/></a><a href="${ctx}/shopCar/queryShopCar"><img src="img/gwc.png"/></a><p><a href="#"><img src="img/smewm.png"/></a></p></div></div></div><ul class="clearfix" id="bott"><li><a href="${ctx}/">首页</a></li><li><a href="#">所有商品</a><div class="sList"><div class="wrapper clearfix"><a href="${ctx}/page/paint.html"><dl><dt><img src="img/nav1.jpg"/></dt><dd>浓情欧式</dd></dl></a><a href="${ctx}/page/paint.html"><dl><dt><img src="img/nav2.jpg"/></dt><dd>浪漫美式</dd></dl></a><a href="${ctx}/page/paint.html"><dl><dt><img src="img/nav3.jpg"/></dt><dd>雅致中式</dd></dl></a><a href="${ctx}/page/paint.html"><dl><dt><img src="img/nav6.jpg"/></dt><dd>简约现代</dd></dl></a><a href="${ctx}/page/paint.html"><dl><dt><img src="img/nav7.jpg"/></dt><dd>创意装饰</dd></dl></a></div></div></li><li><a href="${ctx}/page/flowerDer.html">装饰摆件</a><div class="sList2"><div class="clearfix"><a href="${ctx}/page/proList.html">干花花艺</a><a href="${ctx}/page/vase_proList.html">花瓶花器</a></div></div></li><li><a href="${ctx}/page/decoration.html">布艺软饰</a><div class="sList2"><div class="clearfix"><a href="${ctx}/page/zbproList.html">桌布罩件</a><a href="${ctx}/page/bzproList.html">抱枕靠垫</a></div></div></li><li><a href="${ctx}/page/paint.html">墙式壁挂</a></li><li><a href="${ctx}/page/perfume.html">蜡艺香薰</a></li><li><a href="${ctx}/page/idea.html">创意家居</a></li></ul></div> </div>
这里顺便也把退出的功能做一下【将cookie清除就好啦】
package com.wh.easyshop.controller;import com.sun.corba.se.spi.orb.ParserImplBase; import com.sun.deploy.net.HttpResponse; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.util.CookieUtils; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid;/*** <p>* 用户信息表 前端控制器* </p>** @author wh* @since 2023-12-27*/ @Controller @RequestMapping("/user") public class UserController {@Autowiredprivate IUserService userService;@RequestMapping("/login")@ResponseBodypublic JsonResponseBody<?> login(@Valid UserVo userVo, HttpServletRequest request, HttpServletResponse response){return userService.login(userVo,request,response);}/*** 退出登录* @param request* @param response* @return*/@RequestMapping("/userLogout")public String login(HttpServletRequest request, HttpServletResponse response){CookieUtils.deleteCookie(request,response,"UserToken");CookieUtils.deleteCookie(request,response,"nickname");return "redirect:/";}}
好啦,今天的分享就到这了,希望能够帮到你呢!😊😊
相关文章:
SpingBoot的项目实战--模拟电商【2.登录】
🥳🥳Welcome Huihuis Code World ! !🥳🥳 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 🥳🥳Welcome Huihuis Code World ! !🥳🥳 一.功能需求 二.代码编写 …...
http——https实现指南
第一部分:HTTPS安全证书简介 什么是HTTPS安全证书? 在网络通信中,HTTPS安全证书是一种由可信任的证书颁发机构(CA)签发的数字证书,用于保障网站与用户之间的数据传输安全。通过加密和身份验证,…...
ROS仿真R2机器人之安装运行及MoveIt的介绍
R2(Robonaut 2)是NASA美国宇航局与GM通用联合推出的宇航人形机器人,能在国际空间站使用,可想而知其价格是非常昂贵,几百万美刀吧,还好NASA发布了一个R2机器人的Gazebo模型,使用模型就不需要花钱了,由于我们…...
【linux 多线程并发】线程属性设置与查看,绑定CPU,线程分离与可连接,避够多线程下的内存泄漏
线程属性设置 专栏内容: 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况…...
70.乐理基础-打拍子-三连音
上一个内容:69.乐理基础-打拍子-大切分与变体-CSDN博客 62-66是总拍数为一拍的节奏型,一共有七个,68-69是两拍的节奏型。 三连音说明: 1.三连音的总拍数可以是一拍、两拍、四拍。。。。 2.打拍子比较难,或许需要用V字…...
100天精通Python(实用脚本篇)——第111天:批量将PDF转Word文档(附上脚本代码)
文章目录 专栏导读1. 将PDF转Word文档需求2. 模块安装3. 模块介绍4. 注意事项5. 完整代码实现6. 运行结果书籍推荐 专栏导读 🔥🔥本文已收录于《100天精通Python从入门到就业》:本专栏专门针对零基础和需要进阶提升的同学所准备的一套完整教…...
如何在 NAS 上安装 ONLYOFFICE 文档?
文章作者:ajun 导览 ONLYOFFICE 文档 是一款开源办公套件,其是包含文本文档、电子表格、演示文稿、表单、PDF 查看器和转换工具的协作性编辑工具。它高度兼容微软 Office 格式,包括 .docx、.xlsx 、.pptx 、pdf等文件格式,并支持…...
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK设置相机的图像剪切(ROI)功能(C++)
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK设置相机的图像剪切(ROI)功能(C) Baumer工业相机Baumer工业相机的图像剪切(ROI)功能的技术背景CameraExplorer如何使用图像剪切(ROI)功…...
从 WasmEdge 运行环境读写 Rust Wasm 应用的时序数据
WebAssembly (Wasm) 正在成为一个广受欢迎的编译目标,帮助开发者构建可迁移平台的应用。最近 Greptime 和 WasmEdge 协作,支持了在 WasmEdge 平台上的 Wasm 应用通过 MySQL 协议读写 GreptimeDB 中的时序数据。 什么是 WebAssembly WebAssembly 是一种…...
算法训练营Day34(贪心算法)
1005.K次取反后最大化的数组和 1005. K 次取反后最大化的数组和 - 力扣(LeetCode) 秒了 class Solution {public int largestSumAfterKNegations(int[] nums, int k) {Arrays.sort(nums);// -4 -3 -2 -1 5//-2 -2 0 2 5int last -1;for(int i 0;i<…...
uniapp:全局消息是推送,实现app在线更新,WebSocket,apk上传
全局消息是推送,实现app在线更新,WebSocket 1.在main.js中定义全局的WebSocket2.java后端建立和发送WebSocket3.通知所有用户更新 背景: 开发人员开发后app后打包成.apk文件,上传后通知厂区在线用户更新app。 那么没在线的怎么办&…...
ARM1.2作业
实现数码管不同位显示不同的数字 spi.h #ifndef __SPI_H__ #define __SPI_H__ #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_rcc.h"//MOSI对应的引脚输入高低电平的信号PE14 #define MOSI_OUTPUT_H() do{GPIOE->ODR | (0x1 << 14);}whi…...
【算法专题】递归算法
递归 递归1. 汉诺塔问题2. 合并两个有序链表3. 反转链表4. 两两交换链表中的节点5. Pow(x, n) --- 快速幂 递归 在解决⼀个规模为 n 的问题时,如果满足以下条件,我们可以使用递归来解决: 问题可以被划分为规模更小的子问题,并且…...
不停止业务的情况下优化 Elasticsearch Reindex
在使用 Elasticsearch 时,我们总有需要修改索引映射的时候,这时我们只能进行 _reindex。事实上,这是一个相当昂贵的操作,因为根据数据量和分片数量,完整复制一个索引可能需要几个小时。 花费的时间不是大问题,但更严重的是,它会影响生产环境的性能甚至功能。 相信大家…...
PB 按Excel动态创建对应字段
/* > Function: w_cwjk_xhyy.wf_dw_init >-------------------------------------------------------------------- > 描述: 按excel表格列名,创建对应字段,用于部分接口对应字段导出文件 >-------------------------------------------------------------------- …...
数据结构——红黑树 and B-树
红黑树 根据平衡条件第4、5两点 最短路径,都是黑色 最长路径,红黑相间 最长是最短的两倍 B-树...
Android中线程间的通信-Handler
Handler机制在Android中主要用于线程间的通信,特别是处理从子线程向主线程(UI线程)传递消息和更新界面。 Handler中的四个关键对象及其作用: Message: Message 是在线程间传递的数据载体,它包含了需要处理…...
Spring Boot Admin健康检查引起的Spring Boot服务假死
问题现象 最近在spring boot项目中引入了 spring-boot-starter-actuator 后,测试环境开始出现服务假死的现象, 且这个问题十分怪异,只在多个微服务中的简称A的这个服务中出现,其他服务都没有出现这个问题, 之所以说…...
java企业人事信息管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 java Web企业人事信息管理系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境 为TOMCAT7.0,Myeclipse8.5开发,数据库为M…...
如何通过 useMemo 和 useCallback 提升你的 React 应用性能
背景 在 React 中,useMemo 和 useCallback 这两个 hook 是我们优化应用性能的有力工具。它们会返回 memoized 版本的值或函数,只在依赖项发生变化时才进行重新计算或定义。 Hook 介绍 useMemo useMemo 的作用是返回一个 memoized 值,它接…...
C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...
[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
【VLNs篇】07:NavRL—在动态环境中学习安全飞行
项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战,克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

但是这个异常抛出了,我们需要一个类来处理它--全局异常处理类,编写全局异常处理类,需要 用到一个注解












如果此时输入的是正确的手机号
出现的错误也只是手机号未找到
在项目中,为了方便编码以及后续的优化修复,通常是不会在代码中出现自定义的变量,所以这里我们编写一个常量类,把我们自定义的变量放入其中进行统一管理








