☰
WinForm+SQL Server外卖系统开发实战:选型、表结构与避坑指南
2026/10/10 6:25:00 网站建设 项目流程

简介:这是一套基于WinForm与SQL Server开发的外卖系统完整项目,面向正在做C#课程设计或毕业设计的开发者,也适合希望学习桌面端多角色业务系统的初学者。系统包含用户端、商家端、骑手端和管理员端四个角色:用户端支持商品浏览、跨店铺购物车、订单结算与个人订单管理,商家端支持商品管理、订单处理和骑手派单,骑手端负责订单查询与配送操作,管理员端则对各端人员进行统一管理。压缩包共392个文件,涵盖109个C#源码文件、51个资源与resx界面定义文件、126张界面截图,以及SQL数据库文件和工程配置文件,整体大小约17.63MB,解压后可直接运行并附带项目与数据库。目前已有556人学习下载,适合作为外卖类课程设计的参考模板,也可借此理解WinForm多窗体交互、多角色权限划分及SQL Server数据表设计。

1. 先把话说明白:WinForm+SQL Server外卖系统到底在解决什么

某小餐馆老板找上我时,店里点单还靠手写和微信群,中午高峰期菜品、分量、加急全在外面喊,结账对不上号。他要的其实很简单:一台收银机,几台看单屏,能点单、能催菜、能算账,数据别丢。让WinForm搭前台、SQL Server存底账,正是这套系统最稳的形态。它适合单店或小型连锁,一天几百单、局域网稳定、不想把营业数据放到第三方平台的那种场景。对开发者来说,技术栈老但胜在答案多、调试快,一个人从建库到交付,个把月就能跑通。这篇按“选型、表结构、界面、避坑”的顺序,把能落地的细节一次性说清楚。

2. 为什么这套组合至今还能打:选型逻辑与功能边界

WinForm+SQL Server常被嫌“老”,但外卖行业的真实需求,恰恰是追求稳定和快。C/S架构下收银端直接连数据库,少两层网络转发,高峰期连击也不会因为网页渲染卡顿;SQL Server的事务能力保证订单、明细、库存三条数据同生同死。很多人纠结为什么不换Web、不换MySQL,其实是没想清楚店铺里的物理环境:电脑配置低、打印机是并口转USB、网络最怕丢包。这套组合刚好都在Windows生态内,连驱动都好找。下面把架构、功能和部署方案逐个说透。

2.1 C/S架构在外卖店铺场景里的不可替代性

外卖收银场景有几个硬性要求:响应快、打印稳、断网能撑一阵、数据不出门。C/S架构里,点一下按钮直接发SQL到局域网内的数据库,往返通常不到10毫秒;而B/S方案CPU和内存都花在网页渲染上,老电脑一高负载就转圈。打印更是桌面程序的强项,WinForm直接调用本机打印服务,小票机的驱动不用特意适配Web打印接口。

SQL Server承担数据一致性角色时,比Access和SQLite更让人放心。订单创建要同时写主表、明细表和扣库存,任何一步失败必须整体回滚。SQL Server的事务隔离和锁机制在这里是天然优势。Express版免费,小门店一天几百单也够用,等数据量超过10GB再升开发版也不迟。

我见过一些团队硬改成前后端分离加远程数据库,最后卡在“门店断网就整套系统瘫痪”的难题上,反而不如桌面程序加局域网数据库省心。技术老不丢人,顾客在菜单页多转两圈才出餐才丢人。选型别跟风,先看营业场景。

2.2 最小功能集:点单、厨房、结账、报表

第一版不要做大平台。做外卖系统最容易犯的错,是把小程序点单、骑手端、会员营销全规划进去,结果交付周期翻倍。线下店铺真正需要的功能按优先级排:菜品管理,包括分类、价格、上下架;点单,选桌台或外卖单号,加菜品、改数量、整单备注;厨房单,下单即打印,按菜品合并数量;收银结账,按订单金额收款,记录实付和找零;日结报表,统计当天订单数、营业额、菜品销量和退菜数量。

这套功能闭环对应三个角色:收银员管点单和结账,厨房管出餐,老板管报表和菜品上下架。权限不用复杂,用户表一个角色字段就够了。明确不做外卖平台对接、不做会员储值、不做原料成本核算,这些放到二期再说。边界划清,开发效率和老板的预期都能稳住。

2.3 系统拓扑:一台服务器还是多台收银机

