简介围绕图书管理系统展开的软件工程课程设计文档面向计算机相关专业学生、软件工程初学者及需要完成课程设计或毕业设计的人群帮助梳理从需求分析到系统测试的完整开发脉络。文档以1个doc文件交付压缩包约535KB体量轻便适合作为模板参考与写作框架借鉴。内容按绪论、需求分析、数据库设计、系统实现等章节组织包含系统目标与功能说明、读者借阅与管理员处理借还书的用例图、系统时序图、参与者类及其关系、E-R图与数据表创建、数据库连接以及以登录窗体为例的详细设计思路和VB6.0、Rational ROSE等工具环境说明。已有442人学习可用于对照课程设计规范、补齐UML建模与数据库设计环节并理解三层架构下的实现过程。1. 一份课程设计文档能撑起多少东西图书管理系统软件工程的交付边界很多人拿到「图书管理系统 软件工程」这类资料第一反应是当成交作业的 Word 文档翻两页就扔进硬盘吃灰。我最初也这么想直到照着里面的 UML 图、表结构和 ADO 连接示例用 VB6.0 把登录和借还书这条主链路点通才发现它的价值不在文字说明而在结构需求分析、用例图、时序图、类图、E-R 图、建表语句、数据库连接、窗体设计一路串下来是一条完整的落地链。它适合两类人一类是要交软件工程课设、需要照模板补齐建模与文档的在校开发者另一类是刚接触数据库应用、想搞明白「文档阶段画的图怎么变成能点开的界面」的入门者。下面按这条链逐段拆能直接抄的地方我会标明。2. 需求与 UML 建模三类参与者和六张类图怎么翻译成代码原文的建模部分用的是瀑布模型的顺序先需求描述再用例图再时序图最后类图。这个顺序本身没什么争议关键在于每一张图后面都要能对上至少一个窗体或一个类模块否则这些图就只是文档装饰。我把原文的需求定义重新排了一遍落到可以写代码的粒度上。2.1 参与者划分与用例到窗体的映射原文把参与者切成读者、图书管理员、系统管理员三类每类各配一张用例图。这种切法的好处是权限边界天然清楚坏处是读者和图书管理员的用例有重叠比如查图书信息两边都要用。实际做的时候我建议按「共用查询窗体 角色决定按钮可见性」的方式落地而不是给每类角色各做一套查询界面否则窗体数量会翻倍。读者侧的用例是登录、查个人信息、查图书、借阅、续借、归还。图书管理员侧只有两个核心用例处理借书、处理还书。系统管理员侧是增删改图书、增删改读者、系统状态维护。这三组用例对应到窗体可以列成下面这张映射表写代码之前先把这张表定下来后面窗体命名就不会乱。用例参与者对应窗体主要控件系统登录全部frmLogin两个 TextBox 一个 CommandButton图书查询读者/管理员frmBookQueryMSFlexGrid 条件输入框借阅/归还读者/管理员frmBorrow、frmReturn借书证号、图书编号输入框图书维护系统管理员frmBookMgr表格 增删改按钮组读者维护系统管理员frmReaderMgr表格 增删改按钮组用例之间还有一层关系原文没细写但必须补借书、还书、续借三个用例都 include 登录用例也就是说这三个操作执行前必须先有有效会话。在 VB6 里这个约束最常见的实现方式是全局变量存当前登录用户的证件号主窗体加载时判断这个变量是否为空为空就踢回登录窗体。 模块 basSession.bas —— 全局会话状态所有窗体共用 Public gCurrentCardNo As String 当前登录读者/管理员的证件号 Public gCurrentRole As Integer 0读者 1图书管理员 2系统管理员 Public gCurrentName As String 各业务窗体 Form_Load 里的统一守卫 Private Sub Form_Load() If Len(gCurrentCardNo) 0 Then MsgBox 会话已失效请重新登录, vbExclamation, 提示 Unload Me frmLogin.Show Exit Sub End If 下面是正常的界面初始化 End Sub这段代码里gCurrentCardNo是全局会话标识gCurrentRole决定后面按钮的启用状态。参数说明一下gCurrentRole用整型而不是字符串是为了在 Select Case 里做跳转更快也避免字符串比较拼错。Form_Load里的守卫必须在任何数据加载之前执行否则会出现「用户已经退出但界面还在查数据」的时序错乱。2.2 类图落地从六个类到 VB6 类模块原文的类图列出了六个核心类reader读者、librarian图书管理员、administrator系统管理员、item书目、title书名、loan借出记录。参与者相关的三个类属性比较完整实体类的属性在导出时出现了重复和类型错位比如同一行里连着写两个「图书名称:string」作者字段被标成 int。这是 UML 工具导出到 Word 时常见的乱序照着抄会踩坑我按语义重新整理了一遍。在 VB6 里「类」对应的是 .cls 类模块。属性用 Public 成员变量声明最省事需要做校验的属性再用 Property Get/Let 包一层。方法直接写成 Public Function 或 Public Sub。 类模块 clsReader.cls —— 读者实体 Public ReaderID As String 证件号码对应 user 表主键 Public ReaderName As String Public ReaderSex As String 只存 男/女 Public ReaderAge As Integer Public BorrowedCount As Integer 已借书数目借还书时同步更新 Public Phone As String Public Address As String 查询本人借阅明细返回记录集由调用方负责关闭 Public Function QueryBorrowInfo() As ADODB.Recordset Dim sql As String sql SELECT bannercode, borrowdate, receivedate, renew _ FROM borrow WHERE user_cardno ReaderID Set QueryBorrowInfo OpenRecordset(sql) End FunctionclsReader里最关键的是BorrowedCount这个冗余字段。严格按第三范式它不该存在于 user 表但每次借书都去 count 一遍 borrow 表在小数据量下也无所谓量大了就是性能坑。原文的 user 表没有这个字段我建议加上用借书/还书事务里同步增减的方式维护读的时候直接取值不用聚合。clsBook对应原文的 item 和 title 两个类。原文把「书目」和「书名」拆成两个类意图是想表达「一种书有多本副本」的关系但表设计里又把这层关系压成了一张 book 表加一个 state 字段图和表对不上。我的处理是类层保留 item副本和 title书目两层表层用 book 表 一个 copy_no 字段近似实现够用别在课设规模上过度设计。类之间的关系在原文里标了1 n这样的多重性。reader 和 loan 是一对多title 和 item 是一对多这个关系在 VB6 里没有 ORM 帮你维护全靠外键和手动赋值。写的时候我一般在clsLoan里存一个ReaderID和一个BannerCode需要联动时用这两个字段去查而不是让对象互相持有引用否则循环引用和内存释放会很难收拾。2.3 时序图里的一次借书请求原文给了系统管理员添加图书的时序图三个消息add item()、find(string)、create(integer, title)。这条时序翻译过来就是「先查重不存在才插入」的标准流程。借书请求原文没画但逻辑同构我按同样的时序展开一次方便对照。读者点「借阅」之后时序是这样的界面收下借书证号和图书编号 → 校验证件状态是否有效 → 校验该书 state 是否为「在馆」→ 校验该读者已借数量未超上限 → 写 borrow 表 → 更新 book 表 state 为「借出」→ 返回结果。这七步里任何一步失败都要中断并回滚否则就会出现「借书记录写了但书还显示在馆」这种经典脏数据。 借书主流程返回值 0成功其余为错误码 Public Function DoBorrow(ByVal cardNo As String, _ ByVal bannerCode As String) As Integer Dim conn As ADODB.Connection Dim rs As ADODB.Recordset Dim maxCount As Integer Dim sql As String Set conn GetConnection() On Error GoTo ErrHandler conn.BeginTrans 事务开始后面任一步失败全部回滚 第 1 步校验读者状态 sql SELECT user_state FROM [user] WHERE user_cardnumber cardNo Set rs conn.Execute(sql) If rs.EOF Then DoBorrow 1: GoTo CleanUp 读者不存在 If rs(user_state) 正常 Then DoBorrow 2: GoTo CleanUp 第 2 步校验图书是否在馆 sql SELECT state FROM book WHERE bannercode bannerCode Set rs conn.Execute(sql) If rs.EOF Then DoBorrow 3: GoTo CleanUp If rs(state) 在馆 Then DoBorrow 4: GoTo CleanUp 第 3 步写入借书记录 sql INSERT INTO borrow(user_cardno,bannercode,borrowdate,renew) _ VALUES( cardNo , bannerCode ,Date(),否) conn.Execute sql 第 4 步回写图书状态 conn.Execute UPDATE book SET state借出 WHERE bannercode bannerCode conn.CommitTrans DoBorrow 0 GoTo CleanUp ErrHandler: conn.RollbackTrans DoBorrow 99 CleanUp: If Not rs Is Nothing Then If rs.State adStateOpen Then rs.Close Set rs Nothing End If Set conn Nothing End Function代码里BeginTrans/CommitTrans/RollbackTrans三个调用是整段的骨架缺了任何一个都会在异常路径上留下半截数据。错误码从 0 到 99 的约定界面层可以直接映射成「读者不存在」「已超上限」这类提示文案不用把 SQL 错误抛给用户。Date()是 VB6 取系统日期的函数代价是服务器和客户机时间不一致时会出问题稳妥做法是用 SQL 里的GETDATE()但那会牺牲一点数据库可移植性课设规模无所谓。3. 数据库设计8 张表、E-R 图和 ADO 连接的那点事数据库部分是原文里信息密度最高的一段建表语句、字段类型、主外键关系都给了。但列表里只完整写出了 7 张表book、borrow、login、punishment、user、libraryer、admin而正文里明确说「需建立 8 个表」。第 8 张是哪一张从 book 表的kindno字段和需求里「书籍类别标准的制定」这条功能反推缺的应该是分类表 kind。这是文档类资料的常见毛病图和表对不齐得靠字段间的引用关系自己补全。3.1 表结构逐张过一遍book 表是核心表字段最多。原文把主键标成bookname书名这个设计会直接导致重名图书无法录入同名不同作者的书写不进去改名也很麻烦。我建议主键换成bannercode图书编号书名只做普通字段。原文里bannercode标的是「唯一约束」说明作者本意也想让它唯一只是没提到主键位置。建表语句按调整后的写-- 图书表bannercode 替代 bookname 作主键 CREATE TABLE book ( bannercode VARCHAR(30) NOT NULL PRIMARY KEY, -- 图书编号唯一 bookname VARCHAR(30) NOT NULL, -- 书名允许重复 kindno VARCHAR(20) NULL, -- 分类号外键指向 kind kindname VARCHAR(20) NULL, -- 分类名冗余存储减少联表 author VARCHAR(20) NULL, publishing VARCHAR(30) NULL, publishtime DATETIME NULL, putintime DATETIME NULL, -- 入库日期 price NUMERIC(10,2) NULL, state VARCHAR(10) DEFAULT 在馆, -- 在馆/借出/遗失 introduction VARCHAR(200) NULL );state字段用了中文字符串而不是 0/1/2 编码好处是查询结果直接可读坏处是以后多语言或者状态扩展时得改表。课设阶段这个取舍可以接受真做产品我一般会换成 tinyint 加字典表。另外kindname冗余在 book 表里理论上违反规范化但分类名几乎不变冗余能省掉一次联表实际项目中这种冗余很常见。borrow 表记录借还流水原文的主键标的是user_cardno这明显有问题一个读者会借很多次书证件号做主键只能存一条记录。正确做法是用自增 id 或者 (证件号, 图书编号, 借书日期) 联合主键。CREATE TABLE borrow ( id INT IDENTITY(1,1) PRIMARY KEY, -- 自增流水号 user_cardno VARCHAR(20) NOT NULL, bannercode VARCHAR(30) NOT NULL, borrowdate DATETIME NOT NULL DEFAULT GETDATE(), receivedate DATETIME NULL, -- 为空表示尚未归还 renew VARCHAR(4) DEFAULT 否, CONSTRAINT fk_borrow_user FOREIGN KEY(user_cardno) REFERENCES [user](user_cardnumber) );receivedate为空是「未归还」的判定依据这个写法比单独加一个is_returned标志位更严谨因为归还日期本身就是一个业务事实。查询「当前借出未还」的语句就是WHERE receivedate IS NULL。user、libraryer、admin 三张表结构相似都是「编号 姓名 性别 地址」的组合区别在表名和权限。login 表存账号密码主键user_name和 user 表通过证件号或用户名的命名规则对应。punishment 表存超期罚款字段是证件号加金额。kind 表是分类字典两个字段kindno、kindname。E-R 图部分原文给的是实体间的连线reader 到 loan 是一对多title 到 item 是一对多book 到 kind 是多对一。落到 SQL 上就是上面那几个外键约束。建议建表的时候把外键约束一次性写全别等到测试阶段再补补的时候存量数据不合规就加不上去了。3.2 VB6 里用 ADO 连接 SQL Server 的正确姿势原文 3.4 节的数据库连接代码是整份文档里最容易让人翻车的一段。它写的是using System; using System.Data; using System.Data.SqlClient; public class ConnectionClass { ... }这是 C# 的命名空间引用跟 VB6.0 环境完全不匹配大概率是从别处粘过来凑篇幅的。VB6 连 SQL Server 走的是 ADO 对象模型不是 .NET 的 SqlClient。照着这段敲编译第一步就过不去。VB6 里正确的做法是引用 Microsoft ActiveX Data Objects 2.x 库然后声明 Connection、Recordset、Command 三个对象。我把连接部分封装成一个公共模块所有窗体都调它 模块 basDB.bas —— 全局数据库访问 Public gConn As ADODB.Connection 获取全局连接单例模式避免每个窗体重复打开 Public Function GetConnection() As ADODB.Connection Dim connStr As String If gConn Is Nothing Then Set gConn New ADODB.Connection 连接串建议放到外部文件方便换库不重编译 connStr ProviderSQLOLEDB;Data Source127.0.0.1; _ Initial CatalogLibraryDB;User IDsa;Password你的密码; gConn.ConnectionString connStr gConn.CursorLocation adUseClient 客户端游标断开连接后仍可遍历 gConn.Open End If Set GetConnection gConn End Function 统一的查询入口调用方负责后续 Close Public Function OpenRecordset(ByVal sql As String) As ADODB.Recordset Dim rs As New ADODB.Recordset rs.Open sql, GetConnection(), adOpenStatic, adLockReadOnly Set OpenRecordset rs End FunctionProviderSQLOLEDB是 SQL Server 的 OLE DB 提供程序Data Source填实例地址Initial Catalog填库名。CursorLocation adUseClient这一行很关键服务端游标在记录集打开期间会一直占着连接窗体一多就会把连接池耗尽切到客户端游标后记录集加载完就释放连接。原文里还提到用StreamReader读F://conn.txt拿连接串思路是对的硬编码连接串换台机器就得改代码重编译。但路径写死成 F 盘换机器必然找不到文件。我一般改用App.Path拼相对路径 从程序目录读连接串避免盘符写死 Public Function LoadConnStr() As String Dim fso As Object, ts As Object, p As String Set fso CreateObject(Scripting.FileSystemObject) p App.Path \conn.txt If Not fso.FileExists(p) Then MsgBox 缺少连接配置文件 p, vbCritical End End If Set ts fso.OpenTextFile(p, 1) LoadConnStr ts.ReadLine ts.Close End FunctionApp.Path是 VB6 内置的当前可执行文件目录打包发布后自动指向安装目录不用操心盘符。Scripting.FileSystemObject是后期绑定不需要在工程里加引用部署时少一个依赖。3.3 还书与超期判定的实现还书比借书多两步判定是否超期、超期就写罚款记录。原文在需求里提了「对超期的情况能自动给出提示信息」但没写怎么算。常见的借阅期限是 30 天超期按天计费我按每天 0.1 元这个常见额度写。 还书处理返回超期天数0 表示未超期 Public Function DoReturn(ByVal cardNo As String, _ ByVal bannerCode As String) As Integer Dim conn As ADODB.Connection, rs As ADODB.Recordset Dim borrowDate As Date, days As Integer, overDays As Integer Const LIMIT_DAYS As Integer 30 借阅期限 Const FINE_PER_DAY As Double 0.1 每日罚金 Set conn GetConnection() conn.BeginTrans 取最早一条未归还记录 Set rs conn.Execute(SELECT TOP 1 id, borrowdate FROM borrow _ WHERE user_cardno cardNo AND bannercode bannerCode _ AND receivedate IS NULL ORDER BY borrowdate) If rs.EOF Then DoReturn -1: GoTo CleanUp 没有待还记录 borrowDate rs(borrowdate) days DateDiff(d, borrowDate, Date()) overDays IIf(days LIMIT_DAYS, days - LIMIT_DAYS, 0) 回填归还日期 conn.Execute UPDATE borrow SET receivedateGETDATE() WHERE id rs(id) 图书状态改回在馆 conn.Execute UPDATE book SET state在馆 WHERE bannercode bannerCode 超期则写罚款 If overDays 0 Then conn.Execute INSERT INTO punishment(user_cardnumber,sum_account) _ VALUES( cardNo , overDays * FINE_PER_DAY ) End If conn.CommitTrans DoReturn overDays GoTo CleanUp CleanUp: If Not rs Is Nothing Then If rs.State adStateOpen Then rs.Close Set rs Nothing End If Set conn Nothing End FunctionLIMIT_DAYS和FINE_PER_DAY用 Const 定义在函数顶部参数化之后改规则只动一行。DateDiff(d, ...)算两个日期之间的天数注意是按自然日算跨零点就算一天如果要按 24 小时精确算得换成h再除。超期天数同时作为返回值带给界面层提示文案里直接拼进去就行。4. VB6.0 工程搭建窗体、模块、类模块怎么组织4.1 工程结构和模块划分VB6 的标准 EXE 工程里只有两类文件值得关心.frm 窗体文件和 .bas/.cls 代码模块。原文在目录里列了「创建工程项目」「为系统添加模块」「添加类模块」「界面设计」四节但没给工程结构。我按功能把工程拆成下面这样LibrarySys.vbp 工程文件 ├── Forms/ 窗体 │ ├── frmLogin.frm 登录 │ ├── frmMain.frm 主界面角色决定菜单可见性 │ ├── frmBookQuery.frm 图书查询读者和管理员共用 │ ├── frmBorrow.frm 借书 │ ├── frmReturn.frm 还书 │ ├── frmBookMgr.frm 图书维护仅系统管理员 │ └── frmReaderMgr.frm 读者维护仅系统管理员 ├── Modules/ 标准模块 │ ├── basDB.bas 连接与公共查询 │ ├── basSession.bas 全局会话状态 │ └── basUtil.bas 日期格式化、输入校验 └── Classes/ 类模块 ├── clsReader.cls ├── clsBook.cls └── clsLoan.cls工程结构的核心原则是窗体只管界面和事件业务逻辑一律下沉到类模块数据库访问全部收口到 basDB。这样改业务规则只动类模块改界面只动窗体两边不会互相污染。原文把详细设计思路放在「以登录窗体为例」这一节说明作者也是这个思路只是没把整体结构写出来。4.2 登录窗体的详细设计登录窗体的逻辑分四步取账号密码、查 login 表校验、判断用户类型、按角色跳转主界面。表单上两个 TextBox用户名、密码、一个 CommandButton登录、一个 Label 显示错误提示。密码框的PasswordChar属性设成*这一条原文没写但必须做。 frmLogin.frm Private Sub cmdLogin_Click() Dim rs As ADODB.Recordset Dim sql As String 前端先做非空校验避免空查询打数据库 If Trim(txtUser.Text) Or Trim(txtPwd.Text) Then lblMsg.Caption 用户名和密码不能为空 Exit Sub End If 参数用单引号包裹注意这里只做了简单拼接 sql SELECT psw FROM login WHERE user_name Trim(txtUser.Text) Set rs OpenRecordset(sql) If rs.EOF Then lblMsg.Caption 用户不存在 ElseIf rs(psw) Trim(txtPwd.Text) Then lblMsg.Caption 密码错误 Else 校验通过判断角色证件号前缀区分 gCurrentCardNo Trim(txtUser.Text) gCurrentRole GetRole(gCurrentCardNo) gCurrentName GetNameByCard(gCurrentCardNo) Unload Me frmMain.Show End If If Not rs Is Nothing Then If rs.State adStateOpen Then rs.Close Set rs Nothing End If End Sub 角色判定先查管理员表再查图书管理员表最后默认读者 Private Function GetRole(ByVal card As String) As Integer Dim rs As ADODB.Recordset Set rs OpenRecordset(SELECT ID FROM admin WHERE Adminname card ) If Not rs.EOF Then GetRole 2: GoTo Done rs.Close Set rs OpenRecordset(SELECT ID FROM libraryer WHERE libr_name card ) If Not rs.EOF Then GetRole 1: GoTo Done GetRole 0 Done: If Not rs Is Nothing Then If rs.State adStateOpen Then rs.Close Set rs Nothing End If End Function这里有个明显的安全问题SQL 是字符串拼接的用户名里带个单引号就能把语句结构破坏掉这是 SQL 注入的经典入口。课设阶段能跑通就行但要真用必须换成 ADO 的 Parameter 对象。 参数化写法杜绝注入 Dim cmd As New ADODB.Command cmd.ActiveConnection GetConnection() cmd.CommandText SELECT psw FROM login WHERE user_name? cmd.Parameters.Append cmd.CreateParameter(u, adVarChar, adParamInput, 20, Trim(txtUser.Text)) Set rs cmd.ExecuteCreateParameter的五个参数分别是名称、类型、方向、长度、值。adVarChar对应 varchar长度必须和表里定义的一致否则会截断或者类型不匹配。adParamInput表示输入参数。这套写法比拼接冗长但它是唯一正确的做法后面所有带用户输入的查询都该这么写。4.3 界面控件与事件绑定VB6 的界面能力有限做表格展示基本就是 MSFlexGrid 加 ADO Data Control 这套组合。原文 5.4 节只提了「界面设计」四个字没给控件清单。我列一下图书查询界面必备的几个控件类型关键属性用途txtKeywordTextBoxMaxLength30关键词输入cboFieldComboBoxStyle2选择按书名/作者/编号查cmdSearchCommandButtonDefaultTrue触发查询gridResultMSFlexGridCols5展示结果cmdBorrowCommandButtonEnabled 按角色选中行后借书cboField的Style2是下拉列表模式用户只能选不能输入避免拼出非法字段名。查询按钮设成DefaultTrue之后回车就能触发不用非得点鼠标。表格选中行的事件是RowColChange在这个事件里判断当前行是否是标题行第 0 行是表头是的话要跳过。 查询按钮按选定字段拼接 WHERE空关键词查全部 Private Sub cmdSearch_Click() Dim fld As String, kw As String, sql As String kw Trim(txtKeyword.Text) Select Case cboField.ListIndex Case 0: fld bookname Case 1: fld author Case 2: fld bannercode End Select sql SELECT bannercode,bookname,author,publishing,state FROM book If Len(kw) 0 Then sql sql WHERE fld LIKE % kw % Set mRs OpenRecordset(sql) Set gridResult.DataSource mRs 绑定记录集 End SubLIKE %关键词%是模糊匹配数据量大时会全表扫描课设规模几千条无所谓。mRs是窗体级模块变量因为 MSFlexGrid 绑定后要一直持有记录集引用用完在Form_Unload里关闭。这一条如果漏了窗体反复开关几十次之后 SQL Server 的连接数就满了现象是后面所有查询都超时。5. 避坑排查VB6 加 SQL Server 组合里的常见翻车5.1 数据库层的三个坑现象一录入第二本同名书报「违反主键约束」。原因是 book 表拿书名字段做了主键。书名天然会重复同一本书多买几本录入就会撞键。解决办法是把主键换成bannercode书名降为普通字段实在需要防重名就在应用层做软提示而不是数据库约束。现象二借书成功但图书列表还显示「在馆」。这种状态不一致基本都出在没用事务上。写 borrow 表和更新 book 表是两条独立语句中间任何一条失败另一条已经生效了数据就撕裂了。解决方式是包一层BeginTrans/CommitTrans异常走RollbackTrans参考 2.3 节的写法。另外别忘了在错误处理里也要回滚只写 Commit 不写 Rollback 等于没做。现象三还书时提示「没有待还记录」但借书记录明明存在。十有八九是归还日期的判断写错了。原文里借书表的receivedate允许为空判定未归还应该用receivedate IS NULL不能写receivedate 。SQL Server 里空值和空字符串是两回事前者是 NULL后者是一个长度为零的字符串两者比较永远为假。5.2 代码与部署层的两个坑现象四照抄原文 3.4 节的连接代码编译报「用户定义类型未定义」。那段代码用的是 C# 的System.Data.SqlClient属于 .NET 平台VB6 里没有这个命名空间。解决分两步先在「工程 → 引用」里勾选 Microsoft ActiveX Data Objects 2.8 Library然后用ADODB.Connection重写参考 3.2 节。判断是不是这个问题的简单办法看报错指向的是SqlConnection还是Connection前者就是平台搞错了。现象五本机跑得好好的拷到别人机器上就提示「找不到 F:\conn.txt」。这就是路径写死的代价。原文用 StreamReader 读固定盘符的文件换台机器盘符变了直接崩。改成App.Path \conn.txt之后路径跟着 exe 走打包的时候把配置文件一起带过去就行。同样的坑在数据库文件路径、图片路径、导出目录上都会复发凡是路径一律用App.Path拼。现象六中文书名查不出来或者存进去变问号。VB6 是 ANSI 环境SQL Server 2000 默认排序规则如果不支持中文存取就会丢字符。排查顺序是先确认数据库字段是varchar而非charchar 定长会补空格导致比较失败再确认连接串没加奇怪的字符集参数最后用查询分析器直接SELECT一次看库里存的是不是乱的。如果库里就是乱的问题在写入端不在查询端。6. 二次开发改造从课设文档到能跑的原型把这份文档变成能跑的原型第一步不是写代码是补表。原文说 8 张表却只给了 7 张的字段缺口靠 book 表的kindno反推出分类表这一步必须在建库之前做完否则后面每张业务表都要改。我一般的顺序是先建 kind、book、user 三张主表再建 borrow、punishment 两张流水表最后建 login、admin、libraryer 三张权限表建完立刻插几条测试数据用 SELECT 验证外键约束生效。第二步是把原文里那段 C# 连接代码彻底换掉。整个数据库访问层收敛到 basDB 一个模块对外只暴露GetConnection和OpenRecordset两个函数所有 SQL 必须走参数化。这个改造看着费力实际是省钱以后换数据库、加日志、加缓存只改一个文件。第三步是补业务闭环。原文功能到借还书就停了实际要跑起来还差三块超期罚款的结算入口punishment 表只写不读等于白写、读者借阅上限的配置别硬编码成 5 本做成配置项、图书状态的完整流转在馆/借出/遗失/下架原文只有两个状态。这三块补完系统才勉强算能用。第四步也是我觉得最值钱的一步拿用例图反推测试用例。原文的三张用例图每一条连线都是一个可测的路径。读者借书这条路径至少能派生出五个用例——正常借书、证号不存在、书已借出、超上限、图书编号错误。把这五条各跑一遍能覆盖借书模块八成的缺陷。这个方法比拍脑袋想测试点靠谱得多也不额外增加文档负担用例图本来就在那儿。改造项改动位置预估工作量优先级换主键为 bannercodebook 表 所有引用处半天高连接层参数化basDB 各窗体查询一天高补齐超期结算界面新增 frmFine一天中图书状态扩到四种book 表 state 字段半天中借阅上限做成配置basUtil 加配置读取两小时低最后说个血泪经验。我头一回照着这类文档做的时候图省事把书名当主键测试到一半发现同一本《数据结构》买了三本第二本就录不进去回头改主键的时候 borrow 表、punishment 表全跟着改等于把数据库推倒重来。从那以后我每次建表之前都强制走一遍「这个字段会不会重复」的自问宁可多花十分钟确认也不留这种后悔药都买不到的坑。希望帮到你。本文还有配套的精品资源点击获取