
简介一份面向高校数据库课程设计的图书管理系统完整项目基于Python、PyQt与SQLServer构建专门解决课设选题、系统实现与答辩演示等需求适合软件工程、计算机科学、人工智能等专业的课设作业或入门进阶学习。项目已通过导师指导认可答辩评审分达95分代码在mac及win10/11环境测试运行无误。压缩包共55个文件包含22个Python源码文件、21个编译后的pyc、3个SQL建表与数据脚本、3个备份文件、界面预览图及详细说明文档等整体仅122KB目录按管理员端、读者端、图书借还等模块划分便于快速定位与二次开发。除核心的借书、还书、图书管理、读者类型管理等功能外还提供数据库初始化脚本、示例数据以及垃圾数据清理脚本可一键还原演示环境。已有256人学习下载适合需要快速完成课设或对照学习PyQtSQLServer数据库编程的同学。1. 图书管理系统课设这份 PyQt SQLServer 高分项目到底能直接用吗每年这时候总有同学在课设截止前一周才想起找项目要么代码能跑但讲不出原理要么原理背熟了但程序一启动就闪退。这套基于 Python PyQt SQLServer 的图书管理系统属于少见的「代码完整度 文档补齐度」都在线的课设项目——源码里既有登录、借还书、读者管理这些常规模块也带了三份 SQL 脚本初始化库、样例数据、垃圾数据意味着你拿到手不只是能演示还能自己动手把数据库从零建一遍答辩时被问「表结构怎么设计的」「外键怎么维护的」都能接得住。适合软件工程、计科这类专业的学生拿来直接交课设也适合想在 PyQt 表单开发和 SQLServer 事务逻辑上补实操的开发者。2. 为什么选 PyQt SQLServer技术选型与项目结构拆解2.1 这套组合在课设场景下的优势用 PyQt5 做客户端界面配合 SQLServer 做数据层是数据库课设里比较「讨巧」的搭配。PyQt5 的信号槽机制很适合图书管理这种表单密集型的桌面应用一个按钮点击对应一个槽函数代码走向比写 Tkinter 清晰得多SQLServer 这边则有真实的用户登录名、库级权限和事务概念不像 SQLite 那样把数据库当文件拷来拷去。答辩时老师如果问「为什么不用 MySQL」你可以答SQLServer 的 Management Studio 可以直接跑 T-SQL 脚本做库表初始化图形化查看外键关系更直观而且 Windows 环境下 SQLServer 服务和 ODBC 驱动适配稳定。我见过不少同学用 PyMySQL 连 MySQL 课设但写到「同一本书同时被两个人借」这种并发场景时MySQL 的默认隔离级别和 SQLServer 的可重复读表现有明显差别。这套项目在借书逻辑里直接用事务包裹了库存扣减和借阅记录插入两步这两步要么同时成功要么同时回滚正好是数据库课设评分标准里「事务处理」这个得分点。2.2 源码包里的文件到底各管什么拿到压缩包先别急着运行对照下表把文件角色摸清楚省得后面跑起来找不到入口。文件/目录职责关键说明GUI_Main.py程序入口主窗口框架从这里启动不是从 SignIn.pySignIn.py登录窗口与权限校验登录成功后按角色跳转管理员/学生界面BookManagement.py图书信息维护增删改查调用 addBookDialogReaderManagement.py读者信息维护结合 ReaderTypeManagement 一起看BorrowStatusViewer.py借阅状态查询有 .bak 同名文件别混淆BookStorageViewer.py书库库存视图显示馆藏量与在架量SQLQuery.py自定义 SQL 执行器课设加分项嵌入 T-SQLsql/目录三份 SQL 脚本详见第 3 章171265889347208773632.zip原始打包文件里面是这套源码最初版本一般用不到这里有个典型坑目录里同时存在BorrowStatusViewer.py和BorrowStatusViewer.py.bak.bak是 PyCharm 的本地备份文件不是项目依赖运行时报错如果指向.bak路径说明你在 IDE 里点错了文件。所有入口一律从GUI_Main.py走它会按顺序加载登录窗口、主窗口和各对话框模块。2.3 从启动到进入主界面的调用链运行GUI_Main.py后的执行顺序大概是这样的先实例化SignIn登录窗口exec()进入事件循环用户输入账号密码点击登录槽函数里执行 SQL 查询用户表如果命中且角色为管理员则accept()当前登录框MainWindow构造时把当前用户名和权限级别传过去普通学生账号则打开StudentHome.py的简化界面。Account.py管当前会话UserManage.py负责管理员对操作员账号的增删改。# GUI_Main.py 中的典型启动逻辑核心片段 import sys from PyQt5.QtWidgets import QApplication, QMessageBox from SignIn import SignIn from AdminHome import AdminHome from StudentHome import StudentHome def main(): app QApplication(sys.argv) login SignIn() # 登录窗口 if login.exec_(): # 只有登录成功才返回 True if login.user_role admin: # 管理员进后台 win AdminHome(login.user_id) else: # 学生只读视图 win StudentHome(login.user_id) win.show() sys.exit(app.exec_()) else: sys.exit(0) # 直接关掉登录框就退出 if __name__ __main__: main()登录返回的user_role字段至关重要它决定你能看到哪些菜单。AdminHome里包含图书管理、读者管理、借阅管理等完整菜单StudentHome则只开放查询和本人借阅历史。这套权限设计也是答辩加分点——角色权限不是前端隐藏菜单那么简单而是后端每个查询都带WHERE reader_id 当前用户条件防止学生绕过界面直接改 URL 或调用系统函数。3. 数据库初始化三份 SQL 脚本怎么配合使用3.1 建库脚本 LibraryDB__Init__.sql 的核心表结构这份脚本是整套系统的基础它会创建数据库、用户表、图书表、读者表、借阅表以及外键约束。值得注意的是一般课设项目只会给一张create_all.sql而这份脚本区分了「库初始化」和「业务数据填充」逻辑上更贴近真实开发环境。-- LibraryDB__Init__.sql 核心片段 CREATE DATABASE LibraryDB; GO USE LibraryDB; GO -- 管理员/用户表登录认证 CREATE TABLE TB_User ( UserID INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(20) NOT NULL UNIQUE, PassWord NVARCHAR(50) NOT NULL, UserRole NVARCHAR(10) DEFAULT user, -- admin / user CreateTime DATETIME DEFAULT GETDATE() ); GO -- 图书表馆藏信息 CREATE TABLE TB_Book ( BookID INT IDENTITY(1,1) PRIMARY KEY, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50), Publisher NVARCHAR(50), ISBN NVARCHAR(20) UNIQUE, Category NVARCHAR(20), TotalCount INT DEFAULT 1, -- 馆藏总量 CurrentCount INT DEFAULT 1, -- 当前可借数量 Location NVARCHAR(30) -- 书架位置 ); GO -- 借阅表关联读者和图书 CREATE TABLE TB_Borrow ( BorrowID INT IDENTITY(1,1) PRIMARY KEY, ReaderID INT NOT NULL REFERENCES TB_Reader(ReaderID), BookID INT NOT NULL REFERENCES TB_Book(BookID), BorrowDate DATETIME DEFAULT GETDATE(), ReturnDate DATETIME, -- 实际归还时间 DueDate DATETIME, -- 应还时间 Status NVARCHAR(10) DEFAULT borrowed -- borrowed / returned / overdue ); GOTB_Book表里的TotalCount和CurrentCount是一对镜像字段借书时CurrentCount - 1还书时CurrentCount 1。这个设计比直接COUNT(*)统计借阅表高效但代价是必须在事务里更新否则会出现「库存显示有书但实际借不出」的数据不一致。原项目在存储过程里通过BEGIN TRAN包裹两步更新如果你改成别的数据库这地方是第一个容易翻车的位置。3.2 LibraryTableData.sql 样例数据的作用这份脚本主要往TB_User插入管理员账号、往TB_Reader插入若干读者类型学生、教师以及往TB_Book插入 20 本左右的图书。它的价值在于让界面不至于空荡荡——课设演示时如果书库一片空白不仅难看而且借书流程根本走不通。执行这份脚本前要留意里面是否包含DELETE FROM或TRUNCATE TABLE语句如果有说明它设计成可重复执行的幂等脚本。重复执行不会产生重复数据这是好习惯但如果你改动了部分数据后重新跑脚本原来的修改会被覆盖所以不要在学习阶段反复执行它。3.3 「垃圾数据」脚本到底是不是用来捣乱的文件名为「向TB_Book表录入垃圾数据.sql」这个「垃圾」不是贬义而是指特意构造的边界数据比如超长的书名、NULL 的作者、重复的 ISBN 等。它的用途有两个一是压力测试在数据量大的情况下看界面滚动、查询响应是否正常二是演示 SQL 容错能力——TRIM()、ISNULL()、DISTINCT这些函数在处理脏数据时的表现正好是答辩时老师爱追问的点。-- 垃圾数据脚本示例构造书名超长、作者为空的记录 INSERT INTO TB_Book (BookName, Author, Publisher, ISBN, Category, TotalCount, CurrentCount) VALUES (REPLICATE(压测, 30), NULL, 测试出版社, 978-0000000001, 其他, 0, 0);上面这条记录书名被REPLICATE放大到 60 个字符Author为 NULLTotalCount为 0。运行后你会发现图书管理界面的表格控件需要设置wordWrap或固定列宽才能正常显示而借书对话框对CurrentCount 0的书要弹出「库存不足」提示。这些边界情况正是正常数据测不出来的。3.4 从零到跑起来的完整初始化步骤如果你拿到压缩包后发现数据库还是空的按下面顺序重建打开 SQL Server Management Studio用 Windows 身份认证连接本地实例。新建查询把LibraryDB__Init__.sql整个文件拖入执行。确认输出窗口没有红色报错。再执行LibraryTableData.sql导入基础业务数据。执行「垃圾数据」脚本建议最后跑因为它会改动TB_Book的边界值。右键LibraryDB→ 属性 → 文件记下逻辑名称后面配置连接字符串要用。连接字符串通常在某个全局配置或SignIn.py头部原项目用的是pyodbc驱动格式一般是conn_str ( DRIVER{ODBC Driver 17 for SQL Server}; SERVERlocalhost; DATABASELibraryDB; UIDsa; PWD你的密码; )注意SERVER别写成localhost\\SQLEXPRESS这类带实例名格式除非你安装时确实指定了命名实例。UID/PWD建议单独建一个课设专用登录名权限只开放给LibraryDB这样既不污染sa的默认配置也方便老师检查权限设计。4. 核心功能模块拆解登录、借还书、查询背后的实现细节4.1 登录模块 SignIn.py 的防 SQL 注入与角色判断登录模块是最容易被答辩老师盘问的地方。很多课设代码写着SELECT * FROM user WHERE name %s % input一旦输入 OR 11 --就能绕过登录这是最减分的安全漏洞。这套项目的SignIn.py比较规矩用了参数化查询。# SignIn.py 核心片段 from PyQt5.QtWidgets import QDialog, QMessageBox import pyodbc class SignIn(QDialog): def __init__(self): super().__init__() # ... 界面初始化代码略 ... def check_login(self): uid self.editUser.text().strip() pwd self.editPass.text().strip() if not uid or not pwd: QMessageBox.warning(self, 提示, 用户名和密码不能为空) return sql ( SELECT UserID, UserName, UserRole FROM TB_User WHERE UserName ? AND PassWord ? ) try: cursor self.conn.cursor() cursor.execute(sql, (uid, pwd)) # 参数化查询禁止拼接 row cursor.fetchone() if row: self.user_id row[0] self.user_role row[1] # 取值给 GUI_Main 分流 self.accept() else: QMessageBox.critical(self, 登录失败, 用户名或密码错误) except pyodbc.Error as e: QMessageBox.critical(self, 数据库错误, str(e))执行cursor.execute(sql, (uid, pwd))时pyodbc会把参数转成合法的 SQL 字面量用户输入的单引号会被自动转义注入语句失效。这里有个细节self.user_role建议在__init__里先初始化为空字符串否则用户直接点 X 关闭登录框时GUI_Main.py里访问login.user_role不会报AttributeError而是得到空串流程更健壮。4.2 借书对话框 borrowBookDialog.py 的事务边界借书是图书管理系统中最有「数据库感」的操作。整套逻辑必须保证「书被借出」和「库存减一」同时发生否则要么有人借到不存在的书要么库存越扣越负。原项目把这部分逻辑集中在一个方法里思路清晰。# borrowBookDialog.py 借书核心方法 def confirm_borrow(self): reader_no self.editReader.text().strip() book_no self.editBook.text().strip() if not reader_no or not book_no: QMessageBox.information(self, 提示, 请完整填写读者编号和图书编号) return conn self.get_connection() cursor conn.cursor() try: conn.autocommit False cursor.execute(BEGIN TRAN) # 显式开启事务 # 1. 检查读者是否存在且在有效期内 cursor.execute(SELECT ReaderID, ValidFlag FROM TB_Reader WHERE ReaderNo ?, (reader_no,)) reader cursor.fetchone() if not reader or reader[1] ! 1: conn.rollback() QMessageBox.warning(self, 错误, 读者不存在或已停用) return # 2. 检查图书在架数量 cursor.execute(SELECT BookID, CurrentCount FROM TB_Book WHERE BookNo ?, (book_no,)) book cursor.fetchone() if not book or book[1] 0: conn.rollback() QMessageBox.warning(self, 错误, 该书暂时无可借副本) return # 3. 扣减库存 插入借阅记录 cursor.execute(UPDATE TB_Book SET CurrentCount CurrentCount - 1 WHERE BookID ?, (book[0],)) cursor.execute( INSERT INTO TB_Borrow (ReaderID, BookID, BorrowDate, DueDate, Status) VALUES (?, ?, GETDATE(), DATEADD(day, 30, GETDATE()), borrowed), (reader[0], book[0]) ) conn.commit() QMessageBox.information(self, 成功, 借书成功应还日期为 30 天后) except Exception as e: conn.rollback() QMessageBox.critical(self, 失败, f借书失败事务已回滚{str(e)}) finally: cursor.close() conn.close()值得注意的几个参数点DATEADD(day, 30, GETDATE())把借期固定为 30 天如果你想改成 15 天改这里就行ValidFlag字段如果是 0说明该读者被暂停借阅这条判断是很多简化版课设没有的。另外务必开启autocommit False否则rollback()形同虚设因为每条 SQL 自动提交了你恢复不了库存。4.3 还书对话框 returnBookDialog.py 的逾期计算还书逻辑比借书多了一个「逾期判断」。归还时除了把CurrentCount 1还要根据DueDate和当前时间计算出overdue_days并更新借阅记录的Status为returned或overdue_returned。实现上通常用DATEDIFF(day, DueDate, GETDATE())计算天数差如果差值大于 0就把逾期天数写入一个字段。# returnBookDialog.py 还书核心方法 def confirm_return(self): borrow_id self.editBorrowID.text().strip() if not borrow_id.isdigit(): QMessageBox.warning(self, 提示, 借阅编号必须为数字) return conn self.get_connection() cursor conn.cursor() try: conn.autocommit False cursor.execute(BEGIN TRAN) # 查找借阅记录并锁定防止重复还书并发 cursor.execute( SELECT BookID, DueDate, Status FROM TB_Borrow WHERE BorrowID ? FOR UPDATE, (int(borrow_id),) ) row cursor.fetchone() if not row or row[2] returned: conn.rollback() QMessageBox.information(self, 提示, 该借阅记录不存在或已归还) return overdue_days 0 if row[1] and row[1] datetime.datetime.now(): overdue_days (datetime.datetime.now() - row[1]).days # 库存加回 cursor.execute(UPDATE TB_Book SET CurrentCount CurrentCount 1 WHERE BookID ?, (row[0],)) # 更新借阅状态 if overdue_days 0: cursor.execute( UPDATE TB_Borrow SET ReturnDate GETDATE(), Status overdue_returned, OverdueDays ? WHERE BorrowID ?, (overdue_days, int(borrow_id)) ) else: cursor.execute( UPDATE TB_Borrow SET ReturnDate GETDATE(), Status returned WHERE BorrowID ?, (int(borrow_id),) ) conn.commit() if overdue_days 0: QMessageBox.warning(self, 逾期, f该书逾期 {overdue_days} 天按规定需要缴纳滞纳金) else: QMessageBox.information(self, 成功, 还书成功) except Exception as e: conn.rollback() QMessageBox.critical(self, 失败, f还书失败{str(e)}) finally: cursor.close() conn.close()这里的FOR UPDATE是 SQLServer 的锁提示防止两个窗口同时操作同一条借阅记录造成重复还书。课设里能写出FOR UPDATE而不是盲目UPDATE说明你理解并发控制这一句就能拉开分数差距。OverdueDays字段如果表里没建需要在初始化脚本里补ALTER TABLE TB_Borrow ADD OverdueDays INT DEFAULT 0。4.4 SQLQuery.py 自定义查询器答辩演示的隐藏加分项原装的图书管理界面都是固定表单查询答辩时老师往往想看你「会不会写 SQL」。SQLQuery.py提供了一个文本框 表格控件的组合可以手输任意SELECT语句并即时执行结果直接渲染到QTableView里。这个模块的实现思路可以概括为三步把文本框内容交给pyodbc的cursor.execute()。用cursor.description拿到列名动态构建表头。遍历fetchall()逐行填充QStandardItemModel。# SQLQuery.py 动态查询核心代码 def run_query(self): sql_text self.editSQL.toPlainText().strip() if not sql_text.upper().startswith(SELECT): QMessageBox.warning(self, 限制, 出于安全考虑只允许执行 SELECT 查询) return try: cursor self.conn.cursor() cursor.execute(sql_text) columns [desc[0] for desc in cursor.description] model QStandardItemModel() model.setHorizontalHeaderLabels(columns) rows cursor.fetchall() for row in rows: items [QStandardItem(str(cell)) for cell in row] model.appendRow(items) self.tableView.setModel(model) self.labelRowCount.setText(f共 {len(rows)} 条记录) except pyodbc.Error as e: QMessageBox.critical(self, SQL 错误, str(e))限制只允许SELECT是合理的安全策略否则任何人拿到这个窗口都能DROP TABLE。如果你想开放更新操作加一层确认弹窗和控制条件即可但课设阶段保持只读最稳妥。5. 避坑指南SQLServer 连接、PyQt 显示、数据不一致的五个实战排查记录5.1 SQLServer 2016/2017/2019 安装失败无法找到数据库引擎启动句柄现象安装 SQLServer 到「数据库引擎配置」一步报错提示「无法找到数据库引擎启动句柄」服务无法启动。原因最常见的是安装过程中选择了「默认实例」但机器上已经存在命名实例端口冲突导致引擎进程起不来其次是 Windows 防火墙拦了 1433 端口。这套图书管理系统的连接串写死localhost如果服务起不来p yodbc 会直接抛Cannot open database之类错误。解决先确认服务列表里 SQL Server 服务是否真的在运行打开「SQL Server 配置管理器」查看 SQL Server 服务的状态。如果显示已停止右键启动并查看错误日志C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\Log。日志里若有地址占用把 SQLServer 的端口改成 1434或者干脆卸载重装并选「命名实例SQLEXPRESS」然后SERVERlocalhost\\SQLEXPRESS连接。日常排查中八成以上这类问题都是杀毒软件抢先占用了 1433 端口绑定驱动之前先netstat -ano | findstr 1433看一眼。5.2 pyodbc 报错 Data source name not found and no default driver specified现象运行GUI_Main.py登录窗口能弹出来但输入账号密码后立刻弹出「Data source name not found and no default driver specified」数据库连接串完全不起作用。原因连接串里的驱动名称和系统实际安装的 ODBC 驱动不匹配。原项目写的是ODBC Driver 17 for SQL Server但你的机器可能只装了ODBC Driver 13或者只有SQL Server旧驱动。解决打开「ODBC 数据源管理器」在「驱动程序」标签页查看已安装条目把连接串里的DRIVER改成实际存在的名称。常见适配如下表系统环境推荐 DRIVER 写法Win10 SQLServer 2019 默认安装ODBC Driver 17 for SQL Server老旧 Win7 / 只装过 SQLServer 2008SQL Server装了最新 SQLServer 2022 工具包ODBC Driver 18 for SQL Server改完千万别忘了同时检查SERVER字段如果用的命名实例写成localhost\\SQLEXPRESS反斜杠在 Python 字符串里要写成\\\\或者原始字符串r...不然连接串解析会把\\S当成转义字符。5.3 PyQt5 界面在高分屏2K/4K下模糊、控件错位现象代码在 1080p 显示器上正常换到 2K 或 4K 高分屏上窗口文字模糊、按钮拉伸变形截图发给老师看效果极差。原因PyQt5 默认不支持 Windows 的 DPI 缩放高分辨率下控件按物理像素渲染字号没有等比放大。解决在GUI_Main.py最顶部、import 任何 Qt 模块之前加上全局 DPI 感知设置import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1) # 1 为系统 DPI 感知如果加了之后文字还是偏小再在QApplication创建后设置默认字号app.setFont(QFont(Microsoft YaHei, 10))。这一招对SignIn.py一样有效因为它也是窗口的一部分。课堂演示时如果投影仪分辨率低反而要删掉这两行否则界面被缩成一团这个道理和连显示器分辨率要「先看投影再改代码」一致。5.4 执行「垃圾数据」脚本后书库统计与现实对不上现象跑完三份 SQL 脚本BookStorageViewer.py显示的馆藏总量和图书管理模块里的总数差了几本甚至出现CurrentCount为负数。原因「垃圾数据」脚本里故意构造了TotalCount 0, CurrentCount 0的记录而统计视图如果直接用SUM(TotalCount)和SUM(CurrentCount)这些零值不算事但坏在有些垃圾记录的CurrentCount被写成负数脚本里REPLICATE生成的书籍数量含义是「损坏下架」设计上允许为负。解决确认统计 SQL 是否加了WHERE CurrentCount 0过滤同时在界面层做防御。原项目在BookStorageViewer.py里加载数据后加了一个判断当某行CurrentCount 0时单元格标红提示「数据异常」。如果你在演示时点到这个状态别慌正好借机解释「这是特意造出来的异常数据用来验证系统容错」。5.5 中文乱码登录框字体变成方块查询结果全是问号现象PyQt 界面上中文正常但从SQLQuery.py手输SELECT * FROM TB_Book后表格里的书名显示为???或乱码直接执行 SQL 脚本里的中文也入库变样。原因SQLServer 端没有使用NVARCHAR类型存储中文或者连接串里没有指定CHARSET。原项目表结构用了NVARCHAR正确做法但如果初始化脚本被改成VARCHAR中文会因代码页不对而截断。反过来如果数据库是对的pyodbc默认按 UTF-8 解码有时与 SQLServer 的UCS-2内部编码不匹配查询结果里中文就变乱码。解决先检查TB_Book的建表语句确保书名、作者字段都是NVARCHAR再在连接串末尾追加CHARSETUTF-8示例conn_str ( DRIVER{ODBC Driver 17 for SQL Server}; SERVERlocalhost; DATABASELibraryDB; UIDsa; PWD你的密码; CHARSETUTF-8; )这一条尤其重要很多同学的课设代码在别人电脑上跑得好好的拷到自己机器就乱码原因就是连接串里没带CHARSET参数。Windows 区域设置里如果「非 Unicode 程序语言」改成英语乱码概率会大幅上升执行脚本前先把区域设置切成中文再跑。6. 改造定位把通用代码变成「你自己的课设」的三步走课设答辩里最尴尬的环节不是代码跑不起来而是老师问「这个项目哪里是你写的」你支支吾吾答不上来。所以拿到这套源码后不建议原封不动提交至少做三个层面的改造让项目「长」在你身上。第一步换个功能切入点。图书管理系统最不缺的就是同类项目你可以把「图书」换成「光盘」或「实验设备」字段从TB_Book改成TB_Device目录结构不变只改SQL脚本和界面 label。表结构里TotalCount/CurrentCount这对镜像字段完全适用设备借用场景连借还逻辑都不用动。这一步的成本很低但答辩差异度立现。第二步补一个老师爱问的「统计报表」功能。在AdminHome.py里加一个按钮弹窗展示「近 30 天借阅趋势」SQL 用GROUP BY CAST(BorrowDate AS DATE)配合COUNT(*)查出每日借阅量再用matplotlib的FigureCanvasQTAgg嵌到 PyQt 窗口里。# 借阅趋势统计按天分组输出 cursor.execute( SELECT CAST(BorrowDate AS DATE) AS Day, COUNT(*) FROM TB_Borrow WHERE BorrowDate DATEADD(day, -30, GETDATE()) GROUP BY CAST(BorrowDate AS DATE) ORDER BY Day )这条 SQL 的CAST(BorrowDate AS DATE)是关键它把带时间戳的DATETIME截断到天否则同一天多条记录会被GROUP BY拆成多行。报表加到系统后答辩时你可以指着趋势图说「这是我自己扩展的模块」比「我改了几行注释」有说服力得多。第三步硬着头皮走一遍完整演示链路。打开SQLQuery.py手输三条 SQL一条SELECT测查询一条带JOIN的连表查询一条带GROUP BY的聚合查询然后点执行看结果。这一步能帮你提前踩完CHARSET乱码、驱动不匹配、字段类型不符这些运行时地雷。我当年做课设时就在答辩前一晚发现JOIN查出来中文全乱临时排查到是区域设置问题差点当场上不了台。从那以后我拿到任何带 SQLServer 的项目第一件事都是先让SQLQuery.py里的中文查询跑通再谈界面功能——数据层不干净界面再花哨也是白搭。希望你也能在改代码的过程中把数据库事务和 PyQt 信号槽这两块彻底吃透课设答辩顺利过关希望帮到你。本文还有配套的精品资源点击获取