Skip to content

🤖 Tlias 智能学习辅助系统_1


前言


在黑马的 javaweb-ai :https://www.bilibili.com/video/BV1yGydYEE3H/?spm_id_from=333.788.video.desc.click&vd_source=2fbb4b17075136a98e03423a799add2c

这个课程中的 1-73 已经学习了 Web 开发的基础知识,包括像 Maven、HTTP 协议、SpringBootWeb 基础、

IOC、DI、MySQL、JDBC、Mybatis 等。

接下来呢,我们需要运用前面学的知识来完成一个大的综合案例 - Tlias 智能学习辅助系统 。


那这个案例里面呢,包括以下功能:

  • 部门管理:
80464463-8747-4065-a017-b14c57cf3699
  • 员工管理:
37b4e81c-cac6-4e5e-8c42-cf920f35aea8
  • 员工信息统计:
70281db7-4714-4dd1-89b6-de38343762bb
  • 学员信息统计:
2efc7c55-7fed-4c21-b7a4-6e495f7770dc
  • 班级、学员管理

📌

在整个实战篇中,我们需要完成如下功能:

  • 部门管理:查询、新增、修改、删除
  • 员工管理:
    • 查询、新增、修改、删除
    • 文件上传
  • 报表统计
  • 登录认证
  • 日志管理
  • 班级管理(自己实战内容)
  • 学员管理(自己实战内容)

一、准备工作


1.1 开发规范


1.1.1 前后端分离开发


在之前的课程中,我们介绍过,现在的企业项目开发有 2 种开发模式:

前后台混合开发前后台分离开发

前后台混合开发,顾名思义就是前台后台代码混在一起开发,如下图所示:

72f1c38a-bf76-48cd-9979-4ca2f7e54a80

这种开发模式有如下缺点:

  • 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用
  • 分工不明确:后台开发人员需要开发后台代码,也需要开发部分前端代码。很难培养专业人才
  • 不便管理:所有的代码都在一个工程中
  • 难以维护:前端代码更新,和后台无关,但是需要整个工程包括后台一起重新打包部署。

所以我们目前基本都是采用的 前后台分离开发 方式,如下图所示:

32ea3240-89b3-402f-98ca-08af2155977d
  • 我们将原先的工程分为前端工程和后端工程这 2 个工程,

    然后前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。

  • 前端页面需要数据,可以通过发送异步请求,从后台工程获取。

    但是,我们前后台是分开来开发的,那么前端人员怎么知道后台返回数据的格式呢?

    后端人员开发,怎么知道前端人员需要的数据格式呢?


  • 所以针对这个问题,我们前后台统一制定一套规范!我们前后台开发人员都需要遵循这套规范开发,

    这就是我们的 接口文档 。接口文档有离线版和在线版本,

    在线接口文档地址: https://heuqqdmbyk.feishu.cn/wiki/GyZVwpRf6ir89qkKtFqc0HlTnhg

    txt
    通过网盘分享的文件:接口文档
    链接: https://pan.baidu.com/s/1l0F6CE-8q0cgR7YoK0D6MA?pwd=musv 提取码: musv

  • 那么接口文档的内容怎么来的呢?是我们后台开发者根据产品经理提供的产品原型和需求文档

    所撰写出来的,产品原型如下提供:

    txt
    通过网盘分享的文件:页面原型
    链接: https://pan.baidu.com/s/1e8Up596zA_ME7Qtx7iU9LA?pwd=n95m 提取码: n95m

  • 那么基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢?如下图所示:

0a363414-0a00-46af-accc-6bb4fc1f0930
  1. 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
  2. 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
  3. 前后台并行开发:各自按照接口文档进行开发,实现需求
  4. 测试:前后台开发完了,各自按照接口文档进行测试
  5. 前后段联调测试:前段工程请求后端工程,测试功能

1.1.2 Restful 风格


该案例是基于当前最为主流的前后端分离模式进行开发。

a987ee7a-d8b5-4ff6-b0f0-3ecb1d89887f
  • 前后端进行交互的时候,我们需要基于当前主流的 REST 风格的 API 接口进行交互。

  • 什么是 REST 风格 呢?
    • RESTRepresentational State Transfer),表述性状态转换,它是一种软件架构风格。

  • 传统 URL 风格如下:
    • http://localhost:8080/user/getById?id=1 GET:查询 id 为 1 的用户
    • http://localhost:8080/user/saveUser POST:新增用户
    • http://localhost:8080/user/updateUser POST:修改用户
    • http://localhost:8080/user/deleteUser?id=1 GET:删除 id 为 1 的用户

区别:

