☰
Python+MySQL+Tkinter图书馆管理系统源码实战:从跑通到答辩
2026/10/22 15:52:56 网站建设 项目流程

简介:这是一套基于Python与MySQL的图形化图书馆管理系统完整源码,面向Python初学者、数据库开发者以及需要完成课设或毕设的高校学生。系统涵盖图书管理、读者管理、借阅归还、权限控制与统计报表等核心模块,后端使用Python处理业务逻辑,通过pymysql等库连接MySQL数据库,前端采用HTML、CSS、JavaScript配合Bootstrap、Layui等框架搭建直观交互界面,并包含数据库表设计、查询与更新逻辑、异常捕获与日志记录等关键实现。压缩包共222个文件,以13个Python源码、26个HTML页面、37个JS脚本、75个GIF演示动图为主,另有CSS样式、字体图标、SQLite数据库文件及配置文件,整体大小9.53MB,目录结构清晰,便于直接运行和二次开发。目前已有3427人学习下载,对于希望快速掌握GUI程序设计与MySQL操作、理解图书馆业务流程的开发者而言,是一份实用且完整的参考资料,也可直接用于相关课程项目或毕业设计。

1. Python+MySQL 图形化界面图书馆管理系统源码:拿来跑通是入门,改到能答辩才是目的

Python+MySQL 图形化界面图书馆管理系统源码,听名字像是一个解压就能跑的打包项目,实际拿到的包却多数要你先折腾数据库。更常见的翻车场景是:界面能开,但登录按钮一按就报连接失败;或者图书列表是空的,因为你根本没导入建库脚本。别再盯着代码找 bug 了,先把环境跑通才是正路。这类源码包的完整形态,是用 Tkinter 做桌面窗口、用 MySQL 存图书、读者、借阅数据,把管书、借书、还书、查询串成一个闭环。

它的价值不在「打开就能用」,而在于你补全数据库配置后,能看清一套桌面图书管理软件怎么从窗口布局到 SQL 查询首尾相连。最适合两类人:一类是要交数据库课程设计或 Python 课程作业的学生,一类是想快速理解「GUI + 数据库」配套逻辑的开发者。与其急着点运行,不如先把选型讲清楚,因为后面你踩的坑,多半出在这两层。

2. 技术选型与架构:为什么是 Tkinter + MySQL,而不是 PyQt + SQLite

很多网上流传的图书管理源码,最初只是命令行程序,后面被人套了一层薄薄的 Tkinter 界面就拿去交差。这不是贬义,而是说明 Tkinter 和 MySQL 的组合有一个很现实的好处:两者都在教学大纲里,答辩时你既能讲「界面事件怎么绑定」,也能讲「表结构怎么设计、事务怎么提交」,知识点覆盖面比纯命令行项目好太多。换成 PyQt + SQLite,界面和数据库都能用,但老师很可能问一句「为什么要用 PyQt,它比 Tkinter 好在哪里」,这个问题如果你没真正上手过 PyQt,很容易答成背书。

Tkinter 的选择逻辑里,还有一个很现实的因素叫「依赖面」:它是 Python 自带的标准库,不需要额外下载几十 MB 的 Qt 运行库;在机房或者新电脑上部署时,只要装好 Python 和 pymysql,多半就能直接跑。PyQt5 虽然控件更美观,但打包出来的 exe 体积从几十 MB 起步,还涉及 Qt 插件路径这种莫名的坑。对一个小型图书管理系统来说,Tkinter 的 ttk 控件完全够用,代价只是界面观感没那么现代。

搞懂这两点,后面对源码的改造才有底。下面我会把界面库的取舍边界、本机环境怎么准备、源码包怎么组织逐层拆开。

2.1 图形界面库怎么选:Tkinter 的取舍与边界

Tkinter 能撑起图书管理系统的核心交互,靠的是三个控件。Treeview 负责数据表格,多列展示书名、作者、库存、状态,配合show="headings"还能隐藏首列那个默认空列;Entry 和 Combobox 负责表单输入,前者用于书名、作者这种自由文本,后者用于选择图书分类这种枚举值;messagebox 负责操作结果的弹窗提示,比如「借书成功」或者「库存不足」。这些控件组合起来,已经能覆盖图书管理九成以上的界面需求。

