简介:这份C#进销存管理系统源码面向具备一定C#与WinForms基础的开发者,适合用于课程设计、毕业设计或中小型商贸企业进销存业务的二次开发参考。资源采用经典三层架构组织代码,并附带完整建库脚本,包含数据库文件与SQL脚本,可直接还原运行环境,省去从零搭建数据层的繁琐工作。压缩包共49个文件,约360KB,以19个cs源码文件为核心,配合13个resx窗体资源、3个bmp图标、2个rpt水晶报表模板,以及sql、mdf、ldf、bak等数据库相关文件,覆盖登录、主窗体、商品与客户资料管理、库存与销售查询、权限管理等模块。已有556人学习下载,读者可从中获取分层清晰的工程结构、窗体与报表实现方式、数据库连接与脚本还原思路,便于快速理解进销存业务逻辑并在此基础上扩展功能。
1. 一套能跑起来的 C# 进销存源码,到底该长什么样
很多做企业信息化的朋友第一次接触“C#进销存 源码”,脑子里冒出来的画面是:下载一个压缩包,双击解决方案,F5 一按,登录界面弹出来,然后就能开单、入库、查库存。现实往往相反——能编译通过的不少,能真正跑通一条完整业务链路的不到三成。问题不在代码写得好不好,而在于进销存这个场景对数据一致性、并发扣减、单据状态流转的要求,比一般 CRUD 后台高一个量级。一套合格的 C# 进销存源码,至少要能回答三个问题:采购入库后库存怎么变、销售出库时库存不足怎么拦、退货和盘点如何回滚。本文不聊虚的,直接按“拿到源码后怎么读、怎么改、怎么跑通”的顺序,把选型、表结构、核心事务、打印导出和踩坑点拆开讲,适合正在选型或已经拿到一套源码却不知道从哪下手的开发者。
2. 先看懂一套 C# 进销存源码的技术骨架
拿到源码别急着改界面,先花半小时把骨架摸清楚。进销存系统的复杂度不在 UI,而在数据流:一张采购单从录入到入库,中间要经过单据主表、明细表、库存表、流水表四层,任何一层对不上,后面全是玄学 bug。
2.1 分层结构:为什么大多数源码用 WinForms + 三层
市面上流通的 C# 进销存源码,主流是 WinForms 做客户端、SQL Server 或 SQLite 做存储、中间夹一层 BLL 和 DAL。这个组合不是最时髦的,但它是最稳的。原因很实际:进销存的使用场景多在局域网内、单据录入密集、需要本地打印,WinForms 的表格控件和打印控件成熟度高,部署时一台机器装个运行时就能用。
典型目录结构长这样:
InventorySystem/ ├── InventorySystem.UI/ # 窗体、用户控件 ├── InventorySystem.BLL/ # 业务逻辑:单据审核、库存计算 ├── InventorySystem.DAL/ # 数据访问:SQL 拼接或 ORM ├── InventorySystem.Model/ # 实体类:单据、商品、往来单位 └── InventorySystem.Common/ # 工具:日志、加密、导出读源码时优先看 BLL 层,那里藏着真正的业务规则。UI 层再乱都能重写,BLL 逻辑错了就是数据灾难。
2.2 数据库选型:SQL Server 和 SQLite 的取舍
| 维度 | SQL Server | SQLite |
|---|---|---|
| 部署成本 | 需安装服务,占内存 | 单文件,零安装 |
| 并发能力 | 强,支持行锁 | 弱,写操作串行 |
| 适合规模 | 多客户端联网 | 单机或两三人 |
| 备份方式 | 作业+日志 | 复制文件 |
我一般建议:如果只是单店、单仓库、两三个人用,SQLite 足够,源码里改个连接字符串就能跑;如果涉及多台收银机同时开单,必须上 SQL Server,否则并发扣库存时会出现超卖。很多源码默认给的是 SQL Server 脚本,想换 SQLite 要改自增主键写法和日期函数,这点后面避坑章细说。
2.3 从解决方案到可运行:环境与依赖检查清单
在动手之前,把下面这张表过一遍,能省掉大量“为什么编译不过”的时间:
| 检查项 | 常见值 | 说明 |
|---|---|---|
| .NET 版本 | .NET Framework 4.7.2 / .NET 6 | 老源码多为 Framework |
| 数据库 | SQL Server 2016+ / SQLite 3 | 看连接字符串 |
| 报表组件 | 自带 PrintDocument / 第三方 | 缺 DLL 会编译失败 |
| 权限 | 管理员运行 | 写日志、读配置需要 |
确认 .NET 版本的方法是右键项目看目标框架。如果源码是 Framework 4.x 而本机只装了 .NET 8,要么装对应运行时,要么用 VS 的“重定向解决方案”功能升级,但升级后第三方控件可能不兼容,这是第一个容易翻车的地方。
3. 把源码跑起来:建库、改配置、走通第一张单
骨架看完,接下来是实操。这一章的目标是让你在本地把系统跑起来,并且完整走一遍“采购入库→库存增加→销售出库→库存减少”的闭环。
3.1 建库与初始化数据:脚本执行顺序不能乱
大多数源码的数据库脚本放在DB或Sql目录下,通常分三个文件:建表、建视图/存储过程、初始化基础数据。执行顺序错了,外键约束会直接报错。
-- 1. 先建表,注意主外键依赖顺序 CREATE TABLE Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(50) NOT NULL UNIQUE, ProductName NVARCHAR(100) NOT NULL, Unit NVARCHAR(20), StockQty DECIMAL(18,4) DEFAULT 0 -- 当前库存,冗余字段 ); CREATE TABLE PurchaseOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(30) NOT NULL UNIQUE, SupplierId INT NOT NULL, OrderDate DATETIME DEFAULT GETDATE(), Status INT DEFAULT 0 -- 0草稿 1已审核 2已入库 ); CREATE TABLE PurchaseOrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL FOREIGN KEY REFERENCES PurchaseOrder(OrderId), ProductId INT NOT NULL FOREIGN KEY REFERENCES Product(ProductId), Qty DECIMAL(18,4) NOT NULL, Price DECIMAL(18,4) NOT NULL ); -- 2. 再建库存流水表,用于对账 CREATE TABLE StockFlow ( FlowId INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, ChangeQty DECIMAL(18,4) NOT NULL, -- 正数入库,负数出库 BizType NVARCHAR(20), -- 采购/销售/盘点/退货 BizOrderNo NVARCHAR(30), CreateTime DATETIME DEFAULT GETDATE() );这段脚本的关键在于Product.StockQty是冗余字段,真正的账本在StockFlow。很多新手源码只更新 StockQty 不写流水,一旦库存对不上就查不出原因,这是黑匣子式的设计,必须补上流水表。
3.2 连接字符串与配置文件:三个必须改的地方
源码里的App.config或appsettings.json通常有三处要改:
<configuration> <connectionStrings> <!-- 1. 改成自己的服务器和库名 --> <add name="ConnStr" connectionString="Server=.;Database=InventoryDB;User Id=sa;Password=你的密码;" providerName="System.Data.SqlClient"/> </connectionStrings> <appSettings> <!-- 2. 单据编号前缀,避免和旧数据冲突 --> <add key="OrderPrefix" value="CG"/> <!-- 3. 打印模板路径 --> <add key="PrintTemplate" value="Templates\Purchase.frx"/> </appSettings> </configuration>改完连接字符串后,先别急着登录,用 VS 的“服务器资源管理器”测试一下连接。如果报“登录失败”,八成是 SQL Server 没开混合验证;如果报“找不到网络路径”,检查实例名是否写成了.\SQLEXPRESS。这两个错误占了新手卡壳的七成。
3.3 走通采购入库到销售出库的完整链路
环境通了之后,按这个顺序操作,每一步都验证数据库:
- 新增商品,记下 ProductId,查
Product表确认 StockQty 为 0。 - 录入采购单,保存为草稿,此时
PurchaseOrder.Status=0,库存不变。 - 审核采购单,触发入库逻辑,
Status变 2,Product.StockQty增加,StockFlow插入一条正数记录。 - 录入销售单,选择同一商品,数量小于库存,审核后库存减少,流水插入负数记录。
- 故意录入超过库存的销售数量,看系统是否拦截。
第 5 步是检验源码质量的试金石。如果系统允许负库存直接过账,说明 BLL 层缺少校验,这种源码要么补校验,要么直接放弃。
// BLL 层审核销售单时的库存校验,这是必须有的 public bool AuditSaleOrder(int orderId) { using (var tran = db.Database.BeginTransaction()) { var details = db.SaleOrderDetail.Where(d => d.OrderId == orderId).ToList(); foreach (var d in details) { // 加锁读取,防止并发超卖 var product = db.Product.SqlQuery( "SELECT * FROM Product WITH (UPDLOCK) WHERE ProductId=@p0", d.ProductId).First(); if (product.StockQty < d.Qty) throw new Exception($"商品{product.ProductName}库存不足,当前{product.StockQty}"); product.StockQty -= d.Qty; db.StockFlow.Add(new StockFlow { ProductId = d.ProductId, ChangeQty = -d.Qty, BizType = "销售", BizOrderNo = d.OrderNo }); } db.SaveChanges(); tran.Commit(); return true; } }这段代码里UPDLOCK是重点,没有它,两个收银员同时卖最后一件商品就会超卖。参数@p0是 EF 的占位符,换成 Dapper 就是@ProductId,逻辑一样。
4. 单据、库存与并发:源码里最容易埋雷的三处
跑通链路只是开始,真正决定这套源码能不能上生产,看的是异常场景下的表现。这一章拆三个高频雷区。
4.1 单据状态机:草稿、审核、红冲不能随便跳
进销存的单据不是普通记录,它有状态。采购单至少要有草稿、已审核、已入库、已红冲四个状态。源码里如果只用一个IsAudit布尔字段,后期做退货和反审核时会非常痛苦。
正确的做法是用整型状态加状态流转校验:
// 状态流转白名单,只允许这些跳转 private static readonly Dictionary<int, int[]> AllowedTransitions = new() { { 0, new[] { 1 } }, // 草稿 -> 已审核 { 1, new[] { 2, 4 } }, // 已审核 -> 已入库 / 已红冲 { 2, new[] { 4 } }, // 已入库 -> 已红冲 }; public void ChangeStatus(int orderId, int newStatus) { var order = db.PurchaseOrder.Find(orderId); if (!AllowedTransitions[order.Status].Contains(newStatus)) throw new Exception($"状态{order.Status}不允许变更为{newStatus}"); order.Status = newStatus; db.SaveChanges(); }这段逻辑不长,但能挡住大量误操作。我见过一套源码因为没做状态校验,已入库的单据被重复审核,库存加了两遍,最后盘点对了三天才找出来。
4.2 库存扣减的并发问题:为什么你的系统会超卖
超卖的根因是“读-判断-写”不是原子操作。两个线程同时读到库存 1,都判断充足,都执行扣减,结果库存变成 -1。解决办法有三种:
| 方案 | 做法 | 适用 |
|---|---|---|
| 悲观锁 | SELECT ... WITH (UPDLOCK) | SQL Server,强一致 |
| 乐观锁 | 版本号字段,更新时校验 | 高并发,需重试 |
| 原子更新 | UPDATE SET Qty=Qty-@n WHERE Qty>=@n | 最简单可靠 |
我一般首选原子更新,因为它不依赖事务隔离级别,一条语句搞定:
UPDATE Product SET StockQty = StockQty - @Qty WHERE ProductId = @Pid AND StockQty >= @Qty; -- 检查 @@ROWCOUNT,为 0 说明库存不足在 C# 里用ExecuteSqlCommand执行,返回受影响行数,为 0 就抛异常回滚。这个写法比先查后改稳得多,也是很多成熟源码的标准做法。
4.3 退货与盘点:反向单据怎么冲回库存
退货分两种:采购退货(库存减)和销售退货(库存加)。盘点则是把系统库存调整为实盘数,差额写流水。这三类操作共同点是都会产生库存变动,但业务类型不同。
源码里常见错误是把退货做成直接改原单,导致历史数据被篡改。正确做法是生成一张反向单据,原单不动,新单的明细数量为负或单独标记。盘点则要记录盘点前数量、实盘数量、差异,差异部分写一条BizType='盘点'的流水。这样任何时点的库存都能通过流水累加复算出来,对账时才有后悔药可吃。
5. 避坑与排查:拿到源码后最常见的五个翻车点
这一章按“现象→原因→解决”写,都是实际改源码时反复遇到的。
现象一:编译报错“找不到类型或命名空间”,指向某个第三方控件。原因:源码依赖了报表或表格控件,但 DLL 没随包提供,或者版本对不上。 解决:先看packages.config或.csproj里的引用路径,缺哪个补哪个;如果是商业控件且没有授权,考虑用自带的DataGridView和PrintDocument替换,功能会打折但能跑。
现象二:登录后主界面空白,菜单点不动。原因:权限表没初始化,或者当前用户的角色没有绑定菜单。 解决:查Role、Menu、RoleMenu三张表,手动插入一条管理员角色和全量菜单映射。很多源码的初始化脚本漏了这步,属于打包疏忽。
现象三:保存单据时报“未将对象引用设置到对象的实例”。原因:明细表格里有一行空行,或者商品下拉框没选值就提交。 解决:在保存前遍历明细,过滤掉 ProductId 为 0 或数量为 0 的行;同时给下拉框加DropDownStyle=DropDownList,从源头禁止空值。
现象四:库存数量和流水累加对不上。原因:某次入库只更新了 StockQty 没写 StockFlow,或者事务中途失败但部分提交。 解决:写一个对账存储过程,按商品分组累加 StockFlow,和 Product.StockQty 比对,差异输出。以后每次审核单据都放在同一个事务里,要么全成功要么全回滚。
现象五:打印出来是空白页或格式错位。原因:打印模板路径不对,或者打印机默认纸张和模板不匹配。 解决:先确认PrintTemplate指向的文件存在;再用PrintDocument.DefaultPageSettings强制设置纸张大小;如果是针式打印机,还要调OriginAtMargins。这个属于调试成本高但一旦调好就一劳永逸的事。
6. 让这套源码真正可用:对账、导出与二次开发边界
跑通、避坑之后,最后一步是判断这套源码值不值得长期投入。我的习惯是先做两件事:加一个库存对账查询,再加一个 Excel 导出。对账查询能让你随时验证数据一致性,导出则是业务方最常提的需求。
对账查询的 SQL 不复杂,但很能说明问题:
SELECT p.ProductCode, p.ProductName, p.StockQty AS 系统库存, ISNULL(SUM(f.ChangeQty), 0) AS 流水累加, p.StockQty - ISNULL(SUM(f.ChangeQty), 0) AS 差异 FROM Product p LEFT JOIN StockFlow f ON p.ProductId = f.ProductId GROUP BY p.ProductId, p.ProductCode, p.ProductName, p.StockQty HAVING p.StockQty <> ISNULL(SUM(f.ChangeQty), 0);这条语句查出来有记录,就说明数据已经不一致了,得回头查是哪次操作漏了流水。建议把它做成一个菜单项,每天下班前点一下。
导出 Excel 用ClosedXML或NPOI都行,前者 API 更友好。核心是把DataGridView的数据源直接转成 DataTable 再写工作表,注意日期列要设格式,否则会变成一串数字。
二次开发的边界要提前想清楚:如果源码的 BLL 层和 UI 层耦合严重,比如 SQL 语句直接写在按钮事件里,那改动成本会很高,适合小修小补;如果分层清晰、有接口抽象,那就可以放心加模块。判断方法很简单,搜一下SqlConnection出现在哪些文件里,如果 UI 目录下也有,说明分层没做干净。
我自己的教训是:曾经在一套 UI 和 DAL 混在一起的源码上硬加多仓库功能,改了两个月,最后发现每次加字段都要动十几个窗体,果断放弃重写。所以拿到源码先看分层,比看功能多少重要得多。希望帮到你。
本文还有配套的精品资源,点击获取