☰
SpringBoot社区诊所在线挂号与排队系统设计与实现
2026/10/10 23:19:32 网站建设 项目流程

上个月我陪家人去社区诊所看内科,到了之后发现整个候诊区坐满了人,护士台只有一位护士在忙,手里捏着一沓挂号单,一边喊号一边应付各种“我过号了能插队吗”“我刚挂完号应该排在哪”的询问。就是那个瞬间,我觉得这套流程太需要被一个系统规范起来了。

标题是SpringBoot社区诊所在线挂号与排队系统的设计与实现,重点在就诊签到。这里的“签到”不是简单打个卡,它决定了一个患者挂完号之后能不能进入候诊队列、医生叫号时叫的是谁、过号之后怎么处理。这篇文章把我从需求拆解到上线踩坑的全过程写出来,提供一个可直接照做的实现思路,不管你是拿它做课题,还是诊所想落地一套轻量方案,都能用得上。

1. 某社区诊所的排队之乱:从三个混乱点拆出需求

1.1 排队到底乱在哪

社区诊所和大医院不一样,没有专门的分诊台,也没有电子叫号屏,所有流程全靠护士手动完成。我先蹲点了几天,把混乱点记下来,基本可以归成三类。

第一类是挂号排队和候诊排队混在一起。早上八点半是高峰,挂号窗口前排着长队,候诊区也坐满了人,护士必须一边收挂号单一边人工登记顺序。更麻烦的是,总有患者挂完号之后觉得自己“排上了”,实际护士手里那张纸根本没有他的名字,十点多就开始反复追问。

第二类是过号没有明确规则。医生喊了三遍名字没应答,护士就把号压到后面,但这个“后面”到底是队尾还是当前号码加三,全靠个人理解。患者回来之后发现自己的位置没了,情绪一下子就上来了,这是我见过最多的纠纷来源。

第三类是签到信息完全靠嘴。谁到了谁没到,护士只能凭印象。有一次一个患者早上九点挂了号,出去买了个早饭,回来发现号已经过了,护士也说不清他到底算不算迟到。整个流程里缺少一个“患者到院确认”的动作,后续所有排队逻辑都失去了依据。

1.2 把需求拆成四类角色

观察完现场,我把系统参与者分成四类,每一类都有自己的核心诉求。

患者端要能做三件事:在线查看医生排班并挂号、到院后一键签到、实时查看自己在候诊队列中的位置。护士端要能代签到、退号、处理过号患者、在特殊情况下手动调整队列。医生端要能看到当日待诊患者列表、按顺序叫号、标记就诊完成。管理员端则负责科室维护、医生排班、号源数量配置和基础数据统计。

整个业务流程串起来就是:患者在线预约某个时段 → 到院签到 → 系统把签到患者放入候诊队列 → 医生按顺序叫号 → 患者就诊 → 完成状态。这里最关键的转折点就是“签到”,挂号只代表你有资格来看病,签到才代表你真正进入了这家诊所今天的排队序列。

1.3 为什么用SpringBoot而不是别的技术栈

说实话,社区诊所这类业务量并不大,日均门诊两三百号就算不错了,存在技术选型上完全不需要搞微服务那套。我选SpringBoot的原因很直接:生态成熟、资料多、内嵌Tomcat打包成一个Jar就能跑,部署一台小服务器完全够用。

数据访问层用了MyBatis-Plus,主要看中它内置的CRUD和条件构造器,写业务代码时省很多样板活。排队队列和号源并发控制交给Redis,因为它天然支持List、ZSet这些结构,又提供原子操作,比自己在应用层加锁靠谱。登录态用了JWT,叫号通知用WebSocket推送到候诊区大屏和患者手机端。

最终的技术栈表如下:

层次选型用途
后端框架Spring Boot 2.7业务接口、调度任务
数据访问MyBatis-Plus表操作、分页查询
缓存与队列Redis号源计数、候诊队列
认证JWT登录态与接口鉴权
实时推送WebSocket叫号通知、候诊列表刷新
前端Vue 3管理端与患者端页面

这套组合的好处是每一层都有明确分工,出问题时很容易定位。而且它是单一应用,不需要拆服务,对一个社区诊所的规模和技术维护能力来说,反而是最稳妥的选择。

2. 数据库设计:挂号、排队、签到如何串成一条链路

2.1 六张核心表的结构与设计意图

我设计数据库的时候反复想一个问题:状态数据放在哪、怎么流转才不乱。最终没有用大而全的业务表,而是拆成六张表,各管一段。

