
简介一套面向计算机专业课程设计与大作业场景的Python Flask图书管理系统项目依托Python3FlaskMySQL实现图书增删改查、读者管理和借阅记录等核心功能适合Flask初学者、毕业设计学生及需要快速搭建Web管理系统的开发者参考。资源共583个文件压缩包约15.77MB以443个Python源码文件为主同时包含HTML页面、CSS样式、SQL数据库脚本以及doc/docx格式的课程设计报告目录结构清晰便于按模块阅读与二次开发。项目附带的报告文档不仅涵盖系统总体设计、数据库表结构和功能模块划分还包含实现过程与设计思路既能作为课程设计报告模板也可帮助理解Flask与MySQL交互机制、模板渲染方式和基础SQL操作。目前已有224人学习下载对准备课程答辩或希望系统梳理Web开发流程的读者具有较高的参考价值。1. 用 Flask 写图书管理系统先摸清课程设计的评分点无论是课程设计还是大作业答辩评分从来不是看你功能堆了多少。功能完整度、代码结构可读性、报告文档能否解释设计决策这三项基本决定分数档位。用 Flask 做图书管理系统的优势在于它没有把路由、ORM、表单验证锁死成一套重型框架你可以只挑 Flask-SQLAlchemy 管持久化剩下逻辑全部用自己看得懂的代码来控制。下面按数据建模、核心业务实现、初始化排错、报告文档写作这条线展开覆盖图书从入库到借还再到统计的完整闭环。新手可以按命令逐步复现工作过几年的朋友也能直接在边界条件处理、统计口径、文档结构这些地方找到可搬运的结论。2. 图书管理系统数据建模与 Flask 工程目录2.1 读者、图书、借阅三张表如何撑起借阅闭环我见过不少课程设计把读者、图书、借阅记录全塞进一大张表里演示时能跑一旦写统计 SQL 就非常别扭。图书管理系统的最小业务闭环是读者能借几本、某本书当前是否可借、借出去之后什么时候该还。这三件事分别落在 reader、book、borrow 三张表上。用 SQLite 建表的话最小结构如下CREATE TABLE reader ( id INTEGER PRIMARY KEY AUTOINCREMENT, reader_no VARCHAR(20) UNIQUE NOT NULL, -- 学号/工号业务上唯一 name VARCHAR(50) NOT NULL, max_borrow INTEGER NOT NULL DEFAULT 5, -- 单个读者最多可借本数 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn VARCHAR(20) UNIQUE NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), category VARCHAR(30), total INTEGER NOT NULL DEFAULT 1, -- 采购总库存 available INTEGER NOT NULL DEFAULT 1, -- 当前可借数量 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE borrow ( id INTEGER PRIMARY KEY AUTOINCREMENT, reader_id INTEGER NOT NULL, book_id INTEGER NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, -- 应还日期用于逾期判断 return_date DATE, -- 实际归还日期NULL 表示未还 status VARCHAR(10) NOT NULL DEFAULT borrowed, FOREIGN KEY (reader_id) REFERENCES reader(id), FOREIGN KEY (book_id) REFERENCES book(id) ); CREATE INDEX idx_borrow_reader ON borrow(reader_id, status); CREATE INDEX idx_borrow_book ON borrow(book_id, status);有两个设计点值得在报告里单独讨论。第一book 表把 total 和 available 分开借出时 available 减一、归还时加一避免每次查询列表都要去 borrow 表做聚合列表页加载速度更快。第二borrow.status 只存 borrowed 和 returned 两个值逾期不设单独字段而是通过 due_date 与当前日期比较派生这样结构最简单展示逾期状态只需要一行条件判断。对应的 Flask 模型只保留字段声明建表交给db.create_all()# app/models/book.py from app import db 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(100), nullableFalse) author db.Column(db.String(50)) publisher db.Column(db.String(50)) category db.Column(db.String(30)) total db.Column(db.Integer, default1) available db.Column(db.Integer, default1)Reader 和 Borrow 模型按同样规则定义唯一需要留意的是 borrow 表的外键写法reader_id db.Column(db.Integer, db.ForeignKey(reader.id), nullableFalse)日期列用db.Date。提示课程设计不建议引入数据库触发器或自定义枚举类型。SQLite 原生不强制 CHECK 约束应用层判断足够写报告时还能突出「业务层状态校验」这个工作点。2.2 按功能拆分而不是按文件类型拆分目录初学阶段常见的做法是把所有路由写在一个 app.py 里模型、表单、视图堆在一起。图书管理系统至少要覆盖读者、图书、借还三个领域路由函数超过十个之后改一处 import 就得全局排查。我一般按功能域拆目录而不是按文件类型拆library/ ├── run.py # 启动入口create_app().run() ├── config.py # 配置类数据库路径、SECRET_KEY ├── requirements.txt # 依赖清单答辩现场重装用 ├── init_db.py # 建表 种子数据复位演示环境 └── app/ ├── __init__.py # 应用工厂初始化 db 并注册蓝图 ├── models/ │ ├── reader.py │ ├── book.py │ └── borrow.py ├── routes/ │ ├── book_routes.py │ ├── reader_routes.py │ └── borrow_routes.py ├── forms/ # Flask-WTF 表单类 └── templates/ ├── base.html ├── book/ ├── reader/ └── borrow/文件 / 目录职责config.py集中管理连接串、密钥等配置run.py启动入口负责建表并启动调试服务init_db.py写入种子数据答辩前一键复位app/routes/各功能域路由按蓝图注册app/models/数据模型与字段定义app/templates/页面模板对应各模块目录这样的拆分有直接好处路由文件之间只通过蓝图隔离不需要互相 import模型的改动只影响对应模型文件和借阅路由。对报告里的系统架构图来说这种结构也容易画成分层清晰的模块图评委读图成本低。2.3 用应用工厂和蓝图注册规避循环导入文件拆散之后第一个要解决的工程问题是循环导入。路由要 import db 和模型而 app/init.py 又要 import 路由才能注册蓝图。常见解法是应用工厂模式把会导致循环依赖的导入放进工厂函数内部# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from config import Config db SQLAlchemy() def create_app(): app Flask(__name__) app.config.from_object(Config) db.init_app(app) from .routes.book_routes import book_bp from .routes.reader_routes import reader_bp from .routes.borrow_routes import borrow_bp app.register_blueprint(book_bp, url_prefix/books) app.register_blueprint(reader_bp, url_prefix/readers) app.register_blueprint(borrow_bp, url_prefix/borrows) return app关键在于db.init_app(app)只完成绑定不触发模型导入路由的 import 放在 create_app 内部等 app 实例存在后再执行。模型文件里的from app import db在任何模块加载阶段都不会出问题因为 db 对象在模块顶层已经定义。如果把这些路由 import 写在 app/init.py 顶层加载顺序一变就会报 ImportError这是 Flask 课程设计里最高频的错误之一。url_prefix 给整个模块加了统一前缀例如 /books/add、/borrows/borrow。模板和测试里写 url_for 时端点名称自动变成book.add、borrow.borrow不会和读者模块的函数名冲突。这个工厂模式也顺带解决了测试配置问题create_app({TESTING: True})可以换成内存数据库报告测试章节可以写「系统支持配置注入」作为设计亮点。3. 图书管理系统的核心业务CRUD、借还与统计3.1 图书 CRUD 的完整闭环图书模块通常包含入库、编辑、下架删除和列表搜索。入库接口最需要注意的不是字段校验而是 ISBN 重复。ISBN 在表结构上加了唯一约束但直接让它抛异常会变成 500 错误演示时很尴尬。常见做法是业务代码先查一次再决定提示还是插入# app/routes/book_routes.py from flask import Blueprint, render_template, request, redirect, url_for, flash from app import db from app.models.book import Book from app.models.borrow import Borrow from app.forms.book_form import BookForm book_bp Blueprint(book, __name__) book_bp.route(/add, methods[GET, POST]) def add(): form BookForm() if form.validate_on_submit(): isbn form.isbn.data.strip() if Book.query.filter_by(isbnisbn).first(): flash(该 ISBN 已存在请去编辑页修改, warning) return redirect(url_for(book.add)) book Book( isbnisbn, titleform.title.data.strip(), authorform.author.data.strip(), publisherform.publisher.data.strip(), categoryform.category.data, totalform.total.data, availableform.total.data ) db.session.add(book) db.session.commit() flash(图书入库成功, success) return redirect(url_for(book.list)) return render_template(book/add.html, formform)form.validate_on_submit()一个方法做了三件事判断当前请求是 POST、校验 CSRF token、执行表单字段验证任一失败都返回 False 并重渲染模板显示错误消息。入参里的strip()去掉首尾空格避免「ISBN 带空格」造成重复数据。total 和 available 同时赋值为表单值保证新书入库时总数与可借数一致后续要做采购补货时只需单独修改 total 和 available不影响已有借阅记录。删除图书要谨慎。只要存在未归还的借阅记录物理删除 book 会让 borrow 表的外键悬空列表页 join 出现 None 对象。删除前要先过滤 status 为 borrowed 的借阅记录book_bp.route(/int:book_id/delete, methods[POST]) def delete(book_id): book Book.query.get_or_404(book_id) active Borrow.query.filter_by( book_idbook_id, statusborrowed ).first() if active: flash(存在未归还的借阅记录不能删除, warning) return redirect(url_for(book.list)) db.session.delete(book) db.session.commit() flash(图书已删除, success) return redirect(url_for(book.list))提示删除操作必须用 POST 而不是 GET。答辩演示时刷新页面会导致重复提交POST 加重定向能避开这个问题。列表搜索的常规操作是接收 q 参数用Book.title.contains(q)或Book.author.contains(q)过滤。SQLite 的 LIKE 对中文有效不需要为课程设计引入全文搜索引擎。3.2 借书与还书的边界条件处理借书接口是判断最多的模块。常见实现会漏掉「库存为 0」「读者超限」「重复借同一本书」三个条件中的任意一个逻辑上出现漏洞。我一般按这个顺序判断先查读者和图书是否存在再查库存再查读者当前在借数最后查同一本书是否重复借。# app/routes/borrow_routes.py from datetime import date, timedelta from flask import Blueprint, request, redirect, flash from app import db from app.models.reader import Reader from app.models.book import Book from app.models.borrow import Borrow borrow_bp Blueprint(borrow, __name__) borrow_bp.route(/borrow, methods[POST]) def borrow(): reader_id request.form.get(reader_id, typeint) book_id request.form.get(book_id, typeint) reader Reader.query.get(reader_id) book Book.query.get(book_id) if not reader or not book: flash(读者或图书不存在, error) return redirect(request.referrer) if book.available 0: flash(该图书可借库存为 0, error) return redirect(request.referrer) active_count Borrow.query.filter_by( reader_idreader_id, statusborrowed ).count() if active_count reader.max_borrow: flash(f已超过最大借阅数 {reader.max_borrow} 本, error) return redirect(request.referrer) exist Borrow.query.filter_by( reader_idreader_id, book_idbook_id, statusborrowed ).first() if exist: flash(该读者已有同一本书未归还, error) return redirect(request.referrer) record Borrow( reader_idreader_id, book_idbook_id, borrow_datedate.today(), due_datedate.today() timedelta(days30), statusborrowed ) book.available - 1 db.session.add(record) db.session.commit() flash(借书成功, success) return redirect(request.referrer)四个判断条件的先后顺序可以用一张表说清楚报告里直接照抄这张表也能加分顺序判断条件失败时的处理1读者、图书是否存在提示不存在并返回2book.available 是否大于 0提示可借库存为 03当前在借数是否小于 max_borrow提示达到借阅上限4同一本书是否已借未还提示先归还再借顺序有讲究对象不存在优先拦截避免后续空指针库存是资源维度额度是策略维度重复借阅是状态维度按这个顺序阅读代码的人能顺着业务优先级理解逻辑。request.form.get(reader_id, typeint)里的 typeint 是 Werkzeug 提供的类型转换拿不到或转换失败时返回 None省去手写 try/except。借期默认 30 天timedelta(days30)写在创建借阅记录的地方比在模型里写死更灵活。还书接口要处理重复归还borrow_bp.route(/return, methods[POST]) def return_book(): record_id request.form.get(record_id, typeint) record Borrow.query.get(record_id) if not record: flash(借阅记录不存在, error) return redirect(request.referrer) if record.status returned: flash(该记录已归还请勿重复操作, error) return redirect(request.referrer) record.status returned record.return_date date.today() book Book.query.get(record.book_id) if book: book.available 1 db.session.commit() flash(还书成功, success) return redirect(request.referrer)还书用 record_id 而不是 reader_id 和 book_id 组合判断是因为同一读者可能借过同一本书的多条历史记录用主键定位最准确。归还后恢复 available如果图书记录已被删除if book跳过恢复借阅历史仍然保留。3.3 统计报表与 Top 榜的查询实现统计功能是报告里的加分项评委常问「哪本书借得最多」。用 Flask-SQLAlchemy 的聚合查询就能实现不需要额外引入图表库# app/routes/borrow_routes.py 中继续添加 from sqlalchemy import func borrow_bp.route(/statistics) def statistics(): top_books db.session.query( Book.title, Book.isbn, func.count(Borrow.id).label(borrow_count) ).join(Borrow, Borrow.book_id Book.id) \ .group_by(Book.id) \ .order_by(db.desc(borrow_count)) \ .limit(10).all() active_readers db.session.query( Reader.name, Reader.reader_no, func.count(Borrow.id).label(active_count) ).join(Borrow, Borrow.reader_id Reader.id) \ .filter(Borrow.status borrowed) \ .group_by(Reader.id) \ .order_by(db.desc(active_count)) \ .limit(5).all() return render_template(borrow/statistics.html, top_bookstop_books, active_readersactive_readers)func.count(Borrow.id)生成 COUNT(borrow.id)配合 group_by(Book.id) 实现按书分组。这里故意区分两个口径top_books 统计历史累计借阅次数active_readers 统计当前在借数量用filter(status borrowed)过滤。两个指标含义不同报告里要写明你统计的是哪个口径答辩时就不怕被追问。模板里直接访问row.title、row.borrow_countFlask 查询返回的是 Row 对象属性访问即可。想做可视化就转成[{name: title, value: borrow_count}]传给前端 ECharts 柱状图代码量很小。4. 图书管理系统初始化、运行配置与常见排错4.1 最小可运行配置与实际执行命令把项目从一台机器搬到另一台机器最先卡住的往往不是逻辑而是配置与环境。如果本机还没装 Python安装那一步记得勾选 Add to PATH否则在终端里敲 python 会提示找不到命令。课程设计够用的最小 config.py 如下# config.py import os BASE_DIR os.path.abspath(os.path.dirname(__file__)) class Config: SQLALCHEMY_DATABASE_URI sqlite:/// os.path.join(BASE_DIR, library.db) SQLALCHEMY_TRACK_MODIFICATIONS False SECRET_KEY course-design-secret配置项作用课程设计建议SQLALCHEMY_DATABASE_URI数据库连接串用 sqlite:///library.db避免安装数据库服务SQLALCHEMY_TRACK_MODIFICATIONS是否追踪对象修改False关闭后避免警告与额外内存SECRET_KEYsession 签名与 CSRF 令牌答辩环境写死即可不必搞环境变量依赖安装用 venv 隔离环境避免把全局 Python 环境弄乱。无论你习惯在 PyCharm 里创建虚拟环境还是用 VS Code 的 Python 扩展选择解释器本质都是让编辑器使用项目内的 venv 路径python -m venv venv # Windows 下激活命令是 venv\Scripts\activate source venv/bin/activate pip install flask flask-sqlalchemy flask-wtf pip freeze requirements.txt启动入口 run.py 里一个常被忽略的细节是db.create_all()必须在应用上下文内执行# run.py from app import create_app, db app create_app() if __name__ __main__: with app.app_context(): db.create_all() app.run(host127.0.0.1, port5000, debugTrue)with app.app_context()让 db 能访问当前应用的配置和模型元数据少了它会出现 RuntimeError: Working outside of application context这是 Flask 新手最常遇到的报错。debugTrue 在演示时很有用浏览器页面能直接看到堆栈但 Debugger PIN 暴露也有安全隐患演示完关掉即可。4.2 初始化表结构与写入种子数据答辩现场往往需要快速展示数据所以 init_db.py 要写入可复现的种子数据。常见做法是先 create_all 建表再检查表为空才插入重复执行不会产生重复记录# init_db.py from app import create_app, db from app.models.reader import Reader from app.models.book import Book def seed(): if Reader.query.first() is None: db.session.add_all([ Reader(reader_no2024001, name张同学, max_borrow5), Reader(reader_no2024002, name李同学, max_borrow3), ]) if Book.query.first() is None: db.session.add_all([ # 演示用 ISBN答辩前替换成真实书号 Book(isbn978-7-111-00000-1, titleFlask Web 开发, authorMiguel Grinberg, publisher人民邮电, category编程, total3, available3), Book(isbn978-7-115-00000-2, titleSQL 必知必会, authorBen Forta, publisher人民邮电, category数据库, total2, available2), ]) db.session.commit() if __name__ __main__: app create_app() with app.app_context(): db.create_all() seed() print(数据库初始化完成)种子数据的关键是贴近现实学号、书名、出版社要真实否则列表页一眼假。init_db.py 和 run.py 分开的另一个好处是答辩前数据被改乱时直接执行python init_db.py就能恢复初始状态不需要手动删数据库文件。4.3 模型改了但数据库没变的处理开发期改表结构是常态今天给 reader 加 email明天给 book 加 rack_no。db.create_all()只建不存在的表不会给已有表加列于是你会看到「代码里写了 email 字段页面报 no such column」。最省事的处理是删除数据库文件重新初始化rm library.db python init_db.py python run.py代价是手工数据丢失所以坚持用 init_db.py 维护种子数据重建成本就很低。如果确实要保留数据可以引入 Flask-Migrate但课程设计报告不建议把迁移代码写进正文它带来大量与业务无关的脚本放在「进一步工作」小节里提一句反而显得你有工程意识。library.db 这类二进制文件应加入 .gitignore不提交到代码仓库合并冲突无法解决。答辩现场还有一个高频问题端口被占用。浏览器访问 127.0.0.1:5000 报错时先看终端是否已有旧进程在跑Unix 下lsof -i:5000Windows 下netstat -ano | findstr :5000找到 PID 结束掉再重启 run.py比换端口更稳。5. 报告文档的高分结构与答辩演示顺序5.1 报告按「需求 → 设计 → 实现 → 测试」闭环组织课程设计报告最容易写成功能说明书通篇「系统可以增删改查」。评委想看的是链路需求分析里提的每个功能点在系统设计中是否有对应表结构和接口在测试里是否有对应用例。建议目录是需求分析、总体设计架构图加 E-R 图、详细设计按借阅模块拆节、测试分析、总结与展望。E-R 图直接对应第二章的三张表和三个外键关系画图时把 max_borrow、available 这些约束字段标出来数据字典里注明字段类型、长度、是否为空。这套结构对课程设计和毕业设计都通用。5.2 测试用例表展示边界条件测试章节不要只写「功能正常」把借还书的边界条件写成用例表是工作量最直观的体现用例编号测试点操作步骤预期结果实际结果TC01重复 ISBN 入库添加已存在的 ISBN提示已存在不写入通过TC02库存为 0 借书对 available0 的书执行借出提示库存不足通过TC03超过最大借阅数max_borrow5 的读者连借 6 本第 6 本被拦截通过TC04重复借同一本未归还时再次借同一本提示请先归还通过TC05重复还书对已归还记录再次提交提示重复操作通过TC06统计口径验证造 3 条借阅记录后看排行榜计数与库内一致通过测试表里预期结果要和你代码里的 flash 文案一致评委现场会照着点一遍。每条用例对应一个 if 分支答辩被追问时可以直接指到代码行。5.3 答辩演示的推进顺序演示不要从注册登录开始太拖节奏。先打开首页展示图书列表现场借一本书立刻切到统计页证明数据联动再把某本书库存改到 0 触发提示最后让评委看 SQLite 数据库文件里字段的变化。整个过程两分钟收住。答追问时优先答「为什么这么设计」而不是逐行讲代码比如被问为什么用 SQLite就答课程设计规模下文件型数据库足够且避免评委机器环境依赖。本文还有配套的精品资源点击获取