基于C#与MySQL的仓库管理系统实战:从建库到入库单落地的完整路径
简介这份资源是一套基于C#与MySQL数据库开发的仓库管理系统完整项目包面向学习C#面向对象编程、数据库设计及企业级应用开发的学生与开发者可用于课程设计、毕业设计或自学实践。压缩包共163个文件约1.22MB以49个cs源代码文件为核心辅以18个resx资源文件、18个png界面素材、3个exe可执行程序、3个config配置文件以及sln解决方案、csproj工程文件和sql数据库脚本结构完整、开箱可研。系统涵盖物品入库、出库、查询、统计等基础功能数据库包含物品、库存、供应商、客户等基础表并通过视图与存储过程支持按名称、类别、时间、库存量、供应商、客户等八种查询场景同时实现了登录验证、权限管理与参数化查询等安全机制。目前已有29人学习下载配套README.md与查询案例说明文档便于读者快速理解项目架构、复用代码并掌握C#与MySQL结合开发的实践思路。1. 基于C#的仓库管理系统从建库到入库单落地的完整路径很多做企业信息化的朋友第一次接到仓库管理系统的需求时脑子里第一反应是“不就是增删改查吗”真动手才发现坑全在细节里库存数量对不上、并发扣减超卖、入库单和出库单状态流转混乱、MySQL连接池被打满。基于C#的仓库管理系统含MySQL数据库文件这个方向本质是用C#做上位机/桌面端或ASP.NET服务端用MySQL存商品、库位、单据、流水把“收货—上架—拣货—出库—盘点”这条链路跑通。它适合中小型制造、电商仓储、门店后台的从业者也适合想拿一个完整项目练C#连接MySQL、事务、数据库设计的开发者。下面我按自己实际做过的顺序把建库、连库、单据落库、并发扣减、避坑和进阶验证一层层拆开讲新手能照着敲熟手能直接看参数边界。2. 先把MySQL库建对仓库管理系统需要哪几张核心表2.1 表结构设计商品、库位、库存、单据、流水五件套仓库管理系统的数据库设计决定了后面代码好不好写。我一般会先落五张核心表商品表product存SKU基础信息库位表location存仓库内的物理位置库存表inventory存“某商品在某库位有多少”单据主表bill和单据明细表bill_item存入库单、出库单库存流水表stock_log记录每一次数量变动。很多新手会把库存直接挂在商品表上结果多库位场景直接翻车——同一个SKU放在A区10件、B区5件一个字段根本表达不了。下面是我常用的建库脚本字符集用utf8mb4引擎InnoDB因为后面要靠事务和行锁。CREATE DATABASE wms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE wms; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(64) NOT NULL UNIQUE COMMENT 商品编码, name VARCHAR(128) NOT NULL, unit VARCHAR(16) DEFAULT 件, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE location ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL UNIQUE COMMENT 库位编码如A-01-01, zone VARCHAR(32) COMMENT 区域 ) ENGINEInnoDB; CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, location_id BIGINT NOT NULL, qty INT NOT NULL DEFAULT 0 COMMENT 库存数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, UNIQUE KEY uk_prod_loc (product_id, location_id) ) ENGINEInnoDB; CREATE TABLE bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL UNIQUE, type TINYINT NOT NULL COMMENT 1入库 2出库, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已审核 2已完成, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE bill_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_id BIGINT NOT NULL, product_id BIGINT NOT NULL, location_id BIGINT NOT NULL, qty INT NOT NULL, KEY idx_bill (bill_id) ) ENGINEInnoDB; CREATE TABLE stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, location_id BIGINT NOT NULL, change_qty INT NOT NULL COMMENT 正数入库负数出库, bill_no VARCHAR(32), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;逻辑说明inventory表用(product_id, location_id)做唯一键保证一个商品在一个库位只有一行避免重复插入。version字段是给乐观锁用的后面并发扣减会用到。stock_log只追加不修改方便对账。参数上qty用INT够中小仓库用如果做大宗物料建议换DECIMAL(18,3)。字符集必须utf8mb4否则商品名带生僻字或emoji会报错。2.2 用C#连MySQL连接字符串与连接池的三个关键参数建完库就要在C#里连。常见做法是用MySql.Data或MySqlConnector这两个NuGet包我一般选MySqlConnector异步支持更干净。连接字符串里最容易出事的是连接池配置默认值在高并发下会把MySQL打满。// 安装dotnet add package MySqlConnector using MySqlConnector; public class DbFactory { // 连接字符串Pooling开启连接池Min/Max控制池大小 private const string ConnStr Server127.0.0.1;Port3306;Databasewms;Uidroot;Pwd你的密码; Poolingtrue;MinimumPoolSize2;MaximumPoolSize20; ConnectionTimeout5;DefaultCommandTimeout10;CharSetutf8mb4;; public static MySqlConnection Open() { var conn new MySqlConnection(ConnStr); conn.Open(); return conn; } }逻辑说明MinimumPoolSize2保证有热连接MaximumPoolSize20是中小系统的经验值超过这个数MySQL的max_connections容易顶不住。ConnectionTimeout5秒避免网络抖动时线程全卡死。DefaultCommandTimeout10秒防止慢查询拖垮界面。CharSetutf8mb4要和建库一致否则中文乱码。注意别在循环里反复new MySqlConnection连接池会复用但用完必须Dispose否则池会被占满表现就是“连接超时”这种玄学问题。3. 入库单落库C#事务写法与库存流水一致性3.1 一张入库单从界面到数据库的完整流程入库单的业务动作是录单→审核→上架→库存增加→写流水。这四步必须在一个事务里否则出现“单据完成了但库存没加”这种对不上账的血泪经验。我一般把流程写成先插bill和bill_item再更新inventory最后插stock_log全部包在同一个事务里。public void ConfirmInbound(Bill bill, ListBillItem items) { using var conn DbFactory.Open(); using var tx conn.BeginTransaction(); try { // 1. 更新单据状态为已完成 using (var cmd new MySqlCommand( UPDATE bill SET status2 WHERE bill_nono AND status1, conn, tx)) { cmd.Parameters.AddWithValue(no, bill.BillNo); if (cmd.ExecuteNonQuery() 0) throw new Exception(单据状态不允许完成或不存在); } foreach (var it in items) { // 2. 库存增加存在则累加不存在则插入 using (var cmd new MySqlCommand( INSERT INTO inventory(product_id, location_id, qty) VALUES(pid, lid, qty) ON DUPLICATE KEY UPDATE qty qty qty, conn, tx)) { cmd.Parameters.AddWithValue(pid, it.ProductId); cmd.Parameters.AddWithValue(lid, it.LocationId); cmd.Parameters.AddWithValue(qty, it.Qty); cmd.ExecuteNonQuery(); } // 3. 写库存流水 using (var cmd new MySqlCommand( INSERT INTO stock_log(product_id, location_id, change_qty, bill_no) VALUES(pid, lid, qty, no), conn, tx)) { cmd.Parameters.AddWithValue(pid, it.ProductId); cmd.Parameters.AddWithValue(lid, it.LocationId); cmd.Parameters.AddWithValue(qty, it.Qty); cmd.Parameters.AddWithValue(no, bill.BillNo); cmd.ExecuteNonQuery(); } } tx.Commit(); } catch { tx.Rollback(); throw; } }逻辑说明UPDATE bill时带status1条件是防止重复审核这是幂等性的关键。ON DUPLICATE KEY UPDATE利用唯一键实现“有则加、无则插”比先查后写少一次往返。整个事务里任何一步抛异常都Rollback保证单据、库存、流水三者一致。参数上qty入库为正数出库时传负数即可复用同一套逻辑。注意MySqlConnector里事务对象要显式传给每个Command漏传就会脱离事务这是新手最常见的翻车点。3.2 出库扣减乐观锁和悲观锁怎么选出库比入库危险因为要判断库存够不够。两种常见做法悲观锁用SELECT ... FOR UPDATE锁行简单但并发高时容易锁等待乐观锁用version字段冲突时重试。中小仓库我一般用乐观锁因为锁等待会让界面卡顿。public bool DeductStock(long productId, long locationId, int qty, string billNo) { using var conn DbFactory.Open(); using var tx conn.BeginTransaction(); try { // 乐观锁带version条件更新影响行数为0说明被别人改过 using (var cmd new MySqlCommand( UPDATE inventory SET qty qty - qty, version version 1 WHERE product_idpid AND location_idlid AND qty qty AND versionver, conn, tx)) { cmd.Parameters.AddWithValue(qty, qty); cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(lid, locationId); cmd.Parameters.AddWithValue(ver, GetVersion(conn, tx, productId, locationId)); if (cmd.ExecuteNonQuery() 0) { tx.Rollback(); return false; // 库存不足或版本冲突由上层重试 } } // 写负数流水 using (var cmd new MySqlCommand( INSERT INTO stock_log(product_id, location_id, change_qty, bill_no) VALUES(pid, lid, qty, no), conn, tx)) { cmd.Parameters.AddWithValue(pid, productId); cmd.Parameters.AddWithValue(lid, locationId); cmd.Parameters.AddWithValue(qty, -qty); cmd.Parameters.AddWithValue(no, billNo); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); throw; } }逻辑说明WHERE里qty qty保证不会扣成负数versionver保证没有并发修改。影响行数为0时返回false上层可以重试2到3次。参数上version要先查出来查和更新之间如果有别人改了更新就会失败这正是乐观锁的机制。如果业务要求强一致且并发不高把SELECT换成FOR UPDATE就是悲观锁但要注意加索引否则会锁表。4. 避坑与排查仓库管理系统上线后最容易翻车的五件事4.1 库存对不上先查流水再查事务边界现象盘点时发现某商品库存比实际多或少。原因多半是事务没包全比如库存更新了但流水没写或者单据状态改了但库存没动。解决先拿stock_log按product_id和location_id做SUM(change_qty)和inventory.qty对比差值就是漏记的量。然后检查代码里所有改库存的地方是否都在事务内尤其注意异步方法里using conn和using tx的作用域别让tx提前Dispose。4.2 连接池被打满MaximumPoolSize不是越大越好现象系统跑一段时间后报“Timeout expired... max pool size reached”。原因MaximumPoolSize设太大比如200MySQL的max_connections默认151直接顶爆或者代码里连接没Dispose池被占满。解决池大小按MySQL的max_connections除以应用实例数来定单实例20到50足够。用using包住每个连接或者用依赖注入的Scoped生命周期。排查时在MySQL执行SHOW PROCESSLIST看连接数。4.3 中文乱码字符集要三处一致现象商品名存进去变成问号或乱码。原因建库、连接字符串、表字段三处字符集不一致。解决建库用utf8mb4连接字符串加CharSetutf8mb4表字段默认继承库字符集。如果已经建错用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4修复。注意MySQL 8默认就是utf8mb4但老版本5.7默认latin1这是重灾区。4.4 单据重复审核幂等性没做现象同一张入库单被点了两次“完成”库存加了两遍。原因UPDATE bill时没带status条件或者前端没禁用按钮。解决后端UPDATE必须带status1影响行数为0就拒绝前端按钮点击后立即置灰。这是最典型的后悔药场景上线前一定要测。4.5 慢查询拖垮界面缺索引现象单据列表加载要好几秒。原因bill_item的bill_id没索引或者stock_log按时间查没索引。解决外键字段一律加索引常用查询条件created_at、bill_no加索引。用EXPLAIN看执行计划出现typeALL就是全表扫描。参数上索引不是越多越好写多的表控制在5个以内。5. 进阶验证用对账SQL和压力测试确认系统真的可靠系统写完不算完得验证。我一般做两件事一是写对账SQL每天跑一次确认inventory和stock_log的汇总一致二是用并发测试模拟多个人同时出库看会不会超卖。对账SQL长这样SELECT i.product_id, i.location_id, i.qty AS inv_qty, IFNULL(SUM(s.change_qty), 0) AS log_qty, i.qty - IFNULL(SUM(s.change_qty), 0) AS diff FROM inventory i LEFT JOIN stock_log s ON i.product_id s.product_id AND i.location_id s.location_id GROUP BY i.product_id, i.location_id, i.qty HAVING diff 0;diff不为0就是有问题顺着stock_log的时间线查就能定位是哪张单据出的错。这个查询在数据量大时要给stock_log的(product_id, location_id)加联合索引否则会全表扫。并发测试我用C#开10个线程同时调DeductStock扣同一个库位的库存初始100件每个线程扣10件跑完看最终库存是不是0流水SUM是不是-100。如果出现负数或对不上说明锁没生效。参数上线程数按实际并发峰值的1.5倍设跑三轮取最差结果。var tasks Enumerable.Range(0, 10).Select(_ Task.Run(() { for (int i 0; i 10; i) { bool ok svc.DeductStock(1, 1, 1, TEST- Guid.NewGuid()); if (!ok) Console.WriteLine(扣减失败库存不足或冲突); } })).ToArray(); Task.WaitAll(tasks);跑完执行对账SQLdiff必须为0inventory.qty必须为0。如果qty是负数说明WHERE里的qty qty没生效检查是不是参数传错或SQL拼错。这套验证做完系统才敢上生产。我自己踩过最深的一个坑是早期没做流水表库存错了根本查不出原因只能全量盘点那滋味真不好受。后来所有改库存的地方强制写流水对账SQL每天定时跑心里才踏实。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →