Web后端实战(4)

Tlias智能学习辅助系统

删除员工

需求

当我们勾选列表前面的复选框,然后点击 “批量删除” 按钮,就可以将这一批次的员工信息删除掉了。也可以只勾选一个复选框,仅删除一个员工信息。

问题:我们需要开发两个功能接口吗?一个删除单个员工,一个删除多个员工

答案:不需要。 只需要开发一个功能接口即可(删除多个员工包含只删除一个员工)

接口文档

地址:接口文档 - 飞书云文档

查看 员工管理 -> 删除员工 接口的描述。

sql语句:

1
2
3
4
-- 删除员工
delete from emp where id in (1,2,3);

delete from emp_expr where emp_id in (1,2,3);

思路分析

功能开发

Controller接收参数

EmpController 中增加如下方法 delete ,来执行批量删除员工的操作。

方式一:在Controller方法中通过数组来接收

多个参数,默认可以将其封装到一个数组中,需要保证前端传递的参数名 与 方法形参名称保持一致。

1
2
3
4
5
6
7
8
  /**
     * 删除员工 - 数组
     */
    @DeleteMapping
    public Result delete(Integer[] ids){
        log.info("删除员工: ids={} ", Arrays.asList(ids));
        return Result.success();
    }

方式二:在Controller方法中通过集合来接收

也可以将其封装到一个List 集合中,如果要将其封装到一个集合中,需要在集合前面加上 @RequestParam 注解。

1
2
3
4
5
6
7
8
9
 /**
     * 删除员工 - List
     */
    @DeleteMapping
    public Result delete(@RequestParam List<Integer> ids){
        log.info("批量删除部门: ids={} ", ids);
        empService.delete(ids);
        return Result.success();
    }

结果同方式一

两种方式,选择其中一种就可以,我们一般推荐选择集合,因为基于集合操作其中的元素会更加方便。

Service

1). 在接口中 EmpService 中定义接口方法 deleteByIds

1
2
3
4
/**
* 批量删除员工
*/
void deleteByIds(List<Integer> ids);

2). 在实现类 EmpServiceImpl 中实现接口方法 deleteByIds

在删除员工信息时,既需要删除 emp 表中的员工基本信息,还需要删除 emp_expr 表中员工的工作经历信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//只有出现异常时才需要回滚
@Transactional(rollbackFor = {Exception.class})
@Override
public void deleteByIds(List<Integer> ids) {
    //1. 根据ID批量删除员工基本信息
    empMapper.deleteByIds(ids);

    //2. 根据员工的ID批量删除员工的工作经历信息
    empExprMapper.deleteByEmpIds(ids);
}

由于删除员工信息,既要删除员工基本信息,又要删除工作经历信息,操作多次数据库的删除,所以需要进行事务控制。

Mapper

1). 在 EmpMapper 接口中增加 deleteByIds 方法实现批量删除员工基本信息

1
2
3
4
/**
* 批量删除员工信息
*/
void deleteByIds(List<Integer> ids);

2). 在 EmpMapper.xml 配置文件中, 配置对应的SQL语句

1
2
3
4
5
6
7
<!--批量删除员工信息-->
<delete id="deleteByIds">
        delete from emp where id in 
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

3). 在 EmpExprMapper 接口中增加 deleteByEmpIds 方法实现根据员工ID批量删除员工的工作经历信息

1
2
3
4
/**
* 根据员工的ID批量删除工作经历信息
*/
void deleteByEmpIds(List<Integer> empIds);

4). 在 EmpExprMapper.xml 配置文件中, 配置对应的SQL语句

1
2
3
4
5
6
7
<!--根据员工的ID批量删除工作经历信息-->
<delete id="deleteByEmpIds">
    delete from emp_expr where emp_id in
    <foreach collection="empIds" item="empId" open="(" close=")" separator=",">
            #{empId}
    </foreach>
</delete>

功能测试

功能开发完成后,重启项目工程,打开 Apifox,发起DELETE请求:

除

控制台SQL语句:

数据库删除前:

删除后:

前后端联调

打开浏览器,测试后端功能接口:

删除单条数据

删除多条数据:

修改员工

查询回显

接口文档