我们看到,原始的传统 URL 呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。

而且,对于开发人员来说,每一个开发人员都有自己的命名习惯,就拿根据 id 查询用户信息来说的,

不同的开发人员定义的路径可能是这样的:getByIdselectByIdqueryByIdloadById... 。

每一个人都有自己的命名习惯,如果都按照各自的习惯来,一个项目组,几十号或上百号人,

那最终开发出来的项目,将会变得难以维护,没有一个统一的标准。


基于 REST 风格 URL 如下:

  • http://localhost:8080/users/1 GET:查询 id 为 1 的用户
  • http://localhost:8080/users POST:新增用户
  • http://localhost:8080/users PUT:修改用户
  • http://localhost:8080/users/1 DELETE:删除 id 为 1 的用户

其中总结起来,就一句话:通过 URL 定位要操作的资源,通过 HTTP动词(请求方式) 来描述具体的操作。


在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。

  • GET : 查询
  • POST :新增
  • PUT : 修改
  • DELETE :删除

我们看到如果是基于 REST 风格,定义URL,URL 将会 更加简洁、更加规范、更加优雅


📌注意事项:

  • REST 是风格,是约定方式,约定不是规定,可以打破

  • 描述模块的功能通常使用复数,也就是加 s 的格式来描述,表示此类资源,而非单个资源。

    如:users、emps、books…


1.1.3 Apifox


我们上面讲到,在这个案例中,我们将会基于 Restful 风格的接口进行交互,

那么其中就涉及到常见的 4 种请求方式,包括:POSTDELETEPUTGET

feb0c6ee-d871-4bc3-8fa5-7b588516f5d2

因为在 浏览器地址栏 中所发起的所有的请求,都是 GET 方式的请求。那大家就需要思考两个问题:

  • 前后端都在并行开发,后端开发完对应的接口之后,如何对接口进行请求测试呢?
  • 前后端都在并行开发,前端开发过程中,如何获取到数据,测试页面的渲染展示呢?

那这里我们就可以借助一些接口测试工具,比如项:PostmanApipostApifox 等。

3bd0bec2-f902-4053-a546-81303d656641

那这些工具的使用基本类似,只不过 Apifox 工具的功能更强强大、更加完善。


1.1.3.1 介绍

介绍:Apifox 是一款集成了 Api 文档Api 调试Api MockApi 测试 的一体化协作平台。

作用:接口文档管理、接口请求测试、Mock 服务。

官网: https://apifox.com/


384c8ecf-4de0-471f-badc-4486c626f8b4
1.1.3.2 安装

  • 获取安装包:

      1. 网盘链接提供
    txt
    通过网盘分享的文件:Apifox-2.6.9.exe
    链接: https://pan.baidu.com/s/1hMHPpT9z38PNsWChU6oz8Q?pwd=cg5d 提取码: cg5d
      1. 提供的官网地址下载

  • 安装完成之后,就可以扫码登录 Apifox ,就可以使用啦~
22e3cfb2-2f75-412e-9edf-b2e87b922544
678b6bab-2bde-49e0-b94d-951d6fea8e5e

1.2 工程搭建


  • 1). 创建 SpringBoot 工程,并引入web开发起步依赖、mybatis、mysql驱动、lombok。
6f4db8b9-5747-4ae4-a8d0-02e1e19401c6
  • 2). 创建数据库及对应的表结构,并在 application.yml 中配置数据库的基本信息。

  • 创建 tlias 数据库,并准备 dept 部门表。

sql
CREATE TABLE dept (
  id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 主键',
  name varchar(10) NOT NULL UNIQUE COMMENT '部门名称',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '修改时间'
) COMMENT '部门表';