第一张是用户表user_info,包含患者的基本信息、手机号、证件号和注册时间。手机号是登录账号,也是后面签到时用来快速检索用户的凭证。第二张是医生排班表doctor_schedule,记录某位医生在某个日期、某个时段(上午/下午)、在哪个科室坐诊,同时有一个total_num字段表示这个班次总共放多少个号。

第三张是挂号单表registration_order,这是最核心的表。字段包括:患者ID、排班ID、预约日期、预约时段、状态、创建时间。状态字段是整个业务流转的晴雨表,取值有:0待签到、1已签到、2候诊中、3就诊中、4已完成、5已过号、6已取消。第四张是排队记录表queue_record,记录患者进入候诊队列的时间、当前队列号、排队状态,以及他在队列中的位置信息,方便前端实时刷新。

第五张是签到记录表sign_in_record,记录签到时间、签到方式(扫码/自助机/护士代签)、操作人ID和对应的挂号单ID。这张表单独存在的意义是方便统计患者到院率和护士代签工作量。第六张是科室表department,用来维护科室名称和位置描述。

设计时我坚持一个原则:业务操作尽量只改状态字段,不在主表上做大量模糊更新,这样查询和统计都很清晰。

2.2 状态机:挂号的整个生命周期该怎么走

挂号单的status字段是整个系统的“大动脉”,所有模块都围绕它工作。我把它设计成一个状态机,每一步流转都有明确条件。

患者提交挂号后,单子进入“0待签到”。患者到院并在规定时间窗口内完成签到,状态变为“1已签到”。签到成功后系统立即把患者放入候诊队列,状态随之更新为“2候诊中”。医生点击叫号,患者状态变为“3就诊中”。医生完成问诊并点击结束,状态变为“4已完成”。如果患者在预约时段结束前一直没有签到,状态直接改成“5已过号”。患者也可以主动取消挂号,状态变成“6已取消”。

引入状态机的最大好处是让所有模块的判断条件从“一大坨if else”变成“查一下状态字段”。比如护士处理过号患者时,只需要看这个单子是“1已签到”还是“5已号”,决定是放回队尾还是要求重新挂号,判断逻辑非常干净。

2.3 号源防超卖:数据库和Redis双层保护

在线挂号最怕的就是一个诊号被卖两次,社区诊所虽然并发不高,但早高峰那种几千人同时抢几十个号的情况还是可能出现的,我干脆按大促的思路做了保护。

Redis里维护每个排班ID的剩余号源数,比如schedule:10086:remain = 30。患者发起挂号时,先用DECR这个Key做原子扣减,返回值如果小于0,说明号已经被抢光了,马上返回“号源不足”。这里Redis的减操作是原子的,天然避免并发覆盖。

数据库层再做一道保险。registration_order表里对排班ID和患者ID加了唯一索引,防止同一个患者重复挂同一个号。同时在doctor_schedule表里用乐观锁字段version,提交时执行“update schedule set remain = remain - 1, version = version + 1 where id = ? and remain > 0”,更新行数为0则说明号源已被扣完,事务回滚。

两道防线一起用,既保证了Redis热路径的极速扣减,也保证了数据库冷数据的最终一致。这两步是我实际压测过才确定的,单独靠哪一层都有隐患。

3. 在线挂号与排队叫号的核心实现

3.1 排班与号源生成逻辑

排班是挂号的数据源。管理员在系统里给医生排班时,会同时设置一个班次的号源总量。我的号源没有采用“提前生成30张Ticket”的方式,而是采用动态扣减模型:排班记录里存一个total_num,Redis初始化时把总量写入剩余号数,每次挂号动态扣减。

这样做的好处是管理简单,不用每天跑定时任务生成几十上百条号源记录。而且号源和时间段不绑定,患者只需要看“上午还剩15个号”,不需要选具体几点几分,把粒度从“时间点”放宽到“时间段”,反而更适合社区诊所的实际情况。

唯一要注意的是,诊所如果有固定停诊日,管理员必须提前一天把排班日期维护到位,否则患者能挂到号但医生不上班,系统的排班校验就会形同虚设。

3.2 挂号接口的校验与防重复

挂号接口虽然只有短短几步,但校验逻辑我写得很细致。患者进来时先做什么?我列一下实际顺序:

  • 校验登录态,从JWT中取出用户ID。
  • 校验排班状态:排班必须存在、日期不能是过去、总号数大于已挂号数。
  • 校验冲突:这个患者当天不能已经在同科室有过“待签到”或“已签到”的挂号单。
  • 校验操作幂等:防止用户连续点击导致重复生成挂号单。

