简介面向学习Visual Basic与数据库课程设计、毕业设计的学生这份资料围绕VB学籍管理系统展开可用于梳理学生信息、成绩、出勤与权限管理等模块的设计与实现思路。压缩包内共1个doc文档约523KB内容以毕业论文形式组织涵盖系统需求分析、可行性分析、系统设计、数据库需求与概念结构设计、详细设计等章节目录中还列出主窗体、用户管理、学籍管理、班级管理、课程设置和成绩管理等模块。已有436人学习适合需要参考课程设计文档框架、E-R图与数据表设计、窗体控件布局及ADO数据库交互思路的初学者也可帮助读者理解从需求分析到编码测试的完整开发流程为同类管理系统实践提供可借鉴的文档模板与模块划分参考。1. VB 学籍管理系统源代码真正值钱的东西不在窗体上很多人拿到一份 VB 学籍管理系统源代码第一反应是双击工程文件翻窗体看界面画得像不像、按钮摆得齐不齐。翻完会觉得平平无奇几个 TextBox、几个 CommandButton、一个 DataGrid配色还是那个年代的灰。真正值钱的东西不在这些看得见的地方而在一个叫 Module.bas 的公用模块里 —— 四十来行的ExecuteSQL函数把整个系统的连接管理、语句分发、错误捕获、结果集返回全兜住了。搞懂它等于搞懂了那批桌面管理系统的通用骨架为什么界面要用 MDI、为什么六张表就能撑起一套学籍、为什么一个char(10)的类型选择能埋下几年的坑。这套骨架今天看依然成立只是换了语言和框架重新长了一遍。2. 事件驱动与 MDI窗体不是页面是常驻内存的对象2.1 可视化编程被“封装”掉的到底是什么传统写 Windows 程序你得自己调CreateWindowEx注册窗口类、自己写消息循环GetMessage/DispatchMessage、自己在窗口过程里用switch分发WM_COMMAND。VB 干的事情是把这一整套东西折叠成三样东西属性表、事件过程、控件数组。你在设计器里拖一个按钮IDE 在 .frm 文件里生成一段Begin VB.CommandButton cmdQuery的属性块你双击按钮它给你补一个空的Private Sub cmdQuery_Click()。中间那层消息泵、窗口句柄、回调派发全被吃掉了。这件事的代价和收益都很明确。收益是开发速度一个增删改查界面半天能画完。代价是开发者对“谁在什么时候调用我”失去了直觉 —— 代码不是从上往下执行的是被用户点击、被窗体加载、被定时器触发这些外部事件“叫醒”的。看这段典型的查询按钮代码 学籍查询窗体用户点“查询”后触发不是程序启动时执行 Private Sub cmdQuery_Click() Dim rst As ADODB.Recordset Dim msg As String 把界面上的学号拼进 SQL交给公用函数统一执行 Set rst ExecuteSQL(SELECT * FROM student_Info WHERE student_ID _ Val(txtID.Text), msg) If rst Is Nothing Then Exit Sub 结果集指针停在第一条逐字段回填到界面控件 If Not rst.EOF Then txtName.Text rst!student_Name txtSex.Text rst!student_Sex txtClass.Text rst!class_NO dtpBorn.Value rst!born_Date Else MsgBox 没有找到该学号, vbInformation, 提示 End If End SubcmdQuery_Click里的语句顺序不等于运行时的时序。用户点了五次按钮这个函数就进五次每次都在栈上新建一份rst。这就是事件驱动的本质调用栈由外部消息决定函数只是挂在某个事件上的钩子。理解这一点后面所有关于“全局变量为什么危险”“窗体为什么不能重复实例化”的问题才有解释的根。2.2 MDI 主窗体一个容器如何管住所有子窗体这套系统选的是多文档界面MDI不是单文档SDI。差别不只是视觉SDI 里每个窗体都是独立顶层窗口各自带标题栏、各自能最小化到任务栏MDI 里有一个MDIForm当容器其余子窗体把MDIChild属性设为 True全部被“关”在父窗体的客户区里。当年选 MDI 的理由很实际。第一菜单只需要在父窗体上维护一份子窗体打开时菜单自动合并不用每个窗体画一遍“学籍管理/班级管理/课程设置/成绩管理”。第二状态栏、工具栏只有一处登录用户名显示在父窗体底部所有子窗体共享。第三用户心智上“一个系统就是一个窗口”切换模块不占任务栏。代价藏在实例管理里。MDI 子窗体按名字全局唯一同一时刻只应该存在一个frmStudent。如果用户在菜单上连点两下第二下要么复用已有实例要么报错。老代码里常见这种写法 菜单项点击先查 Forms 集合里有没有同名窗体有就激活没有才新建 Private Sub mnuStuManage_Click() Dim f As Form For Each f In Forms If f.Name frmStudent Then f.SetFocus 已存在抢焦点回到前台 Exit Sub 直接返回绝不重复 Show End If Next f frmStudent.Show 首次打开才真正创建窗口 End SubForms是 VB 运行时维护的全局集合装着当前所有已加载窗体。这段遍历干的事情等价于现代前端框架里的“路由守卫 单例组件”。不做这层判断会怎样每次Show都会新建一个窗体实例两个窗口改同一张表A 窗口看到的还是旧数据用户在两个窗口之间来回切看到的数据互相打架 —— 这就是当年最常见的“数据不刷新”投诉来源。2.3 窗体的生命周期与全局状态一个 VB 窗体的完整生命周期是Initialize对象诞生→Load窗口和控件被创建还没显示→Activate获得焦点→ 用户交互 →QueryUnload要关了可拦截→Unload窗口销毁→Terminate对象释放。这套顺序决定了代码该写在哪数据加载放Load关闭前确认放QueryUnload资源释放放Unload。更方便的是公用模块里的全局变量。整个工程任何地方都能读写于是登录信息天然就放这儿 Module.bas —— 全工程可见的全局状态 Public gUserName As String 当前登录用户名 Public gUserLevel As Integer 0 普通操作员1 管理员 Public gConnStr As String 数据库连接串程序启动时赋值一次gUserLevel是整个权限体系的地基菜单项的Enabled属性、按钮的可见性、甚至某些 SQL 的拼接都要读它。地基牢不牢不牢。任何一个窗体里的任意一行代码都能写gUserLevel 1编译器不会拦运行时也没人查。这是后文权限避坑那节的伏笔 —— 状态是全局的校验却只做在界面上。3. ExecuteSQL一个函数如何吃掉整个系统的数据库访问3.1 从 Split(SQL) 看“约定优于配置”的土办法整个系统几十个窗体、上百处数据库操作没有一处自己New ADODB.Connection全部走同一个入口。这个入口就是ExecuteSQLPublic Function ExecuteSQL(ByVal SQL As String, MsgString As String) _ As ADODB.Recordset SQL : 一条完整 SQL 语句必须以内层关键字开头、单空格分隔 MsgString: 出参回传执行结果描述给调用方弹提示 Dim CNn As ADODB.Connection Dim rst As ADODB.Recordset Dim sTokens() As String On Error GoTo ExecuteSQL_Error 关键一行按空格切分取第一个词当“语句类型”的判据 sTokens Split(SQL) Set CNn New ADODB.Connection CNn.Open ConnectString 首词命中写操作集合走无结果集分支 If InStr(INSERT,DELETE,UPDATE, UCase$(sTokens(0))) Then CNn.Execute SQL MsgString sTokens(0) query successful Else 其余一律当查询处理返回记录集 Set rst New ADODB.Recordset rst.Open Trim$(SQL), CNn, adOpenKeyset, adLockOptimistic Set ExecuteSQL rst MsgString 查询到 rst.RecordCount 条记录 End If ExecuteSQL_Exit: Set rst Nothing 只释放局部引用函数返回值仍持有对象 Set CNn Nothing 断开物理连接交还给连接池 Exit Function ExecuteSQL_Error: MsgString 查询错误 Err.Description Resume ExecuteSQL_Exit 出错也必须走清理路径 End FunctionSplit(SQL)这一行的意图很朴素我不知道调用方传的是读还是写那就看第一个单词。InStr在字符串里找子串命中返回位置非零即真于是INSERT/DELETE/UPDATE走Connection.Execute其余走Recordset.Open。这个设计是典型的“约定优于配置”函数不去解析 SQL 语法树而是要求所有调用方遵守一条纪律 —— SQL 必须以关键字开头、关键字后面跟一个空格、不能有前导空格或换行。守约就没事违约就翻车。比如有人写了 SELECT * FROM student_Info前面多一个空格Split出来的sTokens(0)是空字符串InStr返回 0被当成查询 —— 这次碰巧蒙对但如果写的是 UPDATE student_Info SET ...写操作被当成查询rst.Open一个 UPDATE 语句直接抛错。还有人习惯在 SQL 前面加注释-- 学籍查询 vbCrLf SELECT ...首词变成--同样误判。我更愿意把这行理解成“接口契约的价格”用一次字符串切分换掉了每个调用方各自判断语句类型的重复代码。代价是隐式约束且约束没有写在任何地方只能靠人口口相传。现代 ORM 里这一层被ExecuteNonQuery/ExecuteReader两个显式方法取代本质是把“猜”换成了“声明”。3.2 游标类型 adOpenKeyset 与锁模式 adLockOptimistic 的取舍rst.Open的后两个参数是这套代码里最少被讨论、影响却最大的两处。第一个是游标类型adOpenKeyset。ADO 提供四种adOpenForwardOnly只能往前滚、最省资源adOpenStatic拿一份静态快照别人改了你看不见adOpenDynamic完全动态别人增删改你实时可见代价最大adOpenKeyset居中 —— 它记住结果集里每一行的主键滚动时按主键回表重新取值所以能看到别人对“已有行”的修改但看不到别人新插入的行。放到学籍场景里就很好理解。教务员 A 正在翻 2005 级的学生列表教务员 B 同时改了某个学生的家庭地址。用 KeysetA 往下滚到那一行时会看到新地址用 StaticA 看到的永远是打开那一刻的快照改完不刷新就是错的。而“新增了一个学生”这种变化A 本来就要重新查询才能看到看不见反而不算问题。第二个是锁模式adLockOptimistic。乐观锁的意思是读的时候不加锁只在真正Update提交那一刻检查这行有没有被别人改过改过就报冲突。与之相对的adLockPessimistic是读到就锁住别人改不了 —— 在网络环境里等于把并发度掐死adLockBatchOptimistic是批量更新时才校验适合离线编辑。参数可选值本系统选择选择的理由CursorTypeForwardOnly / Static / Keyset / DynamicKeyset能看到他人对已有行的修改且不必承担 Dynamic 的开销LockTypeReadOnly / Pessimistic / Optimistic / BatchOptimisticOptimistic多人同时改同一行的概率极低不值得读时就锁CursorLocationServer / Client默认 Server数据量小服务端游标够用省客户端内存这套组合的隐含假设是冲突很少发生发生了让用户重试也能接受。对几十人规模的院系教务假设成立换成几千人同时在线的选课系统乐观锁会变成“提交十次失败八次”的灾难那时候要换的是整个并发模型不是一两个参数。还有个细节值得单独拎出来rst.RecordCount在 Keyset 游标下如果不把指针移到末尾返回值可能不准甚至为 -1。原代码里rst.MoveLast get RecordCount这行被注释掉了注释本身说明了作者踩过坑 —— 知道要MoveLast才准但为了性能又把它关了于是查询到 N 条记录这句提示里的 N有时候是错的。这种“知道有问题但选择忍”的注释比任何文档都诚实。3.3 资源释放与 On Error GoTo 的错误处理模型VB6 没有try/finally只有On Error GoTo 标签。这是跳转式错误处理出错就跳到指定标签没有栈展开没有自动清理finally里该干的活全得自己写在标签段里而且必须保证正常路径和异常路径都能走到那段代码。上面那段函数里的清理段就是标准解法ExecuteSQL_Exit: Set rst Nothing Set CNn Nothing Exit Function ExecuteSQL_Error: MsgString 查询错误 Err.Description Resume ExecuteSQL_Exit 跳到清理段而不是 Resume Next三行清理代码每一行都有讲究。Set rst Nothing减一次引用计数 —— 注意函数已经通过Set ExecuteSQL rst把对象引用交给了返回值所以这里置空只是释放局部变量持有的那一份对象本身不会销毁。这是 VB 引用计数语义的核心Set是引用赋值不是拷贝谁持有谁计数计数归零才析构。Set CNn Nothing关闭连接并归还连接池不写这行连接会一直挂着直到进程退出几十次查询之后数据库端的会话数就打满了。Resume ExecuteSQL_Exit和Resume Next的区别是关键。Resume Next从出错语句的下一行继续执行在这里会让流程穿过End If落到清理段看起来也能work但只要出错点之后还有别的语句就会在错误状态下继续跑属于埋雷。Resume 标签是明确说“我要跳到你指定的地方”语义干净。我一般会这么理解这段代码的价值它用三十行给整个系统建立了一个统一的事务边界 —— 进来的 SQL 要么执行成功并返回结果集要么失败并带回一句人话错误描述连接和记录集一定被释放。这套纪律一旦立住上面几十个窗体就可以放心地“只管拼 SQL不管连接”开发效率的提升全来自这里。4. 数据库设计六张表怎么把“学籍”这件事拆干净4.1 从 E-R 图到逻辑表实体和关系的落地学籍这件事抽出来的实体只有四个学生、班级、年级、课程。关系也不复杂一个班级属于一个年级n:1一个年级设置若干门课程m:n所以要一张中间表一个学生属于一个班级n:1一个学生修若干门课产生若干条成绩1:n。从 E-R 图落到物理表规则是死的实体变表属性变列1:n 关系把外键放在“多”的那一端m:n 关系单独建一张关联表。照着走六张表就出来了表名承担的职责关键字段关系定位user_Info系统用户与登录凭据user_ID, user_PWD独立表与业务无外键student_Info学生基本信息student_ID, class_NO多端class_NO 指向班级class_Info班级设置class_No, grade多端grade 指向年级course_Info课程字典course_No, course_Name被成绩表引用gradecourse_Info年级-课程设置grade, course_Namem:n 中间表result_Info成绩记录exam_No, student_ID, course_Name事实表最细粒度这张表最值得看的是gradecourse_Info和result_Info的位置。前者是“教学计划”回答“这个年级该开哪些课”后者是“教学结果”回答“这个学生这门课考了多少”。两者分开才可能出现“某学生修了一门不在计划里的课”这种异常被查出来。如果合并成一张表这个校验就永远做不了。4.2 建表语句里的类型选择char(10) 与 int 的博弈原文给了六段建表脚本挑学生表这段看CREATE TABLE [dbo].[student_Info] ( [student_ID] [int] NOT NULL, -- 学号整型主键 [student_Name] [char](10) COLLATE Chinese_PRC_CI_AS NULL, -- 姓名定长 10 字节 [student_Sex] [char](2) COLLATE Chinese_PRC_CI_AS NULL, -- 性别定长 2 字节 [born_Date] [datetime] NULL, [class_NO] [int] NULL, -- 所属班级逻辑外键 [tele_Number] [char](10) COLLATE Chinese_PRC_CI_AS NULL, -- 联系电话 [ru_Date] [datetime] NULL, -- 入校日期 [address] [varchar](50) COLLATE Chinese_PRC_CI_AS NULL,-- 家庭地址变长 [comment] [varchar](200) COLLATE Chinese_PRC_CI_AS NULL-- 备注 ) ON [PRIMARY] GO第一个坑在char(10)存姓名。SQL Server 里char(n)的 n 是字节数不是字符数在Chinese_PRC_CI_AS排序规则下一个汉字占 2 字节所以char(10)实际只能放 5 个汉字。汉族姓名基本够少数民族姓名、带父名的全名一进去就被截断而且默认设置下不报错只在结果里少几个字。这就是典型的“静默数据损坏”——查的时候才发现源头已经找不回来了。第二个坑在char(2)存性别。存“男”“女”刚好 2 字节没问题但如果后来有人改成存M/Fchar会补空格变成M 。SQL Server 比较时忽略尾部空格所以WHERE student_SexM查得到但界面显示、导出 Excel、拼进字符串里那个空格就跟了一路。第三个坑是student_ID用int。当年设计者想的是学号就是数字用整型还能排序、还能当主键一举两得。问题是真实学号经常带前导零2005001存进去变成2005001无所谓但05001就变成5001或者带字母前缀S2005001一录入就类型转换失败。更麻烦的是导出报表要和教务处的口径对齐前导零没了对不上账。第四个是result字段用了float。浮点数存分数87.5这类二进制可精确表示的值没问题但一旦涉及平均分累加0.1 0.2 ! 0.3的老问题就出来了排名算出来差一位谁都说不清。分数这种场景decimal(5,2)才是正解整数部分三位、小数两位精确到分累加不丢精度。varchar(50)存地址反而是对的 —— 地址长度差异大用变长类型不浪费空间。同一张表里char和varchar混用说明设计者是懂“定长用于长度稳定的字段、变长用于长度浮动的字段”这条原则的只是在“字节 vs 字符”这个中文环境下特有的坑上栽了。4.3 gradecourse_Info一个没有主键的中间表说明了什么看这张表的建表语句CREATE TABLE [dbo].[gradecourse_Info] ( [grade] [char](10) COLLATE Chinese_PRC_CI_AS NULL, [course_Name] [char](10) COLLATE Chinese_PRC_CI_AS NULL ) ON [PRIMARY] GO两个字段都可空没有主键没有唯一约束没有索引。它表达的业务含义是“某年级设置了某门课”逻辑上(grade, course_Name)应该是唯一的。但数据库层面没有任何东西保证这一点。后果是连锁的。假设教务员手滑把“2005级-高等数学”录了两遍。查某个年级的课程列表高数出现两次界面看着别扭但还能忍。真正的问题出在 JOIN 上成绩表按course_Name关联课程设置表算平均分时一门课匹配到两行这门课的所有成绩被复制一份班级平均分直接翻倍。这种错误不会报异常只会让报表数字对不上排查起来极其费劲 —— 因为从界面上看课程列表“就是有两行高数”没人觉得那是 bug。补救办法是补一个复合主键把业务规则变成数据库规则-- 先清掉历史重复数据否则加约束会直接失败 DELETE FROM gradecourse_Info WHERE ctid NOT IN (SELECT MIN(ctid) FROM gradecourse_Info GROUP BY grade, course_Name) -- 再加复合主键从此重复插入会被数据库拒绝 ALTER TABLE [dbo].[gradecourse_Info] ADD CONSTRAINT PK_gradecourse PRIMARY KEY ([grade], [course_Name])更深的耦合在字段选择上。成绩表result_Info里存的是course_Name字符串而不是course_No整型意味着课程一旦改名历史成绩和课程设置之间的链接就断了。正常的做法是成绩表只存course_No课程名通过 JOIN 取。当年这么写图的是查询时少一次 JOIN、界面绑定 DataGrid 时直接显示中文列 —— 用耦合换开发速度在单机小系统里是划算的在需要长期维护的系统里是负债。5. 避坑与排查这套老代码最容易翻车的五个地方5.1 环境与连接换台机器就跑不起来现象在自己的机器上跑得好好的工程拷到同事电脑上一登录就弹“未找到提供程序”或“无法打开登录所请求的数据库”代码一行没改。原因连接串是硬编码在Module.bas里的常量里面写死了服务器名、实例名、数据库名甚至账号密码。换机器等于换环境连接串里的每个要素都可能对不上。另一种情况是工程引用了特定版本的 ADO 类型库msado15.dll目标机器上的组件版本不同New ADODB.Connection在运行时创建失败。解决把连接串从代码里挪出去放到同目录的配置文件里路径用App.Path拼相对路径程序跟着目录走而不是跟着机器走。组件版本问题的通用解法是改成后期绑定CreateObject(ADODB.Connection)不依赖编译期引用运行时找注册表里任意可用版本代价是没有智能提示、常量要自己定义。5.2 数据完整性主键和外键没跟上业务现象删掉一个班级之后原来属于这个班的学生在学籍列表里班级那一列变成空白点“修改”保存时报错但错误信息只有一句“操作失败”。原因六段建表脚本里一条FOREIGN KEY都没有。student_Info.class_NO指向class_Info.class_No这件事只存在于设计者的脑子里和论文的 E-R 图里数据库完全不知道。删班级不会被拦学生行里的class_NO变成一个悬空值后续 JOIN 查班级名自然查不到。解决先在应用层补引用检查删除前SELECT COUNT(*) FROM student_Info WHERE class_NO ?计数不为零就提示用户先处理学生再把约束交给数据库加外键并明确删除策略ON DELETE NO ACTION表示有引用就拒绝删除。应用层的检查是给用户看的提示数据库的约束才是真正的兜底 —— 前者可能被绕过后者绕不过去。现象新生学号录入后导出的报表和教务处的台账对不上前导零消失或者录入带字母的学号时直接报类型转换错误。原因student_ID定义成int。整型不保存前导零也无法容纳非数字字符。解决主键类型改成varchar(20)格式校验放到应用层的LostFocus事件里用正则或者逐字符判断拦住非法输入。改类型这种事越早越好等成绩表里已经有几千条按student_ID关联的记录再改就得停下来做数据迁移。5.3 权限与输入界面挡住了数据库没挡住现象普通操作员账号登录后菜单上看不到“用户管理”这一项但某天发现有人把密码改了。原因权限控制只做在菜单项的Enabled False或Visible False上。菜单藏了窗体还在工程里frmUser.Show照样能调出来窗体内部的保存逻辑没有任何身份校验。全局变量gUserLevel虽然是权限的依据但它只在设置菜单时被读了一次进入窗体后就不再检查。解决在敏感窗体的Form_Load里做二次校验gUserLevel不满足直接Unload Me并提示无权限。原则是界面上的隐藏是体验不是安全真正的校验必须发生在数据被修改的那一层。现象登录框里输入 OR 11密码随便填居然进去了。原因登录 SQL 是字符串拼接出来的用户输入被当成 SQL 语法的一部分解析。拼接后的语句变成WHERE user_ID OR 11 AND user_PWD...恒真条件让Recordset返回了第一行用户记录登录判断只看“有没有记录”而不是“密码是否匹配”于是通过。解决改用ADODB.Command参数化查询让驱动把输入当数据而不是语法 参数化登录校验输入内容永远只是值不参与 SQL 解析 Dim cmd As ADODB.Command Set cmd New ADODB.Command Set cmd.ActiveConnection CNn cmd.CommandText SELECT COUNT(*) FROM user_Info WHERE user_ID? AND user_PWD? 两个 ? 按顺序绑定长度和类型由 CreateParameter 声明 cmd.Parameters.Append cmd.CreateParameter(u, adVarChar, adParamInput, 10, txtUser.Text) cmd.Parameters.Append cmd.CreateParameter(p, adVarChar, adParamInput, 10, txtPwd.Text) Set rst cmd.Execute If rst.Fields(0).Value 0 Then 计数大于零才算登录成功 gUserName txtUser.Text End If参数化还有一个附带好处adVarChar声明了长度 10超长输入会被截断或报错而不是原样拼进语句里。6. 设计取舍这套“土办法”为什么在当年是对的站在今天的角度看这套代码几乎每一处都能挑出毛病两层架构、业务逻辑散落在窗体事件里、SQL 字符串拼接、没有事务、没有单元测试、类型设计粗糙。但如果把时间拨回去这些选择大多是有理由的。两层架构的代价是维护性收益是开发速度。客户端只管画界面和拼 SQL服务端只有数据库中间没有应用服务器要部署、没有接口要联调。一个院系几十个用户、局域网内访问、数据量几万行三层架构带来的复杂度远大于它解决的问题。真正让两层撑不住的是业务规则变复杂之后 —— 同一条“成绩录入要校验选课关系”的规则散落在三个窗体的按钮事件里改一次要翻三个 .frm还容易漏。这才有了后来把规则往中间层收的动机。ExecuteSQL这个统一入口本质是一次早期的“抽象尝试”把连接管理、错误提示、结果集返回收敛到一处。它的失败之处不在抽象本身而在于抽象得太浅 —— 用字符串首词猜语句类型是把语义判断交给了格式约定。现代 ORM 里对应位置的ExecuteNonQuery/ExecuteReader做的还是同一件事只是把“猜”换成了“调用方声明”。这个演进方向说明能用显式接口表达的就不要用隐式约定。至于数据库约束的留白更像是工程节奏的妥协。建表脚本是先跑通、再补的主键和外键如果一开始就加满插入测试数据时会不断被拦开发速度会掉下来。于是先留白、后补约束补的时候发现已经有脏数据加不上去就只能继续留白。这是一条我从这套代码里读出来的血泪经验约束欠下的债只会越滚越大越晚补越贵。从那以后我每次接手这类老工程都强制先走一遍三件事把连接串从代码里抽出来、把所有表的主键和外键补全、把所有“界面级权限”下沉到数据修改的入口。三件事做完系统不一定变好看但至少不会在你不知道的地方悄悄出错。这份源代码里Module.bas的公用函数、六段建表脚本、学籍/班级/课程/成绩四个模块的完整窗体代码都在对照着上面这些分析去翻能看清很多当年没写进论文的设计细节。希望帮到你。本文还有配套的精品资源点击获取