
简介这是一套面向C#初学者与进阶开发者的三层架构超市管理系统完整源码适合用于课程设计、毕业设计或WinForm项目练手。系统功能覆盖销售管理中的商品结算、商品资料维护、商品单位与类别及供应商等基础资料编辑还包含今日销售记录与全部销售记录查询、操作员账号管理与过账等模块界面皮肤美观业务逻辑分层清晰便于理解BLL、DAL与UI之间的协作方式。压缩包共243个文件约12.08MB以82个cs源码文件为核心辅以26个resx与26个resources资源文件、27个gif界面素材、14个dll依赖库及3个mdb数据库文件另含csproj工程与sln解决方案可直接用Visual Studio打开运行。管理员账号密码均为admin方便快速登录体验。目前已有173人学习下载适合希望掌握三层架构落地写法、数据库设计与收银业务流程的读者参考借鉴。1. 三层架构的超市管理系统为什么它成了 C# 练手项目的分水岭很多人第一次接触 C# 三层架构的超市管理系统是在课程设计或者面试准备阶段。标题里这个「源码数据库」的压缩包本质上是一套完整的、能跑起来的业务闭环收银台扫码扣库存、后台补货改价格、会员积分和日结报表。它解决的不是「怎么写出一个界面」而是「怎么把界面、业务规则、数据访问拆开让改一个价格不用动收银代码」。适合谁刚学完 C# 基础语法、能写 WinForm 或 WPF 窗口但一遇到「业务一复杂就全堆在按钮事件里」就翻车的开发者。三层架构UI 表现层、BLL 业务逻辑层、DAL 数据访问层配合 SQL Server 或 SQLite是这类系统最常见的落地形态。热词里频繁出现的「数据库增删改查」「c#类库的使用」「c#与access」恰恰说明大家卡在的不是语言而是分层边界和数据库交互的工程化写法。这一篇就按「先立住分层理由再动手复现最后讲坑」的节奏走。2. 三层架构到底怎么分从一次改价需求反推分层边界2.1 为什么不是两层也不是 MVC 一把梭先看一个真实场景超市要搞促销把某类商品统一下调 10%。如果代码是两层界面直接调数据库你得在收银界面、后台管理界面、报表界面各改一遍 SQL。三层架构的核心价值就在这——把「打折」这个业务规则收进 BLLUI 只负责收集输入和展示结果DAL 只负责执行 SQL。改一次 BLL三个入口同时生效。那为什么不用 MVCMVC 更适合 Web 请求-响应模型而超市管理系统大量是桌面端WinForm/WPF的即时交互三层架构的 UI-BLL-DAL 调用链更贴合桌面事件驱动。常见做法是UI 层引用 BLLBLL 引用 DALDAL 引用数据访问基础类比如 SqlHelper 或 Dapper。注意引用方向是单向的UI 不能直接引用 DAL这是分层的硬约束。层职责不该做的事UI 表现层接收输入、校验格式、展示数据写 SQL、算折扣BLL 业务层业务规则、事务编排、参数校验直接拼 SQL 字符串DAL 数据层执行增删改查、映射实体判断「会员是否满 500 分」2.2 实体类与接口先定契约再写实现动手第一步不是建数据库而是定实体和接口。实体类Model是三层之间传递数据的载体接口Interface是层与层解耦的关键。我一般会先写IProductDAL、IProductBLL这样后面换数据库SQL Server 换 SQLite时UI 和 BLL 一行不用改。// Model/Product.cs —— 实体类三层共用 public class Product { public int Id { get; set; } public string BarCode { get; set; } // 条码收银扫码用 public string Name { get; set; } public decimal Price { get; set; } public int Stock { get; set; } // 库存 public int CategoryId { get; set; } } // DAL/IProductDAL.cs —— 数据访问接口只声明能力 public interface IProductDAL { Product GetByBarCode(string barCode); int UpdateStock(int productId, int newStock); ListProduct GetLowStock(int threshold); } // BLL/IProductBLL.cs —— 业务接口暴露给 UI public interface IProductBLL { Product QueryByCode(string barCode); bool ReduceStock(string barCode, int qty); // 收银扣库存 ListProduct GetRestockList(); }逻辑说明实体类放在独立项目或文件夹被三层共同引用DAL 接口只描述「能查、能改」不暴露 SqlConnectionBLL 接口描述业务动作比如ReduceStock而不是UpdateStock因为扣库存还要判断是否足够、是否触发补货提醒。参数上barCode用 string 而不是 int因为条码可能带字母且前导零不能丢这是新手常踩的坑。2.3 用 SqlHelper 封装增删改查避免 SQL 散落各处DAL 层最忌讳每个方法里都new SqlConnection。常见做法是写一个 SqlHelper把连接字符串、参数化查询、异常处理收口。下面这段是 SQL Server 版本的典型写法换成 SQLite 只需改SqlConnection为SQLiteConnection。// DAL/SqlHelper.cs —— 数据访问基础类 public static class SqlHelper { // 连接字符串从配置文件读不硬编码 private static readonly string ConnStr ConfigurationManager.ConnectionStrings[SupermarketDB].ConnectionString; public static int ExecuteNonQuery(string sql, params SqlParameter[] paras) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteNonQuery(); // 返回受影响行数 } } public static object ExecuteScalar(string sql, params SqlParameter[] paras) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteScalar(); // 返回首行首列 } } }逻辑说明using保证连接和命令对象及时释放避免连接池耗尽params SqlParameter[]强制参数化杜绝 SQL 注入ExecuteScalar用于SELECT COUNT(*)或SELECT Stock这类单值查询。参数上连接字符串放在App.config的connectionStrings节点改数据库地址不用重新编译。注意ExecuteNonQuery返回的是受影响行数扣库存时如果返回 0说明条码不存在或库存没变BLL 要据此抛业务异常。2.4 BLL 里的事务收银扣库存和写流水必须同生共死收银动作涉及两张表商品表扣库存、销售流水表插记录。这两步必须在一个事务里否则扣了库存没写流水日结对不上账。BLL 层负责编排事务DAL 只提供单表操作。// BLL/ProductBLL.cs —— 扣库存并写流水 public bool ReduceStock(string barCode, int qty) { var product dal.GetByBarCode(barCode); if (product null) throw new Exception(条码不存在); if (product.Stock qty) throw new Exception(库存不足); using (var conn new SqlConnection(ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 扣库存 dal.UpdateStock(conn, tran, product.Id, product.Stock - qty); // 写销售流水 saleDal.Insert(conn, tran, product.Id, qty, product.Price * qty); tran.Commit(); return true; } catch { tran.Rollback(); // 任何一步失败全部回滚 throw; } } } }逻辑说明事务对象tran必须透传给 DAL 方法所以UpdateStock和Insert要重载带SqlTransaction参数的版本。参数上qty在进入事务前先校验避免开了事务才发现库存不足。注意catch里先Rollback再throw不要吞异常否则 UI 层不知道失败原因。这是三层架构里 BLL 最该体现价值的地方——业务规则和事务边界都在这一层。3. 从零跑通数据库建表、连接配置与最小可运行闭环3.1 建库建表五张核心表撑起超市业务超市管理系统的数据库不用太复杂五张表就能跑通主流程商品表、分类表、会员表、销售流水表、库存变动表。下面给出 SQL Server 建表脚本SQLite 只需把IDENTITY换成AUTOINCREMENT、NVARCHAR换成TEXT。-- 商品表 CREATE TABLE Product ( Id INT IDENTITY(1,1) PRIMARY KEY, BarCode NVARCHAR(30) NOT NULL UNIQUE, Name NVARCHAR(100) NOT NULL, Price DECIMAL(10,2) NOT NULL, Stock INT NOT NULL DEFAULT 0, CategoryId INT NOT NULL ); -- 销售流水表 CREATE TABLE SaleRecord ( Id INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, Qty INT NOT NULL, Amount DECIMAL(10,2) NOT NULL, SaleTime DATETIME NOT NULL DEFAULT GETDATE() ); -- 会员表 CREATE TABLE Member ( Id INT IDENTITY(1,1) PRIMARY KEY, Phone NVARCHAR(20) NOT NULL UNIQUE, Points INT NOT NULL DEFAULT 0 );逻辑说明BarCode加 UNIQUE 约束防止重复录入Stock默认 0新商品先入库再上架SaleTime默认GETDATE()插入时不用手动传时间。参数上DECIMAL(10,2)保证金额精确到分别用 float否则日结会出现 0.01 的误差累积。注意 SQLite 没有GETDATE()要用DATETIME(now)。3.2 连接字符串配置与 DAL 实现类建完表把连接字符串写进App.config然后实现IProductDAL。!-- App.config -- configuration connectionStrings add nameSupermarketDB connectionStringServer.;DatabaseSupermarket;Trusted_ConnectionTrue; providerNameSystem.Data.SqlClient/ /connectionStrings /configuration// DAL/ProductDAL.cs —— 实现接口 public class ProductDAL : IProductDAL { public Product GetByBarCode(string barCode) { string sql SELECT * FROM Product WHERE BarCodecode; var dt SqlHelper.ExecuteDataTable(sql, new SqlParameter(code, barCode)); if (dt.Rows.Count 0) return null; var row dt.Rows[0]; return new Product { Id (int)row[Id], BarCode row[BarCode].ToString(), Name row[Name].ToString(), Price (decimal)row[Price], Stock (int)row[Stock] }; } public int UpdateStock(int productId, int newStock) { string sql UPDATE Product SET Stockstock WHERE Idid; return SqlHelper.ExecuteNonQuery(sql, new SqlParameter(stock, newStock), new SqlParameter(id, productId)); } }逻辑说明GetByBarCode返回 null 而不是抛异常让 BLL 决定怎么处理「查不到」UpdateStock返回受影响行数BLL 据此判断是否成功。参数上code和id都是参数化别用字符串拼接。注意ExecuteDataTable是 SqlHelper 里另一个方法用SqlDataAdapter填充 DataTable这里没展开但实现很简单。3.3 UI 层只做三件事收集、调用、展示WinForm 的收银界面按钮事件里不要出现 SQL只调 BLL。// UI/CashierForm.cs —— 扫码收银 private void btnScan_Click(object sender, EventArgs e) { string code txtBarCode.Text.Trim(); if (string.IsNullOrEmpty(code)) { MessageBox.Show(请扫描条码); return; } try { var product bll.QueryByCode(code); if (product null) { MessageBox.Show(商品不存在); return; } lblName.Text product.Name; lblPrice.Text product.Price.ToString(F2); lblStock.Text product.Stock.ToString(); } catch (Exception ex) { MessageBox.Show(查询失败 ex.Message); } }逻辑说明UI 层只做输入校验空值判断和展示业务判断商品是否存在、库存是否足够全在 BLL。参数上Trim()去掉扫码枪可能带进的空格ToString(F2)保证金额显示两位小数。注意bll是构造函数注入或字段初始化的IProductBLL实例别在事件里new ProductBLL()否则测试时没法替换。3.4 最小闭环验证扫码→扣库存→查流水跑通的标准是扫码后库存减 1销售流水表多一条记录日结报表能汇总。验证步骤在 Product 表插入一条测试数据INSERT INTO Product VALUES(6901234567890,可乐,3.50,100,1)。启动程序扫码框输入条码点查询确认名称价格显示正确。点结算检查 Product 表 Stock 是否从 100 变 99。查 SaleRecord 表确认多了一条 Qty1、Amount3.50 的记录。把 Stock 改成 0再结算确认弹出「库存不足」且流水表没新增。这五步走完三层架构的调用链就通了。如果第 3 步库存没变先看 BLL 事务有没有 Commit如果第 4 步流水没写看 DAL 的 Insert 是不是漏传了 tran 参数。4. 避坑与排查三层架构落地时最容易翻车的 5 个点4.1 现象UI 层直接 new DAL分层形同虚设原因图省事在按钮事件里直接new ProductDAL().GetByBarCode()。解决UI 只引用 BLL 项目DAL 项目设为 BLL 的私有引用编译期就断了 UI 直接调 DAL 的可能。如果已经写乱了先把所有new XxxDAL()替换成 BLL 方法再逐个补业务规则。4.2 现象扣库存成功但流水没写日结对不上原因BLL 里先调UpdateStock再调Insert但没包事务第二步失败第一步已提交。解决按 2.4 的写法用SqlTransaction包住两步DAL 方法重载带conn和tran参数。注意SqlHelper的静态方法默认自己开连接事务场景要单独写带连接参数的重载。4.3 现象条码前导零丢失扫码查不到商品原因数据库 BarCode 字段用了 INT 类型或者 C# 里用int.Parse转换。解决BarCode 一律用NVARCHAR/stringC# 里不做数值转换。如果历史数据已经是 INT用RIGHT(0000000000000CAST(BarCode AS VARCHAR),13)补零迁移。4.4 现象连接池耗尽程序跑一会儿就卡死原因DAL 里SqlConnection没加using或者Open()后异常路径没关闭。解决所有连接、命令、DataReader 都用using包裹。SqlHelper 里统一收口业务代码不直接碰连接对象。监控方法SQL Server 里查sys.dm_exec_connections连接数持续上涨就是泄漏。4.5 现象换 SQLite 后GETDATE()和IDENTITY报错原因SQL Server 和 SQLite 语法差异。解决建表脚本分开维护SQLite 用AUTOINCREMENT和DATETIME(now)DAL 里如果用了SCOPE_IDENTITY()取自增 IDSQLite 要换成last_insert_rowid()。更稳的做法是 DAL 接口不变换一个SQLiteProductDAL实现UI 和 BLL 零改动。5. 进阶技巧用接口工厂把数据库切换成本降到最低三层架构真正值钱的地方是让你在换数据库、加缓存、接第三方支付时只动一层。下面这个工厂写法是我在多个超市项目里反复用的套路。// DAL/DALFactory.cs —— 根据配置创建 DAL 实现 public static class DALFactory { private static readonly string DbType ConfigurationManager.AppSettings[DbType]; // SqlServer 或 SQLite public static IProductDAL CreateProductDAL() { switch (DbType) { case SQLite: return new SQLiteProductDAL(); case SqlServer: default: return new ProductDAL(); } } }逻辑说明DbType从配置文件读改一行配置就能切换数据库IProductDAL接口保证 BLL 不感知具体实现。参数上AppSettings节点加add keyDbType valueSqlServer/。注意工厂返回接口类型调用方拿到的是IProductDAL不是具体类。验证方法把DbType改成SQLite建一个 SQLite 库跑 3.4 的五步验证。如果 BLL 和 UI 一行没改就跑通说明分层是干净的如果报编译错误说明某处漏了接口、直接用了具体类。再进阶一点可以在 BLL 和 DAL 之间加缓存层QueryByCode先查内存缓存命中直接返回未命中再走 DAL。超市收银场景里热门商品条码查询频率极高加一层MemoryCache能把数据库压力降一个量级。但注意缓存失效——改价、扣库存后要同步清缓存否则会出现「价格改了收银还是旧价」的玄学问题。我一般会在 BLL 的ReduceStock和改价方法里提交事务后立刻cache.Remove(barCode)。血泪经验别在 UI 层做缓存也别在 DAL 层做业务判断。缓存属于 BLL 的编排职责DAL 只认 SQL。这个边界一旦模糊后面加任何功能都会变成「改一处崩三处」。我现在的习惯是每加一个功能先问自己「这行代码属于哪一层」答不上来就先不写。希望帮到你。本文还有配套的精品资源点击获取