2026/10/27 11:00:17

VB6+SQL Server销售管理系统源码:开单、库存与报表全链路

VB6+SQL Server销售管理系统源码:开单、库存与报表全链路 简介一份基于 VB6.0 与 SQL Server 2000 开发的产品销售管理系统完整源代码定位为数据库实例学习与 VB 入门参考适合刚接触窗体编程、ADO 数据访问和 SQL Server 基础操作的开发者。压缩包内共 51 个文件约 1.37MB以 frm/frx 窗体及资源文件、bas 标准模块、cls 类模块、vbp 工程文件为核心并配有 ocx 控件、报表设计文件、SQL 脚本和数据库文件便于还原工程结构与理解各层调用关系。系统已实现产品信息录入、产品管理、查询打印、金额汇总及简单报表等模块登录界面与 SQL 目录中的建库脚本可帮助读者梳理从数据库连接到界面交互的完整流程。已有 441 人学习适合借此熟悉 VB 与 SQL Server 的协同开发、报表输出和基础控件使用作为课程设计或小型管理系统练手素材。1. 从手写销货单到系统开单VB6 销售管理系统要闭合的那条链路接手一套销售管理系统时最省事的做法不是从零拖窗体而是先找一套能把单据链路完整跑通的 VB 源码作底座再对着自己的业务改。这份 VB6 SQL Server 销售管理系统完整源代码解决的正是「数据库实例怎么和界面绑起来」这个最耗时的环节——建库脚本、ADO 连接、开单出库、按客户与商品汇总链路是闭合的。它适合三类人要交毕设或课设、需要一套能演示完整流程的学生想把手工台账搬上电脑的小型贸易或批发团队以及想拿一份 VB 源码-数据库实例作参照、快速搭出自己模块的开发者。下面按需求、设计、实现、验收的顺序拆开讲。2. 需求与技术选型VB6 SQL Server 这套组合该装下哪些单据「销售管理系统」这五个字很容易写成一堆功能罗列真正决定代码结构的是那几个必须有单据凭证的环节。客户要货、报价、开单、出库、收款、对账、看报表这七步里有四步会产生一张有编号的单据而单据一旦有编号就意味着主表加明细、意味着事务、意味着月底对不上账要能追。本章先把功能边界和技术栈定下来后面所有模块都是在这个边界里填肉。2.1 四类真实场景划出的功能边界小型贸易或批发团队的日常是这样的三到十个人一天几十张销货单客户是固定的一批老关系商品有几百个 SKU仓库可能只有一个也可能分两个。他们的痛点很具体——用表格台账时同一个客户的欠款要在三张表里手工核对退货之后库存数字对不上月底老板要一张「这个月哪个商品卖得最好」的排行得现做。这类场景对系统的要求是开单要快、库存要准、欠款要能一眼看到。至于多币种、多组织、审批流、移动端审批这个体量根本用不上硬塞进去只会让代码变复杂。毕设或课设场景的诉求又不同。评审看的不是并发量而是流程是否完整、数据是否一致、界面是否像个正经系统。登录要有角色区分开单要有明细行删除要有确认和回滚报表要能按条件筛选并打印。这套源码里这几样通常都齐拿来当骨架比自己从空工程开始省十天。开发者的诉求最简单要一份能对得上号的 VB 源码-数据库实例。VB6 连 SQL Server 的样板代码网上零散得多但完整的建库脚本、连接封装、事务写法、Recordset 绑定表格的写法凑在一起的不多。把这份源码当参照工程读一遍比自己摸索「为什么 Recordset 一关窗体就报错」快得多。按这几类场景倒推功能边界大致这么切必须有登录与角色权限、客户档案、商品档案、仓库档案、员工档案、销售开单多明细行、销售退货、收款登记与应收查询、实时库存查询、销售日报月报与客户/商品排行、数据备份。可以砍多币种与汇率、多组织架构、采购与销售联动、审批工作流、短信通知、与电商平台对接。看情况加打印模板自定义、赠品与折扣策略、批次与有效期管理。提示先把「必须有」这一列做完能跑通再去碰「看情况加」。我见过太多项目卡在打印模板上结果连开单保存都没跑通。2.2 技术选型VB6 管界面ADO 管管道SQL Server 管账本这个组合不是怀旧是在特定约束下的合理选择。VB6 的优势在桌面单据类系统上非常直接窗体拖拽、控件属性可视化、DataGrid 和 MSHFlexGrid 能直接绑记录集一个熟练的人一天能画出五六个界面。编译产物是一个 exe 加若干 dll/ocx拷到客户机上注册一下就能跑不需要装运行时框架也不需要配 Web 服务器。对于门店里还在用老机器的场景这一点比技术先进重要得多。ADO 是 VB6 访问 SQL Server 最省事的一条路。引用 Microsoft Active Directory Object 库之后Connection 管连接、Recordset 管结果集、Command 管带参数的语句三者配合能覆盖从简单查询到事务提交的全部需求。Recordset 还能直接赋给表格控件的 DataSource省掉手工填充行列的代码。SQL Server 承担的是账本角色事务保证一次开单要么全成要么全不成视图把汇总逻辑沉到数据库备份和还原有成熟工具账号权限能细到表和存储过程。这也是这套系统不选 Access 的根本原因——Access 是文件级锁两三个人同时开单就开始互相等一旦网络抖动或断电mdb 文件损坏的概率高得离谱修一次能让人掉半条命。代价也要说清楚。VB6 内部是 ANSI 字符串中文走 GBK一旦数据库里用了 nvarchar 而代码里传的是普通 String某些字符就会变成问号VB6 没有现代 ORMSQL 得手写部署时要注册 OCX缺一个控件整个界面打不开。这些都是后面第五章要重点处理的问题。注意连接方式我一般用 ADO 的客户端游标加断开式记录集窗体拿到数据后立刻断开避免用户开着界面去泡茶的时候数据库连接一直挂着。2.3 数据表主表 明细 库存 收款这四件套表结构决定了系统能问出什么问题。销售业务的核心是四组表基础资料、单据主从、库存、资金。下面这张表是这套源码里最关键的几张字段名按常见命名习惯给出实际改造时按自己的规范统一前缀即可。表名作用关键字段与其他表的关系Customer客户档案CustomerID, Name, Phone, CreditLimit被 SaleMaster 引用Product商品档案ProductID, Name, Unit, SalePrice被 SaleDetail、Stock 引用Warehouse仓库档案WarehouseID, Name被 Stock 引用SaleMaster销货单主表BillNo, BillDate, CustomerID, TotalAmt, Status一对多关联 SaleDetailSaleDetail销货单明细BillNo, ProductID, Qty, Price, Amount外键回主表Stock库存结存WarehouseID, ProductID, Qty联合主键Receipt收款登记ReceiptNo, CustomerID, Amt, PayDate关联客户SysUser用户与角色UserID, Pwd, RoleID控制菜单与按钮建表时有三个地方值得多花五分钟想清楚。第一金额字段一律用decimal(18,2)不要用float或money的近亲浮点数存金额到最后对账差几分钱是必然的。第二主从表关联用单据号而不是自增 ID因为单据号本身就是业务主键退货、红冲都靠它找原始单。第三库存表用仓库加商品的联合主键这样UPDATE ... WHERE能直接命中索引扣库存不会全表扫。-- 销货单主表单据号做业务主键金额字段一律 decimal CREATE TABLE SaleMaster ( BillNo VARCHAR(20) NOT NULL PRIMARY KEY, -- 形如 XS202405120001 BillDate DATETIME NOT NULL, CustomerID VARCHAR(20) NOT NULL, TotalAmt DECIMAL(18,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0, -- 0 未审核 1 已审核 2 已作废 Operator VARCHAR(20) NULL, Remark VARCHAR(200) NULL ); CREATE INDEX IX_SaleMaster_Date ON SaleMaster(BillDate); CREATE INDEX IX_SaleMaster_Cust ON SaleMaster(CustomerID, BillDate); -- 库存结存联合主键让扣减语句能走索引 CREATE TABLE Stock ( WarehouseID VARCHAR(10) NOT NULL, ProductID VARCHAR(20) NOT NULL, Qty DECIMAL(18,3) NOT NULL DEFAULT 0, CONSTRAINT PK_Stock PRIMARY KEY (WarehouseID, ProductID) );IX_SaleMaster_Date和IX_SaleMaster_Cust这两个索引不是可有可无的装饰。销售报表几乎全是「按日期范围 按客户」两个条件过滤没有索引的时候数据量上到几万行一张月报要等十几秒评审现场点一下就卡住很难看。索引建在 BillDate 单列和 CustomerIDBillDate 组合上覆盖了绝大多数查询。至于商品表的SalePrice我一般建议再加一张价格策略表而不是直接改商品表里的价格——历史单据必须能还原当时的成交价直接改商品主档会让三年前的报表金额跟着变。3. 总体架构设计三层结构与六个模块的职责划分VB6 项目最容易烂掉的地方不是功能写不出来而是 SQL 语句散落在几十个窗体的事件过程里。今天在 Command1_Click 里拼一句 SELECT明天在 Form_Load 里拼一句 UPDATE三个月后要改一个字段名得用全局搜索一句一句核对。所以这套系统的架构目标只有一个让 SQL 只出现在两个地方其他地方想改数据只能调方法。3.1 分层窗体只管取值SQL 只准出现在两个地方界面层frm 系列只做三件事把控件上的值收集成变量、做最基础的格式校验必填、数字、日期范围、把业务层返回的结果显示出来或者弹提示。它不打开连接不拼 SQL不算金额。业务层cls 系列比如 clsSaleBill、clsStock负责业务规则开单时检查客户信用额度、检查库存够不够、算明细行金额和合计、生成单据号、组织事务。它调数据访问层不直接碰 ADO 对象。数据访问层clsDB只有一个类管连接字符串、管打开关闭、管执行命令、管事务的 Begin/Commit/Rollback、管错误信息往外抛。所有 SELECT、INSERT、UPDATE、DELETE 都从它的方法里发出去。例外情况是报表模块。复杂的多维汇总用 VB 代码拼会很痛苦通常的做法是直接建视图报表窗体绑定视图查询结果这时候 SQL 出现在数据库端的视图定义里属于「第二个地方」。这样划分之后改表结构只需要动 clsDB 的调用点和视图窗体的代码基本不用碰。3.2 模块清单与调用关系按菜单结构分这套系统大致有六个模块每个模块对应一组窗体和一两个业务类。模块主要窗体业务类依赖的数据表系统管理frmLogin、frmUser、frmChangePwdclsUserSysUser、SysRole基础资料frmCustomer、frmProduct、frmWarehouseclsBaseDataCustomer、Product、Warehouse销售开单frmSaleBill、frmSaleQueryclsSaleBillSaleMaster、SaleDetail退货与作废frmSaleReturnclsSaleReturnSaleMaster、SaleDetail、Stock收款与应收frmReceipt、frmArrearsclsReceiptReceipt、SaleMaster库存与报表frmStockQuery、frmReportSaleclsReportStock、视图调用方向是单向的窗体 → 业务类 → clsDB → 数据库。反向只有一种情况业务类把校验失败的原因以字符串形式返回给窗体窗体负责弹 MsgBox。不让业务类直接弹窗是因为一旦业务逻辑里混进界面调用这个类就没法在批量导入、定时任务里复用了。登录模块值得单独说一句。登录成功后不要只记一个布尔量要把 UserID、RoleID、真实姓名存到一个全局的模块级变量里比如标准模块 basGlobal后面所有单据的 Operator 字段、权限判断都从这里取。很多人图省事直接在窗体标题上写个名字结果开单记录的经办人永远是空。3.3 数据访问层一个 clsDB 把连接、事务、错误全兜住clsDB 是整套系统里最值得抄的一段代码。它把连接的打开关闭、命令的执行、事务的提交回滚、错误的捕获都包在一个类里业务层只需要调方法。 clsDB.cls —— 连接、执行、事务的统一入口 Option Explicit Private mConn As ADODB.Connection Private mInTrans As Boolean Public Sub Connect(ByVal sConnStr As String) Set mConn New ADODB.Connection mConn.ConnectionTimeout 15 连接超时网络差的时候别让界面假死 mConn.CommandTimeout 60 报表类查询留足时间 mConn.CursorLocation adUseClient 客户端游标支持断开式记录集 mConn.Open sConnStr End Sub 查询返回断开式记录集调用方用完即弃 Public Function Query(ByVal sSQL As String) As ADODB.Recordset Dim rs As New ADODB.Recordset rs.CursorLocation adUseClient rs.Open sSQL, mConn, adOpenStatic, adLockReadOnly Set rs.ActiveConnection Nothing 关键断开连接避免长时间占用 Set Query rs End Function 执行返回受影响行数用于判断 UPDATE 有没有真的打中数据 Public Function Execute(ByVal sSQL As String) As Long Dim lAffected As Long mConn.Execute sSQL, lAffected, adExecuteNoRecords Execute lAffected End Function Public Sub BeginTrans() mConn.BeginTrans mInTrans True End Sub Public Sub CommitTrans() If mInTrans Then mConn.CommitTrans mInTrans False End Sub Public Sub RollbackTrans() If mInTrans Then mConn.RollbackTrans mInTrans False End Sub Public Sub Disconnect() If Not mConn Is Nothing Then If mConn.State adStateOpen Then mConn.Close Set mConn Nothing End If End SubQuery里那句Set rs.ActiveConnection Nothing是整个类里最重要的一行。没有它窗体关掉之后记录集还挂着一个服务端游标SQL Server 那边的连接会一直保持在休眠状态人一多就把连接池占满。断开式记录集把数据一次性拉到客户端内存之后怎么排序、怎么过滤都不再走网络。Execute返回受影响行数这个设计是为了后面扣库存用的。判断「库存够不够」不能先 SELECT 再 UPDATE那样两个收银台同时开单必然超卖正确做法是把判断条件写进 WHERE看返回行数是不是 1。CursorLocation adUseClient在 Conn 和 Recordset 上各设了一次看着重复实际上 Recordsets 自己打开时如果不设会继承连接的设置但有些场景比如从 Command 执行返回的结果集不继承两边都写比较保险。提示连接字符串不要写在这个类里。放在同目录的一个加密配置文件或者 UDL 文件里clsDB 的 Connect 只接收参数。原因在第五章展开。4. 核心链路实现从单据号、事务扣库存到汇总报表前面把骨架搭好了这一章填的是最容易出问题的三个环节。它们共同的特点是单独看逻辑都简单一旦并发或者数据量上来就开始翻车。单据号重复、库存扣成负数、月报跑二十分钟这三件事我都在真实项目里见过。4.1 单据号不能靠 MAX1 裸奔单据号是销售系统的门面格式一般是「前缀 日期 流水」比如 XS202405120001。很多人第一版会写成SELECT MAX(BillNo) FROM SaleMaster WHERE BillNo LIKE XS20240512%然后加一。单机单人测试完全没问题两个人同时点保存就会拿到同一个号主键冲突报错算运气好运气不好的是其中一个静默失败单子没存进去但界面提示成功。正确的做法是把流水号单独放在一张编号表里用行锁保证同一时刻只有一个会话能拿到某个号。 生成下一个单据号在事务内锁住编号行取完立刻更新 Public Function NextBillNo(ByVal sType As String) As String Dim sDate As String, lSeq As Long, rs As ADODB.Recordset sDate Format$(Now, yyyymmdd) UPDLOCK ROWLOCK锁住这一行直到事务结束别的事务只能等 Set rs mDB.Query(SELECT CurSeq FROM BillSeq WITH (UPDLOCK, ROWLOCK) _ WHERE BillType sType AND BillDate sDate ) If rs.EOF Then lSeq 1 mDB.Execute INSERT INTO BillSeq(BillType, BillDate, CurSeq) VALUES( _ sType , sDate ,1) Else lSeq rs.Fields(CurSeq).Value 1 mDB.Execute UPDATE BillSeq SET CurSeq lSeq _ WHERE BillType sType AND BillDate sDate End If rs.Close NextBillNo sType sDate Format$(lSeq, 0000) End Function这段代码必须在调用方开启的事务里执行UPDLOCK的锁才会保持到提交那一刻。如果单独调用函数返回之后锁立刻释放第二个人照样能拿到同一个号。所以 clsSaleBill 里的顺序是BeginTrans→NextBillNo→ 写主表 → 写明细 → 扣库存 →CommitTrans中间任何一步失败就RollbackTrans。编号表还要有一个日常维护的小动作每年年初清一次历史数据或者干脆按 BillDate 分区保留最近两年否则这张表会随着天数线性增长虽然不至于慢但看着乱。4.2 事务开单、扣库存、写应收必须绑在一起一张销货单落库涉及三处写入主表一行、明细若干行、库存表若干行的扣减。这三处必须同生共死。断电、网络闪断、用户手快点了两下任何一种情况都会造成主表有单、明细缺失、库存没扣月底盘点就是一场灾难。事务的边界划在业务类的方法里不在窗体里。 clsSaleBill.SaveBill —— 一次开单的完整事务 Public Function SaveBill(oBill As SaleBillInfo, ByRef sErr As String) As Boolean Dim i As Long, lRows As Long, sSQL As String On Error GoTo EH mDB.BeginTrans 1. 取号并写主表 oBill.BillNo mDB.NextBillNo(XS) sSQL INSERT INTO SaleMaster(BillNo,BillDate,CustomerID,TotalAmt,Status,Operator) _ VALUES( oBill.BillNo , Format$(oBill.BillDate, yyyy-mm-dd) _ , oBill.CustomerID , oBill.TotalAmt ,0, gUserName ) mDB.Execute sSQL 2. 逐行写明细并扣库存 For i 0 To oBill.Items.Count - 1 With oBill.Items(i) sSQL INSERT INTO SaleDetail(BillNo,ProductID,Qty,Price,Amount) VALUES( _ oBill.BillNo , .ProductID , .Qty , .Price , .Amount ) mDB.Execute sSQL 条件写进 WHERE库存不足时受影响行数为 0直接判失败 sSQL UPDATE Stock SET QtyQty- .Qty _ WHERE WarehouseID oBill.WarehouseID AND ProductID .ProductID _ AND Qty .Qty lRows mDB.Execute(sSQL) If lRows 0 Then sErr 商品 .ProductName 库存不足单据未保存 mDB.RollbackTrans Exit Function End If End With Next i mDB.CommitTrans SaveBill True Exit Function EH: mDB.RollbackTrans sErr 保存失败 Err.Description End FunctionUPDATE ... WHERE Qty n这一句是整个方案里防超卖的核心。受影响行数为 0 就说明库存不够或者商品不存在立刻回滚。这比先SELECT Qty再判断再UPDATE少了一次往返而且天然并发安全——SQL Server 在执行 UPDATE 时会持有行锁第二个会话要么等要么看到已扣减后的值。要注意On Error GoTo EH之后必须回滚。VB6 里没被捕获的错误会直接跳出函数事务就悬在那儿直到连接断开才被动回滚。这种悬挂事务在测试阶段很难发现因为下一个人再开单还是能成功只是偶尔慢一下。注意明细行的金额由业务类算不要写成让数据库算。VB6 里用Currency类型做中间计算最后 ToString 到 SQL 时保留两位能避免大多数精度问题。4.3 报表用视图把汇总压力推给数据库报表是最容易被写成性能黑洞的地方。常见的错法是窗体里SELECT * FROM SaleDetail把几万行全拉到客户端然后在 VB 里用双层循环按客户和商品归类。数据量小的时候看不出问题上了十万行之后点一下「查询」界面直接卡死用户以为程序崩了。做法是把汇总沉到数据库视图里把常用的维度算好窗体只负责传日期参数。-- 按客户 商品双维度汇总的销售明细视图 CREATE VIEW v_SaleReport AS SELECT m.BillNo, m.BillDate, m.CustomerID, c.Name AS CustomerName, d.ProductID, p.Name AS ProductName, d.Qty, d.Price, d.Amount FROM SaleMaster m INNER JOIN SaleDetail d ON m.BillNo d.BillNo INNER JOIN Customer c ON m.CustomerID c.CustomerID INNER JOIN Product p ON d.ProductID p.ProductID WHERE m.Status 1; -- 只统计已审核单据作废单不进报表报表窗体拿到日期范围之后两条 SQL 就能覆盖大部分需求一条按客户分组给应收和销售总额一条按商品分组给销量排行。WHERE m.Status 1这个条件看着不起眼但它是很多报表数字对不上的根源——作废的单据如果算进去销售额会虚高而库存那边已经把货退回来了两边永远平不了。日期过滤的写法也有讲究。WHERE BillDate BETWEEN 2024-05-01 AND 2024-05-31会漏掉 5 月 31 日下午的单据因为 BETWEEN 对 datetime 是闭区间到 00:00:00。正确写法是BillDate 2024-05-01 AND BillDate 2024-06-01这个细节每年都有人栽。4.4 权限菜单、按钮、数据三层拦权限控制分三个层次少一层就有漏洞。菜单层决定用户能看到哪些功能按钮层决定能不能点保存和删除数据层决定他能看到哪些客户的单据。前两层靠 SysRole 表里的一组标志位登录时读出来存在全局变量里窗体加载时遍历菜单控件设置 Enabled 和 Visible。数据层通常按角色粗分普通业务员只能看自己开的单Operator gUserName主管能看全部。这个条件拼在查询 SQL 的 WHERE 里而不是查出来再在客户端过滤——后者一旦有人用工具直连数据库就形同虚设前者至少让 SQL Server 承担了过滤职责。界面上的灰色按钮不等于安全。VB6 的窗体在运行时是可以被工具改的真正拦住操作的应该是业务类里的校验删除单据时业务方法先查一遍当前用户的 RoleID 和单据状态不满足就返回失败。界面层的禁用只是为了让用户少走弯路。5. 避坑与排查VB6 配 SQL Server 最容易翻车的五处这套技术组合的问题很有规律基本上跑不出下面五类。每一条我都会按「看到什么现象、根本原因在哪、怎么改」写方便对照自己手上的工程。5.1 换了台电脑就连不上或者密码直接暴露在代码里现象在自己机器上跑得好好的 exe拷到同事电脑上启动就弹「未找到提供程序」或者连接超时另一种情况是打开工程一看连接字符串明晃晃写着uidsa;pwd123456。原因有两个。一是连接字符串硬编码在窗体或类的常量里换环境必须重新编译而编译的人往往忘了改。二是用了 sa 账号权限过大且密码明文任何拿到 exe 的人用记事本翻一遍就能找到。解决分三步把连接字符串挪到程序目录下的一个 UDL 文件或者自定义的加密 ini 里clsDB 的 Connect 只接收参数在 SQL Server 里建一个只有 db_datareader、db_datawriter 加存储过程执行权限的应用账号绝不用 sa打包时把配置文件和 exe 一起拷首次运行如果读不到配置就弹一个设置窗口让用户自己填服务器地址。这样交付到不同客户现场改配置不用重新编译。5.2 金额用 Double 存对账的时候差几分钱现象单张单据的金额看着没问题月报里把几百张单子加起来和财务手工算的总数差了 0.03 元怎么找都找不到是哪张单的问题。原因Double和Single是二进制浮点0.1 这个数在二进制里是无限循环小数存进去就有舍入误差单张单据误差小到看不见几百张累加就显形了。VB6 还有个Single更糟七位有效数字十万级的金额直接丢精度。解决数据库端金额字段全部改成DECIMAL(18,2)VB6 端参与金额运算的变量一律声明成Currency它内部是定点整数四位小数精度足够覆盖分。明细金额计算写成CCur(.Qty) * CCur(.Price)最后入库前用Format$(x, 0.00)转成字符串拼接进 SQL。已经在跑的库要迁移的话加一列新字段、跑一遍转换脚本、核对总数一致之后再改名别直接改类型。5.3 两个人同时开单库存被扣成负数现象库存表里某个商品出现 -3 的数量查单据却只有两张正常的销货单谁都不知道那三件货去哪了。原因扣库存的代码写成了先查后改——SELECT Qty拿到 5判断够然后UPDATE Stock SET Qty 5 - 3。两个会话同时查到 5各自判断够各自更新成 2实际卖了 6 件账面剩下 2差了三件。这是最典型的并发丢更新。解决把判断条件塞进 UPDATE 的 WHERE改成UPDATE Stock SET Qty Qty - 3 WHERE ... AND Qty 3然后看受影响行数是 0 就回滚。这个写法把「判断」和「扣减」合成一个原子操作SQL Server 的行锁保证同一时刻只有一个会话在改这一行。配合 4.2 里的事务从取号到扣库存整条链路要么全成要么全败库存对不上的概率降到接近零。5.4 中文变成问号或者按姓名排序的结果乱七八糟现象客户名称里带生僻字或者某些繁体字存进数据库变成「?」另一种是按客户名排序出来的顺序看着毫无规律。原因VB6 内部字符串是 ANSI简体环境下是 GBK而数据库建库时如果默认排序规则选成了 Latin1_General 或者字段用了varchar存 UTF-8 内容编码就对不上。排序乱则是排序规则不一致导致的客户端认为的「顺序」和服务器端的比较规则不同。解决建库时排序规则明确选Chinese_PRC_CI_AS存中文的字段用nvarcharVB6 侧通过 ADO 传参时注意 ADO 会做一次转换。如果一定要用 varchar 存中文确保整个实例、库、字段三层排序规则一致不要混搭。排序问题在查询里显式写ORDER BY CustomerName COLLATE Chinese_PRC_CI_AS更保险。5.5 打包发给别人界面一片空白或者缺控件现象exe 拷过去双击弹「找不到 msflxgrd.ocx」或者窗体控件位置全乱、字体变成宋体小五。原因VB6 生成的 exe 依赖一堆 OCX 和 DLL没有打包工具的话只会拷 exe目标机器缺控件就报错。控件位置乱则通常是目标机器的屏幕 DPI 或者系统字体与你开发机不同VB6 的窗体是按开发机字体度量保存的。解决用 VB6 自带的打包向导生成安装包它会自动带上依赖并注册控件或者手工维护一份依赖清单写一个 regsvr32 的批处理。窗体字体统一改成「宋体 9 号」这类通用组合Form 的ScaleMode用 Twip避免不同 DPI 下坐标漂移。交付前找一台干净的系统镜像完整装一遍这一步省不得。6. 验收与答辩用一张清单证明这套系统真的能跑功能演示容易验收难。评审老师或者甲方不会顺着你的顺序点他们随口一句「客户退货之后库存回来了吗」就得当场演示得出来。我一般会在验收前自己跑一遍下面这张清单每一项都要能在三分钟内演示完成。验收项判断标准怎么演示单据完整性主表有单时明细必不为空开一张三行的单查 SaleDetail 条数事务一致性库存不足时主表和明细都不留痕故意把库存改成 0 再开单查两张表单据号唯一连续快速点保存不产生重号双击保存按钮看 BillSeq 流水金额精度明细合计等于主表金额两位小数开一张带折扣的单手工核对报表口径作废单据不进销售统计作废一张单重跑月报对比数字权限隔离普通账号看不到别人的单切账号登录看列表筛选结果数据备份能还原到指定时间点备份、删一条数据、还原、查回来答辩被问到最多的问题其实是「如果两个人同时操作会怎样」。这时候不要嘴硬说没问题直接把 4.2 那段事务代码打开给对方看讲清楚UPDLOCK锁编号行、WHERE Qty n防超卖、出错回滚这三件事比任何口头保证都有说服力。第二个高频问题是「数据量大了怎么办」答案不是分库分表而是先把索引建对、把报表沉到视图、把 Recordsets 断开这三样做完十万级数据完全够用。还有一个技巧答辩前把数据库的日期字段全部改成过去的时间让报表默认查询区间有数据。我吃过一次亏演示环境是全新库报表打开全是空的评委会以为功能没做完。从那以后我每次交付前都强制走一遍「造一百条模拟单据、跑一遍全部报表、再清库」的流程形成习惯了比临场反应靠谱。这套源码的价值不在于它写得多漂亮而在于它把从建库到出报表的完整链路都摆在那儿每一处都可以拆开改。你要交作业就换换界面配色和报表样式要做项目就按自己的业务流程替换表结构和校验规则改到自己认得每一行代码为止。希望帮到你。本文还有配套的精品资源点击获取