day15-1 AI完成 班级管理与学员管理-总结

实战总结:班级管理、学员管理、学员信息统计

本文是对《11-后端Web实战(自己完成)》这个实战任务的总结,记录从需求分析、接口设计到代码实现的全过程。
项目地址:project/tlias-web-management(SpringBoot + MyBatis + PageHelper + Lombok)


一、任务概述

上一个实战任务已经完成了员工管理的增删改查和员工信息统计。本任务要求在此基础上,为培训机构系统补齐剩余模块:

  1. 班级管理:班级列表(条件分页)、添加班级、查询回显、修改班级、删除班级
  2. 学员管理:学员列表(条件分页)、添加学员、查询回显、修改学员、删除学员、违纪处理
  3. 学员信息统计:班级人数统计、学员学历统计
  4. 功能完善:删除部门时,若部门下存在员工则禁止删除

总共新增/修改了 16 个接口点,全部严格对照接口文档开发。


二、总体构思

1. 开发前的三个准备

步骤 内容 目的
① 读接口文档 讲义/接口文档.md 明确每个接口的路径、请求方式、参数格式、响应结构,这是前后端联调的契约
② 分析已有项目结构 查看 controller / service / mapper / pojo 各层 复用已有的代码风格、工具类和组件,不引入讲义外的语法
③ 分析数据库表 clazzstudent 表结构 字段名、类型、唯一约束(后续动态 SQL 和统计函数都要用到)

2. 严格遵守已有分层架构

1
2
3
4
5
6
7
Controller (接收请求/响应 Result)

Service 接口 + ServiceImpl 实现 (业务逻辑、事务)

Mapper 接口 + Mapper.xml (SQL)

MySQL (tlias 库)

复用的现成组件:Result(统一响应)、PageResult<T>(分页结果)、PageHelper(分页插件)、GlobalExceptionHandler(全局异常处理器)、@Slf4j 日志。

3. 按模块逐个开发,每个模块都是”老套路”

每个模块的开发顺序统一为:分页/列表查询 → 新增 → 根据ID查询 → 修改 → 删除,与之前员工管理的开发顺序完全一致,只是业务数据不同。


三、实体类与自定义异常

1. Clazz(班级)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Clazz {
private Integer id;
private String name; // 班级名称
private String room; // 班级教室
private LocalDate beginDate;// 开课时间
private LocalDate endDate; // 结课时间
private Integer masterId; // 班主任ID(员工ID)
private Integer subject; // 学科 1:java 2:前端 3:大数据...
private LocalDateTime createTime;
private LocalDateTime updateTime;

private String masterName; // 班主任姓名(联查员工表)
private String status; // 班级状态(未开班/已开班/已结课) - 计算字段
}

2. Student(学员)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private Integer id;
private String name;
private String no; // 学号
private Integer gender; // 1:男 2:女
private String phone;
private String idCard;
private Integer isCollege; // 是否院校学生 1:是 0:否
private String address;
private Integer degree; // 学历 1:初中 2:高中 3:大专 4:本科...
private LocalDate graduationDate;
private Integer clazzId;
private Short violationCount; // 违纪次数(数据库 tinyint)
private Short violationScore; // 违纪扣分
private LocalDateTime createTime;
private LocalDateTime updateTime;

private String clazzName; // 班级名称(联查班级表)
}

说明:masterNamestatusclazzName 这三个字段在数据库表中不存在,是联查或计算出来的扩展字段,用于响应前端。

3. StudentCountData(班级人数统计结果)

仿照已有的 EmpJobData(职位统计用 jobList + dataList),班级人数统计也封装成一个对象:

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentCountData {
private List<String> clazzList; // 班级列表(饼图/柱状图 X 轴)
private List<Long> dataList; // 每个班级的人数(Y 轴)
}

4. BusinessException(自定义业务异常)

1
2
3
4
5
6
7
8
package com.zhang.exception;

// 自定义业务异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}

四、班级管理模块(/clazzs)

1. 条件分页查询 GET /clazzs

接口要点

  • 参数:name(模糊)、begin/end(匹配结课时间 end_date)、pagepageSize
  • 响应每行包含 masterName(班主任姓名)和 status(班级状态)

