创建一、Springboot 项目的搭建

1)创建一个空项目(空项目起手比较干净)1、tlais系统Springboot入门项目2)完成增删改查的基本依赖 start、lombok、mysql、mybatis    ,其他的依赖用哪个添加哪个

    <!--  一定有一个parent父工程  -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>
    <dependencies>
        <!--  跟spring环境有关系的依赖:start  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--  生成set get方法的依赖:lombok  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--  跟mysql有关系的依赖:   -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--  跟mybatis有关系的依赖  mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>
    </dependencies>
    </dependencies>

3)准备基础包结构,并引入实体类及统一的响应结果封装类Result ,准备基础包结构1、tlais系统Springboot入门项目

4)连接数据库

配置连接信息 application.yml

# 应用服务 WEB 访问端口
server:
  port: 8080

#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis:
  mapper-locations: classpath:mappers/*xml
  type-aliases-package: com.itheima.mybatis.entity #指定Mybatis的实体目录
  #mybatis
  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  日志输出
    #开启驼峰命名映射开关
    map-underscore-to-camel-case: true

# 配置数据库的链接信息
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/tlias
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234
    # spring:需要上传大文件的配置  不配置的话默认为1M
    servlet:
      multipart:
        max-file-size: 10MB
        max-request-size: 100MB

#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: java-426-ai
    region: cn-beijing

连接数据库

1、tlais系统Springboot入门项目

基础功能 三层架构:基本的增删改查

准备基础代码结构  完成基本的增删改查

TliasApplication项目启动类

@SpringBootApplication
@MapperScan("com.itheima.mapper")
public class TliasApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasApplication.class, args);
    }
}

DeptController

/**
 * 控制层: 负责接收前端发起的请求,并调用service查询部门数据,然后响应结果。
 */

@RequestMapping("/depts") //抽取请求地址重复的部分
@RestController
public class DeptController {
    //获取业务层对象
    @Autowired //从ioc 容器中找DeptService类型的bean对象
    private DeptService deptService;

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

    /**
     * 2.删除部门数据请求
     * @param id
     * @return
     */
    @DeleteMapping                    //除了查询有,增删改都没有返回值,返回执行成功的Success信息即可
    public Result deleteDept(Integer id) { //根据id删除对应数据,需要有参数id
        deptService.deleteDept(id);
        return Result.success();
    }

    /**
     * 3.新增部门数据
     * @param dept
     */
    @PostMapping                    //@RestController不是已经包含了@RequestBody吗?为什么加
    public Result insertDept(@RequestBody Dept dept) {
        deptService.insertDept(dept);
        return Result.success();
    }

    /**
     * 4.根据id查询部门
     * 数据回显请求
     * @return dept
     */
    @GetMapping("/{id}")            //再看一遍@PathVariable 注解用于将 URL 中的路径变量绑定到控制器方法的参数上,方便从请求 URL 里提取动态数据以进行后续处理。
    public Result selectDeptById(@PathVariable Integer id) {
        Dept dept = deptService.selectDeptById(id);
        return Result.success(dept);
    }

    /**
     * 5.修改数据
     * @param dept
     * @return
     */
    @PutMapping
    public Result updateDept(@RequestBody Dept dept) {
        deptService.updateDept(dept);
        return Result.success();
    }
}

DeptService

public interface DeptService {
    /**
     * 业务: 查询所有数据
     * @return 返回List<Dept>对象集合
     */
    List<Dept> findAll();

    /**
     * 业务: 根据id删除对应的数据
     * @param id
     */
    void deleteDept(Integer id);

    /**
     * 业务:  新增部门数据
     * @param dept
     */
    void insertDept(Dept dept);

    /**
     * 业务:  根据id查询部门(数据回显)业务
     * @param id
     * @return
     */
    Dept selectDeptById(Integer id);

    /**
     * 业务:  根据修改部门业务
     * @param dept
     */
    void updateDept(Dept dept);
}

DeptServiceImpl

//业务层(服务层): 负责处理业务逻辑,实现业务(接口)的功能,并返回数据给控制层

/**
 * @Service 介绍: 是 Spring 框架注解,用于将类标记为服务层组件,
 * 作用: 使 Spring 容器自动扫描并将其注册为 Bean 以处理业务逻辑。(把类的对象交给ioc)
 */

@Service
public class DeptServiceImpl implements DeptService {
   @Autowired //从ioc 容器中找DeptMapper类型的bean对象
    private DeptMapper deptMapper;

    /**
     * 业务:查询部门列表
     * @return
     */
    @Override
    public List<Dept> findAll() {
        //业务层向持久层要返回的对象集合 List<Dept>,然后把对象集合返回给控制层
        List<Dept> deptList = deptMapper.findAll();
        return deptList;
    }

