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

独立实战:学员管理

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

Day14:实现学员管理、违纪处理与统计报表,梳理接口参数、动态 SQL 和业务校验。

我的学习 →

JavaWeb 线 · 线路目录
本页导读

独立实战:学员管理

前面已经完成了班级管理,接下来完成学员管理、违纪处理和学员统计。参照 Day09-后端Web实战(部门管理开发),每个功能按照需求分析、思路分析、代码实现展开。

使用已有 Student、Result 和 PageBean;查询条件通过方法形参接收。统计按照讲义里的 Map 集合方式返回数据,沿用原有 Controller、Service、Mapper 分层,不增加额外的响应实体、异常类或校验工具。

各节方法分别增加到对应的类、接口和 XML 中。参考代码尚未在你的数据库中联调,功能测试部分是完成代码后需要执行的检查。

0. 开发准备

0.1 接口总览

功能方法路径参数位置
条件分页查询GET/students查询参数
全部班级下拉框GET/clazzs/list无,复用 Day13
新增学员POST/studentsJSON
回显GET/students/{id}路径参数
修改PUT/studentsJSON
批量删除DELETE/students/{ids}路径中的逗号分隔 ID
违纪处理PUT/students/violation/{id}/{score}两个路径参数
班级人数统计GET/report/studentCountData无
学历统计GET/report/studentDegreeData无

0.2 表与实体

学员表为 student,使用已有 Student 实体,班级关联字段为 clazz_id。clazzName 是列表展示的班级名称,不对应学员表中的列。

no、phone、id_card 有唯一约束。新增时补充创建、修改时间,违纪次数和累计扣分使用数据库默认值 0。

注意:接口文档把身份证和是否院校字段标为选填,而课程建表脚本要求非空。按课程原表练习时,新增请求填写这两个字段即可;本篇不要求额外改表。

0.3 三层结构

和部门管理相同,Controller 接收请求,Service 处理业务,Mapper 执行 SQL。保留配置 mybatis.configuration.map-underscore-to-camel-case: true。数据库执行失败时,使用项目中原有的全局异常处理器。

下面先准备基本结构,各节的方法分别放进对应的类中。

// StudentController.java
package com.ziggy.controller;

import com.ziggy.entity.*;
import com.ziggy.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/students")
public class StudentController {
    @Autowired
    private StudentService studentService;
    // 放入下面各节的 Controller 方法
}
// StudentService.java:完整接口
package com.ziggy.service;

import com.ziggy.entity.*;

public interface StudentService {
    PageBean page(String name, Integer degree, Integer clazzId, Integer page, Integer pageSize);
    void save(Student student);
    Student getInfo(Integer id);
    void update(Student student);
    void delete(Integer[] ids);
    void violation(Integer id, Integer score);
}
// StudentServiceImpl.java
package com.ziggy.service.Impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.ziggy.entity.*;
import com.ziggy.mapper.StudentMapper;
import com.ziggy.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {
    @Autowired
    private StudentMapper studentMapper;
    // 放入下面各节的 Service 方法
}
// StudentMapper.java:包含统计查询,完整接口
package com.ziggy.mapper;

import com.ziggy.entity.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;

@Mapper
public interface StudentMapper {
    List<Student> page(@Param("name") String name,
                       @Param("degree") Integer degree,
                       @Param("clazzId") Integer clazzId);
    void save(Student student);
    Student getInfo(@Param("id") Integer id);
    void update(Student student);
    void delete(@Param("ids") Integer[] ids);
    void violation(@Param("id") Integer id, @Param("score") Integer score);
    List<Map<String, Object>> studentCountData();
    List<Map<String, Object>> studentDegreeData();
}

统计方法使用 java.util.Map,与讲义中的性别统计写法相同。Mapper XML 放在 src/main/resources/com/ziggy/mapper/StudentMapper.xml,把后续各节 SQL 合并到下列根标签内:

<?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.StudentMapper">
    <!-- 合并各节 SQL,同一个 id 只保留一份 -->
</mapper>

1. 学员列表条件分页查询

对应接口文档:4.1 学员列表查询(4.1.1 基本信息、4.1.2 请求参数、4.1.3 响应数据)

1.1 需求分析

打开学员列表时,按姓名、学历、班级筛选并分页。请求示例:

GET /students?name=张&degree=4&clazzId=1&page=1&pageSize=5

响应 data 是 { "total": ..., "rows": [...] },每名学员额外带 clazzName。

1.2 思路分析

Controller 接收查询参数;Service 启动分页并组装 PageBean;Mapper 使用 left join clazz 查询班级名称。姓名做模糊匹配,学历和班级 ID 做精确匹配。

1.3 代码实现

Controller 直接接收姓名、学历、班级 ID 和分页参数。

1. Controller 层

@GetMapping
public Result page(String name, Integer degree, Integer clazzId,
                   @RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize) {
    PageBean pageBean = studentService.page(name, degree, clazzId, page, pageSize);
    return Result.success(pageBean);
}

2. Service 层

@Override
public PageBean page(String name, Integer degree, Integer clazzId,
                     Integer page, Integer pageSize) {
    PageHelper.startPage(page, pageSize);
    List<Student> list = studentMapper.page(name, degree, clazzId);
    Page<Student> p = (Page<Student>) list;
    return new PageBean(p.getTotal(), p.getResult());
}

3. Mapper XML

<select id="page" resultType="com.ziggy.entity.Student">
    select s.*, c.name as 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, s.id desc
</select>

1.4 功能测试

  • degree、clazzId 是数字,只与 null 比较,不与 '' 比较。
  • 没有筛选条件时正常分页;多个条件同时传入时使用 AND 组合。
  • 传入不存在的姓名应返回空数组,不应报异常。
  • 一名学员只对应一个班级,这里没有一对多集合,不需要 <collection>。

2. 批量删除学员

对应接口文档:4.2 删除学员(4.2.1 基本信息、4.2.2 请求参数、4.2.3 响应数据)

2.1 需求分析

勾选一名或多名学员,发送:

DELETE /students/1,2,3

这是路径参数,不是员工模块的 DELETE /emps?ids=1,2,3。一个 ID 和多个 ID 可以用同一个接口。

2.2 思路分析

Controller 用 @PathVariable 将逗号分隔内容绑定为数组;Service 调用 Mapper;Mapper 用 <foreach> 拼接 IN (...)。

2.3 代码实现

1. Controller 层

@DeleteMapping("/{ids}")
public Result delete(@PathVariable("ids") Integer[] ids) {
    studentService.delete(ids);
    return Result.success();
}

2. Service 层

@Override
public void delete(Integer[] ids) {
    studentMapper.delete(ids);
}

3. Mapper XML

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

2.4 功能测试

  • Mapper 接口已用 @Param("ids") 明确参数名,所以 XML 的 collection 写 ids。
  • item="id" 是每轮元素的临时变量,循环内用 #{id}。
  • 必须有 open="(" 和 close=")";否则拼出 in 1,2,3,SQL 语法错误。
  • 使用你自己新增的练习记录测试;既查删除结果,也检查其他学员未受影响。

3. 新增学员

对应接口文档:4.3 添加学员(4.3.1 基本信息、4.3.2 请求参数、4.3.3 响应数据)

3.1 需求分析

发送 POST /students,JSON 请求体提交学员基本资料。下面是满足课程原始表非空约束的完整演示数据,只用于练习,不代表真实个人信息;班级 ID 要替换为本地存在的 ID。

{
  "name": "测试学员", "no": "2026101901", "gender": 1,
  "phone": "13800000001", "idCard": "110101200001010001",
  "isCollege": 0, "address": "练习地址", "degree": 4,
  "graduationDate": "2026-06-30", "clazzId": 1
}

3.2 思路分析

和新增部门相同,Controller 用 @RequestBody 接收 JSON 对象,Service 补充创建时间和修改时间,Mapper 插入数据库。违纪字段不写入 INSERT,使用表中定义的默认值 0。

3.3 代码实现

1. Controller 层

@PostMapping
public Result save(@RequestBody Student student) {
    studentService.save(student);
    return Result.success();
}

2. Service 层

@Override
public void save(Student student) {
    student.setCreateTime(LocalDateTime.now());
    student.setUpdateTime(LocalDateTime.now());
    studentMapper.save(student);
}

3. Mapper XML

