ARTICLE DETAIL

资讯详情

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

C#仓库条码管理系统源码解析:从WinForms架构到条码与事务实战

C#仓库条码管理系统源码解析:从WinForms架构到条码与事务实战 简介这套基于C#的仓库条码管理系统源码面向毕业设计及初中级C#开发者解决仓库出入库、库存查询与条码识别一体化管理需求。系统覆盖入库、出库、库存预警、条码扫描及报表生成等核心模块代码结构清晰适合学习.NET桌面应用开发全流程。压缩包共132个文件约9.96MB其中47个.cs源码文件构成主要业务逻辑20个.resx与19个.resources负责界面资源18个jpg可用于查看运行效果另有SQL和MDF/LDF数据库文件便于还原库表结构。资源目前已有204人学习下载。sln与csproj解决方案文件齐备可直接用Visual Studio打开编译数据库脚本覆盖商品、出入库等主要数据表配合MDF/LDF文件可快速搭建演示环境。通过完整工程文件与配置文档读者能直观理解WinForms/WPF界面、ADO.NET或Entity Framework数据访问、ZXing.NET条码解析在实际仓库项目中的协同方式也能参考设计思路扩展自定义报表或库存预警功能是一份兼顾代码阅读与二次开发的实用毕业设计参考。1. C#仓库条码管理系统源码不是玩具项目是能跑通的完整业务闭环仓库条码管理系统这个选题在毕业设计里常年热门因为它正好卡在“有点难度但能完成”的线上——比图书管理这种纯CRUD多出条码识别和事务处理又比电商系统少了订单与权限的复杂度。这套C#源码是WinForms形态项目名还停留在Visual Studio模板默认的WindowsFormsApplication1业务层主要由ruku、chuku、kucun三个窗体设计器文件支撑对应入库、出库、库存查询三块主流程。也就是说这份资源不是纯概念Demo而是一个初始化完成、能直接编译运行的系统骨架。适合两类人拿它当毕业设计基座改界面、加字段、补报表的同学想学C#仓储业务流重点研究扫码输入、事务一致性、库存预警怎么实现的初级工程师。接下来按我拆项目的顺序展开先架构和数据层再出入库核心代码最后落到条码、报表和排错。2. 系统架构与数据库设计三窗体四张表划分清楚再写代码2.1 从Designer.cs看模块划分ruku、chuku、kucun各管什么拿到源码先别急着按F5第一步是读文件清单。你会发现三个窗体设计器文件ruku.Designer.cs、chuku.Designer.cs、kucun.Designer.cs。Designer.cs是WinForms自动生成的控件布局代码正常开发基本不手改但它是理解模块划分最好的入口。ruku.Designer.cs里一般会找到条码输入框、商品信息展示区、入库明细网格DataGridView、确认入库按钮。注意条码输入框的设计它要接收扫描枪发来的整段字符串所以要么设置TabIndex为0让窗体打开自动聚焦要么在Load事件里调用txtBarcode.Focus()否则扫完枪的输入落在无关控件上后面所有逻辑都白搭。chuku窗体多了一块库存余额显示和“强制出库”的复选框这是为库存不足时留的补救入口。kucun窗体则是查询条件和库存表格的组合。三窗体对应三个业务动作这是毕业设计最稳的建模方法。全套不要拆成十几个窗体那会让传值和控件同步变成新的难题也不要一个窗体干完所有事代码全塞在Button_Click里后面论文的模块划分部分根本没法写。实践中比较合适的规模是三个业务窗体加一个公共数据访问类再加一个登录窗体。我给课程设计做评审的时候看到三窗体架构基本直接放行看到几十个窗体反而会多问一句这些窗体之间的通用数据交互怎么做的十几个窗体带来的参数传递和界面刷新问题往往比业务本身还难缠。2.2 四张核心业务表商品、流水、库存的关系数据库设计是这套系统的主干。商品表ProductInfo记录条码、名称、规格、单位和预警下限入库表InStockRecord和出库表OutStockRecord记录流水库存表StockInfo保存实时库存。为什么流水要单独立表因为每次出入库都要追加一条历史之后要查“某一天进了多少货”“某个操作员出了多少单”直接查流水表就行不用去翻商品主表。而库存表是一张冗余表每次出入库之后同步更新——冗余在仓库系统里是划算的查询界面不用做SUM聚合加载速度快报表也好写。下面是配套的SQL Server建表脚本字段注释标在每行后面CREATE TABLE ProductInfo ( ProductCode NVARCHAR(32) PRIMARY KEY, -- 商品条码主键 ProductName NVARCHAR(100) NOT NULL, -- 商品名称 Spec NVARCHAR(50) NULL, -- 规格型号可不填 Unit NVARCHAR(10) NULL, -- 计量单位件/箱/包 LowerLimit INT NOT NULL DEFAULT 10 -- 库存预警下限低于它标红 ); CREATE TABLE InStockRecord ( Id INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(32) NOT NULL CONSTRAINT FK_InStock_Product FOREIGN KEY REFERENCES ProductInfo(ProductCode), Quantity INT NOT NULL, Operator NVARCHAR(32) NOT NULL, -- 操作员账号方便追溯 OperateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE OutStockRecord ( Id INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(32) NOT NULL CONSTRAINT FK_OutStock_Product FOREIGN KEY REFERENCES ProductInfo(ProductCode), Quantity INT NOT NULL, Operator NVARCHAR(32) NOT NULL, OperateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE StockInfo ( ProductCode NVARCHAR(32) PRIMARY KEY CONSTRAINT FK_Stock_Product FOREIGN KEY REFERENCES ProductInfo(ProductCode), StockQuantity INT NOT NULL DEFAULT 0, LastInTime DATETIME NULL, LastOutTime DATETIME NULL );几个容易忽略的点要先说明白。ProductCode用NVARCHAR(32)不用INT因为条码可能是EAN-13也可能混合字母NUMBER型存不了。数量和库存都用INT仓库条码系统里的数量一般不涉及小数用INT不容易出精度问题——见过有人用FLOAT存数量的盘点对账对不上查了三天发现是浮点误差这属于血泪经验。OperateTime不靠C#端传建表时用DEFAULT GETDATE()让数据库自己写避免客户端时钟不准导致流水时间错乱。如果学校机房没装SQL Server改成MySQL也不难把NVARCHAR换成VARCHARGETDATE()换成NOW()IDENTITY换成AUTO_INCREMENT表结构设计完全不用动。2.3 数据访问层为什么用ADO.NET而不是Entity Framework数据访问技术选型是论文里绕不开的一节。很多教程上来就推Entity Framework但面对这张只有四张表、最高并发也就十几个界面的系统EF的“少写SQL”优势没多大代价却是要理解DbContext生命周期、延迟加载、迁移这些概念出问题时你看到的是表达式树生成的SQL排查难度反而更高。我的建议是ADO.NET加一个静态公共类。SQL完全受控性能足够论文里还能写SQL参数化、连接池、事务处理这些实打实的点。核心代码就两个方法using System.Data; using System.Data.SqlClient; public static class DataAccess { /// summary /// 执行查询返回DataTable方便绑定DataGridView /// /summary public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(AppConfig.ConnectionString)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } } /// summary /// 执行增删改返回受影响行数 /// /summary public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(AppConfig.ConnectionString)) { conn.Open(); using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } return cmd.ExecuteNonQuery(); } } } }注意几个细节。第一using包住SqlConnection和SqlCommand方法执行完连接自动释放不要等GC去回收否则连接池会被占满WinForms频繁开关窗体时特别容易报“连接超时”。第二参数一律走SqlParameter禁止字符串拼接SQL这是防注入的底线。第三ExecuteQuery返回DataTable直接赋值给dgv.DataSource就能显示不用逐列手动塞值。第四AppConfig.ConnectionString是从配置文件读的这一点后面避坑章节会专门讲。对比这两种方案的取舍可以在论文里放一张表维度ADO.NETEntity Framework学习曲线低会SQL就够中等要理解映射和生命周期性能高SQL完全受控略低有表达式树开销调试直接看写的SQL还得看生成的SQL论文可写性写SQL优化和事务控制写ORM映射和迁移与本项目匹配度四张表场景非常合适有点大材小用3. 出入库与库存核心流程从扫码事件到事务写入的完整链路3.1 入库流程扫码输入、商品回填、明细累积入库操作的第一步是扫描条码。这里有个很多新手不知道的事实——市面上绝大多数USB扫描枪工作在“键盘模拟”模式它把条码内容当作键盘输入发送给当前焦点控件再自动补一个回车。所以程序里根本不用读串口、不用装厂商SDK只要在条码文本框的KeyDown事件里捕获回车就能拿到完整条码。下面是入库窗体最核心的一段逻辑private void txtBarcode_KeyDown(object sender, KeyEventArgs e) { // 扫描枪模拟键盘扫完自动敲回车这里捕获的就是整条条码 if (e.KeyCode Keys.Enter) { string code txtBarcode.Text.Trim(); DataTable dt DataAccess.ExecuteQuery( SELECT ProductCode, ProductName, Spec FROM ProductInfo WHERE ProductCode code, new SqlParameter(code, code)); if (dt.Rows.Count 0) { DataRow row dt.Rows[0]; txtProductName.Text row[ProductName].ToString(); txtSpec.Text row[Spec].ToString(); txtQuantity.Focus(); // 焦点移到数量框人工输入本次数量 AddDetailToList(code, 1); // 先按1条加入明细数量可改 } else { MessageBox.Show(条码不存在请先到商品管理界面建档, 提示, MessageBoxButtons.OK, MessageBoxIcon.Warning); } txtBarcode.Clear(); txtBarcode.Focus(); // 清空后继续扫下一条 e.Handled true; // 吞掉回车避免触发窗体默认按钮 } }这段代码有几个点必须说明。e.Handled true这行非常关键如果不写回车事件还会继续触发窗体的AcceptButton可能出现扫一次条码却弹出两个商品信息的现象。条码用code参数接住而不是直接拼字符串防止用户扫到带特殊字符的条码把SQL弄坏。AddDetailToList是我这边简写的一个方法内部是把商品编码和数量加到DataGridView里一行扫多件商品时明细逐行累积最后一次性点“确认入库”写库。确认入库的按钮事件里用一个事务包住“写流水”和“改库存”两步using (SqlConnection conn new SqlConnection(AppConfig.ConnectionString)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { foreach (DataGridViewRow row in dgvDetail.Rows) { string code row.Cells[colCode].Value.ToString(); int qty Convert.ToInt32(row.Cells[colQty].Value); SqlCommand cmd new SqlCommand( INSERT INTO InStockRecord (ProductCode, Quantity, Operator, OperateTime) VALUES (code, qty, operator, GETDATE()); UPDATE StockInfo SET StockQuantity StockQuantity qty, LastInTime GETDATE() WHERE ProductCode code;, conn, tran); cmd.Parameters.AddWithValue(code, code); cmd.Parameters.AddWithValue(qty, qty); cmd.Parameters.AddWithValue(operator, currentUserName); cmd.ExecuteNonQuery(); } tran.Commit(); MessageBox.Show($入库完成共 {dgvDetail.Rows.Count} 条明细); dgvDetail.Rows.Clear(); } catch (Exception ex) { tran.Rollback(); MessageBox.Show(入库失败事务已回滚 ex.Message); } }事务在这里的意义要能讲清楚流水表写成功但库存表没更新或者反过来都会造成账实不符。事务保证这两条SQL要么都成功要么都失败。Rollback之后界面上的明细还在可以修正数量重新提交。参数里operator用当前登录用户名这是追溯问题的关键字段别省。3.2 出库流程库存扣减、流水记录与余额校验出库与入库结构对称但多一个关键动作——出库前必须查库存余额否则库存会被扣成负数这在仓库系统里是事故级别的bug。下面这段代码用单个事务包住整个出库循环任何一个商品库存不足就全部回滚private void btnConfirmOut_Click(object sender, EventArgs e) { using (SqlConnection conn new SqlConnection(AppConfig.ConnectionString)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { foreach (DataGridViewRow row in dgvOutDetail.Rows) { string code row.Cells[colCode].Value.ToString(); int qty Convert.ToInt32(row.Cells[colQty].Value); // 出库前余额校验 SqlCommand checkCmd new SqlCommand( SELECT StockQuantity FROM StockInfo WHERE ProductCode code, conn, tran); checkCmd.Parameters.AddWithValue(code, code); object result checkCmd.ExecuteScalar(); if (result null || Convert.ToInt32(result) qty) { throw new Exception($商品 {code} 库存不足); } // 写流水 扣库存 SqlCommand cmd new SqlCommand( INSERT INTO OutStockRecord (ProductCode, Quantity, Operator, OperateTime) VALUES (code, qty, operator, GETDATE()); UPDATE StockInfo SET StockQuantity StockQuantity - qty, LastOutTime GETDATE() WHERE ProductCode code;, conn, tran); cmd.Parameters.AddWithValue(code, code); cmd.Parameters.AddWithValue(qty, qty); cmd.Parameters.AddWithValue(operator, currentUserName); cmd.ExecuteNonQuery(); } tran.Commit(); MessageBox.Show(出库成功); LoadStockList(); // 刷新库存列表 } catch (Exception ex) { tran.Rollback(); MessageBox.Show(出库失败事务已回滚 ex.Message); } } }这种“整体事务”的含义是一次出库单里如果有三种商品第二种库存不够第一种的流水和扣减也要一起回滚保证单据完整性。如果用逐行独立事务就会出现一张单子出库一半的奇怪状态。两种方案没有绝对优劣取决于业务要求但答辩时老师大概率会问“为什么选整体事务”你要能说出“出库单是业务上的最小单位不能拆”这个理由。3.3 库存视图与预警条件格式让问题商品一眼可见库存窗体对应的kucun.Designer.cs里最容易被忽略的是DataGridView的CellFormatting事件。很多人会用Timer定时刷新表格来实现“预警”这是误区——频繁刷新会闪烁还白白占用CPU。正确做法是让DataGridView在绘制单元格时按库存量和预警下限做颜色区分private void dgvStock_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex 0) return; if (e.ColumnIndex dgvStock.Columns[colStockQty].Index) { int qty Convert.ToInt32(dgvStock.Rows[e.RowIndex].Cells[colStockQty].Value); int lower Convert.ToInt32(dgvStock.Rows[e.RowIndex].Cells[colLowerLimit].Value); if (qty lower) { // 库存低于下限整行刷成浅红色 dgvStock.Rows[e.RowIndex].DefaultCellStyle.BackColor Color.LightCoral; } else { dgvStock.Rows[e.RowIndex].DefaultCellStyle.BackColor Color.White; } } }把这个事件挂在DataGridView的CellFormatting上之后每次刷新数据源或滚动时自动触发不需要额外刷新机制。颜色别用大红用LightCoral这类不刺眼的颜色打印的时候也不会黑成一团。预警逻辑还能继续扩展低于下限的单元格加粗或者在下限列后补一列“状态”显示“缺货/正常”这些改起来都很快也能丰富论文截图。4. 条码生成与报表模块ZXing.NET 和 Crystal Reports 落地参数4.1 用ZXing.NET生成EAN-13条码格式、尺寸与校验位系统里要给每个商品打印条码用的是ZXing.NET第三方库NuGet包名是ZXing.Net。生成条码的核心代码很短using ZXing; using ZXing.Common; using System.Drawing; public Bitmap GenerateEan13(string productCode) { var writer new BarcodeWriter { Format BarcodeFormat.EAN_13, Options new EncodingOptions { Width 300, Height 120, Margin 2, // 条码与图片边界的空隙单位像素 PureBarcode false // 设为false时保留条码下方数字方便人工核对 } }; return writer.Write(productCode); }Format指定EAN_13。EAN-13是13位数字前12位是数据位最后1位校验位由ZXing自动计算。所以如果你的界面输入的是12位商品编码生成出来就是13位条码如果库里存了完整13位直接传进去就可以。不建议用Code39或Code128来存纯数字EAN-13识别率更稳定写进论文也更规范。两个参数要特别留意。Width300、Height120打印出来约三厘米宽是折中的稳妥尺寸Margin至少留2像素设为0会导致条纹与边缘粘连很多扫码枪会报“条码过密无法识别”。生成后保存为图片bitmap.Save(barcode_ productCode .png, ImageFormat.Png)然后塞进PictureBox预览。如果后续商品编码混入了字母比如带SKU前缀EAN-13会抛异常这时把Format换成BarcodeFormat.CODE_128即可其余代码不用改。4.2 条码解析从位图到商品信息扫码枪已经解决了“扫现实条码”的问题但用户上传一张条码图片让系统识别就要用BarcodeReader在程序内部解码var reader new BarcodeReader(); var result reader.Decode(bitmap); if (result ! null) { string code result.Text; DataTable dt DataAccess.ExecuteQuery( SELECT * FROM ProductInfo WHERE ProductCode code, new SqlParameter(code, code)); // 查到的商品信息回填界面 } else { MessageBox.Show(未识别到条码请检查图片清晰度); }常见的失败原因是图片中条码区域太暗、太小或旋转了。ZXing对旋转条码的容忍度一般拍歪了基本解不出来。实际项目里可以在Decode之前先对图片做灰度化和放大处理识别率会明显提高public static Bitmap PreprocessForBarcode(Bitmap src) { // 先放大两倍让细条纹变宽 Bitmap gray new Bitmap(src.Width * 2, src.Height * 2); using (Graphics g Graphics.FromImage(gray)) { g.DrawImage(src, 0, 0, src.Width * 2, src.Height * 2); } // 再转灰度 for (int x 0; x gray.Width; x) { for (int y 0; y gray.Height; y) { Color c gray.GetPixel(x, y); int v (c.R * 299 c.G * 587 c.B * 114) / 1000; gray.SetPixel(x, y, Color.FromArgb(v, v, v)); } } return gray; }这段预处理用GetPixel/SetPixel逐像素操作速度不快但条码图片一般只有几百像素跑一次不到一秒作为演示足够。如果遇到复杂背景的图片还可以先做二值化这里不展开用到时再加。4.3 Crystal Reports报表传参让库存报表能按条件筛选报表模块一般用的是Crystal Reports。新建一个.rpt文件拖入数据库字段关键点是参数传递ReportDocument report new ReportDocument(); report.Load(Reports\StockReport.rpt); report.SetParameterValue(LowerLimit, 10); crystalReportViewer1.ReportSource report; crystalReportViewer1.Refresh();SetParameterValue的第一个参数必须和报表设计器里定义的参数名完全一致大小写也要一致。很多人报表不显示数据排查半天最后发现是参数名多了一个空格——这个坑我踩过不止一次。报表加载路径建议用Application.StartupPath拼接别写相对路径就以为万事大吉发布后工作目录变了就会找不到文件。另一个高频坑是报表连不上数据库。Crystal Reports的.rpt文件内部独立保存了一套数据库连接信息它跟C#代码里的连接字符串是两套体系所以每次加载报表都要重设一次登录report.SetDatabaseLogon(sa, your_password, localhost, WarehouseDB);这行代码必须出现在每个报表加载的地方否则即使App.config里连接串是对的报表照样弹登录框。做毕业设计时建议把所有报表加载逻辑封装成一个公共方法输入参数是rpt文件路径和报表参数列表这样代码干净答辩时也好讲解。5. 避坑与常见问题这套毕业设计最容易翻车的五个细节5.1 连接字符串写死在代码里换台电脑就崩现象在自己电脑上跑得好好的把项目拷到实验室电脑上启动直接报连不SQL Server的错误或者登录界面提示“用户sa登录失败”。原因连接字符串里写死了本机SQL Server实例名和账号密码目标机器的实例名可能带版本后缀比如localhost\SQLEXPRESS密码也可能不一样。解决把连接字符串放进App.config代码统一从配置读取connectionStrings add nameWarehouseDB connectionStringData Sourcelocalhost\SQLEXPRESS;Initial CatalogWarehouseDB;User IDsa;Password123456; providerNameSystem.Data.SqlClient / /connectionStringsC#端读取用ConfigurationManager.ConnectionStrings[WarehouseDB].ConnectionString。换机器只改配置文件不用重新编译。注意使用前要在项目里引用System.Configuration程序集很多新手栽在这一步——代码看着没错编译报找不到ConfigurationManager。这条排在第一因为80%的仓库系统源码翻车都先翻在数据库连接上。5.2 出库不校验余额库存被扣成负数现象库存明明只剩5件连续出库几次库存表变成-10流水还记了三笔。原因出库逻辑只写了“插入流水 扣减库存”没有先查询当前库存做比较。库存扣减直接写负数账目全乱。解决出库事务开始前先执行SELECT StockQuantity FROM StockInfo WHERE ProductCodecode拿到当前值小于出库数量直接抛异常中断事务。上面第三章的代码已经演示了这个写法。另外建议在表上补一个约束做兜底ALTER TABLE StockInfo ADD CONSTRAINT CK_Stock_NonNegative CHECK (StockQuantity 0);这样即使代码漏查数据库层次也不会出现负数记录。代码和数据库双层校验的思路写进论文里是个加分项。5.3 条码打印出来扫不出生成参数没调对现象用ZXing生成条码打印出来怎么都扫不动但屏幕上显示挺正常。原因常见三类。一是用了EAN-13但编码不合法长度不对或混入字母二是Margin设成0条纹和打印纸边缘粘连三是低分辨率打印机把细条纹糊成一团。解决先确认编码是13位纯数字EAN-13只支持数字混入字母就换Code128。生成时Width至少按300像素设计Margin不要低于2。打印前先用扫码枪扫电脑屏幕上的条码图能扫说明数据没问题问题在打印机——把图片像素加宽到600×200重新打印一般能解决。如果还不行检查打印纸和打印机DPI设置至少300DPI才够清晰。5.4 Crystal Reports报表不显示数据路径失效或登录失败现象报表打开后一片空白或者弹数据库登录框密码输入正确也连不上。原因rpt文件里写死的数据库连接和主程序连接字符串不一致报表文件用了相对路径发布后Reports文件夹没跟着部署。解决加载报表前统一调用SetDatabaseLogon重设账密路径用Path.Combine(Application.StartupPath, Reports, StockReport.rpt)。发布时把.rpt文件的“复制到输出目录”设为“如果较新则复制”这样不会漏文件。调试阶段先确认报表在开发环境能显示再考虑发布问题。5.5 vshost.exe进程残留重新编译提示文件被占用现象文件清单里能看到WindowsFormsApplication1.vshost.exe.config调试运行后VS提示exe文件被占用无法重新生成。原因vshost.exe是Visual Studio的调试宿主进程它启动了你的WinForms程序。如果程序里写了循环查询没退出条件或者异步线程没回收主窗体关闭后进程仍然挂在后台占用着exe文件。解决在Program.cs里给主窗体挂FormClosing事件统一释放资源、停掉Timer、关闭后台线程。调试时如果遇到文件占用打开任务管理器找到对应进程手动结束。发布时记得用Release配置编译Release下不会产生vshost.exe。另外exe.config要跟着exe一起部署它存的就是App.config编译后的内容漏了它配置全丢。6. 从能跑通到能答辩加一个批量导入功能演示更稳答辩时最怕两个现场问题一是逐条录入商品太慢台下老师等得不耐烦二是库存表没几条记录界面空空不像个真系统。我的解决办法是给系统加一个批量导入模块用Excel整理商品数据一键导入到ProductInfo和StockInfo。做完这个功能答辩开场演示就是“导入200条商品入库100条出库30条看库存报表”全程不到一分钟效果比慢慢敲数据好得多。6.1 批量导入的思路与预览界面数据源是一个CSV或Excel文件每一行有商品编码、名称、规格、单位、预警下限、初始库存。用OpenFileDialog选文件读出来后先显示在DataGridView里让用户预览确认无误再写库。这一步的核心价值是给人一个后悔药——看清楚了再导导错了也有机会取消。6.2 用SqlBulkCopy把两千条数据两秒导完逐条INSERT性能太差2000条数据可能要等十几秒。C#里做批量导入的标准方案是SqlBulkCopyusing System.Data.SqlClient; public bool BulkImportProducts(DataTable table) { // table列顺序与目标表ProductInfo对齐列名通过映射指定 using (SqlConnection conn new SqlConnection(AppConfig.ConnectionString)) { conn.Open(); using (SqlBulkCopy bulk new SqlBulkCopy(conn)) { bulk.DestinationTableName ProductInfo; bulk.ColumnMappings.Add(ProductCode, ProductCode); bulk.ColumnMappings.Add(ProductName, ProductName); bulk.ColumnMappings.Add(Spec, Spec); bulk.ColumnMappings.Add(Unit, Unit); bulk.ColumnMappings.Add(LowerLimit, LowerLimit); bulk.BatchSize 500; // 每批500行 bulk.BulkCopyTimeout 30; // 超时30秒防止慢网络卡死 bulk.WriteToServer(table); } } return true; }SqlBulkCopy的核心是ColumnMappings它保证DataTable的列名和目标表列名一一对应哪怕DataTable列顺序和目标表不一致也能正确映射。BatchSize控制每批发送的行数太小浪费带宽太大会超时500是一个稳妥值。导完商品主表后记得再执行一条UPDATE语句把初始库存同步到StockInfo表不然库存表还是空的演示时会露馅。从那以后我每次做仓库类管理系统都会强制把“批量导入”这个功能放在核心流程之后、报表之前实现——它逼你把DataTable、SqlBulkCopy、异常处理、两表数据同步都走一遍远比只会单条增删改查更能撑起一场答辩。希望这份源码加这篇拆解能帮到你祝你项目顺利。本文还有配套的精品资源点击获取
返回列表