C#点菜系统开发实战:WinForms+SQLite从骨架到避坑指南
发布时间:2026/10/1 13:27:17来源:尧图网络
简介这套基于C#开发的火锅馆点菜系统源码工程面向餐饮管理系统学习者与课程设计人员完整覆盖菜品展示、购物车、订单生成、支付结算及小票打印等核心流程。压缩包共71个文件体积约1.55MB以20个C#源码文件、7个窗体资源文件和7个资源映射文件为主同时附带可直接运行的exe/dll程序、SQL数据库mdf/ldf以及Crystal报表文件便于直接运行和二次开发。已有141人浏览学习。源码采用MVC思想与事件驱动机制结合Windows窗体设计、ADO.NET数据访问、DataSet数据集和异常处理等关键知识点能帮助读者掌握桌面应用从界面到数据库的完整实现方法。通过调试该工程还可学习到订单数据结构设计、报表输出以及Visual Studio安装部署配置等实战技能。1. C#点菜系统到底解决什么问题火锅店场景的三大痛点火锅店点菜跟快餐、正餐都不一样锅底先上桌毛肚鸭肠是边吃边涮客人吃到一半加菜是常态服务员手里攥着手写单从前厅跑到后厨。这种场景下C#点菜系统解决的问题很具体——把点菜、传菜、收银三件事统一到一套局域网程序里前厅服务员在客户端点菜下单数据实时进数据库后厨自动出单收银端随时看桌台状态和消费金额。纸质菜单最大的毛病是信息不同步后厨不知道哪桌刚加了菜收银台不知道哪桌送了果盘结账时账单对不上翻台高峰期更是兵荒马乱。这篇笔记面向三类人想给自家门店上系统、但不想被动辄上万的点餐软件套牢的老板接外包的C#开发者需要一套能改能交付的底子以及用C#写过小工具、想拿完整项目练手的入门者。后面从技术选型讲到核心模块、数据库设计和踩坑记录照做能省下不少试错时间。2. 技术选型与项目骨架C/S架构、WinForms与数据库怎么定2.1 为什么点菜系统几乎都用C/S架构而不是浏览器访问做点菜系统第一个决策不是选控件而是选C/S还是B/S。常见做法是C/S理由很实际火锅店后厨环境油腻、Wi-Fi不稳定、PC配置偏低服务员要的是点一下就能用的响应速度不是打开浏览器等加载。C/S架构里客户端直接连数据库或通过TCP连服务端局域网内一次往返几十毫秒体验接近桌面软件。再一个原因是打印机环节。后厨出单要接ESC/POS小票打印机C/S客户端直接驱动本地打印端口USB或网口9100稳定性和兼容性都比浏览器里调打印成熟。如果走B/S还得做打印中间件才能把浏览器的打印请求转发到本地打印机中间多一层就多一个故障点。这不是说Web做不了点菜系统而是以火锅店的现场条件C/S容错率高、部署直接。对比项C/S客户端B/S浏览器部署每台机器安装一次浏览器访问免安装断网表现本地缓存可继续点菜直接不可用打印控制直接驱动本地打印机需打印中间件转发升级覆盖安装或绿色升级改服务器即生效适合规模单店、区域连锁多店远程管理界面层面也有取舍。WinForms在低配收银机上2G内存、集显启动快、占用低控件开发效率高WPF界面好看、动画流畅但同样的列表页在老机器上渲染会有明显卡顿。我一般会选WinForms做前厅点菜端和后厨出单端把UI做简洁左侧菜品分类中间菜品列表带价格和估清标记右侧购物车底部是桌台和下单按钮。这套布局在1024×768分辨率的老触摸屏上也点得准比花哨的WPF效果更实用。2.2 SQLite和SQL Server怎么选先看桌位数和并发量数据库用哪个不是看哪个高级而是看门店规模和并发量。单店10到20桌前厅2到3个点菜客户端后厨1个出单端再加收银端同时在线客户端不超过10个SQLite完全够用。SQLite是文件型数据库零配置、免安装升级直接用文件替换不会出现服务没装好连不上这种外包项目最常见的事故。如果场景变成连锁多店、菜品上千、高峰期同时下单超过50笔SQLite在写入锁上就会成为瓶颈——它同一时间只允许一个写者多客户端并发写库会频繁报database is locked。这种情况换成SQL Server或MySQL更合适。SQL Server在Windows生态里和C#配合最顺安装一个Express版就能撑住单店几百桌的规模连接字符串和管理工具都成熟。一个折中方案是开发阶段用SQLite跑通逻辑数据访问层做成接口发布时通过配置文件切换数据库。数据访问层只依赖SQL参数化查询SQLite和SQL Server的语法差异集中在建表和自增列上切换成本很小。后文所有代码示例以SQLite为准给出同时标注SQL Server对应的写法差异。2.3 把项目骨架搭起来的完整结构一个能交付的点菜系统我一般拆成四个项目用.NET WinForms方案组织DianCai.sln ├── DianCai.Client # 前厅点菜客户端(WinForms) ├── DianCai.Kitchen # 后厨出单端(WinForms) ├── DianCai.DataAccess # 数据访问层(Dapper SQLite) └── DianCai.Common # 实体类、枚举、公共扩展四个项目各司其职Client管点菜交互Kitchen管后厨出单和估清提醒DataAccess封装所有SQLCommon放菜品、订单、桌台实体和状态枚举。分层对新手最重要的意义是改界面不碰SQL改SQL不碰界面出问题一查就知道是哪一层。数据库连接字符串放在客户端项目的App.config里发布时只改配置文件就能切换数据库不用重新编译。SQLite版连接串长这样connectionStrings add nameDianCaiDb connectionStringData SourceD:\DianCaiData\DianCai.db;PoolingTrue;Max Pool Size20; providerNameSystem.Data.SQLite / /connectionStringsData Source是数据库文件路径建议放到程序目录之外比如D:\DianCaiData避免卸载重装时把数据一起清了。PoolingTrue表示启用连接池Max Pool Size20限制最多20个并发连接防止高峰期连接数失控。如果是SQL Server连接字符串改成Server192.168.1.100;DatabaseDianCaiDB;User IDsa;Passwordxxx;PoolingTrue;即可DataAccess层通过工厂模式读配置决定用哪个数据库驱动。数据库初始化在程序启动时做一次如果文件不存在就创建库和四张表然后加载菜品数据到客户端缓存。这一步用启动检查的方式避免把建库脚本散落在各处门店新增一台收银机时也能一键初始化。初始化逻辑放在DataAccess项目里界面启动事件只调用一个Init方法不要在这个方法里塞UI逻辑。提示App.config里的连接字符串默认是明文。如果门店服务器密码泄露风险高可以用DPAPI对密码字段做加密解密逻辑放在DataAccess里。小项目不必上但接政企单时这个点是加分项。3. 核心功能拆解桌台状态、菜品估清、下单流程三块硬骨头3.1 桌台状态机空台、入座、用餐、结账的流转规则桌台是整个系统的主键所有订单都挂在桌台下。火锅店的桌台流程比快餐复杂因为一桌客人可能循环多次加菜。我一般用枚举表达桌台状态public enum TableStatus { Empty 0, // 空台 Occupied 1, // 客人已入座尚未下单 Dining 2, // 已下单用餐中可加菜 Checkout 3, // 客人要求结账收银中 Cleaning 4 // 结账完毕服务员在清理 }五个状态之间允许的转换路径Empty进Occupied只有开台操作Occupied和Dining之间是首次下单和加菜操作Dining到Checkout是客人要求结账Checkout到Empty是结账完成并清台清台后才能重新开台。状态流转用独立方法封装不在按钮点击事件里散写这是后期改需求时避免改漏的关键。public bool ChangeTableStatus(int tableId, TableStatus from, TableStatus to) { const string sql UPDATE Tables SET Status to WHERE TableId id AND Status from; int rows _conn.Execute(sql, new { id tableId, from (int)from, to (int)to }); return rows 1; }这段SQL是典型的乐观更新写法UPDATE时带上前一个状态作为条件如果桌台状态已经被别的窗口改过受影响行数是0ChangeTableStatus返回false界面提示桌台状态已变化请刷新。这样就不会出现两个服务员同时对一张桌子开台、导致状态互相覆盖的情况。注意from和to传的是枚举的int值SQLite里没有枚举类型存整数最直接。在Client界面上桌台用一个DataGridView展示每行一张桌子状态列显示为约定好的颜色空台灰色、入座蓝色、用餐中绿色、结账红色、清理中橙色。颜色逻辑放在桌台实体的属性里计算不放在UI事件里这样厨房端、收银端引用同一个实体类颜色显示不会两端不一致。3.2 菜品估清火锅店卖完了怎么用代码表达火锅店的估清逻辑比普通餐厅频繁毛肚、鸭肠、鲜牛肉这类菜品每天备货量有限卖完就要在菜单上置灰不能再下单。估清的难点在于当日限量和实时扣减两个要求。菜品表里我一般加一个StockToday字段表示当天可售数量默认值是每天的备货量。每天开门营业前用一个定时任务或手动按钮把所有菜品的StockToday重置为默认备货量。点菜端读取菜品时只加载StockToday大于0的菜品public ListDish GetAvailableDishes(int categoryId) { const string sql SELECT * FROM Dish WHERE CategoryId cid AND Status 1 AND StockToday 0 ORDER BY SortNo ASC; return _conn.QueryDish(sql, new { cid categoryId }).ToList(); }Status1表示上架状态下架菜品直接Status0不用删数据SortNo是排序号控制锅底、荤菜、素菜、饮料的展示顺序。这样做的好处是估清只影响当天的可售数量不影响菜品档案第二天重置StockToday后菜品自动恢复上架。如果不想用数量只做估清/有货二值标记也可以但火锅店加菜频繁二值标记无法回答还有几份这个问题。所以建议用数量。数量扣减放在下单事务里和订单明细一起提交这个在3.3小节演示。客户端界面上StockToday等于0的菜品直接置灰并显示估清等于1到5的显示仅剩N份提示让服务员在客人问询时能直接回答。每日重置的定时任务我一般放在收银端因为收银端是每天最早开机的机器。收银端启动时检查当天有没有执行过重置没执行就弹窗提醒店长确认后执行。千万不要让每个客户端都自动重置否则后开的机器会把当天已经卖掉的库存又补回去——这个坑我踩过后面避坑章节还会提。3.3 下单事务一条SQL守住库存别让两桌抢最后一盘毛肚下单是点菜系统里最核心的写操作它同时做三件事写订单头、写订单明细、扣减菜品当日可售数量。这三件事必须在一个事务里完成任何一个失败都要整体回滚否则会出现订单存上了但库存没扣或者库存扣了但订单丢了的脏数据。public int CreateOrder(Order order, ListOrderItem items) { using var tran _conn.BeginTransaction(); try { // 1. 写订单头 const string sqlOrder INSERT INTO Orders(TableId, TotalAmount, Status, CreateTime) VALUES(tableId, amount, 1, now); SELECT last_insert_rowid();; int orderId _conn.ExecuteScalarint(sqlOrder, new { tableId order.TableId, amount order.TotalAmount, now DateTime.Now }, tran); // 2. 写订单明细并扣库存同一条SQL完成 foreach (var item in items) { const string sqlItem INSERT INTO OrderItems(OrderId, DishId, DishName, Price, Count, Amount) VALUES(orderId, dishId, dishName, price, count, amount); _conn.Execute(sqlItem, new { orderId, dishId item.DishId, dishName item.DishName, price item.Price, count item.Count, amount item.Price * item.Count }, tran); const string sqlStock UPDATE Dish SET StockToday StockToday - count WHERE DishId dishId AND StockToday count; int rows _conn.Execute(sqlStock, new { count item.Count, dishId item.DishId }, tran); if (rows 0) throw new Exception($菜品 [{item.DishName}] 库存不足请刷新菜单); } tran.Commit(); return orderId; } catch { tran.Rollback(); throw; } }参数说明BeginTransaction开启事务ExecuteScalar拿到自增列的新订单号UPDATE语句带了StockToday count条件这是库存防超卖的关键——如果剩余可售数量不够受影响行数为0立即抛异常回滚整个订单。这样即使两张桌子同时提交各点一份最后剩下的毛肚数据库层面也只会让一单成功另一单拿到库存不足的明确提示。事务提交后客户端要做两件事把订单号显示在界面上并触发后厨出单。出单不建议在下单事务里同步做——打印失败不应该回滚已提交的订单正确的做法是事务提交后把打印任务扔进队列由后台线程处理3秒内没打进队列就弹提示让服务员手工补单。这个方案在第6章展开。加菜场景复用CreateOrder方法但思路不同加菜是往已存在的订单里插入新明细不建新订单头所以可以拆一个AddItemsToOrder方法逻辑放在同一事务里同样扣库存。4. 数据库设计与数据访问层四张表撑起整个点菜系统4.1 四张核心表的字段设计菜品、桌台、订单、订单明细点菜系统再复杂核心表就四张菜品表Dish、桌台表Tables、订单表Orders、订单明细表OrderItems。其他像员工表、菜品分类表是锦上添花先把这四张建好系统就能跑起来。建表用SQLite语法写一个初始化脚本-- 菜品表一个火锅菜品一条记录 CREATE TABLE IF NOT EXISTS Dish ( DishId INTEGER PRIMARY KEY AUTOINCREMENT, DishName NVARCHAR(50) NOT NULL, CategoryId INT NOT NULL DEFAULT 1, -- 1锅底 2荤菜 3素菜 4饮料 Price DECIMAL(8,2) NOT NULL, -- 单价 StockToday INT DEFAULT 999, -- 当日可售数量 SortNo INT DEFAULT 0, -- 排序号 Status INT DEFAULT 1 -- 1上架 0下架 ); -- 桌台表门店物理桌位 CREATE TABLE IF NOT EXISTS Tables ( TableId INT PRIMARY KEY, -- 桌号如201 TableName NVARCHAR(20) NOT NULL, Capacity INT DEFAULT 4, -- 可坐人数 Status INT DEFAULT 0 -- 0空台 1入座 2用餐 3结账 4清理 ); -- 订单表一桌一次消费一张主单 CREATE TABLE IF NOT EXISTS Orders ( OrderId INTEGER PRIMARY KEY AUTOINCREMENT, TableId INT NOT NULL, TotalAmount DECIMAL(10,2) DEFAULT 0, Status INT DEFAULT 0, -- 0进行中 1已提交 2已结账 3已作废 CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP, PayTime DATETIME ); -- 订单明细订单下的每一道菜 CREATE TABLE IF NOT EXISTS OrderItems ( ItemId INTEGER PRIMARY KEY AUTOINCREMENT, OrderId INT NOT NULL, DishId INT NOT NULL, DishName NVARCHAR(50) NOT NULL, -- 冗余菜品名防止菜品改名影响历史单 Price DECIMAL(8,2) NOT NULL, -- 下单时单价防止后续改价影响历史单 Count INT DEFAULT 1, Amount DECIMAL(10,2) DEFAULT 0 );字段设计上有两个冗余是故意的OrderItems里冗余DishName和Price是因为火锅店菜单会频繁改价、改菜名如果用JOIN去菜品表取名称历史订单打印时菜名和价格会跟着变影响对账。Orders里保留TableId而不直接存桌名也是因为桌台可能改名订单只要守住桌号就够。数据类型注意SQLite没有专门的DateTime类型CURRENT_TIMESTAMP存成文本格式yyyy-MM-dd HH:mm:ss排序和比较都按字符串规则实际用没问题SQL Server环境就把这些字段换成DATETIME DEFAULT GETDATE()。DECIMAL(8,2)表示最大可存999999.99单价完全够用TotalAmount用DECIMAL(10,2)。建表之后补两个索引一个是OrderItems的OrderId一个是Orders的TableId和CreateTime组合索引。点菜系统查询最多的场景是某桌今日订单和某时段全部订单没有索引的话数据量过万后明细查询会明显变慢CREATE INDEX IF NOT EXISTS idx_orderitems_orderid ON OrderItems(OrderId); CREATE INDEX IF NOT EXISTS idx_orders_table_time ON Orders(TableId, CreateTime);索引不是越多越好点菜系统表小两三个索引足够。索引加多了反而拖慢INSERT写入得不偿失。4.2 用Dapper还是EF Core小项目我选Dapper的理由数据访问层我用得比较多的是Dapper不是EF Core。理由就三条SQL可控、性能好、学习成本低。Dapper是轻量ORMSQL还是自己写它只负责把查询结果映射成实体对象几乎不产生额外开销。EF Core虽然开发效率高但生成的SQL对新手是个黑匣子出问题不容易定位尤其在做复杂的库存扣减、联表报表时不如直接写在SQL里看得清楚。以按分类查菜品为例Dapper的写法是这样public ListDish GetByCategory(int categoryId) { using var conn new SQLiteConnection(_connString); const string sql SELECT DishId, DishName, Price, StockToday, CategoryId FROM Dish WHERE CategoryId categoryId AND Status 1 ORDER BY SortNo; return conn.QueryDish(sql, new { categoryId }).ToList(); }Dapper用categoryId参数化查询杜绝拼接SQL的注入风险。Query 泛型方法自动把每行映射成Dish对象前提是实体属性名和字段名一致我在Common项目里按字段一一对应定义实体。conn用using声明确保连接用完即关连接池会复用底层连接所以频繁开关连接不会有性能问题。用Dapper还要注意一点它不追踪实体状态所有更新都是显式写UPDATE语句。这既是缺点也是优点——不会有EF那种改了一个字段整个实体都被更新的隐患。点菜系统的更新逻辑都很明确改状态、加菜、改库存显式SQL反而更安全。真正复杂的报表比如某天每桌的翻台率和人均消费我也是写联表SQL然后用Dapper映射成统计DTO不引入额外的报表框架。-- 按桌汇总当日消费和菜品数常用于结账和营业日报 SELECT t.TableName, COUNT(DISTINCT o.OrderId) AS OrderCnt, SUM(oi.Amount) AS TotalAmount FROM Tables t LEFT JOIN Orders o ON t.TableId o.TableId AND o.CreateTime today AND o.Status ! 3 LEFT JOIN OrderItems oi ON o.OrderId oi.OrderId GROUP BY t.TableId, t.TableName这条SQL在收银端每天用很多次。LEFT JOIN保证没开台的桌子也出现在结果里Amount显示为NULLC#端读取时用result.TotalAmount ?? 0处理即可。Status ! 3排除作废订单Group By桌号汇总要点是COUNT(DISTINCT o.OrderId)而不是COUNT(*)否则一张桌多轮消费会被重复计算。5. 点菜系统避坑并发、卡死、乱码、丢单我都踩过5.1 两桌同时抢最后一份肥牛库存变负数现象高峰期两桌客人同时点最后一份肥牛点菜端都显示有货提交后查看库存变成了-1。原因客户端是先查询StockToday判断大于0后显示可点提交时再执行UPDATE减库存。但两个客户端查询到的是同一份库存提交时没有在SQL层面对库存做条件限制先查后改在并发下必然出错。解决下单事务里带上StockToday count条件让数据库决定是否成功而不是用代码判断。这是3.3小节写的方案——受影响行数为0就抛异常回滚。配套在点菜端加一层本地缓存同步每次打开菜单时刷新一次菜品列表提交失败时立即重新拉取菜品数据界面提示该菜库存不足已刷新清单。血泪经验永远不要让应用层去判断数据库的乐观锁才是最后一道防线。5.2 点下单按钮后界面卡成白屏现象WinForms点菜端点击下单按钮后整个窗口卡住鼠标转圈几秒后才恢复。原因下单操作在UI线程里同步执行数据库写入、事务提交都阻塞了界面消息循环期间界面无法响应重绘和点击。火锅店高峰期数据库响应一慢界面就看起来像死了。解决把下单逻辑放进async/await异步方法UI线程只负责发起和收结果数据库操作在线程池执行private async void btnSubmit_Click(object sender, EventArgs e) { btnSubmit.Enabled false; try { int orderId await Task.Run(() _orderService.CreateOrder(order, items)); lblOrderId.Text $下单成功单号{orderId}; } catch (Exception ex) { MessageBox.Show(ex.Message, 下单失败); } finally { btnSubmit.Enabled true; } }await Task.Run把CreateOrder放到后台线程执行界面保持响应btnSubmit.Enabledfalse防止重复点击产生两笔订单。注意async void只用于事件处理器返回Task的方法里继续用async Task不然异常会丢失。这条规则同样适用于打印、刷新列表等所有耗时操作。C#多线程的知识在这里体现得很直接UI线程和后台线程的职责分清楚界面卡死的问题能消灭一大半。5.3 后厨打印机打出黑方块和乱码现象后厨小票打印机打印出来的单子中文变成了黑方块或乱码英文数字正常。原因ESC/POS小票打印机默认用GBK/GB2312编码而C#的字符串默认是UnicodeUTF-16。直接把字符串交给打印机的文本接口编码不匹配就乱码。解决拼接打印内容时显式转成GBK编码的字节数组再发送别让打印机猜编码// 需引用 System.Text.Encoding byte[] GetPrintBytes(string content) { Encoding gbk Encoding.GetEncoding(GBK); return gbk.GetBytes(content); } // 发送时使用printPort.Write(bytes, 0, bytes.Length);如果打印机支持网口TCP 9100端口用Socket发送同样的字节流即可。还有一个配套坑部分打印机在中文模式下不认某些特殊字符比如①这类符号菜名里尽量避免归档。开发时先用厂家自带的打印测试工具跑一遍中文确认编码正常后再接程序这一步能省很多现场调试点。我当时是在后厨蹲了一下午翻来覆去试了Unicode、ASCII、UTF-8都不行最后换成GBK一次就通了这种问题不看厂商文档基本是玄学。5.4 晚上断电第二天的订单数据没了现象门店晚上营业结束直接断总闸第二天发现当天一部分订单和明细不见了SQLite文件损坏。原因SQLite默认的journal mode是DELETE没有开WALWrite-Ahead Logging。事务提交后数据先写日志断电发生在日志回放前就会丢数据极端情况下主库文件损坏。解决初始化数据库时执行三条PRAGMA把SQLite切到WAL模式using var conn new SQLiteConnection(_connString); conn.Execute(PRAGMA journal_modeWAL;); conn.Execute(PRAGMA synchronousNORMAL;); conn.Execute(PRAGMA busy_timeout5000;);WAL模式让读写可以并发写入性能更好断电恢复能力更强synchronousNORMAL在WAL下保证数据安全的同时减少磁盘写次数busy_timeout5000让并发写冲突时等待5秒而不是直接报database is locked。建议再加一条每天营业结束后把DianCai.db和DianCai.db-wal两个文件一起备份到另一个盘比任何数据库修复工具都管用。WAL模式下还会生成DianCai.db-shm文件备份时只要拷数据库主文件和-wal文件就够了。5.5 门店换了个路由所有客户端连不上服务器现象门店Wi-Fi路由器更换后所有点菜客户端都报无法连接数据库收银端正常。原因数据库连接字符串里写死了服务器的固定IP比如192.168.1.100路由器的DHCP重新分配了IP服务器变成了192.168.1.105客户端自然连不上。解决两个层面。第一数据库服务器或主机在路由器里做IP和MAC地址绑定保证固定IP不变——这一步要写进部署文档。第二客户端把连接字符串放到App.config并提供数据库设置界面让门店在IP变了时自己能改不依赖开发人员到场。连接字符串允许运行时初始化不要编译死在代码里var config ConfigurationManager.ConnectionStrings[DianCaiDb].ConnectionString; // 启动时检查连接失败则弹出设置窗口让用户填写新的连接字符串并保存另外建议在点菜端做服务器心跳检测启动时后台线程每3秒Ping一次数据库服务器IP连续3次失败就弹黄色提示条数据库连接已断开而不是等服务员点菜时才报错。这样网络问题在营业前就能发现而不是高峰期才暴露。用C#的TcpClient连一下服务器端口做个简单的连通性检查比Ping更可靠因为有些网络环境禁ICMP但TCP端口是通的。6. 进阶玩法离线同步、打印队列和分屏叫号怎么落地6.1 客户端离线缓存断网也能下单联网自动补单Wi-Fi再好的火锅店也有断网的时候。进阶做法是把下单请求先写进本地SQLite队列网络恢复后逐条同步到主库。本地库表和主库OrderItems同构多一个SyncFlag字段0表示未同步1表示已同步。同步程序用一个后台线程每10秒扫描一次本地未同步订单逐条调用CreateOrder方法成功就把SyncFlag置1。这个方案要注意冲突离线期间的订单如果扣了本地库存而主库库存早已被其他桌台用完同步时会抛出库存不足。处理策略是同步失败时把订单标记为待人工处理后厨端显示红色警告让服务员找厨师长手工确认能不能出菜。离线同步的价值在于营业不中断而不是数据零冲突这点要和门店讲清楚。6.2 打印消息队列后厨多台打印机不打架后厨可能有多台打印机一个出锅底、一个出荤菜。下单后把所有打印任务放进内存队列后台打印线程按队列顺序分发到对应打印机出单端不用等打印完成。用BlockingCollection 实现最简单它是线程安全的阻塞队列入队和出队天然线程安全var printQueue new BlockingCollectionPrintJob(); // 下单成功后printQueue.Add(new PrintJob(tableId, content, printerName)); // 后台线程foreach (var job in printQueue.GetConsumingEnumerable()) { SendToPrinter(job); }GetConsumingEnumerable会在队列为空时阻塞等待不会空转CPU。打印失败的任务放进重试队列重试3次仍失败就弹窗提醒服务员手工补单。这个队列模式同样适用于分屏叫号取号逻辑生成一个号码菜品做好后叫号显示在大屏上本质也是生产者-消费者。在C#委托和事件的配合下下单端通过事件把订单号广播给打印服务打印服务再根据菜品分类决定分发到哪台打印机各端解耦后期加新设备不用改下单代码。做点菜系统这些年我最大的教训是程序90%的时间不是在写新功能而是在防着现场出意外。库存防超卖、断电防丢数据、断网防营业中断这些防比做更花功夫。先把这几点想明白再回头写界面和按钮心态会完全不一样。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网