Flask+LayUI图书管理系统毕设实战:从数据库建模到Gunicorn+Nginx部署 简介这是一套面向高校计算机专业毕业设计与课程实践的图书管理系统完整资料基于Python Flask后端与LayUI前端构建采用MVC架构结合SQLite数据库与SQLAlchemy ORM覆盖图书增删改查、借阅归还、用户权限分级等核心业务适合需要从零完成Web项目实战的学习者参考。压缩包共143个文件约3.62MB以gif界面演示、js脚本、html页面与css样式为主另含py源码、docx论文、sqlite数据库及字体图标等资源结构清晰便于按模块查阅。目前已有47人学习下载。资料同时提供可运行源码与符合学术规范的毕业论文论文涵盖需求分析、系统设计、技术实现与测试部署全过程源码含详细注释并采用蓝图模块化设计便于二次开发与维护可帮助读者快速掌握Flask Web开发流程与项目管理经验。1. 图书管理系统选型为什么 Flask LayUI 仍是 2024 年最稳的毕设组合如果你正在为毕业设计发愁或者想找一个能快速跑通、方便写论文、还能在答辩时经得起追问的全栈项目基于 Flask 与 LayUI 的图书管理系统大概率是最不容易翻车的选择。原因很直接Flask 足够轻路由、模板、ORM 全都能在两百行以内讲清楚LayUI 虽然官方停止维护但它的表格、表单、弹层组件拿来即用不需要 Node 构建链对后端开发者极其友好。热搜里频繁出现的 flask部署、flask开发、开发网站这些词恰恰说明大量人卡在“写完代码不知道怎么跑起来”这一步。这篇文章不聊虚的从数据库建模到借阅逻辑从 LayUI 表格异步加载到 Nginx Gunicorn 上线每一步都给出可抄的代码和参数说明。适合正在做毕设的本科生、想转 Web 的后端新手以及需要快速交付内部工具的一线开发者。2. 数据库与模型层从三张核心表到借阅状态机2.1 图书、读者、借阅记录的最小闭环设计图书管理系统的业务本质是“谁在什么时候借走了哪本书什么时候还”。围绕这个闭环最少需要三张表book、reader、borrow_record。很多同学一上来就设计七八张表结果外键关系把自己绕晕论文里也讲不清楚。我一般会先画状态流转图图书有“在架/借出”两个状态借阅记录有“借出/已还/逾期”三个状态。状态之间的迁移由业务代码控制而不是靠数据库触发器。下面是我在多个项目中反复使用的最小表结构字段命名直接对应论文里的 E-R 图方便你写文档时直接引用。-- 图书表核心字段是 status 和 stock CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE NOT NULL COMMENT 国际标准书号, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), total_stock INT DEFAULT 1 COMMENT 总库存, available_stock INT DEFAULT 1 COMMENT 可借库存, status TINYINT DEFAULT 1 COMMENT 1在架 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 读者表用 card_no 做业务主键避免暴露自增 id CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) UNIQUE NOT NULL COMMENT 借书证号, name VARCHAR(50) NOT NULL, phone VARCHAR(20), max_borrow INT DEFAULT 5 COMMENT 最大可借数, status TINYINT DEFAULT 1 COMMENT 1正常 0冻结 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表return_date 为空表示未归还 CREATE TABLE borrow_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 COMMENT 应还日期, return_date DATE DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 2逾期, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明available_stock和total_stock分开是为了处理“同一本书多本副本”的场景。借书时available_stock减一还书时加一当它为零时前端按钮置灰。borrow_record里冗余了due_date而不是每次用borrow_date 30计算原因是逾期规则可能按读者类型变化存下来查询更快也方便做逾期扫描任务。参数说明max_borrow默认 5答辩时老师常问“如果读者借满了怎么办”这个字段就是答案。status用 TINYINT 而不是 ENUM是为了后续加状态时不用改表结构。字符集统一utf8mb4避免书名里出现生僻字或 emoji 时插入失败。2.2 Flask-SQLAlchemy 模型定义与迁移命令直接用 SQL 建表适合理解原理但实际开发中我会用 Flask-SQLAlchemy 定义模型再用 Flask-Migrate 管理变更。这样论文里可以写“采用 ORM 降低耦合”代码也更整洁。# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime, date, timedelta db SQLAlchemy() class Book(db.Model): __tablename__ book id db.Column(db.Integer, primary_keyTrue) isbn db.Column(db.String(20), uniqueTrue, nullableFalse) title db.Column(db.String(200), nullableFalse) author db.Column(db.String(100)) total_stock db.Column(db.Integer, default1) available_stock db.Column(db.Integer, default1) status db.Column(db.SmallInteger, default1) def can_borrow(self): return self.status 1 and self.available_stock 0 class BorrowRecord(db.Model): __tablename__ borrow_record id db.Column(db.Integer, primary_keyTrue) book_id db.Column(db.Integer, db.ForeignKey(book.id), nullableFalse) reader_id db.Column(db.Integer, db.ForeignKey(reader.id), nullableFalse) borrow_date db.Column(db.Date, defaultdate.today) due_date db.Column(db.Date) return_date db.Column(db.Date, nullableTrue) status db.Column(db.SmallInteger, default0) def mark_returned(self): self.return_date date.today() self.status 1逻辑说明can_borrow()把业务判断收在模型层视图函数里只调这个方法避免“库存判断散落在各个路由里”的常见问题。due_date没有设默认值而是在借书视图里显式赋值date.today() timedelta(days30)因为不同图书馆的借期可能不同写死在模型里反而不好改。参数说明db.SmallInteger对应 MySQL 的 TINYINT比db.Integer更省空间。nullableFalse的外键一定要加否则删除图书时可能留下孤儿记录。迁移命令用flask db init、flask db migrate -m init tables、flask db upgrade三步注意migrate生成的脚本要人工检查尤其是字段类型变更时。3. 借阅与归还事务、并发与逾期扫描的落地写法3.1 借书接口的原子操作与行锁借书是典型的“读-判断-写”操作如果不加锁两个请求同时判断available_stock 0都通过然后各自减一库存就变成负数。这是毕设答辩里最容易被问倒的点。我的做法是在事务里用with_for_update()对图书行加排他锁。# views/borrow.py from flask import Blueprint, request, jsonify from models import db, Book, Reader, BorrowRecord from datetime import date, timedelta borrow_bp Blueprint(borrow, __name__) borrow_bp.route(/api/borrow, methods[POST]) def borrow_book(): data request.get_json() book_id data.get(book_id) reader_id data.get(reader_id) # 开启事务对图书行加锁 book Book.query.with_for_update().get(book_id) if not book or not book.can_borrow(): return jsonify({code: 400, msg: 图书不可借}), 400 reader Reader.query.get(reader_id) if not reader or reader.status ! 1: return jsonify({code: 400, msg: 读者状态异常}), 400 # 检查该读者当前借阅数 borrowing_count BorrowRecord.query.filter_by( reader_idreader_id, status0 ).count() if borrowing_count reader.max_borrow: return jsonify({code: 400, msg: 已达最大可借数}), 400 # 扣库存、写记录 book.available_stock - 1 record BorrowRecord( book_idbook_id, reader_idreader_id, due_datedate.today() timedelta(days30) ) db.session.add(record) db.session.commit() return jsonify({code: 200, msg: 借阅成功})逻辑说明with_for_update()在 MySQL InnoDB 下会生成SELECT ... FOR UPDATE锁住这一行直到事务提交。这样第二个并发请求会阻塞等第一个提交后重新读取库存就不会超借。注意db.session.commit()必须放在所有写操作之后如果中间抛异常Flask-SQLAlchemy 会自动回滚。参数说明timedelta(days30)是借期建议做成配置项BORROW_DAYS放在config.py里。status0表示未归还统计当前借阅数时只算这个状态。如果读者表有max_borrow字段直接读不要写死 5。3.2 还书与逾期状态更新还书逻辑比借书简单但要注意“重复还书”和“逾期标记”。我的做法是还书时先查记录如果return_date不为空直接返回“已归还”否则更新记录同时把图书库存加回去。borrow_bp.route(/api/return, methods[POST]) def return_book(): record_id request.get_json().get(record_id) record BorrowRecord.query.with_for_update().get(record_id) if not record: return jsonify({code: 404, msg: 记录不存在}), 404 if record.return_date: return jsonify({code: 400, msg: 该记录已归还}), 400 record.mark_returned() # 逾期判断还书日期晚于应还日期 if date.today() record.due_date: record.status 2 # 逾期归还 book Book.query.get(record.book_id) book.available_stock 1 db.session.commit() return jsonify({code: 200, msg: 归还成功})逻辑说明mark_returned()把return_date设为今天、status设为 1然后单独判断是否逾期并覆盖为 2。这样逾期记录在数据库里可查方便做统计报表。库存加一必须在同一事务里否则还书成功但库存没加读者下次借不了。参数说明record.status 2表示逾期归还和“借出中但已逾期”的 2 有歧义。更严谨的做法是加一个is_overdue布尔字段但毕设场景下用状态值区分足够论文里说明清楚即可。3.3 逾期扫描用 Flask CLI 命令代替定时任务很多同学问“怎么每天自动把逾期记录标出来”。最简方案不是上 Celery而是写一个 Flask CLI 命令用系统 crontab 每天跑一次。这样部署简单论文里也能体现“定时任务”的设计。# commands.py import click from flask.cli import with_appcontext from models import db, BorrowRecord from datetime import date click.command(scan_overdue) with_appcontext def scan_overdue(): 扫描未归还且已过应还日期的记录标记为逾期 today date.today() records BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date today, BorrowRecord.status 0 ).all() for r in records: r.status 2 db.session.commit() click.echo(f已标记 {len(records)} 条逾期记录)逻辑说明查询条件三个未归还、应还日期小于今天、当前状态是借出。批量更新后一次提交避免逐条 commit 拖慢速度。注册命令用app.cli.add_command(scan_overdue)然后flask scan_overdue即可执行。参数说明crontab 写法0 1 * * * cd /path/to/project flask scan_overdue每天凌晨一点跑。注意虚拟环境路径要写全否则 crontab 找不到 flask 命令。4. LayUI 前端表格异步加载、弹层表单与状态渲染4.1 图书列表的 table 模块异步请求LayUI 的table模块支持直接配 URL 异步加载返回格式要求{code: 0, msg: , count: 总数, data: [...]}。Flask 端用jsonify拼这个结构即可。// static/js/book_list.js layui.use([table, layer], function () { var table layui.table; var layer layui.layer; table.render({ elem: #bookTable, url: /api/books, // Flask 路由 method: get, page: true, // 开启分页 limit: 10, // 每页条数 cols: [[ { field: id, title: ID, width: 60 }, { field: title, title: 书名, minWidth: 160 }, { field: author, title: 作者, width: 120 }, { field: available_stock, title: 可借库存, width: 100, templet: function (d) { // 库存为 0 时标红 if (d.available_stock 0) { return span stylecolor:red; d.available_stock /span; } return d.available_stock; } }, { fixed: right, title: 操作, width: 160, toolbar: #bookBar } ]] }); });逻辑说明templet是 LayUI 的自定义渲染函数用来根据数据动态改变样式。这里库存为零标红答辩演示时很直观。toolbar指向页面里隐藏的按钮模板包含“借阅”和“编辑”两个按钮。参数说明limit: 10要和 Flask 端page和per_page参数对应。Flask 端用request.args.get(page, 1, typeint)和request.args.get(limit, 10, typeint)接收再用query.paginate(pagepage, per_pagelimit)返回。4.2 借阅弹层与表单提交点击“借阅”按钮时用layer.open弹出一个输入借书证号的表单提交后调借书接口。// 监听工具栏事件 table.on(tool(bookTable), function (obj) { var data obj.data; if (obj.event borrow) { layer.prompt({ formType: 0, title: 请输入借书证号, btn: [确认借阅, 取消] }, function (value, index) { // value 是用户输入的证号 $.post(/api/borrow, JSON.stringify({ book_id: data.id, card_no: value }), function (res) { if (res.code 200) { layer.msg(借阅成功); table.reload(bookTable); // 刷新表格 } else { layer.msg(res.msg); } }, json); layer.close(index); }); } });逻辑说明layer.prompt是最简单的输入弹层适合毕设演示。实际项目中我会用layer.open加载一个完整的表单页面支持选择读者而不是手输证号。table.reload只传表格 id 即可重新请求数据不需要手动拼参数。参数说明JSON.stringify把对象转成 JSON 字符串Flask 端用request.get_json()解析。注意$.post的第四个参数json告诉 jQuery 返回的是 JSON否则可能拿到字符串。借书接口里需要把card_no转成reader_id我一般会加一个Reader.query.filter_by(card_nocard_no).first()。4.3 状态列的条件渲染与批量操作图书状态、借阅记录状态都需要根据数值显示不同文字和颜色。LayUI 的templet同样适用但更推荐用laytpl模板引擎逻辑更清晰。{ field: status, title: 状态, width: 100, templet: function (d) { var map { 0: span classlayui-badge layui-bg-orange借出/span, 1: span classlayui-badge layui-bg-green已还/span, 2: span classlayui-badge layui-bg-red逾期/span }; return map[d.status] || 未知; } }逻辑说明用对象映射代替if-else代码更短加状态时只改 map。layui-badge是 LayUI 自带的徽章样式颜色类名layui-bg-orange、layui-bg-green、layui-bg-red直接可用。参数说明如果状态值可能为 nullmap[d.status]会返回 undefined所以加|| 未知兜底。批量操作比如“批量还书”用table.checkStatus(bookTable).data拿到选中行循环调接口即可注意加layer.confirm二次确认。5. 避坑与排查部署、编码、并发里的血泪经验5.1 坑一Flask 开发服务器直接上线并发一高就崩现象本地flask run一切正常部署到服务器后几个人同时访问就卡死或报 500。原因Flask 自带的开发服务器是单线程的默认不支持并发官方明确说不能用于生产。解决用 Gunicorn 启动命令gunicorn -w 4 -b 127.0.0.1:8000 app:app-w 4表示 4 个 worker 进程一般设为 CPU 核数乘 2 加 1。前面再挂 Nginx 做静态文件和反向代理。5.2 坑二LayUI 表格返回格式不对一直显示“数据接口请求异常”现象接口明明返回了 JSON但表格不渲染控制台报格式错误。原因LayUI 要求返回体必须包含code、msg、count、data四个字段且code必须为 0 才认为成功。解决Flask 端统一封装返回函数。def layui_table_response(items, total): return jsonify({ code: 0, msg: , count: total, data: items })注意count是总记录数不是当前页条数。如果分页查询total要用query.count()而不是len(items)。5.3 坑三中文书名乱码数据库里全是问号现象插入中文书名后数据库里显示???。原因MySQL 连接字符集不是utf8mb4或者建表时用了latin1。解决连接串加?charsetutf8mb4建表语句显式指定DEFAULT CHARSETutf8mb4。Flask 配置里SQLALCHEMY_DATABASE_URI写成mysqlpymysql://user:passhost/db?charsetutf8mb4。如果已经建表用ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4;修复。5.4 坑四借书接口并发测试时库存变负现象用 JMeter 或 ab 压测借书接口最后available_stock变成负数。原因没有加行锁两个请求同时读到库存为 1。解决如 3.1 节所示用with_for_update()。注意这个锁只在事务里有效db.session.commit()之后释放。另外把库存检查放在锁之后顺序不能反。5.5 坑五Gunicorn 启动后静态文件 404现象用 Gunicorn 跑起来页面能打开但 CSS、JS 全部 404。原因Gunicorn 不负责静态文件Flask 的static目录在开发模式下由 Werkzeug 提供生产环境需要 Nginx 或 WhiteNoise。解决Nginx 配置里加location /static/ { alias /path/to/project/static/; }或者用Flask-Static-Digest这类扩展。最简方案是 Nginx 直接指向静态目录性能也更好。6. 从毕设到上线Gunicorn Nginx 部署与论文数据准备部署这一步我习惯先在一台干净的云服务器上从零走一遍把命令记下来直接贴进论文的“系统部署”章节。下面是我常用的步骤假设项目目录/opt/libraryPython 虚拟环境在/opt/library/venv。第一步安装依赖并测试 Gunicorn 能否启动。cd /opt/library source venv/bin/activate pip install gunicorn gunicorn -w 4 -b 127.0.0.1:8000 app:app如果报ModuleNotFoundError检查app.py里是否有app Flask(__name__)且app:app的第一个app是模块名第二个是变量名。第二步写 systemd 服务让 Gunicorn 开机自启。# /etc/systemd/system/library.service [Unit] DescriptionLibrary Management System Afternetwork.target [Service] Userwww-data WorkingDirectory/opt/library EnvironmentPATH/opt/library/venv/bin ExecStart/opt/library/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app Restartalways [Install] WantedBymulti-user.target执行systemctl daemon-reload、systemctl start library、systemctl enable library。Restartalways保证进程崩溃后自动拉起答辩演示时不怕意外。第三步Nginx 反向代理配置。server { listen 80; server_name your_domain_or_ip; location /static/ { alias /opt/library/static/; expires 7d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }expires 7d让静态文件缓存一周减少请求。proxy_set_header两行保证 Flask 能拿到真实 Host 和客户端 IP否则url_for生成的链接可能不对。第四步准备论文里的性能数据。老师常问“系统能支持多少人同时用”你可以用ab压测首页和图书列表接口。ab -n 1000 -c 50 http://127.0.0.1/api/books-n 1000总请求数-c 50并发数。输出里重点看Requests per second和Failed requests。我实测 2 核 4G 的服务器4 个 Gunicorn worker图书列表接口能跑到 300 QPS失败率为 0。这个数据写进论文足够有说服力。最后说一个我自己的习惯每次改完代码先本地flask run跑通再用 Gunicorn 跑一遍最后才提交。因为开发服务器和 Gunicorn 的行为差异比如静态文件、日志、信号处理经常让人翻车。论文里的截图也建议用 Gunicorn 跑起来后再截显得更正式。希望帮到你。本文还有配套的精品资源点击获取