排队机叫号系统源码全解析:架构设计、核心逻辑与踩坑实践
简介这份源码是一套完整可运行的排队机叫号系统项目核心定位是供C#初学者或正在设计类似叫号系统的开发者参考覆盖叫号呼叫、大屏显示、排队流程、数据库访问等常见模块尤其适合学习WinForm多层窗体协作与数据访问封装。资源共249个文件压缩包约1.29MB以172个.cs业务逻辑源码、44个.resx界面资源、9个.csproj工程文件为主另有少量rdlc报表、ico图标及settings配置整体结构清晰且模块划分完整。包内代码经过多年实际使用验证稳定性好并包含DbHelperSQL等数据访问辅助类可直接阅读改造也可借鉴其呼叫分屏、队列管理、历史记录等模块的写法用于扩展成不同类型的叫号系统。已有1615人浏览学习适合需要完整参考实现、快速上手项目或深入理解叫号业务流程的开发者。 带过几套叫号系统的项目从最开始用现成的商用排队机到后来被客户的需求逼着写了完全自研的排队机叫号源代码。这个过程中最大的感受是排队机这个东西听起来不就是“取号、叫号”四个字但真正要把一套系统跑稳、让窗口护士愿意用、让大厅里的患者不骂街涉及到的细节比想象中多得多。这篇文章就把我整理过的排队机叫号系统源码思路、架构设计和踩坑记录完整写出来希望能帮到正在做类似项目的朋友。我做的这套系统是典型的软硬结合项目取号终端负责发号窗口呼叫端负责叫号大屏和语音负责通知。整条链路下来核心不是某个高深算法而是如何处理“多窗口同时呼叫同一个队列”“过号如何重排”“断网时如何降级”这些看起来很不起眼、但直接影响现场体验的工程问题。下面按项目推进的顺序完整拆解。1. 排队叫号的真实痛点为什么不能直接买现成的做任何项目之前先搞清楚一个问题市面上的商用排队机已经很成熟了为什么还要自己写源代码如果只是银行大堂、政务大厅这种标准化场景直接采购成套设备确实省事。但实际项目落到社区医院、体检中心、派出所办事大厅、餐厅等场景时需求会变得特别刁钻。我所遇到的定制需求包括但不限于业务类型分类不是固定的A/B/C而是随时要加新的检查项目同一个队列要支持多窗口并行呼叫但某些窗口只能呼叫特定业务每天零点号段要自动重置但服务器不能重启非工作日取号要弹提示某个窗口临时暂停后队列不能乱。商用排队机通常配置界面有限想调整逻辑得找厂家一次上门改配置报价够买半台机器。另一个痛点是成本。商用排队机的取号终端动辄几千块一台如果大厅需要三四台取号机加六个窗口呼叫器加两个大屏整套下来不是小数目。而自研方案里取号终端可以是一台几百块的Windows工控机或者Android盒子窗口呼叫器直接是一个安装到护士电脑上的客户端大屏随便接一台电视。源码在手硬件成本能压到商用方案的十分之一甚至更低。所以这个项目的目标从一开始就定了做一套可完全自控、可离线运行、可灵活改队列规则的低成本排队叫号系统。技术栈没有选择复杂微服务而是围绕C#和TCP Socket搭建因为现场环境基本都是Windows部署维护最省心。如果你更熟悉Java、Python或者Node.js这套架构思路完全可以平移Socket通信逻辑和队列状态机才是核心。2. 系统总架构取号端、呼叫端、显示端各自的分工整个系统的拓扑结构非常直接我画过很多次给客户讲所有终端通过局域网连接一台服务端服务端保存所有队列状态和号码记录。取号终端、窗口呼叫端、LED大屏端都只是“状态的生产者和消费者”。取号端通常部署在导诊台或自助机上做的事情包括选择业务类型、生成排队号码、打印纸质小票。呼叫端安装在窗口工作人员的电脑上功能是“呼叫下一个”“重新呼叫”“过号”“暂停窗口”需要的时候还能看到当前队列等待人数。显示端负责接收服务端推送的“当前呼叫号码”和“窗口号”在大屏上展示同时触发语音播报。这三个端之间不做点对点通信所有消息统一经过服务端转发原因是为了避免状态不同步。如果呼叫端直接告诉大屏“我呼叫了A001”那取号端怎么知道A001已经被叫走了呼叫端掉线了谁来标记状态所以服务端是绝对唯一的状态中枢各端只和服务端维持一条TCP长连接。通信协议我设计得尽可能简单用JSON文本消息加换行符分隔便于排查问题。消息主要分这么几类消息名称发送方接收方说明REQUEST_NUMBER取号端服务端携带业务类型请求新号码NUMBER_ASSIGNED服务端取号端返回分配的号码和等待人数CALL_NEXT呼叫端服务端请求当前队列的下一个号码CALL_BROADCAST服务端显示端推送当前叫号信息到全部大屏MARK_NO_SHOW呼叫端服务端标记某号码过号HEARTBEAT所有端服务端心跳保活为什么不直接用HTTP轮询因为叫号场景对实时性要求高窗口护士按下“呼叫下一个”大屏最好在几百毫秒内就切换。HTTP轮询的典型延迟在1-3秒体验差而且HTTP是无状态的服务端主动推送要靠WebSocket才能实现底层仍然是Socket。直接用TCP长连接反而更直接自己控制心跳和粘包不依赖IIS这类重量级宿主。另外一个容易忽略的点所有终端的时间基准统一以服务端为准。取号端打印小票的时间、呼叫端显示当前时间、排队超时判断全部要用服务端下发的标准时间避免某台工控机时钟偏差导致“过号判定”失灵。这也是实战里反复踩过的坑后面会展开。3. 号码分配的细节同一天业务去重和断号处理取号逻辑看起来就是做个自增计数但实际考虑周全后可做的事不少。我这里的号码规则是“业务类型前缀 三位流水号”例如抽血业务是B001CT检查是C025复诊是F012。实现时用一个并发字典存放各业务前缀对应的“当前最大流水号”取号时先查当天文件里记录的最大值内存中加锁后自增再写回文件。整个分配过程全部在服务端完成取号端只是个遥控器这样多台取号机同时取号不会撞号。核心代码拆出来大概是这样private readonly object _numberLock new object(); private Dictionarystring, int _currentSeqByType new Dictionarystring, int(); public QueueNumber AssignNumber(string bizType) { lock (_numberLock) { if (!_currentSeqByType.ContainsKey(bizType)) { _currentSeqByType[bizType] LoadLastSeqFromStorage(bizType); } int nextSeq _currentSeqByType[bizType] 1; _currentSeqByType[bizType] nextSeq; // 存储到当日数据文件并返回 SaveCurrentSeq(bizType, nextSeq); return new QueueNumber(bizType, nextSeq); } }这一小段代码看着简单但里面有三个必须注意的工程细节。第一个细节是加锁范围必须覆盖“读旧值、计算新值、写新值”的整段过程如果只在自增那一步加锁两个并发请求在“LoadLastSeqFromStorage”时读到同一个数就会生成两个相同的号码。多线程下的竞态往往不是在加法本身而是在“先读后写”这个组合操作上。第二个细节是存储方式。我用的是每天一个文本文件加内存缓存文件名为日期比如queue_20250612.dat服务启动时加载当天文件里的最大序列号后续自增结果同步追加写入。文件写坏的兜底策略是启动时校验格式遇到损坏记录就从已有最大合法值继续。如果对可靠性要求更高可以换成SQLite但实际使用中发现文本文件完全够用因为写操作频率很低且只在服务端单点写入。第三个细节是跨天重置。实现上不需要定时器在午夜0点做什么特殊操作因为所有号码都带日期维度取号时只要判断当前日期和内存缓存的日期不是同一天就自动重新从0开始计数。也就是说“重置逻辑”被巧妙地转移成“日期切换到新一天时重建状态”这样即使服务端在午夜前后被重启也不会把第二天和第一天的序列号混在一起。还有个容易被忽略但很实用的功能取号时返回“预计等待人数”。这个数字不是窗口当前队列的总长度而是该业务类型下“已经取号但还没被叫到”的人数。实现方法很简单服务端维护一个按业务类型分组的有序队列新号码入队时记录该号码的等待人数就是入队那一刻队列长度减去1。小票上打印这个数字能极大缓解排队焦虑很多患者就看这个数决定先去旁边坐还是在窗口等着。4. 窗口呼叫的核心逻辑防止两个窗口叫到同一个号叫号链路里最容易翻车的场景是大厅有8个窗口其中3个窗口同时在呼叫“抽血B类”队列护士A点了“呼叫下一个”几毫秒后护士B也点了“呼叫下一个”如果两个请求同时到达服务端两个窗口可能会拿到同一个号码这在大厅广播里就是灾难A窗口和B窗口同时喊出B021。这个问题听起来像是数据库层面的“悲观锁/乐观锁”问题但放进实时Socket场景里处理方式要朴素得多。我的方案是服务端用单线程队列处理所有CALL_NEXT消息每个队列同时只允许一个窗口操作。核心思路是“令牌转移”当前号码被分配给某个窗口后它在队列中的状态立即变为“已呼叫”等待队列指针前移。下一个窗口再次呼叫时拿到的已经是队列中的新头部。伪代码如下public CallResult CallNext(string windowId, string bizType) { lock (_queueLock) { var queue _queues[bizType]; var target queue.FirstOrDefault(q q.Status QueueStatus.Waiting); if (target null) return new CallResult(NoNumber); target.Status QueueStatus.Called; target.WindowId windowId; target.CallTime DateTime.Now; // 广播给大屏和语音 BroadcastCall(target); return new CallResult(target); } }这里有两个关键决策。决策一为什么要在业务队列上加锁而不是用数据库事务。因为这个系统本质上是个实时内存状态机高频操作都集中在同一台服务端加锁粒度小、无网络IO吞吐量根本不是瓶颈。如果用数据库存队列每次呼叫都要读写磁盘延迟高了、逻辑也复杂实际没有收益。决策二呼叫后的号码状态流转一定要完整。我这个系统里号码状态分四种Waiting等待中、Called已呼叫未办理、Serving办理中、NoShow过号。CALL_NEXT取到的是Waiting状态的号码护士端“开始办理”后由Called转成Serving“办理完成”后从Serving移除。这样设计有什么好处好处是窗口可以随时知道当前正在办理的号码是哪个如果护士误操作或者病人资料不全要挂起处理有依据系统崩溃重启后也能从持久化的状态文件中恢复队列和已呼叫信息。窗口端还有两个很实用的操作“重呼”和“过号”。“重呼”就是同一个号码再推送一次到大屏适用于患者没听到的情况实现上不需要重新出队只需要把号码状态保持为Called再次调用BroadcastCall即可。“过号”则是把号码标记为NoShow之后患者回来可以手动“激活”重新入队或者进入另一个优先队列。我实现时会把NoShow号码放到队列尾部并打上“已过号重新排队”的标记这样既保证了公平性又不会无限优先。5. 部署时踩过的坑语音卡顿、串口屏时序、时钟漂移前面说到的都是代码逻辑层面的东西但排队系统真正上线后大概率让你头疼的往往不是业务逻辑而是硬件和环境。我在三个不同现场各踩过一个典型的坑这里详细展开每个都是血泪教训。关于语音播报的坑。最初版本直接在每个窗口的护士电脑上调System.Speech.Synthesis播报“请B021号到3号窗口”。逻辑上没毛病实际跑起来发现两个问题第一护士电脑接电话、放歌、系统通知音都会打断播报甚至音量混在一起乱七八糟第二多个窗口同时呼叫时每台电脑同时播报整个大厅就像早晨的菜市场三个喇叭抢着说话。后来方案改成了“集中播报”由一台专门的语音播报主机接收服务端的CALL_BROADCAST消息统一调用语音合成引擎播报窗口端不再单独发声。这台语音主机放在大厅设备间接一个功放音箱声音覆盖均匀再也没有“谁嗓门大谁先被听到”的问题了。实现上只是把播报从呼叫端挪到显示端但体验的提升是质变的。关于串口屏驱动的坑。如果用RS232/RS485串口控制LED显示屏显示“B021 3号窗口”必须注意设备初始化时序。我接的一款屏在发送数据前要求先发一个“握手命令”等待设备返回ACK后才能继续否则第一批数据会丢。更坑的是有些屏上电启动需要5到10秒服务端如果开机就立刻推送数据显示端启动时数据已经被吞了。解决办法是显示端程序启动后主动向服务端拉取“当前正在呼叫的号码”做一次全量同步而不是只依赖实时推送。也就是说服务端要持久化“当前大屏显示内容”新显示终端上线后能查询并恢复到该状态。关于各终端时钟漂移的坑。严谨的时间同步机制往往被忽视直到某天患者拿着“过号”的小票找护士理论。要知道每台电脑都可能时间不准护士电脑慢了5分钟她按下呼叫下一个时服务端记录的CallTime、患者小票上的取号时间、大屏上显示的时间完全对不上。某次现场排查就是因为取号小票打印机时间用的是工控机本地时间而那台工控机时间慢了20分钟。后续所有时间显示、时间判断统一在服务端完成发送到各终端的消息里带上服务端时间戳取号小票和显示端时间一律以这个时间戳为准彻底消灭“时间打架”。6. 源码结构的组织方式和二次开发建议整套源码如果是自己从头写建议按照“一个服务端、三个客户端”的粒度拆分项目服务端和客户端共享一个消息实体类库。我实际使用的代码目录结构大致如下QueueSystem/ ├── QueueServer/ # 服务端主程序 │ ├── Core/ │ │ ├── QueueManager.cs # 队列状态核心管理 │ │ ├── NumberAssigner.cs # 号码分配器 │ │ ├── StorageService.cs # 数据持久化 │ │ └── MessageRouter.cs # Socket消息路由与广播 │ ├── SocketServer.cs # TCP监听、连接管理 │ └── Program.cs ├── QueueClient.GetNumber/ # 取号端程序 ├── QueueClient.CallWindow/ # 窗口呼叫端程序 ├── QueueClient.Display/ # 大屏显示/语音播报端程序 └── Shared/ ├── Messages.cs # 所有消息类型定义 └── MessageParser.cs # JSON序列化/反序列化与粘包处理每个客户端独立编译成exe用配置文件区分角色这样同一个工位既可以部署取号端也可以部署呼叫端灵活度很高。关于粘包处理我再多说一句Socket通信里常见的“粘包/半包”问题在用文本换行符分隔消息时也要处理妥当。我封装了一个接收缓冲类持续从网络流读数据每次按换行符切出完整消息切不出的残留数据留在缓冲区等下一次读取。基本代码如下private StringBuilder _receiveBuffer new StringBuilder(); private void OnDataReceived(TcpClient client, byte[] data) { _receiveBuffer.Append(Encoding.UTF8.GetString(data)); while (true) { var lineEnd _receiveBuffer.ToString().IndexOf(\n); if (lineEnd 0) break; var rawMessage _receiveBuffer.ToString().Substring(0, lineEnd); _receiveBuffer.Remove(0, lineEnd 1); var message MessageParser.Parse(rawMessage); Dispatcher.Handle(client, message); } }这套代码虽然简陋但稳定实测撑住几十个终端同时在线收发叫号消息毫无压力。如果后续要扩展几个可行的方向是小程序取号通过服务端开放HTTP接口给微信小程序用户远程取号、在线看排队进度大厅数据可视化把等候人数和平均办理时长输出到图表大屏对接业务系统把叫号记录回写HIS或CRM系统实现业务流程闭环。每增加一个外部接口服务端只要增加对应的适配层原有队列核心逻辑完全不用动。有一点想特别提醒二次开发时尽量不要改动消息协议格式加需求优先加消息类型而不是改已有消息的字段含义。消息协议一旦在多个版本间不一致终端和服务端升级不同步时会出现“服务端已经广播新格式旧版大屏解析失败”的问题。我在一个项目里就吃过这个亏有两台大屏常年不关机没升级程序服务端改了广播消息字段后那两台屏直接黑屏。7. 项目落地后的几点体会这套排队机叫号系统做下来最大的体会是它一点都不“高科技”核心原理翻来覆去就是队列、锁、状态机、Socket通信。但越是这样越考验工程上的严谨度。窗口护士不会关心你用了什么设计模式她们只会在呼叫按钮按下去没反应的时候立刻吐槽患者不会看你的技术文档他们只会凭大屏显示的号码判断这地方是否正规。把这些“人”和“机器”之间的摩擦点都理顺项目才算真正成功。最后分享一个调试小技巧给所有消息都加上消息ID本地调试时打开日志追踪一个号码从取号到呼叫到办理完成的全生命周期。遇到“患者说叫到号了但大屏没显示”这类纠纷翻日志一看消息路由时间线问题三分钟内定位。这套系统的源代码逻辑并不复杂真正值钱的就是这些边边角角的经验和处理细节希望这篇文章能帮你少走我走过的弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →