简介:基于Java的Web学生成绩管理系统压缩包是一份完整的课程设计/毕业设计参考项目,面向Java Web初学者及有成绩管理需求的教育机构开发者。项目采用Servlet+JSP+JDBC技术栈,遵循MVC分层思想,覆盖学生信息维护、成绩录入修改、条件查询、成绩排序统计等典型业务场景,可帮助快速上手Java Web开发。包内共36个文件,包含10个Java源文件、9个JSP页面、10张运行截图与3张PNG示意图,另附XML配置、MF清单和README说明,压缩包仅2.94MB,结构清晰便于按代码、页面、文档分类阅读。配套截图展示了数据表设计、系统首页、查询结果、成绩筛选与统计等核心页面,便于对照代码理解前后端交互及JDBC持久化过程。目前已有33人学习浏览,适合作为项目实战蓝本,支持在Tomcat与MySQL环境下部署运行,并能根据实际需求扩展统计分析、权限控制等功能。
1. 基于Java的Web学生成绩管理系统.zip:课程设计里的常青树,能跑通才算数
做 Java 课程设计或毕业设计的同学,大概率绕不开「学生成绩管理系统」这个题目。网上流传的基于Java的Web学生成绩管理系统.zip就是这类项目最常见的存在形态:一个压缩包,里面有 Eclipse 或 IDEA 工程、SQL 脚本、部署说明,解压后理论上能跑出一个带登录、带成绩录入和查询的 Web 应用。这类系统看起来不难,但真正动手解压、导入、改配置、启动 Tomcat 之后,翻车率远比你想象的高——版本不匹配、JDK 和 Tomcat 位数对不上、数据库脚本编码错乱,每一步都可能卡住。这篇笔记的目标很直接:把这个压缩包里的东西讲透,从架构拆解到运行调试,让你拿到手之后两小时内跑通,并且知道怎么改成面试能讲的项目。
2. 拆解系统的三层骨架:角色权限、成绩流转、数据访问怎么协同
2.1 用户模型:管理员、教师、学生三条权限线
几乎所有学生成绩管理系统的业务原点都是「谁能用、能用什么」。典型的角色分三种:管理员管基础数据(学生信息、课程信息、教师账号),教师负责录入和修改成绩,学生只能查询自己的成绩和排名。这个权限模型直接决定了数据库用户表的设计和前端页面的跳转逻辑。
我见过很多课程设计在这块做得敷衍——一个 user 表存个 role 字段,登录后靠 JSP 里的 if 判断跳转,权限校验全靠页面超链接藏起来,直接访问 URL 就能越权。如果你的系统后面要写进简历,权限这块值得认真补一下。常见的做法是用 Filter 做一层登录拦截,再根据 role 判断功能菜单的可见性,数据库层面保持简单:
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL COMMENT '1-管理员, 2-教师, 3-学生', real_name VARCHAR(30), status TINYINT DEFAULT 1 COMMENT '1-启用, 0-禁用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;角色字段用 TINYINT 而不是字符串,查询快,业务里用常量类映射就行。实际在 Java 代码里,我习惯登录时把 user 对象塞进 session,再写一个 BaseServlet 或者拦截器统一判断:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 按角色控制访问范围:教师不能访问学生管理模块 User user = (User) loginUser; String uri = request.getRequestURI(); if (uri.contains("/student/manage") && user.getRole() != 1) { response.sendError(403); return; } chain.doFilter(req, resp); }这里有个容易忽略的点:角色判断不要只在前端隐藏按钮,后端 Filter 必须拦截,否则手工拼 URL 就能进管理页。很多老项目就是这么被攻破的,面试时如果被问到 Web 安全,能主动说出「后端权限校验」这个点会加分不少。
2.2 成绩数据流:从教师录入到学生查询的完整链路
成绩是这个系统的核心实体,它的流转链路是这样的:教师登录后选择课程和班级,看到一个学生名单列表,逐个录入分数,提交后写入成绩表;学生登录后按学期、课程查询自己的成绩,并看到总分、平均分和排名。管理员不直接改分,只负责处理「补考」「缓考」这类状态字段。
这条链路里最容易出问题的不是界面,而是事务。教师录入一个班 40 人的成绩,如果循环执行 40 条 insert,中途第 25 条数据格式错了,前面 24 条已经写进去了,整个录入操作处于半完成状态,学生端看到的成绩就是残缺的。所以成绩表的写入必须走事务,要么全部成功,要么全部回滚。
public void saveScoreBatch(List<ScoreInput> list) { Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); String sql = "INSERT INTO t_score(student_id, course_id, term, score, remark) " + "VALUES(?,?,?,?,?) " + "ON DUPLICATE KEY UPDATE score=VALUES(score), remark=VALUES(remark)"; PreparedStatement ps = conn.prepareStatement(sql); for (ScoreInput input : list) { ps.setInt(1, input.getStudentId()); ps.setInt(2, input.getCourseId()); ps.setString(3, input.getTerm()); ps.setBigDecimal(4, input.getScore()); ps.setString(5, input.getRemark()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("成绩批量保存失败", e); } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码里有几个关键点。setAutoCommit(false)之后必须手动 commit,任何一条 insert 失败都会走进 catch 块回滚。ON DUPLICATE KEY UPDATE是 MySQL 的幂等写法,重复提交同一份成绩时直接更新而不是报主键冲突,这正好对应「Java 怎么保证数据一致性」这类面试题里的常见答案——数据库层面做唯一约束,应用层面做事务控制。成绩表上建议建唯一索引(student_id, course_id, term),这是防止重复录入最有效的手段,比在 Java 代码里判断再 insert 靠谱得多。
2.3 数据访问选型:JDBC、DBUtils、MyBatis 改造成本对比
老课程设计项目最常见的数据访问层写法是原生 JDBC 或者 Apache DBUtils,新一些的用 MyBatis。解压 ZIP 后先看 lib 目录和代码里的 import,基本就能判断属于哪一类。这三种选型的改造成本差别很大,选型思路值得先说清楚。
原生 JDBC 的优点是零依赖、部署简单,缺点是样板代码多——每个 DAO 方法都要写获取连接、PreparedStatement、ResultSet 遍历、关闭资源这一套。DBUtils 是轻量封装,用 QueryRunner 省掉了大部分样板代码,还保留了 SQL 的完全控制力,在课程设计这种规模下其实最顺手。MyBatis 则是把 SQL 和 Java 代码分离,适合表结构复杂、查询条件多的场景,但引入配置文件、Mapper 映射,对新手来说排查问题路径更长。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.7</version> </dependency>如果拿到的是 MyBatis 版本,最常见的坑是 Mapper XML 里resultType写错导致查询结果全是 null。排查技巧是把 MyBatis 的 SQL 日志打开:在mybatis-config.xml里加<setting name="logImpl" value="STDOUT_LOGGING"/>,控制台就会打印实际执行的 SQL 和参数,这时候你就能判断是 SQL 本身错了还是结果映射错了。
3. 把 ZIP 变成能用的系统:环境对齐、建库导表、启动调参
3.1 版本对齐:JDK、Tomcat、MySQL 三件套怎么配不出错
课程设计项目的压缩包年代感往往很强,很多是照着 JDK 8 + Tomcat 8 + MySQL 5.7 那一代写的。如果你机器上装的是 JDK 17 + Tomcat 10 + MySQL 8.0,直接跑大概率起不来。版本不对齐的表现五花八门:Tomcat 启动报UnsupportedClassVersionError,说明 class 文件编译版本高于当前 JDK;报ClassNotFoundException通常是 jar 包没拷贝到 WEB-INF/lib;MySQL 连不上则多半是驱动版本和数据库版本不兼容。
我建议按这个组合去对齐,踩坑最少:
| 组件 | 推荐版本 | 注意点 |
|---|---|---|
| JDK | 1.8 | 不要装 JRE,编译需要 javac |
| Tomcat | 8.5.x | 支持 Servlet 3.1,兼容大多数老项目 |
| MySQL | 5.7 | 和 mysql-connector-java 5.x 驱动匹配 |
| IDE | Eclipse 或 IDEA | 确保项目编码设为 UTF-8 |
如果系统装了更高的版本,别急着卸载。可以在 IDEA 里为这个项目单独指定 JDK:Project Structure -> SDKs -> Add JDK,把本机的 JDK 8 路径加进去,再在Modules里把模块的 language level 设为 8。Tomcat 也可以在 IDEA 的 Run Configuration 里指定独立的 Tomcat 目录,避免影响你机器上其他服务。
3.2 导入数据库脚本:五张核心表的结构与外键关系
打开压缩包里的 SQL 文件(一般叫db.sql或init.sql),通常包含五张核心表:用户表、学生表、教师表、课程表、成绩表。学生表和用户表在有些设计里是一张表,通过 role 区分,有些则是分开的,成绩表通过 student_id 关联学生,通过 course_id 关联课程。
导入前先建库,字符集直接指定 utf8mb4,避免后续中文乱码:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS scoresystem DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p scoresystem < db.sql注意 SQL 文件里如果有CREATE DATABASE,那第二种写法里库名要一致,否则会导到别的库去。导入成功后用SHOW TABLES;确认五张表都在,再抽查几条数据看中文是否正常。
SELECT c.course_name, ROUND(AVG(s.score), 2) AS avg_score, COUNT(*) AS total, SUM(CASE WHEN s.score < 60 THEN 1 ELSE 0 END) AS fail_count FROM t_score s JOIN t_course c ON s.course_id = c.id GROUP BY s.course_id;这条统计 SQL 是验证数据完整性的好工具 —— 如果 avg_score 是 null 或者 fail_count 明显不对,说明成绩表里还有脏数据或者关联键有问题。
3.3 三个必改配置:连接串、端口、字符集
把项目导入 IDE 后,第一件事就是改配置文件。最常见的三个配置项集中在db.properties或jdbc.properties里:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/scoresystem?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456连接串里的characterEncoding=UTF-8必须保留,否则 JSP 页面传入的中文会乱码。serverTimezone=Asia/Shanghai是 MySQL 8.0 之前的驱动连 MySQL 5.7 时经常报时区错误的解药。useSSL=false则是为了本地调试省去证书警告。密码改成你自己数据库的密码,别照抄。Tomcat 端口在server.xml里改,如果 8080 被占了,把三个端口(<Connector port="8080"和后面两个 8009、8005)一起改掉,否则只改 HTTP 端口,Tomcat 启动时仍可能因为其他端口占用而报错。
字符集还有一个隐藏配置点:IDEA 中项目文件的编码。如果File -> Settings -> File Encodings里 Global Encoding 和 Project Encoding 不一致,JSP 文件里的中文在编译后就是乱码。统一设成 UTF-8,并在conf/catalina.properties里加一行org.apache.catalina.connector.URIEncoding=UTF-8(老版本 Tomcat 可能需要,8.5 默认已经是 UTF-8)。
4. 核心业务落地:成绩录入、查询、统计的代码实现
4.1 成绩录入接口:分数校验与重复提交处理
成绩录入是系统里写逻辑最密集的地方。分数不是随便填的,0 到 100 之间是正常分数,超过 100 或负数要直接拦截;补考成绩、缓考标记要通过 remark 字段记录。我见过有项目把成绩设计成字符串存「优秀/良好/及格」,统计的时候再转数字,纯粹给自己找麻烦,数字成绩加一个文本备注字段才是合理的结构。
前端表单提交过来的参数,后端必须二次校验,不能信任前端:
public void addScore(HttpServletRequest req, HttpServletResponse resp) { String scoreStr = req.getParameter("score"); if (scoreStr == null || scoreStr.trim().isEmpty()) { writeJson(resp, 400, "分数不能为空"); return; } try { BigDecimal score = new BigDecimal(scoreStr); if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0) { writeJson(resp, 400, "分数必须在0到100之间"); return; } // 通过校验,写入service层 scoreService.saveOne(req); } catch (NumberFormatException e) { writeJson(resp, 400, "分数格式不正确"); } }BigDecimal而不是double,是因为成绩平均分计算涉及精度,double算出来 89.99999999 这种结果在页面显示时很难处理。compareTo比较大小而不是>符号,也是 BigDecimal 的规范用法,直接score > 100在 Java 里编译都过不了。这里校验逻辑写在 Controller,真正落库在 Service 层,层与层之间的参数传递用 DTO 对象,不要用Map到处传,否则项目一复杂就变成一坨。
4.2 多条件查询:按学号、课程、班级拼 SQL 的规范姿势
成绩查询页一般是三到四个筛选条件:学号、学生姓名、课程、班级。老项目里最常见的写法是:
String sql = "SELECT * FROM t_score WHERE 1=1"; if (studentId != null) { sql += " AND student_id = " + studentId; } if (courseId != null) { sql += " AND course_id = " + courseId; }这个写法隐患很大。第一,参数直接拼进 SQL,存在注入风险,学号传个"1 OR 1=1"就能把整个表拖出来;第二,WHERE 1=1虽然能让后续拼接省心,但性能上对索引不友好。规范做法是用 PreparedStatement 的占位符动态拼 SQL:
StringBuilder sql = new StringBuilder("SELECT s.student_no, s.student_name, c.course_name, sc.score, sc.term "); sql.append("FROM t_score sc "); sql.append("JOIN t_student s ON sc.student_id = s.id "); sql.append("JOIN t_course c ON sc.course_id = c.id "); sql.append("WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (studentNo != null && !studentNo.isEmpty()) { sql.append("AND s.student_no LIKE ? "); params.add("%" + studentNo + "%"); } if (courseId != null) { sql.append("AND sc.course_id = ? "); params.add(courseId); } if (term != null && !term.isEmpty()) { sql.append("AND sc.term = ? "); params.add(term); }LIKE 查询的%要拼在参数里,而不是拼在 SQL 里,这是参数化查询的基本规矩。另外,如果t_score表的数据量超过几万条,按课程和学期查询必须走索引,否则每次查询都是全表扫描。建议在成绩表上加复合索引(course_id, term),把 WHERE 条件里最常用的两个字段放前面。Java 面试八股文里常说的「联合索引最左前缀原则」,在这个系统里就能实际感受到——查询条件里有 course_id 但没有 term 时,索引只能用到第一段。
4.3 统计排名:平均分、及格率、分数段分布一条 SQL 算完
课程设计的验收环节,老师通常会在意统计功能——班级平均分、及格率、优秀率、分数段分布。这些指标用一条 SQL 就可以全算出来,不需要在 Java 里循环计算:
SELECT c.course_name, COUNT(*) AS total_students, ROUND(AVG(sc.score), 2) AS avg_score, ROUND(SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS pass_rate, ROUND(SUM(CASE WHEN sc.score >= 90 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS excellent_rate, SUM(CASE WHEN sc.score < 60 THEN 1 ELSE 0 END) AS fail_count FROM t_score sc JOIN t_course c ON sc.course_id = c.id GROUP BY sc.course_id;排名功能则是用窗口函数 RANK 或 DENSE_RANK,MySQL 5.7 不支持窗口函数,5.8(MySQL 8.0 之前的版本号不存在)这里容易记混,MySQL 8.0 才支持。如果你的数据库是 5.7,排名就得用变量实现:
SELECT student_id, course_id, score, @rank := IF(@prev_course = course_id, @rank + 1, 1) AS rank, @prev_course := course_id FROM t_score ORDER BY course_id, score DESC;这个写法的坑在于@rank和@prev_course必须按 course_id 排序后再计算,否则排名会乱掉。而且变量的赋值和查询字段出现在同一条语句里,不同版本 MySQL 的执行顺序有细微差别,建议在 Java 端调好排序后验证一次结果再交付。如果系统跑在 MySQL 8.0 上,直接用ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC)更直观,也不用担心变量作用范围的问题。
5. 老项目翻车实录:ZIP 里跑不起来的原因与解法
5.1 Tomcat 启动报ClassNotFoundException,但 jar 包明明在 WEB-INF/lib 里
现象:启动 Tomcat 时抛出java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,打开 WEB-INF/lib 目录明明能看到 mysql-connector-java.jar。
原因:这个 jar 没有被编译进 classpath。在 Eclipse 老项目里,jar 需要在Deployment Assembly里配置;IDEA 里则是Project Structure -> Artifacts -> WEB-INF/lib里确认 jar 是否被打包进去。很多人的 jar 只是放在文件夹里,IDE 没有把它关联到编译输出。
解决:IDEA 中在 WEB-INF 下新建 lib 目录,把 jar 拖进去,右键 Add as Library;然后在 Artifacts 页面确认 Output Layout 里 lib 目录存在且 jar 在里面。重新 Build -> Rebuild 一次再启动。这个坑在课程设计验收前最容易出现,因为换了一台电脑重新导入项目后,lib 关联经常丢。
5.2 数据库连不上,密码在配置文件里改了三处还是报 Access denied
现象:Access denied for user 'root'@'localhost' (using password: YES),配置文件里密码确认没问题。
原因:同时存在多个配置文件。老项目可能在src下有db.properties,在WEB-INF/classes下有另一份,还有一份在config目录下。代码加载配置的顺序是 classpath 优先,你改的那一份可能根本没被加载。
解决:先看代码里配置文件加载的方式。如果是Properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties")),那就必须改src/main/resources下的那一份。用 IDEA 打开项目后按 Ctrl+Shift+F 全局搜jdbc.password,把所有出现的位置都找出来,确认实际上被读取的是哪一个。还有一种情况是 MySQL 8.0 的密码加密方式变了,驱动 5.x 连不上 8.0,解决办法是换成mysql-connector-java8.x 驱动并把 URL 里的 serverTimezone 参数配上。
5.3 页面中文乱码,浏览器里满屏问号
现象:登录页面标题是中文,用 Tomcat 启动后全是??,部分请求参数里的中文也变成了???。
原因:三层不一致——JSP 文件保存时的编码、Tomcat 解析请求的编码、数据库连接的编码。老项目压缩包里的 JSP 很可能是 GBK 编码保存的,而 Tomcat 默认 UTF-8;数据库表是 latin1 还是 utf8 也要确认。
解决:先确认 JSP 文件头部有<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,然后看文件本身编码——IDEA 右下角能看到文件编码,如果是 GBK 就转成 UTF-8 并选择「转换而不是复制」。数据库连接 URL 里加characterEncoding=UTF-8,MySQL 建表时统一 utf8mb4。修改完成后清浏览器缓存、重启 Tomcat,这一串搞完乱码基本能解决。注意 Filter 里如果写了request.setCharacterEncoding("UTF-8"),必须在第一次 getParameter 之前调用,放在 doFilter 的最前面。
5.4 端口被占,启动日志里 Address already in use
现象:Tomcat 启动到一半报java.net.BindException: Address already in use: JVM_Bind:8080,然后整个启动失败。
原因:本机已经有另一个 Tomcat 实例或者别的进程占了 8080。常见的是上一个调试项目没被 IDEA 停掉,后台还有一个 java 进程活着。
解决:先netstat -ano | findstr 8080看是哪个进程占用的,Windows 下直接taskkill /PID xxx /F。如果确定是本机已有的正式 Tomcat 占用的端口,修改当前项目的配置改用 8081 —— 在 IDEA 的 Run Configuration 里改 HTTP port 为 8081,JMX port 也同步改掉,避免只改一个端口出现新的冲突。这是个容易被忽略的点:Tomcat 要占用三个端口,8080 是 HTTP,8009 是 AJP,8005 是 shutdown,IDEA 里 Run Configuration 只让你改 HTTP port,但server.xml里三个端口都写死了,修改时要一起动。
5.5 数据存进去了,但从页面看到的时间、小数位不对
现象:成绩表里存的是89.5,页面显示89.50或者89.5,跟设计文档对不上;日期字段存进去再取出来,时间偏移了 8 小时。
原因:小数位是BigDecimal的 scale 设置问题,数据库字段 DECIMAL(5,2) 会强制两位小数,但 Java 端setBigDecimal传入的是89.5,JDBC 驱动按数据库精度自动补零或截断。时间偏移 8 小时是 MySQL 驱动连接 URL 没加serverTimezone,默认按服务器本地时区解析,而驱动本身的时区是 UTC。
解决:Java 端统一用BigDecimal.setScale(2, RoundingMode.HALF_UP)再入库,避免数据库隐式转换。时间字段在实体类里用java.time.LocalDateTime,JDBC 4.2 之后原生支持,不要用java.util.Date去接 LocalDateTime 的字段。连接 URL 里 serverTimezone=Asia/Shanghai 加上后重启,再插入一条数据验证时间偏移问题是否消失。
6. 从能跑到能交:打包部署与验收自查清单
课程设计系统验收有两种形式:一种是答辩现场用 IDE 跑起来演示,另一种是要求部署到服务器上通过浏览器访问。后者更考验工程的完整度,因为现场拷贝项目、切换运行环境最容易出幺蛾子。我习惯的做法是打 WAR 包,而不是让人现场开 IDEA 跑——WAR 包丢到 Tomcat 的 webapps 目录,启动后自动解压部署,干净利落。
出 WAR 包的方式是:IDEA 里File -> Project Structure -> Artifacts -> Add -> Web Application: Archive -> Empty,把项目的compile output和WEB-INF/lib下的 jar 都拖进WEB-INF/classes和WEB-INF/lib,然后Build -> Build Artifacts。打包完成后,把 WAR 放到本机 Tomcat 的webapps目录,启动 Tomcat,浏览器访问http://localhost:8080/项目名/,能出登录页就说明部署链路通了。如果项目名带中文,访问路径里要 URL 编码,或者打包前把 Artifact 名字改成英文。
最后做一轮验收自查,按这个顺序检查完,答辩基本不会被问倒:
- 管理员能否创建教师账号、录入学生信息和课程信息,异常数据(重复学号)能否拦截
- 教师登录后能否看到自己负责的课程,录入成绩后刷新页面数据还在,重复提交同一份成绩是更新而不是多了一条
- 学生登录后能否看到自己的全部成绩,排名是否和手动算的一致
- 退出登录后直接访问受保护页面 URL,是否正确跳转回登录页
- 把数据库密码改错一次,系统是否给出友好错误提示而不是堆栈信息
我自己的习惯是把第 5 条当成必测项。很多项目在正常路径下表现完美,一遇到异常输入就崩溃,答辩时老师最喜欢在这种地方试探。把异常处理做成「页面提示 + 日志记录」,而不是一行红字堆栈,整个系统的完成度就上了一个台阶。每次拿到基于Java的Web学生成绩管理系统.zip这类项目,我都先按这套流程跑通再改代码,省下的时间远比折腾环境多,希望帮到你。
本文还有配套的精品资源,点击获取