<insert id="save" useGeneratedKeys="true" keyProperty="id">
    insert into student
        (name, no, gender, phone, id_card, is_college, address,
         degree, graduation_date, clazz_id, create_time, update_time)
    values
        (#{name}, #{no}, #{gender}, #{phone}, #{idCard}, #{isCollege},
         #{address}, #{degree}, #{graduationDate}, #{clazzId},
         #{createTime}, #{updateTime})
</insert>

3.4 功能测试

新增一名学员,检查列表中能否查到,违纪次数和扣分是否为 0。使用不同的学号、手机号、身份证号,避免触发唯一约束。SQL 列名使用 id_card、clazz_id,占位符中使用对象属性 #{idCard}、#{clazzId}。

INSERT 中没有列出违纪字段,数据库才会使用默认值;显式插入 null 不等于使用默认值。

4. 修改学员:查询回显

对应接口文档:4.4 根据ID查询(4.4.1 基本信息、4.4.2 请求参数、4.4.3 响应数据)

4.1 需求分析

点击编辑,发送 GET /students/{id},拿到学员对象后填入表单。

4.2 思路分析

路径参数 ID → Service 调用 Mapper → select * from student where id = ...。返回 clazzId 用于选择班级,graduationDate 用于日期控件,不需要先查中文班级名才能选中下拉框。

4.3 代码实现

1. Controller 层

@GetMapping("/{id}")
public Result getInfo(@PathVariable("id") Integer id) {
    return Result.success(studentService.getInfo(id));
}

2. Service 层

@Override
public Student getInfo(Integer id) {
    return studentMapper.getInfo(id);
}

3. Mapper XML

<select id="getInfo" resultType="com.ziggy.entity.Student">
    select * from student where id = #{id}
</select>

4.4 功能测试

  • 接口 data 为一个对象,不是数组,也不是 PageBean。
  • clazz_id、graduation_date 的回显依赖驼峰映射;响应有值而页面无值,再查前端绑定。
  • 检查违纪次数和扣分也被回显,因为课程修改接口会带回这两个字段。

5. 修改学员:保存数据

对应接口文档:4.5 修改学员(4.5.1 基本信息、4.5.2 请求参数、4.5.3 响应数据)

5.1 需求分析

编辑完成后,发送 PUT /students,JSON 携带 id 和表单字段。课程接口文档还要求传回 violationCount、violationScore。

5.2 思路分析

和修改部门相同,Controller 接收 JSON,Service 刷新修改时间,Mapper 用 <set>、<if> 动态更新。创建时间不需要修改。

5.3 代码实现

1. Controller 层

@PutMapping
public Result update(@RequestBody Student student) {
    studentService.update(student);
    return Result.success();
}

2. Service 层

@Override
public void update(Student student) {
    student.setUpdateTime(LocalDateTime.now());
    studentMapper.update(student);
}

3. Mapper XML

<update id="update">
    update student
    <set>
        <if test="name != null and name != ''">
            name = #{name},
        </if>
        <if test="no != null and no != ''">
            no = #{no},
        </if>
        <if test="gender != null">
            gender = #{gender},
        </if>
        <if test="phone != null and phone != ''">
            phone = #{phone},
        </if>
        <if test="idCard != null and idCard != ''">
            id_card = #{idCard},
        </if>
        <if test="isCollege != null">
            is_college = #{isCollege},
        </if>
        <if test="address != null and address != ''">
            address = #{address},
        </if>
        <if test="degree != null">
            degree = #{degree},
        </if>
        <if test="graduationDate != null">
            graduation_date = #{graduationDate},
        </if>
        <if test="clazzId != null">
            clazz_id = #{clazzId},
        </if>
        <if test="violationCount != null">
            violation_count = #{violationCount},
        </if>
        <if test="violationScore != null">
            violation_score = #{violationScore},
        </if>
        <if test="updateTime != null">
            update_time = #{updateTime},
        </if>
    </set>
    where id = #{id}
</update>

5.4 功能测试

  • 先 GET 回显,再 PUT 保存;本节按讲义用动态 SQL,未传字段不参与更新。
  • 保存后重新打开编辑窗口,检查姓名、毕业日期和所属班级;确认创建时间未变化。
  • 这不是员工“基本信息 + 工作经历”跨表更新,当前只有一条写入 SQL,不需要照搬员工经历的先删后增。
  • 动态 SQL 若使用 <if>,字符串判空写 != '',不是 != ' ';后者可能触发 OGNL 类型转换。

6. 违纪处理

对应接口文档:4.6 违纪处理(4.6.1 基本信息、4.6.2 请求参数、4.6.3 响应数据)

6.1 需求分析

对学员记录一次违纪并扣分,例如 PUT /students/violation/1/5:违纪次数加 1,累计扣分加 5。

6.2 思路分析

Controller 接收两个路径参数,通过 Service 调用 Mapper。Mapper 用一条 UPDATE 完成违纪次数加 1、累计扣分加 score。不要先查旧值,再在 Java 中相加后覆盖数据库。

6.3 代码实现

1. Controller 层

@PutMapping("/violation/{id}/{score}")
public Result violation(@PathVariable("id") Integer id,
                        @PathVariable("score") Integer score) {
    studentService.violation(id, score);
    return Result.success();
}

2. Service 层

@Override
public void violation(Integer id, Integer score) {
    studentMapper.violation(id, score);
}

3. Mapper XML

<update id="violation">
    update student
    set violation_count = violation_count + 1,
        violation_score = violation_score + #{score},
        update_time = now()
    where id = #{id}
</update>

6.4 功能测试

原值为次数 2、扣分 7,调用 /students/violation/1/5 后应为次数 3、扣分 12,而不是把扣分直接改成 5。

课程脚本的计数字段是 tinyint unsigned,练习使用正常范围内的正分数;这里保持课堂的一条 SQL 累加写法。

7. 附加接口:查询班级下拉框

对应接口文档:3.6 查询所有班级(3.6.1 基本信息、3.6.2 请求参数、3.6.3 响应数据;复用 Day13 接口)

7.1 需求分析

新增、修改和筛选学员时需要班级选项,前端调用 GET /clazzs/list。

7.2 思路分析

复用 Day13 的全部班级接口,不在 StudentController 中再声明一份。下拉框文字使用班级 name,选中值使用班级 id,保存到学员对象的 clazzId。

7.3 代码实现

1. 前后端字段对应关系

GET /clazzs/list
    → data: [{id: 1, name: "Java参考班"}, ...]
    → 下拉框 label = name,value = id
    → 学员 JSON: {"clazzId": 1, ...}

7.4 功能测试

  • 下拉框返回的是完整数组,不能把 GET /clazzs 的当前一页当作所有班级。
  • 回显中 clazzId 与选项 ID 的类型要一致,不要一边数字、一边字符串。

8. 学员统计:准备工作

和 Day12 的员工统计相同,Controller 调用 ReportService,Service 调用 Mapper,Mapper 使用 Map 接收统计结果。

接口data 结构
班级人数统计{"clazzList":["A班","B班"],"dataList":[2,0]}
学历统计[{"name":"本科","value":2}]

本篇不创建统计响应实体:柱状图的两个数组放进一个 Map,饼图直接返回 List<Map>。Map 是 Java 集合;MyBatis 的 resultMap 则是另一种结果映射配置,不能混淆。

1. ReportService 接口

package com.ziggy.service;

import java.util.List;
import java.util.Map;

public interface ReportService {
    Map<String, Object> studentCountData();
    List<Map<String, Object>> studentDegreeData();
}

2. ReportServiceImpl 基本结构

package com.ziggy.service.Impl;

import com.ziggy.mapper.StudentMapper;
import com.ziggy.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class ReportServiceImpl implements ReportService {
    @Autowired
    private StudentMapper studentMapper;
    // 增加第9、10节的实现方法
}

3. ReportController 基本结构

package com.ziggy.controller;

import com.ziggy.entity.Result;
import com.ziggy.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/report")
public class ReportController {
    @Autowired
    private ReportService reportService;
    // 增加第9、10节的 Controller 方法
}

如果已经写过员工统计,把方法加入现有 ReportController、ReportService 和实现类,保留原方法,不要再创建一套重复类。StudentMapper 的统计方法已在第0节接口中列出。

9. 班级人数统计

对应接口文档:5.4 班级人数统计(5.4.1 基本信息、5.4.2 请求参数、5.4.3 响应数据)

9.1 需求分析

请求 GET /report/studentCountData,返回班级名称数组和对应人数数组,两个数组的下标必须一一对应。没有学员的班级也应显示,人数为 0。

9.2 思路分析

以班级表作为左表,关联学员表;用 count(s.id) 统计真实学生,而不是 count(*)。按班级 ID 和名称分组,固定按班级 ID 排序。Service 参照讲义使用 Stream 分别取出班级名称列表和人数列表,再封装为 Map 返回。

9.3 代码实现

1. ReportController 层

@GetMapping("/studentCountData")
public Result studentCountData() {
    return Result.success(reportService.studentCountData());
}

2. ReportServiceImpl 层

@Override
public Map<String, Object> studentCountData() {
    List<Map<String, Object>> list = studentMapper.studentCountData();
    List<Object> clazzList = list.stream().map(map -> map.get("name")).toList();
    List<Object> dataList = list.stream().map(map -> map.get("value")).toList();

    Map<String, Object> data = new HashMap<>();
    data.put("clazzList", clazzList);
    data.put("dataList", dataList);
    return data;
}

3. StudentMapper XML

<select id="studentCountData" resultType="java.util.Map">
    select c.name as name, count(s.id) as value
    from clazz c
    left join student s on s.clazz_id = c.id
    group by c.id, c.name
    order by c.id
</select>

9.4 功能测试

  • 空班级被 left join 补出一行,count(*) 会错误地算成 1;count(s.id) 才是 0。
  • 两个数组长度相同;同一位置的名字和人数对应同一个班级。
  • 完全没有班级时返回两个空数组,不返回 null。
  • 若存在引用了不存在班级的历史学员,这个“按有效班级统计”不会计入他们,需另行修复孤儿关联数据。

10. 学员学历统计

对应接口文档:5.3 学员学历统计(5.3.1 基本信息、5.3.2 请求参数、5.3.3 响应数据)

10.1 需求分析

请求 GET /report/studentDegreeData,返回对象数组,每项是学历文案和人数:[{"name":"本科","value":2}]。

10.2 思路分析

按 degree 分组统计人数,使用 CASE 将数字翻译为中文。没有学生的学历不需要补零;其他学历值按课堂 CASE 写法显示为“其他”。

10.3 代码实现

1. ReportController 层

@GetMapping("/studentDegreeData")
public Result studentDegreeData() {
    return Result.success(reportService.studentDegreeData());
}

2. ReportServiceImpl 层

@Override
public List<Map<String, Object>> studentDegreeData() {
    return studentMapper.studentDegreeData();
}

3. StudentMapper XML

<select id="studentDegreeData" 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 as name,
           count(*) as value
    from student
    group by degree
    order by degree
</select>

10.4 功能测试

  • 响应字段必须叫 name 和 value,不要返回 degree/count 后让前端猜字段。
  • 新增一名本科学生后,本科人数应加 1;删除该学生后应减 1。
  • 所有学历人数之和应等于 student 表总人数。
  • resultType="java.util.Map" 将每行查询结果封装成 Map,SQL 别名 name、value 就是 Map 的键。

11. 联调顺序与自查

按部门管理相同的方式,写完一个接口就检查该接口的输入与输出,再串联流程。

  1. 查询学员列表,检查默认分页、姓名/学历/班级组合筛选。
  2. 查询全部班级,确认下拉框 value 对应 clazzId。
  3. 用不重复的学号、手机号和身份证新增练习学生。
  4. 查询回显,检查毕业日期和班级 ID。
  5. 修改并重新打开表单,确认保存后的数据。
  6. 连续记录两次违纪,核对次数和累计扣分。
  7. 检查班级人数、学历统计,再批量删除练习记录,观察图表人数变化。
  8. 回到 Day13 验证:有学员的班级不能删;删完其学员后才允许删。
现象优先检查
405 / DELETE 不支持类和方法的映射路径是否匹配前端
回显日期或班级为 null表列有无值、驼峰映射、返回字段名
NumberFormatException 出现在 OGNL是否把 '' 写成 ' '、是否拿数字与字符串比较
Unknown columnSQL 左边用了 Java 属性名或拼错列名
批量删除 SQL 报错数组是否传到 Mapper、@Param 名称、foreach 括号
新增时非空约束失败身份证等字段与课程脚本、接口是否一致
前端只有“出错了”查看已有全局异常处理器和控制台的具体异常
柱状图不能展示是否误把对象数组作为 clazzList/dataList 返回