    /**
     * 业务: 根据id删除对应的数据
     * @param id
     */
    @Override
    public void deleteDept(Integer id) {
        //判断这个部门下是否有员工,select count(*) from emp where dept_id?
//        Integer count = empMapper.selectEmpNumByDeptID(id);
//        if (count > 0){
//            throw new TliasException("对不起,当前部门下有员工,不能直接删除");
//        }
        deptMapper.deleteDept(id);
    }

    /**
     * 业务:  新增部门数据
     * @param dept
     */
    @Override
    public void insertDept(Dept dept) {
        //给对象补全属性   为什么给对象补全?只有名字吗
        dept.setCreateTime(LocalDateTime.now());
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.insertDept(dept);
    }

    /**
     * 业务:  根据id查询部门(数据回显)业务
     * @param id
     * @return
     */
    @Override
    public Dept selectDeptById(Integer id) {
        Dept dept = deptMapper.selectDeptById(id);
        return dept;
    }

    /**
     * 业务:  根据修改部门业务
     * @param dept
     */
    @Override
    public void updateDept(Dept dept) {
        //给对象补全属性
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.updateDept(dept);
    }
}

DeptMapper

//持久层: 和数据库打交道,负责用sql控制数据库中的数据,并返回数据给业务层

/**
 * @Mapper 是 MyBatis 框架中用于标记接口为 MyBatis 映射器的标签
 * 使 Spring 能自动扫描并将该接口注册为 Bean,以便使用其定义的方法进行数据库操作。
 */
@Mapper
public interface DeptMapper {
    /**
     * 通过sql语句查询所有部门
     * @return List<Dept>部门集合
     */
    @Select("select * from dept")
    List<Dept> findAll();

    /**
     * 通过sql语句根据id删除对应部门
     * @Delete("") 删除注解,
     * 注意:
     * 1. !!!  字段 = #{成员变量}
     * 2.!!! @Param("")
     *      需要向 SQL 映射语句传递多个参数时容易出错,
     *      @Param 注解可以为每个参数指定一个有意义的名称,使 SQL 映射文件中的参数引用更加清晰。
     */
    @Delete("delete from dept where id = #{id}")
    void deleteDept(@Param("id") Integer id);

    /**
     * 通过sql语句新增部门
     * @param dept
     */
    @Insert("insert into dept (name,create_time,update_time) value(#{name},#{createTime},#{updateTime})")
    void insertDept(Dept dept);

    /**
     * 通过sql语句根据ID查询部门回显和修改部门
     * @param id
     * @return
     */
    @Select("select * from dept where id = #{id}")
    Dept selectDeptById(@Param("id") Integer id);

    /**
     * 通过sql语句修改部门
     * @param dept
     */
    @Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
    void updateDept(Dept dept);
}

功能二、分页功能

Emp增删改查功能代码类似,不用的是多了多表查询(这块重点是SQL 语句)

1)实体类Emp、EmpExpr   分页查询返回的封装 PageResult<T> 、EmpQueryParam

@Data                                    //员工表1
public class EmpExpr {
    private Integer id; //ID
    private Integer empId; //员工ID
    private LocalDate begin; //开始时间
    private LocalDate end; //结束时间
    private String company; //公司名称
    private String job; //职位
}
@Data                                    //工作经历表2
public class EmpExpr {
    private Integer id; //ID
    private Integer empId; //员工ID
    private LocalDate begin; //开始时间
    private LocalDate end; //结束时间
    private String company; //公司名称
    private String job; //职位
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T>{
    private Long total;     // 总记录数
    private List<T> rows;   // 当前页的数据
}
@Data
public class EmpQueryParam {
    private Integer page = 1;       //页码
    private Integer pageSize = 10;  //每页展示记录数
    private String name;            //姓名
    private Integer gender;         //性别
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate begin;        //入职开始时间
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate end;          //入职结束时间

}

2)Emp员工管理的基础结构,包括Controller、Service、ServiceImpl、Mapper、Mapper.xml

分页功能涉及 分页、模糊查询、多表查询:引入分页插件依赖-三层架构-SQL模糊和条件查询

引入分页插件依赖

        <!-- 分页插件PageHelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.7</version>
        </dependency>
@RestController
public class EmpController {
    @Autowired
    private EmpService empService;

    /**
     * 1、分页查询请求
     *
     * @param empQueryParam
     * @return
     */
    @GetMapping("/emps")
    public Result selectEmp(EmpQueryParam empQueryParam) {
        //调用业务层方法,把页码和每页数据量传递过去
        PageResult<Emp> pageResult = empService.selectEmp(empQueryParam);
        return Result.success(pageResult);
    }

    /**
     * 2、查询所有员工信息
     *
     * @return
     */
    @GetMapping("/emps/list")  //注意路径
    public Result selectEmpAll() {
        List<Emp> empList = empService.selectEmpAll();
        return Result.success(empList);
    }

