☰
基于JSP+MySQL的学生成绩管理系统:数据库设计、分页查询与统计排名实战
2026/10/7 14:56:20 网站建设 项目流程

简介:这是一套面向高校计算机相关专业课程设计与毕业设计的Java Web教务系统源码,采用JSP加MySQL的B/S结构开发,基于MyEclipse工具构建,适合正在学习Java Web开发或需要完成课程设计的学生参考。系统按角色划分为学生、教师、教务人员三大模块,涵盖登录退出、成绩查询、在线留言、成绩录入、留言回复以及学生信息增删改等核心功能,能够帮助读者理解教务管理类信息系统的完整业务逻辑与数据库设计思路。资源包共238个文件,约2.92MB,包含27个jsp页面文件、8个js脚本、6个htm页面、2个java源文件与2个class编译文件,另有1个sql数据库脚本、1个docx说明文档及若干图片与配置文件,结构较为完整。目前已有531人学习下载,可作为课程设计选题参考或二次开发的基础模板,便于快速搭建环境并对照实现各模块功能。

1. 从一张成绩单说起:JSP+MySQL 学生成绩综合管理系统到底解决什么问题

期末周,教务老师手里三张 Excel:一张学生名册、一张课程表、一张各科成绩汇总。要把它们合成一份能按班级、按课程、按学号查询的报表,还要算加权平均分、排名、不及格名单。手工做一次两小时,改一个分数就得全部重来。这就是「基于 Java(JSP)+ MySQL 实现(Web)学生成绩综合管理系统」要吃掉的需求——把学生、课程、成绩三张核心表放进关系数据库,用 JSP 做服务端页面渲染,浏览器里完成录入、查询、统计、导出。

它适合谁?一是课程设计、毕业设计阶段需要一套能跑通、能讲清楚数据流的学生;二是中小院校或培训机构的内部教务场景,数据量在几千到几万条,不需要微服务、不需要前后端分离,一台装了 Tomcat 和 MySQL 的机器就能撑住。技术栈看着老,但 JSP + Servlet + JDBC 这条链路把 HTTP 请求、会话、SQL 执行、结果渲染全暴露在你面前,调一遍等于把 Web 开发的基本功过了一遍。下面按「表怎么设计 → 环境怎么搭 → 增删改查怎么落 → 统计怎么算 → 坑在哪」的顺序讲透,每一步都能照着复现。

2. 先把数据模型定死:学生、课程、成绩三张表怎么建

2.1 为什么是这三张表,而不是一张大宽表

很多人第一版会建一张score_all,字段堆成学号、姓名、班级、课程名、学分、分数。看着省事,问题在后面:一个学生改班级要更新几十行,一门课改学分要全表扫,统计某班某课平均分时还得靠字符串匹配课程名。正确做法是按第三范式拆成三张主表加必要的关联。

  • student:学号为主键,存姓名、班级、入学年份等相对稳定的属性。
  • course:课程号为主键,存课程名、学分、授课教师。
  • score:学号 + 课程号做联合主键,存分数、考试时间。分数是随每次考试变动的,单独放一张表,改分只动一行。

这样拆完,改班级只动student一行,改学分只动course一行,成绩表只负责「谁在哪门课考了多少」。代价是查询要 JOIN,但几千到几万条数据下 JOIN 的开销完全可以接受。

2.2 建表 SQL 与字段类型选择

