VB6+SQL Server销售系统源码拆解与老系统维护避坑
简介这套基于 VB6.0 与 SQL Server 2000 的产品销售管理系统完整源代码面向希望熟悉桌面数据库应用开发流程的 VB 初学者与数据库实例学习者。项目围绕产品销售场景实现产品信息录入、产品管理、查询打印、金额汇总和简单报表等模块并在 SQL 目录提供数据库脚本便于理解窗体、报表与后端数据表之间的连接及增删改查操作。压缩包共 51 个文件约 1.37MB包含 frm/frx 窗体与资源文件、vbp 工程文件、bas/cls 代码模块、ocx 控件、sql/db 数据库文件以及 jpg 界面截图等目录中还可看到登录、主窗体、查询、库存管理等模块线索。已有 442 人学习适合作为课程设计、毕业设计参考或小型销售管理系统的入门范例读者可据此梳理 VB6 工程结构、SQL Server 建库建表思路与报表打印实现方式。1. VB6 SQL Server 销售系统源码一份能修改的老系统底稿接手一套运行多年的开单系统最麻烦的不是代码老旧而是只剩 exe、没有工程文件想加一列「客户等级」都无从下手。这份压缩包里给的是 VB6 完整工程.vbp工程文件、.frm窗体、.bas标准模块、.cls类模块再配一套 SQL Server 建库脚本业务上覆盖商品档案、客户档案、销售开单、订单明细、库存扣减、销售统计报表从录入到出单的整个流程是通的。源码不是教学 Demo从字段命名、单据状态、红冲退货这些细节能看出真实业务留下的痕迹。适合三类人要接手维护老系统的工程师、想搞清楚 VB6 配 ADO 究竟怎么写业务逻辑的人、需要把老库数据迁到新平台的人。也要说清楚不适合谁想拿它当现代 Web 项目脚手架、希望界面能直接适配触屏或移动端方向就错了。下面按「装起来、看懂表、跑通单据、避开常见问题」的顺序往下拆。2. 把工程跑起来VB6 IDE、ADO 与数据库实例的最小可用组合2.1 先让工程能编译不要一上手就重构很多人在拿到老系统源码后第一反应是「迁到 .NET 或 Java 重写」这个决定本身没错但顺序错了。重写的前提是你得先知道旧系统在算什么而业务口径往往藏在那些没人愿意看的窗体事件里。先把 VB6 工程跑起来、能连库、能开出第一张单你才有对照基线。数据访问层用 ADO 而不是 DAO 或 RDO这是常见做法也是这份源码的合理选择ADO 走 OLE DB连接字符串换一个 Provider 就能换数据库记录集支持断开连接adUseClient游标对当时常见的「窗体绑定表格、关闭连接后还要翻页」这种需求更友好。DAO 只对 Jet/Access 顺手RDO 更早就被淘汰两者在 SQL Server 上的参数化能力和错误信息都不如 ADO 清晰。另一个容易忽视的点是游标位置。老代码里若把CursorLocation设成adUseServer再配adOpenStatic在某些版本的 SQL Server 上会退化成只进游标RecordCount返回 -1界面上的「共 N 条」就永远是空的。统一改成客户端游标行为稳定得多代价是结果集会全量拉到内存报表类查询要自己加TOP或分页。2.2 环境与依赖清单组件常见版本作用注意点VB6 IDE6.0 SP6打开 .vbp、编译 EXE不打 SP6部分控件加载会直接报错ADO2.8随系统 MDAC连库与记录集操作新版 Windows 已内置不要额外装旧 MDAC 覆盖SQL Server2008 R2 及以上存业务数据新实例把数据库兼容级别调到 100 再跑脚本报表/表格控件DataReport、MSHFlexGrid打印与列表展示DataReport 依赖msderun.dll打包时要带上注册工具regsvr32注册 OCX/DLL必须用管理员权限运行装完 IDE 后先别急着按 F5。打开.vbp时如果弹出「不能加载某控件」记下控件名用regsvr32逐个注册再重新打开工程。这一步没有捷径缺少哪个就补哪个硬扛着用「引用丢失」的状态编译出来的 EXE 在客户机上照样起不来。2.3 连接字符串把可变部分集中到一个地方 modConfig.bas —— 连接配置集中管理不要散落在各窗体里 Option Explicit Public Const DB_SERVER As String 192.168.1.10 数据库实例地址 Public Const DB_NAME As String SalesDB 数据库名 Public Const DB_USER As String sa 登录名 Public Const DB_PWD As String ChangeMe 口令正式环境应外置 Public Const DB_TIMEOUT As Long 10 连接超时单位秒 Public Function BuildConnString() As String Dim s As String s ProviderSQLOLEDB.1; _ Data Source DB_SERVER ; _ Initial Catalog DB_NAME ; _ User ID DB_USER ; _ Password DB_PWD ; _ Connect Timeout CStr(DB_TIMEOUT) BuildConnString s End Function 启动时做一次连通性自检失败就明确报错别等到点保存才崩 Public Function TestConnection(ByRef errMsg As String) As Boolean Dim c As ADODB.Connection On Error GoTo EH Set c New ADODB.Connection c.ConnectionTimeout DB_TIMEOUT c.Open BuildConnString() c.Close TestConnection True Exit Function EH: errMsg 数据库连接失败 Err.Description TestConnection False End Function这段代码做三件事地址、库名、账号从常量里出连接串只在一个函数里拼启动时先测一次。参数上的关键点是Connect Timeout默认 15 秒网络不通时界面会僵住很久调成 10 秒以内体验更好。ProviderSQLOLEDB.1是老写法新环境也可换成MSOLEDBSQL但如果客户端机器没装对应驱动反而会报「未找到提供程序」老系统里保守用 SQLOLEDB 更省事。生产环境不要像示例这样把口令写死在代码里。这属于必须改的地方后面第 6 章会给出外置方案。2.4 建库与脚本执行顺序脚本一般不是一个文件常见顺序是建库 → 建表 → 建索引与约束 → 建视图 → 建存储过程 → 插入基础数据商品分类、计量单位、初始管理员。顺序错了会遇到外键找不到主表、存储过程编译时引用的表还不存在这类报错。# 用 sqlcmd 批量执行按文件名前缀排序保证顺序 for f in $(ls sql/0*.sql | sort); do echo running $f sqlcmd -S 192.168.1.10 -U sa -P ChangeMe -d SalesDB -b -i $f done-b参数很关键出错就返回非零退出码脚本能中断而不是带着错误继续往下跑。-d指定目标库避免脚本里忘了写USE。如果某个脚本里自带CREATE DATABASE第一次执行用-d master之后再切到业务库。跑完先做一次最小验证随便查一张基础表确认字符集没问题。老系统常用varchar存中文如果建库时排序规则选成了非中文的界面会显示问号或乱码这类问题越早发现越省事。3. 单据拆表主表、明细与库存流水的三件套设计3.1 五张表撑起一个销售闭环销售系统的表看着多骨架其实就五张客户、商品、订单主表、订单明细、库存流水。前两张是档案后三张是业务。理解它们的关系比记字段名重要得多。表作用关键字段索引建议Customer客户档案CustomerID、Name、Level、StatusName 建非唯一索引Product商品档案ProductID、Code、Price、StockQtyCode 建唯一索引SalesOrder订单主表OrderNo、CustomerID、TotalAmount、StatusOrderNo 唯一、OrderDate 普通SalesOrderDetail订单明细OrderID、ProductID、Qty、Price、AmountOrderID 聚集索引StockFlow库存流水ProductID、ChangeQty、SourceNo、FlowTypeProductID CreatedAt 联合索引明细表里存的不只是数量还有下单时的单价和金额。有人图省事只存 ProductID 和数量出报表时再去 join 商品表取现价结果商品调价后历史单据的金额全变了对账时怎么都说不清。单价必须在下单瞬间确定下来这是必须遵守的规则。库存流水则是另一层保险。商品表上的StockQty是当前余额流水表是变动记录两者互为校验。只留余额不留流水的系统一旦出现差异就无从追查等于把问题藏了起来。3.2 建表脚本金额字段为什么用 decimal(18,4)CREATE TABLE dbo.SalesOrder ( OrderID INT IDENTITY(1,1) NOT NULL PRIMARY KEY, OrderNo VARCHAR(20) NOT NULL, CustomerID INT NOT NULL, OrderDate DATETIME NOT NULL, TotalAmount DECIMAL(18,4) NOT NULL DEFAULT(0), DiscountRate DECIMAL(5,4) NOT NULL DEFAULT(1), Status TINYINT NOT NULL DEFAULT(0), Remark VARCHAR(200) NULL, CreatedBy VARCHAR(20) NOT NULL, CreatedAt DATETIME NOT NULL DEFAULT(GETDATE()) ); GO CREATE UNIQUE INDEX UX_SalesOrder_No ON dbo.SalesOrder(OrderNo); CREATE INDEX IX_SalesOrder_Date ON dbo.SalesOrder(OrderDate) INCLUDE(CustomerID, TotalAmount); CREATE TABLE dbo.SalesOrderDetail ( DetailID INT IDENTITY(1,1) NOT NULL PRIMARY KEY, OrderID INT NOT NULL, ProductID INT NOT NULL, Qty DECIMAL(18,4) NOT NULL, Price DECIMAL(18,4) NOT NULL, Amount DECIMAL(18,4) NOT NULL, CONSTRAINT FK_Detail_Order FOREIGN KEY (OrderID) REFERENCES dbo.SalesOrder(OrderID) ); GO CREATE INDEX IX_Detail_Order ON dbo.SalesOrderDetail(OrderID);金额用decimal(18,4)而不是float或money。float是二进制浮点0.1 0.2得不到0.3几百行明细累加后差几分钱财务一对账就要来问。money只有 4 位小数没问题但它在计算过程中会隐式转成浮点中间步骤仍可能引入误差。decimal是精确定点类型加法乘法都不丢精度代价只是运算稍慢销售系统的量级完全感觉不到。Status用TINYINT加注释而不是直接存中文字符串好处是索引小、比较快、改文案不用动数据。0 草稿、1 已确认、2 已出库、3 已作废状态流转只允许单向前进作废单独走一条路径不允许从作废回到已确认。3.3 业务单号别用自增主键当单号订单号一旦给客户看过、打印过、对过账就不能再变。自增主键做不到这点删一行或回滚一次号就断了客户看到「0001、0003」会追问。常见做法是单独建一张序列表按「单据类型 日期」分桶。CREATE TABLE dbo.BillSequence ( BillType VARCHAR(10) NOT NULL, BillDate CHAR(8) NOT NULL, -- yyyyMMdd CurrentNo INT NOT NULL, CONSTRAINT PK_BillSequence PRIMARY KEY (BillType, BillDate) ); CREATE PROCEDURE dbo.usp_NextOrderNo OrderNo VARCHAR(20) OUTPUT AS BEGIN SET NOCOUNT ON; DECLARE d CHAR(8) CONVERT(CHAR(8), GETDATE(), 112); UPDATE dbo.BillSequence WITH (UPDLOCK, ROWLOCK) SET CurrentNo CurrentNo 1 WHERE BillType SO AND BillDate d; IF ROWCOUNT 0 BEGIN INSERT INTO dbo.BillSequence(BillType, BillDate, CurrentNo) VALUES (SO, d, 1); SET OrderNo SO d 0001; END ELSE BEGIN DECLARE n INT (SELECT CurrentNo FROM dbo.BillSequence WHERE BillType SO AND BillDate d); SET OrderNo SO d RIGHT(0000 CAST(n AS VARCHAR(4)), 4); END END关键在UPDLOCK, ROWLOCK。两个人同时开单不加更新锁会读到同一个 CurrentNo两条记录撞唯一索引第二个用户直接报错。加上之后第二个事务会等第一个提交完再取号串行但只锁一行并发几十个开单员完全撑得住。取号必须在事务里调用和插单在同一个事务。取完号插入失败回滚号作废这属于可接受的损耗反过来先插单后取号一旦并发失败会留下没有单号的脏数据。3.4 库存扣减放哪一层三个位置可选VB 代码里算、触发器里算、存储过程里算。VB 里算的问题是所有扣减入口都得记得写一遍多一个退货入口就多一个漏掉的机会触发器看起来自动但它拿不到业务上下文出错时的错误信息对用户毫无意义排查也麻烦。主流做法是存储过程把「校验库存 → 扣减余额 → 写流水」封成一个原子操作VB 只负责传参和显示结果。CREATE PROCEDURE dbo.usp_DeductStock ProductID INT, Qty DECIMAL(18,4), SourceNo VARCHAR(20), Operator VARCHAR(20) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; IF Qty 0 BEGIN RAISERROR(扣减数量必须大于 0, 16, 1); RETURN; END UPDATE dbo.Product SET StockQty StockQty - Qty WHERE ProductID ProductID AND StockQty Qty; -- 把校验写进 WHERE天然防超卖 IF ROWCOUNT 0 BEGIN RAISERROR(库存不足或商品不存在, 16, 1); RETURN; END INSERT INTO dbo.StockFlow(ProductID, ChangeQty, FlowType, SourceNo, CreatedBy, CreatedAt) VALUES (ProductID, -Qty, SALE, SourceNo, Operator, GETDATE()); ENDSET XACT_ABORT ON让任何运行时错误都终止整个事务并回滚避免留下扣了一半的中间状态。把库存判断塞进UPDATE ... WHERE StockQty Qty而不是先 SELECT 再判断是因为前者在一条语句内完成读和写不会出现「查的时候还有 5 件、扣的时候已经被别人扣走」的窗口。这是最省事也最有效的防超卖写法。4. 事务落地订单保存、库存扣减与报表取数的核心代码4.1 把数据访问封装进类模块SQL 直接写进按钮的 Click 事件里是这类老系统最普遍的问题。改一个字段要全文搜索测试也没法单独测。哪怕不做完整的层次架构至少也要把连接和命令封装成一个类模块。 clsData.cls —— 一个连接 参数化执行窗体只调方法 Option Explicit Private mConn As ADODB.Connection Private mInTrans As Boolean Public Sub Init(ByVal connStr As String) Set mConn New ADODB.Connection mConn.CursorLocation adUseClient mConn.ConnectionTimeout 10 mConn.Open connStr End Sub Public Function Fill(ByVal sqlText As String, ParamArray ps() As Variant) As ADODB.Recordset Dim cmd As ADODB.Command, i As Long Set cmd New ADODB.Command Set cmd.ActiveConnection mConn cmd.CommandType adCmdText cmd.CommandText sqlText For i LBound(ps) To UBound(ps) cmd.Parameters.Append cmd.CreateParameter(p i, adVariant, adParamInput, , ps(i)) Next i Set Fill cmd.Execute End Function Public Function Exec(ByVal sqlText As String, ParamArray ps() As Variant) As Long Dim cmd As ADODB.Command, i As Long Set cmd New ADODB.Command Set cmd.ActiveConnection mConn cmd.CommandType adCmdText cmd.CommandText sqlText For i LBound(ps) To UBound(ps) cmd.Parameters.Append cmd.CreateParameter(p i, adVariant, adParamInput, , ps(i)) Next i cmd.Execute , , adExecuteNoRecords Exec cmd.Parameters.Count End Function Public Sub BeginT() If Not mInTrans Then mConn.BeginTrans: mInTrans True End Sub Public Sub CommitT() If mInTrans Then mConn.CommitTrans: mInTrans False End Sub Public Sub RollbackT() If mInTrans Then mConn.RollbackTrans: mInTrans False End Sub Public Sub CloseIt() If Not mConn Is Nothing Then If mConn.State adStateOpen Then mConn.Close Set mConn Nothing End If End Sub参数化不是可选项。拼接 SQL 字符串最直接的后果是单引号和--能把整条语句改掉商品名里带个「OBrien」都能让保存失败。ParamArray收可变参数调用侧写db.Fill SELECT * FROM Customer WHERE Name LIKE ?, % kw %就够了读起来和拼串差不多但安全性和类型处理完全不同。CursorLocation要统一在这个类里设不要在窗体里改。老代码常见做法是窗体里随手改游标类型最后谁也不知道某个RecordCount为什么是 -1。4.2 订单保存的完整事务流程一张单涉及四次写主表、明细多行、库存扣减、流水。任何一步失败都必须整体回滚否则会出现「单子是有的、库存没扣」这种最难查的账。Public Function SaveOrder(o As clsOrder, ByRef errMsg As String) As Boolean Dim db As clsData, rs As ADODB.Recordset Dim i As Long, orderNo As String, orderId As Long On Error GoTo EH Set db New clsData db.Init modConfig.BuildConnString() db.BeginT 1. 取单号内部带 UPDLOCK必须和插单同事务 db.Exec DECLARE no VARCHAR(20); EXEC dbo.usp_NextOrderNo no OUTPUT; Set rs db.Fill(SELECT dbo.fn_NextOrderNo() AS OrderNo) orderNo rs(OrderNo).Value rs.Close 2. 插主表用 SCOPE_IDENTITY 拿回自增主键 Set rs db.Fill( _ INSERT INTO SalesOrder(OrderNo,CustomerID,OrderDate,TotalAmount,DiscountRate,Status,CreatedBy) _ VALUES(?,?,?,?,?,0,?); SELECT SCOPE_IDENTITY() AS NewID;, _ orderNo, o.CustomerID, o.OrderDate, o.TotalAmount, o.DiscountRate, o.Operator) orderId CLng(rs(NewID).Value) rs.Close 3. 逐行插明细并扣库存 For i 0 To o.Lines.Count - 1 Dim L As clsOrderLine Set L o.Lines(i) db.Exec INSERT INTO SalesOrderDetail(OrderID,ProductID,Qty,Price,Amount) VALUES(?,?,?,?,?), _ orderId, L.ProductID, L.Qty, L.Price, L.Amount db.Exec EXEC dbo.usp_DeductStock ?,?,?,?, L.ProductID, L.Qty, orderNo, o.Operator Next i db.CommitT o.OrderNo orderNo SaveOrder True db.CloseIt Exit Function EH: On Error Resume Next db.RollbackT db.CloseIt errMsg 保存失败 Err.Description SaveOrder False End Function几个必须说清楚的点。第一SCOPE_IDENTITY()而不是IDENTITY后者在有触发器的表上会拿到触发器插入的其他表的自增值是个经典陷阱。第二取号和插单在同一个事务里中间不能被别的事打断。第三明细循环里的任何一次库存不足都会RaiseError抛出走EH分支整体回滚前面插的主表也一并撤销。第四错误处理里必须先RollbackT再CloseIt顺序反了会报「连接有未完成事务」。On Error Resume Next在回滚段是必要的因为回滚本身也可能失败连接已断不能让清理逻辑再抛一个新错误把原始错误盖掉。4.3 金额与折扣类型选错差的是钱类型精度适用风险Double二进制浮点科学计算累加误差对账差几分Currency定点4 位小数VB6 内部金额运算范围较小超大数据会溢出Decimal库内定点可指定落库与汇总VB6 侧需以字符串或 Variant 传参VB6 没有原生 Decimal 类型只有Variant的子类型vbDecimal从记录集读到decimal字段时表现出来就是它。常见做法是界面输入用Currency变量承接落库时通过参数化传Variant数据库侧统一decimal(18,4)绝不在 VB 里用Double算钱。折扣计算要注意顺序先算行金额再汇总再乘折扣还是先乘折扣再汇总结果在小数位上会不同。业务上通常约定「行金额四舍五入保留两位汇总后按折扣率乘再四舍五入到两位」。这套规则必须写死在代码里并写在单据打印的说明上否则财务和业务永远在对同一张单。4.4 报表取数绑定控件还是存储过程老系统里报表常见两条路。一条是 DataReport 直接绑一个ADODB.Recordset拖控件、设字段、跑起来就有适合字段固定的打印单据。另一条是存储过程出结果集再塞进 MSHFlexGrid 或导出 Excel适合条件多、列可变的统计表。CREATE PROCEDURE dbo.usp_RptSalesByProduct BeginDate DATETIME, EndDate DATETIME AS BEGIN SET NOCOUNT ON; SELECT p.Code, p.Name, SUM(d.Qty) AS TotalQty, SUM(d.Amount) AS TotalAmount, COUNT(DISTINCT o.OrderID) AS OrderCount FROM SalesOrder o JOIN SalesOrderDetail d ON d.OrderID o.OrderID JOIN Product p ON p.ProductID d.ProductID WHERE o.Status IN (1, 2) AND o.OrderDate BeginDate AND o.OrderDate EndDate GROUP BY p.Code, p.Name ORDER BY TotalAmount DESC; END日期条件用 开始加 结束次日而不是BETWEEN。BETWEEN 2024-01-01 AND 2024-01-31会把 31 号 00:00 之后的数据漏掉因为结束值被当成当天零点。这个坑每年都有人踩一次。Status IN (1,2)排除草稿和作废单这个过滤条件必须写在存储过程里而不是靠界面控制。报表口径统一在数据库侧换个人出报表也不会出现两个数字。5. 避坑清单老 VB6 系统上最容易翻车的五类问题5.1 现象新机器打开工程提示「部件未注册」或运行时错误 429现象开发机上一切正常拷到另一台机器双击 EXE 弹出「ActiveX 部件不能创建对象」编号 429或者在 IDE 里打开.vbp直接说某控件加载失败。原因工程引用了 OCX 或 DLL注册信息在注册表里拷贝文件本身不解决问题。更隐蔽的是 32 位控件被装到了 64 位系统的SysWOW64目录注册时却用了 64 位的regsvr32注册表项写错位置VB6 找不到。解决用 32 位的注册工具路径是C:\Windows\SysWOW64\regsvr32.exe把依赖的 OCX 一个个注册。实在搞不清依赖关系就在能跑的机器上用依赖查看工具扫一遍 EXE 的导入表把非系统 DLL 全部记下来打包时一起带上并写进安装脚本。# 32 位控件必须用 SysWOW64 下的注册器 C:\Windows\SysWOW64\regsvr32.exe /s msflxgrd.ocx C:\Windows\SysWOW64\regsvr32.exe /s msderun.dll5.2 现象连接字符串本机能跑换台机器报「未找到或无法访问服务器」现象开发机上Data Sourcelocalhost毫无问题部署到应用服务器连数据库服务器报错 17 或超时。原因多半不是账号密码错而是网络协议没通。SQL Server 默认可能只开了共享内存命名管道和 TCP/IP 是关的或者实例不是默认实例端口不固定客户端不知道去哪问。解决先用telnet 数据库IP 1433确认端口通不通不通就是防火墙或协议没开。命名实例的情况确认 SQL Browser 服务在跑否则客户端解析不到端口连接串里直接写IP,端口更省事。跨网段时把Network LibraryDBMSSOCN显式指定为 TCP能绕开客户端协议协商失败的问题。5.3 现象单据金额合计差几分钱明细加起来和主表对不上现象客户对账时发现单头总金额比明细合计多 0.01 元或者一批单据汇总后和财务系统差几毛。原因中间计算用了Double二进制浮点无法精确表示 0.1 这类十进制小数累加几十行后误差被放大或者中途四舍五入的位置不一致有的地方按分进位有的地方截断。解决容器类型统一改成Currency落库字段改成decimal(18,4)四舍五入统一用Round并明确进位规则只在最终展示和落库两处做中间过程保留全精度。改完之后拿一批历史单据重算一遍和实际数据比对这是唯一能确认改对了的办法。5.4 现象日期区间查询在部分机器上一条数据都查不到现象同一段代码、同一个库A 的电脑上查得到B 的电脑上查出来是空的条件放宽到全年也还是空。原因区域设置不同。2024-03-05这种带连字符的字符串在某些区域格式下会被解析成2024年3月5日在另一些格式下被当成2024年5月3日更糟的是当月份大于 12 时直接解析失败返回 NULL条件恒不成立静默返回空集。解决日期参数一律用ADODB的adDate类型参数传不要拼字符串。必须拼的时候统一用yyyymmdd八位无分隔格式SQL Server 对这种格式的解析与语言设置无关。界面上的日期输入控件也要限制格式不要让用户自由输入。5.5 现象多人同时开单库存出现负数或超卖现象库存 10 件两个开单员先后各卖 8 件两张单都保存成功库存变成 -6。原因库存判断在 VB 代码里先SELECT StockQty再比较然后才UPDATE。两步之间有时间窗两个会话都读到 10都认为够都往下扣。这不是并发压力大才会出现两个人手快一点就能撞上。解决把判断和更新合成一条语句用UPDATE ... WHERE StockQty Qty加ROWCOUNT判断或者用UPDLOCK显式锁行。改完后做一次并发验证开两个查询窗口各自开事务扣同一件商品观察第二个会话是不是被阻塞、最后是不是只有一个成功。这个验证花十分钟能省掉后面几个月的对账纠纷。6. 交接前的加固连接配置外置与一次可复现的验收源码能跑通只是第一步要交给别人维护还得做两件事把写死的配置挪出去把验收过程变成可重复执行的清单。配置外置最省事的做法是读一个同目录的 ini 或文本文件用 Windows 自带的 API 读不引入新依赖。 modConfig.bas改造版—— 从同目录 db.ini 读取代码里不再出现口令 Private Declare Function GetPrivateProfileString Lib kernel32 _ Alias GetPrivateProfileStringA (ByVal lpApp As String, ByVal lpKey As String, _ ByVal lpDef As String, ByVal lpRet As String, ByVal nSize As Long, _ ByVal lpFile As String) As Long Private Function ReadIni(ByVal section As String, ByVal key As String, ByVal def As String) As String Dim buf As String * 512, n As Long n GetPrivateProfileString(section, key, def, buf, 512, App.Path \db.ini) ReadIni Left$(buf, n) End Function Public Function BuildConnString() As String BuildConnString ProviderSQLOLEDB.1;Data Source ReadIni(db, server, 127.0.0.1) _ ;Initial Catalog ReadIni(db, name, SalesDB) _ ;User ID ReadIni(db, user, sa) _ ;Password ReadIni(db, pwd, ) ;Connect Timeout10 End FunctionApp.Path取的是 EXE 所在目录注意根目录下会带反斜杠拼路径时要判断一下。ini 文件本身别放版本库口令单独交给运维。这算不上真正的加密但至少代码截图、日志、异常信息里不会再漏出口令比硬编码强得多。第二件事是加一张操作日志表把「谁在什么时候保存了哪张单、改了什么状态」记下来。老系统出问题时最缺的就是这个用户说「我明明点保存了」你没有任何证据。日志表字段不用多时间、操作人、模块、单据号、动作、耗时。写日志放在业务事务之外避免日志失败影响主流程。验收清单我一般会固化成一张表每次交付前逐条走检查项通过标准常见失败原因全新建库并执行脚本无报错表与存储过程齐全脚本顺序错、兼容级别不匹配冷启动连接自检3 秒内给出明确提示超时设太长、异常未捕获开单全流程主表、明细、库存、流水四处一致事务未包住扣库存并发开单两个会话抢同一商品只成功一个库存判断在应用层历史报表与手工核算一致到分浮点累加、日期边界换机部署无需手工注册控件即可运行依赖未打包最后一条是血泪经验验收不要在自己机器上做。找一台从没装过开发环境的干净系统按交付文档从零走一遍装不上的地方就是文档缺的地方。从那以后我每次交付老系统改造都会强制在干净机器上走一遍这张表哪怕时间再紧也不跳过。希望帮到你。本文还有配套的精品资源点击获取