Flask+LayUI图书管理系统:真实项目级权限与状态机实现 简介本资源是一套面向高校计算机专业本科生的毕业设计级图书管理系统基于Python Flask后端与LayUI前端实现适用于图书馆、学校及社区图书室等场景的数字化管理需求助力学生快速掌握Web全栈开发与软件工程实践能力。压缩包共143个文件含21个JavaScript交互逻辑文件、16个HTML页面模板、6个CSS样式文件、2个核心Python后端脚本、1个SQLite数据库及2份完整毕业论文.docx总大小3.62MB结构清晰、注释详尽便于理解MVC分层与蓝图模块化设计。已有46人学习下载适合课程设计、毕设开题与技术复现。用户可直接运行系统获得含借阅管理、多条件检索、角色权限控制、逾期提醒等完整功能的可执行源码同步获取覆盖需求分析、数据库设计、接口实现与部署测试全过程的规范论文代码与文档高度对应显著降低二次开发与学习门槛。1. 这不是又一个“学生作业式”图书管理系统它用 Flask 做稳态后端、LayUI 做可维护前端源码里藏着真实项目才有的权限分层、借阅状态机和部署兜底逻辑你搜“图书管理系统 Python”十有八九点开的是 GitHub 上那个带README.md但app.py里写满print(欢迎来到图书系统)的仓库再点几个发现登录页用input()、数据库硬编码 SQLite 路径、管理员密码明文写在 config.py 里——这不是系统是教学演示幻灯片。而标题里这个「基于 Flask 与 LayUI 的图书管理系统」核心价值不在“能跑”而在它把一个真实中小型图书馆/校内资料室的业务闭环全链路落地了用户借书时自动校验逾期未还、管理员批量导入时跳过重复 ISBN、图书下架后借阅记录仍可追溯、LayUI 表单提交前做字段级防抖必填校验、Flask 启动时校验数据库连接并自动建表而非报错退出。它适合两类人一是想拿完整可运行项目练手的 Python 初学者源码结构清晰、注释密度高、无黑盒依赖二是需要快速交付轻量级内部管理工具的工程师已预留 Nginx Gunicorn 部署钩子、日志分级开关、SQL 注入防护中间件。别被“管理系统”四个字劝退——它没用 Vue 或 ReactLayUI 的模块化加载机制让页面首屏加载控制在 1.2s 内Flask 路由层用login_required和permission_required(book:edit)双重守门比手写 JWT 验证更贴近实际权限模型。2. 从零搭起最小可行骨架Flask 初始化 LayUI 静态资源接入 用户登录闭环2.1 初始化 Flask 应用并配置基础安全项新建项目目录后先执行以下命令创建虚拟环境并安装核心依赖python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install flask flask-sqlalchemy flask-login flask-wtf python-dotenv关键不是装什么而是为什么只装这些flask-sqlalchemy处理 ORM 映射避免手写 SQL 拼接flask-login管理会话生命周期比自己存 session 更防篡改flask-wtf提供 CSRF 保护和表单验证LayUI 提交表单时必须携带 token。python-dotenv用于隔离开发/生产配置——这是很多初学者翻车的第一步把SECRET_KEY devkey直接写进代码里。创建app.py内容如下from flask import Flask, render_template, request, redirect, url_for, flash from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required from werkzeug.security import generate_password_hash, check_password_hash import os # 1. 创建 Flask 实例 app Flask(__name__) app.config[SECRET_KEY] os.environ.get(SECRET_KEY, dev-secret-key-change-in-prod) app.config[SQLALCHEMY_DATABASE_URI] os.environ.get(DATABASE_URL, sqlite:///library.db) app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False # 2. 初始化扩展 db SQLAlchemy(app) login_manager LoginManager() login_manager.init_app(app) login_manager.login_view login # 未登录时跳转的视图函数名 # 3. 定义用户模型继承 UserMixin class User(UserMixin, db.Model): id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(120), nullableFalse) role db.Column(db.String(20), defaultuser) # admin or user def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) # 4. 用户加载回调LoginManager 必需 login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # 5. 创建数据库表仅开发环境首次运行 with app.app_context(): db.create_all() app.route(/) def index(): return render_template(index.html) app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form[username] password request.form[password] user User.query.filter_by(usernameusername).first() if user and user.check_password(password): login_user(user) return redirect(url_for(dashboard)) else: flash(用户名或密码错误, error) return render_template(login.html) app.route(/logout) login_required def logout(): logout_user() return redirect(url_for(index)) app.route(/dashboard) login_required def dashboard(): return render_template(dashboard.html) if __name__ __main__: app.run(debugTrue)提示app.run(debugTrue)仅限开发阶段。生产部署时必须禁用 debug 模式否则会暴露调试器控制台——这是 Flask 最经典的线上安全漏洞。这段代码完成了三件事数据库初始化db.create_all()在首次运行时生成library.db包含users表登录认证闭环load_user回调让 Flask-Login 知道如何从 session ID 查用户login_required装饰器自动拦截未登录请求密码安全存储generate_password_hash()使用 PBKDF2-SHA256默认迭代 260000 次比md5或sha1强得多——别信网上那些教“用 base64 加密密码”的教程那根本不是加密。2.2 LayUI 静态资源接入与登录页渲染LayUI 不是 npm 包它靠 CDN 或本地静态文件加载。本项目采用本地化部署避免 CDN 失效导致页面白屏下载 LayUI 2.8.18 官方压缩包 解压后将lay文件夹整个复制到项目根目录下的static/中路径static/lay/。创建templates/login.html!DOCTYPE html html head meta charsetutf-8 title图书管理系统 - 登录/title meta namerenderer contentwebkit meta http-equivX-UA-Compatible contentIEedge,chrome1 meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalable0 link relstylesheet href{{ url_for(static, filenamelay/css/layui.css) }} /head body classlayui-layout-body div classlayui-container stylemax-width: 400px; margin-top: 100px; div classlayui-card div classlayui-card-header管理员登录/div div classlayui-card-body form classlayui-form action{{ url_for(login) }} methodpost div classlayui-form-item label classlayui-form-label用户名/label div classlayui-input-block input typetext nameusername required lay-verifyrequired placeholder请输入用户名 autocompleteoff classlayui-input /div /div div classlayui-form-item label classlayui-form-label密码/label div classlayui-input-block input typepassword namepassword required lay-verifyrequired placeholder请输入密码 autocompleteoff classlayui-input /div /div div classlayui-form-item div classlayui-input-block button classlayui-btn layui-btn-fluid lay-submit lay-filterlogin立即登录/button /div /div /form /div /div /div script src{{ url_for(static, filenamelay/layui.js) }}/script script layui.use([form], function(){ var form layui.form; // 表单提交监听LayUI 自动处理验证 form.on(submit(login), function(data){ // 此处可加 loading 效果但注意LayUI 默认会阻止默认提交交给后端处理 return true; // 允许表单提交 }); }); /script /body /html关键细节说明url_for(static, filenamelay/css/layui.css)是 Flask 推荐的静态资源引用方式比硬编码/static/lay/css/layui.css更健壮支持 URL 前缀配置lay-verifyrequired触发 LayUI 内置验证比前端 JS 手写if (!input.value)更可靠支持邮箱、URL、数字等规则form methodpost对应 Flask 中app.route(/login, methods[GET, POST])GET 显示表单POST 处理提交layui.use([form])是 LayUI 模块化加载语法确保 form 模块就绪后再绑定事件——漏掉这句表单验证会失效。此时运行python app.py访问http://127.0.0.1:5000/login即可看到 LayUI 渲染的登录页。输入任意用户名密码如admin/123456后端会因密码哈希不匹配返回错误提示——这正是我们期望的验证逻辑在服务端前端只是友好界面。3. 图书核心模型与 CRUD 接口实现用 SQLAlchemy 定义实体关系用 LayUI Table 渲染列表3.1 设计图书、分类、借阅三张主表及关联逻辑真实图书管理不是“增删改查图书名”而是要支撑同一书名可能有多个版本ISBN 不同一本书可属多个分类如《算法导论》既是“计算机”也是“数学”借阅记录需关联用户、图书、借出时间、应还时间、实际归还时间分类支持树形结构“计算机 编程 Python”。因此数据库模型需满足Book表主键id、ISBN唯一、书名、作者、出版社、出版年份、库存数量、封面图片路径Category表主键id、名称、父级 IDparent_id支持无限级分类BookCategory关联表book_idcategory_id实现多对多BorrowRecord表user_id、book_id、borrow_time、due_time、return_time、statuspending/returned/overdue。在app.py中追加模型定义放在User类下方# 在 User 类定义之后、app.route 之前添加 class Category(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100), nullableFalse) parent_id db.Column(db.Integer, db.ForeignKey(category.id), nullableTrue) children db.relationship(Category, backrefdb.backref(parent, remote_side[id]), lazyTrue) class Book(db.Model): id db.Column(db.Integer, primary_keyTrue) isbn db.Column(db.String(17), uniqueTrue, nullableFalse) # 支持 ISBN-10/13 title db.Column(db.String(200), nullableFalse) author db.Column(db.String(100)) publisher db.Column(db.String(100)) publish_year db.Column(db.Integer) stock db.Column(db.Integer, default1) cover db.Column(db.String(200), defaultdefault.jpg) # 封面相对路径 class BookCategory(db.Model): __tablename__ book_category book_id db.Column(db.Integer, db.ForeignKey(book.id), primary_keyTrue) category_id db.Column(db.Integer, db.ForeignKey(category.id), primary_keyTrue) class BorrowRecord(db.Model): id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id), nullableFalse) book_id db.Column(db.Integer, db.ForeignKey(book.id), nullableFalse) borrow_time db.Column(db.DateTime, defaultdb.func.now()) due_time db.Column(db.DateTime, nullableFalse) # 应还时间 borrow_time 30 days return_time db.Column(db.DateTime, nullableTrue) status db.Column(db.String(20), defaultpending) # pending, returned, overdue user db.relationship(User, backrefdb.backref(borrow_records, lazyTrue)) book db.relationship(Book, backrefdb.backref(borrow_records, lazyTrue))注意Category的自关联设计parent_id指向自身是实现树形分类的关键。BookCategory使用联合主键而非自增 ID符合多对多关联表最佳实践。3.2 实现图书列表页LayUI Table Flask 分页接口创建templates/book_list.html!DOCTYPE html html head meta charsetutf-8 title图书列表/title link relstylesheet href{{ url_for(static, filenamelay/css/layui.css) }} /head body classlayui-layout-body div classlayui-layout layui-layout-admin div classlayui-header div classlayui-logo图书管理系统/div ul classlayui-nav layui-layout-left li classlayui-nav-itema href{{ url_for(dashboard) }}首页/a/li li classlayui-nav-item layui-thisa href{{ url_for(book_list) }}图书管理/a/li /ul ul classlayui-nav layui-layout-right li classlayui-nav-itema hrefjavascript:;{{ current_user.username }}/a/li li classlayui-nav-itema href{{ url_for(logout) }}退出/a/li /ul /div div classlayui-body div stylepadding: 15px; div classlayui-btn-group button classlayui-btn layui-btn-sm onclickopenAddBookModal()添加图书/button button classlayui-btn layui-btn-sm layui-btn-primary onclickrefreshTable()刷新/button /div table classlayui-table lay-data{id:bookTable, page:true, limit:10} lay-filterbookTableFilter thead tr th lay-data{field:id, width:80, sort: true}ID/th th lay-data{field:isbn, width:150}ISBN/th th lay-data{field:title, minWidth:200}书名/th th lay-data{field:author, width:120}作者/th th lay-data{field:publisher, width:120}出版社/th th lay-data{field:stock, width:100}库存/th th lay-data{field:operate, width:180, toolbar:#bookBar}操作/th /tr /thead /table script typetext/html idbookBar a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-xs layui-btn-danger lay-eventdel删除/a /script /div /div /div script src{{ url_for(static, filenamelay/layui.js) }}/script script layui.use([table, layer, jquery], function(){ var table layui.table; var layer layui.layer; var $ layui.jquery; // 渲染表格 table.render({ elem: #bookTable, url: /api/books, // 对应 Flask API page: true, cols: [[ {field:id, width:80, sort: true}, {field:isbn, width:150}, {field:title, minWidth:200}, {field:author, width:120}, {field:publisher, width:120}, {field:stock, width:100}, {fixed: right, width:180, align:center, toolbar: #bookBar} ]] }); // 监听工具条事件 table.on(tool(bookTableFilter), function(obj){ var data obj.data; if(obj.event del){ layer.confirm(确定删除该图书, function(index){ $.post(/api/books/ data.id /delete, function(res){ if(res.code 0){ layer.msg(删除成功); obj.del(); } else { layer.msg(删除失败 res.msg); } }); layer.close(index); }); } else if(obj.event edit){ openEditBookModal(data.id); } }); // 刷新表格 window.refreshTable function(){ table.reload(bookTable, { page: {curr: 1} }); }; // 添加/编辑弹窗简化版实际项目中应抽离为独立 HTML window.openAddBookModal function(){ layer.open({ type: 2, title: 添加图书, shadeClose: true, shade: 0.3, area: [600px, 500px], content: /book/add // 对应 Flask 路由 }); }; window.openEditBookModal function(id){ layer.open({ type: 2, title: 编辑图书, shadeClose: true, shade: 0.3, area: [600px, 500px], content: /book/edit/ id }); }; }); /script /body /html对应 Flask 路由追加到app.py中# 在 app.route(/dashboard) 下方添加 app.route(/book/list) login_required def book_list(): return render_template(book_list.html) app.route(/api/books) login_required def api_books(): # 获取分页参数 page int(request.args.get(page, 1)) limit int(request.args.get(limit, 10)) # 查询图书数据含分类名称拼接 query Book.query.outerjoin(BookCategory).outerjoin(Category) pagination query.paginate(pagepage, per_pagelimit, error_outFalse) # 构造返回数据LayUI Table 要求格式 data [] for book in pagination.items: # 获取分类名称简化只取第一个分类 category_name if book.categories: category_name book.categories[0].name data.append({ id: book.id, isbn: book.isbn, title: book.title, author: book.author or , publisher: book.publisher or , stock: book.stock, category: category_name }) return { code: 0, msg: , count: pagination.total, data: data } app.route(/api/books/int:book_id/delete, methods[POST]) login_required def delete_book(book_id): book Book.query.get_or_404(book_id) # 检查是否有未归还借阅记录 active_borrows BorrowRecord.query.filter_by(book_idbook_id, statuspending).count() if active_borrows 0: return {code: 1, msg: 该图书存在未归还借阅记录无法删除} db.session.delete(book) db.session.commit() return {code: 0, msg: 删除成功}逻辑说明api_books()返回 JSON 数据给 LayUI Tablecount字段用于分页总数计算outerjoin确保即使图书未分配分类也能显示删除前检查BorrowRecord表中statuspending的记录这是业务强约束——不能让已借出的书被删掉login_required保证所有 API 都受登录保护无需在每个路由里重复写验证逻辑。此时访问/book/list即可看到 LayUI 渲染的图书列表支持分页、搜索LayUI Table 自带、删除确认。这才是真实可用的管理界面不是静态 HTML 演示。4. 权限控制与借阅状态机用角色字段驱动 LayUI 按钮显隐用状态流转防止业务错乱4.1 基于角色字段的细粒度权限控制LayUI 本身不提供权限控制但可通过 Flask 模板变量动态渲染按钮。在book_list.html的表格操作列中将a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-xs layui-btn-danger lay-eventdel删除/a替换为{{# if (d.status pending) { }} a classlayui-btn layui-btn-xs layui-btn-danger lay-eventdel删除/a {{# } else if (d.status returned) { }} a classlayui-btn layui-btn-xs layui-btn-disabled已归还/a {{# } }}但这只是前端掩耳盗铃。真正的权限控制必须在后端路由层完成。修改delete_book路由app.route(/api/books/int:book_id/delete, methods[POST]) login_required def delete_book(book_id): book Book.query.get_or_404(book_id) # 权限校验只有管理员可删除 if current_user.role ! admin: return {code: 1, msg: 权限不足仅管理员可执行此操作} # 业务校验检查未归还记录 active_borrows BorrowRecord.query.filter_by(book_idbook_id, statuspending).count() if active_borrows 0: return {code: 1, msg: 该图书存在未归还借阅记录无法删除} db.session.delete(book) db.session.commit() return {code: 0, msg: 删除成功}同理在book_list.html的顶部按钮组中用 Jinja2 控制显隐{{# if current_user.role admin #}} button classlayui-btn layui-btn-sm onclickopenAddBookModal()添加图书/button {{# end #}} button classlayui-btn layui-btn-sm layui-btn-primary onclickrefreshTable()刷新/button为什么不用 Flask-Principal 或 Flask-Security因为本项目角色只有admin/user两级且权限粒度固定管理员可增删改普通用户只能借阅。引入重型权限库反而增加学习成本和部署复杂度。current_user.role字段配合login_required已足够稳健——简单场景用简单方案是工程落地的第一原则。4.2 借阅状态机用枚举值 状态转移规则保障业务一致性借阅不是“点一下借”就完事它涉及状态变更、时间计算、库存扣减、通知触发。定义BorrowRecord.status的合法值为pending已借出未归还returned已归还overdue已逾期需定时任务扫描更新。在app.py中添加借阅路由app.route(/api/borrow, methods[POST]) login_required def borrow_book(): book_id request.json.get(book_id) book Book.query.get_or_404(book_id) # 库存检查 if book.stock 0: return {code: 1, msg: 该图书暂无库存} # 创建借阅记录 record BorrowRecord( user_idcurrent_user.id, book_idbook_id, due_timedb.func.date_add(db.func.now(), db.text(INTERVAL 30 DAY)) # MySQL 语法 ) db.session.add(record) # 扣减库存 book.stock - 1 db.session.commit() return {code: 0, msg: 借阅成功应还日期 record.due_time.strftime(%Y-%m-%d)} app.route(/api/return, methods[POST]) login_required def return_book(): record_id request.json.get(record_id) record BorrowRecord.query.get_or_404(record_id) # 仅允许本人归还自己的记录 if record.user_id ! current_user.id: return {code: 1, msg: 无权操作他人借阅记录} # 状态检查只能归还未归还的记录 if record.status ! pending: return {code: 1, msg: 该记录状态异常无法归还} # 更新状态和时间 record.status returned record.return_time db.func.now() # 库存回补 book Book.query.get(record.book_id) book.stock 1 db.session.commit() return {code: 0, msg: 归还成功}关键设计点due_time使用数据库函数date_add(now(), INTERVAL 30 DAY)避免 Python 时区问题归还时校验user_id防止越权操作状态变更与库存操作在同一个事务中完成保证原子性——这是状态机落地的核心。5. 部署避坑指南Gunicorn Nginx 组合下 Flask 静态资源 404、LayUI 模块加载失败、数据库连接池耗尽的 5 个血泪经验5.1 现象LayUI 的layui.use([form])报错Cannot read property use of undefined原因Nginx 未正确代理静态资源或 Flaskstatic_url_path配置与 Nginx location 冲突。解决确保 Nginx 配置中location /static/指向项目static/目录Flask 中不要修改static_url_path保持默认/static检查static/lay/layui.js文件权限是否为644Nginx worker 进程能否读取。5.2 现象Flask 启动后访问首页空白浏览器控制台报Failed to load resource: the server responded with a status of 404 ()指向layui.css原因LayUI 文件未按官方目录结构放置或url_for(static, ...)生成路径与实际文件路径不一致。解决严格按static/lay/css/layui.css、static/lay/layui.js结构存放在模板中使用{{ url_for(static, filenamelay/css/layui.css) }}而非{{ url_for(static, filenamecss/layui.css) }}运行flask run时开启 debug查看 Flask 日志中静态文件请求路径是否匹配。5.3 现象高并发下数据库连接超时报错sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached原因SQLAlchemy 默认连接池太小且未配置 recycle 参数连接在 MySQL 断连后未及时清理。解决在app.config中增加app.config[SQLALCHEMY_POOL_SIZE] 10 app.config[SQLALCHEMY_MAX_OVERFLOW] 20 app.config[SQLALCHEMY_POOL_RECYCLE] 3600 # 1小时回收空闲连接 app.config[SQLALCHEMY_POOL_PRE_PING] True # 每次获取连接前 ping 一次5.4 现象Gunicorn 启动后LayUI 表单提交 400 Bad RequestCSRF token 验证失败原因Gunicorn 多 worker 下SECRET_KEY未统一导致不同 worker 生成的 token 无法互相验证。解决将SECRET_KEY从os.environ.get(SECRET_KEY, dev-key)改为从环境变量强制读取启动前设置export SECRET_KEYyour-32-char-random-string-here gunicorn -w 4 -b 0.0.0.0:8000 app:app或在app.py开头硬编码仅限测试app.config[SECRET_KEY] a-very-secure-and-long-secret-key。5.5 现象Nginx 反向代理后LayUI 的layer.open({type:2})弹窗 iframe 地址变成http://localhost:5000/...而非域名原因Flask 未正确识别反向代理的 Host 头url_for生成绝对 URL 时用了localhost:5000。解决在 Nginx 配置中添加location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }在 Flask 中启用ProxyFixfrom werkzeug.middleware.proxy_fix import ProxyFix app.wsgi_app ProxyFix(app.wsgi_app, x_for1, x_proto1, x_host1, x_port1, x_prefix1)6. 让系统真正“活”起来用定时任务自动标记逾期、用日志追踪借阅行为、用 Docker 封装一键部署6.1 用 APScheduler 实现每日凌晨扫描逾期记录BorrowRecord.status为pending的记录若当前时间超过due_time应自动设为overdue。手动执行不现实必须自动化。安装 APSchedulerpip install apscheduler在app.py底部追加定时任务注意必须在if __name__ __main__:之外from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from datetime import datetime def mark_overdue_records(): 标记所有已逾期但状态仍为 pending 的记录 now datetime.now() overdue_records BorrowRecord.query.filter( BorrowRecord.status pending, BorrowRecord.due_time now ).all() for record in overdue_records: record.status overdue db.session.commit() print(f[{datetime.now()}] 标记 {len(overdue_records)} 条逾期记录) # 初始化调度器仅在主线程启动 scheduler BackgroundScheduler() scheduler.add_job( funcmark_overdue_records, triggerCronTrigger(hour0, minute0), # 每天凌晨 0:00 执行 idmark_overdue, name标记逾期借阅, replace_existingTrue ) # 启动调度器Flask 启动时 app.before_first_request def start_scheduler(): if not scheduler.running: scheduler.start() print(定时任务已启动) # Flask 退出时关闭调度器可选 import atexit atexit.register(lambda: scheduler.shutdown())为什么用BackgroundScheduler而非BlockingScheduler因为BlockingScheduler会阻塞 Flask 主线程导致 Web 请求无法响应。BackgroundScheduler在后台线程运行与 Flask 并行——这是生产环境唯一可行的方案。6.2 用结构化日志记录关键操作便于审计与排查Flask 默认日志太简陋。改用structlog记录借阅、归还、删除等敏感操作pip install structlog在app.py开头添加日志配置import structlog import logging from datetime import datetime # 配置 structlog structlog.configure( processors[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmtiso), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.JSONRenderer() # 输出为 JSON方便 ELK 收集 ], context_classdict, logger_factorystructlog.stdlib.LoggerFactory(), wrapper_classstructlog.stdlib.BoundLogger, cache_logger_on_first_useTrue, ) logger structlog.get_logger() # 在 borrow_book 路由中添加日志 app.route(/api/borrow, methods[POST]) login_required def borrow_book(): book_id request.json.get(book_id) book Book.query.get_or_404(book_id) if book.stock 0: logger.warn(borrow_failed_no_stock, user_idcurrent_user.id, book_idbook_id) return {code: 1, msg: 该图书暂无库存} record BorrowRecord( user_idcurrent_user.id, p a hrefhttps://download.csdn.net/download/ztt123654/92433923 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p