最常见的部署是一台Windows主机装SQL Server当数据服务器,另配一到两台装WinForm客户端的收银机。如果只有一个收银位,数据库和客户端装在同一台机器也能跑,但磁盘和内存压力会挤在一起,高峰期容易卡。条件允许时,尽量把数据库独立出来,哪怕是台二手商用机都行。

多台收银机时,所有客户端通过局域网IP连接数据库。数据库服务器兼任店主查账机,装个客户端只读模式。局域网的交换机百兆够用,但数据库服务器建议用固态盘,订单明细写入很快。

备份方案要在拓扑里一起定。数据库和客户端分开后,备份任务放在服务器上,用SQL Server代理每天半夜跑一次全量备份。别把备份文件存在C盘系统盘,万一系统崩溃,备份也没了。这是部署时最容易忽略的一环。

2.4 账号权限与菜品分类:两件一开始就要定的事

权限控制不需要做到按钮级,但角色要区分清楚。收银员能点单、改单、结账,不能改菜品价格,不能看日结。在代码里做一个权限判断方法,每次进入菜单前判断当前用户的角色,再决定按钮是否可见。把权限判断封装成一个方法,比在每个事件里写if(isAdmin)要清爽得多。

菜品分类也要做成表,而不是在界面上用下拉框写死。分类表存CategoryId,菜品表只存外键,这样销量统计按分类汇总非常方便。否则后期加一个分类,要改代码重新发布,等于是给自己挖坑。这两件事提前定好,后期改功能能省一半沟通成本。

3. 从空库开始搭订单模型:核心表、状态机与关键SQL

数据库是整个外卖系统的心跳。不要急着写程序,先把订单模型设计好。这个阶段定下的表结构,决定了后面写存储过程和界面的效率。重点有三块:订单主表和明细表怎么拆、状态字段怎么流转、下单写库怎么保证原子性。

3.1 订单主表和明细表的拆分原则

订单数据天然是一对多:一个订单对应多个菜品。拆成主表和明细表,主表存不变的金额和状态,明细表存每个菜的数量和金额,统计时按明细表汇总,改单时只动明细不动主表。明细表里只保留下单时的菜品名和单价,避免菜品后来改价导致历史订单金额跑偏。表结构如下:

CREATE TABLE dbo.Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL UNIQUE, TableNo VARCHAR(10) NOT NULL, Status TINYINT NOT NULL DEFAULT 1, TotalAmount DECIMAL(10,2) NOT NULL, Remark NVARCHAR(200) NULL, OpenedTime DATETIME NOT NULL DEFAULT GETDATE(), ClosedTime DATETIME NULL ); CREATE TABLE dbo.OrderItems ( ItemId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL REFERENCES dbo.Orders(OrderId), DishId INT NOT NULL, DishName NVARCHAR(50) NOT NULL, Price DECIMAL(10,2) NOT NULL, Quantity INT NOT NULL, Amount DECIMAL(10,2) NOT NULL ); CREATE INDEX IX_OrderItems_OrderId ON dbo.OrderItems(OrderId);

这里几个关键决定:金额一律用DECIMAL(10,2),不用FLOAT,避免浮点误差导致对账差几分钱;OrderNo用唯一索引,是给老板打印单号找订单用的;DishName和Price在明细里冗余保存,是为了防止菜品后来改价后历史订单统计失真。明细表不保存出餐状态,出餐状态放到主表状态里,或单独用一张制作状态表。如果小店要做“划菜”显示,可以另加一个ItemStatus字段,但别把它混在价格逻辑里。

3.2 用状态字段驱动整个订单流转

订单状态不要用字符串描述,用TINYINT数字,代码里定义枚举。状态机流转是这样:1已下单、2制作中、3已上齐、4待结账、5已结账;任何状态都可以到6已取消,但已结账订单不能取消,只能退款。用状态驱动的好处:不删数据,保留完整轨迹;界面上的按钮根据状态决定显示;日结报表能按状态过滤异常单。

CREATE TABLE dbo.OrderStatus ( Status TINYINT PRIMARY KEY, StatusName NVARCHAR(20) NOT NULL ); INSERT INTO dbo.OrderStatus(Status, StatusName) VALUES (1, N'已下单'), (2, N'制作中'), (3, N'已上齐'), (4, N'待结账'), (5, N'已结账'), (6, N'已取消');

