资讯动态

JavaWeb选课系统源码实战:数据库设计、事务并发与二次开发避坑指南

发布时间:2026/9/28 6:16:24 来源:尧图企业网站定制
简介这份资源是面向高校计算机相关专业学生与JavaWeb初学者的一套学生选课系统课程设计完整源码包可直接用于课程设计、毕业设计选题或JavaWeb入门练手。项目采用JSPServletJavaBean的经典分层结构配合MySQL数据库脚本覆盖学生选课、课程管理、用户登录等核心业务模块适合作为从零理解Web请求处理流程的实战案例。压缩包共477个文件约5.26MB其中76个jsp页面承担视图层展示70个java文件实现业务逻辑与数据访问另有18个js、10个css及若干svg、png、jpg等前端资源15个xml与2个json负责配置1个sql文件提供建库建表与初始数据整体结构清晰、便于二次修改。目前已有786人学习下载读者可据此快速搭建可运行环境对照源码梳理MVC分层思路、数据库表设计与页面跳转逻辑并在此基础上扩展选课冲突校验、成绩录入等功能是课程设计阶段省时省力的参考方案。1. 从一份 JavaWeb 选课系统源码说起它到底能帮你省下多少事如果你正在做课程设计或者刚学完 Servlet 和 JSP 想找个完整项目练手大概率绕不开「学生选课系统」这个题目。它几乎是 JavaWeb 教学里出现频率最高的综合案例没有之一。原因很直接选课这件事本身有清晰的角色划分管理员、教师、学生、有典型的多对多关系学生与课程、有事务需求选课不能超容量、还有增删改查的完整闭环。一个项目能把 JavaWeb 的核心知识点全部串起来而这份「源码数据库」的组合省掉的恰恰是最耗时的建表和页面拼装工作。但我要先说清楚一件事拿到源码不等于学会。我见过太多人把项目导入 IDEA 跑起来看到登录页能打开就以为万事大吉结果答辩时被问「选课冲突怎么判断的」直接卡住。所以这篇不是告诉你「有个源码快拿去用」而是把这份选课系统从数据库设计到业务逻辑、从环境搭建到二次修改的完整路径拆开讲。适合两类人一是课程设计需要快速搭出可运行系统的同学二是想通过一个真实项目理解 JavaWeb 分层架构的自学者。下面从数据库表结构开始一步步走到你能自己改需求。2. 选课系统的数据库设计与建表脚本五张表撑起全部业务2.1 为什么是五张表而不是三张很多人第一反应是「学生表、课程表、选课记录表」三张就够了。但真实的选课系统里教师也是一类需要登录的角色而且课程必须绑定授课教师。所以最小可用的表结构是五张学生表、教师表、课程表、选课记录表、管理员表。这里有个设计决策值得说清楚——管理员要不要单独建表常见做法是单独建因为管理员的权限边界和普通用户完全不同混在学生表里加一个 role 字段虽然省事但后期做权限拦截时容易出玄学问题。选课记录表是整张数据库的核心。它本质上是一张关联表但比普通的中间表多了选课时间、成绩等字段。学生和课程之间是多对多关系一个学生可以选多门课一门课可以被多个学生选这张表就是用来拆解这个多对多关系的。下面直接给建表脚本。-- 创建数据库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE course_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_selection; -- 学生表学号作为唯一标识密码不存明文 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 密码, gender VARCHAR(4) DEFAULT 男 COMMENT 性别, major VARCHAR(50) COMMENT 专业, class_name VARCHAR(50) COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教师表工号作为唯一标识 CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY COMMENT 工号, name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 密码, title VARCHAR(20) COMMENT 职称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表外键关联教师容量字段用于控制选课人数 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) COMMENT 学分, capacity INT DEFAULT 50 COMMENT 容量上限, selected_count INT DEFAULT 0 COMMENT 已选人数, teacher_id VARCHAR(20) COMMENT 授课教师工号, schedule VARCHAR(100) COMMENT 上课时间, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表联合主键防止重复选课 CREATE TABLE selection ( student_id VARCHAR(20), course_id VARCHAR(20), select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 管理员表 CREATE TABLE admin ( admin_id VARCHAR(20) PRIMARY KEY, password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本里有几个参数值得单独说。utf8mb4而不是utf8是因为 MySQL 的 utf8 实际只支持三字节遇到某些生僻字会报错utf8mb4 才是真正的四字节 UTF-8。选课记录表用(student_id, course_id)做联合主键这是防止同一个学生重复选同一门课的第一道防线——数据库层面直接拒绝重复插入比在 Java 代码里先查再插要可靠得多。selected_count字段是一个冗余设计它的值等于该课程在 selection 表里的记录数加这个字段是为了避免每次查课程列表都去 count 一遍选课记录用空间换查询效率。2.2 初始化数据与自增主键的取舍建完表要插测试数据否则登录都进不去。管理员账号至少插一条学生和教师各插几条课程插五六条覆盖不同教师。这里注意密码字段如果源码里用的是 MD5 加密你插入的明文密码在登录时会验证失败。常见做法是先用明文存一条登录成功后看源码里的加密逻辑再决定是否批量改成密文。-- 初始化管理员密码假设源码用明文比对 INSERT INTO admin VALUES (admin, 123456); -- 初始化学生 INSERT INTO student VALUES (2021001, 张三, 123456, 男, 计算机科学, 计科2101), (2021002, 李四, 123456, 女, 软件工程, 软工2102); -- 初始化教师 INSERT INTO teacher VALUES (T001, 王老师, 123456, 副教授), (T002, 赵老师, 123456, 讲师); -- 初始化课程 INSERT INTO course VALUES (C001, 数据结构, 4.0, 50, 0, T001, 周一 1-2节), (C002, 操作系统, 3.5, 40, 0, T001, 周三 3-4节), (C003, 计算机网络, 3.0, 45, 0, T002, 周五 1-2节);关于主键学生表和教师表用的是业务主键学号、工号课程表也是业务主键课程号。这种设计在选课系统里是合理的因为学号和课程号本身就是天然唯一的而且查询时经常按学号查用业务主键省去了一次额外索引查找。但如果你要扩展成更复杂的系统比如允许学号变更那就该用自增 ID 加唯一索引。这是设计取舍不是对错问题。注意外键约束在批量导入数据时可能拖慢速度如果初始化数据量大可以先SET FOREIGN_KEY_CHECKS0导入完再设回 1。但生产环境不建议关。3. 用 IDEA 跑通项目从导入到登录成功的完整路径3.1 环境版本对齐别让 JDK 和 Tomcat 打架JavaWeb 项目跑不起来十次里有七次是环境版本不匹配。这份源码大概率是传统 Servlet JSP 架构不是 Spring Boot所以它需要外部的 Servlet 容器通常是 Tomcat。你需要确认三件事JDK 版本、Tomcat 版本、项目编译级别。常见组合是 JDK 8 配 Tomcat 8.5 或 9如果源码用了 JDK 11 的新语法Tomcat 也得相应升级。我一般会先看源码里有没有module-info.java或者var关键字快速判断它需要的最低 JDK 版本。IDEA 导入项目的步骤不复杂但有几个地方容易翻车。第一步是File - Open选中项目根目录如果根目录下有pom.xml就按 Maven 项目导入没有就按普通项目导入再手动配依赖。第二步是配置Project Structure里的 SDK 和语言级别确保和 Tomcat 的运行环境一致。第三步是配置 Artifacts这一步最容易被忽略——没有 ArtifactTomcat 根本不知道要部署什么。# 检查本机 JDK 版本 java -version # 检查 Tomcat 版本进入 Tomcat 的 bin 目录 cd /path/to/tomcat/bin ./version.sh # Linux/Mac version.bat # Windows上面两条命令是排查环境问题的起点。如果java -version显示的是 JDK 17而项目编译级别是 8通常能跑但可能有警告反过来如果项目用了 JDK 8 没有的 API编译直接报错。Tomcat 版本和 Servlet 规范版本挂钩Tomcat 9 对应 Servlet 4.0Tomcat 10 对应 Servlet 5.0而 Servlet 5.0 把javax.servlet包名改成了jakarta.servlet。如果你的源码里 import 的是javax.servlet那 Tomcat 10 会直接报 ClassNotFound必须降到 Tomcat 9。这是血泪经验很多人在这卡半天。3.2 数据库连接配置与驱动放置项目能编译不代表能连数据库。JavaWeb 项目通常把数据库配置写在db.properties或c3p0-config.xml里你需要改成自己本机的 MySQL 地址、端口、库名、用户名和密码。MySQL 8 和 MySQL 5 的驱动类名不同5.x 是com.mysql.jdbc.Driver8.x 是com.mysql.cj.jdbc.Driver而且 8.x 的连接 URL 要加时区参数否则报时区错误。# db.properties 示例 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_selection?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password你的密码serverTimezoneAsia/Shanghai这个参数在 MySQL 8 里几乎是必须的不加会抛The server time zone value is unrecognized。useSSLfalse是关掉 SSL 连接本地开发无所谓生产环境应该开启。characterEncodingutf8保证中文不乱码。驱动 jar 包要放到WEB-INF/lib目录下如果是 Maven 项目就加依赖坐标。放错位置的表现是编译通过但运行时报No suitable driver found这时候先检查 jar 包在不在WEB-INF/lib里再看驱动类名写对没有。配置完这些启动 Tomcat访问http://localhost:8080/项目名/应该能看到登录页。如果 404检查 Artifact 的上下文路径如果 500看 Tomcat 日志里的堆栈大概率是数据库连不上或者某个类没找到。4. 选课核心逻辑事务、并发与容量控制的实现细节4.1 选课不是一条 INSERT 那么简单表面上看选课就是往 selection 表插一条记录。但实际业务里至少要满足三个条件课程没满、学生没重复选、课程存在。这三个检查加插入必须在一个事务里完成否则并发场景下会出问题。想象两个学生同时选最后一门容量剩 1 的课两个线程都查到selected_count capacity都执行插入结果课程超员。这就是典型的并发漏洞。常见做法是在 Service 层用 JDBC 事务包裹或者用数据库的行锁。下面给一个基于 JDBC 的事务实现这是传统 JavaWeb 项目里最直接的写法。public boolean selectCourse(String studentId, String courseId) { Connection conn null; PreparedStatement checkPs null; PreparedStatement insertPs null; PreparedStatement updatePs null; ResultSet rs null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁定课程行检查容量FOR UPDATE 加行锁 String checkSql SELECT capacity, selected_count FROM course WHERE course_id ? FOR UPDATE; checkPs conn.prepareStatement(checkSql); checkPs.setString(1, courseId); rs checkPs.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; // 课程已满 } // 2. 插入选课记录联合主键会拦截重复选课 String insertSql INSERT INTO selection(student_id, course_id) VALUES(?, ?); insertPs conn.prepareStatement(insertSql); insertPs.setString(1, studentId); insertPs.setString(2, courseId); insertPs.executeUpdate(); // 3. 更新已选人数 String updateSql UPDATE course SET selected_count selected_count 1 WHERE course_id ?; updatePs conn.prepareStatement(updateSql); updatePs.setString(1, courseId); updatePs.executeUpdate(); conn.commit(); // 三步都成功才提交 return true; } catch (SQLIntegrityConstraintViolationException e) { // 联合主键冲突说明重复选课 try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(rs, checkPs, conn); DBUtil.close(null, insertPs, null); DBUtil.close(null, updatePs, null); } }这段代码的关键在FOR UPDATE。它会对查询到的课程行加排他锁另一个事务执行同样的SELECT ... FOR UPDATE时会阻塞直到第一个事务提交或回滚。这样就保证了容量检查的原子性。conn.setAutoCommit(false)开启事务后三条 SQL 要么全成功要么全回滚。捕获SQLIntegrityConstraintViolationException是处理重复选课的兜底——即使前面的检查漏了联合主键也会在数据库层面拒绝重复插入。参数方面studentId和courseId都来自前端表单必须用PreparedStatement的setString传参不要用字符串拼接否则有 SQL 注入风险。DBUtil是自定义的连接工具类通常从连接池获取连接用完必须归还否则连接池很快耗尽。4.2 退课与成绩录入的对称设计退课逻辑和选课相反删除 selection 记录同时把 course 表的selected_count减一。同样需要事务同样要注意并发。成绩录入则是教师角色的功能更新 selection 表的 score 字段。这两个功能经常被课程设计忽略但答辩时老师很可能会问「退课怎么实现的」。public boolean dropCourse(String studentId, String courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 删除选课记录 String delSql DELETE FROM selection WHERE student_id ? AND course_id ?; PreparedStatement delPs conn.prepareStatement(delSql); delPs.setString(1, studentId); delPs.setString(2, courseId); int rows delPs.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 没有选过这门课 } // 已选人数减一用 GREATEST 防止减成负数 String updSql UPDATE course SET selected_count GREATEST(selected_count - 1, 0) WHERE course_id ?; PreparedStatement updPs conn.prepareStatement(updSql); updPs.setString(1, courseId); updPs.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(null, null, conn); } }GREATEST(selected_count - 1, 0)是一个防御性写法保证计数不会变成负数。虽然正常流程下不会出现负数但如果有人手动改了数据库或者并发异常这个函数能兜住。退课功能还要考虑一个业务规则如果课程已经有成绩了还能不能退常见做法是成绩不为空时禁止退课这个判断加在删除之前即可。5. 二次开发与避坑那些源码不会告诉你的问题5.1 中文乱码的三层排查乱码是 JavaWeb 项目的高频问题而且可能出现在三个层面数据库层、请求层、响应层。数据库层看建库建表时字符集是不是 utf8mb4请求层看 POST 请求有没有设request.setCharacterEncoding(UTF-8)GET 请求在 Tomcat 8 以后默认 URI 编码是 UTF-8但老版本需要改 server.xml 的 Connector 加URIEncodingUTF-8响应层看response.setContentType(text/html;charsetUTF-8)有没有设。三层都对了才不会乱码缺一层就前功尽弃。5.2 连接池耗尽与连接泄漏现象是系统用一段时间后所有数据库操作都卡住日志里报Cannot get a connection, pool exhausted。原因通常是某个 DAO 方法里获取了连接但没关闭或者关闭写在了 try 块里而不是 finally 块异常一抛就跳过了关闭。解决方法是统一用DBUtil.close()在 finally 里关或者用 try-with-resources 语法。检查的时候重点看那些有多个 return 分支的方法每个分支都要保证连接被释放。5.3 JSP 页面里嵌 Java 代码的维护噩梦很多课程设计源码在 JSP 里直接写% ... %脚本段查数据库这在演示时没问题但你要改需求时会非常痛苦。常见做法是把数据库操作全部抽到 Servlet 或 Service 里JSP 只负责展示。如果你拿到的源码是 JSP 里嵌 JDBC 的写法建议先不动它等跑通之后再逐步重构。重构顺序是先把 JDBC 代码抽到单独的 Java 类再让 JSP 通过 request 属性拿数据。5.4 外键约束导致的删除失败现象是删除一门课程时报Cannot delete or update a parent row: a foreign key constraint fails。原因是 selection 表里有学生选了这门课外键挡住了删除。解决方式有两种先删选课记录再删课程或者把外键的删除策略改成ON DELETE CASCADE。但级联删除有风险删课程会连带删掉所有选课记录和成绩生产环境要谨慎。课程设计里更安全的做法是先检查有没有人选有人选就不让删。5.5 Tomcat 端口占用与热部署失效启动 Tomcat 报Address already in use: bind说明 8080 端口被占了。Windows 上用netstat -ano | findstr 8080找到进程号再taskkillLinux 上用lsof -i:8080再kill。另一个常见问题是改了 Java 代码后热部署不生效页面还是旧的。这是因为 IDEA 默认不会自动编译需要在Settings - Build - Compiler里勾选Build project automatically然后在 Tomcat 配置里确认On Update action设成了Update classes and resources。6. 把选课系统改成你自己的项目三个可落地的扩展方向拿到源码跑通只是起点真正让课程设计出彩的是你能在它基础上加东西。我一般会从三个方向扩展难度递增你可以按时间选一个做。第一个方向是加选课时间窗口控制。在 course 表加select_start和select_end两个 DATETIME 字段选课方法里先判断当前时间是否在窗口内。这个改动小但业务意义明确答辩时能讲清楚「为什么需要时间窗口」——防止学生在成绩出来后才选课刷分。实现上只需要在selectCourse开头加一个时间比较参数从数据库读出来用LocalDateTime解析。第二个方向是加选课冲突检测。同一时间段只能选一门课这需要解析 course 表的schedule字段。如果 schedule 存的是「周一 1-2节」这种文本解析起来麻烦更好的做法是拆成day_of_week和time_slot两个字段。冲突检测的逻辑是查该学生已选课程的上课时间和新课程比对有重叠就拒绝。这个功能能让你的系统从「能跑」变成「像个真系统」。第三个方向是加简单的权限拦截。用 Filter 实现登录校验未登录的请求一律跳转登录页。再进一步用 Filter 区分学生、教师、管理员三种角色的访问路径。比如/admin/*只有管理员能访问/teacher/*只有教师能访问。Filter 的配置在web.xml里或者用WebFilter注解核心是doFilter方法里检查 session 中的角色字段。WebFilter(urlPatterns {/admin/*, /teacher/*, /student/*}) 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; HttpSession session request.getSession(false); // 未登录一律跳登录页 if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } String role (String) session.getAttribute(role); String path request.getRequestURI(); // 角色与路径不匹配则拒绝 if (path.contains(/admin/) !admin.equals(role)) { response.sendError(403, 无权访问); return; } if (path.contains(/teacher/) !teacher.equals(role)) { response.sendError(403, 无权访问); return; } chain.doFilter(req, resp); } }这个 Filter 的urlPatterns覆盖了三个角色目录doFilter里先查 session 有没有登录标记再比对角色和路径。request.getSession(false)表示不自动创建新 session避免为未登录用户创建无用 session。sendError(403)比跳转登录页更明确告诉调用方是权限不足而不是没登录。我自己的习惯是每次拿到一份源码先不改任何代码原样跑通一遍把登录、增删改查、选课退课全部点一遍确认基线可用。然后再建一个 git 分支所有改动都在分支上做改崩了随时回退。这个习惯帮我省了太多后悔药。选课系统这个题目虽然老但把事务、并发、权限、前后端交互都串起来了认真做一遍比看十篇教程都管用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