简介:面向毕业设计场景的C#仓库条码管理系统源码,围绕入库、出库、库存查询和条码扫描等核心业务展开,提供一套可直接运行的Windows窗体应用方案,适合需要完成课设或毕设的C#学习者。压缩包共132个文件,以47个cs源码、20个resx界面资源、19个resources资源文件、18张jpg图片、3个exe可执行程序及SQL脚本、mdf/ldf数据库文件等为主,整体约9.96MB,并包含.sln工程与.config配置,可在Visual Studio中同步还原数据库与代码工程。系统中入库、出库、库存等模块均有对应的设计器文件,源码结构清晰,涵盖库存预警、交易记录、自动盘点及报表生成;同时涉及ZXing.NET条码解析、ADO.NET或Entity Framework数据访问、报表组件定制等关键开发点,便于理解从界面操作到数据库落地的完整链路。已有204人学习下载,适合作为C#项目实战和仓库管理类毕业设计的参考模板。
1. 网上下的「基于C#的仓库条码管理系统源码.zip」,到底能直接跑吗
花一个晚上解开「基于C#的仓库条码管理系统源码.zip」,最劝退的不是代码量,而是三件小事:数据库脚本只有半套、扫描枪插上没反应、标签打出来扫不上。仓库条码管理系统拆到底就是「扫描枪 + 商品台账 + 出入库流水」三样东西,C# 做这类应用最常见也最稳妥的路线是 WinForms 客户端配 SQL Server 或 SQLite,条码部分交给 ZXing.Net。这篇文章按我接手这类项目的老路数走一遍:从解压开始判断工程完整度,讲清表结构和条码策略,把扫码录入、条码打印的落地代码贴出来,最后补一份高频避坑清单。新手能照着跑通整套流程,熟手也能看出哪些结构值得改、哪些是留着会咬人的坑。
2. 解压后的第一件事:判断这个 C# 工程是完整项目还是残缺骨架
拿到任何「xx源码.zip」,我的习惯都是先不开 Visual Studio,先开文件资源管理器逛一圈目录。VS 一打开就报错的项目,十有八九不是代码写错,而是缺了依赖、缺了数据库脚本、或者连接串指向一台不存在的服务器。一个能跑的仓库条码管理系统源码包,至少该有 .sln 解决方案文件、若干个 .csproj 工程文件、数据库初始化脚本或附加库文件、以及 NuGet 依赖声明。先花十分钟做静态检查,能省掉后面两小时的排错。
2.1 看 sln 与 csproj:项目类型和依赖还原路径
.sln 文件决定了这个源码是 WinForms、WPF 还是 ASP.NET。仓库条码管理这类内部系统,WinForms 出现的概率最高,因为它部署最简单:一台 Windows 工控机,装个 .NET Framework 就能跑。WPF 也有,但老源码里少。如果打开 sln 发现是 Web 项目,那多半是「B/S 版仓库系统」,跟标题里常见的桌面版预期不太一样,这时要先想清楚你要的是哪种形态再继续。
确定项目类型之后,下一步是打开 .csproj 看目标框架。用 PowerShell 批量看一下所有工程文件的 TargetFrameworkVersion 和 OutputType,比逐个用记事本打开快得多:
Get-ChildItem -Path . -Recurse -Include *.csproj | Select-String -Pattern 'TargetFrameworkVersion|OutputType|RootNamespace'这条命令把仓库里所有 C# 工程文件扫一遍,TargetFrameworkVersion告诉你这个项目是 .NET Framework 3.5/4.0/4.6 还是更高版本,OutputType告诉你编译出来是 WinExe(窗口程序)、Exe(控制台)还是 Library(类库)。如果看到 v4.0 以下的老框架,Redemption 也尽量不要在最新版 Visual Studio 里硬开——建议直接用对应版本的 IDE,或者准备好升级项目的心理预期。RootNamespace一般和项目名一致,如果它和文件夹名对不上,说明源码被改过名,后面编译时要注意命名空间引用。
顺便说一句,这类源码里控件的命名风格很统一,几乎都是「txt 开头是文本框、cbo 开头是下拉框、dgv 开头是 DataGridView」这种控件命名简称。看到txtBarcode、cboType、dgvList这种命名,就别抱怨作者没品味了,这是他给你留的快速读代码的路标。
2.2 看 packages 与 bin:第三方库是源码依赖还是编译产物
接下来看两样东西:有没有 packages 文件夹,以及 bin 目录里有没有编译好的 exe。这两样决定了这个源码能不能「解压即跑」。
WarehouseBarcode/ ├─ WarehouseBarcode.sln ├─ App.config ├─ packages.config ├─ Database/ │ └─ warehouse_barcode.sql └─ WarehouseBarcode/ ├─ bin/ │ └─ Debug/ │ └─ WarehouseBarcode.exe ├─ DAL/ │ └─ DBHelper.cs ├─ BLL/ │ └─ GoodsManager.cs └─ UI/ └─ frmMain.cs上面是我见过最典型的仓库条码管理源码目录结构。packages.config存在,说明项目用的是老式 NuGet 包管理方式;如果packages文件夹也在,第三方 DLL 已经下载好了,恢复依赖非常快。如果只有packages.config没有packages文件夹,也不用慌,Visual Studio 打开项目时会自动还原。bin/Debug下面有 exe 是个好消息——说明源码曾经编译成功过,你可以先直接双击 exe 看界面,再决定要不要继续看代码;如果 bin 是空的,也别失望,至少说明这包是「纯源码」,没有被作者拿编译产物凑数。
顺带看目录里有没有 ZXing.NET、System.Data.SQLite 这类关键词。看到 ZXing 相关引用,说明条码生成功能内置了;看到 System.Data.SQLite,说明数据库可能是文件库而非 SQL Server。这两个判断决定了后面你要准备什么环境。
2.3 看数据库脚本与连接串:从 App.config 反推服务端基础设施
数据库是仓库条码系统的地基。源码包里不一定带数据库备份文件,但几乎一定会带 SQL 脚本,或者 App.config 里的连接串泄露了数据库类型。打开 App.config,找到 connectionStrings 节点,这是整个项目第一个要改的地方:
<connectionStrings> <add name="DbConn" connectionString="Data Source=.;Initial Catalog=WarehouseBarcode;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings>这配置指向 SQL Server 本机实例,Windows 身份登录。如果Data Source写的是.\SQLEXPRESS或机器名,说明作者开发时用的是 SQL Server Express;如果写的是本地某个 .mdf 文件路径,那多半是 LocalDB;如果连接串里带Data Source=DataBase.db这种,就是 SQLite 文件库。还有一类老古董用 Access,连接串里能看到Microsoft.ACE.OLEDB.12.0这种 Provider。判断完数据库类型,再去 Database 目录找初始化脚本——这一步能确认你拿到的是「完整可初始化」的包还是「只有代码没有数据」的半成品。
常见做法是,我会顺手把连接串改成指向自己机器上的 SQL Server 实例,或者干脆在开发阶段切成 SQLite,省得每次换电脑都要装数据库服务。连接串挪到 App.config 而不是写死在代码里,是这类源码最容易踩的隐性坑——很多老代码喜欢在 DAL 里 new SqlConnection 时硬编码连接字符串,那种项目改起来就是一场灾难。
3. 数据库地基:四张核心表、条码主键策略与一段可直接执行的初始化脚本
不管下载来的源码里脚本有多复杂,仓库条码管理要跑通业务闭环,最少只需要四张表:商品表、库存表、入库单表、出库单表。很多老源码会把出入库合并成一张「流水表」,用 Type 字段区分入库还是出库,再用 SUM 聚合算出库存。这种设计表少、写起来快,但并发一高就容易把库存算穿,后期维护也绕。我一般建议拿到源码后先看清楚它的库存是怎么算的:如果是 SUM 流水,趁早改成独立库存表,否则后面做盘点对账会非常痛苦。
3.1 表结构与字段设计:入库单、出库单、商品、库存的关联方式
下面这段建表脚本是 SQL Server 语法,也是这类源码最常见的写法。如果你打算用 SQLite,把IDENTITY换成AUTOINCREMENT,去掉GO分隔符就能跑;用 Access 的话字段类型要微调,但表结构思路完全一致:
CREATE TABLE Goods ( Id INT IDENTITY PRIMARY KEY, GoodsCode NVARCHAR(50) NOT NULL, GoodsName NVARCHAR(100) NOT NULL, Spec NVARCHAR(50), Unit NVARCHAR(10), Price DECIMAL(18,2) DEFAULT 0, CreateTime DATETIME DEFAULT GETDATE() ); GO CREATE UNIQUE INDEX UX_Goods_Code ON Goods(GoodsCode); GO CREATE TABLE Stock ( Id INT IDENTITY PRIMARY KEY, GoodsId INT NOT NULL REFERENCES Goods(Id), Quantity DECIMAL(18,2) DEFAULT 0, SafeStock DECIMAL(18,2) DEFAULT 0, UpdateTime DATETIME DEFAULT GETDATE() ); GO CREATE UNIQUE INDEX UX_Stock_Goods ON Stock(GoodsId); GO CREATE TABLE InStockBill ( Id INT IDENTITY PRIMARY KEY, BillNo NVARCHAR(30) NOT NULL, GoodsId INT NOT NULL REFERENCES Goods(Id), Quantity DECIMAL(18,2) NOT NULL, Operator NVARCHAR(50), BillTime DATETIME DEFAULT GETDATE() ); GO CREATE TABLE OutStockBill ( Id INT IDENTITY PRIMARY KEY, BillNo NVARCHAR(30) NOT NULL, GoodsId INT NOT NULL REFERENCES Goods(Id), Quantity DECIMAL(18,2) NOT NULL, Operator NVARCHAR(50), BillTime DATETIME DEFAULT GETDATE() ); GO注意几个选型参数。Price和Quantity用DECIMAL(18,2)而不是FLOAT,这是仓库系统的铁律——浮点数在累加和比较时会出现 0.1 + 0.2 不等于 0.3 的问题,库存和金额经不起这种玄学偏差。GoodsCode加了唯一索引,这是条码系统的生命线,后面细说。Stock表单独存在而不是靠流水 SUM,是为了让「查当前库存」变成一次索引查找,而不是全表聚合;代价是每次出入库都要在同一事务里维护这张表,这笔买卖划算。
BillNo建议存「业务单据号」而不是直接用自增 ID,因为自增 ID 在跨库迁移、报表导出时没有业务含义,而且容易被外部猜测。常见做法是「IO20240617001」这种前缀加日期加流水号的格式,入库单和出库单各有一套前缀,查问题的时候一眼能分辨来源。
3.2 条码字段的四个边界:唯一性、长度、编码方式、校验
条码字段是整个系统的魂。这里我踩过太多坑,直接给你四个边界条件。
第一,唯一性。GoodsCode 必须加唯一索引,并在应用层做二次校验。你会遇到的情况是:两个仓库管理员各拿一台电脑同时录入同一个新商品,如果只在保存时查一次有没有重复,并发窗口期就能插进两条一模一样的条码。数据库唯一索引是最后一道闸门,谁都不能省。
第二,长度。如果用的是商品自带的 EAN-13 条码,长度固定 13 位数字,那么字段 20 位就够;如果用的是内部编码规则,我建议直接给 50 位,并采用 Code128 编码——它支持全 ASCII 字符,字母数字混排无压力。有的老师傅喜欢用自增 ID 直接当条码打出来,这是典型的饮鸩止渴:位数不稳定、没有业务语义、跨系统对接时根本没法用。
第三,编码方式。条码内容只建议用大写字母和数字,不要放小写、不要放空格、更不要放中文。Code128 虽然技术上支持 ASCII 全集,但很多扫描枪默认配置会对小写字母做转换,你在数据库里存了个小写「abc」,扫码进系统变成大写「ABC」,查不到数据,你还以为是扫码枪坏了。
第四,校验位。EAN-13 自带校验位,Code128 也自带模运算校验,所以只要是标准条码,扫描枪本身会拒绝错误内容。真正要防的不是条码错,而是「人眼把 A 看成 8」这种人工录入场景。所以系统里所有手工输入条码的位置,都要配置扫描枪输入而不是键盘敲,敲进去的必须做存在性校验。
3.3 初始化脚本:建库建表插基础数据的执行顺序
拿到源码后别急着点运行,先把数据库初始化做干净。执行顺序有讲究:先建库,再建表,最后插基础数据。很多源码的脚本把CREATE DATABASE和CREATE TABLE混在一个文件里,这在 SQL Server 的 SSMS 里能跑通,但在一些老的查询工具里会因为「当前库不存在」而失败。我一般会先手动建一个空库,再执行建表脚本,这样出问题能准确定位到第几条语句。
基础数据至少插三类:几个测试商品、对应的库存初始值(一般为 0)、以及一个管理员账号(如果你的源码有登录功能)。插商品时注意 GoodsCode 要用真实能扫出来的条码,别随手编一个「123456」,否则后面测扫码功能时你会分不清是枪的问题还是条码的问题。插完后先跑一条最简单的查询:
SELECT g.GoodsCode, g.GoodsName, ISNULL(s.Quantity, 0) AS Qty FROM Goods g LEFT JOIN Stock s ON g.Id = s.GoodsId;能查出商品且库存列不报错,说明表和关联字段没问题,可以进下一步。用 SQLite 的话,对应的执行工具是sqlite3命令行或任何一款数据库浏览器;如果你决定把源码从 SQL Server 迁移到 SQLite,注意把NVARCHAR改成TEXT,DATETIME改成TEXT并统一存 ISO 格式字符串,DECIMAL在 SQLite 里不强校验,但建议保留DECIMAL(18,2)的声明让 ORM 层能正确读类型。
4. 扫码入库的完整链路:扫描枪像键盘一样输入,事务保证库存不穿
数据库准备好了,接下来是整个系统最核心的交互:扫码入库。这一步做得好不好,直接决定仓库大妈愿不愿意用你的系统。我的经验是,一个扫码入库动作,从枪响到库存变化,控制在 0.5 秒内才算合格;超过 1 秒,现场就会有人开始抱怨系统卡。这里面的技术含量不高,但细节非常多,逐个拆开讲。
4.1 扫描枪的两种工作模式:USB 键盘模拟与串口,分别怎么接
市面上的扫码枪,默认出厂模式几乎都是 USB 键盘模拟(USB-KBW),意思是它把自己伪装成一个键盘,扫到的内容直接敲进当前焦点所在的输入框,结尾自动带一个回车。这种模式的好处是零驱动、零配置,插上就能用;坏处是焦点在哪它就往哪敲——你把焦点停在搜索框里,扫一下条码,它会把条码敲进搜索框而不是单据录入框,这是现场最常见的「系统乱跳」问题根源。
另一种模式是串口(RS232/USB 转串口),扫码枪通过 COM 口往电脑发数据,程序用 SerialPort 组件接收。仓库条码管理系统里串口模式通常出现在两种场景:一是工位上有专用的扫码工控机,需要程序主动控制;二是在 C# 上位机项目里要和其他仪器共用串口资源。如果你拿到的源码里写着 SerialPort 相关代码,那它走的就是这条路;什么都没写,默认按键盘模式理解就行。
判断扫描枪当前是什么模式,最简单的办法是打开记事本,光标点进去,扫一下条码:如果看到字符和一个回车,就是键盘模式;如果记事本里没动静,那多半是串口模式,且波特率、数据位等参数还要和枪的说明书对齐。仓库条码管理系统的源码绝大多数是按键盘模式写的,你要是发现扫了没反应,先查枪的模式,别急着改代码。
4.2 在 WinForms 里接住扫码结果:KeyPress 事件与回车结束符
键盘模式下,C# 接扫码结果最简单的方式是给条码输入框挂 KeyPress 事件,监听回车键(ASCII 13)。有些枪的结束符可配置成 Tab(ASCII 9),那就判断 9。下面这段代码是这种方案的骨架:
private void txtBarcode_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)13) { e.Handled = true; HandleScannedCode(txtBarcode.Text.Trim()); txtBarcode.Clear(); } }参数说明:e.KeyChar是当前键入的字符,(char)13对应回车键;e.Handled = true表示这个回车事件已被处理,不再触发按钮的默认行为,防止窗体上某个按钮被意外「按下」。HandleScannedCode是你要实现的业务方法,参数是扫描枪读到的条码内容。清空输入框这步不能省——如果不清空,下一次扫码会跟上一次内容拼接在一起,这是条码录入串号的第一大来源。
还有一个容易被忽略的细节:窗体加载后要把焦点默认设在txtBarcode上,否则扫码枪第一个字符不知道敲到哪。常见做法是在窗体的Shown事件里执行txtBarcode.Focus()。如果单据录入过程中焦点被数量输入框抢走,要做一个「扫码自动回焦」的机制:扫描枪的回车结尾本身就带换行,你可以在数量框的 KeyPress 里也监听回车,回车后把焦点还给条码框,这样整个录入循环就顺畅了。
4.3 入库事务:查商品、写单据、更新库存三件事怎么包在一个事务里
扫码只是拿到了条码,真正入库是后面三件事:查商品是否存在、写入库单、更新库存。这三件事必须在一个数据库事务里完成,否则就会出现「单据写了但库存没变」这种对账对到哭的问题。下面是一个最小可用的入库事务实现:
using (var conn = new SqlConnection(connStr)) { conn.Open(); using (var tx = conn.BeginTransaction()) { try { // 1. 查商品 var cmdFind = new SqlCommand( "SELECT Id FROM Goods WHERE GoodsCode = @code", conn, tx); cmdFind.Parameters.AddWithValue("@code", code); object goodsId = cmdFind.ExecuteScalar(); if (goodsId == null) { MessageBox.Show("条码未建档,请先维护商品资料"); return; } // 2. 写入库单 var cmdBill = new SqlCommand( "INSERT INTO InStockBill(BillNo, GoodsId, Quantity, Operator) " + "VALUES(@billNo, @gid, @qty, @op)", conn, tx); cmdBill.Parameters.AddWithValue("@billNo", GenerateBillNo("IO")); cmdBill.Parameters.AddWithValue("@gid", goodsId); cmdBill.Parameters.AddWithValue("@qty", qty); cmdBill.Parameters.AddWithValue("@op", currentUser); cmdBill.ExecuteNonQuery(); // 3. 更新库存 var cmdStock = new SqlCommand( "UPDATE Stock SET Quantity = Quantity + @qty, UpdateTime = GETDATE() " + "WHERE GoodsId = @gid", conn, tx); cmdStock.Parameters.AddWithValue("@qty", qty); cmdStock.Parameters.AddWithValue("@gid", goodsId); cmdStock.ExecuteNonQuery(); tx.Commit(); MessageBox.Show("入库成功"); } catch { tx.Rollback(); throw; } } }逻辑说明:第一步用ExecuteScalar查单个值,条码不存在直接 return,不做任何写操作。第二步插入单据,BillNo用GenerateBillNo("IO")生成一个带前缀的单号。第三步是库存累加,用Quantity = Quantity + @qty而不是先查再写,这是并发安全的关键写法——如果先 SELECT 再 UPDATE,两个窗口同时操作时后写的人会覆盖先写的人的数据。事务的Commit和Rollback保证了三件事同生共死。
参数说明:AddWithValue虽然写起来方便,但在某些 SQL Server 版本里会因参数类型推断错误导致索引失效,我一般建议显式指定类型,比如cmd.Parameters.Add("@qty", SqlDbType.Decimal).Value = qty。ExecuteScalar返回的是 object,判空必须在转换之前做。事务里不建议嵌套 MessageBox——弹窗会挂起事务线程,事务持有锁的时间越长,并发冲突概率越高,正确做法是先把校验做完,再开事务提交。
4.4 防重复扫码:去抖与状态机
扫码入库还有一个让很多人头疼的问题:同一件商品,仓库员可能因为手抖、或者扫码枪连续触发,扫了两遍。如果你不加任何防护,库存会翻倍。最简陋但有效的办法是「时间窗去抖」:记录最后一次扫码的条码和时间,2 秒内重复的码直接忽略。
private string _lastCode = ""; private DateTime _lastTime = DateTime.MinValue; private bool IsDuplicateScan(string code) { bool isDup = (code == _lastCode && (DateTime.Now - _lastTime).TotalSeconds < 2); if (!isDup) { _lastCode = code; _lastTime = DateTime.Now; } return isDup; }逻辑说明:IsDuplicateScan在每次进入HandleScannedCode时先调用,返回 true 就放弃本次扫码。_lastCode记住上一条码,_lastTime记住上次时间,两者都命中且间隔小于 2 秒才判定为重复。这个方案的问题是:如果连续两件不同商品,但条码相同(不可能,因为唯一索引),或者快速扫了两件同款商品(间隔小于 2 秒但确确实实是两件),会被误杀。所以生产环境更推荐用「状态机」思路:把扫码录入流程建模成 Idle → Scanned → Processing → Idle 四个状态,Processing 状态下任何扫码都排队或忽略,处理完回到 Idle。这个状态机可以直接用一个布尔标志位_isProcessing实现,进事件先判断、处理完再释放,比时间窗更贴近实际业务。时间窗适合「防手抖」,状态机适合「防重入」,两者结合效果最好。
5. 避坑与常见问题排查:仓库条码系统跑起来之后的五个高频翻车点
源码能跑通、扫码能入库,只是开始。真正让一个仓库条码系统从「实验室能跑」变成「库房里好用」的,是那些看起来不起眼、但每天都在咬人的细节。这一章把我这几年在条码项目上踩过的坑和血泪经验整理成五条排查记录,每条都是「现象 → 原因 → 解决」的顺序,按扫码、打印、数据三类分好,方便你对照自己的情况。
5.1 扫码录入侧:扫了没反应、一次出两遍
**现象一:**扫描枪扣下去,程序界面没有任何动静,但同一把枪在记事本里能正常输出字符。
原因八成是焦点不在条码输入框上。扫码枪模拟键盘,字符往「当前有焦点的控件」里敲,如果焦点在 DataGridView 上或者某按钮上,条码就不知道跑到哪里去了。第二常见的原因是扫描枪被切换成了串口模式,而程序里根本没有串口接收代码。
解决:先在窗体的Activated事件里强制txtBarcode.Focus(),并把窗体的KeyPreview属性设为 true,让窗体优先捕获按键。然后打开设备管理器,找到扫码枪对应的 HID Keyboard Device,确认它走的是键盘通道。如果这两步都做了还不行,用记事本扫一下,看结尾是回车还是 Tab,再去代码里核对判断的是哪个字符。
**现象二:**点一次扫码枪,数据库里同一张入库单出现了两行一模一样的条码。
原因通常是事件被处理了两次:文本框的 KeyDown 和 KeyPress 都写了回车判断,或者扫码枪的结束符配置成「回车+换行」(CR+LF),你的代码对回车和换行各触发了一次业务逻辑。我之前还见过有人把手动「添加」按钮的 Click 事件和扫描枪的回车搞混,扫码一次等于点了一次按钮再加一次回车。
解决:统一扫码入口,只保留一个事件处理函数。如果是 CR+LF 的问题,把判断条件从「只识别 13」改成「识别 13 或 10,但同一批字符只处理一次」;或者在 KeyPress 里用e.Handled = true把后续字符吞掉。最后再去扫码枪说明书里把结束符改成纯回车,一劳永逸。
5.2 条码与打印侧:打出来扫不上、标签错位
**现象三:**用条码打印机打出来的 Code128,肉眼看得很清楚,但扫描枪和手机都扫不出来。
原因基本是三个:条码高度太矮、左右空白区(Quiet Zone)不足、或者条码内容里混了不该有的字符。ZXing.Net 生成的条码如果 Margin 设成 0,打印出来贴到包装上,扫码枪会因为找不到起始符直接罢工。
解决:把生成参数调整到「保守档」——Margin 至少 8 到 16 像素,条码高度至少 20 毫米,内容只保留大写字母和数字。下面是我常用的参数模板:
var writer = new BarcodeWriter { Format = BarcodeFormat.CODE_128, Options = new ZXing.Common.EncodingOptions { Width = 300, Height = 80, Margin = 12, PureBarcode = false } }; Bitmap barcode = writer.Write("WH20240617001");参数说明:Width=300和Height=80是像素值,按 300 DPI 打印大约 2.5 厘米 × 0.7 厘米,这是标签纸最常见的尺寸;Margin=12是左右留白,别低于 8;PureBarcode=false表示允许条码下方显示可读字符,方便人眼核对。Format=CODE_128支持字母数字混合,如果你的条码纯数字且长度固定 13 位,换成EAN_13会更标准。
**现象四:**标签打出来第一张位置正,第二张开始越来越偏,最后条码直接打印在标签纸缝上。
原因是打印机驱动的纸张设置和 PrintDocument 的页面设置不一致。PrintDocument 默认用 A4,而你的标签纸是 50mm × 30mm,驱动里如果没选「标签纸/自定义尺寸」,打印机按 A4 排版,每张标签之间就会累积偏移。
解决:在 PrintDocument 的DefaultPageSettings.PaperSize里显式指定标签尺寸,同时把打印机驱动里的纸张类型改成对应规格。注意PaperSize的单位是百分之一英寸,50mm 大约等于 197,30mm 大约等于 118。还有一个现场技巧:第一张打印前先让打印机走一张空白标签,主要是因为撕纸位置和打印起始位置之间有偏差,这也是「第二张才准」这类问题最常见的来源。
5.3 数据与部署侧:库存变负数、换电脑连不上库
**现象五:**出库单保存成功,但库存表里同一个商品变成了负数。
原因一定是更新库存的 SQL 没有做数量保护,或者没有用事务。出库的逻辑应该是:先判断库存够不够,再扣减;但「先查再扣」在两个窗口同时操作时会踩空——两个人都查到库存是 10,各出 8,结果库存变成 2,但实际应该扣除失败。最稳的写法是把判断和扣减放在同一条 UPDATE 里:
UPDATE Stock SET Quantity = Quantity - @qty, UpdateTime = GETDATE() WHERE GoodsId = @gid AND Quantity >= @qty;这条 SQL 的执行结果(影响行数)如果为 0,说明库存不足,事务回滚,应用层给出提示。数据库层面的原子操作比任何应用层加锁都可靠——锁会死锁,原子更新不会。这是我做库存系统最核心的一条经验:能用一条 UPDATE 解决的问题绝不用「SELECT + UPDATE」两步走。
部署侧的坑相对简单:源码里连接串写死成本机实例名,换台电脑就报「建立到服务器的连接」错误。解决思路就一个——连接串永远放 App.config,换库改配置不改代码。如果是给现场几十台电脑部署,直接用 SQLite 文件库能省掉装 SQL Server 的麻烦,但要注意 SQLite 的并发写锁问题,几十个客户端同时写库时要加一个简单的重试机制。
6. 把源码变成自己的系统:一份验收清单,再把扫码入口抽象成接口
源码跑通不算本事,能稳定地跑三个月不出幺蛾子才算。这一章给你一份验收清单和一个小改造建议,前者用来确认系统能真正进库房,后者用来给将来的设备升级留条后路。
6.1 入库前先过这张验收清单
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 用键盘手动输入一个不存在的条码 | 系统提示「条码未建档」,库存无变化 |
| 2 | 扫一个已建档条码,数量填 2,提交 | 入库单生成,库存增加 2 |
| 3 | 连续扫同一把枪同一条码 3 次 | 只入一笔,不产生重复单据 |
| 4 | 出库数量超过当前库存 | 提交被拒绝,提示库存不足 |
| 5 | 打一张标签,用同一把枪扫它 | 能扫出原始条码内容,文字清晰 |
| 6 | 关掉程序重新打开 | 库存数据还在,单据流水完整 |
我一般建议在验收清单上再加一条:让两个客户端同时出库同一个商品,看会不会把库存扣成负数。这一条能一次性检验事务和原子更新有没有写对,比测十遍正常流程都管用。
6.2 给未来的 PDA 留口子:把扫码入口抽象成接口
最后做一个低成本、高回报的改造:把「扫码」这件事从「键盘事件」里解耦出来。
public interface IBarCodeScanner { event Action<string> OnScanned; void Start(); } public class KeyboardScanner : IBarCodeScanner { public event Action<string> OnScanned; private TextBox _input; public KeyboardScanner(TextBox input) { _input = input; } public void Start() { _input.KeyPress += (s, e) => { if (e.KeyChar != (char)13) return; OnScanned?.Invoke(_input.Text.Trim()); _input.Clear(); e.Handled = true; }; } }改造后的效果是:业务代码只认IBarCodeScanner.OnScanned事件,不关心条码是键盘枪扫进来的、PDA 网络传过来的、还是将来某个 ARM 工控机通过串口推送进来的。需要接新设备时,写一个新类实现这个接口,替换注入就完事。顺带再说一个我的习惯:新接手的源码,前两周只做重构和补测试,不改业务逻辑——先让系统在验收清单上稳定跑绿,再谈加功能。条码系统最怕的不是代码丑,而是改着改着库存对不上了,那种黑匣子一样的查账过程,经历过一次就再也不想碰了。希望帮到你。
本文还有配套的精品资源,点击获取