2026/11/5 4:06:18

ASP.NET餐饮管理系统:从桌台订单事务到扫码点餐

ASP.NET餐饮管理系统:从桌台订单事务到扫码点餐 简介面向餐饮行业信息化入门与课程实践的ASP.NET餐饮管理系统完整源码包基于ASP.NET Web Forms与三层架构结合SQL Server 2008构建涵盖菜单、订单、库存、会员、报表及后台权限等常见业务模块适合计算机专业学生用作课程设计、毕业设计参考也适合开发者学习Web项目的分层组织方式。压缩包共2000个文件、约46.58MB以css、gif、js、png等前端静态资源为主另含dll程序集、cs源码、aspx页面、ashx与asmx服务端点、config配置以及csproj、sln等解决方案文件结构接近可直接运行的完整工程。已有949人学习下载具备一定参考基数。读者可从中获取表现层、业务逻辑层与数据访问层的目录划分方式业务规则与数据库操作的衔接写法AJAX与WebService局部刷新的落地示例以及后台管理页面与权限控制的实现思路便于对照理解从页面到数据库的调用链路并在此基础上二次改造。1. 高峰期的账目为什么总对不上中午十一点四十四张桌同时下单收银台那台老电脑卡了两秒后厨的小票漏了一单等顾客起身结账时才发现少上了一道菜于是翻单、补单、重算金额一桌人多等十分钟。这类问题在小店里太常见而一套 asp.net 餐饮管理系统要解决的正是这件事——把桌台、菜品、订单、支付这四个状态锁进数据库让收银员、服务员和后厨看到的是同一份数据而不是三份各自记在纸上和脑子里的账。这套东西适合两类人一类是有 .NET 基础、想接中小餐馆点餐收银的活、或者拿它当毕设课题的开发者另一类是店里已经有一台 Windows 机器、想自建后台不想按年交订阅费的店主找来的技术朋友。它不追求高并发追求的是账目对得上、桌台不打架、后厨的出菜单和前台的点菜单是同一张单。2. 系统定位与技术选型三层架构怎么拆为什么还用 WebForm2.1 从「一台收银机」到「前台 后厨 管理端」的角色划分先把角色分清代码才好落位。这类系统通常拆成四个面向前台点餐服务员选桌、选菜、下单、加菜、收银结账挂单、并台、打折、支付、开发票备注、后厨出菜按订单明细出小票、标记已出菜、后台管理菜品维护、分类、桌台配置、员工账号、营业报表。四个面向共用一套数据区别只在权限和页面入口。我一般会把「桌台」当成系统的核心实体而不是把「订单」当核心。原因是餐馆的真实流程是围绕桌走的客人落座占台 → 点餐生成订单 → 加菜追加明细 → 结账释放桌台。如果以订单为核心加菜、并台、换桌这几个动作会写得非常别扭因为一次用餐可能对应多个订单记录。以桌台为核心订单只是挂在桌台上的一个「进行中」对象状态变化更自然。这也是为什么这套代码里TableInfo表会有Status字段而OrderInfo表会有TableId外键。很多新手照着电商的订单模型去套餐饮做到「加菜」那一步就卡住血泪经验是从桌台反推订单而不是反过来。2.2 运行环境与技术栈清单这类 asp.net 餐饮管理系统多数落在 .NET Framework 而不是 .NET Core原因很实际部署目标是餐馆那台 Windows 主机装 IIS 比装容器简单客户也不接受为了一个点餐系统去学 Docker。典型技术栈是这样层次选型说明运行时.NET Framework 4.6.2 / 4.8装在 IIS 上无需额外运行时Web 框架ASP.NET WebForm部分页面 MVC 风格事件驱动页面和控件绑定直接数据库SQL Server 2012 及以上存储过程 事务账务类数据合适数据访问ADO.NET 手写 DAL不引 ORMSQL 可控排查方便前端Bootstrap 3/4 jQuery LayUI 组件表格、弹窗、日期选择器够用部署IIS 7.5应用程序池 .NET CLR 版本要对上打印浏览器打印 / 小票机驱动后厨小票走驱动前台走 A4 或热敏为什么不用 Entity Framework不是不能而是这套系统的查询以「按桌台查进行中订单」「按日期汇总营业额」这种带聚合和连接的为主手写 SQL 反而更好读、更好改。客户说「这个报表加一列」你改一条 SQL 就行不用去动模型映射。前端不追求 SPA是因为点餐页面的核心诉求是「快」服务员在平板上点页面切换不能有白屏。WebForm 的 PostBack 虽然老但配上 UpdatePanel 做局部刷新在局域网里响应是够的。我试过把它改成 Vue 前后端分离结果发现网络请求数量翻了三倍反而不如原来稳。2.3 数据库表关系菜品、桌台、订单、明细四张表的骨架表结构是这套系统的地基先看清楚再谈代码。核心是六张表下面给出最关键的四张字段做了简化但保留了业务必需的部分。-- 菜品表IsOnSale 控制沽清/下架ImageUrl 给点餐页用 CREATE TABLE Dish ( DishId INT IDENTITY(1,1) PRIMARY KEY, CategoryId INT NOT NULL, DishName NVARCHAR(50) NOT NULL, Price DECIMAL(10,2) NOT NULL, -- 金额一律 decimal不用 float IsOnSale BIT DEFAULT 1, -- 0沽清前台不显示 ImageUrl NVARCHAR(200) NULL, SortNo INT DEFAULT 0 ); -- 桌台表Status 是状态机的载体换桌/并台都改这里 CREATE TABLE TableInfo ( TableId INT IDENTITY(1,1) PRIMARY KEY, TableNo NVARCHAR(20) NOT NULL, -- 如 A01、大厅3号 AreaName NVARCHAR(20) NULL, -- 大厅/包间/二楼 Status TINYINT DEFAULT 0, -- 0空闲 1占用 2预订 3待清台 SeatCount INT DEFAULT 4 ); -- 订单主表OrderNo 用日期序号生成便于人工核对 CREATE TABLE OrderInfo ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(30) NOT NULL UNIQUE, TableId INT NOT NULL, UserId INT NOT NULL, -- 开台服务员 OrderTime DATETIME DEFAULT GETDATE(), TotalAmount DECIMAL(10,2) DEFAULT 0, Status TINYINT DEFAULT 0, -- 0进行中 1已结账 2已退单 3挂单 PayType TINYINT NULL -- 1现金 2微信 3支付宝 4银行卡 ); -- 订单明细Status 独立于主单后厨按明细出菜 CREATE TABLE OrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, DishId INT NOT NULL, Qty INT NOT NULL, Price DECIMAL(10,2) NOT NULL, -- 下单时快照菜价防止后续改价影响历史单 Remark NVARCHAR(100) NULL, -- 少辣、不要葱 Status TINYINT DEFAULT 0 -- 0待制作 1已出菜 2已退菜 );几个字段值得单独说。OrderDetail.Price存的是下单瞬间的菜价快照不是每次都去关联Dish.Price查。原因很直接老板今天调价昨天的单不能跟着变。这个字段是很多初版代码漏掉的等到月末对账发现历史订单金额和实际收款不一致再补就很麻烦。TableInfo.Status用TINYINT而不是字符串是为了做状态迁移判断时能直接写WHERE Status 0。OrderDetail.Status和OrderInfo.Status各管各的主单已结账明细里仍可能有一条「已退菜」报表统计退菜率要靠它。索引方面至少给这三处加上不然数据过万之后报表页面会转圈CREATE INDEX IX_Order_Time ON OrderInfo(OrderTime); CREATE INDEX IX_Order_Table ON OrderInfo(TableId, Status); CREATE INDEX IX_Detail_Order ON OrderDetail(OrderId);2.4 三层目录结构怎么放资源包的目录通常是这个形态看懂了再改代码不会迷路RestaurantSys/ ├── Model/ # 实体类和表一一对应 │ ├── Dish.cs │ ├── TableInfo.cs │ └── OrderInfo.cs ├── DAL/ # 数据访问手写 SQL 集中在这里 │ ├── SqlHelper.cs # 连接、ExecuteNonQuery、ExecuteDataTable │ ├── DishDAL.cs │ └── OrderDAL.cs ├── BLL/ # 业务逻辑事务、状态判断写这层 │ ├── OrderBLL.cs │ └── TableBLL.cs ├── Web/ # 页面 │ ├── Login.aspx │ ├── Order/OrderAdd.aspx │ ├── Cashier/Checkout.aspx │ ├── Admin/DishList.aspx │ └── Web.config └── DB/ └── restaurant.sql # 建库脚本 初始数据判断一份代码值不值得改我看两点DAL里有没有一个统一的SqlHelper以及BLL里有没有事务封装。如果每个页面都new SqlConnection然后拼字符串那这套代码的维护成本会随着需求增长指数上升改造成本比重写还高。如果SqlHelper存在、BLL有ExecuteTransaction这类方法那基本可以放心在上面加功能。3. 环境搭建与首次跑通IIS 配置、连接串和数据库还原3.1 环境清单与安装顺序顺序错了会浪费一下午。我的习惯是按依赖从底往上装SQL ServerExpress 版够用安装时记住实例名默认实例是MSSQLSERVERExpress 版通常是SQLEXPRESS。SSMSSQL Server 管理工具用来还原脚本和查数据。.NET Framework 4.8Win10 以上一般自带Win7 或 Server 2012 需要手动装。IIS在「启用或关闭 Windows 功能」里勾选注意要勾上「ASP.NET 4.x」和「ISAPI 扩展」。Visual Studio可选只跑不改可以不装但改代码基本躲不开。一个常被忽略的点IIS 装完之后必须注册 ASP.NET否则访问.aspx会直接 404 或者提示处理程序未安装。命令行执行一次就行# 以管理员身份运行路径按实际 .NET 版本调整 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis.exe -i执行完在 IIS 里刷新一下应用程序池列表里能看到.NET CLR v4.0的池子说明注册成功。3.2 Web.config 里必须动的四处配置Web.config是这套系统的黑匣子跑不起来八成问题在这里。下面四段是必须检查的。configuration !-- 1. 连接串Data Source 写实例名AttachDbFilename 只在附加模式用 -- connectionStrings add nameConnStr connectionStringData Source.\SQLEXPRESS;Initial CatalogRestaurantDB; User IDsa;Password你的密码;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings system.web !-- 2. 编码中文菜名乱码基本都是这里没设 -- globalization requestEncodingutf-8 responseEncodingutf-8 fileEncodingutf-8 / !-- 3. 会话默认 InProcIIS 回收就会丢购物车 -- sessionState modeInProc timeout120 / !-- 4. 上传限制菜品图片上传超过 4MB 会直接报错 -- httpRuntime maxRequestLength10240 targetFramework4.8 / /system.web system.webServer !-- 集成模式下必须保留否则静态资源和路由会 404 -- modules runAllManagedModulesForAllRequeststrue / /system.webServer /configurationconnectionStrings里的Data Source最容易写错。用 SSMS 连的时候显示的是机器名\SQLEXPRESS但配置里写.\SQLEXPRESS或(local)\SQLEXPRESS更保险因为换机器部署时机器名会变。如果用 SQL 身份验证记得在 SSMS 里把服务器属性改成「SQL Server 和 Windows 身份验证模式」否则sa登录会被拒。sessionState的timeout120是给点餐场景调的。默认 20 分钟服务员把平板放下去忙别的回来购物车就空了用户会以为系统坏了。改成 120 分钟配合后面第 5 章讲的回收设置基本够用。3.3 数据库附加与初始化数据拿到DB/restaurant.sql之后两种方式都行。用 SSMS 打开脚本直接执行适合全新环境用「附加数据库」挂.mdf文件适合资源包里带了数据文件的情况。命令行还原的写法# 先建空库再用 sqlcmd 跑脚本适合没有 GUI 的服务器 sqlcmd -S .\SQLEXPRESS -U sa -P 你的密码 -Q CREATE DATABASE RestaurantDB sqlcmd -S .\SQLEXPRESS -U sa -P 你的密码 -d RestaurantDB -i D:\RestaurantSys\DB\restaurant.sql执行完之后验证三件事Dish表有初始菜品数据一般几十条覆盖凉菜、热菜、主食、饮料几个分类、TableInfo有桌台记录、SysUser有管理员账号。如果只有表结构没有数据说明脚本导的时候没勾「架构和数据」得回去重新导一份。初始管理员账号通常在脚本注释里写着常见是admin / 123456。第一次登录后立刻改密码别留默认值——餐馆的网络虽然是内网但点餐用的平板可能连的是公共 WiFi这个不是危言耸听。3.4 默认账号登录与权限验证链路登录逻辑不长但它是理解整套权限设计的入口。多数实现是「页面基类 Session 校验」的路子// BasePage.cs —— 所有需要登录的页面继承它 public class BasePage : System.Web.UI.Page { protected override void OnInit(EventArgs e) { base.OnInit(e); // 未登录直接踢回登录页带上原始地址便于登录后跳回 if (Session[UserId] null) { Response.Redirect(~/Login.aspx?returnUrl Server.UrlEncode(Request.RawUrl)); return; } // 角色校验页面可以声明自己允许哪些角色访问 var allowRoles this.GetType().GetCustomAttributes(typeof(RoleAttribute), true); if (allowRoles.Length 0) { var role (allowRoles[0] as RoleAttribute).Roles; var currentRole Session[RoleId].ToString(); if (!role.Contains(currentRole)) { Response.Redirect(~/NoPermission.aspx); } } } }OnInit里做拦截而不是Page_Load是因为Init阶段控件还没绑定数据早拦截能省掉一次无谓的数据库查询。returnUrl参数看着小实际体验差别很大收银员点开一个订单详情页被踢去登录登录完应该回到那个页面而不是回到首页重新找。角色校验用自定义特性RoleAttribute挂在页面类上写法是[Role(1,2)]表示收银员和管理员可访问。这种做法的好处是把权限声明和页面放在一起改的时候不用去翻配置文件。如果资源包里是集中式的菜单表权限逻辑类似只是改成查SysRoleMenu表再比对思路一样。4. 核心模块拆解一笔订单从点餐到结账的完整链路4.1 点餐车Session 暂存还是直接落库这是个必须提前定的事因为两种方案的后续代码完全不同。Session 方案服务员点菜时只往Session[Cart]里塞一个ListOrderDetail点「提交」才一次性写库。好处是响应快、不产生垃圾订单坏处是 Session 一丢购物车就没了而且多台平板之间不共享。直接落库方案每点一道菜就写一条OrderDetail状态为「待制作」。好处是数据实时、多端共享、后厨能立刻看到坏处是服务员反复加删会产生大量状态变更记录。这套系统我倾向于混合购物车放 Session但每 30 秒或每次加菜后异步落一次草稿单OrderInfo.Status 3挂单。这样既能快速响应又能在 Session 丢失时从草稿单恢复。实际写的时候购物车结构就是// CartItem 只存必要字段展示用的菜名从内存字典取 [Serializable] public class CartItem { public int DishId { get; set; } public string DishName { get; set; } public decimal Price { get; set; } public int Qty { get; set; } public string Remark { get; set; } } // 加入购物车同菜同备注则数量累加否则新增一行 public void AddToCart(int dishId, string dishName, decimal price, int qty, string remark) { var cart Session[Cart] as ListCartItem ?? new ListCartItem(); var exist cart.FirstOrDefault(c c.DishId dishId c.Remark remark); if (exist ! null) exist.Qty qty; // 加菜数量累加不新增行 else cart.Add(new CartItem { DishId dishId, DishName dishName, Price price, Qty qty, Remark remark }); Session[Cart] cart; }Remark参与去重判断是关键细节。同一道菜「微辣」和「不辣」在厨房是两份活不能合并成数量 2。这个判断条件写错后厨就会收到「水煮鱼 x2 微辣」而实际上有一份是不辣的出菜直接被退回。Session[Cart]存的是ListCartItem需要类标记[Serializable]否则切到 StateServer 或 SQLServer 会话模式时会报序列化失败。这个坑在第 5 章细说。4.2 提交订单一个事务里要写完的四张表提交订单是整套系统里最不能出错的一段。它要同时完成插入主单、批量插入明细、更新桌台状态、扣减库存如果有。任何一步失败都必须整体回滚否则会出现「桌台占了但没有订单」或者「订单有但桌台还是空闲」这类脏数据。// OrderBLL.SubmitOrder —— 开台下单的完整事务 public int SubmitOrder(int tableId, int userId, ListCartItem cart, out string msg) { msg string.Empty; // 前置校验桌台必须是空闲状态防止两个人同时开同一张台 var table tableDAL.GetById(tableId); if (table null || table.Status ! 0) { msg 该桌台已被占用请刷新后重试; return -1; } decimal total cart.Sum(c c.Price * c.Qty); string orderNo DateTime.Now.ToString(yyyyMMddHHmmss) tableId.ToString(D2); using (var conn new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 1. 插主单返回自增 OrderId int orderId orderDAL.InsertOrder(conn, tran, orderNo, tableId, userId, total); // 2. 明细逐条插入SqlBulkCopy 或循环均可量小用循环 foreach (var item in cart) { orderDAL.InsertDetail(conn, tran, orderId, item.DishId, item.Qty, item.Price, item.Remark); } // 3. 更新桌台状态为占用带 Status0 条件天然防并发 int affected tableDAL.UpdateStatus(conn, tran, tableId, 1); if (affected 0) { // 说明在校验之后被别人抢先开台了 throw new Exception(桌台状态已变更请重试); } tran.Commit(); return orderId; } catch (Exception ex) { tran.Rollback(); msg ex.Message; return -1; } } } }逐段说清楚。第一段的前置校验只是快速失败真正的并发保护在第 3 步UPDATE TableInfo SET Status 1 WHERE TableId id AND Status 0这条语句带上了Status 0条件数据库层面保证只有一个人能改成功返回受影响行数为 0 就说明被人抢先。这是最省事的乐观锁写法比在应用层加锁靠谱得多。第二步逐条插入明细菜品数量通常在 5 到 30 之间循环插入完全够用没必要上SqlBulkCopy。但循环内部必须复用同一个conn和tran如果InsertDetail自己又开了连接事务就断了回滚时明细会残留。第三步的返回值判断是这段代码里最容易被删掉的一行。很多人觉得「前面的校验已经查过状态了这里肯定成功」直接不判断。结果就是高峰期两个人同时开一张台两张订单都插入成功只有一张能结账另一张永远挂着。orderNo的生成规则里拼了tableId是为了避免同一秒内多桌下单导致编号重复。加了桌台号后理论碰撞概率降到可以忽略UNIQUE约束也不会误伤。4.3 桌台状态机空闲、占用、预订、待清台桌台的状态不是随便改的得有明确允许的迁移路径否则会出现「空着的桌显示已预订但查不到预订记录」这种玄学问题。这套系统的状态和允许的动作是当前状态允许动作目标状态0 空闲开台下单1 占用0 空闲预订2 预订2 预订客人到店开台1 占用2 预订取消预订0 空闲1 占用加菜 / 换桌1 占用TableId 变1 占用结账3 待清台3 待清台清台完成0 空闲换桌这个动作要单独说。它不是改一下OrderInfo.TableId就完事而是三件事一个事务旧桌台状态改回空闲或待清台、新桌台改占用、主单的TableId更新。漏掉任何一步旧桌台就会一直显示占用直到有人手动去数据库改。代码里通常封装成ChangeTable(int orderId, int newTableId)内部用UPDATE ... WHERE Status 0保证新桌台确实是空的。「待清台」这个中间状态看着多余实际很有用。客人走了但桌子还没收服务员不能马上把新客人安排过去有了这个状态收银员结完账点一下「已清台」状态才回到空闲。少了这一步桌台图上全是「空闲」但其实还没收拾迎宾会把客人领到脏桌子上。状态判断尽量只在一处做别在页面if里散落。我的习惯是在TableBLL里写死CanOccupy、CanCheckout这类方法页面只调方法不直接比对数字。这样以后加「预结」状态时改动范围可控。4.4 结账与日结金额计算与支付方式结账的核心不是算钱而是把「进行中」的订单变成「已结账」并留下不可改的流水。金额计算部分要考虑优惠、抹零、服务费这些规则各店不一样代码里通常留了配置项。// Checkout —— 结账时重算金额不信任前端传来的 total public bool Checkout(int orderId, decimal discount, decimal paid, int payType, out string msg) { msg string.Empty; var order orderDAL.GetById(orderId); if (order null || order.Status ! 0) { msg 订单状态异常; return false; } // 1. 以明细重新汇总防止前端篡改金额 decimal realTotal orderDAL.SumDetailAmount(orderId); // 2. 抹零规则常见的是保留到元或满 100 减 5 decimal payable Math.Round(realTotal - discount, 2); if (payable 0) payable 0; // 3. 实收不足直接拒绝多收记为找零不入账 if (paid payable) { msg 实收金额不足; return false; } using (var conn new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 4. 更新主单金额、支付方式、状态、结账时间 orderDAL.Settle(conn, tran, orderId, payable, payType); // 5. 写支付流水退款/对账都靠它 payDAL.Insert(conn, tran, orderId, payable, payType); // 6. 桌台进入待清台不直接置空闲 tableDAL.UpdateStatus(conn, tran, order.TableId, 3); tran.Commit(); return true; } } }第 1 步用明细重算是安全底线。前端传来的total只能是展示用的后端一律自己算一遍。中间加过菜、退过菜的单子前端金额和后端金额不一致是常态信前端就是给自己埋雷。第 3 步把「多收」当找零处理而不是记入营收这个判断标准要跟店里对清楚。有的店允许把多收的几毛钱留在账上做「长款」那就得单独记一个科目不能混在营业额里否则日结时现金抽屉对不上。日结报表其实是同一套数据的另一种聚合-- 按支付方式汇总当天的实际收款 SELECT PayType, COUNT(*) AS OrderCount, SUM(TotalAmount) AS TotalAmount FROM OrderInfo WHERE Status 1 AND OrderTime dayStart AND OrderTime dayEnd GROUP BY PayType;dayStart和dayEnd要注意是「当天 00:00:00 到次日 00:00:00」不是BETWEEN 00:00 和 23:59:59。后者会漏掉23:59:59.500这样的时间点SQL Server 的DATETIME精度是 3.33 毫秒跨零点那一秒的单子会从报表里消失。这种小数点级别的问题查起来特别费劲。5. 部署与联调排查六个最容易翻车的地方5.1 页面打开是空白或 500日志里什么都没有现象浏览器访问首页转圈最后 500IIS 日志里只有一行 500.0没有堆栈。原因Web.config里customErrors开着On真实异常被藏起来只返回一个友好页面。解决本地调试阶段把customErrors modeOff打开重新访问就能看到黄页详情。上线前记得改回RemoteOnly不然异常堆栈会暴露连接串片段。5.2 中文菜名全变成问号或方块现象数据库里存的中文在页面上显示成???但用 SSMS 查又是好的。原因Web.config的globalization没配或者页面文件本身被存成了 ANSI 编码。解决先加globalization requestEncodingutf-8再看.aspx文件头有没有meta charsetutf-8最后检查Dish表的字符串字段是不是NVARCHAR而不是VARCHAR。三个地方有一个不对就会出现乱码而且现象一样得逐个排除。5.3 服务员点了十几道菜提交时购物车是空的现象点餐页面操作正常点提交提示「购物车为空」刷新后确实没了。原因IIS 应用程序池的闲置超时默认 20 分钟池子一回收InProc模式的 Session 全部清空。加上店里服务员经常把平板放下十几分钟不动撞上回收太正常了。解决短期把应用程序池「闲置超时」改成 0不回收或 480 分钟中期把sessionState改成StateServer或SQLServer模式注意这两个模式要求 Session 里存的类必须标[Serializable]否则会报「无法序列化会话状态」。5.4 部署到 IIS 上访问报 404.17 或「无法识别的配置节」现象在 VS 里跑得好好的挂到 IIS 上直接 404.17 或者启动就报配置错误。原因应用程序池的 .NET CLR 版本是「无托管代码」或 v2.0而项目是 4.x或者池子用了「经典模式」但代码里用了system.webServer的配置节。解决应用程序池改成.NET CLR v4.0 「集成模式」再跑一次aspnet_regiis -i注册处理程序。如果代码是从老项目迁过来的、依赖经典模式那就把Web.config里的system.webServer段挪到system.web里别硬改池子。5.5 两张桌同时开台其中一张的订单查不到现象高峰期后厨收到两份单但前台的桌台图上只有一张桌是「占用」另一张显示空闲结账时找不到单。原因开台时的UPDATE没带AND Status 0条件两个请求都更新成功最后一次覆盖了前一次或者事务没包住明细插进去了、主单更新失败。解决检查UpdateStatus的 SQL 是否带条件判断检查SubmitOrder里所有 DAL 方法是否复用同一个conn和tran。已经产生的脏数据用一条 SQL 找出来查OrderInfo.Status 0但对应TableInfo.Status ! 1的记录手动补状态。5.6 后厨小票打不出来但页面显示「已下单」现象前台提交成功后厨没出小票服务员以为没下单又点了一遍结果出了双份菜。原因打印走的是浏览器window.print()弹窗被浏览器拦截或者小票机驱动没装、默认打印机选到了 A4。解决打印调用前先判断返回值失败就把订单标成「待打印」并在收银台亮红点提示别静默失败。更稳的做法是后厨单独跑一个轮询程序或者浏览器常驻页面按OrderDetail.Status 0主动拉取待出菜明细打印成功再回写状态这样打印机坏了也不会丢单。6. 二次开发给这套系统加一个扫码点餐入口改造点选扫码点餐因为它能把这套系统的模块边界暴露得最清楚。原系统的点餐入口是服务员登录后的页面加扫码意味着要开一个无需登录的入口同时不能让人随便改别人的单。我的做法是不动原有权限体系只在Order目录下加一个独立页面ScanOrder.aspx走桌台令牌校验。第一步给桌台表加一个令牌字段一桌一码换码即失效ALTER TABLE TableInfo ADD QrToken VARCHAR(64) NULL; -- 为每张台生成一个随机令牌打印成二维码贴在桌上 UPDATE TableInfo SET QrToken REPLACE(NEWID(), -, ) WHERE QrToken IS NULL;第二步页面用?t令牌识别桌台校验通过后按现有TableBLL的逻辑开台或追加。关键是把「谁下的单」标记清楚// 扫码进来的单子UserId 统一记 0表示顾客自助便于报表区分 protected void btnSubmit_Click(object sender, EventArgs e) { string token Request.QueryString[t]; var table tableBLL.GetByToken(token); if (table null) { ShowMsg(二维码无效请联系服务员); return; } // 已有进行中的订单就追加没有就新开台 int orderId orderBLL.GetRunningOrderId(table.TableId); if (orderId 0) orderBLL.AppendDishes(orderId, GetCartFromSession()); else orderBLL.SubmitOrder(table.TableId, 0, GetCartFromSession(), out _); Response.Redirect(ScanSuccess.aspx?t token); }AppendDishes是新加的方法内部只做两件事插明细、重算主单金额不动桌台状态。这样顾客扫码加菜和服务员手工加菜走的是同一条明细插入路径后厨不用区分来源。第三步加一个防刷限制。令牌虽然随机但二维码能被拍照传播所以我在ScanOrder.aspx里加了「同一令牌 5 分钟内只允许新开一次台」的判断超过就要求服务员在收银台确认。这个限制放在BLL里而不是页面里因为以后小程序端也要接规则得共用。三个改造点下来真正需要新写的代码不到 200 行剩下全是复用。这也是我判断一套餐饮系统架构成不成熟的标准能不能在不碰核心事务的前提下加一个新入口。如果加扫码要改SubmitOrder的签名说明参数设计一开始就把场景写死了。改造完之后一定要跑一遍完整的回归空桌扫码下单、占用桌扫码加菜、结账后令牌失效、两个人同时扫同一张桌。我吃过一次亏——加菜路径忘了重算主单金额导致扫码加的菜不计入账单顾客结账时少付了三十多块最后还是看日结报表的笔数对不上才发现的。从那以后我每次动订单相关代码都强制走一遍「下单 → 加菜 → 退菜 → 结账 → 对报表」这条链路一步不落。希望这些能帮到你。本文还有配套的精品资源点击获取