但 Tkinter 的边界也很清楚:它不适合做复杂的数据可视化。如果源码里打算展示「每月借阅量」折线图,Tkinter 自带的 Canvas 没有现成图表控件,常见做法是引入 matplotlib 的FigureCanvasTkAgg,把 matplotlib 的 Figure 嵌入 Tk 窗口。这里的坑在于 matplotlib 的事件循环要和 Tk 主循环配合,否则窗口刷新时会闪烁甚至卡住。另一个边界是自定义控件的成本:想做网页后台那种侧边栏、卡片、圆角按钮,Tkinter 要写大量样式代码,效果仍然一般。我的建议是:如果源码界面是「窗口 + 左侧按钮 + 右侧表格」这种传统布局,Tkinter 够用;真想做成产品级界面,才需要换 PyQt 重写。

对比点TkinterPyQt5
安装成本Python 自带,零额外安装需要 pip 安装 PyQt5,依赖较多
界面观感保守但可用现代化,样式表成熟
打包体积较小,一般 30MB 内通常 50MB 起步
学习曲线低,事件绑定直观中等,信号与槽机制需要适应
适用场景课程设计、内部工具产品化、商用桌面应用

选 Tkinter 的另一个理由是代码可读性。同一段借书逻辑,Tkinter 的事件函数就是一个普通def do_borrow():,直接绑在 Button 的 command 上;PyQt5 里如果用信号与槽,代码分层更多,对刚接触桌面开发的人来说理解成本更高。对于拿到源码包要快速上手的人,Tkinter 是阻力最小的路径。

2.2 环境准备:Python 版本、MySQL 版本与依赖安装顺序

我建议的环境组合是 Windows 10/11 + Python 3.10(64 位)+ MySQL 8.0。Python 版本不用追最新,部分第三方库对 3.13 的支持可能滞后;MySQL 5.7 也可以用,但 8.0 的默认认证插件caching_sha2_password会让老版本 pymysql 直接连不上。安装顺序上,先装 MySQL 服务,再装 pip 依赖,最后导入建库脚本。

# 1. 检查 Python 版本,64 位优先 python --version # 2. 安装连接 MySQL 的驱动,pymysql 是源码包里最常见的 pip install pymysql # 3. 连接 MySQL 8 时,默认加密插件需要这个依赖,否则可能报 not available pip install cryptography

装完依赖后,先不要直接跑源码。可以用下面这段最小连接脚本确认环境真的通。注意把 password 改成你自己的 MySQL root 密码,database 先填mysql,因为图书库还没有创建。

