
简介学生选课管理系统是一套面向教育机构及Java初学者的完整项目覆盖数据库设计、Java Web开发与学生信息管理三大环节。压缩包共15个文件约8.56MB其中SQL脚本提供学生表、课程表、选课表等核心表结构及示例数据两个ZIP分别封装项目源码与模板素材另含8张运行截图、Word设计文档和3个TXT说明便于对照环境配置与操作流程。系统具备学生注册登录、课程浏览、选课冲突检测、选课记录查询及成绩查询等功能并涉及数据备份、权限管理和性能优化思路。已有5365人学习下载适合正在做课程设计、毕业设计或想通过实际案例理解Java Web分层架构的开发者。通过这份资料不仅能掌握关系型数据库建表与CRUD操作还能学习如何将JDBC、Servlet和JSP整合进一个真实可用的管理系统。 又到课程设计的高峰期了。每年这个时候我都会收到不少关于“学生选课管理系统”的咨询表怎么建、代码怎么分层、演示的时候怎么讲才能不被老师问倒。这个题目的名字看起来普通但做完你就会发现数据库设计、JDBC事务、角色权限、并发控制、异常排查全都要碰一遍是一个浓缩度很高的综合型作业。这篇文章就基于我实际带人复现过的一套完整方案写出来数据库建表脚本、源码组织思路、核心代码片段都会讲到。如果你正准备用这个题目交一次高质量的课程设计或者单纯想搞懂一个“数据库 JavaWeb”应用是怎么从零落到代码上的这篇会比较对路。我先说结论这个项目想拿高分重点不在页面有多花哨而在选课的时候会不会“超容量”、删数据的时候会不会破坏完整性、事务回滚是不是真的生效。下面我按从设计到实现的顺序一层一层拆开讲。1. 开始之前选课管理系统到底要管什么1.1 三个角色与两条业务主线选课管理系统表面上就是“学生选课、老师看学生、管理员管课程”但落到具体业务上角色之间是有明确的权限边界的。学生登录系统、浏览当前学期可选的课程列表、选课、退课、查看自己已选课程和成绩。教师查看自己负责的课程、查看选课学生名单、录入和修改学生成绩。管理员维护学生和教师信息、开设课程、调整课程容量、删除课程、查看全局选课统计。两条核心业务主线一条是“学生与课程之间”的选课/退课关系另一条是“教师与课程之间”的归属关系。很多初做这个系统的人容易一上来就写增删改查结果写着写着发现逻辑乱掉就是因为没有先把这两条线的数据流捋清楚。我的建议是动手写代码前先拿一张纸把每个角色的行为路径画出来再对照数据库表去落字段。1.2 需求文档里被忽略的四个设计点课程设计的任务书通常只有几句话“实现学生在线选课、退课、成绩管理”。但真正写的时候下面这些细节才是区分高分和及格的关键第一选课容量。课程有容量上限比如60人。学生选课时如果已选人数等于容量必须拦截。第二重复选课。同一个学生不能选同一门课两次。第三先选课、后给成绩。成绩字段在选课记录表里一开始是空的只有教师录入后才非空。第四删除保护。一个课程如果已经有学生选了不能直接物理删除否则外键会报错业务上也说不通。这些点在需求描述里往往没有但它们直接决定了数据库表结构怎么设计、代码里事务边界画在哪里。我见过不少人做完以后演示时踩中“重复选课没拦截”或者“删课程直接报红”就是因为前期没有把这些规则固化成设计约束。下面我会展示怎么把这几条规则直接写进建表语句和业务逻辑里。2. 数据库设计把约束写进建表语句而不是留着后面补2.1 核心表结构与字段说明这套系统我用了四张核心表学生表、教师表、课程表、选课记录表。如果你还需要更完整的权限可以再拆一张管理员表不过一般课程设计里学生、教师、管理员三个角色共有四张表就够了。学生表t_studentCREATE TABLE t_student ( sid VARCHAR(20) PRIMARY KEY, sname VARCHAR(50) NOT NULL, gender CHAR(2), major VARCHAR(50), grade VARCHAR(10), password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;教师表t_teacherCREATE TABLE t_teacher ( tid VARCHAR(20) PRIMARY KEY, tname VARCHAR(50) NOT NULL, title VARCHAR(20), dept VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;课程表t_courseCREATE TABLE t_course ( cid VARCHAR(20) PRIMARY KEY, cname VARCHAR(100) NOT NULL, credit DECIMAL(3,1), type VARCHAR(20), tid VARCHAR(20), capacity INT NOT NULL, selected_count INT NOT NULL DEFAULT 0, semester VARCHAR(20), schedule VARCHAR(100), CONSTRAINT fk_course_teacher FOREIGN KEY (tid) REFERENCES t_teacher(tid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;选课记录表t_scCREATE TABLE t_sc ( sid VARCHAR(20), cid VARCHAR(20), score DECIMAL(5,2), select_time DATETIME, PRIMARY KEY (sid, cid), CONSTRAINT fk_sc_student FOREIGN KEY (sid) REFERENCES t_student(sid), CONSTRAINT fk_sc_course FOREIGN KEY (cid) REFERENCES t_course(cid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果你用的是 MySQL 8上面的脚本基本直接能跑。注意建表时统一指定ENGINEInnoDB只有 InnoDB 才支持外键和事务utf8mb4是为了让中文不乱码后面我会专门讲乱码排查。2.2 联合主键和外键为什么必须加我在t_sc表里用了PRIMARY KEY (sid, cid)也就是学生和课程两个字段组成联合主键。这样做的直接效果是同一个学生对同一门课的选课记录在数据库层面只能存在一条哪怕应用层代码漏写了重复判断数据库也会在插入时直接报主键冲突。这是第一道防重复的防线也是最可靠的一道。外键的作用是保护数据完整性。比如t_course里的tid外键指向教师表那么录入课程时如果填了一个不存在的教师工号数据库直接拒绝写入。t_sc里的两个外键则保证“选课记录里的学生和课程必须真实存在”。这三个外键加完之后应用层代码可以少写很多无意义的过滤判断。有同学担心外键会影响性能在课程设计这个量级的数据下这种担心完全是多余的。我反而见过不少人为了“性能”去掉了外键结果演示时删除教师导致选课记录变得无主无凭场面非常尴尬。课程设计阶段请保留外键它能让你的系统解释起来更有底气。2.3 容量字段统计列与范式之间的权衡注意我在t_course里放了一个selected_count表示已选人数。从严格的第三范式角度看这个字段是冗余的因为它可以通过SELECT COUNT(*) FROM t_sc WHERE cid ?实时统计出来。但这里我故意保留冗余。原因是选课场景下“当前还剩多少个名额”是个高频查询学生打开选课列表就要看一次。如果每次都用 Count 统计数据量稍大一点列表页就会变慢。用selected_count字段每次做一次原子自增或自减成本极低页面加载速度也快。这就是典型的“用可控的冗余换性能”也是课程设计的答辩加分点。只要你能够解释清楚为什么这么设计老师通常都会认可。当然冗余字段的前提是必须保证它和真实数据一致。怎么保证答案是把selected_count的更新和t_sc的插入放到同一个事务里要么一起成功要么一起失败。这个逻辑我放到第4部分重点讲。3. 源码组织课程设计场景下的技术选型与分层3.1 为什么我坚持用 Servlet JSP JDBC这个题目如果用 Spring Boot MyBatis 来做代码写起来确实舒服但那意味着你把大量精力花在了学习框架上而不是理解业务本身。课程设计的核心评价标准是数据库设计是否合理、业务逻辑是否完整、能否讲清楚原理。Servlet JSP JDBC 虽然“原始”但每一行代码都落到了实处老师问你什么你都能答出来因为你亲手卷过轮子。话虽如此如果你已经熟练掌握了 Spring Boot用它来做也没有问题只要你能把事务管理和数据库连接池的原理讲清楚。这篇文章后面的业务逻辑是通用的换成任何技术栈都一样。我的源码组织建议是基于 Servlet JSP 的经典三层结构它最直观。3.2 三层结构怎么分包每个类干什么我把源码按entity、dao、service、servlet、filter五个包组织。不要小看分包这是你代码能不能让老师一眼看懂的起点。entity对应数据库表的实体类Student、Teacher、Course、SC。dao负责和数据库交互每个实体一个 DAO只写 SQL 和执行 SQL。service业务逻辑层选课、退课、成绩录入这些有规则的操作都放这里。servlet接收 HTTP 请求调用 Service控制页面跳转。filter登录拦截器和字符编码过滤器。一个典型请求的流转过程是JSP 页面点击“选课”→ 请求到SelectCourseServlet→ Servlet 调用CourseService.selectCourse(sid, cid)→ Service 内部调用CourseDao和ScDao操作数据库 → 返回结果 → Servlet 转发到选课列表页并提示结果。这样做的好处是某一天你要改数据库字段只需要动 DAO要改业务规则只需要动 Service要调整页面跳转只需要动 Servlet。每一层职责单一答辩时被问到某个功能在哪个类实现你三秒钟就能指给对方看。3.3 数据库连接与字符编码的工程细节导航栏清单数据库连接用 JDBC 的DriverManager获取连接但注意在整个应用启动时只加载一次驱动。写一个DBUtil工具类统一封装getConnection()、close()方法避免每个 DAO 里重复写一堆雷同的样板代码。所有带用户输入的 SQL 一律用PreparedStatement不要用字符串拼接。比如登录查询写成SELECT * FROM t_student WHERE sid ? AND password ?而不是SELECT * ... WHERE sid sid 。前者能有效防止 SQL 注入后者会在答辩时被问得很难看。字符编码方面建议写一个EncodingFilter对所有请求统一设置request.setCharacterEncoding(UTF-8)对所有响应统一设置response.setContentType(text/html;charsetUTF-8)。同时JDBC 连接串里要加上characterEncodingUTF-8。关于数据库连接池课程设计不强制要求但如果你愿意用一个内置连接池效果会更好。比如DruidDataSource它不但能管理连接还自带监控页面。你可以这样初始化DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/student_course?characterEncodingUTF-8serverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); dataSource.setMaxActive(10);这几个参数的设置思路是初始连接数 5不够用再增长最多不超过 10。对于一个几十人同时访问的课程设计系统这个池子完全够用。等你讲清楚连接池的原理——为什么要复用连接而不是每次新建答辩印象分又能往上走一截。4. 选课和退课的核心实现事务边界就是业务边界4.1 第一版代码为什么会写出“超容量选课”选课这个场景最容易翻车的就是并发。我先用一个反例来说明问题。很多新手写完的业务逻辑是这样的// 1. 查询当前已选人数 int count courseDao.getSelectedCount(cid); // 2. 如果没满就插入选课记录 if (count capacity) { scDao.insert(sid, cid); courseDao.increaseCount(cid); return 选课成功; } return 课程已满;这段代码在单用户测试的时候完全没问题但在多人同时选课时就会出大问题。假设课程容量是 1当前已选 0学生 A 和学生 B 同时发起了选课请求。两个请求都先查到了 count0都判定“没满”然后都执行插入最后课程里就有了两个学生。这就是经典的“超卖”问题本质上是检查和更新之间没有做原子保护。4.2 用乐观锁改造选课流程解决并发选课超容量课程设计阶段我推荐用乐观锁也就是把“检查容量”和“占坑”合并成一条 SQL让数据库来做这一步的原子判断UPDATE t_course SET selected_count selected_count 1 WHERE cid ? AND selected_count capacity;这条 UPDATE 会返回一个受影响行数。如果返回 1说明当前课程还有余量占坑成功继续插入选课记录如果返回 0说明课程刚好满了直接回滚并提示“课程已满”。我把完整的选课事务代码放在CourseService里public String selectCourse(String sid, String cid) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); ScDao scDao new ScDao(); CourseDao courseDao new CourseDao(); // 第一步检查是否重复选课 if (scDao.exists(conn, sid, cid)) { conn.rollback(); return 请勿重复选课; } // 第二步乐观锁占坑容量不足返回0 int rows courseDao.decreaseStock(conn, cid); if (rows 0) { conn.rollback(); return 课程容量已满; } // 第三步插入选课记录 scDao.insert(conn, sid, cid); conn.commit(); return 选课成功; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return 选课失败请稍后重试; } finally { DBUtil.close(conn); } }DecreaseStock 对应的 DAO 方法就是执行上面那条 UPDATE SQL。这种做法的巧妙之处在于并发请求同时到达时selected_count capacity这个条件由数据库的行锁保证只会有一个请求成功不用你写一堆synchronized或者分布式锁。课程设计阶段能讲清楚这一点已经超过绝大多数人了。4.3 退课和成绩录入的注意事项退课是选课的逆操作同样要走事务。它的逻辑是先删除t_sc里的选课记录再把t_course里的selected_count减一。删除和自减必须放同一个事务否则会出现“课表没了但容量没还”的数据不一致。这里还有一个小细节退课前要判断成绩是否为 NULL如果教师已经录了成绩一般不允许再退课否则成绩就凭空消失了。成绩录入的权限在教师端。教师只能对自己负责的课程录入成绩这里做了一个权限校验先把教师 ID 和课程 ID 关联起来确认这堂课属于该教师才允许执行UPDATE t_sc SET score ? WHERE sid ? AND cid ?。录入时可以加上正则校验成绩范围 0 到 100超出直接拒绝。这种小校验不复杂但能让系统显得严谨很多。5. 上线前必踩的三个坑从报错信息反推问题根因5.1 重复选课唯一约束报错与应用层提示的对接第一次演示重复选课时很多人会看到类似这样的报错Duplicate entry 20230001-CS101 for key PRIMARY这是好事因为说明联合主键真的在起作用。但这个报错直接抛到页面上很难看用户也看不懂。正确做法是在 Service 层捕获数据库异常识别主键冲突的类型然后转换成友好的业务提示。我的做法是在catch (Exception e)里判断异常信息是否包含Duplicate entry包含则返回“该课程已在你的课表中请勿重复选择”。你可以根据自己的数据库驱动类型做处理但核心思路是一致的数据库约束负责兜底应用层负责给出合适的人话。5.2 删除课程被外键拦截不能删时要怎么给用户解释另一条高频报错长这样Cannot delete or update a parent row: a foreign key constraint fails出现这个报错说明你要删的课程在t_sc里已经有选课记录了。物理删除被外键成功拦住数据完整性保住了但页面直接五百万报错用户体验很差。我对这个问题的处理方案是“三级策略”。第一步删除前先查t_sc有没有关联记录有则提示“该课程已有学生选课不能直接删除可以调整容量或对课程做停用处理”。第二步如果只是想让课程不在选课列表上出现就把课程表加一个status字段停用的课程对普通学生隐藏。第三步如果确实要物理删除同时也想清空选课记录可以在业务代码里先删t_sc再删t_course并放在同一个事务里。课程设计阶段前两种方案已经足够。5.3 中文乱码从URL、请求到数据库的全链路梳理乱码问题我自己写这套系统时也踩过但乱码的根因其实只有一个数据在传输和存储的每一个环节编码必须统一。排查的时候按这个链路走基本能定位检查 JSP 页面是否设置了pageEncodingUTF-8检查请求过滤器是否设置了request.setCharacterEncoding(UTF-8)检查响应是否设置了response.setContentType(text/html;charsetUTF-8)检查数据库连接 URL 是否带了characterEncodingUTF-8检查数据库和表的字符集是否为utf8mb4。这一个链条上只要有一个环节不是 UTF-8就会出现“页面显示正常、存进数据库就乱”或者“数据库正常、页面显示乱”的怪象。我见过很多同学只改了一个地方就以为解决了结果换台电脑部署又乱了。所以我的建议是把五个环节全部固定成统一编码做成规范而不是等出问题再逐个试。另外还有一个小技巧如果使用 GET 请求传递中文参数Tomcat 默认对 URL 参数的编码是 ISO-8859-1需要在连接器配置里把URIEncodingUTF-8加上。否则你用 GET 提交中文时过滤器管不到 URL 里那一段。5.4 演示和答辩准备几个能让你讲得更从容的小细节最后我忍不住想多说几句答辩和演示的把控问题因为这项目我在实际带人的过程中太有体会了。演示之前一定要准备一份量级合适的数据脚本。我一般会生成 5 个教师、30 个学生、15 门课程的数据容量有大有小确保演示时能看到“有课程已满、有课程还剩很多”的对比效果。选几门课给固定学生先选好这样登录进去就能看到已有课表和成绩不用现场现点现等稳定性更高。答辩问得最多的几个问题基本是固定的为什么选课表要用联合主键并发情况下怎么防止超容量选课为什么selected_count不直接由 Count 计算删除课程时被外键拦住了怎么办成绩字段放在选课表里的原因是什么这些我在上面的章节里已经全部覆盖。你能用自己的话说清楚这些并且现场把代码指给老师看这个项目的分数大概率差不了。我自己的体会是这类课程设计最大的价值不在“做出来”而在“每一个按钮背后都讲得出道理”。数据库约束、事务回滚、并发控制这些东西在面试和后续项目里全部会反复用到你现在亲手把它们跑通一遍后面比其他人省下的时间远不止十倍。如果你按这个思路把系统做出来遇到具体报错不知道怎么改欢迎带着日志来聊排查这一类问题比排雷有意思多了。本文还有配套的精品资源点击获取