ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java学生成绩管理系统进阶版:Spring Boot+MyBatis完整设计与实现

Java学生成绩管理系统进阶版:Spring Boot+MyBatis完整设计与实现

简介:学生成绩管理系统进阶版是采用 Java 语言开发的全套源代码项目,集成 Layui 前端框架与 Freemarker 模板引擎,后端使用 SSM(Spring、SpringMVC、MyBatis)架构并基于 MySQL 存储数据,非常适合正在学习 Java Web 分层开发或需要完成课程设计、毕业设计的开发者。压缩包共含 524 个文件,整体约 3.82MB,其中包含 57 个 Java 源文件、109 个 XML 配置与映射文件、62 个 JavaScript 文件、40 个 HTML 页面、14 个样式表文件,以及 150 个 GIF 动态演示截图,另有数据库 SQL 脚本和项目说明文档,便于直接导入运行和对照学习。目前已有 871 人浏览学习,适合作为快速掌握 Spring 系列框架整合实战的入门到进阶资料。源码覆盖学生、教师、课程、成绩等核心模块的增删改查与权限控制,包含控制器、服务层、工具类与 MyBatis 映射等完整代码,可帮助读者理解前后端数据交互、模板渲染和常见业务逻辑实现,是提升 Java 项目开发能力的优质参考。

1. 学生成绩管理系统进阶版 Java实现全部源代码:这门作业到底在考什么

搜「学生成绩管理系统进阶版 Java实现全部源代码」的人,十有八九是课程设计或毕业设计做到一半,被导师一句「太基础,深度不够」卡住了。基础版无非是增删改查、一张成绩表和一个swing界面,进阶版要过的坎其实是清晰的:登录鉴权、教师/学生/管理员三种角色、成绩的统计分析、数据导入导出,以及代码本身是否像个工程而不是一坨能跑的函数。这篇笔记我会按自己实际做过的一个方案,从技术栈选型、数据库设计、核心代码到踩坑一条条拆给你,目标是让你拿到的不是一段「能运行」的代码,而是一套能答辩、能扩展、能讲清楚为什么这么写的完整工程。

2. 把技术栈定下来:为什么选 Spring Boot + MyBatis 而不是 JSP 直写

2.1 两种技术路线的取舍:传统 JSP/Servlet 与 Spring Boot 的边界

很多教程喜欢把学生成绩管理系统做成 JSP + Servlet + JDBC 的三层结构,理由是「简单、好答辩、老师看得懂」。我承认这条路在十年前是对的,但放到现在有一个现实问题:课程设计要求的是进阶版,评委问到你「怎么解决并发更新成绩」「怎么做权限控制」的时候,JSP直写模式很难圆场,因为你所有的代码都在互相 new,连一个依赖注入都没有。

我一般会这样判断:如果你的运行环境是学校机房、必须用 JDK8 且不允许联网下依赖,那 JSP + Servlet 是唯一稳定的选择;如果允许使用 Maven 和现代 IDE,Spring Boot + MyBatis 才是「进阶版」真正值得投入的方案。Spring Boot 的价值不在框架本身,而是内置 Tomcat、自动配置和 starter 机制,让「完整可运行」的交付成本降到最低——你不用再手工配置 web.xml、DispatcherServlet、数据源那一大堆样板文件。而 MyBatis 相比 Spring Data JPA 的优势是 SQL 完全可控,成绩统计里那些 AVG、CASE WHEN、GROUP BY 写起来更直接,不用去看 Hibernate 自动生成的奇怪 SQL。

另一个容易被忽视的点是学习曲线。进阶版要的「进阶」更多体现在你对业务逻辑的把握,而不是对 ORM 魔法的信仰。MyBatis 让你每一条 SQL 都白纸黑字写在 XML 里,出了问题能快速定位,不会被一层一层代理对象套住。

2.2 用 Maven 搭出可运行的工程骨架:最小依赖清单与目录结构

