简介:一套基于Python与SQL Server开发的Web图书管理系统完整课程设计源码包,主要面向高校计算机专业需要完成数据库或Web开发课设的学生。系统参考学校图书馆借阅流程,包含学生/教师借阅端与管理人员后台:借阅者可执行登录、借书、还书、延期等操作;管理端覆盖用户管理、图书增删改下架、超级管理员查看操作记录等模块,能够完整演示图书管理业务。资源包共183个文件、约11.63MB,文件类型涵盖Python后端脚本(py/pyd)、HTML/CSS/JS前端页面、png图片、xmind思维导图以及txt/docx等说明文件,其中pyd和dll属于运行依赖,html/js/css构成可视化界面,xmind与docx可辅助理解项目结构。已有701人学习/下载,适合参考其数据库设计、借阅流程控制与权限管理思路,解压后对照目录结构即可开展学习和二次开发。
1. 图书管理系统课程设计:一套能直接跑通的 Python + SQL Server Web 方案
如果你正在为数据库课程设计头疼,题目又正好是“图书管理系统”,建议先看这套基于 Python + SQL Server 的 Web 实现。它用 Flask 写后端、SQL Server 存业务数据、Bootstrap 搭界面,把学校图书馆的借阅流程——登录、借书、还书、延期、管理员维护——做成了能在浏览器里点击演示的网页,而不是控制台里输命令的作业。管理端还区分了普通管理员和超级管理员,自带操作日志,正好覆盖课程设计里“三层角色”的验收要求。适合正在赶课设、想少踩坑的学生,也适合想补一遍 Python + SQL Server 实操的从业者。下面把选型理由、数据库设计、核心代码、启动步骤和典型故障依次讲完。
2. 架构与数据模型:四张表搞定借阅、用户与日志三条业务线
2.1 Web 框架选型:课程设计场景为什么推荐 Flask
课程设计和真实项目不一样:你要做的东西必须能演示,而且答辩老师一定会追问“这段代码是你写的吗”“这个表为什么这么设计”。所以选型的第一原则不是功能多,而是你自己讲得清楚。
Flask 的优势在于路由直观、模板简单、不强制你用 ORM。整个后端逻辑可以集中在 app.py 里,数据库操作直接用 SQL 语句写,答辩时老师问“你的 where 条件怎么写的”,你直接打开文件指给他看就行。相比之下 Django 自带 Admin 后台,确实省事,但“你自己写的东西”变少,老师追问业务逻辑时反而容易发虚。前端部分项目用的是 Bootstrap,它只负责页面样式和组件,不需要额外学前端框架,对课设来说足够。
还有一点很多人忽略:Flask 的 session 机制和路由装饰器非常适合演示“登录后才能操作”“管理员才能进管理页”这类权限控制,代码量小,判断逻辑一目了然。这套结构基本就是课设的标准答案:一个 app.py 做入口,templates 目录放页面模板,static 目录放 Bootstrap 静态资源,再加一个 db.py 封装数据库连接。
2.2 数据模型设计:四张表覆盖所有业务线
整个系统可以拆成三条业务线:用户身份与权限、图书档案、借阅流转。再加上审计需求,就是四张表。下面把核心字段和设计理由过一遍。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| users | user_id, username, password, real_name, role, status | 学生/老师/管理员/超级管理员共用一个表 |
| books | book_id, isbn, title, author, publisher, category, total_count, available_count, status | available_count 是冗余字段 |
| borrow_records | record_id, user_id, book_id, borrow_date, due_date, return_date, renew_count, operator_id | 借阅与还书记录 |
| operation_logs | log_id, user_id, action, detail, created_at | 管理员操作审计 |
users 表用 role 字段区分四类角色:student、teacher、admin、super_admin,而不是把学生表和老师表分开建。原因很简单——课设阶段多一张表就多一倍联表查询和权限判断,而学生和老师借书行为其实完全一致,分开没意义。password 字段存的是哈希值,不是明文,我习惯用 SHA256 加固定盐,答辩时问“密码怎么存的”这一下就能加分。
books 表里专门留了 available_count 可借数量字段。为什么不用SELECT COUNT(*) FROM borrow_records WHERE book_id=? AND return_date IS NULL现算?因为借书操作非常频繁,每次借出都全表统计一次不仅慢,高峰期还有超借风险。直接UPDATE books SET available_count = available_count - 1 WHERE available_count > 0一行 SQL 就搞定,原子性也有保障。
borrow_records 是业务核心,三个日期字段是关键:borrow_date 借出时间、due_date 应还时间、return_date 实际归还时间。return_date 为空代表还没还,逾期不另存状态,直接比较 due_date 和当前时间就能算出来。renew_count 记录续借次数,限制只能续一次。operator_id 记录是哪个管理员操作的,方便日志追溯。
2.3 借阅状态机:借出、还书、延期的流转规则
借阅流程本质是一个状态机,但这里不建议用单独的 status 字段去存“借出中”“已逾期”“已归还”这些状态,因为这样会导致两个地方维护状态,很容易不一致。更可靠的做法是:状态全部由字段组合计算出来。
| 业务状态 | 判定条件 |
|---|---|
| 在架可借 | books.status=1 AND available_count > 0 |
| 借出中 | borrow_records.return_date IS NULL |
| 已逾期 | due_date < GETDATE() AND return_date IS NULL |
| 已归还 | return_date IS NOT NULL |
借书的约束在课设里通常要覆盖三条:书必须在架且有可借库存;读者不能有逾期未还的旧记录;同一本书不能重复借出未还。还书则把 return_date 置为当前时间,同时库存加一。延期申请必须满足“未归还、未续借过”,然后把 due_date 往后加 30 天,renew_count 置为 1。
这样设计有个实际好处:数据冗余少,逻辑集中。比如“哪些人逾期了”这个需求,一条SELECT ... WHERE return_date IS NULL AND due_date < GETDATE()就查出来,不用额外维护逾期表。课程设计答辩时,老师问“逾期怎么处理”,你把这个 SQL 一亮,整个设计思路就立住了。
3. 核心功能落地:登录、借书、还书、延期的关键代码与事务写法
3.1 登录与会话:三种角色如何共用一套校验逻辑
登录是入口,也是权限控制的基础。Flask 里可以用 session 来维持用户状态,登录成功把 user_id 和 role 写进 session,后续每个需要权限的页面再校验。密码校验的细节值得认真写:数据库里存的是哈希,比对哈希而不是比对明文。
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] # 注意:密码统一用 sha256 + 盐哈希后比对,库里不要存明文 hashed = hashlib.sha256((password + SALT).encode('utf-8')).hexdigest() cur.execute( "SELECT user_id, real_name, role FROM users WHERE username=? AND password=? AND status=1", (username, hashed) ) row = cur.fetchone() if row: session['user_id'] = row[0] session['role'] = row[1] return redirect('/dashboard') return render_template('login.html', error='用户名或密码错误') return render_template('login.html')这段代码有两个细节建议答辩时主动讲:一是用了参数化查询,?占位符由驱动处理转义,是防 SQL 注入的标准做法;二是登录时把 status=1 加进查询条件,被停用的用户直接进不来,比登录成功后再单独判断要少一次查询。SALT 建议写死在配置常量里,课设阶段不必引入环境变量管理。
3.2 借书与还书:库存扣减与逾期计算的 SQL 写法
借书不能只写一条插入语句就完事,必须按“查—验—写”三步走。先查图书是否在架且有库存,再查读者是否符合借阅条件,最后才写借阅记录并扣库存。这三步之间会有并发风险,所以要用事务包起来。
@app.route('/borrow', methods=['POST']) def borrow(): user_id = session.get('user_id') book_id = request.form['book_id'] try: # 第1步:查书是否可借,这里连带锁行,避免并发超借 cur.execute( "SELECT title, available_count FROM books WITH (UPDLOCK) WHERE book_id=? AND status=1", (book_id,) ) book = cur.fetchone() if not book or book[1] <= 0: return "该书不存在或库存为 0" # 第2步:查读者是否有逾期未还,有则拦截 cur.execute( "SELECT COUNT(*) FROM borrow_records " "WHERE user_id=? AND return_date IS NULL AND due_date < GETDATE()", (user_id,) ) if cur.fetchone()[0] > 0: return "你有逾期未还图书,请先归还" # 第3步:插入借阅记录,应还日期默认 30 天后 cur.execute( "INSERT INTO borrow_records (user_id, book_id, borrow_date, due_date) " "VALUES (?, ?, GETDATE(), DATEADD(day, 30, GETDATE()))", (user_id, book_id) ) # 第4步:扣减可借库存 cur.execute( "UPDATE books SET available_count = available_count - 1 WHERE book_id=?", (book_id,) ) conn.commit() return "借阅成功" except Exception as e: conn.rollback() return f"借阅失败:{e}"WITH (UPDLOCK)是这步的关键,它在查询时就对这条图书记录加了更新锁,两个并发请求不会同时看到同一份可借库存。课设虽然不一定有并发压测,但写出来老师会觉得你考虑过真实场景。还书和延期就更明确,核心是把状态流转写进 SQL,而不是在 Python 里改一堆变量:
# 还书:补 return_date,同时库存加一 cur.execute( "UPDATE borrow_records SET return_date = GETDATE() " "WHERE record_id=? AND return_date IS NULL", (record_id,) ) cur.execute( "UPDATE books SET available_count = available_count + 1 " "WHERE book_id=?", (book_id,) ) # 延期:只允许未还且未续借过的记录,续期 30 天 cur.execute( "UPDATE borrow_records SET due_date = DATEADD(day, 30, due_date), renew_count = 1 " "WHERE record_id=? AND return_date IS NULL AND renew_count = 0", (record_id,) )你注意还书更新记录时带了return_date IS NULL条件,这是一种乐观锁思路:如果这条记录已经被还过了,那受影响行数是 0,后续可以判断cur.rowcount来提示用户。日期计算全用 SQL Server 的 GETDATE() 和 DATEADD,原因是 Python 端生成的时间和 SQL Server 服务器时间可能有时区差异,统一交给数据库算,口径一致。
3.3 图书管理:新增、修改、下架的边界条件
管理员模块的核心是图书维护。新增图书要处理 ISBN 重复,修改图书要处理库存联动,下架则要处理外键引用。先说新增和修改:
# 新增图书:先检查 ISBN 是否已存在,存在则提示走修改流程 cur.execute("SELECT COUNT(*) FROM books WHERE isbn=?", (isbn,)) if cur.fetchone()[0] > 0: return "该 ISBN 已存在,请使用修改功能" cur.execute( "INSERT INTO books (isbn, title, author, publisher, category, total_count, available_count, status) " "VALUES (?, ?, ?, ?, ?, ?, ?, 1)", (isbn, title, author, publisher, category, total_count, total_count) ) # 修改图书:只有未借出的数量差才能调整 cur.execute( "UPDATE books SET title=?, author=?, publisher=?, category=?, total_count=? " "WHERE book_id=?", (title, author, publisher, category, total_count, book_id) )修改库存这里有一个坑:之前借出的书还没归还,如果你直接把 total_count 改小,available_count 可能大于 total_count,数据就矛盾了。常见做法是总库存只允许增加,或者至少要保证available_count <= total_count。在页面上直接加一个校验规则:available_count不允许比total_count大。下架操作同理,更不能用 DELETE:
UPDATE books SET status = 0 WHERE book_id = ? AND book_id NOT IN ( SELECT DISTINCT book_id FROM borrow_records WHERE return_date IS NULL )这个NOT IN子查询保证“有未还记录的书不允许下架”,从根源上避免外键纠纷。如果下架成功,affected row count 为 1;如果为 0,说明该书存在未还记录,把这种情况翻译成“请先处理借出记录再下架”提示给管理员。
4. 环境搭建与启动流程:从空白 SQL Server 到浏览器出页面的完整路径
4.1 虚拟环境与依赖:激活 venv 后需要装什么
项目压缩包里已经带了一套 Python 虚拟环境,能看到 activate、activate.bat、deactivate.bat 和 pyvenv.cfg 这些文件,说明创建者已经提前把环境备好了。你要做的第一步是按操作系统激活它。
# Windows 下直接执行 activate.bat # macOS / Linux 下执行 source activate # 激活后安装后端依赖 pip install flask pyodbc激活成功后命令行提示符前面会出现(venv)标志,这时候 pip 装的东西只会进这个虚拟环境,不会污染系统 Python。依赖其实就两个:flask 负责 Web 服务和路由,pyodbc 负责连 SQL Server。如果你在一台全新机器上跑,建议先 updrade 一下 pip 再装,避免源仓库里轮子不匹配导致的玄学安装错误。
4.2 数据库连接:pyodbc 连接串与 db.py 的写法
pyodbc 是 Python 连 Microsoft SQL Server 最主流的驱动,没有任何 ORM 包装,写 SQL 和拿结果集都很直接。连接串的写法是整个项目最先要确认正确的地方,参数不对会直接抛异常。
import pyodbc conn_str = ( "DRIVER={ODBC Driver 17 for SQL Server};" "SERVER=localhost\\SQLEXPRESS;" "DATABASE=LibraryDB;" "Trusted_Connection=yes;" "TrustServerCertificate=yes;" ) conn = pyodbc.connect(conn_str) cur = conn.cursor()参数逐个说明:DRIVER要匹配你机器上实际安装的 ODBC 驱动名,常见的有ODBC Driver 17 for SQL Server、ODBC Driver 18 for SQL Server、SQL Server Native Client 11.0三种,装哪个就用哪个,版本不匹配连接串必挂。SERVER写法有讲究,默认实例直接写localhost,命名实例要写localhost\\SQLEXPRESS这种双反斜杠格式。Trusted_Connection=yes表示用 Windows 身份验证,如果你的课设要求 SQL Server 账号登录,改成UID=sa;PWD=你的密码即可。TrustServerCertificate=yes是用来跳过本地开发时 SSL 证书校验的,不加它会在某些版本的驱动下报加密错误。
提示:首次连接报错时,先用 SQL Server Management Studio 确认能不能连上同一实例。SSMS 能连而 pyodbc 不能,问题九成在驱动名或连接串参数上。
4.3 建库建表与初始数据:一条龙跑通
数据库连接串指向 LibraryDB,但这个库需要先建好。项目里一般会附带 sql 脚本,如果没有,按下面的核心结构手动建也很快:
CREATE DATABASE LibraryDB; GO USE LibraryDB; CREATE TABLE users ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password NVARCHAR(64) NOT NULL, real_name NVARCHAR(50), role NVARCHAR(20) NOT NULL DEFAULT 'student', status INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE books ( book_id INT IDENTITY(1,1) PRIMARY KEY, isbn NVARCHAR(20), title NVARCHAR(100) NOT NULL, author NVARCHAR(50), publisher NVARCHAR(50), category NVARCHAR(50), total_count INT NOT NULL DEFAULT 0, available_count INT NOT NULL DEFAULT 0, status INT NOT NULL DEFAULT 1 ); CREATE TABLE borrow_records ( record_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL REFERENCES users(user_id), book_id INT NOT NULL REFERENCES books(book_id), borrow_date DATETIME NOT NULL DEFAULT GETDATE(), due_date DATETIME NOT NULL, return_date DATETIME, renew_count INT NOT NULL DEFAULT 0, operator_id INT ); CREATE TABLE operation_logs ( log_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT, action NVARCHAR(50), detail NVARCHAR(200), created_at DATETIME NOT NULL DEFAULT GETDATE() ); GO -- 初始化超级管理员,密码为 admin 的 SHA256 哈希 INSERT INTO users(username, password, real_name, role) VALUES ('admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', '超级管理员', 'super_admin');所有字符串字段都用 NVARCHAR,这是配合 SQL Server 中文排序规则的标准做法,以后插入中文不会乱码。ID 列统一 IDENTITY 自增,省去手动生成主键的麻烦。初始化那条 INSERT 里的哈希值是admin的 SHA256 结果,登录时直接用 admin / admin 就能进系统。到这里环境已经齐了,启动整个项目只需要一条命令:
python app.pyFlask 默认监听 127.0.0.1:5000,浏览器打开http://127.0.0.1:5000就能看到登录页。如果 5000 端口被占用,在 app.py 底部的app.run()里把端口改成 5001 或其他值。
5. 实战避坑:SQL Server 对接时最典型的四个翻车现场
5.1 SSL 加密连接报错
现象:pyodbc.connect 执行时抛出“驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接”,程序直接中断。
原因:SQL Server 实例开了强制加密,或者 ODBC 驱动版本太旧。旧版 SQL Server Native Client 对证书校验的处理和 SQL Server 新实例之间经常出现握手失败。很多电脑里装的是旧驱动,但数据库却是 2019 或 2022,两边协议对不上。
解决:分两步。第一步在连接串末尾追加TrustServerCertificate=yes;,跳过本地开发环境的证书校验。第二步如果还报错,去安装最新 ODBC Driver 18 for SQL Server,并把连接串 DRIVER 改成{ODBC Driver 18 for SQL Server}。这个驱动在加密参数上做得更灵活,默认也能自动兼容。
5.2 中文乱码
现象:往 books 表插入“计算机科学”,查询出来是“????????”,页面上也显示一堆问号。
原因:典型的字符集三件套问题。SQL Server 实例安装时选了英文或默认排序规则,或者建表时用了 VARCHAR 而没选 NVARCHAR,再或者 HTML 模板里忘记声明utf-8。这三个里面任意一个不满足,中文就会出问题。
解决:最省事的路径是安装 SQL Server 时排序规则选Chinese_PRC_CI_AS,能选中文排序规则就一劳永逸。建表时所有字符串字段用 NVARCHAR,这一点上面的建表脚本已经做了。最后页面模板的<head>里加<meta charset="utf-8">。如果数据已经乱掉了,先清空该表重新插入,而不是在 Python 代码里做编码转换,那种补丁方法治标不治本。
5.3 数据库连接不上
现象:连接串写的是localhost\\SQLEXPRESS,报错信息是“Network-related instance-specific error”或者“Cannot open database LibraryDB”。
原因:大概率不是连接串写错,而是 SQL Server Browser 服务没启动。具名实例默认使用动态端口,客户端必须通过 Browser 服务才能找到对应端口。Browser 停掉以后,连接串写什么都连不上。另外 TCP/IP 协议在 SQL Server 配置管理器里被禁用也会导致同样问题。
解决:打开 SQL Server Configuration Manager,先看“SQL Server 网络配置”里 TCP/IP 是否已启用,默认可能没开。再看“SQL Server 服务”里 SQL Server Browser 的运行状态,没启动就手动启动并设为自动。最后在命令行执行netstat -an | findstr 1433确认端口在监听。如果用的是命名实例,强烈建议把 Browser 服务打开,这是最隐蔽的坑。
5.4 删除数据被外键拦截
现象:管理员想删掉某本图书,前端直接报外键冲突;或者有人为了绕过约束给外键加了 ON DELETE CASCADE,结果删掉一个用户,他的所有借阅历史也没了。
原因:borrow_records 表用 user_id 和 book_id 引用了 users 和 books,参照完整性在这里是起作用的。直接 DELETE 根本不可能成功,因为历史借阅记录还引用着这条书或这位读者。但加级联删除是更坏的做法,日志全丢,答辩时老师问一句“借阅历史为什么空了”就答不上来。
解决:不物理删除,只做逻辑下架。图书用 status 字段从 1 改成 0,用户用 status 从 1 改成 0,哪怕有未还记录也能停用,只是阻止新的借阅行为。历史借阅记录永远保留,这也是课设答辩里能拿出来讲的设计决策。如果有同学在代码里写了 CASCADE,趁早换掉。
6. 验收与答辩:把课程设计做成能演示的项目
6.1 答辩前按这张清单走一遍
项目能不能过,看演示时顺不顺。我每次做完课设都会列一张验收表,按角色把主流程跑通再交给老师看:
| 操作角色 | 操作步骤 | 预期结果 |
|---|---|---|
| 学生/老师 | 登录错误密码一次 | 页面提示错误,不进入系统 |
| 学生/老师 | 正确登录后进入 dashboard | 看到可借图书列表 |
| 学生/老师 | 借一本在架图书 | 库存减 1,生成借阅记录 |
| 学生/老师 | 对同书再次点击借阅 | 被拦截“重复借阅” |
| 学生/老师 | 还书 | 库存加 1,记录回填 return_date |
| 管理员 | 添加一本新书 | 图书列表立即出现 |
| 管理员 | 对有未还记录的书下架 | 操作失败并提示 |
| 超级管理员 | 查看操作日志 | 能看到以上所有操作记录 |
这张表本身就是答辩时的演示脚本,照它点一遍,全程不到五分钟。中间故意演示一次错误操作,反而能让老师看到系统的容错设计。
6.2 一个容易被忽略的亮点:把逾期计算统一交给 SQL Server
很多同学的逾期功能是在 Python 里写一堆 if 判断,把应还时间拉出来和datetime.now()比。这样不是不行,但答辩时容易被问到“时区怎么处理”“为什么数据库里显示的逾期状态和页面不一致”。我建议全部用 SQL Server 的日期函数算:
SELECT r.*, b.title, u.real_name, CASE WHEN r.return_date IS NULL AND r.due_date < GETDATE() THEN '已逾期' WHEN r.return_date IS NULL THEN '借出中' ELSE '已归还' END AS borrow_status FROM borrow_records r JOIN books b ON r.book_id = b.book_id JOIN users u ON r.user_id = u.user_idCASE 表达式把状态在数据库端算好,Python 侧直接取结果渲染到页面上。这样不管谁什么时候开系统,状态永远是数据库当前时间的准确反映。答辩时老师问逾期逻辑,你把这段 SQL 调出来讲,比讲十句 Python 判断更能说明你理解了“以数据库为准”的设计思想。
我当年第一次做这类系统时,把借阅状态记在 Python 的全局变量里,演示到一半刷新页面,所有状态全乱了,当场社死。从那以后我每次做课程设计,都强制要求自己把业务状态落成数据库字段,写操作全部走事务,最后一定先把验收清单过一遍再碰代码。希望帮到你。
本文还有配套的精品资源,点击获取