SpringBoot学生成绩管理系统:从表结构设计到JWT权限控制实战
2026/9/12 12:51:42 网站建设 项目流程

简介:这是一份基于 Spring Boot 的期末大作业学生成绩管理系统源码与数据库,适合 Java 初学者、课程设计或毕业设计学生作为参考与二次开发基础。压缩包共 648 个文件、约 2.44MB,其中 62 个 Java 源文件负责后端逻辑,113 个 XML 对应 MyBatis 持久层语句,28 个 HTML 与 CSS/JS 构成前端页面,另有 SQL 数据库初始化脚本、application.yml 全局配置等;后端、Mapper、静态资源与前端模板按目录分层存放,便于查找和对照学习。系统采用 MVC 分层和 Maven 管理,涵盖教师、学生、班级、选课、考勤、成绩等模块,支持管理员、教师、学生多角色操作,并包含验证码工具类、登录验证等常见实现;数据库包含管理员信息表等表结构,从数据表设计到前后端交互都有清晰呈现。目前已有 2872 人浏览学习,项目可直接运行演示,也可作为期末答辩讲解和二次开发的脚手架,适合需要完整可运行 Spring Boot 案例的读者。

1. 期末大作业基于SpringBoot的学生成绩管理系统该从哪里下手

做学生成绩管理系统,最常踩的坑不是代码写不出来,而是“写了三天发现表结构撑不住需求”。这个题目的本质并不复杂:学生、课程、成绩三张核心表,加权限控制和基础CRUD,就构成了系统的全部业务闭环。真正值得花时间的地方在于角色边界怎么划、成绩的统计口径怎么定、以及前端到底做到哪一层就算合格。如果你的期末大作业正好选了这个题目,建议不要一上来就写Controller,先把数据模型和权限设计理清楚,这两块定了,后端代码反而是按部就班的事。本文从数据建模、SpringBoot工程搭建、权限认证、成绩业务、到答辩前要重点验证的五类边界情况,按一套可复现的顺序讲下来。整个项目在技术选型上只需要SpringBoot、MyBatis-Plus和MySQL,没有比这更稳的组合了,足够把CRUD、统计和角色权限讲得明明白白。

2. 成绩管理系统的核心模型与表结构设计

2.1 用三张主表加两张关联表覆盖全部业务需求

学生成绩管理系统的实体关系并不复杂,业务上有老师录入成绩、学生查看成绩、管理员管理账号这三个典型角色,所以表设计必须同时照顾到“数据怎么存”和“权限怎么控”两个维度。常见的做法是五张表:sys_user(用户表)、student(学生信息表)、course(课程表)、score(成绩表)、user_role(用户角色表)。它们之间的关系是:

  • sys_user 与 student 通过 user_id 一对一关联,保证账号与学籍信息解耦
  • course 与 score 是一对多,一门课程可以有多条成绩记录
  • score 表中同时存 student_id 和 course_id,作为联合唯一索引,防止同一学生同一课程重复录分
  • user_role 是多对多关联的中间表,一个用户可拥有多个角色

在建表时需要注意一个细节:不要把班级和院系列表单独建表,直接在 student 表里用字段存储即可。期末大作业的评判重点是系统完整性,而不是过度范式化,冗余这两个字段能大幅减少联表查询的复杂度。推荐使用 InnoDB 引擎,字符集统一用 utf8mb4,排序规则选 utf8mb4_general_ci,这样中文姓名和课程名称不会出乱码。

2.2 分角色设计的核心需求决定了字段必须带扩展性

教师端需要“按课程录入成绩、按课程查看成绩分布”,学生端需要“查看自己的成绩和学分绩点”,管理员则需要“维护用户和课程基本信息”。这三类需求映射到字段设计上,要求 score 表除了基本的三要素(学生、课程、分数)外,还建议预留一个 remark 字段,用于记录缓考、补考等特殊情况。

CREATE TABLE `score` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `student_id` bigint(20) NOT NULL COMMENT '学生ID,关联student表', `course_id` bigint(20) NOT NULL COMMENT '课程ID,关联course表', `score` decimal(5,2) DEFAULT NULL COMMENT '成绩,百分制,允许空值表示未录入', `remark` varchar(200) DEFAULT NULL COMMENT '备注:缓考/补考/缺考等', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_course` (`student_id`,`course_id`), KEY `idx_course_id` (`course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生成绩表';

