简介:这套基于JavaWeb的在线教务管理系统源代码,是面向毕业设计场景的完整项目案例,采用Java语言与SSM框架(Spring、SpringMVC、MyBatis)开发,覆盖管理员、教师、学生三类角色的教务管理及在线考试功能,帮助正在准备毕设或想系统学习JavaWeb开发的学生建立完整项目认知。资源包共424个文件,压缩包约14.92MB,以152个class编译文件、93个xml配置、56个jsp页面、55个java源码、30个jar依赖为主,xml包含Spring与MyBatis配置,jsp负责前端视图,java实现业务逻辑,另有SQL脚本供初始化数据库,js、css、properties等完善前端表现与运行配置。目前已有2081人学习下载,口碑热度可见一斑。通过研读源码,可以掌握SSM框架整合、MVC分层、MyBatis映射及在线考试相关环节(如试题管理、组卷、成绩查询)的具体实现;结合清晰的目录结构,还能理解数据库设计、权限控制和前后端交互的落地细节,对毕业设计或复现同类系统有直接帮助。深入项目后,可体会Example类为代表的MyBatis条件查询封装,以及模块化编码规范,是提升JavaWeb工程能力的实用参考。
1. 一个 JavaWeb 在线教务管理系统源代码,值得你花两周吃透
每年毕业设计选题,JavaWeb 在线教务管理系统源代码都会出现在同一个热搜词列表里:学生、教务、选课、成绩、源码。这不算新技术,但它恰好是检验 Java 基础功底的完整载体——你要面对的不只是增删改查,还得处理登录状态、角色权限、选课冲突、成绩事务这些真实业务问题。对正在找 JavaWeb 项目完整案例做毕设参考的同学来说,这套系统能在两周内让你把 Java、Servlet、JSP、MySQL 串成一条能演示、能讲清、能答辩的主线。我给你的建议是别急着换 Spring Boot,先把这套经典结构吃透,后面升级会轻松得多。
2. 从“能跑”到“能答辩”:JavaWeb 教务系统的技术分层、角色边界与 IDEA 运行配置
2.1 为什么选三层架构:Servlet + JSP + MySQL 是这种源代码的主线索
教务管理系统最常见的源代码结构不是 Spring Boot 全家桶,而是 Servlet + JSP + JDBC + MySQL 的经典三层架构。为什么毕设参考资料里大量项目还停留在这个“老方案”上?因为导师和答辩评委最关心你能不能讲清楚请求是怎么走的:浏览器把请求交给 Servlet,Servlet 调 DAO 访问数据库,数据回填到 JSP 渲染页面。中间没有任何框架黑匣子,代码每一步都可以断点跟进去看,这对答辩极其有利。
我在本地跑这种 JavaWeb 项目时,会先确认几件事:JDK 版本是否匹配,Tomcat 是 8.5 还是 9.0,MySQL 是 5.7 还是 8.0。这三点决定了一个项目导入后是十分钟跑通还是一上午原地折腾。如果你的源代码里带了 pom.xml,那它是 Maven 构建,依赖版本集中在 pom 里;如果只有 WEB-INF/lib 下的 jar 包,那是传统 Web 项目,导入 IDEA 时要选“导入为 Project from Existing Sources”,再把 lib 目录标记成依赖。两种方式差别很大,先分清再动手。
一个容易让人误判的点是 JSP 能不能编译通过。JSP 在运行期编译,IDEA 里的红色报错未必是真报错,反而控制台的 500 错误才是你需要盯的。我会在第一次启动前先编译一次整个项目,确认 src/main/java 下没有语法错误,再去启动 Tomcat。这样能把编译期问题和运行期问题隔离开。
2.2 三类角色、六张核心表和权限拦截:先把功能边界切清楚
在线教务管理系统里,用户角色一般分成学生、教师、管理员三类。学生能浏览课程、选课、退选、查成绩;教师能查看自己授课列表、录入成绩;管理员负责维护学生信息、教师信息、课程和排课。这个边界不切清楚,后面对每个 JSP 做访问控制时会非常痛苦。
我一般会把功能边界映射成六张核心表:学生表、教师表、课程表、选课表、成绩表、学期课表。它们之间不是简单的父子关系,而是要注意哪些表先建、哪些表后建。下表是我整理的角色与数据表对应关系,你可以拿来对照自己的源代码结构:
| 角色 | 核心功能 | 涉及数据表 |
|---|---|---|
| 学生 | 查看课程、选课、退选、查成绩 | 课程表、选课表、成绩表 |
| 教师 | 查看授课列表、录入成绩 | 课程表、选课表、成绩表 |
| 管理员 | 维护基础数据、排课、重置密码 | 学生表、教师表、课程表、学期课表 |
权限拦截不建议在每个 Servlet 里重复写 if 判断登录状态,而是在 web.xml 里配置一个 Filter,统一校验 Session 里的登录标记。若 Session 里没有用户信息,直接重定向到 login.jsp;若有,再判断角色是否能访问当前路径。这样做的好处是后续扩充一个新页面时,不用改业务 Servlet,只改 Filter 的路径匹配规则。
2.3 用 IDEA 导入并运行的最小步骤:Tomcat、MySQL、JDBC 配置一次到位
网上问“idea运行javaweb项目配置”的人很多,问题通常不是你代码写错,而是 ID 和运行环境没对齐。我给一个对我很实用的最小操作序列,先按这个走一遍,再去找别的坑。
第一步是准备数据库。先在 MySQL 里建一个和源码同名或同后缀的库,导入源码附带 sql 脚本。注意脚本默认字符集要选 utf8mb4,不要选 utf8,否则后面用户输入生僻字会直接报错。
# 在 MySQL 命令行中执行,-p 后面是你自己的 MySQL 密码 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p edu_admin < src/main/resources/edu_admin.sql参数说明:utf8mb4 比 utf8 多了对四字节字符的支持;collation 选 general_ci 是大小写不敏感的排序,对查询体验更好。如果源码没有提供 sql 文件,你就在 MySQL Workbench 里手建表,这个我在第 3 章给出完整的建表脚本。
第二步是修改 JDBC 配置。多数实训项目会把数据库连接写在 src/main/resources/db.properties 或 src/util/DBUtil.java 里。你需要把 url、username、password 改成自己本机的值,尤其注意 MySQL 8 必须加 serverTimezone。
# db.properties 典型配置 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu_admin?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你自己的密码参数说明:characterEncoding=utf8 是解决中文写入乱码的关键,useSSL=false 是避免本地数据库握手阶段的证书警告,serverTimezone 决定日期读写时的时区处理,allowPublicKeyRetrieval=true 是 MySQL 8 在 caching_sha2_password 认证下必须加的,否则第一次连库会抛 Public Key Retrieval 异常。
第三步是配置 Tomcat 运行入口。在 IDEA 里点击 Run > Edit Configurations,新增 Tomcat Server > Local,在 Deployment 标签页添加 war exploded artifact,Application context 设置为 /edu-admin。设置好以后,访问地址就是 http://localhost:8080/edu-admin/,而不是裸的 8080 端口。
第四步是验证登录。默认管理员账号常见是 admin / admin123,学生账号是学号,初始密码可能是 123456。如果登录页没反应,先看 Tomcat 控制台有没有异常堆栈,不要反复刷新页面。
提示:导入 Maven 项目时,如果 pom.xml 下载依赖太慢,先把 IDEA 的 Maven 仓库指向国内镜像,再重新 Import。依赖没拉全时,启动报 ClassNotFoundException 是最常见的,这不算代码问题。
3. 建库 SQL 怎么设计才能不踩数据坑:选课、成绩、课表的主外键与约束
3.1 六张核心表的字段设计与主外键关系
一个好的表结构能省掉大量 Java 代码里的防御判断。教务系统的数据坑集中在两个地方:选课重复、成绩对不上人。这两个问题靠应用层硬判容易漏,靠数据库约束兜底才稳。
先看学生表和教师表。学生表里学号字段 student_no 必须设 UNIQUE 约束,因为学号才是业务主键,自增 id 主要用于关联。密码字段不建议直接存明文,毕业设计规模不需要做加密算法,但至少用 JDK 自带的 MessageDigest 做一次 SHA-256 存储,答辩提起来是个加分点。
课程表要携带开课学期和容量字段。容量 capacity 是选课判断的硬上限,教师工号 teacher_id 作为外键连接到教师表,表示这门课由谁教授。学期字段 semester 不要设计成字符枚举,简单用 varchar,例如 2024-2025-1,在 Java 里直接字符串比较就能区分先后。
选课表和成绩表要分开拆。很多初学者把成绩字段直接放在选课表里,这看起来很省事,但会造成一个问题:学生退选后,成绩记录也跟着没了;或者教师录入成绩时,找不到一个“学生已选但还未考试”的独立中间记录。拆出去以后,选课表只管状态,成绩表只管分数,业务边界清楚。
3.2 建库脚本与三个业务约束:唯一选课、容量检查、级联删除别乱加
下面这份建库脚本可以作为你检查和补全源码 SQL 的参考基线。它不追求花哨,但把最容易出问题的三个约束都做了。
CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARACTER SET utf8mb4; USE edu_admin; DROP TABLE IF EXISTS score; DROP TABLE IF EXISTS selection; DROP TABLE IF EXISTS course; DROP TABLE IF EXISTS teacher; DROP TABLE IF EXISTS student; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生主键', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男' COMMENT '性别', major VARCHAR(80) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', password VARCHAR(64) NOT NULL COMMENT 'SHA-256 哈希值' ) ENGINE=InnoDB COMMENT='学生表'; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT '教师工号', name VARCHAR(50) NOT NULL, department VARCHAR(80) COMMENT '院系', password VARCHAR(64) NOT NULL ) ENGINE=InnoDB COMMENT='教师表'; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', semester VARCHAR(20) NOT NULL COMMENT '开课学期', capacity INT DEFAULT 60 COMMENT '容量上限', selected_count INT DEFAULT 0 COMMENT '已选人数', CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINE=InnoDB COMMENT='课程表'; CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT '1有效 0退选', UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB COMMENT='选课表'; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL, score DECIMAL(5,2) COMMENT '百分制成绩', UNIQUE KEY uk_score_student_course (student_id, course_id, semester), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB COMMENT='成绩表';逻辑说明:uk_student_course 这条联合唯一约束是在数据库层面兜底“同一个学生不能重复选同一门课”。哪怕 Java 代码里漏判了,第二次 INSERT 也会直接撞唯一索引报错,这比应用层 if 判断可靠得多。容量检查则在 Java 代码里做,因为数据库很难在约束里表达 selected_count < capacity 的逻辑,这属于可接受的应用层校验。
级联删除是我特别想提醒的。外键默认是 NO ACTION,不要在删除课程表记录时设置 ON DELETE CASCADE。教务系统里的选课、成绩属于过程数据,一旦课程被级联删除,所有历史成绩也没了,答辩时被追问会很尴尬。我一般保留 NO ACTION,让需要清理的操作走“先退选、再删成绩、最后删课程”的应用层顺序。
3.3 给答辩准备的 5 条常用查询:从登录校验到成绩汇总
除了建表,你还要能随手写出几条查询。评委经常会让你在黑板上或代码里说明某张报表是怎么查出来的。
-- 1. 登录校验:按学号查学生及密码 SELECT id, student_no, name FROM student WHERE student_no = ? AND password = ?; -- 2. 查询某学生已选课程列表 SELECT c.course_no, c.course_name, c.credit, t.name AS teacher_name FROM selection s JOIN course c ON s.course_id = c.id JOIN teacher t ON c.teacher_id = t.id WHERE s.student_id = ? AND s.status = 1; -- 3. 查询某课程已选人数(用于容量判断) SELECT COUNT(*) AS selected_total FROM selection WHERE course_id = ? AND status = 1; -- 4. 查询某个学生的成绩单 SELECT c.course_name, sc.score FROM score sc JOIN course c ON sc.course_id = c.id WHERE sc.student_id = ? AND sc.semester = ?; -- 5. 统计某教师授课课程的平均分与不及格人数 SELECT c.id, c.course_name, AVG(sc.score) AS avg_score, SUM(CASE WHEN sc.score < 60 THEN 1 ELSE 0 END) AS fail_count FROM course c LEFT JOIN score sc ON c.id = sc.course_id WHERE c.teacher_id = ? GROUP BY c.id, c.course_name;参数说明:第 3 条查询里对 selection 表按 status = 1 过滤,是为了排除退选但未物理删除的记录。第 5 条用 AVG 聚合时,如果某门课还没有成绩,AVG 结果是 NULL,在 Java 里取值时会得到 null,你要做空值处理,否则显示出来的平均分是空白。SUM 配合 CASE WHEN 是统计不及格人数的通用写法。
4. 手写核心业务代码:登录拦截、选课事务与成绩录入
4.1 登录与 Session 拦截:先写 Filter 再写 Servlet,避免每个 JSP 都判断用户态
在线教务系统的访问安全性不体现在复杂加密上,而体现在“未登录用户能不能直接绕过页面访问后台 Servlet”。很多源代码的问题在于:login.jsp 登录成功后存了个 Session,但所有检查都写在 JSP 顶部,若有人直接访问 /student/selectCourseServlet,服务器照样执行了业务逻辑。这需要在入口处统一拦截。
public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 放行登录页、登录接口和静态资源 String uri = req.getRequestURI(); String ctx = req.getContextPath(); if (uri.contains("/login.jsp") || uri.contains("/loginServlet") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".jpg") || uri.endsWith(".png")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); Object role = session != null ? session.getAttribute("role") : null; if (role == null) { resp.sendRedirect(ctx + "/login.jsp"); return; } chain.doFilter(request, response); } }逻辑说明:req.getSession(false) 是“拿不到 Session 就返回 null,而不是创建新 Session”,这可以避免每次匿名访问都产生一个无意义的 Session 对象。放行条件要写全,否则登录页的 CSS 会被拦截重定向,造成页面样式全部丢失。登录成功后,我习惯把 userId、role、userName 三个值都放进 Session,而不是只放一个布尔值,这样后续展示导航栏时不用再查一次库。
web.xml 里的配置也要注意路径写法。这个 Filter 的作用域应该覆盖 / 所有路径,同时把静态资源目录排除在外。若你用的是 Servlet 3.0 以上注解方式,用 @WebFilter("/*") 代替 web.xml 也行。
4.2 选课功能的事务写法:防止重复选课、超容量和并发下的脏数据
选课是教务系统里最容易出并发问题的地方。一个学生连续点两下“选课”按钮,连发两次请求;或者几十个学生同时选一门剩最后一个名额的课,如果代码不加控制,数据库里会出现超容量记录。常见做法是三层防线:前端按钮禁用、Java 代码里做同步判断、数据库唯一索引兜底。这里给你看 DAO 层的关键写法。
public boolean selectCourse(int studentId, int courseId) { String sqlCheckCapacity = "SELECT capacity, selected_count FROM course WHERE id = ? FOR UPDATE;"; String sqlCheckSelected = "SELECT COUNT(*) FROM selection WHERE student_id = ? AND course_id = ? AND status = 1;"; String sqlInsert = "INSERT INTO selection(student_id, course_id, status) VALUES(?, ?, 1);"; String sqlIncreaseCount = "UPDATE course SET selected_count = selected_count + 1 WHERE id = ?;"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 用行级锁锁住课程记录 PreparedStatement psCap = conn.prepareStatement(sqlCheckCapacity); psCap.setInt(1, courseId); ResultSet rs = psCap.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity = rs.getInt("capacity"); int selected = rs.getInt("selected_count"); // 容量和重复选课判断必须在同一个事务内完成 if (selected >= capacity) { conn.rollback(); return false; } PreparedStatement psSel = conn.prepareStatement(sqlCheckSelected); psSel.setInt(1, studentId); psSel.setInt(2, courseId); ResultSet rsSel = psSel.executeQuery(); rsSel.next(); if (rsSel.getInt(1) > 0) { conn.rollback(); return false; } // 写入选课记录,并更新已选人数 PreparedStatement psIns = conn.prepareStatement(sqlInsert); psIns.setInt(1, studentId); psIns.setInt(2, courseId); psIns.executeUpdate(); PreparedStatement psUpd = conn.prepareStatement(sqlIncreaseCount); psUpd.setInt(1, courseId); psUpd.executeUpdate(); conn.commit(); return true; } catch (Exception e) { // 异常时回滚并打日志 e.printStackTrace(); try (Connection conn = DBUtil.getConnection()) { conn.rollback(); } catch (Exception ignored) { } return false; } }参数说明:关键点是 SELECT 语句最后加的 FOR UPDATE。它现在锁的是 course 表中这一行,另一个线程再执行这条语句会一直等待,直到当前事务提交或回滚。没有这行锁,两个请求同时读到 selected = capacity = 1 的旧值,都会认为还有名额,于是都走插入流程,最终超容量。加了行锁后,第二个请求只能读到第一个请求提交后的 selected = 2,触发容量判断失败。
事务里还做了一个在 Java 代码里看起来多余的重复选课校验,目的是用更快的 SELECT 提前拦住重复请求,避免每个重复操作都走到插入后撞唯一索引。这个组合会让控制台少非常多异常堆栈,答辩演示时也是加分点。
4.3 成绩录入的更新策略:一个事务同时写成绩表和均分统计
成绩录入最常见的误区是只写成绩表,不更新课程统计,导致首页展示的“课程平均分”长期不准确。源码如果提供统计表,录入时就应该在同一个事务里同步更新。如果没提供统计表,只是展示时用 AVG 实时算,那你至少要做成“先删后插”,否则学生补考后会出现一行课对应两条成绩。
public void saveScore(int studentId, int courseId, String semester, double score) { String sqlDelete = "DELETE FROM score WHERE student_id = ? AND course_id = ? AND semester = ?;"; String sqlInsert = "INSERT INTO score(student_id, course_id, semester, score) VALUES(?, ?, ?, ?);"; String sqlUpdateAvg = "UPDATE course_stat c SET avg_score = " + "(SELECT AVG(score) FROM score s WHERE s.course_id = c.course_id) " + "WHERE c.course_id = ?;"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); PreparedStatement psDel = conn.prepareStatement(sqlDelete); psDel.setInt(1, studentId); psDel.setInt(2, courseId); psDel.setString(3, semester); psDel.executeUpdate(); PreparedStatement psIns = conn.prepareStatement(sqlInsert); psIns.setInt(1, studentId); psIns.setInt(2, courseId); psIns.setString(3, semester); psIns.setDouble(4, score); psIns.executeUpdate(); PreparedStatement psAvg = conn.prepareStatement(sqlUpdateAvg); psAvg.setInt(1, courseId); psAvg.executeUpdate(); conn.commit(); } catch (Exception e) { e.printStackTrace(); try (Connection conn = DBUtil.getConnection()) { conn.rollback(); } catch (Exception ignored) { } } }逻辑说明:先按“学生 + 课程 + 学期”删除旧成绩再插入,是为了解决重考、补录场景下的数据覆盖问题。如果不删,第二次录入成绩会插入新行,最终同一学期同一门课出现两条成绩,查出来的成绩单是两条重复记录。更新统计表时直接用子查询重新计算平均分,比在 Java 里累加更可靠,因为子查询看到的是事务内已提交的最终状态。
5. 导入运行后最容易翻车的 5 个坑:现象、原因与排查顺序
5.1 现象:Tomcat 启动成功,访问项目首页却一直是 404
原因是 IDEA 部署的 artifact 里没有包含项目依赖 jar,或 Web 根目录识别错误。很多 JavaWeb 毕设源代码是带 lib 目录的,但 IDEA 导入时不会自动把它加入 WEB-INF/lib。
解决:打开 Project Structure > Artifacts,选中你的 Web facet,在 Output Layout 里找到 WEB-INF,确认当前有没有 lib 目录;如果没有,右键选择 Add Copy of > Directory,把源代码目录里的 lib 文件夹加进去。加入后重新 Build,再启动 Tomcat,404 就会消失。
5.2 现象:数据库能连上,但页面显示的中文全是问号
原因是 JDBC 连接 URL 没带字符集参数,或者导入 SQL 脚本时库的字符集被改成 latin1。
解决:先执行 SHOW VARIABLES LIKE 'character_set_database'; 查看库字符集。如果状态不是 utf8mb4,用 ALTER DATABASE edu_admin CHARACTER SET utf8mb4; 修复,再检查 db.properties 里的 jdbc.url 是否包含 characterEncoding=utf8。改完必须重启 Tomcat,因为 DBCP 连接池会缓存旧连接。
5.3 现象:MySQL 8 连接时报 Communications link failure
原因是驱动版本和 URL 参数不匹配。部分源代码默认带的还是 mysql-connector-java 5.x,连不上 MySQL 8 的默认认证方式。
解决:先确认 pom.xml 里的 mysql 驱动版本,把旧版本替换为 8.0.x,再在 jdbc.url 末尾补上 serverTimezone=Asia/Shanghai 和 allowPublicKeyRetrieval=true。如果两个参数都加了还报错,把 useSSL=false 也补上,多数本地开发环境的证书问题就消失了。
5.4 现象:选课页面点“选课”后页面没反应,控制台也不报错
原因是前端没有把 courseId 正确传递到 Servlet,或者 JavaScript 里请求路径少了 context path。
解决:打开浏览器 F12 的 Network,看选课请求实际发出的 URL。如果路径是 /selectCourseServlet 而不是 /edu-admin/selectCourseServlet,说明前端 JS 里写死了路径,没有拼上 context path。建议把所有页面里的动态路径用${pageContext.request.contextPath}拼一次,这是 JavaWeb 项目里最基础的路径规范,能一次性解决多个页面 404。
5.5 现象:两个学生同时选最后一门课,最终都没有选上,数据库也没超容量
原因是业务层只做了 if (selected >= capacity) 判断,没做并发控制。
解决:把容量的查询语句改成 SELECT ... FOR UPDATE,或者让选课状态用一个 Redis 分布式锁兜底,不过毕设阶段用行锁就够。改完后在控制台用两个线程同时执行选课测试,观察第二个连接是否等待行锁。这个现象本身不叫“翻车”,但如果你答不出阻塞原理,扣分就可惜了。
6. 把“会跑代码”讲成“会做系统”:答辩演示顺序与三个低成本升级点
代码能跑只是第一步,剩下的是把“会跑代码”讲成“会做系统”。我建议你演示前把演示路径设计成“学生视角一串通”,不要一个模块一个模块跳。先登录学生账号,去课程列表里选一门课,再去选课结果页看列表变化,最后到成绩页看成绩显示;接着退出登录,换成教师账号,进入成绩录入页修改一个学生分数,回头刷新学生端成绩页看数值更新。这套路径能把整个系统的数据流转链完整讲一遍,评审看到的是“同一个数据库在实时反馈”,比分开演示三个模块强很多。
在技术和数据角度,有三个低成本升级点很值。第一,把项目里的 md5 密码改成 SHA-256,再在表结构里说明为什么不保存明文;第二,把 JSP 里散落的 JDBC 连接代码收敛成一个 DBUtil 单例,控制连接数量;第三,给选课接口增加一个重复点击按钮禁用,只在前端做一层简单防护,就能证明你考虑过用户体验问题。每改一个点,都要能说出“原来是什么、为什么改、改完有什么收益”。
我自己带过的项目里,最吃亏的一次是答辩时被问“选课并发你怎么防”,我只答了“数据库加了唯一约束”,没有提事务和行锁,评委当时就说“那你线上怎么处理超高并发”。吃一堑之后,每次演示前我都会把事务边界和锁点写在 A4 纸上,哪怕对方不追问,我也主动讲出来。这套 JavaWeb 在线教务系统源代码的光环不在功能多少,而在每一个功能背后都能牵出一条清晰的数据库和事务链路。先把这张链路背熟,再谈其他。希望帮到你。
本文还有配套的精品资源,点击获取