
简介这是一套基于Flask框架的图书管理系统项目源码面向Web开发初学者与数据库课程学生可作为期中作业参考或入门练手项目。资源以Flask为核心框架围绕数据库管理展开完整覆盖从数据模型定义、SQLAlchemy交互、Jinja2模板渲染到URL路由配置和Flask-WTF表单处理等关键开发环节帮助读者理解一个完整Web应用的构建流程。压缩包约1.03MB包含Python源码、HTML模板、CSS/JS静态资源及requirements依赖清单等文件目录结构清晰便于按模块定位代码逻辑。目前已有355人学习下载。通过研读源码读者可以快速掌握图书信息增删改查、关键词检索、列表展示等典型功能的实现方法项目还保留了较好的扩展空间可进一步加入用户认证、权限控制等安全机制非常适合作为数据库课程作业的参考实现也是Flask入门实践的良好范例。1. 用 Flask 实现图书管理系统数据库设计比页面更重要这个标题几乎是每个计算机专业学生绕不开的坎用 Flask 写一个图书管理系统当数据库期中作业。你可能以为重点是 Flask 路由怎么写、页面长什么样但实际上老师真正看的是你的表结构、外键关系、事务处理和查询语句。原因很简单Flask 只是壳数据库才是这个作业的灵魂。本文会从数据库设计、ORM 映射、借还书的事务逻辑、常见坑到部署演示把一条能直接复现的路径完整走一遍。适合正在赶作业的学生也适合想拿这个经典项目练手 Flask MySQL 的开发者。2. 先想清楚数据模型图书管理系统的 ER 图与三张核心表2.1 为什么说图书管理系统的难点在数据关系不在页面图书管理系统听起来功能很多图书管理、读者管理、借阅管理、逾期计算……但拆开看核心业务动作只有两个借书和还书。所有页面都是围绕这两个动作做的数据展示。所以数据库设计的第一步不是急着建表而是把实体和关系画出来。经典的三实体模型是图书Book、读者Reader、借阅记录BorrowRecord。图书和读者之间是多对多关系因为一个读者可以借多本书一本书也可以被不同读者在不同时间借阅。这个多对多关系不能直接塞进某一张表里必须拆出一张中间表也就是借阅记录表。还有一个常见误区有人会把“当前是否被借出”直接做成图书表的一个字段。这确实能减少一次联表查询但带来的问题是图书表既要描述“这本书是什么”又要描述“这本书的状态”两个职责耦合在一起。更麻烦的是如果借阅记录里存在异常数据字段和记录对不上系统就出现脏数据了。正确做法是借阅记录表里管状态图书表只管书目信息——这是期中作业拿分的关键点。2.2 建表 SQL从三范式出发定义字段类型与约束明确关系后我们来写建表 SQL。图书表至少要有书名、作者、ISBN、出版社、出版年份、总库存、可借库存。读者表要有读者编号、姓名、联系电话、借书证状态。借阅记录表要有记录 ID、读者 ID、图书 ID、借书日期、应还日期、实际归还日期、状态。CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) UNIQUE NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, publisher VARCHAR(50), publish_year INT, total_count INT DEFAULT 1, available_count INT DEFAULT 1 ); CREATE TABLE reader ( id INT AUTO_INCREMENT PRIMARY KEY, reader_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 ); CREATE TABLE borrow_record ( id INT AUTO_INCREMENT PRIMARY KEY, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, status TINYINT DEFAULT 0, FOREIGN KEY (reader_id) REFERENCES reader(id), FOREIGN KEY (book_id) REFERENCES book(id) );这段 SQL 里有几个细节需要说明。ISBN 加了 UNIQUE 约束因为同一本书的不同副本共用同一个 ISBN但它们在业务上应该被视为同一条书目记录靠 total_count 和 available_count 来维护副本数量而不是把每个副本单独建一行。available_count 这个字段看起来有点违反第二范式因为它可以由 total_count 减去在借数量推导出来。但实际场景里每次查询图书列表都要计算可借数量如果靠统计 borrow_record 表来算列表页每本书都要跑一次匹配查询性能会很差。所以这个冗余字段是刻意的只要保证每次借书、还书时同步更新它就行。这就是反范式的取舍作业里如果能解释清楚这一点是加分项。2.3 用 SQLite 还是 MySQL选型的现实考量标题里只说了“数据库”没有指定具体产品。期中作业最常见的两个选择是 SQLite 和 MySQL。SQLite 是单文件数据库配置为零适合快速验证和交差MySQL 是大多数学校要求的正式数据库因为课程教的是它。我一般建议分两步走路。开发阶段用 SQLite因为 Flask 默认就带支持跑起来不用装任何服务最底层把连接字符串一换切到 MySQL。这样开发期的摩擦最小交付时又符合课程要求。下面这行配置就是切换的关键# config.py import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) class Config: # 开发期用 SQLite零配置启动 SQLALCHEMY_DATABASE_URI sqlite:/// os.path.join(BASE_DIR, library.db) # 交付时切换 MySQL只需改这一行 # SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/library_db?charsetutf8mb4 SQLALCHEMY_TRACK_MODIFICATIONS False切换数据库之后最常踩的坑是字符集。MySQL 如果不指定 utf8mb4中文和 emoji 都可能变成乱码。连接串里那个 charsetutf8mb4 不是可选项是必选项。还有一个坑是 SQLite 和 MySQL 对日期函数的支持不同比如计算逾期天数时SQLite 用 juliandayMySQL 用 DATEDIFF改 SQL 时要留意。3. 搭建 Flask 项目骨架用 Flask-SQLAlchemy 把数据模型固化下来3.1 项目结构怎么组织才不会被老师怀疑是网上抄的很多网上教程喜欢把 app.py 一个文件写到 500 行路由、模型、模板全塞在一起。这种写法演示是够了但结构太乱代码评审时印象分会打折扣。更合理的结构是按功能拆包library/ ├── app.py # 应用入口注册蓝图 ├── config.py # 配置数据库连接 ├── models.py # 数据模型定义 ├── views/ │ ├── __init__.py │ ├── book.py # 图书管理路由 │ ├── reader.py # 读者管理路由 │ └── borrow.py # 借还书路由 ├── templates/ # Jinja2 模板 │ ├── base.html │ ├── book_list.html │ ├── borrow_form.html │ └── ... └── requirements.txtFlask 的蓝图Blueprint机制就是为了这个场景设计的。它的作用是把不同业务的路由拆到不同文件里每个文件只负责自己的那一摊。这样一来图书路由、读者路由、借阅路由互不干扰出问题时定位也快。对那些期末要写实验报告的同学来说这个结构也更好描述——你可以在报告里画一张模块划分图直接对应到代码目录。3.2 用 Flask-SQLAlchemy 定义模型与关系有了建表 SQL接下来把它翻译成 ORM 模型。Flask-SQLAlchemy 是 Flask 官方推荐的 ORM 工具它能让你用 Python 类来操作数据表不需要手写 SQL。上课时老师可能要求你手写 SQL 来考试但项目里用 ORM 的理由很实际避免拼接 SQL 的注入风险同时代码可读性高得多。# models.py from flask_sqlalchemy import SQLAlchemy from datetime import 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(100), nullableFalse) author db.Column(db.String(50), nullableFalse) publisher db.Column(db.String(50)) publish_year db.Column(db.Integer) total_count db.Column(db.Integer, default1) available_count db.Column(db.Integer, default1) # 借阅记录关系便于关联查询 borrow_records db.relationship(BorrowRecord, backrefbook, lazyTrue) class Reader(db.Model): __tablename__ reader id db.Column(db.Integer, primary_keyTrue) reader_no db.Column(db.String(20), uniqueTrue, nullableFalse) name db.Column(db.String(50), nullableFalse) phone db.Column(db.String(20)) status db.Column(db.Boolean, defaultTrue) borrow_records db.relationship(BorrowRecord, backrefreader, lazyTrue) class BorrowRecord(db.Model): __tablename__ borrow_record id db.Column(db.Integer, primary_keyTrue) reader_id db.Column(db.Integer, db.ForeignKey(reader.id), nullableFalse) book_id db.Column(db.Integer, db.ForeignKey(book.id), nullableFalse) borrow_date db.Column(db.Date, defaultdate.today, nullableFalse) due_date db.Column(db.Date, nullableFalse) return_date db.Column(db.Date) # 0-借阅中 1-已归还 2-已逾期 status db.Column(db.SmallInteger, default0)db.relationship 的作用是让你能从 Book 对象直接访问它关联的借阅记录比如 book.borrow_records 就能拿到所有历史借阅记录不用手动写联表查询。lazyTrue 的意思是平时加载 Book 时先不查关联数据等真正访问 borrow_records 这个属性时才发查询。这个机制叫懒加载能避免列表页一次查出大量无关数据。借书日期用 defaultdate.today表示不传值时自动取当天。应还日期需要在借书时手动算常见规则是默认 30 天后还。这些是业务规则不写在 ORM 默认值里因为不同学校、不同图书馆规定的借期可能不一样。3.3 图书列表的分页查询写一个能直接用的视图函数图书列表是所有功能的基础页面。这里我们用 Flask-SQLAlchemy 的分页 API 来实现搜索关键词和分页参数都通过 URL 查询参数传入。# views/book.py from flask import Blueprint, request, render_template from models import Book, db book_bp Blueprint(book, __name__, url_prefix/books) book_bp.route(/) def book_list(): # 从查询参数中获取页码和搜索关键词 page request.args.get(page, 1, typeint) keyword request.args.get(keyword, , typestr).strip() # 按书名或作者模糊搜索 query Book.query if keyword: like_pattern f%{keyword}% query query.filter( db.or_(Book.title.like(like_pattern), Book.author.like(like_pattern)) ) # 按 id 倒序让新入库的书排在前面 pagination query.order_by(Book.id.desc()).paginate( pagepage, per_page10, error_outFalse ) books pagination.items return render_template( book_list.html, booksbooks, paginationpagination, keywordkeyword )request.args.get 用来读 URL 里的查询参数。typeint 是 WTF Form 做的隐式类型转换如果 page 参数不是数字就返回 1不会直接抛 500 错误。search 条件用 db.or_ 把书名和作者两个字段包在一起实现“书名或作者都能搜”的效果。这里用 like 做模糊匹配数据量小没问题但数据量到十万级以上like %keyword% 没法命中索引会有性能问题那是后话。paginate 是 Flask-SQLAlchemy 自带的现成分页方法比手动写 LIMIT/OFFSET 省事得多。error_outFalse 很关键它保证页码超出范围时返回空列表而不是报 404。这也就解释了为什么这个 URL 看起来简洁但该有的边界处理都做了。3.4 模板里怎么展示分页导航分页数据从视图传到模板后模板里需要有上一页、下一页和页码跳转。下面是一个可复用的分页组件写法!-- templates/_pagination.html -- {% if pagination.pages 1 %} nav ul classpagination li class{% if not pagination.has_prev %}disabled{% endif %} a href{{ url_for(book.book_list, pagepagination.prev_num, keywordkeyword) if pagination.has_prev else # }} laquo; 上一页 /a /li {% for p in pagination.iter_pages() %} {% if p %} li class{% if p pagination.page %}active{% endif %} a href{{ url_for(book.book_list, pagep, keywordkeyword) }}{{ p }}/a /li {% else %} lispan…/span/li {% endif %} {% endfor %} li class{% if not pagination.has_next %}disabled{% endif %} a href{{ url_for(book.book_list, pagepagination.next_num, keywordkeyword) if pagination.has_next else # }} 下一页 raquo; /a /li /ul /nav {% endif %}这段模板要注意的是 keyword 必须跟着翻页链接一起传。如果不传用户搜索“三体”后翻到第二页搜索条件就丢了看到的还是全量列表。体验上的一个很小的细节但对作业演示来说很显眼因为老师在演示时大概率会搜一次、翻一次页。iter_pages() 返回页码序列中间跳过的页会用省略号替代这样页数多时导航栏不会挤成一排。4. 借书与还书事务、库存更新与 Flask 表单验证4.1 借书流程的核心逻辑为什么必须写成事务借书的业务逻辑看起来很简单检查书是否可借检查读者是否有效插入一条借阅记录把书的可借数量减一。但这三步必须绑定成一个整体。任何一步失败而其他步骤已经执行就会造成数据不一致——典型的场景是借阅记录插入成功但可借库存没扣掉或者反过来。数据库的事务机制就是用来解决这个问题的。事务的 ACID 特性保证要么全部提交要么全部回滚不会出现只执行一半的情况。在 Flask-SQLAlchemy 里事务的边界由 db.session 管理commit 提交事务rollback 撤销未提交的操作。借书时还会遇到一个并发问题两个读者同时借同一本书。如果不加控制两个人都会看到可借库存为 1然后都执行借书操作最后库存变成 -1。这就是经典的数据库并发超卖问题。为了避免这种问题借书时不能只 SELECT 再 UPDATE而应该在 UPDATE 时带上条件判断。4.2 借书视图函数事务边界与乐观锁写法# views/borrow.py from flask import Blueprint, request, render_template, flash, redirect, url_for from datetime import date, timedelta from models import Book, Reader, BorrowRecord, db borrow_bp Blueprint(borrow, __name__, url_prefix/borrow) borrow_bp.route(/, methods[GET, POST]) def borrow_book(): if request.method GET: # GET 请求时只渲染借书表单 books Book.query.filter(Book.available_count 0).all() readers Reader.query.filter_by(statusTrue).all() return render_template(borrow_form.html, booksbooks, readersreaders) # POST 请求时执行借书操作 reader_id request.form.get(reader_id, typeint) book_id request.form.get(book_id, typeint) # 基础校验参数缺失直接返回 if not reader_id or not book_id: flash(请选择读者和图书) return redirect(url_for(borrow.borrow_book)) reader db.session.get(Reader, reader_id) book db.session.get(Book, book_id) # 业务校验读者是否存在且有效 if not reader or not reader.status: flash(读者不存在或已被停用) return redirect(url_for(borrow.borrow_book)) # 关键操作用条件 UPDATE 扣减库存 # 只有可借数量大于 0 时才扣减否则影响行数为 0 result Book.query.filter_by(idbook_id, available_count 0).update({ available_count: Book.available_count - 1 }) if result 0: db.session.rollback() flash(这本书暂时没有可借的库存) return redirect(url_for(borrow.borrow_book)) # 插入借阅记录 due_days 30 # 借期 30 天可按需调整 record BorrowRecord( reader_idreader_id, book_idbook_id, borrow_datedate.today(), due_datedate.today() timedelta(daysdue_days), status0 ) db.session.add(record) db.session.commit() flash(f借书成功应还日期{record.due_date}) return redirect(url_for(borrow.borrow_book))这个视图函数里最值得琢磨的是那行条件 UPDATE。filter_by 里同时写了 id 和 available_count 0 两个条件update 操作只会影响满足这两个条件的行。如果库存已经是 0影响行数为 0函数拿到 result 0 就知道本次借书失败直接回滚。这种方式叫乐观锁它不需要给表加锁靠条件更新本身的原子性避免超卖。db.session.get() 是 SQLAlchemy 2.0 推荐的主键查询方法比旧版的 query.get() 更规范。要注意的是 sqlalchemy 1.4 以下没有 db.session.get如果你用的 Flask-SQLAlchemy 3.x 才支持。如果环境比较旧用 Book.query.get(book_id) 也一样。4.3 还书流程状态更新、库存回补与过期处理还书是借书的逆向操作逻辑上有三个变化借阅记录状态从 0 变 1、实际归还日期写成今天、可借库存加一。还有一个时间点要注意如果归还日期晚于应还日期那么状态应该直接置成 2已逾期实际上很多系统用 2 表示“已归还且逾期”逾期和已归还可以是两个维度但这个项目里用一个字段表达就够了。# views/borrow.py 中新增还书路由 borrow_bp.route(/return/int:record_id, methods[POST]) def return_book(record_id): record db.session.get(BorrowRecord, record_id) if not record or record.status ! 0: flash(借阅记录不存在或已归还) return redirect(url_for(borrow.active_records)) # 判定是否逾期 today date.today() if today record.due_date: record.status 2 # 已归还且逾期 else: record.status 1 # 正常归还 record.return_date today # 把可借库存加回 book db.session.get(Book, record.book_id) book.available_count 1 db.session.commit() flash(还书成功) return redirect(url_for(borrow.active_records))还书时没有用条件 UPDATE原因是库存加回不会引发负数问题不存在并发超卖的情况。但这里缺少一步校验这本书实际是借出去的理论上可借库存不可能超过总库存。如果你愿意多写一步可以在回补库存之前判断 book.available_count book.total_count如果不满足就说明数据已经污染了记日志比直接改更重要。4.4 用 Flask-WTF 做表单校验比手写 request.form 更稳上面写的视图逻辑里表单数据是直接用 request.form 取的然后手动判断有没有值。这种做法在作业里没问题但存在两个隐患。第一如果你的老师要求演示“恶意输入”防御手写校验很难覆盖完整第二出问题时表单回显需要手动把旧数据塞回模板代码会很碎。更稳的做法是用 Flask-WTF 的 FlaskForm。它在后端做了 CSRF 防护表单字段类型可以声明为 IntegerField、StringField 等校验器负责检查长度、格式、必填。下面是一个加过 WTF 表单的借书表单模型# forms.py from flask_wtf import FlaskForm from wtforms import SelectField, SubmitField from wtforms.validators import DataRequired class BorrowForm(FlaskForm): reader_id SelectField(读者, coerceint, validators[DataRequired()]) book_id SelectField(图书, coerceint, validators[DataRequired()]) submit SubmitField(确认借书)coerceint 很关键它会把 SelectField 返回的字符串自动转成整数。如果没有它你拿到的 reader_id 是字符串 3存进数据库时虽然 SQLAlchemy 会自动转换但判断 reader_id 是否为空时会因为 0 是假值而误判。SelectField 的选项在视图函数里构造app.route(/borrow, methods[GET, POST]) def borrow(): form BorrowForm() form.reader_id.choices [(r.id, r.name) for r in Reader.query.filter_by(statusTrue).all()] form.book_id.choices [(b.id, b.title) for b in Book.query.filter(Book.available_count 0).all()] if form.validate_on_submit(): # form.reader_id.data 已经是 int ...FlaskForm 自动做了三件事检查 CSRF token、检查字段合法性、执行自定义校验器。比手写校验的代码量没有显著增加但边界情况处理得完整得多。如果你赶时间不想引入新依赖那手写也能交差但 validate_on_submit 这套机制值得你用一次后面所有项目都能复用。5. 常见问题排查Flask 图书管理系统开发中的 5 个高频坑5.1 数据库迁移改了模型表结构却没变现象你在 models.py 里给 Book 表加了一个新字段 publisher运行程序后查询这个字段报错no such column。原因SQLAlchemy 的 db.create_all() 只会创建不存在的表不会修改已存在的表结构。你改了模型但数据库文件里的表还是旧结构。这个问题在 SQLite 和 MySQL 下都存在。解决开发阶段最简单的办法是删掉旧表重建。一行命令进 Flask shelldb.drop_all() 然后 db.create_all()。但注意这会清空所有数据只适合没有真实数据的时候。交作业前千万别这么做否则给老师演示时你辛辛苦苦录的测试数据全没了。正规做法是用 Flask-Migrate 做数据库迁移它能生成增量迁移脚本保留数据的前提下加字段、改类型。作业里如果只有两次结构变更手动改 SQL 重导数据也行但时间允许的话学一下 Flask-Migrate 是长期划算的。5.2 外键约束引起的删除失败删除图书报 IntegrityError现象你试图从图书列表删除某本书页面报 500 错误后台日志显示 IntegrityError: FOREIGN KEY constraint failed。原因这本书存在借阅记录borrow_record 表的外键指向 book 表。数据库默认行为是不允许删除被引用行的这是保护数据完整性的设计。解决分业务场景处理。如果这本书只是书目信息没有借阅记录直接删没问题。如果历史上有借阅记录有两种选择其一是只做逻辑删除给 Book 表加一个 is_deleted 字段删书时把字段置为 True查询时过滤掉其二是彻底删除前把 borrow_record 表里的外键先置空或级联删除。生产系统里偏向逻辑删除因为借阅记录是审计数据不能丢。作业里最简单的是在删除前先执行一条查询BorrowRecord.query.filter_by(book_idbid).first()如果有记录就提示“该书存在借阅记录无法删除”给用户明确反馈。5.3 日期比较的类型错误逾期计算算错天数现象计算逾期天数时结果总是差一天或直接报 TypeError。原因两个坑叠加。第一个是 SQLite 里存日期时如果用了字符串python 拿到的可能是 str 而不是 date 对象直接相减就报错。第二个是借书时如果手动传了字符串日期而 due_date 字段期望 date 对象SQLAlchemy 在 SQLite 下可能不报错但存进去的数据类型变了拿出来时行为不一致。解决统一用 Python date 对象不要用字符串。在借书视图里写 date.today() 和 timedelta(days30)不要用 datetime.now().strftime(%Y-%m-%d) 这类操作。从数据库拿出来的字段如果确认是字符串可以用 datetime.strptime 或 date.fromisoformat 转回对象再计算。在 models.py 里把日期字段的 type 显式声明为 db.DateSQLAlchemy 就会自动处理。5.4 Jinja2 模板里显示中文乱码现象页面渲染出来的书名和作者名全是问号或者乱码但数据库里数据是正常的。原因分两种。MySQL 下是数据库连接字符集和表字符集不一致表是 utf8连接用的是 latin1。SQLite 下一般是 HTML 页面没声明 UTF-8 编码浏览器按系统默认编码解析导致乱码。解决MySQL 场景连接串加 charsetutf8mb4建表时指定 DEFAULT CHARSETutf8mb4SQLite 场景在模板最开头写 并且 Flask 端 response 的 Content-Type 里要有 charsetutf-8。后者在 Flask 里默认就有但如果你用了 make_response 手动构造响应需要检查 response.headers[Content-Type]。这个坑虽然小但演示时一眼就能看见特别尴尬。5.5 搜索框提交后点击分页搜索条件丢了现象输入关键词搜索后翻到第二页列表变回全量数据。原因模板里的分页链接只传了 page 参数没有把 keyword 一起传。这是上一章模板代码里特别提过的点但太容易漏了。只要搜索和分页同时存在翻页链接里就必须携带当前搜索词。解决所有分页链接加上 keywordrequest.args.get(keyword, )。如果还有其它筛选条件比如按出版社过滤也要一并传。更省事的做法是把筛选条件封装成一个 dict模板循环生成查询串。如果在模板里写太多 url_for 参数感到繁琐可以定义一个视图函数传入 kwargs 的辅助变量按需拼接。总之任何筛选条件一旦引入分页就不能只传页码。6. 交作业前的最后一步把演示流程和答辩话术准备好作业代码跑通只算完成了七成剩下三成在演示环节。老师会随机点几个功能你的反应速度和解释是否清晰直接决定成绩。我建议你按下面这个顺序自己完整走一遍就当模拟答辩。首先演示初始化删除数据库文件重新执行 create_all导入几条测试数据。这个动作证明你的项目可以从零跑通而不是依赖某个已经存在的数据库。其次是核心流程演示搜索一本书、借给一位读者、查看借阅记录列表、归还这本书、确认可借库存恢复。这五步覆盖了增删改查的全部四个操作也覆盖了外键关联和事务。最后才是界面细节比如表单校验提示、分页、空数据展示。真正拉开分差的是答辩问答。老师大概率会问三个方向的问题为什么这张表这么设计、借书时并发怎么办、ORM 和原生 SQL 的区别。前两个问题在本文第二章和第四章已经讲清楚了你只需要用自己的话复述一遍。第三个问题要诚实ORM 适合业务逻辑复杂但 SQL 不复杂的场景报表统计类的复杂查询还是要手写 SQL。你要是能主动说出这句老师会觉得你是真懂不是在背代码。最后一件事是环境整理。把 requirements.txt 生成出来格式是 pip freeze requirements.txt这样老师换一台电脑也能跑。README 里写清楚运行步骤创建虚拟环境、安装依赖、运行 python app.py。如果用到 MySQL把建立 library_db 数据库的 SQL 也放在项目根目录下的 init.sql 里。这些文件不影响代码运行但它们决定了这个作业的可复现性也是课程报告里“项目部署”章节的素材。我自己做这类项目的时候一直有个习惯从开始写代码起就手动测试每一条可能出错的路比如删除一本正在被借阅的书、给停用读者借书、超期一天还书。这些都是业务边界灰色地带的处理逻辑比主干流程更能体现工程素养。希望这些对你有所帮助也祝你的期中作业顺利落地。本文还有配套的精品资源点击获取