这里有一个关键点:score字段定义为DEFAULT NULL而不是 NOT NULL。原因很实际——教师录入成绩时可能是分批导入的,允许为空意味着可以先把学生名单导入课程,再逐条录入分数。如果一开始就设置为 NOT NULL,导入学生名单时就必须先造一个默认值,反而多出一步清洗工作。唯一索引uk_student_course是从数据库层面兜底防止重复录入,即使 Controller 层忘了校验,数据库也不会产生脏数据。

2.3 建索引前先想清楚查询条件,别把所有字段都加索引

成绩管理系统的查询路径是固定的,不外乎三种:学生查自己的成绩(where student_id=?)、教师查某门课的成绩(where course_id=?)、管理员查全部或按条件筛选。所以索引只需要覆盖这三个方向即可,不需要给 remark、create_time 这些字段单独建索引。很多初学者喜欢给每个字段都加上 KEY,这在小数据量下看不出问题,但会拖慢写入速度,还会让期末答辩时被问到“你为什么在这个字段上建索引”时答不上来。

另一个值得做的优化是把成绩统计放在 SQL 层而不是 Java 内存里。比如计算某门课的平均分、最高分、最低分和及格率,直接用聚合函数一条 SQL 就能完成,比查出全量数据再在 Service 层循环累加要可靠得多。后续章节中涉及统计报表的接口会基于这些表结构来写,所以建表时字段命名一定要规范,不要用拼音缩写,尽量用完整的英文单词,避免在写 MyBatis-Plus 的 LambdaQueryWrapper 时产生歧义。

提示:如果你的期末大作业要求提供 MySQL 数据库脚本,建表语句里务必包含 DROP TABLE IF EXISTS 前缀,并注意执行顺序——先删子表再删父表,否则外键约束会导致删除失败。

3. 用SpringBoot初始化工程并整合MyBatis-Plus

3.1 基础设施搭建:pom.xml 里真正必须的依赖只有三个

在 SpringBoot 工程搭建这一步,网上有大量教程让你引入一堆依赖,但对于学生成绩管理系统这个体量的项目,核心依赖其实只有 Spring Web、MyBatis-Plus Framework、MySQL Driver 三样。至于 Lombok 和 Validation,属于提升开发体验的辅助依赖,可按需引入。Spring Boot 的版本选择建议不要追最新版,选一个稳定但没有过度依赖 JDK 新特性的版本即可,很多人在“springboot版本太高”的问题上翻车,就是因为新版本对 JDK 版本有强制要求。

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

选择 MyBatis-Plus 而非原生 MyBatis 的原因很直接:成绩管理系统 90% 的数据库操作是单表 CRUD,MyBatis-Plus 的BaseMapper接口已经内置了insertselectByIdupdateById等方法,不需要手写 XML 映射文件。对于多表关联查询,再用@Select注解或者 XML 补充即可。这样做的好处是代码量大幅减少,期末答辩时也更容易说清楚每一行代码的作用。需要注意mybatis-plus-boot-starter目前没有跟着 SpringBoot 3 的版本节奏走,如果你使用 SpringBoot 3.x,需要引入mybatis-plus-spring-boot3-starter,这个坑经常被忽略。

3.2 配置文件的正确写法:逻辑删除和驼峰映射必须开

application.yml 的配置决定了 MyBatis-Plus 能否正常工作。以下是一份可直接使用的配置,其中有三项设置是关键项,建议逐行理解而不是直接复制粘贴。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/score_management?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

逐项说明:map-underscore-to-camel-case负责把数据库的create_time自动映射为 Java 实体的createTime,不开启的话查询结果里这些字段会全部为 null。log-impl设为 StdOutImpl 会在控制台打印完整的 SQL 语句,这在调试阶段非常重要,可以直观看到 MyBatis-Plus 生成的 SQL 是否符合预期。逻辑删除是成绩系统的硬性需求——学生的成绩记录不应该被物理删除,否则历史数据会丢失,所以需要设置logic-delete-field: deleted,并在每张表中预留deleted字段。这里必须注意:如果启用了逻辑删除,数据库表里就必须有deleted字段,否则所有查询都会报 Unknown column 异常。

