1. “第四次作业”到底难在哪:先别急着写代码
“第四次作业”这个标题,看起来平淡无奇,但经历过的人都知道,它往往是一门课的分水岭。前三次作业可能还在练语法、写函数、跑通某个小算法,到第四次开始上强度了——通常要求做一个能跑起来、有交互、带数据存储的完整小系统。我这次拿到的题目是做一个简单的图书管理系统Web应用,要求用Flask + SQLite实现,支持图书的增删改查、借阅状态管理、以及按书名和作者搜索。
很多人拿到类似作业的第一反应是:这不就是CRUD吗?网上模板一大把,改吧改吧就能交。这个想法踩过坑的人都懂——课程作业和开源项目模板之间的差距,恰恰是老师想考察的。CRUD谁都会写,但需求理解、边界处理、代码组织、异常情况、甚至答辩时能不能讲清楚自己的设计,这些才是拉开差距的地方。
我拿到作业后没有直接开IDE,先干了一件事:把题目要求拆成功能清单。这件事花了我大概四十分钟,但后来证明是最值的一段时间。拆完之后我意识到,这个作业表面上是考Flask和SQLite,实际上在考三件事:第一,能不能把一个模糊的需求转化为明确的功能点;第二,面对多个功能模块时,代码结构能不能保持清晰;第三,程序在异常输入、重复操作、数据库约束这些“常见但没人明说”的场景下,能不能稳定运行。想清楚这三点,后面所有的编码工作都只是体力活。
这篇博文就围绕这次作业的完整过程展开,从需求拆解、数据库设计、后端接口实现、前端页面联调,到测试与答辩准备,把每一步的判断依据和执行细节都放出来。适合正在做类似课程设计、或者想用Flask + SQLite快速搭建一个带管理功能的小应用的同学参考。内容偏实战,你完全可以照着一步步落地。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 需求拆解与功能清单:四十分钟换来四小时安心
2.1 把一段描述变成一张可执行的功能表
我拿到的原始需求描述大致是这样的:实现一个图书管理系统,能够管理图书信息,包括添加、删除、修改、查询,还要能记录书的借出和归还状态。就这么几句话,没有任何字段说明、界面要求、性能指标。如果直接把这段话甩给一个程序员,每个人做出来的东西都会不一样——这正是课程作业的隐藏考点:你如何定义“管理”这个词的边界。
我把需求拆成了五个功能模块:图书信息管理、借阅状态管理、搜索与筛选、数据持久化、前端展示。每个模块下面继续细化。图书信息管理就包括新增图书(必填项是书名、作者、ISBN、分类)、编辑图书信息、删除图书;借阅状态管理包括标记借出、标记归还、在图书列表上显示当前状态;搜索与筛选要支持按书名模糊匹配、按作者模糊匹配,以及组合条件过滤;数据持久化就是把所有操作落到SQLite数据库,重启程序后数据不丢;前端展示则是提供一个简洁的页面,能看列表、能点按钮操作。
这五个模块一旦写下来,后面所有东西都顺了:数据库需要哪些字段、需要哪些路由、页面上要放哪些按钮,全部一目了然。很多同学第四五次作业翻车,不是代码能力不行,而是拿到需求就开写,写着写着发现字段对不上、页面缺功能、数据库要重建,白白浪费大半天。我的建议是,无论作业多急,先用一张纸把“用户能在页面上做什么操作”和“系统需要记住什么数据”这两列出出来,再开始建项目。
2.2 功能边界:哪些必须做,哪些可以不做
需求不明确的时候,最考验判断力的不是“做什么”,而是“不做什么”。我给自己定了几条边界:不做用户登录注册,因为需求里没提;不做图书封面图片上传,因为需求没提;不做分页,因为测试数据就几十条,分了反而麻烦;不做复杂的借阅天数计算和逾期罚款,因为这不是图书馆管理系统。每砍掉一个功能,我都记在文档里,答辩时能说清楚“为什么没做”——这可比做完所有功能但每个都半吊子强得多。
边界意识在课程作业里特别重要。我见过不少同学,本来一个图书管理作业,非要加上用户角色权限、邮件通知、数据统计图表,结果一跑起来全是bug,光调试就花了一个星期。把一个功能完整地做出来,比做十个残缺的功能要加分得多。老师改了几十份作业,一眼就能看出哪些是认真打磨过的,哪些是功能堆砌但处处露怯的。我最后交上去的版本只实现了五个核心模块,但每个模块都经得起提问。
3. 数据库设计与Flask项目结构:地基打牢,后面不慌
3.1 SQLite建表:字段约束和默认值不嫌多
数据库设计这一步,我用了大概二十分钟。表结构很简单,一张book表就够了,字段我定了六个:id(主键)、title(书名)、author(作者)、isbn(ISBN号)、category(分类)、status(借阅状态)、created_at(创建时间)。这个设计有两个关键点值得展开说。
第一个是status字段。我一开始想过用0和1两个整数表示“在馆”和“借出”,但想了想还是用了TEXT类型,存“available”和“borrowed”两个字符串。原因很简单:代码可读性。你三个月后回来看代码,看到 if book.status == 'available' 立刻明白这是在判断“可借”,但 if book.status == 1 就得停下来想一会儿1到底代表什么。作业里代码量不大,可读性优先于所谓的极致性能。
第二个是字段约束。title、author、isbn都设成NOT NULL,isbn还加了UNIQUE约束。这看起来是小事,但能挡住很多低级bug——比如前端某个必填校验没生效,空字符串被写进数据库,后面查询、展示、编辑全跟着出问题。UNIQUE约束防止同一本书被重复录入,这在批量添加时特别常见。我在建表时不嫌麻烦,把所有能想到的约束都加上,程序运行期间几乎没遇到脏数据问题。
建表语句我放在了项目根目录下的schema.sql里,然后通过Flask启动时自动执行。这样每次重置数据库都很方便,不用手动打开SQLite命令行敲命令。
sql复制CREATE TABLE IF NOT EXISTS book (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
isbn TEXT NOT NULL UNIQUE,
category TEXT,
status TEXT NOT NULL DEFAULT 'available',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CURRENT_TIMESTAMP这个默认值很省事,录入图书时不用在Python代码里手动拼时间戳,数据库自己搞定。category字段我允许为空,因为有些冷门书确实不好分类,强行要求填写反而增加录入成本。
3.2 Flask项目目录:别再写单个app.py了
作业规模不大,不需要微服务那套复杂架构,但也不能把所有代码一股脑塞进一个文件。我采用的是Flask官方推荐的蓝本结构,虽然对一个小作业来说有点“杀鸡用牛刀”,但好处是代码分层清晰,答辩时讲起来很有条理。
项目结构如下:
code复制fourth_assignment/
├── app.py # 应用入口,创建Flask实例、注册蓝本
├── schema.sql # 建表语句
├── database.py # 数据库连接与初始化逻辑
├── models.py # 数据访问层,封装所有SQL操作
├── requirements.txt # 依赖清单
├── templates/
│ ├── base.html # 基础模板
│ ├── index.html # 图书列表页
│ ├── add.html # 新增图书页
│ └── edit.html # 编辑图书页
└── static/
└── style.css # 样式文件
app.py只做三件事:创建Flask实例、加载配置、注册路由蓝本。路由我甚至没写在app.py里,而是直接在app.py中用 @app.route 装饰器定义,毕竟体量不大,拆太碎反而增加文件间的跳转成本。我把数据库操作全部收拢到models.py,所有SQL语句都在这个文件里,视图函数中完全不出现SQL字符串。这样改数据结构时只需要动models.py和schema.sql两个文件,不会出现“SQL散布在多个路由函数里”的噩梦。
这个分层思路值得强调,因为它是从“能跑”到“能维护”的关键一步。很多同学喜欢把数据库连接、SQL查询、HTML渲染全部塞在同一个函数里,跑通的时候很爽,但一旦想加一个功能或者改一个小逻辑,就感觉在雷区里跳舞,动不动踩爆一个隐藏依赖。
3.3 数据库连接:一个请求一个连接的模式
数据库连接这块,没有用SQLAlchemy这样的ORM,因为作业要求只允许用标准库sqlite3。但这不意味着可以随便写着玩。我在数据库连接上有两个心得。
第一个是每处理一个请求就开一个连接,请求结束就关闭,不要搞全局长连接。SQLite是文件型数据库,连接本身很轻量,全局复用反而容易遇到多线程读写互相阻塞的问题。Flask的开发服务器默认是多线程的,全局连接被两个请求同时使用,会抛出SQLite objects created in a thread can only be used in that same thread的异常,这种问题排查起来很痛苦。
第二个是使用with connect(':memory:') as conn这种上下文管理器模式,用完自动提交或回滚,稍微有点Python经验的人都看得懂。
python复制import sqlite3
DATABASE = 'library.db'
def get_db():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
row_factory = sqlite3.Row 这行代码很关键,它让查询返回的每一行像字典一样可以通过列名访问,而不是只能通过下标。模板里写 book['title'] 比 book[1] 可读性高太多了。这是我踩过的一坑——不设row_factory,Flask模板里拿到的每行都是元组,列一多就分不清谁是谁。
4. 后端路由与核心逻辑:把每个接口做扎实
4.1 图书列表:排序和状态标识一起做掉
路由设计我遵循一个原则:URL能表达意图,方法能区分操作。列表页用的是根路径 /,也就是GET请求,查出来所有图书之后按创建时间倒序排列,新添加的显示在最前面。这个排序逻辑必须写到SQL里,别在Python里排。
python复制@app.route('/')
def index():
conn = get_db()
books = conn.execute(
'SELECT * FROM book ORDER BY created_at DESC, id DESC'
).fetchall()
conn.close()
return render_template('index.html', books=books)
注意我为什么在created_at相同的情况下再加一个id DESC排序:created_at的精度是秒级,同一秒内添加两本书是完全可能的,如果没有次级排序条件,两次查询的结果顺序可能不一致,测试和答辩时容易被问住。这种细节如果被老师发现,说明你的代码真实经历过并发场景下的跑测,是加分项。
列表页上我直接显示状态标签——available显示绿色“在馆”,borrowed显示红色“已借出”。这个状态标识我在模板里用Jinja2的条件判断实现,逻辑简单,但视觉效果很直观。
4.2 新增图书:表单校验要前后端同时做
新增图书的接口我拆成了两个:GET /add 返回添加页面,POST /add 处理表单提交。这是Flask最常规的写法,看起来平平无奇,真正有讲究的是表单校验。
前端我用了HTML自带的required属性、minlength等做第一层校验,保证非空字段不被提交。但前端校验是可以绕过的——直接在浏览器控制台里发请求就行,或者用curl直接POST。所以后端必须再做一次完整的校验。我这里的校验规则很简单:书名、作者、ISBN不能为空,ISBN格式要大致符合规范(我用的正则只做基本格式检查,不去校验真实ISBN的校验位)。
python复制@app.route('/add', methods=['POST'])
def add_book():
title = request.form.get('title', '').strip()
author = request.form.get('author', '').strip()
isbn = request.form.get('isbn', '').strip()
category = request.form.get('category', '').strip()
errors = []
if not title:
errors.append('书名不能为空')
if not author:
errors.append('作者不能为空')
if not isbn:
errors.append('ISBN不能为空')
elif not re.match(r'^\d{9}[\dXx]$', isbn):
errors.append('ISBN格式不正确')
if errors:
return render_template('add.html', errors=errors, form=request.form), 400
try:
conn = get_db()
conn.execute(
'INSERT INTO book (title, author, isbn, category) VALUES (?, ?, ?, ?)',
(title, author, isbn, category)
)
conn.commit()
conn.close()
except sqlite3.IntegrityError:
errors.append('该ISBN已存在,请勿重复添加')
return render_template('add.html', errors=errors, form=request.form), 400
return redirect(url_for('index'))
这里有一个容易被忽略但非常容易被问到的点:如果ISBN重复导致IntegrityError,程序不能直接500报错,得捕获异常并提示用户。我在代码里专门处理了这种情况。同时注意request.form.get一定要加.strip(),把首尾空格去掉,否则用户输入“ 9787111213825 ”和“9787111213825”会当成两个ISBN,UNIQUE约束也拦不住(因为字符串确实不同)。
表单提交出错时,我把request.form原样传回模板,这样用户填错后页面不会清空重来,体验好很多。这个小细节是我自己用的时候被坑过——第一次写,校验不过就跳回空表单,填了五六个字段全没了,心态直接崩掉。
4.3 编辑图书:把原数据回显,别让用户重新录入一遍
编辑和新增逻辑很像,但多了一步:先根据id查出旧数据、渲染到表单里,用户改完之后POST提交。编辑时需要注意一个边界情况——用户可能想把ISBN改成和另一本书重复的值。所以在编辑的POST处理里,除了格式校验,还要额外查一遍“除了当前这本书外,是否已有其他书占用这个ISBN”。
python复制@app.route('/edit/<int:book_id>', methods=['GET', 'POST'])
def edit_book(book_id):
conn = get_db()
book = conn.execute('SELECT * FROM book WHERE id = ?', (book_id,)).fetchone()
if book is None:
conn.close()
return '图书不存在', 404
if request.method == 'POST':
title = request.form.get('title', '').strip()
author = request.form.get('author', '').strip()
isbn = request.form.get('isbn', '').strip()
category = request.form.get('category', '').strip()
errors = []
if not title:
errors.append('书名不能为空')
if not author:
errors.append('作者不能为空')
if not isbn:
errors.append('ISBN不能为空')
elif not re.match(r'^\d{9}[\dXx]$', isbn):
errors.append('ISBN格式不正确')
if not errors:
dup = conn.execute(
'SELECT id FROM book WHERE isbn = ? AND id != ?',
(isbn, book_id)
).fetchone()
if dup:
errors.append('该ISBN已被其他图书使用')
if errors:
conn.close()
return render_template('edit.html', book=book, errors=errors), 400
conn.execute(
'UPDATE book SET title = ?, author = ?, isbn = ?, category = ? WHERE id = ?',
(title, author, isbn, category, book_id)
)
conn.commit()
conn.close()
return redirect(url_for('index'))
conn.close()
return render_template('edit.html', book=book)
这里有个容易忽略的设计:GET和POST共用了同一个取book的逻辑,因为无论显示编辑表单还是处理提交,都需要先知道这个book是否存在。我把取book操作放在函数开头,统一处理不存在的情况,返回404,避免后续代码里到处判断book is None。
4.4 删除图书:先确认再操作,别让手滑毁数据
删除接口的URL是/delete/<int:book_id>,只接受POST方法。为什么不用GET?因为如果用GET,用户可能在地址栏直接输入一个删除URL,或者爬虫扫描时误触发,数据就没了。作业虽小,但POST /delete/<id>这种设计习惯值得养成。
我在前端做了一个确认弹窗confirm('确定要删除这本书吗?'),删除前必须点确认。虽然防不了恶意请求,但能防误操作。后端删除后直接重定向到列表页。
python复制@app.route('/delete/<int:book_id>', methods=['POST'])
def delete_book(book_id):
conn = get_db()
cur = conn.execute('DELETE FROM book WHERE id = ?', (book_id,))
conn.commit()
conn.close()
if cur.rowcount == 0:
return '图书不存在', 404
return redirect(url_for('index'))
这里我检查了cur.rowcount——如果删了0条,说明这个id在数据库里压根不存在。虽然在正常操作流程下不会出现这种情况,但万一用户开着两个标签页,一个页面已经删掉了这本书,另一个页面还在老列表上点删除,如果不判断rowcount就会静默成功,用户会以为删掉了其实没删掉。这类边界场景不会出现在功能列表里,但恰恰是课程设计答辩时老师最喜欢追问的地方。
4.5 借出与归还:状态变更要原子化
借出和归还本质上是一次UPDATE操作,把status字段从available改成borrowed,或者反过来。我设计了一个路由,用POST传参区分操作类型:
python复制@app.route('/toggle-status/<int:book_id>', methods=['POST'])
def toggle_status(book_id):
conn = get_db()
book = conn.execute('SELECT * FROM book WHERE id = ?', (book_id,)).fetchone()
if book is None:
conn.close()
return '图书不存在', 404
new_status = 'borrowed' if book['status'] == 'available' else 'available'
conn.execute(
'UPDATE book SET status = ? WHERE id = ?',
(new_status, book_id)
)
conn.commit()
conn.close()
return redirect(url_for('index'))
有人可能会问,为什么不直接暴露两个接口/borrow/<id>和/return/<id>?从业务语义上那种更清晰。但我这里选toggle的方式,因为页面上的操作就是一个按钮,我不想在前端判断当前状态决定调哪个URL。不过每次操作都先把book查出来,拿到当前状态再决定新状态,这保证了“当前状态->新状态”的转换是基于最新数据的,而不是基于页面渲染时的旧状态。这个操作是查询+更新两步,不是原子性的,但在作业场景下够用。
5. 前端页面与交互细节:别让颜值拖后腿
5.1 基础模板:一套Jinja2布局走天下
前端这块我没有引入任何前端框架,就用了纯HTML + CSS + Jinja2模板。三个页面(列表、新增、编辑)共用一套base.html,页面头部的导航栏、CSS文件的引入、Jinja2的block content都定义好,子页面只需要覆盖内容区域即可。
base.html的思路其实和Flask的布局机制如出一辙:公共部分抽出来,变化部分留给子模板。写页面时先把骨架搭出来,再填内容,效率高很多。
5.2 列表页面:状态标签与空状态都要处理
列表页是用户操作的主界面,一行一本书,最后一列放操作按钮。我的页面布局是:顶部一个“添加图书”按钮,下面一张表格,表格列依次是ID、书名、作者、ISBN、分类、状态、操作。
状态列我通过Jinja2判断渲染:
jinja2复制{% if book['status'] == 'available' %}
<span class="badge available">在馆</span>
{% else %}
<span class="badge borrowed">已借出</span>
{% endif %}
CSS里分别定义.badge.available(绿色底)和.badge.borrowed(红色底),视觉上一眼区分。
还有一个我一定要处理的场景——空列表。如果数据库里一本书都没有,表格只有表头没有数据,会显得很寒酸。我在模板里判断if books,如果没数据,就渲染一个友好的提示行“暂无图书,点击右上角添加第一本书”。这虽然是极端边界情况,但万一演示的时候数据库被重置了,这个提示能救场。
操作列有五个按钮:编辑、删除、借出/归还、以及(因为编辑和删除按钮太多)我用小字把删除按钮单独放在右边,借出/归还紧跟状态列。按钮全部用<form>包起来,method="POST",避免用<a>标签伪造请求。
5.3 表单页面:错误提示与输入回显
新增和编辑页面几乎一样,就是一个表单:书名、作者、ISBN、分类四个输入框,一个提交按钮。区别是编辑页要把book数据预填到输入框里,新增页在出错时要把用户上一次提交的内容回显。
错误提示我放在表单上方,用红色列表展示所有校验错误:
jinja2复制{% if errors %}
<div class="error-box">
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
这里有个细节:错误提示必须放在表单外面,并且风格要醒目。我一开始把错误信息放在每个输入框下面,代码写起来复杂,而且一个字段可能对应多个错误,展示逻辑容易乱。统一放在顶部一个list里,简单清晰,改起来也方便。
6. 测试与排错:我被这三个坑困了一下午
6.1 表单提交后页面空白:原来是模板变量名写错了
有一次我在新增页填写完表单点提交,结果页面直接白屏,HTTP状态码是500。我在浏览器开发者工具里看到响应内容是一段很长很长的Python traceback,定位到问题出在模板渲染时book['status']报错——因为我在index.html里写了book['status'],但新增提交之后重定向回index,查出来的字段名应该没错啊。
后来仔细看traceback才发现,问题根本不在index模板,而在于我跳转到的重定向目标URL写错了参数名。也就是说,我在edit_book里render_template('edit.html', book=book, errors=errors),但模板里用的是book_data这个变量名,Jinja2找不到这个变量就抛UndefinedError,导致页面直接崩了。
这类错误非常低级,但就是因为低级才更容易在写代码时走神。我的排查方式是:先把traceback的最后几行读完,找到具体是哪个模板文件的哪一行报错,再回到render_template那里核对变量名。在这个案例里,问题就是模板变量名和Python侧传入的变量名不一致。Flask调试模式下,页面会直接显示出错信息,但生产环境你只会看到一个Internal Server Error。所以本地开发一定要开debug=True,不要觉得麻烦。
6.2 数据库被锁:SQLite的并发写问题
这是我踩的一个真坑。测试的时候,我开了一个页面没关,又用另一个终端跑了个Python脚本去写数据库,结果脚本直接抛database is locked。原因很简单:SQLite允许多个读并发,但写操作是串行的,且整个数据库文件会被临时锁住。当第一个连接还未提交或关闭,第二个连接尝试写的时候就可能被阻塞,超时后抛出锁错误。
这个问题的解决方案有几个层面:第一,在连接时设置timeout参数,让等待时间拉长一些;第二,确保每个请求的数据库连接都及时关闭,不要长持有;第三,避免在事务未提交时做长时间的操作。我用的是第三种思路,配合第一种兜底:
python复制conn = sqlite3.connect(DATABASE, timeout=10)
这样即使偶发锁冲突,等待10秒后会自动重试,基本就不会出现在作业演示现场“点一下按钮卡死”的尴尬了。
6.3 重定向后闪现消息丢失:Flash消息的作用域与时机
我本来想做个简单的“添加成功”“删除成功”的提示,直接在删除函数里用了flash('删除成功')。结果发现页面重定向后消息并没有显示出来。排查后发现,模板base.html里根本没写接收flash消息的代码,我只在index.html里加了。而添加成功跳转到index时消息能显示(因为index里有),删除成功也跳转到index所以也能显示,但编辑成功跳转回index时又消失了。
为了一劳永逸,我把flash消息展示代码放到了base.html的content区块里面顶部位置,这样不管从哪个页面操作、重定向到哪里,只要那个页面继承了base.html,就能显示flash消息:
jinja2复制{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash-message">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
这个顺序很重要——如果把它放在content区块外面,有些子模板可能不继承整个base,导致消息依旧不显示。放在content里、位置在{% block content %}的正上方,是最可靠的。
这件事教会我的是:模板继承链上的公共逻辑,必须放在基础模板中统一处理,而不是在每个子页面各写一遍,否则维护时漏掉一个就相当被动。
7. 演示与答辩的经验:如何把作业讲成一个项目
7.1 设计一条不会翻车的演示路径
作业交上去之后需要现场演示,这是很多人最紧张的部分。我的方法是不背稿,而是设计一条符合操作逻辑的演示路径,让每一步操作都自然引出下一步。
我的演示顺序是:先展示空列表状态(展示“暂无图书”的提示),然后添加第一本书,添加成功后列表里出现新书且状态是“在馆”;接着添加第二本书,然后演示编辑功能,把第二本的书名改掉;接着演示借出,状态变成“已借出”;再演示搜索,输入关键字能找到对应图书;最后演示删除,把一本测试书删掉。这条路径把五个核心功能全部覆盖了,而且每一步都是从上一步自然延伸出来的,看起来不是在“背流程”,而是在“用系统”。
一个重要的演示心法:故意在演示过程中展示一个“正常失败”场景。比如在新增表单里故意填一个格式错误的ISBN,然后给老师看错误提示。这样能展示你对异常输入的处理能力。很多同学演示时一路顺利,反而让老师觉得“太顺利了,是不是有什么没敢展示”。
7.2 被追问时如何回答
答辩中被追问最多的问题,通常不是“你这个功能怎么做出来的”,而是“你为什么要这样做”。我被问到的问题包括:为什么status用字符串不用数字、为什么删除接口用POST、为什么数据库连接在每个请求里都新建、如果数据量大了有什么问题。
回答这些问题的关键是你真正想过这些设计决策。我在写这篇作业时做的每个选择都不是拍脑袋,都有理由。比如status用字符串是为了可读性,删除用POST是为了避免被爬虫或预加载误触发,每次新建连接是为了避开SQLite多线程问题。这些理由只要自己心里清楚,答辩时就能流畅地讲出来,而且讲完之后老师通常会点头表示认可。
7.3 课堂演示的周边准备
除了代码本身,我建议准备几件事:一张画好的系统架构图(手画也行,不用多精美,但要能讲清楚请求怎么流动)、一份简单的功能清单、以及一个“如果数据库出问题了怎么办”的应急预案。我的应急预案是:数据库文件library.db如果被测试搞坏了,可以直接删除该文件,然后重启Flask应用,系统会自动根据schema.sql重新创建表结构。这个方案虽然会丢失数据,但演示现场最重要的是让系统恢复可用,而不是保住几条测试数据。
另外在演示前,我习惯把浏览器缓存清空、把数据库重置一下,确保演示环境是干净的,不要带着一堆历史测试数据上场。脏数据会影响观感,老师会觉得你测试不充分。
8. 作业做完之后的三个延伸想法
整个“第四次作业”从拿到题目到交付答辩,总共用了大约三个晚上。第一个晚上做需求拆解和数据库设计,第二个晚上写后端路由和模板页面,第三个晚上做测试、修bug、准备答辩题目。时间不算长,但每个环节都走了一遍,做完之后对Flask的整体把握比前三次作业加起来都强。
这里想说的是,课程作业的价值不在“交差”,而在于它逼你把零散的知识串起来。前三次作业练的是知识点,第四次作业练的是知识网。我第一次感受到“原来前端页面、后端路由、数据库三者是这样协作的”,就是在完成这个作业的过程中。
如果你也在做类似的作业,我的建议是:不要急着抄网上现成的代码,哪怕最后做出来的东西和网上模板功能一样,你亲自把数据库建一遍、把路由写一遍、把模板调一遍、把bug排一遍,收获是完全不同的。代码能照着抄,但踩坑的经验和“为什么这样设计”的判断力抄不走。
另外做完这个项目后,你可以尝试往里面加两个小功能来加深理解:一是给图书添加“出版年份”字段并支持按年份过滤,二是给列表加一个简单的分页。这两个功能都不难,但会让你对Flask路由参数、数据库查询、模板循环有更深的体会。我就加了一个出版年份字段练手,虽然最后没有交到作业里,但这个练习帮我理解了数据库迁移的基本思路——加字段不是改schema.sql那么简单,还得考虑已有数据的兼容。
说到底,“第四次作业”这个标题,对当时正在赶作业的我来说,是压力;现在回头看,它其实是我真正开始理解一个Web应用是怎么从零到一搭建起来的起点。