import pymysql conn = pymysql.connect( host="127.0.0.1", # 本机用 127.0.0.1,不要写 localhost,避免 IPv6 解析慢 port=3306, # MySQL 默认端口,安装时改过就填改后的 user="root", password="你的密码", database="mysql", # 系统库,测试连接用 charset="utf8mb4", ) cur = conn.cursor() cur.execute("SELECT VERSION()") print(cur.fetchone()) conn.close()

这段脚本如果能在终端打印出类似('8.0.36',)的结果,说明 Python 到 MySQL 的链路已经通了。三个参数最容易错:password 必须和安装 MySQL 时设置的一致;port 如果安装时改成 3307 而这里仍写 3306 会直接超时;database 如果填不存在的库名会报Unknown database。之后源码包里的配置也要和这里的参数保持一致。

还有一类情况是电脑上装了 MySQL 但忘了密码。常见做法是先用管理员权限停掉 MySQL 服务,在my.ini的[mysqld]段加一行skip-grant-tables,重启后用空密码登录并改密码;改完一定要把这行删掉再重启。这个操作不难,但容易忘记最关键的「改完密码后删掉跳过授权表的配置」,否则数据库等于裸奔。如果你只是本地开发,且源码包里自带建库脚本,也可以创建一个专用账号,比如lib_user,只给这个账号library库的权限,比一直用 root 更干净。

2.3 源码包的项目结构:认清每个目录再动手

完整跑通之前,先把结构弄清楚。一个典型的 Python+MySQL 图书馆管理系统源码包,通常包含以下几类文件:

文件/目录作用大概率需要改
main.py程序入口,创建主窗口并启动界面否
db_tool.py数据库连接与查询封装是(数据库密码)
ui_admin.py主管理界面,包含表格和按钮按需
ui_login.py管理员登录窗口按需
sql/library.sql建库、建表、初始化的 SQL 脚本否
config.ini数据库 host、port、user、password 配置是
requirements.txt依赖清单否

拿到包后我的建议是不急着读代码,而是按顺序做三件事:第一,用 MySQL 客户端执行sql/library.sql,把数据库和表建出来;第二,打开config.ini或db_tool.py,把密码改成自己的;第三,运行main.py,如果窗口弹出来并能查询到数据,说明已经跑通了。接下来再面对代码,你会发现自己带着「已经知道结果是什么」的状态去读源码,比从头读快得多。

这里有一个容易忽略的小问题:SQL 脚本的导入。如果源码包里的 sql 文件是 UTF-8 编码,而 MySQL 命令行默认字符集不是 UTF-8,导入时中文注释会变乱码。常见做法是用 source 命令导入前先执行SET NAMES utf8mb4;,或者在 Workbench 里打开文件并设置编码后再 Run SQL Script。字符集问题我会在第四章专门讲,因为这是全项目里最玄学也最常出问题的点。

3. 数据库设计与初始化:从建库脚本到图书借阅核心流程

选型和环境搞定后,接下来就是库里那张表长什么样的问题,这也是源码包能不能跑起来的根本。图书管理业务,本质是「图书表 + 读者表 + 借阅记录表」三张表,外加一些为了查询方便而做的设计。如果表结构设计得乱,后面界面再花哨也白搭。很多源码包跑不通,不是因为 Python 代码有错,而是建库脚本没执行,或者执行了但表名和代码里对不上。

这一章我按「建表 → 连接封装 → 借还书流程」的顺序走,每一步都给出能直接抄的代码。你在源码里看到的内容可能有细微差别,但核心结构和业务逻辑八九不离十。

3.1 建库建表:图书表、读者表、借阅表怎么设计才不被老师挑毛病

先看一套最少但完整的建库建表脚本:

CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library; CREATE TABLE book ( bid INT AUTO_INCREMENT PRIMARY KEY COMMENT '图书编号', bname VARCHAR(100) NOT NULL COMMENT '书名', author VARCHAR(50) DEFAULT NULL COMMENT '作者', publisher VARCHAR(80) DEFAULT NULL COMMENT '出版社', price DECIMAL(6,2) DEFAULT 0.00 COMMENT '价格', stock INT NOT NULL DEFAULT 0 COMMENT '当前可借库存', total INT NOT NULL DEFAULT 0 COMMENT '总册数', category VARCHAR(30) DEFAULT NULL COMMENT '分类' ) COMMENT='图书表'; CREATE TABLE reader ( rid INT AUTO_INCREMENT PRIMARY KEY COMMENT '读者编号', rname VARCHAR(50) NOT NULL COMMENT '姓名', rclass VARCHAR(50) DEFAULT NULL COMMENT '班级/部门', rphone VARCHAR(20) DEFAULT NULL COMMENT '联系电话' ) COMMENT='读者表'; CREATE TABLE borrow_record ( record_id INT AUTO_INCREMENT PRIMARY KEY, bid INT NOT NULL, rid INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0在借 1已还 2逾期', FOREIGN KEY (bid) REFERENCES book(bid), FOREIGN KEY (rid) REFERENCES reader(rid), INDEX idx_borrow_status (status) ) COMMENT='借阅记录表';

这里的几个设计点,写代码的人不一定会在源码注释里说明,但你答辩时一定要能讲。第一,bid用自增主键,图书编号不要用「B001」这种字符串,否则插入时还得自己生成编号,自增省掉这一步。第二,stock和total要分开:同一本书可能有多个副本,total记录这本图书总共进过多少册,stock记录当前还剩多少可借;还书时只做stock+1,total不动,这样既能查可借量,也能查馆藏量。第三,借阅记录表里status用 TINYINT 而不是 VARCHAR 存文字,省空间、索引友好,显示的时候再在 Python 里映射成「在借 / 已还 / 逾期」。

status字段还有一个容易踩坑的设计争议:逾期状态可以不在写入时算,而是每次查询时用due_date < CURDATE() AND return_date IS NULL动态判断。这种方案省一个字段,但如果补录历史还书日期,动态判定会随日期变化。既然借阅表里已经有return_date,我建议保留这个冗余字段,数据写入时就把状态定好,查询和统计都更直接。

3.2 连接数据库:把 pymysql 封装成公共模块,避免到处重复

源码包里如果每个界面文件都自己写一遍pymysql.connect,改密码的时候你会想摔键盘。我一般会单独做一个db_tool.py,把连接和通用查询封装好,所有界面文件统一从它这里取连接:

# db_tool.py:全项目统一从这里取连接 import pymysql DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "123456", "database": "library", "charset": "utf8mb4", } def get_conn(): conn = pymysql.connect(**DB_CONFIG) # 让查询结果支持按键名访问,例如 row["bname"],比下标清晰 return conn def execute_query(sql, params=None): conn = get_conn() try: with conn.cursor(pymysql.cursors.DictCursor) as cursor: cursor.execute(sql, params) return cursor.fetchall() finally: conn.close() def execute_update(sql, params=None): conn = get_conn() try: with conn.cursor() as cursor: cursor.execute(sql, params) conn.commit() return cursor.rowcount except Exception: conn.rollback() raise finally: conn.close()

这里有两个设计决策值得注意。为什么每次调用都新建连接,而不是全局维护一个连接对象?小项目并发量低,短连接足够;全局连接对象一旦断掉,重连逻辑写起来反而复杂。为什么不直接用默认游标,而是用 DictCursor?因为默认游标返回的是元组,代码里写row[1]根本看不出取的是书名还是作者,用字典游标后写row["bname"]可读性好得多,对新手也友好。

在execute_update里,conn.commit()放在try中,任何一步异常都会走except触发rollback(),然后重新抛出异常。调用方可以用rowcount判断影响行数,比如还书时如果没匹配到在借记录,rowcount为 0,界面就能给出「没有找到在借记录」的提示。所有 SQL 参数都通过params传给cursor.execute,禁止用字符串拼接,这段代码在答辩时也能体现你有安全意识。

3.3 核心流程:借书、还书、查询的完整链路

借书是整个系统里最容易出错的一步,因为它涉及「查库存 → 插入借阅记录 → 扣减库存」三个动作。如果只做插入不做扣减,多借几本书后库存就是负数;如果只做扣减不校验,库存为 0 时还能借出去,逻辑就崩了。下面是借书的完整函数:

def borrow_book(rid, bid, days=30): sql_check_stock = "SELECT stock FROM book WHERE bid=%s FOR UPDATE" sql_insert = """ INSERT INTO borrow_record(bid, rid, borrow_date, due_date, status) VALUES(%s, %s, CURDATE(), DATE_ADD(CURDATE(), INTERVAL %s DAY), 0) """ sql_update_stock = "UPDATE book SET stock=stock-1 WHERE bid=%s" conn = get_conn() try: with conn.cursor(pymysql.cursors.DictCursor) as cursor: cursor.execute(sql_check_stock, (bid,)) row = cursor.fetchone() if not row or row["stock"] <= 0: raise ValueError("库存不足") cursor.execute(sql_insert, (rid, bid, days)) cursor.execute(sql_update_stock, (bid,)) conn.commit() return True except Exception: conn.rollback() raise finally: conn.close()

FOR UPDATE是这个函数里的关键点:执行SELECT stock FROM book WHERE bid=%s FOR UPDATE时,MySQL 会锁住这一行,直到事务提交或回滚。这样两个用户同时借同一本最后一本书时,第二个请求会等第一个请求结束,然后发现库存已经变成 0,而不是两个人同时读到 1。这在单机课程设计里可能看不出差别,但属于「写对了没人夸、写错了答辩被问住」的知识点。

还书函数的逻辑要更简单,但要保证「更新记录 + 恢复库存」也在同一个事务里:

def return_book(record_id): conn = get_conn() try: with conn.cursor(pymysql.cursors.DictCursor) as cursor: cursor.execute( "SELECT bid FROM borrow_record WHERE record_id=%s AND status=0", (record_id,), ) row = cursor.fetchone() if not row: raise ValueError("没有找到在借记录") cursor.execute( "UPDATE borrow_record SET return_date=CURDATE(), status=1 WHERE record_id=%s", (record_id,), ) cursor.execute( "UPDATE book SET stock=stock+1 WHERE bid=%s", (row["bid"],), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()

查询借阅记录时,界面通常希望直接显示读者姓名和书名,而不是两个孤零零的编号。这时要用 JOIN 把三张表串起来:

SELECT br.record_id, r.rname, b.bname, br.borrow_date, br.due_date FROM borrow_record br JOIN reader r ON br.rid = r.rid JOIN book b ON br.bid = b.bid WHERE br.status = 0 ORDER BY br.borrow_date DESC;

这段 SQL 查询出来的结果,每一行都能直接丢给 Treeview 做展示。理解了这三个核心链路,源码包里界面按钮绑定的事件,本质就是对execute_query和execute_update的再包装。

4. 实战避坑:连不上 MySQL、中文乱码、窗口卡死、打包失败的现场

源码跑不起来,大多不是逻辑问题,而是环境问题。这一章列了我在实际调试中遇到最多的五类情况,每一条都按「现象 → 原因 → 解决」来写。你遇到其中某一条时不用从头读,直接定位到对应小节就行。

4.1 MySQL 8 连不上:认证插件报错,先别急着改代码

现象:运行最小连接脚本或源码时抛Authentication plugin 'caching_sha2_password' cannot be loaded,或者是RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password。

原因:MySQL 8.0 默认认证插件是caching_sha2_password,pymysql 在这种认证方式下传输密码需要 cryptography 库支持。如果你的 pymysql 版本较老,或者环境里没装 cryptography,连接时就会直接报错。

解决:先执行pip install cryptography再试一次。如果还不行,就不要动 root 账号的认证插件,而是新建一个专用账号,用兼容模式认证:

CREATE USER 'lib_user'@'localhost' IDENTIFIED WITH mysql_native_password BY '这里写密码'; GRANT ALL PRIVILEGES ON library.* TO 'lib_user'@'localhost'; FLUSH PRIVILEGES;

然后把源码里DB_CONFIG的 user 改成lib_user,password 改成上面设置的密码。这里不推荐把 root 从 caching_sha2 改成 mysql_native_password,因为 root 权限太大,为了一个课程设计去削弱整个数据库的默认安全策略,属于过度让步。专用账号还能顺便回答老师「你考虑了权限分离吗」这种问题。

4.2 中文乱码:四处字符集,少改一处就罢工

现象:导入 sql 文件后,表里中文全变成???;或者 Tkinter 界面显示正常,但用 MySQL 命令行一查全是乱码;再或者插入数据时报Incorrect string value: '\xE4\xB8\xAD...' for column。

原因:字符集链路很长,MySQL 服务端、数据库、连接、SQL 文件本身的编码必须一致。Windows 下常见的坑是:SQL 文件用记事本编辑后保存成了 ANSI 编码,MySQL 客户端却按 UTF-8 解析;或者连接参数写charset="utf8",而库表是utf8mb4。

解决:一个不漏地把以下几处全部统一成 utf8mb4。建库语句里带DEFAULT CHARACTER SET utf8mb4;连接参数写charset="utf8mb4";命令行导入前先执行SET NAMES utf8mb4;;SQL 文件用 VSCode 或记事本另存为「UTF-8 without BOM」。命令行导入的完整姿势:

mysql -u root -p library SET NAMES utf8mb4; source D:/library.sql;

顺手解释一个选择:MySQL 里utf8不是完全版,最多支持 3 字节字符,存 emoji 或生僻字会失败;utf8mb4才是完整版。不管源码文件里写的是 utf8 还是 gbk,你接手后都统一改成 utf8mb4 最省心。

4.3 界面卡死:长查询堵住了 Tkinter 主循环

现象:点击「查询全部借阅记录」后整个窗口卡住,Windows 标题栏出现「未响应」;数据量到几万条时几乎必现。

原因:Tkinter 是单线程事件循环,事件函数里执行的数据库查询是同步阻塞的。数据库没有返回结果之前,界面既不能重绘,也不能处理点击,表现出来就是卡死。

解决:把查询放到子线程里执行,拿到结果后再跳回主线程刷新界面。Tkinter 不是线程安全的,子线程里直接操作控件会有偶发崩溃,所以要借助after方法回调。

import threading def on_query(self): # 启动子线程,daemon=True 保证程序退出时子线程不会阻塞 threading.Thread(target=self._load_data, daemon=True).start() def _load_data(self): # 子线程里执行 SQL,不阻塞界面 rows = execute_query("SELECT * FROM borrow_record") # 回到主线程刷新表格,0 表示主循环空闲就执行 self.after(0, self._render, rows) def _render(self, rows): self.tree.delete(*self.tree.get_children()) for row in rows: self.tree.insert("", "end", values=row)

代码里的self.after(0, self._render, rows)是关键:第一个参数 0 表示不延迟,第二个参数是回到主线程后要执行的方法,第三个参数是传给它的数据。直接在子线程里操作self.tree.insert看起来也能跑,但偶发崩溃时你会排查到怀疑人生,所以老老实实回到主线程再更新控件。

4.4 打包 exe 后闪退:先别怪桌面环境,多半是资源路径和看不到报错

现象:本地python main.py一切正常,PyInstaller 打包成 exe 后双击闪退;放在别人电脑上提示找不到config.ini或library.sql。

原因:一是打包时用了-w隐藏控制台,闪退时看不到任何报错;二是代码里用了相对路径读取配置,exe 的工作目录是启动位置,不是 exe 所在目录;三是--add-data没把 ini 或 sql 资源打包进去,程序运行时找不到文件。

解决:先把-w去掉打一次包,在控制台窗口里看到真实报错。然后统一用resource_path()处理资源路径:

import sys import os def resource_path(relative_path): if hasattr(sys, "_MEIPASS"): # PyInstaller 打包后的临时资源目录 return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)

打包命令里用--add-data把资源文件打进去:

pyinstaller -F -w --add-data "config.ini;." --add-data "sql/library.sql;sql" main.py

注意 Windows 上--add-data的分隔符是分号;,Linux 和 macOS 上是冒号:。我给别人的机器打包踩过几次同样的坑,现在每次打包完都会先看一眼生成的warn-*.txt文件,里面会列出 pyinstaller 找不到的依赖和资源,这个文件比猜闪退原因管用得多。

4.5 自己换驱动翻车:pymysql 与 mysql-connector-python 的差异

现象:源码用的是 pymysql,你听人说 mysql-connector-python 更「官方」,改完连接后报TypeError: __init__() got an unexpected keyword argument 'cursorclass',或者事务提交相关的参数报错。

原因:mysql-connector-python 的连接函数签名、游标类型参数和 pymysql 不是一一对应。把 pymysql 的连接写法直接搬过去,参数名对不上就会这种错。两个驱动在日期格式、事务自动提交策略上也略有差异。

解决:项目里已经写好的驱动不要轻易换。pymysql 是纯 Python 实现,部署简单,性能对课程设计完全够用;换驱动不是改一行 import 的事,得逐项核对连接参数、游标方法和事务行为。如果担心 pymysql 的性能,先去加索引和优化 SQL,而不是换驱动。一个小型图书馆管理系统的查询量,远没到需要纠结驱动性能的程度。

5. 进阶:加一个借阅统计视图,再补一个管理员登录,课程设计直接加分

源码包里的基础功能跑通后,你还需要一个能拿出来讲的亮点。常见做法是增加一个借阅统计视图和一个带密码哈希的管理员登录,这两个功能实现成本低,但答辩时很能打。

先建视图:

CREATE VIEW vw_borrow_stats AS SELECT b.bname, COUNT(br.record_id) AS borrow_count FROM borrow_record br JOIN book b ON br.bid = b.bid WHERE br.status = 1 GROUP BY b.bid ORDER BY borrow_count DESC;

视图把「哪些书借得最多」变成了一个普通查询,在 Tkinter 里放一个按钮,点击后执行SELECT * FROM vw_borrow_stats,把结果填进 Treeview 就能展示。这里的关键是只统计status=1的已还记录,避免在借图书被重复计算;如果想要月度统计,可以在 WHERE 里加YEAR(br.borrow_date) = YEAR(CURDATE())或直接加MONTH(br.borrow_date)。

登录部分建议用一张独立的小表,密码不要存明文:

CREATE TABLE admin ( aid INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL );

写入时在 Python 里用hashlib.sha256加盐生成哈希,登录时只比较哈希值。示例逻辑:

import hashlib def make_password(pwd, salt): return hashlib.sha256((salt + pwd).encode()).hexdigest()

在登录界面上限制失败三次就退出程序,这个细节很多课程设计都想不到,但能直接体现「安全意识」。做完这两个功能后,记得做一次全流程验证:新建一个读者,录入三本不同书,分别借出再还回,看库存是否回到原值,看统计视图里排序是否正确;再用错误密码登录三次,确认程序能退出。这轮验证跑下来,比你口头说「能运行」有说服力得多。

我见过太多把按钮事件全部堆在 main.py 里的做法,改一个按钮得在一千行代码里翻半天。我现在做东西会习惯性地让界面层和逻辑层分文件放,功能能跑只是第一步,分得开才是真会。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询