INSERT INTO dept VALUES 
(1,'学工部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(2,'教研部','2023-09-25 09:47:40','2024-08-09 15:17:04'),
(3,'咨询部','2023-09-25 09:47:40','2024-07-30 21:26:24'),
(4,'就业部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(5,'人事部','2023-09-25 09:47:40','2024-07-25 09:47:40'),
(6,'行政部','2023-11-30 20:56:37','2024-07-30 20:56:37');

  • application.yml 配置文件中配置数据库的连接信息。
yml
# 应用名称
spring:
  application:
    name: tlias-web-management
    
  # mysql连接配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/tlias
    username: root
    password: 1234
    
# mybatis 的配置信息
mybatis:
  configuration:
  	# mybatis 日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

  • 3). 准备基础包结构,并引入实体类 Dept 及统一的响应结果封装类 Result

  • 准备基础包结构:

427c884e-4a18-4e95-8760-046aa54f1acc
  • 实体类 Dept :
java
package com.itheima.pojo;

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

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
    private Integer id;
    private String name;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

  • 统一响应结果 Result
java
package com.itheima.pojo;

import lombok.Data;
import java.io.Serializable;

/**
 * 后端统一返回结果
 */
@Data
public class Result {

    private Integer code; //编码:1成功,0为失败
    private String msg; //错误信息
    private Object data; //数据

    public static Result success() {
        Result result = new Result();
        result.code = 1;
        result.msg = "success";
        return result;
    }

    public static Result success(Object object) {
        Result result = new Result();
        result.data = object;
        result.code = 1;
        result.msg = "success";
        return result;
    }

    public static Result error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

  • 基础代码结构:
微信截图_20250721223817
  • DeptMapper :
java
package com.itheima.mapper;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DeptMapper {
}

  • DeptService :
java
package com.itheima.service;

public interface DeptService {
}

  • DeptServiceImpl
java
package com.itheima.service.impl;

import com.itheima.service.DeptService;
import org.springframework.stereotype.Service;

@Service
public class DeptServiceImpl implements DeptService {
}

  • DeptController :
java
package com.itheima.controller;

import org.springframework.web.bind.annotation.RestController;

/**
 * 部门管理控制器
 */
@RestController
public class DeptController {
}

这样的话,这个项目的基本的架子就搭建好了 ~


二、查询部门


2.1 基本实现


2.1.1 需求


查询所有的部门数据,查询出来展示在部门管理的页面中。页面原型效果如下:

0c5af372-85b7-4eb5-b990-166117ecf104

2.1.2 接口描述

  • 参照提供的接口文档。 部门管理 -> 部门列表查询

2.1.3 实现思路


明确了删除部门的需求之后,再来梳理一下实现该功能时,三层架构每一层的职责:

微信截图_20250725012739
  • Controller 层,负责接收前端发起的请求,并调用 service 查询部门数据,然后响应结果。
  • Service 层,负责调用 Mapper 接口方法,查询所有部门数据。
  • Mapper 层,执行查询所有部门数据的操作。

2.1.4 代码实现


  • 1). Controller层

DeptController 中,增加 list 方法,代码如下:

java
/**
 * 部门管理控制器
 */
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    @RequestMapping("/depts")
    public Result list(){
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }
}

  • 2). Service层

DeptService 中,增加 findAll方法,代码如下:

java
public interface DeptService {
    /**
     * 查询所有部门
     */
    public List<Dept> findAll();
}

  • DeptServiceImpl 中,增加 findAll方法,代码如下:
java
@Service
public class DeptServiceImpl implements DeptService {
    
    @Autowired
    private DeptMapper deptMapper;

    public List<Dept> findAll() {
        return deptMapper.findAll();
    }
}

  • 3). Mapper层

DeptMapper 中,增加 findAll方法,代码如下:

java
@Mapper
public interface DeptMapper {
    /**
     * 查询所有部门
     */
    @Select("select * from dept")
    public List<Dept> findAll();
    
}

2.1.5 接口测试


  • 启动项目,然后我们就可以打开 Apifox 进行测试了。
35a34c71-f347-4532-b4d0-700504eede2e
  • 我们发现,已经查询出了所有的部门数据,并且响应回来的就是 json 格式的数据,与接口文档一致。

    那接下来,我们再来测试一下,这个查询操作,我们使用 post、put、delete 方式来请求,

    是否可以获取到数据 ... ...

6a98498b-d4d6-41f3-8a6d-23f812deb6d2

经过测试,我们发现,现在我们其实是可以通过任何方式的请求来访问查询部门的这个接口的。

而在接口文档中,明确要求该接口的请求方式为 GET ,那么如何限制请求方式呢?


  • 方式一:在 controller 方法的 @RequestMapping 注解中通过 method 属性来限定。
java
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    @RequestMapping(value = "/depts", method = RequestMethod.GET)
    public Result list(){
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }
}

  • 方式二:在 controller 方法上使用,@RequestMapping 的衍生注解 @GetMapping

    该注解就是标识当前方法,必须以 GET 方式请求。

java
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    @GetMapping("/depts")
    public Result list(){
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }
}

📌 上述两种方式,在项目开发中,推荐使用第二种方式,简洁、优雅。

  • GET 方式:@GetMapping
  • POST 方式:@PostMapping
  • PUT 方式:@PutMapping
  • DELETE 方式:@DeleteMapping

2.1.6 数据封装


在上述测试中,我们发现部门的数据中,idname 两个属性是有值的,

但是 createTimeupdateTime 两个字段值并未成功封装,

