ARTICLE DETAIL

资讯详情

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

JavaWeb学生选课系统实战:从表设计到事务并发控制与部署

JavaWeb学生选课系统实战:从表设计到事务并发控制与部署 简介面向计算机专业毕设学生及JavaWeb初学者的学生选课系统完整项目包解决从零搭建选课管理平台的难题。系统基于Servlet、JSP、JDBC与DbUtils开发结合EasyUI、jQuery与Ajax实现交互界面提供学生、教师、系统管理员三种角色覆盖学生信息管理、班级管理、课程管理、选课管理、考勤请假管理及成绩管理等多个模块功能完善、权限清晰可直接用于毕业设计或项目实战。资源包共13个文件大小7.67MB包含项目源码压缩包、数据库脚本、PDF与Markdown格式的项目文档、系统运行截图以及软件工具说明等其中源码与SQL脚本便于快速导入运行截图能直观展示登录与各管理界面文档则从需求分析、数据库设计到代码结构给出完整参考。该项目已有6130人学习下载经严格调试可稳定运行适合需要完整选课系统方案、希望学习JavaWeb分层开发或想借鉴毕设文档结构的开发者。1. 为什么「基于JavaWeb实现的学生选课系统」是毕设里最该认真做的普通题每年计算机毕设清单里都会出现「学生选课系统」不少同学觉得这题太普通扭头去选推荐算法、小程序、管理系统。我的看法相反JavaWeb学生选课系统恰恰是性价比最高的一档毕设选题。它表面上是个CRUD内里却把事务、并发控制、权限、表设计、前端交互全都占了课程容量超卖、学生重复选课、教师时间冲突这些都是能写进答辩讲稿的真实问题。源码结构清晰、两三周能调完老师问起来你也答得深。这篇按「从表设计到跑通部署」的落地顺序写适合做JavaWeb课设、毕设设计或者只想快速补一个完整案例的在校生。2. 先把技术栈和表结构定住JSP/Servlet还是SpringBoot选课数据怎么落库2.1 技术选型毕设场景下JavaWeb两边怎么选标题写的是JavaWeb落到具体技术栈时会分成两条线传统Servlet JSP和SpringBoot JSP/Thymeleaf。我的建议是先看学校题目怎么写的。对比项Servlet JSPSpringBoot 前端模板与课程匹配度高直接对应JavaWeb课程要求高但可能偏题配置文件量中web.xml 连接池配置低application.yml一把梭答辩讲解成本低请求到响应链路每一步都看得见中容易被追问框架自动配置的原理适合场景题目明确写JSP/Servlet或课程设计要求题目明确写SpringBoot或选了Vue前后端分离不少学校的JavaWeb课程设计题目就是「基于Servlet/JSP的XXX系统」这种情况下老老实实按Servlet写最稳。SpringBoot本身没有错但如果老师课程里没讲过答辩时他说「MyBatis的Mapper是怎么注入的」你解释起来会比较费劲。反过来如果学院要求必须用SpringBoot那也别硬写成Servlet。角色权限和数据规模是另一个决策点。学生选课系统的用户角色固定三类管理员、教师、学生。数据量撑死几千条选课记录不需要Redis、不需要消息队列。我见过的翻车案例都是过度设计上了个前后端分离加三套框架结果部署环境跑不起来最后一个月在调跨域。毕设的核心是「逻辑完整、能跑能讲」不是框架数量。2.2 选课系统的核心表用户、课程、选课关系与三个关键约束表结构是这类系统第一个要定的东西。四张表起步用户表负责登录与角色课程表负责课程基本信息选课表是学生与课程的多对多关系班级表做学生归属。课程-学生中间表这张表是整个系统的灵魂。t_user 用户表id、username、password、real_name、role、class_id。role字段建议用字符串存「admin / teacher / student」不要用0/1/2否则看数据时要对着注释猜。password字段存加密后的值别存明文。t_course 课程表id、course_name、teacher_id、credit、capacity、selected、semester、course_day、course_start、course_end。注意两点第一selected这个字段是冗余的它存「已选人数」可以通过 select count(*) 查出来但这里冗余出来是为了选课时的原子更新后面3.2会讲第二course_day course_start course_end 用来做时间冲突检测用「星期几 第几节到第几节」这种表示法比直接存时间字符串好算得多。t_student_course 选课表id、student_id、course_id、select_time。这张表必须加唯一约束 UNIQUE(student_id, course_id)这是防重复选课的数据库级兜底。很多毕设项目靠代码里查一遍「有没有选过」来防重复逻辑没错但并发点两下按钮时两请求同时查到「没选过」就都插进去了。唯一约束能在数据库层面拒绝第二条记录。t_class 班级表id、class_name。如果学院规模小班级表可以合并进用户表一个字段但独立成表更规范后面做「按班级导出选课名单」时直接join。2.3 建库脚本与初始化数据一次性把地基铺好直接给一份能跑的MySQL建库脚本。字符集用utf8mb4因为部分学生姓名可能带生僻字utf8mb4比utf8覆盖全。CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存MD5加盐后的值, real_name VARCHAR(50) NOT NULL, role VARCHAR(10) NOT NULL COMMENT admin/teacher/student, class_id INT, INDEX idx_role (role) ) ENGINEInnoDB; CREATE TABLE t_class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB; CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(2,1) NOT NULL DEFAULT 2.0, capacity INT NOT NULL COMMENT 课程容量, selected INT NOT NULL DEFAULT 0 COMMENT 已选人数, semester VARCHAR(20) NOT NULL, course_day TINYINT NOT NULL COMMENT 1-7 代表周一至周日, course_start TINYINT NOT NULL COMMENT 开始节次, course_end TINYINT NOT NULL COMMENT 结束节次, INDEX idx_selected (selected) ) ENGINEInnoDB; CREATE TABLE t_student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME NOT NULL, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB;脚本里三个容易忽略的点。第一t_user 的 username 加了 UNIQUE防止账号重复也方便登录查询走索引。第二t_student_course 的联合唯一索引 uk_student_course 是选课系统的安全网任何时候都不建议去掉。第三所有表显式指定 ENGINEInnoDBMySQL 8 默认就是 InnoDB但写出来能提醒后面的操作要依赖事务。如果建完表发现漏了某张表用 ALTER TABLE 补索引不要删库重来因为初始化数据插进去了再重导很麻烦。初始化数据至少要有一个管理员账号、两个教师账号、三个班级、若干学生、四五门课程。密码字段先统一放一个MD5值比如e10adc3949ba59abbe56e057f20f883e是123456的MD5登录模块写的解密逻辑要和它对齐。课程数据要刻意制造一些冲突场景同一时间段的两个课、容量为1的供选课测试课方便自己演示。3. 选课与退课的核心逻辑事务边界、容量校验与冲突检测3.1 选课主流程先查后插为什么必须在同一个事务里选课逻辑不是一个 INSERT 完事。完整流程是四步查课程是否存在、查学生是否已选、查容量是否已满、插入选课记录并更新已选人数。这四步必须包在同一个事务里中间任何一步失败就整体回滚。public boolean enroll(Connection conn, int studentId, int courseId) throws Exception { // conn 由 Service 层传入此处已设置 setAutoCommit(false) try { // 1. 课程是否存在 PreparedStatement ps1 conn.prepareStatement( SELECT capacity, selected FROM t_course WHERE id ? FOR UPDATE); ps1.setInt(1, courseId); ResultSet rs1 ps1.executeQuery(); if (!rs1.next()) { conn.rollback(); return false; // 课程不存在 } int capacity rs1.getInt(capacity); int selected rs1.getInt(selected); // 2. 是否已选 PreparedStatement ps2 conn.prepareStatement( SELECT COUNT(*) FROM t_student_course WHERE student_id ? AND course_id ?); ps2.setInt(1, studentId); ps2.setInt(2, courseId); ResultSet rs2 ps2.executeQuery(); rs2.next(); if (rs2.getInt(1) 0) { conn.rollback(); return false; // 已选过这门课 } // 3. 容量判断 if (selected capacity) { conn.rollback(); return false; // 容量已满 } // 4. 插入选课记录 更新已选人数 PreparedStatement ps3 conn.prepareStatement( INSERT INTO t_student_course(student_id, course_id, select_time) VALUES(?, ?, NOW())); ps3.setInt(1, studentId); ps3.setInt(2, courseId); ps3.executeUpdate(); PreparedStatement ps4 conn.prepareStatement( UPDATE t_course SET selected selected 1 WHERE id ?); ps4.setInt(1, courseId); ps4.executeUpdate(); conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } }代码里有几个关键点。SELECT ... FOR UPDATE 给课程行加了行锁目的是让并发请求串行化防止两个人同时读到容量还剩1都执行插入。事务边界是「从第一步查询开始到最后一次UPDATE」整体提交而不是每个SQL单独提交。finally里要记得把连接还回连接池并把 autoCommit 恢复为 true否则下次拿到这个连接时事务状态是乱的。参数里的 studentId 和 courseId 都是 int不要用字符串拼SQLPreparedStatement 防注入是基本功答辩时老师大概率会问。3.2 容量控制为什么「先查再判断」会超卖原子UPDATE怎么堵住3.1 是在传统写法下保证正确性但 SELECT ... FOR UPDATE 依赖你每次选课都记得加锁一旦漏了这行就会超卖。更稳的做法是把容量判断直接写进 UPDATE 语句让数据库来做原子判断。// 原子更新只有已选人数小于容量时才加1 PreparedStatement ps conn.prepareStatement( UPDATE t_course SET selected selected 1 WHERE id ? AND selected capacity); ps.setInt(1, courseId); int rows ps.executeUpdate(); if (rows 0) { // 更新0行说明课程不存在或容量已满 conn.rollback(); return false; }这行的逻辑是UPDATE 在执行时会对这一行加锁selected capacity这个条件在锁内判断两个并发请求同时执行时第一个更新成功第二个发现 selected 已经等于 capacity更新0行。你不查也不判断直接用返回值判断成败。这个方法也解决了「先查再插」中间的时间窗口代码量还少了一半。这里有一个容易被新手理解偏的细节MySQL 的 UPDATE 默认是原子的条件不满足就影响0行不需要你写事务它也不会多更新。但在选课系统里UPDATE 之后还要 INSERT 选课记录所以事务还是要写事务管的是「课程计数更新 选课记录插入」要么都成功要么都不成功。容量控制交给单条语句一致性交给事务各司其职。如果学校要求讲「乐观锁」可以在 t_course 加一列 version每次更新时 WHERE version 查询到的值version1。但就这个系统而言atomic UPDATE 已经足够乐观锁反而多一次查询和一次版本号校验显得有些学术化。答辩时能说清楚两种方案的差别即可悲观锁靠行锁串行化原子更新靠条件更新兜底乐观锁靠版本号冲突重试。3.3 时间冲突检测课程时间段怎么比对才不漏课学生一周不能在同一时间段上两门课。表设计里 course_day 是星期几course_start 和 course_end 是节次区间冲突检测就是比较这些字段。public boolean hasTimeConflict(Connection conn, int studentId, int day, int start, int end) throws Exception { PreparedStatement ps conn.prepareStatement( SELECT course_day, course_start, course_end FROM t_course WHERE id IN (SELECT course_id FROM t_student_course WHERE student_id ?)); ps.setInt(1, studentId); ResultSet rs ps.executeQuery(); while (rs.next()) { int existDay rs.getInt(course_day); int existStart rs.getInt(course_start); int existEnd rs.getInt(course_end); // 星期不同肯定不冲突 if (existDay ! day) { continue; } // 判断两个区间是否重叠新课程开始节次 已选课程结束节次 // 且 新课程结束节次 已选课程开始节次 if (start existEnd end existStart) { return true; // 时间冲突 } } return false; }区间重叠的判断条件是一组反向思考不重叠只有两种情况新课程完全在已选课程之前end existStart或者完全在其之后start existEnd。把这俩排除掉剩下的就是重叠。段里的两个 if 条件要一起看start existEnd end existStart是重叠的充分必要条件。这个查询在内存里遍历学生已选课程数据量在毕设级别完全够。如果选课记录几千上万可以改成一条 SQL 用 EXISTS 关联但对这个系统来说没必要。要注意的是节次表示法的边界有些学校一晚上是9-10节有些是9-11节start 和 end 明确按节次号存前端下拉框和数据字典保持一致就行。3.4 退课与选课名单反向操作同样要防并发退课是选课的逆过程删除选课记录、将课程已选人数减1。这里容易翻车的地方是只 DELETE 不更新 selected过几天学生发现课程显示「容量已满但没人选」。另一个坑是减1时没加下限判断数据异常时 selected 变成负数导致容量判断彻底失效。public boolean dropCourse(Connection conn, int studentId, int courseId) throws Exception { try { // 1. 删除选课记录这里带 studentId 条件学生不能退别人的课 PreparedStatement ps1 conn.prepareStatement( DELETE FROM t_student_course WHERE student_id ? AND course_id ?); ps1.setInt(1, studentId); ps1.setInt(2, courseId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 压根没选过这门课 } // 2. 已选人数减1selected 0 防止减成负数 PreparedStatement ps2 conn.prepareStatement( UPDATE t_course SET selected selected - 1 WHERE id ? AND selected 0); ps2.setInt(1, courseId); ps2.executeUpdate(); conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } }DELETE 语句里带student_id ?的意图是数据权限一个学生只能操作自己的选课记录。如果接口被直接调用不带这个条件就可以删任何人的课这是很隐蔽的安全漏洞。两个操作同样在一个事务里避免删除成功但计数没扣的情况。管理员端的选课名单就是一条带 JOIN 的查询按课程查 t_student_course 关联 t_user 和 t_class拿到学生姓名和班级然后导出 Excel 或者生成表格页面这个发生在管理端模块里。4. 前端页面与权限控制JSP Filter Session 的经典组合4.1 登录与角色识别Session与Filter拦截器JavaWeb 体系里权限控制的标准做法是 Session 存登录状态 Filter 拦截请求。登录成功后把用户对象放进 sessionFilter 在每个请求进来时检查 session没登录就重定向到登录页。WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 放行登录页、登录接口和静态资源 String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.contains(/static/) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(req, resp); return; } // 未登录用户踢回登录页 Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 管理员接口做角色校验 if (uri.contains(/admin/) !admin.equals(((User) user).getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } }Filter 的拦截路径是/*表示所有请求都过这道关卡。静态资源放行列表要写全否则 CSS、JS 被拦下来页面打开是纯HTML没有样式很多同学会误以为是前端代码写错了。放行判断用的是 endsWith 和 contains 而不是精确匹配因为 Context Path 会变化写死路径在部署到不同容器时会失效。登录成功后的跳转按角色区分学生去选课页、教师去课程管理页、管理员去后台这个分发逻辑写在 LoginServlet 里。4.2 学生端选课页课程列表与已选状态的联动学生端最核心的页面是课程列表页每门课要显示课程名、教师、学分、容量、已选人数以及「选课/退课」按钮。已选状态的联动通常是后端查一次已选课程ID集合放进 request 域JSP 页面用 JSTL 判断。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered thead tr th课程名/thth教师/thth学分/th th已选/容量/thth操作/th /tr /thead tbody c:forEach varcourse items${courseList} tr td${course.courseName}/td td${course.teacherName}/td td${course.credit}/td td${course.selected} / ${course.capacity}/td td c:choose c:when test${selectedMap[course.id] ! null} a hrefcourse?actiondropcourseId${course.id} classbtn btn-danger btn-sm退课/a /c:when c:otherwise a hrefcourse?actionenrollcourseId${course.id} classbtn btn-primary btn-sm选课/a /c:otherwise /c:choose /td /tr /c:forEach /tbody /tableselectedMap 是在 Servlet 里构建的MapInteger, Booleankey是课程IDvalue是否已选。selectedMap[course.id]这种取值方式是 EL 表达式对 Map 的简化语法如果 Map 里没有记录返回 null配合 c:when 判断正好。按钮还应该在前端加上点击确认或防重复提交表单方式提交比 AJAX 更稳不易出跨域问题如果页面美观要求高再加一点原生 JS 设置按钮 disabled避免连点。选课失败时的提示要友好。Service 返回 boolean 只能表达成功或失败但失败原因可能是「已选过」「容量满」「时间冲突」三种。我一般让 Service 返回一个枚举或错误码字符串Servlet 把它放进 request.setAttribute(msg)页面在表格上方显示成 alert 或文字提示。不要把失败原因直接抛异常给前端那不叫错误处理叫只对自己友好的错误输出。4.3 管理端维护课程管理与选课名单导出管理端负责两块课程CRUD和选课名单。课程CRUD是最普通的增删改查选课名单页面按课程维度展示已选学生后台一条 SQL 搞定。SELECT u.real_name, c.class_name, sc.select_time FROM t_student_course sc JOIN t_user u ON sc.student_id u.id LEFT JOIN t_class c ON u.class_id c.id WHERE sc.course_id ? ORDER BY sc.select_time;查询结果以表格形式展示在课程详情页下方。管理端删除课程时有个联动问题直接 DELETE FROM t_course WHERE id? 会因为外键约束失败或者留下没人能查到的孤儿选课记录。正确顺序是先删 t_student_course 再删 t_course两条语句放一个事务里。如果建表时加了外键可以写ON DELETE CASCADE但链式删除比较隐蔽我更喜欢在 Service 层显式控制顺序逻辑摆出来答辩也好讲。课程的时间冲突和容量管理在设计课程表单时就要控制新增课程时管理端应该能查看该时间段的已被占用情况大多数毕设会忽视这点导致课程与课程冲突。给课程表加一个页面按星期几展示全天节次的课程分布用课程名填充格子这个视图在演示时非常加分能让答辩老师一眼看懂你考虑了排课冲突问题。5. 从跑通到演示的避坑清单你大概率会翻车的五个瞬间5.1 重复点击导致重复选课现象学生在选课页面快速点了两次「选课」按钮后台出现两条一模一样的选课记录课程已选人数加了2。原因前端没有禁用按钮后端「先查后插」的逻辑在两个并发请求下同时通过检查。解决三层防护一起做。前端点击后置灰按钮Service 层保留「是否已选」校验作为常规路径数据库 t_student_course 的唯一索引 uk_student_course 作为最终兜底插入重复记录时抛 DuplicateKeyException捕获后返回错误提示。唯一索引这层很容易被忽略但它才是真正挡并发的那道墙。5.2 已选人数超过课程容量现象课程容量设为50选课系统里显示的已选人数超过50还能继续选。原因很多人写的容量校验是「先 SELECT selected再在代码里比较 selected capacity再 UPDATE」两个请求同时读到相同的 selected都认为还有名额然后把 selected 从50更新到51甚至52。解决把容量判断下沉到 UPDATE 语句里UPDATE t_course SET selected selected 1 WHERE id ? AND selected capacity更新0行就是没名额了代码返回「容量已满」。这条改完并发超卖自然消失因为条件判断和数字递增在同一锁粒度内完成。5.3 Tomcat 10 的 jakarta 前缀问题现象照着 JavaWeb 教程写代码javax.servlet 开头的 import 全部飘红或者强上 Tomcat 10 后启动不报错但每个请求 500日志里写 NoClassDefFoundError: jakarta/servlet/...。原因Tomcat 10 开始把 Servlet API 的包名从 javax.servlet 改成了 jakarta.servlet这是 Java EE 迁到 Eclipse 后的品牌变更老教程代码在新容器上全部失效。解决最省事的做法是装 Tomcat 9.0.x它用 javax.servlet和绝大多数毕设教程完全一致。如果坚持用 Tomcat 10就要把所有import javax.servlet.*改成import jakarta.servlet.*注意是全部代码文件少改一个就运行时报错。建议拿到项目先看一眼 pom.xml 或 lib 目录下的 servlet-api 版本再决定装哪个 Tomcat。5.4 MySQL 8 的驱动类名与时区配置现象数据库连接池启动报ClassNotFoundException: com.mysql.jdbc.Driver或者控制台抛The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8 的驱动包把驱动类改成了com.mysql.cj.jdbc.Driver老驱动类只在旧版 driver 5.x 里存在同时 MySQL 8 要求 JDBC URL 显式指定时区否则驱动拿系统默认时区去解析会乱码。解决驱动 jar 换成 mysql-connector-java 8.0.x代码里写新的类名URL 上补serverTimezoneAsia/Shanghai。完整的连接串是jdbc:mysql://localhost:3306/course_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue每一项都有用useUnicodecharacterEncoding 保证中文不乱码useSSLfalse 避免 MySQL 8 默认 SSL 告警刷屏allowPublicKeyRetrievaltrue 让客户端和服务器交换公钥时不报错。网上很多教程只让你改时区漏了 allowPublicKeyRetrieval连接时照样抛异常。5.5 部署在 IDEA 里页面404修改代码后不生效现象IDEA 里配置好 Tomcat点击运行后浏览器访问 localhost:8080 直接404或者改了 JSP 刷新页面还是旧内容。原因Deployment 里添加的是 war 包而不是 war exploded或者 Artifacts 没有选对IDEA 把编译产出和资源文件打到了一个不包含 JSP 的包里。war exploded 模式的意思是「解压后的war目录」让 Tomcat 直接运行这个目录改代码后热部署才快。解决运行配置里 Deployment 选项卡点加号选择 Artifact一定要选带war exploded的那个Application context 建议直接写/访问的时候就是 localhost:8080/index.jsp少一层路径也少一层404的可能。改 JSP 不生效先按 CtrlF10 重新编译还不行就点 Tomcat 的 Redeploy一般这两步能解决大部分「改了没变化」。6. 部署到IDEA里运行再准备好答辩老师的五个追问6.1 IDEA 里跑通 JavaWeb 项目的固定顺序我不管拿到谁的 JavaWeb 项目源码在 IDEA 里跑通都按同一个顺序来这套流程对标题里的学生选课系统同样适用。第一步确认环境版本JDK 8 配 Tomcat 9JDK 11 以上也建议继续用 Tomcat 9避开 5.3 的坑。第二步导入源码后先把 lib 目录下的所有 jar 加到 Project Structure 的 Libraries 里很多项目跑不起来不是代码问题是 jar 缺失。第三步改数据库配置。找到项目里的数据库连接配置文件常见是 db.properties 或 JDBCUtils.java把 username、password、URL 全改成自己本机的建库脚本先跑一遍。第四步配置 TomcatRun → Edit Configurations → 点加号 → Tomcat Server → LocalServer 选项卡里选 Tomcat 安装目录Deployment 选项卡加 Artifact选war explodedApplication context 设/。第五步点运行控制台出现Server startup in xxx ms后浏览器访问登录页用初始化的管理员账号登一遍验证角色跳转再登学生账号走一遍选课退课。整套下来十五分钟出来的黑匣子状态基本能排除大部分环境问题。6.2 答辩高频问题事务、并发、权限、密码存储这个项目的答辩追问方向非常固定提前把答案编好比被问倒再解释强得多。我把最常见的五个问题和参考回答思路列在下面。老师可能问参考回答思路事务在你的系统里怎么实现的选课方法里手动setAutoCommit(false)选课四步操作成功后commit()任何一步异常rollback()。数据源用的是连接池每次拿到的连接要恢复 autoCommit 状态并发选课怎么保证不超卖容量更新的 SQL 里带selected capacity条件数据库行锁保证同一条课程记录只有一个事务能成功更新影响行数为0就表示容量已满权限控制怎么做的登录时把用户对象放进 Session自定义 Filter 拦截所有请求没登录跳登录页角色字段区分管理员和学生管理端路径再做一次角色校验密码怎么存的安全吗密码不存明文存的是MD5摘要。如果要改进可以对密码加盐或改用BCrypt防止彩虹表攻击为什么这么设计表结构选课表放在学生表和课程表中间联合唯一索引防止重复选课课程表里冗余一个已选人数是为了让容量更新能做成单条原子SQL答这些问题时一个核心原则是每个问题都对应到代码里的一处具体实现不要背概念。比如讲到事务就说「就在 CourseService 的 enroll 方法里conn.setAutoCommit(false) 这行下面」老师顺着你说的地方看一眼代码印象比背书好得多。密码存储如果被追问「MD5可以被撞库」直接承认当前为了演示用了MD5并说明真实部署要做盐值哈希这种「知道边界」的答案是答辩加分的。我自己的习惯是项目交付前把建库脚本、初始化数据、IDEA 运行步骤写进 README方便半个月后的自己也方便指导老师或评委在别的电脑上复现。很多毕设项目本身没问题就死在没写完运行说明评委换个环境跑不起来体验就很差。运行文档补齐后把 5.1 到 5.5 这类坑再照着过一遍一个 JavaWeb 学生选课系统就能稳稳收尾。这套流程对毕设的价值不亚于选课题本身希望帮到你。本文还有配套的精品资源点击获取
返回列表