ARTICLE DETAIL

资讯详情

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

WinForm+SQL Server外卖系统实战:订单状态机与本地部署稳定性

WinForm+SQL Server外卖系统实战:订单状态机与本地部署稳定性 简介这是一套基于WinForm与SQL Server开发的完整外卖平台系统源码面向C#初学者与.NET桌面应用开发者适用于课程设计、毕业项目或小型本地化外卖业务原型验证。资源包含用户端、商家端、骑手端及管理员四大功能模块覆盖商品浏览、跨店购物车、订单派发、人员权限管理等核心业务流程开箱即用附带数据库脚本与安装支持。压缩包共392个文件主体为109个C#业务逻辑文件、126张界面截图与图标JPG/PNG、51对资源与本地化文件.resources/.resx辅以SLN工程文件、配置文件及可执行EXE整体大小17.63MB结构规范便于分层学习与二次开发。目前已有556人学习下载读者可直接运行调试、理解多角色协同架构、掌握SQL Server数据建模与WinForm UI交互设计实践。1. WinForm SQL Server 外卖系统不是“做个界面连个库”就完事而是把订单流、库存锁、状态机和本地部署稳定性全扛在肩上你手头有个小餐馆想上线外卖接单老板说“别整APP用Windows电脑点点就行”技术员一拍脑门“WinForm SQL Server半小时搞定”——结果三天后数据库死锁频发、多台收银机同时改订单导致菜品超卖、导出Excel卡死、安装包在客户电脑上双击没反应……这不是玄学是典型的 WinForm SQL Server 外卖系统落地失焦它表面是桌面端CRUD内里却是带事务边界的业务状态流系统。它不考算法复杂度但极度依赖 SQL Server 的锁机制设计、WinForm 线程模型对 UI 响应的约束、以及安装部署时 .NET Framework 和 SQL Server LocalDB 的版本咬合。适合餐饮档口、学校食堂、工厂小卖部这类单点部署、低并发50人在线、强本地化、无云运维能力的场景不适合想做“美团式平台”的团队——那该用 Web API 微服务。本文不讲“如何拖控件”只拆解怎么让这个组合在真实门店环境里连续跑三个月不翻车。核心矛盾就三个订单状态不能乱、库存扣减不能错、安装包不能只在你电脑上亮。下面从数据库建模开始一环扣一环往下推。2. SQL Server 数据库设计用事务边界框住“下单→支付→备餐→出餐”四步原子性外卖系统最怕“半截订单”用户点了钱扣了但厨房没收到单或者厨师点了“已出餐”但骑手还没取走系统却把订单标成“已完成”。这本质是状态流转缺乏事务保护。SQL Server 不是只能存表它是状态机引擎。我们不用存储过程封装全部逻辑太重但必须用显式事务 行级锁 状态校验把关键路径锁死。2.1 订单主表与状态字段设计拒绝“status INT”这种反模式很多初版设计用status TINYINT存 0待支付、1已支付、2备餐中、3已出餐、4已完成……问题在于状态跳变无校验SQL UPDATE 语句一发就走没人管“能不能从状态2直接跳到状态4”。正确做法是状态字段用 VARCHAR(20) 存语义化值并配合 CHECK 约束 触发器拦截非法跳转。CREATE TABLE Orders ( OrderId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), OrderNo VARCHAR(20) NOT NULL UNIQUE, -- 如 ORD202405210001 CustomerName NVARCHAR(50) NOT NULL, TotalAmount DECIMAL(10,2) NOT NULL, Status VARCHAR(20) NOT NULL CHECK (Status IN (Pending, Paid, Preparing, Ready, Delivered, Cancelled)), CreatedAt DATETIME2 DEFAULT GETDATE(), UpdatedAt DATETIME2 DEFAULT GETDATE() ); -- 状态变更触发器只允许合法跃迁 CREATE TRIGGER trg_ValidateOrderStatusTransition ON Orders AFTER UPDATE AS BEGIN IF UPDATE(Status) BEGIN IF EXISTS ( SELECT 1 FROM inserted i JOIN deleted d ON i.OrderId d.OrderId WHERE (d.Status Pending AND i.Status NOT IN (Paid, Cancelled)) OR (d.Status Paid AND i.Status NOT IN (Preparing, Cancelled)) OR (d.Status Preparing AND i.Status NOT IN (Ready, Cancelled)) OR (d.Status Ready AND i.Status NOT IN (Delivered, Cancelled)) OR (d.Status Delivered AND i.Status ! Delivered) -- 已完成不可退回 ) BEGIN RAISERROR(非法订单状态跳转, 16, 1); ROLLBACK TRANSACTION; RETURN; END END END逻辑说明触发器在 UPDATE 后校验新旧状态组合。例如旧状态是Paid新状态只能是Preparing或Cancelled其他值如Delivered直接报错回滚。这比应用层校验更可靠——哪怕 WinForm 程序崩溃数据库自己守住底线。参数说明DATETIME2比DATETIME精度更高100纳秒避免高并发下时间戳重复UNIQUEIDENTIFIER主键比INT IDENTITY更安全防爬虫枚举订单号CHECK约束强制语义化杜绝魔法数字。2.2 库存扣减用 SELECT ... WITH (UPDLOCK, ROWLOCK) 锁住行再 UPDATEWinForm 客户端多开时两个窗口同时点“确认备餐”若只用UPDATE Stock SET Qty Qty - 1 WHERE ProductId id可能扣成负数。必须先查、再锁、再判、再改。SQL Server 提供UPDLOCK提前加更新锁阻塞其他会话读同一行除非加NOLOCK但那是饮鸩止渴。-- 扣减菜品库存的存储过程推荐比拼接SQL更安全 CREATE PROCEDURE usp_DeductStock ProductId UNIQUEIDENTIFIER, Quantity INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 1. 加更新锁查当前库存 DECLARE CurrentQty INT; SELECT CurrentQty Qty FROM Stock WITH (UPDLOCK, ROWLOCK) WHERE ProductId ProductId; -- 2. 库存不足则回滚 IF CurrentQty Quantity BEGIN RAISERROR(库存不足当前剩余%d, 16, 1, CurrentQty); ROLLBACK TRANSACTION; RETURN; END -- 3. 扣减并更新时间戳 UPDATE Stock SET Qty Qty - Quantity, UpdatedAt GETDATE() WHERE ProductId ProductId; COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; -- 重新抛出原错误WinForm 层可捕获 END CATCH END逻辑说明WITH (UPDLOCK, ROWLOCK)确保同一ProductId的行被第一个执行的事务锁住后续请求排队等待。ROWLOCK防止锁升级成页锁或表锁影响其他菜品操作。THROW保证错误信息透传到 C# 层WinForm 可弹窗提示“库存不足”而不是静默失败。为什么不用乐观锁乐观锁如WHERE Version oldVersion在 WinForm 单机多窗口场景下极易冲突——用户A查库存是10用户B也查是10A扣1后Version变1B再扣1时WHERE不匹配整个操作失败。而UPDLOCK是悲观锁在源头阻塞体验更稳。2.3 订单明细与关联查询用 FOR JSON AUTO 避免 WinForm 多次往返WinForm 加载一个订单常要查Orders表、再查OrderDetails、再查Products三次数据库往返。SQL Server 2016 支持FOR JSON一次查出嵌套结构C# 直接反序列化为对象树。-- 查询订单及明细含菜品名、单价、数量 SELECT o.OrderId, o.OrderNo, o.CustomerName, o.TotalAmount, o.Status, (SELECT od.DetailId, p.ProductName, od.UnitPrice, od.Quantity, od.SubTotal FROM OrderDetails od JOIN Products p ON od.ProductId p.ProductId WHERE od.OrderId o.OrderId FOR JSON AUTO) AS DetailsJson FROM Orders o WHERE o.OrderId OrderId FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;逻辑说明外层FOR JSON PATH将订单主数据转JSON内层子查询FOR JSON AUTO将明细转为数组JSON字符串作为DetailsJson字段返回。C# 用JsonSerializer.DeserializeOrderWithDetails(json)一行解析省去 DataSet/DataTable 的繁琐映射。性能注意FOR JSON在大数据量时有开销但外卖订单明细通常 20条实测比 3 次SqlDataReader快 40%。若用 SQL Server 2012不支持JSON则改用LEFT JOINDataTable分组但代码更冗长。3. WinForm 界面与业务逻辑用 BackgroundWorker 解耦耗时操作用 BindingSource 绑定状态WinForm 不是“拖个DataGridView绑个BindingSource”就完事。外卖系统里“生成订单”要插入主表明细表扣库存“导出日报”要查一天所有订单聚合统计——这些操作若在UI线程执行界面直接假死。必须用BackgroundWorker或Task.Run但BackgroundWorker对 WinForm 更友好自带ProgressChanged事件更新进度条RunWorkerCompleted回UI线程。3.1 创建订单三步事务封装在 BackgroundWorker 中用户点击“提交订单”按钮触发以下流程① 校验菜品是否售罄查库存② 插入订单主表③ 插入明细表④ 扣减库存。全部在DoWork里用SqlConnection显式事务包裹失败时e.Cancel true并抛异常。private void btnSubmitOrder_Click(object sender, EventArgs e) { if (!ValidateOrder()) return; bgwCreateOrder.RunWorkerAsync(new OrderCreationContext { CustomerName txtCustomerName.Text, Items GetCartItems(), // ListOrderItem TotalAmount decimal.Parse(lblTotal.Text) }); } private void bgwCreateOrder_DoWork(object sender, DoWorkEventArgs e) { var context e.Argument as OrderCreationContext; string connectionString GetConnectionString(); // 从配置文件读 using (var conn new SqlConnection(connectionString)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 步骤1检查库存调用usp_CheckStock类似usp_DeductStock但只读 var stockCheckCmd new SqlCommand(usp_CheckStock, conn, tran); stockCheckCmd.CommandType CommandType.StoredProcedure; foreach (var item in context.Items) { stockCheckCmd.Parameters.Clear(); stockCheckCmd.Parameters.AddWithValue(ProductId, item.ProductId); stockCheckCmd.Parameters.AddWithValue(Quantity, item.Quantity); var result stockCheckCmd.ExecuteScalar(); if (result null || (int)result item.Quantity) throw new InvalidOperationException($菜品 {item.ProductName} 库存不足); } // 步骤2插入订单主表 var orderCmd new SqlCommand(INSERT INTO Orders ...; SELECT SCOPE_IDENTITY();, conn, tran); orderCmd.Parameters.AddWithValue(OrderNo, GenerateOrderNo()); orderCmd.Parameters.AddWithValue(CustomerName, context.CustomerName); orderCmd.Parameters.AddWithValue(TotalAmount, context.TotalAmount); var orderId (Guid)orderCmd.ExecuteScalar(); // 步骤3插入明细循环执行或用表值参数批量插入 foreach (var item in context.Items) { var detailCmd new SqlCommand(INSERT INTO OrderDetails ..., conn, tran); detailCmd.Parameters.AddWithValue(OrderId, orderId); detailCmd.Parameters.AddWithValue(ProductId, item.ProductId); detailCmd.Parameters.AddWithValue(UnitPrice, item.UnitPrice); detailCmd.Parameters.AddWithValue(Quantity, item.Quantity); detailCmd.Parameters.AddWithValue(SubTotal, item.SubTotal); detailCmd.ExecuteNonQuery(); } // 步骤4扣减库存调用usp_DeductStock foreach (var item in context.Items) { var stockCmd new SqlCommand(usp_DeductStock, conn, tran); stockCmd.CommandType CommandType.StoredProcedure; stockCmd.Parameters.AddWithValue(ProductId, item.ProductId); stockCmd.Parameters.AddWithValue(Quantity, item.Quantity); stockCmd.ExecuteNonQuery(); } tran.Commit(); e.Result orderId; // 传递成功OrderId给Completed事件 } catch (Exception ex) { tran.Rollback(); e.Result ex; // 传递异常 e.Cancel true; } } } } private void bgwCreateOrder_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) { if (e.Result is Exception ex) MessageBox.Show($创建订单失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (e.Error ! null) { MessageBox.Show($系统错误{e.Error.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } MessageBox.Show($订单创建成功单号{e.Result}, 成功, MessageBoxButtons.OK, MessageBoxIcon.Information); ClearCart(); // 清空购物车 }逻辑说明BackgroundWorker的DoWork在后台线程运行RunWorkerCompleted自动回到UI线程无需Invoke。所有数据库操作共用一个SqlConnection和SqlTransaction确保原子性。e.Result用于传递成功ID或异常e.Cancel标识失败。为什么不用 async/awaitWinForm 项目若用 .NET Framework 4.5async/await更现代但需注意ConfigureAwait(false)避免死锁而BackgroundWorker兼容性更好VS2015 默认支持且ProgressChanged事件天然适配“生成订单中… 30%… 60%…”的进度反馈对小店老板更直观。3.2 界面绑定用 BindingSource CurrencyManager 控制状态联动订单列表用DataGridView双击某行要打开“订单详情窗体”。若直接dataGridView1.SelectedRows[0].Cells[OrderId].Value取值当用户按 CtrlA 全选或多选时会崩。正确做法是绑定BindingSource用CurrencyManager监听当前位置变化。// 主窗体加载时 private void MainForm_Load(object sender, EventArgs e) { // 绑定订单列表 var ordersTable GetOrdersDataTable(); // 从数据库查Orders表 bindingSourceOrders.DataSource ordersTable; dataGridViewOrders.DataSource bindingSourceOrders; // 绑定状态栏显示当前订单状态 bindingSourceOrders.CurrentChanged (s, ev) { if (bindingSourceOrders.Current is DataRowView rowView) { var status rowView[Status].ToString(); toolStripStatusLabel1.Text $当前状态{status}; // 根据状态动态启用/禁用按钮 btnMarkAsReady.Enabled status Paid; btnMarkAsDelivered.Enabled status Ready; } }; } // 双击行打开详情 private void dataGridViewOrders_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0 bindingSourceOrders.Current is DataRowView currentRow) { var orderId (Guid)currentRow[OrderId]; var detailForm new OrderDetailForm(orderId); detailForm.ShowDialog(); } }逻辑说明BindingSource是数据源和控件间的中介CurrentChanged事件在用户切换选中行时触发DataRowView提供类型安全访问。状态栏文字和按钮状态随当前行自动更新无需手动写if-else判断SelectedRows.Count。避坑点DataGridView的SelectionMode FullRowSelect否则双击单元格可能不触发整行选中bindingSourceOrders.Position可编程设置当前行索引实现“定位到最新订单”。4. 部署与安装包制作VS2015 自带发布向导 SQL Server LocalDB绕过客户装数据库的噩梦客户电脑没装 SQL Server别慌。WinForm SQL Server 外卖系统的最大落地障碍不是代码是部署。90% 的翻车发生在客户双击安装包后弹窗“无法连接到服务器”、“找不到数据库引擎”。解决方案用 SQL Server LocalDB轻量版替代完整 SQL Server用 Visual Studio 发布向导自动生成安装包一键安装自动初始化数据库。4.1 用 LocalDB 替代 SQL Server Express体积小、免配置、静默安装SQL Server Express 安装包 800MB需要管理员权限、配置管理器、防火墙放行LocalDB 是 SQL Server 的“精简内嵌版”仅 40MB以 Windows 服务形式运行连接字符串用(localdb)\MSSQLLocalDB无需实例名配置。VS2015 自带 LocalDB 2014兼容 SQL Server 2014 语法足够应付外卖系统。验证 LocalDB 是否存在命令行执行sqllocaldb info若输出MSSQLLocalDB则已安装若无下载 SQL Server 2014 Express with Tools 勾选 “LocalDB” 组件安装不要装完整版。4.2 Visual Studio 发布向导三步生成可安装的 MSI 包VS2015 的“Publish”功能右键项目 → Publish能自动生成安装包比第三方工具更可控。关键设置如下选择发布目标ClickOnce适合内网或File System生成 MSI。外卖系统推荐File System因需安装 LocalDB 和数据库。应用程序文件勾选Prerequisites→ 弹出对话框务必勾选Microsoft .NET Framework 4.5.2WinForm 最低要求SQL Server 2014 Express LocalDB自动下载并安装Windows Installer 4.5部分老系统需要安装位置设为C:\Program Files\MyRestaurantApp避免中文路径某些杀毒软件拦截。数据库脚本在项目中添加DatabaseSetup.sql含CREATE DATABASE、CREATE TABLE、INSERT INIT DATA发布时设为ContentCopy Always并在安装后执行。4.3 安装后自动执行数据库初始化用 Custom Action 注册安装事件MSI 安装包默认不执行 SQL 脚本。需添加 Custom Action在安装程序项目中右键 →View→Custom Actions→Install节点右键 →Add Custom Action→ 选择主程序的.exe文件 → 在Installer类中重写Install方法。// 新建类文件 DatabaseInstaller.cs [RunInstaller(true)] public partial class DatabaseInstaller : System.Configuration.Install.Installer { public override void Install(IDictionary stateSaver) { base.Install(stateSaver); try { // 获取安装目录下的 DatabaseSetup.sql string sqlPath Path.Combine(Context.Parameters[TARGETDIR], DatabaseSetup.sql); string connectionString Data Source(localdb)\MSSQLLocalDB;Integrated Securitytrue;; // 执行SQL脚本逐行执行跳过GO分隔符 string script File.ReadAllText(sqlPath); var commands SplitSqlScript(script); // 自定义方法按GO分割 using (var conn new SqlConnection(connectionString)) { conn.Open(); foreach (string cmd in commands) { if (!string.IsNullOrWhiteSpace(cmd)) { using (var cmdObj new SqlCommand(cmd, conn)) cmdObj.ExecuteNonQuery(); } } } } catch (Exception ex) { throw new InstallException($数据库初始化失败{ex.Message}); } } private string[] SplitSqlScript(string script) { return script.Split(new[] { GO }, StringSplitOptions.RemoveEmptyEntries) .Select(s s.Trim()) .Where(s !string.IsNullOrEmpty(s)) .ToArray(); } }逻辑说明DatabaseInstaller继承InstallerInstall方法在 MSI 安装完成后触发。SplitSqlScript将DatabaseSetup.sql按GO分割SQL Server Management Studio 的批处理分隔符避免SqlCommand执行多语句失败。连接字符串用Integrated Securitytrue依赖 Windows 身份验证无需用户名密码。安全注意DatabaseSetup.sql中不要写明文密码初始数据用INSERT INTO Users VALUES (admin, pbkdf2:sha256:260000:...)存哈希密码而非明文。5. 避坑指南WinForm SQL Server 外卖系统上线后最常踩的 5 个坑部署上线不是终点而是问题爆发的起点。以下是我在 7 个餐饮客户现场踩过的血泪坑按发生频率排序每条都附现象、根因、解法拒绝“重启试试”。5.1 现象多台收银机同时操作订单状态乱跳如“已出餐”变回“备餐中”原因WinForm 程序未启用SqlDependency或轮询各客户端缓存了过期的订单数据。用户A在机器1点“已出餐”数据库更新用户B在机器2仍显示旧状态点“备餐中”覆盖了A的操作。解决短期在订单列表窗体Load事件中每30秒Refresh()BindingSourcebindingSourceOrders.ResetBindings(false)长期用SqlDependency实现数据库变更推送需开启 Service BrokerSQL Server 2005 支持代码见微软文档但对小店老板太重30秒轮询更务实。5.2 现象安装包在客户电脑上双击无反应任务管理器看不到进程原因客户系统是 Windows 7 SP1 以下或 .NET Framework 4.5.2 未预装而安装包未正确检测依赖静默失败。解决在安装包Prerequisites中勾选.NET Framework 4.5.2并设为“从供应商网站下载”发布前用signtool.exe对 MSI 签名避免杀毒软件拦截提供离线安装包包含.NET Framework 4.5.2 offline installer和LocalDB offline installer打包成setup_full.exe。5.3 现象导出Excel时报错“OLE DB provider not registered”尤其Win10 64位原因WinForm 用Microsoft.Jet.OLEDB.4.0导出 Excel 2003.xls但64位系统默认不注册32位OLE DB提供程序。解决弃用 Jet.OLEDB改用EPPlus库纯.NET无需Office// NuGet安装 EPPlus代码示例 using (var package new ExcelPackage()) { var ws package.Workbook.Worksheets.Add(订单日报); ws.Cells[A1].Value 订单号; ws.Cells[B1].Value 客户; // ... // 写入数据... File.WriteAllBytes(Report.xlsx, package.GetAsByteArray()); }若必须用 .xls编译时设项目平台为x86而非AnyCPU强制走32位运行时。5.4 现象SQL Server 2012/2014 安装失败报错“无法找到数据库引擎启动句柄”原因客户电脑已装宝塔面板或其他Web服务占用了 TCP 1433 端口或 Windows Firewall 阻断。解决不装完整 SQL Server坚持用 LocalDB监听命名管道不占1433若必须用 Express安装时在“实例配置”页取消勾选“SQL Server 代理”改用默认实例名SQLEXPRESS端口设为动态避免硬编码1433提前运行netstat -ano | findstr :1433检查端口占用。5.5 现象WinForm 界面卡顿特别是 DataGridView 滚动慢、搜索框输入延迟原因DataGridView默认启用DoubleBuffered false大量数据重绘闪烁且未启用虚拟模式VirtualMode一次性加载全部数据。解决启用双缓冲减少闪烁public class DoubleBufferedDataGridView : DataGridView { protected override CreateParams CreateParams { get { var parms base.CreateParams; parms.Style | 0x02000000; // WS_EX_COMPOSITED return parms; } } }启用虚拟模式大数据量滚动流畅设VirtualMode true重写CellValueNeeded事件按需加载当前可视行数据而非全量绑定。6. 进阶技巧用 SQL Server 图形化工具做实时监控把 WinForm 变成“餐厅作战室”WinForm 不该只是 CRUD 窗口它该是门店的实时作战中心。SQL Server 自带的SQL Server Management Studio (SSMS)和Performance Monitor配合几个简单查询就能让老板一眼看清“现在几单在备餐”、“哪个菜品快卖光了”、“骑手平均等多久”。这才是 WinForm SQL Server 外卖系统的终极价值——把数据库变成活的仪表盘。6.1 实时看板用 SSMS 查询窗口做“三屏监控”在 SSMS 中新建查询粘贴以下三个语句设为“自动刷新10秒”投屏到收银台旁电视监控项SQL 查询说明当前待处理订单SELECT COUNT(*) AS PendingCount FROM Orders WHERE Status Pending OR Status Paid数字越大前台越忙需催厨房库存预警5件SELECT ProductName, Qty FROM Stock WHERE Qty 5 ORDER BY Qty红色高亮采购员手机收到截图立刻补货骑手等待时长SELECT AVG(DATEDIFF(MINUTE, CreatedAt, GETDATE())) AS AvgWaitMin FROM Orders WHERE Status Ready超过15分钟店长打电话问骑手在哪操作步骤SSMS → 新建查询 → 粘贴SQL → 按CtrlR执行 → 右键结果网格 →Refresh Interval→ 设10秒。无需写 WinForm 代码零成本实现。6.2 状态栏动态更新用 SqlConnection.Statistics 获取真实数据库压力WinForm 状态栏不只显示“就绪”还能显示“数据库响应87ms”。SqlConnection的Statistics属性可获取最后一次查询的详细耗时。private void UpdateDbLatencyStatus() { try { using (var conn new SqlConnection(connectionString)) { conn.Open(); // 执行一个轻量查询如 SELECT 1 using (var cmd new SqlCommand(SELECT 1, conn)) cmd.ExecuteNonQuery(); var stats conn.RetrieveStatistics(); var elapsedMs (long)stats[NetworkServerTime]; // 单位毫秒 toolStripStatusLabel2.Text $DB响应{elapsedMs}ms; // 超过500ms标红预警 toolStripStatusLabel2.ForeColor elapsedMs 500 ? Color.Red : SystemColors.ControlText; } } catch { toolStripStatusLabel2.Text DB离线; toolStripStatusLabel2.ForeColor Color.Red; } }参数说明NetworkServerTime是 SQL Server 处理请求的真实耗时不含网络传输比Stopwatch更准RetrieveStatistics()需在conn.Close()前调用否则清零。每30秒调用一次状态栏实时反馈数据库健康度。6.3 打包成安装程序VS2015 发布向导的隐藏技巧VS2015 发布向导默认生成setup.exe但客户常误点setup.msi实际是内部组件。正确做法是在发布向导最后一步勾选Create desktop shortcut和Create start menu shortcut发布后进入publish文件夹找到setup.exe不是.msi这才是真正的安装入口重命名setup.exe为外卖系统安装程序.exe图标用ico文件替换右键项目 → Properties → Application → Icon and manifest终极技巧用Inno Setup二次打包setup.exe添加欢迎页、协议弹窗、卸载入口生成真正友好的安装包——我一般用它因为 VS2015 的 MSI 对中文路径支持仍有 Bug。我干这行八年最深的教训是WinForm SQL Server 外卖系统成败不在代码多炫而在把数据库当状态机用、把安装包当产品交付、把界面当运营工具做。客户不会关心你用了 MVVM 还是三层架构他只关心“今天订单没丢”、“库存没算错”、“新员工3分钟学会”。所以每次写完代码我必做三件事在客户电脑上装一遍安装包、用三台虚拟机模拟并发下单、让老板用鼠标点十次“确认出餐”。只有这样才能把“WinForm SQL Server 外卖系统”从一个标题变成店里每天安稳运转的那台收银机。希望帮到你。本文还有配套的精品资源点击获取
返回列表