3.3 手工编写通用返回体和异常处理器,别过度依赖工具类生成器

在工程结构上,我建议采用标准的四层分包:controller、service、mapper、entity,另外加一个 common 包存放统一返回结果和异常处理。用 MyBatis-Plus 的代码生成器可以一次性生成这四层代码,但生成出来的 Controller 往往带有大量重复代码,并不适合直接作为期末作业提交。更务实的做法是手写以下三个核心类:

  • Result :统一返回体,包含 code、message、data 三个字段,成功时 code 为 200
  • BusinessException:自定义业务异常,用于在 Service 层抛出“课程不存在”“成绩已录入”等业务提示
  • GlobalExceptionHandler:用 @RestControllerAdvice 捕获全局异常,将堆栈信息转换为友好提示返回给前端
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

结果返回体是前后端约定的基础协议,早定下来后面所有接口都好写。这里需要说明的是,很多工程会引用 Hutool 的 Result 类或者阿里的 CommonResult,但期末答辩时,手写的类更容易讲清楚设计思路,也方便现场修改字段。异常处理器专门处理两类情况:参数校验失败时抛出 MethodArgumentNotValidException,业务逻辑异常时抛出 BusinessException,两者在 GlobalExceptionHandler 中分别捕获,返回不同的 code 和 message。

4. 基于JWT实现登录认证与三种角色权限控制

4.1 为什么期末管理系统需要 JWT 而不是 Session

学生成绩管理系统的用户类型有三种,如果只用 Session 做登录态管理,需要同时在服务端维护用户的角色信息,并确保每个请求都能通过 Session 拿到当前用户的身份。这种方式在单体应用里够用,但存在两个问题:一是 Session 数据占用服务器内存,二是前端无法跨域携带 Cookie。更常见的做法是在这个体量的项目中直接用 JWT(JSON Web Token),把用户 ID 和角色编码进 Token,后端通过拦截器完成无状态鉴权。

JWT 的结构是一个三段式的字符串:Header、Payload、Signature。Header 声明加密算法,Payload 存放业务数据(比如 userId 和 role),Signature 用密钥对前两段签名。后端在登录成功后生成 Token 返回给前端,前端后续请求在 Authorization 头中携带。需要注意:JWT 的 Payload 只是 Base64 编码,不是加密,所以千万不要把密码放进去。

4.2 登录接口与JWT工具类的实现细节

在 SpringBoot 工程中接入 JWT,需要引入jjwt依赖,然后编写一个 JwtUtil 工具类。以下是一个生成和解析 Token 的最小实现,已经适配了常见的需求:

@Component public class JwtUtil { // 实际使用时从配置文件读取,不要硬编码在代码里 @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }

这里有两个设计点需要特别说明。第一,将 role 作为 claim 放进 Token,是为了在拦截器中直接判断角色的访问权限,不需要每次请求都查数据库确认角色,提升接口响应速度。第二,expire 时间在配置文件中集中管理,一般设置为 24 小时。如果设置过短,用户频繁掉线会影响演示效果;如果设置过长,则会降低安全性。期末演示时建议设成 24 小时,避免演示到一半 Token 过期。

登录接口的完整业务逻辑分为三步:接收账号密码,调用userService.getByUsername查库,用 BCrypt 算法校验密码是否匹配。如果匹配成功,查询该用户的角色编码并生成 Token 返回。这里推荐使用 Spring Security 自带的 BCryptPasswordEncoder 做密码加密,而不是自己写 MD5——MD5 已经被证实不安全,答辩时被问到这一点会变成减分项。在 pom.xml 中只需要额外引入spring-security-crypto依赖,不需要引入完整的 Spring Security,这样就避开了 Spring Security 的全套认证流程配置。

4.3 拦截器配置中的三个细节决定你的鉴权是否好用

有了 Token 的生成和解析,还需要一个拦截器来保护受控接口。很多人写的拦截器只能判断“有没有登录”,却判断不了“有没有权限”,导致学生账号可以直接访问教师接口。以下是一个包含角色校验的拦截器实现:

@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException("未登录或登录已过期"); } // 解析Token获取角色,适合简单的前后端分离项目 Claims claims = jwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", Long.parseLong(claims.getSubject())); request.setAttribute("role", claims.get("role")); return true; } }