-- 学生表:学号用 varchar 而不是 int,因为学号常带前导零或字母 CREATE TABLE student ( sno VARCHAR(20) NOT NULL COMMENT '学号', sname VARCHAR(50) NOT NULL COMMENT '姓名', class_name VARCHAR(50) NOT NULL COMMENT '班级', enroll_year INT NOT NULL COMMENT '入学年份', PRIMARY KEY (sno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表:学分用 decimal,避免 float 累加误差 CREATE TABLE course ( cno VARCHAR(20) NOT NULL COMMENT '课程号', cname VARCHAR(80) NOT NULL COMMENT '课程名', credit DECIMAL(3,1) NOT NULL DEFAULT 0 COMMENT '学分', teacher VARCHAR(50) DEFAULT NULL COMMENT '授课教师', PRIMARY KEY (cno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表:联合主键保证一个学生一门课只有一条记录 CREATE TABLE score ( sno VARCHAR(20) NOT NULL, cno VARCHAR(20) NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT '分数,允许空表示缺考', exam_date DATE DEFAULT NULL, PRIMARY KEY (sno, cno), KEY idx_cno (cno), CONSTRAINT fk_score_student FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_score_course FOREIGN KEY (cno) REFERENCES course(cno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:score表用(sno, cno)联合主键,天然防止同一学生同一课程重复录入;两个外键保证不会出现「成绩表里有学号但学生表里没有」的脏数据。参数上,score用DECIMAL(5,1)而不是FLOAT,因为浮点数在算加权平均时会出现89.99999这种结果,展示和排名都会出问题。utf8mb4是为了兼容姓名里的生僻字。

提示:如果学校允许缓考、补考,一个学生一门课会有多条成绩,那就把联合主键改成自增 id,再加一个exam_type字段区分正考补考,查询时取最高分或最新分。这是最常见的需求变更点,建表时先想清楚。

2.3 索引加在哪,别等慢查询找上门

成绩表上最常出现的查询是「按学号查某人所有成绩」和「按课程号查某课所有人成绩」。联合主键(sno, cno)已经覆盖了按学号的前缀查询,但按课程号查用不上这个索引,所以额外加了idx_cno。如果还要按班级统计,student表的class_name也建议加索引。索引不是越多越好,每加一个,插入和更新就多一份维护成本,成绩录入频繁的场景要权衡。

3. 环境搭起来:Tomcat + MySQL + JDBC 的最小可运行链路

3.1 版本选择与安装顺序

这套技术栈对版本不敏感,但组合错了会出玄学问题。稳妥组合是 JDK 8 或 11、Tomcat 8.5 或 9、MySQL 5.7 或 8.0。安装顺序建议先 JDK,再 MySQL,最后 Tomcat,因为 Tomcat 启动时要读JAVA_HOME,MySQL 要先建好库和账号。

MySQL 安装时记住三件事:字符集选utf8mb4,root 密码设一个自己记得住的,安装完把bin目录加进系统 PATH,否则后面命令行连不上。装完用下面命令验证:

mysql -u root -p # 输入密码后进入 mysql> 提示符 SHOW VARIABLES LIKE 'character_set_server'; # 期望看到 utf8mb4

3.2 JDBC 连接:把驱动、URL、连接池讲清楚

JSP 里访问数据库,最原始的方式是每次DriverManager.getConnection,但这样每个请求都新建连接,几十个并发就把数据库连接数打满。常见做法是配一个连接池,Tomcat 自带的 DBCP 或 Druid 都行。下面给一个不依赖框架、用 Tomcatcontext.xml配数据源的方案。

在META-INF/context.xml里声明资源:

<Context> <Resource name="jdbc/scoreDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" maxWaitMillis="10000" username="score_user" password="your_password" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/score_db?useUnicode=true&amp;characterEncoding=utf8mb4&amp;serverTimezone=Asia/Shanghai"/> </Context>

逻辑说明:maxTotal是连接池上限,教学场景 20 足够;maxWaitMillis是拿不到连接时的等待时间,设 10 秒,超时抛异常而不是无限卡住。URL 里的serverTimezone必须写,MySQL 8 的驱动不写会报时区错误,这是新手最常翻的车。characterEncoding用utf8mb4保证中文姓名不乱码。

在 JSP 或 Servlet 里取连接:

// 通过 JNDI 拿到容器管理的数据源,避免硬编码账号密码 Context initCtx = new InitialContext(); DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/scoreDB"); try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT s.sname, c.cname, sc.score FROM score sc " + "JOIN student s ON sc.sno = s.sno " + "JOIN course c ON sc.cno = c.cno WHERE sc.sno = ?")) { ps.setString(1, sno); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 渲染到页面 } } }

参数说明:PreparedStatement用?占位再setString,这是防 SQL 注入的基本功,千万别用字符串拼接。try-with-resources保证连接和语句自动关闭,漏关连接是连接池耗尽的第一大原因。

3.3 项目目录结构与 web.xml 关键配置

标准 Web 项目结构是src放 Java 源码,WebContent(或webapp)放 JSP、CSS、WEB-INF。WEB-INF/web.xml里注册 Servlet 映射和欢迎页。JSP 直接放WEB-INF外面能被浏览器直接访问,放里面则必须经 Servlet 转发,后者更安全,因为可以统一做登录校验。教学项目里常见做法是把录入、删除这类写操作放 Servlet,查询展示放 JSP。

4. 增删改查落地:从录入一条成绩到分页查询

4.1 成绩录入:一个 Servlet 处理表单提交

录入页面score_add.jsp提交到ScoreAddServlet,核心逻辑是校验参数、执行 INSERT、根据结果跳转。

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 必须在取参数前设置,否则中文乱码 String sno = req.getParameter("sno"); String cno = req.getParameter("cno"); String scoreStr = req.getParameter("score"); // 参数校验:空值和数字格式都要挡在数据库之前 if (sno == null || sno.trim().isEmpty() || cno == null || cno.trim().isEmpty() || scoreStr == null || scoreStr.trim().isEmpty()) { req.setAttribute("msg", "学号、课程号、分数都不能为空"); req.getRequestDispatcher("/score_add.jsp").forward(req, resp); return; } double score = Double.parseDouble(scoreStr); if (score < 0 || score > 100) { req.setAttribute("msg", "分数必须在 0 到 100 之间"); req.getRequestDispatcher("/score_add.jsp").forward(req, resp); return; } String sql = "INSERT INTO score(sno, cno, score, exam_date) VALUES(?,?,?,?)"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, cno); ps.setDouble(3, score); ps.setDate(4, new java.sql.Date(System.currentTimeMillis())); ps.executeUpdate(); resp.sendRedirect("score_list"); // 重定向防止刷新重复提交 } catch (SQLIntegrityConstraintViolationException e) { // 联合主键冲突,说明该学生该课程已录入 req.setAttribute("msg", "该学生这门课的成绩已存在,请勿重复录入"); req.getRequestDispatcher("/score_add.jsp").forward(req, resp); } catch (SQLException e) { throw new ServletException(e); } }

逻辑说明:setCharacterEncoding必须在第一次getParameter之前调用,放后面等于没设。校验分两层,先挡空值和越界,再交给数据库。插入成功后用sendRedirect而不是forward,这样用户刷新页面不会重复提交。捕获SQLIntegrityConstraintViolationException专门处理主键冲突,比笼统捕获SQLException更精准。

4.2 分页查询:LIMIT 加 COUNT 的标准写法

成绩列表动辄上千条,一次全查出来页面会卡。分页的核心是两条 SQL:一条查总数,一条查当前页。

-- 查总数,用于算总页数 SELECT COUNT(*) FROM score sc JOIN student s ON sc.sno = s.sno WHERE s.class_name = ?; -- 查当前页,offset 从 0 开始 SELECT s.sno, s.sname, c.cname, sc.score FROM score sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE s.class_name = ? ORDER BY s.sno LIMIT ? OFFSET ?;

参数说明:LIMIT是每页条数,OFFSET是(当前页 - 1) * 每页条数。注意 MySQL 的LIMIT参数在PreparedStatement里也能用?,但部分老驱动不支持,稳妥做法是把这两个值算好后拼进 SQL(它们是整数,不存在注入风险)。排序字段一定要加,否则分页时同一条记录可能出现在两页里,这是很多人没意识到的坑。

4.3 修改与删除:主键定位和级联影响

修改成绩用UPDATE score SET score = ? WHERE sno = ? AND cno = ?,删除用DELETE FROM score WHERE sno = ? AND cno = ?。这里要注意外键:如果直接删student表里的学生,而score表还有他的成绩,外键约束会阻止删除。两种处理方式,一是先删成绩再删学生,二是建表时把外键设成ON DELETE CASCADE。教学项目里建议用第一种,显式删除更安全,级联删除一旦误操作会连带清掉一堆数据。

5. 统计与排名:加权平均分、班级排名、不及格名单怎么算

5.1 加权平均分:SQL 聚合还是 Java 计算

加权平均分的公式是SUM(分数 * 学分) / SUM(学分)。这个计算放 SQL 里做最合适,因为数据不用全捞到内存。

SELECT s.sno, s.sname, ROUND(SUM(sc.score * c.credit) / SUM(c.credit), 2) AS weighted_avg FROM score sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE sc.score IS NOT NULL GROUP BY s.sno, s.sname ORDER BY weighted_avg DESC;

逻辑说明:WHERE sc.score IS NOT NULL排除缺考记录,否则缺考会被当成 0 分拉低平均。ROUND(..., 2)保留两位小数。GROUP BY必须包含sno和sname,因为sname不在聚合函数里。如果只想看某个班,在WHERE里加s.class_name = ?。

5.2 班级排名:窗口函数和变量法两种写法

MySQL 8.0 支持窗口函数,排名一行搞定:

SELECT sno, sname, weighted_avg, RANK() OVER (ORDER BY weighted_avg DESC) AS rank_in_class FROM ( SELECT s.sno, s.sname, SUM(sc.score * c.credit) / SUM(c.credit) AS weighted_avg FROM score sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE s.class_name = ? AND sc.score IS NOT NULL GROUP BY s.sno, s.sname ) t;

如果数据库是 5.7,没有窗口函数,就得用变量法:

SET @rank = 0; SELECT sno, sname, weighted_avg, (@rank := @rank + 1) AS rank_in_class FROM ( SELECT s.sno, s.sname, SUM(sc.score * c.credit) / SUM(c.credit) AS weighted_avg FROM score sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE s.class_name = ? AND sc.score IS NOT NULL GROUP BY s.sno, s.sname ORDER BY weighted_avg DESC ) t;

参数说明:RANK()遇到并列会跳号(1,1,3),DENSE_RANK()不跳号(1,1,2),ROW_NUMBER()强制不并列。教务排名通常用RANK()。变量法依赖ORDER BY的执行顺序,MySQL 8 里已经不推荐,能升级就升级。

5.3 不及格名单与补考统计

SELECT s.class_name, s.sno, s.sname, c.cname, sc.score FROM score sc JOIN student s ON sc.sno = s.sno JOIN course c ON sc.cno = c.cno WHERE sc.score < 60 ORDER BY s.class_name, s.sno;

这条查询配合导出功能,就是教务最常用的补考名单。如果要做「不及格门数统计」,把WHERE换成HAVING COUNT(*) >= 2配合GROUP BY s.sno即可。

6. 避坑与排查:这套系统最容易翻车的五个地方

6.1 中文乱码:现象是页面显示问号,原因是编码链路某一环没统一

现象:录入「张三」存进数据库变成??,或者查询出来是乱码。原因通常有三处:JSP 页面pageEncoding没设、请求体没设setCharacterEncoding、数据库或表不是utf8mb4。解决:JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里第一行req.setCharacterEncoding("UTF-8"),建库时CREATE DATABASE score_db DEFAULT CHARSET utf8mb4,连接 URL 带characterEncoding=utf8mb4。四处全对齐才彻底解决。

6.2 连接池耗尽:现象是页面卡死然后报Cannot get a connection

现象:用一会儿之后所有请求都卡住,日志里刷Cannot get a connection, pool exhausted。原因基本是某处Connection、Statement或ResultSet没关,连接被占满。解决:全部改成try-with-resources,或者把maxTotal调大应急。排查时可以在连接池配置里开removeAbandoned="true"和removeAbandonedTimeout="60",让容器回收超时未关的连接,但这只是兜底,根子还是代码里漏关。

6.3 时区报错:现象是启动就抛The server time zone value is unrecognized

现象:MySQL 8 驱动连接时报时区无法识别。原因:驱动要求显式指定时区,而服务器时区是CST这种有歧义的值。解决:URL 里加serverTimezone=Asia/Shanghai。如果还报,检查 MySQL 的time_zone变量,必要时在my.cnf里设default-time-zone='+08:00'。

6.4 分页数据重复或丢失:现象是翻页时同一条记录出现两次

现象:第 1 页和第 2 页有重复行,或者某条记录哪页都找不到。原因:ORDER BY的字段有重复值,MySQL 不保证稳定排序。解决:排序字段加上唯一列做兜底,比如ORDER BY weighted_avg DESC, s.sno ASC。只要排序键唯一,分页就稳定。

6.5 外键约束导致删除失败:现象是删学生时报foreign key constraint fails

现象:在管理页面删一个学生,报外键约束错误。原因:score表里还有这个学生的成绩记录。解决:删除前先查有没有关联成绩,有就提示「请先删除该生成绩」;或者用事务先删成绩再删学生。别图省事直接ON DELETE CASCADE,误删一个学生连带清掉他所有成绩,没有后悔药。

7. 进阶技巧:用存储过程做批量成绩导入和事务保护

批量导入是这套系统里最值得做深的功能。教务给的 Excel 转成 CSV 后,逐条 INSERT 太慢,而且中途失败会留下半截数据。用存储过程配合事务,能把一批导入包成一个原子操作。

DELIMITER // CREATE PROCEDURE batch_import_score(IN p_sno VARCHAR(20), IN p_cno VARCHAR(20), IN p_score DECIMAL(5,1)) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; -- 把原始错误抛回给调用方,方便定位 END; START TRANSACTION; INSERT INTO score(sno, cno, score, exam_date) VALUES(p_sno, p_cno, p_score, CURDATE()) ON DUPLICATE KEY UPDATE score = VALUES(score), exam_date = VALUES(exam_date); COMMIT; END // DELIMITER ;

逻辑说明:ON DUPLICATE KEY UPDATE让「已存在就更新、不存在就插入」一步完成,避免先查后插的竞态。EXIT HANDLER捕获任何 SQL 异常后回滚并重新抛出,保证要么全成功要么全不动。Java 侧用CallableStatement调用:

try (CallableStatement cs = conn.prepareCall("{call batch_import_score(?,?,?)}")) { for (ScoreRow row : rows) { cs.setString(1, row.getSno()); cs.setString(2, row.getCno()); cs.setBigDecimal(3, row.getScore()); cs.addBatch(); // 攒批,减少网络往返 } cs.executeBatch(); }

参数说明:addBatch把多条调用攒起来一次发送,比逐条执行快一个数量级。注意存储过程里已经开了事务,Java 侧就不要再手动setAutoCommit(false),否则事务嵌套行为依赖驱动实现,容易出问题。

验证导入结果,别只看「成功」提示,跑一条对账 SQL:

-- 导入前后各跑一次,条数应该对得上 SELECT COUNT(*) FROM score WHERE exam_date = CURDATE(); -- 抽查几条,确认分数没串行 SELECT * FROM score ORDER BY exam_date DESC LIMIT 10;

我自己做这类系统时养成的习惯是:任何批量写操作,先在测试库跑一遍,用SELECT COUNT(*)对账,确认条数和抽样数据都对,再上生产库。成绩数据不像日志,错了要一条条回滚,代价太大。存储过程加事务这套组合,看着比在 Java 里循环 INSERT 麻烦,但真出问题时它能让你少熬一个通宵。希望帮到你。

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

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

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

立即咨询