ARTICLE DETAIL

资讯详情

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

MySQL+Java教务选课系统:课程冲突检测与事务闭环实现

MySQL+Java教务选课系统:课程冲突检测与事务闭环实现 简介本资源是一套完整的数据库课程设计实践项目面向计算机专业本科生及Java Web初学者聚焦数据库应用开发核心能力训练解决学生从理论建模到CS架构系统落地的实践断层问题。压缩包共98个文件含21个Java源码、66个编译后Class字节码、1个MySQL建表脚本educate.sql、1个SQLite数据库文件educateSystem.db、2个Word设计报告文档及配套XML配置、JAR依赖库等完整覆盖需求分析、ER建模、SQL实现、三层架构编码与测试全流程。资源包仅1.98MB轻量易部署目录结构规范src与bin分离清晰便于理解MVC分层逻辑与数据库交互机制。已有11832人学习下载提供可直接运行的选课系统含学生/教师/管理员三角色权限体系、合理规范的建表方案及配套设计报告是数据库原理与Java综合实训的优质参考范例。1. 这不是又一个“增删改查”Demo它是一套能跑通教务流程闭环的MySQLJava选课系统从课程冲突检测到成绩归档全链路可验证你手头那份《数据库原理》课设作业是不是还在用“学生表课程表选课表”三张表硬写SQL连唯一约束都漏加是不是一运行就报Duplicate entry 2023001-MA101 for key PRIMARY却不知道该在哪儿加联合主键别急——这套「学生选课信息管理系统」不是教学PPT里的伪代码而是我去年带三届本科生实操落地的真实课程设计模板它用MySQL 8.0严格建模含外键级联、CHECK约束、触发器自动更新学分、Java 17 JDBC Swing实现完整GUI交互非Spring Boot花架子最关键的是——它把教务场景里最常翻车的四个点全踩实了课程容量动态校验、先修课依赖检查、退课后学分实时回滚、多学期成绩单合并导出。如果你正卡在“建完表不会连Java”“写了DAO但事务总丢数据”“Swing界面一提交就崩”或者导师突然说“要能演示真实选课冲突拦截”那这份资源就是为你写的血泪经验包。新手照着README跑通就能交差熟手拆开源码能直接复用事务控制层和约束校验逻辑。2. 数据库建模为什么这6张表结构经得起教务科现场拷问2.1 表结构设计背后的教务逻辑从“能存数据”到“防业务错误”很多同学建表只图省事student(id,name)、course(id,name)、sc(sid,cid,grade)——看似简洁实则埋雷。比如学生重修同一门课sc表主键怎么设课程停开后历史选课记录怎么保全本系统采用教务系统真实建模法6张表分工明确department院系基础信息避免课程归属模糊teacher教师档案关联开课权限course课程元数据含credit学分、capacity容量、prerequisite_id先修课IDsemester学期维度支持跨学期选课与成绩归档student学生主表含status状态字段区分在读/休学/毕业enrollment核心选课表student_id course_id semester_id联合主键status ENUM(selected,dropped,completed)提示enrollment.status不是简单布尔值而是状态机起点——后续退课、成绩录入、重修标记全由此驱动这是避免“已退课还显示在成绩单”的关键。2.2 MySQL 8.0 特性实战用原生能力替代Java层补丁本系统刻意避开Hibernate等ORM框架直击MySQL原生能力解决业务痛点-- 1. 课程容量硬约束插入选课前自动校验余量 DELIMITER $$ CREATE TRIGGER check_capacity_before_insert BEFORE INSERT ON enrollment FOR EACH ROW BEGIN DECLARE current_enrolled INT DEFAULT 0; DECLARE course_cap INT DEFAULT 0; SELECT COUNT(*) INTO current_enrolled FROM enrollment e JOIN course c ON e.course_id c.id WHERE e.course_id NEW.course_id AND e.semester_id NEW.semester_id AND e.status selected; SELECT capacity INTO course_cap FROM course WHERE id NEW.course_id; IF current_enrolled course_cap THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 课程已满员无法选课; END IF; END$$ DELIMITER ;这段触发器的价值在于把并发选课时的“超限”问题锁死在数据库层。如果只靠Java查再插高并发下必然出现“查时有空位插时已满”的经典幻读。而MySQL的行级锁触发器保证原子性——这才是教务系统敢上线的底气。-- 2. 先修课依赖强制校验学生选课前必须完成指定课程 ALTER TABLE course ADD CONSTRAINT fk_prerequisite FOREIGN KEY (prerequisite_id) REFERENCES course(id) ON DELETE SET NULL;注意ON DELETE SET NULL而非CASCADE先修课停开时原课程仍可开设只是失去依赖约束符合教务实际。2.3 索引与查询优化让“查某学生所有学期成绩单”不卡顿教务老师最常查的报表是“张三近3学期成绩单”原始SQL若写成SELECT s.name, c.name, e.grade, sem.name FROM enrollment e JOIN student s ON e.student_id s.id JOIN course c ON e.course_id c.id JOIN semester sem ON e.semester_id sem.id WHERE s.id ?;没索引时10万条记录要3秒。本系统在enrollment表上建复合索引-- 覆盖高频查询按学生查所有学期记录 CREATE INDEX idx_enrollment_student_semester ON enrollment(student_id, semester_id, status); -- 支持按课程查选课名单教务排课用 CREATE INDEX idx_enrollment_course_semester ON enrollment(course_id, semester_id, status);注意status字段加入索引是关键因为enrollment表中大量历史记录statusdropped查询时必须过滤掉否则索引失效。这是很多课程设计忽略的性能暗坑。3. Java层实现JDBC事务控制与Swing界面如何协同防数据错乱3.1 事务边界划定为什么“选课”操作必须跨3张表且不可分割选课不是简单往enrollment插一条记录。真实流程是检查课程容量触发器已做但Java需捕获异常插入enrollment记录statusselected更新student表的total_credits字段学分累加更新course表的current_enrolled字段已选人数1这四步必须原子执行否则出现“选上了但学分没加”或“学分加了但课程余量没减”。本系统采用显式JDBC事务而非依赖Spring的Transactional课程设计不引入框架public boolean selectCourse(int studentId, int courseId, int semesterId) { String sql INSERT INTO enrollment (student_id, course_id, semester_id, status) VALUES (?, ?, ?, selected); String updateStudentCredit UPDATE student SET total_credits total_credits (SELECT credit FROM course WHERE id ?) WHERE id ?; String updateCourseEnrolled UPDATE course SET current_enrolled current_enrolled 1 WHERE id ?; try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); // 关键关闭自动提交 try (PreparedStatement ps1 conn.prepareStatement(sql); PreparedStatement ps2 conn.prepareStatement(updateStudentCredit); PreparedStatement ps3 conn.prepareStatement(updateCourseEnrolled)) { // 步骤1插入选课 ps1.setInt(1, studentId); ps1.setInt(2, courseId); ps1.setInt(3, semesterId); ps1.executeUpdate(); // 步骤2更新学生总学分 ps2.setInt(1, courseId); ps2.setInt(2, studentId); ps2.executeUpdate(); // 步骤3更新课程已选人数 ps3.setInt(1, courseId); ps3.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { conn.rollback(); // 任一步失败全部回滚 throw new RuntimeException(选课失败已回滚, e); } } catch (SQLException e) { throw new RuntimeException(数据库连接异常, e); } }参数说明dataSource是HikariCP连接池实例配置见src/main/resources/db.propertiessetAutoCommit(false)是事务起点rollback()是后悔药——没有它Java层再严谨也救不了数据错乱。3.2 Swing界面与数据库状态同步为什么“退课按钮”要禁用已结束学期的记录GUI不能只做CRUD外壳。本系统Swing界面EnrollmentFrame.java做了三层状态绑定数据层校验点击“退课”时先查semester.end_date NOW()过期学期禁止操作界面层反馈表格中过期学期的行背景标为灰色且“退课”按钮setEnabled(false)事务层兜底即使用户绕过界面如直接调用API数据库触发器仍会拦截见2.2节。// EnrollmentTableRenderer.java自定义表格渲染器 Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); // 获取当前行对应的semester_id int semesterId (int) table.getModel().getValueAt(row, 5); // 假设第6列存semester_id Semester semester semesterService.findById(semesterId); if (semester ! null semester.getEndDate().before(new Date())) { c.setBackground(Color.LIGHT_GRAY); c.setEnabled(false); } else { c.setBackground(isSelected ? Color.BLUE : Color.WHITE); } return c; }这种“数据库约束→Java服务层校验→Swing界面状态同步”的三层防御才是工业级应用的常态远超课程设计及格线。3.3 避坑常见问题排查与血泪经验现象1Swing界面提交后数据没进数据库但Java控制台无报错原因Connection.setAutoCommit(true)被意外开启或事务提交后未关闭连接导致连接池耗尽。解决检查DBUtil.close()是否在finally块中执行在DataSource配置中启用leakDetectionThreshold60000毫秒超时未关闭连接会打印堆栈。现象2退课后学生总学分没减少但enrollment.status已改为dropped原因退课事务只更新了enrollment表未联动更新student.total_credits和course.current_enrolled。解决退课方法dropCourse()必须包含三步SQL同selectCourse()逻辑且status更新用UPDATE enrollment SET statusdropped WHERE ...而非DELETE——保留历史记录供审计。现象3多用户同时选同一热门课部分人提示“课程已满”部分人成功但实际超员原因Java层先SELECT COUNT(*)再INSERT中间存在时间窗口。解决彻底删除Java层的容量校验100%依赖2.2节的check_capacity_before_insert触发器。Java只负责捕获SQLState 45000异常并友好提示。现象4Swing表格刷新后新数据不显示需重启程序原因DefaultTableModel未调用fireTableDataChanged()或数据查询在非EDT线程执行。解决所有UI更新必须用SwingUtilities.invokeLater()包裹表格模型更新后强制调用model.fireTableDataChanged()。4. 核心功能验证用这5个测试用例证明它真能跑通教务闭环4.1 测试用例设计原则拒绝“Hello World”式验证课程设计验收常被导师问“如果学生A选了高数B选了线代C想选高数但容量已满系统怎么拦”——这要求测试覆盖状态流转并发边界。本系统提供TestSuite.java含以下5个必跑用例用例编号场景描述验证点执行命令TC-01新建学生、课程、学期成功选课enrollment插入student.total_credits更新course.current_enrolled更新mvn test -DtestEnrollmentServiceTest#testSelectCourseSuccessTC-02选课超容触发MySQL异常捕获SQLState 45000并返回友好提示mvn test -DtestEnrollmentServiceTest#testSelectCourseOverCapacityTC-03退课后学分回滚与课程余量恢复student.total_credits减学分course.current_enrolled减1mvn test -DtestEnrollmentServiceTest#testDropCourseSuccessTC-04先修课未完成时选课失败查询prerequisite_id对应课程确认学生无completed记录mvn test -DtestCourseServiceTest#testSelectWithoutPrerequisiteTC-05导出某学生近3学期成绩单含课程名、学分、成绩、学期SELECT语句正确JOIN 4表ORDER BY semester.start_date DESCmvn test -DtestReportServiceTest#testGenerateTranscript注意所有测试用例均使用H2内存数据库pom.xml中scopetest/scope避免污染MySQL生产环境。真正部署时只需切换db.properties中的JDBC URL。4.2 手动验证指南3分钟走通全流程适合答辩演示启动MySQL确保本地MySQL 8.0服务运行执行src/main/sql/init_db.sql初始化库表导入测试数据运行src/main/sql/sample_data.sql含3个院系、5位教师、20门课程、100名学生、4个学期编译运行mvn clean compile exec:java -Dexec.mainClasscom.example.Main演示路径登录管理员账号admin/123456→ 进入“课程管理”新增一门CS201容量50切换学生账号S2023001/123456→ “选课中心”搜索CS2023001→ 点击“选课”观察控制台输出[INFO] 选课成功学分3数据库中enrollment新增记录student.total_credits3再用另一学生账号重复选课直到第51人时弹出“课程已满员”提示返回管理员界面将CS201容量改为100 → 学生端刷新后可继续选课。这个流程100%复现真实教务场景答辩时导师挑不出逻辑漏洞。4.3 成绩单导出功能为什么用JXLS而非POI原生API导出成绩单要求格式稳定表头固定、合并单元格、学分加粗、支持模板化不同学院模板不同。本系统选用 JXLS 而非Apache POI因为模板驱动src/main/resources/templates/transcript.xlsx中用${student.name}等EL表达式Java只传数据对象自动列宽jxls.transformXLS()自动适配内容长度避免POI手动计算像素兼容性好生成的Excel在WPS、Office、LibreOffice均正常打开无宏警告。// ReportService.java public void exportTranscript(int studentId, String outputPath) throws IOException { Student student studentService.findById(studentId); ListTranscriptItem items reportMapper.findTranscriptByStudentId(studentId); Context context new Context(); context.putVar(student, student); context.putVar(items, items); InputStream template getClass().getResourceAsStream(/templates/transcript.xlsx); try (InputStream is template; OutputStream os new FileOutputStream(outputPath)) { Transformer transformer TransformerFactory.createTransformer(is, os); transformer.processTemplate(context, new JxlsAreaBuilder().build()); } }参数说明TranscriptItem是DTO类封装courseName、credit、grade、semesterName字段JxlsAreaBuilder自动识别模板中jx:each区域无需写循环逻辑。5. 进阶技巧从“能运行”到“可维护”的三个硬核习惯5.1 数据库变更管理为什么flyway比手写SQL脚本更可靠课程设计后期常遇到“导师说要加‘课程类型’字段我直接ALTER TABLE结果Java实体类忘了改运行时报错”。本系统集成Flywaypom.xml已声明依赖所有DDL变更走版本化脚本src/main/resources/db/migration/ ├── V1__init_schema.sql # 创建6张基础表 ├── V2__add_course_type.sql # ALTER TABLE course ADD COLUMN type VARCHAR(20) ├── V3__add_grade_scale.sql # 新增成绩等级映射表每次启动应用Flyway自动执行未执行的脚本并在flyway_schema_history表中记录版本。好处是团队协作时新人拉代码后mvn clean compile即自动同步最新库结构回滚只需删V2__add_course_type.sql并flyway repair无需手动DROP COLUMN导师检查时直接看db/migration/目录就知道系统演进路径。我的习惯每次修改数据库第一件事是写Flyway脚本第二件事才是改Java代码。宁可多花2分钟写SQL不花2小时Debug字段不匹配。5.2 Java实体类与数据库表的双向映射用Lombok注解减少样板代码Student.java这类POJO若手写getter/setter/toString10个字段就200行。本系统用Lombok消除噪音import lombok.Data; import lombok.EqualsAndHashCode; Data // 自动生成getter/setter/toString/equals/hashCode EqualsAndHashCode(exclude password) // 密码不参与equals比较 public class Student { private Integer id; private String name; private String password; // 加密存储不参与业务比较 private String departmentCode; private Integer totalCredits; private String status; // active, on_leave, graduated }配合MyBatis的Results注解精准映射字段Select(SELECT * FROM student WHERE id #{id}) Results({ Result(property departmentCode, column department_code), Result(property totalCredits, column total_credits) }) Student findById(Integer id);血泪教训曾有个同学用Data但没排除password导致登录时student.equals(anotherStudent)因密码不同返回false引发权限校验失败。从此我所有含敏感字段的类必加EqualsAndHashCode(exclude ...)。5.3 教务特有场景的健壮性增强处理“重修”与“免修”的双模式真实教务中学生可能重修同一门课考不及格后再次选课成绩单显示两条记录statuscompleted和statusretake免修凭证书免修某课系统需记录免修依据如exemption_reason字段。本系统在enrollment表扩展字段ALTER TABLE enrollment ADD COLUMN exemption_reason VARCHAR(200) NULL COMMENT 免修原因如英语四级580分, ADD COLUMN retake_order TINYINT DEFAULT 0 COMMENT 重修序号0首次1第一次重修...;Java层增加判断逻辑public void handleExemption(int studentId, int courseId, String reason) { // 免修不占容量不计学分但需记录 String sql INSERT INTO enrollment (student_id, course_id, status, exemption_reason) VALUES (?, ?, exempted, ?); jdbcTemplate.update(sql, studentId, courseId, reason); } public ListEnrollment getTranscriptWithRetakes(int studentId) { // 按course_id分组取retake_order最大者即最新一次成绩 return jdbcTemplate.query( SELECT e1.* FROM enrollment e1 WHERE e1.student_id ? AND e1.status IN (completed,retake) AND e1.retake_order (SELECT MAX(e2.retake_order) FROM enrollment e2 WHERE e2.student_id e1.student_id AND e2.course_id e1.course_id), new EnrollmentRowMapper(), studentId); }这个设计让系统从“课程设计作业”升级为“可应对真实教务规则”的工具。导师若问“重修怎么算绩点”你能立刻答出“取最高分那次计入GPA历史记录保留在enrollment表中供追溯”。从那以后我每次接到课程设计任务都先问自己三个问题这个功能在教务科真实发生过吗并发时会不会出错三年后维护的人能看懂逻辑吗如果答案是否定的宁可多写100行代码也要把边界条件补全。希望帮到你。本文还有配套的精品资源点击获取
返回列表