尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于JavaWeb的学生选课系统毕设实战:数据库建模与并发控制解析

发布时间:2026/9/28 15:51:41

资讯中心
01
ARTICLE

基于JavaWeb的学生选课系统毕设实战:数据库建模与并发控制解析

基于JavaWeb的学生选课系统毕设实战:数据库建模与并发控制解析
简介一套基于JavaWeb的学生选课系统完整毕业设计资料包面向计算机相关专业正在准备毕设的学生也适用于需要项目实战练习的Java开发人群。项目以Servlet、JSP、JDBC、DbUtils构建后台结合EasyUI、jQuery、Ajax完成交互界面数据库采用MySQL系统涵盖学生、教师、系统管理员三种角色包含学生信息、选课、考勤、请假、成绩等完整管理模块功能完善且界面美观操作体验流畅。资料包共13个文件整体大小约7.67MB包含项目源码zip压缩包、MySQL数据库脚本sql、项目文档pdf与md、系统运行截图png以及软件工具下载说明txt其中截图可直观了解各功能模块的实际效果。源码经过严格调试可直接运行数据库脚本可快速完成初始化配套文档便于理解项目整体结构。已有6130人学习下载系统操作简单、管理便捷既适合作为毕业设计参考选题也可作为JavaWeb项目开发实战练习的优质资料。1. 基于JavaWeb的学生选课系统这个毕设到底在做什么做JavaWeb毕设的同学十个里有八个绕不开“学生选课系统”这个题目。它业务完整、边界清楚选课、退课、成绩管理三条线都能讲明白又不像电商系统那样容易把项目做崩。也正因为它太常见“把源码跑起来”和“把项目做完”完全是两码事——数据库怎么建模、并发选课怎么防超容量、IDEA里Tomcat怎么配置每一步都有拦路石。这篇笔记就沿着这套基于JavaWeb的学生选课系统的源码把表结构、事务处理、部署流程和答辩追问一次讲透适合正在做毕设选题、或者拿了源码却不知道怎么落地的同学照着走。2. 需求拆解与数据库建模六张表撑起选课、退课与成绩管理做这类管理系统数据库设计永远不能省。选课系统的核心不是页面多好看而是“学生选课、教师开课、管理员排课”这条业务链在表上能不能自洽。2.1 三种角色的用例边界管理员、教师、学生各做什么学生端是最容易想当然的部分登录后看到课程列表点击选课点击退课期末查看成绩。这四件事背后都牵扯课程表的容量校验和选课表的唯一约束不是简单的增删改查。教师端要处理的是“开课信息”和“选课学生名单”。老师可以新增一门课程也可以在下课之后录入成绩。教师的操作直接影响学生的可用课程池所以教师表与课程表之间要保留外键关系但不建议把教师和管理员混在一张表里而是用角色字段区分登录后按角色跳转到不同主页。管理员负责的是基础数据维护学生账号的导入、教师账号的分配、学期开关。选课系统里最容易被忽略的是“学期”这个概念如果缺少学期字段补考重修的学生会产生业务上的数据混乱。因此课程表和选课表都要带semester字段哪怕默认只填一个值也要把字段位置留出来。2.2 核心表设计课程表容量字段与选课表设计思路课程表是整套系统的状态中枢。它除了课程名、学分、上课时间、授课教师之外一定得有capacity和selected_count这两个字段。capacity是课程容量上限selected_count是当前已选人数。为什么不实时用count()去统计选课表因为选课操作频繁时count()在MySQL里的开销会随选课记录增长越来越慢而且在事务里可读性差。维护冗余计数虽然牺牲了范式但在选课系统这种“读多写少但写有竞争”的场景里是常见的工程做法。选课表要格外注意主键设计。很多同学习惯用(student_id, course_id)做复合主键这在数据上没问题但在后续成绩录入和退课记录追踪时会比较别扭而且复合主键在MySQL的InnoDB里会直接影响聚簇索引结构。更稳的做法是选课表单独用自增id做主键同时给学生选课表加一个UNIQUE(student_id, course_id, semester)唯一索引唯一索引专门负责防重复选课。业务主键和数据主键分开这是我踩过几次坑之后的固定习惯。2.3 初始化SQL从建库到测试数据一步到位拿到源码之后第一步不是急着起Tomcat而是先把数据库跑起来。下面是这套系统最基础的建表SQL直接覆盖管理员、学生、教师、课程、选课、学期六张核心表。CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 学生主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, password VARCHAR(64) NOT NULL COMMENT 密码建议MD5后存储, name VARCHAR(50) NOT NULL, major VARCHAR(50) COMMENT 专业, grade VARCHAR(10) COMMENT 年级 ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(20) COMMENT 职称 ) ENGINEInnoDB COMMENT 教师表; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINEInnoDB COMMENT 管理员表; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL, credit TINYINT NOT NULL COMMENT 学分, teacher_id INT NOT NULL COMMENT 授课教师, capacity INT NOT NULL DEFAULT 0 COMMENT 容量上限, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, time_slot VARCHAR(50) NOT NULL COMMENT 上课时间段如 周一3-4节, semester VARCHAR(20) NOT NULL COMMENT 开课学期, KEY idx_teacher (teacher_id), KEY idx_semester (semester), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩选课阶段为空, UNIQUE KEY uk_stu_course (student_id, course_id, semester), KEY idx_course (course_id) ) ENGINEInnoDB COMMENT 选课表;这段DDL里有几个参数值得说明。ENGINEInnoDB是为了行级锁和事务支持选课场景下MyISAM会因为表级锁把并发完全锁死。utf8mb4比utf8多支持一部分生僻字和特殊符号用户名里的emoji在utf8下会直接报错。UNIQUE KEY uk_stu_course里加semester是为了秋季和春季学期都选同一门课时不会被误判为重复。password字段预留64位如果后面按MD5加盐处理32位会不够用。表建完之后要插入测试数据这个环节别偷懒。至少准备三个学生、两个教师、五门课其中一门课的capacity设为1或者已经满员专门用来验证选课容量控制。没有造满员的测试数据后面写选课事务时无法判断逻辑到底走没走通。3. 技术选型与项目骨架ServletJSP为什么是稳妥的毕设答案不少同学一上来就问能不能换Spring Boot。能换但建议先想清楚这个题目为什么固执地站在JavaWeb这边。3.1 选型理由课程大纲、答辩深度和源码可读性的平衡很多学校的JavaWeb课程大纲就是围绕Servlet、JSP、Filter、Listener来排的。用Spring Boot做选课系统框架替你做掉了大半的请求分发和数据封装答辩时老师一句“你讲讲Spring Boot自动配置的原理”就能让场面冷下来。ServletJSP这套老路线反而实在请求怎么进Servlet、Session怎么存用户、JDBC怎么拿连接每一环都裸露在代码里答得上就是答得上。另一个现实问题是源码可读性。Spring Boot项目结构相对固定但如果对starter、yml配置不熟改一个端口都要翻半天。JavaWeb的源码通常就三层——Filter/Controller、Service、Dao页面直接是JSP。对毕设来说“代码量看起来足够、结构又不算复杂”是最有价值的状态。Spring Boot当然更好写但更容易在答辩时陷入“框架替我做了我不知道它做了什么”的困境。3.2 项目目录结构Servlet三层模型怎么摆拿到源码后先看目录结构。规范一点的JavaWeb项目会按下面的方式组织这个结构在IDEA里直接就是一个标准的Web项目骨架。src ├── main │ ├── java │ │ └── com.course │ │ ├── controller │ │ │ ├── LoginServlet.java │ │ │ ├── LogoutServlet.java │ │ │ ├── SelectCourseServlet.java │ │ │ └── CourseServlet.java │ │ ├── service │ │ │ └── CourseService.java │ │ ├── dao │ │ │ ├── StudentDao.java │ │ │ └── CourseDao.java │ │ ├── entity │ │ │ ├── Student.java │ │ │ └── Course.java │ │ └── util │ │ └── DruidUtil.java │ ├── resources │ │ └── druid.properties │ └── webapp │ ├── WEB-INF │ │ └── web.xml │ ├── jsp │ │ ├── login.jsp │ │ ├── student_home.jsp │ │ └── teacher_home.jsp │ └── index.jsp └── pom.xmlcontroller层只负责接收请求、调用service、携带结果跳转页面service层处理事务和业务判断dao层只做SQL交互。很多JavaWeb入门项目把业务代码全写在Servlet里一个方法几十行确实也能跑但答辩时老师让说说系统结构一句“全写在Servlet里”会让项目档次掉一半。分层不用过度设计三层就够。JSP放在webapp目录下如果希望代码更干净就把JSP都放到WEB-INF里面通过Servlet forward过去。3.3 公共配置Druid连接池参数与JDBC工具类JavaWeb项目里最常用的连接池是Druid它自带监控、参数丰富性能也比手写连接池强得多。配置文件放在resources根目录下名字和加载路径要对应。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyourpass initialSize5 minIdle5 maxActive20 maxWait10000 testWhileIdletrue validationQuerySELECT 1这里最需要注意的是URL参数。serverTimezone必须指定MySQL 8不写它会直接报时区错误useSSLfalse是为了关掉没有必要的SSL握手本地开发能明显加快连接创建的耗时。initialSize和maxActive按毕设的量级配5和20就够maxWait太长会让用户感觉卡死10秒是合理上限。配套的Jdbc工具类长这样public class DruidUtil { private static DataSource ds; static { Properties p new Properties(); try (InputStream in DruidUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { p.load(in); ds DruidDataSourceFactory.createDataSource(p); } catch (IOException e) { throw new ExceptionInInitializerError(druid.properties 未找到); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }getResourceAsStream读取的是classpath根路径配合resources目录下的druid.properties使用即可。写成静态代码块是为了让连接池随类加载一次完成初始化避免每次请求都重新建池。连接池不是越大越好对选课系统这种几十个人同时用的场景20个连接已经覆盖峰值。4. 核心功能实现登录拦截、选课事务与容量控制功能实现是源码里最值得逐行读的部分。登录、选课、退课这三块写明白了这个项目就站得住。4.1 登录与Session管理一个Filter拦截所有未登录请求登录本身不复杂复杂的是登录后的状态保持。Servlet规范里做这件事的标准方案是Session加Filter过滤器。登录成功后把用户对象塞进Session然后写一个LoginFilter拦截所有需要登录的路径。WebFilter(/*) public class LoginFilter implements Filter { 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); // 放行登录页、登录接口和静态资源其余请求必须登录 String uri req.getRequestURI(); if (uri.endsWith(/login) || uri.endsWith(/login.jsp) || uri.contains(/static/)) { chain.doFilter(request, response); return; } if (session null || session.getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp?target uri); return; } chain.doFilter(request, response); } }这里有一个细节getSession(false)不会主动创建Session如果直接getSession()未登录请求也会新建一个空SessionFilter判断永远不成立。放行条件里不能只放行login.jsp还要放行login这个Servlet路径否则登录请求也会被拦截。重定向时带target参数属于锦上添花登录成功后再跳回去这个体验细节在答辩演示时很加分。4.2 选课核心逻辑SELECT ... FOR UPDATE 防止超选选课是整个系统的核心也是最容易在答辩时被追问的地方。先查容量再插入是最直观的写法但两个人同时选最后一门课时都读到已选人数19然后都插入成功最后课程超员。解决这一个问题比做完三个页面的加分都大。下面是选课service的完整代码public boolean selectCourse(int studentId, int courseId) { String selectForUpdateSql SELECT capacity, selected_count FROM course WHERE id ? FOR UPDATE; String insertSql INSERT INTO student_course (student_id, course_id, semester) VALUES (?, ?, ?); String updateCountSql UPDATE course SET selected_count selected_count 1 WHERE id ?; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DruidUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁住课程行其他选课事务必须等这个事务提交 ps conn.prepareStatement(selectForUpdateSql); ps.setInt(1, courseId); rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity rs.getInt(capacity); int selectedCount rs.getInt(selected_count); // 2. 容量判断在锁内做防止读到过期数据 if (selectedCount capacity) { conn.rollback(); return false; } // 3. 插入选课记录重复选课会触发唯一索引异常 ps conn.prepareStatement(insertSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.setString(3, 2024-2025-1); ps.executeUpdate(); // 4. 更新冗余计数 ps conn.prepareStatement(updateCountSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} // 唯一索引冲突会被这里捕获表现为“已选过这门课” return false; } finally { close(rs, ps, conn); } }FOR UPDATE这行是防超选的关键。它把课程行锁住事务提交前其他事务的相同查询只能等锁于是容量判断和插入操作变成原子操作。这种方式叫悲观锁适合选课这种写冲突概率高的场景。如果嫌锁粒度大也可以用UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity用affected rows判断是否超容量这种方式不需要显式锁表但需要把容量判断从业务层挪到SQL层。record插入时会撞上student_course表的唯一索引uk_stu_course这个异常不需要提前判断索引兜底比代码判断更可靠。两个操作放在同一个事务里commit或rollback保持数据一致。这种“锁行 冗余计数 唯一索引”的三层防御就是答辩时可以拿出手的并发方案。4.3 退课、补选与时序问题退课逻辑相对简单但同样要放在事务里先删除student_course中的记录再把course表的selected_count减一。顺序不要反过来。一旦删除成功但更新计数失败只会出现“已选人数虚高”不会出现“学生明明没选却占用名额”的严重错误。除此之外还有两个边界条件值得写一是退课只允许在选课开放周期内进行管理员关闭选课后学生端应隐藏退课按钮二是当course表时间字段与student_course中已有课程的时间段相同时要在选课前做冲突检测。比如课程A是“周一3-4节”课程B也是“周一3-4节”这两门课不能同时选。这个逻辑可以在Service里查询学生已选课程的时间段后逐条比对也可以在插入前查一次课程表。在IDEA里运行这个JavaWeb项目常规操作路径是配置Tomcat Server把项目的war exploded包部署到Tomcat的Deployment里Application context一般填/course数据库先跑初始化SQL然后在IDEA里启动Tomcat浏览器访问http://localhost:8080/course/login.jsp。第一次跑通这条链路后面改代码都不用重启整个Tomcat热部署对JSP和Java类都生效。5. 避坑指南JavaWeb选课系统最常见的五个翻车点毕设跑不起来绝大多数问题不是逻辑难而是环境配置里的隐性坑。5.1 中文乱码从页面到数据库全部变问号现象登录后学生姓名显示成“???”或者一片乱码插入的中文课程名在Navicat里正常但页面上乱。原因多半不是单一环节引起的而是JSP、Servlet、MySQL三处编码不一致。开发的机器上IDEA文件编码默认可能是GBK而JSP页面声明的是UTF-8两边的字节流对不上。解决统一所有环节为UTF-8。JSP顶部写% page contentTypetext/html;charsetUTF-8 %Servlet在读取参数前执行request.setCharacterEncoding(UTF-8)MySQL连接URL带characterEncodingutf8数据库和表的字符集在建库时指定utf8mb4。排查时可以在关键位置各自打印一次看是哪一段开始变乱码。5.2 驱动程序类找不到现象启动Tomcat后访问登录页报ClassNotFoundException: com.mysql.cj.jdbc.Driver或者SMM项目中常见NoClassDefFoundError。原因IDEA里虽然把mysql-connector-java.jar加到了Project Structure的Libraries里但这种方式只在编译期生效运行期Tomcat用的是WEB-INF/lib下的jar包。如果jar没有随着Artifact一起打包进依赖里运行时就找不到驱动。解决把驱动jar包放到src/main/webapp/WEB-INF/lib目录下或者用Maven在pom.xml里引入依赖后重新build Artifact。注意MySQL 8以上版本的驱动类名是com.mysql.cj.jdbc.Driver不是老资料里的com.mysql.jdbc.Driver写错同样报ClassNotFound。5.3 Druid连接池初始化失败现象Tomcat日志里抛出“Error querying database”或者连接超时后面跟的提示很杂有时是Communications link failure有时是timezone。原因MySQL服务没启动、连接URL里的IP或端口写错、账号密码不对这三个占八成。剩下两成是MySQL 8的时区问题URL没带serverTimezone参数时总是提示连接被拒绝。连接池初始化在静态代码块里一旦加载失败整个应用启动就废了。解决先拿数据库工具连一下确认MySQL是可用的再检查druid.properties属性名拼写。特别警惕properties文件里value前后的空格properties文件解析不会自动trimpassword 123456和password123456是两个值。连接不上的时候把Druid的log级别临时调成DEBUG它会直接打印出连接失败的完整栈。5.4 并发选课把容量选超现象两个学生同时点击选课最后课程表capacity1却有两行student_course记录selected_count显示等于capacity但名单超员。原因代码里先SELECT查询已选人数再INSERT选课记录这两个操作之间没有事务隔离。A和B事务同时读到0人先后执行INSERT超员成立。如果用的是MyISAM引擎连行锁都没有问题必然发生。解决按第4章的写法在事务中先SELECT ... FOR UPDATE把课程行锁住再操作或者把容量判断写进UPDATE条件里。改完之后观察selected_count超出容量时插入必然失败。这个修复建议单独验证一次开两个浏览器窗口同时点选课看是否只有一个人成功。5.5 404与Servlet类型的ClassCastException现象启动后访问/login一直404或者Tomcat报错说Servlet不是Servlet类型。前者指向部署配置后者往往和Tomcat版本选错有关。原因404多半是Application context配错IDEA部署时context填的是/访问路径就变成//login干净的写法是保持context为/course登录地址写http://localhost:8080/course/login。ClassCastException则常见于把基于javax.servlet的老项目直接部署到Tomcat 10以上版本Tomcat 10把包名改成了jakarta.servlet老代码的WebServlet注解根本不会被识别。解决用Tomcat 9.0.x跑javax.servlet时代的JavaWeb项目。如果导师规定必须用新版Tomcat就把代码里所有javax.servlet批量替换成jakarta.servlet并把Servlet API依赖版本同步升级这会引入额外工作量非必要不建议。6. 进阶让选课系统在答辩现场“问不倒”代码能跑只是及格线答辩才是决定成绩的地方。准备答辩前建议先把一条“一镜到底”的演示脚本练熟管理员登录创建一门新课切到学生账号登录选这门课再切到教师账号录入成绩最后回到学生端看到成绩。这条线走顺了系统的完整性就能撑住前五分钟。被问“系统有哪些不足”时不要回答“没有不足”。三个比较体面的说法是选课事务目前用的是悲观锁高并发下锁等待偏多可以换成乐观锁或异步削峰课程冲突检测只在Service层做没有下沉到数据库约束分布式扩展时会有风险密码存储用的是加盐哈希但没做登录验证码和操作日志审计。每一条都说得出改进方向比“我做得都很好”有说服力得多。还有一个容易被忽视的动作用日志佐证并发控制。答辩前把Druid的SQL日志打开选课时让两个浏览器同时操作截图或者现场展示其中一条FOR UPDATE语句在等待锁然后再成功执行——这比嘴上说“我用了乐观锁”可信得多。最后是一个习惯建议拿到任何一套源码先把配置文件和SQL脚本通读一遍再启动我见过太多人跳过这一步结果在乱码和连接池上耗掉一整天。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。