实现思路status 不是一个数据库字段,需要根据当前时间计算:

1
2
3
当前时间 > 结课时间  → 已结课
当前时间 < 开课时间 → 未开班
否则 → 已开班

Mapper.xml(联查员工表拿班主任姓名 + 动态条件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="list" resultType="com.zhang.pojo.Clazz">
select c.id, c.name, c.room, c.begin_date, c.end_date, c.master_id,
c.subject, c.create_time, c.update_time, e.name masterName
from clazz c
left join emp e on c.master_id = e.id
<where>
<if test="name != null and name != ''">
c.name like concat('%', #{name}, '%')
</if>
<if test="begin != null and end != null">
and c.end_date between #{begin} and #{end}
</if>
</where>
order by c.update_time desc
</select>

Service(分页 + 在 Java 里算状态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public PageResult page(Integer page, Integer pageSize, String name, LocalDate begin, LocalDate end) {
PageHelper.startPage(page, pageSize);
Page<Clazz> p = (Page<Clazz>) clazzMapper.list(name, begin, end);

// 根据开课时间、结课时间计算班级状态
LocalDate now = LocalDate.now();
p.getResult().forEach(clazz -> {
if (now.isAfter(clazz.getEndDate())) {
clazz.setStatus("已结课");
} else if (now.isBefore(clazz.getBeginDate())) {
clazz.setStatus("未开班");
} else {
clazz.setStatus("已开班");
}
});
return new PageResult(p.getTotal(), p.getResult());
}

为什么不直接在 SQL 里用 CASE WHEN 算状态?两种都可以,选 Java 计算是因为:状态计算逻辑简单清晰、可读性好,且分页结果已经拿到内存里,遍历赋值即可,无需改动 SQL。

2. 添加班级 POST /clazzs

Controller 用 @RequestBody Clazz 接收 JSON,Service 里补 createTimeupdateTime 后插入:

1
2
3
4
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into clazz(name, room, begin_date, end_date, master_id, subject, create_time, update_time)
values (#{name}, #{room}, #{beginDate}, #{endDate}, #{masterId}, #{subject}, #{createTime}, #{updateTime})
</insert>

3. 根据ID查询 GET /clazzs/{id}、修改 PUT /clazzs

  • 查询:select ... from clazz where id = #{id},用 resultType 直接映射(开启了 map-underscore-to-camel-case,下划线字段自动转驼峰)。
  • 修改:动态 SQL <set> + <if>,只更新非空字段,update_time 在 Service 里刷新。

4. 删除班级 DELETE /clazzs/{id} —— 删除校验

需求:班级下关联了学员则不允许删除,提示 “对不起, 该班级下有学生, 不能直接删除”。

实现思路(讲义提示:自定义异常 + 全局异常处理器):

1
2
3
4
5
6
7
8
9
@Override
public void deleteById(Integer id) {
// 判断该班级下是否关联的有学员, 如果有关联, 则不允许删除
Integer count = studentMapper.countByClazzId(id);
if (count != null && count > 0) {
throw new BusinessException("对不起, 该班级下有学生, 不能直接删除");
}
clazzMapper.deleteById(id);
}

StudentMapper 里新增统计方法:

1
2
3
<select id="countByClazzId" resultType="java.lang.Integer">
select count(*) from student where clazz_id = #{clazzId}
</select>

5. 查询所有班级 GET /clazzs/list

给”新增学员”页面下拉框用,直接 select ... from clazz,无分页、无联查,返回 List<Clazz>


五、学员管理模块(/students)

1. 条件分页查询 GET /students

  • 参数:name(模糊)、degree(学历)、clazzId(班级)、pagepageSize
  • 响应每行包含 clazzName(班级名称,联查班级表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select id="list" resultType="com.zhang.pojo.Student">
select s.id, s.name, s.no, s.gender, s.phone, s.id_card, s.is_college,
s.address, s.degree, s.graduation_date, s.clazz_id,
s.violation_count, s.violation_score, s.create_time, s.update_time,
c.name clazzName
from student s
left join clazz c on s.clazz_id = c.id
<where>
<if test="name != null and name != ''">
s.name like concat('%', #{name}, '%')
</if>
<if test="degree != null">
and s.degree = #{degree}
</if>
<if test="clazzId != null">
and s.clazz_id = #{clazzId}
</if>
</where>
order by s.update_time desc
</select>

2. 新增 / 根据ID查询 / 修改

  • 新增:violation_countviolation_score 数据库有默认值 0,不需要插入
  • 修改:动态 <set> 更新非空字段,clazzIddegree 等数字字段用 <if test="xxx != null"> 判断。
  • 注意:nophoneid_card 在表上有唯一约束,重复插入/修改会抛 DuplicateKeyException,已被已有的全局异常处理器捕获并返回 “xxx已存在”。

3. 批量删除 DELETE /students/{ids} —— 路径参数接收数组

与员工删除(查询参数)的区别:学员删除的 id 是放在路径里的,格式 /students/1,2,3

1
2
3
4
5
6
@DeleteMapping("/{ids}")
public Result deleteByIds(@PathVariable Integer[] ids) {
log.info("批量删除学员: ids:{}", Arrays.asList(ids));
studentService.deleteByIds(ids);
return Result.success();
}

知识点:Spring 底层会把逗号分隔的路径参数 "1,2,3" 通过转换服务自动拆分成 Integer[],和查询参数 ids=1,2,3 接收成数组是同一个机制。

Mapper 里用 <foreach>id in (...)

1
2
3
4
5
6
7
<delete id="deleteByIds">
delete from student where id in(
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</delete>

4. 违纪处理 PUT /students/violation/{id}/{score}

需求:违纪处理一次,违纪次数 +1,违纪扣分 + 前端传入的分数。

直接在 SQL 里基于原值自增,一条 update 搞定:

1
void violation(@Param("id") Integer id, @Param("score") Integer score);
1
2
3
4
5
6
7
<update id="violation">
update student
set violation_count = violation_count + 1,
violation_score = violation_score + #{score},
update_time = now()
where id = #{id}
</update>

注意:因为方法有多个参数(id、score),Mapper 接口上必须用 @Param 指定参数名,否则 MyBatis 无法识别 #{id}#{score}


六、学员信息统计(/report)

1. 班级人数统计 GET /report/studentCountData

需求:统计每个班级的人数,返回 { clazzList: [班级名...], dataList: [人数...] }(格式同员工职位统计)。

Mapper(联查班级表取班级名,按班级分组):

1
2
3
4
5
6
<select id="countStudentClazz" resultType="java.util.Map">
select c.name name, count(*) value
from student s
left join clazz c on s.clazz_id = c.id
group by s.clazz_id, c.name
</select>

Service(用 Stream 把 List<Map> 拆成两个 List 封装进 StudentCountData):

1
2
3
4
5
6
7
@Override
public StudentCountData getStudentCountData() {
List<Map<String, Object>> list = studentMapper.countStudentClazz();
List<String> clazzList = list.stream().map(itemMap -> itemMap.get("name").toString()).toList();
List<Long> dataList = list.stream().map(itemMap -> (Long) itemMap.get("value")).toList();
return new StudentCountData(clazzList, dataList);
}

注意强转:count(*) 在 MyBatis 里返回的是 Long,所以取出来要 (Long) 强转;名字取出来是 Object,用 .toString()

2. 学员学历统计 GET /report/studentDegreeData

需求:按学历分组统计人数,返回 [{name: '初中', value: 5}, ...](格式同员工性别统计)。

CASE WHEN 把 degree 数字映射成中文学历(这是之前员工职位统计用过的语法):

1
2
3
4
5
6
7
8
9
10
11
12
<select id="countStudentDegree" resultType="java.util.Map">
select case degree when 1 then '初中'
when 2 then '高中'
when 3 then '大专'
when 4 then '本科'
when 5 then '硕士'
when 6 then '博士'
else '其他' end name,
count(*) value
from student
group by degree
</select>

七、功能完善:删除部门校验

需求:删除部门时,如果部门下有员工,则不允许删除,提示 “对不起,当前部门下有员工,不能直接删除!”

在已有的 DeptServiceImpl.deleteById 中加一层判断(需要注入 EmpMapper):

1
2
3
4
5
6
7
8
9
@Override
public void deleteById(Integer id) {
// 判断该部门下是否关联的有员工, 如果有关联, 则不允许删除
Integer count = empMapper.countByDeptId(id);
if (count != null && count > 0) {
throw new BusinessException("对不起,当前部门下有员工,不能直接删除!");
}
deptMapper.deleteById(id);
}

EmpMapper.xml 新增:

1
2
3
<select id="countByDeptId" resultType="java.lang.Integer">
select count(*) from emp where dept_id = #{deptId}
</select>

八、关键技术点总结

1. 自定义异常 + 全局异常处理器的完整链路

1
2
3
4
Service 抛 BusinessException("对不起, 该班级下有学生, 不能直接删除")
→ 被 @RestControllerAdvice 标记的 GlobalExceptionHandler 捕获
→ @ExceptionHandler 方法返回 Result.error(异常信息)
→ 前端拿到统一格式的 Result,正常解析并弹窗提示

在已有的 GlobalExceptionHandler 中新增一个处理方法(Spring 会自动选择最匹配的异常处理器):

1
2
3
4
5
6
//处理自定义业务异常
@ExceptionHandler
public Result handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return Result.error(e.getMessage());
}

为什么比 try…catch 好?业务代码里不用到处写 try…catch,异常统一收敛到一处,代码干净优雅。

2. 分页:PageHelper 插件

1
2
3
PageHelper.startPage(page, pageSize);        // ① 开启分页(下一行查询自动拼接 limit)
Page<Clazz> p = (Page<Clazz>) clazzMapper.list(...); // ② 查询结果强转为 Page
return new PageResult(p.getTotal(), p.getResult()); // ③ 取出总数和当前页数据

3. resultType vs resultMap

  • 字段能直接对应(下划线自动转驼峰)→ 用 resultType(本任务大部分查询都够用)。
  • 字段对应不上 / 需要手动嵌套封装(如员工带工作经历)→ 用 resultMap
  • 本任务班级列表、学员列表都是简单联查取别名,用 resultType + 列别名即可,不需要 resultMap。

4. 动态 SQL 的四个标签

标签 用途 本任务应用
<where> 自动处理首个条件的 and 班级/学员分页查询
<set> 动态更新,自动去掉末尾逗号 修改班级/学员
<if> 条件判断(字符串判空、数字判 null) 所有动态 SQL
<foreach> 拼接 in 集合 批量删除学员

5. 统计函数的两种写法

函数 语法 用途
CASE WHEN case expr when val1 then res1 else res end 学历数字 → 中文映射
IF() if(条件, 真值, 假值) 员工性别 1/2 → 男/女

6. 两个细节坑

  • 多参数 Mapper 方法必须加 @Param:如违纪处理 violation(@Param("id") ..., @Param("score") ...),否则报 Parameter 'id' not found
  • count(*) 返回类型是 Long:从 Map 中取值做算术或封装时要注意强转。

九、接口清单总览

# 模块 路径 方式 说明
1 班级管理 /clazzs GET 班级列表(条件分页,含 masterName/status)
2 班级管理 /clazzs POST 添加班级
3 班级管理 /clazzs/{id} GET 根据ID查询班级
4 班级管理 /clazzs PUT 修改班级
5 班级管理 /clazzs/{id} DELETE 删除班级(班级下有学生禁止删)
6 班级管理 /clazzs/list GET 查询所有班级(学员下拉框)
7 员工管理 /emps/list GET 查询全部员工(班主任下拉框)
8 学员管理 /students GET 学员列表(条件分页,含 clazzName)
9 学员管理 /students POST 添加学员
10 学员管理 /students/{id} GET 根据ID查询学员
11 学员管理 /students PUT 修改学员
12 学员管理 /students/{ids} DELETE 批量删除学员(路径逗号数组)
13 学员管理 /students/violation/{id}/{score} PUT 违纪处理
14 数据统计 /report/studentCountData GET 班级人数统计
15 数据统计 /report/studentDegreeData GET 学员学历统计
16 部门管理 /depts DELETE 删除部门(部门下有员工禁止删)