而数据库中是有对应的字段值的,这是为什么呢?

d732378c-f021-4c89-a9b6-81cee2de6a99

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis 会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  • 手动结果映射
  • 起别名
  • 全局配置文件开启驼峰命名

  • 1). 手动结果映射

DeptMapper 接口方法上,通过 @Results@Result 进行手动结果映射。

java
@Results({@Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, name, create_time, update_time from dept")
public List<Dept> findAll();

  • 说明:

    • @Results 注解源码:
    212df0d3-c9a6-42a2-b674-db18833dd628
    • @Result 源代码:
    9a7e6919-eaee-4adb-84d5-fa14fa4d001b
  • 2). 起别名

在 SQL 语句中,对不一样的列名起别名,别名和实体类属性名一样。

java
@Select("select id, name, create_time createTime, update_time updateTime from dept")
public List<Dept> findAll();

  • 3). 开启驼峰命名 (推荐)

  • 如果字段名与属性名符合驼峰命名规则,mybatis 会自动通过驼峰命名规则映射。

    驼峰命名规则: abc_xyz => abcXyz

    • 表中字段名:abc_xyz
    • 类中属性名:abcXyz

application.yml 中做如下配置,开启开关。

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

📌 要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。


2.2 前后端联调


2.2.1 联调测试


完成了查询部门的功能,我们也通过 Apifox 工具测试通过了,

下面我们再基于前后端分离的方式进行接口联调。具体操作如下:

  • 1). 把提供的 nginx 压缩包 拷贝到一个没有中文不带空格的目录下。
txt
通过网盘分享的文件:nginx-1.22.0-web.zip
链接: https://pan.baidu.com/s/18XSYGgf4Fe3asVDhHHljAw?pwd=r6gt 提取码: r6gt
1fc6b6d4-c7aa-4267-b3e9-581cab05af97
  • 2). 解压(解压到当前目录)
cd701707-cc72-4888-8078-b5093060fb4b
  • 3). 双击 nginx.exe 启动Nginx,一闪而过,就说明 nginx 已启动完成。
ee7d21ab-3589-4322-8777-30b0c9e08911

如果在任务管理器中,能看到上述两个进程,就说明 nginx 已经启动成功。


  • **4). 打开浏览器,访问:http://localhost:90 **
d7508480-d288-438f-853f-5d66d46d3d37

2.2.2 请求访问流程


  • ❓ ​前端工程请求服务器的地址为 http://localhost:90/api/depts

    是如何访问到后端的 tomcat 服务器的 ?

13be81b2-96a6-4a26-a275-b2576819c9db

其实这里,是通过前端服务 Nginx 中提供的 反向代理 功能实现的。

  • 反向代理:(reverse proxy),指的是代理外网用户的请求到内部的指定的服务器,

    并将数据返回给用户的一种方式客户端不直接与后端服务器进行通信,

    而是与反向代理服务器进行通信 , 隐藏了后端服务器的 IP 地址

271a8d30-ca19-4ae9-b5a0-a44406db630e
  • 1). 浏览器发起请求,请求的是 localhost:90 ,那其实请求的是 nginx 服务器。

  • 2). 在 nginx 服务器中呢,并没有对请求直接进行处理,而是将请求转发给了后端的 tomcat 服务器,

    最终由 tomcat 服务器来处理该请求。


  • 这个过程就是通过 nginx 的反向代理 实现的。

    那为什么浏览器不直接请求后端的 tomcat 服务器,

    而是直接请求 nginx 服务器呢,主要有以下几点原因:

6f425239-e270-4b38-96c6-074fab42de55
  • 1). 安全:由于后端的 tomcat 服务器一般都会搭建集群,会有很多的服务器,

    把所有的 tomcat 暴露给前端,让前端直接请求 tomcat,对于后端服务器是比较危险的。

  • 2). 灵活:基于nginx 的反向代理 实现,更加灵活,后端想增加、减少服务器,

    对于前端来说是无感知的,只需要在 nginx 中配置即可。

  • 3). 负载均衡:基于 nginx 的反向代理,可以很方便的实现后端 tomcat 的负载均衡操作。


具体的请求访问流程如下:

2e44920c-48eb-4117-8c13-31312e4ccbc0
  1. location:用于定义匹配特定 uri 请求的规则。
  2. ^~ /api/:表示精确匹配,即只匹配以 /api/ 开头的路径。
  3. rewrite:该指令用于重写匹配到的 uri 路径。
  4. proxy_pass:该指令用于代理转发,它将匹配到的请求转发给位于后端的指令服务器。

三、删除部门


