Java课设医院挂号系统:表结构设计、Swing界面与并发控制实战
2026/9/17 14:52:14 网站建设 项目流程

简介:面向Java课程设计的医院挂号系统项目,基于GUI/Swing与MySQL实现,定位清晰,适合正在完成课设或复习Java数据库编程的学生参考。项目包含完整可运行的源码与数据库脚本,围绕挂号业务实现了登录验证、用户挂号、医生接诊、管理员维护科室与医生信息等基础模块,逻辑完整且界面风格朴素,便于理解与二次修改,用于课设答辩足够。压缩包共75个文件,其中14个Java源文件便于阅读修改,38个class为编译产物,12个png为界面截图,7个xml为工程配置文件,另附yygh.sql数据库脚本和登录账号密码文档,整体大小仅175KB。目前已有267人学习下载,具备开箱即用的特点,通过源码和SQL脚本可快速复现整个系统,适合需要高效完成Java课设的同学。

1. 课设做到什么程度,评委才会觉得“这系统能跑”

医院挂号系统是 Java 课设里的常青树,几乎每个学校都出过这道题。但从答辩现场看,九成学生交上去的东西长得差不多:一个 Swing 窗口套几个文本框,点“挂号”往 MySQL 里 INSERT 一行,然后演示结束。这类作品拿个及格分不难,想冲优秀或者写进简历,缺的不是功能,而是“把边界情况当真”的意识。

本文按一条可落地的路线把整个系统拆开讲:先设计 MySQL 表结构,再搭 Swing 界面,最后处理挂号当天最容易翻车的并发和跨天问题。整个过程覆盖 Java 基础、JDBC、MySQL 事务和 Swing 事件模型,正好对应课设评分表里“数据库设计、界面交互、业务完整性”三个维度。适合正在做课设的学生,也适合准备 Java 后端面试时顺便把项目经历整理清楚的人。

2. 设计 MySQL 表结构:用 5 张表撑起一次完整挂号

2.1 先想清楚挂号业务的几个硬约束

挂号系统的核心不是“能查到医生”,而是“同一时间同一医生不能被挂两次”。这个约束必须在数据库层面可表达、可验证,否则界面做得再好也是一堆脏数据。

常见的表设计是科室表、医生表、患者表、挂号记录表四张,但我会加一张号源配置表,理由后面细说。先看表之间的依赖关系:科室和医生是 1:N,医生和号源时段是 1:N,患者和挂号记录是 1:N,挂号记录通过医生 ID 关联到具体排班时段。字段类型上,时间类型统一用DATETIME,金额用DECIMAL(10,2),状态用TINYINT加注释,不要用字符串存状态,否则后面写 SQL 统计时全是LIKE '%已挂%'这种写法。

CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_code VARCHAR(20) NOT NULL UNIQUE, dept_name VARCHAR(50) NOT NULL, intro VARCHAR(200) ); CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, doc_no VARCHAR(20) NOT NULL UNIQUE, doc_name VARCHAR(30) NOT NULL, title VARCHAR(20), fee DECIMAL(10,2) DEFAULT 0, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT, id_card VARCHAR(18) NOT NULL UNIQUE, pat_name VARCHAR(30) NOT NULL, phone VARCHAR(11) );

科室表的dept_code用拼音首字母缩写或数字编码都行,只要唯一。医生表里的title是职称,fee是挂号费,这两个字段在界面上要直接展示,所以在设计阶段就把冗余信息放在医生表里,避免挂号界面每次都要 JOIN 科室表才能显示全名。

2.2 号源表:把“今天上午有没有号”变成可查询的数据

很多课设把挂号状态直接存在挂号记录表里,判断逻辑写在 Java 代码中:查出该医生当天所有挂号记录的数量,大于某个阈值就提示“已满”。这种做法的毛病在于限号规则写死在代码里,想改成“上午限号 25、下午限号 20”就得改代码重新编译。

我的做法是加一张schedule表,把日期、时段、总量、已挂量拆开存。字段设计上特意保留slot_date不带时分秒,用来按“天”做聚合查询。

CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, slot_date DATE NOT NULL, slot_period ENUM('AM','PM') NOT NULL, total_num INT NOT NULL DEFAULT 20, used_num INT NOT NULL DEFAULT 0, UNIQUE KEY uk_doc_date (doctor_id, slot_date, slot_period), FOREIGN KEY (doctor_id) REFERENCES doctor(id) ); CREATE TABLE registration ( id INT PRIMARY KEY AUTO_INCREMENT, reg_no VARCHAR(32) NOT NULL UNIQUE, patient_id INT NOT NULL, doctor_id INT NOT NULL, schedule_id INT NOT NULL, reg_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-已退号', FOREIGN KEY (patient_id) REFERENCES patient(id), FOREIGN KEY (doctor_id) REFERENCES doctor(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id) );

UNIQUE KEY uk_doc_date保证同一医生同一天同一时段最多只有一条号源配置记录。used_num就是“已挂量”的计数器,每次挂号把它 +1,同时判断used_num < total_num。这个字段放在 schedule 表里是反范式的,但它正好把并发控制的核心收拢到单行更新上,否则每次挂号都要COUNT(*)扫一遍 registration 表,数据一多就慢。

2.3 初始化脚本:演示现场不能等用户慢慢注册

课设答辩最怕的场面是:老师点开“查看医生”按钮,列表是空的。所以在建表之后必须写初始化脚本,造出 3 个科室、5 个医生、每个医生未来 7 天每天 AM/PM 各 20 个号源的数据。写存储过程是最省事的方案,虽然 MySQL 存储过程在课设里用得不多,但一张批量插入的脚本比手写几十条 INSERT 更显专业。

-- 用存储过程批量生成未来 7 天的号源 DELIMITER // CREATE PROCEDURE init_schedule(IN p_doctor_id INT, IN p_days INT) BEGIN DECLARE v_i INT DEFAULT 0; WHILE v_i < p_days DO INSERT INTO schedule (doctor_id, slot_date, slot_period, total_num, used_num) VALUES (p_doctor_id, CURDATE() + INTERVAL v_i DAY, 'AM', 20, 0), (p_doctor_id, CURDATE() + INTERVAL v_i DAY, 'PM', 15, 0); SET v_i = v_i + 1; END WHILE; END// DELIMITER ;

存储过程的参数说明:p_doctor_id是医生 ID,p_days是往后生成的天数。循环体里每天插两条记录,上午 20 个号、下午 15 个号,这个差异恰好用来说明号源配置是灵活可调的。调用时对每个医生执行一次,不要写在循环里对同一医生重复调用。

3. 用 Swing 搭挂号界面:从窗口骨架到表格联动

3.1 主窗口布局和登录态设计

Swing 做界面的核心是布局管理器的组合使用。整个系统的主窗口我采用BorderLayout,北边放操作按钮栏,中间放JTabbedPane切换“挂号操作”“号源查询”“患者管理”三个页签。挂号页内部用一个JSplitPane,左边放科室和医生的联动列表,右边放号源表格和挂号按钮。

这种组合的意义在课设答辩时可以直接讲:BorderLayout负责整体骨架,JSplitPane解决两个信息面板的比例问题,JTabbedPane把不同业务场景隔离开。比用一个巨型GridBagLayout硬撑全部控件的做法清晰得多。

挂号的流程必须有个身份前提——当前操作者是谁。课设里不需要做完整的登录鉴权,但至少要有一个“当前患者”的上下文对象。常见做法是挂一个JFrame作为登录窗口,输入身份证号查 patient 表,查到就进入主界面并把 patient 对象存在一个Context工具类里。

public class AppContext { private static Patient currentPatient; // 保存当前登录患者 public static void setCurrentPatient(Patient p) { currentPatient = p; } public static Patient getCurrentPatient() { return currentPatient; } }

这个类要注意两点:static变量保存会话信息在单机 Swing 课设里够用,但不要让它持有数据库连接;患者对象只存内存中的业务数据。后续所有挂号按钮的事件里取AppContext.getCurrentPatient(),就能保证每个操作都落到同一个患者名下。

3.2 联动的核心:JTable 刷新不等于重新 new 一个表

科室到医生再到号源,这是挂号系统界面的经典联动链。实现方案是在科室列表的ListSelectionListener里触发医生表查询,再把医生表结果渲染到右侧。最常见的坑是每次刷新都重新创建JTableadd到面板上,一段演示操作下来界面上叠了十几个表格。

正确做法是让JTable只创建一次,数据更新通过替换TableModel完成。下面这段代码展示科室列表选中后如何刷新号源表格:

JTable table = new JTable(); // 界面上只创建这一次 DefaultTableModel model = new DefaultTableModel(new Object[]{"日期", "时段", "剩余/总数", "挂号费"}, 0); doctorList.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { Doctor selected = doctorList.getSelectedValue(); if (selected == null) return; // 查询排班表,返回剩余号和总号 List<Schedule> schedules = scheduleDao.findByDoctorAndDate(selected.getId(), LocalDate.now()); model.setRowCount(0); // 清空旧数据 for (Schedule s : schedules) { model.addRow(new Object[]{ s.getSlotDate().toString(), s.getSlotPeriod(), (s.getTotalNum() - s.getUsedNum()) + "/" + s.getTotalNum(), selected.getFee() }); } table.setModel(model); // 表结构不变,只换数据模型 } });

getValueIsAdjusting()是 ListSelectionModel 的一个状态位:鼠标拖拽选择多行时会触发多次选择事件,不加这个判断,一次点击可能连续查两次数据库。每次查询前先setRowCount(0)清空模型,再逐行addRow,这样只会触发一次表格重绘。JTable 的刷新效率瓶颈不在渲染,而在频繁创建 TableModel,所以“先清空再填充”是标准操作。

3.3 挂号按钮的核心逻辑:先查后挂,还是先挂后查?

挂号的动作本质是两件事:检查号源是否可挂、插入挂号记录并更新号源余量。在 Swing 里这个动作直接写在ActionListeneractionPerformed方法中,代码结构应该是:

btnRegister.addActionListener(e -> { int selectedRow = table.getSelectedRow(); if (selectedRow < 0) { JOptionPane.showMessageDialog(frame, "请先选择一个号源时段"); return; } Schedule s = getScheduleFromTable(selectedRow); if (s == null) return; boolean ok = registerService.register(AppContext.getCurrentPatient(), s); if (ok) { JOptionPane.showMessageDialog(frame, "挂号成功,流水号为 " + regNo); refreshScheduleTable(); } else { JOptionPane.showMessageDialog(frame, "号源已满或该时段已停用"); } });

注意这段代码里没有写任何 SQL,业务判断全部封装在registerService.register()里。这样做的好处是按钮事件只负责界面反馈,挂号规则(是不是满号、能不能重复挂)变化时只改 Service 层。在课设代码评审时,这种分层结构本身就是加分项。

4. 挂号核心逻辑:预检查 + 事务提交,把并发和跨天问题一起解决

4.1 用“UPDATE 行数”判断号源是否成功,而不是 SELECT 后判断

这是全文最关键的一段代码。很多课设版挂号逻辑是这样写的:先SELECT * FROM schedule WHERE id = ?,然后在 Java 里判断used_num < total_num,成立就执行UPDATEINSERT。这在单线程演示下没问题,但一旦同时有两个请求查到了used_num = 19total_num = 20,两个窗口都会进入 if 分支,导致超挂。

正确做法是把判断下沉到 UPDATE 的条件里,利用 MySQL 的原子性保证只更新成功一行:

public boolean register(Patient patient, Schedule s, Connection conn) throws SQLException { // 1. 扣减号源,条件里放余量判断 String lockSql = "UPDATE schedule SET used_num = used_num + 1 " + "WHERE id = ? AND used_num < total_num"; PreparedStatement ps = conn.prepareStatement(lockSql); ps.setInt(1, s.getId()); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); return false; // 号源已满或该日期/时段被停用 } // 2. 生成唯一流水号并插入挂号记录 String regNo = "REG" + System.currentTimeMillis(); String insertSql = "INSERT INTO registration (reg_no, patient_id, doctor_id, schedule_id, reg_time, status) " + "VALUES (?, ?, ?, ?, NOW(), 1)"; PreparedStatement ps2 = conn.prepareStatement(insertSql); ps2.setString(1, regNo); ps2.setInt(2, patient.getId()); ps2.setInt(3, s.getDoctorId()); ps2.setInt(4, s.getId()); ps2.executeUpdate(); conn.commit(); return true; }

这段代码的逻辑拆开讲。UPDATE ... WHERE used_num < total_num是原子操作,MySQL 的行锁保证同一时刻只有一个事务能对该行执行更新,所以即使并发 100 个请求也只有一个能返回rows > 0。先执行 UPDATE 再插入挂号记录,顺序不能反:如果先 INSERT 挂号记录再 UPDATE 号源,失败回滚时要把两条 SQL 都撤销,事务边界变大,出错概率也变高。

4.2 事务边界和连接复用:同一个 Connection 贯穿整个方法

上面代码里conn参数是外部传入的,这意味着事务的开启和提交必须放在调用的外层。常见做法是在 Service 层获取连接、设置setAutoCommit(false)、调用 register 方法、在 finally 里关连接:

public boolean registerFlow(Patient patient, Schedule s) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 boolean ok = register(patient, s, conn); if (ok) { conn.commit(); } else { conn.rollback(); } return ok; } catch (SQLException ex) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignore) {} } log.error("挂号失败", ex); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException ignore) {} try { conn.close(); } catch (SQLException ignore) {} } } }

