简介:基于Java Swing和MySQL的医院预约挂号系统源码包,面向计算机相关专业学生及Java桌面应用开发者,以MVC架构完整呈现医院预约挂号流程,涵盖用户管理、医生管理、科室管理和挂号预约等业务模块,可帮助读者快速理解Java界面开发与数据库交互的协作方式。压缩包共172个文件,以86个class和73个java源码为主体,另含界面渲染所需的png图片、运行依赖的jar包、系统配置用的properties文件及项目说明md,整体大小3.54MB,目录结构清晰。该套源码已有62人学习浏览,代码注释详细、命名规范,视图切换监听器等关键组件的使用贯穿多个管理面板,借助完整工程可直接运行体验。通过阅读与调试项目,可掌握Java Swing事件处理、MySQL数据持久化、MVC分层设计的具体落地方法,适合作为课程设计、毕业设计或自学Java桌面开发的重要参考。
1. 一套 JavaSwing 医院预约挂号系统源码,为什么值得你花一周去跑通
手里拿着医院预约挂号系统的源码包,却不知道从哪下手,这是很多初学者拿到这类项目的第一反应。JavaSwing +Mysql 的组合听起来像是十几年前的技术栈,和现在满天飞的 Spring Boot、Vue 有代差。但你换个角度看:这类纯桌面端管理系统的业务闭环非常完整——排班、挂号、退号、患者信息管理、号源统计,每一项都踩在数据库设计和 GUI 交互的交叉点上。我见过不少同学靠改这套系统拿到了不错的毕业设计成绩,也见过有人因为跑不起来而熬夜到天亮。
这套源码能解决的问题很明确:给你一个不用搭前端、不用配 Maven 私服,打开就能看到界面的完整桌面应用。它适合三类人:需要交课程设计/毕业设计的学生,想快速理解 Swing 事件模型和 Mysql 事务配合的初级开发者,以及要在内网环境里做小规模预约管理 demo 的从业者。下面我用把它真正跑通、改明白的路径,拆给你看。
2. 挂号系统的业务模型与核心表设计:先把“号”搞清楚
2.1 预约挂号的核心流程:从排班到就诊的闭环
医院预约挂号系统的难点不在界面,而在“号”的状态流转。你把一个号的周期走完,业务就通了:科室创建排班计划,排班计划产生号源,患者选择某个时间段的号源提交预约,预约成功后生成挂号记录,就诊完成后记录归档。整个过程涉及三个关键对象:医生排班、号源、挂号单。
常见错误是直接按“挂号单表”建模,把医生姓名、科室、时间全部塞进一张表。这样做的后果是:同一位医生每天上午 8:00-8:30 的号有 20 个,你只能在挂号单里重复存 20 条一模一样的排班信息。一旦医生停诊,你得批量改 20 条;一旦号满了,你得 count 一下再决定是否允许插入,并发一高就超挂。
正确做法是拆分排班和号源。排班表只存“这个科室的某位医生在哪个日期、哪个时段出诊,总号量多少”;真正被患者抢的是号源序号,例如“上午 1 号、上午 2 号”。挂号记录表则引用排班表和患者表,形成一条流水。这样设计以后,停诊只需修改排班状态,退号只需回滚号源状态,统计某个时段履约率也只要关联排班和挂号记录两张表。
2.2 数据库设计:六张核心表撑起挂号闭环
我拆过不少同类型源码,常规的表结构是六张:用户/管理员表、科室表、医生表、排班计划表、号源表、挂号记录表。有的会把患者信息单独拆出来,有的直接以被监护人身份挂在用户上。下面是我在实际跑通项目后认为最稳的一套结构。
科室表最低限度要有科室编码和科室名称;医生表必须关联科室,否则排班时下拉框的联动逻辑会很痛苦;用户表需要区分角色,用类型字段区分管理员、医生、患者,避免为每种角色建一套登录表。排班计划表是信息中枢,存放科室、医生、日期、午别(上午/下午)、总号数、已约号数、班次状态;号源表则是排班计划的展开,排班表有多少个总号数,号源表就生成多少条记录,每条记录包含号序、时间段、状态。
挂号记录表属于流水表,需要记录谁挂的、挂哪个排班、哪个号序、挂号和退号时间。有的项目会把支付金额和支付状态也放进来,但纯预约场景下可以精简,避免和挂号业务混淆。
2.3 可抄作业的建表脚本:关键字段与索引设计
下面给出表结构的核心脚本,你可以直接放进 Navicat 或 Mysql 命令行执行。我将拆分号源表和排班表,这是避免超挂的基础。
-- 医生排班计划表 CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, schedule_date DATE NOT NULL, period_type TINYINT NOT NULL COMMENT '1上午 2下午', total_num INT NOT NULL COMMENT '总号数', booked_num INT NOT NULL DEFAULT 0 COMMENT '已约号数', status TINYINT NOT NULL DEFAULT 1 COMMENT '1出诊 0停诊', UNIQUE KEY uk_doctor_date_period (doctor_id, schedule_date, period_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 号源表:每个具体时段对应一个号序 CREATE TABLE schedule_number ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, number_seq INT NOT NULL COMMENT '第几号', start_time VARCHAR(10) NOT NULL, end_time VARCHAR(10) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1被预约 2已过号', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 挂号流水表 CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, number_id BIGINT NOT NULL, create_time DATETIME NOT NULL, cancel_time DATETIME NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1已预约 2已就诊 3已退号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个参数值得说明。status TINYINT用数字枚举而不直接用字符串,是为了节省存储空间并防止拼写错误,在 Java 代码里用常量映射。UNIQUE KEY uk_doctor_date_period是硬约束:同一医生同一天同一午别只能有一条排班记录,这是防止重复插入排班的关键。schedule_number表里的version是乐观锁字段,当多用户同时挂同一个号源时,UPDATE 条件里带上version就能避免两人拿到同一号。
很多人会问:为什么不直接在挂号前 SELECT 判断booked_num < total_num?因为 SELECT 和 UPDATE 之间有空窗,两个会话同时读到已约 19 号且总数 20 号,然后各自插入,就会超挂。正确做法是在事务里直接执行UPDATE doctor_schedule SET booked_num = booked_num + 1 WHERE id = ? AND booked_num < total_num,如果受影响行数为 0 就说明已满。这就是为什么排班表里一定要保留booked_num字段,而不是用COUNT现算。
2.4 为什么不用外键,而是靠应用层维护关系
初学者喜欢建外键,因为看起来严谨。但这套系统里我建议不建物理外键,原因有两个:第一,Swing 应用的并发程度低,但操作路径复杂,一个退号操作涉及号源、排班、挂号流水三张表的更新,有物理外键时先删哪个表记录会报错,还会影响插入顺序的灵活性;第二,外键约束的检查代价在业务增长后是隐患,维护时会让人抓狂。
替代方案是:在 Java 数据访问层里保证引用关系。比如删除排班前,先查挂号流水是否有关联记录;插入号源时,先确认排班记录存在。这样逻辑清晰,报错时也更容易定位是“业务不允许”而不是数据库叽叽歪歪。前提是你要把 DAO 层放到一个包里统一管理 SQL,别把 SQL 散落在按钮事件里。
3. Swing 客户端的界面分层与事件驱动:别把逻辑堆在按钮里
3.1 登录窗口与主窗体:为什么用 CardLayout 而不是多个 JFrame
拿到源码后要做的第一件事,是区分“登录窗口”和“主窗体”。很多质量不高的源码直接用new JFrame()一个套一个,登录完了把登录窗口 hidden,再 new 一个主窗口。这种做法的问题在于:窗口之间内存无法统一管理,弹窗关闭后 Java 进程不退出,而且主窗体要借用登录用户信息时还得靠静态变量传递。
推荐做法是登录窗口只负责认证,认证成功后把当前用户对象塞进主窗体的构造函数,主窗体内部用CardLayout切换不同功能面板。这样做的好处是主窗体只创建一次,切换面板只是换卡片,不会每次打开预约面板就重新 connected 数据库。
// 主窗体中初始化 CardLayout CardLayout cardLayout = new CardLayout(); JPanel cardPanel = new JPanel(cardLayout); cardPanel.add(new LoginPanel(), "login"); cardPanel.add(new DoctorSchedulePanel(), "schedule"); cardPanel.add(new RegisterPanel(), "register"); cardPanel.add(new MyOrderPanel(), "myOrder"); cardLayout.show(cardPanel, "login"); // 切换面板 cardLayout.show(cardPanel, "schedule");这段代码是“新建登录面板、排班面板、挂号面板、我的订单面板”的典型写法。CardLayout.show的参数是字符串 key,对应add时传入的第二个参数,容易写错,建议把卡片 key 定义为常量类,比如CardKeys.SCHEDULE,避免到处写字符串。这四个面板的创建其实只执行一次,数据刷新要靠面板自己的刷新方法,而不是重建面板。
3.2 医生排班、挂号登记、我的挂号三个功能面板的实现
这三个面板基本覆盖了系统的 80% 交互。排班面板负责展示当前医生未来一周的出诊计划,挂号面板让患者选医生和时段后抢占号源,我的挂号面板展示当前登录用户的预约记录并允许退号。
在挂号面板里,核心逻辑是:加载下拉列表时从数据库查科室,科室选中后再查医生,医生选中后再加载未满的排班时段。这里要防止每选一个下拉项就重新Statement.executeQuery一次,否则界面会明显卡顿。常见做法是:窗体加载时把科室、医生、排班全部查到一个内存 Map 里,界面交互只是从 Map 里取值;患者点了“挂号”按钮才真正写数据库。
“挂号登记”按钮的事件里建议做三步校验:第一步检查排班状态是否为出诊且未停诊;第二步用UPDATE ... SET booked_num = booked_num + 1 WHERE booked_num < total_num抢占号源;第三步插入号源明细号和挂号流水,整个流程包在事务里。如果你在源码包看到的是“先 SELECT 看余号,再 INSERT 挂号”,那你要警惕并发问题,后面避坑章节细说。
3.3 连接 MySQL 的工具类与密码加密的取舍
源码里一般会有一个DBUtil.java,用DriverManager.getConnection(url, user, password)连接本地数据库。建议用连接池代替裸连,虽然 Swing 桌面端并发不高,但你反复打开关闭面板时,裸连会不断创建和销毁 Connection,消耗性能。轻量做法是使用HikariCP,在工具类里初始化一次连接池,后续所有 DAO 去连接池拿连接。
public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/hospital_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }注意参数里必须有characterEncoding=utf8或utf8mb4,否则中文乱码是大概率事件;serverTimezone=Asia/Shanghai是 Mysql 8.0 以上驱动的硬性要求,不写就会报时间区错误。密码硬编码在工具类里确实不安全,但作为课程设计级别的源码,这样做最直观,也能让审核者一眼看懂。生产环境建议从外部配置文件读取,并给配置文件限定权限。
3.4 Swing 多线程:JDBC 查询不能直接阻塞事件线程
这是所有 Swing 桌面应用最容易翻车的地方。假设你在“查询今日排班”按钮的actionPerformed里直接执行数据库查询,数据库一旦慢,整个窗口就“假死”,按钮点不动,窗口拖不动,用户以为程序崩溃了。
正确做法是把耗时查询放到SwingWorker里,查询完成后在done()方法里更新界面。源码里如果没有这个习惯,你要自己补上。这里给一个最小模板:
SwingWorker<List<Schedule>, Void> worker = new SwingWorker<List<Schedule>, Void>() { @Override protected List<Schedule> doInBackground() throws Exception { return scheduleDao.findAvailableSchedule(doctorId); } @Override protected void done() { try { List<Schedule> list = get(); scheduleTable.setModel(convertToTableModel(list)); } catch (Exception e) { JOptionPane.showMessageDialog(frame, "查询失败:" + e.getMessage()); } } }; worker.execute();doInBackground里不能碰任何 Swing 组件,done里不能做数据库操作。这套蚂蚁搬家式的规则是为了避免多线程同时修改界面状态,理解不了先记住,照做就不会出现窗口白屏和数据错乱。
4. 把源码包在本地跑通:导入、配置与启动顺序
4.1 导入 IDE 前的项目结构检查
拿到.zip源码包后,先别急着解压。建一个空目录,把所有内容解压进去,然后看目录下有没有.classpath和.project文件。有这两个文件说明是 Eclipse 工程;没有的话多半是手动整理过的源码,要用 IDEA 的“新建项目从现有源码”导入。
我建议用 IDEA 打开,选 New Project 时指向解压后的根目录,然后让 IDE 自动识别 SDK 和源码目录。如果源码里同时有src和bin目录,一定把bin目录排除掉,否则你会看到两套代码,改了src跑的还是旧 class。接着检查 lib 目录下有没有mysql-connector-java的 jar 包,没有就看build path或pom.xml。常见做法是手动把 jar 包 add 到项目的库中,注意驱动版本要匹配你的 Mysql 版本。
4.2 修改数据库配置与初始化数据
源码里的数据库配置一般在DBUtil.java或application.properties里。你需要先在本地 Mysql 里建一个同名数据库,然后执行项目里自带的.sql脚本。如果源码包没给 SQL 脚本,只有实体类,那你要反向建表,比较吃力;好在大多数这类项目都会附一个hospital.sql。
执行 SQL 时注意编码:最好用 Navicat 或命令行设置set names utf8mb4,避免把中文注释和初始数据导成乱码。导入完成后,先手动执行几条基础 SQL,验证表结构是否存在,再检查用户表里有没有初始管理员账号和密码,很多源码初始密码是admin / admin或123456,如果你怎么都登不上,直接去查用户表的 SELECT 结果,看密码是不是 MD5 加密的。
4.3 启动后必测的五个业务用例
跑通界面只是第一步,你至少要验证下面五个用例,才能在答辩或演示时不掉链子。
第一个是登录鉴权,输入正确账号密码能否进入主窗体,错误密码有无提示。第二个是首次加载排班列表,右侧表格是否展示了未来七天的出诊安排。第三个是挂号流程,先选科室,再选医生,选时段,点击挂号后“我的挂号”里立刻出现新记录。第四个是号源控制,把某个排班的剩余号数挂完,再尝试挂号应该被拒绝。第五个是退号,退号后号源余量加一,重复退同一条记录应报错。
每个用例都要对应到数据库的实际变化,比如挂一次号,registration表多一条记录,schedule_number里对应号序状态变成 1,doctor_schedule的booked_num加 1。如果这三处只改了一处,说明事务没包好,后面业务数据就会对不上。
5. JavaSwing + Mysql 挂号系统避坑指南:编译、乱码与并发事务
5.1 现象:启动即报ClassNotFoundException或窗口闪退
原因很直接:mysql-connector-java的 jar 没有加到运行环境里。在 IDE 里你给项目添加了库,但直接java -jar运行时,JVM 找不到驱动类。解决方法是把 jar 放到一个lib目录,并在想打包成可运行 jar 时用带 classpath 的清单文件,或者干脆用 fat jar 插件把依赖打进去。
另外,如果你的 JDK 是 17 或更高,而驱动是 5.x 旧版本,也会因为模块访问权限问题直接闪退。常见做法是升级mysql-connector-java到 8.0.x,把Class.forName("com.mysql.cj.jdbc.Driver")和连接 URL 里的serverTimezone一并改好。
5.2 现象:向数据库插入中文变成问号
这是老生常谈,但每天都有人踩。现象是界面显示正常,SQL 查询结果里中文变成一串问号。根源从连接 URL 的字符集到表字段的字符集都可能。解决方法是逐层排查:先看连接 URL 有没有characterEncoding=utf8mb4;再看建表语句是否用了DEFAULT CHARSET=utf8mb4;最后看 MySQL 的my.ini配置文件里character-set-server是否设为 utf8mb4。三层都对了,中文基本不会乱。
如果源码里用的是Statement拼字符串,务必改成PreparedStatement。PreparedStatement会正确处理参数类型和编码,还能防止 SQL 注入。像患者姓名、备注这类用户输入,用字符串拼接本身就是高危行为,这门课可是有实际教训的。
5.3 现象:多人同时挂号,最后一个号被挂重
这是业务系统最怕的“超卖”。现象是两个人同时提交,数据库里出现同一条schedule_number被两条挂号记录引用。原因就是前面说的“先查询余号再插入”的默认写法。
解决方法是把余号扣减和插入号源放在同一个事务,并执行带条件的 UPDATE。下面给出正确的事务片段:
START TRANSACTION; UPDATE doctor_schedule SET booked_num = booked_num + 1 WHERE id = 1001 AND booked_num < total_num; -- 检查受影响行数,若为0立即回滚 INSERT INTO schedule_number (schedule_id, number_seq, start_time, end_time, status) VALUES (1001, 21, '08:00', '08:30', 1); INSERT INTO registration (patient_id, schedule_id, number_id, create_time, status) VALUES (2, 1001, 5001, NOW(), 1); COMMIT;这里的灵魂语句是UPDATE ... SET booked_num = booked_num + 1 WHERE booked_num < total_num。如果影响行数是 0,说明号已经满,此时回滚并提示“该时段已约满”。这比 SELECT 判断安全得多,也是面试官最爱问的“如何防超卖”的答案。Swing 这样的桌面应用单机操作体验不到并发问题,但一旦你改成网络版,这条就是保命逻辑。
5.4 现象:退号后,号源状态和排班余量对不上
经常看到的现象是:退号成功了,registration表状态改成已退号,但schedule_number还是“已预约”,doctor_schedule.booked_num也没有减一。原因是你直接在界面上删除了挂号记录,或者直接把状态字段改成“已退号”,没有联动更新另外两张表。
正确退号要三步走:第一步更新挂号记录状态为已退号;第二步更新号源状态为空闲;第三步把排班表的booked_num减一。这三步必须包在一个事务里,否则任何一个步骤失败都会留下脏数据。源码里如果退号逻辑只做了一步,你一定要补全,否则演示退号后重新挂号会失败。
5.5 现象:窗口在不同分辨率下控件重叠、布局乱飞
Swing 用的是LayoutManager管理位置,很多源码怕麻烦,给每个控件都setBounds(x, y, w, h)绝对定位。这样在你电脑上看起来正常,换一台宽屏或高 DPI 的机器就完全错位。
解决方法是给每个面板指定一个BorderLayout或GridBagLayout,让内容区域自然伸缩。比如主窗体中心区域放卡片面板,顶部放功能按钮,底部放状态栏;内部面板里用GridLayout排列多行表单。如果源码已经全用了绝对定位,我这里没有后悔药,只能建议你用 IDEA 自带的 Swing GUI Designer 拉一遍布局,或者接受“固定窗口大小不可缩放”这个妥协方案——课程设计里也够用,但不体面。
6. 让答辩或面试拿得出手的三个进阶技巧
第一个技巧是给系统加一张“操作日志表”。不需要复杂的框架,记录谁在什么时间点执行了哪个功能即可。你可以用一个AOP思想,在 DAO 层的方法入口记录日志,或者更简单地,在关键按钮的事件里插入日志写入代码。这一改动成本极低,却能让答辩老师觉得你考虑了审计和安全。
第二个技巧是用乐观锁改造号源抢占逻辑。在schedule_number表加version字段,更新号源状态时携带旧的 version,如果更新影响行数为 0,说明号源被并发改了,此时抛异常提示用户重试。这套机制比上一篇提到的UPDATE ... WHERE booked_num < total_num更精细,两者可以结合用。
第三个技巧是做一个简单的号源可视化,用一个JTable以 30 分钟为时间段,把每个排班的剩余号数用颜色深浅表示,红色表示即将约满,绿色表示充足。这就要写一个自定义TableCellRenderer,代码量不大,但属实的视觉效果能提升整个项目的完成度。
坦白说,我第一次跑通这类源码时,也被 Swing 的线程模型折腾得不行,查了整晚才明白不能在actionPerformed里直接 sleep 等查询。如果你正在调这套系统,我建议你先把项目复制一份,随意改代码,跑挂了再从原始包恢复,这种“手贱”操作反而是成长最快的路径。希望帮到你。
本文还有配套的精品资源,点击获取