资讯动态

基于Spring Boot的社区诊所在线挂号排队与就诊签到系统设计

发布时间:2026/10/5 15:50:36 来源:尧图企业网站定制
我越来越觉得社区诊所这类场景用一套完善的在线挂号系统其实比大医院的系统更讲究。大医院人多流程冗长系统哪怕复杂点也能容忍但社区诊所通常就几个诊室、几位医生患者对“排队多久”“下一个到谁”的感知极其敏感。设计得不好挂号、排队、签到三步就能把前台和医生都搞崩溃。这篇就围绕 Spring Boot 社区诊所在线挂号与排队系统中“就诊签到”这个核心环节说说我是怎么从需求梳理一路做到落地实现的。这个项目最核心的四个关键词是Spring Boot、在线挂号、排队系统、就诊签到。整套系统的脉络其实就是一条就诊生命周期线患者注册登录 → 在线选医生选时段挂号 → 当天到院签到 → 自动进入排队队列 → 叫号就诊 → 完成。每一个节点都牵扯着下一个节点的状态流转所以做的时候不能孤立地去写“挂号接口”或者“签到接口”而是得把整条链路当做一个状态机来设计。这套东西适合谁参考一个是正在做 Java 毕设 / 课设的学生照着这套思路和代码能直接搭出一份结构清晰、答辩有底气的东西另一个是真正有开发任务在手的同行尤其是要给小型诊所、社区服务站做轻量级 HIS 的这套方案已经剥离掉大量不必要的复杂度。我尽量把每一步为什么这么做、踩过哪些坑都写清楚。1. 项目需求拆解与整体架构设计1.1 社区诊所的流程痛点动工之前我先花了一天时间站在诊所前台的视角把传统流程走了一遍只有把现状痛点摸清楚后面的表结构和接口设计才有依据。传统社区诊所的就诊流程大概是这样的患者到店 → 前台询问要看哪个科 → 翻纸质排班表确认今天哪位医生出诊 → 手写挂号单 → 患者拿着号在诊室门口等 → 医生看完一个喊下一个。这里面有几个致命的体验问题号源情况不透明患者到了才知道今天能挂什么号排队纯靠“肉眼看”和“嗓子喊”一忙起来容易叫错人、漏人患者中途出去买药回来位置就被别人占了纠纷不断。所以在设计在线系统时我给自己定了三个硬性指标。第一个是号源透明化患者在家就能看到今天哪位医生出诊、还剩几个号这能大幅减少线下到店扑空。第二个是队列可视化每个患者都能在手机端看到自己前面还有几个人而不是挤在诊室门口干等。第三个是状态可追踪从挂号到签到到就诊完成每个环节都有时间戳和状态记录出了纠纷能追溯管理上也能统计医生工作量。1.2 技术选型与模块划分技术栈选型上我还是用最稳的一套Spring Boot 作为后端骨架MyBatis 负责数据持久化MySQL 存业务数据前端用 Vue Element UI 做管理后台患者端直接走 H5 页面。这套组合是 Java 方向最主流、资料最多的搭配毕设答辩和实际项目维护都很顺手。这里我要解释一个容易被忽略的点为什么排队系统不用 Redis 的 List 结构或者延迟队列而是直接用数据库表来管理社区诊所的并发量级就算全部患者集中到早上九点挂号同时在线的也不过百来号人数据库完全扛得住。而引入 Redis 意味着要额外维护一套中间件对部署环境的要求也提高了一截反而增加了不确定性。实际上线时我更愿意保证“简单、可靠、逻辑全部在一个事务里可控”这也是这类小体量系统的现实选择。模块上我拆了四个部分患者端 H5注册登录、医生排班查询、在线挂号、我的号源、签到取号、排队进度查看管理后台医生排班管理、号源池管理、签到核销、叫号操作、统计报表后端服务认证鉴权、排班/挂号/签到/队列四大核心业务模块、定时任务消息通知挂号和签到成功后通过短信或公众号模板消息推送状态变更这个作为可扩展项四个模块之间通过统一返回结构、统一异常处理和状态机流转衔接互不干扰每个模块都能独立测试这也是后面写业务代码时不会越写越乱的前提。2. 数据库表设计与核心实体建模2.1 围绕“号源状态”设计核心表这一块是整个系统的地基我前前后后改了三版表结构才稳定下来。核心原则是一切业务动作都是对“号源”这个实体状态的流转。挂号是锁定号源签到是激活号源开始就诊是消费号源。基于这个想法几张核心表这样设计。医生排班表schedule记录某位医生在某个时间段的出诊计划。字段包括id、doctor_id、schedule_date排班日期、period时段如上午/下午、total_slots总号源数、remain_slots剩余号源数、status排班状态、create_time。这里关键字段是remain_slots每次挂号都要对它做原子扣减防止超挂。挂号记录表registration这是整个系统的核心表。字段包括id、reg_no挂号单号、patient_id、schedule_id、doctor_id、reg_type号别类型如普通号/专家号、status0待就诊、1已签到、2就诊中、3已完成、4已取消、5已爽约、period、visit_date、queue_no排队序号、create_time、checkin_time签到时间。status字段就是状态机的载体所有接口的业务逻辑都围绕这个字段的合法性校验展开。签到记录表checkin_record单独抽出来是因为签到行为有独立的审计价值。它记录了id、reg_id、patient_id、checkin_code签到凭证可以是二维码内容或签到码、checkin_time、checkin_type1线上签到、2现场扫码签到、3前台代签。这样运营上就能分析有多少人线上挂完号但到了现场没签到直接决定你的爽约策略。除了这三张核心表还需要患者表patient、医生表doctor、排队记录表queue_record。患者表比较简单重点提一下member_level字段可以为后续做优先排队比如老年人、孕妇优先留下扩展位。排队记录表queue_record记录id、schedule_id、doctor_id、reg_id、queue_no、priority优先级、status等待中/已叫号/已过号/已完成、first_call_time首次叫号时间、finish_time。这张表是后面排队叫号模块的源数据。2.2 防止号源超卖原子扣减是关键在线挂号最怕的问题就是“号源超卖”——同一个医生上午就 20 个号结果系统里卖出去 22 个这就是严重的生产事故。解决这个问题的方式有好几种我最终选择的是数据库层面的原子更新。逻辑是这样的患者在提交挂号请求后后台执行一条类似UPDATE schedule SET remain_slots remain_slots - 1 WHERE id ? AND remain_slots 0的 SQL同时判断更新影响的行数。如果影响行数为 1说明扣减成功号源抢占成功如果影响行数为 0说明号源已经被抢完直接抛出“号源已满”的异常。这段逻辑不能先 SELECT 再 UPDATE必须在一条 UPDATE 语句里完成检查和扣减本质上是利用了数据库的行锁机制来保证并发安全。这里我踩过一个坑一开始我是在 Service 层先查remain_slots判断大于 0 再 UPDATE结果在并发量稍微上来一点的时候两个请求同时读到remain_slots 1两个都判断成功都去 UPDATE最后一个号被挂给了两个人。原因就是中间隔了一个网络往返的时间窗口判断和数据变更不是原子的。改成一条原子 UPDATE 之后这个坑就彻底填平了。还有一个细节号源扣减和生成挂号记录必须在同一个事务里。做法是给 Service 方法加Transactional当插入registration失败时自动回滚号源扣减避免出现“号扣了但挂号记录没生成”的脏数据。2.3 状态机的显式定义实体表的status字段用数字存但在代码里我不会用魔法数字裸奔而是用枚举把整套状态流转显式化。public enum RegStatus { PENDING(0, 待就诊), CHECKED_IN(1, 已签到), SEEING(2, 就诊中), FINISHED(3, 已完成), CANCELED(4, 已取消), NO_SHOW(5, 已爽约); private final int code; private final String desc; // 构造方法、getter 省略 }映射到 MyBatis 时typeHandler处理枚举和int的互相转换。这样所有的业务代码都在操作语义明确的枚举而不是到处写又丑又容易出错的if (status 1)。合法的状态流转我做了硬性约束在 Service 层统一校验PENDING → CHECKED_IN只有挂完号且到了就诊当天才能签到CHECKED_IN → SEEING只有医生/前台确认叫号后才能置为就诊中SEEING → FINISHED就诊完成PENDING → CANCELED患者在规定时间内主动取消PENDING → NO_SHOW过了签到截止时间仍未签到由定时任务批量处置非法流转比如从 PENDING 直接跳到 SEEING或者 CHECKED_IN 直接 CANCELED都会被拦截。这一步保证了就算前端再怎么瞎传参数后端的数据都不会乱。3. 核心业务接口实现挂号与签到3.1 在线挂号接口的完整链路挂号接口是整个系统请求压力最大、逻辑最密集的接口我把它的完整流程捋一遍。前端传入scheduleId、patientId、regType后端依次执行五个步骤。第一步校验排班状态。查排班表确认排班在今天之后且状态为“启用”防止挂到已停诊的号。第二步原子扣减号源。执行前面提到的原子 UPDATE失败直接抛异常返回友好提示。第三步生成挂号记录。生成唯一挂号单号reg_no规则我用的是yyyyMMdd 医生ID 四位流水号比如20250612001这样光看单号就知道是哪天哪个医生挂的号。初始化status 0queue_no先置为空因为还没签到队列序号在签到时才生成。第四步更新患者与排班关联。如果有需要的话在schedule下维护一个已挂号患者列表用于前端展示“已约 X 人”。第五步发送通知。异步调用消息服务发送挂号成功通知附上签到码和“请于就诊时段开始前 15 分钟签到”的提醒。挂号接口的并发控制是这套系统的生命线。我实测过通过 JMeter 模拟 200 个并发请求抢同一个医生上午的号源池原子 UPDATE 方案下没有出现一例超卖响应时间平均在 80ms 左右。这个数据也验证了普通 MySQL 配置完全够支撑社区诊所的负载。3.2 就诊签到从“线下确认”到“线上联动”签到是整个排队系统的“发令枪”患者只有完成签到才会真正进入排队队列。设计签到功能时有两个关键决策一个是签到凭证用什么另一个是签到时间窗口怎么定。签到凭证我选的是“签到码 二维码”双通道。患者挂号成功后会生成一个 6 位纯数字签到码如482913同时 H5 端会渲染一个二维码内容是CHECKIN:{reg_no}:{checkin_code}。现场有两种签到方式一种是在诊所大厅的取号机或前台平板上扫描二维码另一种是患者手机在自己 H5 页面输入签到码完成签到。两种方式最终调用的是同一个后端接口传的参数略有不同扫码会直接解析出reg_no和checkin_code手动输入则要后端先根据签到码反查挂号记录。签到接口的核心逻辑是这样的校验挂号记录存在且状态为 PENDING → 校验当前时间在允许签到的窗口内 → 校验签到码匹配 → 置状态为 CHECKED_IN → 写入签到时间 → 生成排队序号 → 插入排队记录。排队序号的生成规则是“优先级优先同优先级按签到时间先后”。普通患者默认优先级 0系统内部如有老年患者或急危重症患者前台可手动将其优先级提到 1这部分我放在下一节详细展开。时间窗口上面有个细节值得提醒。社区诊所不像大医院患者往往卡点来稍微晚几分钟很正常。所以我做了一个梯度设计就诊时段开始前 30 分钟开放签到时段开始后 15 分钟内仍可签到记为“迟到签到”会排到当前队列末尾超过 15 分钟仍未签到则状态自动置为爽约。这个窗口弹性在实际运营中特别重要太严格会让前台电话被打爆太宽松又会让号源浪费。3.3 爽约与取消的定时任务设计爽约检查不能用实时接口做得靠定时任务。我在项目里用 Spring Boot 自带的Scheduled注解实现了一个每分钟跑一次的清理任务查询所有就诊时段已开始超过 15 分钟、状态仍为 PENDING 的挂号记录批量将状态改为 NO_SHOW同时回补号源。这里有一个值得讨论的回补细节回补号源时remain_slots 1但回补的号源对现场患者不一定有意义因为时段已经过了一半。所以我维护了一个字段叫release_channel释放渠道标记这个号是“原路退回”还是“现场号”。回补的号源自动转入现场号池前台在管理后台可以看到并引导现场患者直接挂这个号。这样既保证了数据不丢失又让号源管理有了弹性。取消挂号的逻辑同样要设截止时间。规定就诊时段开始前 2 小时允许线上取消取消时状态置为 CANCELED 并回补号源超过 2 小时只能在现场由前台操作取消且要记录操作人。这既保护了医生排班的确定性也给患者留了容错空间。4. 排队叫号机制与队列调度逻辑4.1 队列数据结构与排队序号生成我们知道在 Redis 里可以用 List 做队列但前面已经决定用数据库实现。所以关键在于怎么在数据库表里优雅地表达“队列顺序”我的方案是在queue_record表里维护一个queue_no字段生成规则不做简单的自增而是根据优先级动态计算。普通患者签到后取当前医生当日队列中最大序号加 1优先患者签到后序号插入到第一个普通患者之前比它序号小的普通患者序号整体后移一位。这个后移操作放在事务里对社区诊所几百条排队记录来说成本微不足道。具体到 MyBatis 里实现序号后移一条 SQL 就够了update idmoveBackByShift UPDATE queue_record SET queue_no queue_no 1 WHERE schedule_id #{scheduleId} AND queue_no gt; #{insertPosition} AND status 0 /update注意这里status 0是“等待中”加条件是为了避免把已经叫过号、正在就诊的患者序号也挪了。然后新插入的优先患者记录queue_no insertPosition。完成了。这个方案的优点是逻辑完全可控缺点是需要事务包裹如果有一天真的并发量涨到 Redis 不可避再考虑迁移队列存储业务接口层面不用大改。4.2 叫号规则与过号重排医生每看完一个患者在前台系统上点击“下一个”触发叫号接口。叫号逻辑是这样的在本医生当日的queue_record里找到状态为等待中、queue_no最小的记录将其状态改为“已叫号”同时更新first_call_time。然后系统向该患者推送叫号通知“您前面的患者正在就诊您是下一位请到 X 诊室门口等候”。过号重排是我在实际调研中学到的一个重要细节。患者被叫号但 60 秒内未到诊室门口系统不会直接把患者作废——因为社区诊所空间小患者可能去洗手间或接电话了。规则是超过 60 秒未响应状态置为“已过号”但记录保留当医生看完当前患者后过号患者可以选择“重新排队”插入到当前队列的倒数第二位不是最后一位保留一点优先权或者找前台人工安排。这个“倒数第二位”的设定最初我设计成直接插入队首结果被实际运营反馈打回来了如果每个过号患者都插队首守时的患者会极度不满诊室门口容易吵架。插到倒数第二位既照顾了过号者的体验又不至于打乱守约者的预期。这种细节只靠技术是推不出来的必须对场景有真实体感。4.3 队列可视化的实现思路患者端 H5 需要实时展示“前面还有几个人”这倒不必用 WebSocket 做真推流太重了。我用的是前端轮询患者进入排队详情页后每 5 秒请求一次排队进度接口返回当前队列中排在自己前面的人数、预计等待时间。预计等待时间的公式是前面人数 × 该医生近 7 天平均就诊时长平均时长由定时任务统计得出。这里的值不是精确值但它的作用本质上是安抚情绪让患者有预期而不是让他干等。管理后台的大屏则直接展示整个诊所各诊室的实时队列看板按医生分组显示当前叫号、当前等待人数、今日已完成人数。医生端操作界面非常克制只有三个大按钮“呼叫下一个”“过号”“完成就诊”每个按钮都有二次确认弹窗防止误触。我做这套系统最大的体会就是面向患者的界面可以做得丰富面向操作者的界面一定越简单越好误操作率降下来整个就诊流程的摩擦就少一大半。5. 踩坑实录与常见问题排查5.1 Spring Boot 版本和依赖兼容的坑这个项目最早我用的 Spring Boot 版本是较新的 3.x踩了不少坑最后生产部署老老实实换回 2.7.x。不是说新版本不好而是很多第三方组件和 MyBatis 的适配没有完全跟上比如javax包名改成了jakarta导致一些老教程里的代码直接编译不过比如 Spring Boot 3 对一些参数校验注解的包路径做了调整网上资料鱼龙混杂处理起来非常折腾。如果你们是毕设或者快速上线项目我建议直接锁定 Spring Boot 2.7.xJDK 用 8 或 11。这套组合经过大量项目验证踩坑成本最低。等到对 Spring Boot 的机制熟悉了再往 3.x 迁移也不迟。还有一个小坑是 MyBatis 的LocalDateTime映射问题。默认情况下如果涉及时间字段的查询可能会因为类型处理器没注册而映射失败。解决方案是引入mybatis-typehandlers-jsr310依赖或者在配置里显式注册时间类型的 TypeHandler。这个问题出现的场景是本地跑好好的部署到服务器上时间字段突然变成 null 或格式不对多半就是这里。5.2 事务失效的三个典型场景Transactional用起来简单但失效场景极多。这个项目我亲自踩过三个列出来供大家排查时参考。第一个是同类内部调用导致事务失效。比如RegistrationServiceImpl里 A 方法调用同类 B 方法B 方法虽然加了Transactional但因为注入的是当前对象而非代理对象事务不会生效。解决方式是把 B 方法拆到另一个 Service 类里或者注入ApplicationContext获取代理对象调用。第二个是异步线程内的事务问题。挂号成功后发通知我是用Async异步处理的但如果在异步方法里也加了事务注解由于事务依赖线程绑定数据库连接异步线程的事务是独立的很容易出现通知发不出去但主流程看着正常的假象。最佳实践是异步方法只做通知不做核心业务写操作。第三个是异常被 catch 导致回滚失效。Spring 的事务默认只在 RuntimeException 时回滚如果业务代码里自己try-catch吞掉了异常事务自然就不会回滚。所以我的习惯是Service 层尽量不 catch 异常统一由全局异常处理器处理既保证事务边界干净也方便给前端返回统一的错误结构。5.3 前端打包放进 Spring Boot 的注意点毕设项目通常要求前后端一体化部署Vue 项目打包后需要放进 Spring Boot 的静态资源目录。操作上很简单在vue.config.js里设置publicPath为./相对路径然后npm run build把生成的dist目录下的文件复制到 Spring Boot 的src/main/resources/static下。这里要注意两个问题。第一如果前端路由用的是 history 模式刷新页面会出现 404因为前端路由是路径后端没有对应映射。解决方式是让前端改用 hash 模式或者在 Spring Boot 里加一个WebMvcConfigurer把非 API 路径统一转发到index.html。第二后端接口路径和前端访问路径要提前约定好统一前缀比如/api/这样在前后端联调时后端用 nginx 代理、前端用 devServer 代理修改成本都极低。5.4 时间字段时区问题时间字段的坑永远排得上号。MySQL 连接串里如果没配置serverTimezoneAsia/Shanghai就会出现“数据库存的时间比实际多八小时”或者少八小时的诡异现象。我的配置是jdbc:mysql://localhost:3306/clinic?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai同时 MySQL 表字段统一用datetimeJava 实体统一用LocalDateTime两边口径一致避免Date和LocalDateTime互转时的各种莫名其妙的问题。页面展示时再按前端所在时区格式化这样整个链路的时间就统一了。6. 扩展方向这个系统还能怎么进化如果这个项目做完还有余力我会建议往三个方向做扩展。一个是引入 Redis 接管排队队列把排队记录从数据库提升到内存中队列成员变更的原子性和速度都会更好这是一个自然演进路径另一个是接入企业微信或公众号的模板消息通知替代短信通知的低效和成本让患者随时知道叫号进度第三个是做一个医生端的接诊工作台让医生在自己的电脑或平板上直接查看患者历史病历、过敏史和当前主诉把挂号和就诊信息流彻底打通。从实际体验来说这套 Spring Boot 社区诊所在线挂号与排队系统的核心价值并不在于用了多前沿的技术而在于把线下就诊流程的每一个摩擦点都转换成清晰的数据状态流转并且让患者、前台、医生三方都能在一个可控的节奏里协作。我自己在跑通整条链路的那一刻最有成就感的不是接口全部测完而是看到一个模拟患者从 H5 挂完号、到现场扫码签到、再到诊室大屏上自己的名字被叫到那种“原来软件真的可以让一个小诊所运转得这么顺”的感觉。最后分享一个个人经验这类系统的复杂度永远不会来自于代码本身而来自于你对业务流程理解的颗粒度。如果你打算基于这个项目做二次开发或者毕业设计第一件事不是打开 IDE而是去一家真实的小诊所坐半小时看看前台怎么排班、医生怎么叫号、患者怎么候诊。看到那些“看似混乱但有其内在逻辑”的细节你的设计才能真正落地。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