地址:接口文档 - 飞书云文档

查看 员工管理 -> 根据ID查询 接口的描述。

思路分析

在查询回显时,既需要查询出员工的基本信息,又需要查询出该员工的工作经历信息。

我们可以先通过一条SQL语句(根据id进行查询员工信息),查询出指定员工的基本信息,及其员工的工作经历信息。SQL如下:

1
2
3
4
5
6
7
select e.*,
       ee.id ee_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 e.id = ee.emp_id where e.id = 39;

具体的实现思路如下:

代码实现

1). EmpController 添加 getInfo 用来根据ID查询员工数据,用于页面回显

1
2
3
4
5
6
7
8
9
/**
 * 查询回显
 */
@GetMapping("/{id}")
public Result getInfo(@PathVariable Integer id){
    log.info("根据id查询员工的详细信息");
    Emp emp  = empService.getInfo(id);
    return Result.success(emp);
}

2). EmpService 接口中增加 getInfo 方法

1
2
3
4
/**
 * 根据ID查询员工的详细信息
 */
Emp getInfo(Integer id);

3). EmpServiceImpl 实现类中实现 getInfo 方法

1
2
3
4
@Override
public Emp getInfo(Integer id) {
    return empMapper.getById(id);
}

4). EmpMapper 接口中增加 getById 方法

1
2
3
4
/**
 * 根据ID查询员工详细信息
 */
Emp getById(Integer id);

5). EmpMapper.xml 配置文件中定义对应的SQL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!--自定义结果集ResultMap-->
<!--通过resultMap进行手动结果封装-->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
    <!-- id用于封装id -->
    <id column="id" property="id" />
    <!-- result用于封装普通属性 -->
    <!-- property用于往这个字段里面进行封装 -->
    <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" />

    <!--封装exprList-->
    <!-- collection用于封装集合 -->
    <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>

<!--根据ID查询员工的详细信息-->
<select id="getById" resultMap="empResultMap">
    select e.*,
        ee.id ee_id,
        ee.emp_id ee_empid,
        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 e.id = ee.emp_id
    where e.id = #{id}
</select>

在这一步中我们先按之前的操作步骤只将sql语句复制粘贴进EmpMapper.xml 配置文件中,此时打开apifox就会发现报了500的错误,如果要解决这种步骤就需要按照上述步骤进行手动结果封装

我们需要将结果封装到一个Emp对象当中,但是这条sql语句执行完后发现了两条数据,两条数据是没有办法往一个对象当中封装的,所以此时就报错了,但是根据id查询员工信息只能返回一个员工对象(id是主键,唯一的),之所以返回两条数据是因为这个员工有两段工作经历,最终结果应该封装到一个Emp员工对象当中,但是我们使用Mybatis将结果自动封装,那么两条数据就会封装到两个对象当中,如果返回的结果是单个对象,那么就会报错

在这种一对多的查询中,我们要想成功的封装的结果,需要手动的基于 <resultMap> 来进行封装结果。

Mybatis中封装查询结果,什么时候用 resultType,什么时候用resultMap ?

  • 如果查询返回的字段名与实体的属性名可以直接对应上,用resultType 自动封装。
  • 如果查询返回的字段名与实体的属性名对应不上,或实体属性比较复杂,可以通过resultMap手动封装 。

功能测试

重新启动服务,基于Apifox进行接口测试。

前后端联调测试

打开浏览器,进行前后端联调测试。

选择与我们刚刚查询id相同的员工信息,点击编辑,发现员工的信息都展示出来了

修改员工

查询回显之后,就可以在页面上修改员工的信息了。

  • 当用户修改完数据之后,点击保存按钮,就需要将数据提交到服务端,然后服务端需要将修改后的数据更新到数据库中 。
  • 而此次更新的时候,既需要更新员工的基本信息; 又需要更新员工的工作经历信息 。

接口文档

地址:接口文档 - 飞书云文档

查看 员工管理 -> 修改员工 接口的描述。

思路分析

在修改员工经历的操作时,有三种情况:增加员工信息,删除员工信息,修改员工信息,对于修改员工信息,我们可以先删再加

代码实现

1). EmpController 增加 update 方法接收请求参数,响应数据