仅仅拦截“是否登录”还不够,角色权限的校验需要在 Controller 层配合自定义注解完成。一个简单可行的方案是定义@RequireRole("teacher")注解,在拦截器中通过HandlerMethod.getMethodAnnotation(RequireRole.class)拿到注解值,与 Token 中的 role 进行比对,不一致则抛出“无权访问”异常。这样做的好处是权限要求和接口放在一起,可读性远高于在拦截器里写死 URL 前缀判断。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value(); }

在拦截器中添加角色校验逻辑,核心代码如下:

if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole != null && !requireRole.value().equals(role)) { throw new BusinessException("权限不足,无法访问"); } }

这个方案的优点在于接口的权限边界一目了然,教师接口加@RequireRole("teacher"),管理员接口加@RequireRole("admin"),学生接口加@RequireRole("student"),同时在 WebMvcConfig 中注册拦截器并设置excludePathPatterns放行登录接口。

5. 成绩上报与统计:从CRUD到可答辩的业务闭环

5.1 成绩录入接口:事务和唯一索引双重保险

成绩录入是整个系统中业务逻辑最完整的操作。它的流程不仅是往 score 表里插一条记录,而是要完成三个步骤:校验课程存在、校验学生存在、判断是否已有成绩。如果用insert直接操作,数据量一旦增长,重复数据不可避免。更推荐的做法是先查再改——通过 LambdaQueryWrapper 检查同 studentId 和 courseId 的记录是否存在,存在则执行updateById,否则执行insert

