
简介这是一份四川大学数据库系统原理课程设计项目源自2021年陈鹏班学生主题为简单的图书馆管理系统。项目面向数据库课程学习者及初级开发者将数据库理论应用于图书信息维护、读者管理、借还书流程、条件查询与统计报表等真实场景。压缩包共一千八百六十二个文件主要包含前端脚本、配置文件、技术文档、脚本语言源码及数据库脚本并配有编译工具与辅助脚本整体大小约十六点八四兆字节目录划分清晰便于分模块研究。已有约一百三十九人学习下载。通过该项目可学习数据库表结构规划、事务处理与存储过程等实现细节也可参考其从需求建模到编码落地的完整思路作为课程设计或项目开发的实用范本。1. 数据库系统原理课程设计里的“简单图书馆管理”这个ZIP值不值得打开一位2021年四川大学计算机专业的学生在陈鹏老师的数据库系统原理课上交了一份课程设计打包成“A-Simple-Library-Management.zip”。这个包在二手代码库里流传很久很多后来者把它当作业参考也有人照着它一路搭到毕业设计。一个图书管理系统几乎是每个学数据库的人绕不过去的第一道坎——它看起来只有“增删改查”但把这本书、那张借阅表、那个还书日期放到一起所有数据库原理课的重点都会在写代码时一一浮现。很多人拿到这份ZIP以后第一反应是“怎么这么简单”第二反应是“为什么我按它的表结构写还是会翻车”。这篇笔记就把这个方向拆开先讲清楚这个课程设计里数据模型该怎么立住再给出一套能跑通的最小实现然后把我在带这类项目时踩过的坑一条条列出来。无论你是在校生想参考课程设计还是想借这个题目练手把数据库系统原理落到实处照着这套思路走比直接解压看代码有用得多。2. 拆图书管理系统的数据模型实体怎么抽、主键怎么定、约束怎么给2.1 从需求里抽实体图书、读者、借阅以及最容易漏掉的“副本”图书管理系统最怕一上来就建一张“图书表”然后直接开写。为什么不对因为现实中“一本书”和“书架上那一本”并不是同一个概念。图书馆采购十本《数据库系统概论》书名、作者、出版社、ISBN都一样但它们有不同的馆藏条码可以被不同读者同时借走。课程设计里评委最爱问的问题之一就是“你这种书只有一本库存怎么让三个人同时借”所以最少需要拆出两层一层是图书书目book_title记录这本书的元数据另一层是馆藏副本book_copy记录图书馆里实际存在的每一本物理书。一个title对应多个copy这是第一次设计时最容易漏掉的关系。读者reader和借阅记录borrow再各成一张表四张表就能覆盖80%的业务。我第一次给课程设计做评审时看到不少同学把“是否借出”直接做成book_title里的一个字段结果同一本书两个副本一个借出、一个在馆时字段就写不下了。这不是技术不够是实体抽取这一步没过关。数据库系统原理课里讲的“概念模型→逻辑模型”落到这个项目里就是先把“书目/副本/读者/借阅”四个概念分清楚再谈建表。2.2 借阅关系表的设计主键和两个外键为什么要定义一个复合唯一约束实体分完之后关系的设计决定这个系统能不能跑下去。借阅记录borrow是联系读者和副本的中间表字段至少要有borrow_id主键、reader_id外键、copy_id外键、borrow_date借出日期、due_date应还日期、return_date实际归还日期为空表示未还、status状态位。这里有一个关键约束同一本书的同一个副本不能同时存在两条“未归还”的借阅记录。如果在程序里靠逻辑判断“先查再插”两个请求并发过来就会同时通过检查造成一本书记在两个读者名下。正确的做法是在数据库层面挡住它——给(copy_id, status)加一个复合唯一约束status为0时记录在借为1时已归还。同一副本如果已有status0的记录第二条插入会被唯一键直接拒绝。课程设计里能写出这一步的通常已经理解约束是数据库最后一道防线。主键选型上borrow_id用自增整数足够。读者表用自增id_card、手机号这些业务字段当主键都会遇到修改成本和索引膨胀的问题初期不用纠结自增主键加上唯一索引就够了。图书书目的ISBN也不要直接当主键——重印版ISBN不变但版次不同、丛书有分册ISBN、编目输入偶尔出错这些情况都会让ISBN当主键变得很痛苦。给每一条书目一个自动增长的title_idISBN只做普通唯一索引。2.3 索引不是越多越好借阅表上真正有用的三个索引课程设计的数据量小几千条记录怎么查都快但评委和老师都会追问一句“这个表要不要加索引”。借阅表是查询压力最大的表有三个索引值得加。第一个是(copy_id, status)的复合唯一索引刚才已经讲过它同时承担了唯一约束和查询加速。第二个是(reader_id, status)用于查看某个读者的在借列表和逾期列表这是图书管理系统里最常见的操作。第三个是due_date单列索引用于“拉出所有超过应还日期还没归还的记录”这种定时任务。三个索引覆盖了借阅、查询、统计三条主路径。不要在varchar字段上盲目加前缀索引更不要给每条书目都把title_name、author、publisher三个字段全加上索引。课程设计如果当场被问“索引为什么这么建”回答“因为查询模式里筛选条件集中在这些列上”就比“为了防止查询慢”更有说服力。下面是一份可以当作起点的建表SQL用MySQL 8.0的InnoDB引擎CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE library; CREATE TABLE book_title ( title_id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL, title_name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), publish_year YEAR, total_copies INT NOT NULL DEFAULT 0, UNIQUE KEY uk_isbn (isbn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book_copy ( copy_id INT AUTO_INCREMENT PRIMARY KEY, title_id INT NOT NULL, barcode VARCHAR(50) NOT NULL, location VARCHAR(50), status TINYINT NOT NULL DEFAULT 0 COMMENT 0在馆 1借出 2遗失 3损坏, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_barcode (barcode), CONSTRAINT fk_copy_title FOREIGN KEY (title_id) REFERENCES book_title (title_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reader ( reader_id INT AUTO_INCREMENT PRIMARY KEY, reader_name VARCHAR(50) NOT NULL, id_card VARCHAR(18), phone VARCHAR(20), reg_date DATE NOT NULL DEFAULT (CURRENT_DATE), status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1停借 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow ( borrow_id INT AUTO_INCREMENT PRIMARY KEY, reader_id INT NOT NULL, copy_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, renew_count TINYINT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出中 1已归还 2逾期未还, CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader (reader_id), CONSTRAINT fk_borrow_copy FOREIGN KEY (copy_id) REFERENCES book_copy (copy_id), UNIQUE KEY uk_active_borrow (copy_id, status), INDEX idx_borrow_reader (reader_id, status), INDEX idx_borrow_due (due_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表脚本里值得注意的细节有三个。一是utf8mb4而不是utf8MySQL的utf8实际上最多存3字节遇到生僻字和emoji直接报错。二是status字段用TINYINT注释说明而不是用字符串“未归还/已归还”查询比较效率高程序里用常量映射改动状态含义时只改注释和代码不需要改表结构。三是外键约束全部显式命名这样后面删除外键或排查错误时报错信息能直接看到是哪一个约束在拦截。3. 把课程设计跑起来环境搭建、初始化数据与借还书的最小闭环3.1 本地环境怎么搭MySQL 8.0 Python 3.10字符集一次到位课程设计最常见的交付形式是源码加SQL脚本评审老师会现场导入运行。不要用SQLite交作业更不要用Access——数据库系统原理课程设计的核心是体现关系数据库的能力MySQL 8.0是本地复现成本最低的选择。安装MySQL之后第一件事是确认连接字符集。很多同学建库时用了utf8mb4程序里却因为缺了charset参数而读出一堆乱码。统一的原则是建库、连接、表结构三处都显式指定utf8mb4。下面是建库和验证的命令# 创建数据库字符集和排序规则一次设好 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; # 查看当前库的字符集确认不是latin1或utf8 mysql -u root -p -e SHOW CREATE DATABASE library;-- 导入上一节的表结构脚本 source /path/to/schema.sql; -- 验证三张核心表的字符集 SHOW TABLE STATUS FROM library WHERE Name IN (book_copy, borrow) \G连接层同样要带字符集以Python的pymysql为例pymysql.connect(..., charsetutf8mb4)少这个参数写入emoji或生僻字时连接会直接断开。这部分我见过太多课程设计翻车在这里不是SQL写错是客户端连接和服务器端配置不一致。3.2 初始化数据别手写INSERT写一段可重复执行的数据脚本图书管理系统评审时最怕打开数据库是空的评委没法点“借书”按钮。初始化数据要写成一个独立的seed.sql内容包括35种图书书目每种23个馆藏副本5个读者以及两三组借阅记录——其中一条要故意设置为逾期状态方便演示借阅查询。seed.sql里有一个坑插入顺序受外键约束限制必须先插book_title再插book_copy再插reader和borrow。如果先插副本外键找不到对应书目直接报1452错误。下面是一段最小化的初始化脚本USE library; INSERT INTO book_title (isbn, title_name, author, publisher, publish_year, total_copies) VALUES (9787302519582, 数据库系统概论, 王珊 萨师煊, 高等教育出版社, 2014, 2), (9787111558422, 高性能MySQL, Baron Schwartz, 电子工业出版社, 2013, 2), (9787121362145, SQL必知必会, Ben Forta, 人民邮电出版社, 2020, 1); INSERT INTO book_copy (title_id, barcode, location, status) VALUES (1, BK001, A区1架, 1), (1, BK002, A区1架, 0), (2, BK003, B区2架, 0), (2, BK004, B区2架, 1), (3, BK005, C区3架, 0); INSERT INTO reader (reader_name, id_card, phone, status) VALUES (张同学, 510104200201011234, 13800000001, 0), (李同学, 510104200205056789, 13800000002, 0); INSERT INTO borrow (reader_id, copy_id, borrow_date, due_date, return_date, status) VALUES (1, 1, DATE_SUB(CURDATE(), INTERVAL 20 DAY), DATE_SUB(CURDATE(), INTERVAL 5 DAY), NULL, 2), (2, 4, DATE_SUB(CURDATE(), INTERVAL 3 DAY), DATE_ADD(CURDATE(), INTERVAL 27 DAY), NULL, 0), (1, 5, DATE_SUB(CURDATE(), INTERVAL 40 DAY), DATE_SUB(CURDATE(), INTERVAL 10 DAY), DATE_SUB(CURDATE(), INTERVAL 8 DAY), 1);注意borrow表里第一条记录把status置为2逾期未还同时copy表里对应副本status1借出两边状态要人工对齐。这种一致性在真实系统里应当由事务或存储过程维护初始化脚本里手工写数据时尤其容易漏掉对应关系。每次重新导入时先执行SET FOREIGN_KEY_CHECKS 0;清理旧数据再按顺序导入最后把开关恢复为1可以省掉很多“为什么删不掉旧数据”的麻烦。3.3 借书还书的最小闭环把两步更新放进同一个事务课程设计里借书不能只做一次INSERT。借书这个动作在业务上包含两件事往borrow表插入一条新记录同时把book_copy里对应副本的status从0改成1。这两步必须在一个事务里否则插入成功了而状态没更新书就凭空消失了或者状态更新了而插入失败副本状态变成借出却又没有借阅记录。用Python写最小实现会长这样import pymysql from datetime import date, timedelta conn pymysql.connect( hostlocalhost, userroot, password你的密码, databaselibrary, charsetutf8mb4, autocommitFalse, ) try: with conn.cursor() as cur: # 第一步查一个在馆副本用 FOR UPDATE 锁住这一行防止并发重复借出 cur.execute( SELECT copy_id, title_name FROM book_copy JOIN book_title USING (title_id) WHERE book_copy.status 0 AND book_title.title_name LIKE %s ORDER BY copy_id LIMIT 1 FOR UPDATE , (%数据库%,)) row cur.fetchone() if row is None: raise Exception(没有可借的在馆副本) copy_id row[0] # 第二步写入借阅记录 today date.today() due today timedelta(days30) cur.execute( INSERT INTO borrow (reader_id, copy_id, borrow_date, due_date, status) VALUES (%s, %s, %s, %s, 0) , (1001, copy_id, today, due)) # 第三步把副本状态置为借出 cur.execute(UPDATE book_copy SET status 1 WHERE copy_id %s, (copy_id,)) conn.commit() # 全部成功才提交 except Exception as e: conn.rollback() print(借书失败事务已回滚, e) finally: conn.close()这段代码有三个细节值得说明。第一SELECT ... FOR UPDATE会把查到的副本行锁定另一个并发借书请求会阻塞到当前事务结束从根上避免两笔借阅同时拿到同一个副本。如果课程设计里讲了事务隔离级别这里正好可以解释“为什么用行锁而不是靠应用层判断”。第二UPDATE影响的函数用cur.rowcount可以确认是否真的改了一行若rowcount为0说明锁到的记录被并发改掉了应当立刻回滚。第三commit放在try块末尾任何一步异常都走rollback这是借还书这类多步写操作的标准姿势。还书逻辑与其对称先UPDATE borrow把return_date置为今天、status置为1再UPDATE book_copy把status置为0。同样放在一个事务里两个操作任何一步失败都不会留下“已还书但副本还在借出”的数据不一致问题。4. 课程设计避坑指南乱码、外键、日期和并发的5个排查记录4.1 中文全变成问号字符集三层配置不一致现象建库和表都用了utf8mb4程序里读出来的中文却是“???”写入带生僻字的数据直接报Incorrect string value错误。原因客户端连接参数没有传charset。pymysql和JDBC都默认按latin1处理连接服务器端和客户端两边字符集不一致中文在多字节转换时被吞掉。建库、表结构、连接参数三个环节必须同时指定utf8mb4缺一环就乱码。解决连接字符串里显式加charsetutf8mb4JDBC则是在URL后面追加characterEncodingutf8。已经出现乱码的数据把表ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4但注意已存入的乱码字符无法恢复只能重新导入数据。所以正确的顺序是先查SHOW VARIABLES LIKE character_set%确认服务器端配置再写连接参数最后导数据。4.2 删图书时被外键卡住1451错误不是bug是约束在工作现象执行DELETE FROM book_title WHERE title_id 1;报错Cannot delete or update a parent row: a foreign key constraint failsNavicat里怎么删都删不掉。原因book_copy表有外键引用book_title只要还存在任何副本记录父表不允许被删。这是InnoDB外键的默认行为属于数据库的正确保护不是配置问题。不少同学会直接SET FOREIGN_KEY_CHECKS0删完之后再恢复这在课程设计里是危险的——如果删除时副本还处于借出状态数据逻辑就彻底坏了。解决区分“物理删除”和“逻辑下架”。真实图书管理系统里书目和副本几乎不做物理删除而是加status字段标记为“下架”。课程设计里如果要演示删除应当先检查该书目是否还有在借副本SELECT 1 FROM borrow WHERE copy_id IN (SELECT copy_id FROM book_copy WHERE title_id1) AND status0有在借记录则禁止删除没有则先删borrow、再删copy、最后删title顺序不能反。4.3 还书日期早于借书日期数据库接受了不该接受的数据现象程序里传入了borrow_date2021-05-01、return_date2021-04-20的还书请求数据库插入成功还书超期计算结果变成负数。原因应用层没做日期先后校验MySQL 8.0之前的老版本对CHECK约束是解析但不执行——即使用户写了CHECK (return_date borrow_date)MySQL 5.7也照样把非法数据放进去这一点是历史遗留问题很多老教程没提。解决双保险。第一道在应用层插入前判断return_date borrow_date直接拒绝第二道在数据库层MySQL 8.0已经开始支持CHECK约束补上ALTER TABLE borrow ADD CONSTRAINT chk_return_after_borrow CHECK (return_date IS NULL OR return_date borrow_date);给课程设计加这道约束评审时分数会有明显差异——它证明你理解“数据库是数据的最终守门人”。4.4 视图在Navicat里能跑程序里报错图形化工具生成的SQL不能直接搬现象在Navicat里右键建了一个逾期统计视图运行正常。把同一段SQL粘到Java代码里执行时报语法错误或者查出来结果少了数据。原因图形化工具生成的SQL里常带有反引号包裹的库名表名、DEFINER声明以及工具自己加的排序和类型转换。程序端执行时SQL的上下文和权限不一样更隐蔽的问题是把NULL日期参与了DATE_DIFF计算返回了NULL而不是0统计结果“看起来不对”。解决写视图坚持用纯ANSI SQL不依赖任何工具的图形化产物。逾期视图的核心逻辑这样写CREATE OR REPLACE VIEW v_overdue_borrow AS SELECT borrow_id, reader_id, copy_id, due_date, DATEDIFF(CURDATE(), due_date) AS overdue_days FROM borrow WHERE status 0 AND due_date CURDATE();注意WHERE里直接比较日期而不对NULL做运算NULL日期不会进入结果集。程序里执行这个视图时直接SELECT * FROM v_overdue_borrow不要重建视图视图建好后当作表来查即可。4.5 并发借同一本书导致库存变负数应用层检查拦不住并发现象两个同学同时打开系统借最后一本《数据库系统概论》两个请求都通过了“剩余库存大于0”的判断最后total_copies变成-1借阅记录多了一条。原因应用层的“先查库存再判断”是两个独立步骤两个事务并发时都读到旧值各自判断通过然后各自执行插入和更新。MySQL默认隔离级别REPEATABLE READ下普通SELECT不构成锁定读到的都是快照更新时就会互相覆盖。解决三条路任选。一是UPDATE语句带上条件UPDATE book_copy SET status1 WHERE copy_id%s AND status0然后检查影响行数等于0说明副本已经被抢走整体回滚二是查询时加FOR UPDATE就像3.3节写的那样三是给货存表加一个version字段做乐观锁更新时对比版本号。课程设计里推荐第一种改动最小、最容易讲清楚。5. 从60分到90分验收顺序、测试方法和三个加分项课程设计交上去之前按下面的顺序自己过一遍先做回归验收再造一点压力再看查询计划。验收内容围绕核心业务展开——借一本在馆的书、借一本已被借走的书、还书、续借、拉取读者在借列表、列出逾期记录。每一项都要验证“成功时的数据变化”和“失败时数据没变”两个侧面后者比前者更能暴露设计问题。测试借贷操作时不要只开一个窗口点按钮。打开两个MySQL客户端同时执行相同的借书请求看第二个是报唯一键冲突还是正常通过。如果正常通过说明你的借书流程没有走到表级约束这是最大的隐患。验证时执行EXPLAIN SELECT ... FROM borrow WHERE reader_id1 AND status0;看索引是否被用上确认idx_borrow_reader不是白建的。三个加分项里性价比最高的是给不同角色做权限区分。创建一个只读用户给演示用一个读写用户给图书管理员不要全程用root连接。这一步能体现数据库系统原理课里安全管理这一章不是白学的。第二个加分项是写一个简单的mysqldump备份脚本每天定时导出演示数据丢失后恢复的过程。第三个是把逾期未还书的列表做成视图并配一个触发器插入借阅记录时检查读者是否已有逾期未还有则拒借。触发器逻辑写在数据库里比写在应用层更能防住绕过程序直连数据库的操作。最后说一个我带这类课程设计时反复遇到的情况很多同学的评价标准是“功能能跑”而拿到高分的人标准是“数据经得起问”。评委随手点几下按钮看不出来架构差距但只要你把上面的约束、事务、索引和账户权限每一处都用一两句话说清楚“为什么这么设计”比“能跑”值钱得多。把这份ZIP当成一个起点照着这个方向改一遍你会发现自己对数据库系统原理的理解比抄十份代码都扎实。希望帮到你。本文还有配套的精品资源点击获取