ARTICLE DETAIL

资讯详情

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

Swing+MySQL医院管理系统:JDBC事务与分层实战

Swing+MySQL医院管理系统:JDBC事务与分层实战 简介这份资源是面向高校计算机相关专业学生的 Java 课程设计参考方案以医院管理系统为主题采用 Swing 窗口界面搭配 MySQL 数据库实现适合正在准备 Java 课设、需要一套可运行完整项目来参考或直接交付的同学。压缩包共 82 个文件约 580KB包含 16 个 java 源码文件、25 个 class 编译文件、10 个 xml 配置、1 个 sql 数据库脚本以及 28 张 png 界面截图源码、数据库脚本与运行截图齐备导入后即可对照搭建环境。系统涵盖登录注册、医生与患者信息管理、挂号记录查询与增删改查等模块界面风格偏原始朴素更贴近学生实际水平便于顺利通过课设验收。目前已有 86 人学习下载可作为课程设计的完整参考方案帮助快速理解 Swing 与 JDBC 的整合思路及数据库表结构设计。1. 医院管理系统用 Swing MySQL 落地课设之外的真实工程价值很多同学拿到「医院管理系统【Swing窗口MySQL】Java课设」这个题目第一反应是去网上找一份现成源码改改交差。但如果你真打算把 Java 基础打扎实或者想拿这个项目去面试 Java 工程师岗位我建议你认真从零搭一遍。Swing 负责桌面端交互MySQL 负责数据持久化两者之间的 JDBC 层才是真正练手的地方——事务处理、连接池、SQL 注入防护、表结构设计这些在真实业务系统里天天用。这篇文章不讲空泛概念我会把表怎么设计、窗口怎么分层、JDBC 怎么封装、MySQL 8.0 怎么装怎么配、排序和事务怎么用一步步拆开讲清楚。适合正在做 Java 课设的在校生也适合想补桌面端 数据库联动经验的初级开发者。2. 先把 MySQL 装明白Windows 10 下的安装配置与建库建表2.1 MySQL 8.0 安装配置的完整路径课设翻车最常见的原因不是 Java 代码写错而是 MySQL 根本没跑起来。Windows 10 上装 MySQL 8.0我一般走 ZIP 解压版比 MSI 安装包可控。下载地址去 MySQL 官方站点找mysql-8.0.x-winx64.zip解压到D:\tool\mysql-8.0.46-winx64这类没有中文和空格的路径下。然后在根目录手动建一个my.ini[mysqld] basedirD:/tool/mysql-8.0.46-winx64 datadirD:/tool/mysql-8.0.46-winx64/data port3306 character-set-serverutf8mb4 default_authentication_pluginmysql_native_password [client] port3306 default-character-setutf8mb4basedir和datadir必须用正斜杠或双反斜杠单反斜杠会被当转义符。default_authentication_plugin设成mysql_native_password是为了兼容老版本 JDBC 驱动不然连接时会报Public Key Retrieval is not allowed。配好后以管理员身份开 CMDcd /d D:\tool\mysql-8.0.46-winx64\bin mysqld --initialize-insecure --console mysqld --install MySQL80 net start MySQL80--initialize-insecure表示 root 初始密码为空课设阶段够用生产环境千万别这么干。net start MySQL80如果卡住不动多半是my.ini路径写错或者 3306 端口被占用用netstat -ano | findstr 3306查一下。2.2 医院管理系统的表结构设计与建库脚本医院管理系统的核心实体就四个科室、医生、患者、挂号记录。表设计要遵循第三范式但课设阶段可以适度反范式减少 JOIN。下面是我常用的建库脚本CREATE DATABASE hospital_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hospital_db; CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, dept_phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE doctor ( doctor_id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(30) NOT NULL, dept_id INT NOT NULL, title VARCHAR(20), password VARCHAR(64) NOT NULL DEFAULT 123456, status TINYINT DEFAULT 1, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB; CREATE TABLE patient ( patient_id INT PRIMARY KEY AUTO_INCREMENT, patient_name VARCHAR(30) NOT NULL, id_card VARCHAR(18) UNIQUE, phone VARCHAR(20), gender TINYINT DEFAULT 0, age INT DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE registration ( reg_id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, doctor_id INT NOT NULL, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP, fee DECIMAL(10,2) DEFAULT 0.00, status TINYINT DEFAULT 0, FOREIGN KEY (patient_id) REFERENCES patient(patient_id), FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ) ENGINEInnoDB;status字段用TINYINT而不是BOOLEAN因为 MySQL 的 BOOLEAN 本质就是 TINYINT(1)直接写 TINYINT 更直观。fee用DECIMAL(10,2)而不是 FLOAT金额计算绝不能用浮点。id_card加 UNIQUE 约束防止重复建档。外键约束在课设阶段建议保留能帮你提前发现数据一致性问题。提示建表时如果报Specified key was too long检查字符集是不是 utf8mb4 且索引字段长度超过 191把 VARCHAR 长度调小或改用前缀索引。3. Swing 窗口分层从登录到挂号主界面的代码组织3.1 Swing 界面为什么要做分层新手写 Swing 最容易把所有代码塞进一个JFrame子类里几百行堆在一起改一个按钮要翻半天。我一般分三层view包放窗口和面板service包放业务逻辑dao包放数据库操作。这样改界面不影响 SQL改 SQL 不影响按钮布局。登录窗口只负责收集用户名密码验证逻辑交给DoctorService.login()它再去调DoctorDao.findByNameAndPassword()。这种分层在课设答辩时也是加分项老师一眼能看出你懂 MVC。3.2 登录窗口与主界面的最小可运行代码先看登录窗口的核心代码public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; private DoctorService doctorService new DoctorService(); public LoginFrame() { setTitle(医院管理系统 - 登录); setSize(380, 260); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(null); JLabel userLabel new JLabel(账号); userLabel.setBounds(60, 60, 60, 25); add(userLabel); userField new JTextField(); userField.setBounds(120, 60, 180, 25); add(userField); JLabel passLabel new JLabel(密码); passLabel.setBounds(60, 110, 60, 25); add(passLabel); passField new JPasswordField(); passField.setBounds(120, 110, 180, 25); add(passField); JButton loginBtn new JButton(登录); loginBtn.setBounds(120, 160, 80, 30); loginBtn.addActionListener(e - doLogin()); add(loginBtn); } private void doLogin() { String name userField.getText().trim(); String pass new String(passField.getPassword()); if (name.isEmpty() || pass.isEmpty()) { JOptionPane.showMessageDialog(this, 账号和密码不能为空); return; } Doctor doctor doctorService.login(name, pass); if (doctor null) { JOptionPane.showMessageDialog(this, 账号或密码错误); return; } dispose(); new MainFrame(doctor).setVisible(true); } }setLayout(null)配合setBounds是绝对定位课设够用但不利于缩放。setLocationRelativeTo(null)让窗口居中。doLogin()里先做空值校验再查库避免无意义的 SQL 查询。登录成功后dispose()关掉登录窗把当前医生对象传给主窗口后续挂号时直接用这个对象拿doctor_id不用再查一次库。主界面用JTabbedPane分页挂号、患者管理、医生管理各占一页public class MainFrame extends JFrame { public MainFrame(Doctor doctor) { setTitle(医院管理系统 - 当前用户 doctor.getDoctorName()); setSize(900, 600); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); JTabbedPane tabs new JTabbedPane(); tabs.addTab(挂号管理, new RegistrationPanel(doctor)); tabs.addTab(患者管理, new PatientPanel()); tabs.addTab(医生管理, new DoctorPanel()); add(tabs); } }每个 Panel 独立成一个类内部自己管自己的表格和按钮。RegistrationPanel构造时接收当前医生对象挂号时默认选中该医生减少操作步骤。3.3 JTable 绑定查询结果的正确姿势Swing 里显示数据用JTable但很多人直接把ResultSet往表里塞结果关掉连接后数据全没了。正确做法是用DefaultTableModel先把数据读进内存public class PatientPanel extends JPanel { private JTable table; private DefaultTableModel model; private PatientDao patientDao new PatientDao(); public PatientPanel() { setLayout(new BorderLayout()); String[] columns {编号, 姓名, 身份证, 电话, 性别, 年龄}; model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; table new JTable(model); table.setRowHeight(28); add(new JScrollPane(table), BorderLayout.CENTER); JButton refreshBtn new JButton(刷新); refreshBtn.addActionListener(e - loadData()); add(refreshBtn, BorderLayout.SOUTH); loadData(); } private void loadData() { model.setRowCount(0); ListPatient list patientDao.findAll(); for (Patient p : list) { model.addRow(new Object[]{ p.getPatientId(), p.getPatientName(), p.getIdCard(), p.getPhone(), p.getGender() 1 ? 男 : 女, p.getAge() }); } } }isCellEditable返回 false 防止用户直接双击改表格数据所有修改走弹窗表单。model.setRowCount(0)先清空再加载避免刷新时数据重复。PatientDao.findAll()返回ListPatient而不是ResultSet连接在 DAO 内部就关掉了不会泄漏。4. JDBC 封装与事务处理挂号扣费不翻车的核心4.1 为什么必须封装 JDBC 工具类每个 DAO 方法都写一遍DriverManager.getConnection、PreparedStatement、close代码重复不说还容易忘关连接导致 MySQL 连接数爆掉。我一般写一个DBUtil类统一管理public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/hospital_db ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD ; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未加载, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }serverTimezoneAsia/Shanghai不加会报时区错误这是 MySQL 8.0 的经典坑。useSSLfalse在课设环境关掉 SSL 省事生产环境要开。Class.forName在静态块里执行一次即可新版驱动其实可以省略但写上兼容性更好。4.2 挂号事务插入挂号记录和更新患者状态必须原子挂号业务涉及两张表往registration插一条记录同时可能更新患者状态。这两步必须在一个事务里否则插了挂号但患者状态没更新数据就脏了。看代码public class RegistrationService { public boolean register(int patientId, int doctorId, BigDecimal fee) { Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String sql1 INSERT INTO registration(patient_id, doctor_id, fee, status) VALUES(?,?,?,0); ps1 conn.prepareStatement(sql1); ps1.setInt(1, patientId); ps1.setInt(2, doctorId); ps1.setBigDecimal(3, fee); ps1.executeUpdate(); String sql2 UPDATE patient SET age age WHERE patient_id ?; ps2 conn.prepareStatement(sql2); ps2.setInt(1, patientId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps1, null); if (ps2 ! null) try { ps2.close(); } catch (SQLException ignored) {} } } }conn.setAutoCommit(false)开启事务两步都成功才commit()任何一步抛异常就rollback()。setBigDecimal对应 DECIMAL 字段别用setDouble。注意finally里先关ps1再关ps2顺序反了可能报错。这个事务模板在课设里能覆盖 90% 的多表操作场景。4.3 用 PreparedStatement 防 SQL 注入登录查询如果用字符串拼接输入 OR 11就能绕过密码。必须用PreparedStatementpublic Doctor findByNameAndPassword(String name, String password) { String sql SELECT * FROM doctor WHERE doctor_name ? AND password ? AND status 1; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, name); ps.setString(2, password); rs ps.executeQuery(); if (rs.next()) { Doctor d new Doctor(); d.setDoctorId(rs.getInt(doctor_id)); d.setDoctorName(rs.getString(doctor_name)); d.setDeptId(rs.getInt(dept_id)); d.setTitle(rs.getString(title)); return d; } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return null; }?占位符由驱动负责转义用户输入什么都不会改变 SQL 语义。status 1过滤掉已停用的医生账号。返回null表示登录失败调用方判空即可。5. 避坑与排查课设里最容易翻车的 5 个点5.1 中文乱码从建库到 JDBC 全链路排查现象界面上显示的中文变成???或者乱码方块。原因字符集在某一环没统一。解决建库用utf8mb4my.ini里character-set-serverutf8mb4JDBC URL 加characterEncodingutf8Swing 源文件保存为 UTF-8。四环缺一不可用SHOW VARIABLES LIKE character%确认服务端字符集。5.2 驱动版本不匹配ClassNotFoundException 与时区报错现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或The server time zone value is unrecognized。原因MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver老版本才是com.mysql.jdbc.Driver时区不配会报错。解决驱动换成mysql-connector-j-8.x.jar类名加cjURL 加serverTimezoneAsia/Shanghai。5.3 连接未关闭MySQL 连接数耗尽现象程序运行一段时间后报Too many connections。原因DAO 方法里Connection没在finally关闭或者异常路径跳过了关闭。解决统一用DBUtil.close()在finally里关或者上连接池。课设阶段至少保证每个方法都有finally块。5.4 Swing 线程阻塞查询大数据量时界面卡死现象点击查询按钮后窗口无响应转圈圈。原因数据库查询在 EDT事件调度线程里执行阻塞了界面刷新。解决用SwingWorker把查询放到后台线程new SwingWorkerListPatient, Void() { Override protected ListPatient doInBackground() { return patientDao.findAll(); } Override protected void done() { try { ListPatient list get(); model.setRowCount(0); for (Patient p : list) { model.addRow(new Object[]{p.getPatientId(), p.getPatientName(), ...}); } } catch (Exception e) { e.printStackTrace(); } } }.execute();doInBackground在后台线程跑查询done回到 EDT 更新表格。课设数据量小可能感觉不到但养成习惯没坏处。5.5 外键约束导致删除失败现象删除科室时报Cannot delete or update a parent row。原因该科室下还有医生记录外键约束阻止删除。解决先删子表记录再删父表或者建表时设ON DELETE CASCADE。课设阶段建议手动处理先查该科室有没有医生有就提示用户先转移或删除医生。6. 进阶技巧用排序和存储过程让课设更像真实系统6.1 MySQL 排序在挂号列表里的实际用法挂号记录默认按时间倒序排最新的在最上面。SQL 里加ORDER BY reg_time DESC就行。但如果有多个排序条件比如先按状态排未就诊在前再按时间排SELECT r.reg_id, p.patient_name, d.doctor_name, r.reg_time, r.fee, r.status FROM registration r JOIN patient p ON r.patient_id p.patient_id JOIN doctor d ON r.doctor_id d.doctor_id ORDER BY r.status ASC, r.reg_time DESC;status ASC让未就诊0排在已就诊1前面同状态内按时间倒序。Java 端如果要对内存中的List排序用list.sort(Comparator.comparing(Registration::getStatus).thenComparing(Registration::getRegTime, Comparator.reverseOrder()))效果一样但数据量大时不如 SQL 排。6.2 用存储过程统计科室挂号量课设答辩时如果老师问「能不能统计每个科室的挂号人数」写个存储过程比在 Java 里循环查库优雅得多DELIMITER // CREATE PROCEDURE count_reg_by_dept() BEGIN SELECT d.dept_name, COUNT(r.reg_id) AS total FROM department d LEFT JOIN doctor doc ON d.dept_id doc.dept_id LEFT JOIN registration r ON doc.doctor_id r.doctor_id GROUP BY d.dept_id, d.dept_name ORDER BY total DESC; END // DELIMITER ;Java 端用CallableStatement调用CallableStatement cs conn.prepareCall({call count_reg_by_dept()}); ResultSet rs cs.executeQuery(); while (rs.next()) { System.out.println(rs.getString(dept_name) : rs.getInt(total)); }LEFT JOIN保证没有挂号记录的科室也显示数量为 0。DELIMITER //是告诉 MySQL 存储过程内部的;不是语句结束符别漏了。6.3 一个我踩过的坑密码明文存储最早做课设时密码直接明文存doctor表后来被老师点名批评。改成 MD5 加盐public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }登录时把用户输入的密码 MD5 后再和库里比对。MD5 现在不安全但课设够用至少不是明文。想再进一步就上 BCrypt引入jbcrypt依赖即可。做这个课设最大的收获不是写完多少行代码而是养成了「先设计表再写界面先封装工具类再写业务」的习惯。我后来做 Web 项目时发现当年 Swing 里练的 JDBC 事务和分层思想换成 Spring 之后只是换了个壳底层逻辑一模一样。如果你正在做这个课设别急着找源码自己把DBUtil和RegistrationService敲一遍比抄十份代码都值。希望帮到你。本文还有配套的精品资源点击获取
返回列表