ARTICLE DETAIL

资讯详情

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

Python+tkinter+MySQL图书管理系统:从课程设计到高并发实战

Python+tkinter+MySQL图书管理系统:从课程设计到高并发实战 简介这是一套面向计算机相关专业学生的图书管理系统课程设计资源采用Python结合tkinter图形界面与MySQL数据库实现适合正在做大作业、课程设计或需要项目实战练习的学习者参考。资源包共13个文件包含6个py源码文件、4个txt数据与配置文本、1个jpg界面背景图、1个md说明文档和1份pdf设计报告压缩包约3.04MB体量轻便。源码按功能拆分为借阅、归还、查询、添加、卡片管理等模块配合Gui.py统一组织界面结构清晰便于理解各业务逻辑的调用关系。设计报告与说明文档可辅助梳理系统架构与实现思路数据文本与配置文件则方便直接运行调试。目前已有108人学习下载项目难度适中适合作为课程设计参考或二次开发的基础模板。1. 从课程设计到能跑的系统PythontkinterMySQL 图书管理到底在做什么很多人对「图书管理系统」这四个字的第一反应是又是课程设计随便糊一个增删改查就完事。但真正动手做过的人知道一个能拿得出手的图书管理系统难点从来不在界面有多花哨而在于三件事能不能同时成立——借还书的库存状态不能算错、多人同时操作时数据不能打架、界面卡不卡。用 Python tkinter MySQL 这套组合恰好能把这三件事都覆盖到而且门槛低到一台普通笔记本就能跑起来。这套方案的本质是tkinter 负责桌面端的交互窗口MySQL 负责持久化存储和并发控制Python 在中间做业务逻辑的粘合层。它适合两类人——一类是正在做课程设计、需要一份结构完整且能讲清楚设计思路的同学另一类是刚学完 Python 基础语法想找一个真实项目把「界面 数据库 业务逻辑」串起来的入门者。下面我会按「先想清楚数据怎么存、再动手写界面、最后处理并发和踩坑」的顺序把整套东西拆开讲每一步都能直接抄。2. 数据库先立住图书管理系统的表结构和 MySQL 连接方案2.1 四张核心表怎么设计才不返工图书管理系统的数据模型看着简单但设计不好后期改表会非常痛苦。我一般会拆成四张表books图书信息、readers读者信息、borrow_records借阅记录、users管理员账号。关键点在于图书的可借数量不要单独存一个字段然后手动加减而是通过借阅记录实时算或者用一个冗余字段配合事务来维护。-- 图书表isbn 唯一total_copies 是总库存available 是可借数量 CREATE TABLE books ( book_id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), total_copies INT DEFAULT 1, available INT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表status 用枚举区分借出/归还/逾期 CREATE TABLE borrow_records ( record_id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, status ENUM(borrowed,returned,overdue) DEFAULT borrowed, FOREIGN KEY (book_id) REFERENCES books(book_id), FOREIGN KEY (reader_id) REFERENCES readers(reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个参数必须注意。第一存储引擎一定要用InnoDB因为只有它支持事务和行级锁后面处理并发借书时全靠它。第二字符集用utf8mb4而不是utf8否则书名里出现生僻字或者 emoji 会直接报错。available字段是冗余设计目的是避免每次查询可借数量都要去borrow_records里 count 一遍但代价是每次借还都必须在一个事务里同时更新books.available和插入borrow_records否则数据会对不上。2.2 Python 连接 MySQL 的三种方式和选型理由Python 连 MySQL 常见的有三条路mysql-connector-python官方驱动、PyMySQL纯 Python 实现、SQLAlchemyORM 框架。课程设计场景下我推荐PyMySQL原因是它不需要编译 C 扩展pip install pymysql就能用跨平台不会出幺蛾子。如果你想让代码看起来更「工程化」可以套一层SQLAlchemy但对课程设计来说容易把简单问题复杂化。import pymysql from pymysql.cursors import DictCursor def get_connection(): 每次操作获取新连接避免长连接超时问题 return pymysql.connect( hostlocalhost, port3306, userroot, passwordyour_password, databaselibrary_db, charsetutf8mb4, cursorclassDictCursor, # 返回字典而不是元组取值更方便 autocommitFalse # 手动控制事务 ) def borrow_book(book_id, reader_id): conn get_connection() try: with conn.cursor() as cursor: # 先锁定该图书行防止并发超借 cursor.execute( SELECT available FROM books WHERE book_id%s FOR UPDATE, (book_id,) ) row cursor.fetchone() if not row or row[available] 0: conn.rollback() return False, 库存不足 # 扣减库存 cursor.execute( UPDATE books SET availableavailable-1 WHERE book_id%s, (book_id,) ) # 插入借阅记录 cursor.execute( INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date, status) VALUES (%s, %s, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), borrowed), (book_id, reader_id) ) conn.commit() return True, 借阅成功 except Exception as e: conn.rollback() return False, f借阅失败: {e} finally: conn.close()这段代码里最关键的是SELECT ... FOR UPDATE。它的作用是给这一行数据加排他锁别的事务想改这行就得等。如果没有这一句两个人同时借同一本只剩一本的书两个事务都读到available1然后都执行扣减最后库存变成 -1这就是典型的并发超借。autocommitFalse配合手动commit/rollback是保证原子性的前提任何一步失败都要回滚不能只提交一半。注意FOR UPDATE必须在事务里才有意义。如果你用的是默认 autocommit 模式这条语句加锁后会立刻释放等于没加。3. tkinter 界面层从登录窗口到主界面的完整搭建3.1 窗口布局用 grid 还是 pack为什么我选 gridtkinter 的布局管理器有pack、grid、place三种。pack适合简单的上下排列place用绝对坐标适合做固定尺寸的启动画面但图书管理系统这种表单密集的界面grid是最优解。原因很直接grid用行列来定位改一个控件的位置不会影响其他控件而pack一旦嵌套层级多了调间距能调到怀疑人生。import tkinter as tk from tkinter import ttk, messagebox class LibraryApp: def __init__(self, root): self.root root self.root.title(图书管理系统) self.root.geometry(900x600) self.current_user None self.build_login_frame() def build_login_frame(self): 登录界面用 Frame 做容器方便整体切换 self.login_frame ttk.Frame(self.root, padding40) self.login_frame.place(relx0.5, rely0.5, anchorcenter) ttk.Label(self.login_frame, text用户名).grid(row0, column0, pady10, stickye) self.username_entry ttk.Entry(self.login_frame, width25) self.username_entry.grid(row0, column1, pady10) ttk.Label(self.login_frame, text密码).grid(row1, column0, pady10, stickye) self.password_entry ttk.Entry(self.login_frame, width25, show*) self.password_entry.grid(row1, column1, pady10) ttk.Button(self.login_frame, text登录, commandself.do_login)\ .grid(row2, column0, columnspan2, pady20, stickyew) def do_login(self): username self.username_entry.get().strip() password self.password_entry.get().strip() if not username or not password: messagebox.showwarning(提示, 用户名和密码不能为空) return # 这里调用数据库校验成功后销毁登录框、构建主界面 if self.verify_user(username, password): self.login_frame.destroy() self.build_main_frame() else: messagebox.showerror(错误, 用户名或密码错误)place(relx0.5, rely0.5, anchorcenter)这行的作用是把登录框整体居中relx/rely是相对坐标取值 0 到 1anchorcenter表示以控件中心点对齐到指定位置。用Frame做容器再整体销毁重建比在一个窗口里反复pack_forget要干净得多这是我在多个项目里验证过的做法。3.2 用 Treeview 做图书列表和借阅记录展示图书列表如果用Label一行行拼数据一多界面直接卡死。正确做法是用ttk.Treeview它内部是 C 实现的几千条数据滚动起来也不卡。def build_book_list(self, parent): 图书列表Treeview 滚动条 双击借阅 columns (book_id, isbn, title, author, available) self.tree ttk.Treeview(parent, columnscolumns, showheadings, height15) # 定义列头和列宽 headers {book_id: 编号, isbn: ISBN, title: 书名, author: 作者, available: 可借数量} for col in columns: self.tree.heading(col, textheaders[col]) self.tree.column(col, width120, anchorcenter) # 滚动条必须和 Treeview 绑定否则数据多了没法看 scrollbar ttk.Scrollbar(parent, orientvertical, commandself.tree.yview) self.tree.configure(yscrollcommandscrollbar.set) self.tree.pack(sideleft, fillboth, expandTrue) scrollbar.pack(sideright, filly) # 双击某一行触发借阅 self.tree.bind(Double-1, self.on_book_double_click) def load_books(self, keyword): 从数据库加载图书支持模糊搜索 for item in self.tree.get_children(): self.tree.delete(item) # 先清空旧数据 conn get_connection() try: with conn.cursor() as cursor: if keyword: sql SELECT * FROM books WHERE title LIKE %s OR author LIKE %s cursor.execute(sql, (f%{keyword}%, f%{keyword}%)) else: cursor.execute(SELECT * FROM books) for row in cursor.fetchall(): self.tree.insert(, end, values( row[book_id], row[isbn], row[title], row[author], row[available] )) finally: conn.close()showheadings表示隐藏默认的第一列tkinter 默认会加一个空的树形列height15控制显示行数。load_books里每次刷新前先delete所有子项这是必须的否则搜索一次就追加一批列表会越来越长。模糊搜索用LIKE %keyword%参数化查询里的%s由 PyMySQL 负责转义不要自己用字符串拼接否则会有 SQL 注入风险。提示Treeview 的values里所有值都会被转成字符串显示如果你要按数字排序需要在heading里指定command做自定义排序默认的字符串排序会把 10 排在 2 前面。4. 业务逻辑层借还书、逾期计算和搜索分页怎么落地4.1 借书还书的完整事务流程借书和还书是这套系统里唯二需要严格事务保证的操作。借书要同时做三件事检查库存、扣减库存、插入借阅记录。还书要做两件事更新借阅记录状态、恢复库存。任何一步失败都必须整体回滚。def return_book(self, record_id): 还书更新记录状态 恢复库存必须在一个事务里 conn get_connection() try: with conn.cursor() as cursor: # 先查出这条记录对应的 book_id并锁定 cursor.execute( SELECT book_id, status FROM borrow_records WHERE record_id%s FOR UPDATE, (record_id,) ) record cursor.fetchone() if not record: conn.rollback() return False, 借阅记录不存在 if record[status] returned: conn.rollback() return False, 该书已归还请勿重复操作 # 更新记录 cursor.execute( UPDATE borrow_records SET statusreturned, return_dateCURDATE() WHERE record_id%s, (record_id,) ) # 恢复库存 cursor.execute( UPDATE books SET availableavailable1 WHERE book_id%s, (record[book_id],) ) conn.commit() return True, 归还成功 except Exception as e: conn.rollback() return False, f归还失败: {e} finally: conn.close()这里先SELECT ... FOR UPDATE锁定借阅记录行是为了防止同一条记录被两个操作同时归还。如果不用锁两个请求都读到statusborrowed都执行更新和库存恢复库存就会多加一次。return_dateCURDATE()用数据库的当前日期而不是 Python 的datetime.now()是为了避免客户端时区不一致导致日期偏差。4.2 逾期天数计算和自动标记逾期计算有两种做法一种是在查询时实时算另一种是定时任务批量更新状态。课程设计场景下我推荐实时算因为不需要额外部署定时任务逻辑也更简单。def get_overdue_records(self): 查询所有逾期未还的记录实时计算逾期天数 conn get_connection() try: with conn.cursor() as cursor: cursor.execute( SELECT r.record_id, b.title, rd.name AS reader_name, r.due_date, DATEDIFF(CURDATE(), r.due_date) AS overdue_days FROM borrow_records r JOIN books b ON r.book_id b.book_id JOIN readers rd ON r.reader_id rd.reader_id WHERE r.status borrowed AND r.due_date CURDATE() ORDER BY overdue_days DESC ) return cursor.fetchall() finally: conn.close()DATEDIFF(CURDATE(), r.due_date)返回的是两个日期之间的天数差正数表示已经逾期。这个查询只筛statusborrowed且due_date小于今天的记录已经归还的不管有没有逾期过都不再显示。如果你想让逾期记录在界面上标红可以在 Treeview 里用tag_configure给特定行设置前景色。# 在 Treeview 里给逾期行标红 self.tree.tag_configure(overdue, foregroundred) # 插入数据时判断 tag overdue if row[overdue_days] 0 else self.tree.insert(, end, values(...), tags(tag,))4.3 搜索分页LIMIT OFFSET 的正确用法和性能边界图书数量少的时候全量加载没问题但一旦超过几千条一次性查出来再塞进 Treeview 会明显卡顿。分页的标准做法是LIMIT加OFFSET。def search_books_paged(self, keyword, page1, page_size20): 分页搜索page 从 1 开始 offset (page - 1) * page_size conn get_connection() try: with conn.cursor() as cursor: # 先查总数用于计算总页数 cursor.execute( SELECT COUNT(*) AS total FROM books WHERE title LIKE %s, (f%{keyword}%,) ) total cursor.fetchone()[total] cursor.execute( SELECT * FROM books WHERE title LIKE %s ORDER BY book_id LIMIT %s OFFSET %s, (f%{keyword}%, page_size, offset) ) rows cursor.fetchall() return rows, total finally: conn.close()LIMIT %s OFFSET %s里的参数由 PyMySQL 转成整数不会引发注入。但要注意OFFSET越大查询越慢因为 MySQL 需要先扫描并丢弃前 offset 行。数据量到十万级以上时应该改用「基于游标的分页」——记住上一页最后一条的book_id下一页用WHERE book_id last_id LIMIT 20。课程设计的数据量通常到不了这个级别但面试时被问到分页优化这就是标准答案。5. 避坑与排查课程设计里最容易翻车的五个地方5.1 中文乱码从建库到连接的全链路排查现象界面上显示的书名全是问号或者方块。原因乱码可能出现在三个环节——数据库建库时没指定字符集、表字段字符集不对、Python 连接时没设charset。任何一环掉链子都会乱码。解决建库时用CREATE DATABASE library_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;建表时也显式指定DEFAULT CHARSETutf8mb4PyMySQL 连接参数里加charsetutf8mb4。三处都对齐之后中文就不会出问题。如果已经建好了表用ALTER TABLE books CONVERT TO CHARACTER SET utf8mb4;可以补救。5.2 界面卡死数据库操作放在主线程的后果现象点击「查询」按钮后窗口无响应转圈几秒才恢复。原因tkinter 的所有界面更新都在主线程里跑如果你在按钮回调里直接执行耗时的数据库查询主线程被阻塞界面自然卡死。解决简单查询几百毫秒内可以接受但批量导入或者复杂统计必须放到子线程。用threading.Thread执行数据库操作查完后用root.after(0, callback)把结果传回主线程更新界面。切记不要在子线程里直接操作 tkinter 控件否则会报RuntimeError: main thread is not in main loop。import threading def on_search_click(self): keyword self.search_entry.get() def worker(): rows, total self.search_books_paged(keyword) # 用 after 把更新界面的操作调回主线程 self.root.after(0, lambda: self.update_tree(rows)) threading.Thread(targetworker, daemonTrue).start()5.3 连接超时MySQL 8 小时空闲断连现象程序放了一晚上第二天第一次操作报Lost connection to MySQL server during query。原因MySQL 默认的wait_timeout是 28800 秒8 小时空闲超过这个时间连接会被服务端断开但客户端不知道下次用这个连接就报错。解决最省事的做法是每次操作都新建连接、用完就关前面代码就是这么写的。如果一定要用长连接池需要在连接参数里加pool_recycle3600SQLAlchemy或者自己写心跳检测。课程设计场景下每次新建连接完全够用不要为了「性能」提前引入连接池那是给自己找麻烦。5.4 外键约束导致删书失败现象删除一本图书时报Cannot delete or update a parent row: a foreign key constraint fails。原因这本书在borrow_records里有借阅记录外键约束不允许删除被引用的父行。解决两种思路。一是软删除给books加一个is_deleted字段删除时只标记不真删。二是先检查有没有未归还的记录有就拒绝删除并提示用户。我一般用第一种因为借阅历史是需要保留的数据真删了以后统计就没法做了。5.5 tkinter 打包成 exe 后找不到 MySQL 驱动现象用 PyInstaller 打包后在自己电脑上能跑换台电脑就报ModuleNotFoundError: No module named pymysql。原因PyInstaller 有时候检测不到动态导入的模块pymysql就是典型。解决打包时加--hidden-importpymysql完整命令是pyinstaller -F -w --hidden-importpymysql main.py。-w表示不显示控制台窗口-F表示打包成单个 exe。如果还缺别的模块同样用--hidden-import补上。另外目标机器上不需要装 MySQL但需要能网络访问到数据库服务器。6. 让课程设计多拿十分的两个进阶技巧第一个技巧是给系统加一个「数据导出」功能。课程设计答辩时老师经常会问「你这个数据能不能导出来」如果你能当场点一个按钮把借阅记录导成 Excel印象分会高很多。用openpyxl库十行代码就能搞定from openpyxl import Workbook def export_borrow_records(self, records, filepathborrow_records.xlsx): wb Workbook() ws wb.active ws.title 借阅记录 ws.append([记录编号, 书名, 读者, 借出日期, 应还日期, 状态]) for r in records: ws.append([r[record_id], r[title], r[reader_name], str(r[borrow_date]), str(r[due_date]), r[status]]) wb.save(filepath) messagebox.showinfo(导出成功, f已导出到 {filepath})ws.append接收一个列表按行写入。日期字段要先用str()转成字符串否则 openpyxl 会尝试写入 datetime 对象虽然也能存但格式不好控制。导出路径建议用filedialog.asksaveasfilename让用户自己选不要硬编码到 C 盘根目录。第二个技巧是给数据库加索引。课程设计的数据量小索引的收益看不出来但答辩时你能说清楚「我在books.title和borrow_records.reader_id上建了索引因为这两个字段是查询频率最高的」这就是加分项。建索引的语句很简单CREATE INDEX idx_books_title ON books(title); CREATE INDEX idx_borrow_reader ON borrow_records(reader_id); CREATE INDEX idx_borrow_status ON borrow_records(status);但索引不是越多越好每个索引都会拖慢插入和更新的速度。原则是只在 WHERE 子句和 JOIN 条件里频繁出现的列上建索引区分度低的列比如性别只有男女两种值建了也没用。最后说一个我自己的习惯每次改完数据库相关的代码先不急着开界面写一个小的测试脚本直接调业务函数确认数据对了再调界面。界面层的问题和数据库层的问题混在一起排查是最浪费时间的。这个习惯帮我省下了大量「到底是 SQL 写错了还是按钮没绑对」的纠结时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表