幂等处理我用了一个令牌机制。患者进入挂号页面时,后端生成一个UUID作为防重令牌下发给前端,前端提交挂号时必须携带这个令牌,后端以“是否存在该令牌并删除成功”作为是否放行的条件。Redis的del操作返回1才代表令牌被自己拿走,可以继续挂;返回0代表别人已经用过,直接拒绝。

这个设计看似简单,但效果很好。我实测双击按钮、断网重试、浏览器回退再提交,三种场景都不会产生重复数据。

挂号成功后,前端马上跳到“已挂号”页面,展示一条提醒:请于预约时段内到院签到,过时将自动号。签到的倒计时逻辑由前端从接口返回的deadline时间戳计算。

3.3 候诊队列与医生叫号推送

队列模块是我花时间最多的地方。一开始想过用数据库表存排队顺序,用一个expire字段表示序号,但发现改顺序、插队、过号重排这些操作用数据库改起来非常别扭。最后选了Redis的List结构,左侧入队右侧出队,天然就是一个先进先出的排队模型。

患者签到成功后,执行RPUSH queue:{doctorId} patientId,把患者ID推入队列尾部。医生端点击“呼叫下一位”时,执行LPOP queue:{doctorId},弹出队首患者,先把挂号单状态改成“3就诊中”,再向候诊区大屏推送一条叫号消息。

推送依赖WebSocket。患者在手机端订阅了主题topic:queue:{doctorId},大屏订阅同一个主题,医生一叫号,所有人手上的页面同时刷新。这里有个体验细节:叫号消息里不能只有“XXX请到3号诊室”,还要附带当前队列剩余人数,让下一个患者心里有数,知道自己还要等多久,这样能大幅减少反复到诊室门口扒望的次数。

过号的处理也放在队列模块里。患者被叫到后如果没应答,医生可以选择“标记过号”,此时系统不立刻把他踢出队列,而是给他一个3分钟的缓冲期。如果患者在缓冲期内点击“回到队列”,就把他重新插入队列末尾;超过缓冲期还没操作,状态才真正变成“5已过号”。这样既照顾了客观突发情况,又保证了排队规则的严肃性。

4. 就诊签到模块:把“挂了的号”变成“排上的队”

4.1 为什么挂号了还要签到

如果只看表面,签到只是一个“到院打卡”动作,但我更愿意把它理解为线上和线下流程的桥接点。

患者线上挂号时,系统并不知道他是不是真的会到医院。有些人预约了但不来,有些人来了却错过了窗口,如果系统在挂号成功那一刻就把他放进候诊队列,现实中这个人没到,医生的叫号就会扑空,后面真正到场的患者反而被堵住。签到存在的意义,是把“意愿”转化成“事实”,确认人已经在诊所了,才允许进入实际排队序列。

我做过一个小统计,假设一个诊所一天放200个号,其中有15%的爽约率,如果不做签到而直接排队,相当于每天有三十个“幽灵号”卡在前面,真实患者的候诊时间会被拉长将近四分之一。所以签到不是可有可无,而是整个排队系统公平性的基石。

4.2 三种签到方式的后端设计

考虑到社区诊所的患者年龄跨度大,我做了三种签到方式,共用同一个后端校验逻辑。

第一种是线上扫码签到。这也是最常用的方式。患者在候诊区找到一个二维码海报,手机扫一扫,实际上打开的是带签名参数的签到链接。二维码内容为sign/{signId}?token=xxxx,signId是加密后的挂号单ID,token有时间戳签名,防止有人恶意构造地址替别人签到。后端校验签名后,把挂号单状态从“0待签到”改为“1已签到”,并把签到记录写入sign_in_record表,同时执行入队操作。

第二种是自助机签到。这部分需要对接诊所里放置的自助机,患者输入手机号和证件号后四位即可完成身份校验。它本质上是扫码签到的脱机变体,用手机号定位用户,再用证件号后四位做二次验证,防止输错手机号误签到别人头上。

第三种是护士台代签。老年人用不惯手机,到了诊所直接找护士报名字,护士在后台搜索到挂号单后执行代签。代签需要护士账号有权限,操作记录里会保存操作人ID,方便事后追溯。

三种方式最终都落到同一个方法上:signIn(registrationOrderId, signSource, operatorId)。这样做的好处是后续如果要增加新签到渠道,比如闸机自动签到,只要复用这个核心方法就行。