    /**
     * 3、添加员工请求
     *
     * @param emp
     * @return
     */
    @PostMapping("/emps")
    public Result save(@RequestBody Emp emp) throws Exception {
        empService.save(emp);
        return Result.success();
    }

    /**
     * 4、批量删除 员工请求
     *
     * @param ids
     * @return
     */
    @DeleteMapping("/emps")
    public Result deleteEmp(@RequestParam("ids") List<Integer> ids) {  //数组  集合都行
        empService.deleteEmp(ids);
        return Result.success();
    }

    /**
     * 5、修改员工信息 时的根据id数据回显(映射回显)的请求
     *
     * @param id
     * @return
     */
    @GetMapping("/emps/{id}")
    public Result getByID(@PathVariable("id") Integer id) {
        Emp emp = empService.getEmpWithExprById(id);
        return Result.success(emp);
    }
    /**
     * 6、修改员工信息:先删除后新增
     * @param emp
     * @return
     * @throws Exception
     */
    @PutMapping("/emps")
    public Result updateEmp(@RequestBody Emp emp) throws Exception {
        empService.updateEmp(emp);
        return Result.success();
    }

}

EmpService

//业务层:
public interface EmpService {
    /**
     * 分页查询的业务功能
     *
     * @param empQueryParam
     * @return
     */
    PageResult<Emp> selectEmp(EmpQueryParam empQueryParam);

     /**
     * 查询
     * @return
     */
    List<Emp> selectEmpAll();

    /**
     * 添加员工的业务功能
     *
     * @param emp
     */
    void save(Emp emp) throws Exception;

    /**
     * 批量删除员工的业务功能
     *
     * @param ids
     */
    void deleteEmp(List<Integer> ids);

    /**
     * 修改员工信息业务功能-回显:
     * 一次性把员工基本信息和员工工作经历都查询并回显出来
     *
     * @param id
     * @return
     */
    Emp getEmpWithExprById(Integer id);

    /**
     * 修改员工信息-修改:
     * 修改的逻辑->先删除后新增
     * @param emp
     */
    void updateEmp(Emp emp);

    /**
     * 登录功能
     * @param emp
     * @return
     */
    LoginInfo login(Emp emp);
}

EmpServiceImpl

//业务层:
@Service
public class EmpServiceImpl implements EmpService {
    //从ioc容器中找到持久层层对象
    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private EmpExprMapper empExprMapper;

    @Autowired
    private EmpLogService empLogService;

    /**
     * 分页查询的业务功能
     *
     * @param empQueryParam
     * @return
     */
    @Override
    public PageResult<Emp> selectEmp(EmpQueryParam empQueryParam) {
        //三步固定写法使用分页插件(使用前需要导入插件依赖 PageHelper)
        //1.设置分页参数
        PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());

        //2.调用查询所有的方法
        List<Emp> empList = empMapper.selectAll(empQueryParam);

        //3.把查询所有数据的集合强转成 Page 对象
        Page p = (Page) empList;

        //创建一个分页查询的数据封装对象(固定写法)                         //?什么是泛型
        PageResult<Emp> pageResult = new PageResult<>();
        pageResult.setTotal(p.getTotal());
        pageResult.setRows(p.getResult());
        //封装好的数据返回给控制层
        return pageResult;
    }

     /**
     * 查询
     *
     * @return
     */
    @Override
    public List<Emp> selectEmpAll() {
        List<Emp> empList = empMapper.selectAll(new EmpQueryParam());
        return empList;
    }

    /**
     * 添加员工的业务功能
     *
     * @param emp
     */
    //点一事务管理: 一组操作事务的集合,要么同时成功要么同时失败
    //分析: 转账一方转账出去,手账一方没收到就会回滚,退回给转账方

    //将当前方法交给spring进行事务管理,通过spring事务管理注解@Transactional控制业务层方法的事务
    //个人理解:添加该注解后进行事务管理,员工信息和员工经历一起添加成功或者失败弥补了之前程序的漏洞   (为什么用事务管理功能)
    //@Transactional

    //点二事务管理细节:
    //默认出现RuntimeException(运行时异常)才会回滚事务。
    //让所有的异常都回滚,配置@Transactional   注解属性rollbackFor即可实现  记住★★★
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void save(Emp emp) {
        try {
            //1.补全基础属性
            emp.setCreateTime(LocalDateTime.now());
            emp.setUpdateTime(LocalDateTime.now());
            emp.setPassword("123456");

            //2.保存员工基本信息
            empMapper.insert(emp);

            //int i = 1 / 0;
            // 检验事务管理是否开启,员工信息添加成功 员工经历添加失败 则都添加失败
            //if (true) {throw new Exception("出异常啦.....");}

            //3.保存员工的工作经历 - 批量
            Integer empId = emp.getId();
            List<EmpExpr> exprList = emp.getExprList();
            if (!CollectionUtils.isEmpty(exprList)) {
                exprList.forEach(empExpr -> empExpr.setEmpId(empId));
                empExprMapper.insertBatch(exprList);
            }
        } finally {
            //记录操作日志
            EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString());
            empLogService.insertLog(empLog);
        }
    }

    /**
     * 批量删除员工的业务功能
     *
     * @param ids
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteEmp(List<Integer> ids) {
        //删除员工基本信息
        empMapper.deleteByIds(ids);
        //删除员工工作经历
        empExprMapper.deleteByEmpIds(ids);
    }

    /**
     * 修改员工信息:
     * 一次性把员工基本信息和员工工作经历都查询出来
     *
     * @param id
     * @return
     */
    @Override
    public Emp getEmpWithExprById(Integer id) {
        return empMapper.getEmpWithEmpExprById(id);
    }