3.1 需求


  • 删除部门数据。在点击 "删除" 按钮,会根据 ID 删除部门数据。
b647034b-922a-4cc5-8f19-08c53519a861

了解了需求之后,我们再看看接口文档中,关于删除部门的接口的描述,

然后根据接口文档进行服务端接口的开发。


3.2 接口描述

  • 参照课程资料中提供的接口文档。 部门管理 -> 删除部门

3.3 思路分析


  • 明确了删除部门的需求之后,再来梳理一下实现该功能时,三层架构每一层的职责:
微信截图_20250725013043

3.4 简单参数接收


  • 我们看到,在 controller 中,需要接收前端传递的请求参数。

    那接下来,我们就先来看看在服务器端的 Controller 程序中,

    如何获取这类简单参数。 具体的方案有如下三种:


  • 方案一:通过原始的 HttpServletRequest 对象获取请求参数
java
/**
* 根据ID删除部门 - 简单参数接收: 方式一 (HttpServletRequest)
*/
@DeleteMapping("/depts")
public Result delete(HttpServletRequest request){
    String idStr = request.getParameter("id");
    int id = Integer.parseInt(idStr);
    
    System.out.println("根据ID删除部门: " + id);
    return Result.success();
}

这种方案实现较为繁琐,而且还需要进行手动类型转换。【项目开发很少用】


  • 方案二:通过Spring提供的 @RequestParam 注解,将请求参数绑定给方法形参
java
@DeleteMapping("/depts")
public Result delete(@RequestParam("id") Integer deptId){
    System.out.println("根据ID删除部门: " + deptId);
    return Result.success();
}

@RequestParam 注解的 value 属性,需要与前端传递的参数名保持一致 。

📌 注意:

@RequestParam 注解 required 属性默认为 true,代表该参数必须传递,

如果不传递将报错。 如果参数可选,可以将属性设置为 false


  • 方案三:如果请求参数名与形参变量名相同,直接定义方法形参即可接收。(省略@RequestParam)
java
@DeleteMapping("/depts")
public Result delete(Integer id){
    System.out.println("根据ID删除部门: " + deptId);
    return Result.success();
}

对于以上的这三种方案呢,我们 推荐第三种方案


3.5 代码实现


  • 1). Controller层

DeptMapper 中,增加 delete 方法,代码实现如下:

java
/**
 * 根据id删除部门 - delete http://localhost:8080/depts?id=1
 */
@DeleteMapping("/depts")
public Result delete(Integer id){
    System.out.println("根据id删除部门, id=" + id);
    deptService.deleteById(id);
    return Result.success();
}

  • 2). Service层

DeptService 中,增加 deleteById 方法,代码实现如下:

java
/**
 * 根据id删除部门
 */
void deleteById(Integer id);

DeptServiceImpl 中,增加 deleteById 方法,代码实现如下:

java
public void deleteById(Integer id) {
    deptMapper.deleteById(id);
}

  • 3). Mapper层

DeptMapper 中,增加 deleteById 方法,代码实现如下:

java
/**
 * 根据id删除部门
 */
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);

  • 如果 mapper 接口方法形参只有一个普通类型的参数,

    #{…} 里面的属性名可以随便写,如:#{id}#{value}

  • 对于 DML 语句来说,执行完毕,也是有返回值的,返回值代表的是增删改操作,影响的记录数,

    所以可以将执行 DML 语句的方法返回值设置为 Integer。 但是一般开发时,

    是不需要这个返回值的,所以也可以设置为 void


代码编写完毕之后,我们就可以启动服务,进行测试了。

2f532e61-3801-409b-a064-6f74bbd4f60f

四、新增部门


4.1 需求


点击 "新增部门" 的按钮之后,弹出新增部门表单,填写部门名称之后,点击确定之后,保存部门数据。

5bafef2b-badd-4b56-b225-46aff2504760

了解了需求之后,我们再看看接口文档中,关于新增部门的接口的描述,

然后根据接口文档进行服务端接口的开发 。


4.2 接口描述

参照课程资料中提供的接口文档。 部门管理 -> 新增部门


4.3 思路分析


明确了新增部门的需求之后,再来梳理一下实现该功能时,三层架构每一层的职责:

微信截图_20250725013307

4.4 json 参数接收


我们看到,在 controller 中,需要接收前端传递的请求参数。

那接下来,我们就先来看看在服务器端的 Controller 程序中,如何获取 json 格式的参数。

  • JSON 格式的参数,通常会使用一个 实体对象 进行接收 。
  • 规则:JSON 数据的键名与方法形参对象的属性名相同,并需要使用 @RequestBody 注解标识。

