资讯详情 数据库课程设计图书管理系统:从ER图到SQL与Flask演示的完整实战
📅 2026/10/12 4:03:20
简介面向本科数据库课程设计与SQL大作业的实验报告完整范例以图书管理系统为项目场景完整展示从需求分析、概念结构设计、逻辑结构设计到数据库实施、代码实现与总结的全流程可作为课程设计选题及报告撰写的直接参考。内容按标准实验报告章节组织包含项目背景与编写目的、数据字典、全局ER图、关系模式、基本表/视图/索引设计、建库建表SQL和相关模块代码并配目录、总结与参考文献结构清晰便于逐节对照修改开发环境为SQL Server 2005适合熟悉关系型数据库的本科生使用。包内共1个文件为doc格式文档大小1.05MB可打开阅读、二次标记和打印便于作为模板填充自己的设计方案。目前已有89人学习该资源对有数据库课程设计、图书管理系统设计或实验报告写作需求的学生有较强参考价值。1. 数据库课程设计图书管理系统实验报告在写什么一条从 ER 图到 SQL 再到答辩验证的硬链路每年到这个节点总有同学拿着别人分享的“数据库课程设计(图书管理系统)实验报告.doc”来找我问能不能照着改一改直接交。我的回答一向是报告能抄数据库不能抄。实验报告只是最终呈现物真正的得分点在于你能否从一份 ER 图出发完成建库、建表、增删改查、事务处理和演示系统的一整条链路——图书管理系统恰好是这条链路上最经典的题目因为它涉及实体关系建模、外键约束、库存扣减和逾期计算足以覆盖数据库课程 80% 的考点。这篇笔记就把我做这类课程设计的完整路径拆给你表怎么建模、SQL 怎么写才不会被老师追问倒、演示系统怎么用最少代码跑起来、哪些坑是每年都有人踩的。2. 从需求到表结构图书管理系统的数据建模与字段取舍2.1 借还书流程决定实体划分先画流程图再画 ER 图很多人的第一反应是打开设计工具直接画表这恰恰是翻车的起点。图书管理系统的核心业务动作只有三个入库、借出、归还。围绕这三个动作你需要回答的问题分别是一本书的信息存在哪一个读者能借几本、逾期怎么办一次借阅行为如何完整记录。由此拆出来的实体至少有四类图书、读者、借阅记录、图书分类。常见做法是先画一张业务流程图把“读者查书→提交借书申请→管理员确认库存→扣减库存→生成借阅记录→归还时更新状态”这条链路走一遍再去映射数据表。E-R 图里的关系也要在表结构上落到实处。图书与分类是多对一读者与借阅记录是一对多图书与借阅记录也是一对多。这里有个容易被老师追问的点为什么不把借阅记录并入图书表或读者表因为借阅记录是“行为历史”一条记录对应一次借出行为如果并进图书表一本书被借 100 次就得复制 100 条图书数据数据冗余会直接破坏第二范式。所以借阅记录必须独立成表用两个外键分别指向图书和读者。2.2 核心表字段设计类型、默认值、约束一次到位我一般会在一张表上同时完成字段定义、类型选择和约束设计而不是先画字段再补约束。下面这四张表的结构是我在课程设计里推荐的最小可行方案也方便你直接抄进实验报告的“数据库设计”章节。图书表book字段名类型约束/默认值说明book_idINT UNSIGNEDAUTO_INCREMENT PRIMARY KEY内部主键与 ISBN 解耦isbnVARCHAR(17)NOT NULL UNIQUE存带横杠的 ISBN便于阅读titleVARCHAR(100)NOT NULL书名authorVARCHAR(50)NOT NULL作者publisherVARCHAR(50)DEFAULT NULL出版社priceDECIMAL(10,2)DEFAULT 0.00价格不用 FLOATcategory_idINT UNSIGNEDNOT NULL外键分类stockINT UNSIGNEDNOT NULL DEFAULT 1库存数量locationVARCHAR(20)DEFAULT NULL馆藏位置create_timeDATETIMEDEFAULT CURRENT_TIMESTAMP入库时间价格用 DECIMAL(10,2) 而不是 FLOAT这是很多人不理解的取舍。FLOAT 是近似值0.1 在二进制里无法精确表示累加 10 次就会产生可观察的误差图书价格和逾期罚款都涉及金额精确到分是最低要求。ISBN 用 VARCHAR(17) 是因为标准的 ISBN-13 带连字符后恰好 17 个字符用 INT 存不下用 CHAR(13) 又不方便人读。库存字段必须是无符号整数并加默认值否则新增图书时如果忘记填库存NULL 会让后面的“扣库存”操作直接出错。读者表reader只需抓住关键字段reader_id 自增主键、name、student_no学号可选唯一键、phone、max_borrow最大可借数量默认 5、fine_balance当前欠款金额、register_date。注意学号要做唯一约束因为同一个读者不能重复注册电话字段用 VARCHAR(11) 而不用 INT避免将来遇到区号或分机号时溢出也避免手机号前面的 0 被抹掉。借阅表borrow是整个系统的核心字段设计直接影响事务逻辑和逾期计算字段名类型约束/默认值说明borrow_idBIGINT UNSIGNEDAUTO_INCREMENT PRIMARY KEY借阅记录号book_idINT UNSIGNEDNOT NULL外键借哪本书reader_idINT UNSIGNEDNOT NULL外键谁借的borrow_dateDATETIMEDEFAULT CURRENT_TIMESTAMP借出时间due_dateDATETIMENOT NULL应还时间return_dateDATETIMEDEFAULT NULL实际归还时间NULL 表示未还statusTINYINTDEFAULT 00 借出中1 已归还2 逾期未还fineDECIMAL(10,2)DEFAULT 0.00本次借阅产生的罚款状态字段用 TINYINT 而不是 VARCHAR因为程序里判断整数比判断字符串更高效也便于扩展状态机。比状态更重要的是 return_date 用 NULL 表示“未归还”而不是用一个特殊日期如 2099-01-01 去占位——用 NULL 可以直接通过WHERE return_date IS NULL过滤在借图书语义清晰避免日期比较带来的边界错误。2.3 外键策略什么时候用 RESTRICT什么时候用 CASCADE图书分类表category与图书表之间存在外键约束删除策略的选择在实验报告里要能自圆其说。我的建议是分类表被删除时如果该分类下还有图书直接禁止删除即ON DELETE RESTRICT如果允许“删除分类时把该分类图书一并删除”等于让一次误操作毁掉整批数据这在真实系统里是不可接受的。读者表的删除同理——一个读者如果有未归还的图书绝不允许直接删除必须ON DELETE RESTRICT。借阅记录表与图书表的外键则用ON DELETE RESTRICT因为借阅记录是历史凭证图书被物理删除后历史记录的外键会悬空查询时拿到 NULL 关联统计数据就会失真。如果你确实想体现“删除图书时同步清理无意义历史记录”的设计可以用ON DELETE CASCADE但这在答辩时会被追问“数据追溯怎么做”我一般不建议在课程设计里主动引入级联删除。2.4 范式与反范式的取舍演示系统里要不要冗余字段第三范式要求非主键字段不传递依赖但在实际系统里完全遵守第三范式会让查询变得很啰嗦。比如查询“当前借出中的所有图书”你要 join 图书表拿书名也可以直接把书名冗余在借阅表里。我的结论是课程设计报告里坚持第三范式不要做冗余字段。原因有两层一是答辩老师会拿范式理论来核对你的表结构冗余字段容易被扣“设计不合规范”的分二是图书管理系统的数据量级根本到不了需要冗余优化的程度join 三张表毫秒级返回完全没有优化的必要。真正值得做的优化是索引。book 表的 title 字段在查询里会被模糊匹配加普通索引就够用borrow 表的 (status, due_date) 联合索引可以同时支撑“查出所有逾期图书”和“统计某状态下借阅量”两类高频查询。索引设计与范式检查这两部分内容写进实验报告属于明显的加分项。3. 把 DDL 和三类核心 SQL 一次写对建库、借还书事务与报告里必须有的连表查询3.1 一段能在 MySQL 8 上直接执行的建库建表脚本实验报告要求贴 SQL 脚本但很多人贴的脚本在自己的电脑上跑都报错大多是字符集、引擎和约束顺序的问题。下面这段 DDL 是我在课程设计里常用的基线版本以 MySQL 8 为目标环境建库和建表一起完成。-- 创建数据库字符集必须与表一致否则中文乱码 CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library; -- 图书分类表先建被依赖的表 CREATE TABLE category ( category_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(30) NOT NULL UNIQUE, sort_order INT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表外键依赖 category CREATE TABLE book ( book_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(17) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, publisher VARCHAR(50) DEFAULT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, category_id INT UNSIGNED NOT NULL, stock INT UNSIGNED NOT NULL DEFAULT 1, location VARCHAR(20) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(category_id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 读者表 CREATE TABLE reader ( reader_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, phone VARCHAR(11) DEFAULT NULL, max_borrow INT NOT NULL DEFAULT 5, fine_balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, register_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表外键依赖 book 和 reader CREATE TABLE borrow ( borrow_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, book_id INT UNSIGNED NOT NULL, reader_id INT UNSIGNED NOT NULL, borrow_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, due_date DATETIME NOT NULL, return_date DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出中 1已归还 2逾期, fine DECIMAL(10,2) NOT NULL DEFAULT 0.00, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id) ON DELETE RESTRICT, CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ON DELETE RESTRICT, INDEX idx_borrow_status_due (status, due_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表顺序是先建被依赖的表再建依赖表。category 不依赖任何表所以最先建book 依赖 category第二个建borrow 依赖 book 和 reader最后建。如果顺序颠倒MySQL 会直接报“无法添加外键约束”的错误这是新手最常见的翻车点。引擎统一用 InnoDB 而不是 MyISAM因为课程设计涉及事务演示MyISAM 不支持行级锁后面第 5 章的借书事务会专门展开。注意status字段用了注释COMMENT这是为了让数据库本身携带可读语义。做实验报告截图时用SHOW CREATE TABLE borrow\G展示表结构注释会一并显示比你在 Word 里手打表格更有说服力。借阅记录表的联合索引idx_borrow_status_due放在建表语句内部不需要单独执行 CREATE INDEX这体现了“索引随表定义一并设计”的习惯。3.2 图书增删改查参数化写法与四条标准语句实验报告里必须有一组完整的图书增删改查 SQL这也是热搜词“数据库增删改查”出现的场景。下面四条语句覆盖了业务侧最常见的操作注意都把参数用占位符表示方便你套到任何后端语言里。-- 新增图书先确认分类存在再插入图书 INSERT INTO book (isbn, title, author, publisher, price, category_id, stock, location) VALUES (978-7-302-12345-6, 数据库系统概论, 王珊, 清华大学出版社, 45.00, 2, 5, A-3-1); -- 查询图书按书名模糊搜索同时带出分类名称 SELECT b.book_id, b.isbn, b.title, b.author, b.publisher, b.price, c.category_name, b.stock, b.location FROM book b LEFT JOIN category c ON b.category_id c.category_id WHERE b.title LIKE CONCAT(%, 数据库, %) ORDER BY b.book_id DESC LIMIT 10 OFFSET 0; -- 修改图书只更新需要变更的字段主键作为过滤条件 UPDATE book SET price 49.00, stock stock 2 WHERE book_id 1; -- 删除图书先确保该图书没有未归还的借阅记录 DELETE FROM book WHERE book_id 3;INSERT 之前为什么要先确认分类存在因为 category_id 是外键插入一个不存在的分类会直接违反约束并报错。更优雅的做法是提前把分类表用 INSERT IGNORE 预置好种子数据再插入图书。UPDATE 语句里stock stock 2是在数据库层面做数值增减比先 SELECT 出来再在程序里 2 再 UPDATE 回去要安全后者在并发时会产生丢失更新。DELETE 语句单独执行时如果这本图书已经被借阅过会被外键约束拦截。所以在实验报告里要写明业务规则删除前先查询 borrow 表是否存在该书的未归还记录有则拒绝删除。这比直接放弃外键约束更符合真实系统设计答辩时能站住脚。3.3 借书事务用一张事务把库存扣减和借阅记录绑定借书是整个系统里最需要讲清楚的事务场景。如果不加事务扣减库存成功但插入借阅记录失败就会出现“库存少了但没人借到书”的脏数据。下面这段 SQL 是借书操作的事务版START TRANSACTION; -- 锁定图书行防止并发下超借 SELECT book_id, stock FROM book WHERE book_id 1 FOR UPDATE; -- 扣减库存 UPDATE book SET stock stock - 1 WHERE book_id 1 AND stock 0; -- 检查库存是否扣减成功 SELECT ROW_COUNT() AS affected_rows; -- 插入借阅记录应还日期为当前时间加30天 INSERT INTO borrow (book_id, reader_id, due_date, status) VALUES (1, 1001, DATE_ADD(NOW(), INTERVAL 30 DAY), 0); COMMIT;SELECT ... FOR UPDATE是 InnoDB 提供的行级锁它在事务内锁定 book_id1 这一行期间其他事务如果想修改这行数据会被阻塞直到当前事务提交。UPDATE ... WHERE book_id 1 AND stock 0是双重保障即使锁没有生效条件更新也不会把库存扣成负数。执行后立刻用ROW_COUNT()检查受影响行数如果为 0 说明库存已空这时应该ROLLBACK而不是继续插入借阅记录。整套逻辑可以用一个存储过程封装但在课程设计里直接在代码层用事务更便于断点演示。这里有一个很容易被忽略的细节DATE_ADD(NOW(), INTERVAL 30 DAY)中的 INTERVAL 参数决定了借阅期限。把 30 天作为一个可配置参数放进程序里比把具体日期硬编码在 SQL 中更合理。还书操作则是反向过程先 UPDATE 库存加一再 UPDATE 借阅记录的 return_date 为当前时间、status 改为 1同时按 DATEDIFF 计算逾期天数写入 fine 字段。逾期罚款的 SQL 可以这样写UPDATE borrow SET return_date NOW(), status 1, fine CASE WHEN DATEDIFF(NOW(), due_date) 0 THEN DATEDIFF(NOW(), due_date) * 0.10 ELSE 0.00 END WHERE borrow_id 1 AND return_date IS NULL;CASE 表达式把“是否逾期”的判断直接写进 UPDATE替代了先查出 should_fine 再程序判断的两步走。这里有两个边界要注意return_date IS NULL条件保证同一条记录不会被重复还书罚款单价 0.10 元/天要单独定义在系统常量中不要散落在 SQL 各处。3.4 报告里放这三条连表查询答辩能少被问三个问题实验报告的“系统功能实现”部分很多人的做法是截图几个表单页面这样很容易被质疑“没有实际数据库查询”。正确做法是放三条带业务含义的连表查询 SQL并配上结果截图。我推荐这三条-- 查询当前逾期未还的图书及读者信息 SELECT r.name, r.student_no, b.title, br.borrow_date, br.due_date, DATEDIFF(NOW(), 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.status 0 AND br.due_date NOW(); -- 统计借阅量最高的前5本书热门图书排行榜 SELECT b.title, COUNT(br.borrow_id) AS borrow_times FROM borrow br JOIN book b ON br.book_id b.book_id GROUP BY b.title ORDER BY borrow_times DESC LIMIT 5; -- 按分类统计馆藏数量 SELECT c.category_name, COUNT(b.book_id) AS book_count FROM category c LEFT JOIN book b ON c.category_id b.category_id GROUP BY c.category_id, c.category_name ORDER BY book_count DESC;第一条查询的筛选条件同时落在 status 和 due_date 上正好命中前面建的联合索引idx_borrow_status_due可以用EXPLAIN验证。第二条 GROUP BY 的字段不能只写 b.title因为 SELECT 里还有聚合函数MySQL 的 ONLY_FULL_GROUP_BY 模式会限制不明确的非聚合列。第三条用 LEFT JOIN 是因为要统计“还没有图书的分类”普通 INNER JOIN 会把空分类过滤掉。三条查询分别覆盖了“业务查询”“统计分析”“分组汇总”三种 SQL 能力在实验报告里各有各的用途即使老师现场追问每条语句的执行过程你也能从表关联关系和分组逻辑两个层面说清楚。4. 用 Flask SQLite 跑一个可演示的最小系统增删改查与分页检索落地4.1 为什么演示系统选 SQLite而实验报告写 MySQL实验报告里写 MySQL现场演示却用 SQLite这是我推荐的做法也是很多学校机房环境下的实际选择。SQLite 是单文件数据库不需要安装服务、不需要账号密码拷到哪都能跑适合答辩时启动演示MySQL 则用于报告里的 DDL 和事务语句因为语法更通用、也更符合企业实际。两者在 SQL 语法上的差异主要在三处SQLite 用?作占位符MySQL 用%sSQLite 没有FOR UPDATE行级锁SQLite 的日期函数是datetime(now, 30 days)MySQL 是DATE_ADD(NOW(), INTERVAL 30 DAY)。业务模型不变变的是底层方言这也是课程设计里“换一个数据库环境也能跑通”的加分论证。搭建演示系统我用 Flask因为它路由写起来最直接一个文件就能承载所有功能。项目的目录结构可以简化成两个文件init_db.py负责建库建表和写入种子数据app.py负责全部 HTTP 接口。4.2 初始化脚本建表、外键开关和种子数据# init_db.py import sqlite3 DB_PATH library.db def get_connection(): # 关键每次连接都要开启外键支持否则外键约束形同虚设 conn sqlite3.connect(DB_PATH) conn.execute(PRAGMA foreign_keys ON) return conn def init_db(): conn get_connection() cur conn.cursor() cur.executescript( CREATE TABLE IF NOT EXISTS category ( category_id INTEGER PRIMARY KEY AUTOINCREMENT, category_name TEXT NOT NULL UNIQUE, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS book ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL, author TEXT NOT NULL, publisher TEXT, price REAL NOT NULL DEFAULT 0, category_id INTEGER NOT NULL, stock INTEGER NOT NULL DEFAULT 1, location TEXT, FOREIGN KEY (category_id) REFERENCES category(category_id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS borrow ( borrow_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_id INTEGER NOT NULL, borrow_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, due_date TEXT NOT NULL, return_date TEXT, status INTEGER NOT NULL DEFAULT 0, fine REAL NOT NULL DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(book_id) ON DELETE RESTRICT, FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ON DELETE RESTRICT ); ) # 种子数据 cur.execute(INSERT OR IGNORE INTO category (category_id, category_name) VALUES (1, 数据库), (2, 编程语言), (3, 操作系统)) cur.execute(INSERT OR IGNORE INTO book (book_id, isbn, title, author, category_id, stock) VALUES (1, 978-7-302-12345-6, 数据库系统概论, 王珊, 1, 5)) conn.commit() conn.close() if __name__ __main__: init_db()SQLite 的PRAGMA foreign_keys ON必须针对每次连接单独执行这是最容易被忽略的细节。很多人在 SQLite 里建了外键却发现删除父表记录照样成功就是因为没有开启这一行。executescript可以一次性执行多段 DDL比逐条 execute 更省事。种子数据用INSERT OR IGNORE保证脚本重复运行时不会产生主键冲突。注意 SQLite 的 REAL 类型对应 MySQL 的 DECIMAL在演示环境里精度损失不明显但报告里要说明正式环境应使用 DECIMAL。4.3 图书列表接口分页、模糊搜索和参数化查询# app.py 关键代码片段 from flask import Flask, request, jsonify, render_template_string import sqlite3 app Flask(__name__) DB_PATH library.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 让查询结果支持按字段名访问 conn.execute(PRAGMA foreign_keys ON) return conn app.route(/books) def book_list(): # 分页参数页码从1开始每页默认10条 page int(request.args.get(page, 1)) page max(page, 1) page_size int(request.args.get(page_size, 10)) offset (page - 1) * page_size keyword request.args.get(keyword, ).strip() conn get_conn() cur conn.cursor() # 参数化查询用 ? 占位符避免拼接 SQL 导致注入 if keyword: cur.execute( SELECT b.book_id, b.title, b.author, b.stock, c.category_name FROM book b LEFT JOIN category c ON b.category_id c.category_id WHERE b.title LIKE ? ORDER BY b.book_id DESC LIMIT ? OFFSET ? , (f%{keyword}%, page_size, offset)) books cur.fetchall() cur.execute(SELECT COUNT(*) FROM book WHERE title LIKE ?, (f%{keyword}%,)) else: cur.execute( SELECT b.book_id, b.title, b.author, b.stock, c.category_name FROM book b LEFT JOIN category c ON b.category_id c.category_id ORDER BY b.book_id DESC LIMIT ? OFFSET ? , (page_size, offset)) books cur.fetchall() cur.execute(SELECT COUNT(*) FROM book) total cur.fetchone()[0] conn.close() # 实际演示时这里会渲染模板或返回 JSON返回 JSON 用于接口验证 return jsonify({ page: page, total: total, data: [dict(row) for row in books] })SQLite 的 LIMIT 和 OFFSET 参数不能像 MySQL 那样直接内插要用?传参。这里把 LIKE 通配符%放在 Python 侧拼接进参数而不是写死在 SQL 里是为了让同一个查询在不同关键词下复用。page max(page, 1)是对页码下界的防御用户手动传 page0 或 page-1 时仍能取到第一页。返回前把 Row 对象转成 dict是为了让 JSON 序列化不报错。逻辑上先查列表再查总数两次查询共用同一个 conn保证数据一致性。4.4 借书接口事务、库存检查与异常回滚app.route(/borrow, methods[POST]) def do_borrow(): book_id request.form.get(book_id, typeint) reader_id request.form.get(reader_id, typeint) if not book_id or not reader_id: return jsonify({success: False, message: 参数缺失}), 400 conn get_conn() cur conn.cursor() try: # 开启事务 cur.execute(BEGIN) # 先查库存再决定是否插入借阅记录 cur.execute(SELECT stock FROM book WHERE book_id ?, (book_id,)) row cur.fetchone() if row is None: conn.rollback() return jsonify({success: False, message: 图书不存在}), 404 if row[stock] 0: conn.rollback() return jsonify({success: False, message: 库存不足}), 400 # 扣减库存 cur.execute(UPDATE book SET stock stock - 1 WHERE book_id ?, (book_id,)) # 插入借阅记录借期30天 cur.execute( INSERT INTO borrow (book_id, reader_id, due_date, status) VALUES (?, ?, datetime(now, 30 days), 0) , (book_id, reader_id)) conn.commit() return jsonify({success: True, message: 借书成功}) except Exception: conn.rollback() return jsonify({success: False, message: 借书失败已回滚}), 500 finally: conn.close()这段两处值得在报告里写清楚一是先 SELECT 查库存再 UPDATE 扣减在 SQLite 单用户演示环境下够用但换成 MySQL 高并发环境必须加FOR UPDATE或在 UPDATE 条件里加stock 0否则两个请求同时读到 stock1 就会超借二是datetime(now, 30 days)是 SQLite 的日期计算语法与 MySQL 的写法不同报告中的 SQL 应保留 MySQL 版本以统一技术栈。异常捕获后统一 rollback保证半截操作不会留在数据库里。5. 避坑图书管理系统在 MySQL 和 SQLite 上最容易翻车的 5 个点5.1 MySQL 8 连接报错字符集与认证插件不匹配现象是图形化客户端连接本地 MySQL 8 提示Access denied for user或Public Key Retrieval is not allowed但命令行能正常登录。原因是 MySQL 8 默认的认证插件是caching_sha2_password老版本客户端或图形化工具不认识这个插件同时 MySQL 8 默认字符集排序规则是utf8mb4_0900_ai_ci如果建库时没显式指定通用排序规则后续在 Java 或 Python 连接时也可能报排序规则冲突。解决方法是建库时显式声明DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci并让连接串在请求 SSL 时关闭或选择允许不加密认证例如在 Python 连接串里配置ssl_disabledTrue。5.2 SQLite 外键约束不生效少了 PRAGMA现象是你在 SQLite 表里写了 FOREIGN KEY删除父表记录却没有任何拦截删除后子表还能查到悬挂数据。原因是 SQLite 默认关闭外键约束且这个开关是针对连接的、不是针对数据库的。解决方式就是在每次获取连接的代码里执行PRAGMA foreign_keys ON。光在建表脚本里写外键没用必须代码层面打开开关。课程设计报告里如果写了 SQLite 作为演示环境最好把这个细节写进“环境配置与注意事项”一节属于体现出真实经验的细节。5.3 借书事务形同虚设MyISAM 引擎不支持行级锁现象是并发测试下库存从 1 变成负数借阅记录还照常插入。原因是建表时用了默认的 MyISAM 引擎它不支持行级锁SELECT ... FOR UPDATE直接不生效两个事务可以同时读到旧库存值。解决方法是统一指定ENGINEInnoDB并在借书 SQL 里同时使用FOR UPDATE锁行和UPDATE ... WHERE stock 0条件更新两道保险缺一不可。在实验报告的“系统测试”部分可以写一个并发借阅的压测结果来佐证事务确实生效这比口头说明更有说服力。5.4 ISBN 与主键混乱自增主键被当成可展示编号现象是代码里到处用 isbn 字段当作记录标识去更新和删除连表查询时又把 isbn 当主键关联。原因是设计时过度依赖唯一键忽略了主键自增的稳定性。解决方法是坚持用自增主键 book_id 作为所有内部引用的依据isbn 只做唯一约束和展示当图书改版导致 isbn 变化时借阅记录仍然指向 book_id不会因为 isbn 变更而失去关联。课程设计里把这条规则写进“数据库设计原则”能有效阻止答辩老师追问“如果书再版了怎么办”。5.5 图形化工具连不上本机库端口、host 串号与版本错位现象是代码能连上数据库但 Navicat 或 DBeaver 连接时报 1045 或 2003 连接错误于是开始怀疑驱动、怀疑防火墙陷入玄学排错。大多数情况下是三个原因之一连接地址写成了 localhost 而 MySQL 配置的 bind-address 只允许特定 IP端口不是默认 3306 而漏填了自定义端口密码加密规则是新的 caching_sha2_password 而驱动还是老版本。解决方式是按顺序排查先SELECT port, hostname确认实际服务参数再在连接工具里把主机写成 127.0.0.1 而不是 localhost最后升级 JDBC 或 MySQL Connector 到 8.x。这三步能覆盖九成的连接失败场景。6. 实验报告撰写与答辩自测一份能被老师追问的数据面6.1 报告结构示例从目录到测试的完整段落一份合格的“数据库课程设计(图书管理系统)实验报告.doc”章节顺序我推荐按“需求分析 → 概念结构设计 → 逻辑结构设计 → 物理实现 → 系统功能测试 → 总结”来排。其中逻辑结构设计章节必须包含 E-R 图、表结构定义表和关系模式说明物理实现章节必须包含关键 SQL 脚本和运行结果截图。截图有个容易被看穿的坑只截表记录不截 SQL 语句。正确做法是把执行 SQL 的命令行窗口同步截进去让人看出这条结果是由哪条语句产出的。每个功能模块的截图后面用两三句话说明操作流程和输入输出不要堆大段文字。6.2 答辩验证清单交报告前花十分钟自测交报告前一晚我习惯做一次从零开始的冷启动测试删除数据库文件重新执行建库脚本启动演示系统完成“查书→借书→还书→查逾期”四个动作全程录屏。这个过程能在十分钟内暴露连接配置、依赖版本、路径写死三类隐藏问题。答辩现场有网络波动或数据库服务没启动的突发状况时你因为刚跑过冷启动处理起来不会慌。需要预先想清楚的三道必问题借书时库存怎么保证不会扣成负数删除一个还有借阅记录的读者会怎样为什么借阅表要用自增主键而不是直接用图书编号。把这三问的答案提前写在报告末尾的封底作为附注答辩时被问到了能直接脱稿讲。依赖版本和运行环境的说明要写清楚MySQL 客户端版本、Flask 版本、SQLite 版本、操作系统类型。这些内容看似琐碎但答辩老师最讨厌的就是“在我电脑上能跑”这句话。写清楚版本意味着你承认环境是可复现的将来任何人按同版本都能跑起来。这也是判断一份课程设计报告是抄的还是自己动手做的分水岭。我个人的习惯是每次交课设前都要把自己当成答辩老师去读一遍报告有没有哪张表只有字段名没有类型说明有没有哪条 SQL 自己都解释不了执行计划有没有哪个界面截图和表结构对不上。这三处只要有一处对不上报告就还是半成品。建议你也用同样的标准过一遍自己的报告改完再去提交。希望这篇笔记能帮到你也祝你这次课程设计不再是为了交差而做而是真正能拿出去讲的那种作品。本文还有配套的精品资源点击获取