简介:基于C#的仓库管理系统是一套面向中小型企业的仓库管理实践项目,配套完整的MySQL数据库文件,适合正在学习C#面向对象开发或数据库设计的开发者参考。系统覆盖物品入库、出库、查询、统计等核心业务,数据库部分除基础信息表外,还提供视图与存储过程,支持按名称、类别、入库时间、出库时间、库存量、供应商、客户等八种查询场景。整个资源包共163个文件,压缩后仅1.22MB,其中以C#源码(.cs)为主,同时包含SQL数据库脚本、可执行程序(.exe)、程序界面截图、说明文本与配置文件等,便于直接运行和对照学习。目前已有29人学习该资源。搭配详细的安装配置文档与八种查询案例说明,能帮助读者快速完成环境配置,理解系统架构与查询逻辑;源码模块划分清晰,对掌握C#访问MySQL、设计仓储业务逻辑具有不错的参考价值。
1. 一套带 MySQL 数据库文件的 C# 仓库管理系统:它解决的远不止“录单据”
我在一个小工厂的备件库里见过最典型的翻车现场:入库用 Excel,出库也用 Excel,月底三个人拿着三个版本的表对账,对到下班也没对上。后来换了一套基于 C# 的仓库管理系统,入库、出库、查库存、翻供应商记录全在一个界面里完成,MySQL 里再也找不到“两本账”。这份资源就是这套系统的源码包,自带 MySQL 数据库文件,除了物品表、库存表、供应商表、客户表,还写了支持“查询物品的八种情况”的视图和存储过程。适合刚学完 C# 和 SQL、想找一个完整实战项目练手的开发者,也适合小企业内部做工具选型时拿来做参考。它算不上商业级 WMS,但把 OOP 设计、参数化查询、事务处理这些必修课都串了起来,值得拆一遍。
2. 数据库是地基:四张核心表、一个视图和一条存储过程的搭建思路
2.1 选型逻辑:为什么这套系统用 WinForms + MySQL 而不是 Web
先交代一下选型。这套项目用的是 C# WinForms 加 MySQL,没有走 Web 方向,原因很实际:仓库管理系统的使用场景是局域网内的几台客户端,操作员开电脑、双击 exe、登录就能干活,不需要部署 IIS、Nginx,也不用写前端页面。WinForms 在 .NET Framework 下拖控件很快,DataGridView 绑定 DataTable 就能把查询结果铺出来,一个上午能把主界面搭完。对中小企业来说,这是性价比很高的方案。
数据库选 MySQL 而不是 SQL Server,是从版权和部署门槛考虑的。MySQL 免费、安装包小,Windows 上装完就是一个服务,资料也多。配 5.7 还是 8.0 都行,关键点是连接驱动得用对——后面第 4 章会专门讲认证插件那个坑。用 C# 访问 MySQL 一般装 NuGet 上的 MySql.Data 包,类名是 MySqlConnection、MySqlCommand,写法和 SqlClient 全家桶几乎一样,学过 ADO.NET 的人上手没什么成本。很多人用 C# 写上位机或者内部管理系统,技能是相通的,WinForms 这套东西到今天依然有大量存量项目。
再说说这套代码为什么值得拆。项目里能看到 DefaultFrm.Designer.cs,说明是标准的 WinForms 单窗体加子窗体结构;stock1.csproj 里的各类缓存文件说明它在 Visual Studio 里实际编译运行过。资源既然带了 MySQL 数据库文件,说明数据字典、视图、存储过程已经替你写好了,省去了从零建模的工作量。你拿到后要做的是读懂结构,然后在上面加自己的业务,而不是重新发明一遍轮子。
2.2 建表:物品、库存、供应商、客户,外加一张流水表
我接手这类项目的第一件事,是先把 SQL 脚本整份读一遍,确认表设计是否正经。下面这套基础表结构对应资源里的 MySQL 数据库文件,字段名以你拿到手的脚本为准:
CREATE DATABASE warehouse_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE warehouse_db; CREATE TABLE supplier ( id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(64) NOT NULL, contact VARCHAR(32), phone VARCHAR(32) ) ENGINE=InnoDB; CREATE TABLE customer ( id INT AUTO_INCREMENT PRIMARY KEY, customer_name VARCHAR(64) NOT NULL, contact VARCHAR(32), phone VARCHAR(32) ) ENGINE=InnoDB; CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, goods_code VARCHAR(32) NOT NULL UNIQUE, goods_name VARCHAR(64) NOT NULL, category VARCHAR(32), spec VARCHAR(64), unit VARCHAR(16), supplier_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_goods_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINE=InnoDB; CREATE TABLE stock ( goods_id INT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0, update_time DATETIME, CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ) ENGINE=InnoDB; CREATE TABLE stock_record ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, record_type TINYINT COMMENT '1 入库, 2 出库', quantity INT NOT NULL, operator VARCHAR(32), record_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_record_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ) ENGINE=InnoDB;几个设计点解释一下。字符集用 utf8mb4 而不是 utf8,是因为 MySQL 的 utf8 最多存 3 字节,碰到生僻字和 emoji 会直接报错,仓库单据里客户备注偶尔会有这类字符。引擎必须用 InnoDB,它的行级锁和事务支持是 MyISAM 给不了的,后面入库出库要在事务里更新库存,不用 InnoDB 这个系统就立不住。
库存表单独建一张,而不是在 goods 表里放一个 quantity 字段,这有讲究。库存是热点数据,每次入库出库都要 update,如果和物品基础信息挤在一行,并发更新时锁的粒度更大;拆开后,库存表只剩 goods_id 和 quantity,索引极小,更新速度快。stock_record 是流水表,每次出入库都记一条,月底对账、按时间查询全靠它。外键要建,虽然有人嫌它有性能开销,但这种规模的项目里,外键能挡住大量脏数据,保留更划算。
2.3 视图与存储过程:把八种查询情况提前做成数据库能力
资源描述里反复提到“查询物品的八种情况”,还附了说明文件。这个“八种”不是八条写死的 SQL,而是把查询条件组合成八类场景:按名称查、按类别查、按入库时间查、按出库时间查、按库存量查、按供应商查、按客户查,以及它们的组合。把这些能力下沉到数据库里,C# 端只负责传参数,是这套系统比较聪明的地方。
先看视图。视图把物品、库存、供应商三张表预先关联好,C# 查询时不用每次重写 join:
CREATE VIEW v_stock_detail AS SELECT g.id AS goods_id, g.goods_code, g.goods_name, g.category, g.spec, g.unit, s.quantity AS stock_quantity, s.update_time, sup.supplier_name FROM goods g LEFT JOIN stock s ON g.id = s.goods_id LEFT JOIN supplier sup ON g.supplier_id = sup.id;视图用 LEFT JOIN 而不是 INNER JOIN,是因为还没建库存的物品(quantity 为 NULL)也要能出现在查询结果里。按入库时间、出库时间、按客户查这三种条件来自 stock_record 的维度,视图里不预关联,查询时用子查询或再次 join 流水表即可。“查询物品的八种情况.txt”对每一条该用子查询还是 join 都有说明,拿到后建议逐个跑一遍。
再看存储过程。它解决的是多条件组合时 SQL 写死的问题:
DELIMITER // CREATE PROCEDURE sp_query_goods( IN p_name VARCHAR(64), IN p_category VARCHAR(32), IN p_supplier INT, IN p_min_qty INT ) BEGIN SELECT v.goods_code, v.goods_name, v.category, v.supplier_name, v.stock_quantity FROM v_stock_detail v WHERE (p_name IS NULL OR v.goods_name LIKE CONCAT('%', p_name, '%')) AND (p_category IS NULL OR v.category = p_category) AND (p_supplier IS NULL OR v.supplier_id = p_supplier) AND (p_min_qty IS NULL OR v.stock_quantity >= p_min_qty); END// DELIMITER ;这里的写法是“参数为 NULL 就当这个条件不存在”,调用时可以任意组合,传 NULL 跳过某一维。注意 LIKE 的拼法:MySQL 里不能直接写 LIKE '%p_name%',那样会把参数名当字面量,必须用 CONCAT('%', p_name, '%')。调用端执行 CALL sp_query_goods('轴承', NULL, NULL, NULL) 就能查出所有名称含“轴承”的物品。MySQL 5.7 和 8.0 对这个写法都兼容,唯一要注意的是命令行导入时 DELIMITER 的处理,分号别被提前截断。
3. C# 侧实现:连接串、事务入库和八种查询拼装的具体写法
3.1 App.config 里的连接串:Charset 和 SslMode 一个都不能省
C# 这边第一步是让程序连上 MySQL。连接串写在项目根目录的 App.config 里,编译后自动复制成 stock1.exe.config。这是这套系统里最值得先读的文本文件,因为接手别人的机器,八成是“能编译但连不上库”:
<?xml version="1.0" encoding="utf-8"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> </startup> <connectionStrings> <add name="WarehouseDb" connectionString="Server=127.0.0.1;Port=3306;Database=warehouse_db;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;" providerName="MySql.Data.MySqlClient" /> </connectionStrings> </configuration>连接串里各参数的含义如下,照着改就行:
| 参数 | 作用 | 踩坑注意 |
|---|---|---|
| Server | MySQL 主机地址 | 本机用 127.0.0.1,跨机器改局域网 IP |
| Port | MySQL 端口 | 默认 3306,装过多个实例会变 |
| Database | 库名 | 要和脚本里的 warehouse_db 一致 |
| Charset | 客户端字符集 | 写 utf8mb4,否则中文乱码 |
| SslMode | SSL 模式 | MySQL 8 默认开 SSL,本地没证书就写 None |
| Pwd | 密码 | 不要提交到 git,部署时再改 |
连接串不想写死也可以放环境变量或独立配置文件,但默认项目就是 App.config,保持原样最省事。接着封装一个 DbHelper,所有窗体统一走它访问数据库,避免每个窗体都 new MySqlConnection,这也是 OOP 里最简单也最实用的封装:
using System.Configuration; using System.Data; using MySql.Data.MySqlClient; public static class DbHelper { public static string ConnString => ConfigurationManager.ConnectionStrings["WarehouseDb"].ConnectionString; public static DataTable ExecuteQuery(string sql, params MySqlParameter[] paras) { using (var conn = new MySqlConnection(ConnString)) { conn.Open(); using (var cmd = new MySqlCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); using (var da = new MySqlDataAdapter(cmd)) { var dt = new DataTable(); da.Fill(dt); return dt; } } } } public static int ExecuteNonQuery(string sql, params MySqlParameter[] paras) { using (var conn = new MySqlConnection(ConnString)) { conn.Open(); using (var cmd = new MySqlCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); return cmd.ExecuteNonQuery(); } } } }代码里 using 是必须的,MySqlConnection 和 MySqlCommand 都实现了 IDisposable,不释放连接会积压在连接池里,跑几天达到最大连接数后就开始抛“Too many connections”。ExecuteQuery 用 MySqlDataAdapter.Fill 的好处是查询结果直接进 DataTable,WinForms 的 DataGridView.DataSource 绑定就能显示。两个方法都接收 params MySqlParameter[],这是给后面的参数化查询留的口子。
提示:MySql.Data 的参数名必须带 @ 前缀,写法是 @name,不像 SqlClient 那样允许省略,漏了会提示参数未找到。
3.2 入库出库:一条事务里完成库存更新和流水写入
入库的业务逻辑是两件事:库存数量增加 + 写入一条入库流水。这两件事必须在一个事务里完成,否则会出现库存加了但流水没记,或者流水记了但库存没变。下面这段入库代码是这套系统的核心逻辑,我在类似项目里也是这个写法:
public static bool Inbound(int goodsId, int quantity, string operatorName) { if (quantity <= 0) return false; string sqlUpdateStock = "UPDATE stock SET quantity = quantity + @qty, update_time = NOW() " + "WHERE goods_id = @gid"; string sqlInsertRecord = "INSERT INTO stock_record(goods_id, record_type, quantity, operator) " + "VALUES(@gid, 1, @qty, @op)"; using (var conn = new MySqlConnection(DbHelper.ConnString)) { conn.Open(); using (var tx = conn.BeginTransaction()) { try { using (var cmd = new MySqlCommand(sqlUpdateStock, conn, tx)) { cmd.Parameters.AddWithValue("@gid", goodsId); cmd.Parameters.AddWithValue("@qty", quantity); cmd.ExecuteNonQuery(); } using (var cmd = new MySqlCommand(sqlInsertRecord, conn, tx)) { cmd.Parameters.AddWithValue("@gid", goodsId); cmd.Parameters.AddWithValue("@qty", quantity); cmd.Parameters.AddWithValue("@op", operatorName); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); return false; } } } }细节要注意。库存更新写的是 quantity + @qty,不是先 select 再 update,避免并发下读到旧值再覆盖新值,算是简单版的乐观并发控制。第二个 cmd 必须把 tx 传给 MySqlCommand 构造函数,否则它在另一个隐式事务里执行,第一个提交了但它还没提交,数据就不一致。异常时 catch 块里 tx.Rollback() 就够,不用在 finally 里重复处理。AddWithValue 会把 C# 的 int 自动映射成 MySQL 的 INT 参数。
出库逻辑类似,只是更新语句换成 quantity = quantity - @qty,执行前先查一次库存,不够就提示不执行。更严格的做法是把判断写进 UPDATE:WHERE goods_id = @gid AND quantity >= @qty,然后看受影响行数,影响 0 行就说明库存不足,直接回滚。这套系统规模有限,先做到事务一致够用,追求更严谨就用后一种写法。
3.3 八种查询的拼装:WHERE 1=1 + 参数化,而不是拼字符串
查询是这套系统最常被改的部分。八种情况的本质是:用户可能只填一个条件,也可能同时填四个。最忌讳的写法是 if 判断里直接拼 “AND goods_name = '” + name + “'”,先不说 SQL 注入,光单引号和特殊字符就能把查询炸掉。正确做法是动态拼 SQL 骨架,值全部走参数:
public DataTable QueryGoods(string name, string category, DateTime? inStart, DateTime? inEnd, int? minQty, int? supplierId) { var sql = new System.Text.StringBuilder(@" SELECT v.goods_code, v.goods_name, v.category, v.supplier_name, v.stock_quantity FROM v_stock_detail v WHERE 1 = 1"); var paras = new List<MySqlParameter>(); if (!string.IsNullOrEmpty(name)) { sql.Append(" AND v.goods_name LIKE @name"); paras.Add(new MySqlParameter("@name", "%" + name + "%")); } if (!string.IsNullOrEmpty(category)) { sql.Append(" AND v.category = @category"); paras.Add(new MySqlParameter("@category", category)); } if (inStart.HasValue) { sql.Append(@" AND v.goods_id IN (SELECT goods_id FROM stock_record WHERE record_type = 1 AND record_time >= @inStart)"); paras.Add(new MySqlParameter("@inStart", inStart.Value)); } if (inEnd.HasValue) { sql.Append(@" AND v.goods_id IN (SELECT goods_id FROM stock_record WHERE record_type = 1 AND record_time <= @inEnd)"); paras.Add(new MySqlParameter("@inEnd", inEnd.Value)); } if (minQty.HasValue) { sql.Append(" AND v.stock_quantity >= @minQty"); paras.Add(new MySqlParameter("@minQty", minQty.Value)); } if (supplierId.HasValue) { sql.Append(" AND v.supplier_id = @supplierId"); paras.Add(new MySqlParameter("@supplierId", supplierId.Value)); } return DbHelper.ExecuteQuery(sql.ToString(), paras.ToArray()); }WHERE 1 = 1 这个写法第一次看到会觉得多余,它的作用是让后面每条 AND 都不需要判断“这是不是第一个条件”,代码里少一堆分支。参数化的核心在于 MySqlParameter 是独立对象,执行时参数和 SQL 分开发给服务端,用户输入里的单引号、分号都只是数据,不会变成 SQL 的一部分,SQL 注入这条路就断掉了。
按出库时间查、按客户查和上面的 inStart 是同一个套路,只是把子查询里的 record_type 改成 2,或者去 join 客户表。“查询物品的八种情况.txt”的价值就在这里:它把八种条件分别对应的 WHERE 片段列出来了,拿到后可以直接搬进上面的 QueryGoods 扩展。我一般会在这八个条件之外留一个“全部”分支,让 DataGridView 初始就显示全量库存,操作员不用猜。
4. 避坑实录:字符集、认证插件、锁等待与界面卡死的五处现场
这套系统我在不同机器上跑过几轮,下载源码后最容易翻车的不是业务逻辑,而是环境。下面五条每条都是实际踩过或帮别人排过的,按“现象-原因-解决”写清楚,照着能省半天。
4.1 界面显示中文全是问号
现象:导入 SQL 后,WinForms 界面上物品名称显示成一串问号,直接在 MySQL 里 SELECT 中文也乱。
原因:三个层级的字符集不一致。MySQL 服务端默认 character_set_server 是 latin1,建库建表时没指定 utf8mb4,同时连接串里也没写 Charset。你看到的问号,是数据在入库那一刻就已经按错误编码存进去了,后面怎么调界面都没用。
解决:三层一起改。建库时用 DEFAULT CHARACTER SET utf8mb4,第 2 章的脚本里已经写了,别手动删掉;连接串加 Charset=utf8mb4;已经乱码的旧数据救不回来,只能删表重新导入,所以导入前先检查脚本头部有没有 SET NAMES utf8mb4。我现在的习惯是装完 MySQL 直接改 my.ini,把 character-set-server 也设成 utf8mb4,从根上断掉这个问题。
4.2 MySQL 8 连接报 Authentication method 'caching_sha2_password' not supported
现象:用旧版 MySql.Data(6.x 之类)连 MySQL 8,Open() 直接抛异常,提示认证方式不支持;连接串加了 SslMode=None 也没用。
原因:MySQL 8 默认的 root 用户认证插件是 caching_sha2_password,旧版连接驱动不认识。新版驱动默认会带 SSL 握手,本地测试环境没配证书,照样卡在 SSL 校验上。
解决:两个方向任选。要么把 NuGet 里的 MySql.Data 升到 8.0.x,驱动原生支持新认证方式;要么把用户认证方式降回去,执行 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';。我选前者,升级驱动是正向操作,改认证方式是倒退。顺便说一句,新版驱动下连接串最好显式写 SslMode=None,避免每次启动都做无谓的 SSL 校验。
4.3 库存 UPDATE 之后程序卡住,其他客户端全部锁死
现象:入库点了一次没反应,再开另一个客户端查库存也一直转圈,MySQL 命令行执行 SELECT 也是同样的表现。
原因:入库方法里 BeginTransaction 之后,某个语句抛了异常,但 catch 里没调 Rollback,或者干脆没写 try-catch。事务一直挂着,InnoDB 的行锁不释放,后续所有对 stock 表同一条记录的读写都被阻塞。
解决:先排查再修复。执行 SHOW PROCESSLIST 看有没有异常 Sleep 的连接,再查 SELECT * FROM information_schema.innodb_trx 看未提交事务。修复就是给事务代码全部加上 try-catch-rollback,第 3.2 节的写法直接抄。另外提醒:测试阶段连接用完就 Close,连接池里的坏连接一多,各种怪问题都会冒出来。
4.4 按名称模糊查询越来越慢,数据过万后直接没响应
现象:五千条以内查询秒开,数据涨到几万条之后,带 LIKE 的查询要两三秒起步,界面下拉都要等半天。
原因:v_stock_detail 是跨三张表的 LEFT JOIN,视图本身存储不了索引,查询条件里的 goods_name LIKE '%xxx%' 因为前缀不确定,B+ 树索引用不上,只能全表扫描。如果 WHERE 条件直接作用在视图上,MySQL 还得先物化视图再过滤,代价更大。
解决:给 goods_name、category、supplier_id 建普通索引,先保证等值查询走索引;模糊搜索本质上和索引冲突,数据量真上去了得考虑全文索引或单独做搜索表,这套系统的量级用普通索引足够。建索引语句放在 SQL 脚本最后:ALTER TABLE goods ADD INDEX idx_goods_name (goods_name); 每加一种新查询条件之前,先 EXPLAIN SELECT ... 看一眼有没有走索引,别等界面上卡了再回来找。
4.5 WinForms 加载数据白屏、窗口无响应,拖动都卡
现象:点“查询”按钮后整个窗体变白,鼠标变转圈,数据量大的时候窗口拖动都抖,严重时弹“未响应”。
原因:查询方法直接写在按钮 Click 事件里,数据库往返、DataTable 填充全在 UI 线程执行。按钮点下去,UI 线程阻塞在数据库 IO 上,窗口的消息泵没人处理,表现就是卡死。数据量一大问题被放大。
解决:把查询丢到后台线程,典型做法是 BackgroundWorker 或 Task.Run,查询完用 Invoke 回到 UI 线程绑定 DataGridView。简单版如下:
private void btnQuery_Click(object sender, EventArgs e) { btnQuery.Enabled = false; Task.Run(() => { var dt = QueryGoods(txtName.Text.Trim(), null, null, null, null, null); dataGridView1.Invoke((Action)(() => { dataGridView1.DataSource = dt; btnQuery.Enabled = true; })); }); }跨线程操作控件必须走 Invoke 或 BeginInvoke,直接在外层线程给 DataGridView 赋值会抛“线程间操作无效”。这个坑在 WinForms 项目里出现频率极高,不光是查询,导出报表、批量导入都要记住这一条。
5. 从下载到跑起来:数据库导入、连接串修改与闭环验证
拿到压缩包先别急着双击 exe。先盘点文件:项目文件 stock1.csproj、主窗体 DefaultFrm.Designer.cs、App.config,以及数据库脚本。README.md 讲安装配置,“查询物品的八种情况.txt”讲查询逻辑,两个都值得先扫一遍。下面按实际部署顺序来。
5.1 装 MySQL 5.7/8.0,把自带 SQL 脚本导进去
第一步装数据库。Windows 上装 MySQL 5.7 有 msi 安装包,一路下一步;MySQL 8 更建议用 Zip 包解压方式,初始化命令如下。两种方式装完都先确认服务起来了:
mysqld --initialize-insecure mysqld --install MySQL80 net start MySQL80 mysql -uroot初始化用 --initialize-insecure 生成空密码 root 账户,方便本地开发,正式环境再改密码和认证插件。导入数据库脚本用重定向最稳:
mysql -uroot -p < warehouse_db.sql执行完验证表和视图是否齐全:
USE warehouse_db; SHOW TABLES; SELECT COUNT(*) FROM v_stock_detail; SHOW PROCEDURE STATUS WHERE Db = 'warehouse_db';SHOW TABLES 能看到 supplier、customer、goods、stock、stock_record;v_stock_detail 视图和 sp_query_goods 存储过程也要在。如果提示字符集错误,检查脚本文件本身是不是 UTF-8 编码,Windows 记事本“另存为 UTF-8”就能解决。有个常见弯路:有人喜欢在 MySQL Workbench 里打开 .sql 再执行,Workbench 默认按系统编码解析,要把连接菜单里的 Character Set 改成 utf8mb4,否则中文注释全是乱码。
5.2 改 App.config:四行配置决定了你能不能连上库
打开 App.config,把连接串里的 Server、Pwd 改成你自己的。最常见的错误是只改密码忘记改端口,或者本机 MySQL 用了 3307 还在拿 3306 连。改完保存,编译后生成 stock1.exe.config,注意如果项目里同时存在 App.config 和 stock1.exe.config,运行时会读 exe.config,两个要保持同步,否则改了半天没生效。
<add name="WarehouseDb" connectionString="Server=127.0.0.1;Port=3306;Database=warehouse_db;Uid=root;Pwd=你的密码;Charset=utf8mb4;SslMode=None;" providerName="MySql.Data.MySqlClient" />MySQL 8 + 新驱动时,SslMode=None 必须写,否则连接阶段会尝试 SSL 校验失败;MySQL 5.7 下这个参数可省。Uid 不建议用 root 上生产,开发阶段先跑通,后面再换最小权限账号。连接串改完可以先自测:写一个 3 行控制台程序调用 DbHelper.ExecuteQuery("SELECT 1"),能返回 1 说明连通,再回来打开界面,别在界面里查半天才发现是连接问题。
5.3 编译、登录、走一遍入库出库验证闭环
用 Visual Studio 打开 stock1.csproj,NuGet 还原 MySql.Data,右键重新生成解决方案。如果报缺少引用,检查目标框架是不是 .NET Framework 4.x,这套项目不依赖 .NET Core。生成成功后运行,先看登录窗体能不能进入主界面 DefaultFrm。
验证环节按下面的清单走一遍,每项都通过才算部署成功:
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 登录 | 输入测试账号 | 进入主界面 DefaultFrm |
| 新建物品 | 填写名称、类别、供应商 | 保存成功,列表出现 |
| 入库 | 数量 50 | 库存变为 50 |
| 出库 | 数量 10 | 库存变为 40 |
| 流水 | 查询 stock_record | 2 条记录,type 分别为 1 和 2 |
| 查询 | 按名称、按供应商查询 | 结果与视图数据一致 |
这套验证覆盖了连接、增删改查、事务、视图、参数化查询五条链路,跑通一个顶得上界面上随便点十下。如果哪一步挂了,按失败的环节回查:登录挂看连接串,入库挂看事务,查询乱码看字符集,全是第 4 章讲过的坑。
6. 进阶:把八种查询扩展成统计报表,顺手把登录安全补上
系统跑通之后,实际使用中最高频的两个需求是:把查询结果导成 Excel 给财务;以及登录密码别明文存。这两件事都不难,但做没做,决定了这套系统能不能从“能跑”变成“敢用”。
导出 Excel 的思路是复用 QueryGoods 得到的 DataTable,逐行列写入,数据量小直接循环,量大用 NPOI 的 XSSFWorkbook 写 .xlsx。我一般会封装一个通用方法,参数是报表标题和 DataTable,这样月结存、收发存汇总都能复用同一个导出函数。别用旧版的 Microsoft.Office.Interop.Excel,服务器上没有 Office 就直接崩。
登录安全这块,给密码加盐哈希是最低底线。库存里的密码不能再是明文,而是 Hash(密码 + 盐):
public static string HashPassword(string password, string salt) { using (var sha = System.Security.Cryptography.SHA256.Create()) { var bytes = System.Text.Encoding.UTF8.GetBytes(password + salt); return Convert.ToHexString(sha.ComputeHash(bytes)).ToLower(); } }注册时给每个用户生成随机盐存进用户表,登录时取出该用户的盐,重新哈希再比对。参数化查询本来就是防 SQL 注入的底线,把登录 SQL 里的用户名和密码都换成 MySqlParameter,网上流传的万能密码注入就失效了。做完这两件事,这套系统离正式投产又近了一步。
最后说个我自己的习惯。这套 C# 仓库管理系统从数据库脚本开始拆,拆完最大的收获不是学会了 WinForms,而是建立起一套检查清单:拿到任何一套“带数据库文件”的资源,先看建表语句是不是 InnoDB、字符集是不是 utf8mb4、有没有视图和存储过程、连接串长什么样,再决定要不要把它跑起来。这个顺序帮我避开了不少坑。希望你下载后也能照这套流程走一遍,把八种查询和事务入库真正跑通,遇到和我不一样的坑,回来对照第 4 章再查一遍。希望帮到你。
本文还有配套的精品资源,点击获取