//    @Override
//    public Emp getEmpById(Integer id) {
//        //回显的简单写法: 执行两个SQL
//        //查询员工基本信息
//        Emp emp = empMapper.getById(id);
//        //查询此员工的工作经历
//        List<EmpExpr> exprList = empExprMapper.selectByEmpId(id);
//        return emp;
//    }

    /**
     * 修改员工信息:
     * 修改的逻辑->先删除后新增
     *
     * @param emp
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateEmp(Emp emp) {
        //1.更新员工的基本信息
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.updateById(emp);

        //2.删除此员工的工作经历
        Integer id = emp.getId();
        empExprMapper.deleteByEmpIds(Arrays.asList(id));

        //3.新增此员工的工作经历
        if (!CollectionUtils.isEmpty(emp.getExprList())) {
            emp.getExprList().forEach(empExpr -> empExpr.setEmpId(id));
            empExprMapper.insertBatch(emp.getExprList());
        }
    }

    /**
     * 登录功能
     *
     * @param emp
     * @return
     */
    @Override
    public LoginInfo login(Emp emp) {
        Emp emp1 = empMapper.selectByUsernameAndPassword(emp.getUsername(), emp.getPassword());
        if (emp1 != null) {
            LoginInfo loginInfo = new LoginInfo();
            loginInfo.setId(emp1.getId());
            loginInfo.setUsername(emp1.getUsername());
            loginInfo.setName(emp1.getName());
            //loginInfo.setToken();// TODD 先不处理
            //这会儿处理->使用JWT工具类生成令牌token
            Map<String, Object> claims = new HashMap<>();//这个map不能放用户的敏感信息 password
            claims.put("id", emp1.getId());
            claims.put("username", emp1.getUsername());
            String token = JwtUtils.generateJwt(claims);
            loginInfo.setToken(token);
            return loginInfo;
        }
        return null;
    }
}

EmpMapper

@Mapper
public interface EmpMapper {
    /**
     * 查询员工列表以及分页查询
     * @param empQueryParam
     * @return
     */
    List<Emp> selectAll(EmpQueryParam empQueryParam);

    /**
     * 新增员工数据
     * @param emp
     */
    //Mybatis的主键返回功能标签@Options: 保存工作经历后获取员工的ID 完成对应员工工作经历的新增
    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into emp(username,password, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)"  +
            "values (#{username},#{password},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
    void insert(Emp emp);

    /**
     * 删除员工数据
     * @param ids
     */
    void deleteByIds(@Param("ids") List<Integer> ids);

    //映射回显
    Emp getEmpWithEmpExprById(@Param("id") Integer id);
    Emp selectEmpById(@Param("id") Integer id);

    //修改员工数据
    void updateById(Emp emp);
    //查询部门人数
    Integer selectEmpNumByDeptID(@Param("deptId") Integer deptId);

}

EmpExprMapper

@Mapper
public interface EmpExprMapper {
    /**
     * 批量插入员工工作经历数据
     * @param exprList
     */
    void insertBatch(@Param("exprList") List<EmpExpr> exprList);

    void deleteByEmpIds(@Param("ids") List<Integer> ids);
}

EmpMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace属性的值 : 给的是接口的全限定名 (包名.接口名)
-->
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--
        id的属性值: 绑定的是方法名
    -->
    <delete id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

    <!--  修改员工信息  -->
    <update id="updateById">
        update emp
        <!--   优化修改功能: 单个修改时更加灵活更有扩展性     -->
        <!-- set标签的作用是: 消除sql语句中的,   -->
        <set>
            <if test="username != null and username !=''">username=#{username},</if>
            <if test="password != null and password !=''">password=#{password},</if>
            <if test="name != null and name !=''">name=#{name},</if>
            <if test="gender != null">gender=#{gender},</if>
            <if test="phone != null and phone !=''">phone=#{phone},</if>
            <if test="salary != null">salary=#{salary},</if>
            <if test="job != null">job=#{job},</if>
            <if test="image != null and image !=''">image=#{image},</if>
            <if test="entryDate != null">entry_date=#{entryDate},</if>
            <if test="deptId != null">dept_id=#{deptId},</if>
            <if test="updateTime != null">update_time=#{updateTime},</if>
        </set>
        where id=#{id}
    </update>
    <!-- 根据id查询员工信息(回显) -->
    <select id="selectEmpById" resultType="com.itheima.pojo.Emp">
        select * from emp where id = #{id};
    </select>

    <!--  查询员工基本信息和员工经历  -->
    <select id="selectAll" resultType="com.itheima.pojo.Emp">
        select emp.*,dept.name as deptName
        from emp
        left join dept on emp.dept_id = dept.id
        <where>
            <if test="name!=null and name!=''">
                emp.name like concat('%',#{name},'%')
            </if>
            <if test="gender!=null">
                and emp.gender = #{gender}
            </if>
            <if test="begin!=null and end!=null">
                and emp.entry_date between #{begin} and #{end}
            </if>
        </where>
    </select>

    <resultMap id="baseEmpMap" type="com.itheima.pojo.Emp">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="name" property="name"/>
        <result column="gender" property="gender"/>
        <result column="phone" property="phone"/>
        <result column="job" property="job"/>
        <result column="salary" property="salary"/>
        <result column="image" property="image"/>
        <result column="entry_date" property="entryDate"/>
        <result column="dept_id" property="deptId"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>

        <collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
            <id column="ee_id" property="id"/>
            <result column="ee_company" property="company"/>
            <result column="ee_job" property="job"/>
            <result column="ee_begin" property="begin"/>
            <result column="ee_end" property="end"/>
            <result column="ee_empid" property="empId"/>
        </collection>
    </resultMap>
    <select id="getEmpWithEmpExprById" resultMap="baseEmpMap">
        SELECT e.*,
        ee.id      ee_id,
        ee.emp_id  ee_emp_id,
        ee.begin   ee_begin,
        ee.end     ee_end,
        ee.company ee_company,
        ee.job     ee_job
        FROM emp e
        LEFT JOIN emp_expr ee ON ee.emp_id = e.id
        WHERE e.id = #{id}
    </select>

    <!-- 统计各个职位的员工人数 -->
    <select id="getEmpJobData" resultType="map">
        select
        case job when 1 then '班主任'
        when 2 then '讲师'
        when 3 then '学工主管'
        when 4 then '教研主管'
        when 5 then '咨询师'
        else '其他' end job
        ,count(*) total from emp group by job order by total desc
    </select>

    <!-- 统计员工的性别信息 -->
    <select id="getEmpGenderData" resultType="java.util.Map">
        select
        if(gender = 1,'男','女') as name,
        count(*) as value
        from emp group by gender;
    </select>
    <!-- 删除部门判断员工是否为空 -->
    <select id="selectEmpNumByDeptID" resultType="java.lang.Integer">
        select count(0)
        from emp
        where dept_id = #{deptId};
    </select>
</mapper>

EmpExprMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace属性的值 : 给的是接口的全限定名 (包名.接口名)
-->
<mapper namespace="com.itheima.mapper.EmpExprMapper">

    <!--
        id的属性值: 绑定的是方法名
    -->
    <!--  批量插入员工工作经历信息  -->
    <insert id="insertBatch">
        insert into emp_expr (emp_id,begin,end,company,job) values
        <foreach collection="exprList" item="expr" separator=",">
            (#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
        </foreach>
    </insert>

    <delete id="deleteByEmpIds">
        delete from emp_expr where emp_id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

功能三、分多表操作涉及事务管理

事务管理问题分析:

目前我们实现的新增员工功能中,操作了两次数据库,执行了两次 insert 操作。

第一次:保存员工的基本信息到 emp 表中。

第二次:保存员工的工作经历信息到 emp_expr 表中。

我们看到,员工表 emp 数据保存成功了, 但是 emp_expr 员工工作经历信息表,数据保存失败了。 那是否允许这种情况发生呢?不允许

因为这属于一个业务操作,如果保存员工信息成功了,保存工作经历信息失败了,就会造成数据库数据的不完整、不一致。

那如何解决这个问题呢? 这需要通过数据库中的事务来解决这个问题。

事务是一组操作的集合要么同时成功,要么同时失败

1)开启事务加一个@Transactional注解就行了:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

添加删除修改ServiceImpl业务层都加上@Transactional

  /**
     * 3、添加员工的业务功能
     *
     * @param emp
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void save(Emp emp) {

      /**
     * 4、批量删除员工的业务功能
     *
     * @param ids
     */
    @Transactional(rollbackFor = Exception.class)

     /**
     * 6、修改员工信息:修改的逻辑->先删除后新增
     *
     * @param emp
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
#spring事务管理日志配置
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

面试题:事务有哪些特性?

原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。

一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。

隔离性(Isolation)数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。

持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

功能四、上传图片

准备阿里云OSS

1)开通OSS-创建一个Bucket-配置AccessKey(AccessKey保存到本地环境变量)

2)引入依赖

<!--阿里云OSS依赖-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

3)AliyunOSSOperator配置阿里云oss的类 和读取类

@Component
public class AliyunOSSOperator {
    @Autowired
    private OperatorPropertie operatorPropertie;
    //修改一:改成自己的OSS服务器中的bucket对应的域名中
    //private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    //修改二:改成自己的OSS服务器中的bucket名称
    //private String bucketName = "java-426-ai";
    //修改三:改成自己的OSS服务器所属区域
    //private String region = "cn-beijing";

    /*优化
        为了方便统一管理   配置少的时候可以用@Value来读取配置文件中的值
        @Value("${aliyun.oss.endpoint}")
        private String endpoint;
        @Value("${aliyun.oss.bucketName}")
        private String bucketName;
        @Value("${aliyun.oss.region}")
        private String region;
     */

    /*  配置多的时候用@ConfigurationProperties注解
        需要单独来一个配置类,然后使用get方法获取配置文件中的值
     */
    public String upload(byte[] content, String originalFilename) throws Exception {

        String endpoint = operatorPropertie.getEndpoint();
        String bucketName = operatorPropertie.getBucketName();
        String region = operatorPropertie.getRegion();

        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        // 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
        //获取当前系统日期的字符串,格式为 yyyy/MM
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));  //2025/04
        //生成一个新的不重复的文件名
        String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = dir + "/" + newFileName;

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
        } finally {
            ossClient.shutdown();
        }

        return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
    }

}
@Data
@ConfigurationProperties(prefix = "aliyun.oss")//prefix 前缀
@Component
//@ConfigurationProperties 读取方式
public class OperatorPropertie {
    private String endpoint;
    private String bucketName;
    private String region;
}

4)上传文件的UploadController、UploadService、UploadServiceImpl

@RestController
public class UploadController {
    @Autowired
    private UploadService uploadService;

    /**
     * 上传文件
     */
    @PostMapping("/upload")
    public Result upload(MultipartFile file) throws Exception {
        String url = uploadService.upload(file);
        return Result.success(url);
    }
}
public interface UploadService {
    String upload(MultipartFile file) throws Exception;
}
@Service
public class UploadServiceImpl implements UploadService {
    @Autowired
    private AliyunOSSOperator aliyunOSSOperator;

    @Override
    public String upload(MultipartFile file) throws Exception {
        String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
        return url;
    }
}

5)阿里云Oss和上传文件的大小配置

# 配置数据库的链接信息
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/tlias
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234
    # spring:需要上传大文件的配置  不配置的话默认为1M
    servlet:
      multipart:
        max-file-size: 10MB
        max-request-size: 100MB

#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: java-426-ai
    region: cn-beijing

功能五、全局异常处理

问题分析

当我们在修改部门数据的时候,如果输入一个在数据库表中已经存在的手机号,点击保存按钮之后,前端提示了错误信息,但是返回的结果并不是统一的响应结果,而是框架默认返回的错误结果 。

1、tlais系统Springboot入门项目

状态码为500,表示服务器端异常,我们打开idea,来看一下,服务器端出了什么问题。

1、tlais系统Springboot入门项目

上述错误信息的含义是,emp员工表的phone手机号字段的值重复了,因为在数据库表emp中已经有了13309090027这个手机号了,我们之前设计这张表时,为phone字段建议了唯一约束,所以该字段的值是不能重复的。

而当我们再将该员工的手机号也设置为 13309090027,就违反了唯一约束,此时就会报错。

我们来看一下出现异常之后,最终服务端给前端响应回来的数据长什么样。

1、tlais系统Springboot入门项目

响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据 。

接下来我们需要思考的是出现异常之后,当前案例项目的异常是怎么处理的? 答案:没有做任何的异常处理

1、tlais系统Springboot入门项目

那么在三层构架项目中,出现了异常,该如何处理?

方案:全局异常处理器

1、tlais系统Springboot入门项目


我们该怎么样定义全局异常处理器?

1)定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。

2)在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。

GlobalExceptionAdvice

@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler(TliasException.class)
    public Result exceptionHandler(TliasException e) {//处理自定义异常
        e.printStackTrace();
        return Result.error(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result allException(Exception e){    //所有异常
        e.printStackTrace(); //报错打印到后台
            return Result.error("服务器异常,请联系管理员"); //Result统一数据格式
    }

    @ExceptionHandler(DuplicateKeyException.class)
    public Result allException(DuplicateKeyException e){    //有重复值的异常
        e.printStackTrace(); //报错打印到后台
        if (e.getMessage().contains("emp.username")){
            return Result.error("您新增的用户名已存在");
        }
        if (e.getMessage().contains("emp.phone")){
            return Result.error("对应手机号的用户已存在");
        }
        if (e.getMessage().contains("emp.image")){
            return Result.error("对应手已存在");
        }
        return Result.error("数据已存在"); //Result统一数据格式
    }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
重新启动SpringBoot服务,打开浏览器,再来测试一下 修改员工 这个操作,我们依然设置已存在的 13309090027这个手机号:
1、tlais系统Springboot入门项目
此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。

以上就是全局异常处理器的使用,主要涉及到两个注解:
@RestControllerAdvice //表示当前类为全局异常处理器
@ExceptionHandler //指定可以捕获哪种类型的异常进行处理


功能六、基本的登录认证

先上图看实现效果:

1、tlais系统Springboot入门项目

需求:

用户名、密码都正确才可以登录成功访问系统,没有登录的话阻止直接访问和使用系统

一、登录功能

1). 准备实体类 LoginInfo, 封装登录成功后, 返回给前端的数据 。

/**
 * 封装登录结果
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id; //ID
    private String username; //用户名
    private String name; //姓名
    private String token; //令牌
}

2). 定义LoginController

@Slf4j
@RestController
public class LoginController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody Employee employee){
        log.info("员工进行登录了..... ");
        LoginInfo loginInfo = employeeService.login(employee);
        if(loginInfo != null){
            return Result.success(loginInfo);
        }
        return Result.error("用户名或密码错误!");
    }

    /**
     * 空请求[不用管, 也不要动]
     */
    @GetMapping("/index")
    public Result index(){
        return Result.success();
    }

}

3)EmployeeService接口中增加 login 登录方法

public interface EmployeeService {

    /**
     * 登录
     */
    LoginInfo login(Employee employee);

4)EmployeeServiceImpl接口实现类中增加 login 登录方法

@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public LoginInfo login(Employee employee) {
        //1. 调用mapper方法查询数据库
        Employee e = employeeMapper.selectByUsernameAndPass(employee);

        //2. 判断
        if (e != null) { //登录成功 - 生成JWT令牌
            Map<String,Object> claims = new HashMap<>();
            claims.put("id", e.getId());
            claims.put("username", e.getUsername());
            claims.put("name", e.getName());

            String jwt = JwtUtils.generateJwt(claims);
            return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);
        }
        return null;
    }
 }   

5)EmployeeMapper增加接口方法

@Mapper
public interface EmployeeMapper {

    /**
     * 根据用户名查询用户
     */
    @Select("select * from tb_employee where username = #{username} and password = #{password} and status = 1")
    Employee selectByUsernameAndPass(Employee employee);
}    

效果如下:

1、tlais系统Springboot入门项目

登录校验功能(会话技术)

什么是登录校验?

所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。

主流用JWT令牌技术进行会话跟踪

在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:

  1. 在登录成功之后,要生成令牌。
  2. 每一次请求当中,要接收令牌并对令牌进行校验。

首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

1)引入JWT工具类:在项目工程下创建 com.itheima.util 包,并把提供JWT工具类复制到该包下

public class JwtUtils {

    private static String signKey = "SVRIRUlNQQ==";
    private static Long expire = 43200000L;

    /**
     * 生成JWT令牌
     * @return
     */
    public static String generateJwt(Map<String,Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

2)完善EmployeeServiceImpl接口实现类中 login 登录方法的逻辑,生成令牌并返回

@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public LoginInfo login(Employee employee) {
        //1. 调用mapper方法查询数据库
        Employee e = employeeMapper.selectByUsernameAndPass(employee);

        //2. 判断
        if (e != null) { //登录成功 - 生成JWT令牌
            Map<String,Object> claims = new HashMap<>();
            claims.put("id", e.getId());
            claims.put("username", e.getUsername());
            claims.put("name", e.getName());

            String jwt = JwtUtils.generateJwt(claims);
            return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);
        }
        return null;
    }

登录请求完成后,可以看到JWT令牌已经响应给了前端,此时前端就会将JWT令牌存储在浏览器本地。

1、tlais系统Springboot入门项目

登录校验过滤器功能的实现(统一拦截技术)

在后续的请求当中,都会在请求头中携带JWT令牌到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。那怎么样来统一拦截到所有的请求校验令牌的有效性呢?我们有两种解决方案:

  1. Filter过滤器
  2. Interceptor拦截器

这里先用Filter过滤器,还是先实现效果

1). 定义过滤器-配置过滤器

@Slf4j
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

        //2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){ //登录请求
            log.info("登录请求 , 直接放行");
            chain.doFilter(request, response);
            return;
        }

        //3. 获取请求头中的令牌(token)。
        String jwt = request.getHeader("token");

        //4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果401");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        //6. 放行。
        log.info("令牌合法, 放行");
        chain.doFilter(request , response);
    }
}
/**
 * 启动类
 */
@SpringBootApplication
@ServletComponentScan  //使用过滤器,别忘了开启对Servlet组件的支持
public class ShopAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopAdminApplication.class, args);
    }
}

