ARTICLE DETAIL

资讯详情

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

C#超市管理系统实战:收银与库存管理核心实现

C#超市管理系统实战:收银与库存管理核心实现 简介这份资源是一套基于C#开发的超市管理系统源码面向具备一定C#基础、希望学习企业级桌面应用开发的开发者与计算机专业学生重点解决收银结算与库存管理两大业务场景的实现问题。压缩包共134个文件约629KB以97个cs源码文件为核心配合17个resx界面资源、9个csproj工程文件与2个sln解决方案另含2个sql数据库脚本及config、settings等配置项结构完整可直接导入Visual Studio运行调试。系统涵盖商品扫描、价格显示、支付方式选择、小票打印等收银流程以及商品入库出库、库存数量跟踪、保质期与库存预警等管理功能并涉及ADO.NET数据库访问、事务处理、多线程并发控制与MVC设计模式等知识点。目前已有88人学习下载适合作为课程设计、毕业设计或C#桌面开发练手项目帮助读者理解从界面布局到数据持久化的完整实现思路。1. 从一台收银机说起c#超市管理系统到底要解决什么很多人第一次接触 c#超市管理系统是老板丢过来一句「做个能收银、能看库存的软件」。听起来简单真动手才发现坑在细节里扫码枪是键盘模拟输入还是串口库存扣减和收银是不是同一个事务盘点时后台改价前台正在结算的单子怎么办这些问题不解决系统上线第一天就会被收银员骂。这个标题背后其实是一套典型的桌面端进销存系统核心就两块收银前台快速结算、找零、小票和库存管理入库、出库、盘点、预警。技术栈上C# WinForm 是最常见的落地组合数据库用 SQL Server 或 SQLite 都行小超市用 SQLite 足够连锁店再上 SQL Server。适合谁看一是刚学完 C# 基础想找个完整项目练手的二是接了小商户私活需要快速交付的三是店里现成系统太贵想自己维护的。下面按「先跑通收银 → 再做库存 → 最后处理并发和坑」的顺序讲每一步都能直接抄。2. 收银模块从扫码到小票的完整链路2.1 扫码枪输入怎么接键盘模拟 vs 串口市面上九成 USB 扫码枪默认是「键盘模拟」模式扫一下等于快速敲了一串字符再回车。这意味着你不需要任何驱动只要在收银界面的条码输入框上监听 KeyPress遇到回车就触发查询。但这里有个血泪经验如果收银员手速快扫码和手动输入会混在一起必须用一个标志位区分。// 条码输入框区分扫码枪输入和手动输入 private StringBuilder _scanBuffer new StringBuilder(); private DateTime _lastKeyTime DateTime.Now; private void txtBarcode_KeyPress(object sender, KeyPressEventArgs e) { // 扫码枪输入间隔通常小于 50ms手动输入远大于此 var now DateTime.Now; if ((now - _lastKeyTime).TotalMilliseconds 100) _scanBuffer.Clear(); // 间隔太久认为是新的一次输入 _lastKeyTime now; if (e.KeyChar (char)Keys.Enter) { string code _scanBuffer.ToString().Trim(); if (!string.IsNullOrEmpty(code)) AddToCart(code); // 查询商品并加入购物车 _scanBuffer.Clear(); e.Handled true; // 阻止回车触发默认按钮 } else { _scanBuffer.Append(e.KeyChar); } }逻辑说明用时间间隔判断是不是同一次扫码避免手动输入被误判。参数上100ms 这个阈值可以调扫码枪快的可以降到 50ms慢的可以放到 150ms。e.Handled true很关键否则回车会触发窗体上默认按钮导致重复提交。如果扫码枪是串口模式工业级常见那就用SerialPort类设置好波特率通常 9600、数据位 8、停止位 1在DataReceived事件里读数据。注意串口回调在非 UI 线程更新控件必须Invoke否则会抛跨线程异常。2.2 购物车与结算金额计算和找零的精度陷阱购物车用BindingListCartItem绑定到 DataGridView改动会自动刷新界面。金额计算千万别用double0.1 0.2 不等于 0.3 这种玄学问题在收银场景是致命的一律用decimal。public class CartItem { public string Barcode { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Quantity { get; set; } public decimal SubTotal Price * Quantity; // 小计 } // 结算 private void btnCheckout_Click(object sender, EventArgs e) { decimal total cartList.Sum(i i.SubTotal); decimal paid decimal.Parse(txtPaid.Text); if (paid total) { MessageBox.Show(收款金额不足); return; } decimal change paid - total; // 找零 // 开启事务写销售单 扣库存 using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { int saleId InsertSale(conn, tran, total, paid, change); foreach (var item in cartList) { InsertSaleDetail(conn, tran, saleId, item); DeductStock(conn, tran, item.Barcode, item.Quantity); } tran.Commit(); } catch { tran.Rollback(); throw; } } } PrintReceipt(total, paid, change); // 打印小票 cartList.Clear(); }参数说明decimal保证金额精确到分事务保证「写销售单」和「扣库存」要么全成功要么全回滚这是收银系统最基本的可靠性要求。DeductStock里要用UPDATE Stock SET Qty Qty - qty WHERE Barcode code AND Qty qty带条件更新防止库存扣成负数。2.3 小票打印用 PrintDocument 还是第三方控件WinForm 自带PrintDocument就能打小票热敏打印机一般 58mm 或 80mm 宽。设置PaperSize时注意单位是百分之一英寸58mm 约等于 228。如果嫌麻烦也可以用 Codesoft 这类标签软件做模板C# 通过 COM 调用但部署时每台机器都要装维护成本高。我一般直接用PrintDocument画可控性最强。private void PrintReceipt(decimal total, decimal paid, decimal change) { PrintDocument pd new PrintDocument(); pd.DefaultPageSettings.PaperSize new PaperSize(Receipt, 228, 800); pd.PrintPage (s, ev) { var g ev.Graphics; var font new Font(宋体, 9); float y 10; g.DrawString(某某超市, new Font(宋体, 12, FontStyle.Bold), Brushes.Black, 60, y); y 25; g.DrawString($合计{total:F2}, font, Brushes.Black, 10, y); y 20; g.DrawString($实收{paid:F2}, font, Brushes.Black, 10, y); y 20; g.DrawString($找零{change:F2}, font, Brushes.Black, 10, y); }; pd.Print(); }注意热敏打印机走的是 Windows 打印驱动如果打印出来是空白先检查驱动是否装对再看PaperSize高度是否够高度太小内容会被截断。3. 库存管理入库、出库、盘点和预警3.1 数据库表设计四张表撑起整个系统库存模块的表不用多四张就够商品表、库存表、入库记录、出库记录。商品表和库存表分开是因为商品信息名称、条码、进价、售价变动少库存数量变动频繁分开后锁冲突小。表名关键字段说明ProductId, Barcode, Name, PurchasePrice, SalePrice商品基础信息StockBarcode, Quantity, WarningQty当前库存和预警值StockInId, Barcode, Quantity, InTime, Operator入库流水StockOutId, Barcode, Quantity, OutTime, Reason出库流水销售、损耗建表时Barcode加唯一索引Stock表的Quantity加CHECK (Quantity 0)从数据库层面兜底防止负库存。3.2 入库与出库用事务保证流水和库存一致入库逻辑先插StockIn流水再UPDATE Stock SET Quantity Quantity qty。出库同理但出库要判断库存够不够。销售出库时Reason写「销售」盘点损耗写「损耗」方便后续对账。private void StockIn(string barcode, int qty, string operatorName) { using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 1. 写入库流水 var cmd1 new SQLiteCommand( INSERT INTO StockIn(Barcode, Quantity, InTime, Operator) VALUES(b,q,datetime(now),o), conn, tran); cmd1.Parameters.AddWithValue(b, barcode); cmd1.Parameters.AddWithValue(q, qty); cmd1.Parameters.AddWithValue(o, operatorName); cmd1.ExecuteNonQuery(); // 2. 更新库存不存在则插入 var cmd2 new SQLiteCommand( INSERT INTO Stock(Barcode, Quantity, WarningQty) VALUES(b,q,10) ON CONFLICT(Barcode) DO UPDATE SET Quantity Quantity q, conn, tran); cmd2.Parameters.AddWithValue(b, barcode); cmd2.Parameters.AddWithValue(q, qty); cmd2.ExecuteNonQuery(); tran.Commit(); } } }参数说明ON CONFLICT是 SQLite 的 upsert 语法SQL Server 用MERGE或先查后插。WarningQty默认给 10后续可以在商品设置里改。整个操作包在事务里中途断电也不会出现「流水写了库存没加」的情况。3.3 库存预警定时扫描还是触发式检查预警有两种做法一是每次出库后检查当前库存是否低于WarningQty低于就标红二是用Timer每隔几分钟全表扫一遍。我倾向第一种实时性好且不浪费资源。在出库事务提交后查一下该商品库存低于预警值就在界面上把该行标黄同时写一条预警日志。private void CheckWarning(string barcode) { var cmd new SQLiteCommand( SELECT s.Quantity, s.WarningQty, p.Name FROM Stock s JOIN Product p ON s.Barcode p.Barcode WHERE s.Barcode b, conn); cmd.Parameters.AddWithValue(b, barcode); using (var reader cmd.ExecuteReader()) { if (reader.Read()) { int qty reader.GetInt32(0); int warn reader.GetInt32(1); if (qty warn) { string name reader.GetString(2); // 更新界面预警列表 Invoke(new Action(() { lblWarning.Text $【预警】{name} 库存仅剩 {qty}; })); } } } }注意Invoke是因为如果出库操作在后台线程执行直接改控件会崩。如果全在 UI 线程可以去掉。4. 避坑与排查上线后最容易被骂的五个问题4.1 收银时库存扣成负数现象两个收银台同时卖最后一件商品都显示成功库存变成 -1。 原因扣库存时只判断了「库存 数量」在代码层但两个线程同时读到库存为 1都通过了判断。 解决把判断和扣减合并到一条 SQL 里UPDATE Stock SET Quantity Quantity - q WHERE Barcode b AND Quantity q然后检查ExecuteNonQuery返回的影响行数为 0 说明库存不足回滚事务。4.2 扫码枪扫出来是乱码现象扫中文条码或特殊字符时输入框显示乱码。 原因扫码枪的字符编码和系统不一致常见于进口扫码枪默认 UTF-8而 WinForm 按 GBK 解析。 解决扫码枪说明书里找配置码扫一下切换到「中文模式」或「GBK 编码」。代码层面无法修复必须在硬件层配置。4.3 打印小票时程序卡死现象点结算后界面无响应几秒后小票才出来。 原因PrintDocument.Print()是同步阻塞调用打印机响应慢就会卡 UI 线程。 解决把打印放到后台线程或者用PrintDialog让用户确认。更稳妥的做法是先把销售数据写库再异步打印打印失败也不影响交易。4.4 盘点时后台改价前台结算金额对不上现象收银员加购时价格是 5 元结算时后台改成 6 元小票打出来是 6 元收银员按 5 元收了钱。 原因购物车里的价格是加购时从数据库读的结算时又读了一次最新价。 解决加购时就把价格快照到CartItem.Price结算时直接用快照价不再查库。这是零售系统的通用做法叫「价格快照」。4.5 数据库文件被占用备份失败现象想复制 SQLite 文件备份提示「文件被另一个程序占用」。 原因连接字符串没加Poolingfalse连接池一直持有文件句柄。 解决备份前先执行SQLiteConnection.ClearAllPools()或者连接字符串加Poolingfalse;。SQL Server 则用BACKUP DATABASE语句不要直接复制 mdf 文件。5. 进阶技巧让这套系统多撑三年5.1 用配置表代替硬编码改价改预警不用重编译很多新手把预警值、税率、小票抬头写死在代码里老板一改就要重新编译发版。正确做法是建一张Config表键值对存储启动时加载到内存字典。public static class Config { private static Dictionarystring, string _dict new Dictionarystring, string(); public static void Load(SQLiteConnection conn) { var cmd new SQLiteCommand(SELECT Key, Value FROM Config, conn); using (var reader cmd.ExecuteReader()) while (reader.Read()) _dict[reader.GetString(0)] reader.GetString(1); } public static string Get(string key, string def ) _dict.TryGetValue(key, out var v) ? v : def; public static int GetInt(string key, int def 0) int.TryParse(Get(key), out var v) ? v : def; }这样改小票抬头、改预警阈值直接改数据库就行不用动程序。Config表初始化时插入默认值程序启动调一次Load。5.2 日志写法别再用 MessageBox 调试了上线后出问题没有日志就是黑匣子。用NLog或log4net都行我习惯用最简单的文件日志按天分文件保留 30 天。public static class Logger { private static readonly string LogDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logs); public static void Write(string msg) { try { if (!Directory.Exists(LogDir)) Directory.CreateDirectory(LogDir); string file Path.Combine(LogDir, ${DateTime.Now:yyyyMMdd}.log); File.AppendAllText(file, ${DateTime.Now:HH:mm:ss} {msg}\r\n); } catch { /* 日志失败不能影响主流程 */ } } }关键点日志写入必须 try-catch 包住磁盘满或权限不足时不能让主程序崩。收银、扣库存、打印这几个关键节点都要打日志出问题能快速定位。5.3 防止反编译小商户系统的现实做法C# 编译出来是 IL用 dnSpy 能直接看源码。如果老板在意这个可以用 Costura.Fody 把 DLL 合并进 exe再用 ConfuserEx 混淆。但说实话小超市系统被反编译的概率极低与其花时间搞混淆不如把数据库密码加密、把关键校验放服务端。如果纯单机至少把连接字符串里的密码用 DPAPI 加密别明文写在 config 里。5.4 验证方法用三个场景压一遍再交付交付前我一定跑这三个场景一是连续扫 50 个商品结算看内存和界面是否卡顿二是两个收银端同时卖同一件库存为 1 的商品看是否只有一个成功三是拔掉打印机电源再结算看程序是否崩溃、数据是否已入库。这三个过了基本不会出大问题。我自己踩过最深的坑是早期没做价格快照盘点改价那天收银员和顾客吵了半小时。从那以后凡是收银相关的数据一律在加购时快照结算只用快照值。希望帮到你。本文还有配套的精品资源点击获取
返回列表