1
2
3
4
5
6
7
8
9
/**
* 更新员工信息
*/
@PutMapping
public Result update(@RequestBody Emp emp){
    log.info("修改员工信息, {}", emp);
    empService.update(emp);
    return Result.success();
}

2). EmpService 接口增加 update 方法

1
2
3
4
5
/**
* 更新员工信息
* @param emp
*/
void update(Emp emp);

3). EmpServiceImpl实现类实现 update 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Transactional(rollbackFor = {Exception.class})//只有一个元素可以省略{}
@Override
public void update(Emp emp) {
    //1. 根据ID更新员工基本信息
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.updateById(emp);

    //2. 根据员工ID删除员工的工作经历信息 【先删除原有的】
    //Arrays.asList:快速将一个数组(或一组元素)转换成一个 List 集合。
    empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));

    //3. 新增员工的工作经历数据 【再新增新的】
    Integer empId = emp.getId();
    List<EmpExpr> exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -> empExpr.setEmpId(empId));
        empExprMapper.insertBatch(exprList);
    }
}

4). EmpMapper 接口中增加 updateById 方法

1
2
3
4
/**
* 更新员工基本信息
*/
void updateById(Emp emp);

5). EmpMapper.xml 配置文件中定义对应的SQL语句,基于动态SQL更新员工信息

程序优化后:基于动态SQL优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!--根据ID更新员工信息-->
<update id="updateById">
    update emp
    <!--set标签:会自动生成set关键字;会自动的删除更新后多余的","-->
    <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="job != null">job = #{job},</if>
        <if test="salary != null">salary = #{salary},</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>

初版的配置文件如下:【不灵活、扩展性差】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<update id="updateById">
        UPDATE emp
	    SET username = '#{username}',
        	password = '#{password}',
        	name = '#{name}',
        	gender = #{gender},
        	phone = '#{phone}',
        	job = #{job},
        	salary = #{salary}
        	image = #{image},
        	entry_date = #{entryDate},
        	dept_id = #{deptId},	
        	update_time = #{updateTime}
        where id = #{id}
    </update>

功能测试

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

前后端联调测试

修改前:

修改后再次点击编辑进行查看信息:

异常处理

问题分析

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

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

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

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

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

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

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

当我们没有做任何的异常处理时,我们三层架构处理异常的方案:

  • Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。
  • service 中也存在异常了,会抛给controller。
  • 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给前端框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。

解决方案

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

  • 方案一:在所有Controller的所有方法中进行try…catch处理

缺点:代码臃肿(不推荐)

  • 方案二:全局异常处理器

好处:简单、优雅(推荐)

全局异常处理器

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

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*
* 全局异常处理器
* */
//记录日志
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    //处理异常
    @ExceptionHandler
    public Result ex(Exception e){//方法形参中指定能够处理的异常类型
        //使用日志
        log.error("程序出错啦~",e);//注意:这里的error方法要使用第二个带两个参数的才可以抛出异常
        //e.printStackTrace();//打印堆栈中的异常信息
        //捕获到异常之后,响应一个标准的Result
        return Result.error("对不起,操作失败,请联系管理员");
    }
    
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

重新启动SpringBoot服务,打开浏览器,再来测试一下 修改员工 这个操作,我们依然设置已存在的 13390901232这个手机号:

此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。

以上就是全局异常处理器的使用,主要涉及到两个注解:

  • @RestControllerAdvice //表示当前类为全局异常处理器
  • @ExceptionHandler //指定可以捕获哪种类型的异常进行处理

那么我们可不可以让这个异常捕获的更精准一点呢?具体是哪里出现异常了? 答案是可以的

打开控制台我们发现了下面这个异常,可以对其进行捕获,然后获取这个异常的堆栈信息,之后可以捕获到这个异常的手机号,再对其进行提示手机号重复了

按照继承关系从下往上匹配

