简介:这是一套基于C#开发的三层架构超市管理系统源码,面向.NET初学者与中小型项目开发者,聚焦零售场景下的收银、库存与人员管理核心需求。资源包含完整可运行系统,涵盖销售结算、商品资料维护、基础单位/类别/供应商管理、销售记录查询及操作员账号权限控制等功能,界面采用美化皮肤设计,管理员默认账号密码均为admin,开箱即用。压缩包共243个文件,以82个C#源码文件(.cs)构成业务逻辑与UI层,26个.resx与.resources支持多语言资源,14个.dll和3个.exe体现编译成果,另有27个.gif用于界面动效,整体12.08MB,结构清晰体现典型BLL+DAL+UI分层实践。目前已有173人学习下载,读者可直接导入Visual Studio(含.sln与.csproj工程文件),深入理解三层解耦设计、ADO.NET数据库交互及WinForm界面组织方式,是掌握C#企业级桌面应用开发的优质实操范例。
1. 为什么一个“超市管理系统”要用三层架构?——C#里最常被跳过的分层逻辑,恰恰是改需求时不重写的后悔药
你拿到的这个“基于C#的三层架构的超市管理系统(源码+数据库).zip”,表面看是个课程设计级小项目,但真正拉开新手和能扛住业务迭代的工程师之间差距的,不是它能不能增删商品、查销售报表,而是三层架构在代码里是否真实可拆、可测、可换。我见过太多人解压后直接双击SuperMarketSystem.sln,跑通登录就以为“学会了”,结果两周后老板说“加个微信扫码支付”,整个BusinessLogic层像被焊死在UI窗体里,改一行,崩三处——因为所谓“三层”,只是文件夹名字叫DAL、BLL、UI,而实际代码里Form1.cs里直接new了SqlConnection,还手写SQL拼接字符串。这不是三层,这是“三层皮”。本篇不讲抽象概念,只带你用这个真实.zip包为蓝本,亲手验证三层是否真分得开、改得动、测得准:从数据库连接如何隔离、业务规则怎么抽成独立类、UI层如何彻底不碰SQL、到最关键的——当你要把SQL Server换成SQLite或加个API接口时,哪几行必须改、哪几行根本不用碰。适合刚学完ADO.NET想落地、或正被老系统维护折磨的C#开发者。别怕源码“土”,它的价值不在炫技,而在暴露真实分层中的每一道裂缝。
2. 三层不是三个文件夹:从源码结构反推真实分层契约
拿到.zip解压后,你会看到典型的目录树:
SuperMarketSystem/ ├── SuperMarketSystem.UI/ // WinForms窗体项目 ├── SuperMarketSystem.BLL/ // 业务逻辑层(Class Library) ├── SuperMarketSystem.DAL/ // 数据访问层(Class Library) ├── SuperMarketSystem.Model/ // 实体模型(Class Library) └── SuperMarketSystem.DB/ // 数据库文件(.mdf + .ldf)但目录存在 ≠ 分层成立。我们得用代码说话。打开SuperMarketSystem.UI/LoginForm.cs,搜索关键词SqlConnection、SqlCommand、SqlDataAdapter——如果这些出现在UI层,说明DAL已失守。再打开SuperMarketSystem.BLL/ProductService.cs,检查方法签名:是否所有方法参数和返回值都是Model.Product这类实体类?有没有直接传DataTable或DataSet?后者意味着BLL和DAL耦合过紧。最后看SuperMarketSystem.DAL/ProductDAO.cs:它是否只做CRUD,不包含任何“库存不足提醒”“会员折扣计算”这类业务规则?这才是三层的铁律:UI只负责展示和交互指令,BLL只封装业务规则,DAL只管数据搬运。
2.1 模型层(Model):不是DTO,是业务语义的锚点
SuperMarketSystem.Model项目里,Product.cs通常长这样:
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int StockQuantity { get; set; } public DateTime CreateTime { get; set; } }注意:这里没有属性验证逻辑(如[Required])、没有数据库映射特性(如[Column("ProductName")])、没有业务方法(如GetDiscountedPrice())。Model的唯一使命是承载跨层传递的数据结构。它必须是POCO(Plain Old CLR Object),干净得像一张白纸。常见错误是把Product塞进ToString()格式化逻辑,或加IsInStock计算属性——这会让BLL层无法复用该模型做不同场景的判断(比如采购入库时不需要关心“是否在售”)。我一般会额外建一个ProductQueryResult类专用于查询返回,避免Product被污染。
2.2 数据访问层(DAL):连接字符串藏在哪?才是安全第一课
打开SuperMarketSystem.DAL/DatabaseHelper.cs(或类似名称),找连接字符串初始化位置。正确做法是:
// SuperMarketSystem.DAL/DatabaseHelper.cs public static class DatabaseHelper { private static readonly string ConnectionString = ConfigurationManager.ConnectionStrings["SuperMarketDB"].ConnectionString; public static SqlConnection GetConnection() => new SqlConnection(ConnectionString); }而App.config中必须有:
<configuration> <connectionStrings> <add name="SuperMarketDB" connectionString="Data Source=.;Initial Catalog=SuperMarketDB;Integrated Security=true;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>提示:绝不能在DAL代码里硬编码连接字符串!否则换数据库服务器时要全局搜索替换,且密码明文暴露风险极高。
ConfigurationManager是.NET Framework时代标准方案;若项目用.NET Core/.NET 5+,则应通过IConfiguration注入,但本zip包大概率是Framework,我们按实际走。
DAL的核心方法示例(ProductDAO.cs):
public class ProductDAO { public List<Product> GetAllProducts() { var list = new List<Product>(); using (var conn = DatabaseHelper.GetConnection()) { conn.Open(); using (var cmd = new SqlCommand("SELECT * FROM Products", conn)) { using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Product { Id = Convert.ToInt32(reader["Id"]), Name = reader["Name"].ToString(), Price = Convert.ToDecimal(reader["Price"]), StockQuantity = Convert.ToInt32(reader["StockQuantity"]), CreateTime = Convert.ToDateTime(reader["CreateTime"]) }); } } } } return list; } public bool UpdateProduct(Product product) { const string sql = "UPDATE Products SET Name=@Name,Price=@Price,StockQuantity=@StockQuantity WHERE Id=@Id"; using (var conn = DatabaseHelper.GetConnection()) { conn.Open(); using (var cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@Name", product.Name); cmd.Parameters.AddWithValue("@Price", product.Price); cmd.Parameters.AddWithValue("@StockQuantity", product.StockQuantity); cmd.Parameters.AddWithValue("@Id", product.Id); return cmd.ExecuteNonQuery() > 0; } } } }关键点:
- 所有SQL语句用参数化(
@Name),杜绝SQL注入; using确保连接及时释放;- 方法名直白(
GetAllProducts、UpdateProduct),不带业务词(如GetAvailableProducts——这属于BLL职责); - 返回类型严格为
List<Product>或bool,不返回DataTable。
2.3 业务逻辑层(BLL):规则在这里结晶,而不是在UI里if-else
打开SuperMarketSystem.BLL/ProductService.cs,典型结构:
public class ProductService { private readonly ProductDAO _productDAO; public ProductService() { _productDAO = new ProductDAO(); // 简单构造,无DI容器时常见 } // 业务规则:库存不能为负,价格必须大于0 public bool AddProduct(Product product) { if (product.StockQuantity < 0 || product.Price <= 0) throw new ArgumentException("库存数量不能为负,价格必须大于0"); // 调用DAL执行插入 return _productDAO.InsertProduct(product); } // 复杂业务:销售时扣减库存,并检查是否低于预警线 public bool SellProduct(int productId, int quantity) { var product = _productDAO.GetProductById(productId); if (product == null) return false; if (product.StockQuantity < quantity) throw new InvalidOperationException($"商品{product.Name}库存不足,当前库存{product.StockQuantity}"); product.StockQuantity -= quantity; bool result = _productDAO.UpdateProduct(product); // 库存预警:低于10件发通知(此处简化为Console,实际应解耦) if (product.StockQuantity < 10) { Console.WriteLine($"警告:{product.Name}库存仅剩{product.StockQuantity},请及时补货!"); } return result; } }这里体现三层精髓:
- 输入校验(
if (product.StockQuantity < 0...))在BLL,UI只传原始数据; - 状态判断(
if (product.StockQuantity < quantity))在BLL,UI不感知库存逻辑; - 副作用处理(库存预警)在BLL,但
Console.WriteLine是临时占位,真实项目应定义IInventoryAlertService接口,由上层注入具体实现——这为未来对接邮件、短信、企业微信留了钩子; - 绝不出现SQL或Connection,只调用DAL方法。
2.4 表示层(UI):WinForms里如何做到“零SQL”?
SuperMarketSystem.UI/MainForm.cs中,添加商品按钮事件:
private void btnAddProduct_Click(object sender, EventArgs e) { try { var product = new Product { Name = txtProductName.Text.Trim(), Price = decimal.Parse(txtPrice.Text), StockQuantity = int.Parse(txtStock.Text) }; var productService = new ProductService(); // 或从IoC容器获取 bool success = productService.AddProduct(product); if (success) { MessageBox.Show("添加成功!"); LoadProductList(); // 刷新列表 } } catch (ArgumentException ex) { MessageBox.Show($"输入错误:{ex.Message}"); } catch (InvalidOperationException ex) { MessageBox.Show($"业务错误:{ex.Message}"); } catch (Exception ex) { MessageBox.Show($"系统错误:{ex.Message}"); } }关键纪律:
- UI只做三件事:收集用户输入 → 转成Model对象 → 调用BLL方法 → 处理BLL抛出的特定异常;
- 所有
try-catch捕获的是BLL定义的业务异常(ArgumentException、InvalidOperationException),而非SqlException——后者应在DAL内部处理并转为业务异常; LoadProductList()方法里,应调用ProductService.GetAllProducts(),而非自己去DAL查数据。
3. 数据库同步与迁移:从.mdf到真实部署的三道坎
.zip包里的SuperMarketSystem.DB/目录含.mdf和.ldf文件,这是SQL Server LocalDB或Express版的数据库文件。但直接双击运行,常遇到“数据库文件被占用”“登录失败”等问题。根源在于:开发机上的数据库实例配置,和目标部署环境不一致。
3.1 本地调试:用SQL Server Express Attach Database
- 确保已安装SQL Server Express(免费版);
- 打开SQL Server Management Studio (SSMS),连接
localhost\SQLEXPRESS; - 右键“数据库” → “附加” → 添加
SuperMarketSystem.DB/SuperMarketDB.mdf; - 在
App.config中修改连接字符串:<add name="SuperMarketDB" connectionString="Data Source=localhost\SQLEXPRESS;AttachDbFilename=|DataDirectory|\SuperMarketSystem.DB\SuperMarketDB.mdf;Integrated Security=True;Connect Timeout=30;" />注意:
|DataDirectory|会自动解析为程序运行目录(即bin\Debug),确保.mdf文件被复制到输出目录(项目属性 → 文件 → 属性 → “复制到输出目录”设为“始终复制”)。
3.2 生产部署:生成SQL脚本,脱离.mdf依赖
.mdf文件无法直接部署到客户服务器(尤其没装SQL Server)。必须导出为可执行SQL脚本:
- 在SSMS中右键附加后的数据库 → “任务” → “生成脚本”;
- 向导中选择“整个数据库”,设置“要编写脚本的数据类型”为“架构和数据”;
- 输出选项:勾选“将此脚本保存到文件”,编码选UTF-8;
- 生成的
SuperMarketDB_Script.sql需手动修改:- 删除
CREATE DATABASE语句(客户环境已有实例); - 将
USE [SuperMarketDB]改为USE [YourTargetDBName]; - 检查
INSERT语句中的GO分隔符,确保兼容性。
- 删除
部署时,在客户SQL Server上新建数据库,执行该脚本即可。
3.3 连接字符串加密:保护敏感信息
App.config中明文密码极危险。使用aspnet_regiis.exe工具加密:
# 命令行(以管理员身份运行) cd C:\Windows\Microsoft.NET\Framework\v4.0.30319 aspnet_regiis.exe -pef "connectionStrings" "C:\Path\To\Your\Project\bin\Debug"执行后,App.config中<connectionStrings>节点变为加密内容,运行时自动解密。注意:加密密钥绑定到本机,换机器需重新加密。
4. 避坑:那些让三层架构形同虚设的5个血泪现场
三层架构最大的陷阱,不是不会写,而是“看起来分了,实际全糊在一起”。以下是我在真实维护这个超市系统时踩过的坑,按现象→原因→解决列清:
4.1 现象:UI层直接调用DAL,BLL项目被闲置
原因:为赶进度,开发者在LoginForm.cs里写了new ProductDAO().GetUserByUsername(...),绕过UserService。久而久之,BLL变成摆设,所有业务逻辑散落在各个窗体里。
解决:用Visual Studio“查找所有引用”功能,搜索ProductDAO、CustomerDAO等DAL类名,定位所有UI层直接调用点,逐个重构为BLL方法调用。建立团队规范:UI层引用只允许BLL和Model,禁止引用DAL。
4.2 现象:BLL方法返回DataTable,UI层遍历渲染
原因:开发者认为DataTable比List<T>更灵活,可在UI层动态列绑定。但DataTable携带数据库元数据(列类型、约束),使BLL无法做纯业务计算,且序列化困难。
解决:强制BLL方法返回强类型集合。UI层用BindingSource绑定List<Product>,通过DataGridView.AutoGenerateColumns = true实现动态列,效果相同但类型安全。
4.3 现象:Model类里塞了数据库特性,导致跨平台失败
原因:为方便Entity Framework,给Product.cs加了[Table("Products")]、[Key]等特性,但本项目用的是原生ADO.NET,这些特性无用,且当未来想迁移到SQLite时,EF特性不兼容。
解决:Model层绝对纯净。数据库映射逻辑全部移至DAL(如ProductDAO中SQL字段名硬编码),或引入Dapper等轻量ORM,用[ExplicitColumns]显式控制。
4.4 现象:连接字符串写死在DAL,换环境要改源码
原因:DatabaseHelper.cs里private const string connStr = "...",导致测试环境、生产环境无法区分。
解决:严格使用ConfigurationManager.ConnectionStrings,并在不同环境部署时,用Web Deploy或PowerShell脚本替换App.config对应节点。
4.5 现象:事务跨多表操作时,BLL里手动Open/Close Connection
原因:SellProduct需同时更新Products表和插入SalesRecords表,开发者在BLL里new SqlConnection()并BeginTransaction,但忘记Commit或Rollback,或异常时未释放连接。
解决:将事务控制权交还DAL。BLL传入SqlConnection和SqlTransaction参数:
// BLL public bool SellProduct(SqlConnection conn, SqlTransaction trans, int productId, int quantity) { // ...业务逻辑 return _productDAO.UpdateProduct(conn, trans, product) && _saleRecordDAO.InsertRecord(conn, trans, record); }UI层统一管理连接和事务生命周期,BLL只专注业务。
5. 进阶验证:用单元测试证明三层真的可拆、可替、可测
光看代码结构不能证明分层有效,只有测试能撕开伪装。本节教你用NUnit(.NET Framework)为BLL层写三个关键测试,验证分层契约是否成立。
5.1 测试前提:解耦DAL依赖,引入接口
先改造ProductService,使其不依赖具体ProductDAO:
// SuperMarketSystem.BLL/IProductDAO.cs public interface IProductDAO { List<Product> GetAllProducts(); bool UpdateProduct(Product product); } // SuperMarketSystem.BLL/ProductService.cs(重构后) public class ProductService { private readonly IProductDAO _productDAO; // 构造函数注入,便于测试时传入Mock public ProductService(IProductDAO productDAO) { _productDAO = productDAO; } public bool UpdateProductPrice(int productId, decimal newPrice) { var product = _productDAO.GetAllProducts().FirstOrDefault(p => p.Id == productId); if (product == null) return false; product.Price = newPrice; return _productDAO.UpdateProduct(product); } }5.2 编写测试:验证BLL不依赖SQL Server
创建测试项目SuperMarketSystem.Tests,引用NUnit和Moq:
[TestFixture] public class ProductServiceTests { [Test] public void UpdateProductPrice_WhenProductExists_ReturnsTrue() { // Arrange: Mock DAL,返回预设数据 var mockDAO = new Mock<IProductDAO>(); var products = new List<Product> { new Product { Id = 1, Name = "苹果", Price = 5.0m, StockQuantity = 100 } }; mockDAO.Setup(x => x.GetAllProducts()).Returns(products); mockDAO.Setup(x => x.UpdateProduct(It.IsAny<Product>())).Returns(true); var service = new ProductService(mockDAO.Object); // Act bool result = service.UpdateProductPrice(1, 6.0m); // Assert Assert.IsTrue(result); Assert.AreEqual(6.0m, products[0].Price); // 验证BLL修改了Model } [Test] public void UpdateProductPrice_WhenProductNotFound_ReturnsFalse() { var mockDAO = new Mock<IProductDAO>(); mockDAO.Setup(x => x.GetAllProducts()).Returns(new List<Product>()); var service = new ProductService(mockDAO.Object); bool result = service.UpdateProductPrice(999, 10.0m); Assert.IsFalse(result); } }关键点:测试中完全不启动SQL Server,不连接数据库。
mockDAO模拟DAL行为,BLL逻辑在内存中验证。这证明:只要BLL依赖抽象(IProductDAO),它就能脱离数据库独立测试。
5.3 真实场景:为库存预警添加可插拔通知器
原SellProduct方法里Console.WriteLine是硬编码。现在将其解耦:
// 定义通知接口 public interface IInventoryAlertService { void AlertLowStock(string productName, int currentStock); } // BLL中注入 public class ProductService { private readonly IProductDAO _productDAO; private readonly IInventoryAlertService _alertService; public ProductService(IProductDAO productDAO, IInventoryAlertService alertService) { _productDAO = productDAO; _alertService = alertService; } public bool SellProduct(int productId, int quantity) { // ...省略业务逻辑 if (product.StockQuantity < 10) { _alertService.AlertLowStock(product.Name, product.StockQuantity); } return _productDAO.UpdateProduct(product); } } // 实现邮件通知(生产环境) public class EmailAlertService : IInventoryAlertService { public void AlertLowStock(string productName, int currentStock) { // 发送邮件逻辑 } } // 实现控制台通知(开发环境) public class ConsoleAlertService : IInventoryAlertService { public void AlertLowStock(string productName, int currentStock) { Console.WriteLine($"低库存警报:{productName}剩余{currentStock}件"); } }UI层初始化时:
// 开发时 var service = new ProductService(new ProductDAO(), new ConsoleAlertService()); // 生产时 var service = new ProductService(new ProductDAO(), new EmailAlertService());这就是三层的价值:BLL不变,只换实现,就能从控制台日志切换到企业微信机器人推送。
我带新人做超市系统,第一课永远不是教怎么连数据库,而是让他们删掉UI层里所有SqlConnection,再跑一遍。当所有按钮都弹出“未实现业务逻辑”异常时,他们才真正看见三层的骨架。后来有次客户要求加人脸识别入库,我们只新增了FaceRecognitionService实现IInventoryScanner接口,BLL和UI一行未动——那晚加班到凌晨三点,但没人抱怨,因为大家心里清楚:分层不是为了写更多代码,而是让下次改需求时,你能笑着喝杯咖啡,而不是跪着修bug。希望帮到你。
本文还有配套的精品资源,点击获取