4.3 签到边界场景:迟到、过号、重复签到怎么处理

签到模块表面上只有“成功/失败”两种结果,实际上藏着不少边界情况,处理不好就会挨骂。

提前签到要限制。患者挂了下午的号,早上八点就到诊所了,直接签到合适吗?我会检查当前时间是否处于预约时段开始前N分钟。如果患者早到超过30分钟,提示“未到签到时间,请稍后再试”,避免有人故意提前签到把队列占住。

重复签到要幂等。有些患者签完到之后手滑又扫了一次码,后端要先查挂号单状态,如果已经是“1已签到”或“2候诊中”,直接返回“您已签到成功,请耐心等待叫号”,不能重复入队。这个判断看似基础,但漏了就会导致一个人占两个队列位。

迟到自动转过号。我定义了一个窗口规则:上午时段的挂号单,如果中午12点前未签到,就自动把状态改成“5已过号”。实现方式是定时任务每五分钟扫一次registration_order表,凡是“0待签到”且预约时段已结束的单子,统一批量置为过号。这部分不需要实时,扫表频率完全可以放开。

过号重签处理也不是一棍子打死。如果患者确实因为特殊原因迟到,比如路上堵车、临时急性病发作,护士可以在后台把他的单子恢复为“0待签到”状态,让他重新走一次签到流程。这里保留了人工干预的口子,因为再完善的规则也得接住现实世界里的意外。

5. 上线验证时踩过的坑,以及回头看的一些优化

5.1 预约时间段粒度:最容易把医生和患者都逼疯的设计

一开始我采用了十五分钟一个时间段的精细预约,每个患者都分配一个精确到两位分钟数的就诊时刻。系统跑了一天就被人吐槽了:医生看一个感冒患者可能只要五分钟,看一个高血压随访可能要十五分钟,前者看完了发现后面那位还没到,中间空着一大截;后者又可能让后面排队的患者延误二十分钟。

后来我改成了粗粒度设计。排班只区分上午、下午和晚间三个时段,每个时段不再给患者指定具体分钟数。医生叫号顺序只看队列里的实际到场顺序,预约时段只作为一个“最晚到场时间”的约束条件。这样一来,时间维度从“强制计划”变成了“软约束”,医生压力小了,患者等得也更公平。

这个改动让我意识到,早先的设计是把大医院的“精准分时预约”模式硬搬到了社区诊所,但两者业务节奏完全不同。社区诊所的客流随机性更强,系统应该做的是把到场患者按顺序理顺,而不是替医生规划每一分钟。

5.2 双击提交和号源预扣引发的重复单问题

这个问题是在联调阶段暴露的。有个测试账号在弱网环境下点了两次挂号,后端收到两个请求,虽然Redis的DECR做了原子扣减,但前端的两次请求携带着不同的请求追踪ID,最终还是产生了两笔单据。

靠Redis扣减拦不住这种情况,真正的解法是把幂等控制前置到令牌层。我把令牌生成和删除的逻辑改成:进入挂号页就生成防重令牌,提交挂号时先删除令牌,删除成功才允许落单。删除失败说明这个请求已经是重复的,直接抛出“请勿重复提交”。数据库再加了唯一索引兜底,三层一起才算是把重复单问题彻底按住。

有意思的是,这个坑提醒了我:小系统最容易忽略的往往不是高并发下的性能问题,而是高并发下的“重复数据”问题。社区诊所虽然不追求每秒扛几万请求,但数据一旦污染,人工清理的成本比开发成本高得多。

5.3 WebSocket断连导致叫号提醒丢失

叫号提醒是系统体验的“最后一公里”。有一次测试中我发现,患者手机切到后台再切回来,WebSocket连接经常已经断了,但这期间医生叫过号,患者完全没收到通知,回过神来发现已经过号三分钟。

后来我在前端加了断线重连和自动刷新机制:WebSocket关闭后自动重新建立连接,并携带最后一次收到的队列版本号;同时候诊区大屏每隔三十秒主动拉一次候诊列表接口,作为WS推送的兜底。核心原则是:实时推送负责体验,轮询兜底负责不遗漏。

如果让我重做或扩展这个项目,我会优先把短信提醒和微信公众号模板消息加上,因为部分老年患者子女不在身边,用的是非智能手机,他们才是社区诊所最需要被覆盖的人群。这一次做完最大的体会是:系统功能不在多,把签到和排队这两件事做透,诊所的日常运行就能顺畅一大半。

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

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

立即咨询