1. 文章
  2. JavaWeb 线
  3. Tlias 后端接口实战

独立实战:班级管理

更新于 13 分钟阅读 第 14 / 24 站 #java#javaweb#后端

Day13:按 Controller、Service、Mapper 分层实现班级查询、新增、修改与删除,并完成业务校验。

我的学习 →

JavaWeb 线 · 线路目录
本页导读

独立实战:班级管理

前面完成了部门管理和员工管理,接下来完成班级管理。每个功能按照“需求分析 → 思路分析 → 代码实现”讲解,代码按 Controller、Service、Mapper 分层增加到对应文件中。

代码使用你现有的 Clazz、Result、PageBean,包名为 com.ziggy。查询参数和课堂代码一样直接通过方法形参接收。删除班级时先判断是否有学员,由 Controller 使用已有的 Result.error 返回提示。

本篇是根据实战资料和课堂代码整理的参考讲解,尚未在你的数据库中联调;功能测试写的是完成代码后应执行的检查。

0. 开发准备

0.1 本日接口

功能请求方式请求路径参数位置
条件分页查询GET/clazzs查询参数
班主任下拉框GET/emps/list无
新增班级POST/clazzsJSON 请求体
查询回显GET/clazzs/{id}路径参数
修改班级PUT/clazzsJSON 请求体
删除班级DELETE/clazzs/{id}路径参数
全部班级下拉框GET/clazzs/list无

班级模块自身有 6 个接口,另外复用员工模块的 /emps/list。班主任下拉框查询员工;学员页面中的班级下拉框查询 /clazzs/list,两者不能混淆。

0.2 表结构与字段映射

数据库列Java 属性含义
idid班级 ID
namename班级名称,有唯一约束
roomroom教室
begin_datebeginDate开课日期
end_dateendDate结课日期
master_idmasterId班主任,关联 emp.id
subjectsubject学科编号
create_time / update_timecreateTime / 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 &gt; current_date then '未开班'
               when current_date &gt; 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 &gt;= #{begin}
        </if>
        <if test="end != null">
            and c.end_date &lt;= #{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实战(自己实战-学员管理) 使用。