简介本资源是一套基于Java Swing与MySQL实现的医院预约挂号系统源码采用MVC架构设计面向学习Java桌面应用开发与数据库编程的学生及开发者可用于课程设计、毕业设计参考或技术练手。压缩包共172个文件约3.54MB其中73个java源文件与86个class文件构成核心业务逻辑另有4个jar依赖包、6个png界面素材、2个properties配置文件及1个md说明文档结构完整、层次分明。系统通过Swing构建用户界面借助视图切换监听器管理标签页等组件状态变化后台以MySQL完成预约挂号数据的存储与查询代码注释详尽、编写规范便于理解与二次开发。目前已有62人学习浏览适合希望掌握MVC分层思想、Swing界面事件处理与JDBC数据库操作的读者参考借鉴。1. 从一份 JavaSwing 挂号系统源码说起桌面端 CRUD 到底能跑多远医院预约挂号系统这个题目在课程设计和企业内训里出现的频率高得离谱。但真正拿到一份基于 JavaSwing 和 MySQL 的源码包时多数人第一反应是「这不就是个带界面的增删改查吗」。我最初也这么想直到把一份类似结构的代码翻了三遍才发现它其实是一个被低估的桌面端工程样本Swing 负责事件分发与界面渲染JDBC 负责连接池与事务边界MySQL 负责约束与并发控制三者叠在一起恰好覆盖了中小型管理系统最核心的落地链路。它适合两类人一类是想把 Java 桌面开发从「能跑」推到「能交付」的在校生或转行者另一类是需要快速搭一套内部挂号/排班工具、又不想上前后端分离的中小机构技术负责人。这篇文章不聊虚的只拆这套源码里真正值得抄的部分——表结构怎么定、JDBC 连接怎么配、Swing 事件怎么不卡界面、MySQL 那边有哪些参数一改就翻车。2. 表结构与 MySQL 侧准备挂号系统到底该建几张表2.1 从业务动作倒推表设计而不是先画 ER 图很多人拿到源码第一件事是打开 Navicat 看表然后照着建一遍。这个顺序是反的。挂号系统的核心业务动作只有四个患者注册、医生排班、患者选号、管理员核销。把这四个动作拆成数据实体最少需要五张表患者表、医生表、排班表、挂号记录表、科室表。源码里如果多出「用户权限表」或「日志表」那是为了扩展不是最小闭环。我一般会先写一张排班表因为它是整个系统里唯一带时间维度和并发争抢的表。排班表的设计直接决定了后面挂号记录能不能防超卖。常见做法是CREATE TABLE doctor_schedule ( schedule_id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, dept_id INT NOT NULL, work_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, total_quota INT NOT NULL DEFAULT 0, used_quota INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, UNIQUE KEY uk_doc_date_slot (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句里有三个参数值得单独说。used_quota和total_quota分开存而不是只存剩余量是为了在挂号记录表里做对账时能还原「当时放了多少号」。version字段是给乐观锁用的Swing 客户端并发不高但同一台机器上开两个窗口抢同一个号的情况真实存在。UNIQUE KEY把医生、日期、时段锁成唯一防止管理员重复排班。挂号记录表则要留一个status字段取值 0 未核销、1 已核销、2 已取消。不要用布尔值因为后面加「爽约」状态时你会后悔。2.2 MySQL 安装与连接配置避开 error 2002 和 SSL 两个高频坑源码跑不起来十次里有六次卡在数据库连接。热搜里error 2002 (hy000): cant connect to local mysql server through socket /tmp和mysql jdbc usessl 与 sslmode 使用这两个词能反复出现说明踩的人足够多。先说 error 2002。这个报错的字面意思是客户端找不到本地 socket 文件但根因通常有三种MySQL 服务根本没启动、socket 路径和客户端默认路径不一致、或者你用的是 Docker 安装 MySQL 但没做端口映射。排查顺序我固定为# 第一步确认服务状态 systemctl status mysqld # 第二步确认 socket 实际路径 mysqladmin variables | grep socket # 第三步如果是 Docker确认端口映射 docker ps | grep 3306如果是 Docker 安装 MySQL连接串里不要写localhost要写127.0.0.1因为localhost在部分 JDBC 驱动里会走 socket 而不是 TCP。这个细节在docker 安装 mysql的场景下几乎必踩。再说 SSL。MySQL 8.x 默认开启 SSLJDBC 连接串如果不显式声明会报javax.net.ssl.SSLException或者直接连接超时。源码里如果用的是老版本驱动连接串通常长这样String url jdbc:mysql://127.0.0.1:3306/hospital_reg?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8;四个参数逐个解释useSSLfalse关闭 SSL 握手本地开发够用allowPublicKeyRetrievaltrue配合 MySQL 8 的 caching_sha2_password 认证插件不加会报公钥检索失败serverTimezone必须设否则DATE字段取出来会差 8 小时characterEncodingutf8防止中文科室名变问号。生产环境不要照抄useSSLfalse应该配sslmodeREQUIRED并导入证书但那是另一条链路。提示连接池不要用 DriverManager 裸连。源码里如果每次操作都DriverManager.getConnection界面点十次就会卡。换成 HikariCP最小配置maximumPoolSize10、connectionTimeout3000即可。3. Swing 界面与 JDBC 的缝合事件线程为什么总在挂号按钮上翻车3.1 EDT 规则所有数据库操作必须滚出事件分发线程Swing 最容易被忽视的一条铁律是所有界面更新必须在事件分发线程EDT里做所有耗时操作必须不在 EDT 里做。挂号按钮的actionPerformed默认就跑在 EDT 上如果你在里面直接调 JDBC 查询排班余量网络稍微抖一下整个窗口就白屏假死。这不是玄学是线程模型决定的。正确做法是把数据库操作包进SwingWorker。下面这段是挂号动作的最小骨架new SwingWorkerBoolean, Void() { Override protected Boolean doInBackground() throws Exception { // 后台线程执行挂号事务 return registrationService.register(patientId, scheduleId); } Override protected void done() { // EDT更新界面 try { if (get()) { quotaLabel.setText(挂号成功); } else { quotaLabel.setText(号源已满); } } catch (Exception e) { JOptionPane.showMessageDialog(null, 系统异常 e.getMessage()); } } }.execute();doInBackground里做事务done里碰组件这是唯一安全的写法。参数上要注意SwingWorker的泛型第一个是返回值类型第二个是中间进度类型不要混。如果挂号逻辑里要更新进度条用publish和process配对。3.2 挂号事务的 SQL 写法乐观锁比 synchronized 更靠谱防超卖这件事在 Swing 单机场景下很多人用synchronized锁方法。能跑但一旦部署成两台机器就废了。更稳的做法是把并发控制下沉到 MySQL用一条带条件的 UPDATE 完成扣减UPDATE doctor_schedule SET used_quota used_quota 1, version version 1 WHERE schedule_id ? AND used_quota total_quota AND version ?;执行后检查executeUpdate()返回值。返回 1 说明扣减成功返回 0 说明要么号满了要么版本号被别人改过。版本号从哪来在同一个事务里先SELECT version FROM doctor_schedule WHERE schedule_id ? FOR UPDATE拿到当前值再拼进 UPDATE。FOR UPDATE会加行锁事务提交后释放。这里有个参数细节used_quota total_quota这个条件必须写在 WHERE 里不能先查再判断再更新否则中间有窗口期。MySQL 的UPDATE本身是原子的把判断条件塞进去就省掉了应用层的锁。事务边界用 JDBC 手动控制Connection conn dataSource.getConnection(); try { conn.setAutoCommit(false); // 1. 查版本号 FOR UPDATE // 2. 执行扣减 UPDATE // 3. 插入挂号记录 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }setAutoCommit(false)之后别忘了在 finally 里还原连接池回收连接时如果 autocommit 状态不对下一个借到的人会莫名其妙丢事务。3.3 界面刷新与数据一致性别让用户看到过期号源Swing 的JTable默认不会自动感知数据库变化。挂号成功后如果只更新了标签文字表格里的余量还是旧的。常见做法是封装一个refreshScheduleTable()方法在done()里调用。但更细一点的做法是只刷新受影响的那一行用tableModel.fireTableRowsUpdated(rowIndex, rowIndex)避免整表重查。如果源码里用的是DefaultTableModel注意它的setValueAt会触发fireTableDataChanged在数据量大时会有性能问题。换成自定义AbstractTableModel只在你明确知道哪一行变了的时候发对应事件。注意不要在done()里再开一个新线程去查数据库那样又绕回 EDT 之外了。所有数据在doInBackground里查完通过get()带回 EDT。4. 避坑与排查源码跑不起来时先看这五条4.1 现象登录界面能开点挂号就报 NullPointerException原因通常是dataSource在界面类里被声明成实例变量但初始化放在main方法里而界面类被new出来的时机早于初始化。解决把数据源初始化提到静态块或者用单例工具类DBUtil.getDataSource()统一取。4.2 现象中文科室名在表格里显示成问号原因有三层数据库字符集不是utf8mb4、JDBC 连接串没带characterEncodingutf8、Swing 组件字体不支持中文。排查顺序从数据库开始SHOW VARIABLES LIKE character%确认character_set_server是utf8mb4再查连接串最后查JTable的字体设置。4.3 现象MySQL 8 下报Public Key Retrieval is not allowed原因MySQL 8 默认认证插件是caching_sha2_passwordJDBC 驱动在非 SSL 连接下需要显式允许公钥检索。解决连接串加allowPublicKeyRetrievaltrue。如果生产环境不允许就把用户认证插件改成mysql_native_password但这是降级方案。4.4 现象排班日期在界面上总是差一天原因java.sql.Date和java.util.Date互转时区处理不一致加上连接串没设serverTimezone。解决连接串固定serverTimezoneAsia/Shanghai界面层统一用LocalDate接收不要用java.util.Date。4.5 现象连续点两次挂号第二次报主键冲突原因挂号记录表的主键用了自增但业务上患者和排班组合应该唯一。如果源码里没加唯一约束重复提交就会插两条。解决在(patient_id, schedule_id)上加唯一索引并在应用层捕获SQLIntegrityConstraintViolationException后提示「请勿重复挂号」。5. 把源码改造成能交付的内部工具三个进阶动作5.1 用存储过程封装挂号事务减少网络往返当挂号逻辑变复杂比如要同时扣减号源、写记录、更新患者累计挂号次数在 Java 里拼多条 SQL 不如直接写一个存储过程。MySQL 存储过程的声明语法热搜里也有人问这里给一个可用的骨架DELIMITER // CREATE PROCEDURE register_appointment( IN p_patient_id INT, IN p_schedule_id INT, OUT p_result INT ) BEGIN DECLARE v_version INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result -1; END; START TRANSACTION; SELECT version INTO v_version FROM doctor_schedule WHERE schedule_id p_schedule_id FOR UPDATE; UPDATE doctor_schedule SET used_quota used_quota 1, version version 1 WHERE schedule_id p_schedule_id AND used_quota total_quota AND version v_version; IF ROW_COUNT() 0 THEN ROLLBACK; SET p_result 0; ELSE INSERT INTO registration_record(patient_id, schedule_id, status, create_time) VALUES(p_patient_id, p_schedule_id, 0, NOW()); COMMIT; SET p_result 1; END IF; END // DELIMITER ;DELIMITER //是必须的否则 MySQL 客户端会把存储过程内部的分号当成语句结束。EXIT HANDLER捕获异常后回滚并返回 -1调用方根据p_result判断结果。Java 侧用CallableStatement调用参数用registerOutParameter注册输出类型。5.2 给排班表加索引别等数据量上来才后悔排班表最常用的查询是「按科室和日期查可挂号医生」。如果只在主键上有索引数据到几万行时界面加载会明显变慢。加一个联合索引CREATE INDEX idx_dept_date_slot ON doctor_schedule(dept_id, work_date, time_slot);索引列顺序有讲究dept_id选择性最高放最左work_date范围查询放中间time_slot等值查询放最后。如果查询条件里经常只有work_date没有dept_id那这个索引用不上需要再补一个(work_date, dept_id)的索引。用EXPLAIN验证看type是不是ref或rangekey是不是命中了你建的索引。5.3 用连接池监控暴露慢查询而不是等用户投诉HikariCP 自带metrics支持配合setLeakDetectionThreshold(5000)可以在连接被持有超过 5 秒时打日志。这个参数在开发期设小一点能帮你抓到忘记close()的连接。生产环境设 30 秒以上避免误报。另外MySQL 侧开慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;long_query_time设 1 秒跑一周后看日志里有没有挂号相关的 SQL。如果有多半是排班表缺索引或者事务里做了全表扫描。我自己的习惯是每次改完挂号逻辑先在本地把long_query_time设成 0.1跑一遍完整流程确认没有慢查询再提交。这个习惯帮我省掉了至少三次上线后的紧急回滚。希望帮到你。本文还有配套的精品资源点击获取