我习惯先搭好骨架再写业务,这样后面每加一个功能都有固定位置。第一步是建一个标准 Maven 工程,pom.xml里只需要放五类依赖:web、mybatis、mysql驱动、lombok、poi(Excel导入导出备用)。阿里的 druid 连接池我一般不急着加,HikariCP 是 Spring Boot 默认配好的,小项目够用。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 成绩导出Excel时使用 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency> </dependencies>

这里有两个参数值得说明。Spring Boot 2.7.18 是 2.x 系列的最后一个版本,兼容性最稳,javax.servlet 命名空间,网上大部分教程和插件都是按它写的;如果你用 Spring Boot 3,很多类名要从javax改成jakarta,拦截器配置也会变,容易把新手绕晕。MySQL 驱动用的是mysql-connector-j,这是 MySQL 官方从 8.0.31 之后的新坐标,老坐标mysql-connector-java已经停止维护,这里别照抄旧教程。

工程目录我按下面这张表来组织,每一层职责单一,答辩时解释起来也顺畅:

包路径职责关键类
config拦截器、跨域、全局异常LoginInterceptor、WebConfig
controller接收请求、参数校验、返回统一结果StudentController、ScoreController
service业务逻辑,事务边界在这里ScoreService、StatsService
mapperMyBatis 数据访问接口ScoreMapper、StudentMapper
entity数据库实体Student、Course、Score
common统一返回体、分页结果、状态码Result<T>、PageResult<T>

