为了巩固一些基础知识,所以现在我重新开一个小项目,重点还是深入了解一下原理和补全一些之前没有关注的知识点。

1、业务范围

2、基础架构构建

3、用户模块开发

4、分类模块开发

1、业务范围

主要关注的是登陆注册的原理

简单的分页查询

前端知识补齐

2、基础架构构建

2.1、创建maven项目

直接新建一个maven项目,选择的脚手架是quickstart

注意的是我这边idea一直默认识别自己的maven,需要在创建之前先配置好自己的maven路径。

2.2、引入基础依赖

创建好的项目非常干净,我们把pom里面默认引入的junit也删了,增加自己的配置

添加了夫工程配置

web的依赖、mybatis的依赖、mysql驱动、lombok

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>


  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
  </parent>

  <groupId>com.hm</groupId>
  <artifactId>big-event</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>big-event</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!--  web依赖  -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--  mybatis依赖  -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>

    <!--  mysql驱动  -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
    </dependency>

    <!--  lombok  -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>

  </dependencies>
</project>

2.3、基础架构搭建和简单配置

创建resources目录和下面的application.yml,做好数据源的配置。

server:
  port: 8888
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://100.66.1.1:33306/big_event?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: bigevent
    password: Sz9227328

再创建好mvc架构的目录。

2.4、启动测试

改造启动类:修改名字,修改为spring boot启动:src/main/java/com/hm/BigEventApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Hello world!
 *
 */
@SpringBootApplication
public class BigEventApplication
{
    public static void main( String[] args ){
        SpringApplication.run(BigEventApplication.class, args);
    }
}

启动后发现报错:java: 没有为模块 'big-event' 指定 JDK

处理办法:

在模块配置里给模块配置好jdk,

在启动就正常。

3、用户模块开发

一个6个接口开发:

3.1、注册

请求路径:/user/register 请求方式:POST 接口描述:该接口用于注册新用户

请求数据样例:username=zhangsan&password=123456

响应数据样例:

{ "code": 0, "message": "操作成功", "data": null }

controller:

@Autowired
private UserService userService;

/***
 * 注册
 * @return
 */
@PostMapping("/register")
public Result register(String username, String password) {
    // 先判断是有重复的用户名
    User user = userService.findUserByUsername(username);
    if (user == null){//  没有重复
        userService.insertUser(username, password);
        return Result.success("注册成功");
    } else { //  有重复
        return Result.error("用户名重复");
    }
}

src/main/java/com/hm/service/UserService.java

/**
 * 根据用户名查询用户
 * @param username
 * @return
 */
User findUserByUsername(String username);

/**
 * 注册用户
 * @param username
 * @param password
 */

void insertUser(String username, String 

src/main/java/com/hm/service/impl/UserServiceImpl.java

@Autowired
private UserMapper userMapper;
@Override
public User findUserByUsername(String username) {
    return userMapper.findUserByUsername(username);
}

@Override
public void insertUser(String username, String password) {
    // 把密码加密一下
    password = Md5Util.getMD5String(password);
    userMapper.insertUser(username, password);
}

src/main/java/com/hm/mapper/UserMapper.java

@Select("select * from user where username = #{username}")
User findUserByUsername(String username);

@Select("insert into user(username, password, create_time, update_time)" +
        " values(#{username}, #{password}, now(), now())")
void insertUser(String username, String password);

用postman测试的时候出现问题,数据可以成果入库,但是返回406

{
    "timestamp": "2025-06-14T04:55:08.079+00:00",
    "status": 406,
    "error": "Not Acceptable",
    "path": "/user/register"
}

查看日志:

Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]

原因是返回的Result没有添加get和set导致

添加后问题解决。

package com.hm.pojo;


//统一响应结果

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    private T data;//响应数据

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }
}

postman返回正常。

3.1.1、参数校验

3.1.1.1、springValication和全局异常处理器