根据这个异常进行编写代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@ExceptionHandler
//如果是DuplicateKeyException,那么这个处理器就会生效,如果是别的上面的Exception就会生效
public Result handleDuplicateKeyException(DuplicateKeyException e){
    log.error("程序出错啦~",e);
    // 获取异常的具体描述信息,通常包含 "Duplicate entry 'xxx' for key 'xxx'"
    String message = e.getMessage();
    // 定位 "Duplicate entry" 关键字的位置,作为截取错误信息的起点
    int i = message.indexOf("Duplicate entry");
    // 从关键字位置开始截取字符串,获取包含具体重复值的片段
    String errMsg = message.substring(i);
    // 按照空格将截取的字符串分割成数组
    String[] arr = errMsg.split(" ");
    // 数组下标为2的元素通常是具体的重复值(如 'admin'),拼接提示语后返回
    // 注意:这里假设了异常信息的格式是固定的,且重复值在数组的第三个位置
    return Result.error(arr[2] + "已存在");
}

此时再来测试上面的手机号就会出现以下报错信息:

再来测试用户名重复:

员工信息统计

员工管理的增删改查功能我们已开发完成,接下来,我们再来完成员工信息统计的接口开发。 对于这些图形报表的开发,其实呢,都是基于现成的一些图形报表的组件开发的,比如:Echarts、HighCharts等。

而报表的制作,主要是前端人员开发,引入对应的组件(比如:ECharts)即可。 服务端开发人员仅为其提供数据即可。

官网:https://echarts.apache.org/zh/index.html

职位统计

需求

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

三层架构每层的职责:

接口描述

地址:接口文档 - 飞书云文档

查看 数据统计 -> 员工职位统计 接口的描述。

代码实现

1). 定义封装结果对象JobOption

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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,并添加方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@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);
    }
}

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

1
2
3
4
5
6
7
public interface ReportService {
    /**
     * 统计各个职位的员工人数
     * @return
     */
    JobOption getEmpJobData();
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@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);
    }
}

4). 定义EmpMapper 接口

统计的是员工的信息,所以需要操作的是员工表。 所以代码我们就写在 EmpMapper 接口中即可。

1
2
3
4
5
6
/**
 * 统计各个职位的员工人数
 */
@MapKey("pos")
//先将集合封装到Map中,再将Map封装到集合当中
List<Map<String,Object>> countEmpJobData();

如果查询的记录往Map中封装,可以通过@MapKey注解指定返回的map中的唯一标识是那个字段。【也可以不指定】

5). 定义EmpMapper.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- 统计各个职位的员工人数 -->
<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 表达式1 then res1 [ when 表达式2 then res2 ] else res end ;

  • 含义:如果 表达式1 成立, 取 res1。 如果 表达式2 成立,取 res2。 如果前面的条件都不成立,则取 res。

  • 语法二(仅适用于等值匹配):case expr when val1 then res1 [ when val2 then res2 ] else res end ;

  • 含义:如果 expr 的值为 val1 , 取 res1。 如果 expr 的值为 val2 ,取 res2。 如果前面的条件都不成立,则取 res。

功能测试

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

前后端联调测试

性别统计

需求

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

接口描述

地址:接口文档 - 飞书云文档

查看 数据统计 -> 员工性别统计 接口的描述。

代码实现

1). 在ReportController,添加方法。

1
2
3
4
5
6
7
8
9
/**
 * 统计员工性别信息
 */
@GetMapping("/empGenderData")
public Result getEmpGenderData(){
    log.info("统计员工性别信息");
    List<Map> genderList = reportService.getEmpGenderData();
    return Result.success(genderList);
}

2). 在ReportService接口,添加接口方法。

1
2
3
4
/**
 * 统计员工性别信息
 */
List<Map> getEmpGenderData();

3). 在ReportServiceImpl实现类,实现方法

1
2
3
4
@Override
public List<Map> getEmpGenderData() {
    return empMapper.countEmpGenderData();
}

4). 定义EmpMapper 接口

统计的是员工的信息,所以需要操作的是员工表。 所以代码我们就写在 EmpMapper 接口中即可。

1
2
3
4
5
/**
 * 统计员工性别信息
 */
@MapKey("name")
List<Map> countEmpGenderData();

5). 定义EmpMapper.xml

1
2
3
4
5
6
7
<!-- 统计员工的性别信息 -->
<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

功能测试

前后端联调测试

本站于2026年3月31日建立
使用 Hugo 构建
主题 StackJimmy 设计