苍穹外卖笔记day02

第二天 员工管理、分类管理

第一节 – 新增员工

一、需求分析和设计
  1. 产品原型
  2. 接口设计

管理端发出的请求,统一使用 **/admin **作为前缀
用户端发出的请求,统一使用 /user 作为前缀



3. 数据库表设计(employee表):

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status Int 账号状态 1正常 0锁定
create_time Datetime 创建时间
update_time Datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id
二、代码开发
1. 根据新增员工接口设计对应的DTO:


注意: 当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装前端提交的对应数据,如下图所示:

2. 代码实现:新增员工
(1)在EmployeeController中创建新增员工方法,接收前端提交的参数:
EmployeeController.java
  	 /**
     * 新增员工方法
     * @param employeeDTO
     */
    @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO) {  
    //@RequestBody 传递的是json格式的代码,要加这个注解
        log.info("新增员工:{}", employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }
(2)Alt + Enter,选择Create method’save’ in ‘EmployeeService’

新增员工

(3)在EmployeeService.java中添加方法
	/**
     * 新增员工方法
     * @param employeeDTO
     */
    void save(EmployeeDTO employeeDTO);

EmployeeService.java新增员工方法

(4)在EmployeeServiceImpl实现类中,实现新增员工方法

在Mapper层持久层把数据插入,传进来是DTO(方便封装前端提交的数据),但最终传回持久层,建议使用实体类。也就是说需要做一个实体的对象转换。

       /**
    * 新增员工方法
    * @param employeeDTO
    */
   public void save(EmployeeDTO employeeDTO) {

       //DTO转换为实体
       Employee employee = new Employee();

       // 手写传递:把一个个属性都传递过去 ,繁琐
       //employee.setName(employeeDTO.getName());

       //对象的属性拷贝,一行代码(属性名必须一致)
       BeanUtils.copyProperties(employeeDTO , employee);

       //设置账号状态,默认正常(1表示正常,0表示锁定)
       employee.setStatus(StatusConstant.ENABLE);  //ENABlE启用=1,DISABLE禁用=0

       //设置密码,默认密码123456 ,MD5加密
       employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

       //设置当前记录的创建时间和修改时间
       employee.setCreateTime(LocalDateTime.now());
       employee.setUpdateTime(LocalDateTime.now());

       // 设置当前记录创建人的id和修改人的id
       // TODO 后期需要改为当前用户的id
       employee.setCreateUser(10L);
       employee.setUpdateUser(10L);
   
    //调用持久层
       employeeMapper.insert(employee);
   }

在这里插入图片描述

🙋‍学知识: DTO对象转换的方法 与 解释链接请看下面:

employeeDTO: 是“数据传输对象”,通常从前端或远程服务接收数据。
employee:是“领域实体/持久化对象”,最终要落库。
对象的转换:
1. 传统手动字段拷贝,原封不动直接塞:
 写法:

Employee e1 = new Employee();
e1.setName(dto.getName());
 e1.setAge(dto.getAge()); 
……

2. 使用spring BeanUtils,对象的属性拷贝(浅拷贝):
  (反射,同名字段自动拷)DTO属性和employee属性都对应得上, 一行代码拷贝所有同名字段。
  写法:BeanUtils.copyProperties (源对象,目标对象)

Employee e2 = new Employee();
BeanUtils.copyProperties(dto, e2);

xml 依赖(Maven):

<!-- Spring BeanUtils -->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>5.3.32</version>
 </dependency>

3. 使用Lombok + Builder(无反射,字段少时最直观):
写法:

Employee e3 = Employee.builder()
              .name(dto.getName())
              .age(dto.getAge())
              .build();

xml 依赖(Maven):

 <!--  Lombok (想用Builder) -->
<dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.18.30</version>
     <scope>provided</scope>
</dependency>

总结:
字段少:直接手写或 Builder 最清晰。
字段多:用 BeanUtils 图省事,但反射性能略低。
代码好看:用Lombok +builder
高并发或复杂转换:上 MapStruct,一次编译终生“飞”。

详细的DTO传递教程 + 解释, 请点我

(5)在持久层Mapper插入员工数据

把数据插入Mapper,Mapper执行sql EmployeeMapper.insert(emp)
↓ 1 MapperProxy 代理 → 2 SqlSession → 3 JDBC PreparedStatement ↓ 4 MySQL Server 解析 → 5InnoDB 写内存+Redo Log → 6 后台线程刷磁盘


    /**
     *插入员工数据
     * @param employee
     */
    //单表的插入工作(application.yml 支持驼峰命名,id_number就可以自动转换成idNumber)
    @Insert("insert into employee (name,  username, password, phone, sex, id_number, create_time, update_time, create_usr, updata_user, status)" +
            "values" +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

在这里插入图片描述

🙋‍学知识: Web 请求处理链路 的 完整流程

浏览器发请求 → Controller 接收 DTO → ServiceImpl 负责 DTO→Entity 转换 & 业务实现 → Mapper 生成 SQL → MyBatis → JDBC → MySQL Server → 磁盘

面试:
“我们采用 DTO 模式 隔离对外契约,请求链路是:
Controller 接收 DTO → ServiceImpl (实现类)做转换 & 业务 → Mapper → SQL → JDBC → MySQL → 磁盘。”

  1. HTTP 进来
    POST http://localhost:8080/employeeBody: { "name":"Alice","username":"ali", ... }
  2. Controller 接收并调用 Service
  • Controller层仅做参数接收 + 调用下一层
  1. Service接口,定义契约
  • Service(接口)在这一套架构里只做一件事:定义契约,

(1) 对外(Controller)暴露稳定的方法签名

  • 不管内部怎么换实现,Controller 只认接口,达到隔离变化。

(2) 对内(ServiceImpl)提供实现占位,方便:

  • Spring 用 JDK 动态代理做 AOP(事务、日志、权限)
  • 单元测试时mock 接口即可,不用关心真实实现

(3) 本身不含任何业务代码或转换逻辑;所有具体工作交给 ServiceImpl 完成

Service = 门面 + 契约;ServiceImpl = 真正干活的

  1. ServiceImpl做业务 + 事务
  • ServiceImpl = 参数校验 + 业务补全 + 事务控制 + 下发 SQL 的“一站式指挥官”。
  1. Mapper(DAO)接口 → MyBatis 代理
  • 实际动作:MyBatis 生成 SQL → 送 JDBC
@Mapper
public interface EmployeeMapper {
   @Insert("INSERT INTO employee(name,username,...) " +
           "VALUES(#{name},#{username},...)")
   void insert(Employee e);
}
  1. JDBC → MySQL Server
  • JDBC 把 SQL 通过网络发到 MySQL Server
  • Server 解析 → 优化 → 执行 → 写 InnoDB Buffer Pool
  1. InnoDB → 磁盘
  • 事务提交时 redo log 刷盘
  • 后台线程把脏页写入 .ibd 文件
  • 最终数据保存在:datadir/数据库名/表名.ibd
三、功能测试

方式:

(1)通过接口文档测试
(2)通过前后端联调测试
注意: 由于开发阶段前端和后端是并存开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法前后端联调测试。所以在开发阶段后端测试主要以接口文档测试为主。

通过后端联调测试
  • debug运行项目

  • 打开localhost:8080/doc.html后端项目接口文档
    →员工相关接口→POST 新增员工页面→点击调试

  • 输入请求参数
    新增员工:请求参数

  • 添加一个断点
    新增员工:请求断点

  • 再回到后端接口文档,点击“发送按钮”。
    发送后报401
    发送后结果:401

  • 报错原因:我们执行接口时,并没有把Jwt令牌提交过来。
    此时我们在sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java中,添加一个断点,然后在后端接口文档中,再次点击“发送”按钮,运行下一步,我们发现,tokennull,运行下一步,token如果为空,则抛出异常,响应码为401。
    Jwt令牌token: null

 /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
  • 获取令牌
    在员工共相关接口中→员工登录页面→调试→发送登录请求→即可正常获取到令牌
    登录账号,正常获取令牌
  • 复制令牌,前往文档管理→全局参数设置中→添加参数按钮
     参数名称必须输入token,参数值为刚刚我们复制的,点击确定
      该操作表面:我们添加了一个全局的参数
    全局参数设置-添加参数
  • 我们回到新增员工接口文档,关闭打开的“新增员工”等标签页。
    重新打开接口文档中的“POST 新增员工” 接口→调试→请求头部出现1个红色提示。
    新增员工:请求头部
  • 点击发送按钮,回到代码中看Jwt令牌,token不再是null值。
    Jwt令牌:token
  • 取消Jwt的断点,点击Resume Program放行,Controller层成功获取到前端发送的数据。并且已封装在了DTO里面。
    employeeDTO
  • 找到ServiceImpl层的save()方法,添加断点,然后点击放行,这时我们可以在save中看到已经获取到了。
    ServiceImpl:save
  • 单步运行,到创建对象,我们的employee对象还是空值。
    创建对象:值都为null
    再单步运行到对象属性拷贝,我们发现,employee的相关属性都有值了。
    对象属性拷贝
    单步运行到调用持久层这一行,数据都已经准备好了,除了id
    调用持久层Mapper
    放行:可以看到发了一条insert插入语句,然后下面我们看到一条报错信息,不要怕,我们仔细看,column的create_usr属性没认出来,原因是在mapper中,我们少写了个e,补上create_user,还有修改updata_userupdate_user。所以写这个的时候一定要小心的写,啊哈!!
    在这里插入图片描述
    改好后,重新运行, 接着在后端接口文档中,“新增员工”页面重新发送一下,然后放行调试,我们就可以成功新增一条数据了
    新增员工数据成功
  • 验证是否成功,成功插入数据,最终返回return Result.success();code为1,表示成功。
    在这里插入图片描述
    Result类的配置:成功就把code赋值为1
    在这里插入图片描述
    数据库刷新一下,就出现了我们刚刚增加的数据。
    在这里插入图片描述
提问1:问什么这里新增有Jwt令牌验证,我都没有写在Controller等三层架构这种里面?

原因:因为在sky-server/src/main/java/com/sky/config/WebMvcConfiguration.java中我们注册了自定义拦截器,并且执行任何有/admin/**的都会被拦截做Jwt令牌校验,校验成功就会放行。

/**
 * 把自定义的 JWT 拦截器注册到 SpringMVC 拦截器链。
 * 任何匹配 "/admin/**" 的请求都会先被 jwtTokenAdminInterceptor 拦下做 JWT 校验;
 * 登录接口 "/admin/employee/login" 直接放行,不做校验。
 *
 * @param registry Spring 提供的注册器,用来“挂”拦截器并指定拦截/放行规则
 */
@Override          // 覆盖父类方法,Spring 启动时会自动回调
protected void addInterceptors(InterceptorRegistry registry) {

    log.info("开始注册自定义拦截器...");   // 日志:方便排查拦截器有没有被加载

    registry
        // 1. 把 JWT 拦截器实例挂到链里(jwtTokenAdminInterceptor 已提前 @Component 注入)
        .addInterceptor(jwtTokenAdminInterceptor)

        // 2. 拦截范围:所有 /admin/** 开头的后台接口(如 /admin/employee/page、/admin/category/list ...)
        .addPathPatterns("/admin/**")

        // 3. 白名单:以下路径不走拦截器(登录/退出等不需要 token 也能访问)
        .excludePathPatterns("/admin/employee/login");
}

在这里插入图片描述

提问2:为什么WebMvcConfiguration.java会被执行

Spring Boot 会“自动”把 WebMvcConfiguration 扫描成配置类,原因如下:

  1. 你写了 @Configuration
    告诉 Spring:这是一个“配置 Bean”,启动时要加载。
  2. 类实现了 WebMvcConfigurer(或继承 WebMvcConfigurationSupport
  3. Spring MVC 启动时会自动检测所有实现该接口的 Bean,并调用其
    addInterceptors(...) 方法,从而把你的拦截器注册到全局拦截器链。
    拦截器本身已经加了 @Component(或 @Autowired 注入)
    所以jwtTokenAdminInterceptor先被 Spring 扫描成 Bean,再在这里被引用。
    在这里插入图片描述
总结

Spring Boot 启动 → 扫描到 @Configuration 类 → 发现它实现了 WebMvcConfigurer → 回调addInterceptors()→ 把 jwtTokenAdminInterceptor 注册进拦截器链 → 此后所有匹配 /admin/** 的请求都会先执行拦截器里的 preHandle() 方法。

前后端联调测试
  • 输入前端登录地址:我的是http://mysky:8083/#/login,再输入账号密码进行登录操作。
  • 点击员工管理→添加员工按钮在这里插入图片描述
  • 录入员工数据后保存
    在这里插入图片描述
  • 验证
    在这里插入图片描述在这里插入图片描述
四、 代码完善
程序存在的问题
1、录入的用户已存在,抛出异常后没处理

如果用户已存在,再次新增,代码会报500 在这里插入图片描述
并且后台提示Duplicate entry ‘zhangsan’ for key ‘idx_username’,用户名已经存在了
在这里插入图片描述
原因:username添加了唯一约束Unique索引。
在这里插入图片描述
(1) 捕获sql异常
复制SQLIntegrityConstraintViolationException,把这个异常在
sky-server/src/main/java/com/sky/handler/GlobalExceptionHandler.java
重写一个处理异常的方法,来捕获异常,再返回设置的异常信息。

/**
 * 统一处理数据库 SQL 完整性约束异常(如唯一索引冲突)。
 * 目前主要场景:注册用户时用户名已存在,MySQL 会抛出
 * SQLIntegrityConstraintViolationException,其错误信息格式为
 * "Duplicate entry 'xxx' for key 'idx_xxx'"。
 *
 * 本方法把原始英文异常解析成用户友好的中文提示,并返回统一响应对象 Result。
 *
 * @param ex 数据库抛出的 SQLIntegrityConstraintViolationException
 * @return 错误响应 Result,提示"用户名已存在"或"未知错误"
 */
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {

    // 从异常消息中提取完整错误描述,例如:
    // "Duplicate entry 'zhangsan' for key 'idx_username'"
    String message = ex.getMessage();

    // 仅处理"唯一索引冲突"场景
    if (message.contains("Duplicate entry")) {
        // 按空格拆分,第 3 段即为冲突值(单引号包裹)
        String[] split = message.split(" ");
        String username = split[2];              // 'zhangsan'
        // 去掉首尾单引号,再拼接中文"已存在"
        String msg = username.replace("'", "") + MessageConstant.ALREADY_EXISTS;
        log.error("异常信息:{}", msg); //控制台也提示异常信息
        //前端返回异常信息
        return Result.error(msg);
    }

    // 其他未知约束异常,统一返回"未知错误"
    return Result.error(MessageConstant.UNKNOWN_ERROR);
}

在这里插入图片描述
测试:
重新启动项目,debug,清空一下控制台,回到苍穹外卖项目接口文档,再新增员工出再点击一下发送,回到控制台,查看处理异常的信息,响应码200,并给出异常信息。
在这里插入图片描述在这里插入图片描述

问题3:为什么GlovalExceptionHandler会抓取到业务异常

原因:GlobalExceptionHandler 是 Spring 提供的“异常切面”,只要加 @RestControllerAdvice + @ExceptionHandler,所有 Controller 抛出的指定异常都会先被它截获,统一日志、统一提示、统一返回,让异常处理不再散落各处。

知识点: (业务层 抛出的自定义异常BaseException)
SpringMVC 的异常处理时机 请求进入 Controller之后,任何环节(Controller、Service、Mapper) 抛出的异常,只要没在被调用栈里被 try-catch 掉,就会一路冒到 DispatcherServlet; 此时 Spring 发现你有 @RestControllerAdvice + @ExceptionHandler(BaseException.class),就把异常对象传给你写的 exceptionHandler(BaseException ex) 方法。

/**
 * 统一处理 业务层 抛出的自定义异常。
 * 项目代码中凡是继承 {@link BaseException} 的异常(如账号已锁定、库存不足等)
 * 都会被此方法捕获,日志记录错误描述,并返回统一 JSON 结果给前端。
 *
 * @param ex 继承 BaseException 的业务异常对象
 * @return 统一响应体 Result,前端可直接取 message 字段提示用户
 */
@ExceptionHandler(BaseException.class)   // 只抓 BaseException 及其子类,可把换成其他处理器
public Result exceptionHandler(BaseException ex) {
    // 记录业务异常描述(不打印整个堆栈,避免日志膨胀)
    log.error("业务异常:{}", ex.getMessage());
    
    // 把异常描述原样返回,前端弹窗/文案即可使用
    return Result.error(ex.getMessage());
}

在这里插入图片描述###### 问题4:有了BaseException处理器捕获异常,为什么还要写SQLintegrityConstraintViolationException的处理器
理由:
BaseException只能截住我们自己抛的业务异常
SQLIntegrityConstraintViolationException 数据库抛的底层异常,不会被 BaseException 处理器 抓到,必须单独声明,否则用户会看到 500 + 英文堆栈
BaseExceptionSQLIntegrityConstraintViolationException ,两者完全独立,各管各的异常来源
在这里插入图片描述

2、新增员工时,创建人id和修改人id设置为了固定值。

基于Jwt认证流程(新增员工,给创建者赋予id)

在这里插入图片描述
登录成功生成Jwt令牌:
在这里插入图片描述
员工登录EmployeeController做的事情,员工登录成功后会生成JWT令牌并响应给前端,文件位置:
sky-server/src/main/java/com/sky/controller/admin/EmployeeController.java

/**
 * 员工登录入口
 * 路径:POST /admin/employee/login
 * 前端传入:{"username":"admin","password":"123456"}
 * 后端返回:{"code":1,"msg":"success","data":{"id":1,"userName":"admin","name":"管理员","token":"eyJxxx..."}}
 */
@PostMapping("/login")                                    // 只接收 POST 请求
@ApiOperation(value = "员工登录")                         // Knife4j/Swagger 文档注释
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
    log.info("员工登录:{}", employeeLoginDTO);              // 打印入参,方便排查

    /* 1. 业务层校验账号+密码;失败抛 BaseException,由 GlobalExceptionHandler 统一返回错误信息 */
    Employee employee = employeeService.login(employeeLoginDTO);

    /* 2. 登录成功,开始生成 JWT ------------------------------------------- */
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); // 把员工主键存进 JWT 负载
    String token = JwtUtil.createJWT(
            jwtProperties.getAdminSecretKey(),  // 签名密钥(application.yml 里配置)
            jwtProperties.getAdminTtl(),        // 过期时间(默认 2 小时)
            claims);                            // 自定义声明

    /* 3. 组装返回给前端的 VO --------------------------------------------- */
    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
            .id(employee.getId())
            .userName(employee.getUsername())
            .name(employee.getName())
            .token(token)                       // 前端后续请求带此 token 放在 header
            .build();

    /* 4. 统一包装返回给前端 ---------------------------------------------------- */
    return Result.success(employeeLoginVO);      // {code:1,msg:success,data:...}
}

后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前员工id:
文件位置:sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java
问题:解析出登录员工ID后,如何传递给Service的save方法?

 // ============== 统一 JWT 登录校验拦截器(preHandle)==============

// 1.从请求头中获取令牌: 从请求头里取出前端带过来的 token(header 名称在 yml 配置,默认 "token")
String token = request.getHeader(jwtProperties.getAdminTokenName());

// 2. 校验令牌:开始验 token —— 任何解析失败都抛异常,进入 catch 块
try {
    log.info("jwt校验:{}", token);                       // 打印日志,方便排查
    
    // 2-1 用**签名密钥**解析 JWT,若过期、被篡改、格式错误都会抛异常
    Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminTokenKey(), token);
    
    // 2-2 取出登录时存进去的员工 id(key = "empId")
    Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
    
    log.info("当前员工id:{}", empId);            // 记录当前操作用户
    //BaseContext.setCurrentId(empId);          // 原码这里还没写:放进线程变量,后续 Service/Mapper 随时拿
    
    // 2-3 全部正常 → 放行,继续进入 Controller
    return true;
    
} catch (Exception ex) {
    // 3. 解析失败(无 token、过期、篡改)→ 返回 401,前端收到未授权
    response.setStatus(401);
    return false;   // 中断请求链,不再进入 Controller
}