参数校验要求输入的用户名密码长度是5到16位的非空字符串,用if判断不优雅,可以用spring validation实现。

先引入依赖:

<!--  spring validation  -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在入参加上@Pattern注解,在controller加上@Validated注解。

@RestController
@RequestMapping("/user")
@Validated
public class UserController {

    @Autowired
    private UserService userService;

    /***
     * 注册
     * @return
     */
    @PostMapping("/register")
    public Result register(@Pattern(regexp = "^\\S{5,16}$", message = "用户名格式错误") String username,
                           @Pattern(regexp = "^\\S{5,16}$", message = "密码格式错误") String password) {
        // 先判断是有重复的用户名
        User user = userService.findUserByUsername(username);
        if (user == null){//  没有重复
            userService.insertUser(username, password);
            return Result.success("注册成功");
        } else { //  有重复
            return Result.error("用户名重复");
        }

    }
}

但是这样有问题,spring validation在判断入参格式有问题后,会直接抛出异常,前端收到的是一个500的报错,没法获取到具体的报错信息。

后台报错

jakarta.validation.ConstraintViolationException: register.username: 用户名格式错误
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:170) ~[spring-context-6.2.7.jar:6.2.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.7.jar:6.2.7]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728) ~[spring-aop-6.2.7.jar:6.2.7]
	at com.hm.controller.UserController$$SpringCGLIB$$0.register(<generated>) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258) ~[spring-web-6.2.7.jar:6.2.7]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191) ~[spring-web-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.41.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.7.jar:6.2.7]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.41.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.7.jar:6.2.7]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.7.jar:6.2.7]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.7.jar:6.2.7]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.7.jar:6.2.7]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.7.jar:6.2.7]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.7.jar:6.2.7]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.41.jar:10.1.41]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

postman返回数据:

{
    "timestamp": "2025-06-14T05:18:58.503+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/user/register"
}

这显然不是我们要的。所以要创建一个全局异常处理器,把报错的信息封装到Result对象里面去。src/main/java/com/hm/exception/GlobalExceptionHandler.java

package com.hm.exception;

import com.hm.pojo.Result;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

//  全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        e.printStackTrace();
        return Result.error(StringUtils.hasLength(e.getMessage())?  e.getMessage() : "服务更新中,请稍后再试");
    }
}

还遇到一个问题,@Validated注解在处理实体类的非空校验的时候,会出现不同接口对实体类的非空要求不一样的情况。这时候需要用到分组校验。直接去看4.1.

3.2、登录

请求路径:/user/login 请求方式:POST 接口描述:该接口用于登录

请求数据样例:username=zhangsan&password=123456

响应数据样例:{ "code": 0, "message": "操作成功", "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjUsInVzZXJuYW1lIjoid2FuZ2JhI n0sImV4cCI6MTY5MzcxNTk3OH0.pE_RATcoF7Nm9KEp9eC3CzcBbKWAFOL0IsuMNjnZ95M" }

src/main/java/com/hm/controller/UserController.java

/***
 * 登录
 * @return
 */
@PostMapping(value = "/login")
public Result login(@Pattern(regexp = "^\\S{5,16}$", message = "用户名格式错误") String username,
                       @Pattern(regexp = "^\\S{5,16}$", message = "密码格式错误") String password) {
    User user = userService.findUserByUsername(username);
    if (user == null){
        return Result.error("该用户不存在");
    } else {
        if (Md5Util.checkPassword(password, user.getPassword())){
            return Result.success("登录成功");
        } else {
            return Result.error("密码错误");
        }
    }
}

这里涉及到一个jwt的概念,登录成果以后会生成一个token,后续的前端接口都需要携带token请求。

用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,浏览器都需要在请求头header中携带 到服务端,请求头的名称为 Authorization,值为 登录时下发的JWT令牌。 如果检测到用户未登录,则http响应状态码为401

3.2.1、jwt令牌

一个 JWT 由三部分组成,用 . 分隔:

1、Header:包含 Token 类型("typ": "JWT")和签名算法(如 HS256RS256):

{
  "alg": "HS256",  // 签名算法(如 HS256、RS256)
  "typ": "JWT"     // Token 类型
}

2、Payload(负载):存放实际数据(如用户ID、角色、过期时间),分为三类:

  • Registered Claims(标准字段)
    iss(签发者)、exp(过期时间)、sub(主题)。

  • Public Claims(公共字段)
    可自定义,但需避免冲突(建议用 IANA 注册的字段)。

  • Private Claims(私有字段)
    用于业务数据(如 userIdrole)。

示例:

{
  "sub": "1234567890",      // 用户ID
  "name": "John Doe",       // 用户名
  "admin": true,           // 角色
  "exp": 1710000000        // 过期时间(Unix 时间戳)
}

3、Signature(签名):HeaderPayload 的签名,防止篡改:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secretKey  // 密钥(HS256)或私钥(RS256)
)

pom直接使用现成的就完事了,引入java-jwt,顺便用个单元测试

<!--  java-jwt  -->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>4.4.0</version>
</dependency>

<!--  springboot的单元测试-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>

引入工具类src/main/java/com/hm/utils/JwtUtil.java

package com.hm.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    private static final String KEY = "itheima";
    
    //接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
                .sign(Algorithm.HMAC256(KEY));
    }

    //接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}

修改一些登录的逻辑就行。src/main/java/com/hm/controller/UserController.java

/***
 * 登录
 * @return
 */
@PostMapping(value = "/login")
public Result login(@Pattern(regexp = "^\\S{5,16}$", message = "用户名格式错误") String username,
                       @Pattern(regexp = "^\\S{5,16}$", message = "密码格式错误") String password) {
    User user = userService.findUserByUsername(username);
    if (user == null){ //  没有该用户
        return Result.error("该用户不存在");
    } else { //  有该用户
        if (Md5Util.checkPassword(password, user.getPassword())){ //  密码正确
            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("username", user.getUsername());
            map.put("id", user.getId());
            String token = JwtUtil.genToken(map);
            return Result.success(token);
        } else { //  密码错误
            return Result.error("密码错误");
        }
    }
}

查看token生成是否成功。

{
    "code": 0,
    "message": "操作成功",
    "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjEsInVzZXJuYW1lIjoiemhhbmdzYW4ifSwiZXhwIjoxNzQ5OTMxNDg0fQ.fcL0Vlj4auc103q7JJP-dTtMoJzrPz6R5LMnr2U84Wg"
}

3.2.2、拦截器实现鉴权校验

首先创建一个拦截器处理登录信息的鉴权,并把登录注册接口排除。src/main/java/com/hm/interceptor/LoginInterceptor.java

package com.hm.interceptor;

import com.hm.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {
    // 创建登录拦截器

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            response.setStatus(401);
            return false;
        }
        try {
            Map<String, Object> claims = JwtUtil.parseToken(authorization);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }

    }
}

还需要把自定义的拦截器注册到spring:src/main/java/com/hm/config/WebConfig.java

package com.hm.config;

import com.hm.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor interceptor;

    /**
     * 配置拦截器
     * 
     * 该方法用于向Spring MVC的InterceptorRegistry中添加拦截器,以配置哪些请求需要被拦截处理
     * 主要目的是设置拦截器interceptor,同时排除用户登录和注册页面,避免这些请求被拦截
     *
     * @param registry InterceptorRegistry实例,用于注册拦截器
     */
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,同时指定不应用拦截器的路径
        registry.addInterceptor(interceptor).excludePathPatterns("/user/login", "/user/register");
    }

}

这样就可以实现拦截所有请求,校验鉴权信息后再决定是否放行。

3.3、获取用户详细信息

请求路径:/user/userInfo 请求方式:GET 接口描述:该接口用于获取当前已登录用户的详细信息

请求参数:无

响应数据样例:

{
"code": 0,
"message": "操作成功",
  "data": {
  "id": 5,
  "username": "wangba",
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "nickname": "",
  "email": "",
  "userPic": "",
  "createTime": "2023-09-02 22:21:31",
  "updateTime": "2023-09-02 22:21:31"
  }
}

思路:从header里面获取到token,解析出用户名再去查询出数据。

src/main/java/com/hm/controller/UserController.java

    /***
     * 获取用户信息
     * @return
     */
    @GetMapping(value =  "/userInfo")
    public Result  getUserInfo(@RequestHeader("Authorization") String authorization) {
        Map<String, Object> map = JwtUtil.parseToken(authorization);
        User user = userService.findUserByUsername((String) map.get("username"));
        return Result.success(user);
    }

逻辑很简单,但是有两个知识点。一个是如何在实体类转换为JSON的时候忽略某个字段?二是如何开启mybatis的驼峰命名?

实体类转换为JSON的时候忽略某个字段:在实体类的字段上加上@JsonIgnore

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;//主键ID
    private String username;//用户名
    @JsonIgnore
    private String password;//密码
    private String nickname;//昵称
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

如何开启mybatis的驼峰命名:配置文件上配置:

mybatis:
  configuration:
    map-underscore-to-camel-case: true

另外还有一个知识点,就是接口里是用JwtUtil解析出用户名,这一步在登陆拦截器里已经做过了,这里重复了,不太优雅。

办法就是利用ThreadLocal存储不同登陆用户的信息。

3.3.1、ThreadLocal核心特性

  1. 线程隔离

    • 每个线程拥有独立的变量副本,互不干扰。

    • 示例:线程 A 修改自己的副本,不会影响线程 B 的副本。

  2. 避免同步

    • 由于变量不共享,无需使用 synchronized 或锁,天然线程安全。

  3. 内存结构

    • 每个线程的 Thread 类内部维护一个 ThreadLocalMap(类似哈希表)。

    • ThreadLocal 作为 Key,线程的变量副本作为 Value 存储。

新增工具类来实现:src/main/java/com/hm/utils/ThreadLocalUtil.java

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 工具类
 */
@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }
    
    //存储键值对
    public static void set(Object value){
        THREAD_LOCAL.set(value);
    }


    //清除ThreadLocal 防止内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}

在登陆拦截器里把用户信息写入到Threadlocal,最后还需要把里面的数据清理,以防止内存泄露。

import com.hm.utils.JwtUtil;
import com.hm.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    // 创建登录拦截器

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("--------开始执行--------preHandle----------------");
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            response.setStatus(401);
            return false;
        }
        try {
            // 解析token是否成功
            Map<String, Object> claims = JwtUtil.parseToken(authorization);
            // 把数据存储到ThreadLocal
            ThreadLocalUtil.set(claims);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        } finally {
            log.info("--------结束执行--------preHandle----------------");
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("--------开始执行--------afterCompletion----------------");
        ThreadLocalUtil.remove();
        log.info("--------结束执行--------afterCompletion----------------");
    }
}

重新修改获取用户信息的controller

    /***
     * 获取用户信息
     * @return
     */
    @GetMapping(value =  "/userInfo")
    public Result  getUserInfo(/** @RequestHeader("Authorization") String authorization **/) {
//        Map<String, Object> map = JwtUtil.parseToken(authorization);
        // 改为从ThreadLocal中获取
        Map<String, Object> map = ThreadLocalUtil.get();
        User user = userService.findUserByUsername((String) map.get("username"));
        return Result.success(user);
    }

3.4、更新用户基本信息

请求路径:/user/update

请求方式:PUT

接口描述:该接口用于更新已登录用户的基本信息(除头像和密码)

请求数据样例:

响应数据样例:

{
  "code": 0,
  "message": "操作成功",
  "data": null
}

controller:src/main/java/com/hm/controller/UserController.java