最终要实现的效果:如果员工没有登录的情况下,拿着功能地址访问系统,自动跳转到登录页面。

1、tlais系统Springboot入门项目

1、tlais系统Springboot入门项目


功能六、日志管理技术

功能七、员工性别和员工职位统计

                                                                                              员工性别统计

需求

1、tlais系统Springboot入门项目

对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据就是一个json格式的数据。

接口描述

参照资料中提供的接口文档,查看 数据统计  -> 员工性别统计 接口的描述。

代码实现

1). 在ReportController,添加方法。

/**
 * 统计员工性别信息
 */
@GetMapping("/empGenderData")
public Result getEmpGenderData(){
    log.info("统计员工性别信息");
    List<Map> genderList = reportService.getEmpGenderData();
    return Result.success(genderList);
}
2). 在ReportService接口,添加接口方法。
/**
 * 统计员工性别信息
 */
List<Map> getEmpGenderData();
3). 在ReportServiceImpl实现类,实现方法
@Override
public List<Map> getEmpGenderData() {
    return empMapper.countEmpGenderData();
}
4). 定义EmpMapper 接口
统计的是员工的信息,所以需要操作的是员工表。 所以代码我们就写在 EmpMapper 接口中即可。
/**
 * 统计员工性别信息
 */
@MapKey("name")
List<Map> countEmpGenderData();
5). 定义EmpMapper.xml
<!-- 统计员工的性别信息 -->
<select id="countEmpGenderData" resultType="java.util.Map">
    select
    if(gender = 1, '男', '女') as name,
    count(*) as value
    from emp group by gender ;
</select>

if函数语法:if(条件, 条件为true取值, 条件为false取值)
ifnull函数语法:ifnull(expr, val1)    如果expr不为null,取自身,否则取val1

Apifox测试

1、tlais系统Springboot入门项目

联调测试

1、tlais系统Springboot入门项目

员工职位统计


1、tlais系统Springboot入门项目

对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据其实就是X轴展示的信息,和对应的数据。

1、tlais系统Springboot入门项目

参照资料中提供的接口文档,查看 数据统计  -> 员工职位统计 接口的描述。

代码实现:

1)定义封装结果对象JobOption

在 com.itheima.pojo 包中定义实体类 JobOption

package com.itheima.pojo;

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

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
    private List jobList;
    private List dataList;
}

2). 定义ReportController,并添加方法。

@Slf4j
@RequestMapping("/report")
@RestController
public class ReportController {

    @Autowired
    private ReportService reportService;

    /**
     * 统计各个职位的员工人数
     */
    @GetMapping("/empJobData")
    public Result getEmpJobData(){
        log.info("统计各个职位的员工人数");
        JobOption jobOption = reportService.getEmpJobData();
        return Result.success(jobOption);
    }
}

3). 定义ReportService接口,并添加接口方法。

public interface ReportService {
    /**
     * 统计各个职位的员工人数
     * @return
     */
    JobOption getEmpJobData();
}

4). 定义ReportServiceImpl实现类,并实现方法

@Service
public class ReportServiceImpl implements ReportService {

    @Autowired
    private EmpMapper empMapper;

    @Override
    public JobOption getEmpJobData() {
        List<Map<String,Object>> list = empMapper.countEmpJobData();
        List<Object> jobList = list.stream().map(dataMap -> dataMap.get("pos")).toList();
        List<Object> dataList = list.stream().map(dataMap -> dataMap.get("total")).toList();
        return new JobOption(jobList, dataList);
    }
}

5). 定义EmpMapper 接口

/**
 * 统计各个职位的员工人数
 */
@MapKey("pos")
List<Map<String,Object>> countEmpJobData();
//如果查询的记录往Map中封装,可以通过@MapKey注解指定返回的map中的唯一标识是那个字段。【也可以不指定】

6). 定义EmpMapper.xml

<!-- 统计各个职位的员工人数 -->
<select id="countEmpJobData" resultType="java.util.Map">
    select
    (case job when 1 then '班主任' 
                     when 2 then '讲师' 
                     when 3 then '学工主管' 
                     when 4 then '教研主管' 
                     when 5 then '咨询师' 
                     else '其他' end)  pos,
    count(*) total
    from emp group by job
    order by total
</select>
case流程控制函数:
- 语法一:case when cond1 then res1 [ when cond2 then res2 ] else res end ;
- 含义:如果 cond1 成立, 取 res1。  如果 cond2 成立,取 res2。 如果前面的条件都不成立,则取 res。

- 语法二(仅适用于等值匹配):case expr when val1 then res1 [ when val2 then res2 ] else res end ;
- 含义:如果 expr 的值为 val1 , 取 res1。  如果  expr 的值为 val2 ,取 res2。 如果前面的条件都不成立,则取 res。

重新启动服务,打开Apifox进行测试。

1、tlais系统Springboot入门项目

联调测试

1、tlais系统Springboot入门项目