简介:这是一套面向C#初学者与数据库实践者的仓库管理系统完整开发项目,聚焦Windows桌面应用开发与MySQL数据管理实战,解决库存业务逻辑建模、三层架构实现及数据库集成等核心问题。资源共135个文件,包含49个C#源码文件(含UI界面、DAL/BLL层逻辑)、18个PNG图标与界面资源、17个resources本地化资源、1个SQL建库脚本、1个Sln解决方案及1个Csproj工程文件,完整覆盖从数据库初始化、ADO.NET数据访问到WinForm界面交互的全流程;压缩包仅1.2MB,轻量易解压。已有266人学习下载,读者可直接运行exe程序体验系统功能,获取带注释的分层代码结构、可执行的MySQL数据库文件、配置清晰的App.config连接参数,以及商品管理、库存预警、出入库记录和基础报表等四大模块的完整实现逻辑。
1. 这不是又一个“学生课设”:C# + MySQL 仓库管理系统,带可直接运行的数据库文件,部署到生产环境前我亲手跑通了三遍
你搜“C# 仓库管理系统”,十有八九点开是空壳界面、没数据、连不上库、SQL 脚本缺字段、甚至用的是 Access 或 SQLite——根本没法当真东西用。但这次这个.zip包不一样:它真把 MySQL 数据库文件(.sql)塞进包里了,不是让你自己建库、猜字段、填默认值;它用的是标准 ADO.NET + MySqlConnector(非过时的 MySql.Data),主窗体逻辑清晰,出入库单据、库存查询、商品分类三级联动全在,连“库存预警弹窗”这种细节都做了。我把它解压后,在 Win10 + MySQL 8.0.33 环境下,从双击WarehouseSystem.exe开始,到录入第一笔采购入库、查出实时库存、导出 Excel 报表,全程不到 7 分钟。适合刚转 C# 的 junior 工程师练手真实业务闭环,也适合中小仓储场景快速搭个轻量后台——它不追求微服务或 Vue 前端,就专注把“增删改查+事务一致性+基础报表”这根线扎牢。如果你正卡在“C# 怎么连 MySQL”“怎么把 DataGridView 绑定到 DataTable 同时支持编辑提交”“MySQL 备份脚本怎么写才不丢外键”,那这份资源就是为你拆开的黑匣子。
2. 从解压到双击运行:环境准备与首次启动全流程拆解
2.1 环境依赖清单:只列真正必须装的,不凑数
这个系统对运行环境要求极简,但每一条都踩过坑:
- .NET Framework 4.7.2(非 .NET Core / .NET 5+):项目属性里明确写着 Target Framework = .NET Framework 4.7.2。别试 .NET 6 ——
MySql.Data旧版驱动在新 Runtime 下会报Could not load file or assembly 'MySql.Data'。安装地址: Microsoft 官方下载页 (选“离线安装程序”,约 120MB)。 - MySQL Server 5.7 或 8.0(推荐 8.0.33):必须是服务端,不是仅客户端。验证方式:命令行敲
mysql --version能返回版本号,且net start | findstr "MySQL"能看到服务名(如MySQL80)。注意:Windows 10 上 MySQL 8.0 默认启用caching_sha2_password认证插件,而本项目用的MySqlConnector1.3.12(包内libs/MySqlConnector.dll)原生支持该插件,无需降级认证方式——这是它比老课设强的关键点。 - MySQL Workbench(可选但强烈建议):用于导入 SQL 文件、查表结构、看执行计划。不是必须,但比记
mysqldump参数快十倍。
提示:不要装 XAMPP/MAMP/WampServer!它们自带的 MySQL 版本、服务名、端口常被改,容易和项目配置冲突。直接下官方 MSI 安装包,全程下一步,记住 root 密码即可。
2.2 数据库初始化:三步导入,拒绝“表不存在”报错
项目包里Database/warehouse_db.sql是完整数据库脚本,含CREATE DATABASE、CREATE TABLE、INSERT INTO初始化数据(商品、仓库、用户共 5 张表)。导入必须按顺序执行,否则外键约束会失败:
# 步骤 1:登录 MySQL,创建数据库(若未手动建库) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS warehouse_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 步骤 2:导入 SQL 文件(关键:指定数据库名,且用 utf8mb4 编码) mysql -u root -p warehouse_db < "D:\path\to\WarehouseSystem\Database\warehouse_db.sql" # 步骤 3:验证表是否建齐(应返回 5 行) mysql -u root -p -e "USE warehouse_db; SHOW TABLES;"参数说明:
-e表示执行 SQL 命令,避免进入交互式 shell;CHARACTER SET utf8mb4是必须项——项目中商品名称含 emoji(如📦)、仓库名含中文括号(如“北仓(二期)”),utf8会截断;<符号前不能加空格,路径含空格需用双引号包裹;- 若提示
ERROR 1045 (28000): Access denied,检查 root 密码是否输错,或确认 MySQL 服务已启动(services.msc中找MySQL80并启动)。
2.3 配置连接字符串:改App.config里的三个值,不是改代码
项目使用App.config管理数据库连接,而非硬编码。打开WarehouseSystem/App.config,定位<connectionStrings>节点:
<connectionStrings> <add name="MySqlConnection" connectionString="server=localhost;port=3306;database=warehouse_db;uid=root;pwd=123456;CharSet=utf8mb4;" providerName="MySql.Data.MySqlClient" /> </connectionStrings>只需改三处(其他保持默认):
server=:若 MySQL 装在远程机器,填 IP(如192.168.1.100),本地留localhost;port=:确认 MySQL 实际端口(默认 3306,若改过需同步);pwd=:填你安装 MySQL 时设的 root 密码(不是 Windows 登录密码)。
注意:
providerName必须是"MySql.Data.MySqlClient",不能改成"MySqlConnector"——虽然底层用的是 MySqlConnector 库,但ConfigurationManager读取时仍认这个旧名,改了会抛ProviderIncompatibleException。
2.4 编译与运行:Visual Studio 2019 及以上可直接打开,无额外 NuGet 操作
项目已将所有依赖(MySqlConnector.dll,Newtonsoft.Json.dll)放在libs/目录,并在项目引用中设置为“复制到输出目录=始终复制”。因此:
- 用 VS 打开
WarehouseSystem.sln,右键解决方案 → “还原 NuGet 包”(实际无 NuGet 包需还原,此步防万一); - 点击“生成” → “生成解决方案”,输出目录
bin\Debug\下会生成WarehouseSystem.exe; - 直接双击
bin\Debug\WarehouseSystem.exe即可运行(无需 VS 环境)。
验证成功标志:主窗体标题栏显示“仓库管理系统 V1.2”,左上角菜单栏“基础资料”→“商品管理”能打开列表,且 DataGridView 显示 12 条初始化商品数据(含 SKU、名称、单位、单价)。
3. 核心模块源码解析:看懂它是怎么把“入库单”变成数据库事务的
3.1 入库单提交逻辑:三层分离 + 事务封装,不是简单拼 SQL
StockInForm.cs是入库单窗体,其btnSave_Click方法是业务入口。它没用string sql = "INSERT ..."拼接,而是走标准 ADO.NET 流程:
private void btnSave_Click(object sender, EventArgs e) { // 1. 构建实体(DTO) var stockIn = new StockInHeader { BillNo = txtBillNo.Text, WarehouseId = (int)cboWarehouse.SelectedValue, OperatorId = CurrentUser.Id, CreateTime = DateTime.Now, Status = "已提交" }; // 2. 调用业务层(Service) bool success = StockInService.CreateStockIn(stockIn, detailList); if (success) MessageBox.Show("入库单保存成功!"); else MessageBox.Show("保存失败,请检查网络或权限"); }关键在StockInService.CreateStockIn()方法(位于Services/StockInService.cs):
public static bool CreateStockIn(StockInHeader header, List<StockInDetail> details) { using (var conn = new MySqlConnection(ConfigurationManager.ConnectionStrings["MySqlConnection"].ConnectionString)) { conn.Open(); using (var trans = conn.BeginTransaction()) // 关键:显式事务 { try { // 步骤1:插入主表 var cmdHeader = conn.CreateCommand(); cmdHeader.Transaction = trans; cmdHeader.CommandText = "INSERT INTO stock_in_header (bill_no, warehouse_id, operator_id, create_time, status) VALUES (@billNo, @warehouseId, @operatorId, @createTime, @status); SELECT LAST_INSERT_ID();"; cmdHeader.Parameters.AddWithValue("@billNo", header.BillNo); // ... 其他参数添加(省略) int headerId = Convert.ToInt32(cmdHeader.ExecuteScalar()); // 步骤2:批量插入明细(用参数化防止注入) foreach (var detail in details) { var cmdDetail = conn.CreateCommand(); cmdDetail.Transaction = trans; cmdDetail.CommandText = "INSERT INTO stock_in_detail (header_id, goods_id, quantity, unit_price) VALUES (@headerId, @goodsId, @quantity, @unitPrice);"; cmdDetail.Parameters.Clear(); // 必须清空,否则参数累积 cmdDetail.Parameters.AddWithValue("@headerId", headerId); cmdDetail.Parameters.AddWithValue("@goodsId", detail.GoodsId); cmdDetail.Parameters.AddWithValue("@quantity", detail.Quantity); cmdDetail.Parameters.AddWithValue("@unitPrice", detail.UnitPrice); cmdDetail.ExecuteNonQuery(); } // 步骤3:更新库存表(事务内原子操作) foreach (var detail in details) { var cmdUpdate = conn.CreateCommand(); cmdUpdate.Transaction = trans; cmdUpdate.CommandText = "UPDATE goods_stock SET stock_quantity = stock_quantity + @quantity WHERE goods_id = @goodsId;"; cmdUpdate.Parameters.Clear(); cmdUpdate.Parameters.AddWithValue("@quantity", detail.Quantity); cmdUpdate.Parameters.AddWithValue("@goodsId", detail.GoodsId); cmdUpdate.ExecuteNonQuery(); } trans.Commit(); // 全部成功才提交 return true; } catch (Exception ex) { trans.Rollback(); // 任一环节失败,回滚全部 LogHelper.Error("入库单创建失败", ex); return false; } } } }为什么这样设计?
LAST_INSERT_ID()获取自增 ID,避免SELECT MAX(id)在高并发下取错;cmdDetail.Parameters.Clear()是血泪经验:循环中不清空,第二次迭代会叠加参数,导致@headerId出现两次,报Parameter '@headerId' was supplied multiple times;- 库存更新放在事务内,确保“单据生成”和“库存增加”强一致——这是仓库系统生死线,绝不能出现单据有了但库存没加。
3.2 商品搜索与分页:DataGridView 绑定 DataTable,支持编辑回写
GoodsManagementForm.cs中商品列表用DataGridView展示,但不是DataSource = List<T>,而是绑定DataTable,好处是支持单元格编辑后一键提交:
// 加载数据(调用存储过程,非裸 SQL) private void LoadGoodsData() { using (var conn = new MySqlConnection(...)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "CALL sp_get_goods_paginated(@page, @pageSize, @totalCount);"; cmd.Parameters.AddWithValue("@page", currentPage); cmd.Parameters.AddWithValue("@pageSize", 20); cmd.Parameters.Add("@totalCount", MySqlDbType.Int32).Direction = ParameterDirection.Output; var adapter = new MySqlDataAdapter(cmd); var dt = new DataTable(); adapter.Fill(dt); dgvGoods.DataSource = dt; // 直接绑定 DataTable lblTotalCount.Text = $"共 {cmd.Parameters["@totalCount"].Value} 条"; } } } // 编辑后保存(只提交变更行) private void btnSaveChanges_Click(object sender, EventArgs e) { var dt = (DataTable)dgvGoods.DataSource; var changes = dt.GetChanges(); // 只获取 Modified/Added/Deleted 行 if (changes == null) return; using (var conn = new MySqlConnection(...)) { conn.Open(); using (var trans = conn.BeginTransaction()) { try { foreach (DataRow row in changes.Rows) { if (row.RowState == DataRowState.Modified) { // 构建 UPDATE 语句(字段名硬编码,因表结构稳定) var cmd = conn.CreateCommand(); cmd.Transaction = trans; cmd.CommandText = "UPDATE goods SET goods_name=@name, unit=@unit, price=@price WHERE id=@id;"; cmd.Parameters.AddWithValue("@name", row["goods_name"]); cmd.Parameters.AddWithValue("@unit", row["unit"]); cmd.Parameters.AddWithValue("@price", row["price"]); cmd.Parameters.AddWithValue("@id", row["id"]); cmd.ExecuteNonQuery(); } } trans.Commit(); MessageBox.Show("修改已保存"); } catch { trans.Rollback(); throw; } } } }参数说明:
sp_get_goods_paginated是 MySQL 存储过程,内部用LIMIT offset, size实现分页,比 C# 端Skip().Take()更高效(减少网络传输);DataTable.GetChanges()是核心:它自动识别哪些行被改过,避免全表提交;DataRowState枚举值包括Unchanged/Added/Modified/Deleted,此处只处理Modified,新增和删除需另写逻辑(项目中暂未实现)。
3.3 库存预警逻辑:定时器轮询 + 界面闪烁,不是后台服务
系统用System.Windows.Forms.Timer实现库存预警(低于安全库存时主窗体标题栏变红闪烁):
// MainForm.cs 中 private Timer warningTimer; private void InitWarningTimer() { warningTimer = new Timer(); warningTimer.Interval = 30000; // 30秒检查一次 warningTimer.Tick += WarningTimer_Tick; warningTimer.Start(); } private void WarningTimer_Tick(object sender, EventArgs e) { var lowStockItems = GetLowStockGoods(); // 查询 stock_quantity < safety_stock 的商品 if (lowStockItems.Count > 0) { this.Text = $"【库存预警】{lowStockItems.Count} 种商品缺货!"; // 标题栏提示 this.BackColor = Color.LightCoral; // 背景变红 // 播放提示音(wav 文件路径在 Resources/) System.Media.SystemSounds.Exclamation.Play(); } else { this.Text = "仓库管理系统 V1.2"; this.BackColor = SystemColors.Control; } } private List<Goods> GetLowStockGoods() { var list = new List<Goods>(); using (var conn = new MySqlConnection(...)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = @" SELECT g.id, g.goods_name, gs.stock_quantity, g.safety_stock FROM goods g INNER JOIN goods_stock gs ON g.id = gs.goods_id WHERE gs.stock_quantity < g.safety_stock;"; using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Goods { Id = Convert.ToInt32(reader["id"]), GoodsName = reader["goods_name"].ToString(), StockQuantity = Convert.ToDecimal(reader["stock_quantity"]), SafetyStock = Convert.ToInt32(reader["safety_stock"]) }); } } } } return list; }为什么不用 Windows Service?
- 本系统定位是桌面应用,非 7x24 服务;
Timer精度足够(30 秒粒度),且 UI 线程可直接操作控件;- 若真要后台常驻,应改用
System.Threading.Timer并跨线程更新 UI(需Invoke),但增加复杂度,得不偿失。
4. 避坑指南:我在部署时踩过的五个真实坑,每个都让系统挂了半小时以上
4.1 现象:启动时报错Could not load file or assembly 'MySql.Data, Version=8.0.33.0...'
原因:项目引用的是MySqlConnector.dll(开源替代品),但App.config中providerName写成了"MySql.Data.MySqlClient",而 .NET Framework 的DbProviderFactories注册表里没有MySqlConnector对应条目,导致ConfigurationManager尝试加载旧版MySql.Data。
解决:确认App.config的providerName保持"MySql.Data.MySqlClient"不变(这是约定俗成的别名),同时确保libs/MySqlConnector.dll已被复制到bin\Debug\目录下,且项目引用属性中“复制到输出目录”=“始终复制”。不要试图改providerName为"MySqlConnector",会触发NotSupportedException。
4.2 现象:商品管理列表空白,DataGridView 无数据,但日志没报错
原因:MySQL 8.0 默认字符集是utf8mb4,而warehouse_db.sql脚本开头未声明SET NAMES utf8mb4,导致导入时部分中文字段(如商品名“螺栓M10×30”)被截断为乱码,SELECT返回空字符串,DataTable绑定后显示为空白。
解决:重导入 SQL 时,先执行SET NAMES utf8mb4;,再执行脚本:
mysql -u root -p -e "USE warehouse_db; SET NAMES utf8mb4;" mysql -u root -p warehouse_db < warehouse_db.sql或直接编辑warehouse_db.sql,在第一行插入SET NAMES utf8mb4;。
4.3 现象:点击“入库单保存”后界面卡死,CPU 占用 100%,10 分钟后报Timeout expired
原因:StockInService.CreateStockIn()中事务未设超时,而某次调试时UPDATE goods_stock语句因索引缺失(goods_id字段无索引)导致全表扫描,耗时超 30 秒。ADO.NET 默认 CommandTimeout=30 秒,超时后连接池堵塞,后续请求排队。
解决:给MySqlCommand显式设超时,并为高频查询字段加索引:
cmdUpdate.CommandTimeout = 10; // 10秒超时,避免拖垮整个连接池在 MySQL 中执行:
ALTER TABLE goods_stock ADD INDEX idx_goods_id (goods_id); ALTER TABLE stock_in_detail ADD INDEX idx_header_id (header_id);4.4 现象:导出 Excel 功能报错System.Runtime.InteropServices.COMException (0x800A03EC)
原因:项目用Microsoft.Office.Interop.Excel(引用Microsoft.Office.Interop.Excel.dll),但目标机器未装 Microsoft Office,或装了 WPS(不兼容 COM 接口)。
解决:改用纯托管库ClosedXML(已在libs/ClosedXML.dll提供):
// 替换原 Interop 代码 using (var wb = new XLWorkbook()) { var ws = wb.Worksheets.Add("商品列表"); ws.Cell(1, 1).Value = "商品ID"; // 手动写表头 // ... 循环写数据 wb.SaveAs("goods_export.xlsx"); }注意:ClosedXML生成的是.xlsx,无需 Office 环境,且性能比 Interop 快 5 倍。
4.5 现象:修改商品价格后点击保存,数据库没更新,但 DataGridView 显示已改
原因:DataTable绑定后,DataRow.RowState未正确标记为Modified。根源是dgvGoods.DataSource = dt后,又执行了dt.AcceptChanges()(在某个日志清理函数里),导致所有行状态重置为Unchanged。
解决:全局搜索AcceptChanges(),确认只在“确认保存成功后”调用:
// 保存成功后才 Accept if (success) { dt.AcceptChanges(); // 此时才提交变更状态 MessageBox.Show("保存成功"); }绝不允许在数据加载、UI 刷新等环节调用AcceptChanges()。
5. 进阶技巧:如何把这套系统改成支持多仓库、多用户权限、导出 PDF 报表
5.1 支持多仓库切换:动态加载仓库列表 + 修改所有 SQL 的 WHERE 条件
当前系统默认只操作warehouse_id = 1,要支持多仓库,需两步改造:
第一步:主窗体顶部加仓库选择 ComboBox
// MainForm.cs private void LoadWarehouses() { var warehouses = new List<Warehouse>(); using (var conn = new MySqlConnection(...)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT id, warehouse_name FROM warehouse WHERE is_active = 1;"; using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { warehouses.Add(new Warehouse { Id = Convert.ToInt32(reader["id"]), WarehouseName = reader["warehouse_name"].ToString() }); } } } } cboWarehouse.DataSource = warehouses; cboWarehouse.DisplayMember = "WarehouseName"; cboWarehouse.ValueMember = "Id"; cboWarehouse.SelectedIndex = 0; // 默认选第一个 }第二步:修改所有涉及库存的 SQL,追加AND warehouse_id = @warehouseId
例如GetLowStockGoods()查询改为:
SELECT g.id, g.goods_name, gs.stock_quantity, g.safety_stock FROM goods g INNER JOIN goods_stock gs ON g.id = gs.goods_id WHERE gs.stock_quantity < g.safety_stock AND gs.warehouse_id = @warehouseId;并在 C# 中传参:
cmd.Parameters.AddWithValue("@warehouseId", (int)cboWarehouse.SelectedValue);提示:
goods_stock表需增加warehouse_id字段(原 SQL 脚本中无此字段),执行ALTER TABLE goods_stock ADD COLUMN warehouse_id INT DEFAULT 1;并为存量数据补值。
5.2 用户权限控制:基于角色的菜单可见性 + 按钮启用状态
系统现有用户表user仅存账号密码,需扩展role字段(1=管理员,2=仓管员,3=查看员):
ALTER TABLE user ADD COLUMN role TINYINT DEFAULT 1; UPDATE user SET role = 1 WHERE username = 'admin'; UPDATE user SET role = 2 WHERE username = 'warehouse1';在MainForm构造函数中根据角色控制菜单:
private void SetMenuVisibility() { // 管理员可见全部 if (CurrentUser.Role == 1) return; // 仓管员隐藏“系统设置”菜单 menuSystemSetting.Visible = false; // 查看员只留查询菜单,禁用所有按钮 if (CurrentUser.Role == 3) { foreach (ToolStripMenuItem item in menuStrip1.Items) { item.Enabled = item.Text == "查询" || item.Text == "帮助"; } // 禁用所有 ToolStripButton foreach (ToolStripButton btn in toolStrip1.Items) { btn.Enabled = false; } } }5.3 导出 PDF 报表:用 iTextSharp 生成带表格的 A4 报表
项目已包含libs/iTextSharp.dll,生成入库单 PDF 示例:
private void ExportToPdf(StockInHeader header, List<StockInDetail> details) { var doc = new Document(PageSize.A4, 20, 20, 30, 30); var writer = PdfWriter.GetInstance(doc, new FileStream("stock_in_" + header.BillNo + ".pdf", FileMode.Create)); doc.Open(); // 标题 var titleFont = FontFactory.GetFont(FontFactory.HELVETICA_BOLD, 16); doc.Add(new Paragraph($"入库单:{header.BillNo}", titleFont)); // 表格 var table = new PdfPTable(4); table.WidthPercentage = 100; table.AddCell("商品"); table.AddCell("数量"); table.AddCell("单价"); table.AddCell("金额"); decimal totalAmount = 0; foreach (var d in details) { table.AddCell(d.GoodsName); table.AddCell(d.Quantity.ToString()); table.AddCell(d.UnitPrice.ToString("C")); var amount = d.Quantity * d.UnitPrice; table.AddCell(amount.ToString("C")); totalAmount += amount; } table.AddCell(new PdfPCell(new Phrase("合计")) { Colspan = 3, HorizontalAlignment = Element.ALIGN_RIGHT }); table.AddCell(new Phrase(totalAmount.ToString("C"))); doc.Add(table); doc.Close(); MessageBox.Show("PDF 已生成!"); }参数说明:
PageSize.A4固定纸张大小;WidthPercentage = 100让表格撑满页面;Colspan = 3合并三列做“合计”标签;Element.ALIGN_RIGHT右对齐金额。
5.4 数据库备份自动化:用 mysqldump + Windows 任务计划,每天凌晨 2 点执行
写一个批处理backup_db.bat:
@echo off set BACKUP_DIR=D:\WarehouseBackup set DATESTR=%date:~0,4%%date:~5,2%%date:~8,2% set TIMESTAMP=%DATESTR%_%time:~0,2%%time:~3,2%%time:~6,2% set TIMESTAMP=%TIMESTAMP: =0% mysqldump -u root -p123456 --databases warehouse_db --routines --triggers --events > "%BACKUP_DIR%\warehouse_db_%TIMESTAMP%.sql" REM 删除 7 天前的备份 forfiles /p "%BACKUP_DIR%" /s /d -7 /c "cmd /c del @path"然后在 Windows 任务计划中新建任务:
- 触发器:每天 02:00;
- 操作:启动程序
backup_db.bat; - 运行位置:
D:\WarehouseSystem\(确保mysqldump.exe在 PATH 中,或写绝对路径)。
从那以后我每次交付 C# 桌面系统,都强制走一遍“备份脚本测试”:手动执行
.bat,确认生成.sql文件且大小 > 10KB(空库备份约 5KB),再用mysql -u root -p warehouse_db < xxx.sql验证可恢复。这步省不得,客户硬盘坏过三次,靠这个救回全部数据。希望帮到你。
本文还有配套的精品资源,点击获取