ARTICLE DETAIL

资讯详情

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

C# WinForm超市收银系统开发:数据库事务与参数化查询实战

C# WinForm超市收银系统开发:数据库事务与参数化查询实战 简介C# WinForm开发的超市收银POS系统完整源码及配套SQL数据库脚本面向需要学习桌面端业务系统开发、或希望快速搭建零售收银解决方案的开发者。系统覆盖商品管理、销售处理、库存跟踪、报表生成、多方式收银结账、用户权限管理、数据备份恢复等核心功能从数据库设计到界面交互形成一套可运行参考方案。压缩包共90个文件以28个C#源文件为主另含SQL建库脚本、xsd数据集定义、resx界面资源、EntityFramework依赖组件及项目配置文件等包体约12.02MB目录结构完整便于直接加载调试。资源已有87人学习下载。通过源码可学习WinForm窗体布局、数据绑定、业务分层处理与SQL交互的实践写法借助初始化脚本可快速还原数据库结构理解商品、订单等表间关系适合课程设计参考、入职练手或基于实际需求做二次功能扩展。1. C# WinForm 超市收银系统2025年还在用图的不是先进而是省事你把标题扫了两遍才确认“收营”是“收银”的错别字。这种错别字在国内中小型项目里太常见了它往往不是笔误而是一个不太懂开发的人在需求文档里随手打的后面照着翻译成代码的人也没纠正。2025年掏出一套 C# WinForm 超市收银系统听起来有点老土但它能活下来的理由恰恰很现实几乎所有 Windows 电脑都能双击运行不需要 IIS、不需要 Linux 服务器、不需要容器数据库一台 SQL Server 就能扛住日流水几千单的小超市。这套系统的价值不在技术栈炫技而在把一类需求完整走通——登录与权限、商品档案与库存、前台扫码结算、销售报表。你拿到源码和 SQL 文件不是跑起来就算完而是能看懂每一步之间怎么衔接、哪些地方容易埋雷。适合两类人一类是想靠完整项目练手的学生另一类是给自家小店或朋友门店搭收款系统、预算有限又不想为 SaaS 年费掏钱的店主。2. 把模块先切开再落库设计超市收银系统的功能边界与订单结构2.1 收银链路拆成四个功能域各管各的别混在一个窗口里动手写代码前我一般会先在纸上把功能域画出来。超市收银系统最常见的错误是把“商品维护”“库存查询”“收银结账”“报表统计”全塞进一个主窗口加十几个 TabPage写着写着代码就变成一坨。常见做法是拆成四个域基础档案管理商品、供应商、会员库存中心管入库、出库、盘点收银台管扫码、结算、小票报表中心管日结、月结和趋势。四个域的数据流是一条直线商品档案先建好库存才有据可加收银台才能扣减报表再汇总。数据流一旦画清楚数据库表结构也就跟着确定下来。基础档案是源头库存是中间状态订单是结果。很多新手把“库存”当成一张静态表每次收银直接改库存字段这样做月底对账迟早对不上因为缺少流水。库存域至少要有一张库存台账和一个流水表实收实发都留痕。商品表只存当前库存量流水表记录每一次变动这是后续做报表和盘点的基础。另外要注意窗口之间的数据传递。WinForm 里最常见的方式是登录成功后把用户 ID 和用户名放在一个静态会话类里而不是到处打开数据库连接重新查。会话类虽然简单但要控制好生命周期退出登录时清空否则切换账号后会串身份。这个小细节在多人共用一台收银机的超市里非常容易出现两个店员交接班不退出后一个人用前一个人的权限登录问题就大了。2.2 订单主表与订单明细表一单一品用 SQL 建出最稳的关系收银系统的核心表不是商品表而是订单主表和订单明细表。一张小票对应一个主表记录票面上的每一行商品对应明细表的一条记录。为什么不能把商品直接拼成一个字符串塞进一个字段里因为后续要做销售统计、退换货、按品类分析拆开存才能用 SQL 高效聚合。主表存订单号、收银员、会员、折扣、应收实收、支付方式、下单时间明细表存订单号、商品 ID、商品名称、数量、单价、小计。注意明细表里的商品名称要冗余一份商品改名或删除后历史订单仍能还原。下面是建表脚本的核心片段我在 SQL Server 2012 到 2022 上都用过同一套写法CREATE TABLE dbo.invoice_header ( invoice_no VARCHAR(24) NOT NULL, -- 订单号业务生成带日期前缀 user_id INT NOT NULL, -- 收银员ID member_id INT NULL, -- 会员ID未登录可为空 discount_amt DECIMAL(10,2) NOT NULL DEFAULT 0, -- 整单折扣金额 total_amt DECIMAL(10,2) NOT NULL, -- 应收总额 pay_amt DECIMAL(10,2) NOT NULL, -- 实收金额 pay_type TINYINT NOT NULL DEFAULT 1,-- 1现金 2微信 3支付宝 create_time DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT pk_invoice_header PRIMARY KEY (invoice_no) ); CREATE TABLE dbo.invoice_line ( id BIGINT IDENTITY(1,1) NOT NULL, -- 行号自增 invoice_no VARCHAR(24) NOT NULL, product_id INT NOT NULL, product_name NVARCHAR(120) NOT NULL, -- 冗余商品名称防止历史订单失真 quantity DECIMAL(10,2) NOT NULL, -- 数量保留两位支持称重商品 price DECIMAL(10,2) NOT NULL, -- 成交单价 sub_total DECIMAL(10,2) NOT NULL, CONSTRAINT pk_invoice_line PRIMARY KEY (id), CONSTRAINT fk_line_header FOREIGN KEY (invoice_no) REFERENCES dbo.invoice_header (invoice_no) );订单号我不用自增主键而是用业务流水号比如20250113120045 收银台编号 三位流水。这样好处是两台收银机同时下单不会撞号报表按时间查也方便。但要用它做主键就必须保证在代码里生成时不重复常见做法是用日期加随机后缀或者用一个独立的序列表每次取出下一个序号。简单的门店系统用日期加收银台编号加三位循环号就够前提是单台收银机下单量不大。数量字段用DECIMAL(10,2)而不是INT因为超市有称重商品0.55 千克的苹果按 0.55 计算四舍五入到整数会在日结对账时差出几毛钱累积一个月就成了大问题。2.3 连接串、字符集与主键策略三个在建库前就要定下来的参数很多人的 SQL 文件导入失败翻车点不在业务表而在最开始几个参数。字符集建议用Chinese_PRC_CI_AS它是简体中文常用的排序规则直接用默认的Latin1_General会出现中文字段排序不按拼音、部分汉字显示异常的问题。数据库所有表的主键和索引放在同一个文件组就行不需要为小型超市做文件组拆分拆了反而增加备份复杂度。连接串是另一个高频踩坑点。WinForm 程序里连接字符串建议写在App.config中而不是硬编码在代码里。这样换电脑部署时只需要改配置不用改代码重新编译。?xml version1.0 encodingutf-8 ? configuration connectionStrings add nameSuperMarketDb connectionStringData Source.;Initial CatalogSuperMarketDb;User IDsa;Password123456;MultipleActiveResultSetstrue; providerNameSystem.Data.SqlClient / /connectionStrings /configuration连接串里MultipleActiveResultSetstrue值得专门说一句。WinForm 里很容易出现一个连接对象执行查询后又在同一个连接上执行另一条命令的情况不开这个参数就会报“已有打开的与此连接相关联的 DataReader”。对单用户小店开它不需要付出什么代价对多人同时在线这个参数也能减少一部分连接超时的报错。另一个参数是Connect Timeout默认 15 秒如果门店网络环境不好建议显式设为 5让用户快速知道连不上而不是卡住十几秒看起来像死机。连接串的账号不要用 sa最低权限账号对保护数据更有用。系统启动时先测一次连通性失败就弹提示并给出“修复数据库连接”的入口而不是在登录界面上转圈。3. 把登录、商品、结算三段逻辑写进 WinForm能直接抄的代码与参数3.1 登录窗口MD5 哈希 参数化查询挡住万能密码和拖库登录是每套收银系统都有的入口但也是最容易被顺手写坏的入口。常见做法是把用户输入的密码直接拼进 SQL 字符串然后执行查询这样遇到 OR 11这类万能密码查询条件恒为真整个系统就被破解了。另一个常见问题是密码明文入库门店内部人员顺手打开表就能看到所有人的密码隐私和安全都谈不上。正确的做法是密码存哈希值登录时把用户输入的密码做同样的哈希再比对。以下代码我一般放在登录按钮的 Click 事件里数据库层用参数化查询private string Md5Hash(string input) { using (var md5 System.Security.Cryptography.MD5.Create()) { byte[] bytes Encoding.UTF8.GetBytes(input); byte[] hash md5.ComputeHash(bytes); StringBuilder sb new StringBuilder(); for (int i 0; i hash.Length; i) { sb.Append(hash[i].ToString(x2)); } return sb.ToString(); } } private bool VerifyLogin(string loginName, string loginPwd) { string connStr ConfigurationManager.ConnectionStrings[SuperMarketDb].ConnectionString; string sql SELECT COUNT(1) FROM dbo.users WHERE user_nameloginName AND user_pwdloginPwd; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(loginName, loginName); cmd.Parameters.AddWithValue(loginPwd, Md5Hash(loginPwd)); conn.Open(); return (int)cmd.ExecuteScalar() 0; } }代码里有两个关键点。一是所有用户输入都走AddWithValue参数化SQL 引擎会把输入当作字面量而不是可执行代码万能密码就没用了。二是密码先做 MD5 再比较数据库里即使被拖走也只拿到一串哈希。需要说明的是 MD5 用在这里并不是密码学上的最优方案但超市这类本地系统它的成本最低。你要更稳妥可以改成 SHA256替换哈希函数时注意把循环里的MD5.Create()换成SHA256.Create()其余逻辑不动。另外AddWithValue在 SQL Server 里对NVARCHAR字段偶尔会引发隐式转换导致索引失效更严谨的做法是写cmd.Parameters.Add(loginName, SqlDbType.NVarChar, 50).Value loginName;登录表数据量不大影响可以忽略。登录成功后把用户 ID 和用户名存到一个静态类里后续所有窗口都能取到。还要记录登录时间方便后面做交接班日志。3.2 商品管理DataGridView 绑定数据表搜索用 LIKE 参数化商品管理的核心界面是 DataGridView 加一个搜索框。很多人在 TextChanged 事件里每次敲一个字母就去数据库查一次数据量小感觉不到商品几千条后开始卡顿。常见做法是加载一次商品表到 DataTable然后作为 DataGridView 的数据源本地做过滤。单品数量在五千以内的超市这种方式足够流畅。加载代码可以复用同一段逻辑以减少重复。private DataTable productTable; private void LoadProducts() { string connStr ConfigurationManager.ConnectionStrings[SuperMarketDb].ConnectionString; string sql SELECT product_id, product_name, spec, unit, stock_qty, sale_price, warn_qty FROM dbo.products ORDER BY product_id; using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { productTable new DataTable(); adapter.Fill(productTable); dataGridView1.DataSource productTable; } } private void FilterProducts(string keyword) { if (productTable null) return; DataView dv productTable.DefaultView; if (string.IsNullOrWhiteSpace(keyword)) { dv.RowFilter null; } else { dv.RowFilter string.Format(product_name LIKE %{0}% OR product_id LIKE %{0}%, keyword.Replace(, )); } }这种过滤方式完全走内存比每次敲键盘都查数据库快几个数量级。但这里有个容易翻车的细节RowFilter使用的是 DataTable 的表达式语法如果搜索词里包含单引号会直接把过滤表达式搞坏所以要先把单引号替换成双引号上面代码里Replace(, )就是专门做这个的。从几百条商品里过滤出一个中文关键字对 DataGridView 来说已经足够不必上后台线程。DataGridView 相关的参数还有一个常被忽视的如果 DataGridView 只是展示不打算在线编辑一定要把ReadOnly设为true把SelectionMode设为FullRowSelect否则用户双击单元格就能改数据误操作后库存就悄悄变了。需要编辑价格和库存时单独弹出一个编辑窗口保存时再写数据库这样每一步操作都有明确的意图。3.3 结算按钮事务包裹订单与扣库存避免月底对账翻车结算是整个收银系统里风险最高的一段逻辑。新手最容易犯的错误是先把订单主表插进去再插明细最后扣库存每一步独立提交。任何一个环节失败数据库中就会出现一张缺明细的订单或者库存扣了但订单没生成月底对账时怎么都对不上。正确做法是把这几步包在同一个数据库事务里要么全部成功要么全部回滚。我一般会先检查库存够不够再插入订单头、订单明细最后扣减库存。using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 检查库存 string checkSql SELECT stock_qty FROM dbo.products WITH(UPDLOCK, ROWLOCK) WHERE product_idpid; using (SqlCommand cmd new SqlCommand(checkSql, conn, tran)) { cmd.Parameters.AddWithValue(pid, productId); int stock (int)cmd.ExecuteScalar(); if (stock qty) throw new Exception(商品[ productName ]库存不足); } // 插入订单主表 string headerSql INSERT INTO dbo.invoice_header (invoice_no, user_id, total_amt, pay_amt, pay_type) VALUES (invoiceNo, userId, totalAmt, payAmt, payType); // 省略 SqlCommand 参数赋值细节 // 插入订单明细表循环商品列表 // 逐条 INSERT 或使用 SqlBulkCopy // 扣减库存 string deductSql UPDATE dbo.products SET stock_qty stock_qty - qty WHERE product_idpid; // 省略 SqlCommand 参数赋值细节 tran.Commit(); } catch { tran.Rollback(); throw; } } }这段逻辑里有两个参数值得解释。第一个是查询库存时用了WITH(UPDLOCK, ROWLOCK)锁提示作用是在事务里锁定这一行防止另一台收银机同时卖同一件商品时读出旧库存造成超卖。单机门店感觉不到它的存在两台收银机同时运行时这个提示能避免库存变成负数。第二个是扣库存的 SQL 用了stock_qty stock_qty - qty而不是先读出值再减这一步要查的旧值由数据库自己维护可以挡住大部分并发冲突。如果用了SqlBulkCopy批量写明细注意事务必须传给SqlBulkCopy的Transaction属性否则批量写入不受当前事务控制一旦后面扣库存失败明细已经持久化就失去了事务的意义。找零计算不要用 double 或 float用decimal。浮点数的二进制表示会让 0.1 加 0.2 不等于 0.3 这类问题出现在找零里账目会差到以分为单位的细节上日结时想查查不出来。整单金额、实收金额、找零金额一律用 decimal控件上输入的字符串用decimal.TryParse转换转换失败就提示重新输入不强行解析。4. SQL 文件怎么组织建库、存储过程、初始化数据三件套4.1 建库建表脚本字符集与约束一次写对“源码sql文件”里的 SQL 文件不是随手导出的而是要让拿源码的人第一次就能在空数据库上跑通。一套合格的 SQL 文件应该按照固定顺序排列建库、建表、建视图、建存储过程、插入初始化数据。很多人拿到 sql 文件后直接全选执行结果因为表之间外键依赖顺序错乱而报错就是因为没有组织好脚本顺序。建库语句写在最前面表结构按依赖顺序从主表到子表排列先建不依赖别的表的表再建有外键的表。下面是一个基础的商品表脚本示例里面把约束都写清楚了CREATE TABLE dbo.products ( product_id INT IDENTITY(1,1) NOT NULL, product_code VARCHAR(20) NOT NULL, -- 商品条码扫的就是它 product_name NVARCHAR(120) NOT NULL, spec NVARCHAR(50) NULL, -- 规格如500g/包 unit NVARCHAR(10) NOT NULL DEFAULT N个, stock_qty DECIMAL(10,2) NOT NULL DEFAULT 0, warn_qty DECIMAL(10,2) NOT NULL DEFAULT 10, -- 库存预警线 sale_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1, -- 1在售 0停售 CONSTRAINT pk_products PRIMARY KEY (product_id), CONSTRAINT uq_product_code UNIQUE (product_code) );字段类型里有几个关键点。product_code用VARCHAR而不是NVARCHAR商品条码和后台条码本身对中文做不了什么用定长字符串空间更省。NVARCHAR(120)用于商品名称因为要存中文。DECIMAL(10,2)用于价格和数量不是 float。status字段用TINYINT而不是直接用 BIT方便以后扩展状态位比如从在售到停售再到清仓。每张表都加了主键商品表额外做了唯一约束防止重复条码进系统。执行建表脚本时如果已经存在同名表SQL Server 会报错可以在脚本开头加IF OBJECT_ID(dbo.products, U) IS NOT NULL DROP TABLE dbo.products;这样的判断让脚本具备重复执行的能力。4.2 统计与预警存储过程销售额按日汇总、库存低于阈值弹提醒报表功能在 WinForm 项目里通常用两条路实现一条是前端拼接 SQL 查询后填 DataGridView另一条是提前建好视图和存储过程前端只调用。小店系统没有复杂到必须上 OLAP但把统计逻辑放进存储过程有一个明显的好处换一个前端界面统计口径不用重写。我一般至少建两个存储过程日销售汇总和库存预警查询。日销售汇总 stored procedure 核心逻辑是按支付类型分组求销售额同时把现金、微信、支付宝分开看CREATE PROCEDURE dbo.usp_DailySalesReport businessDate DATETIME AS BEGIN SET NOCOUNT ON; SELECT pay_type, COUNT(1) AS order_count, SUM(total_amt) AS sale_total, SUM(pay_amt - total_amt) AS discount_total FROM dbo.invoice_header WHERE CONVERT(DATE, create_time) CONVERT(DATE, businessDate) GROUP BY pay_type; END这里CONVERT(DATE, create_time)的作用是忽略时间部分只按天对比。参数businessDate由前端传入日期最好在传入时就固定为当天的零点而不是传DateTime.Now因为报表的“业务日期”和“实际运行日期”不同晚班单据可能跨到第二天凌晨按计算机时间汇总会导致前一天少记。库存预警存储过程更简单直接查出库存低于预警线的商品清单然后前端用 MessageBox 或者小窗口弹出来。阈值不要写死在代码里商品表里已经有warn_qty字段让每个商品可以单独设置比如鸡蛋的预警线是 50 盒洗发水可设为 5 瓶。存储过程写完千万别忘了给执行权限。很多门店电脑上 SQL Server 登录账号权限很小默认没有EXECUTE权限前端调存储过程会报“对象名无效”或权限不足别把精力花在冤枉路上。4.3 初始化数据与备份恢复第一次双击就能登进去SQL 文件的最后一部分是初始化数据。至少要包含一个默认管理员账号、一个测试商品列表、一个会员示例。管理员密码不要用明文直接预置 MD5 哈希值这样首次登录不用先找密码表再改系统。很多分享的源码把用户名密码写在 README 里但用起来不方便别人拿到后没有善后可能就直接带着默认密码上线风险太高。我在编写初始化脚本时会往用户表里插一个admin账号密码哈希值是e10adc3949ba59abbe56e057f20f883e也就是 123456 的 MD5首次登录后强制改密。初始化商品数据可以做成 INSERT 一段几十条记录覆盖饮料、零食、日用、生鲜几类典型商品条码用真实超市常见码模拟。如果你要部署到自己的门店不要直接用这些演示数据要把商品档案重新导入否则收银台上扫码枪扫出来的价格是别人的。SQL 文件里还要带上备份恢复说明至少给出一个标准备份脚本BACKUP DATABASE SuperMarketDb TO DISK ND:\backup\SuperMarketDb_202501.bak WITH INIT, COMPRESSION;恢复时的脚本也要附上但需要注意恢复前要强制断开现有连接ALTER DATABASE SuperMarketDb SET SINGLE_USER WITH ROLLBACK IMMEDIATE; RESTORE DATABASE SuperMarketDb FROM DISK ND:\backup\SuperMarketDb_202501.bak WITH REPLACE; ALTER DATABASE SuperMarketDb SET MULTI_USER;备份文件命名里带日期每天凌晨用 Windows 计划任务跑一次比任何高深的备份策略都实在。小店不会有人天天记得手工备份自动化是唯一可靠的路。5. WinForm 收银系统避坑手册五条踩坑记录现象原因解决一条条对5.1 界面假死和跨线程崩溃Timer 刷新和 UI 线程打架现象系统跑一会儿就卡住尤其在高峰期点击按钮没反应过几秒又活过来。如果让后台线程直接操作控件程序直接抛异常崩掉。原因WinForm 的 UI 控件只能在创建它的主线程里操作。很多人在代码里开了后台线程查库存查询完成后直接执行label1.Text ....运行时遇到跨线程操作就会抛InvalidOperationException。另一方面如果所有查询都堆在主线程执行数据库响应慢时界面就假死看起来像整个程序停摆。解决跨线程更新控件用Control.BeginInvoke把操作调度回 UI 线程不要用Control.CheckForIllegalCrossThreadCalls false压制报错那是把隐患压下去后面会让数据不同步得更邪门。后台线程只负责取数据拿到结果后丢回 UI 线程更新。如果是主线程等待数据库考虑把查询封装成异步方法。门店系统数据量不大最简单的做法是收银台结算时给主界面一个“正在结算”的提示让用户知道系统在工作而不是卡死。5.2 中文乱码SQL 文件导入后整表问号现象运行别人给的 sql 文件商品表里的中文变成一排问号或者导入时报“字符串或二进制数据将被截断”。原因SQL 文件本身保存的编码和 SQL Server 解析时使用的编码不一致。用记事本另存为 ANSI 的脚本导入到 SQL Server 2012 以上实例中秋风扫落叶一样把中文字符解析错。另一个原因是 INSERT 语句里的中文字符串没有加N前缀导致隐式转换后乱码。解决SQL 文件统一保存为带 BOM 的 UTF-8 编码在 SSMS 打开时选择“Unicode UTF-8”。所有插入中文的字符串常量都写成N中文例如INSERT INTO dbo.products (product_name) VALUES (N可口可乐)。文件已经乱码的只能用文本编辑器重新把内容保存为正确编码再执行没有别的后悔药。部署到门店时建议把 SQL 文件放进源代码目录一起走不要从微信聊天记录里复制微信传输会动文件编码。5.3 扫码枪输入自动触发按钮焦点控制与回车拦截现象扫码枪扫一个商品条码商品没有被添加进购物车反而弹出了登录窗口或者把当前编辑框的内容提交了收银台操作完全混乱。原因大多数扫码枪是“键盘模拟器”相当于在聚焦的控件上快速输入一串字符再敲一个回车。如果焦点刚好在“查询”按钮上回车就触发了按钮的 Click 事件。这个行为在 WinForm 里特别容易让人一头雾水看起来是程序乱跳其实是焦点和回车事件在作祟。解决为扫码枪做一个专门的输入框不让焦点跑到其他按钮上。在输入框的 KeyPress 事件里判断如果按下的是回车就取出完整条码去查询并添加购物车同时用e.Handled true吞掉这个回车事件。这样扫码枪的自动回车不会再触发按钮人工键盘在输入框里按回车也只会添加商品。如果系统里有多个窗口都希望响应扫码把扫码输入的逻辑统一封装到一个控件里不要在每个窗口里各写一遍否则改一处忘一处。private void txtScan_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar (char)13) { string barcode txtScan.Text.Trim(); AddProductToCart(barcode); txtScan.Clear(); txtScan.Focus(); e.Handled true; // 关键拦截回车防止触发按钮 } }5.4 库存变负数两台收银机并发扣减同一件商品现象月底盘点发现库存比账面少了甚至出现负数。日志里看不到谁删过数据数据库也找不到异常操作。原因两台收银机同时卖同一件商品时两边的程序都先查询库存查到的都是 10各自判断库存足够后扣减 1结果两次 UPDATE 后库存变成 8实际只卖了 2 件却扣了 2 次库存。这种问题在单品数量少、价格高的小超市特别常见一瓶贵价酒被卖成负数。解决扣库存的 SQL 用原子更新不再先查后改。UPDATE dbo.products SET stock_qty stock_qty - qty WHERE product_id pid AND stock_qty qty这一步如果影响行数为 0说明库存不足或商品不存在再在事务里回滚并提示用户。配合上一章提到的UPDLOCK锁提示两台收银机同时点击结算时数据库会在行级串行化处理不会再出现各自读旧库存的竞态。如果你把商品表放到内存缓存里做扣减一定要保证缓存更新和数据库更新在同一事务里否则缓存与数据库不一致的坑更大不建议小系统做这么复杂。5.5 换电脑跑不起来连接串与安装打包的坑现象源码在自己的电脑上运行正常打包安装到门店另一台电脑上打开就报“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误”。原因最常见的是连接串写死了电脑名或 IP拿到新环境后数据库实例名、账号密码都不一样而代码里后缀是写死的.或者开发机的实例名。其次是目标机器没装 SQL Server 或者只装了 Express连接串却连接的是默认实例。还有人忘了把App.config一起发布导致新机器拿不到配置。解决连接串读ConfigurationManager部署时直接改App.config文件不用重新编译。打包工具我一般用 Inno Setup比 VS 自带的 InstallShield 直观一些主程序、运行库、配置文件一起打包。门店电脑必须有 SQL Server 实例装 Express 也可以用但连接串要写成Data Source.\\SQLEXPRESS并且配好账号。如果你用 ClickOnce 发布注意App.config会被转换调试好的连接串可能被覆盖得在发布选项里排除配置文件或者部署后重新设置。一个更省心的做法是程序启动时检测数据库连接失败则弹出一个小窗口让维护人员填服务器地址、账号、密码自动写回App.config。这样新门店部署时就不需要任何开发者到场多出一段二十行的配置对话框能省掉不知道多少个电话和远程。6. 今晚就能做的性能验证把收银台反应时间压到一百毫秒以内收银台体验好坏核心指标是扫一个商品到购物车多了一行这个过程用户能不能感到“秒开”。如果扫完条码还要转圈等一秒高峰期排队的顾客就能感受到这家店系统很慢负面影响直接反应在门店口碑上。这套 WinForm 系统性能瓶颈不在 WinForm而在数据库访问频率。我建议你今晚做三个测试不需要改架构只需要在现有代码上加一层优化。第一个测试是商品缓存。收银台扫商品时程序每次去数据库按条码查一条商品这是最常见的慢点。几百毫秒的数据库往返加上网络延迟叠加每个商品查一次结账十条商品的订单就多出好几秒。改法是在程序启动时把商品表加载进一个Dictionarystring, ProductCacheItem键是条码扫一个取一个完全不打数据库。缓存里的stock_qty只在结账扣库存后才更新同时后台每隔五分钟刷新一次缓存。第二个测试是 DataGridView 虚拟模式。商品多到万级时直接绑定 DataTable 滚动会肉眼可见地卡。把 DataGridView 的VirtualMode设为 true自己实现CellValueNeeded事件只提供当前屏幕需要显示的几十行滚动极流畅。虚拟模式的代价是失去自动排序和自动编辑但收银系统根本不依赖这些功能适合纯展示场景。第三个测试是批量写入。一个订单十条明细如果用十个 INSERT 语句逐条执行每条都有连接往返放本地还好走网络就连累结账速度。改成SqlBulkCopy一次性把明细表写入数据库或者用表值参数传入存储过程能把这十次往返变成一次。SqlBulkCopy 要记得把事务对象传进去否则订单主表成功明细表失败数据完整性直接破防。我以前接手过一家门店的收银系统结账高峰期顾客排了四条队还是慢查了半天发现他们在明细循环里每次都写日志文件日志写盘和数据库查询串行执行把整个收银速度拖到了三秒一单。去掉日志同步写加一个商品缓存整体响应时间压到一百毫秒以内。那次之后我养成了一个习惯任何收银系统优化第一件事永远是看数据库请求次数而不是纠结界面美观。先把扫商品不查库、结算只写一次库这两件事做到收银台就基本不会再被吐槽慢。希望这些经验和踩过坑的参数能帮到你照着这个方向调完你再回来看这套 WinForm 项目就会觉得处处都能解释得通了。本文还有配套的精品资源点击获取
返回列表