首页 / 资讯中心 / 文章详情

C#+MySQL实现WinForm商品销售管理系统:事务与库存扣减实战

C#+MySQL实现WinForm商品销售管理系统:事务与库存扣减实战 ★ FEATURED ARTICLE
简介这是一份基于C#与MySQL的WinForm商品销售与管理系统课程设计资源适合做课设或毕业设计的初学者。系统覆盖用户、商品、订单三大数据源包含登录验证、权限判断、收银台、商品/用户/订单管理等子系统并支持增删改查、模糊查询、批量修改、库存补充和异常处理。 资源共127个文件压缩包13.93MBcs源文件与resx/resources构成核心工程png为界面截图xmind梳理模块结构SQL脚本初始化数据库docx与PDF提供说明参考附带sln解决方案可在Visual Studio中直接运行。 已有343人学习浏览。下载后可获得完整源码、数据库脚本、功能结构图与界面效果图既能节省搭建时间也能借鉴事件处理、MySQL访问、权限控制和异常捕获等实现思路。1. 商品销售与管理系统C#MySQL的WinForm方案适合谁难点在哪小门店老板把销货记在Excel里月底对账全靠回忆这是很多商品销售与管理系统立项的起点。基于C#MySQL实现的WinForm商品销售与管理系统用桌面窗体管商品资料、采购入库、销售开单、库存流水数据统一落进MySQL。它适合三类人正在找C#练手项目的学生、需要给中小门店交付进销存的一线开发、以及想参考WinForm项目案例的桌面工程师。这个组合开发速度快、部署简单、总体成本低真正的难处不在画界面而在数据库设计和销售扣库存的并发安全。很多人上来就把所有代码堆进Form1.cs按钮事件里直接拼SQL结果项目没跑三个月改个字段要翻遍十几个窗体。这篇文章会从工程骨架、MySQL表结构、销售事务、避坑清单一直聊到打包交付按一条能直接复现的路线走。过程中我会把连接串参数、事务边界、行锁扣库存这些容易翻车的点单独拎出来讲。2. 搭建WinForm骨架工程分层、登录跳转与MySQL连接2.1 为什么不能把业务逻辑都堆在Form1.cs里我接过不少WinForm项目十个里面有七个把查询、赋值、写库全写在按钮Click事件里。项目刚起步时这样写很快但一旦要增加采购、报表、权限代码就变成一锅粥。所以即使是这个规模的项目我也习惯先按UI、BLL、DAL、Model四层切目录。Model放商品、订单、库存的实体DAL只负责SQL和MySQL读写BLL处理库存扣减、金额计算这类业务规则UI层只做输入输出和窗体跳转。WinForm不像Web有完整的IOC容器我不会硬上MVVM但会写一个静态的AppService类保存当前登录用户和数据库连接工厂。分层后的调用关系其实很简单UI调用BLLBLL调用DALDAL操作MySQL。这样做最大的好处是将来把MySQL换成SQL Server只需要改DAL层想在BLL层加一个“不足库存不允许开单”的规则也不用去DataGridView的单元格事件里找代码。VS2015和VS2019都能直接建这种普通WinForm工程不需要额外装插件。层典型文件职责UILoginForm.cs, MainForm.cs, GoodsForm.cs收集输入、展示结果不写SQLBLLGoodsService.cs, OrderService.cs库存判断、金额计算、事务编排DALGoodsRepository.cs, OrderRepository.cs参数化SQL、统一连接管理ModelGoods.cs, SalesOrder.cs, CartItem.cs属性对应表字段只存数据CommonDbHelper.cs, AppService.cs连接读取、当前用户等公共状态2.2 用NuGet装好MySQL驱动并把连接字符串放进App.config连接MySQL最简单的方式是官方Connector。在项目上右键管理NuGet程序包搜索MySql.Data安装。喜欢写轻量SQL的可以再装Dapper但不是必须。这里有一个很常见的翻车点Connector版本和MySQL服务器版本差太多MySQL 8.0默认的caching_sha2_password认证插件会让旧版Connector直接报错。一般做法是装MySql.Data 8.0.x然后在连接字符串里把两个参数写清楚。?xml version1.0 encodingutf-8? configuration connectionStrings add nameShopDb connectionStringServer127.0.0.1;Port3306;Databaseshop;Uidroot;Pwd123456;Charsetutf8mb4;SslModenone;AllowPublicKeyRetrievaltrue; providerNameMySql.Data.MySqlClient / /connectionStrings /configurationSslModenone是为了避开MySQL默认的SSL验证AllowPublicKeyRetrievaltrue解决MySQL 8.0第一次连接时“Public Key Retrieval is not allowed”的提示Charsetutf8mb4保证中文不会乱码。如果MySQL装在另一台机器Server要改成内网IP并且MySQL账号必须允许远程主机访问。连接字符串不要直接写死在代码里原因后面避坑章节会详细说。读取连接字符串的代码我放在Common/DbHelper.cs里顺便写一个创建并打开连接的方法。using MySql.Data.MySqlClient; using System.Configuration; namespace ShopManage.Common { public static class DbHelper { public static string ConnStr ConfigurationManager.ConnectionStrings[ShopDb].ConnectionString; public static MySqlConnection OpenConnection() { var conn new MySqlConnection(ConnStr); conn.Open(); return conn; } } }ConnectionString属性用只读表达式调用方每次拿到的都是新连接。WinForm里不要缓存同一个MySqlConnection长期用连接池本身会复用底层连接你只需要保证每个操作短生命周期即可。注意OpenConnection里没做try/catch为的是让连接错误尽早暴露在调用方而不是被吞掉后留下一堆诡异空数据。2.3 登录窗体到主窗体ShowDialog比Show更可控登录是程序的门。很多新手在Program.cs里直接new MainForm().Show()结果登录还没完成主窗体就出来了。正确做法是用ShowDialog弹登录框等登录成功后再启动主窗体。using System; using System.Windows.Forms; using ShopManage.UI.Forms; namespace ShopManage.UI { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var login new LoginForm(); if (login.ShowDialog() ! DialogResult.OK) return; Application.Run(new MainForm()); } } }用ShowDialog弹登录框点取消或关闭时DialogResult不是OK就直接return进程退出主窗体没有机会在未登录状态下打开。LoginForm里点击登录按钮执行的是BLL层的ValidateUser方法内部用参数化SQL查用户表并比对密码哈希。密码不要用明文存储至少做一次SHA256加盐。private void btnLogin_Click(object sender, EventArgs e) { var user bll.Login(txtUserName.Text.Trim(), txtPassword.Text); if (user null) { MessageBox.Show(用户名或密码错误, 登录失败); return; } AppService.CurrentUser user; this.DialogResult DialogResult.OK; this.Close(); }bll是BLL层的登录服务Login方法内部会用MySqlCommand参数化查询把输入的密码哈希后和数据库比对。AppService.CurrentUser是静态属性方便主窗体显示当前操作员名称也方便后面做菜单权限控制。主窗体在构造函数里读取CurrentUser.Role如果是收银员角色就把进货管理、库存调整这些菜单禁用掉。这一层控制不算复杂但足够拦住大多数误操作。3. 数据库设计与数据访问层建表、事务与Dapper的使用边界3.1 商品、销售单、明细、库存流水四张表的建表SQL设计上我把当前库存字段放在商品表里但每次变动都写一张库存流水表。这样查询“现在还剩多少”很快追溯“这批货什么时候进的、哪张单出的”也有据可查。只建商品表和订单表的项目后面要查历史库存时一定会后悔。CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4; USE shop; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 2 COMMENT 1管理员 2收银员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_goods ( id INT AUTO_INCREMENT PRIMARY KEY, goods_code VARCHAR(30) NOT NULL UNIQUE COMMENT 条码或商品编码, goods_name VARCHAR(100) NOT NULL, spec VARCHAR(50) DEFAULT COMMENT 规格, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 售价, cost_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 成本价, stock_qty INT NOT NULL DEFAULT 0 COMMENT 当前库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_sales_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, sale_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(200) DEFAULT ) ENGINEInnoDB; CREATE TABLE t_sales_order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(100) NOT NULL COMMENT 冗余商品名防止商品改名后历史订单混乱, price DECIMAL(10,2) NOT NULL, qty INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, KEY idx_order_id (order_id) ) ENGINEInnoDB; CREATE TABLE t_stock_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, change_qty INT NOT NULL COMMENT 正数入库负数出库, order_no VARCHAR(32) DEFAULT COMMENT 关联单号, op_user_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_goods (goods_id) ) ENGINEInnoDB;订单明细表里冗余goods_name目的是防止商品改名或删除后历史订单里的商品名变成一串无意义的ID。这是个常见设计取舍宁可多存一个字段也不要以后联表查不到。字段类型用DECIMAL而不用float/double金额精度是这类系统的生命线。InnoDB必须指定因为后面要用事务和行锁。3.2 C#数据访问层的基本写法参数化SQL不能省DAL类里我不会用字符串拼接SQL那等于把后门打开。MySqlCommand的参数化写法并不难但很多人图快用$直接拼结果一次单引号就让系统崩掉更别提SQL注入风险。public class GoodsRepository { public ListGoods Search(string keyword) { var list new ListGoods(); using (var conn DbHelper.OpenConnection()) using (var cmd new MySqlCommand()) { cmd.Connection conn; cmd.CommandText SELECT id, goods_code, goods_name, price, stock_qty FROM t_goods WHERE status1 AND (goods_code LIKE kw OR goods_name LIKE kw) ORDER BY goods_code LIMIT 100; cmd.Parameters.AddWithValue(kw, % keyword %); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Goods { Id reader.GetInt32(id), GoodsCode reader.GetString(goods_code), GoodsName reader.GetString(goods_name), Price reader.GetDecimal(price), StockQty reader.GetInt32(stock_qty) }); } } } return list; } }using确保连接和命令都及时释放不会把MySQL连接占满。参数化查询把keyword当成数据而不是SQL片段这是写数据访问层的基本底线。LIMIT 100是防止用户输入一个空格就查全表把数据库和WinForm界面一起拖死。3.3 使用Dapper简化读取但写事务时回到原生连接如果不想写DataReader可以用Dapper。我在简单查询时用Dapper比如using Dapper; public Goods GetById(int id) { using (var conn DbHelper.OpenConnection()) { return conn.QueryFirstOrDefaultGoods( SELECT id, goods_code, goods_name, price, stock_qty FROM t_goods WHERE idid, new { id id }); } }Dapper会把查询结果映射到Goods实体代码量少一半。但要注意Dapper不是完整ORM不带状态跟踪和缓存碰上事务和行锁我仍然用MySqlConnection原生命令因为控制粒度更直接。很多人以为ORM万能实际上一旦要手动加FOR UPDATE、控制事务隔离级别Dapper用起来反而碍手碍脚。3.4 销售开单的事务模板同一个连接里完成三步写库这里是最容易翻车的地方。很多新手把订单头Insert、订单明细Insert、库存Update分开执行任何一步失败数据库都会留下半截数据。正确做法是开一个事务把所有写库操作放在同一个连接和同一个事务里最后Commit异常时Rollback。using MySql.Data.MySqlClient; public bool CreateOrder(ListCartItem items, string remark) { using (var conn DbHelper.OpenConnection()) { var tx conn.BeginTransaction(); try { var total items.Sum(x x.Price * x.Qty); var orderNo DateTime.Now.ToString(yyyyMMddHHmmssfff); long orderId; using (var cmd new MySqlCommand()) { cmd.Connection conn; cmd.Transaction tx; cmd.CommandText INSERT INTO t_sales_order(order_no, user_id, total_amount, remark) VALUES(orderNo, user, total, rem); SELECT LAST_INSERT_ID();; cmd.Parameters.AddWithValue(orderNo, orderNo); cmd.Parameters.AddWithValue(user, AppService.CurrentUser.Id); cmd.Parameters.AddWithValue(total, total); cmd.Parameters.AddWithValue(rem, remark); orderId Convert.ToInt64(cmd.ExecuteScalar()); } foreach (var item in items) { using (var cmd new MySqlCommand()) { cmd.Connection conn; cmd.Transaction tx; cmd.CommandText INSERT INTO t_sales_order_item(order_id, goods_id, goods_name, price, qty, subtotal) VALUES(oid, gid, name, price, qty, sub);; cmd.Parameters.AddWithValue(oid, orderId); cmd.Parameters.AddWithValue(gid, item.GoodsId); cmd.Parameters.AddWithValue(name, item.GoodsName); cmd.Parameters.AddWithValue(price, item.Price); cmd.Parameters.AddWithValue(qty, item.Qty); cmd.Parameters.AddWithValue(sub, item.Price * item.Qty); cmd.ExecuteNonQuery(); } using (var cmd new MySqlCommand()) { cmd.Connection conn; cmd.Transaction tx; cmd.CommandText UPDATE t_goods SET stock_qty stock_qty - qty WHERE id gid AND stock_qty qty;; cmd.Parameters.AddWithValue(qty, item.Qty); cmd.Parameters.AddWithValue(gid, item.GoodsId); int affected cmd.ExecuteNonQuery(); if (affected 0) { throw new Exception($商品 {item.GoodsName} 库存不足); } } using (var cmd new MySqlCommand()) { cmd.Connection conn; cmd.Transaction tx; cmd.CommandText INSERT INTO t_stock_log(goods_id, change_qty, order_no, op_user_id) VALUES(gid, -qty, orderNo, user);; cmd.Parameters.AddWithValue(gid, item.GoodsId); cmd.Parameters.AddWithValue(qty, item.Qty); cmd.Parameters.AddWithValue(orderNo, orderNo); cmd.Parameters.AddWithValue(user, AppService.CurrentUser.Id); cmd.ExecuteNonQuery(); } } tx.Commit(); return true; } catch { tx.Rollback(); throw; } } }每个命令都带上cmd.Transaction确保它们属于同一个事务。扣减库存这条UPDATE是关键它不是先查库存再判断而是直接在UPDATE语句里加上stock_qty qty条件。如果库存不够受影响行数为0立刻抛异常整个事务回滚订单头和明细都不会残留。这种写法同时解决了并发超卖问题后面第4章会细讲。4. 核心业务落地商品管理、销售开单与库存扣减的细节4.1 商品管理窗体DataGridView绑定与软删除商品管理窗体的功能就是增删改查。加载时用上一章的Search方法把数据绑定到DataGridView注意要把Id那一列设为隐藏否则客户会看到一堆数字。private void LoadGoods() { var list goodsService.Search(txtKeyword.Text.Trim()); dataGridView1.DataSource list; dataGridView1.Columns[Id].Visible false; dataGridView1.Columns[GoodsCode].HeaderText 商品编码; dataGridView1.Columns[GoodsName].HeaderText 商品名称; dataGridView1.Columns[Price].HeaderText 售价; dataGridView1.Columns[StockQty].HeaderText 库存; }删除按钮事件里我一般不会直接DELETE而是调用BLL层的Delete方法做软删除把status字段改成0。这样历史订单里的商品关联还能查到只是不再出现在销售列表中。private void btnDelete_Click(object sender, EventArgs e) { if (dataGridView1.SelectedRows.Count 0) { MessageBox.Show(请先选择要删除的商品); return; } int id Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[Id].Value); if (MessageBox.Show(确认将该商品下架, 删除确认, MessageBoxButtons.YesNo) DialogResult.Yes) { goodsService.SoftDelete(id); LoadGoods(); } }给用户一个确认框减少误操作。真实场景里手一抖点错删除按钮的事太多了软删除就是后悔药。4.2 销售开单多行明细录入与金额计算销售开单界面一般是一个TextBox用来扫描条码一个DataGridView用来展示购物车下面一个Label显示合计。扫描条码或手工输入编码后回车首先要根据商品编码查出商品再判断购物车里有没有这行有就把数量加1没有则新增一行。private void txtBarcode_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode ! Keys.Enter) return; var code txtBarcode.Text.Trim(); var goods goodsService.GetByCode(code); if (goods null) { MessageBox.Show(没有找到这个商品); return; } if (goods.StockQty 0) { MessageBox.Show(商品库存不足不能销售); return; } var index FindGoodsRow(goods.Id); if (index 0) { dataGridView1.Rows[index].Cells[Qty].Value Convert.ToInt32(dataGridView1.Rows[index].Cells[Qty].Value) 1; } else { dataGridView1.Rows.Add(goods.Id, goods.GoodsCode, goods.GoodsName, goods.Price, goods.StockQty, 1, goods.Price); } txtBarcode.Clear(); txtBarcode.Focus(); CalcTotal(); }FindGoodsRow就是遍历DataGridView的行判断第一列的Id是否等于当前商品Id。数量列我用的是DataGridViewTextBoxColumn用户可以直接改数量。改完后触发CellEndEdit事件重新计算这行小计和整单金额。4.3 提交开单把UI明细交给BLL层用异步避免界面卡死提交按钮点击后不能直接在UI线程里跑事务否则数据量大时窗体就变成“未响应”。我用async/await加Task.Run把数据库操作放到线程池。private async void btnPay_Click(object sender, EventArgs e) { var items GetCartItems(); if (items.Count 0) { MessageBox.Show(购物车是空的); return; } btnPay.Enabled false; try { var orderService new OrderService(); var ok await Task.Run(() orderService.CreateOrder(items, txtRemark.Text)); if (ok) { MessageBox.Show(下单成功); ResetCart(); } } catch (Exception ex) { MessageBox.Show($下单失败{ex.Message}); } finally { btnPay.Enabled true; } }async void事件处理器在WinForm里很常见但里面的异常必须自己catch否则会直接崩掉进程。Task.Run把CreateOrder放到后台线程执行这样界面还能响应鼠标点击。这里有一个容易被忽略的点GetCartItems和CreateOrder不要都放到后台任务里因为GetCartItems要访问DataGridView控件只能在UI线程上操作。跨线程访问UILabel或DataGridView会抛System.InvalidOperationException这是很多初学WinForm的人被吓到的地方。4.4 金额计算一定用decimal别用double商品价格、折扣、小计、总计全部用decimal类型。double在0.1 0.2这种计算上会得到0.30000000000000004界面显示不难看但对账就差一分钱。C#里decimal的精度是28-29位有效数字足够覆盖绝大多数商品销售场景。private decimal GetSubtotal(decimal price, int qty) { return Math.Round(price * qty, 2, MidpointRounding.AwayFromZero); }Math.Round的第二个参数是保留小数位数第三个参数AwayFromZero表示四舍五入时远离零。银行家算法在这里反而不是门店想要的门店要的是“四舍五入到分”。5. 上线前避坑MySQL连接、界面卡顿与打包安装的5个典型问题5.1 MySQL 8.0连接报错认证插件和SSL的双重坑现象程序在开发机跑得好好的部署到客户机器上一点查询就报“Authentication plugin caching_sha2_password cannot be loaded”或者“SSL Connection Error”。原因MySQL 8.0默认使用caching_sha2_password认证插件而项目引用的MySql.Data是6.x或更早版本不认识这个插件另外MySQL 8.0默认开启SSLWinForm客户端如果没有正确配置连接会被安全协议卡住。解决把NuGet里的MySql.Data升到8.0.x连接字符串里加SslModenone;AllowPublicKeyRetrievaltrue;这两个参数缺一不可。SslModenone告诉客户端不要强求SSL握手AllowPublicKeyRetrievaltrue允许客户端从服务器取RSA公钥做密码传输。注意这个组合只适合内网生产环境公网部署时应该反过来开启SSL验证。5.2 中文乱码数据库、表、连接串三处统一utf8mb4现象插入商品名称“耐克”变成“耐??”或者从MySQL查到C#里变成字符串带问号。原因数据库建表时用了latin1连接串也没指定字符集C#端默认UTF-16两边一协商就乱码。解决建库语句加DEFAULT CHARACTER SET utf8mb4每个表也显式指定ENGINEInnoDB DEFAULT CHARSETutf8mb4连接字符串加Charsetutf8mb4。这三处缺一处都可能出问题。我见过有人在代码里执行SET NAMES gbk绕过结果换个环境又乱。统一utf8mb4是现在的标准做法MySQL 5.7.44和8.0都支持。5.3 库存扣成负数没有做扣减前检查现象两个收银员同时卖同一件商品一个显示库存只剩1件却开出两张单最后库存变成-1。原因代码写成“先SELECT stock_qty判断大于0再UPDATE”两个线程可能同时通过判断然后都执行UPDATE后写的把负数覆盖上去。解决用UPDATE t_goods SET stock_qty stock_qty - qty WHERE id gid AND stock_qty qty;执行后检查受影响行数为0就抛异常回滚。这条语句里的条件判断和更新是原子操作InnoDB会对命中的行加锁后到的请求会等待等锁释放后重新判断条件发现库存不足就不更新。这个写法的前提是事务隔离级别不能是READ UNCOMMITTED默认的REPEATABLE READ就够用。5.4 WinForm界面卡顿主线程做了耗时数据库查询现象点击“生成报表”后整个窗体变成白屏鼠标转圈几分钟后才有响应或者启动登录时卡顿几秒。原因数据库查询、Excel导出、大集合排序都在UI线程同步执行。MySQL查询本身可能只要100毫秒但界面重绘被阻塞后用户体感会被放大到几秒。解决耗时操作全部用async/await数据库查询放到Task.Run里定时刷新用System.Windows.Forms.Timer。更新控件时如果从后台线程操作用Control.Invoke或BeginInvoke。我一般把耗时操作封装成Task 方法UI层await回调里再赋值。这里还要注意一个坑async void事件里忘记catch异常任务异常会直接抛到主线程导致崩溃所以事件方法必须有try/catch/finally。5.5 打包安装后连不上数据库连接字符串写死在代码里现象在开发机上用Visual Studio跑得好好的用打包工具装到别的电脑启动程序就报“无法连接MySQL”。原因开发时为了让测试方便把Server127.0.0.1、Uidroot、Pwd123456直接写在代码里。客户电脑上的MySQL不在这台机器或者账号密码完全不同程序当然连不上。解决连接字符串放在App.config部署时根据客户环境改配置文件不要改代码重新编译。打包安装程序时把App.config一并带上并在部署清单里写明MySQL的IP、端口、数据库名。更好的做法是做一个“数据库连接测试”窗口安装后第一次启动时让用户填服务器和账号测试通过后保存到本地配置文件。这个窗口不复杂但能省掉大量现场调试时间。6. 从能用到好用报表导出、权限控制与一键打包商品销售系统做完基础功能后用户最常追加的需求是三个把销售明细导出成Excel、区分收银员和管理员权限、把程序打包成能双击安装的exe。这三个点都不难但做得不好会直接影响交付体验。导出Excel我一般不用Office COM组件因为客户电脑不一定装了Office。用EPPlus这个NuGet包生成xlsx很稳。using OfficeOpenXml; public void ExportToExcel(DataTable dt, string filePath) { using (var pkg new ExcelPackage()) { var sheet pkg.Workbook.Worksheets.Add(销售明细); sheet.Cells[A1].LoadFromDataTable(dt, true); pkg.SaveAs(new FileInfo(filePath)); } }LoadFromDataTable会把DataTable的表头和数据一次性写入真正的Excel文件生成逻辑只有五行。权限控制则可以在MainForm的Load事件里遍历菜单项根据CurrentUser.Role设置Visible。一键打包我推荐用Inno Setup脚本里要包含三件事发布文件夹里的exe和dll、App.config、一份部署说明。MySQL不打包进安装程序因为服务器端软件不适合塞进桌面安装包。部署说明里第一行就写清楚先装MySQL再导入shop.sql最后跑安装程序。我这个习惯是从一次交付踩出来的客户装了程序发现数据库没建整栋楼等着后来每次都在安装包旁边放一个README.txt。WinForm这条技术路线虽然看起来老但在内网桌面收银、进销存这类场景里部署快捷、调试直观、成本够低。我这些年做下来最大的教训是不要小看事务边界和连接串配置越是看起来不起眼的地方越容易在上线当天出问题。希望这些方法和踩坑记录能帮你在自己的商品销售与管理系统里少走几步弯路做到真正能交付、能验收、能长期维护。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站