@Override public void saveScore(ScoreDTO dto) { // 校验课程和学生的合法性,避免插入脏数据 Course course = courseService.getById(dto.getCourseId()); if (course == null) { throw new BusinessException("课程不存在"); } Student student = studentService.getById(dto.getStudentId()); if (student == null) { throw new BusinessException("学生不存在"); } LambdaQueryWrapper<Score> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Score::getStudentId, dto.getStudentId()) .eq(Score::getCourseId, dto.getCourseId()); Score existing = this.getOne(wrapper); if (existing != null) { // 如果存在则更新分数,不新增记录,保证一门课只有一个成绩 existing.setScore(dto.getScore()); existing.setRemark(dto.getRemark()); this.updateById(existing); } else { Score score = new Score(); BeanUtils.copyProperties(dto, score); this.save(score); } }

这段逻辑没有使用数据库层的ON DUPLICATE KEY UPDATE,原因是要在应用层完成更明确的业务提示。比如“课程不存在”和“学生不存在”必须返回给前端明确的提示信息,这在数据库层面是无法完成的。在 Service 方法上加上@Transactional(rollbackFor = Exception.class),保证两步操作——校验和写入——要么全部成功,要么全部回滚,防止出现学生存在但写成绩失败导致的状态不一致。

5.2 成绩查询时如何做到“学生只能看自己的分”

权限控制除了在拦截器层做角色校验,还需要在业务查询层做数据隔离。学生的角色虽然不能访问教师接口,但学生完全可能通过修改请求参数来查询其他人的成绩。比如“查询成绩列表”接口接收一个 studentId 参数,学生把该参数改成别人的 ID,接口就会返回别人的成绩。这种越权行为在期末答辩的代码审查中是常见扣分点。

正确的做法是:在 Controller 层不从请求参数获取 studentId,而是从 request attribute 中读取当前登录用户的信息。由于登录时已经将 userId 和 role 写入了 request 属性,业务代码可以直接取用:

@GetMapping("/my-scores") public Result<List<ScoreVO>> getMyScores(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); String role = (String) request.getAttribute("role"); // 学生角色查询成绩列表,必须带上userId,防止横向越权 List<ScoreVO> scores = scoreService.getScoresByUserId(userId, role); return Result.success(scores); }

对于教师角色,则需要查询自己授课课程的成绩列表。这里就涉及到“教师-课程”的关联关系,简单的设计可以在 course 表中增加 teacher_id 字段,表示该课程的授课教师。查询成绩时先查 teacher_id 等于当前 userId 的课程列表,再查这些课程下的成绩,而不是让教师传一个 courseId 就返回全部成绩。数据隔离这个点讲清楚了,整个系统在答辩时的技术层次就会明显高于普通CRUD项目。

5.3 统计报表:三条SQL覆盖平均分、及格率和分数段分布

成绩管理系统另一个容易出彩的功能项是统计报表。这里需要实现的不是花哨的图表展示,而是准确的后端统计数据。常见的统计需求有两个:单门课程的成绩统计、某位学生的学期汇总。前者用一条聚合 SQL 解决,后者则涉及多表关联。

MyBatis-Plus 的 BaseMapper 不提供聚合查询方法,这类统计功能需要在 Mapper 接口中自定义方法并配合注解实现。以下是一个统计课程分数分布的实现:

@Mapper public interface ScoreMapper extends BaseMapper<Score> { @Select("SELECT " + "COUNT(*) AS totalCount, " + "AVG(score) AS avgScore, " + "MAX(score) AS maxScore, " + "MIN(score) AS minScore, " + "SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS passRate " + "FROM score " + "WHERE course_id = #{courseId} AND deleted = 0") Map<String, Object> selectCourseStatistics(@Param("courseId") Long courseId); }

使用聚合函数时需要注意两点:第一,因为启用了 MyBatis-Plus 的逻辑删除,自己手写 SQL 必须带deleted = 0条件,否则统计数据会把已删除的记录也算进去。第二,AVG(score)计算的是算术平均分,如果系统有学分绩点(GPA)的需求,需要在 Service 层通过课程学分加权计算,SQL 层不做加权。成绩分数段分布可以类似地用SUM(CASE WHEN score BETWEEN 90 AND 100 THEN 1 ELSE 0 END)来实现,SQL 语句会稍长,但比查出全量数据在内存中分组要高效得多。

提示:手写统计 SQL 时,建议在成绩表增加deleted字段后,先在 Navicat 或命令行验证 SQL 语句执行结果正确,再复制到 Mapper 中。直接写进代码一旦出错,排查成本会更高。

6. 答辩前必须验证的五类边界场景与接口自测方法

学生成绩管理系统做到最后,功能跑通只是及格线,真正拉开差距和保证验收顺利的关键在细节处理上。以下按常见程度整理了期末大作业中最容易出问题的场景和验证方法:

一是空数据处理。成绩表中存在空分数,说明老师还没录完成绩,此时统计接口不能报空指针。建议在统计时用IFNULL(score, 0)或在前端做判空显示。

二是重复提交处理。教师连点两次保存成绩,第二次点击应该提示“成绩已存在”或自动变为更新操作,而不是报唯一键冲突异常。

三是跨域配置。Vue 前端开发服务器默认端口是 8081 或 5173,而后端是 8080,如果不在后端配置@CrossOrigin或者 WebMvcConfigurer 中设置addCorsMappings,前端调接口会失败。这一步经常被忽略,但演示时又是最先暴露问题的环节。

四是测试数据量。数据库造数时不要只插入两条记录做演示,建议造 30 个学生、10 门课、每门课都有完整成绩,这样统计报表接口的展示效果才会充实,答辩时截图也更有说服力。

五是接口自测。推荐用 SpringBoot 自带的 Swagger 接口文档依赖,或者直接使用 Postman / Apifox 做接口自测。启动项目后,逐个验证登录、权限拦截、成绩录入、统计报表这几个核心接口,确认返回格式统一、异常提示合理。用 Swagger 可以在线调试接口,省去手动拼 URL 的麻烦。

# 启动项目的完整流程 mvn clean package -DskipTests java -jar target/score-management-0.0.1-SNAPSHOT.jar # 启动后访问后端接口文档(已集成swagger时):http://localhost:8080/swagger-ui.html

成绩管理系统的验收重点从来不是功能的堆砌,而是逻辑的严密性。如果你能做到“学生接口越权访问被拦截、相同成绩重复提交被更新、统计 SQL 不加 deleted 条件查出来的是脏数据”这三点,再把表设计里的唯一索引和权限注解的设计思路讲清楚,这个期末大作业的技术含量就已经超过大部分 CRUD 项目了。最后补充一个实用技巧:在 resource 目录下建一个sql文件夹,将建表语句和初始化数据分开存放,数据库文件和源码压缩包保持同目录结构,评阅老师解压后能直接导入运行,这是最直观的工程规范体现。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询