🙋‍学习知识:JWT令牌【生成→传输→获取】全流程一览表
阶段 位置 关键代码/动作 数据形态 说明
1.登录校验 后端Controller employeeService
.login(dto)
Java对象 校验账号密码,失败抛业务异常
2.生成令牌 后端Controller JwtUtil.createJWT
(secret,ttl,claims)
字符串JWT 把员工id写进claims并签名
3.返回前端 后端Controller Result.success(vo) JSON响应体 SpringMVC自动把VO→JSON
4.前端保存 前端浏览器 localStorage
.setItem('token',res.data.token)
字符串 完整JWT串存本地
5.请求携带 前端Axios拦截器 axios.defaults.headers
['token'] = 本地值
HTTPHeader 每次请求自动带此头
6.后端获取 后端拦截器 request.getHeader
(jwtProperties
.getAdminTokenName())
字符串 按配置名"token"取出整串JWT
7.后端解析 后端拦截器 JwtUtil.parseJWT
(secret,token)
Claims对象 验签、过期、篡改
8.取出员工id 后端拦截器 Long empId =
claims.get(...)
Long 得到当前登录人主键
9.线程保存 后端拦截器 BaseContext
.setCurrentId(empId)
ThreadLocal 后续Service/Mapper随时取
10.放行 后端拦截器 return true 布尔 令牌合法,请求进入Controller

