简介:围绕VB学籍管理系统展开的毕业设计文档,面向学习Visual Basic、数据库应用开发,以及需要课程设计或毕业设计参考的初、中级开发者,可用于梳理学籍管理类项目的需求分析与实现路径。压缩包共1个doc文件,约523KB,内容以论文形式组织,依次覆盖系统需求分析、可行性分析、系统设计、数据库设计与详细设计。文中讨论了学生信息、成绩、出勤和权限管理等模块,并给出E-R图、数据表字段、主外键约束,以及通过ADO连接Access或SQL Server的思路。详细设计部分还涉及主窗体、用户管理、学籍管理、班级管理、课程设置和成绩管理界面,提到利用Label、TextBox、Button、DataGridView等控件展示与操作数据,并设置查询、输入校验和错误处理。已有436人学习,适合据此理解VB数据库项目的功能结构、表间关系与模块划分,作为课程设计或毕业设计的参考材料。
1. 一套 2006 年的 VB 学籍管理系统,为什么还值得当底稿
每年选题季,学籍管理这类题目都会被翻出来做一遍。很多人第一反应是找一套现成的 VB 学籍管理系统源代码,改改标题就交,真打开压缩包才发现:六个窗体、六张表、一个 Module.bas,不知道该从哪一页读起。界面是灰色按钮加 MDI 子窗体,数据库还是 SQL Server 2000,看起来离现在的技术栈很远。
但课设和毕设的评分点从来不是"用了多新的框架",而是需求有没有理清、表设计得对不对、增删改查能不能跑通、异常有没有兜住。这套源码的价值就在这里:用户、学籍、班级、课程设置、成绩五条业务线,用六张表和一个公共数据访问函数串成完整闭环,代码量不大,该有的分层一个不少。适合两类人——正在做 VB 课设、需要一份结构完整可改的底稿的人;以及要讲清"一个管理信息系统到底由哪些零件组成"的指导者。下面按需求、架构、建表、编码、避坑、验收的顺序拆。
2. 需求与可行性:把"管学生"拆成五个模块和可验收的动作
2.1 从一句"要能管学生信息"落到功能点
原始需求写得比较笼统:操作人员计算机知识普遍较差,要求界面友好;使用对象多,要求权限管理;原始数据要能多条件修改;查询要支持多条件;在权限范围内删除数据要稳;数据计算尽量自动完成,少靠人工。这六条如果直接搬到开题报告里,评审看一眼就会问"所以你到底要做几个功能"。
我的做法是把每条需求翻译成"谁、在哪个窗体、点了什么按钮、数据库里发生了什么"。比如"多条件修改"落到学籍管理模块,就是学号、姓名、班级三个输入框加一个查询按钮,命中记录回填到 DataGrid,双击某行打开编辑窗体。翻译完,需求就从形容词变成了动作。
按这个口径拆下来,系统是五个模块、每个模块三到四个动作,正好对得上后面要建的六张表:
| 模块 | 主要动作 | 落到的表 | 验收方式 |
|---|---|---|---|
| 用户管理 | 新增用户、改密码、按角色授权 | user_Info | 用低权限账号登录,看不到用户管理菜单 |
| 学籍管理 | 添加、修改、多条件查询、删除 | student_Info | 录入一条学籍后能按姓名模糊查到 |
| 班级管理 | 班级设置、年级信息维护 | class_Info | 新增班级后学籍录入界面的班级下拉能刷出来 |
| 课程设置 | 课程增改查、年级课程配置 | course_Info、gradecourse_Info | 给某年级配两门课,成绩录入时只出现这两门 |
| 成绩管理 | 录入、修改、按班级/课程统计 | result_Info | 录入 10 条成绩,平均分与手算一致 |
这张表建议直接放进开题或任务书里,它同时是需求清单和验收清单,后面写测试用例不用重新想。
2.2 为什么是 VB6 + SQL Server 2000,而不是更"新"的组合
选型这件事在课设里经常被写成"因为老师要求",其实能讲出理由。VB6 的两个特性对这个题目是实打实的收益:一是可视化编程,界面拖控件就能出来,窗体布局代码由 IDE 自动生成,人机界面友好这条需求基本靠它兜住;二是 VB4 之后支持面向对象,把窗体和数据封装成对象,改一个模块不影响另一个。
数据库选 SQL Server 2000 而不是 Access,核心理由是并发和权限。Access 是单文件库,多人同时开着录入成绩容易锁住,而且文件级权限挡不住"谁能删学籍"这种需求;SQL Server 有登录账号、有数据库角色,用户管理模块的权限才能真正落地。代价是要装服务端、要配连接串,这一点在可行性分析里必须写清楚,别到中期才发现机房机器装不上。
连接方式走 ADO,这是 VB6 访问 SQL Server 最直接的路子。整个工程只在公共模块里写一次连接串,其它地方一律调函数:
' Module.bas —— 全局连接串收敛到一处,换机器只改这里 Public ConnectString As String Public Sub InitConnect() ' Provider 固定用 SQLOLEDB;Data Source 填实例名或 IP;Initial Catalog 填库名 ' 实际部署时不要用 sa,建一个只有增删改查权限的独立登录账号 ConnectString = "Provider=SQLOLEDB.1;Persist Security Info=False;" & _ "User ID=mis_user;Password=******;" & _ "Initial Catalog=StudentMIS;Data Source=127.0.0.1" End SubPersist Security Info=False的作用是连接打开后不再把密码留在连接对象里,答辩时被问到"密码怎么管的"可以答这一句。Data Source写127.0.0.1只适合本机演示,要交两台机器的环境就改成服务端实例名。
2.3 可行性分析的三个维度怎么写才不是凑字数
原稿的可行性只写了技术一条,说"对象单一、关联性强、计算不复杂,所以适合数据库管理",逻辑没错但太薄。合格的写法是三条线各给一句可验证的结论。
技术可行性要落到"哪个技术点已经被验证过",而不是"VB 很强大"。比如:ADO 的 Connection 和 Recordset 对象能完成六张表的全部增删改查,DataGrid 控件能直接绑定 Recordset 做列表展示,MDI 窗体支持多文档界面,菜单可以按角色动态隐藏。这三条都能在半天内写个 Demo 验证完,写进报告就有底气。
经济可行性不是写"成本低"三个字就完事,要算清楚:开发工具是课程已有的,数据库用教学版,运行环境是现成的 Windows XP 机器,除人力外没有采购项。这条在毕设里通常一笔带过,但评审喜欢看。
操作可行性最容易被忽略,恰恰是原需求里点名的——操作人员计算机知识普遍差。对策要具体:所有删除操作弹二次确认框,输入框失焦时校验学号只能是数字,查询无结果时提示"没有符合条件的记录"而不是留一片空白。这三条写出来,操作可行性才不是空话。
可行性结论最后要收成一句可执行的话,比如"本系统在现有软硬件条件下可在一个学期内完成开发与测试",比"完全可行"有用得多。
3. 总体架构与数据库设计:MDI 主窗体加六张表的骨架
3.1 模块划分:五个功能模块的边界与调用关系
功能模块图在原稿里是一张树状图,落到文字上就是三层:顶层是学生信息管理系统,第二层是学籍管理、班级管理、课程管理、成绩管理四个业务模块加一个用户管理,第三层是每个模块下的添加、修改、查询三个动作。截图放进论文时记得把"用户管理"补上,原图的第二层漏了它,但流程图里又有,前后会对不上。
划分依据是业务对象而不是操作类型,这点值得在答辩时讲一句。按操作分,会变成"所有添加一个窗体、所有修改一个窗体",字段完全不同的学籍和课程挤在一起,维护起来是灾难;按对象分,学籍模块只管 student_Info,课程模块只管 course_Info 和 gradecourse_Info,模块之间只通过学号、班号、课程名三个键关联,边界清楚。
模块之间的调用关系也要交代:班级管理和课程设置是前置数据,学籍录入时要选班级,成绩录入时要选课程,所以这两个模块的数据必须先有;成绩管理依赖学籍和课程两张表的主键,是最下游。这个依赖顺序决定了演示顺序,答辩现场如果先点成绩录入再录学籍,会看到一堆空下拉框,很尴尬。
3.2 MDI 主窗体与系统流程:登录校验到菜单分发
系统流程是线性的:启动,登录,判断账号密码,通过则进主窗体,选功能,打开对应子窗体,操作完返回,退出。原稿流程图里画了 Y/N 分支,但没画失败三次锁定,这一条建议补上,属于"权限管理"需求的一部分。
主窗体用 MDI 窗体而不是 SDI,原因是子窗体可以统一在主窗体内最大化、最小化,不会满屏乱飞,视觉上更像一个系统。VB6 里通过"工程 → 添加 MDI 窗体"生成,菜单用菜单编辑器逐项加,一级菜单对应五个模块,二级菜单对应添加、修改、查询。
登录校验的代码不长,但有几个容易写漏的点:
Private Sub cmdLogin_Click() Dim rst As ADODB.Recordset Dim msg As String ' 空值先挡掉,避免拼出 user_ID='' 这种合法但无意义的查询 If Trim$(txtUserID.Text) = "" Or Trim$(txtPwd.Text) = "" Then MsgBox "用户名和密码不能为空", vbExclamation, "提示" Exit Sub End If ' 单引号做转义,防止密码里带 ' 把 SQL 拼断 Set rst = ExecuteSQL("SELECT user_ID, user_Des FROM user_Info " & _ "WHERE user_ID='" & Replace(Trim$(txtUserID.Text), "'", "''") & "' " & _ "AND user_PWD='" & Replace(Trim$(txtPwd.Text), "'", "''") & "'", msg) If rst Is Nothing Then Exit Sub If rst.EOF Then MsgBox "用户名或密码错误", vbCritical, "登录失败" txtPwd.Text = "" txtPwd.SetFocus Exit Sub End If ' 登录成功,把当前用户写进全局变量,后续菜单按角色显示 CurrentUser = rst!user_ID CurrentRole = rst!user_Des Unload Me frmMain.Show End Subrst.EOF是判断有无命中记录最省事的写法,比数 RecordCount 稳。Replace(..., "'", "''")是单引号转义的土办法,只能挡住拼接语法错误,挡不住恶意输入,但在课设场景够用,答辩时主动说一句"生产环境应该用参数化查询"是加分项。
user_Des这个字段名直译是"用户描述",实际被拿来存角色,字段名和用途对不上,属于原稿的历史遗留。建议在文档里注明"user_Des 存角色标识,取值为 admin 或 teacher",别改表结构,改文档成本更低。
3.3 公共模块 Module.bas:把连接和 SQL 执行收敛到一处
整个工程最值得先读的文件是 Module.bas。它做两件事:存全局变量(当前用户、当前角色、连接串),提供统一的 SQL 执行入口 ExecuteSQL。所有窗体都调它,没有一处自己 Open 连接,这是这套代码结构上最干净的地方。
' Module.bas —— 统一的数据访问入口 Public CurrentUser As String Public CurrentRole As String Public Function ExecuteSQL(ByVal SQL As String, MsgString As String) As ADODB.Recordset Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Dim sTokens() As String On Error GoTo ExecuteSQL_Error ' 取首个单词判断语句类型,注意先 Trim 再 Split,避免前导空格把首词切空 sTokens = Split(Trim$(SQL)) Set cnn = New ADODB.Connection cnn.Open ConnectString If InStr("INSERT,DELETE,UPDATE", UCase$(sTokens(0))) > 0 Then ' 非查询语句直接 Execute,不返回记录集 cnn.Execute SQL MsgString = sTokens(0) & " 执行成功" Else Set rst = New ADODB.Recordset ' 客户端游标:记录集取回后脱离连接也能继续用,RecordCount 也准 rst.CursorLocation = adUseClient rst.Open Trim$(SQL), cnn, adOpenStatic, adLockReadOnly Set ExecuteSQL = rst MsgString = "查询到 " & rst.RecordCount & " 条记录" End If ExecuteSQL_Exit: Set rst = Nothing Set cnn = Nothing Exit Function ExecuteSQL_Error: MsgString = "数据库操作失败:" & Err.Description Set ExecuteSQL = Nothing Resume ExecuteSQL_Exit End Function三个参数要点说明一下。SQL传完整语句,函数内部不再拼任何片段;MsgString是 ByRef 的返回信息,调用方拿它显示提示;返回值是 Recordset,非查询语句返回 Nothing,调用方必须先判空再判 EOF。
比起原稿,这里改了两处:Split(Trim$(SQL))保证首词干净,CursorLocation = adUseClient让记录集脱离连接后还能 MoveNext、还能取 RecordCount。这两处在第 5 章会展开说,是踩过坑才改的。
有一点必须提醒:InStr("INSERT,DELETE,UPDATE", ...)这种判断法有隐患,如果首词恰好是这些字符串的子串会误判。稳妥写法是显式比较:
Select Case UCase$(sTokens(0)) Case "INSERT", "UPDATE", "DELETE" cnn.Execute SQL MsgString = sTokens(0) & " 执行成功" Case Else ' 走查询分支 End Select4. 数据库设计:六张表怎么定字段、怎么建、怎么连
4.1 从需求分析推导字段清单
数据库需求分析在原稿里是按"谁需要存什么"列的,这个方法对:先列业务对象,再列对象属性,最后才谈表。六个对象分别是用户、学生、班级、课程、年级课程设置、成绩。
用户信息要存用户名、密码、用户描述三项。学生基本信息是字段最多的一张:学号、姓名、性别、出生日期、班号、联系电话、入校日期、家庭地址、备注。班级信息有班号、所在年级、班主任姓名、所在教室。课程基本信息有课程号、课程名称、课程类别、课程描述。年级课程设置只有年级和课程名两项,本质是一张关系表。成绩信息有考试编号、班号、学号、姓名、课程名、分数。
对照一下会发现两个设计意图:一是学生表用 class_NO 外键挂到班级,班级表用 grade 字段挂到年级,年级没有单独建表而是以字符串形式存在;二是成绩表里冗余存了 student_Name 和 class_No,本来通过学号 join 就能拿到。冗余的理由是查询方便、少一次关联,代价是改名后不同步,这个取舍在第 4.4 节细说。
4.2 E-R 关系:四类实体和它们的联系
概念结构设计在原稿里是五张 E-R 图,文字描述可以压缩成四句话:学生实体以学号为键,属性是姓名、性别、出生日期、电话、地址;班级实体以班号为键,属性是年级、班主任、教室;课程实体以课程号为键,属性是名称、类别、描述;年级实体以年级名为键。
联系有四种:班级与学生是一对多,一个班多个学生,一个学生只属于一个班;年级与班级是一对多,一个年级多个班;年级与课程是多对多,靠 gradecourse_Info 这张中间表落地,表里两个字段就是两个外键;学生与课程也是多对多,靠 result_Info 落地,成绩是这条联系上的属性。
画 E-R 图时容易犯的错是把"成绩"画成实体再连学生和课程,这样会多出一层。正确画法是学生和课程之间画菱形"选修/考试",成绩挂在菱形上,转成关系模型正好就是 result_Info 这张表。
4.3 建表脚本与字段类型取舍
逻辑结构转物理结构,原稿是在 SQL Server 查询分析器里逐条执行 CREATE TABLE。这里把它整理成一份可重复执行的脚本,顺带补上原稿缺的主键和索引:
-- 系统用户表 CREATE TABLE user_Info ( user_ID varchar(20) NOT NULL PRIMARY KEY, -- 登录名,主键 user_PWD varchar(32) NOT NULL, -- 密码,课设可明文,建议存哈希 user_Des varchar(20) NULL -- 角色标识:admin / teacher ); -- 学生基本信息表 CREATE TABLE student_Info ( student_ID int NOT NULL PRIMARY KEY, -- 学号,主键 student_Name varchar(20) NULL, -- 姓名,中文占 2 字节,别用 char(10) student_Sex char(2) NULL, -- 性别 born_Date datetime NULL, class_NO int NULL, -- 外键,指向 class_Info.class_No tele_Number varchar(20) NULL, ru_Date datetime NULL, -- 入校日期 address varchar(100) NULL, comment varchar(200) NULL ); CREATE INDEX IX_student_class ON student_Info(class_NO); CREATE INDEX IX_student_name ON student_Info(student_Name); -- 班级基本信息表 CREATE TABLE class_Info ( class_No int NOT NULL PRIMARY KEY, grade varchar(20) NULL, -- 所在年级,如 2005 director varchar(20) NULL, -- 班主任 classroom_No varchar(20) NULL ); -- 课程基本信息表 CREATE TABLE course_Info ( course_No int NOT NULL PRIMARY KEY, course_Name varchar(30) NULL, course_Type varchar(20) NULL, -- 必修 / 选修 course_Des varchar(100) NULL ); -- 年级课程设置表:年级与课程的多对多中间表 CREATE TABLE gradecourse_Info ( grade varchar(20) NULL, course_Name varchar(30) NULL, CONSTRAINT PK_gradecourse PRIMARY KEY (grade, course_Name) ); -- 学生成绩表 CREATE TABLE result_Info ( exam_No varchar(20) NOT NULL PRIMARY KEY, -- 考试编号 student_ID int NOT NULL, student_Name varchar(20) NULL, -- 冗余字段 class_No int NULL, -- 冗余字段 course_Name varchar(30) NULL, result decimal(5,1) NULL, -- 分数,保留一位小数 CONSTRAINT FK_result_student FOREIGN KEY (student_ID) REFERENCES student_Info(student_ID) ); CREATE INDEX IX_result_course ON result_Info(course_Name);类型选择上有三处值得讲。姓名从char(10)改成varchar(20):char 是定长,SQL Server 里 char(10) 指 10 个字节,一个汉字占 2 字节,只能存 5 个汉字,超过直接报"将截断字符串或二进制数据"。地址同理,varchar(50) 换成 varchar(100) 更稳。分数用 decimal(5,1) 而不是 float,浮点数做平均分会出现 78.33333 这种尾巴,展示时还得再 round 一次。考试编号用 varchar 而不是自增 int,是因为业务上它可能是"2005-2006-1-001"这种带语义的编号。
主键和索引是原稿没有的,必须补。没有主键的表,同一条学籍可以录三遍,删除时一次删三条,成绩表 join 出来直接翻倍。学号、班号、课程号加主键是底线,学生表的 class_NO 和成绩表的 course_Name 加普通索引,是查成绩、查班级名单时的性能保障。
4.4 数据一致性:冗余字段与查询口径
成绩表里存 student_Name 和 class_No,是为了列表展示时少一次 join,代价是学生改名后成绩表还留着旧名字。这个取舍在课设里可以接受,但要在文档里写明白,并且给出补救办法:要么写一个触发器在读的时候不依赖冗余字段,要么定期跑一次同步语句。
-- 学籍姓名变更后,同步成绩表里的冗余姓名 UPDATE r SET r.student_Name = s.student_Name FROM result_Info r INNER JOIN student_Info s ON r.student_ID = s.student_ID WHERE r.student_Name <> s.student_Name;查询口径也要统一。查某个班某门课的成绩单,正确写法是从学生表出发关联成绩表,而不是直接按成绩表里的 class_No 过滤,因为成绩表的 class_No 是录入时快照,学生转班后就不准了:
SELECT s.student_ID, s.student_Name, c.grade, c.class_No, r.course_Name, r.result FROM student_Info s INNER JOIN class_Info c ON s.class_NO = c.class_No INNER JOIN result_Info r ON s.student_ID = r.student_ID WHERE c.class_No = 3 AND r.course_Name = '数据库原理' ORDER BY r.result DESC;这个口径问题在答辩时被问到的概率不低,能答出"成绩表里的班号是快照,以学籍表为准"就很稳。
5. 详细设计:五大模块的窗体与代码落地
5.1 学籍管理模块:多条件组合查询怎么拼
学籍管理是这套系统里字段最多、操作最全的模块,做透了,其它四个模块基本是复制粘贴改字段。窗体布局是上方三个查询条件(学号、姓名、班级),中间一个 DataGrid 显示结果,下方添加、修改、删除三个按钮。
多条件查询的难点不在 SQL,在于"用户只填了一部分条件"。直接拼字符串很容易拼出WHERE AND student_ID = 1这种语法错误。可行的写法是先拼一串以 AND 开头的片段,最后把第一个 AND 换成 WHERE:
Private Sub cmdQuery_Click() Dim sql As String, where As String, msg As String Dim rst As ADODB.Recordset sql = "SELECT student_ID, student_Name, student_Sex, born_Date, " & _ "class_NO, tele_Number, ru_Date, address FROM student_Info" ' 每个非空条件追加一个 AND 片段,统一以 AND 开头 where = "" If Trim$(txtID.Text) <> "" Then where = where & " AND student_ID = " & Val(txtID.Text) End If If Trim$(txtName.Text) <> "" Then ' 姓名走模糊匹配,单引号做转义 where = where & " AND student_Name LIKE '%" & _ Replace(Trim$(txtName.Text), "'", "''") & "%'" End If If cboClass.ListIndex >= 0 Then where = where & " AND class_NO = " & Val(cboClass.ItemData(cboClass.ListIndex)) End If ' 有任意条件时,把第一个 " AND " 换成 " WHERE " If Len(where) > 0 Then sql = sql & " WHERE " & Mid$(where, 6) End If Set rst = ExecuteSQL(sql, msg) If rst Is Nothing Then Exit Sub Set grdStudent.DataSource = rst lblCount.Caption = msg End SubMid$(where, 6)是把前 5 个字符(空格 + AND + 空格)切掉。cboClass.ItemData取的是下拉项绑定的班号而不是显示文本,这样班级改名不影响查询。lblCount.Caption = msg把"查询到 N 条记录"显示在状态栏,用户对"没有结果"和"查询失败"能区分开,这算需求里"人机界面友好"的一条落地。
DataGrid 绑定 Recordset 只需一行Set grdStudent.DataSource = rst,前提是工程里引用了 Microsoft DataGrid Control 6.0 (OLEDB)。绑定后列头是字段名,中文列头在 DataGrid 的 Columns 属性里逐个改,或者在 SELECT 里用别名student_ID AS 学号,后者更省事但字段名带中文在代码里不好引用,建议用前者。
5.2 数据录入与修改:从 TextBox 到 Recordset 的字段映射
添加和修改共用一个窗体是最常见的省事做法,靠一个模块级变量EditMode区分。添加时清空所有输入框,修改时把选中行的值回填,保存时走不同分支。
Private Sub cmdSave_Click() Dim msg As String Dim rst As ADODB.Recordset ' 必填校验放在最前面,避免脏数据进库 If Trim$(txtID.Text) = "" Or Trim$(txtName.Text) = "" Then MsgBox "学号和姓名不能为空", vbExclamation, "提示" Exit Sub End If If EditMode = "ADD" Then ' 先查重,主键冲突要在库里拦之前拦下来,提示才友好 Set rst = ExecuteSQL("SELECT student_ID FROM student_Info WHERE student_ID=" & Val(txtID.Text), msg) If Not (rst Is Nothing) Then If Not rst.EOF Then MsgBox "该学号已存在", vbCritical, "提示" Exit Sub End If End If ExecuteSQL "INSERT INTO student_Info(student_ID, student_Name, student_Sex, " & _ "born_Date, class_NO, tele_Number, ru_Date, address) VALUES(" & _ Val(txtID.Text) & ",'" & Replace(Trim$(txtName.Text), "'", "''") & "','" & _ Trim$(cboSex.Text) & "','" & Format$(dtpBorn.Value, "yyyy-mm-dd") & "'," & _ Val(cboClass.ItemData(cboClass.ListIndex)) & ",'" & Trim$(txtPhone.Text) & "','" & _ Format$(dtpRu.Value, "yyyy-mm-dd") & "','" & _ Replace(Trim$(txtAddress.Text), "'", "''") & "')", msg Else ExecuteSQL "UPDATE student_Info SET student_Name='" & _ Replace(Trim$(txtName.Text), "'", "''") & "', student_Sex='" & _ Trim$(cboSex.Text) & "', class_NO=" & Val(cboClass.ItemData(cboClass.ListIndex)) & _ " WHERE student_ID=" & Val(txtID.Text), msg End If MsgBox msg, vbInformation, "提示" cmdQuery_Click ' 保存后立刻刷新列表,用户能看见结果 End Sub三个细节值得说。日期字段统一用Format$(..., "yyyy-mm-dd")转成字符串再拼,是因为中文系统默认日期格式是2006-12-1,直接& dtpBorn.Value拼进去,SQL Server 在某些语言设置下会认不出。学号在录入界面锁定不让改,修改时按学号定位,避免"改学号"这种主键变更带来的连锁问题。保存完主动调一次查询刷新列表,比弹一句"保存成功"然后让用户自己点查询体验好。
5.3 成绩管理模块:录入、统计与排名
成绩录入比学籍录入多一层依赖:课程下拉要从 gradecourse_Info 里按学生所在年级过滤,不能把全校课程都列出来。做法是学籍选定后,用学号查出 class_NO,再查出 grade,再查课程:
Private Sub LoadCourse() Dim msg As String Dim rst As ADODB.Recordset Set rst = ExecuteSQL("SELECT g.course_Name FROM gradecourse_Info g " & _ "INNER JOIN class_Info c ON g.grade = c.grade " & _ "INNER JOIN student_Info s ON s.class_NO = c.class_No " & _ "WHERE s.student_ID = " & Val(txtStudentID.Text), msg) cboCourse.Clear Do While Not rst.EOF cboCourse.AddItem rst!course_Name rst.MoveNext Loop End Sub统计是成绩模块里最容易出彩的部分。按班级按课程算平均分、最高分、最低分、及格人数,一条 GROUP BY 就够,SQL Server 2000 支持 CASE 表达式:
SELECT course_Name AS 课程, COUNT(*) AS 参考人数, CAST(AVG(result) AS decimal(5,1)) AS 平均分, MAX(result) AS 最高分, MIN(result) AS 最低分, SUM(CASE WHEN result >= 60 THEN 1 ELSE 0 END) AS 及格人数 FROM result_Info WHERE class_No = 3 GROUP BY course_Name ORDER BY 平均分 DESC;排名要单独说一下。SQL Server 2000 没有 ROW_NUMBER() 窗口函数,用子查询数"比我高的人有几个"来算名次,是那个年代的通行写法:
SELECT a.student_ID AS 学号, a.student_Name AS 姓名, a.result AS 分数, (SELECT COUNT(*) + 1 FROM result_Info b WHERE b.course_Name = a.course_Name AND b.result > a.result) AS 名次 FROM result_Info a WHERE a.course_Name = '数据库原理' AND a.class_No = 3 ORDER BY a.result DESC;这段 SQL 在答辩时是可以讲两分钟的:为什么不用窗口函数(版本不支持)、并列名次怎么处理(>会让同分并列同名次,>=会跳号)、数据量大时子查询的性能怎么优化(加 course_Name + result 的联合索引)。能说出这些,说明不是抄的。
5.4 用户管理与权限:角色表怎么加、菜单怎么控
原稿的用户表只有三个字段,用户描述被拿来当角色用。要真正做到"不同角色看不同菜单",最省事的改法是不动表结构,约定 user_Des 取值:admin 能看全部五个模块,teacher 只能看学籍、成绩、课程三个模块,看不到用户管理和班级管理的删除按钮。
主窗体加载时按角色隐藏菜单:
Private Sub MDIForm_Load() InitConnect ' 未登录不允许操作任何菜单 mnuUser.Enabled = False mnuClass.Enabled = False End Sub Public Sub ApplyRole() If CurrentRole = "admin" Then mnuUser.Enabled = True mnuClass.Enabled = True Else mnuUser.Enabled = False ' 教师角色看不到用户管理 mnuClass.Enabled = True End If End Sub菜单隐藏只是界面层,真正的权限要在数据层再挡一次:删除学籍的 SQL 执行前判断CurrentRole = "admin",非管理员直接返回。只做界面隐藏不做数据层校验,是这类系统里最常见的"看起来有权限,实际没有"。
用户密码这块,课设里明文存可以接受,但文档里要写清楚这是简化处理。稍微进一步的做法是存 MD5:
' 登录时对输入密码做一次 MD5,再与库中值比对 ' 引用 Microsoft 的 MD5 组件或自写函数均可,库中 user_PWD 长度改为 32改这一处不影响其它模块,答辩时能讲"密码不以明文存储"是一个明确的加分点。
6. 避坑与常见问题排查:五处最容易翻车的地方
6.1 现象:姓名保存后少了半个字,或者直接报"将截断字符串或二进制数据"
原因在字段长度。SQL Server 里char(10)的 10 是字节数不是字符数,一个汉字占 2 字节,char(10) 最多存 5 个汉字。原稿的 student_Name 是 char(10),三个字的姓名刚好 6 字节没问题,四个字的姓名 8 字节也塞得下,遇到五个字以上的姓名就截断,如果是 varchar 加 NOT NULL 还会直接抛错。地址用 varchar(50) 同理,25 个汉字就满。
解决办法是统一改成 varchar 并放宽长度:姓名 varchar(20)、地址 varchar(100)、课程名 varchar(30)。改完记得同步改窗体上 TextBox 的 MaxLength,否则界面上还能输进去,到库里才报错。这件事我一般会在建表阶段一次做完,后面改表要连带改 INSERT 语句里的字段顺序,很容易漏。
6.2 现象:同一个学号录了三遍,删一条成绩单跟着少三行
原因是建表时没写主键。原稿的 CREATE TABLE 里只有NOT NULL,没有PRIMARY KEY,数据库层面完全允许重复。用户手抖点两次保存,库里就是两条一模一样的记录;成绩表按学号 join 学生表,一条成绩变成三条;删除时DELETE FROM student_Info WHERE student_ID=1一次删三行,连带成绩全没。
解决是建表时把主键补上:user_Info 用 user_ID,student_Info 用 student_ID,class_Info 用 class_No,course_Info 用 course_No,gradecourse_Info 用 (grade, course_Name) 联合主键,result_Info 用 exam_No。补主键之后,重复插入会在数据库层报错,代码里再配合 5.2 节的查重提示,用户看到的是"该学号已存在"而不是一串英文错误。已经建好的表用ALTER TABLE student_Info ADD CONSTRAINT PK_student PRIMARY KEY(student_ID)补,补之前先清重复数据。
6.3 现象:查询明明有数据,提示却显示"查询到 0 条记录",翻页也翻不动
这是 ADO 游标类型的问题。默认的adOpenForwardOnly是只进游标,不支持 RecordCount,返回 -1;服务端游标下,连接一关记录集就废,后面再 MoveNext 直接报"对象关闭时不允许操作"。原稿代码里那行被注释掉的'rst.MoveLast 'get RecordCount就是在跟这个坑较劲。
解决是在打开记录集之前设CursorLocation = adUseClient,再用adOpenStatic+adLockReadOnly打开。客户端游标把数据一次性拉到本地,连接关了也能继续用,RecordCount 也准。代价是数据量大时占内存,几万条以内无所谓。另一种思路是完全不依赖 RecordCount,需要总数就单独发一条SELECT COUNT(*) FROM ...,各取所需。3.3 节的 ExecuteSQL 已经是改好的版本,直接抄不会踩这个坑。
6.4 现象:多条件查询报"第 1 行: 'AND' 附近有语法错误",或者姓名里带单引号直接崩
前一种是自己拼串拼出来的:第一个条件就是空,代码却照样追加" AND ...",最后得到WHERE AND xxx。后一种是用户输入了O'Brien这类带单引号的内容,拼进 SQL 变成name='O'Brien',字符串提前闭合。
解决分两层。结构层用 5.1 节的"先攒 AND 片段、最后 Mid 换成 WHERE",保证 WHERE 只会出现一次、后面一定跟着条件。内容层对每个字符串参数做Replace(s, "'", "''"),把单引号翻倍。更进一步是改用 ADO 的 Command 对象加参数:
' 参数化查询写法,彻底避免拼接 Dim cmd As New ADODB.Command cmd.ActiveConnection = cnn cmd.CommandText = "SELECT * FROM student_Info WHERE student_Name LIKE ?" cmd.Parameters.Append cmd.CreateParameter("name", adVarWChar, adParamInput, 20, "%" & txtName.Text & "%") Set rst = cmd.Execute课设里用拼接能跑通,但心里要清楚它的边界在哪,答辩被问"SQL 注入怎么防"时,答"当前用转义,正式环境改参数化"是标准答案。
6.5 现象:菜单点两次,任务栏里出现两个一样的子窗体,改了一个另一个不刷新
MDI 子窗体没有单例约束,每点一次菜单就Dim f As New frmStudent+f.Show一次,就多一个实例。两个实例各自持有一份数据,在 A 里改了学籍,B 的 DataGrid 还是旧数据,用户会以为没保存成功。
解决是打开前先检查有没有同类窗体存在,有就激活它:
Private Sub mnuStudent_Click() Dim f As Form For Each f In Forms If TypeOf f Is frmStudent Then f.SetFocus Exit Sub End If Next frmStudent.Show End Sub另一个相关的问题是连接未释放。ExecuteSQL 里每次都 New 一个 Connection,用完 Set Nothing,这是对的;但如果在窗体里自己 Open 了连接忘了关,SQL Server 的连接数会越积越多,演示到一半突然所有操作都超时。习惯是:除了 Module.bas,任何地方都不出现New ADODB.Connection。
7. 答辩与验收:用一张表把源码讲成一个项目
最后一步不是把程序跑起来给人看,而是让别人相信这套东西是你做的、你知道它哪里强哪里弱。我一般会准备一张验收表,从左到右是"功能点、操作路径、预期结果、实际结果",答辩现场照着点,比嘴上说"实现了增删改查"有说服力得多。
| 功能点 | 操作路径 | 预期结果 |
|---|---|---|
| 登录校验 | 输入错误密码点登录 | 提示用户名或密码错误,密码框清空并获焦 |
| 角色权限 | 用教师账号登录 | 用户管理菜单置灰,直接调接口也被拦截 |
| 学籍多条件查询 | 只填姓名片段,不填学号 | 返回模糊匹配结果,状态栏显示条数 |
| 学籍查重 | 录入已存在的学号 | 提示"该学号已存在",不写库 |
| 成绩统计 | 选班级 3,看统计列表 | 平均分与手算一致,及格人数正确 |
| 课程联动 | 学籍选定后打开成绩录入 | 课程下拉只出现该年级配置的课程 |
| 删除确认 | 点删除按钮 | 弹二次确认,取消则不删 |
表格右边那列"实际结果"现场填,填完签名,就是一份简易测试记录,附录里能直接用。
演示顺序按第 3.1 节的依赖关系走:先班级、再课程设置、再学籍、最后成绩。反过来点会看到空下拉框,评委第一印象就差了。每一步只演示一条数据,别现场录二十条,时间不够还容易翻车。
有几个问题几乎必问,提前把答案写在一张卡片上:为什么用 VB6,答案落在"课程要求 + 可视化 + ADO 成熟"三点,不要贬低新框架;数据库为什么是 SQL Server 2000 而不是 Access,答案落在"多用户并发和账号权限";成绩表为什么要冗余姓名,答案落在"减少关联查询、以学籍表为准、有同步语句兜底";用户名密码明文存怎么解释,答案落在"当前为教学简化,可改 MD5,表结构已预留长度"。
从那以后我每次交这类系统,都会在答辩前一晚按验收表自己走一遍,把每一条的截图存进一个文件夹。翻车过一次——现场点删除把唯一一条演示数据删了,后面所有功能都没数据可演,只能重启数据库重来。这个习惯从那以后就没断过,成本半小时,收益是上台不慌。希望帮到你。
本文还有配套的精品资源,点击获取