前端传递的请求参数格式为 json,内容如下:{"name":"研发部"}

这里,我们可以通过一个对象来接收,只需要保证对象中有 name 属性即可。

6630083a-09b7-434a-909c-ed397b63f068

4.5 代码实现


  • 1). Controller层

DeptController 中增加方法 save,具体代码如下:

java
/**
 * 新增部门 - POST http://localhost:8080/depts   请求参数:{"name":"研发部"}
 */
@PostMapping("/depts")
public Result save(@RequestBody Dept dept){
    System.out.println("新增部门, dept=" + dept);
    deptService.save(dept);
    return Result.success();
}

  • 2). Service层

DeptService 中增加接口方法 save ,具体代码如下:

java
/**
 * 新增部门
 */
void save(Dept dept);

DeptServiceImpl 中增加 save 方法,完成添加部门的操作,具体代码如下:

java
public void save(Dept dept) {
    //补全基础属性
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    //保存部门
    deptMapper.insert(dept);
}

  • 3). Mapper层
java
/**
 * 保存部门
 */
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
  • 如果在 mapper 接口中,需要传递多个参数,可以把多个参数封装到一个对象中。

    SQL 语句中获取参数的时候,#{...} 里面写的是 对象的属性名【注意是属性名,不是表的字段名】


代码编写完毕之后,我们就可以启动服务,进行测试了。

67207e9d-ca6c-4b74-8df1-64353ecedad6


50f34ede-ec5f-4991-a00a-484c7117adef


五、修改部门


对于任何业务的修改功能来说,一般都会分为两步进行:查询回显修改数据


5.1 查询回显


5.1.1 需求


当我们点击 "编辑" 的时候,需要根据 ID 查询部门数据,然后用于页面回显展示。

526ba4b5-c3e4-41de-bc7f-edae94c90dda

5.1.2 接口描述

参照参照课程资料中提供的接口文档。 部门管理 -> 根据ID查询


5.1.3 思路分析


明确了根据 ID 查询部门的需求之后,再来梳理一下实现该功能时,三层架构每一层的职责:

微信截图_20250725015938

了解了需求之后,我们再看看接口文档中,关于根据 ID 查询部门的接口的描述,

然后根据接口文档进行服务端接口的开发 。


5.1.4 路径参数接收


/depts/1/depts/2 这种在 url 中传递的参数,我们称之为 路径参数

那么如何接收这样的路径参数呢 ?

路径参数:通过请求 URL 直接传递参数,使用 {…} 来标识该路径参数,

需要使用 @PathVariable 获取路径参数。

如下所示:

3f544303-c561-48c4-813b-35a37db879ee

如果路径参数名与 controller 方法形参名称一致,

@PathVariable 注解的 value 属性是可以省略的 ( 如上右图 )。


5.1.5 代码实现


  • 1). Controller层

DeptController 中增加 getById 方法,具体代码如下:

java
/**
 * 根据ID查询 - GET http://localhost:8080/depts/1
 */
@GetMapping("/depts/{id}")
public Result getById(@PathVariable Integer id){
    System.out.println("根据ID查询, id=" + id);
    Dept dept = deptService.getById(id);
    return Result.success(dept);
}

  • 2). Service层

DeptService 中增加 getById方法,具体代码如下:

java
/**
 * 根据id查询部门
 */
Dept getById(Integer id);

DeptServiceImpl 中增加 getById方法,具体代码如下:

java
public Dept getById(Integer id) {
    return deptMapper.getById(id);
}

  • 3). Mapper层

DeptMapper 中增加 getById 方法,具体代码如下:

java
/**
* 根据ID查询部门数据
*/
@Select("select id, name, create_time, update_time from dept where id = #{id}")
Dept getById(Integer id);

代码编写完毕之后,我们就可以启动服务,进行测试了。

714d6558-af1f-4d2c-976a-68fb4b22a671

5.2 修改数据


5.2.1 需求


查询回显回来之后,就可以对部门的信息进行修改了,修改完毕之后,点击确定,

此时,就需要根据 ID 修改部门的数据。

eee41dae-aa2f-4d12-824d-40544f015bba

了解了需求之后,我们再看看接口文档中,关于修改部门的接口的描述,

然后根据接口文档进行服务端接口的开发 。


5.2.2 接口描述

参照参照课程资料中提供的接口文档。 部门管理 -> 修改部门


5.2.3 思路分析


参照接口文档,梳理三层架构每一层的职责:

微信截图_20250725032859

通过接口文档,我们可以看到前端传递的请求参数是 json 格式的请求参数,

Controller 的方法中,我们可以通过 @RequestBody 注解来接收,并将其封装到一个对象中。