现在代码里面没有写第9个线程保护:

  1. 必须写吗?
    不是语法必须,但项目规范里“强烈建议写”。
    不写也能跑,只是后续代码想拿当前登录人 ID 时得自己再解析一次 JWT,或者层层传参。
  2. 为什么我们代码里现在没有?
    我们那段拦截器那里现在还没写,还没写而已,实现自动填充人是要写的
    BaseContext.setCurrentId(mpId); 补上即可实现自动填充人这种
  3. 线程保护(ThreadLocal)作用
  • 一次解析,全线程复用
  • 避免在 Service/Mapper 里反复解析 JWT
  • 避免层层方法显式传递 empId 参数
  • 同一次请求内随时:
    只有当你需要在 Service/Mapper 里知道“当前是谁”时才写。
    Long loginUserId = BaseContext.getCurrentId(); // 拿到当前登录员工主键
    自动填充人;
public void saveCategory(CategoryDTO dto) {
   Long loginUserId = BaseContext.getCurrentId(); // 拿到当前登录员工主键
   Category category = new Category();
   category.setName(dto.getName());
   category.setCreateUser(loginUserId);    // 自动填创建人
   categoryMapper.insert(category);
}
  • 请求结束线程归还池子,ThreadLocal 被清理,数据不会串请求(我们项目没写,强烈建议添加)
    (1) 现在没有 afterCompletion
    → 线程池复用线程时 ThreadLocal 里仍残留上一次请求的 empId,下次请求可能串数据(看起来就像“用户A突然变成用户B”)。
    (2))补上只需 3 行代码,放在类里即可:
    位置在:sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java