为什么状态机比直接DELETE好?因为餐饮行业“取消订单”经常是口头说一声,老板回头要对账,看到缺失的单子会慌。保留已取消状态,并在报表里单列“取消金额”,比彻底抹掉数据更符合实际经营。状态变更时在代码里用一个UpdateOrderStatus方法统一处理,避免各处直接UPDATE导致状态跳乱。

3.3 写一个好用的下单存储过程

订单创建要同时写主表、明细表和扣库存,这三步必须在一个事务里。在WinForm里逐个SQL语句执行也能做,但事务控制不直观,而且每来一个客户端就重复一遍事务代码。常见做法是把下单逻辑收进存储过程,客户端只传参数,数据库保证原子性。

CREATE TYPE dbo.OrderItemType AS TABLE ( DishId INT NOT NULL, DishName NVARCHAR(50) NOT NULL, Price DECIMAL(10,2) NOT NULL, Quantity INT NOT NULL ); GO CREATE PROCEDURE dbo.usp_CreateOrder @TableNo NVARCHAR(20), @Items dbo.OrderItemType READONLY, @Remark NVARCHAR(200) = '', @OrderId INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; DECLARE @TotalAmount DECIMAL(10,2); SELECT @TotalAmount = SUM(Price * Quantity) FROM @Items; INSERT INTO dbo.Orders(OrderNo, TableNo, Status, TotalAmount, Remark, OpenedTime) VALUES(CONVERT(VARCHAR(8), GETDATE(), 112) + RIGHT('000' + CAST(ISNULL(MAX(OrderId),0)+1 AS VARCHAR(4)), 4), @TableNo, 1, @TotalAmount, @Remark, GETDATE()); SET @OrderId = SCOPE_IDENTITY(); INSERT INTO dbo.OrderItems(OrderId, DishId, DishName, Price, Quantity, Amount) SELECT @OrderId, DishId, DishName, Price, Quantity, Price * Quantity FROM @Items; UPDATE dbo.Dish SET Stock = Stock - i.Quantity FROM @Items i WHERE dbo.Dish.DishId = i.DishId AND dbo.Dish.Stock >= i.Quantity; IF @@ROWCOUNT <> (SELECT COUNT(*) FROM @Items) BEGIN RAISERROR(N'菜品库存不足', 16, 1); ROLLBACK TRANSACTION; RETURN; END COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH END

这个存储过程里的OrderNo生成方式比较简陋,生产环境建议用一个独立编号表或序列生成,避免高并发下主键冲突。关键是事务逻辑:插入主表和明细都在同一事务里;库存扣减用Stock >= i.Quantity条件判断,如果某一行的库存不够,更新行数为0,再和传入行数比对,任何一样不足就整体回滚。@Items是表值参数,客户端传一个DataTable进来,减少数据库往返。RAISERROR抛错后CATCH块会把未提交的事务完全回滚,不会出现只写了一半订单的情况。

3.4 查询当天营业流水:参数化与索引

日结报表最常见的SQL是按当天汇总。这里有个容易踩坑的地方:很多人习惯把OpenedTime转成字符串再和今天比较,比如WHERE CONVERT(VARCHAR(10), OpenedTime, 23) = '2025-01-01',这样会让列上的索引失效,表变大后查询越来越慢。正确写法是用一个半开区间:

DECLARE @Today DATETIME = '2025-01-01'; SELECT COUNT(*) AS OrderCount, SUM(TotalAmount) AS TotalAmount FROM dbo.Orders WHERE Status <> 6 AND OpenedTime >= @Today AND OpenedTime < DATEADD(DAY, 1, @Today);

对应建立索引:

CREATE INDEX IX_Orders_OpenedTime_Status ON dbo.Orders(OpenedTime) INCLUDE(Status, TotalAmount);

这个查询返回当天非取消订单的总额和单数。OpenedTime >= @Today AND OpenedTime < DATEADD(DAY, 1, @Today)是标准写法,既能命中索引,又不会漏掉当天最后一秒的订单。INCLUDE列让覆盖查询不用再回表取数据,日结报表几百行订单时毫秒级出结果。需要按菜品统计销量时,再从OrderItems聚合,条件同样用Orders的OpenedTime范围。

4. 在WinForms里落地业务:界面绑定、事务与分页加载

数据库模型定好后,WinForm端的工作大头在三个地方:列表怎么绑定、下单操作怎么保证事务、打印什么时候触发。很多新手喜欢把所有业务都塞进窗体代码,结果改一个字段要翻几百行。我这里按常用模式拆开讲。

