
简介这是一套面向C#初学者与数据库课程实践者的仓库管理系统完整项目源码配套MySQL数据库文件可用于课程设计、毕业设计或自学练手。系统覆盖物品入库、出库、查询、统计等日常作业并实现按名称、类别、入库时间、库存量、供应商、客户等八种查询场景同时包含用户登录验证、权限管理与参数化查询等安全机制。压缩包共163个文件约1.22MB以49个cs源代码文件、18个resx资源文件、18个png界面截图、27个zbak备份文件及sql数据库脚本、sln解决方案、csproj工程文件为主另附README.md安装说明与查询案例文档。目前已有29人学习下载。读者可从中获得完整的C#面向对象架构范例、MySQL表结构与视图存储过程设计、Windows Forms界面实现以及防SQL注入的编码思路适合对照源码理解业务逻辑与数据库交互流程。1. 基于C#的仓库管理系统从建库到跑通入库出库的完整落地路径很多做 C# 上位机或者企业内训的同行接到的第一个带数据库的完整项目往往就是仓库管理系统。它不像纯算法项目那样玄学也不像纯前端那样容易翻车但真正要把「基于C#的仓库管理系统含MySQL数据库文件」跑起来新手最容易卡在三个地方MySQL 装完连不上、C# 连 MySQL 的驱动选错、数据库文件导入后中文乱码。这篇笔记就按我实际带人做项目的顺序把选型理由、建库脚本、C# 数据访问层、入库出库核心逻辑和几个血泪坑一次讲清楚。适合刚学完 C# 基础语法、想找一个能写进简历的完整项目练手的开发者也适合需要快速给内部搭一套轻量仓储工具的工程师。整套方案不依赖任何云服务本地 MySQL 加一个 WinForms 或 WPF 客户端就能跑通。2. 环境选型与数据库文件落地MySQL 8.0 还是 5.7C# 驱动怎么挑2.1 为什么这套系统我优先推荐 MySQL 8.0 而不是 5.7热词里反复出现「mysql 5.7.44 安装过程详细」和「mysql安装教程8.0」说明很多人还在纠结版本。我的实际经验是新项目直接上 MySQL 8.0老机器或者公司强制要求兼容旧环境才退回 5.7。原因有三个。第一8.0 默认字符集是 utf8mb4仓库管理系统里商品名称、供应商备注经常出现生僻字或者 emoji5.7 默认 latin1 或 utf8 三字节导入含四字节字符的数据库文件时直接报错。第二8.0 的窗口函数和 CTE 在写库存流水统计时非常顺手比如按商品分组取最近一次入库记录5.7 得绕子查询。第三8.0 的安装器在 Windows 10 上已经比较稳定热词里「mysql在windows10上怎么安装」的痛点主要集中在 5.7 的 zip 解压版手动配置 my.ini。安装时记住两个关键点端口保持 3306认证方式选Use Legacy Authentication Method或者装完执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。因为 C# 的 MySQL 驱动对 caching_sha2_password 的支持在旧版本 Connector/NET 里会报Authentication method caching_sha2_password not supported这个坑后面避坑章节还会细说。2.2 数据库文件怎么导入建库脚本与三个必改参数所谓「含MySQL数据库文件」通常是一个.sql导出文件。拿到后不要直接双击先建库再导入。下面是我常用的建库和导入命令在 MySQL 命令行或者 Navicat 查询窗口执行都行。-- 创建仓库管理系统专用数据库字符集必须 utf8mb4 CREATE DATABASE warehouse_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 切换到该库 USE warehouse_db; -- 导入前先确认 sql 文件里的建表语句没有指定其他库名 -- 如果有用文本编辑器把 CREATE DATABASE 和 USE 语句删掉或改掉导入用命令行更可控# 在 Windows 命令行中进入 MySQL 的 bin 目录 mysql -u root -p warehouse_db D:\warehouse_db.sql执行后如果报ERROR 1273 (HY000): Unknown collation: utf8mb4_0900_ai_ci说明导出文件来自 MySQL 8.0而你的目标库是 5.7。解决办法是把 sql 文件里的utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci再重新导入。这个替换用记事本的替换功能就能做但注意别把数据内容里的同名字符串误替换最好用支持正则的编辑器限定在COLLATE后面。导入完成后执行SHOW TABLES;应该能看到product、inventory、inbound_record、outbound_record、user这几张核心表。如果表数量不对检查 sql 文件是否被截断尤其是用 phpMyAdmin 导出的文件有时会因为超时只导出一半。2.3 C# 侧驱动选择Connector/NET 还是 MySqlConnector这是新手最容易翻车的地方。Visual Studio 的 NuGet 里搜MySql会出来两个包MySql.DataOracle 官方 Connector/NET和MySqlConnector社区异步驱动。我的建议是WinForms 同步操作为主的小项目用MySql.Data就行教程多、资料全如果打算用 async/await 写 WPF 或者后续要上 ASP.NET Core直接选MySqlConnector它的异步是真异步不会像MySql.Data那样在 UI 线程上假死。安装命令# 在 Visual Studio 的包管理器控制台执行 Install-Package MySql.Data -Version 8.0.33或者用 .NET CLIdotnet add package MySql.Data --version 8.0.33版本号不要盲目追最新8.0.33 是我在多个项目里验证过和 MySQL 8.0 服务端配合稳定的版本。装完后在App.config或appsettings.json里配置连接字符串connectionStrings add nameWarehouseConn connectionStringserverlocalhost;port3306;databasewarehouse_db;uidroot;pwd你的密码;charsetutf8mb4;SslModenone; providerNameMySql.Data.MySqlClient/ /connectionStringsSslModenone在本地开发时能避免自签名证书报错生产环境如果 MySQL 开了 SSL 再改成Preferred。charsetutf8mb4必须写否则 C# 写入的中文在数据库里显示成问号。3. C# 数据访问层与入库出库核心逻辑从连接封装到事务扣减3.1 封装一个不翻车的 MySQL 帮助类不管用 WinForms 还是 WPF数据访问层都建议单独放一个DAL文件夹。下面这个MySqlHelper类是我用了很多项目的精简版核心是把连接创建、参数化查询和事务入口收在一起。using MySql.Data.MySqlClient; using System; using System.Configuration; using System.Data; namespace Warehouse.DAL { public static class MySqlHelper { // 从配置文件读取连接字符串避免硬编码 private static readonly string ConnStr ConfigurationManager.ConnectionStrings[WarehouseConn].ConnectionString; // 创建并打开连接调用方负责 using 释放 public static MySqlConnection GetConnection() { var conn new MySqlConnection(ConnStr); conn.Open(); return conn; } // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params MySqlParameter[] parameters) { using (var conn GetConnection()) using (var cmd new MySqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } // 执行查询返回 DataTable适合绑定 DataGridView public static DataTable ExecuteQuery(string sql, params MySqlParameter[] parameters) { using (var conn GetConnection()) using (var cmd new MySqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); using (var adapter new MySqlDataAdapter(cmd)) { var dt new DataTable(); adapter.Fill(dt); return dt; } } } // 执行标量用于 COUNT、SUM 等 public static object ExecuteScalar(string sql, params MySqlParameter[] parameters) { using (var conn GetConnection()) using (var cmd new MySqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } } } }逻辑说明GetConnection每次新建连接依赖 MySQL 的连接池不要自己写单例连接否则多线程下会出Connection is already in use。ExecuteNonQuery和ExecuteQuery都用了using确保连接释放。参数用params MySqlParameter[]是为了强制调用方走参数化杜绝 SQL 拼接。参数说明连接字符串里的SslModenone只在本地开发用charsetutf8mb4必须保留如果数据库端口不是 3306改port值。3.2 入库操作事务里同时写流水和更新库存入库不是简单插一条记录它要同时做两件事往inbound_record插流水往inventory增加对应商品库存。这两步必须在一个事务里否则插了流水没加库存盘点时就是一笔糊涂账。public bool Inbound(int productId, int quantity, string operatorName) { using (var conn MySqlHelper.GetConnection()) using (var tran conn.BeginTransaction()) { try { // 第一步插入入库流水 string sqlRecord INSERT INTO inbound_record (product_id, quantity, operator, inbound_time) VALUES (pid, qty, op, NOW()); using (var cmd new MySqlCommand(sqlRecord, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(op, operatorName); cmd.ExecuteNonQuery(); } // 第二步更新库存如果该商品还没有库存记录则插入 string sqlStock INSERT INTO inventory (product_id, stock_qty) VALUES (pid, qty) ON DUPLICATE KEY UPDATE stock_qty stock_qty qty; using (var cmd new MySqlCommand(sqlStock, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); // 实际项目里写日志这里简单抛出 throw new Exception(入库失败 ex.Message); } } }逻辑说明ON DUPLICATE KEY UPDATE依赖inventory表的product_id上有唯一索引或主键。如果建表时没加这个语句会变成一直插入新行库存就错了。执行前先确认SHOW INDEX FROM inventory;里有product_id的唯一索引。参数说明quantity建议在业务层校验大于 0数据库层面可以加CHECK (quantity 0)但 MySQL 5.7 会忽略 CHECK8.0 才真正生效。operatorName从登录用户会话里取不要从界面文本框直接拿否则谁都能改。3.3 出库操作先查库存再扣减防止负库存出库比入库多一步校验当前库存是否足够。这个校验必须在事务里做并且用SELECT ... FOR UPDATE锁住那一行否则两个出库单同时提交会把库存扣成负数。public bool Outbound(int productId, int quantity, string operatorName) { using (var conn MySqlHelper.GetConnection()) using (var tran conn.BeginTransaction()) { try { // 锁定该商品库存行防止并发扣减 string sqlCheck SELECT stock_qty FROM inventory WHERE product_id pid FOR UPDATE; int currentStock; using (var cmd new MySqlCommand(sqlCheck, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); var result cmd.ExecuteScalar(); if (result null) throw new Exception(该商品没有库存记录无法出库); currentStock Convert.ToInt32(result); } if (currentStock quantity) throw new Exception($库存不足当前库存 {currentStock}请求出库 {quantity}); // 扣减库存 string sqlUpdate UPDATE inventory SET stock_qty stock_qty - qty WHERE product_id pid; using (var cmd new MySqlCommand(sqlUpdate, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.ExecuteNonQuery(); } // 写出库流水 string sqlRecord INSERT INTO outbound_record (product_id, quantity, operator, outbound_time) VALUES (pid, qty, op, NOW()); using (var cmd new MySqlCommand(sqlRecord, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(op, operatorName); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } }逻辑说明FOR UPDATE只在事务里生效所以BeginTransaction必须在查询之前。如果库存表数据量很大这个行锁会影响并发但仓库管理系统的出库频率通常不高可以接受。真正高并发的场景会把库存扣减放到 Redis 里预扣但那是另一个话题了。参数说明productId和quantity都走参数化不要用字符串拼接。operatorName同样来自登录态。3.4 用 DataGridView 绑定库存列表的完整写法WinForms 里展示库存最省事的方式是DataGridView加DataTable。下面这段代码放在窗体加载事件里配合一个刷新按钮。private void LoadInventory() { string sql SELECT p.product_code AS 商品编码, p.product_name AS 商品名称, p.unit AS 单位, IFNULL(i.stock_qty, 0) AS 当前库存 FROM product p LEFT JOIN inventory i ON p.product_id i.product_id ORDER BY p.product_code; DataTable dt MySqlHelper.ExecuteQuery(sql); dataGridView1.DataSource dt; dataGridView1.AutoSizeColumnsMode DataGridViewAutoSizeColumnsMode.Fill; }逻辑说明用LEFT JOIN保证没有库存记录的商品也能显示出来库存显示 0。IFNULL是 MySQL 的函数等价于 C# 里的空合并。ORDER BY按商品编码排序方便仓管对账。参数说明AutoSizeColumnsMode.Fill让列宽自适应窗体如果列太多出现横向滚动条改成DisplayedCells并手动设置每列Width。4. 避坑与排查连接失败、中文乱码、事务不生效的 5 个真实记录4.1 现象C# 报 Authentication method caching_sha2_password not supported原因MySQL 8.0 默认认证插件是caching_sha2_password而旧版MySql.Data驱动不支持。解决要么升级MySql.Data到 8.0.33 以上要么在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;然后FLUSH PRIVILEGES;。我一般选后者因为改驱动版本可能引入其他兼容问题。4.2 现象数据库里中文显示成问号或者 C# 读取出来是乱码原因建库时字符集不是 utf8mb4或者连接字符串没写charsetutf8mb4。解决先SHOW VARIABLES LIKE character_set%;确认服务端字符集如果是 latin1需要改my.ini里的character-set-serverutf8mb4并重启 MySQL 服务。连接字符串里charsetutf8mb4必须加缺一不可。已经存进去的问号数据救不回来只能重新导入。4.3 现象入库流水插入了但库存没变原因事务没有提交或者inventory表缺少product_id的唯一索引导致ON DUPLICATE KEY UPDATE没按预期更新。解决检查代码里tran.Commit()是否执行到执行SHOW INDEX FROM inventory;确认product_id有唯一索引没有就ALTER TABLE inventory ADD UNIQUE INDEX uk_product (product_id);。注意加索引前先清理重复数据否则加不上。4.4 现象出库时两个客户端同时操作库存扣成负数原因没有用FOR UPDATE锁行或者锁行和扣减不在同一个事务里。解决确保BeginTransaction在SELECT ... FOR UPDATE之前并且扣减和流水插入都在同一个conn和tran上。如果用了连接池注意MySqlHelper.GetConnection()每次返回的是不同连接事务必须绑定同一个连接对象。4.5 现象导入 sql 文件时报 Unknown collation utf8mb4_0900_ai_ci原因导出文件来自 MySQL 8.0目标库是 5.7。解决用文本编辑器把 sql 文件里的utf8mb4_0900_ai_ci全部替换为utf8mb4_general_ciutf8mb4_0900_ai_ci是 8.0 特有的排序规则。替换后重新导入。如果文件很大用sed命令sed -i s/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g warehouse_db.sql。5. 进阶技巧用存储过程做月度库存汇总与 C# 调用5.1 为什么把汇总逻辑放进存储过程仓库管理系统跑一段时间后老板会要「这个月每个商品入了多少、出了多少、结存多少」。如果在 C# 里用多次查询拼结果代码又长又容易在并发时读到不一致的数据。把汇总做成存储过程一次调用返回结果集既快又稳。MySQL 存储过程在热词里也被提到这里给一个实际能用的版本。DELIMITER // CREATE PROCEDURE sp_monthly_summary(IN p_year INT, IN p_month INT) BEGIN SELECT p.product_code AS 商品编码, p.product_name AS 商品名称, IFNULL(SUM(CASE WHEN r.type IN THEN r.quantity END), 0) AS 入库合计, IFNULL(SUM(CASE WHEN r.type OUT THEN r.quantity END), 0) AS 出库合计, IFNULL(SUM(CASE WHEN r.type IN THEN r.quantity ELSE -r.quantity END), 0) AS 结存 FROM product p LEFT JOIN ( SELECT product_id, quantity, IN AS type, inbound_time AS op_time FROM inbound_record UNION ALL SELECT product_id, quantity, OUT AS type, outbound_time AS op_time FROM outbound_record ) r ON p.product_id r.product_id AND YEAR(r.op_time) p_year AND MONTH(r.op_time) p_month GROUP BY p.product_id, p.product_code, p.product_name ORDER BY p.product_code; END // DELIMITER ;逻辑说明用UNION ALL把入库和出库流水合并成一张临时表再用CASE WHEN分别求和。LEFT JOIN保证没有流水的商品也显示结存为 0。YEAR和MONTH函数在流水量大的时候会走全表扫描如果数据超过几十万行建议在inbound_time和outbound_time上建索引或者改成按时间范围查询。参数说明p_year和p_month由 C# 传入调用前在业务层校验月份在 1 到 12 之间。存储过程创建后可以用CALL sp_monthly_summary(2025, 1);测试。5.2 C# 调用存储过程并绑定到报表界面public DataTable GetMonthlySummary(int year, int month) { using (var conn MySqlHelper.GetConnection()) using (var cmd new MySqlCommand(sp_monthly_summary, conn)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(p_year, year); cmd.Parameters.AddWithValue(p_month, month); using (var adapter new MySqlDataAdapter(cmd)) { var dt new DataTable(); adapter.Fill(dt); return dt; } } }逻辑说明CommandType.StoredProcedure告诉驱动这是存储过程参数名要和存储过程定义的一致。返回的DataTable直接绑到报表的DataGridView或者导出 Excel 都行。导出 Excel 可以用ClosedXML或者NPOI这里不展开。参数说明year和month从界面上的DateTimePicker取注意DateTimePicker的CustomFormat设成yyyy-MM更直观。5.3 一个我踩过的坑存储过程参数名大小写MySQL 存储过程的参数名在 Windows 上不区分大小写但在 Linux 上区分。如果开发用 Windows、部署到 Linux 服务器p_year和P_Year会被当成两个参数调用时报Incorrect number of arguments。我的习惯是全部用小写加下划线C# 里也保持一致。这个坑不常遇到但一旦遇到能查半天。5.4 验证整套系统是否跑通的最小清单部署到新机器后按这个顺序验证第一步mysql -u root -p -e SHOW DATABASES;确认warehouse_db存在第二步SELECT COUNT(*) FROM product;确认基础数据导入第三步在 C# 里点一次入库然后SELECT * FROM inbound_record ORDER BY inbound_time DESC LIMIT 1;看流水第四步SELECT * FROM inventory WHERE product_id 刚入库的商品;看库存是否增加第五步点一次出库重复第三步和第四步看扣减。五步都过系统基本可用。我自己的习惯是每次改完数据访问层代码先把这五步跑一遍再提交比写单元测试快也比事后拍脑袋找 bug 强。希望帮到你。本文还有配套的精品资源点击获取