ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

带MySQL数据库文件的C#仓库管理系统:数据库设计与实战

带MySQL数据库文件的C#仓库管理系统:数据库设计与实战 简介基于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) ) ENGINEInnoDB; CREATE TABLE customer ( id INT AUTO_INCREMENT PRIMARY KEY, customer_name VARCHAR(64) NOT NULL, contact VARCHAR(32), phone VARCHAR(32) ) ENGINEInnoDB; 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB;几个设计点解释一下。字符集用 utf8mb4 而不是 utf8是因为 MySQL 的 utf8 最多存 3 字节碰到生僻字和 emoji 会直接报错仓库单据里客户备注偶尔会有这类字符。引擎必须用 InnoDB它的行级锁和事务支持是 MyISAM 给不了的后面入库出库要在事务里更新库存不用 InnoDB 这个系统就立不住。库存表单独建一张而不是在 goods 表里放一个 quantity 字段这有讲究。库存是热点数据每次入库出库都要 update如果和物品基础信息挤在一行并发更新时锁的粒度更大拆开后库存表只剩 goods_id 和 quantity索引极小更新速度快。stock_record 是流水表每次出入库都记一条月底对账、按时间查询全靠它。外键要建虽然有人嫌它有性能开销但这种规模的项目里外键能挡住大量脏数据保留更划算。2.3 视图与存储过程把八种查询情况提前做成数据库能力资源描述里反复提到“查询物品的八种情况”还附了说明文件。这个“八种”不是八条写死的 SQL而是把查询条件组合成八类场景按名称查、按类别查、按入库时间查、按出库时间查、按库存量查、按供应商查、按客户查以及它们的组合。把这些能力下沉到数据库里C# 端只负责传参数是这套系统比较聪明的地方。先看视图。视图把物品、库存、供应商三张表预先关联好C# 查询时不用每次重写 joinCREATE 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 version1.0 encodingutf-8? configuration startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.7.2 / /startup connectionStrings add nameWarehouseDb connectionStringServer127.0.0.1;Port3306;Databasewarehouse_db;Uidroot;Pwd123456;Charsetutf8mb4;SslModeNone; providerNameMySql.Data.MySqlClient / /connectionStrings /configuration连接串里各参数的含义如下照着改就行参数作用踩坑注意ServerMySQL 主机地址本机用 127.0.0.1跨机器改局域网 IPPortMySQL 端口默认 3306装过多个实例会变Database库名要和脚本里的 warehouse_db 一致Charset客户端字符集写 utf8mb4否则中文乱码SslModeSSL 模式MySQL 8 默认开 SSL本地没证书就写 NonePwd密码不要提交到 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 的好处是查询结果直接进 DataTableWinForms 的 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执行前先查一次库存不够就提示不执行。更严格的做法是把判断写进 UPDATEWHERE goods_id gid AND quantity qty然后看受影响行数影响 0 行就说明库存不足直接回滚。这套系统规模有限先做到事务一致够用追求更严谨就用后一种写法。3.3 八种查询的拼装WHERE 11 参数化而不是拼字符串查询是这套系统最常被改的部分。八种情况的本质是用户可能只填一个条件也可能同时填四个。最忌讳的写法是 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 ListMySqlParameter(); 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 章的脚本里已经写了别手动删掉连接串加 Charsetutf8mb4已经乱码的旧数据救不回来只能删表重新导入所以导入前先检查脚本头部有没有 SET NAMES utf8mb4。我现在的习惯是装完 MySQL 直接改 my.ini把 character-set-server 也设成 utf8mb4从根上断掉这个问题。4.2 MySQL 8 连接报 Authentication method caching_sha2_password not supported现象用旧版 MySql.Data6.x 之类连 MySQL 8Open() 直接抛异常提示认证方式不支持连接串加了 SslModeNone 也没用。原因MySQL 8 默认的 root 用户认证插件是 caching_sha2_password旧版连接驱动不认识。新版驱动默认会带 SSL 握手本地测试环境没配证书照样卡在 SSL 校验上。解决两个方向任选。要么把 NuGet 里的 MySql.Data 升到 8.0.x驱动原生支持新认证方式要么把用户认证方式降回去执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。我选前者升级驱动是正向操作改认证方式是倒退。顺便说一句新版驱动下连接串最好显式写 SslModeNone避免每次启动都做无谓的 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_recordv_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 nameWarehouseDb connectionStringServer127.0.0.1;Port3306;Databasewarehouse_db;Uidroot;Pwd你的密码;Charsetutf8mb4;SslModeNone; providerNameMySql.Data.MySqlClient /MySQL 8 新驱动时SslModeNone 必须写否则连接阶段会尝试 SSL 校验失败MySQL 5.7 下这个参数可省。Uid 不建议用 root 上生产开发阶段先跑通后面再换最小权限账号。连接串改完可以先自测写一个 3 行控制台程序调用 DbHelper.ExecuteQuery(SELECT 1)能返回 1 说明连通再回来打开界面别在界面里查半天才发现是连接问题。5.3 编译、登录、走一遍入库出库验证闭环用 Visual Studio 打开 stock1.csprojNuGet 还原 MySql.Data右键重新生成解决方案。如果报缺少引用检查目标框架是不是 .NET Framework 4.x这套项目不依赖 .NET Core。生成成功后运行先看登录窗体能不能进入主界面 DefaultFrm。验证环节按下面的清单走一遍每项都通过才算部署成功验证项操作预期结果登录输入测试账号进入主界面 DefaultFrm新建物品填写名称、类别、供应商保存成功列表出现入库数量 50库存变为 50出库数量 10库存变为 40流水查询 stock_record2 条记录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 章再查一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表