Java实现医院排队叫号系统:状态机设计与队列核心源码

发布时间:2026/10/2 9:20:37
Java实现医院排队叫号系统:状态机设计与队列核心源码 简介基于Java的医院排队叫号系统设计源码面向医院信息化建设者、Java开发人员及医疗系统学习者旨在解决门诊候诊排队无序、医生工作量不均、就医环境嘈杂等实际问题。系统能够接收HIS单据信息结合患者签到情况、医生排班与患者优先级生成合理队列同时覆盖医生主页、预约检查、预约取药、预约问诊、医生叫号及缴费等典型就医环节有助于提升医院运营效率与患者体验。压缩包共228个文件约7.06MB主要包括82个Java源文件、76个JSP页面、31个PNG图片、17个KEEP文件、10个HTML页面以及SQL数据库脚本其中Java代码承载核心业务JSP与HTML搭建前端界面SQL脚本便于快速初始化数据。资源内容完整、目录清晰适合用于课程设计、毕业设计或医院信息化项目的二次开发。目前已有830人学习下载具备较高参考价值。1. 医院排队叫号系统为什么值得自己用Java写一套去医院挂过号你就会发现现在叫号系统的屏幕一块比一块大但真去网上找基于Java的医院排队叫号系统设计源码能直接落地的没几个。我前后维护过两版这样的系统从取号机、医生端到候诊大屏全线用Java打通核心就是三件事一个号码生成器、一个队列状态机、一组TCP广播。这套东西既能当Java课程设计源码提交也够小门诊直接上线。我会把业务模型、源码结构和部署翻车点拆开讲适合正在做Java毕业设计、课程设计或想给门诊做信息化的开发者照着重现也告诉你哪些地方要绕开。2. 先立模型再选型从门诊流程抽象出Java队列状态机2.1 叫号系统看着在管队列骨子里是状态机新手写医院排队叫号系统最容易犯的错是把所有精力放在“队列”上——队列怎么排、怎么插队、怎么叫下一个。实际跑过门诊你会发现队列只是表面真正要设计的是就诊状态流转。一个患者从走进科室到离开经历的状态大致是取号成功进入候诊队列状态为等待中医生点击叫号大屏和语音提示患者进入诊室状态为已叫号患者进门开始就诊状态为就诊中患者没来医生点击过号状态为已过号回到候诊队列末尾等待重呼回到候诊队列后再次被叫号状态重新变为已叫号就诊结束状态改为已完成中途弃号或离开状态改为已退号所以表结构里一定要有一个status字段这比单纯设计“一个队列类”更接近真实需求。很多开源项目所谓“医院排队叫号系统源码”只做了取号—叫号—占用这三步缺少过号重呼、回诊二次排队拿到门诊当天就会翻车。我一般会把状态流转画成一张表状态值含义可流转到0等待中1叫号、5退号1已叫号2就诊中、3过号、4已完成2就诊中4已完成、3过号3已过号1重呼、5退号4已完成无5已退号无把这个状态机立住后面的代码就是围绕状态迁移做校验和落库。面试时被问到“Java怎么保证数据一致性”你也能拿这套状态迁移举例同一号记录不能被两个窗口同时改成“就诊中”。2.2 Java技术栈选型从SwingSocket到Spring Boot怎么选叫号系统不算复杂业务选型主要看部署环境和开发者熟悉度。常见做法有三条路线第一种是Java Swing/JavaFX做医生端和护士端服务端用原生ServerSocket数据库用MySQL。优点是代码直观、源码结构清楚适合课程设计和毕业设计评委一眼能看懂缺点是Java桌面程序在Windows上分发需要带JRE字体渲染和分辨率适配要额外处理。第二种是Spring Boot做后端Vue或简单HTML页面做医生端和候诊大屏。适合已经有Web开发经验的人部署用Tomcat或内嵌容器浏览器访问即可。缺点是诊室电脑配置老旧时浏览器打开多个页面会有明显的卡顿医院门诊电脑很多还是Windows 7加4G内存。第三种是Android平板做医生端Java后端用Spring Boot或直接Socket。现在不少新建门诊采用这种方式因为触屏和界面美观度更好。我个人给课程设计或小门诊推荐第一条路线Java原生Socket加Swing。理由很简单——源码足够短逻辑透明TCP长连接的实时性秒杀HTTP轮询而且一个Java文件就能跑通通信链路。对于并发量不超过五十个诊室的门诊场景原生Socket完全够用。2.3 数据库设计一张queue_record表覆盖所有叫号状态不管用哪种界面方案后端表结构是通用的。我习惯只建一张主表不拆排队表、过号表、历史表这样查询和事务都简单。CREATE TABLE queue_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_date DATE NOT NULL COMMENT 就诊日期, dept_code VARCHAR(20) NOT NULL COMMENT 科室代码, dept_name VARCHAR(50) DEFAULT COMMENT 科室名称, queue_no INT NOT NULL COMMENT 当日排队序号, patient_name VARCHAR(50) NOT NULL COMMENT 患者姓名, patient_id_card VARCHAR(18) DEFAULT COMMENT 患者身份证号, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0等待中 1已叫号 2就诊中 3已过号 4已完成 5已退号, window_no VARCHAR(10) DEFAULT COMMENT 诊室号或窗口号, doctor_name VARCHAR(20) DEFAULT COMMENT 接诊医生, called_count INT DEFAULT 0 COMMENT 叫号次数, first_called_at DATETIME DEFAULT NULL COMMENT 首次叫号时间, last_called_at DATETIME DEFAULT NULL COMMENT 最后一次叫号时间, finish_at DATETIME DEFAULT NULL COMMENT 完成时间, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_dept_date_no (dept_code, biz_date, queue_no), KEY idx_status (status), KEY idx_biz_date (biz_date) );这里有几个设计要点。唯一键uk_dept_date_no保证同一天同一个科室不会出现重复排队号这是防重号的第一道屏障。状态字段用 TINYINT 而不是字符串查询快、代码也容易写枚举。called_count记录叫号次数过号重呼后这个值递增可以用来统计医生叫号负荷。first_called_at和last_called_at分开存方便算患者平均候诊时长。不要用两张表分别存“今天排队的号”和“历史完成的号”。医院运维人员经常要查某个患者在某个时段的状态一张表加biz_date和status索引检索和归档都容易。3. 核心源码实现号码生成、排队队列与叫号逻辑的Java代码3.1 取号端按科室日期生成唯一排队号号码生成是排队叫号系统里最容易出并发问题的环节。两个患者同时在取号机上按键如果代码没加锁就可能拿到同一个号码。第一种方案是数据库自增生成利用行锁保证唯一性public int generateQueueNo(LocalDate bizDate, String deptCode) { String selectSql SELECT queue_no FROM queue_record WHERE biz_date ? AND dept_code ? ORDER BY queue_no DESC LIMIT 1 FOR UPDATE; ListInteger lastList jdbcTemplate.queryForList(selectSql, Integer.class, bizDate, deptCode); int nextNo (lastList.isEmpty() ? 0 : lastList.get(0)) 1; String insertSql INSERT INTO queue_record (biz_date, dept_code, queue_no, patient_name, status) VALUES (?, ?, ?, ?, 0); jdbcTemplate.update(insertSql, bizDate, deptCode, nextNo, 未知患者); return nextNo; }这段代码的逻辑是先用FOR UPDATE锁住当前科室当天的最大排队号拿到后加一再插入一条新的排队记录。FOR UPDATE是数据库层面的悲观锁多台取号机并发执行时后到的请求会等待前一个事务提交从而避免重复。注意实际的门诊取号流程里患者信息往往是先通过身份证读卡器或挂号系统同步再调用这个生成方法。课程设计时可以先把patient_name写成参数传进来不要像我上面这样写死成“未知患者”。第二种方案是 Redis INCR但小门诊不一定会部署 Redis没必要为这点并发引入额外中间件。数据库行锁已经足够。3.2 医生端叫号从候诊队列取下一个患者号码生成解决“给号”队列管理解决“叫谁”。我一般用一个内存队列做候诊列表用数据库做持久化两者结合。内存队列保证响应速度数据库保证重启后能恢复。public class CallService { private static final MapString, DequeQueueRecord WAITING_MAP new ConcurrentHashMap(); public synchronized QueueRecord callNext(String deptCode) { DequeQueueRecord deque WAITING_MAP.computeIfAbsent(deptCode, key - new ConcurrentLinkedDeque()); QueueRecord record deque.pollFirst(); if (record null) { return null; } record.setStatus(1); record.setCalledCount(record.getCalledCount() 1); record.setLastCalledAt(new Date()); queueRecordMapper.updateStatus(record); return record; } }WAITING_MAP以科室代码为维度把候诊队列拆成多个独立队列这样内科叫号不会抢外科的患者。ConcurrentLinkedDeque是无界非阻塞队列取号时往尾部加叫号时从头部取刚好适配“先进先出”的规则。synchronized加在callNext方法上保证同一时刻只有一次出队操作。虽然ConcurrentLinkedDeque本身是线程安全的但出队之后还要更新数据库这个“出队 更新”的组合操作必须整体互斥否则两个诊室同时叫号会把同一个患者叫走。3.3 过号与重呼状态流转是叫号系统最脆弱的环节患者在候诊区走开没听到叫号医生一般点“过号”让下一位患者先看。过号不是说放弃这个患者而是让他自动排到当前队列最后。实现时要注意不能真的从队列里删除记录要先把状态改成已过号再重新放回队列尾部。public void markPassed(Long queueRecordId, String deptCode) { QueueRecord record queueRecordMapper.selectById(queueRecordId); if (record null || record.getStatus() ! 1) { throw new IllegalStateException(当前记录状态不允许执行过号操作); } record.setStatus(3); queueRecordMapper.updateStatus(record); DequeQueueRecord deque WAITING_MAP.get(deptCode); if (deque ! null) { deque.addLast(record); } } public void reCall(Long queueRecordId) { QueueRecord record queueRecordMapper.selectById(queueRecordId); if (record null || record.getStatus() ! 3) { throw new IllegalStateException(只有已过号记录才能重呼); } record.setStatus(1); record.setCalledCount(record.getCalledCount() 1); record.setLastCalledAt(new Date()); queueRecordMapper.updateStatus(record); broadcastCallMessage(record); }过号操作有两个关键点。第一个是状态校验只有在“已叫号”状态下的记录才能过号防止患者还在就诊中就被误标成过号。第二个是放回队列尾部的时机过号后立刻放回尾部会让刚过号的患者马上又被叫到所以很多系统会在markPassed里加一个延后时间这里课程设计可以直接使用deque.addLast真实项目可以改成“延迟N分钟再入队”。重呼reCall是给过号患者二次机会的。逻辑核心是状态校验——只有状态为3已过号的记录才能重呼。这里用到了第2章定义的状态机代码直接对应状态迁移表面试时拿出来讲就很有说服力。3.4 候诊大屏和语音播放的推送候诊大屏一般挂在候诊区显示当前叫到的号码和诊室号。最原始的做法是前端每秒轮询后端接口但这样大屏一多服务端压力成倍上涨。这里我采用服务端主动广播客户端是Swing窗口通过 TCP 长连接接收消息后直接更新界面。public void broadcastCallMessage(QueueRecord record) { String message String.format( { \event\: \CALL\, \queueNo\: %d, \deptName\: \%s\, \windowNo\: \%s\, \patientName\: \%s\ }, record.getQueueNo(), record.getDeptName(), record.getWindowNo(), record.getPatientName()); SocketServer.broadcast(record.getDeptCode(), message); }广播代码的核心是构造一条 JSON 消息推送到指定科室的所有终端。deptCode作为过滤条件只推给对应科室的大屏和诊室避免内科的叫号消息跑到外科屏幕上。语音播报我建议在客户端做不要在服务端生成音频文件。客户端收到 CALL 事件后用 Java 自带的TTS库或者调用 Windows 的System.out提示音简单高效。课程设计阶段甚至可以只显示不发声在界面上做一个闪烁提示即可。4. 多诊室联网与并发TCP广播协议和锁的最小实现4.1 架构一个小型Socket服务端广播叫号消息真实门诊是多个诊室同时运行每个诊室有自己的医生端候诊区有大屏。所有终端需要实时感知叫号事件。常见做法是一个常驻的Socket服务端所有终端作为客户端连接上来服务端收到某个诊室的叫号请求后把消息广播给同一科室下的其他终端。public class SocketServer { private static final CopyOnWriteArrayListClientSession sessions new CopyOnWriteArrayList(); public static void broadcast(String deptCode, String message) { for (ClientSession session : sessions) { if (deptCode.equals(session.getDeptCode())) { session.send(message); } } } }CopyOnWriteArrayList是 Java 并发包里的线程安全列表遍历时允许并发修改非常适合“客户端频繁上下线、服务端频繁广播”的场景。每个客户端连接封装成一个ClientSession持有 Socket 输出流和科室代码。这里不用 NIO 或 Netty原因很简单五十个终端以内的并发BIO阻塞IO每连接一个线程完全扛得住而且代码直观课程设计答辩好解释。如果未来要扩展到几百个终端再迁移 Netty 不迟。4.2 消息协议一个JSON结构驱动三端联动服务端广播的消息结构要统一。取号、叫号、过号、重呼、退号本质上都是事件我定义一套标准协议通过event字段区分{ event: CALL, deptCode: NYK, queueNo: 12, windowNo: 3号诊室, patientName: 张三, timestamp: 2025-06-01 09:30:00 }四个核心事件类型事件名触发时机接收方响应动作WAIT患者取号大屏刷新候诊人数CALL医生叫号大屏显示号码、诊室诊室端高亮当前号PASS医生点过号大屏从当前叫号区清除FINISH就诊完成诊室端状态复位可叫下一位消息里的event是字符串客户端用一个switch分支处理。课程设计阶段不要过度设计成protobuf或者MessagePackJSON 足够而且可以手动拼字符串省去引入Jackson的麻烦。4.3 并发安全三个必须加锁的地方与事务边界叫号系统的并发不像电商秒杀那么夸张但有几个点不加锁一定会出问题。第一个是号码生成。前面用了FOR UPDATE保证了同一科室同一天的最大号不会被并发取号重复。这个锁粒度最粗事务提交前其他取号请求都阻塞在SELECT ... FOR UPDATE上。第二个是内存队列的出队。callNext方法里出队和更新状态是两个操作合在一起加了synchronized。如果去掉这个锁两个诊室同时叫号可能取出同一个患者记录。第三个是重呼和过号的状态变迁。markPassed和reCall两个方法都依赖先读状态、再改状态如果并发执行需要用一个分布式锁或数据库乐观锁。小门诊场景下服务端是单点直接在方法上加synchronized就够如果是 Spring Boot 多实例部署就要考虑 Redis 锁或者数据库version字段实现乐观锁。事务边界这里有一个容易被忽略的细节不要把内存队列操作和数据库操作放在同一个事务里。内存操作是毫秒级数据库事务可能因为网络原因耗时几百毫秒期间队列已经被其他线程修改。正确顺序是先更新数据库、再操作内存队列或者反过来都行但必须保证“数据库更新失败时内存队列能回滚”。5. 门诊实战避坑五个真实的翻车现场与排查思路5.1 现象一取号机号码到99后从1重新开始导致重号某次门诊下午突然出现两个患者都拿“A001”号查日志发现取号机服务重启过。原因号码生成逻辑只依赖内存计数启动时没从数据库恢复当天最大号。解决方法是把号码生成改成查询当前日期的最大号加一也就是我在第2章写的SELECT ... ORDER BY queue_no DESC LIMIT 1。服务重启后这个查询拿到的是当天真实的最后编号。排查时直接看数据库中该科室当天的MAX(queue_no)如果内存计数值小于数据库值就是初始化逻辑遗漏。5.2 现象二过号患者点重呼把当前就诊患者顶掉了护士操作过号后患者马上回来点击重呼结果大屏显示的是这个患者的号码而诊室里当前患者还在就诊。原因重呼逻辑没有校验诊室当前是否有正在就诊的记录直接把过号患者设为已叫号状态。修复方案重呼前先查询当前诊室是否有状态为2就诊中的记录有则先完成或挂起。另外过号重呼的消息要延迟3到5秒推送给正在看的患者一个缓冲。5.3 现象三医生端开了两个窗口叫号命令重复执行医生在诊室电脑上开了两个医生端窗口双击叫号按钮两次同一个患者被叫了两次数据库里叫号次数变成2大屏上显示相同的号码。原因客户端没有做按钮防重服务端也没有做幂等校验。解决分两层。客户端在按钮点击后立即setEnabled(false)等收到服务端响应再恢复。服务端在callNext方法里用AtomicBoolean做状态标记同一诊室同一秒内只能发起一次叫号。更稳妥的做法是给消息加一个唯一的requestId服务端用ConcurrentHashMap对requestId去重。5.4 现象四断网后各诊室屏幕各显神通恢复后数据对不上诊室网络偶尔闪断Socket连接断开后医生端本地操作继续更新界面但数据库没收到请求。网络恢复后界面显示“已叫号”的患者数据库里还是“等待中”。原因客户端本地状态是UI展示服务端状态是数据真相。解决方法是客户端所有操作都走服务端接口服务端成功返回后才刷新本地界面。同时客户端要处理Socket断开重连重连成功后主动请求一次全量数据同步。我在实际项目里加了一个syncSnapshot接口客户端重连后调用一次把当天当前科室的所有记录状态拉下来覆盖本地展示。这个接口不用写复杂逻辑就是按biz_date和dept_code查询整表。5.5 现象五Windows下Java Swing字体发虚大屏文字看不清候诊大屏用的是一种老式Windows系统加1080P电视Java Swing默认字体加粗后边缘发虚候诊区患者说看不清号码。原因Swing默认字体是逻辑字体在Windows下没有正确映射到中文字体渲染效果差。解决在代码里显式设置字体不要用默认值。用new Font(微软雅黑, Font.BOLD, 48)替换默认字体。同时屏幕缩放显示设置要统一不要在一台电脑上设125%缩放另一台设100%会导致界面布局错位。Java 9以上版本还要注意 HiDPI 问题可以通过System.setProperty(sun.java2d.uiScale.enabled, false)关闭自动缩放采用手动设置固定字号。6. 上线前多跑几遍边界演练比修功能更重要系统写完到上线之间我习惯用一个清单做验证每个场景都是一段手工操作但能覆盖大部分翻车点。第一步压测并发取号。用一个多线程Java程序模拟二十个线程同时调generateQueueNo结束后查数据库当天的MAX(queue_no)确认没有重复值。这里的经验值是并发线程数至少要是诊室数的两倍不然测不出问题。第二步过号重呼闭环。把候诊队列人为造出“取号—叫号—过号—重呼—就诊完成”完整一条链每步操作后都查数据库状态字段确认和预期一致。特别注意在过号后重呼前去查看一下诊室是否已有就诊中患者。第三步断网恢复演练。运行中拔掉医生端的网线操作几次叫号再插回网线确认客户端能自动重连并且状态同步成功。没有自动重连机制这步一定会暴露问题。第四步跨天清零验证。门诊跨天时号码要从1重新开始。确认biz_date字段写入的是当天日期而不是服务启动日期否则过了凌晨系统会发“已用完明天号”的错误提示。我自己的习惯是每次改完代码先把这三步跑完再上机这套动作已经固定了。新手不要觉得这些操作琐碎真实门诊不会给你第二次试错机会患者排到一半系统崩溃赔上的就是信任。希望帮到你。本文还有配套的精品资源点击获取