注意finally里把autoCommit重置为 true 再关连接。很多 JDBC 连接池(比如 C3P0、Druid)复用一个连接时默认autoCommit=true,如果你在事务里把它设为 false 但没改回来,归还给连接池后下一个使用者做普通查询时会莫名提交事务。课设里如果用DriverManager.getConnection()每次新建连接,这个问题不明显;但当你使用连接池后,这行重置代码就是必须的。

4.3 跨天和退号边界:slot_dateCURDATE()条件,别让“昨天的号”混进来

查询号源时不能只查schedule表有没有数据,还要过滤日期范围。一个常见的需求是“只看未来 7 天可挂的号”,SQL 里就要写成:

String sql = "SELECT * FROM schedule WHERE doctor_id = ? " + "AND slot_date >= CURDATE() AND slot_date < CURDATE() + INTERVAL 7 DAY " + "AND used_num < total_num ORDER BY slot_date, slot_period";

这里的CURDATE()是在 MySQL 端计算的,不要在 Java 端用字符串拼日期再传进去。让数据库算当前日期可以避免应用服务器和数据库服务器时间不一致导致的边界问题。这个查询返回的每一行都是“当前可挂”的号源,界面就可以把已满的时段直接过滤掉,不用再到 Java 层做判断。

退号逻辑同样要处理跨天:退号操作的 SQL 是UPDATE schedule SET used_num = used_num - 1 WHERE id = ?,同时把 registration 表对应记录的status改为0。这两个操作放同一个事务里,不能只改一边。另外一个细节是:如果退的是“当天”的号,要看医院规则是否允许;课设里建议统一允许退未来任何一天的号,但退号时间记录在 reg_time 字段里,便于答辩时展示数据变化。

4.4 排错场景:启动时 MySQL 连接失败的三个排查方向

课设跑不起来的场景里,数据库连接失败占一半以上。现象通常是点击挂号按钮后控制台报Communications link failure或者Access denied。按下面的顺序排查,大多数问题能在两分钟内定位:

检查项命令/位置常见问题
MySQL 服务是否启动系统服务里查 mysqld,或用mysql -uroot -p试连安装了 MySQL 但没启动服务
JDBC URL 是否写对jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=Asia/Shanghai漏了serverTimezone参数会报时区错误
驱动包是否导入检查 IDE 里 External Libraries 是否有 mysql-connector-java忘记把 jar 加入项目,运行时ClassNotFoundException

MySQL 8.x 及以上版本还要注意useSSL=falseallowPublicKeyRetrieval=true两个参数:前者避免 SSL 握手的警告,后者解决用caching_sha2_password认证时的公钥检索报错。这两个参数加在 JDBC URL 里,是连接 MySQL 8 最常见的两个坑。另外关掉杀毒软件或防火墙的误拦,Windows 上本地回环连接 MySQL 偶尔会被防火墙拦。

5. 演示前的数据验证:用 JDBC 手动模拟并发,把评委可能问的边界都测一遍

课设答辩的演示环节,最稳的收尾不是展示“挂号成功”的弹窗,而是展示系统在边界条件下的行为。构建代码并发场景有两个途径:用 JUnit 写测试代码模拟多线程同时挂号,或者在 MySQL 命令行里手动造数据验证约束。

命令行验证方式更快。打开两个终端窗口,同时执行下面的 UPDATE:

-- 终端 1 START TRANSACTION; UPDATE schedule SET used_num = used_num + 1 WHERE id = 1 AND used_num < total_num; -- 终端 2 START TRANSACTION; UPDATE schedule SET used_num = used_num + 1 WHERE id = 1 AND used_num < total_num;

第二个窗口会阻塞,直到第一个窗口执行COMMIT才返回,且rowcount显示为 0。把这两个窗口的操作序列演示给评委看,比口头说“我的系统支持并发”有说服力得多。

Java 侧验证并发可以用CountDownLatch同时启动多个线程调用registerFlow(),断言返回true的数量恰好等于剩余号源数,返回false的数量等于超额请求数。这个测试跑通,说明代码层的并发控制是可靠的,即使以后把 Swing 换成 Web 接口,这段 Service 逻辑可以直接复用。

最后补一个演示数据展示技巧:把 schedule 表里某个时段的used_num手动改成total_num - 1,然后在界面上快速点击两次挂号,第一次成功、第二次弹出“号源已满”。这个小操作在答辩现场非常直观,能让评委一眼看到数据库约束和业务代码在同时起作用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询