简介:本资源是一份面向计算机专业本科生的MySQL课程设计实战项目,聚焦学生宿舍管理场景,完整呈现数据库设计、Java后端逻辑与Swing桌面界面开发的全流程实践。适用于数据库原理、Java程序设计等课程综合实训,帮助学习者掌握MySQL建模、JDBC连接、GUI组件布局及MVC分层结构等核心技能。压缩包共334个文件,含47个可读Java源码(涵盖AdminManageComponent、StuManageComponent等核心业务类)、202个编译后class文件、22张系统界面与ER图PNG、9个配置XML及配套文档,整体112.72MB,结构规范便于教学复现与代码研读。已有268人下载学习,资源提供完整可运行工程(含sln解决方案、jar可执行包、MySQL数据库文件),附带详细设计文档与功能模块说明,特别适合课程设计选题参考、答辩材料准备及Swing+MySQL集成开发入门实践。
1. 学生宿舍管理系统:为什么选 MySQL + Java + Swing 而不是 Spring Boot 或 Vue?
这不是一个“炫技型”课设——它要解决的是某高校计算机专业大三学生在两周内交出一份可运行、可演示、可答辩、可写进文档的完整课程设计作业。真实场景是:机房只装了 JDK 8 和 MySQL 5.7,没有 Docker,不配 Nginx,老师明确要求“不能用框架封装掉数据库操作逻辑”,且答辩时需现场打开.java文件讲解事务控制点。所以这套方案没上 Spring Boot,不是因为它不行,而是因为——学生在没学过 IOC/AOP 的前提下,硬套框架只会让 DAO 层变成黑匣子,答辩时连executeUpdate()返回值都解释不清。它用 Swing 不是因为“复古”,而是因为:零依赖打包(jar双击即启)、界面控件与事件绑定直白(addActionListener一行一逻辑)、所有 SQL 都明文写在StudentDao.java里,方便老师逐行查重和溯源。整套资源面向的是“需要交作业、怕翻车、要能讲清楚每一行代码来龙去脉”的真实需求,不是开源社区里的 Demo 项目。
2. 总体架构设计:三层结构与四个核心模块的职责边界
本系统严格遵循经典的GUI–Service–DAO 三层分离结构,不引入任何中间件或配置中心,所有逻辑均通过 Java 原生类库实现。三层之间通过接口契约通信,避免直接 new 实例,为后续单元测试和模块替换留出余地。整个系统划分为四个功能模块:学生信息管理、宿舍分配管理、维修报修管理、管理员权限控制。每个模块对应独立的数据表、DAO 接口、Service 实现类及 Swing 窗体类,模块间仅通过 Service 层方法调用交互,杜绝 GUI 层直连数据库或跨模块访问私有字段。
2.1 数据库设计:6 张表 + 3 个关键约束的落地逻辑
数据库共设计 6 张表,全部采用 InnoDB 引擎,显式声明外键并启用级联更新(非级联删除,防止误删主数据)。建表语句中刻意规避了AUTO_INCREMENT在多线程插入时可能引发的间隙锁争用问题,改用BIGINT主键 + 应用层生成 UUID(后四位取时间戳+随机数)作为逻辑主键,物理主键仍保留自增 ID 用于索引优化。
-- 学生表(student) CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_id VARCHAR(12) NOT NULL UNIQUE COMMENT '学号,业务主键', name VARCHAR(20) NOT NULL, gender ENUM('男','女') DEFAULT '男', class_name VARCHAR(30), phone VARCHAR(11), dorm_id BIGINT COMMENT '关联宿舍ID', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_stu_id (stu_id), INDEX idx_dorm_id (dorm_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 宿舍表(dormitory) CREATE TABLE dormitory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dorm_no VARCHAR(10) NOT NULL UNIQUE COMMENT '宿舍号,如301、412A', building VARCHAR(10) NOT NULL COMMENT '楼栋,如A栋、B栋', floor TINYINT NOT NULL COMMENT '楼层', capacity TINYINT NOT NULL DEFAULT 4 COMMENT '床位数', used_count TINYINT NOT NULL DEFAULT 0 COMMENT '已住人数,业务字段,非计算字段', status ENUM('空闲','满员','维修中') DEFAULT '空闲', CHECK (capacity BETWEEN 2 AND 8), CHECK (floor BETWEEN 1 AND 12) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;提示:
used_count字段是典型“反范式设计”。它不从student表实时 COUNT,而是在每次入住/退宿时由 Service 层显式UPDATE dormitory SET used_count = used_count + 1 WHERE id = ?。这样做的代价是增加一次 DB 写操作,但换来的是查询宿舍剩余床位时的 O(1) 响应,且避免了高并发下SELECT COUNT(*)+INSERT的竞态风险。这是我在带某跨平台系统课设时踩过的坑——学生用COUNT(*)判断是否可入住,结果两人同时点击“申请”,双双插入成功,宿舍超员。
2.2 Java 层结构:接口驱动开发与事务边界的显式标注
Java 工程采用标准 Maven 目录结构,src/main/java下分dao、service、ui、util四包。DAO 层全部定义为接口(如StudentDao),实现类命名为StudentDaoImpl,强制依赖倒置。所有涉及多表写操作的方法(如“分配宿舍+更新学生状态+扣减宿舍余量”)均封装在 Service 接口方法中,并在实现类上使用@Transactional注解(基于 JDBC 手动事务管理,非 Spring):
// service/impl/DormAssignmentServiceImpl.java public class DormAssignmentServiceImpl implements DormAssignmentService { private final StudentDao studentDao; private final DormitoryDao dormitoryDao; private final RepairRecordDao repairRecordDao; public DormAssignmentServiceImpl(StudentDao studentDao, DormitoryDao dormitoryDao, RepairRecordDao repairRecordDao) { this.studentDao = studentDao; this.dormitoryDao = dormitoryDao; this.repairRecordDao = repairRecordDao; } @Override public boolean assignDorm(String stuId, long dormId) throws SQLException { Connection conn = null; try { conn = JdbcUtil.getConnection(); // 自封装工具类,统一管理连接 conn.setAutoCommit(false); // 显式开启事务 // 步骤1:检查宿舍是否可分配(状态非维修中、未满员) Dormitory dorm = dormitoryDao.findById(dormId, conn); if (dorm == null || !"空闲".equals(dorm.getStatus()) || dorm.getUsedCount() >= dorm.getCapacity()) { throw new SQLException("宿舍不可分配:" + dorm.getDormNo()); } // 步骤2:更新学生 dorm_id int stuRows = studentDao.updateDormId(stuId, dormId, conn); // 步骤3:更新宿舍 used_count int dormRows = dormitoryDao.increaseUsedCount(dormId, conn); conn.commit(); return stuRows == 1 && dormRows == 1; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; // 抛出原异常,便于上层捕获日志 } finally { if (conn != null) conn.close(); } } }这段代码的关键在于:事务控制粒度精确到业务用例,而非单个 DAO 方法;所有 SQL 执行均传入同一Connection对象;异常路径确保回滚且不吞异常。很多学生把conn.commit()放在每个 DAO 方法里,结果 A 成功 B 失败,数据就处于不一致状态——这在答辩时被问到“怎么保证 ACID”,基本当场卡壳。
2.3 Swing 界面组织:基于 CardLayout 的单窗体多面板切换策略
UI 层摒弃JFrame多窗口弹窗模式(易造成内存泄漏、焦点混乱、答辩时切屏卡顿),统一采用JFrame主窗体 +CardLayout管理子面板。主窗体只含顶部菜单栏(文件、学生、宿舍、报修、帮助)和中央JPanel容器,各功能模块以独立JPanel形式存在(如StudentManagePanel、DormAssignPanel),通过cardLayout.show(container, "student")切换,所有面板共享同一JFrame生命周期。
// ui/MainFrame.java public class MainFrame extends JFrame { private final CardLayout cardLayout; private final JPanel contentPanel; private final StudentManagePanel studentPanel; private final DormAssignPanel dormPanel; private final RepairManagePanel repairPanel; public MainFrame() { setTitle("学生宿舍管理系统 v1.0"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 初始化菜单栏 JMenuBar menuBar = createMenuBar(); setJMenuBar(menuBar); // 初始化卡片布局容器 cardLayout = new CardLayout(); contentPanel = new JPanel(cardLayout); studentPanel = new StudentManagePanel(this); // 传入 this 用于回调刷新 dormPanel = new DormAssignPanel(this); repairPanel = new RepairManagePanel(this); contentPanel.add(studentPanel, "student"); contentPanel.add(dormPanel, "dorm"); contentPanel.add(repairPanel, "repair"); add(contentPanel, BorderLayout.CENTER); pack(); setLocationRelativeTo(null); } public void showPanel(String panelName) { cardLayout.show(contentPanel, panelName); } }这种结构的好处是:答辩时只需启动一个进程,按 Alt+数字键(菜单快捷键)即可秒切模块,无窗口堆叠干扰;所有面板可复用主窗体的数据库连接池和 Service 实例,避免重复初始化;后期若需导出 PDF 报表,可直接对contentPanel截图生成。我带过的某次课设中,有学生用了 7 个独立JFrame,答辩时切错窗口、关错进程,演示中断三次,最终文档分被扣 20%。
3. 核心功能实现:从“分配宿舍”到“报修闭环”的四步落地
本章聚焦四个模块中最易出错、最常被提问的两个高频业务:“宿舍分配”与“维修报修”。它们覆盖了增删改查、事务控制、状态流转、界面反馈四大课设考察点。实现不追求花哨动画,而强调每一步操作都有明确的数据库响应、界面状态同步、错误拦截与用户提示。
3.1 宿舍分配功能:三阶段校验 + 可视化床位热力图
分配宿舍不是简单UPDATE student SET dorm_id=?,而是包含前置校验、执行分配、结果反馈三个阶段。其中“可视化床位热力图”是加分项:在DormAssignPanel中嵌入一个JTable,行表示楼栋(A/B/C),列表示楼层(1–6),单元格显示该楼层该楼栋的“空闲率”(1 - used_count/capacity),颜色由TableCellRenderer动态渲染(绿色→黄色→红色)。
// ui/renderer/DormHeatRenderer.java public class DormHeatRenderer extends DefaultTableCellRenderer { @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); if (value instanceof Double) { double rate = (Double) value; if (rate <= 0.25) c.setBackground(Color.RED); // 满员 else if (rate <= 0.5) c.setBackground(Color.ORANGE); // 紧张 else c.setBackground(Color.GREEN); // 充足 } return c; } }分配按钮点击事件逻辑如下:
// ui/panel/DormAssignPanel.java 内部类 private void onAssignBtnClick() { String stuId = stuIdField.getText().trim(); String dormNo = dormNoField.getText().trim(); if (stuId.isEmpty() || dormNo.isEmpty()) { JOptionPane.showMessageDialog(this, "请填写学号和宿舍号", "输入错误", JOptionPane.WARNING_MESSAGE); return; } try { // 1. 查询学生是否存在且未分配 Student stu = studentService.findByStuId(stuId); if (stu == null) { JOptionPane.showMessageDialog(this, "学号不存在", "错误", JOptionPane.ERROR_MESSAGE); return; } if (stu.getDormId() != null && stu.getDormId() > 0) { JOptionPane.showMessageDialog(this, "该学生已分配宿舍", "提示", JOptionPane.INFORMATION_MESSAGE); return; } // 2. 查询宿舍是否存在且可分配 Dormitory dorm = dormitoryService.findByDormNo(dormNo); if (dorm == null) { JOptionPane.showMessageDialog(this, "宿舍号不存在", "错误", JOptionPane.ERROR_MESSAGE); return; } if (!"空闲".equals(dorm.getStatus())) { JOptionPane.showMessageDialog(this, "宿舍当前状态:" + dorm.getStatus(), "不可分配", JOptionPane.WARNING_MESSAGE); return; } // 3. 执行分配(含事务) boolean success = dormAssignmentService.assignDorm(stuId, dorm.getId()); if (success) { JOptionPane.showMessageDialog(this, "分配成功!\n宿舍:" + dorm.getDormNo() + "\n楼栋:" + dorm.getBuilding() + ",楼层:" + dorm.getFloor(), "成功", JOptionPane.INFORMATION_MESSAGE); refreshDormTable(); // 刷新热力图表格 clearInputFields(); } else { JOptionPane.showMessageDialog(this, "分配失败,请重试", "错误", JOptionPane.ERROR_MESSAGE); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, "数据库错误:" + e.getMessage(), "系统异常", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); // 控制台输出便于调试 } }这里的设计要点是:校验顺序不可颠倒(先查学生再查宿舍),错误提示必须具体到字段和原因,成功后强制刷新 UI 状态。很多学生把校验全丢给 SQL 的WHERE子句,结果报错只显示“SQLSyntaxErrorException”,答辩时根本说不清哪条数据不满足条件。
3.2 维修报修功能:状态机驱动 + 时间戳自动填充
报修模块模拟真实流程:学生提交 → 宿管审核 → 维修员接单 → 完成维修 → 学生评价。状态流转通过status字段(ENUM: '待审核','已受理','维修中','已完成','已评价')控制,禁止跳步(如不能从“待审核”直跳“已完成”)。RepairRecord表中apply_time、accept_time、finish_time、evaluate_time四个时间戳全部由 Java 层new Timestamp(System.currentTimeMillis())生成,不在数据库设DEFAULT CURRENT_TIMESTAMP,确保时间精度可控且可追溯。
// dao/impl/RepairRecordDaoImpl.java @Override public long insert(RepairRecord record) throws SQLException { String sql = "INSERT INTO repair_record (stu_id, dorm_id, title, description, " + "apply_time, status, creator_role) VALUES (?, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, record.getStuId()); ps.setLong(2, record.getDormId()); ps.setString(3, record.getTitle()); ps.setString(4, record.getDescription()); ps.setTimestamp(5, record.getApplyTime()); // Java 生成,非 DB 默认 ps.setString(6, record.getStatus()); ps.setString(7, record.getCreatorRole()); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) return rs.getLong(1); } } return -1; } // service/impl/RepairRecordServiceImpl.java @Override public boolean updateStatus(long recordId, String newStatus, String operatorRole) throws SQLException { // 状态迁移规则校验(有限状态机) Map<String, Set<String>> validTransitions = new HashMap<>(); validTransitions.put("待审核", Set.of("已受理")); validTransitions.put("已受理", Set.of("维修中")); validTransitions.put("维修中", Set.of("已完成")); validTransitions.put("已完成", Set.of("已评价")); RepairRecord current = repairRecordDao.findById(recordId); if (current == null) return false; Set<String> allowed = validTransitions.get(current.getStatus()); if (allowed == null || !allowed.contains(newStatus)) { throw new SQLException("非法状态迁移:" + current.getStatus() + " → " + newStatus); } // 根据新状态自动填充对应时间戳 Timestamp now = new Timestamp(System.currentTimeMillis()); switch (newStatus) { case "已受理": current.setAcceptTime(now); break; case "维修中": current.setStartTime(now); break; case "已完成": current.setFinishTime(now); break; case "已评价": current.setEvaluateTime(now); break; } current.setStatus(newStatus); return repairRecordDao.update(current) > 0; }这个实现把“状态合法性”从数据库 CHECK 约束移到 Java 层,好处是:错误提示可定制(如“您无权将报修单设为‘已完成’”),且能结合operatorRole做权限判断(宿管只能到‘已受理’,维修员才能到‘维修中’)。这是课设中体现“工程思维”的关键细节——不是所有约束都适合放数据库。
4. 关键避坑指南:五个让答辩老师皱眉的高频翻车点
做课设最怕的不是功能没做完,而是功能看似跑通,答辩时被问一句就哑火。以下是我在指导多届学生过程中,统计出的最高频、最致命、最容易被忽略的五个翻车点。每一条都对应真实血泪经验,不是理论推演。
4.1 现象:程序启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL 8.0+ 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver,但学生复制的旧教程或百度代码仍用老类名;或mysql-connector-java-8.x.jar未正确添加到项目lib目录及CLASSPATH。更隐蔽的是:IDEA 中Project Structure → Modules → Dependencies添加了 jar,但Artifacts打包时未勾选“Include in project build”,导致生成的jar缺少驱动。
解决:统一使用mysql-connector-java-5.1.49.jar(兼容 MySQL 5.7,类名稳定);手动检查生成的jar包内是否含com/mysql/jdbc/Driver.class(用jar -tf xxx.jar | grep Driver);在JdbcUtil.getConnection()开头加Class.forName("com.mysql.jdbc.Driver");强制加载并捕获ClassNotFoundException输出友好提示。
4.2 现象:中文插入数据库后变成???,但 Navicat 查看正常
原因:JDBC 连接 URL 缺少字符集参数,如jdbc:mysql://localhost:3306/dormdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai。学生常只写jdbc:mysql://localhost:3306/dormdb,此时 MySQL 默认用latin1解析,Java 发送 UTF-8 字节流,结果存成乱码。Navicat 能正常显示是因为它自己做了字符集转换,具有欺骗性。
解决:连接字符串必须显式声明characterEncoding=UTF-8;同时确认 MySQL 服务端配置my.cnf中[mysqld]段落有character-set-server=utf8mb4;建库时用CREATE DATABASE dormdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。
4.3 现象:JTable显示数据但无法编辑,双击单元格无反应
原因:DefaultTableModel默认isCellEditable(int row, int column)返回false,学生未重写该方法。或者,虽重写了,但返回true的列包含了主键(如id列),导致更新 SQL 拼接时把id=?当作SET字段,破坏主键约束。
解决:继承DefaultTableModel并重写isCellEditable,仅对业务字段(如name,phone)返回true;在setValueAt()方法中,先super.setValueAt(),再调用studentService.updateById(...)同步 DB;务必在JTable构造后调用table.setAutoCreateRowSorter(true)启用排序,否则getValueAt()获取的行号与物理行号不一致,更新错数据。
4.4 现象:多用户同时申请同一宿舍,出现超员(如容量4,却住了5人)
原因:典型的“检查后使用(TOCTOU)”竞态。线程 A 读used_count=3,判断可入住;线程 B 同时读used_count=3,也判断可入住;A、B 先后执行UPDATE ... SET used_count = used_count + 1,结果used_count=5。数据库层面未加锁,Java 层也未用synchronized(会锁整个方法,性能差且不解决分布式问题)。
解决:在Dormitory表增加version字段(乐观锁),每次更新SET used_count = used_count + 1, version = version + 1 WHERE id = ? AND version = ?;或更彻底——用数据库行锁:SELECT * FROM dormitory WHERE id = ? FOR UPDATE(需在事务内),确保检查与更新原子性。课设中推荐后者,代码清晰,且FOR UPDATE是 MySQL 原生支持,无需额外依赖。
4.5 现象:导出jar后双击无反应,命令行运行报Could not find or load main class MainFrame
原因:MANIFEST.MF中Main-Class指向错误。学生常写Main-Class: MainFrame(缺包名),或Main-Class: ui.MainFrame(但实际类在ui.MainFrame,而jar解压后目录结构是ui/MainFrame.class,正确)。更常见的是:jar打包时未包含lib下的mysql-connector-java.jar,导致类加载失败。
解决:用jar -tf xxx.jar | head -20检查META-INF/MANIFEST.MF内容及ui/目录是否存在;MANIFEST.MF必须写Main-Class: ui.MainFrame;Class-Path: lib/mysql-connector-java-5.1.49.jar(路径相对于jar位置);终极方案:用 Maven Shade Plugin 打成“fat jar”,所有依赖打平,MANIFEST.MF自动生成,命令为mvn clean compile shade:shade。
5. 文档与答辩实战:如何让老师觉得“这学生真懂,不是抄的”
课设交付物不只是能跑的代码,更是一套可验证、可追溯、可讲解的完整工程证据链。老师看文档,不是为了挑错,而是想确认:你是否真正理解每个设计决策背后的 trade-off?是否经历过调试、重构、验证的全过程?以下是我总结的“让老师眼前一亮”的三板斧,全部来自真实答辩现场。
5.1 文档结构:用“问题驱动”替代“功能罗列”
别写“第一章 系统概述,第二章 需求分析……”。改成:
| 文档章节 | 写法要点 | 为什么有效 |
|---|---|---|
| “我们为什么不用 Hibernate?” | 用半页纸对比:Hibernate 自动生成 SQL 的不可控性 vs 手写 SQL 对事务边界的绝对掌控;附截图:studentDao.updateDormId()方法中UPDATE student SET dorm_id=?, updated_time=? WHERE stu_id=? AND version=?的乐观锁实现 | 证明你做过技术选型,不是盲目跟风 |
| “如何保证宿舍不超员?——从数据库约束到应用层双保险” | 左栏列 SQLCHECK (used_count <= capacity),右栏列 Java 层if (dorm.getUsedCount() >= dorm.getCapacity()) throw ...;中间画流程图:用户点击→Service 校验→DB 更新→触发器二次校验(可选) | 展示防御性编程思维,老师最爱问“如果 DB 约束失效怎么办” |
| “界面响应慢?我们这样定位瓶颈” | 截图 IDEA Profiler 中DormAssignPanel.onAssignBtnClick()方法耗时分布:studentService.findByStuId()占 80%,进而发现缺少INDEX idx_stu_id (stu_id),补索引后耗时从 1200ms 降至 15ms | 用数据说话,证明你真调过优,不是纸上谈兵 |
注意:所有截图必须真实,宁可只放一张精准的 Profiler 截图,也不要拼凑三张模糊的 Navicat 界面。老师一眼就能分辨真假。
5.2 答辩演示:准备三个“故意留的破绽”
答辩不是秀完美,而是秀思考深度。我建议主动埋三个可控的“小问题”,并在老师指出时,从容展示你的预案:
- “老师,您看这个宿舍分配按钮,我故意没加防抖(debounce),所以快速连点两次会弹出两个成功框——但这恰恰说明我清楚知道问题在哪:应该在
onAssignBtnClick()开头加assignBtn.setEnabled(false);,完成后setEnabled(true)。我留着是为了演示时能自然引出‘前端防重’这个点。” - “这个报修列表的‘评价’按钮,目前只允许学生评价自己的单子。但如果老师您用宿管账号登录,会发现按钮是灰色的——因为我实现了
SecurityUtil.canEvaluate(userId, recordId),它查student_id和当前登录user_id是否匹配。这比单纯隐藏按钮更能体现权限意识。” - “您注意到所有
JTextField都加了DocumentFilter限制输入长度了吗?比如学号框只允许 12 位数字。但手机号框我故意放开,因为不同运营商号段长度不同(11/12/13位),真正的校验放在 Service 层正则^1[3-9]\\d{9,11}$。这说明我区分了‘前端体验’和‘后端安全’。”
这三个破绽,每一个都能引出你对用户体验、权限模型、输入校验层次的理解,远胜于背诵“MVC 三层架构”的定义。
5.3 验收自查清单:答辩前最后一遍“压力测试”
别等答辩当天才发现 bug。用这张表,花 30 分钟走一遍,覆盖 95% 的老师提问点:
| 检查项 | 操作步骤 | 预期结果 | 失败应对 |
|---|---|---|---|
| 数据库一致性 | 手动UPDATE dormitory SET used_count = 10 WHERE id = 1;(超容量);然后用系统分配一个学生到该宿舍 | 系统弹窗报错“宿舍不可分配”,数据库used_count保持 10 不变 | 立即检查DormAssignmentServiceImpl.assignDorm()中的校验逻辑和事务回滚 |
| 异常流程覆盖 | 在student表中插入一条dorm_id=NULL但status='已退宿'的记录;打开学生管理界面 | 表格中该行“宿舍号”列显示为空白(非 null 字符串),且“操作”列“修改宿舍”按钮可用 | 检查StudentTableModel.getValueAt()中对null的处理,应返回""而非"null" |
| 时间敏感逻辑 | 修改系统时间至 2030 年;提交一条报修;再改回当前时间;查看repair_record表 | apply_time字段值为 2030 年,证明时间戳由 Java 生成,未受系统时间篡改影响 | 若失败,说明用了DEFAULT CURRENT_TIMESTAMP,需立即修复 DAO 层 |
| 资源泄漏验证 | 连续分配宿舍 100 次;用 VisualVM 连接进程;查看java.sql.Connection实例数 | 实例数稳定在 1~3 个(连接池大小),不随操作次数增长 | 若持续上涨,检查JdbcUtil.getConnection()是否每次都close(),特别注意finally块 |
从那以后我每次带学生做课设,都强制他们答辩前用这张表跑一遍。不是为了“不出错”,而是为了当老师问“如果数据库挂了,你的系统会怎样?”时,你能立刻答出:“会弹窗提示‘连接数据库失败’,所有按钮置灰,且日志记录Connection refused错误——因为我们封装了JdbcUtil,所有异常都统一捕获并转化为用户语言。”这种回答,比写出一百行完美代码更有说服力。
希望帮到你。
本文还有配套的精品资源,点击获取