ARTICLE DETAIL

资讯详情

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

基于Java Web的学生成绩管理系统:从表结构设计到部署避坑全指南

基于Java Web的学生成绩管理系统:从表结构设计到部署避坑全指南 简介这是一套基于Java语言的Web学生成绩管理系统完整项目主要面向正在学习Java Web开发的高校学生或初级开发者适用于课程设计、毕业设计以及个人项目练习。系统以Servlet、JSP、JDBC为核心技术实现了学生信息管理、成绩录入与修改、条件查询、成绩筛选和成绩统计等常见功能整体采用MVC分层思路能够帮助读者理解业务逻辑、数据库访问和页面展示之间的协作关系。压缩包内共36个文件其中包含10个Java源码文件、9个JSP页面文件以及10张运行截图、3张相关图示另附有XML配置、MF清单、README说明等辅助文件压缩包大小仅为2.94MB目录结构简洁便于快速导入开发环境并对照学习。通过源码和截图可以直观了解数据库表设计、JDBC连接数据库、JSP页面渲染等关键实现细节也能看到系统首页、成绩录入或修改页面、查询结果页面和统计页面的实际效果。目前已有34人学习下载对希望快速上手Java Web项目开发的学习者来说是一份轻量实用的参考案例。1. 基于Java的Web学生成绩管理系统为什么值得你亲自动手写一遍“基于Java的Web学生成绩管理系统.zip”这个标题听起来很常见它不是微服务架构、不是高并发中间件就是一个围绕三张表做增删改查的课程设计级项目。但你如果真把它当“CRUD糊一脸”来看大概率会写出一个连自己都看不下去的代码所有查询揉在一个Servlet里、SQL字符串拼接、Tomcat一换就404。反过来如果你把表结构、分层、事务、权限和统计查询一次性想清楚它就是一份能讲给面试官听的完整java web项目。适合三类人正在准备课程设计的在校生、练手Web项目想转Java开发岗的自学者、以及需要一个成绩模块做二次开发的一线开发者。这篇就按我实际落地时的顺序把表结构、后端、页面、部署和踩坑一次讲透。2. 先拆三张表和前后端分层成绩管理系统的最小骨架2.1 表结构先行学生、课程、成绩三张表怎么建才不返工很多新手拿到成绩管理系统第一反应是打开IDEA或者Eclipse写登录页。我一般反过来先打开MySQL把三张表建好因为表结构一旦定死后面的DAO、Servlet、JSP全都是围绕字段名转的表错了返工成本最高。三张表分别是学生表、课程表、成绩表。学生表存学号、姓名、班级、入学年份课程表存课程编号、课程名、学分成绩表是中间表记录哪个学生选了哪门课、考了多少分。为什么要单独建课程表因为成绩必须挂在“学生—课程”这个组合上同一门课多个学生有成绩同一个学生多门课有成绩不拆成三张表就会大量冗余而且删改某门课成绩时会牵连一串数据。这种三表结构也是mybatisplus根据java实体类生成创建表的sql语句最典型的使用场景但手写SQL反而更直观。建表时有两个细节必须提前定好。第一个是外键约束成绩表的student_id引用学生表主键course_id引用课程表主键。第二个是字符集整库统一用utf8mb4而不是utf8否则插入姓名里的生僻字或emoji会出现乱码这个坑我见过不止一次。CREATE DATABASE IF NOT EXISTS grade_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE grade_db; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) COMMENT 班级, enroll_year YEAR COMMENT 入学年份, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分 ) ENGINEInnoDB; CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT 成绩百分制, exam_date DATE COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB;成绩表里的uk_student_course联合唯一索引非常关键。它保证“同一学生同一门课只能有一条成绩记录”是后续录入、更新成绩时判断“新增还是修改”的依据。外键ON DELETE CASCADE是一件后悔药删除学生时成绩自动清理避免删除父表时被子表挡住报错。但要注意课程表的外键我没加级联删除因为课程一旦被成绩引用直接级联删掉会造成历史成绩丢失这种业务决策必须留在Service层里做判断。2.2 分层与选型ServletJSP路线和Spring Boot路线怎么选学生成绩管理系统的Java Web实现主流就那么两条路。一条是Servlet JSP JDBC另一条是Spring Boot MyBatis Thymeleaf。我建议课程设计或刚入门的人走第一条因为Servlet的生命周期、请求转发、过滤器这些Java Web底层概念是java面试题里绕不开的基础Spring Boot把Tomcat内嵌、自动配置全藏起来了你调通了却讲不清原理。如果是做求职项目展示再上Spring Boot不迟代码结构从Servlet迁移过去也很快。分层这块我习惯按四层切model层放实体类dao层只做SQL交互service层处理业务逻辑和事务web层放Servlet和Filter视图层是JSP。学生成绩管理系统的业务不复杂但“事务”必须留在service层——比如一次录入某学生的五门课成绩中间失败一门要么全部提交要么全部回滚这个逻辑放Servlet里会随着URL增多迅速失控。2.3 工程配置Maven依赖和Tomcat运行参数工程形态建议直接用Maven的war包结构这样换机器时mvn clean package打出来的war丢进Tomcat的webapps目录就能跑不依赖IDE的坑爹Artifacts。JDK版本用8或11都行jdk8 zip下载解压配置JAVA_HOME是很成熟的做法没有安装版也完全不影响。project modelVersion4.0.0/modelVersion groupIdcom.school/groupId artifactIdgrade-manage/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies /projectjavax.servlet-api的scope设成provided意思是打包时不要把Tomcat自带的Servlet类打进去否则和Tomcat自带的同名类冲突轻则告警重则启动失败。这个和Spring Boot完全不同Spring Boot内嵌Tomcat依赖scope是compile。另外如果JDK版本高于8mysql-connector建议往8.x版本对齐老旧的5.1.49连MySQL 8会出现时区报错和认证协议不兼容这个在第5章细说。3. 从数据库连接到登录鉴权成绩系统的第一个可运行闭环3.1 数据库连接工具类URL里的每个参数都是血泪教训写DAO之前先写一个DBUtil把所有连接数据库的逻辑收敛在一个类里。很多项目图省事在每个DAO里直接写DriverManager.getConnection结果数据库地址一变要改十几个文件。DBUtil的核心作用有两个加载驱动、提供连接。连接池先不上课程设计级项目用DriverManager足够但要把参数写对。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/grade_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查pom依赖); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL里的三个参数决定了你能否顺利连上MySQL 8useSSLfalse关闭SSL避免本机调试时SSL握手告警serverTimezoneAsia/Shanghai处理MySQL 8和JDBC时区不一致的“难言之隐”不配的话经常报The server time zone value Öйú±ê׼ʱ¼ä这种乱码时间错characterEncodingutf8保证读写中文不乱码。static块里加载驱动触发一次类加载就完成驱动注册。密码是明文写在代码里的只适合本机学习调试真要给别人部署应该改成读取配置文件或环境变量否则等于把数据库裸奔给别人看。3.2 登录与角色为什么不能只查一次用户名密码登录是整个系统的入口。学生、教师、管理员三种角色如果只建一张用户表然后用type字段区分是可以的但角色多了以后每个Servlet都要判断type代码里全是if后面改权限会想骂人。我常用的做法是建user表和role表用户表存登录凭证角色表存权限中间用user_role关联。但成绩管理系统是课程设计没有Spring Security可借力有一个折中方案用户表直接带role字段登录成功后把role写进session再写一个简单的Filter做白名单拦截。import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(urlPatterns {/score, /student, /course, /statistics}) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); if (session null || session.getAttribute(userId) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } String role (String) session.getAttribute(role); String path req.getRequestURI(); if (student.equals(role) path.contains(/statistics)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, 学生不允许查看全员统计); return; } chain.doFilter(request, response); } }登录本身要做两件事验证用户名密码是否存在验证通过后把userId、role、name塞进session后续的Filter和JSP都从session里取身份信息。这里有个新手特别容易踩的坑只判断session.getAttribute(userId) ! null就放行完全不看角色就允许学生访问管理员的成绩录入口。所以我加了一个粗粒度的角色判断path里带/statistics且角色是student直接拒绝。密码用明文还是加密至少用MessageDigest做一次SHA-256加盐不要明文落库。虽然课程设计没人攻击你但养成习惯以后写企业项目不吃亏。3.3 统一响应与日志输出让后端的黑匣子变得可查Servlet这层有个通病输出全靠resp.getWriter().println(success)或者拼一段HTML前端拿到的是一堆没法程序化处理的东西。我一般会写一个极简的WebUtil把响应统一成JSON哪怕页面是JSPAjax请求也能复用同一个后端接口。import com.google.gson.Gson; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class WebUtil { public static void writeJson(HttpServletResponse resp, int code, String msg, Object data) throws IOException { resp.setContentType(application/json;charsetutf-8); MapString, Object result new HashMap(); result.put(code, code); result.put(msg, msg); result.put(data, data); resp.getWriter().write(new Gson().toJson(result)); } }注意setContentType里必须带charsetutf-8否则前端拿到中文会乱码。日志方面课程设计级项目用System.out.println打印参数和SQL其实是够用的但你要是给Servlet加上一个简单的logback配置后续排查线上问题时能少走很多弯路。打印的时机选在入口和出口Servlet接收参数后打一行写完响应前再打一行这样能快速定位是参数丢失还是业务抛异常。4. 成绩录入与统计查询手写JDBC的完整SQL链路4.1 成绩CRUD的Servlet一个URL怎么承载增删改查登录闭环跑通后核心功能就剩成绩的增删改查。成绩管理系统的业务动作无外乎录入成绩、修改成绩、删除成绩、按条件查询成绩。我习惯用一个ScoreServlet接收所有/score请求通过隐藏字段action区分操作这样web.xml里的Servlet映射只需要配置一处。WebServlet(/score) public class ScoreServlet extends HttpServlet { private ScoreDao scoreDao new ScoreDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (add.equals(action)) { addScore(req, resp); } else if (update.equals(action)) { updateScore(req, resp); } else if (delete.equals(action)) { deleteScore(req, resp); } else if (query.equals(action)) { queryScore(req, resp); } else { WebUtil.writeJson(resp, 400, 未知操作, null); } } private void addScore(HttpServletRequest req, HttpServletResponse resp) throws IOException { int studentId Integer.parseInt(req.getParameter(studentId)); int courseId Integer.parseInt(req.getParameter(courseId)); double score Double.parseDouble(req.getParameter(score)); boolean ok scoreDao.insertScore(studentId, courseId, BigDecimal.valueOf(score)); WebUtil.writeJson(resp, ok ? 200 : 500, ok ? 录入成功 : 该学生已有此课程成绩, null); } }doPost第一行req.setCharacterEncoding(UTF-8)是防止请求体里的中文参数乱码必须在读取任何参数之前调用。action分发模式比一个Servlet里堆十个if要清楚比十个Servlet少一堆web.xml映射属于这个体量项目的黄金选择。insertScore时如果联合唯一键已存在数据库会抛DuplicateKeyException所以Dao层要先做一次存在性判断或者捕获异常后转为业务提示。我这里是返回false提示“已有成绩”逻辑更直白。4.2 统计查询SQL平均分、及格率、排名一次算清成绩系统有一半的价值在统计。如果只做了CRUD和Excel记录没区别。至少要能按课程算出平均分、最高分、最低分、及格率。这一步SQL写不好代码怎么写都补不回来。SELECT c.course_name AS 课程名, COUNT(s.id) AS 考试人数, ROUND(AVG(s.score), 2) AS 平均分, MAX(s.score) AS 最高分, MIN(s.score) AS 最低分, CONCAT(ROUND(SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) / COUNT(s.id) * 100, 1), %) AS 及格率 FROM score s JOIN course c ON s.course_id c.id GROUP BY c.id, c.course_name ORDER BY 平均分 DESC;这里用CASE WHEN配合SUM实现条件计数算及格率不要在应用层把成绩拉出来再逐行判断那样做数据量一上来就慢而且代码啰嗦。GROUP BY里写c.id, c.course_name是因为MySQL的ONLY_FULL_GROUP_BY模式要求非聚合列必须出现在GROUP BY里否则5.7以上的MySQL会直接报错。ROUND控制小数位CONCAT把及格率转成百分数字符串。这个查询结果丢给JSP的c:forEach两分钟就能做出一个统计页。4.3 事务边界一次录入多门课成绩时的原子性问题成绩录入通常是一个学生多门课一次性提交比如期中考试录入五门课成绩。如果循环里每门课调一次insertScore第二门课因为重复键插入失败时第一门已经写进库了这会造成数据不一致。解决办法就是事务。public boolean batchInsertScores(ListScoreVO scores) throws SQLException { Connection conn DBUtil.getConnection(); PreparedStatement ps null; try { conn.setAutoCommit(false); String sql INSERT INTO score (student_id, course_id, score, exam_date) VALUES (?, ?, ?, ?); ps conn.prepareStatement(sql); for (ScoreVO vo : scores) { ps.setInt(1, vo.getStudentId()); ps.setInt(2, vo.getCourseId()); ps.setBigDecimal(3, vo.getScore()); ps.setDate(4, new java.sql.Date(vo.getExamDate().getTime())); ps.addBatch(); } ps.executeBatch(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { if (ps ! null) ps.close(); if (conn ! null) conn.close(); } }setAutoCommit(false)后Connection的commit和rollback由代码显式控制这是JDBC事务的标准姿势。addBatch批量提交比循环 executeUpdate 快一个数量级课程设计里数据量不大感觉不明显但这是一个好的职业习惯。注意事务和连接池的关系如果用了连接池conn.close()并不是真的关连接而是归还给池事务状态必须显式处理否则下次拿到这个连接时会带着上一次未提交的事务——这是最常见的所谓“玄学BUG”实际是你没把状态清理干净。5. 部署出现的玄学问题Tomcat 404、中文乱码和MySQL 8连接的避坑清单5.1 Tomcat 404与ClassNotFoundException八成是部署产物不对现象在IDEA里点运行能打开登录页但点登录后跳转/score直接404或者启动Tomcat时控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因IDEA里跑的是开发模式依赖由IDE临时提供一旦部署到外部TomcatWEB-INF/lib里没有mysql驱动运行时自然找不到类。404则是因为IDEA的Artifacts配置把项目打成了一个空的exploded目录JSP和class没有同步进去。解决放弃IDE内部署直接Maven打包。命令行执行mvn clean package把target目录下的grade-manage-1.0.0.war复制到Tomcat的webapps下启动Tomcat后它会自动解压部署。访问路径变成http://localhost:8080/grade-manage-这种带上下文路径的URLJSP里所有action和链接都要加上${pageContext.request.contextPath}前缀。我见过太多人把URL写死成/score换一台机器部署就翻车统一用EL表达式是最稳的。5.2 中文乱码从请求到响应整条链路都要检查现象录入的学生姓名在MySQL里显示正常页面上却是问号或者反过来页面提交的中文存进数据库变成了乱码。原因乱码不是单一环节的锅是请求编码、Tomcat连接编码、响应编码、数据库编码四个环节任何一个不一致都会出现。常见的是JSP页面用了GBK数据库表是utf8mb4表单提交后Tomcat用ISO-8859-1解码。解决一个CharacterEncodingFilter统一处理。filter filter-nameencodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时确认Tomcat的server.xml里Connector加了URIEncodingUTF-8数据库连接URL里有characterEncodingutf8JSP页面第一行声明% page contentTypetext/html;charsetUTF-8 languagejava %。这一组配置全部对齐后乱码基本绝迹。如果已经出现乱码数据用UPDATE student SET name CONVERT(CAST(CONVERT(name USING latin1) AS BINARY) USING utf8mb4)这类转换语句补救但以后别再问为什么乱码了先查这四个环节。5.3 MySQL 8.0 zip安装与连接时区和驱动版本别踩现象MySQL驱动加载失败、连接被拒、时区报错、认证插件不支持这四个问题在Windows上用mysql80 zip配置教程装MySQL 8时经常组团出现。原因MySQL 8默认使用caching_sha2_password认证老版的mysql-connector-java 5.x不认识MySQL 8对时区要求更严格URL不指定serverTimezone就会报错。解决驱动版本统一用8.xURL带上serverTimezoneAsia/Shanghai。如果是zip解压版配置步骤是解压到纯英文路径新建my.ini设置basedir和datadir用管理员权限执行mysqld --initialize-insecure初始化data目录生成root空密码账号然后net start mysql启动服务。启动失败先看data目录下的.err日志九成是my.ini路径里的斜杠方向错了。另一个常见翻车点端口3306被占用先netstat -ano | findstr 3306查一下。5.4 删除学生报外键约束先想清楚子表父表的顺序现象删除一个学生时页面报错Cannot delete or update a parent row: a foreign key constraint fails。原因student表是父表score表通过外键引用它。如果score表里还有这个学生的成绩记录数据库按默认约束会拒绝删除父表记录。解决我在建表SQL里给score表的student外键加了ON DELETE CASCADE这样删除学生时成绩自动清掉。但同样手段不能乱用在course表上课程被成绩引用时级联删除会把历史成绩全抹掉业务上不能接受。正确的做法是在Service层先查该课程是否被引用有成绩时提示“先处理该课程下的成绩数据”。外键策略设计也是java面试题里常被追问的点讲清楚“哪些外键该级联、哪些该限制”比背一堆概念更有说服力。6. 给成绩系统加一个“及格率报表”页验证你前面写的所有代码这个报表页是对整个系统的一次端到端验证。它会把统计SQL的结果渲染成表格同时检验你前面写的四件事Filter是否放行/statistics、DBUtil连接是否正常、JSP的JSTL标签是否生效、WebUtil和页面之间的数据流转是否顺畅。我实际开发中会把这一步放在“项目看起来已经能跑”之后因为只做CRUD的系统是不能证明三层架构设计得好的报表页能把DAO层、Service层、表现层的边界逼出来。报表页的核心是一个接受时间范围筛选的查询。比如要查“2025年春季学期的课程及格率”SQL需要按exam_date过滤再分组统计。写这个SQL时你会自然理解为什么score表要有exam_date字段如果没有这个字段你只能把所有历史数据混在一起统计报表就失去了业务意义。这就是我从第2章开始反复强调表结构先行的原因。你在报表页看到的每一列最后都能追溯到建表SQL里的一个字段这才是“管理系统”而不是“界面合集”。验证的时候按这个清单过一遍用管理员身份登录 → 录入一个新学生和两门课成绩 → 修改其中一门成绩 → 访问报表页查看那门课的及格率是否有变化 → 删除学生再去报表页确认对应成绩已消失。任何一步失败回去看日志定位而不是改页面重新试这个习惯能帮你省下大量“改着试试”的时间。我自己的教训是刚写Servlet那两年每次报表数据不对就去JSP里东改西改后来发现全是SQL的JOIN条件写错了——一个查询里多JOIN少JOIN一张表结果天差地别。希望你做完这个项目也能养成“先查SQL再怀疑页面”的排查顺序这比记住任何框架都值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表