
简介面向高校计算机专业课程设计场景的《学校图书借阅管理系统》数据库系统设计报告以VFP为实现环境完整呈现从需求分析到编码实现的开发链条。文档围绕数据字典、数据流图、结构图与E-R图展开概要设计并逐项说明欢迎界面、读者与管理员双入口权限控制、图书信息录入与修改、读者注册与信息维护、按书名作者出版社检索及借还书、数据备份与恢复、帮助与密码设置等模块的功能划分随后给出主程序、封面、登录、录入、修改、查询借阅归还等表单的关键代码并附运行结果截图、设计分析与参考文献。压缩包内为1个doc文件约4.16MB是一份可直接参照的课程设计报告范本。该资源已有11186人学习下载适合需要套用E-R建模思路、补齐功能模块划分与表单代码细节的本科生及指导教师参考。1. 从一份 VFP 课设拆起5 张表撑起一个借阅闭环图书馆前台最怕听到一句话——“我明明还了”。纸质登记本一旦被翻乱、Excel 被覆盖保存借阅台账就成了黑匣子谁也说不清一本书的去向。这份《学校图书借阅管理系统》的数据库系统设计正是冲这个场景来的Visual FoxPro 做表单交互DBF 表做数据底座Booksinfo、Readeruinfo、Lendinfo、passwordinfo、Dairy 五张表把图书、读者、借阅流水、管理员凭证和登录日志分开管配上读者/管理员双入口权限、三次错误锁定、借还书和备份恢复。它是典型的课程设计报告配套源码依赖极少一台装好 VFP 的机器跑 main.prg 就能起来。适合三类人要交数据库系统设计报告、还得上台演示的同学想把 E-R 图、数据字典、表单代码对齐成一套材料的从业者以及想拿个小体量 CRUD 系统练手数据一致性的开发者。下面按“表怎么建、程序怎么起、权限怎么卡、业务怎么闭环、坑在哪”一层层拆。2. 数据字典落库五张表的宽度、索引与关系怎么钉死2.1 字段宽度在 VFP 里不是可以后期凑合的东西DBF 是定长记录CHARACTER 类型的宽度直接决定每条记录在文件里占多少字节。表里灌了几千条数据之后再想改宽度没有ALTER TABLE一条命令了事的便宜事只能建新表、导数据、删旧表、重建索引中间还要停掉所有打开这张表的表单。所以数据字典阶段就得把宽度钉死。原设计的数据字典里有几处类型选择值得拿出来说姓名给C(6)GBK 下一个汉字两字节正好三个字四个字的名字直接截断性别给C(6)实际只需要C(2)年龄给C(2)看着省字节可一旦要写“判断读者是否成年”这类逻辑每次都得VAL()转一遍最要命的是 Lendinfo 里的“借阅天数”被定义成字符型而这个字段本来是要参与日期减法的字符型没法直接算延期。这不是挑刺是课程设计里极常见的路径先拖表单控件再倒推字段能存进去就算完成。等到写查询统计全是CAST和VAL。2.2 五张表的建表脚本VFP 支持在数据库容器里用 SQL 的CREATE TABLE。下面这份脚本按原数据字典的字段顺序重建同时把上面提到的几处类型改掉改动点在注释里标了出来* 建库并打开容器 CREATE DATABASE library SET DATABASE TO library * 1. 图书信息表 CREATE TABLE Booksinfo (; 编号 C(10) NOT NULL,; 书号 C(28),; 书名 C(40),; 作者 C(20),; 出版社 C(15),; 定价 N(6,2),; 册数 N(3,0) DEFAULT 0,; 图书种类 C(18),; 入库日期 D,; 备注 M) * 2. 读者信息表原表名 Readeruinfo CREATE TABLE Readeruinfo (; 读者编号 C(10) NOT NULL,; 密码 C(10),; 姓名 C(20),; 原为 C(6)放宽到 20 字节 性别 C(2),; 原为 C(6) 年龄 N(2,0),; 原为 C(2)改成数值才能比较 详细住址 C(50),; 联系电话 C(11),; 注册日期 D,; 是否允许借 L DEFAULT .T.,; 备注 M) * 3. 借阅流水表 CREATE TABLE Lendinfo (; 读者编号 C(10) NOT NULL,; 书号 C(28) NOT NULL,; 借阅日期 D,; 预约日期 D,; 借阅天数 N(2,0) DEFAULT 30,; 原为 C(8)字符型算不了延期 归还日期 D,; 借阅情况 C(8),; 是否延期 L DEFAULT .F.,; 备注 M) * 4. 管理员凭证表 CREATE TABLE passwordinfo (; 用户名 C(8) NOT NULL,; 密码 C(12),; 原为 C(7)太短 权限级别 N(1,0) DEFAULT 1) * 5. 登录日志表 CREATE TABLE Dairy (; 用户名 C(10),; 登录时间 T,; 原来用 D 型日期精度不够改成日期时间型 离开时间 T)每张表的作用和字段选择逻辑值得过一遍。Booksinfo 用“编号”做业务主键、用“书号”做索引用两者不是一回事编号是馆内自增或人工编码书号是 ISBN 或馆藏号同一本书多册副本时书号会重复编号不能重复。Lendinfo 不设主键一条借阅记录由“读者编号 书号 借阅日期”三元组定位同一读者同一本书可以借多次只要日期不同。Dairy 改成日期时间型是因为原设计用D型存登录和离开时间只能记到天一个人一天登录三次日志就全糊了。2.3 索引和“主外键”在 VFP 里怎么落地VFP 的数据库容器支持候选索引和永久关系但没有 MySQL 那种FOREIGN KEY语法。主键靠候选索引CANDIDATE声明外键靠永久关系Persistent Relation加参照完整性生成器生成的触发器来维护。* 主键候选索引值唯一且非空 SELECT Booksinfo INDEX ON 编号 TAG pk_book CANDIDATE SELECT Readeruinfo INDEX ON 读者编号 TAG pk_reader CANDIDATE SELECT passwordinfo INDEX ON 用户名 TAG pk_admin CANDIDATE * 普通索引查询和连接用 SELECT Booksinfo INDEX ON 书号 TAG idx_bookno SELECT Lendinfo INDEX ON 读者编号 TAG idx_lend_reader INDEX ON 书号 TAG idx_lend_book建好索引之后在数据库设计器里拉永久关系Readeruinfo.读者编号 → Lendinfo.读者编号Booksinfo.书号 → Lendinfo.书号关系类型选“一对多”。拉完打开参照完整性生成器把更新级联设成“级联”、删除设成“限制”VFP 会给相关表挂上触发器。这一步很多课设直接跳过后果是删掉一本书Lendinfo 里还挂着它的借阅记录查询时 JOIN 出来一堆孤儿行。如果不想依赖触发器纯代码校验也是常见做法。借书动作里先SEEK读者、再SEEK图书两个都命中才允许写流水LOCAL lcReader, lcBook lcReader ALLTRIM(Thisform.txtReader.Value) lcBook ALLTRIM(Thisform.txtBook.Value) SELECT Readeruinfo SET ORDER TO TAG pk_reader IF NOT SEEK(lcReader) MESSAGEBOX(读者编号不存在, 48, 提示) RETURN ENDIF IF NOT Readeruinfo.是否允许借 MESSAGEBOX(该读者已被禁止借书, 48, 提示) RETURN ENDIF这里的SET ORDER TO TAG是关键它把记录顺序切到主键索引上SEEK才能走折半查找而不是全表扫。忘了这行SEEK会失败或者走错顺序这个坑在第 4 章还会遇到一次。3. main.prg 到 menu_main.mpr主程序骨架与表单链怎么搭3.1 开场那几行 SET 到底在关什么主程序是整个系统的入口VFP 里通常叫 main.prg编译后是 main.fxp。原设计的前几行是环境清场SET TALK OFF 关掉命令结果回显不然每条命令都在屏幕上刷 SET SAFETY OFF 关掉文件已存在是否覆盖的确认框 SET SYSMENU OFF 关掉 VFP 自带的系统菜单 CLOSE ALL 关掉所有已打开的表、库、索引 CLEAR ALL 释放内存变量、对象引用 MODIFY WINDOW SCREEN TITLE 迷你图书管理系统 ZOOM WINDOW SCREEN MAX SET DEFA TO LEFT(SYS(16), RAT(\, SYS(16))) SET PATH TO data;form;pic;class OPEN DATABASE librarySET SAFETY OFF要单独说一句它关掉了覆盖确认好处是不用人盯着点“是”坏处是COPY FILE覆盖同名文件时一声不吭备份做错了都发现不了。这是后面第 5 章备份翻车的伏笔之一。SET DEFA TO LEFT(SYS(16), RAT(\, SYS(16)))这行是路径自适应的常见写法。SYS(16)返回当前正在执行的程序文件的完整路径RAT(\)从右往左找最后一个反斜杠的位置LEFT()截到目录部分效果是把当前目录设成 main.prg 所在目录。这样整个工程换台机器、换个盘符也不用改路径。SET PATH TO data;form;pic;class把四个子目录加进搜索路径分号是 VFP 认可的分隔符。后面DO FORM start找 start.scx 时会在 form 目录里命中。这行的价值在于表单和类库可以分目录放不至于全堆在根目录。3.2 PUBLIC 变量与 READ EVENTS 事件循环原程序用三个全局变量串起登录态PUBLIC cyonghu, cmima, jiage, checked checked 1 cyonghu cmima DO FORM start READ EVENTS _screen.visible .t. _screen.picture lib.gif DO FORM 入口 READ EVENTS DO menu_main.mpr READ EVENTS QUITREAD EVENTS是 VFP 表单程序的核心机制。它把主程序挂起把控制权交给事件循环等某个表单里执行CLEAR EVENTS才回来然后继续往下走。这段代码的设计意图是“封面 → 入口 → 主菜单”三段式start 表单跑完清事件主程序露面再开入口表单再清事件再挂菜单。听起来顺实际跑起来会发现一个问题如果某个表单在执行过程中自己调了CLEAR EVENTS整条链就被拦腰截断直接跳到下一段。原代码里读者登录成功的分支就写了thisform.release后面跟CLEAR EVENT这一句会让事件循环直接结束主程序继续往下执行封面又弹一次。这个现象很难在单步调试时看出来因为单步不走事件队列。更稳的做法是表单之间用模态调用把链路交给表单自己管* main.prg 里只保留一个事件循环 DO FORM start READ EVENTS QUIT * start 表单的点击事件 DO FORM 入口 Thisform.Release * 入口表单的读者按钮 DO FORM dzdl Thisform.ReleaseDO FORM 入口默认是模态的WindowType 属性为 1子表单关闭后才回到父表单的下一行。这样只需要最外层一个READ EVENTS中间的跳转不会因为某处多写一句CLEAR EVENTS而断链。全局变量能省则省cyonghu这类登录态更适合挂在表单的Property上往下传PUBLIC一旦被同名局部变量遮蔽排查起来相当费劲。3.3 表单链的三种接法选错一种就等着返工VFP 里串表单至少有三条路各有适用边界接法写法适用场景隐患非模态平铺DO FORM x不加READ EVENTS需要多窗口并存的工具面板表单关不掉、主程序提前退出模态嵌套DO FORM xWindowType1登录、确认框、向导嵌套过深时调试栈难读事件分段DO FORM xREAD EVENTS封面、启动页中间任意一处CLEAR EVENTS就断链课程设计里最常见的是第三种因为它看起来最“像程序”。但只要中途有一个表单多写了CLEAR EVENTS或者某个表单被RELEASE时没有正确退出整条链就乱套。我的习惯是只留最外层一个事件循环内部全部用模态。这样程序的生命周期只有一个入口、一个出口出问题时看READ EVENTS的配对就够了。菜单文件menu_main.mpr是同样的道理。它本质上是一组ON SELECTION命令的集合挂在_MSYSMENU上DO menu_main.mpr之后菜单生效但要想让菜单里的“退出”真正结束程序还得在对应过程里写CLEAR EVENTS而不是QUIT否则表单还没释放干净DBF 表可能还开着直接QUIT会留下未刷盘的索引文件。4. 双入口鉴权LOCATE 查找、三次锁定与密码表4.1 两张凭证表为什么不能合成一张原设计把读者和管理的凭证拆成了两张表Readeruinfo 里存读者编号、密码和个人信息passwordinfo 里只存用户名、密码。这个分工是对的理由是读者的主键是“读者编号”管理员的主键是“用户名”两者语义不同、长度不同10 位对 8 位合成一张表要么加类型字段要么把编号字段拉宽到能兼容两种值查询时还得每次过滤。但 passwordinfo 原表里多了一个“册数 N(3,0)”字段这个字段在管理员表里没有任何业务含义大概率是从 Booksinfo 复制表结构时带过来的。这种残留字段在演示时不会报错但答辩时导师问一句“管理员为什么会有册数”就得现场解释。4.2 LOCATE 和 SEEK 的差别决定了登录能不能稳定命中原设计读者登录用的是LOCATE FORSELECT Readeruinfo LOCATE FOR ALLT(读者编号) ALLT(Thisform.Text1.Value) IF FOUND() IF ALLT(密码) ALLT(Thisform.Text2.Value) * 登录成功 ELSE * 密码错误 ENDIF ENDIF这段代码能跑但有两个隐患。第一LOCATE是从当前记录指针位置开始往后找不是从表头开始。如果这张表之前被其他表单打开过、指针停在第五条记录上那么前四条里的匹配记录就永远搜不到。跑一次两次没事多切换几次窗口就“登录不进去”典型的玄学 bug。第二ALLT()只裁掉尾部空格。如果录入时手一抖在编号前面多敲了个空格ALLT( 1001)还是 1001等值比较失败用户怎么输都进不去。改成索引加SEEK更稳LOCAL lcUser, lcPwd lcUser ALLTRIM(Thisform.txtUser.Value) lcPwd ALLTRIM(Thisform.txtPwd.Value) IF EMPTY(lcUser) WAIT WINDOW 读者编号不能为空 TIMEOUT 1 Thisform.txtUser.SetFocus RETURN ENDIF SELECT Readeruinfo SET ORDER TO TAG pk_reader IF SEEK(lcUser) IF ALLTRIM(Readeruinfo.密码) lcPwd * 登录成功记录登录日志 INSERT INTO Dairy (用户名, 登录时间) VALUES (lcUser, DATETIME()) Thisform.Release ELSE Thisform.txtPwd.Value Thisform.txtPwd.SetFocus WAIT WINDOW 密码错误请重新输入 TIMEOUT 1 ENDIF ELSE WAIT WINDOW 该读者编号不存在 TIMEOUT 1 ENDIFSET ORDER TO TAG pk_reader把顺序切到候选索引上SEEK走的是折半查找不管当前指针在哪都从索引顶端开始。ALLTRIM()双边裁空格配合录入端在LostFocus里统一做一次ALLTRIM()回写脏数据进不了表。4.3 三次锁定为什么要写成表单属性而不是 PUBLIC原设计的锁定逻辑依赖一个PUBLIC n在表单的Init里n 1IF n 3 * 正常校验失败时 n n 1 ELSE n 1 Thisform.txtUser.Enabled .f. Thisform.txtPwd.Enabled .f. Thisform.cmdOK.Enabled .f. Thisform.lblTip.Caption 登录已错误三次系统即将自动退出 ENDIF问题出在Init里那句n 1。每打开一次表单都会重置计数用户输错两次、关掉窗口、再打开又是三次机会所谓的“锁定”形同虚设。而且PUBLIC变量在整个应用里全局可见任意一个过程写一句n 0就能绕开。稳妥写法是把计数挂在表单属性上* 表单自定义属性 nTry初始值 0 * Init 事件 Thisform.nTry 0 * 登录按钮 Click 事件 IF Thisform.nTry 3 MESSAGEBOX(连续错误次数过多程序退出, 16, 提示) CLEAR EVENTS RETURN ENDIF * ……校验失败分支 Thisform.nTry Thisform.nTry 1 Thisform.lblTip.Caption 已错误 ALLTRIM(STR(Thisform.nTry)) 次累计 3 次将退出属性随表单生命周期存在表单释放计数自然清零也避免了PUBLIC被外部干扰。Thisform.nTry在表单设计器里添加自定义属性时类型选数值型、初始值填 0就不会出现第一次点击时nTry为.NULL.导致加法报错的情况。还有一处细节原代码用WAIT WINDOW AT 17.5,65把提示钉死在屏幕坐标上换台不同分辨率的机器提示就飘到表单外面去了。MESSAGEBOX或表单内嵌的 Label 都比硬编码坐标靠谱。5. 借还书闭环与备份恢复排查四个高频翻车点5.1 借书的扣减顺序先写流水还是先减库存这是整个系统里最容易出数据不一致的地方。理想顺序是校验读者存在且是否允许借 .T.校验图书存在且册数 0写一条 Lendinfo 流水归还日期留空Booksinfo 的册数减 1提交事务VFP 的数据库容器支持BEGIN TRANSACTION/END TRANSACTION/ROLLBACK但这套事务只对容器内的表生效而且嵌套最多五层。写法LPARAMETERS tcReader, tcBookNo LOCAL lnCnt BEGIN TRANSACTION SELECT Booksinfo SET ORDER TO TAG idx_bookno IF NOT SEEK(tcBookNo) ROLLBACK RETURN 书号不存在 ENDIF lnCnt Booksinfo.册数 IF lnCnt 0 ROLLBACK RETURN 该书库存为 0无法借出 ENDIF SELECT Lendinfo APPEND BLANK REPLACE 读者编号 WITH tcReader, ; 书号 WITH tcBookNo, ; 借阅日期 WITH DATE(), ; 借阅天数 WITH 30, ; 借阅情况 WITH 在借, ; 是否延期 WITH .F. SELECT Booksinfo REPLACE 册数 WITH 册数 - 1 END TRANSACTION RETURN 先写流水再减库存是刻意的如果减库存成功、写流水失败账面上书少了但没人借属于“库存蒸发”反过来写流水成功、减库存失败是“一本书借给了两个人”两种都是事故但后者更难查。事务包住的范围内任何一步ROLLBACK都会把两边一起回滚这才是BEGIN TRANSACTION存在的意义。一个必须知道的边界VFP 事务不支持INSERT INTO ... SELECT这类跨表批量语句的部分回滚也管不了COPY FILE这种文件级操作。备份恢复挪到事务里包一层是包不住的。5.2 还书与延期判定日期减法别用字符型还书的判定逻辑很简单找到未归还的流水算借阅天数和实际持有天数超过就标记延期。前提是“借阅天数”必须是数值型原设计给的字符型在这里会直接卡住SELECT Lendinfo SET ORDER TO TAG idx_lend_book SEEK tcBookNo SCAN WHILE 书号 tcBookNo IF EMPTY(归还日期) lnHold DATE() - 借阅日期 IF lnHold 借阅天数 REPLACE 是否延期 WITH .T. ENDIF REPLACE 归还日期 WITH DATE(), 借阅情况 WITH 已还 EXIT ENDIF ENDSCAN SELECT Booksinfo SET ORDER TO TAG idx_bookno IF SEEK(tcBookNo) REPLACE 册数 WITH 册数 1 ENDIFSCAN ... WHILE依赖索引顺序所以SET ORDER TO TAG idx_lend_book不能省否则WHILE条件在第一条不匹配时就退出一本书借过多次就只查到第一条。EMPTY(归还日期)用来判断未还比归还日期 {}更稳空日期和.NULL.都能命中。5.3 备份恢复DBF 从来不是单个文件这是课设里血泪经验最多的一块。很多人备份时只拷booksinfo.dbf恢复时发现备注字段全空了或者索引全丢了。原因是 VFP 的一张表实际是三个文件后缀内容漏拷的后果.dbf表结构 定长字段数据表打不开.fpt备注字段M 型的实际内容备注全空.cdx复合索引查询变慢、SEEK 失效正确做法是三件套一起拷LPARAMETERS tcSrcDir, tcBakDir LOCAL lcFile, lnErr lnErr 0 IF NOT DIRECTORY(tcBakDir) MKDIR (tcBakDir) ENDIF * 逐表拷贝三件套 FOREACH lcFile IN Booksinfo,Readeruinfo,Lendinfo,passwordinfo,Dairy IF FILE(tcSrcDir lcFile .dbf) COPY FILE (tcSrcDir lcFile .dbf) TO (tcBakDir lcFile .dbf) ENDIF IF FILE(tcSrcDir lcFile .fpt) COPY FILE (tcSrcDir lcFile .fpt) TO (tcBakDir lcFile .fpt) ENDIF IF FILE(tcSrcDir lcFile .cdx) COPY FILE (tcSrcDir lcFile .cdx) TO (tcBakDir lcFile .cdx) ENDIF ENDFOREACH * 数据库容器本身也要备 COPY FILE (tcSrcDir library.dbc) TO (tcBakDir library.dbc) COPY FILE (tcSrcDir library.dct) TO (tcBakDir library.dct) COPY FILE (tcSrcDir library.dcx) TO (tcBakDir library.dcx)FOREACH ... IN遍历的是逗号分隔的字符串这是 VFP 里批量处理表名的省事写法。恢复时顺序反过来先CLOSE DATABASES ALL关掉所有打开的表再拷回去否则文件被占用COPY FILE会静默失败——因为前面SET SAFETY OFF关掉了覆盖提示失败也不弹窗。5.4 四个高频翻车点现象一读者输入正确密码仍提示“密码错误”。原因是密码字段定义为C(7)用户在录入时输了 8 位以上DBF 字段超出宽度会被截断而登录端拿完整输入去比对永远不等。解决把密码字段放宽到C(12)以上录入端在Valid事件里检查LEN(ALLTRIM(x)) 字段宽度超长直接拦下。现象二删掉一本书借阅记录还在报表里出现空书名。原因是没配参照完整性或者配置了但触发器没生成。解决在数据库设计器里拉永久关系后打开参照完整性生成器把Lendinfo对Booksinfo的删除规则设为“限制”删书前先查有没有未还流水。现象三备份还原之后所有表单打开都报“找不到别名”。原因是恢复时只拷了 DBF没拷library.dbc容器文件表虽然在目录里但没有登记进数据库OPEN DATABASE之后找不到。解决备份脚本必须包含.dbc、.dct、.dcx三件套。现象四读者连续输错三次没有退出关掉窗口重开又是三次。原因就是第 4 章说的计数变量放PUBLIC、在Init里重置。解决计数挂表单属性锁定后直接CLEAR EVENTS结束整个应用而不是只禁用一个按钮。6. 答辩前跑一遍用 4 条 SELECT 揪出脏数据功能演示跑通不等于数据是干净的。答辩现场最常见的尴尬是导师随手点一个查询结果里冒出一条“册数为 -1”的书。我的习惯是交报告前用一组 SQL 把脏数据扫一遍这套动作在 VFP 的命令窗口或表单按钮里都能跑。第一条查库存与在借数量对不上SELECT Booksinfo.书号, Booksinfo.书名, Booksinfo.册数, COUNT(*) AS 在借数; FROM Booksinfo; JOIN Lendinfo ON Booksinfo.书号 Lendinfo.书号; WHERE EMPTY(Lendinfo.归还日期); GROUP BY Booksinfo.书号; HAVING COUNT(*) Booksinfo.册数; INTO CURSOR csBadStockHAVING里直接引用了Booksinfo.册数这在 VFP 的方言里能过因为分组字段里只有书号册数是函数依赖。查出来的每一行都意味着“借出去的数量超过了馆藏”通常来自借书时减库存那一步被跳过。第二条查读者编号重复SELECT 读者编号, COUNT(*) AS 重复条数; FROM Readeruinfo; GROUP BY 读者编号; HAVING COUNT(*) 1; INTO CURSOR csDupReader重复的读者编号会让SEEK只命中第一条后面那条的密码永远登不进去用户投诉“我改了密码还是登不上”其实就是这个原因。第三条查时间倒挂SELECT 读者编号, 书号, 借阅日期, 归还日期; FROM Lendinfo; WHERE NOT EMPTY(归还日期) AND 归还日期 借阅日期; INTO CURSOR csBadDate这条一般来自手工改库或者测试时乱填日期量不大但特别显眼。第四条查“已还但库存没回补”SELECT L.读者编号, L.书号, L.归还日期, B.册数; FROM Lendinfo L; JOIN Booksinfo B ON L.书号 B.书号; WHERE NOT EMPTY(L.归还日期) AND B.册数 0; INTO CURSOR csNoRestore册数为 0 且存在已还记录说明还书时REPLACE 册数 WITH 册数 1那行没执行到可能是SCAN提前EXIT也可能是还书按钮的事件里漏了。四条查完_TALLY都是 0 才算能上台。我一般会把这四条SELECT做成一个“数据体检”按钮藏在管理员菜单里演示完顺手点一下比口头说“数据是一致的”有说服力得多。血泪教训是有一回改完还书逻辑只测了正常路径忘了测“同一本书借两次再还一次”结果只回补了一本答辩时被问了一句“你这本书怎么借出去 3 本只有 2 本在架”当场翻车。从那以后我每次改完借还书逻辑都会把上面四条 SQL 强制走一遍一条不落地看_TALLY。希望帮到你。本文还有配套的精品资源点击获取