5.2.4 代码实现


  • 1). Controller层

DeptController 中增加 update 方法,具体代码如下:

java
/**
 * 修改部门 - PUT http://localhost:8080/depts  请求参数:{"id":1,"name":"研发部"}
 */
@PutMapping("/depts")
public Result update(@RequestBody Dept dept){
    System.out.println("修改部门, dept=" + dept);
    deptService.update(dept);
    return Result.success();
}

  • 2). Service层

DeptService 中增加 update 方法。

java
/**
 * 修改部门
 */
void update(Dept dept);

DeptServiceImpl 中增加 update 方法。

由于是修改操作,每一次修改数据,都需要更新 updateTime 。所以,具体代码如下:

java
public void update(Dept dept) {
    //补全基础属性
    dept.setUpdateTime(LocalDateTime.now());
    //保存部门
    deptMapper.update(dept);
}

  • 3). Mapper层

DeptMapper 中增加 update 方法,具体代码如下:

java
/**
 * 更新部门
 */
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void update(Dept dept);

代码编写完毕之后,我们就可以启动服务,进行测试了。

2a88017c-40e2-4940-ad65-1cd6e376bddc

修改完成之后,我们可以看到最新的数据,如下:

f97010ff-2df7-4020-95f6-95d0b1b5e159

5.2.5 @RequestMapping


到此呢,关于基本的部门的增删改查功能,我们已经实现了。

我们会发现,我们在 DeptController 中所定义的方法,所有的请求路径,都是 /depts 开头的,

只要操作的是部门数据,请求路径都是 /depts 开头。

那么这个时候,我们其实是可以把这个公共的路径 /depts 抽取到类上的,

那在各个方法上,就可以省略了这个 /depts 路径。 代码如下:

690acd32-242d-4856-8d8f-2ce528c87ee9

📌 总的来说:

一个完整的请求路径,应该是类上的 @RequestMapping

value 属性 + 方法上的 @RequestMappingvalue 属性。


六、日志技术


6.1 概述


  • 什么是日志?
    • 日志就好比生活中的日记,可以随时随地记录你生活中的点点滴滴。
    • 程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息的。

  • 为什么要在程序中记录日志呢?
    • 便于追踪应用程序中的数据信息、程序的执行过程。
    • 便于对应用程序的性能进行优化。
    • 便于应用程序出现问题之后,排查问题,解决问题。
    • 便于监控系统的运行状态。
    • ... ...

  • ❓ ​之前我们编写程序时,也可以通过 System.out.println(...) 来输出日志

    为什么我们还要学习单独的日志技术呢?

    这是因为,如果通过 System.out.println(...) 来记录日志,会存在以下几点问题:

    • 硬编码。所有的记录日志的代码,都是硬编码,没有办法做到灵活控制,

      要想不输出这个日志了,只能删除掉记录日志的代码。

    • 只能输出日志到控制台。

    • 不便于程序的扩展、维护。


所以,在现在的项目开发中,我们一般都会使用专业的日志框架,来解决这些问题。


6.2 日志框架


526c402e-4dbd-486f-99a3-11edc8ea7413
  • JUL : 这是JavaSE平台提供的官方日志框架,也被称为JUL。配置相对简单,但不够灵活,性能较差。

  • Log4j : 一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。

  • Logback : 基于 Log4j 升级而来,提供了更多的功能和配置选项,性能优于 Log4j

  • Slf4j : (Simple Logging Facade for Java)简单日志门面,

    提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。


6.3 logback 入门


  • 1). 准备工作:引入 logback 的依赖( springboot 中无需引入,在 springboot已经传递了此依赖
xml
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

  • 引入如下配置文件 logback.xml ( 可创建一个 logback.xml 放在 src/main/resources 目录下

    复制下面提供的代码,也可直接用 ai 生成 )

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度  %msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
        </encoder>
    </appender>

    <!-- 日志输出级别 -->
    <root level="ALL">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

  • 3). 记录日志:定义日志记录对象 Logger,记录日志
    • 下面的类位置 test/java/com.itheima.LogTest
    • 注意导入的是 org.slf4j 下的
java
package com.itheima;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest {
    
    //定义日志记录对象
    private static final Logger log = LoggerFactory.getLogger(LogTest.class);

    @Test
    public void testLog(){
        log.debug("开始计算...");
        int sum = 0;
        int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        log.info("计算结果为: "+sum);
        log.debug("结束计算...");
    }

}

运行单元测试,可以在控制台中看到输出的日志,如下所示:

62c0115d-b890-4603-bd6a-ae9d738a9a6a

