简介这份资源是一套基于C#与C/S架构的库存管理系统源码面向正在学习C#桌面开发、希望动手实践数据库与界面交互的初学者和进阶者。系统围绕商品入库、出库、库存盘点与预警等核心业务展开客户端负责图形界面交互服务端处理数据存储与业务逻辑可作为课程设计或自学练手的参考项目。压缩包共309个文件约4.74MB以62个cs源码文件、25个resx与resources资源文件、83张jpg与50张bmp界面素材为主另含dll依赖库、xsd数据集、db数据库文件及Visual Studio解决方案与项目配置结构完整便于直接打开继续开发。目前已有350人学习下载。读者可从中了解数据库表结构设计、数据操作实现与用户界面搭建思路并借助数据库表注释文档快速理解各表字段含义适合作为构建同类库存管理应用的实践范本。1. 用 C# 写一套库存管理系统从建表到出库一个 C/S 项目的完整落地路径很多做企业内部工具的朋友都遇到过这种场景仓库那边还在用 Excel 记进出库月底对账对到怀疑人生老板一句“能不能做个系统”就落到你头上。这个用 C# 做的库存管理系统就是冲着这个需求去的——它是一套典型的 C/S 架构桌面应用客户端负责界面交互和业务操作数据库负责数据持久化覆盖商品档案、入库、出库、库存查询、预警提醒这几条主线。适合谁适合手里有 C# 基础、想找一个完整项目练手或直接改造成公司内部工具的开发者。它不追求花哨胜在结构清晰、能跑通、能改。下面我按“先看清它是什么再跑起来最后动手改”的顺序拆一遍。2. 项目定位与技术选型为什么是 WinForms SQL Server 这套组合2.1 这套系统到底解决什么问题库存管理系统的核心诉求其实就三件事东西进来了要记清楚、东西出去了要扣得准、账面上的数要随时能查。这个项目围绕这三件事展开功能模块大致分成五块商品管理维护商品编号、名称、规格、单位、分类、库存上下限等基础信息是所有业务的数据源头。入库管理记录采购入库、退货入库等场景入库时自动增加对应商品库存。出库管理记录销售出库、领用出库等场景出库时校验库存是否充足然后扣减。库存查询按商品、分类、时间等条件筛选当前库存和流水记录。预警提醒当某个商品库存低于设定的下限时在界面上给出提示。这套功能划分是库存类项目最通用的骨架不管后面怎么扩展基本都跑不出这个范围。项目正文里没有给出更细的模块说明但按这个场景下合格开发最常采用的做法上面这几块是标配。2.2 技术栈拆解C# WinForms ADO.NET SQL Server项目标题明确写了 C#关键词里带了 C/S所以技术选型基本锁定在桌面端。具体到实现层面常见做法是这样一套组合层次技术选择说明界面层WinForms拖控件快适合内部工具学习成本低业务逻辑层C# 类库/服务类封装入库、出库、库存计算等规则数据访问层ADO.NET直接用 SqlConnection、SqlCommand 操作数据库数据库SQL Server关系型存储事务支持好适合库存扣减场景为什么不用 WPF 或者 Web因为 C/S 内部工具的核心诉求是开发快、部署简单、不依赖浏览器。WinForms 虽然老但拖拽式布局加上事件驱动模型对于表单密集型的库存系统来说效率很高。ADO.NET 虽然写起来比 ORM 啰嗦但胜在可控——库存扣减这种涉及事务的操作手写 SQL 反而更清楚每一步在干什么。提示如果你的团队已经在用 Entity Framework 或 Dapper完全可以替换掉 ADO.NET 那一层业务逻辑不受影响。2.3 数据库表结构设计库存系统的地基是表结构。按这个项目的功能范围核心表大概长这样-- 商品表存基础信息 CREATE TABLE Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(50) NOT NULL UNIQUE, -- 商品编号唯一 ProductName NVARCHAR(100) NOT NULL, Category NVARCHAR(50), Unit NVARCHAR(20), StockQty INT DEFAULT 0, -- 当前库存数量 MinStock INT DEFAULT 0, -- 库存下限用于预警 CreateTime DATETIME DEFAULT GETDATE() ); -- 入库记录表 CREATE TABLE StockIn ( InId INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, Quantity INT NOT NULL, InTime DATETIME DEFAULT GETDATE(), Operator NVARCHAR(50), Remark NVARCHAR(200), FOREIGN KEY (ProductId) REFERENCES Product(ProductId) ); -- 出库记录表 CREATE TABLE StockOut ( OutId INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, Quantity INT NOT NULL, OutTime DATETIME DEFAULT GETDATE(), Operator NVARCHAR(50), Remark NVARCHAR(200), FOREIGN KEY (ProductId) REFERENCES Product(ProductId) );这里有个设计取舍值得说库存数量直接存在 Product 表里而不是每次从流水表实时汇总。原因是库存查询是高频操作实时汇总在数据量大了之后会明显变慢。代价是每次入库出库都要同步更新 StockQty 字段必须放在同一个事务里否则会出现流水和库存对不上的情况。这个坑后面避坑章节还会展开。3. 核心模块拆解入库、出库、库存查询的代码实现3.1 数据访问层的封装不管业务怎么变数据库连接和命令执行是重复度最高的部分。我一般会先抽一个简单的数据库帮助类把连接字符串管理和基础执行封装起来public class DbHelper { // 连接字符串实际项目中建议放到配置文件 private static readonly string ConnStr Server.;DatabaseStockDB;Integrated SecurityTrue;; // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null parameters.Length 0) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } // 执行查询返回 DataTable public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null parameters.Length 0) cmd.Parameters.AddRange(parameters); var adapter new SqlDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); return dt; } } }这段代码的关键点在于参数化查询。很多新手图省事直接拼 SQL 字符串比如SELECT * FROM Product WHERE ProductCode code 这样写不仅有 SQL 注入风险遇到商品名里带单引号还会直接报错。用SqlParameter传参是必须养成的习惯。另外using语句保证连接和命令对象用完即释放避免连接池被占满。3.2 入库操作事务里同时写流水和更新库存入库看起来简单——加库存、记一笔——但这两步必须原子化。如果流水写进去了、库存没更新账就乱了。所以入库方法要开事务public bool StockIn(int productId, int quantity, string operatorName, string remark) { using (var conn new SqlConnection(ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) // 开启事务 { try { // 第一步插入入库流水 string sqlIn INSERT INTO StockIn(ProductId, Quantity, Operator, Remark) VALUES(pid, qty, op, remark); using (var cmd new SqlCommand(sqlIn, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(op, operatorName); cmd.Parameters.AddWithValue(remark, remark ?? ); cmd.ExecuteNonQuery(); } // 第二步更新商品库存 string sqlUpdate UPDATE Product SET StockQty StockQty qty WHERE ProductId pid; using (var cmd new SqlCommand(sqlUpdate, conn, tran)) { cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(pid, productId); cmd.ExecuteNonQuery(); } tran.Commit(); // 两步都成功才提交 return true; } catch (Exception ex) { tran.Rollback(); // 任何一步失败就回滚 // 实际项目中这里应该记日志 throw new Exception(入库失败 ex.Message); } } } }参数说明productId是商品主键quantity是入库数量正整数operatorName是操作人remark是备注。事务的Commit和Rollback是这段代码的灵魂——要么两步都成功要么都不生效。库存系统里这种“写流水 改余额”的组合操作全都得这么处理。3.3 出库操作先校验库存再扣减出库比入库多一层校验库存不够不能出。而且校验和扣减之间不能有间隙否则并发情况下会出现超卖。常见做法是在事务里用UPDATE ... WHERE StockQty qty这种带条件的更新通过受影响行数判断是否成功public bool StockOut(int productId, int quantity, string operatorName, string remark) { using (var conn new SqlConnection(ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 带条件的库存扣减只有库存足够时才会更新成功 string sqlUpdate UPDATE Product SET StockQty StockQty - qty WHERE ProductId pid AND StockQty qty; int affected; using (var cmd new SqlCommand(sqlUpdate, conn, tran)) { cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(pid, productId); affected cmd.ExecuteNonQuery(); } // 受影响行数为 0说明库存不足直接回滚 if (affected 0) { tran.Rollback(); return false; } // 库存扣减成功后再写流水 string sqlOut INSERT INTO StockOut(ProductId, Quantity, Operator, Remark) VALUES(pid, qty, op, remark); using (var cmd new SqlCommand(sqlOut, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(op, operatorName); cmd.Parameters.AddWithValue(remark, remark ?? ); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); throw new Exception(出库失败 ex.Message); } } } }这里把库存扣减放在前面、流水插入放在后面是因为扣减是可能失败的库存不足先做可能失败的步骤失败就直接回滚不用白写一条流水。WHERE StockQty qty这个条件同时起到了校验和防并发的作用——数据库层面保证不会扣成负数。3.4 库存查询与预警查询模块相对简单主要是条件拼接。预警则是在查询结果上做一层过滤// 查询库存低于下限的商品用于预警提示 public DataTable GetWarningProducts() { string sql SELECT ProductCode, ProductName, StockQty, MinStock FROM Product WHERE StockQty MinStock; return DbHelper.ExecuteQuery(sql); }在 WinForms 里查询结果直接绑到DataGridView上就行。预警可以在主窗体加载时调一次把结果展示在一个专门的提示区域或者用MessageBox弹出来。我一般倾向于在界面上放一个状态栏显示预警数量点进去看详情比弹窗打扰小。4. 跑起来环境准备、建库建表和首次运行4.1 环境准备清单在动手之前先把环境确认一遍Visual Studio2019 或 2022 社区版即可安装时勾选“.NET 桌面开发”工作负载。.NET Framework项目如果是老版本可能是 4.7.2 或 4.8如果是新写的可能是 .NET 6/8 的 WinForms。看项目文件里的TargetFramework确认。SQL ServerExpress 版就够用安装时记住实例名默认实例是.或(local)命名实例类似.\SQLEXPRESS。SSMSSQL Server Management Studio用来建库建表和查数据比在代码里调试方便得多。4.2 建库建表打开 SSMS连上本地实例新建一个数据库叫StockDB然后执行第 2 章里那三段建表 SQL。如果想省事也可以把建表语句存成一个.sql文件在 SSMS 里打开一次性执行。建完表之后往Product表里插几条测试数据INSERT INTO Product(ProductCode, ProductName, Category, Unit, StockQty, MinStock) VALUES (P001, A4 打印纸, 办公用品, 包, 100, 20), (P002, 签字笔, 办公用品, 盒, 50, 10), (P003, 文件夹, 办公用品, 个, 8, 15);注意第三条的StockQty是 8MinStock是 15库存低于下限正好用来测试预警功能。4.3 修改连接字符串并运行项目里数据库连接字符串通常写在App.config或代码里的常量中。找到它改成你自己环境的实例名connectionStrings add nameStockConn connectionStringServer.\SQLEXPRESS;DatabaseStockDB;Integrated SecurityTrue; providerNameSystem.Data.SqlClient/ /connectionStrings如果用的是 SQL Server 身份验证而不是 Windows 身份验证把Integrated SecurityTrue换成User Id你的用户名;Password你的密码;。改完之后在 Visual Studio 里按 F5 运行主窗体应该能正常打开商品列表里能看到刚才插的三条数据。注意如果运行时报“无法打开登录所请求的数据库 StockDB”八成是连接字符串里的数据库名或实例名写错了回到 SSMS 确认一下。4.4 走一遍完整业务流程跑起来之后建议按这个顺序验证一遍在商品管理里新增一个商品确认能保存并出现在列表里。对这个商品做一次入库数量填 10然后去数据库查StockIn表和Product.StockQty确认流水和库存都对。做一次出库数量填 3同样确认两边数据一致。试着出库一个超过库存的数量确认系统拒绝并提示库存不足。把某个商品的库存改到低于下限重新打开主窗体看预警有没有触发。这五步走完核心链路就算通了。哪一步不对问题基本就锁定在对应的模块里。5. 避坑与排查库存系统最容易翻车的几个地方5.1 库存扣成负数现象出库之后StockQty变成负数或者两个操作员同时出库同一商品库存扣多了。原因校验库存和扣减库存分成了两步中间有时间窗口或者扣减 SQL 没带StockQty qty条件。解决把校验和扣减合并成一条带条件的 UPDATE用受影响行数判断是否成功就像 3.3 里那样。整个操作放在事务里不要先 SELECT 再 UPDATE。5.2 流水和库存对不上现象StockIn表里有一笔记录但Product.StockQty没变或者反过来。原因写流水和更新库存没放在同一个事务里其中一步失败后另一部已经提交了。解决所有涉及多表写入的操作一律用BeginTransaction包起来成功一起提交失败一起回滚。这是血泪经验别偷懒。5.3 连接字符串写死在代码里现象换一台机器部署就要重新编译或者把项目发给别人跑不起来。原因连接字符串直接写在.cs文件里没有抽到配置文件。解决放到App.config的connectionStrings节点里代码里用ConfigurationManager.ConnectionStrings[StockConn].ConnectionString读取。部署时只改配置文件不用动代码。5.4 DataGridView 绑定后数据不刷新现象入库成功了但列表里的库存数量还是旧的。原因DataGridView绑定的DataTable是查询时的快照数据库变了它不会自动更新。解决操作成功后重新执行一次查询把新的DataTable重新绑上去。简单粗暴但有效。如果数据量大可以考虑只更新受影响的那一行。5.5 中文乱码现象商品名称存进去变成问号或者乱码。原因数据库字段用了VARCHAR而不是NVARCHAR或者连接字符串没指定字符集。解决所有存中文的字段一律用NVARCHAR参数化查询时SqlParameter会自动处理编码。建表时别图省事用VARCHAR。6. 二次开发从能跑到好用几个改造方向项目跑通之后接下来就是按自己的需求改。我一般会从这几个方向入手按优先级排第一加登录和权限。内部工具不可能谁都能改库存。加一张User表存用户名、密码哈希、角色登录后根据角色控制哪些按钮可用。密码别存明文用SHA256或者BCrypt哈希一下。这个改造涉及面广但价值最高建议优先做。第二把数据访问层换成 Dapper。现在手写SqlCommand和SqlParameter的代码重复度很高换成 Dapper 之后查询可以写成conn.QueryProduct(SELECT * FROM Product).ToList()插入更新也能省掉大量样板代码。事务支持也没问题conn.BeginTransaction()照样用。改造时注意一点Dapper 的参数化查询默认就是安全的但 SQL 字符串本身还是要用参数占位符别拼字符串。第三加操作日志。谁在什么时候改了哪个商品的库存这个在出问题的时候是后悔药。可以加一张OperationLog表记录操作人、操作类型、目标商品、变更前后数量、时间。在业务方法里顺手写一笔就行成本很低。第四库存预警做成后台定时检查。现在是打开窗体才检查如果系统一直开着库存降到下限了也不知道。可以加一个Timer控件每隔几分钟查一次预警商品有新的就弹提示或者改变状态栏颜色。第五导出 Excel。仓库那边经常要把库存表导出来发给别人。用NPOI或者ClosedXML这类库几行代码就能把DataTable写成.xlsx文件。注意导出的时候把列宽和表头格式化一下不然发出去不好看。改造的时候有个习惯我一直保持每改一个模块先把原来的业务流程完整走一遍确认没改坏再继续。库存系统的数据是有连带关系的动了入库逻辑可能影响到库存查询和预警牵一发动全身。从那以后我每次动核心业务代码都强制走一遍 4.4 里那五步验证流程确认流水、库存、预警三边都对得上才敢提交。希望这些能帮到你少走点弯路。本文还有配套的精品资源点击获取