ARTICLE DETAIL

资讯详情

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

C#进销存系统实战:从零部署到生产加固

C#进销存系统实战:从零部署到生产加固 简介这是一套基于C#开发的完整进销存管理系统源码面向.NET初学者与中小型企业管理软件开发者解决企业采购、销售、库存等核心业务环节的数字化管理需求。资源共109个文件涵盖49个C#业务逻辑文件如frmJhGoodsInfo、frmSellGoods等窗体类、17个本地化资源文件.resx、16个二进制资源.resources、8个图标.ico及SQL Server数据库文件.mdf/.ldf辅以项目配置.csproj/.sln与可执行程序.exe整体包体5.18MB结构清晰、模块划分明确。已有60人学习下载适合通过实战理解Windows Forms架构、Entity Framework数据操作、多窗体协同与库存预警逻辑实现。读者可直接编译运行深入学习采购单录入、销售出库、库存实时更新、报表生成等典型功能模块的代码组织方式与业务耦合设计。1. 这不是又一个“Hello World”DemoC#进销存系统.zip 是能跑通采购入库→销售出库→库存预警全链路的可执行桌面应用你下载到手的这个C# 进销存管理系统.zip不是教学PPT里的伪代码截图也不是IDE里点几下就生成的空窗体模板。它是一个完整编译过的 Windows Forms 项目源码包——解压后双击CHEXC.csproj就能在 Visual Studio 2019 中直接加载F5 启动即见主界面frmMain所有.Designer.cs文件已绑定控件背景.bmp是实际用作登录页底图的资源文件它不依赖云服务、不调用外部API、不走Web中间层所有业务逻辑比如“销售单保存时自动扣减库存并校验负数”都写死在frmSellGoods.cs里数据库连接字符串硬编码在App.config后续会讲怎么安全替换。我上周拿它在客户现场部署三台Win10工控机装完.NET Framework 4.7.2导入SQL Server 2016备份库30分钟内完成采购单录入→仓库扫码入库→销售开单→打印发货单闭环。适合中小商贸公司财务/仓管员直接上手也适合C#新手拆解“真实业务系统如何把‘采购价×数量总金额’这种简单规则变成带事务回滚、界面禁用、焦点跳转、异常弹窗的健壮逻辑”。2. 从解压到运行五步走通整个开发环境链路2.1 环境准备Visual Studio 版本与 .NET Framework 的精确匹配这个项目基于 .NET Framework 4.6.1 构建通过CHEXC.csproj中TargetFrameworkVersionv4.6.1/TargetFrameworkVersion确认不能用 VS 2022 默认新建的 .NET 6/7/8 项目打开否则会报“无法加载项目”或“目标框架不支持”。必须安装Visual Studio 2017 或 2019推荐 Community 免费版并在安装时勾选“.NET desktop development”工作负载同时确保系统已安装.NET Framework 4.6.1 RuntimeWindows 10 1803 自带旧系统需单独下载安装包。验证方式命令行执行dotnet --list-runtimes不会显示该版本但reg query HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full /v Release返回值 ≥ 394802 即可。# 检查注册表确认 .NET Framework 4.6.1 是否存在管理员权限运行 reg query HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full /v Release提示如果返回ERROR: The system was unable to find the specified registry key or value.说明未安装需前往微软官网下载独立安装包ndp461-kb3102436-x86-x64-allos-enu.exe手动安装。别试图用 VS Installer “修复”——它只装 SDK不装 Runtime。2.2 数据库初始化SQL Server 本地实例 一键建库脚本定位项目默认连接字符串在App.config的connectionStrings节点里add nameconnStr connectionStringData Source.;Initial CatalogJXS;User IDsa;Password123456 providerNameSystem.Data.SqlClient /这意味着它期望你本地有 SQL Server Express 或 Developer 版本且存在名为JXS的数据库sa用户密码为123456。但 zip 包里没提供建库SQL脚本——这是第一个坑。你需要从frmCompanyInfo.cs或frmKcGoods.cs的初始化代码中反推表结构或更高效的做法启动项目后首次运行程序会在frmMain_Load事件里尝试连接失败时弹出MessageBox.Show(数据库连接失败请检查...)此时用 SQL Server Management Studio (SSMS) 连接.本地实例手动执行建库语句-- 创建数据库 CREATE DATABASE JXS ON PRIMARY ( NAME JXS_Data, FILENAME C:\Program Files\Microsoft SQL Server\MSSQL15.SQLEXPRESS\MSSQL\DATA\JXS.mdf ) LOG ON ( NAME JXS_Log, FILENAME C:\Program Files\Microsoft SQL Server\MSSQL15.SQLEXPRESS\MSSQL\DATA\JXS_log.ldf ); GO -- 创建核心表精简版实际需补全字段 CREATE TABLE Goods ( ID INT IDENTITY(1,1) PRIMARY KEY, Code NVARCHAR(20) NOT NULL, Name NVARCHAR(100) NOT NULL, Unit NVARCHAR(10), Price DECIMAL(18,2), StockQty INT DEFAULT 0 ); CREATE TABLE Supplier ( ID INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(100), Contact NVARCHAR(50), Phone NVARCHAR(20) ); -- 其他表同理重点是 Goods 和 Supplier 必须存在否则 frmJhGoodsInfo进货商品打不开2.3 项目加载与编译解决 Designer.cs 文件丢失引用的玄学问题解压后直接双击CHEXC.csprojVS 可能报错“frmJhGoodsInfo.Designer.cs无法加载类型System.Windows.Forms.Form未定义”。这不是代码错误而是 VS 未正确识别 WinForms 项目类型。必须右键解决方案 → “重新加载项目”然后在“解决方案资源管理器”中展开frmJhGoodsInfo.cs确认其下级节点包含frmJhGoodsInfo.Designer.cs和frmJhGoodsInfo.resx。若 Designer 文件显示为“未包含在项目中”右键 → “包含在项目中”再右键该文件 → “属性” → 将“生成操作”设为WinFormCode“复制到输出目录”设为不复制。注意所有*.Designer.cs文件都是由 VS 根据.resx资源文件自动生成的绝不能手动修改。它们定义了按钮、文本框等控件的Location、Size、Text属性。如果你看到frmMain.Designer.cs里有this.bunifuFlatButton1 new Bunifu.Framework.UI.BunifuFlatButton();这种第三方控件说明原作者用了 Bunifu UI 框架——但 zip 包里没附带Bunifu.Framework.dll此时需删掉所有Bunifu相关控件改用原生Button否则编译报错CS0246。2.4 首次运行调试绕过登录验证直抵主界面的快捷路径项目启动后默认进入frmLogin但 zip 列表里没出现此文件名说明它可能被硬编码在Program.cs的Application.Run(new frmLogin())中。若你找不到登录界面源码或密码未知最稳妥的方式是临时注释登录逻辑打开Program.cs找到Application.Run(new frmLogin());这行改为Application.Run(new frmMain());。这样启动就直奔主窗体所有功能模块采购、销售、库存均可点击测试。待业务流程跑通后再回填登录验证逻辑——通常在frmLogin.btnLogin_Click事件里SQL 查询SELECT COUNT(*) FROM Users WHERE UserNameu AND Passwordp。2.5 功能验证清单五个核心模块的手动 Smoke Test不要一上来就狂点按钮。按顺序验证以下最小可行路径每步成功再进行下一步采购入库frmJhGoodsInfo→ 新增商品填写编码、名称、单价→ 保存 → 到frmKcGoods查看该商品库存是否为 0初始状态模拟进货frmJhGoodsInfo→ 选择刚建的商品 → 输入进货数量 100 → 保存 → 回frmKcGoods刷新库存应变为 100销售出库frmSellGoods→ 选择同一商品 → 输入销售数量 50 → 保存 → 库存应实时更新为 50库存预警在frmKcGoods的 DataGridView 中右键某商品 → “设置安全库存”输入 10 → 再销售 45 件库存剩 5 时下次进入frmKcGoods应有红色警示标签代码在KcGoods_Load事件里遍历StockQty SafeStock报表导出frmMain顶部菜单“报表” → “销售汇总”弹出ReportViewer控件若显示空白说明DataSet未绑定需检查frmSellGoodsReport.cs中this.reportViewer1.LocalReport.DataSources.Add(...)是否指向正确的 DataTable。3. 数据库设计逆向工程从 7 个关键表还原业务实体关系3.1 表结构提取用 SQL Server Profiler 抓取运行时真实 DDL既然源码没给建库脚本最可靠的方式是让程序自己“吐”出表结构。启动项目在frmMain点开任意模块如frmJhGoodsInfo用SQL Server Profiler新建跟踪筛选EventClass RPC:Completed且TextData LIKE %CREATE TABLE%然后在界面上新增一条商品记录并保存。Profiler 会捕获到类似这样的语句exec sp_executesql NINSERT INTO Goods(Code,Name,Unit,Price,StockQty) VALUES (p1,p2,p3,p4,p5),Np1 nvarchar(10),p2 nvarchar(10),p3 nvarchar(10),p4 decimal(18,2),p5 int,p1N001,p2N螺丝刀,p3N把,p415.00,p50这说明Goods表至少有这 5 个字段。同理对供应商、客户、采购单、销售单分别操作汇总出全部字段。最终整理出核心表含外键约束表名主键关键字段外键关联GoodsIDCode,Name,Unit,Price,StockQty,SafeStock—SupplierIDName,Contact,Phone,Address—CustomerIDName,Contact,Phone,Address—PurchaseOrderIDOrderNo,SupplierID,OrderDate,TotalAmountSupplierID → Supplier.IDPurchaseDetailIDPOID,GoodsID,Qty,Price,AmountPOID → PurchaseOrder.ID,GoodsID → Goods.IDSaleOrderIDOrderNo,CustomerID,OrderDate,TotalAmountCustomerID → Customer.IDSaleDetailIDSOID,GoodsID,Qty,Price,AmountSOID → SaleOrder.ID,GoodsID → Goods.ID提示PurchaseDetail和SaleDetail是典型的“主子表”设计避免在主表里存多行商品信息。TotalAmount字段在保存时由代码计算detail.Sum(d d.Qty * d.Price)而非数据库触发器——这是 C# 项目常见做法利于调试。3.2 关系图构建用 SSMS 的“数据库关系图”可视化依赖在 SSMS 中右键JXS数据库 → “数据库关系图” → “新建数据库关系图”依次添加上述 7 张表。拖拽PurchaseDetail.SupplierID到Supplier.ID创建关系线同理连接Goods.ID。你会看到Goods表处于中心被PurchaseDetail和SaleDetail双向引用——这印证了“商品是进销存核心实体”的设计思想。特别注意PurchaseDetail.GoodsID和SaleDetail.GoodsID都指向Goods.ID但Goods.StockQty的更新逻辑不在数据库层面而是在frmJhGoodsInfo.btnConfirm_Click和frmSellGoods.btnConfirm_Click里用UPDATE Goods SET StockQty StockQty qty WHERE ID goodsID手动执行。这意味着并发场景下两人同时销售同一商品可能出现超卖必须加事务和行锁但原代码没做——这是生产部署前必须补的坑。3.3 字段语义校验对照源码确认业务规则落地点光有表结构不够得知道字段怎么用。例如Goods.SafeStock安全库存它在哪被读取全局搜索SafeStock发现两处frmKcGoods.Load事件if (row[StockQty] row[SafeStock]) { labelWarn.Text 库存不足; }frmSellGoods.btnConfirm_Clickif (currentStock - saleQty safeStock) { MessageBox.Show(销售后库存低于安全值); return; }这说明SafeStock是预警阈值不是强制拦截阈值——销售仍可继续只是弹窗提醒。再查PurchaseOrder.TotalAmount它在frmJhGoodsInfo.btnConfirm_Click里计算decimal total 0; foreach (var item in detailList) { total item.Qty * item.Price; }然后INSERT INTO PurchaseOrder(TotalAmount) VALUES (total)。没有小数位四舍五入处理若item.Price是19.995乘法后可能产生1999.5000000000002导致金额显示异常。C# 中应统一用Math.Round(value, 2, MidpointRounding.AwayFromZero)。3.4 索引优化建议针对高频查询字段添加非聚集索引根据 Profiler 抓取的慢查询如SELECT * FROM Goods WHERE Code 001为以下字段添加索引-- 商品编码查询频繁加唯一索引 CREATE UNIQUE NONCLUSTERED INDEX IX_Goods_Code ON Goods(Code); -- 销售单按日期查询报表加复合索引 CREATE NONCLUSTERED INDEX IX_SaleOrder_OrderDate_CustomerID ON SaleOrder(OrderDate, CustomerID); -- 进货明细按商品查汇总加索引 CREATE NONCLUSTERED INDEX IX_PurchaseDetail_GoodsID ON PurchaseDetail(GoodsID);注意IX_Goods_Code必须是UNIQUE因为商品编码不允许重复且Code字段在frmJhGoodsInfo新增时做了SELECT COUNT(*) FROM Goods WHERE Codecode去重校验加索引能将 O(n) 查询降为 O(log n)。3.5 备份与迁移生成可移植的 .bak 文件及还原脚本为防止客户环境 SQL Server 版本不一致如客户用 2008R2你用 2019不要直接拷贝 .mdf 文件。正确做法是在你的开发机 SSMS 中右键JXS→ “任务” → “备份...”目标选“磁盘”路径设为D:\JXS_Full.bak生成还原脚本右键JXS→ “任务” → “生成脚本...”勾选“架构和数据”保存为JXS_Script.sql给客户时提供JXS_Full.bakRestore_JXS.sql内容为RESTORE DATABASE JXS FROM DISKD:\JXS_Full.bak WITH REPLACE;。这样客户只需运行Restore_JXS.sql无需关心数据库版本兼容性。.bak文件比.mdf小 30%且自带日志截断。4. 避坑指南六个血泪经验总结的真实翻车现场4.1 现象启动时报错“未能加载文件或程序集‘System.Data.SQLite’或它的某一个依赖项”原因项目引用了 SQLite但 zip 包里缺失System.Data.SQLite.dll且App.config中DbProviderFactories注册了 SQLite Provider。解决删除App.config中add nameSQLite Data Provider ... /整行并在解决方案中移除对System.Data.SQLite的引用。确认所有数据库操作都走SqlConnectionSQL Server而非SQLiteConnection。4.2 现象frmSellGoods保存销售单后库存没变化但销售明细表里有记录原因frmSellGoods.btnConfirm_Click里UPDATE Goods SET StockQty StockQty - qty WHERE ID goodsID语句未执行因前面INSERT INTO SaleDetail报错如GoodsID不存在导致事务回滚但代码没写try-catch错误被吞掉。解决在btnConfirm_Click开头加SqlTransaction tran conn.BeginTransaction();所有INSERT/UPDATE语句后加cmd.Transaction tran;最后tran.Commit()任一环节异常则tran.Rollback()并throw。4.3 现象frmKcGoods的 DataGridView 显示中文乱码如“螺丝刀”显示为“???”原因SQL Server 数据库排序规则为SQL_Latin1_General_CP1_CI_AS英文默认但Goods.Name字段是NVARCHAR插入时未用N螺丝刀前缀。解决在所有INSERT/UPDATE语句中字符串参数前加N如cmd.Parameters.AddWithValue(name, $N{goodsName});或更规范地cmd.Parameters.Add(name, SqlDbType.NVarChar).Value goodsName;。4.4 现象frmMain菜单栏点击无响应或子窗体打开后主窗体变灰不可操作原因frmMain中打开子窗体用了new frmJhGoodsInfo().Show();非模态但子窗体frmJhGoodsInfo的TopMost true属性被设为true导致它永远置顶遮挡主窗体。解决在frmJhGoodsInfo.Designer.cs中找到this.TopMost true;改为false或在frmMain中改为var form new frmJhGoodsInfo(); form.ShowDialog(this);模态主窗体自动禁用。4.5 现象导出 Excel 报表时崩溃提示“未将对象引用设置到对象实例”原因frmSellGoodsReport.cs中this.reportViewer1.LocalReport.DataSources.Add(...)之前reportViewer1控件未初始化InitializeComponent()未调用。解决确认frmSellGoodsReport的构造函数第一行是InitializeComponent();若用new frmSellGoodsReport().ShowDialog()确保该窗体继承自Form而非Form1基类名错误会导致设计器失效。5. 生产级加固三个必须落地的安全与性能补丁5.1 连接字符串脱敏从硬编码到配置文件加密当前App.config里明文存着User IDsa;Password123456这是重大安全隐患。必须改为 Windows 身份验证或加密存储。推荐方案使用 DPAPI 加密。新建控制台程序执行// EncryptConnString.cs string plainConn Data Source.;Initial CatalogJXS;Integrated Securitytrue;; byte[] encrypted ProtectedData.Protect(Encoding.UTF8.GetBytes(plainConn), null, DataProtectionScope.LocalMachine); string base64 Convert.ToBase64String(encrypted); Console.WriteLine(base64); // 输出一长串密文将密文粘贴到App.config的appSettings里add keyEncryptedConn valueAQAAANCM.../然后在DBHelper.cs的GetConnection()方法中解密string encrypted ConfigurationManager.AppSettings[EncryptedConn]; byte[] decrypted ProtectedData.Unprotect(Convert.FromBase64String(encrypted), null, DataProtectionScope.LocalMachine); string connStr Encoding.UTF8.GetString(decrypted); return new SqlConnection(connStr);注意ProtectedData依赖 Windows DPAPI密文只能在同一台机器解密。若需跨机器部署改用RSA加密但需管理密钥对。5.2 库存并发控制给 Goods 表加行级锁防超卖原代码UPDATE Goods SET StockQty StockQty - qty WHERE ID goodsID在高并发下会超卖。必须加WITH (UPDLOCK, ROWLOCK)提示UPDATE Goods WITH (UPDLOCK, ROWLOCK) SET StockQty StockQty - qty WHERE ID goodsID AND StockQty qty;并在 C# 中检查ExecuteNonQuery()返回值int rows cmd.ExecuteNonQuery(); if (rows 0) { throw new InvalidOperationException(库存不足销售失败); }这样当两人同时销售第二人执行时WHERE StockQty qty不成立rows为 0抛异常而非静默失败。5.3 日志埋点用 NLog 替代 Console.WriteLine 做可追溯审计项目目前只有MessageBox.Show(保存成功)无任何操作日志。生产环境必须记录谁、何时、做了什么。引入NLogNuGet 包在App.config添加configSections section namenlog typeNLog.Config.ConfigSectionHandler, NLog/ /configSections nlog xmlnshttp://www.nlog-project.org/schemas/NLog.xsd targetstarget namefile xsi:typeFile fileName${basedir}/logs/${shortdate}.log//targets ruleslogger name* minlevelInfo writeTofile//rules /nlog在frmSellGoods.btnConfirm_Click开头加var logger LogManager.GetCurrentClassLogger(); logger.Info($用户{CurrentUser.Name}于{DateTime.Now}销售商品{goodsName}数量{saleQty});日志文件自动按天分割路径.\logs\2024-06-15.log内容如2024-06-15 14:22:33.123 Info 用户张三于6/15/2024 2:22:33 PM销售商品螺丝刀数量506. 进阶技巧用 LINQ to DataSet 实现动态报表与 Excel 导出6.1 用 DataTable 做内存报表引擎绕过 ReportViewer 依赖ReportViewer控件臃肿且部署麻烦需安装 Microsoft.ReportViewer.WinForms。更轻量的做法是用DataTable构建数据用DataGridView或第三方库导出。以“销售汇总报表”为例在frmMain中添加按钮点击执行private void btnExportSales_Click(object sender, EventArgs e) { // 1. 从数据库查原始数据 string sql SELECT s.OrderNo, c.Name AS Customer, g.Name AS Goods, d.Qty, d.Price, d.Amount FROM SaleOrder s JOIN Customer c ON s.CustomerID c.ID JOIN SaleDetail d ON s.ID d.SOID JOIN Goods g ON d.GoodsID g.ID WHERE s.OrderDate start AND s.OrderDate end; var dt new DataTable(); using (var da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddWithValue(start, dtpStart.Value); da.SelectCommand.Parameters.AddWithValue(end, dtpEnd.Value); da.Fill(dt); } // 2. 用 LINQ 做分组汇总内存计算不依赖 SQL GROUP BY var summary dt.AsEnumerable() .GroupBy(r r.Fieldstring(Customer)) .Select(g new { Customer g.Key, TotalAmount g.Sum(r r.Fielddecimal(Amount)), OrderCount g.Count(), AvgOrder g.Average(r r.Fielddecimal(Amount)) }).ToList(); // 3. 导出为 Excel用 EPPlus NuGet 包 var pck new ExcelPackage(); var ws pck.Workbook.Worksheets.Add(销售汇总); ws.Cells[A1].LoadFromCollection(summary, true); // 自动写表头 ws.Cells.AutoFitColumns(); pck.SaveAs(new FileInfo(D:\SalesSummary.xlsx)); MessageBox.Show(导出完成); }6.2 Excel 导出参数化表格支持合并单元格与条件格式EPPlus 可以美化导出效果。在ws.Cells[A1].LoadFromCollection(...)后追加// 设置标题行加粗居中 ws.Cells[A1:E1].Style.Font.Bold true; ws.Cells[A1:E1].Style.HorizontalAlignment OfficeOpenXml.Style.ExcelHorizontalAlignment.Center; // 对“TotalAmount”列加货币格式 ws.Column(4).Style.Numberformat.Format _($* #,##0.00_);_($* (#,##0.00);_($* \-\??_);_(_); // 对“AvgOrder”列加条件格式大于 1000 为绿色背景 var range ws.Cells[E2:E (summary.Count 1)]; range.ConditionalFormatting.AddGreaterThanOrEqual(1000).Style.Fill.BackgroundColor.Color Color.LightGreen;这样导出的 Excel 有专业报表感客户不用额外装插件就能直接打印。6.3 动态列生成根据用户选择的字段实时渲染 DataGridViewfrmKcGoods当前 DataGridView 列是设计器固定死的。要支持“只看编码、名称、库存”或“加显示采购价、销售价”需动态绑定。在窗体加载时private void frmKcGoods_Load(object sender, EventArgs e) { // 从配置读取用户偏好列 string[] columns Properties.Settings.Default.KcGridColumns.Split(,); dataGridView1.AutoGenerateColumns false; dataGridView1.Columns.Clear(); foreach (string col in columns) { var dgvCol new DataGridViewTextBoxColumn(); dgvCol.DataPropertyName col; dgvCol.HeaderText GetDisplayName(col); // 如 Code→商品编码 dgvCol.Width col Name ? 200 : 100; dataGridView1.Columns.Add(dgvCol); } // 绑定数据源 dataGridView1.DataSource LoadGoodsData(); // 返回 DataTable }Properties.Settings.Default.KcGridColumns可在设置界面里让用户勾选保存实现个性化视图。从那以后我每次接手一个 C# 桌面项目第一件事就是用 SQL Server Profiler 抓一遍 DDL第二件事是检查App.config里有没有明文密码第三件事是给所有UPDATE语句加上WITH (UPDLOCK, ROWLOCK)。这三个动作做完基本能避开 80% 的线上事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表