/**
 * 更新用户信息
 * 
 * 该方法通过HTTP PUT请求接收一个用户对象,用于更新用户信息
 * 使用@PutMapping注解指定请求路径和方法,@RequestBody注解用于将HTTP请求正文转换为User对象,
 * @Validated注解用于验证传入的User对象属性是否符合规范
 * 
 * @param user 待更新的用户对象,需要通过JSON格式传入请求体中
 * @return 返回一个Result对象,表示更新操作的结果
 */
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
    userService.update(user);
    return Result.success();
}

service:src/main/java/com/hm/service/UserService.java

/**
 * 更新用户
 * @param user
 */
void update(User user);

src/main/java/com/hm/service/impl/UserServiceImpl.java

/**
 * 更新用户信息
 * @param user
 */
@Override
public void update(User user) {
    userMapper.update(user);
}

mapper:src/main/java/com/hm/mapper/UserMapper.java

@Select("update user set email = #{email},nickname = #{nickname}, update_time = now() where id = #{id}")
void update(User user);

最后看看后端是如何参数校验的:首先是在实体类加上规则,再去入参上加上注解@Validated

package com.hm.pojo;



import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @NotNull
    private Integer id;//主键ID
    private String username;//用户名
    @JsonIgnore
    private String password;//密码
    @NotEmpty
    @Pattern(regexp = "^\\S{5,16}$", message = "昵称格式错误")
    private String nickname;//昵称
    @Email
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

测试不规范传参的后果:

3.5、更新用户头像

请求路径:/user/updateAvata

请求方式:PATCH

接口描述:该接口用于更新已登录用户的头像

请求数据样例:

avatarUrl=
https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ade0f4631cbed4.png

响应数据样例:

{
  "code": 0,
  "message": "操作成功",
  "data": null
}

controller:src/main/java/com/hm/controller/UserController.java

@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam String avatar){
    userService.updateAvata(avatar);
    return Result.success();
}

service:src/main/java/com/hm/service/UserService.java

/**
 * 更新用户头像
 * @param avatar
 */
void updateAvata(String avatar);

serviceimpl:src/main/java/com/hm/service/impl/UserServiceImpl.java

@Override
public void updateAvata(String avatar) {
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer id = (Integer) map.get("id");
    userMapper.updateAvata(avatar,id);
}

mapper:src/main/java/com/hm/mapper/UserMapper.java

@Select("update user set user_pic = #{avatar}, update_time = now() where id = #{id}")
void updateAvata(String avatar,Integer id);

测试:这次的参数是拼接在url里面的,参数在params里面,所以controller要用@RequestParam接收。

3.6、更新用户密码

请求路径:/user/updatePwd

请求方式:PATCH

接口描述:该接口用于更新已登录用户的密码

请求数据样例:

{
    "old_pwd":"123456",
    "new_pwd":"234567",
    "re_pwd":"234567"
}

响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": null
}


4、分类模块开发

还遇到一个问题,@Validated注解在处理实体类的非空校验的时候,会出现不同接口对实体类的非空要求不一样的情况。这时候需要用到分组校验。

4.1、Validated分组校验

实体类:

package com.hm.pojo;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Category {
    @NotNull(message = "ID不能为空", groups = {Category.Update.class})
    private Integer id;//主键ID
    @NotEmpty(message = "分类名称不能为空", groups = {Category.Add.class, Category.Update.class})
    private String categoryName;//分类名称
    @NotEmpty(message = "分类别名不能为空", groups = {Category.Add.class, Category.Update.class})
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;//更新时间

    public interface Add {
    }
    public interface Update {
    }
}

接口处理:

package com.hm.controller;

@RestController
@RequestMapping(value = "/category")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    @PostMapping
    public Result add(@RequestBody @Validated(Category.Add.class) Category category){
        Category categoryadd =  categoryService.add(category);
        return Result.success(categoryadd);
    }



    @PutMapping
    public Result update(@RequestBody @Validated(Category.Update.class) Category category){
        return Result.success(categoryService.update(category));
    }
}

坐标