简介基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 号源表每个具体时段对应一个号序 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 乐观锁版本号 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号流水表 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已退号 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数值得说明。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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); 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(); } }注意参数里必须有characterEncodingutf8或utf8mb4否则中文乱码是大概率事件serverTimezoneAsia/Shanghai是 Mysql 8.0 以上驱动的硬性要求不写就会报时间区错误。密码硬编码在工具类里确实不安全但作为课程设计级别的源码这样做最直观也能让审核者一眼看懂。生产环境建议从外部配置文件读取并给配置文件限定权限。3.4 Swing 多线程JDBC 查询不能直接阻塞事件线程这是所有 Swing 桌面应用最容易翻车的地方。假设你在“查询今日排班”按钮的actionPerformed里直接执行数据库查询数据库一旦慢整个窗口就“假死”按钮点不动窗口拖不动用户以为程序崩溃了。正确做法是把耗时查询放到SwingWorker里查询完成后在done()方法里更新界面。源码里如果没有这个习惯你要自己补上。这里给一个最小模板SwingWorkerListSchedule, Void worker new SwingWorkerListSchedule, Void() { Override protected ListSchedule doInBackground() throws Exception { return scheduleDao.findAvailableSchedule(doctorId); } Override protected void done() { try { ListSchedule 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里对应号序状态变成 1doctor_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 有没有characterEncodingutf8mb4再看建表语句是否用了DEFAULT CHARSETutf8mb4最后看 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 等查询。如果你正在调这套系统我建议你先把项目复制一份随意改代码跑挂了再从原始包恢复这种“手贱”操作反而是成长最快的路径。希望帮到你。本文还有配套的精品资源点击获取