
简介面向大学数据库课程设计的《学校图书借阅管理系统》课程设计报告覆盖数据库设计的完整流程从需求分析、数据字典、数据流图到概要设计、E-R图与主要功能说明逻辑清晰章节完备。压缩包内仅1个doc文档约4.16MBWord格式便于直接阅读和修改可作为课程设计模板或答辩资料。已有11178人浏览学习是同类型设计报告中较热门的参考资源。内容围绕系统核心模块展开包括欢迎界面、基于登录身份的权限管理、读者/管理员双入口、图书信息录入与修改、读者注册与信息维护、图书查询借阅归还、数据备份与恢复以及密码设置与VFP环境恢复等报告还附有主程序和各表单代码能帮助理解VFP环境下的数据库操作与界面开发。整体结构清晰适合需要完成图书借阅类数据库课程设计的读者参考借鉴。1. 学校图书借阅管理系统的数据库设计难点不在建表在借阅关系怎么建模学校图书借阅管理系统的数据库设计最容易被低估的环节是借阅关系建模。见过不少课程设计和真实业务场景里的同类系统图书表和读者表通常建得很规整借阅记录表却随手设计几个字段结果系统一开放就翻车同一本书被两个读者同时借走、月底统计借阅量时数字怎么都对不上、想查某本实体书现在在谁手里只能人肉翻记录。这篇文章把整个设计过程拆开讲从业务需求梳理、ER 建模开始到关系模式设计、建表落地再到借书、还书、续借、预约的完整 SQL 实现最后把设计阶段最容易踩的坑逐个拆开。适合正在做课程设计、或者准备用数据库课程知识搭一个能真正跑起来的借阅管理后端的读者。2. 需求梳理与实体建模先画出借阅系统的 ER 图再动手写 SQL2.1 从业务流倒推数据流借书、还书、续借、预约背后各有哪些数据要落库我一般不直接画 ER 图而是先把业务流程列出来再倒推每个动作要读写哪些数据。学校图书馆的核心场景拆开看无非就是采编入库、借书、还书、续借、预约这几条线。每条线都对应明确的数据落点列成一张表之后需要建几张表基本就清楚了。业务用例要落库的数据涉及的表采编入库书目标信息、物理副本信息books、book_copies借书借出记录、副本状态变更、库存扣减borrow_records、book_copies、books还书归还时间、逾期罚款、副本状态回写borrow_records、fines、book_copies、books续借应还日期延后、续借次数累加borrow_records预约排队记录、保留状态、过期作废reservations、book_copies从这张表能看出三个容易漏的点。第一罚款不是一本书一个字段而是一条独立记录因为有逾期历史、缴纳状态、按天计费金额这些信息塞在借阅记录里会让表变得臃肿而且没法追溯。第二库存不是简单的「图书总量减当前借出量」因为同一本书有多个物理副本每个副本状态可能不同这个在后面 2.2 节重点讲。第三预约必须单独成表因为预约涉及排队顺序、保留截止日期而且预约状态和借阅状态是两套生命周期。每个用例再往下拆一层字段就出来了。借书时系统要记录谁借的、借的是哪个副本、哪天借的、应还日期是哪天、当前操作的管理员是谁。还书时除了更新归还日期还要算逾期天数生成罚款金额。续借时要判断续借次数上限和是否有预约排队只更新应还日期和续借次数。把这些字段汇总起来实体清单就已经成形了不需要凭感觉猜表结构。2.2 核心实体与关系识别以及一个典型的关系基数判断方法实体一共七个外加一个分类表。图书书目标books存的是「某一本书」的统一信息比如书名、作者、ISBN、出版社馆藏副本book_copies存的是「这一本书的某一个物理实体」比如某本具体的书摆在哪个书架、条码是多少、当前状态是什么。读者、管理员、借阅记录、罚款记录、预约记录各自独立。分类表按需加学校图书馆的图书量不算大但分类查询和统计报表很常见拆出来更规范。这里必须展开讲「书目-副本」为什么要拆两张表。常见错误设计是在 books 表里直接放一个库存数字用 quantity 表示总量、available 表示可借数。这个设计在演示 Demo 里能跑但实际一用就露馅。比如同一本书有十本副本其中一本破损、两本借出、一本被预约保留单靠两个整数是描述不了这种状态的。再比如要查「某个条码的书现在在谁手里」如果借阅记录只关联 book_id根本定位不到具体实体。拆成两张表之后每次借阅记录关联的是 copy_id 而不是 book_id所有和实体书相关的问题都能回答。实体间的关系基数不要凭概念推要从业务规则反推。规则是「一个馆藏副本同一时刻只能处于一条未归还的借阅记录里」那 Copy 和 BorrowRecord 就是 1:N规则是「一个读者可以同时借多本书」那 Reader 和 BorrowRecord 就是 1:N规则是「一本书可以被多个读者预约排队」那 Book 和 Reservation 就是 1:N规则是「一次借阅最多产生一条罚款」那 BorrowRecord 和 Fine 是 1:0..1。每条规则都是系统里真实执行的约束反推出来的关系基数才不会前后矛盾。ER 图在这个阶段也顺手就能画了实体画矩形关系画菱形基数和参与度标在连线上。画完之后做一次检查确认每个关系都能对应到 2.1 里列的业务用例没有「画了关系但不知道什么时候用」的悬空设计就可以进入关系模式转换了。3. 关系模式与建表落地把 ER 图转成不冗余的 MySQL 表结构3.1 从 ER 到关系模式的转换规则与规范化检查ER 图转关系模式的规则是固定的每个实体转一张表实体的属性变成字段1:N 关系把外键放在 N 端M:N 关系需要额外建中间表。这个系统里最典型的是 Book 和 Copy 的 1:N外键 book_id 放在了 book_copies 表借阅记录通过 copy_id 间接关联到书目整体关系链就是 readers → borrow_records → book_copies → books。这条链从头到尾都是 1:N不需要中间表设计负担小很多。转完表之后要做规范化检查重点看是否满足第三范式。举一个高频错误books 表里直接存 category_name 分类名称而不是 category_id。表面上看查询少了一次 JOIN但代价是要修改分类名时必须 UPDATE 全表里所有相关行而且不同管理员录入时手写的分类名稍微不一致「文学类」和「文学」就成了两条数据。把分类拆成独立表books 只存 category_id就消除了这个传递依赖。读者表的院系、班级字段同理如果只是展示用途可以存文本但要做按院系统计的报表就必须拆。有一个字段要单独讨论books 表里的 available_copies。严格按第三范式它是冗余字段因为可以由「total_copies 减去所有未归还的借阅记录数」推导出来。但实际业务里这个值被高频读取每次现算都要聚合 borrow_records代价太高。常见做法是保留这个冗余字段但给它立两条规矩只能通过存储过程或者触发器维护不允许应用层直接 UPDATE每次借还操作后要自查它和明细记录是否一致。冗余本身不是问题不受控的冗余才是问题。3.2 核心表的字段设计与类型选择字段类型的选择直接决定后面会不会踩坑。以下几类是这套系统里最容易选错的ISBN 用 VARCHAR(20) 而不是 BIGINT。ISBN 可能以 0 开头纯数字类型会丢前导零还有带连字符的展示形态BIGINT 存不了ISBN-13 已经超过 INT 范围用 BIGINT 也只是勉强够。金额统一用 DECIMAL(10,2)FLOAT 和 DOUBLE 在累加计算时会产生精度误差罚款金额虽然小统计报表里差几分钱也解释不清。业务日期用 DATE不用 DATETIME 或 TIMESTAMP因为应还日期、归还日期这类字段只需要日历日DATE 不涉及时区换算能避开很多玄学问题。状态字段用 TINYINT 加 COMMENT不用字符串字符串枚举值一旦写错大小写查询结果就缺一块而且 TINYINT 扩展新状态时不用改表结构。核心表的字段设计如下建表脚本在 3.3 节可以直接复制。categories 表含 id、name、descriptionbooks 表含 id、isbn、title、author、publisher、publish_year、category_id、total_copies、available_copies、statusbook_copies 表含 id、book_id、barcode、status、locationreaders 表含 id、student_no、name、gender、phone、email、register_date、status、max_borrow_countborrow_records 表含 id、reader_id、copy_id、borrow_date、due_date、return_date、renew_count、status、operator_idfines 表含 id、borrow_record_id、reader_id、amount、fined_days、reason、is_paid、paid_datereservations 表含 id、book_id、reader_id、reserve_date、expire_date、status。3.3 建库建表 DDL完整可复制的最小建表脚本下面的建表脚本基于 MySQL 8.0 语法字符集统一用 utf8mb4引擎统一用 InnoDB。所有外键默认 RESTRICT防止误删父表数据。状态字段加了 CHECK 约束如果你的 MySQL 版本较老检查约束可能只做语法解析而不强制执行这种情况下需要应用层或者存储过程兜底校验。CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_db; CREATE TABLE categories ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_categories_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT图书分类表; CREATE TABLE books ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, publisher VARCHAR(100) DEFAULT NULL, publish_year SMALLINT UNSIGNED DEFAULT NULL, category_id INT UNSIGNED NOT NULL, total_copies INT UNSIGNED NOT NULL DEFAULT 0, available_copies INT UNSIGNED NOT NULL DEFAULT 0, status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), UNIQUE KEY uk_books_isbn (isbn), KEY idx_books_category (category_id), CONSTRAINT fk_books_category FOREIGN KEY (category_id) REFERENCES categories (id), CONSTRAINT chk_books_available CHECK (available_copies total_copies) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT书目标表; CREATE TABLE book_copies ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, barcode VARCHAR(30) NOT NULL, status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 0在架 1已借 2预约保留 3破损 4丢失, location VARCHAR(50) DEFAULT NULL COMMENT 馆藏位置, PRIMARY KEY (id), UNIQUE KEY uk_book_copies_barcode (barcode), KEY idx_book_copies_book (book_id), CONSTRAINT fk_book_copies_book FOREIGN KEY (book_id) REFERENCES books (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT馆藏副本表; CREATE TABLE readers ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT(1) DEFAULT NULL COMMENT 0女 1男, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, register_date DATE NOT NULL, status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 1正常 0停用, max_borrow_count TINYINT UNSIGNED NOT NULL DEFAULT 5, PRIMARY KEY (id), UNIQUE KEY uk_readers_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT读者表; CREATE TABLE admins ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(50) NOT NULL, role TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 1管理员 2超级管理员, last_login_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_admins_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT管理员表; CREATE TABLE borrow_records ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, reader_id INT UNSIGNED NOT NULL, copy_id INT UNSIGNED NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, renew_count TINYINT UNSIGNED NOT NULL DEFAULT 0, status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 0借出中 1已还 2逾期已还 3丢失, operator_id INT UNSIGNED NOT NULL, PRIMARY KEY (id), KEY idx_borrow_records_reader_status (reader_id, status), KEY idx_borrow_records_copy (copy_id), KEY idx_borrow_records_due_date (due_date), CONSTRAINT fk_borrow_records_reader FOREIGN KEY (reader_id) REFERENCES readers (id), CONSTRAINT fk_borrow_records_copy FOREIGN KEY (copy_id) REFERENCES book_copies (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT借阅记录表; CREATE TABLE fines ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, borrow_record_id INT UNSIGNED NOT NULL, reader_id INT UNSIGNED NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, fined_days INT UNSIGNED NOT NULL DEFAULT 0, reason VARCHAR(100) NOT NULL DEFAULT overdue, is_paid TINYINT(1) NOT NULL DEFAULT 0, paid_date DATE DEFAULT NULL, PRIMARY KEY (id), KEY idx_fines_reader (reader_id), KEY idx_fines_record (borrow_record_id), CONSTRAINT fk_fines_record FOREIGN KEY (borrow_record_id) REFERENCES borrow_records (id), CONSTRAINT fk_fines_reader FOREIGN KEY (reader_id) REFERENCES readers (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT罚款记录表; CREATE TABLE reservations ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, reader_id INT UNSIGNED NOT NULL, reserve_date DATE NOT NULL, expire_date DATE NOT NULL, status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 0排队 1保留 2已借 3取消 4过期, PRIMARY KEY (id), KEY idx_reservations_book_status (book_id, status), KEY idx_reservations_reader (reader_id), CONSTRAINT fk_reservations_book FOREIGN KEY (book_id) REFERENCES books (id), CONSTRAINT fk_reservations_reader FOREIGN KEY (reader_id) REFERENCES readers (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT预约记录表;建表脚本里有几个设计决策需要说明。所有表统一用 utf8mb4 而不是 utf8是因为馆藏数据里可能录入生僻字分类描述和书名里也可能出现特殊符号utf8mb4 覆盖全部 Unicode 字符避免入库时报错。所有外键列都建了索引这是 InnoDB 的硬性要求之一如果外键列没有索引MySQL 会自动补一个隐藏索引与其让系统悄悄建不如在 DDL 里写清楚。borrow_records 的复合索引 idx_borrow_records_reader_status 覆盖了「查某个读者当前借了哪些书」这个最高频的查询单独建 reader_id 索引和 status 索引都不如这一个复合索引有效。fines 表和 reservations 表最常用的是按读者和按状态过滤所以索引都往这两个方向建。4. 借阅与归还主流程实现用存储过程包住事务用触发器守住约束4.1 借书流程库存检查、状态更新、记录插入如何串成原子操作借书不是一个 INSERT 就能完成的动作它横跨三张表校验读者资格、校验副本状态、扣减库存、更新副本、插入借阅记录。任何一个环节失败前面做的修改都要撤销。所以常见做法是把整个流程封装成存储过程用事务包起来。DELIMITER $$ CREATE PROCEDURE borrow_book( IN p_reader_id INT UNSIGNED, IN p_copy_id INT UNSIGNED, IN p_operator_id INT UNSIGNED ) BEGIN DECLARE v_reader_status TINYINT UNSIGNED; DECLARE v_max_borrow TINYINT UNSIGNED; DECLARE v_borrowed_count INT UNSIGNED; DECLARE v_copy_status TINYINT UNSIGNED; DECLARE v_book_id INT UNSIGNED; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 校验读者状态FOR UPDATE 防止同时被多条请求处理 SELECT status, max_borrow_count INTO v_reader_status, v_max_borrow FROM readers WHERE id p_reader_id FOR UPDATE; -- 注意SELECT 没查到数据时变量为 NULL必须先判断 NULL IF v_reader_status IS NULL OR v_reader_status ! 1 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT reader is not active; END IF; -- 统计该读者当前在借数量 SELECT COUNT(*) INTO v_borrowed_count FROM borrow_records WHERE reader_id p_reader_id AND status 0; IF v_borrowed_count v_max_borrow THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT borrow limit reached; END IF; -- 校验副本状态并锁定该行 SELECT status, book_id INTO v_copy_status, v_book_id FROM book_copies WHERE id p_copy_id FOR UPDATE; IF v_copy_status IS NULL OR v_copy_status ! 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT copy is not available; END IF; -- 条件更新只在库存大于 0 时才扣减避免并发把库存扣成负数 UPDATE books SET available_copies available_copies - 1 WHERE id v_book_id AND available_copies 0; IF ROW_COUNT() 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT no available stock; END IF; -- 副本置为已借出 UPDATE book_copies SET status 1 WHERE id p_copy_id; -- 插入借阅记录借期 30 天 INSERT INTO borrow_records ( reader_id, copy_id, borrow_date, due_date, return_date, renew_count, status, operator_id ) VALUES ( p_reader_id, p_copy_id, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), NULL, 0, 0, p_operator_id ); COMMIT; END$$ DELIMITER ;这段存储过程有四个关键点。第一SELECT ... FOR UPDATE 把读者行和副本行锁住两个会话同时借同一本副本时第二个会等第一个提交从源头避免并发竞争。第二UPDATE books 用的是条件更新而不是「先查再改」就算前面的锁漏掉了 books 表这一句也能保证库存不会扣成负数。第三每个 SELECT 之后都判断了 NULLMySQL 的 INTO 语句查不到数据时不会抛错而是把变量置 NULL如果不对 NULL 做拦截后面 IF 判断会因为 NULL 比较结果不是 TRUE 而直接跳过这是新手最容易忽略的坑。第四借期硬编码为 30 天实际项目中最好把借期上限和续借次数做成配置表通过参数传入不要散落在存储过程里。调用方式很简单管理员在借书页面点确认按钮后应用层执行CALL borrow_book(1001, 2001, 1)参数依次是读者 ID、副本 ID、操作管理员 ID。如果读者已停用、副本不在架、或者库存已清零存储过程会通过 SIGNAL 抛出带 MESSAGE_TEXT 的异常应用层捕获后直接展示给用户。4.2 还书与续借逾期天数计算和状态回写逻辑还书流程是借书的镜像操作多出来的部分是要计算逾期天数并生成罚款记录。如果还书时发现应还日期已经过了状态不能简单置为「已归还」要标记为「逾期已还」同时按逾期天数和每日罚金标准计算罚款。CREATE PROCEDURE return_book( IN p_record_id INT UNSIGNED, IN p_operator_id INT UNSIGNED ) BEGIN DECLARE v_reader_id INT UNSIGNED; DECLARE v_copy_id INT UNSIGNED; DECLARE v_book_id INT UNSIGNED; DECLARE v_status TINYINT UNSIGNED; DECLARE v_due_date DATE; DECLARE v_overdue_days INT; DECLARE v_fine_amount DECIMAL(10,2); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; SELECT reader_id, copy_id, status, due_date INTO v_reader_id, v_copy_id, v_status, v_due_date FROM borrow_records WHERE id p_record_id FOR UPDATE; IF v_status IS NULL OR v_status ! 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT record is not in borrowing status; END IF; SELECT book_id INTO v_book_id FROM book_copies WHERE id v_copy_id; -- 逾期天数当天归还 DATEDIFF 为 0不算逾期 SET v_overdue_days DATEDIFF(CURDATE(), v_due_date); IF v_overdue_days 0 THEN SET v_overdue_days 0; END IF; IF v_overdue_days 0 THEN UPDATE borrow_records SET return_date CURDATE(), status 2, operator_id p_operator_id WHERE id p_record_id; SET v_fine_amount v_overdue_days * 0.50; INSERT INTO fines ( borrow_record_id, reader_id, amount, fined_days, reason, is_paid, paid_date ) VALUES ( p_record_id, v_reader_id, v_fine_amount, v_overdue_days, overdue, 0, NULL ); ELSE UPDATE borrow_records SET return_date CURDATE(), status 1, operator_id p_operator_id WHERE id p_record_id; END IF; -- 副本状态回写 UPDATE book_copies SET status 0 WHERE id v_copy_id; -- 库存回补 UPDATE books SET available_copies available_copies 1 WHERE id v_book_id; COMMIT; END$$逾期天数用DATEDIFF(CURDATE(), v_due_date)算这里有个边界应还日期就是今天时 DATEDIFF 等于 0不应该算逾期所以代码里把所有负数都归 0。罚款金额按每天 0.50 元计算这个数值在真实场景中应该做成配置图书馆可能有不同的计费规则。还书后副本状态直接置回 0对应「在架」但如果这本书存在预约排队就不能直接回 0而应该置为 2「预约保留」这一步在第 4.3 节展开。续借流程相对简单但有一个容易忽略的检查项预约队列。如果有读者排着队等这本书续借就不能通过要把机本文还有配套的精品资源点击获取