资讯动态

C# WinForms超市管理系统:收银+库存双模块实战源码

发布时间:2026/10/26 15:43:54 来源:尧图企业网站定制
简介这是一套基于C#开发的超市管理系统实战项目源码面向C#初学者与Windows桌面应用开发者聚焦收银结算与库存管理两大核心业务场景助力掌握WinForms开发、SQL数据库交互及企业级模块化设计。资源共134个文件含97个C#业务逻辑与界面代码文件如FormMain.cs、FormInvertoryManage.Designer.cs、17个本地化资源文件.resx、9个工程配置文件.csproj、2个解决方案文件.sln及2个SQL建库脚本完整覆盖UI构建、数据访问、事务处理与基础配置压缩包仅629KB轻量易部署。已有88人学习下载提供开箱即用的可运行工程结构包含商品管理、库存预警、收银小票打印等典型功能模块代码分层清晰、注释规范适合作为课程设计、毕业设计或C#桌面应用入门实践范例。1. C#超市管理系统不是Demo是能跑通收银库存双模块的真实WinForms工程这不是一个“Hello World”级别的教学项目而是一套完整落地、带界面、连数据库、有业务逻辑的C#超市管理系统源码包。它不依赖Web服务器不走ASP.NET Core纯Windows Forms架构开箱即用——你双击FormMain.cs所在的项目按F5就能启动主界面扫码录入商品、结算生成交易记录、库存变动实时刷新、历史销售可查可导出。整个系统围绕两个刚性需求构建收银台要快、稳、不出错库存要准、可溯、能预警。它用的是ADO.NET直连SQL Server兼容LocalDB没上Entity Framework所有SQL语句手写在DAL层事务控制明确到SqlTransaction级别库存增减走InventoryService统一入口避免多点并发写入导致数量错乱收银小票打印用PrintDocument原生实现不调第三方SDK规避授权和兼容性黑匣子。适合刚学完C#基础、正卡在“学了语法但写不出完整业务系统”的中级开发者也适合需要快速交付小型商超内部管理工具的外包工程师——它不是玩具是能进真实门店试用的最小可行产品MVP。2. 从App.config到FormMain解析项目结构与核心模块职责划分这个C#超市管理系统采用典型的三层架构雏形UI层WinForms窗体、业务逻辑层隐含在各Form的事件处理中、数据访问层集中于DataAccess类或DBHelper静态类。虽然没严格分离成独立项目但文件组织已体现清晰边界。我们先拆解关键文件再说明它们如何协同完成一次收银闭环。2.1 App.config不只是连接字符串它是运行时配置中枢App.config是整个系统的配置起点绝非仅存数据库连接串。打开它你会看到类似如下结构?xml version1.0 encodingutf-8? configuration startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.7.2 / /startup connectionStrings add nameSuperMarketDB connectionStringData Source.\SQLEXPRESS;Initial CatalogSuperMarketDB;Integrated SecurityTrue; providerNameSystem.Data.SqlClient / /connectionStrings appSettings add keyDefaultPrinterName valueHP LaserJet M1005 / add keyAutoPrintReceipt valuetrue / add keyLowStockThreshold value5 / /appSettings /configuration提示connectionString必须指向你本地已安装的SQL Server实例如.\SQLEXPRESS或localhost\SQLEXPRESS若用LocalDB路径应为Data Source(localdb)\mssqllocaldb;...。DefaultPrinterName需与Windows设备管理器中打印机名称完全一致否则小票打印会静默失败。App.config中的appSettings段落才是业务开关AutoPrintReceipt控制是否自动触发打印调试时建议设为falseLowStockThreshold定义库存预警阈值——这个值被FormInvertoryManage读取后用于高亮显示临界商品。不要忽略它很多新手改了数据库连接却忘了同步修改打印机名结果收银成功但小票没出来以为是代码问题实则配置未生效。2.2 MessageBoxEx.cs封装不是炫技是解决WinForms原生弹窗的三大硬伤标准MessageBox.Show()在超市场景下有三个致命缺陷无法设置图标位置警告图标总在左文字居中视觉割裂按钮文本固定为“确定/取消”无法改为“继续扫描/结束交易”无超时自动关闭机制收银员忙时可能忘记点确认阻塞流程。MessageBoxEx.cs正是为此而生。它继承自Form重绘布局暴露ShowTimeout()方法// MessageBoxEx.cs 关键片段 public static DialogResult ShowTimeout(string text, string caption, int timeoutMs 3000) { var frm new MessageBoxEx(); frm.lblMessage.Text text; frm.Text caption; frm.timerAutoClose.Interval timeoutMs; frm.timerAutoClose.Start(); return frm.ShowDialog(); }调用示例在FormMain.cs的扫码事件中// 扫到缺货商品时 if (product.StockQuantity Convert.ToInt32(ConfigurationManager.AppSettings[LowStockThreshold])) { MessageBoxEx.ShowTimeout($【{product.Name}】库存仅剩{product.StockQuantity}件请补货, 库存预警, 5000); // 5秒后自动关闭不打断收银流 }参数说明timeoutMs单位毫秒设为0则禁用自动关闭caption作为窗体标题比原生MessageBox更易识别上下文返回值仍为DialogResult可无缝替换旧代码。2.3 FormMain.cs 与 Designer 文件收银主界面的事件驱动骨架FormMain.cs是系统心脏其.Designer.cs文件由VS自动生成定义控件ID与布局。关键控件命名遵循C# WinForms控件命名简称规范见热搜词txtBarcode条码输入框TextBoxbtnAddItem添加商品按钮ButtonlvCart购物车列表ListViewViewDetailslblTotalAmount应付金额标签LabelbtnCheckout结算按钮Button收银核心逻辑在btnCheckout_Click事件中展开private void btnCheckout_Click(object sender, EventArgs e) { if (cartItems.Count 0) return; using (var conn new SqlConnection(ConfigurationManager.ConnectionStrings[SuperMarketDB].ConnectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) // 显式事务开始 { try { // 1. 插入交易主表 var orderId InsertOrder(conn, trans); // 2. 插入交易明细每行商品 foreach (var item in cartItems) InsertOrderDetail(conn, trans, orderId, item); // 3. 扣减库存关键必须在此处执行 foreach (var item in cartItems) UpdateInventory(conn, trans, item.ProductId, -item.Quantity); trans.Commit(); // 全部成功才提交 PrintReceipt(orderId); // 调用打印 ClearCart(); // 清空购物车 MessageBoxEx.Show(交易成功小票已打印。); } catch (Exception ex) { trans.Rollback(); // 任一环节失败全部回滚 MessageBoxEx.Show($交易失败{ex.Message}); } } } }逻辑说明InsertOrder()生成唯一订单号如202405200001写入Orders表InsertOrderDetail()将购物车每项写入OrderDetails表关联订单号UpdateInventory()执行UPDATE Inventory SET Quantity Quantity - qty WHERE ProductId id必须在事务内完成否则出现“钱收了但库存没扣”的资损PrintReceipt()调用PrintDocument绘制小票字体大小、边距、换行均硬编码适配热敏纸58mm宽。注意lvCart的Columns需手动在Designer中设置宽度如商品名:150, 单价:80, 数量:60, 小计:80否则列宽自适应会导致文字重叠。这是WinForms ListView常见坑非代码问题。3. 数据库设计与ADO.NET操作为什么不用EF而坚持手写SQL本系统数据库设计紧扣超市业务本质共5张核心表无冗余字段关系清晰。它不追求范式理论完美而强调查询效率与事务安全——尤其在收银高频写入场景下手写SQL比ORM更可控。3.1 核心表结构与字段含义SQL Server表名字段类型说明索引ProductsProductIdINT PK IDENTITY商品主键聚集索引BarcodeVARCHAR(20) UNIQUE条码扫码依据唯一索引NameNVARCHAR(100)商品名称非聚集索引UnitPriceDECIMAL(10,2)单价—CategoryIdINT分类ID外键非聚集索引InventoryProductIdINT PK FK关联商品聚集索引复合主键WarehouseIdINT PK FK仓库ID支持多仓—QuantityINT当前库存—OrdersOrderIdVARCHAR(20) PK订单号日期流水聚集索引OrderTimeDATETIME2下单时间非聚集索引TotalAmountDECIMAL(10,2)总金额—OrderDetailsDetailIdINT IDENTITY PK明细ID聚集索引OrderIdVARCHAR(20) FK关联订单非聚集索引ProductIdINT商品ID非聚集索引QuantityINT数量—UnitPriceDECIMAL(10,2)结算单价防价格变动—设计理由Products.Barcode设为UNIQUE确保扫码不重复匹配Inventory表用(ProductId, WarehouseId)作复合主键天然支持多仓库库存隔离Orders.OrderId用字符串而非INT便于按日期分片归档如202405200001表示2024年5月20日第1单OrderDetails.UnitPrice冗余存储保证历史订单价格不变——这是超市刚需EF的延迟加载在此场景反而是负担。3.2 ADO.NET事务操作手写SQL的不可替代性以库存扣减为例UpdateInventory()方法如下private void UpdateInventory(SqlConnection conn, SqlTransaction trans, int productId, int deltaQty) { const string sql UPDATE Inventory SET Quantity Quantity deltaQty WHERE ProductId productId AND WarehouseId 1; -- 检查更新行数防止扣减超限 IF ROWCOUNT 0 OR (SELECT Quantity FROM Inventory WHERE ProductId productId AND WarehouseId 1) 0 BEGIN THROW 50000, 库存不足或商品不存在, 1; END; using (var cmd new SqlCommand(sql, conn, trans)) { cmd.Parameters.AddWithValue(productId, productId); cmd.Parameters.AddWithValue(deltaQty, deltaQty); cmd.ExecuteNonQuery(); // 此处抛异常即触发事务回滚 } }参数说明deltaQty传入负数如-2表示扣减正数表示入库WarehouseId 1为默认主仓实际部署时可从App.config读取THROW语句主动抛出错误使catch块捕获并回滚——这是手写SQL对业务规则强校验的优势EF需额外写SaveChanges()后检查ChangeTracker状态代码更冗长。提示ROWCOUNT必须紧跟UPDATE语句中间不能有其他SQL执行否则值被覆盖。这是T-SQL经典陷阱新手常在此翻车。3.3 查询优化为什么FormLogQuery用存储过程而非LINQFormLogQuery.cs负责销售流水查询用户常按日期范围、商品名、收银员筛选。若用LINQ to SQL动态拼接WHERE条件极易引发参数嗅探Parameter Sniffing导致执行计划劣化。本系统采用预编译存储过程CREATE PROCEDURE sp_GetSalesByDateRange StartDate DATE, EndDate DATE, ProductName NVARCHAR(100) NULL AS BEGIN SET NOCOUNT ON; SELECT o.OrderId, o.OrderTime, p.Name AS ProductName, od.Quantity, od.UnitPrice, (od.Quantity * od.UnitPrice) AS Amount FROM Orders o INNER JOIN OrderDetails od ON o.OrderId od.OrderId INNER JOIN Products p ON od.ProductId p.ProductId WHERE o.OrderTime BETWEEN StartDate AND EndDate AND (ProductName IS NULL OR p.Name LIKE % ProductName %) ORDER BY o.OrderTime DESC; ENDFormLogQuery中调用using (var cmd new SqlCommand(sp_GetSalesByDateRange, conn, trans)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(StartDate, dtpStart.Value.Date); cmd.Parameters.AddWithValue(EndDate, dtpEnd.Value.Date.AddDays(1).AddTicks(-1)); if (!string.IsNullOrEmpty(txtProductFilter.Text)) cmd.Parameters.AddWithValue(ProductName, txtProductFilter.Text); // ... 执行查询填充DataGridView }优势存储过程执行计划缓存首次编译后复用比动态SQL快30%以上ProductName参数化LIKE查询避免SQL注入AddDays(1).AddTicks(-1)精确计算截止时间如2024-05-20 23:59:59.999解决BETWEEN包含边界的时间精度问题。4. 收银与库存模块的并发控制Mutex不是银弹Monitor才是生产级选择超市多收银台同时开单库存扣减若无并发控制必然出现“超卖”——两人同时扫同一商品库存从10扣到9再扣到8实际只卖出1件却扣了2次。本系统在InventoryService中采用Monitor实现轻量级线程同步而非全局Mutex跨进程开销大且WinForms单进程无需跨进程锁。4.1 库存操作的临界区锁定策略InventoryService.cs虽未在文件列表中显式列出但逻辑内聚于FormMain的库存更新方法核心代码private static readonly object _inventoryLock new object(); public static bool TryDeductStock(int productId, int quantity) { lock (_inventoryLock) // 进入临界区 { // 1. 查询当前库存必须在锁内 var currentStock GetStockQuantity(productId); if (currentStock quantity) return false; // 不足则拒绝 // 2. 执行扣减已在事务中此处仅做逻辑判断 // 实际扣减由UpdateInventory()在事务内完成 return true; } }为什么用lock而非MutexMutex是内核对象每次等待/释放涉及用户态-内核态切换耗时约10μslock是用户态自旋内核等待混合平均耗时1μs本系统为单进程多线程WinForms UI线程后台打印线程lock完全满足Mutex名称需全局唯一如SuperMarket_Inventory若名称冲突会导致锁失效排查困难。4.2 收银界面的线程安全实践禁止跨线程访问UI控件FormMain中小票打印使用PrintDocument.Print()该方法在后台线程执行但PrintPage事件中若直接更新lblStatus.Text会抛InvalidOperationException。正确做法private void printDoc_PrintPage(object sender, PrintPageEventArgs e) { // 绘制小票逻辑... e.Graphics.DrawString(交易完成, font, brush, x, y); // 安全线程更新UI this.Invoke((MethodInvoker)delegate { lblStatus.Text 小票打印中...; lblStatus.Refresh(); }); }参数说明this.Invoke()将委托封送到UI线程执行MethodInvoker是无参无返回值委托类型比Action更轻量Refresh()强制重绘避免文字闪烁。4.3 避坑常见并发与线程问题排查清单现象原因解决收银成功但库存没变UpdateInventory()未在事务内执行或conn/trans对象被意外Dispose检查btnCheckout_Click中using块嵌套层级确保UpdateInventory()调用时conn和trans均有效在UpdateInventory()开头加if (conn.State ! ConnectionState.Open) throw new Exception(连接已关闭);多收银台同时扣减库存变为负数TryDeductStock()与UpdateInventory()之间存在时间窗口A查到库存10B也查到10A扣5剩5B扣5剩0但B的扣减基于旧值必须将库存查询与扣减合并为一条SQL见3.2节UPDATE...IF ROWCOUNTlock仅用于防止查询阶段并发不能替代数据库层面的原子操作小票打印卡死UI无响应PrintDocument.Print()在UI线程同步阻塞且打印机离线时无限等待改用printDoc.PrintAsync()并在PrintCompleted事件中更新UI添加打印机状态检测if (!PrinterSettings.IsValid) { MessageBoxEx.Show(打印机未就绪); return; }ListView点击某行后SelectedItems[0]报NullReferenceExceptionlvCart的HideSelection属性为true失去焦点时选中项视觉消失但逻辑仍存在用户误点空白处在lvCart_SelectedIndexChanged事件开头加if (lvCart.SelectedItems.Count 0) return;或设HideSelection false保持视觉反馈App.config修改后重启程序无效Visual Studio调试时bin\Debug下生成的是YourApp.exe.config而非源码中的App.config修改源码App.config后需重新生成项目修改后右键项目→“重新生成”确认bin\Debug\YourApp.exe.config内容已更新或直接编辑bin\Debug\YourApp.exe.config仅调试用注意lock对象必须是static readonly若为实例字段不同窗体实例的锁互不干扰失去同步意义。5. 从零部署SQL Server初始化、WinForms打包与防反编译加固拿到源码后90%的新手卡在环境搭建。本章给出可复制的部署路径覆盖数据库初始化、程序发布、以及生产环境必备的防反编译措施——毕竟超市系统涉及财务数据代码裸奔风险极高。5.1 SQL Server数据库初始化脚本含测试数据创建数据库SuperMarketDB后执行以下脚本保存为.sql文件用SSMS执行-- 1. 创建Products表 CREATE TABLE Products ( ProductId INT IDENTITY(1,1) PRIMARY KEY, Barcode VARCHAR(20) UNIQUE NOT NULL, Name NVARCHAR(100) NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL, CategoryId INT DEFAULT 1 ); -- 2. 创建Inventory表主仓WarehouseId1 CREATE TABLE Inventory ( ProductId INT NOT NULL, WarehouseId INT NOT NULL DEFAULT 1, Quantity INT NOT NULL DEFAULT 0, PRIMARY KEY (ProductId, WarehouseId), FOREIGN KEY (ProductId) REFERENCES Products(ProductId) ); -- 3. 插入测试商品5个高频商品 INSERT INTO Products (Barcode, Name, UnitPrice) VALUES (6901234567890, 蒙牛纯牛奶250ml, 3.50), (6921234567891, 康师傅红烧牛肉面, 5.00), (6931234567892, 农夫山泉矿泉水550ml, 2.00), (6941234567893, 金龙鱼调和油5L, 65.00), (6951234567894, 清风抽纸3层120抽, 12.50); -- 4. 初始化库存每种100件 INSERT INTO Inventory (ProductId, Quantity) SELECT ProductId, 100 FROM Products; -- 5. 创建Orders与OrderDetails表略见3.1节结构 -- 执行后数据库即具备运行基础验证方法在FormMain中扫码6901234567890应显示“蒙牛纯牛奶250ml”单价3.50结算后Inventory表中对应ProductId的Quantity应减1。5.2 WinForms程序发布ClickOnce vs Publish Folder选后者Visual Studio中右键项目→“发布”不要选ClickOnce需IIS部署超市无IT运维人员。选择“文件夹”发布目标位置设为.\publish\相对路径安装模式选“依赖于目标计算机上已安装的.NET Framework”本项目为.NET Framework 4.7.2生成后publish文件夹内含SuperMarketSystem.exe主程序SuperMarketSystem.exe.config配置文件含数据库连接串System.Data.dll等依赖.NET Framework已内置此为可选部署步骤将整个publish文件夹拷贝至收银电脑修改SuperMarketSystem.exe.config中connectionString指向该电脑的SQL Server实例双击SuperMarketSystem.exe即可运行。提示若收银电脑未装.NET Framework 4.7.2需先下载离线安装包ndp472-kb4054530-x86-x64-allos-enu.exe否则启动报错“未能加载文件或程序集”。5.3 防反编译加固Costura.Fody合并DLL ConfuserEx混淆超市系统代码若被反编译商品进价、供应商信息等敏感数据将泄露。本方案分两步Step 1用Costura.Fody合并依赖DLLNuGet安装Costura.Fody版本4.1.0编译后所有引用的DLL如System.Data.SQLite.dll被嵌入SuperMarketSystem.exe无需额外分发验证用ILSpy打开SuperMarketSystem.exe搜索System.Data应无独立程序集节点。Step 2用ConfuserEx混淆主程序下载ConfuserEx 1.5.0开源免费配置confuser.crprojproject outputDir.\obfuscated\ baseDir.\publish\ ... rule patterntrue presetmaximum protection idconstants / protection idanti debug / protection idcontrol flow / protection idresources / /rule /project执行混淆输出obfuscated\SuperMarketSystem.exe即为最终交付物。效果反编译后变量名变为a,b,c字符串加密控制流扁平化FormMain.cs中btnCheckout_Click方法被拆解为20个无意义跳转业务逻辑不可读启动速度下降5%可接受。5.4 收银小票打印适配热敏纸58mm宽度的像素换算公式PrintDocument默认使用100dpi但热敏打印机物理分辨率常为203dpi。若按默认设置小票文字过小。修正方法private void printDoc_PrintPage(object sender, PrintPageEventArgs e) { // 获取打印机真实DPI关键 float dpiX e.Graphics.DpiX; float dpiY e.Graphics.DpiY; // 58mm 228.35像素按203dpi计算58 * 203 / 25.4 float pageWidthPx 228.35f; // 字体缩放默认10pt在100dpi下为13.3px需按dpi缩放 Font font new Font(SimSun, 10 * dpiY / 100, FontStyle.Regular); // 绘制区域左边界10px右边界pageWidthPx-10 RectangleF bounds new RectangleF(10, 20, pageWidthPx - 20, 1000); e.Graphics.DrawString(超市收据, font, Brushes.Black, bounds, new StringFormat { Alignment StringAlignment.Center }); }参数说明dpiX/dpiY从e.Graphics获取避免硬编码pageWidthPx 58 * dpiX / 25.4为通用公式25.4mm1inchFont大小按DPI线性缩放确保文字物理尺寸一致。从那以后我每次交付超市系统都强制走一遍这四步①用SSMS执行初始化脚本并查Inventory表确认数据②修改publish\*.exe.config连接串后用sqlcmd命令行测试连通性sqlcmd -S .\SQLEXPRESS -d SuperMarketDB -Q SELECT TOP 1 * FROM Products③用ILSpy打开混淆后EXE确认无敏感字符串明文④在目标打印机上打印测试页用尺子量实际宽度是否为58mm。少一步上线后就可能半夜被电话叫醒。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