简介:面向正在完成数据库课程设计的高校学生,这份学生选课信息管理系统以 Java + MySQL 实现,采用 C/S 架构,覆盖学生、教师、管理员三类角色。学生端可完成个人信息维护、课程查询、选课退课、成绩查询与打印、奖惩信息查看;教师端支持课程信息查看、选课名单查询与成绩录入;管理员端则统一管理学生、教师、课程、选课、成绩、院系及奖惩信息,业务模块完整,适合作为课设项目参考或二次开发基础。压缩包约 1.99MB,核心内容为 Java 源代码、MySQL 建表脚本与配套设计报告,建表设计合理规范,可帮助读者快速理解表结构关系、业务流转与系统实现思路。资源已有 6063 人浏览学习,尤其适合需要从零搭建选课系统、撰写设计文档或完善数据库建模的读者。
1. 学生选课信息管理系统是什么:最常见的课程设计题目到底考什么
学生选课信息管理系统是数据库课程设计里出现频率最高的题目。它不是简单的增删改查,而是把ER模型设计、MySQL表结构、Java通过JDBC读写数据库、并发控制这些考点串在一起,最后还要交一份撑得住答辩的设计报告。
很多同学把“能跑起来”当目标,答辩时一句“为什么选课表用联合主键”就卡住。这个项目其实是完整的数据库入门闭环:建表、登录、选课、退课,代码和表结构互相咬合,适合正在做课程设计或想拿完整案例练手的人。
我见过不少提交上来的源码,脚本和Java风格割裂,字段对不上,事务没做,中文乱码。下面按实际做过的方案讲:建表脚本、JDBC代码、设计报告写法,最后是避坑清单,让你交出去的东西能演示、能讲、能答辩。
2. 从需求到表结构:选课系统的ER模型与MySQL建表脚本
选课系统业务不复杂,但表结构设计决定了后面Java代码好不好写。先把实体和关系画清楚再建表,别一上来就写CREATE TABLE。设计报告里这一部分对应“需求分析”和“概念结构设计”,表结构定错,后面全盘返工。
2.1 需求拆解:学生、课程、教师、选课四类实体怎么划边界
常见的需求描述是:学生登录后可查看课程列表、选课、退课、查看已选课程;教师可查看自己课程的学生名单;管理员维护课程信息。抽实体时注意,学生和课程之间是多对多关系,必须拆出选课表;教师和课程是一对多,教师工号放在课程表里做外键即可,不需要单独建关联表。
四个实体的属性划分:
- t_student:学号、姓名、性别、专业、班级、登录密码
- t_teacher:工号、姓名、职称、学院
- t_course:课程号、课程名、学分、上课时间、上课地点、容量、已选人数、授课教师工号
- t_sc(选课记录):学号、课程号、选课时间、成绩
两个关键判断:成绩字段放选课表而不是课程表,因为“成绩”是某学生选某门课这个行为产生的结果,不是课程的属性;selected_count 这个冗余字段建议保留,每次选课都COUNT(*)扫全表在数据量上来后会很慢,但冗余字段必须在事务里维护,否则会出错,这个坑在第5章单独说。
提示:设计报告里的实体属性表必须和你后面的CREATE TABLE完全一致,答辩老师经常逐项对照检查。
2.2 建表SQL与主外键约束:一份能直接跑的脚本
以下脚本在MySQL 5.7和8.0都能运行,字符集用utf8mb4,避免生僻字和特殊符号写入报错。建库时把字符集定死,后面少很多麻烦。
-- 建库,字符集一定用 utf8mb4 DROP DATABASE IF EXISTS course_db; CREATE DATABASE course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_db; -- 学生表 CREATE TABLE t_student ( student_id CHAR(10) NOT NULL COMMENT '学号', student_name VARCHAR(20) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男' COMMENT '性别', major VARCHAR(40) DEFAULT NULL COMMENT '专业', class_name VARCHAR(30) DEFAULT NULL COMMENT '班级', password VARCHAR(64) NOT NULL DEFAULT '123456' COMMENT '登录密码', PRIMARY KEY (student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; -- 教师表 CREATE TABLE t_teacher ( teacher_id CHAR(8) NOT NULL COMMENT '教师工号', teacher_name VARCHAR(20) NOT NULL COMMENT '姓名', title VARCHAR(20) DEFAULT NULL COMMENT '职称', department VARCHAR(40) DEFAULT NULL COMMENT '学院', PRIMARY KEY (teacher_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师表'; -- 课程表 CREATE TABLE t_course ( course_id CHAR(8) NOT NULL COMMENT '课程号', course_name VARCHAR(50) NOT NULL COMMENT '课程名', credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT '学分', course_time VARCHAR(50) DEFAULT NULL COMMENT '上课时间,如 周一3-4节', course_place VARCHAR(50) DEFAULT NULL COMMENT '上课地点', capacity INT NOT NULL DEFAULT 60 COMMENT '容量', selected_count INT NOT NULL DEFAULT 0 COMMENT '已选人数', teacher_id CHAR(8) DEFAULT NULL COMMENT '授课教师工号', PRIMARY KEY (course_id), KEY idx_teacher (teacher_id), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES t_teacher (teacher_id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 选课表 CREATE TABLE t_sc ( student_id CHAR(10) NOT NULL COMMENT '学号', course_id CHAR(8) NOT NULL COMMENT '课程号', select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', score DECIMAL(5,2) DEFAULT NULL COMMENT '成绩,未录入前为NULL', PRIMARY KEY (student_id, course_id), KEY idx_course (course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES t_student (student_id) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES t_course (course_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课表';参数说明:t_course 里 teacher_id 用外键关联 t_teacher,ON DELETE SET NULL 表示教师离职后课程保留、教师字段置空;t_sc 里两个外键都用 ON DELETE CASCADE,学生毕业删除时选课记录自动清掉,省得在Java里手动处理。DECIMAL(3,1) 用来存学分,DECIMAL(5,2) 存成绩,比FLOAT精确,不会被浮点误差坑。ENGINE=InnoDB 必须写,外键和事务都依赖它,MyISAM 不支持行级锁和事务,这是设计报告里“物理结构设计”要写清楚的理由。
2.3 选课表为什么要联合主键:三个容易犯的设计错误
第一个错误:给 t_sc 加一个自增id做主键,然后不限制 (student_id, course_id) 唯一。这样同一个学生可以选同一门课两遍,业务上说不通。用联合主键天然去重,插入重复数据时数据库直接报主键冲突。
第二个错误:联合主键字段顺序写反。(student_id, course_id) 作为主键,最左前缀是 student_id,适合“查某学生选了哪些课”;如果系统里更常查“某门课有哪些学生选”,主键可以换成 (course_id, student_id)。教学系统里一般前者更常见,但你要能说清这个选择,答辩时这就是一个送分题。
第三个错误:外键列没加独立索引。InnoDB 里外键列需要索引,否则删除课程时外键检查会做全表扫描。上面脚本里手动加了 KEY idx_course (course_id),就是为了让 ON DELETE CASCADE 走索引,同时保证按课程查选课名单时不回表扫主键。这个细节写进设计报告的索引设计部分,老师会觉得你是真做过,而不是照抄模板。
3. 用Java连接MySQL:JDBC封装与登录、选课、退课的完整代码
表结构定了,Java侧就是围绕数据库增删改查做业务。这里不推荐一上来就用MyBatis这种框架,课程设计用原生JDBC更能讲清楚连接、事务这些基础,答辩也少被追问框架底层原理。下面代码基于Java 8和MySQL Connector/J 8.0,换5.7数据库时驱动写法不同,第5章会单独说明。
3.1 驱动加载与连接配置:URL、账号密码放进配置文件
常见做法是把驱动和连接参数放在classpath下的db.properties,而不是硬编码在类里。这样换数据库只需要改配置文件,源代码里不出现任何环境相关的信息。
# db.properties,放在 src 根目录 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/course_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456参数说明:driver 用 com.mysql.cj.jdbc.Driver 是 Connector/J 8.0 的类名;如果你用的还是 5.1.x 老驱动,这里要写成 com.mysql.jdbc.Driver,混用会直接报 ClassNotFoundException。URL 里的 characterEncoding=utf8 解决中文乱码,serverTimezone=Asia/Shanghai 解决MySQL 8.0 驱动要求显式指定时区的报错。注意 MySQL 8.0 的驱动类在旧包名下不存在,这是第5章第一个坑的根源。
import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class JdbcUtil { private static String url; private static String username; private static String password; static { try (InputStream in = JdbcUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError("加载数据库配置失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }逻辑说明:静态代码块在类加载时执行一次,把配置读进内存并注册驱动,之后所有方法共用。getConnection() 每次返回新连接,课程设计规模完全够用。注意处理 JDBC 的 Connection、Statement、ResultSet 都要关闭,这里用 try-with-resources 自动关,避免连接泄漏。如果你加连接池,只需要改这一个方法,业务代码不用动。
3.2 登录模块用PreparedStatement:防注入同时省去引号拼接
登录是每个选课系统都有的功能,写法最能看出基本功。错误写法是用 Statement 拼字符串:SELECT ... WHERE student_id = '" + studentId + "',这样的SQL注入漏洞,输入 ' or '1'='1 直接绕过密码。正确写法是用 PreparedStatement:
public Student login(String studentId, String password) { String sql = "SELECT student_id, student_name, major FROM t_student " + "WHERE student_id = ? AND password = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentId); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Student s = new Student(); s.setStudentId(rs.getString("student_id")); s.setStudentName(rs.getString("student_name")); s.setMajor(rs.getString("major")); return s; } } } catch (SQLException e) { e.printStackTrace(); } return null; }参数说明:PreparedStatement 的 ? 占位符由 setString 填充,MySQL驱动会对值做转义,单引号和反斜杠都不会破坏SQL结构,这就是PreparedStatement防注入的原理。课程列表查询就是另一条 SQL:SELECT * FROM t_course ORDER BY credit DESC, course_id,排序写在SQL里而不是拿到Java里排序,这是数据库使用的基本习惯。另外密码明文存表只够教学演示,设计报告里要补一句“正式系统应存MD5加盐或bcrypt哈希”,答辩时这是明显加分项。
3.3 选课退课的事务边界:先查余量再写入的正确顺序
选课涉及三个动作:判断余量、插入选课记录、更新已选人数。这三步必须在同一个事务里,否则中间任何一步失败,数据就对不上。比如先插入记录再更新人数,插入成功而更新失败,余量就虚了。下面是选课核心代码:
public void selectCourse(Connection conn, String studentId, String courseId) throws Exception { // 调用方先执行 conn.setAutoCommit(false) 开启事务 String checkSql = "SELECT capacity, selected_count FROM t_course " + "WHERE course_id = ? FOR UPDATE"; String insertSql = "INSERT INTO t_sc (student_id, course_id, select_time) " + "VALUES (?, ?, NOW())"; String updateSql = "UPDATE t_course SET selected_count = selected_count + 1 " + "WHERE course_id = ?"; try (PreparedStatement psCheck = conn.prepareStatement(checkSql); PreparedStatement psInsert = conn.prepareStatement(insertSql); PreparedStatement psUpdate = conn.prepareStatement(updateSql)) { psCheck.setString(1, courseId); try (ResultSet rs = psCheck.executeQuery()) { if (!rs.next()) { throw new RuntimeException("课程不存在"); } int capacity = rs.getInt("capacity"); int selected = rs.getInt("selected_count"); if (selected >= capacity) { throw new RuntimeException("该课程容量已满"); } } psInsert.setString(1, studentId); psInsert.setString(2, courseId); psInsert.executeUpdate(); psUpdate.setString(1, courseId); psUpdate.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } }逻辑说明:SELECT ... FOR UPDATE 是行级锁,把这条课程记录锁住,防止两个学生同时读到余量1然后都通过判断,这是解决超选的正确姿势。setAutoCommit(false) 关闭自动提交后,commit() 之前的所有操作要么全部生效,要么 rollback() 全部撤销。退课就是反方向:先确认存在选课记录,再 DELETE 选课记录,再把 selected_count 减一,同样用事务包裹。代码里事务的开启和关闭建议放在 Service 层而不是 DAO 层,否则两个DAO方法之间没法共享同一个事务。
4. 设计报告怎么写得像样:结构、图表和测试用例的套路
源代码能跑只是及格,设计报告才是课程设计的大头。报告不用写得华丽,但结构要完整,图表要和代码自洽。很多人的报告是网上模板改的,ER图里实体和实际建的表对不上,这种一眼就能看出来,答辩必被追问。
4.1 设计报告的章节骨架:从需求分析到数据库物理设计
一份标准的课程设计报告,章节骨架是固定的,按下面这张表组织,每个章节写什么、常见扣分点是什么,一次看清楚:
| 章节 | 必写内容 | 常见扣分点 |
|---|---|---|
| 需求分析 | 功能需求、性能需求、可行性分析 | 功能列表和实际代码对不上 |
| 概念结构设计 | ER图、实体属性表 | 多对多关系漏拆,实体画错 |
| 逻辑结构设计 | 关系模式、函数依赖、范式分析 | 没写范式,或写了和表结构矛盾 |
| 物理结构设计 | 建表语句、索引设计、存储引擎选择 | 只贴SQL,不讲为什么选InnoDB |
| 系统实现 | 模块划分、关键代码、运行截图 | 截图模糊或日期是旧时间 |
| 测试 | 测试用例表、测试结果 | 用例只有正常路径,没有边界值 |
范式分析是报告的省钱项:t_student 里学号决定所有属性,满足BCNF;t_sc 是 (student_id, course_id) 联合决定 score,无部分函数依赖,满足第二范式以上。写一段“本设计所有表均满足BCNF”就能堵住一半提问。逻辑结构设计里要把每个关系模式写出来,格式是“学生(学号,姓名,性别,专业,班级,密码)”,主键加下划线,外键标FK,这样老师一眼看出你理解了关系模式转表的规则。
4.2 数据流图与用例图画到几层才不会被答辩老师挑刺
数据流图(DFD)画两层足够。顶层图画一个“选课系统”圆圈,外部实体是学生、教师、管理员,数据流标注“登录信息”“选课请求”“课程信息”。一层图画分解成“登录验证”“课程查询”“选课处理”“退课处理”“成绩录入”五个加工,每个加工旁边标注输入输出和涉及的数据存储。不用画到二层加工,问就说“二层加工与一层一致,按模块实现细节见系统实现部分”。
用例图画四个核心用例:学生登录、学生选课、学生退课、管理员维护课程。每个用例要有基本流和备选流,比如“选课用例,备选流:容量已满,提示错误并终止选课”。“备选流”这三个字写进报告,答辩老师基本就不会再追用例设计,因为多数人只会画一个椭圆加几条线。
4.3 测试用例表这样写:边界值进去,报告立刻有分量
测试用例表是多数人敷衍的地方,写几个“登录成功”“选课成功”就交差。光有正常路径,测试那章毫无说服力。把边界值用例补进去,报告立刻不一样:
| 用例编号 | 测试项 | 输入 | 预期结果 | 实际结果 |
|---|---|---|---|---|
| TC001 | 登录-密码正确 | 学号2023010101,密码123456 | 登录成功跳转主页 | 通过 |
| TC002 | 登录-密码错误 | 密码000000 | 提示密码错误 | 通过 |
| TC003 | 选课-余量充足 | 容量60,已选59 | 选课成功,已选变60 | 通过 |
| TC004 | 选课-容量已满 | 容量60,已选60 | 提示已满,不插入记录 | 通过 |
| TC005 | 退课-未选课程 | 未选过该课 | 提示未选,不执行删除 | 通过 |
| TC006 | 选课-重复选课 | 已选过该课 | 主键冲突,提示不可重复 | 通过 |
说明:TC004、TC005、TC006 这三条边界用例分别对应容量边界、业务前置条件、数据库主键约束。报告里注明“TC006 与 t_sc 表联合主键对应”,体现的是你理解表设计和业务规则一致。测试结果里如果 TC004 第一次失败,不要删掉,保留“失败后修复记录”,这比全篇“通过”更真实,老师反而认可。
5. 避坑排查实录:MySQL+Java选课系统最常见的5个翻车现场
下面5个问题是我在课程设计作业里看到重复率最高的,每条按“现象 → 原因 → 解决”写清楚,对应你自己的项目排查时,按这个路径走就能定位。
5.1 中文乱码:连接串少了characterEncoding导致全表问号
现象:Java里插入学生姓名后,MySQL里看到“???”,控制台打印正常,但库里全是问号。原因:连接串没指定字符集,MySQL服务端和客户端连接用了不同编码;另一种可能是建表时没指定字符集,表默认建成了latin1。解决:表统一用utf8mb4,连接串加 characterEncoding=utf8,插入前执行 SET NAMES utf8mb4。排查时用 SHOW CREATE TABLE 看表定义,用 SHOW VARIABLES LIKE 'character_set%' 看服务端设置,先确认库和连接,别一开始就怀疑Java代码。乱码问题里,连接串参数漏写占了七成,表字符集错了占三成,代码本身基本没错。
5.2 ClassNotFoundException:驱动版本和MySQL版本不对付
现象:运行时报 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因:MySQL 8.0 用的 Connector/J 8.0 里驱动类改名了,com.mysql.jdbc.Driver 这个老类名只存在于 5.1.x 及之前的驱动包。解决:要么用 com.mysql.cj.jdbc.Driver 配 8.0 驱动包,要么把驱动 jar 换回 5.1.49 配合 MySQL 5.7。另外 jar 没放进项目的库路径、没加到构建路径也会报这个错,先用 jar tf 命令查看包里的类名确认版本,再检查依赖有没有真的引入。这个报错的本质是版本不匹配,不是代码逻辑问题,所以改驱动版本而不是改代码。
5.3 并发选课余量变负数:没有事务和锁时的经典事故
现象:课程容量60,两个学生同时选课,最后已选人数变成62,极端情况变成负数。原因:无锁时两个连接同时读到余量=59,都判断可以通过,然后各自执行更新 +1,后写覆盖先写,产生丢失更新。解决:用第3章的 SELECT ... FOR UPDATE 锁行加事务,这是 MySQL InnoDB 处理并发更新的标准做法。如果用了 MyISAM 引擎,FOR UPDATE 不生效,因为 MyISAM 不支持行锁,这也是建表时指定 ENGINE=InnoDB 的原因。答辩被问“怎么解决超选”,把这条背下来,再补一句“生产环境还可以用乐观锁版本号”,就完全够用了。
5.4 时间字段比较出错:DATETIME和String的隐式转换
现象:查询“周一3-4节”的课程冲突时,用字符串比较 course_time 得到错误结果;或者把 String 转 Date 再比较,结果全对不上。原因:course_time 存的是“周一3-4节”这种文本,设计上就不能用时间区间比较;如果存了 DATETIME 字段,Java 侧应该用 java.sql.Timestamp 或 LocalDateTime 而不是 java.util.Date,否则驱动做隐式转换时会有时区差。解决:教学系统里上课时间用规范化文本(周几+节次)存储,冲突判断交给 Java 逻辑规则;如果真要存时间查询,用 JDBC 4.2 的 setObject 传 LocalDateTime,不要用字符串拼日期。这个坑的根源是字段类型选错,而不是代码写错。
5.5 设计报告插图模糊:Visio导出的图片被缩放
现象:Word里插入的ER图放大后糊成一片,答辩时老师根本看不清实体名和联系。原因:从微信或截图工具里拿的低分辨率图,Word缩放后像素不够。解决:Visio 导出 PNG 时把分辨率设到 300dpi,或者导出 SVG 再转高清图;Word 里图片宽度控制在 14cm 以内,不要拉伸变形。代码截图也一样,IDE 里把字号调大一些再截图,保证答辩投影时能看清。这个坑不涉及技术,但每年都有项目因为图看不清被扣分,属于白送的失分点。
6. 答辩前必做的三件事:从能运行到能讲清楚
代码能跑只是起点,答辩看的是能不能讲明白、扛得住追问。我一般答辩前固定做三件事。
第一件,把 JDBC 裸连接换成连接池。用 HikariCP 或 Druid 都行,核心是让老师知道你清楚“连接不能裸用”。配置里写清 maximumPoolSize=10、minimumIdle=2,解释“每次 getConnection 从池里取,用完归还,避免频繁建连”。只改 JdbcUtil.getConnection() 一个方法,业务代码不动,面试和答辩都能聊。
第二件,加一个批量导入选课入口,用 PreparedStatement 批处理提升插入效率:
String sql = "INSERT INTO t_sc(student_id, course_id) VALUES(?, ?)"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (String[] row : rows) { // rows: 待导入的学号、课程号列表 ps.setString(1, row[0]); ps.setString(2, row[1]); ps.addBatch(); } ps.executeBatch(); conn.commit(); }addBatch 攒一批再 executeBatch 提交,比逐条 executeUpdate 快一个量级;事务包裹保证中间某条主键冲突时整体回滚。老师追问“冲突怎么办”,答“用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 跳过已选”,顺手把 MySQL 的 upsert 语法也覆盖了,这题就能站起来聊。
第三件,讲清楚索引。在选课表上演示 EXPLAIN SELECT * FROM t_sc WHERE student_id='2023010101',主键是 (student_id, course_id),这条查询走主键索引,type 是 ref 而不是 ALL;换成分号查 course_id,走的是 idx_course。把 EXPLAIN 结果截图放进报告附录,再准备一句话“索引设计遵循最左前缀原则,选课表两个查询方向分别建了索引”。
最后说个我的习惯:哪怕演示代码里密码存明文,报告里也要写一句“生产环境不能存明文,应存哈希加盐”。老师问得深就聊哈希和盐值,问得浅就一句话带过,这道题答出来,整个项目的可信度就上来了。希望这些踩过的坑和收尾动线对你有用,希望帮到你。
本文还有配套的精品资源,点击获取