简介:基于C#与SQL Server 2008开发的超市管理系统源码与数据库包,适合需要学习桌面数据库应用开发的学生、初级程序员,也适合有超市信息化实践需求的项目使用者。系统覆盖商品管理、采购管理、销售管理、会员管理、库存预警与报表生成等业务模块,支持维护商品名称、价格与库存量,记录供应商进货信息、销售流水与会员积分,能够直观了解超市进销存的完整流程。压缩包共59个文件,大小约1.97MB,以C#窗体代码、资源文件、数据集定义、数据库附加文件及可执行程序为主,同时包含工程配置、项目文件与说明文档,可在Visual Studio 2010中直接打开运行。已有119人浏览学习。实际代码的实践价值在于:可查看登录、员工信息维护、主页等窗体的界面布局与事件处理,理解数据集绑定和数据库连接类的调用;同时学习Windows身份验证登录方式以及附加数据库文件的方法,便于课程设计或后续二次开发,也可在此基础上扩展采购报表与库存预警等模块。
1. 这个C#超市管理系统zip值不值得解压:先看懂它装了什么
验收现场最常见的一幕:你把一件商品卖出去,库存却没减。导师皱着眉说,回去把账对平再来。这类名为“基于C#的超市管理系统”的源码包,正好卡在每个C#入门者都会撞上的墙——界面能画、单表能增删改查,但多表联动和事务一多就乱。zip里装的通常是一个Visual Studio解决方案加SQL Server数据库文件,覆盖商品、进货、销售、用户权限四块最朴素的C#增删改查逻辑。把这套结构拆开,你就能判断它值不值得改成自己的课设或小店后台;拆不开,它只是个双击就报错的灰色文件夹。下面按落地顺序拆:先看骨架,再跑起来,再动手改,最后一节给生产级补丁。
2. 拆源码骨架:三层结构、8张表和登录调用链,决定从哪下手改
拿到这份C#超市管理系统源码,第一件事不是双击.sln,而是先看目录结构。这类课设项目九成是WinForms加ADO.NET,看懂它的分层方式,就等于拿到了整份源码的命门。
2.1 用目录结构判断分层:找到SQLHelper就找到了命门
解压后一般能看到三类东西:解决方案文件(.sln)、工程文件夹(.csproj)、数据库文件(.bak或.sql)。工程文件夹里通常有bin、obj这两个编译产物目录,可以直接忽略;真正要看的,是那些Form开头的.cs文件,以及一个叫SQLHelper.cs或DBHelper.cs的类文件。这个文件是整个C#系统里所有数据库操作的入口,先打开它。
// SQLHelper.cs —— 这类管理系统里最常见的集中式数据访问入口 using System.Data; using System.Data.SqlClient; public class SQLHelper { // 课设版本最常见的写法:连接串写死在类里 private static readonly string connStr = "Server=.;Database=SuperMarket;User ID=sa;Password=123456;"; // 执行增删改,返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } // 查询返回一张表 public static DataTable ExecuteQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlDataAdapter da = new SqlDataAdapter(sql, conn)) { if (ps != null) da.SelectCommand.Parameters.AddRange(ps); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } // 查询返回单个值,登录时数行数、查重名都用它 public static object ExecuteScalar(string sql, params SqlParameter[] ps) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteScalar(); } } }这段代码里有几个关键信息。第一,连接字符串用的是SQL Server的SqlClient,说明这套源码的数据库是SQL Server而不是MySQL。第二,connStr写死在类里,意味着后面改数据库路径时要改这里,而不是找配置文件。第三,三个方法都用了using,这是标准写法,如果看到某个版本没有using,就要小心连接泄漏问题。第四,ExecuteQuery用DataAdapter填充DataTable,它会自动处理连接开关;而ExecuteScalar和ExecuteNonQuery要自己Open和Close,忘记Close就会出现第5章里说的连接池耗尽。
判断完分层再回头看目录:如果工程里只有Form目录加SQLHelper,这是两层结构;如果还有BLL(业务逻辑层)和DAL(数据访问层)文件夹,就是标准三层。两层结构适合课设,改起来快;三层结构适合后续扩展。拿到源码第一步,搜索一下“new SqlConnection”出现在几个文件里——如果只在SQLHelper里出现,代码质量尚可;如果每个Form里都new了一次连接,这代码要重构的地方就多了。
2.2 8张核心表:先看外键关系,再决定功能加在哪
超市管理系统的数据库设计大同小异,常见的是8张表左右。打开SSMS连上数据库后,先别急着跑,把下面这张表和字段清单过一遍。
| 表名 | 核心字段 | 作用 |
|---|---|---|
| Users | UserID, UserName, Password, Role | 登录账号和角色 |
| Category | CategoryID, CategoryName | 商品分类 |
| Supplier | SupplierID, SupplierName, Phone | 供应商 |
| Product | ProductID, ProductName, CategoryID, SupplierID, Price, Stock | 商品主表 |
| PurchaseOrder | OrderID, OrderNo, SupplierID, OrderDate, TotalAmount | 进货单主表 |
| PurchaseDetail | DetailID, OrderID, ProductID, Qty, Price | 进货明细 |
| SalesOrder | SalesID, OrderNo, CashierID, SaleDate, TotalAmount | 销售单主表 |
| SalesDetail | DetailID, SalesID, ProductID, Qty, Price | 销售明细 |
在SSMS的数据库关系图里把这些表拖出来,能看到典型的“主表-从表”结构:SalesOrder和SalesDetail通过SalesID关联,PurchaseOrder和PurchaseDetail通过OrderID关联,Product通过CategoryID关联Category。改代码之前先把这张关系图理清,因为后面增删改查最容易翻车的点就在这里——删一个商品时,销售明细表里还挂着它的外键,DELETE直接被数据库拒绝。
字段命名也是坑。不同表里同一商品,有的叫ProductID,有的叫PID,有的叫GoodID,多表JOIN时要手工映射。搜一下源码里出现最多的JOIN语句,能帮你快速建立字段对应关系。再提醒一句:Users表里Role字段可能是int(0管理员、1收银员),也可能是nvarchar(存“管理员”三个字),改权限功能前先确认它是什么类型,免得比较时类型不匹配。
2.3 登录调用链:参数化查询、明文密码和当前用户传递
几乎所有C#超市管理系统源码的第一个功能都是登录。这个功能虽然简单,却是整条调用链的缩影:按钮事件 -> 拼接SQL或参数化SQL -> SQLHelper执行 -> 返回结果 -> 跳转窗体。看懂了登录,你就看懂了这套源码的代码风格。
// frmLogin.cs 登录按钮的典型写法 private void btnLogin_Click(object sender, EventArgs e) { string sql = "SELECT COUNT(1) FROM Users WHERE UserName=@u AND Password=@p"; SqlParameter[] ps = { new SqlParameter("@u", txtUser.Text.Trim()), new SqlParameter("@p", txtPwd.Text) // 课设常见:明文密码入库 }; int count = Convert.ToInt32(SQLHelper.ExecuteScalar(sql, ps)); if (count > 0) { // 跳转主窗体 this.Hide(); new frmMain().Show(); } else { MessageBox.Show("用户名或密码错误"); } }这段代码用了参数化查询,值得表扬。但注意两个常见毛病:密码是明文存储的,数据库里看一眼Users表就知道所有人的密码;跳转主窗体时只new了frmMain,没有把当前登录用户的ID和角色带过去,后面主窗体想判断“该不该显示某个按钮”,还得再查一次数据库。这是C#入门项目最典型的“能用但不优雅”。
如果源码里出现的是这种写法——string sql = "SELECT * FROM Product WHERE ProductName LIKE '%" + txtName.Text + "%'"——那就是字符串拼接SQL,不但有注入风险,商品名里带个单引号还会让程序直接崩。遇到这种代码,统一改成参数化写法,这也是后面二次开发的第一步。
3. 把数据库还原进SQL Server再按F5:最小环境四步跑通
源码能不能跑,九成取决于数据库能不能挂上。这章按常见做法走四步:还原数据库、改连接字符串、重新生成解决方案、首次登录验证。每一步都对应一个具体报错点。
3.1 先用RESTORE FILELISTONLY确认备份逻辑名,再还原
压缩包里的数据库文件基本有两种形态:.bak备份文件,或者.mdf/.ldf数据文件。先处理.bak形态。很多人直接在SSMS里右键“还原数据库”选文件,结果报“媒体集有误”或“找不到文件”,原因是备份文件里的逻辑名和界面显示不一致。最稳的办法是先查逻辑名。
-- 在SSMS新建查询里执行,先看备份里到底有哪些逻辑文件 RESTORE FILELISTONLY FROM DISK = N'D:\SuperMarket\db\SuperMarket.bak';执行结果会列出LogicalName、物理文件名等信息。常见的LogicalName是SuperMarket和SuperMarket_log。拿到这两个名字后,再写完整的还原脚本,注意MOVE后面的目标路径要写本机实际存在的目录。
RESTORE DATABASE SuperMarket FROM DISK = N'D:\SuperMarket\db\SuperMarket.bak' WITH MOVE 'SuperMarket' TO N'D:\SuperMarket\db\SuperMarket.mdf', MOVE 'SuperMarket_log' TO N'D:\SuperMarket\db\SuperMarket_log.ldf', REPLACE;这段脚本里REPLACE的意思是不管目标数据库是否存在,直接覆盖还原,适合反复测试的场景。如果压缩包里不是.bak而是.sql脚本文件,就简单得多:在SSMS里新建查询,打开这个.sql文件,直接执行即可。.sql文件的缺点是如果里面有几百条INSERT,执行时间会长;优点是版本兼容性比.bak好,高版本库导出的脚本在低版本上也能跑。
如果解压出来的是.mdf和.ldf两个文件,说明别人直接把数据文件拷了出来,这时用附加方式:
CREATE DATABASE SuperMarket ON (FILENAME = N'D:\SuperMarket\db\SuperMarket.mdf'), (FILENAME = N'D:\SuperMarket\db\SuperMarket_log.ldf') FOR ATTACH;注意附加时两个文件的路径要写全,ldf文件如果丢了或改名,要先用sp_attach_single_file_db处理,但那种情况属于文件损坏,不如找原始zip重新解压来得快。这里有个血泪经验:.bak文件是高版本SQL Server(比如2022)备份出来的,低版本(比如2016)还原时会直接报版本不兼容。解决最快的路径是装一个同版本或更高版本的SQL Server Express Developer版,别想着转格式。
3.2 改连接字符串:Server、Database、身份验证三个值对准本机
数据库挂上之后,连接字符串不对一样跑不起来。常见做法是连接串写在App.config里,但很多课设版本就像第2章那样写死在SQLHelper里。先检查有没有App.config或Web.config。
<!-- App.config 里的连接字符串节点 --> <connectionStrings> <add name="SMConn" connectionString="Server=.\SQLEXPRESS;Database=SuperMarket;User ID=sa;Password=123456;" providerName="System.Data.SqlClient"/> </connectionStrings>三个最常改错的值:Server写的是数据库实例名。本机装的是默认实例,写.或localhost;装的是命名实例,要写成.\SQLEXPRESS。Database写的是数据库名,必须和还原出来的库名一模一样,大小写不敏感但名字不能错。User ID和Password对应SQL Server登录账号,常见的是sa加一个简单密码。如果源码里用的是Windows身份验证,连接串要改成Integrated Security=SSPI;,并把User ID和Password删掉。
判断本机实例名的方法:按Win+R输services.msc回车,找“SQL Server (MSSQLSERVER)”是默认实例,找“SQL Server (SQLEXPRESS)”就是命名实例。也可以在PowerShell里查:
Get-Service | Where-Object { $_.Name -like 'MSSQL*' } | Select-Object Name, DisplayName, Status如果看到服务状态是Stopped,先右键启动,否则程序会报“无法打开登录所请求的数据库”。连接串改完,在C#代码里测试连接的最快方式是让SQLHelper跑一个最简单的查询,而不是直接F5整个项目。
3.3 重新生成解决方案:Framework版本、缺失引用与清理残留
连接串改好,数据库服务也启动了,接下来按F5之前务必先做“重新生成解决方案”。课设源码常见的生成失败有三种:目标Framework版本和本机Visual Studio不匹配(比如项目要4.8,机器上只有4.6.2),引用的dll缺失(引用管理器里出现黄色感叹号图标),以及bin目录里的旧文件残留导致程序集加载冲突。
用一条批处理清理编译残留最省事。把下面这条命令存成bat文件,放在解决方案根目录执行:
@echo off REM 清理课设最常见的“生成残留”问题:删掉bin/obj再重新打开VS for /d /r "%~dp0" %%i in (bin,obj) do @if exist "%%i" rd /s /q "%%i" pause这段批处理把当前目录下所有bin和obj文件夹递归删除,然后重新打开.sln文件再生成。它能解决大多数“明明代码没改,突然编译失败”的玄学问题。如果报的是“未能加载文件或程序集”,去项目引用里把带黄色感叹号的引用移除,再右键“添加引用”重新定位到本机对应程序集。Framework版本不对则打开项目属性,在“目标框架”里改成当前机器有的版本,常见的是.NET Framework 4.6.2或4.8。
3.4 首次登录与验证:库里有没有默认账号
项目生成成功,数据库也连上了,剩下就是登录。先用SSMS查一下Users表里到底有没有初始账号:
SELECT UserID, UserName, Role FROM Users;很多源码包自带初始化数据,默认账号是admin/admin123;如果表是空的,说明初始化代码写在Form的Load事件里,或者压缩包里还附了一个InitData.sql没执行。找到后执行一遍再登录。首次验证登录功能时,至少测两条路径:输错密码要弹提示框而不是程序崩溃;输对密码要能正常跳转主窗体。还有一个小坑:有些源码登录窗体点击右上角关闭时,进程没有完全退出,任务管理器里能看到残留的SuperMarket进程。按F5调试时如果提示“无法启动调试,因为系统找不到指定的文件”,多半就是上一个调试进程没关干净,到任务管理器里结束进程再试。
4. 二次开发切哪里:商品增删改查、库存预警和销售事务的代码落点
跑通只是开始。把这套C#超市管理系统改成自己的,核心工作集中在三个点:商品管理页面的增删改查、库存预警的筛选逻辑、销售收银的事务处理。这三处改完,系统就算真正被你接住了。
4.1 商品管理:一个LoadData和三个参数化操作撑起整个页面
商品管理窗体基本都长一个样:上面几个文本框和下拉框,中间一个dataGridView,下面增删改查四个按钮。核心代码就两个部分:加载数据、执行增删改。
// frmProduct.cs 加载商品列表,把分类名和供应商名join出来 private void LoadData() { string sql = @"SELECT p.ProductID, p.ProductName, c.CategoryName, s.SupplierName, p.Price, p.Stock FROM Product p JOIN Category c ON p.CategoryID = c.CategoryID LEFT JOIN Supplier s ON p.SupplierID = s.SupplierID"; dataGridView1.DataSource = SQLHelper.ExecuteQuery(sql); // 隐藏主键列,避免用户误改 dataGridView1.Columns["ProductID"].Visible = false; }加载方法用JOIN把三张表拼成一张视图,是这类管理系统的主流做法。新增按钮要注意一个边界:商品名重复会导致数据混乱,所以插入前先查重。
private void btnAdd_Click(object sender, EventArgs e) { string check = "SELECT COUNT(1) FROM Product WHERE ProductName=@name"; int exists = Convert.ToInt32(SQLHelper.ExecuteScalar(check, new SqlParameter("@name", txtName.Text.Trim()))); if (exists > 0) { MessageBox.Show("同名商品已存在,请检查"); return; } string sql = @"INSERT INTO Product(ProductName, CategoryID, SupplierID, Price, Stock) VALUES(@name, @cat, @sup, @price, @stock)"; SqlParameter[] ps = { new SqlParameter("@name", txtName.Text.Trim()), new SqlParameter("@cat", cboCategory.SelectedValue), new SqlParameter("@sup", cboSupplier.SelectedValue), new SqlParameter("@price", txtPrice.Text.Trim()), new SqlParameter("@stock", txtStock.Text.Trim()) }; SQLHelper.ExecuteNonQuery(sql, ps); LoadData(); }两个注意点:下拉框SelectedValue拿的是Category表的CategoryID,如果绑定下拉框时没有设置ValueMember,SelectedValue会是null,插入时外键就出问题;价格和库存是数值字段,txtPrice.Text直接传字符串,SQL Server会做隐式转换,但用户输入字母时会在转换时报错,生产环境应该用decimal.TryParse校验。删除按钮比新增更危险——商品一旦有过销售记录,直接DELETE会触发外键冲突。这类C#系统里最常见的做法不是真删除,而是给Product表加一个IsDelete或Status字段,删除时执行UPDATE Product SET Status=0 WHERE ProductID=@id,查询时默认过滤掉Status=0的数据,这样历史销售明细也不会丢。
4.2 库存预警:C#端dt.Select与SQL端CASE WHEN的取舍
库存预警常见的实现思路有两种:在C#内存里筛选,或者在SQL里计算状态。数据量小、课设阶段,C#端筛选更直观,改阈值也方便,但要注意DataTable.Select的坑。
// 用DataTable.Select在内存里筛低于阈值的商品 DataTable dt = SQLHelper.ExecuteQuery("SELECT ProductID, ProductName, Stock FROM Product"); DataRow[] low = dt.Select("Stock < 10"); // 10是预警阈值 if (low.Length > 0) { // low里就是要补货的商品,可以绑定到另一个DataGridView或弹提示 dataGridView2.DataSource = low.CopyToDataTable(); }DataTable.Select返回的是DataRow数组,空数组时如果直接调CopyToDataTable会抛异常,要先判断Length。阈值为10写死在代码里,虽然简单,但要调整就得重新编译。更稳妥的方案是放到App.Config或单独一张参数表,下次要改成5就只改配置不动代码。
如果商品数据到了十万级,C#端整表查询再筛选就费内存了,把判断压下SQL更合理。
SELECT ProductName, Stock, CASE WHEN Stock < 10 THEN '补货' WHEN Stock < 20 THEN '临界' ELSE '充足' END AS Level FROM Product ORDER BY Stock;这条SQL直接在结果集里多出一列Level,界面上遍历行时根据Level值设置单元格背景色就行。实际项目里我是先看数据量再选方案:几千行用C#端筛选,几万行以上用SQL端计算。别一上来就上存储过程,课设系统的数据量根本到不了那个级别,过度设计反而让新手改不动。
4.3 销售收款:SqlTransaction把主表、明细、减库存捆成一笔
最关键的代码在这。销售一笔商品,数据库层面要做三件事:往SalesOrder插一条主表记录、往SalesDetail插若干条明细、把Product表对应商品库存减掉。这三件事缺一不可,必须用事务包住。如果源码里销售和减库存是分开的两个按钮、两个方法各自提交,那账一定对不平。
// frmSale.cs 收银结算,用事务把三个操作捆成一笔 using (SqlConnection conn = new SqlConnection(SQLHelper.ConnStr)) { conn.Open(); using (SqlTransaction tran = conn.BeginTransaction()) { try { // 第一步:写销售主表,拿到新生成的SalesID string s1 = @"INSERT INTO SalesOrder(OrderNo, SaleDate, CashierID, TotalAmount) VALUES(@no, GETDATE(), @op, @total); SELECT SCOPE_IDENTITY();"; SqlCommand cmd1 = new SqlCommand(s1, conn, tran); cmd1.Parameters.AddWithValue("@no", "SO" + DateTime.Now.ToString("yyyyMMddHHmmssfff")); cmd1.Parameters.AddWithValue("@op", currentUser.UserID); cmd1.Parameters.AddWithValue("@total", totalAmount); int salesId = Convert.ToInt32(cmd1.ExecuteScalar()); // 第二步:逐条写销售明细则表 foreach (SaleItem item in items) { string s2 = @"INSERT INTO SalesDetail(SalesID, ProductID, Qty, Price) VALUES(@sid, @pid, @qty, @price)"; SqlCommand cmd2 = new SqlCommand(s2, conn, tran); cmd2.Parameters.AddWithValue("@sid", salesId); cmd2.Parameters.AddWithValue("@pid", item.ProductID); cmd2.Parameters.AddWithValue("@qty", item.Qty); cmd2.Parameters.AddWithValue("@price", item.Price); cmd2.ExecuteNonQuery(); // 第三步:同步减库存,受影响行数为0说明商品已被删除 string s3 = "UPDATE Product SET Stock = Stock - @qty WHERE ProductID=@pid"; SqlCommand cmd3 = new SqlCommand(s3, conn, tran); cmd3.Parameters.AddWithValue("@qty", item.Qty); cmd3.Parameters.AddWithValue("@pid", item.ProductID); if (cmd3.ExecuteNonQuery() == 0) { throw new Exception("商品不存在或已下架,事务回滚"); } } tran.Commit(); } catch (Exception ex) { tran.Rollback(); MessageBox.Show("结算失败:" + ex.Message); } } }这段代码有几个参数含义要理解清楚。OrderNo用日期加毫秒拼成,是为了防止同一秒两笔订单撞单号,如果只到秒,高峰期大概率重复。所有SqlCommand构造函数的第二个参数都传了tran,这是事务生效的关键——漏传任何一个,执行时会报“操作已从不同上下文访问”。AddWithValue虽然方便,但生产环境建议改成new SqlParameter("@qty", SqlDbType.Int) { Value = item.Qty },避免隐式类型转换带来的性能损耗。
事务里throw异常会直接跳到catch执行Rollback,所以那行if (cmd3.ExecuteNonQuery() == 0)特别重要。它保证了如果商品刚被人删掉,整笔销售不会只写入一半。这一步就是血泪教训:很多源码包为了省事,只写了INSERT,不写UPDATE库存,或者UPDATE库存时不检查受影响行数,结果销售明细和库存对不上,盘点时一查一个准。
5. 连接不上、乱码、超时、误报:五个必踩的坑与排查记录
这章所有坑都有一个共同点:现象看着像代码写错了,实际是环境或资源没管好。下面是这5个坑的现象、原因和解决。
5.1 还原/附加数据库失败:逻辑名不对、权限不够、版本降级
现象:SSMS还原数据库时提示“媒体集有误”,或者附加时提示“无法打开物理文件”。原因有几种,最常见是备份文件来自高版本SQL Server,当前实例版本太低无法读取;其次是mdf文件所在目录没有SQL Server服务账号的读取权限;还有一个隐蔽情况,备份文件的逻辑名和你猜的不一致,导致MOVE子句写错。
解决:先执行RESTORE FILELISTONLY FROM DISK = N'完整路径\xxx.bak'把逻辑名列出来,再照着逻辑名写MOVE子句。版本不兼容就别想着转换格式了,直接装一个同版本或更高版本的SQL Server Express,Developer版免费而且功能足够跑完整个超市系统。文件权限问题则打开SQL Server的默认数据目录(通常C:\Program Files\Microsoft SQL Server\MSSQLxx.MSSQLSERVER\MSSQL\DATA),把mdf和ldf拷进去再附加,比在D盘根目录折腾权限快得多。
5.2 sa登录失败:身份验证模式、实例名和防火墙三连问
现象:连接数据库报SqlException,提示“用户'sa'登录失败”。原因分三种:SQL Server服务处于Windows身份验证模式,没开混合模式;sa密码不是你以为的那个;程序里Server实例名写错,连到了别的实例。
解决:用SSMS以Windows身份登录,右键服务器选属性,在“安全性”里切到“SQL Server和Windows身份验证模式”,然后在“安全->登录名->sa”里重置密码并启用登录。改完要重启SQL Server服务,连接串里的实例名要和服务管理器里的完全一致。如果程序部署到别的机器,还要在Windows防火墙里放行1433端口,否则远程连不上。
5.3 中文显示成问号:排序规则、N前缀和控件字体
现象:数据库里查出来中文正常,程序界面上显示成???,或者反过来,程序里存进去的中文在数据库里变成乱码。原因多半是建库时没指定中文排序规则,或者SQL语句里中文字符串常量没加N前缀,再有一种可能是控件字体不支持中文。
解决:建库时显式指定排序规则,CREATE DATABASE SuperMarket COLLATE Chinese_PRC_CI_AS。查询条件的中文字面量加N前缀,比如WHERE ProductName = N'牙膏',这样即使数据库默认排序规则不是中文也能匹配上。排序规则不对是改库不是改程序,乱码数据已经写入就麻烦了,只能从源头重新导入。避免这个坑的方法是拿到源码后第一件事就检查建库脚本里有没有COLLATE子句,没有就自己重建库,别在乱码数据上继续开发。
5.4 运行十分钟后全库超时:连接池泄漏的定位与修复
现象:程序刚启动时一切正常,用了一阵子之后所有数据库操作都弹“超时时间已到”,重启程序又好。原因是连接池被占满了,并不是数据库死了。SqlConnection默认走连接池,池上限100,如果代码里Open了连接没Close,100次之后第101次就会等超时。
解决:所有SqlConnection都改成using写法,或者至少catch里保证Close。先用下面这条SQL确认是不是连接泄漏:
SELECT login_name, status, COUNT(*) FROM sys.dm_exec_sessions WHERE database_id = DB_ID('SuperMarket') GROUP BY login_name, status;如果看到大量sleeping状态的会话堆积,就是连接没关。对应到代码里,搜索所有new SqlConnection,把没有用using包住的地方改掉。这条坑在C#进阶讨论里被反复点名,MySQL有MySQL的连接池,SQL Server也有自己的连接池,不是只有互联网架构才有这个问题。
5.5 exe被拦或无法启动:杀毒误报、管理员权限与运行目录
现象:按F5运行没反应,任务管理器里进程一闪而过;或者杀毒软件弹出风险提示;或者程序能启动但一操作就报“对路径的访问被拒绝”。
解决:Debug目录下每次编译都会生成新的exe,实时防护容易把这种“频繁变脸”的程序误判为风险,最直接的办法是给项目目录加白名单。如果提示需要管理员权限,右键exe属性,勾选“以管理员身份运行”,这是因为程序往Program Files或系统目录写了文件。需要说明的是,连接SQL Server本身不需要管理员权限,报权限错误多半是日志文件、导出文件或临时文件写到了受保护目录,改到ProgramData或当前用户目录就行。遇到这种问题别先重装VS,按“杀毒白名单 -> 管理员权限 -> 写入路径”三步排查,五分钟内能定位。
6. 给课设源码补上三个生产级动作:权限分级、操作日志与备份验证
这章的三个动作,可以理解为把一份“交课设”的源码改成“能上线”的源码的最小补丁。
6.1 用对象传当前用户,而不是再查一次密码
登录成功后不要再满世界查数据库判断角色了,把用户对象直接传给主窗体。C#高级编程里讲的委托和事件,在这里最自然的用处就是跨窗体传数据。
public class CurrentUser { public int UserID { get; set; } public string UserName { get; set; } public string Role { get; set; } // 角色判断封装成属性,界面层不直接比字符串 public bool IsAdmin { get { return Role == "管理员"; } } } // frmLogin.cs 中的登录成功分支 CurrentUser user = new CurrentUser { UserID = userId, // 登录时查出的ID UserName = txtUser.Text.Trim(), Role = role // 登录时查出的角色 }; frmMain main = new frmMain(user); main.Show(); this.Hide();主窗体拿到user对象后,菜单权限判断就变成if (currentUser.IsAdmin),收银员登录时“商品管理”或“数据删除”按钮直接隐藏。这才是权限分级的正确姿势,比在页面里再调一次SELECT强得多。
6.2 加一张操作日志表和一句备份SQL
日志表记录敏感操作,SQL Server代理的维护计划做每日备份,这是最朴素也最稳妥的两个动作。
CREATE TABLE OperationLog( LogID INT IDENTITY PRIMARY KEY, UserID INT, ActionType NVARCHAR(20), -- 增/删/改/登录 ActionDesc NVARCHAR(200), -- 操作描述 LogTime DATETIME DEFAULT GETDATE() );日志在代码里的落点就是增删改按钮执行成功后,顺手往这张表插一条记录。真正上线时要同步多个门店数据库,那需要选型成熟的数据同步工具去解决,这里不展开。备份层面,一条T-SQL就能在界面上挂个“备份”按钮:
BACKUP DATABASE SuperMarket TO DISK = N'D:\SuperMarket\backup\SuperMarket.bak' WITH FORMAT;密码明文、没日志、没备份这三点,决定了这套C#超市管理系统源码是“能跑”还是“能抗”。我当年把第一版交上去,第二天盘点时销售明细和库存差了12条,就是因为销售那笔没写事务;后来每次拿到别人的源码,第一件事就是搜有没有using、有没有事务、密码是不是明文。希望你这次拿到zip后,先花半小时把这三个补丁打上,再开始加功能——这会比一路踩坑省出整整一个周末,希望帮到你。
本文还有配套的精品资源,点击获取