ARTICLE DETAIL

资讯详情

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

图书管理系统课设实战:MySQL表设计、Flask事务与避坑指南

图书管理系统课设实战:MySQL表设计、Flask事务与避坑指南 简介面向计算机专业课程设计的图书管理系统项目包同时提供GUI与B/S两种架构思路核心采用B/S模式并基于SQL Server数据库实现适合正在完成数据库课设或需要参考完整项目流程的学生使用。压缩包共168个文件、约1.38MB其中67个JSP页面负责前端展示23个Java源文件与27个class文件构成业务逻辑另含SQL脚本、GIF/JPG图片及DOC文档方便直接部署和阅读设计说明。资源已有6928人学习或下载是经过较多实践检验、适合参考的课程设计样本。项目涵盖用户登录、图书添加、读者管理、借还书登记等典型模块SQL脚本可导入SQL Server 2005生成库表文档部分整理了ER图、关系模式以及索引、触发器、存储过程等高级特性的应用思路能帮助开发者完整理解从需求分析、数据库建模到编码实现的全过程同时也会涉及连接本地数据库的配置调整便于在此基础上快速迁移或扩展为自己的项目。1. 数据库课程设计图书管理系统为什么越老的题目越容易翻车数据库课程设计选“图书管理系统”几乎是每个计算机专业学生都会撞上的题目。这道题看起来平平无奇——无非是两张表加一个借还操作可真正能一次做完、答辩不翻车的并不算多。我反复看到两类失败一类把界面堆得很丰富后台却是一堆拼出来的 SQL参数校验几乎没有一类把读者信息、借阅记录、图书信息塞进同一张宽表字段二十多个范式一眼全乱。这篇笔记想做的是替你绕开这些常见坑把表结构设计、增删改查实现、易错点排查和答辩加分串成一条可复制的路线。适合正在做课设的在校生也适合第一次用 Python MySQL 做完整管理系统的初学者。2. 先把表结构设计明白ER 模型、范式与图书管理系统建表 SQL2.1 别一上来就写代码实体、关系与冗余字段怎么定很多同学拿到题目第一反应是打开 IDE 建数据库结果边写边改表结构改了三次前端跟着返工。我现在的习惯是先在纸上画 ER 图十五分钟画完后边的代码基本不用大改。图书管理系统里真正需要建模的实体只有四个图书、读者、借阅记录、管理员。图书和读者之间是多对多关系——一个读者可以借多本书一本书也可以被多个读者借过这个多对多关系落到数据库里必须拆成一张独立的借阅表而不是在读者表里塞一个“借过的书”字段。有人问为什么不用数组或者 JSON 存课设阶段我不建议因为你没法回答老师关于“怎么统计逾期”“怎么按日期筛选”的追问。再往下要赌一个设计决策馆藏副本怎么表达。常见的错误做法是同一本书买了三本就插三行内容几乎一样的记录只改一个自增主键。这会让“总共几本、借出几本”的统计变得十分啰嗦而且 ISBN 重复的三行数据在业务上根本不该分开。更好的做法是每一行 book 记录代表一种书用 total_copies 表示总册数用一个可借数表示当前还剩几本。第二范式要求非主键字段完全依赖主键这种冗余在我看来是值得的因为借书时可以只扣减一个整数不需要去 count 具体哪一本在外。课程设计报告里老师常问三范式但你要答的不是“我满足了第三范式”而是“我在第三范式的基础上做了哪些有意的冗余”。available_copies 就是典型例子——它不是必须存在的却不应该被批评因为在借还高频场景下每次 count 未归还记录的开销更大。写清楚这一点范式相关的提问基本就稳了。2.2 图书管理系统建表 SQL一份能直接跑的 mysql 脚本以下是我常用的建表脚本数据库名用 book_manager字符集统一 utf8mb4。表结构不复杂四张表就能覆盖课程设计的全部功能点CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE book_manager; CREATE TABLE admin ( admin_id INT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录名, password_hash VARCHAR(64) NOT NULL COMMENT 哈希后的密码, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (admin_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT管理员表; CREATE TABLE book ( book_id INT NOT NULL AUTO_INCREMENT COMMENT 图书内部编号, isbn VARCHAR(20) NOT NULL COMMENT ISBN 号同一本书多副本共用, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) NOT NULL DEFAULT COMMENT 作者, publisher VARCHAR(64) NOT NULL DEFAULT COMMENT 出版社, publish_date DATE DEFAULT NULL COMMENT 出版日期, total_copies INT NOT NULL DEFAULT 1 COMMENT 馆藏总册数, available_copies INT NOT NULL DEFAULT 1 COMMENT 当前可借册数, PRIMARY KEY (book_id), KEY idx_title (title), KEY idx_isbn (isbn) ) ENGINEInnoDB COMMENT图书表; CREATE TABLE reader ( reader_id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL COMMENT 身份证号带唯一约束, phone VARCHAR(20) DEFAULT NULL, max_borrow_count INT NOT NULL DEFAULT 5 COMMENT 最大可借数量, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (reader_id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB COMMENT读者表; CREATE TABLE borrow ( borrow_id INT NOT NULL AUTO_INCREMENT, reader_id INT NOT NULL COMMENT 关联读者, book_id INT NOT NULL COMMENT 关联图书, borrow_date DATE NOT NULL COMMENT 借出日期, due_date DATE NOT NULL COMMENT 应还日期, return_date DATE DEFAULT NULL COMMENT 实际归还日期NULL 表示未还, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 在借 1 已还 2 逾期未还, PRIMARY KEY (borrow_id), KEY idx_due_date (due_date), KEY idx_reader_status (reader_id, status), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (book_id) ) ENGINEInnoDB COMMENT借阅记录表;这份脚本里有几个值得注意的参数。第一borrow 表外键故意设成默认的 RESTRICT 而不是 ON DELETE CASCADE这会让删除有未还记录的读者时直接报错逼着你先处理借阅记录对课设来说这个行为反而是安全的。第二status 用 TINYINT 而不是字符串把“在借、已还、逾期”三种状态用 0、1、2 表达程序里维护一份状态字典即可比中文枚举值更容易写条件判断。第三available_copies 用 INT 而不是把剩余数实时查出来牺牲了一点一致性换来了借书接口里只需要一次条件 UPDATE。建完表之后不要忘记插入初始数据。没有数据的系统界面一片空白演示时也很尴尬。我一般会准备几条书名带检索价值的记录比如不同出版社的同名书这样演示模糊搜索时更有效果。2.3 字段类型、默认值与结构修改决定后面少返工的四条规则第一日期字段统一用 DATE不用 DATETIME。借书和还书业务关心的是“哪天到期”不是“几点几分借的”。DATE 在 Python 里对应 datetime.date计算逾期天数时直接相减得到 timedelta不需要处理时区。第二凡是涉及金额、罚款的字段用 DECIMAL不要用 FLOAT。FLOAT 是二进制浮点0.1 在机器里是无限循环存罚款金额会出现 0.30000000000000004 这种答辩时没法解释的诡异数字。DECIMAL(8,2) 做定点数不会出这种问题。第三手机号、身份证这类定长字段长度按真实场景给够。身份证 18 位手机号 11 位别为了省空间设个 10 位。第四如果表已经建好、数据也录了线上修改结构用 ALTER TABLE不要删表重建。课程设计里最常见的需求是给读者表加一个“最大借书数”限制字段ALTER TABLE reader ADD COLUMN max_borrow_count INT NOT NULL DEFAULT 5 AFTER phone;这条语句的关键参数是 AFTER它控制新字段插入的位置。如果对已有数据表执行务必带上 NOT NULL 和 DEFAULT否则 MySQL 会因为已有行没有该字段的值而报错。ALTER TABLE 不像删表重建那么暴力但也要先在测试库上跑一遍确认索引和外键没被影响。3. 用 Flask Python 把借书还书跑通增删改查的最小完整链路3.1 技术栈选择为什么我用 PyMySQL 加 Flask而不是直接写一个桌面程序图书管理系统可以用 Swing、C# WinForms 做但最近几年课程设计出现频率最高的是 Python 系。原因很实际系统要能演示必须能跨机器跑起来Flask 应用启动后浏览器里就能操作不需要在答辩教室装运行库。数据库访问层我推荐 PyMySQL 而不是 SQLAlchemy 这类 ORM。理由不是 ORM 不好而是答辩时老师很喜欢问“你这条 SQL 是怎么设计的”如果全程用 ORMSQL 都藏在框架里你很难讲清楚。PyMySQL 是纯 Python 实现的驱动安装不需要编译写出来的代码就是 SQL 本身每一步都能解释。课程设计追求的是你能驾驭的工具不是看起来高级的工具。依赖只需要两个flask 和 pymysql。连接数据库时不建议每写一个接口就创建一个连接而是创建一个模块级连接对象复用。对于课设的单机演示场景一个连接足够了真正到了连接不够用的时候再谈连接池。3.2 借书还书设计成事务接口代码与最关键的两条 SQL借书接口是整个系统的核心也是老师最容易深挖的部分。先看代码再讲为什么这样写import pymysql from datetime import date, timedelta conn pymysql.connect( host127.0.0.1, userroot, password123456, databasebook_manager, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) BORROW_PERIOD_DAYS 30 MAX_BORROW_COUNT 5 def borrow_book(reader_id, book_id): today date.today() due_date today timedelta(daysBORROW_PERIOD_DAYS) try: with conn.cursor() as cur: # 第一步扣减可借库存条件里直接带上 available_copies 0 sql ( UPDATE book SET available_copies available_copies - 1 WHERE book_id %s AND available_copies 0 ) affected cur.execute(sql, (book_id,)) if affected ! 1: conn.rollback() return {code: 4001, msg: 图书不存在或已被借完} # 第二步判断读者在借数量是否已达上限 cur.execute( SELECT COUNT(*) AS cnt FROM borrow WHERE reader_id %s AND status 0, (reader_id,), ) row cur.fetchone() if row[cnt] MAX_BORROW_COUNT: conn.rollback() return {code: 4002, msg: 超出最大借书数} # 第三步写入借阅记录 cur.execute( INSERT INTO borrow (reader_id, book_id, borrow_date, due_date, status) VALUES (%s, %s, %s, %s, 0), (reader_id, book_id, today, due_date), ) conn.commit() return {code: 0, msg: 借书成功应还日期为 %s % due_date} except pymysql.MySQLError as e: conn.rollback() return {code: 5000, msg: 系统异常%s % str(e)}这段代码里最关键的是第一步的条件更新。很多人写借书逻辑时习惯先 SELECT available_copies判断大于 0再 UPDATE。问题在于这两个操作之间存在时间差两个借书窗口如果同时读到同一本还剩 1 本的书后一个 UPDATE 会把可借数扣成负数这就是数据库并发锁机制要解决的事务边界。把“判断”和“扣减”合并进一条 UPDATE只要影响行数为 0就说明这本书在扣减那一刻已经不可借了这个方案叫乐观锁是保证库存不为负最简单的方式。接着看还书接口它和借书是一对写在一起才能看出门道def return_book(borrow_id): today date.today() try: with conn.cursor() as cur: # 先查出这条借阅记录的图书和读者信息 cur.execute( SELECT reader_id, book_id FROM borrow WHERE borrow_id %s AND return_date IS NULL AND status 0, (borrow_id,), ) row cur.fetchone() if not row: return {code: 4003, msg: 记录不存在或已经还过} # 更新借阅记录 cur.execute( UPDATE borrow SET return_date %s, status 1 WHERE borrow_id %s AND return_date IS NULL, (today, borrow_id), ) # 归还库存注意是加一而不是设置为 total_copies cur.execute( UPDATE book SET available_copies available_copies 1 WHERE book_id %s, (row[book_id],), ) conn.commit() return {code: 0, msg: 还书成功} except pymysql.MySQLError as e: conn.rollback() return {code: 5000, msg: 系统异常%s % str(e)}还书时有一个很容易犯的错误直接执行 “UPDATE book SET available_copies total_copies WHERE book_id %s”。这是典型的想当然因为这本书可能被借出了多本其中一本还回来库存应该加一而不是恢复成总量。还有一点是 UPDATE 语句里加了 return_date IS NULL 这个条件这保证了同一条记录不会被重复归还也算是另一层次的并发防护。借书和还书都涉及多条 SQL而且必须保证要么全部成功、要么全部失败所以都用事务包起来出错时 rollback。事务不能只想到数据库出错业务规则触发时也要回滚比如超出借阅上限。conn.commit() 放在 try 块的末尾一旦任何一步异常都回滚到最初状态。3.3 图书与读者的增删改查查询分页、修改结构及其参数说明图书列表接口要支持书名模糊搜索和分页这是所有课程设计系统都会有的功能。分页在 MySQL 里用 LIMIT 实现但要注意计算偏移量时类型必须正确def list_books(keyword, page1, page_size10): offset (page - 1) * page_size where params [] if keyword: where WHERE title LIKE %s OR author LIKE %s like_pattern % keyword % params.extend([like_pattern, like_pattern]) with conn.cursor() as cur: cur.execute( SELECT SQL_CALC_FOUND_ROWS * FROM book where ORDER BY book_id DESC LIMIT %s OFFSET %s, params [page_size, offset], ) books cur.fetchall() cur.execute(SELECT FOUND_ROWS() AS total) total cur.fetchone()[total] return {books: books, total: total, page: page, page_size: page_size}这里分页参数 page_size 和 offset 也通过参数列表传进去不要用字符串格式化拼到 SQL 末尾。LIMIT 的值在 MySQL 里也会参与解析虽然注入风险不如字符串字段高但传入负数或超大数会让查询行为异常参数化能一并解决类型问题。SQL_CALC_FOUND_ROWS 配合 FOUND_ROWS() 是 MySQL 计算总行数的常见姿势比先单独执行 COUNT(*) 少一次查询。当然如果数据量上万这个方案有性能瓶颈课设阶段没有关系答辩时能说出它的代价反而是加分点。新增图书和修改图书结构是另一组接口。新增图书时有一个细节如果同 ISBN 的书已经在表里应该把 total_copies 加一而不是插入重复行。修改图书信息时如果调整了 total_copiesavailable_copies 也要同步调整否则会出现可借数大于馆藏数的逻辑错误。这个约束用代码维护很啰嗦放到数据库触发器里更稳妥。我给一个可参考的触发器写法DELIMITER // CREATE TRIGGER trg_book_after_update AFTER UPDATE ON book FOR EACH ROW BEGIN IF NEW.total_copies NEW.available_copies THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT available_copies cannot exceed total_copies; END IF; END// DELIMITER ;触发器的作用是让数据库自己保证数据完整性不依赖后端写没写对。SIGNAL 语句在 MySQL 5.6 以上可用课程设计环境基本都满足。触发器适合放这种简单的业务约束但不要用来实现复杂逻辑否则排错会变成一场灾难。4. 图书管理系统避坑手册5 个答辩前必查的翻车点4.1 SQL 注入一个单引号就让搜索接口失控现象在读者搜索框输入王 OR 11接口返回了全部读者记录甚至首页直接白屏报错。原因代码用字符串格式化把参数拼进了 SQL。比如SELECT * FROM reader WHERE name %s % name。单引号闭合了原本的字符串字面量后面的 OR 条件被当作 SQL 执行。解决一律改用参数化查询不要手动拼接任何用户输入。很多同学以为 LIKE 查询不能参数化其实通配符可以放在参数值里# 错误写法 sql SELECT * FROM reader WHERE name LIKE %%%s%% % keyword # 正确写法 sql SELECT * FROM reader WHERE name LIKE %s cur.execute(sql, (% keyword %,))参数化之后MySQL 会把整个字符串当成一个值来处理单引号不会再闭合 SQL 语句。答辩时如果老师问“怎么防 SQL 注入”你说出“参数化 不信任任何用户输入”这一句就足够了不需要背各种注入 payload。4.2 并发借书两个窗口抢最后一本库存被借成负数现象演示时开两个浏览器窗口同时借同一本只剩一本的书两个页面都提示借书成功但图书表里 available_copies 变成了 -1。原因你用了“先查询、再判断、后更新”的三步流程。A 窗口查到剩余 1 本B 窗口也查到剩余 1 本两个窗口都认为可以借于是都执行了减一操作。这是数据库并发控制里典型的检查与执行之间的竞态条件。解决把判断和扣减合并成一条条件更新也就是第 3 章借书接口里的写法靠 affected 行数判断是否真的扣成功。如果这条 UPDATE 影响了 0 行说明书已经没了直接回滚。这种方法本质上是用数据库的行锁来保证原子性不需要显式开启 SELECT FOR UPDATE也更符合课程设计的使用场景。另一个副作用是即便以后引入连接池或多线程这段代码依然不会超借这是一次性做对后面不用返工。4.3 逾期天数算错日期时区不一致系统早还了三天现象读者逾期两天还书系统却显示“已逾期 -1 天”或者显示逾期 5 天怎么也对不上。原因数据库里 borrow_date 存的是 DATETIME包含时分秒程序里取出来后又用一个带时区的时间对象去减减出来的 timedelta 带着小时数转成整数天数时被四舍五入带偏了。如果服务器时区是 UTCPython 本地是东八区还会出现整整 8 小时的偏移这已经不是玄学是必现的 bug。解决建表时统一用 DATE 类型程序设计里统一用 date.today() 生成日期。跨时区问题最稳妥的办法是别让时区参与计算。如果确实需要在界面上显示几点几分借的那就在数据库中保留一个 created_at DATETIME 作为审计字段而业务计算继续用 DATEfrom datetime import date overdue_days (date.today() - borrow_record[due_date]).days if overdue_days 0: print(逾期 %d 天 % overdue_days)date 对象相减得到的 timedelta直接取 .days 就是整数天数没有小时、时区的干扰。这是唯一的正确答案。4.4 删除读者失败外键约束在保护你的数据现象删除一个读者时报错Cannot delete or update a parent row: a foreign key constraint fails。原因外键约束默认阻止删除被引用的行。读者表里有借阅记录引用 reader_id直接删读者会导致这些记录变成悬空数据。解决有两个方向。一是把状态检查前置代码里先查这个读者有没有未归还的借阅记录有就提示“请先完成还书”没有就删除该读者的所有历史借阅记录最后再删读者。二是建表时给外键加上 ON DELETE CASCADE删读者时自动删掉他的全部借阅记录。课程设计我推荐第一种因为在真实图书馆里读者的借阅历史是需要保留的不能因为注销账号就清空。而且第一种策略更好演示你可以现场给老师展示“业务规则阻止了危险操作”这个场景。# 删除读者的正确顺序 cur.execute(SELECT COUNT(*) AS cnt FROM borrow WHERE reader_id %s AND status 0, (reader_id,)) if cur.fetchone()[cnt] 0: return {code: 4004, msg: 该读者有未还图书不能删除} cur.execute(DELETE FROM borrow WHERE reader_id %s, (reader_id,)) cur.execute(DELETE FROM reader WHERE reader_id %s, (reader_id,))4.5 密码明文入库做课设也要至少走一次哈希现象打开 admin 表里面的 username、password 清清楚楚是明文。原因偷懒没有做任何处理。图书管理系统的管理员表虽然不像商业系统那样高价值但这是答辩时老师一眼就能看到的低级错误。解决用哈希函数处理密码。Flask 生态里自带 werkzeug.security不需要额外引入其他依赖from werkzeug.security import generate_password_hash, check_password_hash # 注册管理员时 password_hash generate_password_hash(admin123) # 登录校验时 is_ok check_password_hash(password_hash, input_password)需要说明的一点是哈希不是加密它不可逆。哈希后的结果只有校验作用即使数据库泄露攻击者也拿不到原始密码。课设阶段做到这一层已经足够答辩加分很多。不要自己写一个简单的 hash(passwordsalt) 然后到处宣扬直接说使用了 werkzeug 的标准哈希实现简洁有力。5. 从“跑通”到“答辩顺”索引验证、连接池与报告里的加分手法系统功能跑通只是完成了 60%剩下 40% 在“能不能讲清楚”。我的习惯是在答辩前做三件小事验证索引是否真正生效、给数据库加一层连接复用、用一张图说明数据流转过程。索引验证用 EXPLAIN。图书表的 title 字段已经建了索引但如果你在查询里写成WHERE title LIKE %三国%前置通配符会让索引失效。实际验证mysql EXPLAIN SELECT * FROM book WHERE title LIKE 三国%\G看 key 字段如果显示 idx_title说明索引被用上了如果显示 NULL就要检查 SQL 写法。这个动作很简单但很能体现你对数据库性能是动过脑子的而不是写完功能就去等答辩。连接池的引入时机我放在功能稳定之后。PyMySQL 本身每次请求建立一个连接在课设演示环境下完全没有问题只有当你要强调“系统能支撑多少并发”时才值得改成 DBUtils 的 PooledDB。连接池的好处是复用 TCP 连接减少握手开销坏处是代码里多了一层抽象老师问你连接池参数怎么配时你得能答得上来。不建议为了炫技而加。写课程设计报告时不要罗列界面截图。我会画一张时序图把借书流程从“前端提交请求 → Flask 路由 → PyMySQL 执行条件 UPDATE → 事务提交”串起来标注出每一步的返回值和异常处理分支。这张图比十张截图都更能证明系统是你自己做的。这些年我看过的课设里翻车最多的不是不会写代码而是没想清楚就动手写到最后自己和代码一起成了一团黑匣子。我现在拿到这类题目会先花二十分钟画 ER 图、列出状态字段、标出事务边界再开始敲键盘。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取
返回列表