简介:这份资源是一套基于C#与SQL Server开发的库存管理系统完整工程,面向学习WinForm桌面开发与数据库应用的学生、初级开发者,以及需要参考仓储管理实现思路的技术人员。压缩包共194个文件,约3.98MB,以56个cs源代码文件为核心,配合26个resx与resources资源文件、17个ico图标、42张jpg界面素材,另有7个sql脚本、mdf与ldf数据库文件及bak备份,并附一份使用说明doc,构成可直接运行与研读的完整项目。系统覆盖商品管理、入库出库、库存查询与报表生成等模块,源码中可看到主窗体、出入库与报损报溢等业务窗体的分层实现,便于理解界面逻辑与数据访问的分离方式。已有719人学习下载,适合按模块拆解代码、对照数据库表结构梳理业务流程,并在此基础上做功能扩展或课程设计参考。
1. 从一份能跑起来的 C# 库存管理系统源码说起
接手一个仓库管理项目时,最怕的不是业务复杂,而是拿到一堆只有界面没有数据库、或者只有建表脚本没有业务逻辑的“半成品”。这次拆的这份 C# 库存管理系统源代码,配套了完整数据库和说明文档,属于少见的“开箱能跑”类型:入库、出库、库存查询、盘点、基础资料维护这几条主线都在,数据库脚本和字段说明也一并给了。适合两类人——刚学完 C# 想找一个完整 CRUD 项目练手的,以及手上有个小型仓储场景、想拿现成骨架改一改就上线的。下面按“它是什么、怎么跑、怎么改”的顺序,把目录、核心代码和踩坑点一次讲透。
2. 项目定位与技术栈:WinForms + ADO.NET 的经典三层结构
2.1 这套系统解决的是哪类库存问题
从功能面看,它面向的是中小型仓库的日常台账管理,不是大型 WMS。核心业务对象就四个:物料(商品)档案、供应商/客户档案、出入库单据、库存余额。典型操作流是:先维护物料和往来单位,再录入入库单增加库存,录入出库单扣减库存,随时查当前库存和流水,月底做一次盘点校正。这种规模用 WinForms 桌面端 + 本地或局域网数据库完全够用,部署成本低,也不需要额外搭 Web 服务。
技术栈上,界面层是 C# WinForms,数据访问走 ADO.NET(SqlConnection、SqlCommand、SqlDataAdapter),数据库是 SQL Server,业务逻辑夹在窗体和数据访问之间。这套组合在今天看不算新,但胜在依赖少、调试直观,对想理解“一个单据如何落库并影响库存”的开发者来说,链路非常清晰。
2.2 目录结构长什么样
拿到源码包,先别急着打开解决方案,先把目录扫一遍。这类项目常见的组织方式如下(具体文件夹名可能略有差异,但职责划分基本一致):
InventorySystem/ ├── InventorySystem.sln # 解决方案入口 ├── InventorySystem/ │ ├── Forms/ # 各业务窗体 │ │ ├── FrmLogin.cs │ │ ├── FrmMain.cs │ │ ├── FrmProduct.cs # 物料档案 │ │ ├── FrmStockIn.cs # 入库单 │ │ ├── FrmStockOut.cs # 出库单 │ │ └── FrmInventory.cs # 库存查询/盘点 │ ├── DAL/ # 数据访问层 │ │ ├── SqlHelper.cs # 连接与命令封装 │ │ ├── ProductDAL.cs │ │ └── StockDAL.cs │ ├── Models/ # 实体类 │ │ ├── Product.cs │ │ └── StockBill.cs │ ├── App.config # 连接字符串 │ └── Program.cs ├── Database/ │ ├── CreateDB.sql # 建库建表脚本 │ └── InitData.sql # 初始数据 └── Docs/ └── 使用说明.docxDAL和Models分开是这套结构里最值得保留的部分——窗体只负责收集输入和展示结果,SQL 集中在 DAL,实体类做参数传递。很多新手项目把 SQL 直接写在按钮点击事件里,改一个字段要翻遍所有窗体,这份源码没有踩这个坑。
2.3 数据库表设计的关键几张表
配套脚本里最核心的是四张表,理解它们的关系,后面看代码就不会迷路:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| Product | 物料档案 | ProductId、ProductName、Unit、SafetyStock |
| StockBill | 出入库单据主表 | BillId、BillType(入/出)、BillDate、Operator |
| StockBillDetail | 单据明细 | DetailId、BillId、ProductId、Qty、Price |
| StockBalance | 库存余额 | ProductId、CurrentQty、LastUpdate |
单据主表 + 明细表是标准的一对多结构,一张入库单可以有多行物料。StockBalance单独存当前库存,是为了查询快——不用每次把历史单据加总。代价是余额表必须和单据写入保持同步,这也是后面要重点讲的坑。
3. 跑起来:环境准备、建库与连接配置
3.1 环境与依赖清单
跑这个项目需要的东西不多,但版本要对上:
- Visual Studio 2019 或更高(社区版即可),安装时勾选“.NET 桌面开发”工作负载
- .NET Framework 4.6 以上(老项目常见 4.5/4.6,打开
.csproj看TargetFrameworkVersion) - SQL Server 2016 以上,Express 版也行
- SQL Server Management Studio(SSMS),用来执行建库脚本
如果打开解决方案提示“找不到引用”,多半是 NuGet 包没还原,右键解决方案选“还原 NuGet 包”即可。这类项目依赖通常很少,最多一个报表或日志库。
3.2 建库与初始化数据
打开 SSMS,连上本地实例,新建查询窗口,按顺序执行脚本:
-- 1. 先执行建库建表脚本 -- 打开 Database/CreateDB.sql,整段执行 -- 脚本内部一般包含 CREATE DATABASE InventoryDB 和 USE InventoryDB -- 2. 再执行初始数据脚本 -- 打开 Database/InitData.sql,整段执行 -- 会插入默认管理员账号、计量单位、示例物料执行完在对象资源管理器里刷新,应该能看到InventoryDB库和四张核心表。如果CreateDB.sql里没有USE语句,记得手动切到目标库再建表,否则表会落到master里,这是新手最常见的翻车点之一。
3.3 连接字符串怎么改
数据库建好后,打开项目里的App.config,找到connectionStrings节点:
<connectionStrings> <add name="InventoryConn" connectionString="Data Source=.;Initial Catalog=InventoryDB;User ID=sa;Password=你的密码" providerName="System.Data.SqlClient" /> </connectionStrings>几个参数说明:Data Source=.表示本机默认实例,如果是命名实例要写成.\SQLEXPRESS;Initial Catalog是库名,必须和建库脚本一致;认证方式可以用 SQL 账号(User ID/Password),也可以改成Integrated Security=True走 Windows 认证,后者在本地开发时更省事。改完记得重新生成解决方案,App.config的改动才会进到bin目录下的配置文件里。
3.4 首次登录与主界面
按说明文档,初始管理员账号一般是admin,密码在InitData.sql里能看到(常见是加密后的固定值或明文提示)。登录成功后进入主窗体,左侧或顶部是功能导航,点“物料档案”能看到示例数据,就说明数据库连通了。如果登录报“连接失败”,先看异常信息里的服务器名和库名,九成是连接字符串没对上。
4. 核心代码拆解:一张入库单如何落库并更新库存
4.1 SqlHelper:所有数据库操作的入口
数据访问层通常有一个SqlHelper类,把连接打开、命令执行、参数化查询封装起来。核心方法大致是这样:
public static class SqlHelper { private static readonly string connStr = ConfigurationManager.ConnectionStrings["InventoryConn"].ConnectionString; // 执行增删改,返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null && parameters.Length > 0) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } // 查询返回 DataTable,供窗体绑定 DataGridView public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlDataAdapter adapter = new SqlDataAdapter(sql, conn)) { if (parameters != null && parameters.Length > 0) adapter.SelectCommand.Parameters.AddRange(parameters); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } }逻辑说明:using保证连接和命令对象及时释放,避免连接池被占满;params SqlParameter[]让调用方以参数化方式传值,杜绝字符串拼接 SQL 带来的注入风险。参数说明:sql是带@参数名占位符的语句,parameters按顺序对应占位符。我一般会在这个类里再加一个ExecuteScalar,用于取单值(比如查当前库存数量),省得每次都走DataTable。
4.2 入库单保存:主表、明细、余额三步走
入库保存是整个系统最关键的写操作,必须保证三件事同时成功:写单据主表、写明细、增加库存余额。正确做法是放进一个事务:
public bool SaveStockIn(StockBill bill, List<StockBillDetail> details) { using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran = conn.BeginTransaction()) { try { // 1. 插入主表,用 SCOPE_IDENTITY 拿回自增 BillId string sqlMain = @"INSERT INTO StockBill(BillType,BillDate,Operator) VALUES(@type,@date,@op); SELECT SCOPE_IDENTITY();"; SqlCommand cmdMain = new SqlCommand(sqlMain, conn, tran); cmdMain.Parameters.AddWithValue("@type", "IN"); cmdMain.Parameters.AddWithValue("@date", bill.BillDate); cmdMain.Parameters.AddWithValue("@op", bill.Operator); int billId = Convert.ToInt32(cmdMain.ExecuteScalar()); // 2. 逐行插入明细,并同步更新库存余额 foreach (var d in details) { string sqlDetail = @"INSERT INTO StockBillDetail(BillId,ProductId,Qty,Price) VALUES(@bid,@pid,@qty,@price)"; SqlCommand cmdDetail = new SqlCommand(sqlDetail, conn, tran); cmdDetail.Parameters.AddWithValue("@bid", billId); cmdDetail.Parameters.AddWithValue("@pid", d.ProductId); cmdDetail.Parameters.AddWithValue("@qty", d.Qty); cmdDetail.Parameters.AddWithValue("@price", d.Price); cmdDetail.ExecuteNonQuery(); // 余额不存在则插入,存在则累加 string sqlBalance = @" IF EXISTS(SELECT 1 FROM StockBalance WHERE ProductId=@pid) UPDATE StockBalance SET CurrentQty=CurrentQty+@qty, LastUpdate=GETDATE() WHERE ProductId=@pid ELSE INSERT INTO StockBalance(ProductId,CurrentQty,LastUpdate) VALUES(@pid,@qty,GETDATE())"; SqlCommand cmdBal = new SqlCommand(sqlBalance, conn, tran); cmdBal.Parameters.AddWithValue("@pid", d.ProductId); cmdBal.Parameters.AddWithValue("@qty", d.Qty); cmdBal.ExecuteNonQuery(); } tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); // 记录日志或抛给上层提示 throw new Exception("入库保存失败:" + ex.Message); } } } }逻辑说明:SCOPE_IDENTITY()取当前作用域内刚插入的自增主键,比@@IDENTITY安全,不会误取触发器产生的 ID。明细循环里对StockBalance做“有则更新、无则插入”,避免首次入库时余额表没记录导致更新零行。参数说明:bill是主表实体,details是明细集合,@qty在入库场景是正数,出库场景传负数或单独写扣减逻辑。整个方法包在SqlTransaction里,任何一行失败都整体回滚,不会出现“单据写了但库存没加”的脏数据。
4.3 库存查询与盘点校正
库存查询相对简单,一条联表语句把物料名和余额拼出来:
public DataTable GetStockList(string keyword) { string sql = @" SELECT p.ProductId, p.ProductName, p.Unit, ISNULL(b.CurrentQty,0) AS CurrentQty, p.SafetyStock FROM Product p LEFT JOIN StockBalance b ON p.ProductId = b.ProductId WHERE p.ProductName LIKE @kw OR p.ProductId LIKE @kw ORDER BY p.ProductId"; return SqlHelper.ExecuteQuery(sql, new SqlParameter("@kw", "%" + keyword + "%")); }用LEFT JOIN加ISNULL是为了让从没入过库的物料也能显示出来,库存显示 0,而不是直接查不到。盘点校正的思路是:录入实盘数量,系统算出差异,生成一条调整单据并直接改写StockBalance,同时留一条盘点记录备查。这部分代码各项目写法差异较大,改造时建议保留“差异留痕”这个设计,否则对不上账时没有后悔药。
5. 避坑与排查:这类项目最容易翻车的五个地方
5.1 单据写了但库存没变
现象:入库单保存成功,列表里能看到单据,但库存查询数量没增加。原因通常是主表和余额表没放在同一个事务里,或者余额更新语句的WHERE ProductId参数传错。解决:按 4.2 的写法把三步包进SqlTransaction,并在保存后立刻查一次余额做自检。我一般会在保存方法末尾加一行日志,打印billId和受影响行数,出问题时一眼能定位。
5.2 并发下库存被覆盖
现象:两个人同时出库同一物料,最终库存比实际多扣或少扣。原因:先SELECT当前数量到内存,加加减减后再UPDATE回去,中间存在时间窗。解决:改成UPDATE StockBalance SET CurrentQty=CurrentQty-@qty WHERE ProductId=@pid AND CurrentQty>=@qty,把判断和扣减合成一条原子语句,受影响行数为 0 就说明库存不足,直接回滚提示。
5.3 连接字符串改了不生效
现象:App.config里明明改了库名,运行还是连旧库。原因:程序读的是bin\Debug下的InventorySystem.exe.config,不是项目根目录的App.config。解决:改完配置后重新生成,或者直接去输出目录确认配置文件内容。这个坑几乎每个 WinForms 新手都会踩一次。
5.4 DataGridView 绑定后改了数据不落库
现象:在表格里改了数量,界面显示变了,数据库没变。原因:DataTable只是内存副本,没有回写逻辑。解决:要么在单元格编辑结束时手动收集值调 DAL 更新,要么用SqlDataAdapter配合Update方法并配置UpdateCommand。前者可控性更强,小项目里我更倾向手动更新,逻辑清楚不容易出玄学问题。
5.5 中文乱码或日期格式报错
现象:物料名存进去变成问号,或者日期插入报转换错误。原因:数据库排序规则不是中文,或代码里用字符串拼日期。解决:建库时选Chinese_PRC_CI_AS排序规则;日期一律用SqlParameter传DateTime类型,不要拼成'2024-01-01'这种字符串。参数化不仅防注入,也顺带解决了格式问题。
6. 二次开发:从能跑到好用,我的改造顺序
拿到能跑的系统只是起点,真正花时间的是按自己场景改。我的习惯是先动数据库再动代码,顺序如下。
第一步,扩字段。比如物料表加Barcode、CategoryId,余额表加WarehouseId支持多仓。改表后同步改Models里的实体类和 DAL 的 SQL,编译一遍让报错把漏改的地方全暴露出来,比人工找靠谱。
第二步,抽公共查询。原项目里各窗体的查询 SQL 容易重复,我把常用联表查询收进StockDAL,窗体只传条件。这样以后加字段只改一处。
第三步,加操作日志。在SqlHelper.ExecuteNonQuery里统一记录 SQL 类型和影响行数到日志表,出问题时能回溯是谁在什么时候改了库存。这个改动很小,但排查价值极高。
第四步,验证。改完不要只看界面,写几条对照 SQL 手工核对:
-- 核对某物料库存是否等于出入库流水净额 SELECT p.ProductName, ISNULL(b.CurrentQty,0) AS BalanceQty, ISNULL(SUM(CASE WHEN sb.BillType='IN' THEN d.Qty ELSE -d.Qty END),0) AS FlowQty FROM Product p LEFT JOIN StockBalance b ON p.ProductId=b.ProductId LEFT JOIN StockBillDetail d ON p.ProductId=d.ProductId LEFT JOIN StockBill sb ON d.BillId=sb.BillId GROUP BY p.ProductName, b.CurrentQty HAVING ISNULL(b.CurrentQty,0) <> ISNULL(SUM(CASE WHEN sb.BillType='IN' THEN d.Qty ELSE -d.Qty END),0);这条语句查出不为空的结果,就说明余额表和流水对不上,得回去查是哪次写入没进事务。我一般把它做成一个“对账”按钮放在库存查询页,开发期每次改完 DAL 都点一遍。
最后提醒一句:这类源码项目的价值不在代码多漂亮,而在业务链路完整、能当骨架用。改的时候尽量保留“单据 + 明细 + 余额 + 事务”这个核心结构,外围界面怎么换都行。从那以后我每次接手类似的库存项目,都先把对账 SQL 跑通再动业务代码,省了太多对不上账的血泪时间。希望帮到你。
本文还有配套的精品资源,点击获取