4.1 DataGridView绑定DataTable的取舍

DataGridView是WinForm里最常用的表格控件,直接绑定一个DataTable到DataSource,适合展示菜品列表和订单明细。但绑定也有个坑:如果允许用户直接在单元格里编辑,再写CellEndEdit事件写库,很容易出现界面改了数据库没改的错位。

我的习惯是DataGridView只做展示,编辑走单独的新增或修改窗体。展示端代码就这样:

private void LoadDishList(string keyword) { DataTable dt = new DataTable(); string sql = "SELECT DishId, DishName, Price, Stock, CategoryName " + "FROM dbo.Dish d JOIN dbo.DishCategory c ON d.CategoryId = c.CategoryId " + "WHERE d.IsActive = 1 AND (@keyword = '' OR d.DishName LIKE '%' + @keyword + '%')"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.Add("@keyword", SqlDbType.NVarChar, 50).Value = keyword; SqlDataAdapter da = new SqlDataAdapter(cmd); da.Fill(dt); } dataGridViewDish.DataSource = dt; dataGridViewDish.Columns["DishId"].Visible = false; }

重点说明:SQL里用@keyword参数化,不拼接用户输入;LIKE '%' + @keyword + '%'中如果传入空字符串,就相当于全表。DataGridView的AutoGenerateColumns默认生成所有列,把主键列设为不可见,避免误改。这个模式适合几百行的菜品列表,数据量更大时再考虑分页加载。

4.2 下单按钮背后的完整事务逻辑

下单按钮是系统最高频的操作。不要在后台代码里拼SQL,而是调用之前定义的usp_CreateOrder。C#端这样写:

private void btnSubmitOrder_Click(object sender, EventArgs e) { DataTable itemTable = new DataTable(); itemTable.Columns.Add("DishId", typeof(int)); itemTable.Columns.Add("DishName", typeof(string)); itemTable.Columns.Add("Price", typeof(decimal)); itemTable.Columns.Add("Quantity", typeof(int)); foreach (DataGridViewRow row in dataGridViewCart.Rows) { DataRow dr = itemTable.NewRow(); dr["DishId"] = Convert.ToInt32(row.Cells["DishId"].Value); dr["DishName"] = row.Cells["DishName"].Value.ToString(); dr["Price"] = Convert.ToDecimal(row.Cells["Price"].Value); dr["Quantity"] = Convert.ToInt32(row.Cells["Quantity"].Value); itemTable.Rows.Add(dr); } using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand("dbo.usp_CreateOrder", conn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@TableNo", SqlDbType.NVarChar, 20).Value = txtTableNo.Text; cmd.Parameters.Add("@Remark", SqlDbType.NVarChar, 200).Value = txtRemark.Text; cmd.Parameters.Add("@Items", SqlDbType.Structured).Value = itemTable; cmd.Parameters.Add("@OrderId", SqlDbType.Int).Direction = ParameterDirection.Output; conn.Open(); cmd.ExecuteNonQuery(); int newOrderId = (int)cmd.Parameters["@OrderId"].Value; MessageBox.Show($"下单成功,单号:{newOrderId}"); RefreshCart(); PrintKitchenOrder(newOrderId); } }

C#这边重点是@Items参数的类型是SqlDbType.Structured,对应SQL Server的表值参数,客户端直接传DataTable。数据库存储过程里用@Items READONLY接收。@OrderId设成输出参数,下单后立刻拿到新订单号,然后去打印厨房单。整个调用被using包住,即使抛异常连接也会关闭,不会占住连接池。

注意参数不要用AddWithValue。AddWithValue会把NVarChar默认按NVARCHAR(4000)推断,大字段时容易导致索引失效,而且小数精度也可能丢失。显式指定SqlDbType和长度,行为更可控。

4.3 菜品管理和库存扣减

菜品管理的核心是防止改了菜名,历史订单跟着变。保存菜品时,菜品主表的DishName跟着更新,但OrderItems里的DishName是历史快照,不能动。库存扣减已经在下单存储过程里做了,那么退菜时的库存回补呢?用另一个存储过程usp_CancelOrderItem,反向加库存并更新明细状态。这里也要事务:

CREATE PROCEDURE dbo.usp_CancelOrderItem @ItemId INT, @Reason NVARCHAR(100) AS BEGIN BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.OrderItems SET Status = 6 WHERE ItemId = @ItemId; UPDATE dbo.Dish SET Stock = Stock + (SELECT Quantity FROM dbo.OrderItems WHERE ItemId = @ItemId) WHERE DishId = (SELECT DishId FROM dbo.OrderItems WHERE ItemId = @ItemId); COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH END

这里的库存回补逻辑在UPDATE dbo.Dish里直接查明细表的数量,不需要先取到客户端再传回来。整单退菜和单菜退菜都用这套,只是@ItemId来自不同入口。界面端调用这个存储过程时,传的是选中明细行的ItemId,不是菜品ID,因为退菜退的是某个订单里的某个菜。

4.4 打印小票:用快照还是实时查询

打印要分清两种单子:厨房单下单时立刻打,顾客单结账时才打。这两种单子的内容都应该来自下单那一刻的数据快照,而不是打印时刻再重新查库。为什么用快照?因为顾客可能中途加菜、退菜,厨房单如果实时查库,打出来的是改过的版本,厨师会漏做。

实现上,WinForm端在调用usp_CreateOrder拿到OrderId后,立即查一次OrderItems,把数据存在一个List里,传给打印方法。打印格式不复杂,用PrintDocument绘制文本就行:

private void PrintKitchenOrder(int orderId) { List<OrderLine> lines = GetOrderSnapshot(orderId); PrintDocument doc = new PrintDocument(); doc.PrintPage += (sender, e) => { float y = e.MarginBounds.Top; foreach (var line in lines) { e.Graphics.DrawString($"{line.DishName} x{line.Quantity}", PrintFont, Brushes.Black, e.MarginBounds.Left, y); y += 20; } }; doc.Print(); }

GetOrderSnapshot在内存里保存了一份明细,打印过程里即使数据库被改成已结账,厨房单也保持原始内容。如果打印量大,最好把打印任务放到后台线程,避免界面在打印时假死。小票打印机的DPI很怪,建议先在测试纸上试打,调整好字体和行距再上线。

5. 上线后最容易翻车的五个坑:连接池、并发、路径与备份

这一章建议你收藏。前面说得再好,不如上线后踩一遍。下面五条都是外卖系统里出现频率极高的实际问题,按“现象、原因、解决”列清楚。

5.1 连接字符串和连接池耗尽

现象:客户端运行几小时后,某次点单突然弹“超时时间已到,但操作尚未完成”,重启程序又好了,过一阵再次出现。

原因:每次数据库操作都new了SqlConnection,但连接没有关闭。连接池默认上限是100,长时间运行泄漏的连接把池占满,新请求只能等待超时。排查时打开SQL Server的活动监视器,能看到大量Sleep状态的进程数不断上涨。

解决:确保所有SqlConnection都放在using块里,或者至少finally里调用conn.Close()。连接字符串里可以设置Max Pool Size=200,但如果代码泄漏,调高池大小只是推迟问题。建议在出错时记录连接未关闭的前端调用栈,一周内就能定位是哪个窗体漏了Close。

"Server=192.168.1.100;Database=WaimaiDB;User Id=sa;Password=xxx;Max Pool Size=200;Connect Timeout=5"

5.2 超卖:点了两份鸡腿饭库存变负数

现象:下午查库存,发现鸡腿饭库存显示-2,但店里明明没做这么多。

原因:客户端先查库存数量,判断足够后再执行下单,两个操作之间没有锁。两台收银机同时点同一道菜,都通过了库存足够的检查,然后各自扣减,库存就扣穿了。

解决:把库存判断和扣减放到同一个存储过程事务里,用条件更新形式UPDATE dbo.Dish SET Stock = Stock - @qty WHERE DishId=@dishId AND Stock >= @qty,受影响行数为0说明不够,整体回滚。参考第3章的usp_CreateOrder,在事务里执行,不依赖客户端预先检查。

5.3 局域网SQL Server实例名连不上

现象:客户端在收银机上打开,报“在建立与服务器的连接时出错。在连接到 SQL Server 时,默认设置 SQL Server 不允许进行远程连接”。

原因:默认安装时SQL Server只启用了Shared Memory协议,TCP/IP没开;或者Windows防火墙挡了1433端口。不少数据库装在命名实例上,连接字符串写.\SQLEXPRESS在本机有效,换到其他机器就不认。

解决:打开SQL Server配置管理器,启用SQL Server网络配置里的TCP/IP协议,重启SQL Server服务。如果用了命名实例和动态端口,在连接字符串里指定Server=192.168.1.100,1433\实例名。然后在服务器上放行防火墙入站规则“SQL Server”或直接放行1433端口。最后在客户端测试连接,能通再试程序。

5.4 图片路径写本地绝对路径,换机器就崩

现象:菜品图片在开发机器上显示正常,部署到收银机后全部破图。

原因:数据库里存的是开发机路径,收银机上根本没有这个目录。图片是二进制文件,不该在数据库里接触绝对路径。

解决:把图片文件统一复制到客户端程序目录下的images文件夹,数据库中只存文件名,界面显示时用Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images", fileName)动态拼接路径。换机器时整个程序目录拷贝过去,文件名不变就不会崩。如果要在数据库服务器和客户端之间共享,用共享路径接图片服务,但会引入权限问题,小店不建议上。

5.5 备份只靠手工,删了单子找不回

现象:老板误删了一笔已结账的订单,到处翻数据库想恢复,发现上次手动备份是两周前,那笔数据早没了。

原因:开发时没有建立自动备份机制,运维依赖人工维护,一忙就忘了。数据库文件只有一份,任何误操作都不可逆。

解决:在数据库服务器上新增SQL Server Agent作业,每天凌晨执行一次数据库全量备份,并保留最近7天。也可以用简单方式:应用启动时检查今天的备份文件是否存在,不存在就立刻执行BACKUP DATABASE语句:

BACK UP DATABASE WaimaiDB TO DISK = N'D:\Backup\WaimaiDB_20250101.bak' WITH FORMAT, INIT, COMPRESSION;

这样即使没装Agent也能兜底。备份文件不要放C盘,最好放在另一块物理磁盘上,防止单块磁盘坏了连备份一起丢。

6. 让系统更耐用的几个习惯:离线兜底、自动备份与启动检查

前面把主体功能和坑都讲完了,这章补三个交付时都会加上的习惯。它们不增加多少代码量,但能明显减少上线后的夜间电话。

6.1 数据库短时宕机时的收银降级

外卖店最怕中午高峰期数据库服务器突然重启,几台收银机同时瘫痪。一个轻量做法是客户端在连不上数据库时进入降级模式:把新订单先写到本地XML或SQLite文件里,等数据库恢复后再批量导入。操作流程是:检测连接失败时,在界面顶部显示离线模式,订单暂存本地,恢复后点击同步把本地订单通过usp_CreateOrder逐单写入。这个方案有订单号冲突的风险,离线单的编号要加本地前缀,同步后重新分配正式单号。如果订单量不大,这个功能开发只要两天,换来的安全感很值。

6.2 启动自检清单

客户端每次启动时不要急着进主界面,先跑一遍自检:连不上数据库时给出明确提示而不是报错;检查本地磁盘剩余空间,如果低于500MB就警告;检查今天的备份文件是否在服务器上生成。自检逻辑写成独立方法,让不懂技术的老板也能看懂哪里出了错。

private bool RunStartupChecks() { string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "checkup.log"); try { using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); File.AppendAllText(logPath, $"{DateTime.Now}: DB OK\n"); } return true; } catch (Exception ex) { MessageBox.Show($"数据库连接失败:{ex.Message}"); return false; } }

代码里把自检结果追加到日志文件,以后排查问题时可以先看checkup.log,再决定要不要远程过去。这个习惯帮我筛掉过很多“程序打不开”的低级故障。

6.3 把异常和操作日志留到本地数据库

在界面上用Try-Catch吞异常是最容易埋雷的做法。我会在全局异常处理器里把错误堆栈写到本地日志表,同时记录关键操作:谁在什么时候删了什么菜、退了多少货。操作日志表很小,一年也占不了多少空间,但对账和扯皮时就是后悔药。这一条不花多少工时,但很多开发者嫌麻烦不做,最后出了事只能靠翻库恢复。

这套系统的核心其实不是技术,而是把订单、库存、钱的状态管清楚。早点把状态机、事务、备份和日志做好,比后期补功能省太多事。我自己的习惯是每次交付前都会再问一句:如果服务员点了两下“下单”按钮,会不会重复扣钱?如果SQL Server凌晨自动重启,明天开门能不能正常用?这几个问题想通了,系统才敢交给老板。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询