简介:这份资源是面向VB初学者、课程设计学生及彩票销售点信息化改造需求者的文档资料,围绕Visual Basic 6.0开发彩票销售管理信息系统展开,可解决小型销售管理系统从需求分析到编码落地的完整设计参考问题。压缩包内共1个docx文件,约465KB,为论文形式的系统设计文档,包含摘要、目录与正文,便于整篇通读与排版参考。内容按系统概述、系统分析、系统设计、详细设计、系统实现与结束语组织,涵盖可行性分析、业务流程与数据流程分析、E-R图与功能层次图、软件模块结构设计及数据库设计;详细设计部分具体展开主界面、销售、开奖、中奖查询、设置与彩票打印等模块,销售支持自选与机选并关联打印输出,开奖按数学规律生成号码,查询提供六个奖级结果与单注即时核对。文中还梳理了VB的可视化编程、事件驱动、API函数、OLE与DLL等技术要点,并讨论单元测试、集成测试、运行维护与系统转换方案。目前已有63人学习下载,适合需要一份结构完整、难度适中的管理信息系统课程设计范例的读者参考。
1. 拆开一份 VB6 彩票销售系统论文源码,真正要复现的是哪几件事
拿到这份《VB 开发彩票销售系统》的资料,不少人的第一反应是"VB 过时了为什么还要学"。耐着性子把里面的窗体代码读完会发现,它把一套 MIS 该有的骨架全走了一遍:Access 建表、ADO 取数、控件数组接多注投注、随机数机选、按位匹配判奖、Printer 对象出票。整个系统只靠 Visual Basic 6.0 加一个 .mdb 文件,就能在单机上闭环运行,硬件门槛低到 PII 处理器配 64MB 内存。它的价值不在语言本身,而在于这是一份能完整跑通的最小业务系统标本:销售、开奖、查询、设置四个模块的边界怎么切,数据库字段怎么定,窗体之间怎么做互斥显隐,全部摆在明面上。想照着复现,或者改造成自己的小工具,把下面几条主线捋直就够了。
2. 彩票销售系统的 E-R 建模与 Access 表结构落地
2.1 从三个实体切入:客户、彩票、销售站
做这类系统最先要落的不是界面,是实体。原文给出的 E-R 图里只有三个实体:客户、彩票、销售站。销售站是出票方,客户是购买方,彩票是两者之间的凭证。三者的联系也很直白——一个销售站可以卖出多张彩票,一个客户可以在同一个站买多注,彩票本身又归属于某一期。
这种"低实体数 + 强关联"的模型,恰恰是选 Access 而不是 SQL Server 的理由。实体少意味着表少、join 浅;强关联意味着查询大多是按"期号"这一个维度做过滤和聚合,索引建对了就够用。单选题的数据库没必要上重型方案,桌面库的部署成本几乎为零,拷个 .mdb 就能走。
建模时有一个容易被忽略的点:期(Period)不是实体,是属性。很多新手会把"期"单独拎出来做一张表再关联,对这个小系统来说反而增加了一层多余的 join。把期号作为彩票表的一个冗余字段,查询时直接WHERE 期号 = ?,性能更好,代码也短。
2.2 两张核心表的字段设计与类型选择
原文明确列出了两张表的字段,我把它整理成可执行的建表依据:
| 表名 | 字段名 | 类型 | 宽度/说明 | 备注 |
|---|---|---|---|---|
| 彩票 | 序号 | 自动编号 | 长整型 | 主键,Access 自增 |
| 彩票 | 期号 | 文本 | 7 | 与每期信息表外联 |
| 彩票 | 投注号码 | 文本 | 7 | 定长数字串,如 1234567 |
| 彩票 | 购买日期 | 日期/时间 | — | 出票时刻 |
| 每期信息表 | 期号 | 文本 | 7 | 主键 |
| 每期信息表 | 中奖号码 | 文本 | 7 | 开奖后写入 |
| 每期信息表 | 开奖日期 | 日期/时间 | — | 设置模块维护 |
| 每期信息表 | 兑奖截止日期 | 日期/时间 | — | 设置模块维护 |
这里有两个设计决策值得说清楚。第一,投注号码用文本而不是长整型。号码是 7 位数字串,可能以 0 开头(比如 0123456),存成数字会丢前导零,判奖时按字符串做Mid截取也更自然。第二,期号宽度定 7。原文没解释,但按国内玩法惯例,期号通常是"年份后两位/三位 + 期序",留 7 位是给未来扩展的余量。
提示:Access 的"文本"字段默认允许 255 字符,宽度必须在表设计视图或 DDL 里显式声明,否则设成 7 的限制形同虚设。
2.3 用 DDL 建表并打通 ADO 连接
Access 支持通过 ADO 执行 DDL,直接在 VB 里初始化数据库结构,比手工在 Access 界面点来点去更可复现:
-- 彩票销售台账表 CREATE TABLE 彩票 ( 序号 COUNTER PRIMARY KEY, -- 自动编号,等价于 AUTOINCREMENT 期号 TEXT(7) NOT NULL, 投注号码 TEXT(7) NOT NULL, 购买日期 DATETIME ); CREATE INDEX idx_qi_hao ON 彩票 (期号); -- 每期基础信息表 CREATE TABLE 每期信息表 ( 期号 TEXT(7) PRIMARY KEY, 中奖号码 TEXT(7), 开奖日期 DATETIME, 兑奖截止日期 DATETIME );建完表,VB 侧用 ADO 连上去。老项目常见的写法是模块级连接对象 + 全局Recordset:
' 标准模块中声明 Public cnn As ADODB.Connection Public rs As ADODB.Recordset Public Sub OpenDB() Set cnn = New ADODB.Connection Set rs = New ADODB.Recordset ' 用 Jet 4.0 提供者连当前目录下的 mdb,避免绝对路径 cnn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=" & App.Path & "\lottery.mdb;" cnn.CursorLocation = adUseClient ' 客户端游标,支持 RecordCount cnn.Open End Sub Public Sub CloseDB() If Not rs Is Nothing Then If rs.State = adStateOpen Then rs.Close Set rs = Nothing End If If Not cnn Is Nothing Then If cnn.State = adStateOpen Then cnn.Close Set cnn = Nothing End If End SubCursorLocation = adUseClient这一行是老代码里最容易漏的配置。默认的服务端游标(Jet 驱动下)拿不到可靠的RecordCount,而查询模块要显示销量统计,必须知道行数。App.Path则保证工程换目录后数据库路径跟着走。
2.4 索引与并发:单机 MIS 的取舍
idx_qi_hao这个索引是必须的。查询模块里最高频的三条 SQL 全部以期号做过滤条件——统计销量、取中奖号码、拉出该期全部投注号码做批量判奖。没有索引,几千注数据就会让界面明显卡顿。
并发问题上不用想太多。原文明确写了"本系统在单机上运行",销售点的实际场景就是一两个人一台机器,不存在写冲突。真要留余地,把 ADO 的LockType设成adLockOptimistic即可,不需要引入事务嵌套或连接池。
需要留意的是 Access 单文件 2GB 的容量天花板。按一张彩票一条记录、单条不到 100 字节估算,撑到千万级记录都没问题,够一个销售点用很久。真正会先撞墙的不是容量,而是查询响应——这也是后面加分页的原因。
3. 销售模块的控件数组、机选随机数与开奖判奖算法
3.1 一屏五注:控件数组 txt_no(0..4) 与 VScroll 联动
销售界面的核心约束是"每次最多操作五注"。实现方式不是摆五个独立文本框,而是建一个控件数组txt_no(0)到txt_no(4)。数组的好处是可以用循环统一遍历,判断哪些格子已经被客户填了号码。
原文里rand_sum_Change和txt_no_LostFocus两个过程其实在做同一件事——统计已填注数,然后限制机选注数的上限:
Private Sub txt_no_LostFocus(Index As Integer) Dim MyCounter As Integer, Picked_no As Integer MyCounter = 0 Picked_no = 0 ' 遍历 0..4,统计非空的投注框数量 Do While MyCounter <= 4 If Len(Trim(txt_no(MyCounter).Text)) <> 0 Then Picked_no = Picked_no + 1 End If MyCounter = MyCounter + 1 Loop ' 滚动条上限 = 还能机选的注数 = 5 - 已手填注数 VScroll1.Min = 5 - Picked_no VScroll1.Max = 0 End Sub Private Sub VScroll1_Change() rand_sum.Text = VScroll1.Value ' 注数为 0 时禁用机选按钮,避免生成空结果 cmdGenerateRand.Enabled = (rand_sum.Text <> 0) End Sub这段逻辑的关键参数是VScroll1.Min = 5 - Picked_no。VB 的滚动条Min必须小于等于Max,所以把Min设成剩余可机选注数、Max固定为 0,滚动方向就变成"从 0 往下拉、拉到负数"。这是个取巧写法,如果想让方向更自然,把两者对调并在Value上取绝对值也行。
cmdGenerateRand.Enabled的联动是防呆:客户已经手填了 5 注,剩余可机选数就是 0,此时机选按钮必须变灰,否则点下去会生成一批无效号码。
3.2 机选号码生成:Rnd + Randomize 与重复校验
机选是这套系统里最像"业务代码"的部分。VB6 的Rnd如果不先Randomize,每次启动程序都会给出同一串随机序列,明眼人一看就知道有问题。完整的机选过程要处理三件事:生成、去重、写入。
Private Sub cmdGenerateRand_Click() Dim temp_no As String Dim d As Integer Dim MyCounter As Integer Dim i As Integer Dim dup_flg As Boolean Dim firstEmpty As Integer Randomize Timer ' 用系统时钟做种子,避免序列固定 MyCounter = 0 Do While MyCounter < Val(rand_sum.Text) temp_no = "" For i = 1 To 7 ' 每注 7 位号码 d = Int(Rnd * 10) ' Int(Rnd*10) 取值 0..9 temp_no = temp_no & CStr(d) Next i ' 与本次已生成的机选号做去重 dup_flg = False For i = 0 To 4 If Trim(txt_no(i).Text) = temp_no Then dup_flg = True Exit For End If Next i If dup_flg Then GoTo Retry ' 撞号则重来,注数不递增 ' 找到第一个空框写入 firstEmpty = -1 For i = 0 To 4 If Len(Trim(txt_no(i).Text)) = 0 Then firstEmpty = i Exit For End If Next i If firstEmpty = -1 Then Exit Sub txt_no(firstEmpty).Text = temp_no MyCounter = MyCounter + 1 Retry: Loop End Sub这里有几个参数含义要说透。Int(Rnd * 10)是 VB6 里生成 0 到 9 整数的标准写法,Rnd返回[0,1)区间,乘以 10 取整后正好覆盖 0 到 9。去重循环遍历的是txt_no(0..4)这五个框,而不是整张彩票表——同一次销售中给同一个客户开出两张一模一样的号码毫无意义,但不同客户之间撞号是无所谓的,那属于真实业务范畴。Retry:标签配合GoTo是为了在撞号时重新生成而不推进计数器,保证最终写入的注数正好等于客户要的注数。
注意:
Randomize Timer要放在循环外。放进循环里会导致短时间内多次调用拿到相近的种子,反而降低随机性。
3.3 开奖与判奖:从 7 位到 2 位的按位匹配
判奖逻辑是这套系统里最值得单独拎出来看的一段。原文的实现思路是:拿投注号码和该期中奖号码,从 7 位长度开始逐级向下比,先命中高奖级就跳出。
' 返回奖级:0=特等奖 1=一等奖 ... 5=五等奖,-1=未中奖 Private Function GetPrizeLevel(ByVal BetNo As String, ByVal WinNo As String) As Integer Dim i As Integer, j As Integer For i = 7 To 2 Step -1 ' 匹配长度从 7 位递减到 2 位 For j = 1 To 8 - i ' 起始位置滑动,保证不越界 If Mid(WinNo, j, i) = Mid(BetNo, j, i) Then GetPrizeLevel = 7 - i ' 7位全中→0(特等),6位→1(一等) Exit Function End If Next j Next i GetPrizeLevel = -1 End Function参数关系值得画一张表对照:
| 匹配长度 i | 起始位置 j 范围 | 返回奖级 | 含义 |
|---|---|---|---|
| 7 | 1 | 0 | 特等奖,全串一致 |
| 6 | 1..2 | 1 | 一等奖,连续 6 位一致 |
| 5 | 1..3 | 2 | 二等奖 |
| 4 | 1..4 | 3 | 三等奖 |
| 3 | 1..5 | 4 | 四等奖 |
| 2 | 1..6 | 5 | 五等奖 |
Mid(字符串, 起始位置, 长度)是 VB6 的三参数截取函数,起始位置从 1 开始计数(不是 0)。j的上限写成8 - i是为了保证j + i - 1 <= 7,也就是截取窗口不会超出 7 位号码的边界。
需要说清楚的是,这只是原文给出的一种判定模型——按位对齐的连续区段匹配。真实彩票玩法里,有的按位次对位、有的按号码集合命中个数,规则完全不同。如果要搬到实际业务,GetPrizeLevel这个函数体是唯一需要替换的地方,调用方不用动,这是把判奖抽成独立函数的最大好处。
批量判奖时,原文用了一个k(0..5)数组做各奖级计数器,遍历该期所有投注记录后统一写回界面:
Dim k(0 To 5) As Long ' ... 逐条读取 rs("betno"),调用 GetPrizeLevel Dim lvl As Integer lvl = GetPrizeLevel(rs("betno"), winNo) If lvl >= 0 Then k(lvl) = k(lvl) + 1k的下标直接对应奖级,省掉了一堆Select Case分支,这在实际维护时比原文那种把提示语和计数混在一起写要清爽得多。
4. 查询、设置与打印:DataCombo 绑定、窗体互斥与 Printer 出票
4.1 窗体互斥:Frame 显隐 + Enabled 的按钮状态机
主界面把五个模块塞在同一个窗体里,靠Frame控件的Visible切换。原文的CmdExitPreset_Click是退出设置模块时用的,逻辑是"所有 Frame 隐藏 + 所有主按钮可用":
Private Sub HideAllFrames() frameSale.Visible = False frameWinner.Visible = False frameQuery.Visible = False framePreset.Visible = False cmdSale.Enabled = True cmdWinner.Enabled = True cmdQuery.Enabled = True cmdPreset.Enabled = True cmdQuit.Enabled = True End Sub进入某个模块时则反过来:显示对应 Frame,同时把其余按钮置灰,防止客户在销售过程中误点开奖。这套"显隐 + 置灰"的状态机是小窗体多模块的标准做法,比开五个独立 Form 省事,代价是主窗体的代码量集中在同一个 .frm 里。
| 控件 | 销售中 | 开奖中 | 查询中 | 设置中 | 空闲 |
|---|---|---|---|---|---|
| frameSale | 显示 | 隐藏 | 隐藏 | 隐藏 | 隐藏 |
| frameWinner | 隐藏 | 显示 | 隐藏 | 隐藏 | 隐藏 |
| frameQuery | 隐藏 | 隐藏 | 显示 | 隐藏 | 隐藏 |
| framePreset | 隐藏 | 隐藏 | 隐藏 | 显示 | 隐藏 |
| cmdSale 等主按钮 | 置灰 | 置灰 | 置灰 | 置灰 | 可用 |
用一张状态表把每个模块的显示约束固定下来,改代码时就不会出现"两个 Frame 同时可见"的低级错误。
4.2 中奖查询:期号下拉、销量统计与单注即时判定
查询模块分两条路径:按整期查,和按单注查。
按整期查的第一步是选期号。原文用的是DataCombo控件(属于 MSDATLST.OCX),把它绑到期号列表上:
' 在 Form_Load 里绑定 DataCombo 的数据源 Private Sub InitQueryCombo() Set DataComboQuery1.RowSource = rsPeriod ' 已打开的期号记录集 DataComboQuery1.ListField = "期号" ' 下拉里显示的字段 DataComboQuery1.BoundColumn = "期号" ' 选中后取值的字段 End Sub选定期号后,依次执行三条 SQL。第一条取中奖号码,第二条统计销量,第三条拉出全部投注号码做批量判奖:
-- 1) 取该期中奖号码 SELECT 中奖号码 AS winno FROM 每期信息表 WHERE 期号 = '2024001'; -- 2) 统计该期销量(每注 2 元,乘 2.00 得到销售金额) SELECT COUNT(期号) * 2.00 AS sub_sum FROM 彩票 WHERE 期号 = '2024001'; -- 3) 取该期全部投注号码,供逐条判奖 SELECT 投注号码 AS betno FROM 彩票 WHERE 期号 = '2024001';第二条 SQL 里的* 2.00有两个作用。显式写小数位让结果保持货币量精度,不至于被整除成整数;同时把"单价"这个业务参数收拢在 SQL 里,将来调价只改一处。如果销售点要区分玩法单价,加一个玩法字段做GROUP BY再乘不同系数即可。
单注即时查询走的是txtQuery_Change事件——客户一边敲号码一边判,敲满 7 位就触发:
Private Sub txtQuery_Change() If Len(Trim(txtQuery.Text)) <> 7 Then Exit Sub If Not IsNumeric(txtQuery.Text) Then MsgBox "请输入 7 位 0~9 的数字!!!" txtQuery.SetFocus Exit Sub End If Dim lvl As Integer lvl = GetPrizeLevel(Trim(txtQuery.Text), lblQuery3.Caption) If lvl < 0 Then MsgBox "谢谢您对社会福利事业的支持! 您没有中奖!" Else MsgBox "恭喜您! 中了" & Array("特等奖", "一等奖", "二等奖", _ "三等奖", "四等奖", "五等奖")(lvl) & "!!!" End If End SubLen(...) <> 7是长度闸门,IsNumeric是字符闸门,两道校验都过了才进判奖。原文用的是Select Case 7 - i逐个分支写提示语,换成数组下标取值后代码从 20 行压到 3 行,且新增奖级时只改数组。
4.3 设置模块与彩票打印输出
设置模块干的事最单纯:把新一期的期号、开奖日期、兑奖截止日期写进每期信息表。
Private Sub cmdSavePreset_Click() Dim sql As String sql = "INSERT INTO 每期信息表 (期号, 开奖日期, 兑奖截止日期) VALUES ('" & _ txtNewPeriod.Text & "', #" & Format(dtpOpen.Value, "yyyy-mm-dd") & "#, #" & _ Format(dtpDeadline.Value, "yyyy-mm-dd") & "#)" cnn.Execute sql MsgBox "第 " & txtNewPeriod.Text & " 期已建立" End SubAccess 的日期常量用#包裹,这是 Jet SQL 的语法要求,和标准 SQL 的单引号字符串不同。Format(..., "yyyy-mm-dd")统一格式是为了避开不同区域设置下月/日/年与日/月/年的歧义。
打印部分用 VB6 内置的Printer对象直接输出到默认打印机:
Private Sub cmdPrint_Click() Dim i As Integer Printer.Font.Name = "宋体" Printer.Font.Size = 10 Printer.Print "期号:" & txtPeriod.Text Printer.Print "--------------------------" For i = 0 To 4 If Len(Trim(txt_no(i).Text)) = 7 Then Printer.Print "第 " & (i + 1) & " 注:" & txt_no(i).Text End If Next i Printer.Print "--------------------------" Printer.Print "出票时间:" & Format(Now(), "yyyy-mm-dd hh:nn:ss") Printer.EndDoc ' 必须调用,否则不输出 End SubPrinter.EndDoc是这段代码里唯一不能省的一行。少了它,打印内容会一直留在缓冲区里直到程序退出,客户拿不到票。Printer.Font的设置要在Print之前,且属于全局状态——如果打印结束后不还原,会影响后续其他打印任务。
4.4 数据一致性检查:复位与重复提交防护
复位按钮看似简单,实则要做三件事:清空五个投注框、把滚动条归零、恢复机选按钮的可用状态。
Private Sub CmdReset_Click() Dim i As Integer For i = 0 To 4 txt_no(i).Text = "" Next i VScroll1.Value = 0 rand_sum.Text = 0 cmdGenerateRand.Enabled = False End Sub顺序上有讲究:先把值置 0,再设Enabled。反过来的话VScroll1_Change会先被触发一次,把rand_sum.Text又改回去。
重复提交靠"出票后才允许复位"这条业务规则挡住。一个更稳的做法是落库时给彩票.期号 + 投注号码 + 购买日期建唯一索引,让数据库层做最后一道防线,但销售点场景下人工确认一次就够,没必要增加复杂度。
5. 在 Win8/Win10 上跑起来:ocx 注册、工程关联与老代码的兜底技巧
5.1 mscomctl.ocx 与 msdatlst.ocx 注册失败的处理
VB6 项目换到新系统报的第一个错,几乎都是"部件未正确注册"。工程里用到的控件按文件名分两类:界面增强控件在 MSCOMCTL.OCX,数据绑定下拉框在 MSDATLST.OCX。64 位系统下必须先确认注册的是 32 位版本,命令行提示符要以管理员身份打开:
:: 以管理员身份运行,注意用 SysWOW64 目录下的 regsvr32 cd C:\Windows\SysWOW64 regsvr32 MSCOMCTL.OCX regsvr32 MSDATLST.OCX如果 regsvr32 报"模块已加载但找不到入口点",多半是拷贝到了 64 位版本的 ocx,或者 ocx 位数和 regsvr32 位数不匹配。另一种常见情况是注册成功但打开工程仍提示缺控件,这时候检查.vbp文件里的Object=行,看对方要求的版本号是否与你注册的版本一致——版本号差一位都不认。
5.2 .vbp 关联丢失与 .vb 文件打不开
用高版本 Visual Studio 打开老工程,双击某个窗体文件时弹出"确保已安装文件类型(.vb)的应用程序",这不是代码问题,是.vb扩展名在系统里没有关联到任何程序。直接在 VS 里通过解决方案资源管理器打开即可,不要走资源管理器双击。如果.vbp本身也打不开,用记事本打开它,把Reference=那几行指向的组件逐个核对一遍,缺哪个补哪个。
| 故障现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 打开工程提示缺少部件 | ocx 未注册或版本不符 | 管理员身份 regsvr32,核对 .vbp 版本号 |
| 双击 .vb 提示选择程序 | 文件关联丢失 | 从 IDE 内部打开 |
| 编译后运行报 429 | 运行机器缺 ocx | 用打包向导生成安装包,或手动注册 |
| 打印无输出 | 缺少 Printer.EndDoc | 补上 EndDoc 调用 |
5.3 Access 容量、分页与后续演进
前面提过 Access 单文件 2GB 的上限,这在销售点规模下几乎撞不到。真正需要提前处理的是列表查询的响应速度:SELECT * FROM 彩票 WHERE 期号 = ?在几万行时还能秒回,超过十万行、又没有索引的情况下,界面会假死。补索引是第一优先级,idx_qi_hao必须建。
第二优先级是分页。Access 的 SQL 方言不支持LIMIT ... OFFSET,只能靠TOP加NOT IN变通:
-- 每页 20 条,取第 3 页(即第 41~60 条) SELECT TOP 20 * FROM 彩票 WHERE 期号 = '2024001' AND 序号 NOT IN ( SELECT TOP 40 序号 FROM 彩票 WHERE 期号 = '2024001' ORDER BY 序号 ) ORDER BY 序号;这里TOP 40是(页码-1) × 页大小,外层TOP 20是页大小,ORDER BY 序号必须内外一致,否则分页会错位。这种写法在数据量大时性能并不好,子查询和主查询各扫一遍。更实用的做法是记住上一页最后一条的序号,用WHERE 序号 > 上页末序号做游标式翻页,代价是不能跳页。
最后提一句判奖函数的可替换性。GetPrizeLevel把规则和界面彻底解耦,将来无论换成哪种玩法,只要保证输入是 7 位号码串、返回值是 0 到 5 的奖级或 -1,上层的查询、统计、打印代码都不用动。这种"把变化点收进一个函数"的手法,比语言新旧重要得多——也是这份老资料最值得带走的一点。
本文还有配套的精品资源,点击获取