ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java医院排队叫号系统源码解析:队列模型、叫号策略与WebSocket实时推送

Java医院排队叫号系统源码解析:队列模型、叫号策略与WebSocket实时推送 简介这是一套基于Java开发的医院排队叫号系统设计源码面向医疗信息化方向的开发者、计算机专业学生及需要完成相关课程设计或毕业设计的人群。系统应用于医院各门诊科室通过接收HIS单据信息结合患者签到情况、医生排班与患者优先级生成排队队列着力解决就诊、检查、取药环节排队无序、医生工作量不均、就诊环境嘈杂等痛点帮助提升医院运营效率与患者就诊体验。资源包共228个文件以82个java源码、76个jsp页面、31个png与8个jpg界面素材、10个html页面为主另含sql建库脚本与properties配置文件压缩包约7.06MB涵盖医生叫号、预约问诊、预约检查、预约取药、缴费及主页等业务模块前后端结构完整。目前已有832人学习下载适合作为医疗排队叫号类项目的参考实现便于读者理解队列生成逻辑、页面交互与数据库设计快速搭建可运行的演示环境。1. 从挂号窗口的队尾说起这套 Java 源码到底能跑出什么上周陪家人去区医院早上七点半挂号大厅已经排到门口。导诊台旁边贴着一张 A4 纸写着「请到分诊台取号按号就诊」但分诊台前照样挤成一团。这种场景下一套能自动分诊、叫号、显示队列的系统就不是「锦上添花」而是刚需。我手上这份基于 Java 的医院排队叫号系统设计源码解决的就是从患者取号到医生呼叫、从队列调度到窗口屏显示这一整条链路。它适合两类人一是做 Java 课程设计或毕业设计的学生需要一套结构完整、能讲清楚业务逻辑的参考工程二是中小型门诊或社区医院的信息化负责人想先跑通一个轻量级叫号原型再决定要不要上商业系统。源码本身不依赖特定硬件普通 PC 加一块 HDMI 显示屏就能把核心流程演示出来这也是我当初愿意花时间拆它的原因——门槛低但业务闭环是完整的。2. 拆开工程看骨架队列模型、叫号策略与数据库表怎么对上2.1 先搞清楚号源是怎么生成的叫号系统的核心不是「叫」而是「号从哪来、按什么顺序排」。这份源码里号源生成走的是「科室 日期 序号」三段式。每个科室每天独立计数序号从 1 开始递增跨天自动归零。常见做法是用一张queue_sequence表记录当前科室当天的最大序号取号时先SELECT ... FOR UPDATE锁行再UPDATE加一最后插入一条patient_queue记录。这样做的原因是避免并发取号时出现重号——两个窗口同时取号如果没有行锁很容易拿到同一个序号。-- 号源序列表每个科室每天一条记录 CREATE TABLE queue_sequence ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_code VARCHAR(32) NOT NULL COMMENT 科室编码, queue_date DATE NOT NULL COMMENT 排队日期, current_seq INT DEFAULT 0 COMMENT 当前最大序号, UNIQUE KEY uk_dept_date (dept_code, queue_date) ); -- 患者排队表一条记录就是一张号 CREATE TABLE patient_queue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, queue_no VARCHAR(16) NOT NULL COMMENT 展示用号码如 A012, dept_code VARCHAR(32) NOT NULL, patient_name VARCHAR(64), status TINYINT DEFAULT 0 COMMENT 0等待 1已叫号 2已完成 3过号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, call_time DATETIME, finish_time DATETIME, INDEX idx_dept_status (dept_code, status) );上面两张表是整个系统的地基。queue_sequence负责发号不重patient_queue负责记录每一张号的完整生命周期。status字段是状态机的核心0 到 3 的流转决定了患者当前在队列里的位置。queue_no用字母加数字是为了大屏显示时更醒目A 代表普通号B 可以留给复诊或优先号这个前缀规则在源码里是写在配置文件的改起来不用动代码。2.2 叫号策略FIFO 之外还得处理过号和优先纯 FIFO 在门诊场景下不够用。医生临时停诊、患者去做检查、老年人优先这些都会打乱队列。源码里把叫号逻辑抽成了一个CallStrategy接口默认实现是FifoCallStrategy按create_time升序取第一条status0的记录。但真正跑起来我建议至少再加两个策略一个是「过号重排」患者被叫三次未到状态置为 3同时把他的create_time更新为当前时间让他重新排到队尾另一个是「优先号插队」在patient_queue加一个priority字段叫号时先按priority DESC再按create_time ASC排序。public interface CallStrategy { PatientQueue next(String deptCode); } Component public class FifoCallStrategy implements CallStrategy { Autowired private PatientQueueMapper queueMapper; Override public PatientQueue next(String deptCode) { // 取等待中最早的一条同时排除已过号的 return queueMapper.selectOne( new QueryWrapperPatientQueue() .eq(dept_code, deptCode) .eq(status, 0) .orderByAsc(create_time) .last(LIMIT 1) ); } }这段代码的关键在status0和orderByAsc(create_time)两个条件。status过滤掉已叫号和已完成的create_time保证先来先服务。如果你要加优先号把orderByAsc改成orderByDesc(priority).orderByAsc(create_time)就行。注意last(LIMIT 1)这种写法在 MyBatis-Plus 里是拼 SQL 片段用的时候要确保没有 SQL 注入风险因为这里没有外部参数所以是安全的。2.3 医生端和窗口屏怎么拿到实时队列叫号系统最怕「医生点了叫号窗口屏没反应」。源码里用的是 WebSocket 推送医生端调用/api/call/next后服务端更新数据库同时通过SimpMessagingTemplate往/topic/dept/{deptCode}发一条消息窗口屏页面订阅这个主题收到消息就刷新显示。这种做法的好处是窗口屏不需要轮询延迟低但要注意 WebSocket 连接断开后的重连逻辑。源码里在前端加了一个心跳检测每 30 秒发一次 ping超过两次没收到 pong 就重新建立连接。// 窗口屏订阅逻辑 const deptCode new URLSearchParams(location.search).get(dept); const socket new SockJS(/ws); const stompClient Stomp.over(socket); stompClient.connect({}, function () { stompClient.subscribe(/topic/dept/ deptCode, function (msg) { const data JSON.parse(msg.body); document.getElementById(currentNo).innerText data.queueNo; document.getElementById(patientName).innerText data.patientName; }); }); // 心跳保活 setInterval(() { if (stompClient.connected) { stompClient.send(/app/ping, {}, ); } }, 30000);这段前端代码里deptCode从 URL 参数取意味着每个科室的窗口屏只需要改一个参数就能复用同一个页面。stompClient.subscribe订阅的是科室级别的主题医生叫下一个号时只有对应科室的屏幕会更新。心跳那一段是血泪经验——医院网络偶尔抖动没有心跳的话屏幕会卡在最后一个号上患者以为系统坏了。3. 把工程跑起来环境配置、建表与三个必调参数3.1 环境准备与依赖版本这份源码是标准的 Spring Boot MyBatis-Plus 结构JDK 要求 1.8 以上我实测用 JDK 11 和 JDK 17 都能跑。数据库默认配的是 MySQL 5.7如果你用 MySQL 8需要把驱动类改成com.mysql.cj.jdbc.Driver并在连接串里加上serverTimezoneAsia/Shanghai。前端是 Thymeleaf 加原生 JS没有 Node.js 构建步骤省了不少事。# application.yml 关键配置 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_queue?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false server: port: 8080characterEncodingutf8不能省否则患者姓名里的生僻字会变问号。thymeleaf.cachefalse在开发阶段建议开着改页面不用重启。server.port如果 8080 被占用改成 8081 或 9090 都行但记得同步改前端 WebSocket 的连接地址。3.2 建表与初始化数据源码的resources/sql目录下有一个init.sql直接导入就能建好库表。但有一个坑它默认只建了patient_queue和queue_sequence没有建科室表和医生表。如果你要演示多科室需要自己补两张表。CREATE TABLE department ( dept_code VARCHAR(32) PRIMARY KEY, dept_name VARCHAR(64) NOT NULL, prefix CHAR(1) DEFAULT A COMMENT 号码前缀 ); CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(32), dept_code VARCHAR(32), room_no VARCHAR(16) ); INSERT INTO department VALUES (IM, 内科, A), (SG, 外科, B), (PD, 儿科, C);科室表里的prefix字段决定了号牌的第一个字母。内科用 A外科用 B儿科用 C这样患者拿到号就知道自己该去哪个区域。doctor表目前只是展示用源码里没有做医生登录和权限如果你要加可以在doctor表上加password字段然后走 Spring Security 的简单认证。3.3 三个必调参数叫号间隔、过号阈值、大屏刷新源码的application.yml里有三个自定义参数默认值不一定适合你的场景。第一个是queue.call-interval默认 2000 毫秒意思是两次叫号之间至少隔 2 秒防止医生狂点按钮导致屏幕闪太快。第二个是queue.miss-threshold默认 3患者被叫 3 次未到就自动过号。第三个是queue.screen-refresh默认 5000 毫秒是窗口屏在没有 WebSocket 消息时的兜底刷新间隔。queue: call-interval: 2000 miss-threshold: 3 screen-refresh: 5000call-interval如果设得太小比如 500 毫秒医生连续点两次第一个患者还没走到诊室第二个号已经叫了现场会乱。miss-threshold设成 1 的话患者去个洗手间回来号就没了投诉率会上升。我一般建议社区医院设 3三甲医院门诊量大的科室可以设 2。screen-refresh是兜底WebSocket 正常时用不到但一旦网络出问题它能让屏幕至少每 5 秒刷一次不至于彻底卡死。4. 避坑与排查重号、消息丢失和数据库连接超时4.1 取号时出现重号现象两个窗口同时操作患者拿到一模一样的号码。原因号源生成没有加锁两个线程同时读到current_seq5各自加一后都写回 6。解决在queue_sequence的查询上加FOR UPDATE或者直接用UPDATE queue_sequence SET current_seq current_seq 1 WHERE dept_code ? AND queue_date ?然后SELECT刚更新的值。源码里用的是前者但如果你用 MySQL 8 且隔离级别是 RRFOR UPDATE在唯一索引上会锁行不会锁表性能可以接受。4.2 医生叫号后窗口屏不更新现象医生端显示叫号成功但窗口屏还是上一个号。原因WebSocket 消息发了但窗口屏的 STOMP 连接已经断了前端没有重连。解决在stompClient的onWebSocketClose回调里加一个重连定时器同时检查/topic/dept/{deptCode}的订阅是否还在。源码里前端有重连逻辑但如果你改了科室编码记得同步改订阅地址否则消息发到了旧主题上。4.3 运行一段时间后数据库连接超时现象系统跑了一上午下午突然报Communications link failure。原因MySQL 的wait_timeout默认 8 小时但连接池里的空闲连接可能被防火墙或中间设备提前断开。解决在application.yml里加spring.datasource.hikari.max-lifetime180000030 分钟idle-timeout60000010 分钟并且开启connection-test-query: SELECT 1。这样连接池会定期回收和检测不会拿到已经死掉的连接。4.4 过号患者重新取号后序号冲突现象患者过号后重新取号新号和队列里已有的号重复。原因过号重排时直接改了create_time但没有重新生成queue_no。解决过号重排应该走完整的取号流程重新从queue_sequence拿一个新序号而不是复用旧号。源码里CallStrategy只负责叫号重排逻辑在QueueService里改的时候要确保queue_no和current_seq同步更新。4.5 大屏显示乱码现象患者姓名里的生僻字显示成问号或方块。原因数据库连接串没有指定characterEncodingutf8或者 Tomcat 的URIEncoding没配。解决连接串加characterEncodingutf8同时在application.yml里加server.tomcat.uri-encoding: UTF-8。如果还有问题检查 MySQL 的character_set_server是不是utf8mb4utf8在三字节以上的字符会丢。5. 进阶把叫号数据变成科室效率看板跑通基础叫号之后我习惯再加一层统计。patient_queue表里已经有create_time、call_time、finish_time三个时间戳一减就能算出「等待时长」和「就诊时长」。等待时长是患者从取号到被叫号的时间就诊时长是被叫号到完成的时间。这两个指标能直接反映科室的拥堵情况。-- 按科室统计当天平均等待时长和就诊时长 SELECT dept_code, COUNT(*) AS total_patients, AVG(TIMESTAMPDIFF(MINUTE, create_time, call_time)) AS avg_wait_min, AVG(TIMESTAMPDIFF(MINUTE, call_time, finish_time)) AS avg_serve_min FROM patient_queue WHERE queue_date CURDATE() AND status 2 GROUP BY dept_code;这条 SQL 跑出来的结果如果某个科室avg_wait_min超过 30 分钟说明号放多了或者医生不够如果avg_serve_min低于 2 分钟可能是医生点「完成」点得太快数据不准。我一般会把这个查询挂到/api/stats/dept接口上然后用 ECharts 画一个柱状图放在护士站的第二块屏幕上。护士长看到哪个科室等待时间飙升就能及时调人支援。还有一个技巧是「叫号间隔动态调整」。源码里call-interval是固定值但实际场景下上午 9 点到 10 点是人流高峰医生叫号可以快一点下午 3 点以后人少了叫号慢一点反而能让患者从容走到诊室。我自己的做法是写一个定时任务每小时根据当前等待人数调整call-interval等待人数超过 20 人时设为 1000 毫秒低于 5 人时设为 3000 毫秒。这个逻辑不复杂但需要把call-interval从配置文件挪到数据库或者 Redis 里让定时任务能改。Scheduled(cron 0 0 * * * ?) public void adjustCallInterval() { int waiting queueMapper.countWaiting(); int interval waiting 20 ? 1000 : (waiting 5 ? 3000 : 2000); redisTemplate.opsForValue().set(queue:call-interval, interval); }这段代码每小时跑一次根据等待人数动态设置叫号间隔。countWaiting是一个简单的SELECT COUNT(*)redisTemplate存的值在叫号接口里读。注意Scheduled需要启动类上加EnableScheduling否则不生效。从那以后我每次部署叫号系统都会先把统计接口和动态间隔加上再让护士站试跑一天——数据不会骗人等待时长曲线一出来哪里堵、哪里闲一目了然。希望帮到你。本文还有配套的精品资源点击获取
返回列表