/** 完整模板:*/
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object >handler) throws Exception {
       ...  // 解析、setCurrentId、return true/false
   }
   //加上这三行即可
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object >handler, Exception ex) throws Exception {
       // 请求结束,清空 ThreadLocal,防止线程复用串数据
       BaseContext.removeCurrentId();
   }
}
代码完善(解决自动获取当前用户ID)

看了我的学习知识,在这里就不会迷糊
继续了解:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
  ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
验证:

  1. 在EmployeeController.java的save方法中第一行,

  2. 在拦截器JwtTokenAdminInterceptor.java的preHandel方法中第一行,

  3. 在EmployeeServiceImpl.java中的save方法中,
    都添加一句:
    System.out.println("当前线程的ID:" + Thread.currentThread().getId());

  4. 停止项目→debug重启项目→在前端重新登录账号→在新增员工处“新增已有账号laoliu”→点击保存
    在这里插入图片描述

  5. 返回控制台观察,本次操作,线程id都相同
    在这里插入图片描述

  6. 再回到页面,再次点击“保存”按钮,回到控制台查看:线程ID已改变,但是本次线程id依旧一一对应。 在这里插入图片描述
    至此可以验证:ThreadLocal可以为每个线程提供单独一份存储空间,保证线程安全。
    且也说明,在ThreadLocal的整个生命周期之内,我们就可以共享这一份存储空间。
    而这,也就是说我们可以再拦截器中,把我们当前的用户ID存储到这个空间里面去。
    再到Service层中,我们就可以从这个存储空间里面把用户ID拿出来。