我们可以看到在输出的日志信息中,不仅输出了日志的信息,

还包括:日志的输出时间、线程名、具体在那个类中输出的。


6.4 logback 配置文件


Logback 日志框架的配置文件叫 logback.xml

该配置文件是对 Logback 日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等。

常用的两种输出日志的位置:控制台、系统文件。


  • 1). 如果需要输出日志到控制台。添加如下配置:
xml
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
    </encoder>
</appender>

  • 2). 如果需要输出日志到文件。添加如下配置:
xml
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!-- 日志文件输出的文件名, %i表示序号 -->
        <FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
        <!-- 最多保留的历史日志文件数量 -->
        <MaxHistory>30</MaxHistory>
        <!-- 最大文件大小,超过这个大小会触发滚动到新文件,默认为 10MB -->
        <maxFileSize>10MB</maxFileSize>
    </rollingPolicy>

    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
    </encoder>
</appender>

  • 3). 日志开关配置 (开启日志(ALL),取消日志(OFF))
xml
<!-- 日志输出级别 -->
<root level="ALL">
    <!--输出到控制台-->
    <appender-ref ref="STDOUT" />
    <!--输出到文件-->
    <appender-ref ref="FILE" />
</root>

6.5 日志级别


日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(优先级由低到高):

日志级别说明记录方式
trace追踪,记录程序运行轨迹【使用很少】log.trace("...")
debug调试,记录程序调试过程中的信息,
实际应用中一般将其视作最低级别【使用较多】
log.debug("...")
info记录一般信息,描述程序运行的关键事件,
如:网络连接、io 操作 【使用较多】
log.info("...")
warn警告信息,记录潜在有害的情况【使用较多】log.warn("...")
error错误信息 【使用较多】log.error("...")

  • 可以在配置文件logback.xml中,灵活的控制输出那些类型的日志。

    (大于等于配置的日志级别的日志才会输出)

xml
<!-- 日志输出级别 -->
<root level="info">
    <!--输出到控制台-->
    <appender-ref ref="STDOUT" />
    <!--输出到文件-->
    <appender-ref ref="FILE" />
</root>

6.6 案例日志记录


📌 改成下面代码之前注意:

把日志输出级别改成 info , 否则会输出很多底层源码的日志,导致控制台输出很多信息


java
/**
 * 部门管理控制器
 */
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    //@RequestMapping(value = "/depts", method = RequestMethod.GET)
    @GetMapping
    public Result list(){
        //System.out.println("查询部门列表");
        log.info("查询部门列表");
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }

    /**
     * 根据id删除部门 - delete http://localhost:8080/depts?id=1
     */
    @DeleteMapping
    public Result delete(Integer id){
        //System.out.println("根据id删除部门, id=" + id);
        log.info("根据id删除部门, id: {}" , id);
        deptService.deleteById(id);
        return Result.success();
    }

    /**
     * 新增部门 - POST http://localhost:8080/depts   请求参数:{"name":"研发部"}
     */
    @PostMapping
    public Result save(@RequestBody Dept dept){
        //System.out.println("新增部门, dept=" + dept);
        log.info("新增部门, dept: {}" , dept);
        deptService.save(dept);
        return Result.success();
    }

    /**
     * 根据ID查询 - GET http://localhost:8080/depts/1
     */
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        //System.out.println("根据ID查询, id=" + id);
        log.info("根据ID查询, id: {}" , id);
        Dept dept = deptService.getById(id);
        return Result.success(dept);
    }

    /**
     * 修改部门 - PUT http://localhost:8080/depts  请求参数:{"id":1,"name":"研发部"}
     */
    @PutMapping
    public Result update(@RequestBody Dept dept){
        //System.out.println("修改部门, dept=" + dept);
        log.info("修改部门, dept: {}" , dept);
        deptService.update(dept);
        return Result.success();
    }
}

附录


406 问题解决方案


如果基于最新版本的 SpringBoot 官方骨架创建的 SpringBoot 项目,

在勾选了 lombok 的依赖之后,会在 pom.xml 中引入如下两个插件:

9a79fa5b-8a11-424d-a195-2c3e9178b20d

由于第一个插件 maven-compiler-plugin 的引入导致了这个问题,

解决这个问题的方案呢,就是直接将第一个插件删除即可。

bd070a3d-e367-4bc2-a12a-78d22f2c5d58

否则,可能就会出现 406 问题,或者程序运行时出现如下错误:

9368b24d-48c2-4186-aa7b-e97ada6954a1

记得:pom.xml 配置文件一旦变动,记得重新加载,然后把整个项目 clean 一下,然后再执行。