简介:面向VB6初学者与数据库入门开发者的销售管理系统源码实例,围绕VB6.0与SQL Server 2000的联合开发展开,可用于学习窗体界面、数据库连接及销售业务数据处理的基本流程。压缩包共51个文件,约1.37MB,包含10个frm窗体与10个frx窗体资源、3个ocx控件及oca授权文件、3个bas标准模块、2个cls类模块,另有vbp工程、sql数据库脚本和db数据文件等,结构上覆盖界面、报表与业务逻辑等部分。已有442人学习,适合作为课程设计、毕业设计或自学练手参考。功能涉及产品信息录入、产品管理、查询打印、金额汇总和简单报表,可帮助读者理解VB与SQL Server之间的数据读写、窗体事件处理和基础报表输出思路,并借助随包SQL脚本还原运行环境、按模块拆解学习。
1. 一份能编译的 VB6 + SQL Server 销售管理系统源码,比十篇教程都值得拆
接手一套老系统做二次开发,最耗时间的不是写代码,而是猜上一任作者把单据号生成、库存扣减、权限判断分别藏在了哪个窗体里。这份 VB6 销售管理系统完整源代码带着 SQL Server 建库脚本一起给到,商品、客户、销售开单、退货、库存、统计报表这几条主线在工程里是通的,能编译、能连库、能出单。它适合三类人:要给小型客户交付进销存的外包开发者、在老旧系统上改需求的维护人员、以及想弄明白 ADO 事件驱动写法到底长什么样的新手。下面按“先跑起来、再看结构、最后改得动”的顺序拆。
2. 环境与连接:把 VB6、SQL Server 与 ADO 凑成能跑的第一版
2.1 三件套版本对齐:VB6 SP6、SQL Server 与 32 位运行时
这套工程的骨架非常传统:VB6 标准 EXE + ADO 数据访问 + SQL Server 关系库。它的优点是很轻,缺点也很直接——VB6 只能产出 32 位程序,所有和它打交道的组件都必须是 32 位,一旦有人在中途混进 64 位驱动,报错信息往往和真正的原因毫无关系。
IDE 侧建议装 VB6 SP6,装在独立机器或虚拟机里,不要和日常办公环境混用。VB6 的安装程序对新系统识别很差,安装过程中弹出的组件选择框全部保持默认即可,装完补一次 SP6 补丁,否则某些控件在保存窗体时会崩溃。
数据库侧从 SQL Server 2008 R2 到 2019 都能跑这套脚本,因为它只用到基础表、视图和存储过程。真正需要留意的是数据访问层选谁:
| 提供程序 | 连接串写法 | 适用场景 | 注意事项 |
|---|---|---|---|
| SQLOLEDB | Provider=SQLOLEDB.1 | 老机器、老系统迁移 | 随 MDAC 分发,不支持 TLS 1.2,服务端强制加密时握手失败 |
| MSOLEDBSQL | Provider=MSOLEDBSQL | 新版 SQL Server、需要加密 | 需单独安装驱动,工程里要重新引用 |
| MSDASQL + ODBC | Provider=MSDASQL.1 | 只能用 DSN 的老代码 | 32 位 DSN 要用 SysWOW64 下的 odbcad32 建 |
工程引用里只需要一个:Microsoft ActiveX Data Objects 2.8 Library。如果打开工程时看到 2.5 和 2.8 同时被勾选,直接取消 2.5,两个版本共存会让ADODB.Recordset在运行期变成“未知类型”,编译期不报错,一跑就崩。
2.2 建库:把 .sql 脚本跑成中文不乱码的表
脚本不要一次性全选执行。先翻到最前面看有没有CREATE DATABASE和USE,很多源码包的脚本默认库名是SalesDB,而机器上已经有一个同名库,直接跑会把老数据盖掉。
第一步,确认库的排序规则和字符类型:
-- 先单独执行,确认服务端排序规则 SELECT SERVERPROPERTY('Collation') AS ServerCollation; -- 库里如果已经有中文数据,排序规则不要随便改 CREATE DATABASE SalesDB COLLATE Chinese_PRC_CI_AS; GO USE SalesDB; GO第二步,按“基础资料表 → 单据头表 → 单据明细表 → 视图和存储过程”的顺序分批执行。带外键的明细表必须等主表建完,脚本里如果有GO,说明它是一个批次分隔符,SSMS 会分段提交,中途报错不会自动回滚前面的表。
第三步,把存中文的列统一成nvarchar。原脚本里常见的varchar(50)存客户名称,在Chinese_PRC_CI_AS下能存进去,但一旦换到别的排序规则或换台机器还原备份,就会出现问号。改法是在建表阶段就定死:
CREATE TABLE Customer( CustomerID int IDENTITY(1,1) NOT NULL PRIMARY KEY, CustName nvarchar(100) NOT NULL, LinkMan nvarchar(30) NULL, Tel varchar(30) NULL, Address nvarchar(200) NULL, IsActive bit NOT NULL DEFAULT 1 ); GOCustomerID用IDENTITY而不是手工取号,是因为主数据表并发低,让数据库自己发号最省事;单据号则完全相反,业务上要求“前缀 + 日期 + 流水”,必须自己控制,这个放到第 3 章讲。
建完之后跑一遍行数校验,确认脚本里的初始化数据真的进去了:
SELECT 'Customer' AS Tbl, COUNT(*) AS Cnt FROM Customer UNION ALL SELECT 'Product', COUNT(*) FROM Product UNION ALL SELECT 'SaleBill', COUNT(*) FROM SaleBill;2.3 连接串集中管理:一个模块管住所有窗体
源码包里最常见的问题是连接串被硬编码在十几个窗体的Form_Load里,密码还是sa。换台机器部署,得全文搜索改十几处,漏一处就报“登录失败”。我一般的做法是抽一个modConn.bas,把连接串和连接对象都收口:
' modConn.bas —— 连接串集中管理,换机器只改 config.ini 一处 Option Explicit Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpAppName As String, ByVal lpKeyName As String, ByVal lpDefault As String, _ ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long Public gConn As ADODB.Connection Public Function ConnString() As String Dim sServer As String, sDb As String, sUser As String, sPwd As String sServer = ReadIni("db", "server", "127.0.0.1,1433") sDb = ReadIni("db", "database", "SalesDB") sUser = ReadIni("db", "user", "salesapp") sPwd = ReadIni("db", "pwd", "") ' 端口直接写在 Data Source 里,避免命名实例解析不到 ConnString = "Provider=SQLOLEDB.1;Data Source=" & sServer & _ ";Initial Catalog=" & sDb & _ ";User ID=" & sUser & ";Password=" & sPwd & _ ";Persist Security Info=False;Connect Timeout=10" End Function Public Function DBOpen() As Boolean On Error GoTo EH If Not gConn Is Nothing Then If gConn.State = adStateOpen Then DBOpen = True: Exit Function End If Set gConn = New ADODB.Connection gConn.CursorLocation = adUseClient ' 客户端游标,界面拿到的记录集可断开使用 gConn.CommandTimeout = 30 gConn.Open ConnString() DBOpen = True Exit Function EH: MsgBox "连接数据库失败:" & Err.Description, vbCritical, "数据库" End Function Private Function ReadIni(ByVal Section As String, ByVal Key As String, ByVal DefVal As String) As String Dim buf As String, n As Long buf = String$(512, vbNullChar) n = GetPrivateProfileString(Section, Key, DefVal, buf, 512, App.Path & "\config.ini") ReadIni = Left$(buf, n) End Function几个参数值得单独说。CursorLocation = adUseClient决定记录集在客户端内存里维护,好处是窗体可以拿着断开后的Recordset慢慢翻页、重新排序,不占用服务端游标;代价是大结果集会吃内存,报表查询必须自己加TOP或者分页。Connect Timeout=10是连接阶段的超时,网络不通时能让用户十秒内看到提示而不是界面假死。Persist Security Info=False表示连接打开后不再把密码留在连接对象里,排查日志时不会把明文密码打出来。
config.ini放在程序目录下,内容就四行:
[db] server=192.168.1.10,1433 database=SalesDB user=salesapp pwd=ChangeMe_2024应用账号不要用sa。在 SSMS 里给它单独建登录,只授予db_datareader、db_datawriter和必要存储过程的EXECUTE,出问题时能一眼看出是权限问题还是代码问题,这比事后翻日志省事得多。
3. 销售单据主线:头表、明细表与库存扣减在代码里怎么落地
3.1 单据号生成:靠一张序列表和行锁,而不是取 MAX+1
销售单据号是这套系统里最容易翻车的地方。原代码里我见过两种写法:SELECT MAX(BillNo)+1和用时间戳拼随机数。前者在两个人同时开单时必然撞号,后者会在数据库里留下无法排序的脏数据,月底对账时对不上。
正确做法是把发号动作交给数据库,用一张序列表加UPDLOCK:
CREATE TABLE BillSeq( Prefix varchar(10) NOT NULL, BillDate char(8) NOT NULL, -- yyyymmdd Seq int NOT NULL, CONSTRAINT PK_BillSeq PRIMARY KEY(Prefix, BillDate) ); GO CREATE PROCEDURE usp_NextBillNo @Prefix varchar(10), @BillNo varchar(20) OUTPUT AS BEGIN SET NOCOUNT ON; DECLARE @d char(8) = CONVERT(char(8), GETDATE(), 112); UPDATE BillSeq WITH (UPDLOCK, ROWLOCK) SET @BillNo = @Prefix + @d + RIGHT('0000' + CAST(Seq AS varchar(4)), 4), Seq = Seq + 1 WHERE Prefix = @Prefix AND BillDate = @d; IF @@ROWCOUNT = 0 BEGIN BEGIN TRY INSERT INTO BillSeq(Prefix, BillDate, Seq) VALUES(@Prefix, @d, 1); SET @BillNo = @Prefix + @d + '0001'; END TRY BEGIN CATCH -- 两个会话同时插入时,让后到的那个重走一次 UPDATE EXEC usp_NextBillNo @Prefix, @BillNo OUTPUT; END CATCH END ENDUPDLOCK保证同一行的更新被串行化,ROWLOCK避免升级成表锁把整张序列表锁住。日期前缀每天换一次,Seq从 1 重来,业务上看起来干净。这里把SET NOCOUNT ON放在存储过程里,是为了减少网络往返的数据包,但要注意:调用方如果靠RecordsAffected判断成功,NOCOUNT ON会让它返回 -1,这个坑放到第 5 章细说。
3.2 主从表保存:一个事务里写完单据头和明细行
销售开单界面上是一张主表加一个网格,保存时必须一口气写完SaleBill和SaleDetail,中间任何一行失败都要整体回滚,否则会留下“有单头没明细”的废单,月底统计金额直接错。
' 保存一张销售单:头 + N 行明细,全程一个事务 Public Function SaveBill(ByVal BillNo As String, ByVal CustID As Long, _ ByVal dtBill As Date, ByRef Lines As Collection) As Boolean Dim cmd As ADODB.Command, vLine As Variant, i As Long Dim curTotal As Currency On Error GoTo EH gConn.BeginTrans ' 1) 先算总额,避免明细写完之后再回头 UPDATE 单头 For i = 1 To Lines.Count curTotal = curTotal + Lines(i)(2) * Lines(i)(3) ' Qty * Price Next i ' 2) 写单头 Set cmd = New ADODB.Command Set cmd.ActiveConnection = gConn cmd.CommandType = adCmdText cmd.CommandText = "INSERT INTO SaleBill(BillNo,BillDate,CustomerID,TotalAmount,Operator,Status) " & _ "VALUES(?,?,?,?,?,0)" cmd.Parameters.Append cmd.CreateParameter("BillNo", adVarWChar, adParamInput, 20, BillNo) cmd.Parameters.Append cmd.CreateParameter("BillDate", adDBTimeStamp, adParamInput, , dtBill) cmd.Parameters.Append cmd.CreateParameter("CustID", adInteger, adParamInput, , CustID) cmd.Parameters.Append cmd.CreateParameter("Total", adCurrency, adParamInput, , curTotal) cmd.Parameters.Append cmd.CreateParameter("Op", adVarWChar, adParamInput, 20, CurrUser) cmd.Execute , , adExecuteNoRecords ' 3) 逐行写明细,同时扣库存 For i = 1 To Lines.Count vLine = Lines(i) ' Array(ProductID, RowNo, Qty, Price) Set cmd = New ADODB.Command Set cmd.ActiveConnection = gConn cmd.CommandType = adCmdText cmd.CommandText = "INSERT INTO SaleDetail(BillNo,RowNo,ProductID,Qty,Price) VALUES(?,?,?,?,?)" cmd.Parameters.Append cmd.CreateParameter("b", adVarWChar, adParamInput, 20, BillNo) cmd.Parameters.Append cmd.CreateParameter("r", adInteger, adParamInput, , CLng(vLine(1))) cmd.Parameters.Append cmd.CreateParameter("p", adInteger, adParamInput, , CLng(vLine(0))) cmd.Parameters.Append cmd.CreateParameter("q", adDouble, adParamInput, , CDbl(vLine(2))) cmd.Parameters.Append cmd.CreateParameter("pr", adCurrency, adParamInput, , CCur(vLine(3))) cmd.Execute , , adExecuteNoRecords If Not CutStock(CLng(vLine(0)), CDbl(vLine(2))) Then Err.Raise vbObjectError + 1001, , "商品 " & vLine(0) & " 库存不足" End If Next i gConn.CommitTrans SaveBill = True Exit Function EH: On Error Resume Next gConn.RollbackTrans MsgBox "保存失败:" & Err.Description, vbCritical, "销售开单" SaveBill = False End Function这段代码里有三个刻意的设计。总额在写库之前就算好,避免“先插 0 再 UPDATE”的两次写;明细行号RowNo由界面维护而不是数据库自增,是因为用户会在网格里插行、删行,行号必须跟着界面视觉顺序走;库存扣减放在事务内,扣失败就用Err.Raise抛出去,让统一的错误分支回滚,而不是在每个调用点写一堆If。
Lines用Collection装数组,是 VB6 里最省事的容器写法,比定义自定义类型数组少写一堆ReDim Preserve。数组下标按ProductID, RowNo, Qty, Price固定顺序,读的时候必须和界面填充时严格一致,这类“隐式约定”在老代码里遍地都是,改的时候建议在注释里写清楚。
3.3 库存扣减:把“先查后改”换成带条件的 UPDATE
原代码里库存扣减常见这样写:先SELECT Qty FROM Stock WHERE ProductID=?,在 VB 里比较一下,再UPDATE Stock SET Qty=?。两个人同时卖最后一件商品,都能查到 1,都能通过判断,最后库存变成 -1 或者被覆盖。
干净的写法是把判断塞进WHERE:
' 原子扣减:库存够才扣,返回 True 表示本次确实扣掉了 1 行 Public Function CutStock(ByVal ProductID As Long, ByVal Qty As Double) As Boolean Dim cmd As ADODB.Command, n As Long Set cmd = New ADODB.Command Set cmd.ActiveConnection = gConn cmd.CommandType = adCmdText cmd.CommandText = "UPDATE Stock SET Qty = Qty - ? WHERE ProductID = ? AND Qty >= ?" cmd.Parameters.Append cmd.CreateParameter("q", adDouble, adParamInput, , Qty) cmd.Parameters.Append cmd.CreateParameter("p", adInteger, adParamInput, , ProductID) cmd.Parameters.Append cmd.CreateParameter("chk", adDouble, adParamInput, , Qty) cmd.Execute n, , adExecuteNoRecords CutStock = (n = 1) End Functioncmd.Execute n的第一个参数是受影响行数的出参,n = 1说明这一行确实被扣了,n = 0说明库存不够或者商品不存在。整个判断和更新由数据库一条语句完成,不存在中间被别人插队的窗口。
如果业务允许负库存(比如先发货后补货),那就把这句换成不带Qty >= ?的版本,但仍然要走UPDATE ... SET Qty = Qty - ?的增量写法,绝不可以在 VB 里先算好绝对值再写回。
3.4 查询列表:MSFlexGrid 手工填充比数据绑定更可控
这套界面用的是MSFlexGrid,不少人图省事接ADODC做数据绑定,结果一改列宽、一加隐藏列,绑定就乱。手工填充反而清楚:
' 把断开式记录集填进 MSFlexGrid,空值统一显示为空白 Public Sub FillGrid(rs As ADODB.Recordset, grd As MSFlexGrid) Dim r As Long, c As Long grd.Clear grd.Cols = rs.Fields.Count grd.Rows = 1 For c = 0 To rs.Fields.Count - 1 grd.TextMatrix(0, c) = rs.Fields(c).Name Next c Do Until rs.EOF grd.AddItem "" r = grd.Rows - 1 For c = 0 To rs.Fields.Count - 1 If IsNull(rs.Fields(c).Value) Then grd.TextMatrix(r, c) = "" Else grd.TextMatrix(r, c) = CStr(rs.Fields(c).Value) End If Next c rs.MoveNext Loop grd.ColWidth(0) = 0 ' 隐藏第一列,用来存主键 End Sub先设Cols再设Rows = 1,顺序反了会多出一行空白。空值必须单独判IsNull,CStr(Null)会直接抛“无效使用 Null”的错误,这是老代码里最高频的崩溃点之一。第一列藏起来存BillNo或ProductID,双击行的时候用grd.TextMatrix(grd.Row, 0)取主键,比在Tag里拼字符串可读得多。
4. 换掉字符串拼接:ADO 参数化、事务模板与存储过程调用
4.1 拼接 SQL 的两个真实代价
“"SELECT * FROM Customer WHERE CustName='" & txtName.Text & "'"”这种写法在老 VB 代码里几乎是标配,它的代价不只是安全。第一条代价是执行计划复用:每次文本不同,SQL Server 都要重新编译,几百个客户名查下来,计划缓存里全是只执行过一次的短命计划,内存被挤占,别人的查询跟着变慢。
第二条代价是格式解析。日期在拼接里以本地格式传过去,客户端是yyyy/m/d,服务端DATEFORMAT是mdy,2024/03/05就被解析成 3 月 5 日还是 5 月 3 日完全看运气。字符串里的单引号不转义就直接语法错误。参数化一次解决这三件事,顺带让代码可读性上一个台阶。
4.2 CreateParameter 的五个必填细节
用ADODB.Command传参时,CreateParameter的参数顺序是Name, Type, Direction, Size, Value,每一项写错都表现为不同的怪现象:
Dim cmd As ADODB.Command Set cmd = New ADODB.Command Set cmd.ActiveConnection = gConn ' 注意:adCmdText 下 SQLOLEDB 只认 ? 占位,不认 @名字 cmd.CommandType = adCmdText cmd.CommandText = "SELECT BillNo,BillDate,TotalAmount FROM SaleBill " & _ "WHERE CustomerID = ? AND BillDate >= ? AND BillDate < ? ORDER BY BillNo" cmd.Parameters.Append cmd.CreateParameter("CustID", adInteger, adParamInput, , lngCustID) cmd.Parameters.Append cmd.CreateParameter("D1", adDBTimeStamp, adParamInput, , dtFrom) cmd.Parameters.Append cmd.CreateParameter("D2", adDBTimeStamp, adParamInput, , DateAdd("d", 1, dtTo)) Dim rs As ADODB.Recordset Set rs = cmd.Execute第一,adVarWChar一定要给Size,比如 20 或 50;不给Size在某些驱动下会被当成varchar(1),查出来全是空。第二,文本类用adVarWChar而不是adVarChar,否则中文在varchar列上又是一场乱码纠纷。第三,金额用adCurrency,别用adDouble,0.1 + 0.2这类浮点误差累积到几百张单据上,报表和财务对不上。第四,adCmdText加SQLOLEDB时只能用?,占位符按添加顺序匹配;写了@CustID不会报错,但也不会被替换,最后变成语法错误。第五,日期用adDBTimeStamp传Date类型,让驱动自己处理格式,别在 VB 里Format成字符串再塞进 SQL。
4.3 事务模板:把 BeginTrans 和 Rollback 收在一个出口
VB6 没有try...finally,靠On Error GoTo跳转,很容易出现“开了事务,某条分支直接Exit Sub”的漏回滚。我一般把写操作全塞进一个模板函数里,业务逻辑用一个独立的过程实现,返回值表示成功与否:
' 统一事务模板:业务过程返回 False 就回滚 Public Function RunInTrans(ByVal ProcName As String, ParamArray Args() As Variant) As Boolean On Error GoTo EH gConn.BeginTrans If Not CallByName(Me, ProcName, VbMethod, Args) Then gConn.RollbackTrans Exit Function End If gConn.CommitTrans RunInTrans = True Exit Function EH: On Error Resume Next gConn.RollbackTrans MsgBox "操作失败,已回滚:" & Err.Description, vbExclamation, "提示" End FunctionCallByName在这里只是让调用点统一,实际项目里如果嫌它慢,直接写两个过程也可以:一个负责BeginTrans/Commit/Rollback,一个只负责业务 SQL。关键点是事务的开启和结束必须在同一个函数里成对出现,不要出现“界面层BeginTrans、数据层CommitTrans”这种跨层写法,那是维护期最难查的一类 bug。
还有一点:连接对象必须是全局同一个。VB6 里New ADODB.Connection每次都会开一个新连接,如果一个事务里用了两个连接对象,RollbackTrans只回滚其中一个,另一部分写操作已经落地了。
4.4 存储过程调用:命名参数与输出参数怎么拿
adCmdStoredProc和adCmdText在参数上的规则完全不同——前者支持@名字,由驱动转成 RPC 调用,性能也更好:
' 调用发号存储过程,取回输出参数 Public Function NextBillNo(ByVal Prefix As String) As String Dim cmd As ADODB.Command Set cmd = New ADODB.Command Set cmd.ActiveConnection = gConn cmd.CommandType = adCmdStoredProc cmd.CommandText = "usp_NextBillNo" cmd.Parameters.Append cmd.CreateParameter("@Prefix", adVarWChar, adParamInput, 10, Prefix) cmd.Parameters.Append cmd.CreateParameter("@BillNo", adVarWChar, adParamOutput, 20) cmd.Execute , , adExecuteNoRecords NextBillNo = cmd.Parameters("@BillNo").Value End Function输出参数的Size必须给足,@BillNo是 20 位就给 20,给小了返回值会被截断,还是静默截断、不报错。取值用cmd.Parameters("@BillNo").Value按名字取,比按下标稳,后面加参数不会把取值位置弄错。
另外,存储过程里如果写了SET NOCOUNT ON,调用方用cmd.Execute n拿到的n会是 -1。这时候判断成功与否只能靠业务字段(比如返回一个@Result int输出参数),不能靠受影响行数。
5. 避坑排查:这套源码在新机器上跑不起来的五个现场
5.1 一打开工程就弹“找不到工程或库”
现象:.vbp双击打开,编译菜单是灰的,引用列表里有一项前面打着“丢失”。
原因:工程引用的某个组件在机器上没注册,或者注册的是别的版本。老项目常见的是引用了开发机上的第三方控件,拷到新机器就找不到。
解决:先打开“工程 → 引用”,把打叉的项取消,重新勾选同名的可用版本;如果列表里确实没有,说明缺控件文件,得先把 OCX/DLL 拷到System32(32 位系统)或SysWOW64并注册。别急着全选取消,MSADO15.dll这类核心引用去掉之后,代码里所有ADODB.前缀都会报“用户定义类型未定义”。
5.2 编译能过,运行时提示 MSCOMCTL.OCX 或 MSFLXGRD.OCX 未注册
现象:开发机好好的,拷到客户机上双击就报“Component 'MSCOMCTL.OCX' or one of its dependencies not correctly registered”。
原因:VB6 的窗体控件是独立的 32 位 OCX,安装程序没带上,或者被 Office 的补丁覆盖成了不兼容的版本。
解决:用管理员身份开命令行,跑regsvr32 "C:\Windows\SysWOW64\MSCOMCTL.OCX"。注意路径必须是SysWOW64,因为 VB6 程序是 32 位,System32下放的是 64 位版本。如果注册报 0x80004005,先确认文件本身没被替换成精简版,把原版文件拷进去再注册一次。
5.3 连接串看着没错,就是连不上
现象:gConn.Open直接抛“SQL Server 不存在或访问被拒绝”,但用 SSMS 在同一台机器能连。
原因:三种可能——SQL Server 的 TCP/IP 协议没启用,只开了共享内存;连的是命名实例,端口是动态的;客户端和服务端不在同一网段,防火墙拦了 1433。
解决:在 SQL Server 配置管理器里把TCP/IP启用,IPAll的 TCP 端口固定成 1433,重启服务;连接串里写成Data Source=192.168.1.10,1433,用 IP 加端口,不要用机器名加实例名;防火墙放行入站 1433。命名实例如果一定要用名字连,得保证 SQL Server Browser 服务在跑。
5.4 查询条件里日期和中文一起出问题
现象:按日期段查,明明有单子却查不出来;按客户名模糊查,带中文的查不到、英文的能查到。
原因:日期是拼接进去的字符串,2024/03/05在服务端按mdy解析成 3 月 5 日还是 5 月 3 日取决于登录语言;中文则是varchar列存nvarchar常量时发生的隐式转换。
解决:日期一律走adDBTimeStamp参数,或者至少用yyyymmdd这种在任何DATEFORMAT下都无歧义的格式;中文列统一改nvarchar,查询参数用adVarWChar。改完之后记得在 SSMS 里用SET LANGUAGE切成不同语言各跑一遍,确认结果一致。
5.5 更新语句执行了,数据没变
现象:UPDATE没报错,界面刷新后数值还是旧的。
原因:第一种,存储过程里有SET NOCOUNT ON,Execute返回的受影响行数是 -1,代码里If n > 0 Then的判断永远不成立,走了“失败”分支但没提示;第二种,连接对象的CursorLocation是adUseClient,记录集是断开式的,改完rs.Update只改了内存,没回写数据库;第三种,另一个会话持着行锁,UPDATE在等锁,而CommandTimeout到了之后被静默取消。
解决:先看存储过程的NOCOUNT设置,判断成功改用业务返回值;断开式记录集批量回写要用UpdateBatch,并且必须重新打开连接;把CommandTimeout从 30 调到 60 观察是否变慢,同时用sp_who2看有没有阻塞源。
6. 改得动才算拿到手:加一个字段、打一次包
二次开发最常接到的需求是“销售单上要加一列备注”。在这套结构里,一个字段最少要动六处,漏一处就是各种“保存后不见了”。
| 改动位置 | 具体内容 | 漏改的表现 |
|---|---|---|
| 表结构 | ALTER TABLE SaleBill ADD Remark nvarchar(200) NULL | 保存报无效列名 |
| 单头保存语句 | INSERT 的列清单和?占位各加一个 | 备注永远为空 |
| 参数列表 | CreateParameter("Remark", adVarWChar, adParamInput, 200, ...) | 类型不匹配或截断 |
| 查询语句 | SELECT 列表加字段,FillGrid自动多一列 | 界面看不到 |
| 编辑回填 | txtRemark.Text = rs("Remark") & "" | 打开旧单备注被清空 |
| 打印/报表 | DataReport的DataMember和文本框绑定 | 打印出来是空白 |
ALTER TABLE加可空列在千万级以内的表上是秒级的,但如果表上有一堆索引,建议放在业务低峰做。加完列之后把SELECT *的写法全部改成显式列名——这套代码里SELECT *出现得非常多,多一个字段就会让FillGrid的列顺序和打印模板的字段序号全部错位,那是一种查起来非常痛苦的“玄学”错位。
打包交付时用 VB6 自带的打包向导,依赖列表里重点是MSCOMCTL.OCX、MSFLXGRD.OCX、MSADO15.DLL和msvbvm60.dll。前三者注册,最后一个不注册,只拷到目标目录即可。setup.lst里的版本号要手工核对一遍,如果客户机上已经有一个更高版本的同名 OCX,安装程序会跳过复制,结果就是“装完了还是报未注册”,这时候要么提升安装包的版本号,要么在安装脚本里强制覆盖后重注册。
血泪经验是:每次交付前,我一定在一台从没装过 VB6 的干净机器上完整走一遍“装库 → 装程序 → 开单 → 打印 → 卸载重装”。开发机上永远不会暴露的控件依赖、权限、区域设置问题,只有在那台干净机器上才会一起冒出来。从那以后我再也不敢只在自己机器上点两下就说“没问题”。
希望帮到你。
本文还有配套的精品资源,点击获取