☰
VB6销售管理系统源码解析:ADO事务与库存并发实战
2026/11/4 17:15:46 网站建设 项目流程

简介:这是一套基于 VB6.0 与 SQL Server 2000 开发的销售管理系统完整源代码,面向刚接触数据库编程的 VB 初学者,以及需要参考数据库实例的开发者。系统已实现产品信息录入、产品管理、查询打印、金额汇总和简单报表等模块,并附带数据库脚本,可用于学习窗体交互、数据访问、报表输出以及 VB 与 SQL Server 之间的连接和操作流程。压缩包共 51 个文件,约 1.37MB,包含 frm 窗体、frx 窗体资源、bas 标准模块、cls 类模块、dsr 报表设计文件及 ocx 控件等,工程结构较为齐全,便于按模块阅读和调试。目前已有 442 人学习下载,适合作为课程设计、毕业设计前期的功能参考,也可用于拆解销售管理系统的表结构、查询逻辑与界面组织方式,快速建立数据库应用开发的整体认识。

1. 从一张开单界面说起:这套 VB6 销售管理系统到底管什么

接手一套跑了十几年的老系统时,最怕的不是代码丑,而是没人知道它到底怎么跑起来的。这套 VB6 销售管理系统源代码就是典型:双击工程文件,一堆 frm 窗体、bas 模块和 SQL Server 脚本摊在目录里,看似散乱,实际功能边界很清楚——商品资料、客户档案、销售开单、退货、库存变动、应收对账、单据打印,一条链都齐。数据层由 ADO 直接访问 SQL Server,前端用 DataGrid 和 DataReport 做表格与打印。它适合三类人:维护存量 VB6 系统的开发者、需要学习 VB6 桌面端配 SQL Server 的完整数据库案例的人、以及要拿一套能跑通的开单流程做底子的同学。这份完整源代码的价值不在语法新,而在于业务链路是闭环的。

2. 工程骨架拆解:.vbp 工程、窗体与 SQL 脚本的对应关系

拿到压缩包,别急着双击运行。先把目录里各文件类型弄清楚,能少走半小时弯路。VB6 项目的组织方式和现代工程差别不小:没有命名空间,没有依赖注入,所有东西靠 .vbp 一份工程文件串起来,窗体、模块、类模块在编译期静态绑定。

2.1 目录与工程文件清单

一个典型的销售管理系统目录大致长这样,每类文件职责单一:

文件/目录扩展名作用
工程文件.vbp记录所有窗体、模块、引用、编译选项,双击即打开 IDE
窗体.frm + .frx界面布局 + 二进制资源(图片、图标、控件属性)
标准模块.bas全局函数,如连接管理、公用计算、权限判断
类模块.cls业务实体封装,如订单对象
数据报表.dsrDataReport 设计文件,对应打印模板
资源文件.res图标、字符串表
数据库脚本.sql建库建表、初始数据、存储过程
说明文档.txt / .doc部署说明、默认账号

.frx是和.frm成对出现的,删除窗体时两个文件必须一起删,否则下次打开工程会报“文件未找到”。这一点在多人协作或拷贝代码时经常出问题,我见过有人只复制了 .frm,结果界面上的按钮全变成了空白框——图标资源都在 .frx 里。

.vbp里有两处值得盯:一是Reference=行,记录引用的类型库,比如 ADO 的msado15.dll;二是Object=行,记录 ActiveX 控件,比如数据网格控件。换机器打开报“找不到工程或库”,基本都是这两行指向的组件在新机器上没注册。

2.2 窗体分层:从登录到主界面的调用顺序

窗体不是平铺的,有明确的启动链。工程属性里设置的启动对象一般是登录窗体,登录成功后卸载自己、加载主窗体。主窗体承担菜单容器和状态栏的角色,业务窗体都从菜单里以模态方式弹出。按职责可以把窗体分成四层:

第一层是入口层,登录窗体负责账号密码校验,顺便把当前用户的角色 ID 写进全局变量,后续所有按钮的可用性都靠它判断。第二层是主框架层,主窗体挂菜单、工具栏、状态栏,一般不做业务,只负责调度。第三层是业务单据层,销售开单、销售退货、收款单这些,界面复杂,一个窗体上往往同时有表头录入区、明细表格、合计区。第四层是基础资料与查询层,商品、客户、供应商的增删改查,结构高度相似。

这种分层的实际意义在于:改一个业务模块,只需动对应窗体加公用模块,不会牵连全局。反过来说,很多新手二次开发翻车,就是直接在业务窗体里写死了连接字符串和 SQL,改一处要全工程搜。

2.3 数据库表设计:主从表加流水表的组合

销售系统的表结构基本围绕“单据”展开。主表存表头(单号、客户、日期、金额、制单人),明细表存行(商品、数量、单价、金额),库存表存当前结存,流水表存每一次变动记录。四张表配合,才能既查当前库存又查历史轨迹。

-- 销售单主表:一张单一条记录 CREATE TABLE t_SaleMaster ( BillID INT IDENTITY(1,1) PRIMARY KEY, BillNo VARCHAR(20) NOT NULL, -- 单号,业务上要求唯一 CustomerID INT NOT NULL, BillDate DATETIME NOT NULL, TotalAmount MONEY NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0, -- 0草稿 1已审核 2已作废 CreateUser VARCHAR(20) NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE UNIQUE INDEX UX_SaleMaster_BillNo ON t_SaleMaster(BillNo); -- 销售单明细:一张单多条记录,靠 BillID 关联 CREATE TABLE t_SaleDetail ( DetailID INT IDENTITY(1,1) PRIMARY KEY, BillID INT NOT NULL, ProductID INT NOT NULL, Qty DECIMAL(18,4) NOT NULL, Price MONEY NOT NULL, Amount MONEY NOT NULL ); CREATE INDEX IX_SaleDetail_BillID ON t_SaleDetail(BillID); -- 库存表:一个商品一条记录,扣减就在这里发生 CREATE TABLE t_Stock ( ProductID INT PRIMARY KEY, Qty DECIMAL(18,4) NOT NULL DEFAULT 0, UpdateTime DATETIME NOT NULL DEFAULT GETDATE() );

BillNo上建唯一索引是必须的,并发下两个人同时点保存,靠前端判重根本挡不住,最后一道闸只能在数据库。Amount用 MONEY 而不是 FLOAT,是因为金额做累加时浮点误差会滚雪球,一张单几十行看不出来,月结汇总就对不上账。

Status字段是这套系统的关键设计:草稿状态允许改,已审核状态锁定,作废状态回滚库存。二次开发时如果想加“反审核”,改的就是这个状态机的流转规则,而不是去动单据数据本身。

3. 数据访问层:modDB.bas 里的 ADO 封装与参数化查询

VB6 项目最容易失控的地方就是数据访问。窗体里到处Dim rs As New ADODB.Recordset,连接字符串复制粘贴十几份,改一次数据库密码要全工程替换。这套代码里有一个标准模块专门管这摊事,值得单独拆开看。

3.1 modDB.bas 的三层封装:连接、执行、取数

封装思路很朴素:全局只维护一个连接对象,提供三个入口函数——拿连接、执行无返回的 SQL、取记录集。业务窗体只调函数,不碰连接。

' 模块:modDB.bas ' 职责:全局连接管理 + 统一 SQL 执行入口 Option Explicit Private mConn As ADODB.Connection ' 模块级连接,整个进程复用 ' 取全局连接:已打开就直接返回,关闭状态就重新打开 Public Function GetConn() As ADODB.Connection If mConn Is Nothing Then Set mConn = New ADODB.Connection End If If mConn.State = adStateClosed Then ' 用 OLE DB 提供程序直连,不依赖每台机器配 ODBC 数据源 mConn.ConnectionString = "Provider=SQLOLEDB.1;" & _ "Persist Security Info=False;" & _ "User ID=sa;Password=******;" & _ "Initial Catalog=SaleDB;" & _ "Data Source=192.168.1.10,1433;" & _ "Connect Timeout=8" mConn.CursorLocation = adUseClient ' 客户端游标,断开后仍可读 mConn.Open End If Set GetConn = mConn End Function ' 执行增删改:返回受影响行数,调用方据此判断是否成功 Public Function ExecSQL(ByVal sqlText As String, _ Optional ByRef conn As ADODB.Connection) As Long Dim c As ADODB.Connection Dim lngAffected As Long On Error GoTo EH If conn Is Nothing Then Set c = GetConn() Else Set c = conn ' 事务中传入同一个连接,保证不跨连接 End If c.Execute sqlText, lngAffected, adCmdText ExecSQL = lngAffected Exit Function EH: Err.Raise Err.Number, "modDB.ExecSQL", _ Err.Description & vbCrLf & "SQL: " & sqlText End Function

CursorLocation设成adUseClient是关键一行。服务端游标在连接断开后记录集就废了,客户端游标把数据一次性拉到本地,窗体关闭连接后表格还能显示。代价是数据量大时内存吃紧,所以查询函数里一般会带TOP或日期范围限制。

ExecSQL的Optional参数设计是为了事务服务:事务里的多条语句必须跑在同一个连接上,否则BeginTrans开了也白开。这是新手最容易忽略的点——事务对象是挂在连接上的,不是全局的。

取记录集的函数形状类似,只是返回ADODB.Recordset:

' 取记录集:调用方负责在不用时 Set rs = Nothing Public Function GetRS(ByVal sqlText As String, _ Optional ByRef conn As ADODB.Connection) As ADODB.Recordset Dim rs As ADODB.Recordset Set rs = New ADODB.Recordset ' adOpenStatic + adLockReadOnly:只读快照,最省资源 rs.Open sqlText, IIf(conn Is Nothing, GetConn(), conn), _ adOpenStatic, adLockReadOnly, adCmdText Set GetRS = rs End Function

3.2 用 Command 对象挡掉引号型注入

拼接 SQL 在 VB6 里几乎是本能反应,但客户名里带一个单引号就能让整条语句崩掉,更别提注入。凡是带用户输入的地方,都得换成参数化写法。

' 新增客户:用 Command + Parameters,彻底避开引号拼接 Public Function AddCustomer(ByVal cName As String, _ ByVal phone As String, _ ByRef newID As Long) As Boolean Dim cmd As ADODB.Command Dim c As ADODB.Connection On Error GoTo EH Set c = GetConn() Set cmd = New ADODB.Command Set cmd.ActiveConnection = c cmd.CommandType = adCmdText ' 参数占位符用 ?,顺序必须和 Append 的顺序一致 cmd.CommandText = "INSERT INTO t_Customer(CustName, Phone, CreateTime) " & _ "VALUES(?, ?, GETDATE()); SELECT SCOPE_IDENTITY();" cmd.Parameters.Append cmd.CreateParameter("CustName", adVarWChar, adParamInput, 100, cName) cmd.Parameters.Append cmd.CreateParameter("Phone", adVarWChar, adParamInput, 30, phone) ' 拿自增主键:把最后一句 SELECT 的结果读出来 Dim rs As ADODB.Recordset Set rs = cmd.Execute If Not rs.EOF Then newID = CLng(rs(0)) AddCustomer = True Exit Function EH: AddCustomer = False End Function

参数类型用adVarWChar而不是adVarChar,对应数据库端NVARCHAR。中文名、生僻字、带空格的地址,用 varchar 在某些排序规则下会变问号,这个坑在后面排查章节还会提到。

SCOPE_IDENTITY()比@@IDENTITY安全,后者在有触发器的情况下会返回触发器里插入的 id,取错主键导致明细挂错单,属于那种“偶发、难复现、一查半天”的问题。

提示:CommandText里参数占位符只能是?,且必须按出现顺序Append。SQL Server 的命名参数在 ADO 这条链路上不生效,写成@name会直接报参数缺失。

3.3 记录集读取:游标类型和锁类型怎么选

rs.Open的后三个参数决定了性能和行为,很多人直接照抄默认值,结果在大数据量下卡成幻灯片。

参数常用值含义适用场景
CursorTypeadOpenStatic静态快照,数据不随他人修改变化列表查询、报表取数
CursorTypeadOpenForwardOnly只能向前滚,最省资源一次性遍历、导出
CursorTypeadOpenKeyset能看到他人修改,看不到新增需要实时性的编辑界面
LockTypeadLockReadOnly只读,不加锁绝大多数查询
LockTypeadLockOptimistic提交时才加锁单条记录编辑
LockTypeadLockBatchOptimistic批量更新时统一提交DataGrid 批量改

经验上,查询列表一律adOpenStatic + adLockReadOnly,编辑单条记录才用adLockOptimistic。用adLockPessimistic(编辑即锁)在多人环境里就是血泪经验——一个人打开单据去接了个电话,整张表被人堵在那里等锁超时。

取字段值时也别用rs.Fields("Amount").Value这种全路径,写rs!Amount简洁还不容易拼错。但要注意字段顺序变化时按索引取会错位,rs(0)只适合临时脚本,正式代码一律按名取。

4. 核心业务链路:开单、扣库存与事务并发的落点

系统里真正有技术含量的不是界面,是保存一张销售单时发生的事:写主表、拿主键、循环写明细、扣库存、写流水、更新客户应收。这五步必须原子完成,中间任何一步失败都得回滚,否则就是账实不符。

4.1 单据主从表写入的事务模板

下面这段是保存销售单的主干,把事务边界、参数化、错误回滚三件事一次做对:

' 保存销售单:主表 + 明细 + 库存,全部在一个事务里 ' 入参 oBill 是已校验过的单据对象,返回 True 表示整体成功 Public Function SaveSaleBill(ByRef oBill As SaleBill, ByRef sMsg As String) As Boolean Dim c As ADODB.Connection Dim cmd As ADODB.Command Dim lngBillID As Long Dim i As Long On Error GoTo EH Set c = GetConn() c.BeginTrans ' 事务从这里开始 ' ---- 第一步:写主表,拿回自增主键 ---- Set cmd = New ADODB.Command Set cmd.ActiveConnection = c cmd.CommandText = "INSERT INTO t_SaleMaster(BillNo,CustomerID,BillDate,TotalAmount,Status,CreateUser) " & _ "VALUES(?,?,?,?,0,?); SELECT SCOPE_IDENTITY();" cmd.Parameters.Append cmd.CreateParameter("BillNo", adVarChar, adParamInput, 20, oBill.BillNo) cmd.Parameters.Append cmd.CreateParameter("CustomerID", adInteger, adParamInput, , oBill.CustomerID) cmd.Parameters.Append cmd.CreateParameter("BillDate", adDBTimeStamp, adParamInput, , oBill.BillDate) cmd.Parameters.Append cmd.CreateParameter("Total", adCurrency, adParamInput, , oBill.TotalAmount) cmd.Parameters.Append cmd.CreateParameter("User", adVarChar, adParamInput, 20, gUserName) lngBillID = CLng(cmd.Execute()(0)) ' ---- 第二步:循环写明细,同时扣库存 ---- For i = 0 To oBill.Items.Count - 1 Dim it As SaleItem Set it = oBill.Items(i) Set cmd = New ADODB.Command Set cmd.ActiveConnection = c cmd.CommandText = "INSERT INTO t_SaleDetail(BillID,ProductID,Qty,Price,Amount) VALUES(?,?,?,?,?)" cmd.Parameters.Append cmd.CreateParameter("BillID", adInteger, adParamInput, , lngBillID) cmd.Parameters.Append cmd.CreateParameter("ProductID", adInteger, adParamInput, , it.ProductID) cmd.Parameters.Append cmd.CreateParameter("Qty", adDecimal, adParamInput, , it.Qty) cmd.Parameters.Append cmd.CreateParameter("Price", adCurrency, adParamInput, , it.Price) cmd.Parameters.Append cmd.CreateParameter("Amount", adCurrency, adParamInput, , it.Amount) cmd.Execute ' 扣库存:受影响行数为 0 说明库存不足,立即抛错触发回滚 If DeductStock(c, it.ProductID, it.Qty) = 0 Then Err.Raise vbObjectError + 1001, "SaveSaleBill", _ "商品 " & it.ProductName & " 库存不足,单据未保存" End If Next i c.CommitTrans ' 全部成功才提交 SaveSaleBill = True Exit Function EH: On Error Resume Next If Not c Is Nothing Then If c.State = adStateOpen Then c.RollbackTrans End If sMsg = Err.Description SaveSaleBill = False End Function

事务边界的写法有个细节:BeginTrans之后的所有Command都必须挂同一个连接对象c,不能图省事去调ExecSQL让它自己取全局连接。虽然GetConn返回的是同一个实例,但一旦将来引入连接池或换实现,这里就会静默失效——事务提交了,但某条语句跑在另一条连接上,等于没回滚。

错误处理块里的On Error Resume Next是为了防止回滚本身再抛错覆盖原始错误信息。回滚失败往往意味着连接已经断了,这时候原始错误才是真正要看的,别被“连接已关闭”这种二次错误带偏。

4.2 库存扣减的乐观锁写法

扣库存是并发冲突最集中的地方。两个收银员同时卖最后一件货,如果按“先查再改”的老套路,都能查到库存 1,都能通过校验,最后一减变成 -1。

' 乐观锁扣库存:把校验条件写进 WHERE,靠受影响行数判断成败 ' 返回 1 表示扣减成功,0 表示库存不足或商品不存在 Public Function DeductStock(ByRef c As ADODB.Connection, _ ByVal productID As Long, _ ByVal qty As Double) As Long Dim cmd As ADODB.Command Set cmd = New ADODB.Command Set cmd.ActiveConnection = c ' 关键:AND Qty >= ? 让数据库自己判断够不够扣 cmd.CommandText = "UPDATE t_Stock SET Qty = Qty - ?, UpdateTime = GETDATE() " & _ "WHERE ProductID = ? AND Qty >= ?" cmd.Parameters.Append cmd.CreateParameter("Q", adDecimal, adParamInput, , qty) cmd.Parameters.Append cmd.CreateParameter("P", adInteger, adParamInput, , productID) cmd.Parameters.Append cmd.CreateParameter("Q2", adDecimal, adParamInput, , qty) DeductStock = cmd.Execute End Function

这一句 SQL 是整套并发控制的核心。UPDATE ... WHERE Qty >= ?在 SQL Server 里会对目标行加排他锁,两个事务同时进来,第二个会等第一个提交,然后重新评估条件,发现库存不够,影响行数为 0,直接走回滚。整个过程不需要显式SELECT,也不需要应用层加锁。

adDecimal的参数必须显式传 Precision 和 Scale,否则 ADO 按默认精度处理,小数位会被截断。这是踩过的坑:数量 1.05 存进去变成 1,单价格式化后金额少几毛,对账时才发现。

注意:扣减函数必须接收外部传入的连接c,不能内部自己取。事务外的单条扣减可以传全局连接,事务内的一定要传事务连接,否则锁的是两条会话,乐观锁形同虚设。

4.3 明细行校验与金额精度处理

明细校验要在写库之前做,别指望数据库约束兜底——报错信息传不到界面上,用户只会看到“保存失败”四个字。

前置校验至少覆盖四条:商品不能为空、数量必须大于 0、单价不能为负、明细行数至少一行。金额计算统统用Currency类型,它是 VB6 里定点小数,小数位固定四位,累加不会漂移。

' 逐行校验并累计金额,返回 False 时 sMsg 里带具体哪一行出错 Public Function ValidateBill(ByRef oBill As SaleBill, ByRef sMsg As String) As Boolean Dim i As Long, total As Currency total = 0 If oBill.Items.Count = 0 Then sMsg = "单据没有明细行,无法保存" Exit Function End If For i = 0 To oBill.Items.Count - 1 With oBill.Items(i) If .ProductID = 0 Then sMsg = "第 " & (i + 1) & " 行未选择商品" Exit Function End If If .Qty <= 0 Then sMsg = "第 " & (i + 1) & " 行数量必须大于 0" Exit Function End If If .Price < 0 Then sMsg = "第 " & (i + 1) & " 行单价不能为负数" Exit Function End If ' 行金额以数量乘单价重算,不信任界面传入值 .Amount = CCur(.Qty * .Price) total = total + .Amount End With Next i oBill.TotalAmount = total ValidateBill = True End Function

.Amount一律重算而不是用界面传进来的值,这条规矩能挡住绝大部分“合计对不上”的投诉。界面上用户先改数量、再改单价,中间某个LostFocus没触发,金额就没刷新,直接存进去就是错账。

CCur转换不是可选项。VB6 里Double * Currency的结果是Double,不转回来,小数位会带上浮点尾巴,打印出来是123.450000000001这种。

5. 排查与避坑:在 Win10 上让这套 VB6 代码真正跑起来的 5 个坑

代码能编译不等于能跑。VB6 这套技术栈对新系统不太友好,下面五条是复现时最高频的翻车点,按“现象 → 原因 → 解决”记下来,省得一个个试。

坑一:打开工程弹“找不到工程或库”,或者某些控件显示为空白框

现象是双击.vbp后 IDE 报错,引用列表里有一项标着“缺失”,界面上数据表格控件变成一个灰方块。原因是工程引用了 ActiveX 控件,.vbp里只记了组件的 GUID,新机器上没有注册对应的 OCX 文件。解决分两步:先确认系统里有没有这些 OCX,没有的话从安装介质里找出来;再以管理员身份打开命令行,用注册工具逐个注册,路径必须写绝对路径且带引号。注册完重启 IDE,引用列表里的“缺失”标记才会消失。这里要注意 32 位和 64 位的问题——VB6 是 32 位开发环境,OCX 也必须用 32 位的,系统目录搞错了注册会“成功”但依然找不到。

坑二:连接数据库报“未找到提供程序”,或者“SQL Server 不存在或访问被拒绝”

第一种报错通常是连接字符串里的Provider=SQLOLEDB.1在这台机器上没有注册,或者装的是精简运行库。第二种是网络层问题:实例名写错、端口没开、TCP/IP 协议在数据库端被禁用、或者防火墙拦了 1433。排查顺序是先确认数据库端能本地连上,再用命令行工具从客户端测端口通不通,最后才是改连接字符串。连接字符串里建议显式写Data Source=IP,端口,用逗号分隔,比依赖实例名解析稳。至于账号,先用sa跑通,再换成应用专用账号——很多“登不上”其实是账号没给目标库的权限,只能看到 master。

坑三:中文客户名存进去变成问号

现象是界面上录入正常,写库后查出来一片???。原因一般是两边字符集没对齐:数据库字段用了VARCHAR,而客户端传的是 Unicode 字符串,中间某一层做了转换。解决是把字段类型改成NVARCHAR,同时 ADO 参数的Type用adVarWChar,长度按字符数算。还有一个隐蔽原因:连接字符串里没写字符集相关参数,某些驱动默认按单字节处理非 ASCII 字符。改完字段类型记得重建索引,NVARCHAR的索引键长度上限比VARCHAR小,超长会直接建不上。

坑四:开单保存时界面卡死几十秒,最后报超时

现象是点保存没反应,鼠标转圈,过一会儿弹“超时已过期”。九成情况是事务开了没提交或没回滚——调试时断点停在BeginTrans和CommitTrans之间,或者某条语句抛错后错误处理没走到RollbackTrans,连接被持有,后续所有请求排队等锁。解决办法是在错误处理块里无条件尝试回滚,并把连接状态检查写进去;调试阶段可以临时开一个会话查系统视图,看哪个会话阻塞了谁、阻塞了多久,一眼就能定位到是哪个模块没释放。另外,保存里如果有循环逐行写库,几百行明细会明显拖慢,改成批量插入或者一次提交多行,能快一个数量级。

坑五:日期条件查不到数据,明明库里有

现象是查询界面选“2024-01-01 到 2024-01-31”,结果为空。原因是用字符串拼日期,'2024/1/1'这种格式在不同区域设置下解析结果不一样,有的被当成月日互换,有的直接解析失败但不报错。解决是用参数化,传adDBTimeStamp类型,让 ADO 端做类型转换;如果必须拼字符串,统一用yyyymmdd这种无歧义格式。还有一个常见误区:表头字段存的是带时分秒的DATETIME,查询用<= '2024-01-31'会把当天 00:00 之后的数据全漏掉,正确写法是< '2024-02-01'。

6. 二次开发:加一张单据、加一个权限位、把连接串从代码里抽出去

改造这类老项目,最忌讳一上来重构。按“加一张单据 → 加一个权限位 → 抽配置”的顺序走,风险可控,每步都能验证。

加单据模块的套路是复制一份最相近的现有窗体,改标题、改表名、改字段。主从表结构不变的话,SaveSaleBill那段事务模板能直接复用,只改 SQL 里的表名和字段。新建的窗体要在.vbp里注册(IDE 里“工程 → 添加窗体”会自动处理),然后在主窗体菜单的点击事件里加上调用。

加权限位不用改表结构。t_Role里加一个整型字段,每一位对应一个功能,登录时把角色值读到全局变量,窗体加载时按位判断控件是否可用:

' 权限判断:第 3 位代表“销售开单”,第 4 位代表“单据作废” Public Function HasRight(ByVal bitIndex As Integer) As Boolean HasRight = (gRoleFlag And (2 ^ bitIndex)) <> 0 End Function ' 窗体加载时:没有开单权限就禁用保存按钮 Private Sub Form_Load() cmdSave.Enabled = HasRight(3) cmdVoid.Enabled = HasRight(4) End Sub

用位运算的好处是加功能不用改表,加一个位就行。缺点是超过 31 位会溢出,不过一个销售系统用不到那么多功能点。真正的坑是前端禁用了按钮,但后端没校验——有人直接用快捷键或者改接口就能绕过。所以每个保存函数开头都要再判一次权限,前端的禁用只是体验,不是安全边界。

抽配置这件事收益最直接。把连接字符串从modDB.bas里挪到工程同目录的一个 ini 文件,启动时读,改数据库地址不用重新编译:

' 从 ini 读取数据库配置,读不到就用默认值兜底 Private Function LoadConnString() As String Dim s As String s = ReadIni(App.Path & "\config.ini", "DB", "ConnStr", "") If Len(Trim(s)) = 0 Then s = "Provider=SQLOLEDB.1;User ID=sa;Password=******;" & _ "Initial Catalog=SaleDB;Data Source=127.0.0.1,1433;" End If LoadConnString = s End Function

ReadIni用系统 API 封装一下就行,三五行的东西。配置文件放App.Path而不是硬编码路径,打包部署到别的机器才不会失效——App.Path在根目录下会带反斜杠,拼接时要做个判断,这个小细节翻过一次车。

验证方法很土但有效:每次改完,先用一套完整的“开单 → 审核 → 退货 → 查库存 → 打报表”流程走一遍,重点看库存表和流水表的数量能不能对上。数据对不上,八成是哪个分支漏了事务或者漏了回滚。

从那以后我每次动这类老项目的数据层,都强制先在测试库上跑一遍完整单据链路,再动正式环境。老代码没有单元测试兜底,人工回归就是唯一的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询