
简介这份资源是面向高校数据库课程设计场景的图书管理系统完整项目采用Python结合SQL Server实现Web端应用适合正在完成课程作业或希望实践数据库知识的学生与开发者。系统参照学校图书馆借阅流程设计区分学生/老师、管理者、图书馆员工与超级管理四类角色覆盖登录、借阅、还书、延期、用户管理、图书上下架及操作记录查看等业务能帮助读者理解权限划分与借阅流程的落地方式。压缩包共183个文件约11.63MB包含16个py源码、23个html页面、13个js脚本、10个css样式及35张png截图另有pyd、dll、wheel等依赖与运行组件以及xmind、docx等设计文档结构完整便于二次开发与部署参考。目前已有701人学习下载可作为课程设计选题、功能拆解与数据库实践的综合参考。1. 从一张借书证说起Python SQL Server 的 Web 图书管理系统到底解决什么问题图书馆里最容易被低估的环节不是找书而是「这本书现在到底在谁手上」。我见过不少小型图书室借还记录还停留在 Excel 甚至纸质登记本上一旦有人问「上周借走的那本还有没有」管理员就得翻半天。基于 Python SQL Server 实现 Web 图书管理系统核心就是把「书目、读者、借还流水」这三件事塞进一张关系网里再用浏览器把它暴露出来让任何人打开网页就能查、能借、能还。它适合学校图书室、企业资料室、社区书屋这类藏书几千到几万册、并发不高但要求数据不丢的场景。选 Python 是因为写业务逻辑快选 SQL Server 是因为它对事务和约束的支持足够硬借还这种「要么全成功要么全回滚」的操作交给它比交给文件靠谱得多。2. 技术选型与数据库设计为什么是 Flask pyodbc 而不是别的组合2.1 Web 框架和驱动怎么挑Python 做 Web 的框架很多Django 自带 Admin 和 ORM看起来最省事但它对 SQL Server 的官方支持一直不算第一梯队遇到复杂存储过程或老版本实例时容易卡住。我一般会选 Flask轻路由和模板自己掌控配合 pyodbc 直接走 ODBC 驱动连 SQL ServerSQL 写起来是什么样执行就是什么样排查问题时不用猜 ORM 生成了什么语句。前端不用上 React 或 VueJinja2 模板加 Bootstrap 足够图书管理系统的交互复杂度撑不起前后端分离的成本。驱动这块要特别注意pyodbc 依赖系统里的 ODBC Driver for SQL ServerWindows 上装好「ODBC Driver 17」或「ODBC Driver 18」都行Linux 上则要装 msodbcsql 包。连接字符串里TrustServerCertificateyes在本地开发时能省掉证书报错但上线后应该换成正式证书否则等于把加密通道的验证关掉了。2.2 三张核心表加两张流水表数据库设计不用花哨但字段类型和约束必须一次定对后面改表结构比改代码痛苦得多。下面是我常用的最小可用结构直接在 SSMS 或 Navicat 里执行-- 图书表一本书一条记录副本用 quantity 表示 CREATE TABLE Books ( book_id INT IDENTITY(1,1) PRIMARY KEY, isbn VARCHAR(20) NOT NULL UNIQUE, title NVARCHAR(200) NOT NULL, author NVARCHAR(100), publisher NVARCHAR(100), total_copies INT NOT NULL DEFAULT 1, -- 总册数 avail_copies INT NOT NULL DEFAULT 1, -- 可借册数 created_at DATETIME DEFAULT GETDATE() ); -- 读者表用学号/工号做业务主键避免重名 CREATE TABLE Readers ( reader_id INT IDENTITY(1,1) PRIMARY KEY, card_no VARCHAR(20) NOT NULL UNIQUE, name NVARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1, -- 1正常 0冻结 max_borrow INT DEFAULT 5 -- 可借上限 ); -- 借阅流水一条记录代表一次借出归还时回填 return_date CREATE TABLE BorrowRecords ( record_id INT IDENTITY(1,1) PRIMARY KEY, book_id INT NOT NULL FOREIGN KEY REFERENCES Books(book_id), reader_id INT NOT NULL FOREIGN KEY REFERENCES Readers(reader_id), borrow_date DATETIME DEFAULT GETDATE(), due_date DATETIME NOT NULL, return_date DATETIME NULL, -- NULL 表示未归还 status TINYINT DEFAULT 0 -- 0在借 1已还 2逾期 );avail_copies这个冗余字段是故意的。每次借书都去BorrowRecords里COUNT未归还记录当然也能算但书一多、并发一上来这个聚合查询会成为瓶颈。用冗余字段换查询速度代价是借还时必须放在同一个事务里同时更新Books和BorrowRecords否则会出现「账实不符」——这是后面避坑章节要重点说的。2.3 连接层封装一个连接池省掉一半麻烦Flask 每个请求都新建数据库连接压测时很快就报连接数超限。我一般用dbutils的PooledDB做一层薄封装import pyodbc from dbutils.pooled_db import PooledDB POOL PooledDB( creatorpyodbc, maxconnections10, # 按 Web 并发量调一般 5~20 mincached2, blockingTrue, # 池满时等待而不是直接报错 ping1, # 每次取连接前 ping 一下防断连 dsnNone, driver{ODBC Driver 17 for SQL Server}, server127.0.0.1, databaseLibraryDB, uidsa, pwdYourStrongPwd, TrustServerCertificateyes ) def get_conn(): return POOL.connection()ping1这个参数救过我很多次。SQL Server 默认会断开空闲太久的连接没有 ping 的话用户隔夜第一次访问必然报「连接已关闭」。maxconnections不要贪大SQL Server Express 版对并发连接有限制设成 10 到 20 之间比较稳。3. 借书还书这条主线事务、并发和状态流转怎么写才不出错3.1 借书接口的完整事务借书看起来简单实际要同时做四件事检查读者状态、检查可借册数、检查是否超上限、写流水并扣减库存。任何一步失败都必须整体回滚否则就会出现「流水写了但库存没扣」这种对不上账的情况。def borrow_book(reader_id, book_id): conn get_conn() cursor conn.cursor() try: conn.autocommit False # 1. 锁定图书行防止两个人同时借最后一本 cursor.execute( SELECT avail_copies FROM Books WITH (UPDLOCK, ROWLOCK) WHERE book_id?, book_id ) row cursor.fetchone() if not row or row[0] 0: raise Exception(该书暂无可借副本) # 2. 校验读者状态和已借数量 cursor.execute( SELECT status, max_borrow FROM Readers WHERE reader_id?, reader_id ) r cursor.fetchone() if not r or r[0] ! 1: raise Exception(读者状态异常) cursor.execute( SELECT COUNT(*) FROM BorrowRecords WHERE reader_id? AND status0, reader_id ) if cursor.fetchone()[0] r[1]: raise Exception(已达可借上限) # 3. 写流水 扣库存同一事务 cursor.execute( INSERT INTO BorrowRecords(book_id, reader_id, due_date, status) VALUES(?, ?, DATEADD(DAY, 30, GETDATE()), 0), book_id, reader_id ) cursor.execute( UPDATE Books SET avail_copies avail_copies - 1 WHERE book_id?, book_id ) conn.commit() return True except Exception as e: conn.rollback() return str(e) finally: cursor.close() conn.close()关键在WITH (UPDLOCK, ROWLOCK)。没有它两个请求同时读到avail_copies1都判断可以借最后库存被扣成 -1。加上更新锁后第二个请求会等第一个事务提交再读到 0正确拒绝。due_date用DATEADD在数据库端算避免应用服务器和数据库服务器时区不一致导致还书日期差一天。3.2 还书与逾期判定还书要把流水状态改成已还、回填归还时间、库存加一同时判断是否逾期。逾期判定不要写死在代码里用数据库的DATEDIFF算def return_book(record_id): conn get_conn() cursor conn.cursor() try: conn.autocommit False cursor.execute( SELECT book_id, due_date FROM BorrowRecords WHERE record_id? AND status0, record_id ) row cursor.fetchone() if not row: raise Exception(流水不存在或已归还) book_id, due_date row cursor.execute( UPDATE BorrowRecords SET return_dateGETDATE(), status CASE WHEN GETDATE() due_date THEN 2 ELSE 1 END WHERE record_id?, record_id ) cursor.execute( UPDATE Books SET avail_copies avail_copies 1 WHERE book_id?, book_id ) conn.commit() return True except Exception as e: conn.rollback() return str(e) finally: cursor.close() conn.close()status用CASE WHEN在 SQL 里直接判定比先查出来再在 Python 里比较更省一次往返也避免应用和数据库时间不一致。注意还书时库存加一要防止超过total_copies可以在UPDATE上加AND avail_copies total_copies作为兜底。3.3 查询与分页别让首页拖垮数据库图书列表页最容易写成SELECT * FROM Books书一多直接卡死。分页用OFFSET FETCHSQL Server 2012 及以上都支持SELECT book_id, isbn, title, author, avail_copies FROM Books WHERE title LIKE ? OR author LIKE ? ORDER BY book_id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY;ORDER BY是OFFSET FETCH的硬性要求不能省。LIKE的参数要写成%关键词%但注意前置通配符会让索引失效数据量超过十万条时应该改用全文索引。每页条数我一般设 20前端传页码后端算offset (page-1)*size。4. 避坑与排查那些让我加班到凌晨的报错4.1 中文乱码NVARCHAR 和 VARCHAR 混用现象书名里的中文存进去变成问号或者查询时匹配不到。原因建表时用了VARCHAR而不是NVARCHARSQL Server 的VARCHAR按当前排序规则编码中文容易丢。解决所有可能存中文的字段一律用NVARCHAR连接字符串里加CHARSETUTF8对 pyodbc 无效真正管用的是字段类型和cursor.execute传参时用 Unicode 字符串。4.2 连接池耗尽Too many connections现象压测或多人同时访问时页面报连接超时。原因maxconnections设得太大超过 SQL Server 实例上限或者代码里conn.close()没放在finally里异常时连接泄漏。解决把maxconnections降到 10 左右确保每个get_conn()都有对应的close()用try/finally包住。4.3 库存对不上事务没包全现象流水显示借出但avail_copies没变或者还书后库存多了一本。原因写流水和更新库存不在同一个事务里中间抛异常只回滚了一半。解决把autocommit设为False所有相关操作放在同一个try块里异常时统一rollback()。上线前用脚本模拟 100 次并发借还核对流水总数和库存变化是否一致。4.4 日期差一天时区和 GETDATE 的坑现象明明今天还的书系统判定逾期。原因应用服务器和数据库服务器时区不同或者due_date在 Python 里用datetime.now()算和数据库GETDATE()差了几个小时。解决所有时间统一用数据库的GETDATE()或SYSDATETIME()生成应用层只负责展示不参与时间计算。4.5 部署后 500ODBC 驱动版本不匹配现象本地跑得好好的部署到服务器就报Data source name not found。原因服务器上没装对应版本的 ODBC Driver或者驱动名写的是SQL Server而实际装的是ODBC Driver 17 for SQL Server。解决用odbcinst -q -dLinux或「ODBC 数据源管理器」Windows确认驱动全名连接字符串里的driver参数必须和它一字不差。5. 进阶技巧用存储过程把借还逻辑收进数据库业务逻辑放在 Python 里不是不行但借还这种对一致性要求极高的操作放到存储过程里能再少一层网络往返也方便 DBA 直接审计。下面这个sp_BorrowBook把校验、写流水、扣库存全包进去应用层只负责调用和展示结果CREATE PROCEDURE sp_BorrowBook reader_id INT, book_id INT, result INT OUTPUT, message NVARCHAR(100) OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; IF NOT EXISTS (SELECT 1 FROM Books WITH (UPDLOCK) WHERE book_idbook_id AND avail_copies 0) BEGIN SET result -1; SET message N无可借副本; ROLLBACK TRANSACTION; RETURN; END IF NOT EXISTS (SELECT 1 FROM Readers WHERE reader_idreader_id AND status1) BEGIN SET result -2; SET message N读者状态异常; ROLLBACK TRANSACTION; RETURN; END INSERT INTO BorrowRecords(book_id, reader_id, due_date, status) VALUES(book_id, reader_id, DATEADD(DAY, 30, GETDATE()), 0); UPDATE Books SET avail_copies avail_copies - 1 WHERE book_id book_id; COMMIT TRANSACTION; SET result 0; SET message N借阅成功; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; SET result -99; SET message ERROR_MESSAGE(); END CATCH ENDPython 端调用就三行cursor.execute( DECLARE r INT, m NVARCHAR(100); EXEC sp_BorrowBook ?, ?, r OUTPUT, m OUTPUT; SELECT r, m, reader_id, book_id ) result, message cursor.fetchone()把逻辑收进存储过程后最大的好处是并发测试时不用再担心应用层漏写事务数据库自己保证原子性。代价是调试不如 Python 方便改逻辑要重新部署 SQL 脚本。我的习惯是核心借还走存储过程查询和报表走 Python 拼 SQL两边各取所长。验证这套系统是否真的可靠我一般会做三件事一是用脚本模拟 50 个并发借同一本书看最终avail_copies是否为 0 且流水恰好 50 条二是随机抽 100 条流水手工核对借出和归还后库存是否守恒三是把数据库服务重启一次看连接池能否自动恢复。这三关过了基本就能交给图书室实际用了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。