Java+MySQL+Swing 实现医院挂号管理系统:从建表到事务防超挂

发布时间:2026/10/4 10:17:44
Java+MySQL+Swing 实现医院挂号管理系统:从建表到事务防超挂 简介这份资源是面向高校计算机专业学生与Java初学者的一套课程设计级项目源码主题为基于Java与MySQL的医院简易挂号管理系统采用GUI桌面应用形态帮助读者理解从需求分析到编码实现的完整软件开发流程。压缩包共331个文件约3.43MB以html说明页、h与cpp头源文件、class编译产物、png与gif界面素材、ui与fxml布局文件、java源码及jar与lib依赖库为主另含少量md文档与工程配置结构完整便于直接导入运行。系统覆盖用户登录、号源展示、挂号缴费、记录查询、退号退费与统计打印等模块并区分医护与患者角色权限密码加密存储配合异常提示与数据库持久化设计。目前已有752人学习下载适合作为课程设计参考、Java Swing与JDBC练习或数据库课程实践素材可据此梳理表结构设计、界面交互与业务逻辑实现思路。1. 从一张挂号单说起JavaMySQL 做 GUI 挂号系统到底在解决什么门诊高峰期挂号窗口前排着长队收费员一边翻纸质登记本一边手写科室、医生、序号患者等得不耐烦医生那边还看不到实时号源。这个场景里真正卡住的不是人手而是信息没有结构化——号源、患者、医生排班三份数据各管各的。基于 JavaMySQL 实现GUI医院简易挂号管理系统要干的事就是把这三份数据塞进一张关系表里用图形界面把「选科室→选医生→选时段→生成挂号单」这条链路串起来。它适合两类人一是刚学完 Java 基础、想找一个能写进简历的完整 CRUD 项目练手的学生二是需要给小型诊所、社区门诊搭一套轻量登记工具的开发者。核心词 Java、MySQL、GUI、挂号管理系统对应的就是后端逻辑、数据持久化、桌面交互和业务场景四件事。这个项目不追求高并发追求的是把「一张挂号单从生成到落库」的完整闭环跑通让你能看清一个业务系统最朴素的骨架长什么样。2. 技术选型为什么是 Swing JDBC 而不是 Spring Boot 前端框架2.1 桌面 GUI 与 Web 前端的取舍逻辑很多人第一反应是用 Spring Boot 加 Vue 做前后端分离但对「简易挂号管理系统」这个体量来说那套架构的部署成本远大于业务价值。Swing 是 JDK 自带的桌面 GUI 工具包不需要额外装 Node、不需要配 Nginx、不需要处理跨域一个java -jar就能在收费窗口那台 Windows 机器上跑起来。挂号这个场景的使用者就是窗口收费员操作半径不超过两米桌面程序反而比浏览器更稳——不会因为网络抖动导致挂号单提交失败。选 Swing 还有一个现实原因它逼你把注意力放在业务逻辑和数据流上而不是花三天调前端样式。JFrame、JPanel、JTable、JButton这几个组件组合起来足够撑起挂号、退号、查询三个核心界面。如果你后续想换成 JavaFX组件命名和事件模型有差异但分层思路是一样的迁移成本可控。2.2 MySQL 表结构设计与 JDBC 连接方式数据库选 MySQL 是因为它安装配置资料多、社区成熟遇到mysql ssl连接错误或者mysql 5.7.44 安装过程这类问题一搜就有答案。挂号系统的表不用多四张就够department科室、doctor医生、schedule排班号源、registration挂号记录。下面是我一般会用的建表语句字段类型和约束都按实际业务卡过-- 科室表一个科室对应多条医生记录 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, dept_desc VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生表外键关联科室职称用于挂号费分级 CREATE TABLE doctor ( doctor_id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(50) NOT NULL, dept_id INT NOT NULL, title VARCHAR(20) DEFAULT 住院医师, reg_fee DECIMAL(6,2) DEFAULT 10.00, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表号源的核心remaining 字段控制余号 CREATE TABLE schedule ( schedule_id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL, total_num INT DEFAULT 20, remaining INT DEFAULT 20, FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号记录表一条记录对应一次挂号行为 CREATE TABLE registration ( reg_id INT PRIMARY KEY AUTO_INCREMENT, patient_name VARCHAR(50) NOT NULL, patient_id_card VARCHAR(18), schedule_id INT NOT NULL, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1有效 0已退号, FOREIGN KEY (schedule_id) REFERENCES schedule(schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有两个参数值得单独说。remaining字段是余号计数器挂号时减一、退号时加一用UPDATE ... SET remaining remaining - 1 WHERE schedule_id ? AND remaining 0这种带条件的更新来防止超挂。status用 TINYINT 而不是布尔是为了以后扩展「已就诊」「已取消」等状态留余地。字符集统一用utf8mb4避免患者姓名里有生僻字时出现乱码。JDBC 连接这块我习惯把连接参数抽到一个db.properties文件里而不是硬编码在 Java 类中jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_reg?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.passwordyour_passworduseSSLfalse是本地开发常见做法能绕开mysql ssl连接错误那类握手问题serverTimezoneAsia/Shanghai不加的话reg_time存进去可能差 8 小时这个坑我踩过不止一次。连接池对这个小系统不是必须的但如果你想让代码看起来更专业可以引入 HikariCP配置也就多五行。2.3 项目分层把界面代码和 SQL 语句隔开新手最容易犯的错是把JButton的点击事件里直接写Statement.executeUpdate结果界面文件和 SQL 搅在一起改一个字段要翻三个文件。我一般会分三层ui包放 Swing 窗口service包放业务逻辑比如挂号前检查余号dao包放数据库操作。这样做的直接好处是当你要把挂号逻辑从「先查再改」换成「带条件更新」时只需要动dao层一个方法界面代码一行不用碰。3. 挂号主流程落地从选科室到生成挂号单的完整代码链路3.1 科室与医生下拉框的联动实现挂号界面第一个交互是选科室选完科室后医生下拉框要自动刷新。这个联动逻辑用ActionListener监听科室下拉框的选中事件然后查库填充医生列表// 科室下拉框监听选中后刷新医生列表 deptCombo.addActionListener(e - { Department selected (Department) deptCombo.getSelectedItem(); if (selected null) return; // 清空旧数据避免重复叠加 doctorCombo.removeAllItems(); ListDoctor doctors doctorService.findByDeptId(selected.getDeptId()); for (Doctor d : doctors) { doctorCombo.addItem(d); // Doctor 类重写 toString 返回医生姓名 } });这里的关键点是Doctor类要重写toString()让下拉框直接显示医生姓名而不是对象地址。findByDeptId对应的 SQL 是SELECT * FROM doctor WHERE dept_id ?用PreparedStatement传参不要用字符串拼接否则科室名里带单引号就会报语法错误。3.2 号源查询与余号扣减的事务处理选完医生后要查这个医生未来几天的排班和余号用JTable展示。用户选中某一行排班点「挂号」按钮触发扣减余号和插入挂号记录两个操作。这两个操作必须在一个事务里否则可能出现余号扣了但挂号记录没插进去的情况// 挂号核心逻辑扣余号 插记录同一事务 public boolean register(String patientName, String idCard, int scheduleId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步带条件扣减余号remaining 0 防止超挂 String updateSql UPDATE schedule SET remaining remaining - 1 WHERE schedule_id ? AND remaining 0; PreparedStatement ps1 conn.prepareStatement(updateSql); ps1.setInt(1, scheduleId); int affected ps1.executeUpdate(); if (affected 0) { conn.rollback(); return false; // 余号已满挂号失败 } // 第二步插入挂号记录 String insertSql INSERT INTO registration(patient_name, patient_id_card, schedule_id) VALUES(?, ?, ?); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setString(1, patientName); ps2.setString(2, idCard); ps2.setInt(3, scheduleId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException ex) { if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} ex.printStackTrace(); return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }这段代码里affected 0的判断是防超挂的关键。如果两个收费员同时给最后一个号挂号数据库层面只有一个UPDATE会成功另一个返回 0 行受影响直接回滚。这比「先 SELECT 查余号再判断」可靠得多后者在并发下必然出问题。setAutoCommit(false)和commit()成对出现finally里恢复自动提交是为了连接归还连接池时状态干净。3.3 挂号记录查询与退号的状态回滚查询界面用JTable加TableModel展示挂号记录支持按患者姓名模糊查询。退号操作是挂号的逆过程把registration.status置为 0同时把对应排班的remaining加一。同样要放在一个事务里// 退号状态置0 余号加1 public boolean cancelRegister(int regId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 查出该挂号对应的排班ID String querySql SELECT schedule_id FROM registration WHERE reg_id ? AND status 1; PreparedStatement ps1 conn.prepareStatement(querySql); ps1.setInt(1, regId); ResultSet rs ps1.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int scheduleId rs.getInt(schedule_id); // 状态置0 PreparedStatement ps2 conn.prepareStatement( UPDATE registration SET status 0 WHERE reg_id ?); ps2.setInt(1, regId); ps2.executeUpdate(); // 余号加1 PreparedStatement ps3 conn.prepareStatement( UPDATE schedule SET remaining remaining 1 WHERE schedule_id ?); ps3.setInt(1, scheduleId); ps3.executeUpdate(); conn.commit(); return true; } catch (SQLException ex) { if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }退号里status 1的条件不能省否则重复退号会导致余号被多次加回号源越退越多。这个 bug 在测试阶段不容易发现因为正常人不会连点两次退号但收费员手快的时候什么都可能发生。4. 避坑与排查挂号系统开发中最容易翻车的五个点4.1 中文乱码从数据库到 Swing 界面全链路排查现象是患者姓名存进数据库变成问号或者JTable里显示乱码。原因通常出在三个环节之一数据库字符集不是utf8mb4、JDBC URL 没带characterEncodingutf8、Swing 组件没设置字体。解决顺序是先SHOW VARIABLES LIKE character%确认数据库字符集再检查连接串最后在JFrame初始化时统一设置UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, 14))。三个环节都对了中文就不会出问题。4.2 余号扣成负数并发下的超挂问题现象是某个排班的remaining变成 -1 甚至 -2。原因是用「先查后改」的逻辑两个线程同时查到余号是 1都判断可以挂然后各自减一。解决办法就是 3.2 节里那种带remaining 0条件的UPDATE把判断和扣减合并成一条原子语句。如果你用的是 MySQL还可以给remaining加CHECK (remaining 0)约束作为最后一道防线但 5.7 版本对 CHECK 约束支持不完整8.0 才真正生效。4.3 时间差 8 小时serverTimezone 参数漏配现象是挂号记录的reg_time比实际时间早或晚 8 小时。原因是 MySQL 驱动 8.0 以后要求显式指定时区不指定就按 UTC 存。解决就是在 JDBC URL 里加serverTimezoneAsia/Shanghai。如果你用的是mysql 5.7.26或更早版本驱动版本也要对应com.mysql.jdbc.Driver和com.mysql.cj.jdbc.Driver混用会报类找不到。4.4 界面卡死在事件线程里做数据库查询现象是点击「查询」按钮后整个窗口无响应几秒钟。原因是 Swing 的事件分发线程EDT里直接执行了数据库查询查询期间界面无法重绘。解决办法是把耗时操作放到SwingWorker的doInBackground里查询结果在done方法里更新JTable。对于挂号这种数据量极小的系统查询通常很快但养成这个习惯没坏处。4.5 连接泄漏Connection 没关导致连接数耗尽现象是系统运行一段时间后报Too many connections。原因是Connection、PreparedStatement、ResultSet没有在finally里关闭。我一般用 try-with-resources 写法让编译器自动生成关闭代码。如果项目里已经写了很多手动关闭的代码至少确保Connection在finally里关Statement和ResultSet跟着Connection关也会释放但显式关闭更稳妥。5. 进阶技巧用存储过程封装挂号逻辑与界面数据校验5.1 把挂号事务写进 MySQL 存储过程当挂号逻辑需要在多个地方调用时把它封装成存储过程可以减少 Java 层的重复代码。下面这个存储过程接收患者信息和排班 ID内部完成扣余号和插记录通过输出参数返回结果DELIMITER // CREATE PROCEDURE sp_register( IN p_patient_name VARCHAR(50), IN p_id_card VARCHAR(18), IN p_schedule_id INT, OUT p_result INT ) BEGIN DECLARE v_affected INT DEFAULT 0; -- 开启事务 START TRANSACTION; -- 扣减余号 UPDATE schedule SET remaining remaining - 1 WHERE schedule_id p_schedule_id AND remaining 0; SET v_affected ROW_COUNT(); IF v_affected 0 THEN SET p_result -1; -- 余号不足 ROLLBACK; ELSE INSERT INTO registration(patient_name, patient_id_card, schedule_id) VALUES(p_patient_name, p_id_card, p_schedule_id); SET p_result LAST_INSERT_ID(); -- 返回挂号ID COMMIT; END IF; END // DELIMITER ;Java 层用CallableStatement调用CallableStatement cs conn.prepareCall({call sp_register(?,?,?,?)})前三个是输入参数第四个用registerOutParameter注册为输出参数。存储过程的好处是事务边界在数据库端Java 代码更薄坏处是调试不如 Java 方便改逻辑要重新部署 SQL 脚本。我一般只在逻辑稳定后才考虑迁移到存储过程。5.2 界面输入校验身份证号与姓名的合法性检查挂号界面如果不做输入校验患者姓名填成空、身份证号填成 20 位都能提交脏数据进了库后面查询和统计都麻烦。我习惯在点击「确认挂号」时先做三层校验姓名非空且长度不超过 20、身份证号用正则^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$匹配、排班必须已选中。校验不通过用JOptionPane.showMessageDialog弹提示不要静默失败。// 身份证号校验18位最后一位可为X private boolean isValidIdCard(String idCard) { if (idCard null || idCard.length() ! 18) return false; String regex ^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2]) (0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$; return idCard.matches(regex); }这个正则不校验校验位只做格式过滤对简易挂号系统够用。如果你要严格校验需要按 GB 11643 的加权算法算最后一位代码量会多三十行左右。5.3 用 JTable 自定义渲染器标记已满号源排班表里余号为 0 的行如果和正常行长得一样收费员容易误点。给JTable加一个自定义TableCellRenderer余号为 0 时把背景设成浅灰、文字设成红色// 自定义渲染器余号为0的行标灰 scheduleTable.setDefaultRenderer(Object.class, new 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); int remaining (int) table.getValueAt(row, 4); // 第5列是余号 if (remaining 0) { c.setBackground(Color.LIGHT_GRAY); c.setForeground(Color.RED); } else { c.setBackground(Color.WHITE); c.setForeground(Color.BLACK); } return c; } });这个渲染器要设在JTable上而不是列上否则只对某一列生效。table.getValueAt(row, 4)里的列索引要和你的TableModel列顺序一致写错了会取到别的字段显示效果就全乱了。5.4 从这个小系统能带走的通用能力做完这个挂号系统你手里真正值钱的东西不是那几百行 Swing 代码而是三个可迁移的习惯一是把事务边界画清楚知道哪些操作必须原子完成二是用带条件的 UPDATE 代替「先查后改」这个思路在库存扣减、抢票、点赞计数里都一样三是界面和逻辑分层改需求时不至于牵一发动全身。我带过几个做这个项目练手的人后面去面 Java 开发岗面试官问「你怎么处理并发扣减」能把这个挂号系统的余号扣减讲清楚基本就过关了。这个方向值不值得做取决于你是想要一个能跑通闭环的练手项目还是想要一个能撑住真实门诊流量的生产系统——前者这个方案足够后者你需要考虑连接池调优、读写分离和前端分离部署。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询