ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

图书借阅管理系统课设:从数据模型到并发控制的完整实践

图书借阅管理系统课设:从数据模型到并发控制的完整实践 简介这是一份数据库课程设计资源面向高校计算机相关专业学生用于完成图书借阅管理系统的设计与开发。资源围绕数据库设计、关系模型、SQL查询、事务处理、安全权限、性能优化、备份恢复及界面交互等知识点展开可帮助读者理解数据库理论在真实项目中的落地方式。压缩包共81个文件主要包括14个Java源文件、48个class编译文件、3个jar依赖库、1个Word说明文档及数据库文件另有项目配置、图片和临时文件整体约11.12MB便于直接导入开发环境查看与运行。目前已有1544人学习下载适合作为课程设计参考、答辩准备和数据库实践练习。读者可从中获得完整的项目源代码、数据库文件与说明文档结合自身需求进行扩展和改造进而掌握图书借阅场景下的表结构设计、外键关联、事务控制和权限管理等核心技能。1. 图书借阅管理系统课设为什么说难点全在“借书”这一个动作上数据库课程设计的经典固定需求里图书借阅管理系统大概是最容易上手、也最容易写浅的题目——图书、读者、借阅记录三张表就能交差但一做深就发现难点全在“借书”这个动作上库存怎么减、逾期怎么算、并发借同一本书会不会翻车、统计报表怎么写才好看。这篇文章就给出一条能直接在课设里照做的路径数据模型怎么收敛借书/还书/逾期三条业务主线的SQL怎么写账号权限、预约、排行榜这些加分项怎么加最后一章是一套验收方案。适合正在选题、或表已建好但业务逻辑写不顺的同学。全文围绕一个原则功能不必多但每一条增删改查都要能经得起追问。2. 数据模型设计三张核心表如何撑起借还书闭环2.1 从E-R图到关系模式谁在借什么书、什么时候借、什么时候还先画清楚E-R图再动手建表这个顺序不能反过来。这个系统的实体只有两个核心图书和读者中间加一条“借阅”联系。借阅是典型的“多对多联系带属性升级为实体”情况这一点在答辩时一定会被问到所以最好在建表前就把话说圆一个读者可以借多本图书一本图书可以被多个读者在不同时间段借阅借阅本身还有借出日期、应还日期、实际归还日期、逾期罚金这些属性因此要拆成独立的 borrow 表。确认E-R图之后关系模式自然收敛成三张表book 保存图书本身的信息和库存reader 保存读者档案和状态borrow 保存每一笔借还记录。这里最容易纠结的问题是book 表里到底放不放 available_copies 这个冗余字段。我的建议是放而且维护好。如果不放判断一本书是否可借就要SELECT COUNT(*) FROM borrow WHERE book_id? AND return_date IS NULL表一大会拖慢每一次借书操作而且要写进事务里才能保证准确。放了 available_copies借书扣一、还书加一查询时能直接判断性能更好代价是必须在同一事务里同时修改 book 和 borrow少一步就是数据不一致。我一般还会给 borrower 加上 status 字段用来处理“冻结/正常”状态而不是删记录——课设里强行 DELETE 一条有借阅历史的读者会被外键挡回来处理不好代码里全是异常分支。同样图书下架也建议用status字段软删除而不是物理删除这个选择在后面的避坑章节会展开讲。关系模式转换好之后可以用一句话检查模型是否自洽任何一本在借的图书都能在 borrow 表里找到一条未归还记录且该记录外键关联的 reader 必须存在。这个规则贯穿后面的所有 SQL。2.2 建表SQL约束、索引、字符集一次到位的写法我用 MySQL 8.0 的 InnoDB 引擎来建表这也是课设最常见的环境。建库时直接指定字符集避免事后因为乱码返工。下面这组建表语句是一个可以直接抄的基线版本CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE library; CREATE TABLE category ( category_id TINYINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB; CREATE TABLE book ( book_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, publisher VARCHAR(100), publish_date DATE, category_id TINYINT UNSIGNED, total_copies INT UNSIGNED NOT NULL DEFAULT 1, available_copies INT UNSIGNED NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-在架 0-下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_available_copies CHECK (available_copies total_copies), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(category_id), INDEX idx_book_title (title) ) ENGINEInnoDB; CREATE TABLE reader ( reader_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, reader_name VARCHAR(50) NOT NULL, id_card CHAR(18) UNIQUE, phone VARCHAR(20), email VARCHAR(100), reg_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-冻结 ) ENGINEInnoDB; CREATE TABLE borrow ( borrow_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, reader_id INT UNSIGNED NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-借出 1-已还, fine_amount DECIMAL(6,2) NOT NULL DEFAULT 0.00, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ON DELETE RESTRICT ON UPDATE CASCADE, INDEX idx_borrow_reader_date (reader_id, borrow_date), INDEX idx_borrow_status_due (status, due_date) ) ENGINEInnoDB;书先拆一个 category 表而不是直接存分类名是为了让“每本图书属于一个分类、一个分类下有多本图书”的一对多关系在表结构上可见这也是答辩时的一个得分点。book 表里的available_copies total_copies是一个 CHECK 约束MySQL 8.0.16 之前会被忽略8.0.16 之后真正生效如果课设环境是 5.7这道防线要靠事务里的业务判断来补。borrow 表是整张业务的事实表所以索引要重点照顾。idx_borrow_reader_date支撑“某读者借过哪些书”和“某读者当前借了几本”这两类高频查询idx_borrow_status_due支撑“现在有哪些书逾期未还”的统计。外键约束用ON DELETE RESTRICT意思是只要借阅记录存在就不允许物理删除图书或读者这个设计逼着你的代码走软删除反而对数据完整性有好处。2.3 字段选择的一个易错点状态字段和金额字段的类型status 字段我全部用 TINYINT不用 ENUM。ENUM 看着可读性好但后期要加状态就得改表结构而且 JDBC 或 Python 驱动取出来还要多一层转换课设里完全没必要。TINYINT 加注释是更稳的做法。借阅记录的 ID 我故意设计成BIGINT UNSIGNED因为三年数据量再小每年几千条借阅记录也正常INT 上限虽然够用但课设里读者多次重复借还会频繁插入和删除自增主键会跳号用 BIGINT 更省心。fine_amount 用DECIMAL(6,2)不用 FLOAT 或 DOUBLE。金额计算在逾期费结算时最容易出现 0.1 0.2 0.30000000000000004 这类问题答辩时一旦被追问为什么用浮点就不好收场DECIMAL 是最简单的正确答案。-- 推荐把以下两行放在说明文档的“设计依据”部分 -- 1) 所有状态字段使用 TINYINT COMMENT避免 ENUM 后续改枚举值要 ALTER TABLE -- 2) 金额字段使用 DECIMAL(6,2)逾期费按 0.5 元/天累计6 位精度足够 cover 三年账单到这里三张核心表已经能支撑“借书-还书-逾期”的完整流程。下一步就是把借书这个动作写成一个真正能扛住并发的事务。3. 借书、还书、统计三条业务主线增删改查背后的锁与边界3.1 借书行锁、库存校验、借阅记录写在同一个事务里借书不是一句INSERT就完事它至少要完成三件互相依赖的事检查书还有没有库存、检查读者是否还能借、扣减库存并写入借阅记录。这三件事必须在一个事务里按顺序执行中途任何一步失败都要整体回滚。否则就会出现“借阅记录插进去了但库存没扣”这种查不出原因的脏数据。下面是一个完整的借书存储过程直接用SELECT ... FOR UPDATE锁住图书行避免并发借同一本书时把库存扣成负数DELIMITER $$ CREATE PROCEDURE sp_borrow_book( IN p_book_id INT UNSIGNED, IN p_reader_id INT UNSIGNED, IN p_days TINYINT ) BEGIN DECLARE v_available INT UNSIGNED; DECLARE v_borrowing INT UNSIGNED; DECLARE v_max_books INT UNSIGNED DEFAULT 5; START TRANSACTION; -- 1. 锁定图书行其他事务必须等这个事务提交或回滚 SELECT available_copies INTO v_available FROM book WHERE book_id p_book_id AND status 1 FOR UPDATE; IF v_available IS NULL OR v_available 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT book not available; END IF; -- 2. 统计该读者当前未归还的借阅数含逾期未还 SELECT COUNT(*) INTO v_borrowing FROM borrow WHERE reader_id p_reader_id AND status 0; IF v_borrowing v_max_books THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT borrow limit exceeded; END IF; -- 3. 写借阅记录并扣减库存 INSERT INTO borrow (book_id, reader_id, borrow_date, due_date, status) VALUES (p_book_id, p_reader_id, CURDATE(), DATE_ADD(CURDATE(), INTERVAL IFNULL(p_days, 30) DAY), 0); UPDATE book SET available_copies available_copies - 1 WHERE book_id p_book_id; COMMIT; END$$ DELIMITER ;FOR UPDATE是 InnoDB 的行锁语法它锁住的是 book 表中book_id p_book_id这一行。两个事务同时执行这个存储过程借同一本书时第二个事务会阻塞在SELECT ... FOR UPDATE上直到第一个事务提交或回滚。这样就堵死了“库存还剩 1 本两个人同时借出去”的最典型并发翻车场景。SIGNAL SQLSTATE 45000是 MySQL 5.6 之后的标准抛异常写法配合ROLLBACK保证业务校验失败时整个事务不留痕。p_days 参数让应还日期可以被外部指定默认 30 天如果你想把规则定死成某一类书 14 天、另一类 30 天可以在这个存储过程里加一个分支判断。3.2 还书重复还书会让库存翻倍必须设置幂等条件还书的对称逻辑是回写 return_date、计算逾期罚金、把 available_copies 加回去。但这里埋着一个很容易被忽视的坑——同一个订单被重复点击两次“还书”会把库存加上两次。解决方法是更新时强制带上return_date IS NULL条件第二笔更新影响 0 行业务层捕获到“影响行数为 0”就提示已在借阅中。START TRANSACTION; -- 锁定这条借阅记录防止还书的同时被续借逻辑误改 SELECT borrow_id, book_id, due_date, return_date FROM borrow WHERE borrow_id 1234 FOR UPDATE; -- 只处理还没有归还的记录重复执行时影响行数为 0 UPDATE borrow SET return_date CURDATE(), status 1, fine_amount GREATEST(0, DATEDIFF(CURDATE(), due_date)) * 0.50 WHERE borrow_id 1234 AND return_date IS NULL; -- 只有上面 UPDATE 影响行数为 1 时才执行下一句 UPDATE book SET available_copies available_copies 1 WHERE book_id (SELECT book_id FROM borrow WHERE borrow_id 1234); COMMIT;DATEDIFF(CURDATE(), due_date)返回负数代表提前还所以外面套一个GREATEST(0, ...)把罚金下限锁在 0 元。罚金每天 0.50 元这个费率在课设里够用你完全可以改成 0.1 或 1 元但建议把费率单独拎出来放配置表不要散落在各处 SQL 里后期调参不用翻代码。这里还有一个事务边界问题UPDATE borrow和UPDATE book必须在一个事务里。真实课设里见过同学把这两句分开执行中间程序崩了结果书还了但库存没加回去。还书这个动作的原子性必须靠事务保证不是靠心情保证。3.3 统计SQL逾期清单、热门图书、借阅趋势一次写好统计类 SQL 是课设答辩的问答重灾区因为评委很喜欢问“你的报表数据怎么来的”。三条常用的统计查询要提前写好并且清楚每一条的 group by 和索引使用情况。逾期未还清单查询idx_borrow_status_due索引可以直接覆盖 status 和 due_date 两个条件。SELECT r.reader_name, b.title, br.borrow_date, br.due_date, DATEDIFF(CURDATE(), br.due_date) AS overdue_days FROM borrow br JOIN reader r ON br.reader_id r.reader_id JOIN book b ON br.book_id b.book_id WHERE br.return_date IS NULL AND br.due_date CURDATE() ORDER BY br.due_date ASC;热门图书按月排行注意GROUP BY里必须带上所有非聚合的 SELECT 字段否则默认sql_mode里的ONLY_FULL_GROUP_BY直接报错。SELECT b.title, DATE_FORMAT(br.borrow_date, %Y-%m) AS month, COUNT(*) AS borrow_count FROM borrow br JOIN book b ON br.book_id b.book_id WHERE br.borrow_date DATE_SUB(CURDATE(), INTERVAL 12 MONTH) GROUP BY b.title, DATE_FORMAT(br.borrow_date, %Y-%m) ORDER BY month DESC, borrow_count DESC;借阅趋势用 DATE_FORMAT 按月聚合配合前端图表库可以画出一张连续 12 个月的借阅曲线这属于课设里性价比最高的“可视化加分项”。4. 把课设做出“卖相”账号权限、预约排队、逾期费结算4.1 用户与权限不要用 root 连接业务代码到这一步基础功能已经完整但要拿高分还得加业务闭环外的模块。最常见、也最容易展示的是账号体系和权限控制。我在借阅表外围加一张独立的用户表把“图书管理员”和“普通读者”区分开CREATE TABLE sys_user ( user_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, pwd_hash CHAR(64) NOT NULL COMMENT SHA-256 或 bcrypt 摘要, role TINYINT NOT NULL DEFAULT 2 COMMENT 1-管理员 2-读者, reader_id INT UNSIGNED NULL, status TINYINT NOT NULL DEFAULT 1, CONSTRAINT fk_sys_user_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ) ENGINEInnoDB;不建议把密码字段直接命名为 password 并存明文这几乎是安全相关的必问题。课设里不需要做完整的注册登录流程但至少要用SHA2(username, 256)存一下摘要。密码不用加盐也没关系但别明文存。数据库侧的权限配合最小授权原则给业务代码创建一个专用账号而不是用 root-- 管理员账号可以维护图书和读者 CREATE USER lib_adminlocalhost IDENTIFIED BY Admin123; GRANT SELECT, INSERT, UPDATE, DELETE ON library.* TO lib_adminlocalhost; -- 读者端账号只能查询图书以及操作自己的借还记录 CREATE USER lib_readerlocalhost IDENTIFIED BY Reader123; GRANT SELECT ON library.book TO lib_readerlocalhost; GRANT SELECT, INSERT, UPDATE ON library.borrow TO lib_readerlocalhost; FLUSH PRIVILEGES;这段 SQL 展示了一个重要的工程习惯应用连接数据库用的是受控账号不是有全部权限的 root。答辩时能把最小授权讲清楚比多做两个业务功能更让评委觉得你理解“数据库安全”。4.2 预约排队在借书前先检查预约者预约是很多课设会写进需求但做不出来的功能它表面上是多一张表实际上改变了借书流程的判定顺序。预约表的模型可以这样建CREATE TABLE reservation ( reserve_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, reader_id INT UNSIGNED NOT NULL, reserve_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-排队中 1-已借出 2-已取消, CONSTRAINT fk_res_book FOREIGN KEY (book_id) REFERENCES book(book_id), CONSTRAINT fk_res_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), INDEX idx_res_queue (book_id, status, reserve_time) ) ENGINEInnoDB;借书存储过程里要插入一段预约判断这本书有人排队时只有排在第一个且状态为“排队中”的读者能借其他人被拒。核心逻辑如下可以直接嵌在sp_borrow_book里-- 在写 borrow 之前执行 IF EXISTS ( SELECT 1 FROM reservation WHERE book_id p_book_id AND status 0 ) THEN -- 排队的第一位不是当前读者拒绝借书 IF NOT EXISTS ( SELECT 1 FROM reservation WHERE book_id p_book_id AND status 0 ORDER BY reserve_time, reserve_id LIMIT 1 ) OR (SELECT reader_id FROM reservation WHERE book_id p_book_id AND status 0 ORDER BY reserve_time, reserve_id LIMIT 1) p_reader_id THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT reservation queue first; ELSE -- 当前借阅者就是排队第一名把预约置为已借出 UPDATE reservation SET status 1 WHERE book_id p_book_id AND reader_id p_reader_id AND status 0; END IF; END IF;这段代码里有两个易错点一是判断“当前读者是否是第一名”必须单独查一次并用比较不能复用 EXISTS 的上下文二是ORDER BY reserve_time, reserve_id必须同时带上主键 reserve_id 做第二排序键否则两个读者在同一秒预约时谁先谁后没有确定性。并发预约场景下还要在 reservation 表加索引idx_res_queue否则排序查会走全表扫描性能一塌糊涂。4.3 触发器与存储过程逾期费结算放在哪一层更合理逾期费的计算既可以用存储过程在还书时算也可以用触发器在修改 borrow 表时自动算。常见做法是两者配合触发器负责回填计算列存储过程负责业务流程。下面这个BEFORE UPDATE触发器会在还书更新 return_date 的瞬间自动重算罚金避免每个还书入口各写一遍计算逻辑DELIMITER $$ CREATE TRIGGER trg_borrow_fine BEFORE UPDATE ON borrow FOR EACH ROW BEGIN IF NEW.return_date IS NOT NULL THEN SET NEW.fine_amount GREATEST(0, DATEDIFF(NEW.return_date, NEW.due_date)) * 0.50; END IF; END$$ DELIMITER ;触发器的优点是逻辑集中缺点是容易变成“黑匣子”——病发时很难排查数据是被哪段逻辑改的。所以我一般建议触发器只做“计算列回填”这种确定性规则不做“查另一张表再决定是否抛异常”这种跨表业务跨表业务全部收口到存储过程里。否则一个调试的同学看到 update 语句执行成功了但多出一行罚金记录会花半天找原因。排行榜模块不建议用触发器维护。借阅趋势和热门排行完全可以在查询时聚合数据量几千条的情况下毫秒级就能返回。只有数据量真到百万级才需要引入每日统计表由定时任务在凌晨汇总。课设阶段不要做这种提前优化把精力省下来确保核心流程稳。5. 图书借阅课设避坑与排查从死锁到乱码的 5 个处理案例5.1 删除图书报外键冲突不是删不掉是还有借阅记录现象点删除图书时程序报错Cannot delete or update a parent row: a foreign key constraint fails或者拿到 SQL 里执行 DELETE 直接被数据库拒绝。原因borrow 表还有指向这本书的借阅记录外键ON DELETE RESTRICT默认阻止删除父表行。这不是数据库出问题而是业务状态还没有清理完。解决不要试图删外键或改约束而是改业务逻辑。前端删除按钮改成“下架”执行UPDATE book SET status 0 WHERE book_id ?如果一定要物理删除必须先确认没有未还记录并删除对应借阅流水。课设里选择软删除是更稳的路线既保住历史数据又避免在答辩现场演示删除时当场翻车。5.2 并发借同一本书出现死锁先统一锁顺序现象两个读者同时借同一本书或一个读者连续快速借两本书程序报Deadlock found when trying to get lock; try restarting transactionMySQL 错误码 1213。原因事务 A 锁了 book 表某行、想再锁 borrow 表事务 B 锁了 borrow 表某行、想再锁 book 表。两个事务互相等对方释放锁InnoDB 检测到死锁后直接把其中一个事务回滚掉。解决统一所有事务获取锁的顺序先锁 book再写 borrow。上面 3.1 的存储过程就是这个顺序。另一个排查点是在同一事务里避免“先查预约再查库存然后回头更新预约”这种交叉锁所有涉及 reservation 和 book 的操作要按 book_id → reservation 的固定顺序访问。把innodb_print_all_deadlocksON打开死锁发生后去错误日志看持有和等待的锁链能快速定位代码里哪个事务破坏了顺序。5.3 中文乱码字符集不统一前端和表各说各话现象界面插入的“张三”显示成“???”或者从一个库导出的 SQL 导入另一个库后标题变成乱码。原因建库用默认 latin1前端连接串没指定 utf8mb4或表字段用了不同字符集。MySQL 的字符集校验是层层叠加的客户端连接、数据库、表、字段四级不一致就必然乱码。解决建库时像 2.2 一样显式指定CHARACTER SET utf8mb4连接串里带useUnicodetruecharacterEncodingutf8并且连接建立后执行一次SET NAMES utf8mb4。之前已经建错的库用ALTER DATABASE library CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;修改后再逐表转。注意导出的 SQL 文件头部也要确认包含SET NAMES utf8mb4;否则导入时仍然乱码。5.4 GROUP BY 报错ONLY_FULL_GROUP_BY 模式下的隐藏炸弹现象一条统计 SQL 在本地好好的换到课设演示的电脑上报Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column。原因MySQL 5.7 之后默认打开sql_mode的ONLY_FULL_GROUP_BYSELECT 列表里的非聚合字段必须全部出现在 GROUP BY 中。之前的写法能跑只是没开这个模式换环境就直接报错。解决改 SQL 而不是改 sql_mode。比如按图书统计借阅次数必须写成GROUP BY b.title而不是只GROUP BY b.book_id但 SELECT 里带 b.title。如果确定要去掉这个模式执行SET sql_mode (SELECT REPLACE(sql_mode, ONLY_FULL_GROUP_BY, ));但这只影响当前会话且属于掩盖问题答辩不建议这么讲。5.5 答辩演示翻车没有干净的演示库和自检脚本现象演示到“还书”时页面突然出现一条昨天的脏数据或者因为提前测试把库存改成负数界面显示库存不合理的数字。原因平时开发测试数据混在演示库里借还记录和库存早已不一致演示时的操作触发了隐藏的脏数据问题。解决答辩前导出一份干净的初始化脚本包含建表语句、基础字典数据、演示账号和几十条模拟数据。演示过程中只操作脚本里预置的三五本特定图书不要随机点。这本质上是把演示路径固定成剧本属于投入最小、回报最大的准备方式。6. 数据来源与验收一套能坚持到答辩的演示方案6.1 用存储过程生成三年假数据课设里最尴尬的提问是“你的系统跑过多大的数据量”提前生成一个三年跨度、几千条借阅记录的假数据集能让报表、排行榜、逾期统计都有内容可看。用存储过程一次插入比手写 INSERT 高效得多DELIMITER $$ CREATE PROCEDURE sp_generate_test_data(IN p_reader_count INT, IN p_borrow_count INT) BEGIN DECLARE i INT DEFAULT 0; DECLARE v_book_id INT; DECLARE v_reader_id INT; DECLARE v_offset INT; SET max_book (SELECT MAX(book_id) FROM book); SET max_reader (SELECT MAX(reader_id) FROM reader); WHILE i p_borrow_count DO SET v_book_id 1 FLOOR(RAND() * max_book); SET v_reader_id 1 FLOOR(RAND() * max_reader); SET v_offset FLOOR(RAND() * 1000) 1; INSERT INTO borrow (book_id, reader_id, borrow_date, due_date, return_date, status, fine_amount) SELECT v_book_id, v_reader_id, DATE_SUB(CURDATE(), INTERVAL v_offset DAY), DATE_ADD(DATE_SUB(CURDATE(), INTERVAL v_offset DAY), INTERVAL 30 DAY), IF(RAND() 0.3, DATE_ADD(DATE_SUB(CURDATE(), INTERVAL v_offset DAY), INTERVAL 31 DAY), NULL), IF(RAND() 0.3, 0, 1), 0.00 WHERE (SELECT available_copies FROM book WHERE book_id v_book_id) 0; SET i i 1; END WHILE; END$$ DELIMITER ;生成逻辑里有一个关键保护插入前检查库存大于 0否则跳过避免造出借阅记录存在但库存为负的假数据。IF(RAND() 0.3, ...)模拟了七成按时还、三成逾期几天还的真实分布这部分随机性会让报表曲线更有说服力。注意存储过程里的赋值要用SET 变量加SET 变量两种方式区分用户变量和局部变量混用容易踩坑。6.2 验收前最后跑一遍的一致性自检答辩前用这三条 SQL 做最终检查全部通过再去开演示页面。检查一是否存在未归还记录但 book 表对应库存已为负检查二是否存在 return_date 非空但 status 仍为 0 的脏数据检查三逾期未还记录中是否有人已经超过最大借阅数还在借书。-- 检查一库存为负的图书 SELECT book_id, title, available_copies FROM book WHERE available_copies 0; -- 检查二还书状态与日期不一致 SELECT borrow_id, book_id, reader_id, return_date, status FROM borrow WHERE return_date IS NOT NULL AND status 1; -- 检查三同一读者未还记录超过上限 SELECT reader_id, COUNT(*) AS borrowing_count FROM borrow WHERE status 0 GROUP BY reader_id HAVING COUNT(*) 5;这三条 SQL 是按“事实表状态是否自洽”的思路写的也是我在给课设做排查时常用的入口。很多同学功能全部能跑却在演示时被一句“库存对吗”问住原因就是没跑过这个检查。整个课设里最值得培养的习惯不是写完功能就结束而是给数据写体检脚本——任何增删改查做完第一件事不是看界面而是查表里那些数字是否还对得上。希望这个习惯帮你也帮到这里。本文还有配套的精品资源点击获取
返回列表