ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于JSP+MySQL的学生成绩管理系统:数据库设计、分页查询与统计排名实战

基于JSP+MySQL的学生成绩管理系统:数据库设计、分页查询与统计排名实战 简介这是一套面向高校计算机相关专业课程设计与毕业设计的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. 从一张成绩单说起JSPMySQL 学生成绩综合管理系统到底解决什么问题期末周教务老师手里三张 Excel一张学生名册、一张课程表、一张各科成绩汇总。要把它们合成一份能按班级、按课程、按学号查询的报表还要算加权平均分、排名、不及格名单。手工做一次两小时改一个分数就得全部重来。这就是「基于 JavaJSP 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表学分用 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表联合主键保证一个学生一门课只有一条记录 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明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_HOMEMySQL 要先建好库和账号。MySQL 安装时记住三件事字符集选utf8mb4root 密码设一个自己记得住的安装完把bin目录加进系统 PATH否则后面命令行连不上。装完用下面命令验证mysql -u root -p # 输入密码后进入 mysql 提示符 SHOW VARIABLES LIKE character_set_server; # 期望看到 utf8mb43.2 JDBC 连接把驱动、URL、连接池讲清楚JSP 里访问数据库最原始的方式是每次DriverManager.getConnection但这样每个请求都新建连接几十个并发就把数据库连接数打满。常见做法是配一个连接池Tomcat 自带的 DBCP 或 Druid 都行。下面给一个不依赖框架、用 Tomcatcontext.xml配数据源的方案。在META-INF/context.xml里声明资源Context Resource namejdbc/scoreDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 maxWaitMillis10000 usernamescore_user passwordyour_password driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/score_db?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/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,3DENSE_RANK()不跳号1,1,2ROW_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 contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 里第一行req.setCharacterEncoding(UTF-8)建库时CREATE DATABASE score_db DEFAULT CHARSET utf8mb4连接 URL 带characterEncodingutf8mb4。四处全对齐才彻底解决。6.2 连接池耗尽现象是页面卡死然后报Cannot get a connection现象用一会儿之后所有请求都卡住日志里刷Cannot get a connection, pool exhausted。原因基本是某处Connection、Statement或ResultSet没关连接被占满。解决全部改成try-with-resources或者把maxTotal调大应急。排查时可以在连接池配置里开removeAbandonedtrue和removeAbandonedTimeout60让容器回收超时未关的连接但这只是兜底根子还是代码里漏关。6.3 时区报错现象是启动就抛The server time zone value is unrecognized现象MySQL 8 驱动连接时报时区无法识别。原因驱动要求显式指定时区而服务器时区是CST这种有歧义的值。解决URL 里加serverTimezoneAsia/Shanghai。如果还报检查 MySQL 的time_zone变量必要时在my.cnf里设default-time-zone08: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 麻烦但真出问题时它能让你少熬一个通宵。希望帮到你。本文还有配套的精品资源点击获取
返回列表