这种分包方式最大的好处是:评审老师问「事务放在哪一层」的时候,你可以直接回答 service 层并用代码指给他看;问「SQL 在哪」的时候,打开resources/mapper/*.xml即可,不用在整个项目里翻。

3. 数据库建模与初始化:五张表解决成绩的“一对多”核心关系

3.1 表结构与外键设计:用户表撑起权限,成绩表撑起统计

成绩管理系统看起来只有成绩一个实体,但进阶版要从角色出发,没有老师谁录成绩?没有学生成绩挂在谁名下?没有用户表怎么做登录鉴权?所以我一般固定用五张表:用户表t_user、学生表t_student、教师表t_teacher、课程表t_course、成绩表t_score。

用户表管登录,role字段用字符串而不是数字,虽然占用空间大一点,但阅读性极好,ROLE_ADMIN、ROLE_TEACHER、ROLE_STUDENT一眼能看出语义。学生表和教师表分别存业务属性,比如学生有学号、班级、入学年份,教师有工号、职称,它们通过user_id和用户表关联,而不是直接往用户表里堆字段。这样设计的好处是:用户表只关心认证,业务表只关心领域数据,后期加一个「家长」角色,不需要动学生表。

成绩表是整个系统的心脏,它不能只有student_id、course_id、score三个字段。进阶版必须考虑一次考试的不同阶段——平时分、期中、期末——以及不同学期。我习惯用一个exam_type字段区分,配合semester字段(如2024-2025-1),这样一条记录就能精确表示「张三在2024-2025学年第一学期《Java程序设计》的期末成绩」。最关键的是给这四列加上联合唯一索引,杜绝同一学生在同一学期同一门课同一考试类型出现两条记录。

CREATE TABLE t_score ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, exam_type VARCHAR(16) NOT NULL COMMENT 'EXAM_TYPE: REGULAR/MIDTERM/FINAL', score DECIMAL(5,1) NOT NULL COMMENT '成绩,0-100,保留一位小数', semester VARCHAR(16) NOT NULL COMMENT '学期,如2024-2025-1', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course_rtype_semester (student_id, course_id, exam_type, semester), KEY idx_course_semester (course_id, semester) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';

这里我刻意用了DECIMAL(5,1)而不是FLOAT。先说结论:成绩计算里所有平均分、及格率都依赖这个字段,浮点类型在反复聚合时会产生 0.00000001 级的误差,虽然不影响及格判定,但打印报表时很难看。定点数不会,DECIMAL(5,1)表示最大 999.9,成绩扩大到百分制之外也够用。联合唯一索引uk_stu_course_rtype_semester是防止按钮被双击导致重复提交的最后一道闸门,业务层和数据库层双重保险,血泪经验告诉我,这是最值得加的一行约束。

3.2 初始化脚本:用 schema.sql 管住表结构,而不是 MyBatis-Plus 自动建表

有些同学喜欢用 MyBatis-Plus 的ddl-auto: update让框架根据实体类自动建表。我强烈反对在学生成绩管理系统里这么干,因为自动建表生成的字段注释是英文的、类型可能不符合预期,而且一旦实体类改错,线上表结构会被悄悄改掉。对于这种交付型课程设计,我宁可写一个完整的schema.sql,在每个字段上写清楚中文注释,建表脚本本身就是答辩时的设计文档。

CREATE DATABASE IF NOT EXISTS grade_db DEFAULT CHARSET utf8mb4; USE grade_db; -- 用户表:登录认证与角色区分 CREATE TABLE IF NOT EXISTS t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL COMMENT '存入BCrypt密文,不是明文', role VARCHAR(16) NOT NULL DEFAULT 'STUDENT' COMMENT 'ADMIN/TEACHER/STUDENT', enabled TINYINT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 初始化数据:密码均为 admin123 的BCrypt密文 INSERT INTO t_user (username, password, role) VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsS8rcE8uW5oW4qVhQ5yO', 'ADMIN'), ('teacher01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsS8rcE8uW5oW4qVhQ5yO', 'TEACHER'), ('student01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsS8rcE8uW5oW4qVhQ5yO', 'STUDENT');

注意这里的密文我故意写成了一个固定的 BCrypt 值,对应明文admin123。如果你要换成自己的密码,不要直接用在线网站生成——BCrypt 每次生成的 salt 不同,但都可以用PasswordEncoder.matches(rawPassword, encodedPassword)验证。enabled字段是给管理员用的,用来停用毕业学生或离职教师的账号,这个细节在进阶版里很加分。

鉴于这是一篇能照着做的教程,我还顺手把t_student、t_teacher、t_course三张表的建表语句一并给你放在脚本里。学生表和课程表之间不需要选课表,因为成绩表本身就表达了「某个学生某门课考了多少分」,选课关系可以由成绩表反推出来,少一张表少一份维护成本。

4. 把核心代码写出来:登录鉴权、成绩录入、统计报表三个主流程

4.1 用拦截器做登录校验,把 Controller 里的重复代码解放出来

进阶版最明显的特征是「不同的角色看到不同的东西」。学生登录后只能查自己的成绩,教师能录成绩、改成绩,管理员能维护班级和课程信息。如果每个 Controller 里都写一段if (session.getAttribute("user") == null) throw new RuntimeException("未登录"),代码就散落得到处都是。正确做法是写一个拦截器,在进入 Controller 之前统一校验登录态和角色权限。

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String uri = request.getRequestURI(); if (uri.startsWith("/api/auth/") || uri.startsWith("/static/")) { return true; } HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或会话已过期\"}"); return false; } // 角色校验:检查当前用户是否被允许访问该资源 String role = (String) session.getAttribute("role"); String method = request.getMethod(); String path = uri.replace("/api/", ""); // 简单规则:POST/DELETE 写入操作仅 ADMIN/TEACHER 可用 if (("POST".equals(method) || "DELETE".equals(method)) && !"ADMIN".equals(role) && !"TEACHER".equals(role)) { response.setStatus(403); response.getWriter().write("{\"code\":403,\"msg\":\"当前角色无权操作\"}"); return false; } return true; } }

这个拦截器有两个业务要点。第一,OPTIONS必须放行,否则前端用 axios 跨域调用时会先发一个预检请求,被拦截下来就说「CORS 验证失败」,这是新手最容易撞上的问题。第二,我在拦截器里做了粗粒度的写操作限制,学生只能 GET,教师和管理员能 POST/DELETE,细粒度的「教师只能改自己课程的成绩」留在 Service 层做,拦截器管不了那么细,硬管反而会把业务逻辑拆散。

逻辑说明:preHandle返回true表示放行,false表示拦截,在拦截返回时我直接输出 JSON 而不是跳转页面,因为现在的前后端分离项目前端拿到 401 状态码就自动跳到登录页,不需要后端管页面跳转。

参数说明:uri.startsWith("/api/auth/")这行是把登录接口本身排除在外,不然登录也要求先登录,死循环了。课程设计里如果加了注册功能,同样要把注册接口放进白名单。

4.2 成绩聚合查询:一条 SQL 算出平均分、及格率、最高最低分

成绩列表谁都会写,SELECT * FROM t_score WHERE ...,这不算进阶。进阶体现在统计页面上:按班级、按课程、按学期展示平均分、及格率、最高分、最低分。这个 SQL 写起来稍复杂,但逻辑清晰,值得单独拆出来讲。

SELECT c.id AS course_id, c.course_name, COUNT(s.id) AS total_students, ROUND(AVG(s.score), 1) AS avg_score, ROUND(MAX(s.score), 1) AS max_score, ROUND(MIN(s.score), 1) AS min_score, ROUND( SUM(CASE WHEN s.score >= 60 THEN 1 ELSE 0 END) / COUNT(s.id) * 100, 1 ) AS pass_rate FROM t_score s JOIN t_course c ON s.course_id = c.id WHERE s.exam_type = 'FINAL' AND s.semester = #{semester} GROUP BY c.id, c.course_name ORDER BY avg_score DESC;

这条 SQL 值得讲透。SUM(CASE WHEN ... THEN 1 ELSE 0 END)是统计及格人数的惯用写法,它比先查总人数再查及格人数两次查询要高效得多,一次扫表同时算出四个指标。COUNT(s.id)统计的是成绩记录数,不是学生数——如果同一门课有缺考、缓考的NULL记录,COUNT(*)会把它们也算进去,而COUNT(s.id)会跳过NULL,成绩表里缺考一般存NULL,所以用COUNT(s.id)更安全。ROUND(..., 1)让报表保留一位小数,答辩时直接可以截图当结果展示。

配套的 Mapper 接口声明很简单,注意参数要用@Param注解绑定,否则 XML 里#{semester}会报「Parameter 'semester' not found」:

public interface StatsMapper { List<Map<String, Object>> getCourseStats(@Param("semester") String semester, @Param("examType") String examType); }

返回类型我用List<Map<String, Object>>而不是定义一个 VO 类,因为统计报表的字段以后很可能要加(比如优秀率、各分数段人数),用 Map 灵活性最高,少写一个类。但要注意 Map 里的 key 是 SQL 列的别名,前端用row.avg_score读取时大小写必须对齐,MySQL 默认返回大写列名的情况需留意,稳妥做法是给列起一个全小写别名,就像上面 SQL 里我已经做的那样。

4.3 成绩导入导出的落地:POI 操作 Excel 的二十分钟实现

进阶版让评委眼前一亮的常见加分组是 Excel 导入导出。教师端批量录入成绩,从 Excel 模板里粘贴几十行,比在网页上一行一行手输快得多。我用的是 Apache POI,下面是导出学期末成绩单的简化版实现,核心概念只有四个:Workbook(整个Excel文件)、Sheet(工作簿)、Row(行)、Cell(单元格)。

public void exportCourseScore(HttpServletResponse response, Long courseId, String semester) throws IOException { List<ScoreVO> list = scoreMapper.selectByCourse(courseId, semester); try (Workbook wb = new XSSFWorkbook()) { Sheet sheet = wb.createSheet("期末成绩"); // 表头 String[] headers = {"学号", "姓名", "班级", "课程", "成绩"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 for (int i = 0; i < list.size(); i++) { ScoreVO vo = list.get(i); Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(vo.getStudentNo()); row.createCell(1).setCellValue(vo.getStudentName()); row.createCell(2).setCellValue(vo.getClassName()); row.createCell(3).setCellValue(vo.getCourseName()); row.createCell(4).setCellValue(vo.getScore().doubleValue()); } // 设置文件下载响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=score_" + courseId + ".xlsx"); wb.write(response.getOutputStream()); } }

这段代码里藏着两个容易踩的坑,先说文件类型。XSSFWorkbook对应.xlsx(Excel 2007+),HSSFWorkbook对应.xls(老格式)。.xls最大行数是 65536,成绩表不至于超,但文件体积更大、写入更慢,现在统一用xlsx就好。Response 的Content-Type必须写成application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,写成text/csv或用错了 MIME 类型,浏览器下载后可能变成乱码。try-with-resources保证 workbook 在写出后自动关闭,避免文件被占用——这是个只有被 Windows 弹过「文件正在使用」警告才懂得痛的细节。

逻辑说明:这里doubleValue()是因为 POI 写入数值单元格时用double,从DECIMAL字段读出来的BigDecimal要先转换再写入,否则空指针或者精度丢失。如果成绩里有缺考的空值,写代码时要在循环里判一下空再转换。

5. 学生成绩管理系统踩坑实录:五个让新手翻车的高频问题

5.1 坑一:控制台一脸懵,MySQL 数据全变乱码“???”

现象:插入的中文在数据库里显示成???,或者网页端读出来是乱码,前端看到「张??」这种半残废数据。
原因:三处字符集不一致。MySQL 库用的是latin1(老版本初始化时常这样),JDBC 连接串没指定characterEncoding=utf8,HTML 页面声明的是gb2312,三条链路任何一个断掉都会出乱码。我排查这类问题的顺序是:先看数据库建表语句里的DEFAULT CHARSET,再看连接串,最后看前端请求头。
解决:建库建表一律显式写DEFAULT CHARSET=utf8mb4(它兼容 UTF-8 并能存 emoji,成绩表里的表情符号虽然用不上,但这是统一标准);JDBC URL 末尾加useUnicode=true&characterEncoding=UTF-8两个参数;如果是从 Excel 导入的中文乱码,大概率是导入时用了InputStream的默认编码,改用FileInputStream配合 POI 读,POI 内部按 Unicode 解析,不碰平台编码。

5.2 坑二:成绩录重复了却不知道,统计出来数字翻倍

现象:同一学生同一门课期末成绩,录入了两份,平均分被算成两份,学生投诉分数不对。
原因:Controller 层处理了「新增」和「修改」两种请求,前端按钮点击两次发起两次 POST,后端没有做唯一性校验。我当时就是少看了t_score的联合唯一索引,以为业务层判空就够了。
解决:双向保险。数据库层面保留 3.1 节那个UNIQUE KEY uk_stu_course_rtype_semester,这是最后一道防线,违反唯一约束时 MyBatis 会抛DuplicateKeyException。应用层面在 Service 里先查一遍同条件记录再决定 insert 还是 update。不要只依赖应用层——并发场景下两条线程同时查到「不存在」,就会双双 insert,唯一索引兜底才能在数据库层面拒绝。

5.3 坑三:平均分精确到 59.9999999,及格率算出来 120%

现象:统计报表里平均分显示78.999999999,及格率超过 100%。
原因:成绩字段误用了FLOAT,聚合时触发浮点误差累积;及格率的 SQL 写法错误,比如SUM(CASE WHEN ...)里的条件不是按>=60而是按成绩不为空判断,或者分母用了COUNT(*)把缺考NULL记录也算进去。
解决:一张表设计就提前预防——把score列定成DECIMAL(5,1),从根上避免浮点误差。SQL 里及格率的分子分母都锁定为COUNT(s.id),因为s.id是主键不会为 NULL,用COUNT(s.id)就等于有效成绩数。写完后我用一条已知数据验算一遍:全班 10 人,8 人及格,及格率显示 80.0%,小于 100%,这一步一定要做。

5.4 坑四:Tomcat 8080 端口被占用,Spring Boot 启动直接失败

现象:满心期待点 Run,控制台报Port 8080 was already in use,然后APPLICATION FAILED TO START。
原因:本机可能装了另一个 Tomcat,或者之前有一次未正常关闭的开发进程还占着端口。这个报错信息实际上已经把解决办法写在里面了,但很多新手看到红字就慌。
解决:两步走。先找到占用进程——Windows 用netstat -ano | findstr 8080,Linux 用lsof -i:8080,拿到 PID 后直接结束进程。不想动其他进程,则改端口,在application.yml里加一行server.port: 8081,改完重启。这里额外提一句:端口号不要选 80 和 443,这两个在部分校园网环境会被禁,选 8081/8888/9000 这类高位端口最省事。

5.5 坑五:同一条成绩修改接口,运气好时能改,运气差时报「事务不回滚」

现象:Service 方法里做了「更新成绩 + 写一条操作日志」两个操作,更新成功了但日志写入失败,结果成绩被改了,日志没记录,数据对不上;或者反过来说日志写了但成绩没更新。
原因:@Transactional注解没生效。常见原因有三个:方法被同类内部调用(this.updateScore(),代理没介入)、方法不是public、类没被 Spring 管理。这里面最隐蔽的是「同类内部调用」,Spring 的事务是基于 AOP 代理的,内部调用走的是this而不是代理对象,注解直接失效。
解决:把事务方法放到不同的 Service 类里,由 Controller 调用外部 Service 的方法;或者自己注入TransactionTemplate,用编程式事务包住那段逻辑。我在项目里一般选前者,因为声明式事务可读性最好。另一个实战习惯:事务方法里只做数据操作,不要做耗时的文件 IO——日志文件同样写在事务里的话,磁盘满了会导致整个事务回滚,成绩也白改了。

6. 跑通之后这样自我验证:一份冒烟脚本加三个扩展方向

我每写完一个模块都会用一条最简单的命令把关键接口打一遍,这条命令在答辩前也一定能帮上大忙。假设项目已经启动在 8081 端口,下面这组 curl 就是成绩系统的「体检报告」:

# 1. 登录拿会话 curl -X POST http://localhost:8081/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"teacher01","password":"admin123"}' \ -c cookies.txt # 2. 带会话查询某课程成绩 curl http://localhost:8081/api/score?courseId=1&semester=2024-2025-1 \ -b cookies.txt # 3. 带会话查询统计报表 curl http://localhost:8081/api/stats?semester=2024-2025-1&examType=FINAL \ -b cookies.txt | python3 -m json.tool

依次执行后,看三个结果:登录接口返回的 JSON 里code是否为 200;成绩列表是否有数据;统计报表的平均分和及格率是不是和你预期数据吻合。如果登录后没有带cookies.txt直接访问成绩接口返回 401,说明拦截器生效了,这是正确行为,不是 bug。

做完这些基础验证,我再给你三个「进阶版再进阶」的扩展方向,按投入产出比排序。第一是给成绩趋势加折线图,前端用 ECharts 拉取统计接口的数据,展示某学生历次考试成绩曲线,后端无需大改;第二是把「导出 Excel」升级成「导出带汇总行的 Excel」,在文件末尾追加一行平均分、及格率,这比单纯导出明细更像教务系统;第三是引入操作日志表,记录谁在什么时间改了哪个学生的成绩,这个方向的价值在于「数据可追溯」,也是毕业答辩时最有话可聊的点。

最后说一个我自己的经验教训:千万别在答辩前两天才开始写系统的部署说明。写完这个项目后,顺手在你的工程目录里加一个README.md,写清楚 JDK 版本、MySQL 版本、初始化脚本的位置、默认账号密码,这个文件对答辩评委来说比任何代码注释都有说服力——它代表你能交付、能维护、能交接,软件工程考试到这里才算真正及格。希望帮到你。

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

返回列表