ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot学生成绩管理系统:从表结构设计到JWT权限控制实战

SpringBoot学生成绩管理系统:从表结构设计到JWT权限控制实战 简介这是一份基于 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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-Plus3.1 基础设施搭建pom.xml 里真正必须的依赖只有三个在 SpringBoot 工程搭建这一步网上有大量教程让你引入一堆依赖但对于学生成绩管理系统这个体量的项目核心依赖其实只有 Spring Web、MyBatis-Plus Framework、MySQL Driver 三样。至于 Lombok 和 Validation属于提升开发体验的辅助依赖可按需引入。Spring Boot 的版本选择建议不要追最新版选一个稳定但没有过度依赖 JDK 新特性的版本即可很多人在“springboot版本太高”的问题上翻车就是因为新版本对 JDK 版本有强制要求。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies选择 MyBatis-Plus 而非原生 MyBatis 的原因很直接成绩管理系统 90% 的数据库操作是单表 CRUDMyBatis-Plus 的BaseMapper接口已经内置了insert、selectById、updateById等方法不需要手写 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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 为 200BusinessException自定义业务异常用于在 Service 层抛出“课程不存在”“成绩已录入”等业务提示GlobalExceptionHandler用 RestControllerAdvice 捕获全局异常将堆栈信息转换为友好提示返回给前端Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT 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。更常见的做法是在这个体量的项目中直接用 JWTJSON Web Token把用户 ID 和角色编码进 Token后端通过拦截器完成无状态鉴权。JWT 的结构是一个三段式的字符串Header、Payload、Signature。Header 声明加密算法Payload 存放业务数据比如 userId 和 roleSignature 用密钥对前两段签名。后端在登录成功后生成 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(学生不存在); } LambdaQueryWrapperScore 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 ResultListScoreVO getMyScores(HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); String role (String) request.getAttribute(role); // 学生角色查询成绩列表必须带上userId防止横向越权 ListScoreVO 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 BaseMapperScore { 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) MapString, 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文件夹将建表语句和初始化数据分开存放数据库文件和源码压缩包保持同目录结构评阅老师解压后能直接导入运行这是最直观的工程规范体现。本文还有配套的精品资源点击获取
返回列表