独立实战:班级管理
Day13:按 Controller、Service、Mapper 分层实现班级查询、新增、修改与删除,并完成业务校验。
继续上次小节 →JavaWeb 线 · 线路目录
JavaWeb 线
课程介绍与前端基础4 站
Tlias 后端接口实战6 站
认证、AOP 与框架原理3 站
工程化、前端案例与部署6 站
本页导读
独立实战:班级管理
前面完成了部门管理和员工管理,接下来完成班级管理。每个功能按照“需求分析 → 思路分析 → 代码实现”讲解,代码按 Controller、Service、Mapper 分层增加到对应文件中。
代码使用你现有的 Clazz、Result、PageBean,包名为 com.ziggy。查询参数和课堂代码一样直接通过方法形参接收。删除班级时先判断是否有学员,由 Controller 使用已有的 Result.error 返回提示。
本篇是根据实战资料和课堂代码整理的参考讲解,尚未在你的数据库中联调;功能测试写的是完成代码后应执行的检查。
0. 开发准备
0.1 本日接口
| 功能 | 请求方式 | 请求路径 | 参数位置 |
|---|---|---|---|
| 条件分页查询 | GET | /clazzs | 查询参数 |
| 班主任下拉框 | GET | /emps/list | 无 |
| 新增班级 | POST | /clazzs | JSON 请求体 |
| 查询回显 | GET | /clazzs/{id} | 路径参数 |
| 修改班级 | PUT | /clazzs | JSON 请求体 |
| 删除班级 | DELETE | /clazzs/{id} | 路径参数 |
| 全部班级下拉框 | GET | /clazzs/list | 无 |
班级模块自身有 6 个接口,另外复用员工模块的 /emps/list。班主任下拉框查询员工;学员页面中的班级下拉框查询 /clazzs/list,两者不能混淆。
0.2 表结构与字段映射
| 数据库列 | Java 属性 | 含义 |
|---|---|---|
id | id | 班级 ID |
name | name | 班级名称,有唯一约束 |
room | room | 教室 |
begin_date | beginDate | 开课日期 |
end_date | endDate | 结课日期 |
master_id | masterId | 班主任,关联 emp.id |
subject | subject | 学科编号 |
create_time / update_time | createTime / updateTime | 创建、修改时间 |
masterName 是关联查询得到的姓名,status 是按日期计算的状态。这两个属性没有对应的班级表列,不参与新增和修改。删除校验还要使用 student.clazz_id。
保留已有 Clazz、Result 和 PageBean;Clazz 的日期属性用 LocalDate,时间属性用 LocalDateTime。配置文件必须启用驼峰映射:
mybatis:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
这只控制查询结果如何映射到 Java 属性,不会把你 SQL 中的 entrydate、masterId 自动改成数据库列名。SQL 左侧仍然要写真实列名。
0.3 三层结构
- Controller:
src/main/java/com/ziggy/controller/ClazzController.java - Service 接口:
src/main/java/com/ziggy/service/ClazzService.java - Service 实现:
src/main/java/com/ziggy/service/Impl/ClazzServiceImpl.java - Mapper 接口:
src/main/java/com/ziggy/mapper/ClazzMapper.java - Mapper XML:
src/main/resources/com/ziggy/mapper/ClazzMapper.xml
注意 resources 有复数 s;你项目的实现包是大写 Impl,本文与现有项目保持一致。类的基本骨架如下;后面每节方法分别放进对应类中。
// ClazzController.java
package com.ziggy.controller;
import com.ziggy.entity.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import com.ziggy.service.ClazzService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/clazzs")
public class ClazzController {
@Autowired
private ClazzService clazzService;
// 放入下面各节的 Controller 方法
}
// ClazzService.java:完整接口
package com.ziggy.service;
import com.ziggy.entity.*;
import java.time.LocalDate;
import java.util.List;
public interface ClazzService {
PageBean page(String name, LocalDate begin, LocalDate end, Integer page, Integer pageSize);
void save(Clazz clazz);
Clazz getInfo(Integer id);
void update(Clazz clazz);
boolean delete(Integer id);
List<Clazz> list();
}
// ClazzServiceImpl.java:把后面的实现方法合并到这里
package com.ziggy.service.Impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.ziggy.entity.*;
import com.ziggy.mapper.ClazzMapper;
import com.ziggy.service.ClazzService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class ClazzServiceImpl implements ClazzService {
@Autowired
private ClazzMapper clazzMapper;
// 放入下面各节的 Service 方法
}
// ClazzMapper.java:完整接口,注意是 interface
package com.ziggy.mapper;
import com.ziggy.entity.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
@Mapper
public interface ClazzMapper {
List<Clazz> page(@Param("name") String name,
@Param("begin") LocalDate begin,
@Param("end") LocalDate end);
void save(Clazz clazz);
Clazz getInfo(@Param("id") Integer id);
void update(Clazz clazz);
Long countStudents(@Param("id") Integer id);
void delete(@Param("id") Integer id);
List<Clazz> list();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ziggy.mapper.ClazzMapper">
<!-- 将各节 SQL 放到这里,同一 id 只保留一份 -->
</mapper>
1. 班级列表条件分页查询
对应接口文档:3.1 班级列表查询(3.1.1 基本信息、3.1.2 请求参数、3.1.3 响应数据)
1.1 需求分析
打开班级列表时,前端需要当前页的数据和总记录数,可以按班级名称、结课日期筛选。
| 项目 | 约定 |
|---|---|
| 请求 | GET /clazzs |
| 示例 | /clazzs?name=Java&begin=2026-01-01&end=2026-12-31&page=1&pageSize=5 |
| 返回 | Result.success(PageBean),data 中包含 total 和 rows |
begin、end 筛选的是结课日期,不是开课日期。
1.2 思路分析
Controller 通过方法形参接收查询参数 → Service 启动 PageHelper 分页 → Mapper 关联员工表获取班主任姓名并计算班级状态 → Service 组装分页结果。
使用 left join,因为班主任允许为空。班级状态定义为:今天早于开课日是“未开班”;今天晚于结课日是“已结课”;其余是“已开班”。开课当天和结课当天都算“已开班”,与接口文档文案保持一致。
1.3 代码实现
查询参数直接在 Controller 的方法形参中接收,日期用 @DateTimeFormat 指定格式,分页用 @RequestParam(defaultValue = ...) 提供默认值。
1. Controller 层
@GetMapping
public Result page(String name,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("班级分页查询:{},{},{},{},{}", name, begin, end, page, pageSize);
PageBean pageBean = clazzService.page(name, begin, end, page, pageSize);
return Result.success(pageBean);
}
2. Service 层
@Override
public PageBean page(String name, LocalDate begin, LocalDate end,
Integer page, Integer pageSize) {
PageHelper.startPage(page, pageSize);
List<Clazz> list = clazzMapper.page(name, begin, end);
Page<Clazz> p = (Page<Clazz>) list;
return new PageBean(p.getTotal(), p.getResult());
}
3. Mapper XML
<select id="page" resultType="com.ziggy.entity.Clazz">
select c.*, e.name as masterName,
case
when c.begin_date > current_date then '未开班'
when current_date > c.end_date then '已结课'
else '已开班'
end as status
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 c.end_date >= #{begin}
</if>
<if test="end != null">
and c.end_date <= #{end}
</if>
</where>
order by c.update_time desc, c.id desc
</select>
1.4 功能测试
- GET 查询不用
@RequestBody;对象字段名必须是pageSize,不是pagesize。 startPage后紧跟需要分页的 Mapper 查询,中间不要插入其他查询。- 日期是
LocalDate,只判断!= null,不要与''比较。 - 不传筛选条件能返回分页;没有匹配数据时应是
total=0、rows=[]。 - 分别准备今天开课、今天结课、未来开课、过去结课的班级,检查状态边界。
2. 删除班级:有学员时禁止删除
对应接口文档:3.2 删除班级(3.2.1 基本信息、3.2.2 请求参数、3.2.3 响应数据)
2.1 需求分析
发送 DELETE /clazzs/{id}。有学生的班级不允许删除,前端应收到:对不起, 该班级下有学生, 不能直接删除。
2.2 思路分析
Service 先按 student.clazz_id 统计人数;有学员就返回 false,不执行删除。没有学员时删除班级并返回 true,Controller 根据结果调用 Result.error 或 Result.success。这沿用已经学过的条件判断和统一返回结果。
1. Controller 层
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") Integer id) {
boolean success = clazzService.delete(id);
if (!success) {
return Result.error("对不起, 该班级下有学生, 不能直接删除");
}
return Result.success();
}
2. Service 层
@Override
public boolean delete(Integer id) {
if (clazzMapper.countStudents(id) > 0) {
return false;
}
clazzMapper.delete(id);
return true;
}
3. Mapper XML:两个方法对应两条 SQL
<select id="countStudents" resultType="java.lang.Long">
select count(*) from student where clazz_id = #{id}
</select>
<delete id="delete">
delete from clazz where id = #{id}
</delete>
2.4 功能测试
- 一个有学员的班级:返回
code=0,班级和学员都保留。 - 一个新建且无学员的班级:删除后再查,不能查到。
- 当前是路径参数
/clazzs/8,不是员工删除的/emps?ids=...。 - 删班级不等于删该班学生,不能把“禁止删除”写成“先删所有学生再删班级”。
3. 新增班级
对应接口文档:3.3 添加班级(3.3.1 基本信息、3.3.2 请求参数、3.3.3 响应数据)
3.1 需求分析
填写班级名称、教室、开课日期、结课日期、班主任和学科后,点击确定,发送 POST /clazzs,参数在 JSON 请求体中。班主任可以不选。
示例 JSON:
{"name":"Java就业参考班01","room":"101","beginDate":"2026-10-01","endDate":"2027-04-01","masterId":1,"subject":1}
示例 masterId 请换成实际存在的员工 ID。
3.2 思路分析
Controller 用 @RequestBody 接收 JSON。Service 补上创建、修改时间。Mapper 插入数据库列,不插入 masterName、status。
3.3 代码实现
1. Controller 层
@PostMapping
public Result save(@RequestBody Clazz clazz) {
log.info("新增班级:{}", clazz.getName());
clazzService.save(clazz);
return Result.success();
}
2. Service 层
@Override
public void save(Clazz clazz) {
LocalDateTime now = LocalDateTime.now();
clazz.setCreateTime(now);
clazz.setUpdateTime(now);
clazzMapper.save(clazz);
}
3. Mapper XML
<insert id="save" 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.4 功能测试
#{beginDate}读取对象属性;SQL 列名写begin_date。- 同名班级会触发唯一约束,交给项目中已有的全局异常处理器处理。
- 新增完成后查询列表,确认有创建时间、班主任姓名和计算状态。
- 本节只有一条写入 SQL,不需要为“看起来完整”而强行加事务注解。
4. 修改班级:查询回显
对应接口文档:3.4 根据ID查询(3.4.1 基本信息、3.4.2 请求参数、3.4.3 响应数据)
4.1 需求分析
点击编辑时,先发送 GET /clazzs/{id},例如 /clazzs/8。返回一个班级对象,用来填充表单。
4.2 思路分析
路径中的 ID 用 @PathVariable 接收。查询 clazz 单表即可,前端需要的班主任选中值是 masterId,不必查询 masterName 才能回显。这里没有员工工作经历那种一对多结构,使用 resultType 即可。
4.3 代码实现
1. Controller 层
@GetMapping("/{id}")
public Result getInfo(@PathVariable("id") Integer id) {
return Result.success(clazzService.getInfo(id));
}
2. Service 层
@Override
public Clazz getInfo(Integer id) {
return clazzMapper.getInfo(id);
}
3. Mapper XML
<select id="getInfo" resultType="com.ziggy.entity.Clazz">
select * from clazz where id = #{id}
</select>
4.4 功能测试
- 检查响应
data中的beginDate、endDate、masterId、subject。 - 数据库有值而 Java 属性为空,先核对驼峰映射;返回值正确但页面未选中,再检查下拉框 value 与返回字段的类型。
resultMap是结果映射规则,不是 Java 的Map集合;单表正常映射不需要为了回显而强行写resultMap。
5. 修改班级:保存数据
对应接口文档:3.5 修改班级(3.5.1 基本信息、3.5.2 请求参数、3.5.3 响应数据)
5.1 需求分析
回显后编辑表单,点击确定发送 PUT /clazzs。JSON 必须携带 id 和要修改的字段。
5.2 思路分析
Controller 接收对象。Service 设置 updateTime。Mapper 根据 ID 更新可编辑字段,保留原创建时间。
参照修改部门的写法,用 <set>、<if> 根据传入值动态更新。没有传入的字段不参与修改;masterId=null 表示本次不修改班主任。
1. Controller 层
@PutMapping
public Result update(@RequestBody Clazz clazz) {
clazzService.update(clazz);
return Result.success();
}
2. Service 层
@Override
public void update(Clazz clazz) {
clazz.setUpdateTime(LocalDateTime.now());
clazzMapper.update(clazz);
}
3. Mapper XML
<update id="update">
update clazz
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="room != null and room != ''">
room = #{room},
</if>
<if test="beginDate != null">
begin_date = #{beginDate},
</if>
<if test="endDate != null">
end_date = #{endDate},
</if>
<if test="masterId != null">
master_id = #{masterId},
</if>
<if test="subject != null">
subject = #{subject},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
</set>
where id = #{id}
</update>
5.4 功能测试
where id = #{id}不能遗漏,否则会更新全表。- 两个字段赋值之间必须有逗号;
<set>会去掉最后一个多余逗号。 - 修改后重新查询回显,确认新值落库且
createTime未被重写。 - 如果你选择动态
<set>写法,每项赋值末尾写逗号,由<set>去掉最后一个;它不会自动补中间的逗号。
6. 查询全部班级:学员页面下拉框
对应接口文档:3.6 查询所有班级(3.6.1 基本信息、3.6.2 请求参数、3.6.3 响应数据)
6.1 需求分析
发送 GET /clazzs/list,返回班级数组,供 Day14 查询条件和新增、修改学员表单选择班级。
6.2 思路分析
此处不分页,不复用带 PageHelper 的 page 方法。Controller 类上的 /clazzs 与方法上的 /list 组合成最终地址。
6.3 代码实现
1. Controller 层
@GetMapping("/list")
public Result list() {
return Result.success(clazzService.list());
}
2. Service 层
@Override
public List<Clazz> list() {
return clazzMapper.list();
}
3. Mapper XML
<select id="list" resultType="com.ziggy.entity.Clazz">
select * from clazz order by update_time desc, id desc
</select>
6.4 功能测试
GET /clazzs返回分页对象,GET /clazzs/list返回数组;前端取值方式不同。- 类已经有
@RequestMapping("/clazzs"),方法不要再写/clazzs/list,否则会组合成重复路径。
7. 附加接口:查询全部员工(班主任下拉框)
对应接口文档:2.6 查询全部员工(2.6.1 基本信息、2.6.2 请求参数、2.6.3 响应数据)
7.1 需求分析
新增和修改班级时要选择班主任,前端调用 GET /emps/list,返回员工数组,不需要分页。此接口属于员工模块,不放进 ClazzController。
7.2 思路分析
在已有员工模块分别增加 listAll 方法,获取员工 ID、姓名等字段。用实例 empService.listAll() 调用,不要写成接口名 EmpService.listAll()。下拉框展示 name,提交的班主任字段是员工的 id,存入 masterId。
7.3 代码实现
1. 在现有 EmpController 中增加
@GetMapping("/list")
public Result listAll() {
return Result.success(empService.listAll());
}
2. EmpService 接口声明与实现(分开放到两个文件)
// EmpService 接口:需要 import java.util.List;
List<Emp> listAll();
// EmpServiceImpl 实现类
@Override
public List<Emp> listAll() {
return empMapper.listAll();
}
3. EmpMapper 接口声明
List<Emp> listAll();
4. 放进已有 EmpMapper.xml
<select id="listAll" resultType="com.ziggy.entity.Emp">
select id, username, name, gender, phone, job, salary, image,
entry_date, dept_id, create_time, update_time
from emp
order by id
</select>
7.4 功能测试
- 这里假设
EmpController类上已经有@RequestMapping("/emps");完整地址为/emps/list。 - 接口返回数组,不应返回带
total/rows的分页对象;不需要把密码返回给下拉框。 /emps/list是具体路径,Spring 会优先匹配它,而不是把list当成/{id}的 ID。
8. 实现后检查
- 班级列表筛选的是结课日期,分页返回
total/rows。 - 未选择班主任的班级仍能出现在列表中。
- 班主任下拉框读取员工数组,选中值提交为
masterId。 - 新增、修改成功后重新查询,日期、班主任、学科均可回显。
- 同名班级返回失败消息。
- 有学员时禁止删除,无学员时可以删除。
- 查询全部班级返回数组,供 Day14-后端Web实战(自己实战-学员管理) 使用。