ARTICLE DETAIL

资讯详情

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

C# WinForm排队叫号系统实战:从取号到叫号的完整链路与避坑指南

C# WinForm排队叫号系统实战:从取号到叫号的完整链路与避坑指南 简介这是一套基于C# WinForm开发的智能排队叫号系统完整源码面向计算机相关专业课程设计、毕业设计学生及需要搭建叫号业务的中小型项目开发者。系统覆盖预约、取号、微信取号、绿色通道、服务评价与数据统计分析等完整流程并整合取号端、硬件叫号器、软件叫号器、LED条屏、综合显示屏、消息服务端及语音端等多类终端主要模块采用C/S架构数据服务基于Remoting通信并可发布兼容WebAPI支持消息服务自由扩展业务。数据维护模块采用B/S架构与MVC模式支持响应式布局可在PC、平板和手机上浏览操作综合显示屏端为安卓原生应用内嵌B/S方式支持实时更新与样式维护。资源包共1560个文件以938个cs源码、135个png图片、75个resx资源、59个js脚本、52个dll及43个csproj工程文件为主另含cshtml视图、sql脚本与配置文件压缩包约25.55MB。已有484人学习适合作为课程设计参考与二次开发基础。1. 排队叫号系统在 WinForm 里到底怎么落地从取号到叫号的完整链路银行网点、医院门诊、政务大厅里那台吐小票的机器背后跑的逻辑比多数人想的要朴素。基于 C# 开发WinForm排队叫号系统本质是把「取号—排队—叫号—过号—统计」这条业务链路用 WinForm 的窗体、控件和事件机制串起来再配一个本地数据库做持久化。它不需要分布式、不需要微服务一台取号机加一块叫号大屏就能跑通。适合谁做做 C# 上位机、WinForm 项目案例练手的开发者或者要给小型营业厅做一套轻量叫号工具的人。热搜里常出现的 winform 控件、c# 线程、c# 与 access 这些点在这套系统里全都会碰到。下面按「先立住结构、再动手复现、最后讲坑」的顺序拆开讲每一步都能照着敲。2. 系统架构与数据模型三端一库怎么划分2.1 取号端、叫号端、显示端各自的职责边界排队叫号系统拆开看就是三个角色。取号端负责生成号码、打印小票、写入排队记录叫号端是工作人员操作的窗口负责呼叫下一位、重呼、过号、转移显示端是挂在大厅的屏幕实时刷新当前叫号和服务窗口。三端可以跑在同一台机器上也可以分机器部署取决于网点规模。我一般会把它们做成三个独立的 WinForm 项目放在同一个解决方案里共享一个数据访问类库。这样做的好处是取号机坏了不影响叫号叫号端升级不用动取号端。共享类库里放实体类、数据访问层和业务规则三端各自引用。热搜词里的「c# 类库的使用」在这里就是刚需——不抽类库三个项目各写一份 SQL改一个字段要改三处血泪经验。数据模型不复杂核心就几张表。号码表存流水号、业务类型、取号时间、状态窗口表存窗口号、当前呼叫号码、工作人员业务类型表存业务名称、前缀字母、是否启用。状态字段是整个系统的灵魂用枚举值管理0 等待、1 已呼叫、2 已办理、3 过号、4 已取消。状态流转错了整个叫号逻辑就乱套。2.2 用 Access 还是 SQLite本地库选型的三个判断点小型叫号系统用本地数据库就够了常见做法是 Access 或 SQLite。两者都能被 C# 直接读写但选型要看三个点。第一部署环境。Access 依赖 Office 驱动或 ACE 引擎客户机没装对应组件就跑不起来这是最常见的翻车点。SQLite 只需要一个 DLL随程序发布零依赖。第二并发量。叫号系统并发极低取号端和叫号端同时写的情况很少两者都扛得住。第三备份和迁移。Access 是单文件拷走就行SQLite 也是单文件但需要程序释放连接后才能拷。我的选择是 SQLite理由是部署省心。热搜里「c#与access」出现频率高说明很多人还在用 Access能用但记得在安装包里带上驱动否则客户机上直接报「未找到提供程序」。// 使用 System.Data.SQLite 连接本地库 // 连接字符串指向程序目录下的 queue.db string dbPath Path.Combine(Application.StartupPath, queue.db); string connStr $Data Source{dbPath};Version3;; using (var conn new SQLiteConnection(connStr)) { conn.Open(); // 建表号码表Status 用整数表示状态 string sql CREATE TABLE IF NOT EXISTS Ticket ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Number TEXT NOT NULL, -- 完整号码如 A001 BizType TEXT NOT NULL, -- 业务类型前缀如 A SeqNo INTEGER NOT NULL, -- 当日流水号 Status INTEGER DEFAULT 0, -- 0等待 1已呼叫 2已办理 3过号 CreateTime TEXT NOT NULL, CallTime TEXT ); using (var cmd new SQLiteCommand(sql, conn)) { cmd.ExecuteNonQuery(); } }这段代码做了两件事拼连接字符串、建号码表。Data Source指向程序运行目录避免路径写死导致换机器就找不到库。Status用整数而不是字符串查询和更新都更快也避免中文状态值在不同编码下出问题。SeqNo单独存当日流水号是为了每天重置号码时不用去解析字符串。注意CREATE TABLE IF NOT EXISTS程序每次启动都执行一遍也不会报错省去判断库是否存在的逻辑。2.3 号码生成规则前缀加流水号怎么保证不重复号码格式一般是「字母前缀 三位数字」比如 A001、B012。前缀对应业务类型数字是当日该业务的流水号。生成逻辑是查当天该业务的最大流水号加一格式化成三位。这里有个并发陷阱。如果两个取号端同时取号都查到最大号是 005都生成 006就重号了。单机单取号端不会遇到但多台取号机就会。解决办法有两种一是用数据库事务加锁二是用全局唯一序列。小型系统我一般用事务包住「查询最大号 插入」两步SQLite 默认的串行化隔离级别能挡住。// 在事务中生成号码避免并发重号 public string GenerateNumber(string bizType) { using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 查当日该业务最大流水号 string maxSql SELECT IFNULL(MAX(SeqNo),0) FROM Ticket WHERE BizTypebiz AND date(CreateTime)date(now,localtime); int maxSeq; using (var cmd new SQLiteCommand(maxSql, conn, tran)) { cmd.Parameters.AddWithValue(biz, bizType); maxSeq Convert.ToInt32(cmd.ExecuteScalar()); } int nextSeq maxSeq 1; string number bizType nextSeq.ToString(D3); // D3 补零到三位 string insertSql INSERT INTO Ticket(Number,BizType,SeqNo,Status,CreateTime) VALUES(n,b,s,0,datetime(now,localtime)); using (var cmd new SQLiteCommand(insertSql, conn, tran)) { cmd.Parameters.AddWithValue(n, number); cmd.Parameters.AddWithValue(b, bizType); cmd.Parameters.AddWithValue(s, nextSeq); cmd.ExecuteNonQuery(); } tran.Commit(); return number; } } }BeginTransaction把查询和插入绑成一个原子操作其他连接在这期间写不进来。date(now,localtime)用本地时间过滤当天记录避免 UTC 时差导致跨天判断错误。ToString(D3)保证 1 显示成 001超过 999 会自然变成四位数不会截断。参数化查询biz、n是必须的拼接字符串既慢又有注入风险。3. 叫号端核心逻辑状态机与界面刷新怎么配合3.1 呼叫下一位的完整流程与状态流转叫号端的核心动作是「呼叫下一位」。流程是根据当前窗口绑定的业务类型从等待队列里取排在最前面的号码把状态从 0 改成 1记录呼叫时间更新窗口当前号码然后通知显示端刷新。状态流转必须严格。等待(0) → 已呼叫(1) 是正常路径已呼叫(1) → 已办理(2) 是办完已呼叫(1) → 过号(3) 是没人来过号(3) 可以重新入队回到等待(0)。任何一步跳过都会让队列错乱。我见过有人直接把等待改成已办理结果统计报表里办理数虚高排查半天。// 呼叫下一位取等待队列最前更新状态 public Ticket CallNext(string bizType, string windowNo) { using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 取该业务等待中最小的流水号 string selSql SELECT Id,Number,SeqNo FROM Ticket WHERE BizTypeb AND Status0 ORDER BY SeqNo LIMIT 1; Ticket t null; using (var cmd new SQLiteCommand(selSql, conn, tran)) { cmd.Parameters.AddWithValue(b, bizType); using (var rd cmd.ExecuteReader()) { if (rd.Read()) { t new Ticket { Id rd.GetInt32(0), Number rd.GetString(1), SeqNo rd.GetInt32(2) }; } } } if (t null) { tran.Rollback(); return null; } // 队列空 // 更新为已呼叫 string updSql UPDATE Ticket SET Status1,CallTimedatetime(now,localtime) WHERE Idid; using (var cmd new SQLiteCommand(updSql, conn, tran)) { cmd.Parameters.AddWithValue(id, t.Id); cmd.ExecuteNonQuery(); } tran.Commit(); return t; } } }ORDER BY SeqNo LIMIT 1保证先来先服务不能用 Id 排序因为 Id 是全局自增跨业务类型会乱。Status0过滤只取等待中的。队列空时回滚并返回 null界面据此提示「当前无等待」。整个查询加更新在事务里防止两个窗口同时叫到同一个号。3.2 用 Timer 还是线程显示端刷新的两种做法显示端要实时反映当前叫号刷新方式有两种WinForm 的System.Windows.Forms.Timer定时查库或者后台线程加事件通知。小型系统我推荐 Timer简单可靠。Timer 的间隔设 1000 到 2000 毫秒。太短了频繁查库没必要太长了叫号有延迟感。在 Tick 事件里查当前各窗口的最新呼叫记录更新 Label 或 ListView。注意 Timer 的回调在 UI 线程上直接改控件属性没问题不用 Invoke。后台线程方案适合多显示端、需要推送的场景但要处理跨线程更新控件的InvokeRequired复杂度上去了。热搜里「c# 线程」和「winform 之 listview」经常一起出现如果显示端用 ListView 列出所有窗口状态Timer 加 ListView 刷新是最省事的组合。// 显示端定时刷新每 1.5 秒查一次各窗口当前号码 private void timerRefresh_Tick(object sender, EventArgs e) { string sql SELECT w.WindowNo, w.CurrentNumber, w.BizType FROM Window w WHERE w.Enabled1 ORDER BY w.WindowNo; using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) using (var rd cmd.ExecuteReader()) { listView1.BeginUpdate(); // 暂停重绘减少闪烁 listView1.Items.Clear(); while (rd.Read()) { var item new ListViewItem(rd.GetString(0)); // 窗口号 item.SubItems.Add(rd.IsDBNull(1) ? 空闲 : rd.GetString(1)); // 当前号码 item.SubItems.Add(rd.GetString(2)); // 业务类型 listView1.Items.Add(item); } listView1.EndUpdate(); // 恢复重绘 } } }BeginUpdate和EndUpdate成对使用清空和填充之间不重绘避免列表闪烁这是 WinForm ListView 刷新的标准做法。rd.IsDBNull(1)判断当前号码是否为空空闲窗口显示「空闲」而不是空白。Timer 间隔在属性面板设Interval 1500单位毫秒。3.3 语音播报与窗口联动叫号后怎么通知大厅叫到号之后大厅要听到「请 A006 号到 3 号窗口」。WinForm 里播报有两种做法一是用System.Speech.Synthesis.SpeechSynthesizer做 TTS 合成二是预录好音频文件用SoundPlayer播放。TTS 灵活号码变了不用重录但音色机械预录音频自然但号码组合多录不全。我一般用 TTS代码量小。叫号成功后触发播报把号码和窗口号拼成句子传进去。注意 TTS 是异步的连续叫号要排队否则会叠音。用一个队列加单独线程消费或者简单点用SpeakAsync加完成回调。// 叫号成功后语音播报 private SpeechSynthesizer synth new SpeechSynthesizer(); private void SpeakCall(string number, string windowNo) { // 选择中文语音系统没装中文包会回退默认 var zhVoice synth.GetInstalledVoices() .FirstOrDefault(v v.VoiceInfo.Culture.Name.StartsWith(zh)); if (zhVoice ! null) synth.SelectVoice(zhVoice.VoiceInfo.Name); synth.Rate 0; // 语速-10 到 100 为正常 synth.Volume 100; // 音量 0-100 string text $请 {number} 号到 {windowNo} 号窗口; synth.SpeakAsync(text); // 异步播报不阻塞界面 }GetInstalledVoices过滤中文语音没装中文包时用默认语音读中文会很难听这是常见问题。Rate和Volume按现场环境调大厅嘈杂就调高音量。SpeakAsync不阻塞 UI 线程叫号按钮点完立刻能响应下一次操作。如果要严格排队播报把 text 塞进ConcurrentQueue单独起线程while取出来Speak同步版播完再取下一个。4. 避坑与排查叫号系统上线后最容易翻车的五件事4.1 号码重号或跳号现象两个客户拿到同一个号或者号码从 005 直接跳到 007。原因重号是多取号端并发写库没加事务跳号是插入失败但流水号已经算出来了或者事务回滚后序列没回退。解决号码生成必须包在事务里查询最大号和插入要么都成功要么都失败。跳号如果只是偶尔出现业务上可接受不必强求连续如果频繁跳检查是不是有异常被吞掉了。4.2 显示端界面卡死现象叫号端点了呼叫显示端半天不刷新或者整个窗口无响应。原因Timer 的 Tick 里做了耗时操作比如查库慢、网络请求超时阻塞了 UI 线程。解决Tick 里只做轻量查询复杂逻辑丢到后台线程查库加超时ListView 刷新用 BeginUpdate/EndUpdate。如果用了Thread.Sleep在 Tick 里那是自找的删掉。4.3 语音播报没声音或叠音现象叫号了但大厅没声音或者两个号的声音叠在一起听不清。原因没声音多半是系统没装中文语音包或者Volume被设成 0或者默认音频设备不对。叠音是连续调用SpeakAsync前一句没播完后一句就来了。解决先确认系统语音包用GetInstalledVoices打印出来看播报改成队列串行一句播完再播下一句。4.4 数据库文件被占用无法备份现象想拷贝 queue.db 做备份提示文件正在使用。原因SQLite 连接没释放或者程序一直持有连接。解决所有连接用using包住用完即关备份时先停程序或者用 SQLite 的在线备份 API。Access 也有同样问题而且 Access 的锁文件 .ldb 更麻烦。这是本地库的通病养成「连接即开即关」的习惯。4.5 跨天号码不重置现象昨天取到 A050今天第一个号变成 A051没从 A001 重新开始。原因生成号码时查最大流水号没按日期过滤把昨天的也算进去了。解决查询条件加date(CreateTime)date(now,localtime)只统计当天。注意时区用localtime而不是默认 UTC否则凌晨会错乱。这个坑我在跨年那天踩过血泪经验。5. 进阶技巧把叫号系统做成可配置、可扩展的形态基础版跑通后值得往上加的是配置化和扩展性。业务类型不该写死在代码里应该做成配置表运营人员能自己加「A 业务」「B 业务」改前缀和名称。窗口和业务的绑定关系也做成配置哪个窗口办哪些业务改配置不改代码。这样一套程序能适配不同网点不用每家重新编译。配置化落地就是多两张表BizType 表存业务前缀、名称、是否启用WindowBiz 表存窗口和业务的对应关系。取号端读 BizType 生成按钮叫号端读 WindowBiz 决定当前窗口能叫哪些业务。代码里所有硬编码的「A」「B」都换成从配置读。再往上走是数据统计。每天的取号量、办理量、过号量、平均等待时长这些数据对网点管理有价值。统计不实时算每天闭店后跑一次批处理把结果写进统计表报表端直接读。平均等待时长 呼叫时间 - 取号时间按业务类型分组求平均。过号率 过号数 / 取号数超过阈值说明叫号节奏有问题要么窗口太少要么客户没听到。验证一套叫号系统是否可靠我的习惯是跑三个场景单窗口满负荷连续叫 100 个号看有没有重号跳号两个取号端同时取 50 个号看流水号是否连续无重复跨天测试把系统时间调到 23:59 取号再调到 00:01 取号看号码是否正确重置。这三个场景过了基本能上线。最后一个技巧给叫号端加一个「重呼」和「转移」按钮。重呼是把当前号码再播一遍客户没听到时用转移是把当前号码转到另一个窗口窗口设备故障时用。这两个功能实现简单但现场很实用没有它们工作人员遇到异常只能干瞪眼。做这类系统功能不在多在于每个异常路径都有出口。希望帮到你。本文还有配套的精品资源点击获取
返回列表