
简介基于C#的超市管理系统是一套完整的源码与数据库打包资源面向需要完成课程设计、毕业设计或学习C#窗体开发与数据库交互的开发者。系统包含商品管理、采购管理、销售管理、会员管理、库存预警和报表生成等核心功能基本覆盖超市日常运营的主要业务流程。压缩包内共有五十九个文件整体体积约两兆。文件以C#源码为主并配有界面资源、工程配置、数据库数据文件和可执行程序等结构清楚可以直接打开编译运行也方便二次修改。目前已有119人学习下载。资源附带了完整的数据库备份附加数据文件后即可连接省去了手动建表和初始化的步骤项目采用系统身份验证登录配合开发环境即可启动。对于想了解超市业务表结构、窗体事件处理以及数据访问层写法的读者这套源码提供了一个完整且可直接运行的参考范例。1. 基于C#的超市管理系统源码包到手后先确认它是能跑的拿到“基于C#的超市管理系统源码数据库.zip”第一反应别急着解压看代码先把它跑起来。这类压缩包十有八九是课程设计、毕业设计或小型进销存项目的底子里面是一个 Visual Studio 解决方案加一份数据库备份或 SQL 脚本。它解决的实际问题很窄商品建档、库存扣减、前台收银、简单统计。适合两类人一是刚学 C# 和数据库想找一个完整链路照着改的人二是小门店想低成本落地一套系统先看功能是否匹配的人。源码的价值不在“能编译”而在你能改得动、数据表能对得上。我一般会先确认 .sln 的框架版本、数据库类型、连接字符串位置这三样决定你能否在两小时内把登录窗亮出来。2. 跑通源码前的三步环境、数据库还原、连接字符串2.1 开发环境与版本选择WinForms 还是 WPFFramework 还是 .NET 8一打开 .sln 先看两处项目类型和目标框架。超市管理系统绝大多数用 WinForms少数用 WPF。WinForms 的好处是控件拖拽快DataGridView 直接绑定数据库结果集适合这种表单密集的桌面应用。WPF 界面更好看但改动成本高压缩包拿到什么就用什么别在第一步重写 UI。常见做法是 .NET Framework 4.6.1 到 4.8配 Visual Studio 2019/2022。用 VS2022 打开老项目时会弹出“需要 retarget”我一般直接确认把目标框架升到当前机器已有的 .NET Framework 版本编译通过率最高。如果项目里引用了第三方组件比如报表控件升级框架后可能报错这时候先不升级宁可去“Visual Studio Installer”里补装老版本开发组件也别让一堆依赖变成黑匣子。这里要特别提醒看引用比看名字更准。如果项目引用的是 System.Data.SqlClient数据库大概率是 SQL Server如果引用的是 MySql.Data 或 Pomelo.EntityFrameworkCore.MySql那就是 MySQL。这个区别决定后面还原数据库的方式完全不同。不要看到 .bak 就默认是 SQL Server先看 packages.config 或 .csproj 里的引用。2.2 恢复数据库不要直接附加用备份还原压缩包里常见的数据库交付形式有三种.bak 备份文件、.sql 脚本、.mdf/.ldf 附加文件。拿到 .bak 时不少人用 SSMS 右键“附加”结果失败因为附加只认 .mdf。正确做法是“还原数据库”。USE master; GO RESTORE DATABASE SuperMarketDB FROM DISK NE:\SuperMarket\SuperMarketDB.bak WITH MOVE SuperMarketDB_Data TO NE:\SuperMarket\SuperMarketDB.mdf, MOVE SuperMarketDB_Log TO NE:\SuperMarket\SuperMarketDB_log.ldf, REPLACE, RECOVERY; GO这段代码先切到 master 库避免目标库正被占用。MOVE 后面的逻辑名要先从 .bak 里读出来常见做法是先用RESTORE FILELISTONLY FROM DISK NE:\SuperMarket\SuperMarketDB.bak查看逻辑名再填进 MOVE。REPLACE 表示覆盖同名数据库RECOVERY 表示还原后进入可用状态。不要同时开着 SSMS 的表设计窗口或查询窗口指向这个库否则还原会因文件占用而卡住。如果给的是 .sql 脚本直接在 SSMS 里新建查询执行即可但要注意脚本开头是否包含CREATE DATABASE如果没有先手动建一个空库再执行表结构脚本。这一步最常翻车在脚本里的日志路径比如FILENAME NC:\Program Files\...在不同机器上不存在报错后把路径改成当前实例实际目录就行。提示还原前先确认 SQL Server 服务账号对目标目录有写权限。否则报“操作系统错误 5(拒绝访问)”不是命令写错是权限问题。2.3 连接字符串改对三个地方登录窗才不白屏源码能编译、数据库也还原之后最卡人的是登录窗一直报“建立连接时出错”。这类系统的连接字符串通常集中在 App.config、Web.config 或一个DbHelper.cs类里先全局搜索Data Source。connectionStrings add nameSuperMarketDB connectionStringData Source.;Initial CatalogSuperMarketDB;User IDsa;Password123456;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / /connectionStrings这里三个地方最容易错。Data Source 是 SQL Server 实例名本机默认实例填.或localhost命名实例要填.\SQLEXPRESS这取决于你装的 SQL Server 实例。User ID 和 Password 对应 SQL Server 登录名如果系统把 sa 禁用、只开 Windows 登录那需要先用管理员身份打开 SSMS在服务器右键“属性→安全性”里启用 SQL Server 和 Windows 身份验证模式然后重启 SQL Server 服务。TrustServerCertificateTrue 是给新版 SqlClient 用的避免证书校验报错如果你在 .NET Framework 4.7.2 以下的旧项目里不存在这一项就维持原样别乱加。改完连接字符串后先做一个最小连通测试在解决方案里临时建一个控制台项目写几行代码using(SqlConnection conn new SqlConnection(connStr)) { conn.Open(); Console.WriteLine(ok); }。如果这里能过登录窗还白屏问题就不在数据库而在主窗体构造里抛了异常要去看 VS 输出窗口和事件日志里的 .NET Runtime 错误。3. 从数据库设计反推系统功能六张表撑起一个超市3.1 商品与分类先有树形结构才有前台可言打开数据库看到表比看代码更能理解系统边界。超市管理系统的核心是商品表字段通常有 ProdId、ProdCode条码或编码、ProdName、CategoryId、Unit、SalePrice、StockQty、WarningQty。条码要么手录要么扫码枪输入前台查商品时第一查询条件就是它所以在数据库上一定要给 ProdCode 建唯一索引否则数据一多收银台查一条要顿一下。分类表常见做法是自关联父级分类用 ParentId 实现“食品 饮料 碳酸饮料”这种层级。如果源码里分类表只有一个分类名字段系统就只支持一级分类这在数据库字段上是能提前看出来的功能边界。分类层级影响后续报表的汇总口径也影响补货单能不能按大类筛选。拿到压缩包先看这两张表就能判断这个系统值得大改还是小改。ALTER TABLE Product ADD CONSTRAINT FK_Product_Category FOREIGN KEY (CategoryId) REFERENCES Category(CategoryId); GO上面这句是常见的补外键操作。很多课程设计源码为了导入方便故意不建外键导致商品表里能插入一个 CategoryId 为 0 的孤立数据。你把它补上后收银、库存、报表都会更稳。代价是以后删分类得先处理商品这正是外键存在的意义。3.2 销售主表与明细表一主一从是记账的底线超市收银不能只记一个总数。最少要有 SaleOrder 主表和 SaleOrderDetail 明细表主表存单号、收银员、时间、应收、实收明细表存每一条商品的商品 ID、数量、单价、折扣。单号一般用时间加流水号生成比如202502141530001也有直接 Identity 自增的。自增简单但对账时不好和线下小票对应我比较推荐在代码里生成业务单号。关键点主表与明细表必须通过 SaleOrderId 关联并且在事务里同时写入。如果你发现源码里只有一张销售流水表每条记录存商品名和数量那说明它不是真正的进销存日报表和退货单都会很难写。拿到这种系统改造的第一步是先拆成两表而不是继续打补丁。CREATE TABLE SaleOrder ( SaleOrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL UNIQUE, CashierId INT NOT NULL, SaleTime DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(18,2) NOT NULL, Received DECIMAL(18,2) NOT NULL, ChangeAmt DECIMAL(18,2) NOT NULL ); CREATE TABLE SaleOrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, SaleOrderId INT NOT NULL, ProdId INT NOT NULL, Qty INT NOT NULL, Price DECIMAL(18,2) NOT NULL, SubTotal DECIMAL(18,2) NOT NULL );主表里 CashierId 指向用户表SaleTime 默认取数据库时间避免客户端时间不准。明细表里 SubTotal 可以直接存也可以查询时用 Qty * Price 算存下来的好处是订单历史价格不会被商品表调价影响。这就是为什么要在明细表里冗余一个 Price商品当前售价会变但订单里的成交价必须固定。3.3 用户表与权限字段别把密码明文放进数据库用户表是另一个容易被忽略的安全底线。很多课堂项目把密码直接存明文字符串登录时WHERE UserNameadmin AND UserPwd123456能跑但任何拿到源码的人都能看到管理员口令。更麻烦的是商品定价和库存变更都用同一个账号出了问题查不出是谁。理想情况是至少加两个字段Salt盐值和 DisplayName显示名哪怕不引入员工表也要让每个登录账号在操作日志里能对应到人。CREATE TABLE Users ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, UserPwd NVARCHAR(64) NOT NULL, Salt NVARCHAR(32) NOT NULL, DisplayName NVARCHAR(50) NULL, RoleId TINYINT NOT NULL DEFAULT 2, IsActive BIT NOT NULL DEFAULT 1 );UserPwd 存的是加盐哈希不是密码本身。代码里取出一段随机字符串作为盐把盐拼到密码后面做 SHA256再把盐和哈希一起存库。登录时用同一条盐重新算一遍比对。这样即使有人拿到数据库文件也得不到明文口令。RoleId 为 1 表示管理员2 表示收银员3 表示仓管代码里打开业务窗体前判断角色就好不需要上 RBAC 那一套重型模型。4. 核心功能的 C# 实现登录、商品管理、收银结账4.1 登录校验参数化 SQL 先封死注入再谈功能很多老源码的登录代码长这样字符串拼接SELECT COUNT(*) FROM Users WHERE UserName txtUser.Text AND UserPwd txtPwd.Text 。这个写法在课程演示里没问题但放到真实环境就是灾难文本框里输一个 OR 11 --就能绕过口令。我把这个当成接手源码后第一个要改的点。private bool ValidateLogin(string userName, string password) { string connStr ConfigurationManager.ConnectionStrings[SuperMarketDB].ConnectionString; string sql SELECT COUNT(1) FROM Users WHERE UserName u AND UserPwd p AND IsActive 1; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.Add(u, SqlDbType.NVarChar, 50).Value userName; cmd.Parameters.Add(p, SqlDbType.NVarChar, 64).Value password; conn.Open(); return (int)cmd.ExecuteScalar() 1; } }这里做两件事第一所有条件都走参数化不让用户输入直接拼进 SQL第二查询里加IsActive 1即使数据库里留着离职员工的账号也能在入口处拦住。注意cmd.Parameters.Add比AddWithValue更可控因为AddWithValue对 NVarChar 和 VarChar 的推断不稳定容易让索引失效。密码字段实际存的是加盐哈希所以上层先算好哈希再交给这个方法不要把加密逻辑写进 SQL 里。4.2 商品管理DataGridView 绑定与增删改查商品管理窗体通常是一个 DataGridView 加四个按钮和几个文本框。常见做法是窗体加载时用 DataTable 接查询结果直接赋给 DataGridView.DataSource。这里有一个容易踩的坑修改单元格后直接点保存其实绑定数据源还没结束编辑要先把 DataSource 转成 DataTable并且调用BindingContext的EndCurrentEdit()。private void btnSave_Click(object sender, EventArgs e) { if (dgvProducts.CurrentRow null) return; // 先结束编辑否则改动还停留在控件里 dgvProducts.EndEdit(); DataTable dt (DataTable)dgvProducts.DataSource; DataRow row dgvProducts.CurrentRow.DataBoundItem as DataRowView; if (row null) return; string sql UPDATE Product SET ProdName name, SalePrice price, StockQty qty WHERE ProdId id; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.Add(name, SqlDbType.NVarChar).Value row[ProdName]; cmd.Parameters.Add(price, SqlDbType.Decimal).Value row[SalePrice]; cmd.Parameters.Add(qty, SqlDbType.Int).Value row[StockQty]; cmd.Parameters.Add(id, SqlDbType.Int).Value row[ProdId]; conn.Open(); cmd.ExecuteNonQuery(); LoadProductList(); // 重新查询刷新 } }逻辑说明很简单先从当前行拿数据然后执行单行 UPDATE。参数全部从 DataRow 里取避免 DataGridView 显示格式和数据库字段类型不一致。LoadProductList()是独立的查询方法重新 SELECT 一次而不是只修改界面因为库存数量可能被其他窗口改过。新增商品和删除商品同理唯一要注意的是删除前先判断有没有销售明细引用它否则外键会报错。更稳妥的做法是给 Product 表加一个 IsDeleted 字段删除时只做软删除查询时默认过滤。这也是我从“能用”到“敢用”的一个血泪经验真实的超市里商品档案被误删造成的对账问题比想象中难处理得多。4.3 收银结账事务保住的不是性能是账收银是超市系统里最容易出乱子的功能。点“结账”要同时做三件事写入销售主表、写入明细表、扣减库存。这三件事任何一个失败账就对不上。如果不用事务前两步成功、第三步失败库存会比实际多而第一步失败、后两步成功就更麻烦。所以结账方法必须包在一个SqlTransaction里。using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) using (SqlCommand cmd conn.CreateCommand()) { cmd.Transaction tran; try { cmd.CommandText INSERT INTO SaleOrder(OrderNo, CashierId, TotalAmount, Received, ChangeAmt) VALUES(orderNo, cashierId, total, received, change); SELECT SCOPE_IDENTITY();; cmd.Parameters.AddWithValue(orderNo, GenerateOrderNo()); cmd.Parameters.AddWithValue(cashierId, currentUserId); cmd.Parameters.AddWithValue(total, totalAmount); cmd.Parameters.AddWithValue(received, received); cmd.Parameters.AddWithValue(change, received - totalAmount); int orderId Convert.ToInt32(cmd.ExecuteScalar()); cmd.Parameters.Clear(); foreach (var item in cart) { cmd.CommandText INSERT INTO SaleOrderDetail(SaleOrderId, ProdId, Qty, Price, SubTotal) VALUES(orderId, prodId, qty, price, sub); UPDATE Product SET StockQty StockQty - qty WHERE ProdId prodId;; cmd.Parameters.AddWithValue(orderId, orderId); cmd.Parameters.AddWithValue(prodId, item.ProdId); cmd.Parameters.AddWithValue(qty, item.Qty); cmd.Parameters.AddWithValue(price, item.Price); cmd.Parameters.AddWithValue(sub, item.Qty * item.Price); cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); } tran.Commit(); } catch { tran.Rollback(); throw; } } }说明几点SCOPE_IDENTITY()获取刚插入的主表自增 ID比IDENTITY安全因为后者会受到触发器影响。每一次循环都要先Clear()再重新AddWithValue否则上一件商品的参数会残留。事务里先插明细再扣库存顺序统一后续排查日志时更清楚。一个真实项目里我还会在 UPDATE 库存那句加上条件AND StockQty qty然后用ExecuteNonQuery()的返回值判断是否扣减成功。返回 0 说明库存不足直接抛业务异常回滚整单而不是让库存变成负数。这才是收银事务最关键的一层保护。5. 超市管理系统避坑指南从编译报错到运行时翻车5.1 编译报错 CS0246命名空间或类型找不到现象解决方案打开后一生成就报“找不到类型或命名空间 SqlConnection”或者报找不到某个报表控件。原因缺引用。老项目交付时经常没有带上 packages 文件夹或者你用的是 .NET 6/8 环境而项目引用的是 .NET Framework 组件。另一个原因是代码里using System.Data.SqlClient;没写但 Visual Studio 的错误列表往往把真正缺的引用藏住了。解决先看 .csproj 里有没有Reference IncludeSystem.Data /没有就右键“添加引用”补上。如果缺的是 NuGet 包打开包管理器控制台执行Install-Package System.Data.SqlClient再重新生成。我习惯把“生成”改成“重新生成解决方案”因为增量编译有时不重新加载新引用。5.2 登录时提示 Login failed for user sa现象连接字符串没改数据库也还原了但登录窗体一点就报登录失败或者报“Cannot open database”。原因SQL Server 默认不允许 sa 空密码登录甚至默认禁用 sa。另一个原因是连接字符串里的 Initial Catalog 跟你还原出来的数据库名不一致比如还原成了 SuperMarketDB代码里写的是 Supermarket。还有一种是数据库文件还在原机器路径下还原时没指定 MOVE 到本机目录。解决先用 SSMS 用 Windows 身份登录检查“安全性→登录名→sa”是否启用并设置一个强密码。然后在服务器属性里打开 SQL Server 和 Windows 身份验证模式重启服务。最后执行一遍上面第 2.2 节的 RESTORE 语句确认数据库名和连接字符串完全一致。如果还不行在 SSMS 里直接执行SELECT SERVERNAME看实例名连接字符串不要猜。5.3 DataGridView 编辑后点保存数据没变化现象界面上改了商品单价点保存没报错但重新打开还是旧值。原因DataGridView 的编辑状态没有结束绑定 DataSource 里的 DataRow 还没收到控件里的新值。另一个原因是代码里更新后没有重新查询只是改了 DataTable但保存方法用的还是另一份旧 DataTable。解决保存前先调用dgv.EndEdit()再取BindingContext[dgv.DataSource].EndCurrentEdit()。然后直接在当前行对应的 DataRowView 上做 UPDATE。保存成功后重新执行查询并重新绑定别信任模棱两可的状态。这一点看似基础却是我接手源码时最常看到的问题。5.4 报表或导出表格中文乱码现象程序运行正常但导出的 CSV、TXT或者 RDLC 报表里的中文变成问号。原因编码不对。超市管理系统如果历史数据是用 GB2312 或 GBK 存的你用 UTF-8 导出就会乱反过来新系统默认 UTF-8老控件用 ANSI 读也会乱。SQL Server 里 NVarChar 一般不会乱乱在文件读写那一步。解决导出文件时显式指定编码比如File.WriteAllText(path, content, Encoding.UTF8)。如果对接的老 Excel 模板只认 ANSI就改用Encoding.GetEncoding(GB2312)。数据库连接字符串里如果是 MySQL加上CharSetutf8mb4;同时把表的排序规则统一成 utf8mb4_general_ci。5.5 并发收银把库存卖成负数现象两台收银机同时卖同一件商品扣完库存后出现负数或者订单号和单号重复。原因两个进程同时读库存都读到 10各自卖了 10 件最后数据库写成 0但实际销售了 20 件。另一个原因是代码没有使用事务或者 UPDATE 库存没有把“库存足够”作为条件。解决把库存扣减写成原子操作UPDATE Product SET StockQty StockQty - qty WHERE ProdId prodId AND StockQty qty并检查影响行数。配合第 4.3 节的整个事务能同时防止超卖和单号重复。如果并发量再大就给 SaleOrder 的 OrderNo 加唯一约束让数据库兜底拒绝重复单号而不是靠代码里的 Random 或时间戳碰运气。6. 从能用变顺手报表、导出与二次开发的三个着力点一套超市管理系统的源码跑通只是及格线。真正要投入日常使用我会优先做三件事按性价比排第一加一张“当日销售汇总”报表按收银员和商品两个维度汇总这是门店每天对账的刚需。第二把商品导入导出做起来用 Excel 或 CSV 批量维护商品比在 DataGridView 里一行行改高效得多。第三给所有扣减库存的方法加上库存不足的业务校验并把异常日志写到本地文本文件。报表我一般用 RDLC 或第三方报表控件。如果源码里已经带报表先别替换先看它的数据源是不是直连数据库。很多时候报表慢不是报表本身而是 SQL 里没有按日期过滤把全表数据拖到内存里再筛。优化办法是在 SQL 层加WHERE SaleTime start AND SaleTime end让数据库先过滤而不是等客户端处理。批量导入如果要写代码建议用SqlBulkCopy它比循环 INSERT 快一个数量级。但务必注意SqlBulkCopy对列顺序和列名敏感目标表结构一变动导入就可能翻车。稳妥做法是先 SELECT 目标表的结构到一个 DataTable再按列名映射。这也是热词里常被搜“sqlbulkcopy 表变动有影响”的原因我的习惯是导入前先备份或先导入到临时表核对无误后再合并。我的教训是别把源码当成终点要当成一个可改动的起点。数据库表结构、连接字符串、事务边界这三样先搞清楚后面加功能就不会越改越乱。找到一个能跑的版本之后立刻把数据库备份文件复制一份放到别的机器上这是你最重要的“后悔药”。希望帮到你。本文还有配套的精品资源点击获取