具体实现

ThreadLocal常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值
  • public T get() 返回当前线程所对应的线程局部变量的值
  • public void remove() 移除当前线程的线程局部变量
    该ThreadLocal封装位置:
    sky-common/src/main/java/com/sky/context/BaseContext.java
package com.sky.context;

/**
 * ThreadLocal 工具类
 * 作用:在同一次请求线程内随处存/取/删除当前登录员工 ID,
 *      避免层层传参,也防止线程复用时数据串号。
 */
public class BaseContext {

    // 每个线程独享的变量,生命周期 = 请求开始到请求结束
    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 把当前登录员工主键保存到线程变量(一般在拦截器 preHandle 里调用)
     * @param id 员工主键
     */
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }
    
    /**
     * 从线程变量获取当前登录员工主键(Service/Mapper 随处调用)
     * @return 员工主键;线程无值时返回 null
     */
    public static Long getCurrentId() {
        return threadLocal.get();
    }
    
    /**
     * 请求结束务必调用,清理线程变量,防止线程池复用导致串号
     * 建议在拦截器 afterCompletion 里使用
     */
    public static void removeCurrentId() {
        threadLocal.remove();
    }
}

在程序当中使用,就只需要BaseContext.xxx就可以了

  • 首先,在我们的JwtTokenAdminInterceptor.java中,我们这句Long empId = - Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());已经解析出来empId;

  • 接下来,我们只需要在该句下面添加:BaseContext.setCurrentId(empId);把这个当前用户ID存入进去。
    在这里插入图片描述

  • 程序继续执行→Ctroller→Service,最终执行到ServiceImpl的Sava方法中。

  • 然后,把TODO完善:

// 设置当前记录创建人的id和修改人的id
       employee.setCreateUser(BaseContext.getCurrentId());
       employee.setUpdateUser(BaseContext.getCurrentId());

在这里插入图片描述

测试:(是否自动获取到id)
  1. 重新启动,debug,给JwtTokenAdminInterceptor.java和EmployeeServiceImpl.java都添加一个断点。
    在这里插入图片描述
  2. 回到前端页面,添加员工,添加一个没有的账号,然后点击保存
    在这里插入图片描述
  3. 程序运行到拦截器,点击单步运行Set Over,empId值已经解析出来了=1,然后再单步运行,BaseContext已经把empId存进去了
    在这里插入图片描述
  4. 点击放行Resume Program,运行到EmployeeServiceImpl.java,我们想获取BaseContext.getCurrentId()的返回结果,我们可以选中它,然后右键,选择Evaluate Expression…,点击Evaluate,计算出结果value = 1,这个就是当前登录用户id。
    在这里插入图片描述
    Alt点击employee,也可以看到
    在这里插入图片描述
  5. 放行Resume Program,看控制台, updates: 1,新增成功。
    在这里插入图片描述
  6. navicat刷新一下,也看到了新增的王五的结果,create_user和update_user也都是1
    在这里插入图片描述
    7.至此已成功自动填充创建人,可以把//TODO删除了
提交代码

代码确认无误,点击Git→Conmmit,输入“新增员工业务代码开发”,再点击Commit and Push…
在这里插入图片描述
如果提交和推送后,弹出下面信息,不是失败,而是 IDE 在提交前做代码质量检查(Commit Checks)发现 62 个警告,让你先 Review。Review 就是让你“过一遍警告清单”,决定要不要修复。
觉得无关紧要 → 直接 Commit anyway
想保持代码整洁 → 逐条改掉再 commit
在这里插入图片描述
IDE 的 Review code analysis 会把所有 代码检查器(Inspection) 报出来的问题列成清单,常见类型如下:

类型 举例 是否必须修
缺失 JavaDoc 注释 public void save() 缺少方法注释 否(warning)
变量/导入未使用 import java.util.List 从未使用 否(warning)
潜在空指针 String s = null; int len = s.length(); 建议修(warning → 可能运行时异常)
语法/编译错误 少分号、类型不匹配 必须修(error)
代码风格 缩进、行长超过限制 否(warning)
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