SpringBoot+MySQL学生成绩管理系统:从建表到JWT权限实战
简介这套文档资源是围绕SpringBoot学生成绩管理系统的完整设计与实现说明面向需要完成课程设计、毕业设计或了解JavaWeb项目开发流程的开发者。系统基于SpringBoot整合MySQL与JavaWeb技术规划了管理员、教师、学生三种角色覆盖院系管理、考试成绩管理、用户权限维护等典型业务模块。全文从绪论、系统分析、概要设计到具体功能实现与测试层层展开包含数据库概念模型与表结构设计、各端口页面与功能逻辑说明以及注册登录等关键模块的单元测试与集成测试记录可直接作为项目开发参考或论文撰写蓝本。压缩包内共1个文件格式为docx大小1.14MB内容即该设计文档的完整版本。已有8554人浏览/学习说明该选题与实现方案具有较高的参考价值。阅读这份资源可以快速梳理出基于SpringBoot的学生成绩管理系统整体架构掌握从需求分析、数据库建模到后端功能实现与测试的方法适合SpringBoot初学者对照实践也适合即将做类似管理系统项目的人群借鉴定题与设计思路。1. 用 SpringBoot 重做学生成绩管理这套 JavaWeb 项目到底解决了什么期末帮某高校教务处整理成绩数据时我对着那套老旧的 JSP Servlet 系统卡了整整一下午一个字段对不上就得重启整个 Tomcat。后来我用 SpringBoot MySQL 重新搭了一套学生成绩管理系统从建表、登录到成绩统计报表只花了三个晚上。这份资源就是那套可跑通的 JavaWeb 完整实现带前端页面和后端接口覆盖学生信息维护、课程管理、成绩录入与班级统计报表。适合正在做课程设计的在校学生也适合想快速看一套业务闭环的 JavaWeb 管理系统的开发者。下面直接拆开讲表怎么建、接口怎么设计、权限怎么做、哪里最容易翻车。2. 从零搭工程SpringBoot MySQL 的表结构、分层与启动2.1 为什么选 SpringBoot 2.7 JDK 8做课设或毕设最怕的不是功能难而是环境带不动。SpringBoot 3.x 强制要求 JDK 17很多实验室的虚拟机里装的是 JDK 8装了 3.x 以后编译都过不去只能回头降版本。SpringBoot 2.7.x 是最后一代原生支持 JDK 8 的主线版本资料多、报错少、答辩老师也熟悉所以我一般建议直接用 2.7.18。内嵌 Tomcat 是另一个省心点。以前写 Servlet 要单独装 Tomcat、配置 server.xml、把 war 包丢进 webapps稍有不对就是 404。SpringBoot 把 Tomcat 打进 jar 里java -jar一条命令就能起服务这对课设演示来说非常关键不用再折腾外部容器。工程依赖只需要四个核心东西web 起步依赖、MyBatis 的 Spring Boot 整合包、MySQL 驱动和 JWT 工具库。pom 里的坐标直接抄这份dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies这里有两个容易踩的细节。第一MySQL 8.x 驱动类的全限定名是com.mysql.cj.jdbc.Driver网上老教程里写的com.mysql.jdbc.Driver在 8.x 下虽然不报错但会打出弃用警告看着像出问题了。第二mybatis-spring-boot-starter的版本不要随便升到 3.x那个是给 SpringBoot 3 用的JDK 8 环境跑不起来。接着是数据源配置。我用的是 application.yml连接串里必须把时区和编码一次性写清楚server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/student_score?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你自己的密码 mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml jwt: secret: score-demo-secret-key-2026-please-change expire: 7200 header: AuthorizationserverTimezoneAsia/Shanghai不加的话MySQL 8 连接会直接报时区错误后面避坑章节再细说。useSSLfalse是关掉 SSL 握手本地开发没必要开开了反而偶尔报证书警告。map-underscore-to-camel-case这个配置特别重要数据库字段是student_noJava 属性是studentNo靠它自动映射否则查出来全是 null。2.2 四张表的设计成绩表必须加联合唯一索引成绩管理系统的表不需要设计得很复杂核心就是用户、学生、课程、成绩四张。用户表存登录账号和角色学生表存学号姓名班级课程表存课程名和学分成绩表存学生和课程的关联分数。我按下面的结构建表数据库名用student_scoreCREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 加密后存储, role VARCHAR(20) NOT NULL COMMENT ADMIN/TEACHER/STUDENT, name VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender VARCHAR(4), class_name VARCHAR(50), phone VARCHAR(20) ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1), teacher_name VARCHAR(50), semester VARCHAR(20) ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_type VARCHAR(10) DEFAULT 期末, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id, exam_type) );user是 MySQL 的保留字所以建表时一定要包反引号不然 SQL 直接报语法错误这是新手最常见的第一个坎。score表上的联合唯一索引uk_stu_course是重点。同一个学生在同一门课的同一考试类型下只能有一条成绩这是业务上的硬约束。光靠 Java 代码先查再插不够安全并发请求同时进来时两个线程都可能查到没有记录然后各自插入一条数据就重复了。加了这个唯一索引数据库层面直接拦住第二个插入会抛 DuplicateKeyException。代码里做一次友好提示索引做最后兜底这才是正规做法。至于学生表和课程表要不要建物理外键我的建议是建。虽然很多生产环境为了性能不用物理外键但课设场景数据量小物理外键能让成绩表不出现幽灵学生演示和答辩都更站得住脚。代价就是后面删除学生时要先删成绩记录否则外键约束会报错这个坑在第五章专门讲。2.3 工程目录按 controller、service、mapper 四层拆工程结构不搞花活就按最标准的四层拆。entity 放实体类mapper 放数据库访问接口和 XMLservice 写业务逻辑controller 只负责接参数和返回结果。这样拆的好处是答辩被问到业务逻辑写在哪儿时你能直接指出 service 包而不是说全写在 Controller 里。src/main/java/com/example/score/ ├── config/ │ ├── CorsConfig.java │ └── JwtInterceptor.java ├── controller/ │ ├── LoginController.java │ ├── StudentController.java │ ├── CourseController.java │ └── ScoreController.java ├── entity/ │ ├── User.java │ ├── Student.java │ ├── Course.java │ └── Score.java ├── mapper/ │ ├── UserMapper.java │ ├── StudentMapper.java │ ├── CourseMapper.java │ └── ScoreMapper.java ├── service/ │ └── ScoreService.java ├── util/ │ └── JwtUtil.java └── ScoreApplication.java src/main/resources/ ├── mapper/ │ ├── StudentMapper.xml │ ├── CourseMapper.xml │ └── ScoreMapper.xml └── application.yml启动类上记得加MapperScan(com.example.score.mapper)这样 MyBatis 会自动扫描 mapper 包下的接口不用在每个 Mapper 接口上重复写Mapper注解。SpringBootApplication MapperScan(com.example.score.mapper) public class ScoreApplication { public static void main(String[] args) { SpringApplication.run(ScoreApplication.class, args); } }这个类一旦写好基本不用再动。后面的开发节奏就是加一张表对应一个 entity写一个 Mapper 接口配一个 XML再往上写 service 和 controller全部是重复劳动没有技术风险。3. 核心业务落地学生、课程、成绩录入与统计报表3.1 成绩录入接口事务和唯一索引配合别只做前端校验成绩录入是整个系统的核心操作前端页面会校验分数范围但后端必须再校验一次。接口设计成 POST/api/score接收学生 ID、课程 ID、分数和考试类型。我在 Controller 里只做参数合法性判断真正的业务规则放在 Service 里。RestController RequestMapping(/api/score) public class ScoreController { Autowired private ScoreService scoreService; PostMapping public Result add(RequestBody ScoreDto dto) { if (dto.getScore() null || dto.getScore() 0 || dto.getScore() 100) { return Result.error(分数必须在 0~100 之间); } scoreService.addScore(dto); return Result.success(成绩录入成功); } }Service 里的逻辑分三步查重、组装实体、插入。这里的关键是事务注解成绩录入虽然只有一条 insert但后续如果扩展录入成绩同时更新学生平均分之类的操作事务能保证两步要么都成功要么都回滚。Service public class ScoreService { Autowired private ScoreMapper scoreMapper; Transactional(rollbackFor Exception.class) public void addScore(ScoreDto dto) { int count scoreMapper.countByStudentAndCourse( dto.getStudentId(), dto.getCourseId(), dto.getExamType()); if (count 0) { throw new RuntimeException(该学生此课程已录入过成绩); } Score score new Score(); score.setStudentId(dto.getStudentId()); score.setCourseId(dto.getCourseId()); score.setScore(dto.getScore()); score.setExamType(dto.getExamType()); scoreMapper.insert(score); } }Transactional(rollbackFor Exception.class)这个写法要特别说明一下。默认情况下 Spring 的事务只对运行时异常回滚如果你在代码里 catch 住了异常又往外抛了一个检查异常不加rollbackFor的话事务不会回滚。写rollbackFor Exception.class就是告诉 Spring只要是异常就回滚别侥幸。这是我做过一次数据错乱之后学乖的。countByStudentAndCourse对应的是score表上的联合唯一索引查询不是全表扫描性能没问题。如果查重发现已存在直接抛业务异常由全局异常处理器转成 JSON 返回给前端。这里还留了一个兜底万一两个请求同时通过了查重第二个插入会因为唯一索引冲突报 DuplicateKeyException全局异常处理器也要接住这个异常返回成绩重复录入而不是一堆堆栈。3.2 成绩查询接口MyBatis 动态 SQL 处理多条件组合成绩查询是使用频率最高的功能学生要看自己的成绩老师要按班级、按课程查参数组合非常多。常见查询条件是学生姓名、课程名称、分数区间、考试类型。如果每个组合都写一个 SQL那要写十几个方法维护起来是灾难。MyBatis 的动态 SQL 就是为这个场景设计的用if标签拼条件核心 XML 如下select idqueryScores resultTypecom.example.score.entity.Score SELECT * FROM score where if teststudentName ! null and studentName ! AND student_id IN (SELECT id FROM student WHERE name LIKE CONCAT(%, #{studentName}, %)) /if if testcourseName ! null and courseName ! AND course_id IN (SELECT id FROM course WHERE course_name #{courseName}) /if if testminScore ! null AND score gt; #{minScore} /if if testmaxScore ! null AND score lt; #{maxScore} /if /where ORDER BY create_time DESC /select这里有个细节很多人第一次写会翻车SQL 里的和在 XML 文件里必须转义成gt;和lt;不然 XML 解析直接报错。另一个细节是where标签会自动处理第一个条件前面的AND所以每个if里都放心写AND不用判断是不是第一个条件。按学生姓名模糊查询时的CONCAT(%, #{studentName}, %)我建议在 SQL 里做拼接而不是在 Java 代码里拼好再传进来。直接在 Java 里写成%name%也能跑但参数传到 SQL 里时要小心引号转义在 XML 里用CONCAT更干净也避免注入风险。#{}预编译是防注入的关键千万别用${}拼接。这套动态 SQL 能覆盖 90% 的查询场景。前端页面上四个输入框用户填了几个条件就传几个参数没填的自然不会拼进 SQL。3.3 统计报表一条聚合 SQL 算完平均分和及格率成绩管理系统如果只有增删改查答辩时的亮点是不够的。统计报表是加分项也是实际教务工作中最常用的功能每门课的选课人数、平均分、最高分、最低分、及格率。我用一条聚合 SQL 完成这些统计不写 Java 循环去内存里算SELECT c.course_name, COUNT(s.id) AS total_count, ROUND(AVG(s.score), 2) AS avg_score, ROUND( SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(s.id), 2 ) AS pass_rate, MAX(s.score) AS max_score, MIN(s.score) AS min_score FROM score s LEFT JOIN course c ON s.course_id c.id WHERE s.exam_type 期末 GROUP BY c.id, c.course_name ORDER BY avg_score DESC;及格率的计算逻辑是重点CASE WHEN score 60 THEN 1 ELSE 0 END把每条成绩转成 0 或 1SUM求和就是及格人数再除以总数乘以 100 就是百分比。ROUND(..., 2)保留两位小数。这里有个小坑SUM(...) * 100.0里的100.0不能写成100否则整数除法会直接舍去小数及格率算出 0 或 100 这种离谱值。考试类型参数可以从前端传过来WHERE条件也可以改成动态 SQL。这张统计表的数据可以直接返回给前端用 ECharts 画柱状图或折线图都行。哪怕前端只用一个table展示这条 SQL 的讲解也足够支撑答辩时的系统亮点提问。4. 登录与权限JWT 无状态鉴权的落地方式4.1 为什么不用 Session 而是 JWT传统 JavaWeb 项目做登录最常见的方案是HttpSession存用户信息。这个方案在老项目里没问题但前后端分离开发时就别扭了前端跑在 8081 端口后端跑在 8080 端口跨端口请求带 Cookie 时需要额外配置浏览器同源策略还会拦一下。改 JWT 之后登录成功后端返回一个 token 字符串前端存到 localStorage 里每次请求在 header 里带上后端验签即可。这个方案无状态后端不需要维护会话部署到多台服务器也不用考虑 Session 同步。跟 Shiro 和 Spring Security 相比JWT 加拦截器的做法代码量更少、逻辑更直白。Shiro 虽然强大但配置项多课设阶段被它的过滤器链卡住很常见。Spring Security 更重学习曲线陡为了一个成绩管理系统引入它性价比不高。我一般用 JWT HandlerInterceptor 做鉴权重点代码少还容易在答辩时讲清楚。JWT 相关的参数已经在 application.yml 里配置过了三参数的含义和调整建议见下表参数示例值说明jwt.secretscore-demo-secret-key-2026-please-change签名密钥越长越安全不要硬编码在代码里jwt.expire7200token 有效期单位秒7200 即 2 小时可根据演示时长调整jwt.headerAuthorization前端请求时携带 token 的 header 名称保持前后端一致4.2 登录签发 Token 与拦截器校验登录接口接收用户名和密码校验通过后生成 token 返回。密码我在这个项目里用了 BCrypt 加密存储登录时比对密文而不是明文比对。不过要注意查数据库时必须用username去查不能用密码直接查否则绕过 BCrypt 校验的同时也暴露了用户存在性。public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private long expire; public String createToken(String username, String role) { return Jwts.builder() .setSubject(username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }setSubject(username)把用户名写进 token 主体claim(role, role)把角色信息作为自定义字段放进 token。这两个信息在后面的权限判断中都会用到。expire * 1000是因为配置里写的是秒而Date构造函数需要毫秒漏掉这个换算的话 token 会秒过期。拦截器的核心逻辑很简单每个请求进来先取 header 里的 token解析成功就放行解析失败返回 401。Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims jwtUtil.parseToken(token); request.setAttribute(username, claims.getSubject()); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }拦截器里有个坑OPTIONS请求必须放行。跨域请求时浏览器会先发一个 OPTIONS 预检请求这个请求不会带自定义 header如果拦截器把它拦了前端会报 401真正的请求根本发不出去。这个检查必须放在最前面。4.3 角色权限让不同账号看到不同功能系统里设计了三种角色管理员、教师、学生。管理员管账号和学生信息教师录成绩学生只能查自己的成绩。权限判断我习惯放在拦截器里统一处理而不是散落在各个 Controller 里。一个简单做法是给需要权限的请求做一个写入校验private boolean isWriteRole(HttpServletRequest request) { Object role request.getAttribute(role); return ADMIN.equals(role) || TEACHER.equals(role); }在拦截器的preHandle里根据请求方法判断POST、PUT、DELETE 这类写操作要求 ADMIN 或 TEACHER 角色GET 请求所有登录用户都能访问。学生账号点击录入成绩的按钮前端可以隐藏入口后端接口返回 401双重保险。更规范的做法是自定义一个RequireRole(ADMIN)注解配合 Spring MVC 的HandlerMethod判断。拦截器里通过handler instanceof HandlerMethod判断当前是不是真正的接口方法再读注解上的角色值做比对。这个方案去答班会好看很多也确实是生产项目常见的设计但代码量会上去。课设阶段先用角色字符串判断也能跑通两种写法都能讲清楚权限控制这件事别小看后面这种简单写法最怕的是把逻辑写成一团又讲不清。5. 避坑与排查五个最常见的运行和配置问题5.1 启动报错端口被占用现象启动 SpringBoot 时控制台直接抛异常日志里能看到Port 8080 was already in use。原因本地有其他 Java 进程或开发工具占用了 8080 端口最常见的是之前启动过同一个项目没关干净。解决两种方式任选。一是改端口在application.yml里把server.port改成 8081但改完记得前端所有 API 请求地址都要跟着改二是找到占用端口的进程杀掉。Windows 下的操作是netstat -ano | findstr 8080输出里能找到占用 8080 端口的进程 PID然后taskkill /PID 进程号 /FMac 或 Linux 下用lsof -i :8080找到 PID再kill -9 进程号。这个问题本身不难但每次启动前先确认端口干净能省很多时间。我自己的习惯是固定端口后单独写一个启动脚本里面先做端口检查再启动减少手动排错的次数。5.2 数据库连接失败时区和 SSL 警告现象应用启动时访问数据库的接口报错堆栈里有The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者控制台打出一长串 SSL 警告。原因MySQL 8.x 驱动默认要求服务器时区明确而本机 MySQL 的全局时区设置不规范导致驱动拿到一个无法识别的时区值。SSL 警告是驱动默认尝试建立加密连接本地开发环境没有配置证书。解决数据源 URL 后面固定追加两个参数?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseserverTimezoneAsia/Shanghai写死时区useSSLfalse关闭加密连接。注意只改 URL 不用改 MySQL 配置我一般把这个 URL 当成模板存在笔记里每次新建项目直接复制没再出过时区问题。那个 SSL 警告本质上不是错误项目能正常跑但信息量大容易把新手吓到关了useSSL后日志干净很多。5.3 查询结果全是 nullMyBatis 驼峰映射没开现象登录成功后查询学生列表接口返回 JSON发现studentNo、className这类字段全是 null但数据库里明明有值。原因数据库字段是下划线风格student_noJava 实体属性是驼峰风格studentNoMyBatis 默认不做自动映射字段对不上就返回 null。解决在application.yml里加上这条配置mybatis: configuration: map-underscore-to-camel-case: true加完重启就生效。如果项目里个别字段仍然对不上比如数据库列名stu_name、Java 属性name这种情况在 XML 的resultMap里显式写映射就可以。我见过不少同学卡在这个问题上查了半天数据库、反复打印日志最后发现就是少了一行配置属于典型的配置型翻车。5.4 前端跨域请求失败现象前端页面单独跑在一个端口比如 8081后端接口在 8080打开浏览器访问前端页面点登录按钮F12 控制台出现 CORS 相关错误请求根本到不了后端。原因浏览器的同源策略拦截了跨端口请求。前后端分离开发时前端和后端端口不同属于跨域。解决后端加一个全局跨域配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }addAllowedOriginPattern(*)表示允许所有来源setAllowCredentials(true)表示允许携带 Cookie 和认证信息。如果两条配置像网上某些老帖那样用addAllowedOrigin(*)再配合setAllowCredentials(true)浏览器会直接报错因为规范不允许带凭证的同时通配来源。这是 CORS 配置里最微妙的一个坑。5.5 删除学生或课程失败外键约束卡住现象删一条学生记录页面报错数据库抛Cannot delete or update a parent row: a foreign key constraint fails。原因score表的student_id做了物理外键关联默认外键行为是RESTRICT只要成绩表里还有这个学生的记录就不允许删学生。解决两个方案。一是业务上先删成绩再删学生Service 里把它做成一个事务方法先执行DELETE FROM score WHERE student_id ?再执行DELETE FROM student WHERE id ?两步放在同一个事务里保证一致性。二是在建表时就把外键行为指定为ON DELETE CASCADE这样删学生时数据库自动删关联成绩。课设演示时我推荐方案一因为能展示你对业务逻辑和事务的理解实际图省事的话方案二也没问题。方案一要注意的是必须先删子表再删主表顺序反了一样报外键错误。6. 部署与进阶打包、索引优化和演示检查清单6.1 按环境拆配置避免演示时改来改去开发环境连本地 MySQL演示环境可能要连另一台机器上的数据库最省心的方式是拆两个配置文件。application-dev.yml放本地数据库连接application-prod.yml放演示环境的连接主配置里只留公共项。启动时指定环境java -jar score-system.jar --spring.profiles.activeprod打包的命令也固定下来mvn clean package -DskipTests-DskipTests跳过测试课设项目往往没有完整的自动化测试跑测试反而可能因为环境问题打包失败。打包完的 jar 在target目录下直接java -jar就能起服务这个流程我在演示前会完整走一遍确保 jar 能独立跑起来而不是依赖 IDE。6.2 给统计报表补索引成绩表的数据量在演示阶段不大但在答辩时如果老师问数据量大怎么办可以主动说明已经做了索引优化。高频查询是成绩列表和统计报表对应的student_id和course_id是最常出现在 WHERE 和 JOIN 条件里的列ALTER TABLE score ADD INDEX idx_student_id (student_id), ADD INDEX idx_course_id (course_id);用EXPLAIN验证索引生效EXPLAIN SELECT * FROM score WHERE student_id 1;结果里type从ALL变成refkey列显示用的是idx_student_id说明全表扫描已经变成索引查找。这个操作在答辩时顺手演示比空口说系统性能好有说服力得多。6.3 演示检查清单和高频追问演示最怕的是现场翻车所以我每次做课设演示前都强制自己走一遍固定的检查流程清空成绩表保证统计数据是从零开始计算的用管理员账号重新登录录入一名学生和两门课程的成绩回到查询页确认记录出现再打开统计报表确认平均分和及格率正确切换一个学生账号访问录入接口确认返回 401这个流程走完系统的主要功能都覆盖了。演示时主动点一个重复录入的异常场景页面提示该学生此课程已录入过成绩反而更能体现系统的完整性。答辩追问里出现频率最高的三个问题是为什么用 JWT、及格率怎么算的、唯一索引有什么用。JWT 的回答重点是无状态、不需要 Session及格率的重点是CASE WHEN加SUM的聚合逻辑唯一索引的重点是数据库层防止重复数据。这三个问题都是这套系统的设计核心理解了就能讲顺。从那以后我每次演示前都会强制在干净的数据库上走一遍清库—录入—回查—统计的完整流程绕开所有不确定性。希望帮到你这份基于 SpringBoot 和 MySQL 的成绩管理系统源码可以下载下来跑一遍替换成自己的数据就是一份完整的课设作品。本文还有配套的精品资源点击获取