ARTICLE DETAIL

资讯详情

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

Java web学生选课系统课程设计:数据库设计与并发选课避坑指南

Java web学生选课系统课程设计:数据库设计与并发选课避坑指南 简介这份资源是面向高校计算机相关专业学生的Java Web课程设计完整交付包围绕学生选课系统展开适合正在做数据库原理或Web开发课程设计、需要一套可运行参考项目的同学。压缩包共164个文件约5.37MB以57个Java源文件、36个JSP页面为核心配合11个SQL脚本、11个XML配置、14个JAR依赖以及CSS、JS、图片等前端资源另附课程设计报告文档覆盖从建库建表到前后端实现的完整链路。系统按需求划分基本信息查询、学生与课程信息维护、学生选课及系统维护四个子系统涉及学生、课程、学习三张基本表并在选课环节体现参照完整性与用户自定义完整性约束可作为理解数据库设计与Java Web整合的实践样本。目前已有4582人学习下载读者可据此对照需求梳理表结构、理清MVC分层与页面跳转逻辑并参考报告组织自己的设计文档与答辩材料。1. 学生选课系统为什么成了 Java web 课程设计的“硬骨头”每年一到课程设计季Java web、学生选课系统、源码、数据库这几个词就会被反复搜。表面上看这个题目简单得不像话学生选课、教师开课、管理员管人三张表就能跑起来。但真正动手的人会发现它几乎是本科阶段能遇到的最完整的 CRUD 综合题——你要同时处理并发选课、容量控制、时间冲突、权限分级还要把数据库设计得经得起答辩老师追问。很多人卡在“能跑但一选就超员”“页面能打开但数据对不上”这类问题上最后交上去的源码自己都不敢再看第二遍。这篇笔记面向正在做课程设计的学生也面向想拿一个完整 Java web 项目练手的初学者。我会按“先想清楚数据模型再搭后端再补前端最后处理并发和排错”的顺序把一套能直接复现的选课系统讲透。数据库用 MySQL后端用 Servlet JDBC 或 Spring Boot 都行我会把两种路线的取舍讲明白。你照着做至少能拿到一个逻辑自洽、答辩时不怕被问的版本。2. 先把数据库设计对选课系统的表结构与三个关键约束2.1 五张核心表怎么拆选课系统的数据模型新手最容易犯的错是把所有东西塞进一张表。正确的拆法是围绕“谁、选什么、选上了没”来分。我一般会建五张表学生表、教师表、课程表、开课表、选课记录表。注意课程和开课是两回事——课程是“数据结构”这门课开课是“2024 秋季张老师开的数据结构”容量、时间、地点都挂在开课上。-- 学生表 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE, -- 学号登录用 name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, -- 存哈希别存明文 major VARCHAR(50), grade INT ); -- 教师表 CREATE TABLE teacher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tno VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, title VARCHAR(30) -- 职称 ); -- 课程表课程本身不含开课信息 CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE, cname VARCHAR(80) NOT NULL, credit DECIMAL(3,1) NOT NULL, hours INT ); -- 开课表某学期某老师开某门课 CREATE TABLE course_offering ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 50, selected INT NOT NULL DEFAULT 0, -- 已选人数冗余字段 day_of_week TINYINT, -- 1-7 start_section TINYINT, -- 第几节开始 end_section TINYINT, FOREIGN KEY (course_id) REFERENCES course(id), FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); -- 选课记录表 CREATE TABLE enrollment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, offering_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1, -- 1 已选 0 已退 UNIQUE KEY uk_stu_off (student_id, offering_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (offering_id) REFERENCES course_offering(id) );这段建表语句里有三个地方是血泪经验换来的。第一enrollment上的唯一索引uk_stu_off必须加它从数据库层面挡住“同一个人重复选同一门课”比在 Java 里查一遍再插要可靠得多。第二course_offering.selected是冗余字段用空间换查询速度列表页不用每次去 count 选课记录。第三status用逻辑删除而不是物理删除退课只改状态方便统计和恢复。2.2 容量控制为什么不能只靠 Java 判断很多人写选课逻辑是这样的先select selected, capacity from course_offering where id?在 Java 里判断selected capacity然后update ... set selected selected 1。这个写法在单机、低并发下没问题但课程设计答辩时老师最爱问的就是“两个人同时选最后一个名额怎么办”。答案是会超员因为查询和更新之间有窗口。正确做法是把判断和更新合并成一条带条件的 SQLUPDATE course_offering SET selected selected 1 WHERE id ? AND selected capacity;执行后看返回的影响行数如果是 0说明名额已满或课程不存在直接告诉用户选课失败。这一条语句在 InnoDB 行锁的保护下是原子的不需要额外加锁。参数说明id是开课记录主键selected capacity是容量约束返回值为 1 表示占位成功为 0 表示失败。这个技巧在数据库课程设计里非常加分因为它体现了你对并发的基本理解。2.3 时间冲突检测的 SQL 写法选课系统另一个必做功能是防止同一时间段选两门课。假设一节课占start_section到end_section冲突条件是同一天且区间有重叠。重叠的判定是a.start b.end AND b.start a.end。SELECT COUNT(*) FROM enrollment e JOIN course_offering o ON e.offering_id o.id WHERE e.student_id ? AND e.status 1 AND o.day_of_week ? -- 待选课程的星期 AND o.start_section ? -- 待选课程的结束节次 AND ? o.end_section; -- 待选课程的开始节次四个占位参数依次是学生 id、待选课程的星期、待选课程的结束节次、待选课程的开始节次。返回大于 0 就说明冲突。注意这里只查status 1的记录退掉的课不参与冲突判断。这个查询配合前面的容量更新就构成了选课接口的核心校验。3. 后端接口怎么落地从登录到选课的一条完整链路3.1 用 Servlet JDBC 还是 Spring Boot课程设计常见的两种路线一种是纯 Servlet JSP JDBC另一种是 Spring Boot MyBatis。我的建议是如果学校要求“体现 Java web 基础”用 Servlet 路线因为答辩老师能看到你手写doGet/doPost、手写 JDBC 连接如果允许自由发挥用 Spring Boot开发快、代码少、更容易做出完整功能。两条路线的数据库设计完全一样区别只在控制器和持久层。下面用 Servlet 路线讲因为它是“Java web”这个词最本分的含义也最容易在答辩时讲清楚每一层在干什么。项目结构按controller、service、dao、entity分包别把所有代码堆在一个 Servlet 里。3.2 登录接口与密码处理登录的本质是拿学号/工号查库比对密码。密码绝对不要明文存用 SHA-256 加盐或者 BCrypt。课程设计里用MessageDigest做 SHA-256 就够了重点是让老师看到你有这个意识。// 登录 Servlet 核心逻辑 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String sno req.getParameter(sno); String rawPwd req.getParameter(password); String hashed DigestUtils.sha256Hex(rawPwd salt_2024); // 加盐 Student stu studentDao.findBySnoAndPwd(sno, hashed); if (stu null) { req.setAttribute(msg, 学号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(currentUser, stu); req.getSession().setAttribute(role, student); resp.sendRedirect(req.getContextPath() /student/courses); }逻辑说明先取参数再哈希再查库。查不到就回登录页带错误提示查到了把用户对象放进 session。参数说明sno是学号password是前端传来的明文salt_2024是固定盐值真实项目应每用户随机盐课程设计固定盐可接受但要能解释。注意 session 里存的是对象不是密码别把敏感信息塞进去。3.3 选课接口的完整事务选课接口要同时做四件事查是否已选、查时间冲突、占容量、写选课记录。这四步必须在一个事务里否则会出现“容量加了但记录没写”的脏数据。public String enroll(Long studentId, Long offeringId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 是否已选唯一索引兜底这里先查给友好提示 if (enrollmentDao.exists(conn, studentId, offeringId)) { conn.rollback(); return 你已经选过这门课; } // 2. 时间冲突 if (enrollmentDao.hasTimeConflict(conn, studentId, offeringId)) { conn.rollback(); return 与已选课程时间冲突; } // 3. 占容量靠影响行数判断 int rows offeringDao.tryOccupy(conn, offeringId); if (rows 0) { conn.rollback(); return 名额已满; } // 4. 写选课记录 enrollmentDao.insert(conn, studentId, offeringId); conn.commit(); return 选课成功; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明整个方法是一个事务单元任何一步失败都回滚。参数说明studentId和offeringId是入参tryOccupy返回影响行数exists和hasTimeConflict都接收同一个conn以保证在事务内。注意conn必须从外部传入 DAO不能让每个 DAO 自己开连接否则事务不生效——这是新手最常翻车的地方。3.4 退课与容量回退退课不是删记录而是把status改成 0同时把selected减一。这两步也要在一个事务里并且减容量时要防止减成负数。-- 退课第一步改状态影响行数为 1 才继续 UPDATE enrollment SET status 0 WHERE student_id ? AND offering_id ? AND status 1; -- 退课第二步回退容量加 selected 0 保护 UPDATE course_offering SET selected selected - 1 WHERE id ? AND selected 0;参数说明第一步的status 1保证只有已选记录能被退重复退课影响行数为 0直接返回“未选该课”。第二步的selected 0是防御性写法避免任何异常情况下容量变负。两步都成功才提交。4. 前端页面与权限控制三个角色看到的东西必须不一样4.1 用 Filter 做登录拦截和角色校验学生、教师、管理员三种角色能访问的 URL 必须隔离。最省事的做法是写一个AuthFilter在web.xml或注解里配置拦截路径。WebFilter(urlPatterns {/student/*, /teacher/*, /admin/*}) public class AuthFilter implements Filter { 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(currentUser) 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); } }逻辑说明先判断有没有登录再判断角色和路径是否匹配。参数说明urlPatterns覆盖三个角色目录session.getAttribute(role)在登录时写入。注意getSession(false)表示不新建 session避免为未登录用户创建无用会话。4.2 课程列表页的分页与状态显示学生选课页要显示课程名、教师、时间、容量、已选人数还要标出“已满”“已选”“冲突”。这些状态最好在服务端算好前端只负责渲染别把逻辑丢给 JavaScript。// 查询开课列表带学生维度的状态 String sql SELECT o.id, c.cname, t.name AS tname, o.day_of_week, o.start_section, o.end_section, o.capacity, o.selected, (SELECT COUNT(*) FROM enrollment e WHERE e.offering_id o.id AND e.student_id ? AND e.status 1) AS my_selected FROM course_offering o JOIN course c ON o.course_id c.id JOIN teacher t ON o.teacher_id t.id WHERE o.semester ? ORDER BY o.day_of_week, o.start_section LIMIT ?, ?;参数说明第一个?是当前学生 id用于子查询判断“我选没选”第二个?是学期LIMIT ?, ?是分页偏移和条数。逻辑说明用子查询而不是 JOIN 是为了避免一个学生多条记录导致行数膨胀。前端拿到my_selected和selected/capacity就能渲染出“已选”“已满”标签。4.3 教师端录入成绩与管理员端统计教师端最核心的功能是查看自己开的课、查看选课学生名单、录入成绩。成绩字段可以加在enrollment表上加一个score DECIMAL(5,1)。管理员端则是增删改查学生、教师、课程、开课以及一个简单的统计每门课选课人数、每个学生已选学分。-- 学生已选学分统计 SELECT s.sno, s.name, SUM(c.credit) AS total_credit FROM student s JOIN enrollment e ON s.id e.student_id AND e.status 1 JOIN course_offering o ON e.offering_id o.id JOIN course c ON o.course_id c.id GROUP BY s.id, s.sno, s.name;参数说明status 1只统计有效选课SUM(c.credit)是学分合计。这个查询在答辩时很实用能说明你理解多表连接和聚合。5. 避坑与排查选课系统最容易翻车的五个地方5.1 现象选课成功但列表里已选人数没变原因通常是容量更新和选课记录写入不在同一个事务或者 DAO 各自开了连接导致事务失效。解决确保enroll方法里所有 DAO 操作共用同一个Connection并且setAutoCommit(false)在业务方法开头调用commit在最后调用。5.2 现象两个人同时选最后一个名额结果都选上了原因是先查后改中间有并发窗口。解决把容量判断写进UPDATE ... WHERE selected capacity用影响行数判断是否占位成功。这是选课系统最经典的并发坑答辩必问。5.3 现象退课后容量变成负数原因是退课接口没有校验当前状态重复调用导致多次减一。解决退课第一步UPDATE enrollment SET status 0 WHERE ... AND status 1只有影响行数为 1 才执行第二步减容量并且减容量时加selected 0。5.4 现象中文课程名在页面显示乱码原因是数据库、连接、页面三处字符集不一致。解决建库时用utf8mb4JDBC URL 加useUnicodetruecharacterEncodingutf8JSP 页面pageEncodingUTF-8Servlet 响应setContentType(text/html;charsetUTF-8)。四处对齐就不会乱码。5.5 现象登录后刷新页面就退出登录原因是 session 超时时间太短或者重定向时丢了 cookie。解决在web.xml里配置session-configsession-timeout30/session-timeout/session-config并检查浏览器是否禁用了 cookie。课程设计里 30 分钟足够。6. 让答辩加分的两个进阶技巧乐观锁与接口幂等6.1 用版本号做乐观锁替代行锁前面用UPDATE ... WHERE selected capacity是悲观思路靠行锁。另一种做法是给course_offering加一个version INT字段更新时带上版本号UPDATE course_offering SET selected selected 1, version version 1 WHERE id ? AND version ? AND selected capacity;参数说明第二个?是查询时读到的版本号。影响行数为 0 说明期间有人改过重试或提示失败。这个写法在答辩时能引出“乐观锁 vs 悲观锁”的讨论是加分项。注意课程设计并发量低两种都能用但你要能说清区别。6.2 选课接口的幂等设计幂等是指同一个请求执行多次结果和执行一次一样。选课接口天然需要幂等学生手抖点两次“选课”不能扣两次容量。实现靠enrollment表的唯一索引uk_stu_off插入时捕获DuplicateKeyException直接返回“已选过”。这样即使前端重复提交数据库也不会产生两条记录。try { enrollmentDao.insert(conn, studentId, offeringId); } catch (DuplicateKeyException e) { conn.rollback(); return 你已经选过这门课; }逻辑说明唯一索引是幂等的最后一道防线前面的exists查询只是友好提示真正兜底的是数据库约束。参数说明DuplicateKeyException是 Spring 的异常纯 JDBC 下捕获SQLIntegrityConstraintViolationException。6.3 一个验证清单交作业前自己跑一遍验证项操作预期结果重复选课同一学生连点两次选课第二次提示已选容量只加 1容量边界把容量改成 1两个学生同时选只有一个成功另一个提示已满时间冲突选两门同天同节次的课第二门提示冲突退课回退选课后退课再看容量容量减 1记录状态为 0权限隔离学生直接访问 /admin/ 路径返回 403字符集课程名含中文增删改查无乱码这张表我每次交课程设计前都会跑一遍能挡掉八成“演示时翻车”的情况。尤其是容量边界那条自己用两个浏览器或者写个简单循环就能测。做课程设计这些年我最大的习惯是先把数据库约束写死再写 Java 逻辑。因为 Java 里的判断可能被绕过、可能漏写但数据库的唯一索引和条件更新不会骗你。选课系统看着简单真正把它做扎实你对事务、并发、权限的理解就上了一个台阶。希望帮到你。本文还有配套的精品资源点击获取
返回列表