ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flask+SQLite二次作业实战:从用户登录到待办事项持久化

Flask+SQLite二次作业实战:从用户登录到待办事项持久化 “第二次作业”这几个字听起来平平无奇但每个经历过的人都懂第一次作业大多是小打小闹第二次作业开始上真章。我也是刚交完第二次作业趁热把整个过程拆开揉碎复盘一遍。这次作业要求是做一个带用户登录的待办事项管理数据库要持久化界面不用多好看但逻辑必须完整。我选了 Flask 加 SQLite前后花了两个晚上踩了不少坑也总结出一些能直接用上的经验。这篇东西不是教科书更像是我在工位上跟你复盘整个过程。不管你是刚学编程准备交作业还是带学生的老师想找常见错误案例都能从里面翻到点有用的东西。1. 作业需求深度拆解先想清楚再动手1.1 需求文档里没明说的考点我的作业要求原文只有一行“实现一个待办事项管理页面支持注册登录后添加、删除、修改待办事项数据需要持久化。”看起来简单但老师不会告诉你这句话里藏着至少五个考点用户注册登录怎么做、密码怎么存、用户数据怎么隔离、增删改查怎么能不出错、刷新页面后数据还在不在。我第一版代码就是没做需求拆解登录做完直接写任务结果任务表里所有人的任务全混在一起改了一晚上。所以拿到需求先别急着开 IDE拿张纸把它翻译成功能点。我翻译出来的功能列表是这样的用户注册用户名 密码密码不能明文存、用户登录会话保持能区分当前登录的是谁、任务列表只显示当前用户自己的任务、新增任务内容不能为空可设置截止日期、修改任务编辑内容、标记完成、删除任务需要二次确认至少不能误删。这个列表看起来不复杂但它决定了后面数据库表的字段和每个路由的写法。如果一开始就漏掉“作者归属”这件事后面加字段加条件会让代码变得非常难看。1.2 技术选型少即是多技术选型上我选了 Flask SQLite Jinja2。原因很朴素第二次作业不要求高并发、不要求微服务重点是业务逻辑完整。Flask 是 Python 里最轻的 Web 框架SQLite 直接内置不用额外装服务Jinja2 跟着 Flask 走模板语法也不难。要是用 Django虽然自带 Admin 方便但那一堆迁移命令就够喝一壶用 Spring BootJava 的编译配置会占据大量时间。我不是说它们不行而是你得评估投入产出比。选型还有一个标准你能否向老师解释清楚每个组件的角色。能讲明白的选型才是好选型。比如我可以说“SQLite 负责存储Flask 负责接收请求和返回页面Jinja2 负责把数据填进模板”这就是一个让人满意的答案。如果你选了一堆框架自己却说不清路由和中间件的关系演示时一问就露馅。第二次作业最怕的就是技术栈看起来高大上但自己其实是个半吊子最后连部署都成问题。1.3 项目规划拆出可交付的功能列表我把需求拆成五个模块用户注册、登录注销、任务列表、任务编辑、任务删除。每个模块再拆成基础任务比如注册包含用户名校验、密码哈希、重复检查任务编辑包含回显数据、更新提交、权限校验。拆完以后心里就有数了估计工作量大概是数据库设计半小时注册登录两小时任务 CRUD 两小时测试和修 bug 两小时。一共六个半小时左右。实际我花了大概八个半小时多出来的时间都在和模板语法较劲。这个偏差正常但提前规划至少不会让你做到凌晨三点还看不到曙光。我更想强调的是规划不是写文档而是在脑子里把每个功能的路走过一遍。比如注册完成后跳转到登录页登录成功后跳转到任务列表任务列表页要同时支持新增和删除。把这些跳转关系画成一条线代码写起来就是按图索骥而不是写一步想一步。2. 项目骨架搭建与数据库设计2.1 初始化环境每一步都要有理由我用的 Python 3.10系统是 Ubuntu。创建虚拟环境的命令如下mkdir todo_app cd todo_app python3 -m venv venv source venv/bin/activate pip install flask为什么非要虚拟环境因为系统里可能已经有旧版 Flask 或者其他包万一版本冲突交作业时候的报错会让你崩溃。虚拟环境把依赖隔离在项目目录里用 requirements.txt 还能让老师直接复现。安装完以后记得执行pip freeze requirements.txt这既是规范也是让你少被扣分的小习惯。顺便说一下如果用 Windows激活虚拟环境的命令是venv\Scripts\activate其他流程一样。验证环境是否 OK可以进 Python 命令行输入import flask不报错就是成功。这一步花不了两分钟但能避免后面因为环境问题生成的诡异报错。我见过同学在教室演示时项目跑不起来结果发现是没激活虚拟环境系统调用了全局的旧版 Python。所以每次开新终端第一件事就是激活虚拟环境养成肌肉记忆。2.2 目录结构别把文件全塞进一个 py很多人第二次作业还是把所有代码堆在一个 app.py 里几千行排着队连自己都看不清谁是谁。我建议用最小但清晰的目录todo_app/ ├── app.py # 主程序路由入口 ├── schema.sql # 建表语句 ├── requirements.txt ├── templates/ │ ├── base.html │ ├── index.html │ ├── login.html │ └── register.html └── static/ └── style.css第一次作业确实是单文件就行但第二次作业页面多了模板文件就得拆开。这样做的好处有两个一是模板层和逻辑层分离改样式不用翻 Python 代码二是老师看结构就知道你有工程意识。HTML 文件放在 templates 里是 Flask 的默认约定静态文件放在 static 里也是约定。遵守框架约定可以少写很多配置比你自己发明的路径方案要稳妥得多。2.3 数据库设计外键与哈希的第一次亲密接触我用的是 SQLite 内置模块没有额外装 SQLAlchemy因为作业不需要 ORM直接写 SQL 反而更容易理解。建表语句如下CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE TABLE task ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, due_date TEXT, is_done INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now)), FOREIGN KEY (user_id) REFERENCES user (id) );这里有两处是作业最容易批评的点。第一字段名是 password_hash 而不是 password因为存的是哈希而不是明文。第二task 表用 user_id 外键这样才能保证任务归属某个用户。我还故意把日期存成 TEXT 而不选 TIMESTAMP原因很实际HTML 表单里 date 输入框提交的就是“2025-06-01”格式字符串直接存进去比来回转换简单。等以后需要复杂时间计算再换第二次作业不折腾这些。执行 schema.sql 的方式如下没有 sqlite3 的话可以用 Python 的 sqlite3 模块执行但 Ubuntu 一般自带。sqlite3 todo.db schema.sql3. 核心功能实现从注册到任务管理3.1 用户注册密码处理是第一堂安全课注册页面的路由逻辑我最早是这么写的直接从 request.form 取用户名和密码然后拼 SQL 插入。还好没交后来发现是灾难。正确写法是借助 werkzeug 自带的哈希函数from flask import Flask, render_template, request, redirect, url_for, session, flash from werkzeug.security import generate_password_hash, check_password_hash import sqlite3 app Flask(__name__) app.secret_key replace-with-your-own-key def get_db(): conn sqlite3.connect(todo.db) conn.row_factory sqlite3.Row return conn app.route(/register, methods[GET, POST]) def register(): if request.method POST: username request.form[username].strip() password request.form[password] if not username or not password: flash(用户名和密码不能为空) return redirect(url_for(register)) conn get_db() try: conn.execute( INSERT INTO user (username, password_hash) VALUES (?, ?), (username, generate_password_hash(password)) ) conn.commit() return redirect(url_for(login)) except sqlite3.IntegrityError: flash(用户名已存在) return redirect(url_for(register)) finally: conn.close() return render_template(register.html)这段代码里有几个细节值得说。strip()去掉首尾空格避免用户不小心多打一个空格导致注册出“admin ”这种奇怪用户名。generate_password_hash默认使用一种安全的慢哈希算法不是那种可以秒破的 MD5。数据库的 username 字段有 UNIQUE 约束所以重复注册会抛 IntegrityError我在这里直接捕获并提示用户比注册前先查一遍数据库更简洁也更安全。还有一个容易被忽略的点conn.row_factory sqlite3.Row。设置了它之后查询结果就可以像字典一样用字段名访问比如user[username]而不是user[0]。这个小设置能少写很多无意义的索引代码也让代码可读性高一个档次。3.2 登录会话用 session 记住你是谁注册完就可以登录。登录逻辑同样要检查密码哈希唯一标识是用户 id因为 id 是数据库自动生成、不可能重复的字段。登录成功后要把用户信息写进 sessionFlask 的 session 是基于签名 cookie 的客户端可以看到内容但无法篡改。登录路由的写法如下app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form[username].strip() password request.form[password] conn get_db() user conn.execute( SELECT * FROM user WHERE username ?, (username,) ).fetchone() conn.close() if user is None or not check_password_hash(user[password_hash], password): flash(用户名或密码错误) return redirect(url_for(login)) session.clear() session[user_id] user[id] session[username] user[username] return redirect(url_for(index)) return render_template(login.html)注意两点第一check_password_hash会把用户输入的密码和存好的哈希比对返回布尔值。第二我用session.clear()清掉可能残留的旧会话虽然现在没有但养成习惯没有坏处。另外密码错误和用户不存在返回同样的提示这是安全惯例不让别人试探哪些用户名存在。这里插一个我踩过的坑一开始我把登录成功后的重定向写成了redirect(url_for(login))导致登录成功又回到了登录页看起来就像没登录一样。排查了十分钟才发现是 URL 写错了。所以写完路由一定要从用户视角走一遍流程而不是只在浏览器里点一下按钮就算完。3.3 任务 CRUD每条 SQL 都必须带 user_id任务列表、新增、编辑、删除是作业的核心。我第一版写的是UPDATE task SET ... WHERE id ?演示的时候被同学恶作剧他登录后直接删掉了我的任务。原因就是 SQL 里没有AND user_id ?。改完之后的代码是这样app.route(/add, methods[POST]) def add_task(): title request.form[title].strip() description request.form.get(description, ).strip() due_date request.form.get(due_date, ) if not title: flash(任务标题不能为空) return redirect(url_for(index)) conn get_db() conn.execute( INSERT INTO task (user_id, title, description, due_date) VALUES (?, ?, ?, ?), (session[user_id], title, description, due_date) ) conn.commit() conn.close() return redirect(url_for(index))编辑和删除同理更新时一定带上 user_idconn.execute( UPDATE task SET title ?, description ?, due_date ?, is_done ? WHERE id ? AND user_id ?, (title, description, due_date, is_done, task_id, session[user_id]) )删除时更要带conn.execute( DELETE FROM task WHERE id ? AND user_id ?, (task_id, session[user_id]) )这样即使知道别人的任务 id 也动不了。这个细节是第二次作业最容易忽视但是最拉分的地方。其实逻辑很简单任务属于哪个用户数据库表里已经通过 user_id 记录清楚了你在操作时就必须把条件和记录的来源对应上。很多人写 SQL 时只想着“我要更新 id 为 5 的任务”忘了还要检查“这个 id 为 5 的任务是不是当前用户的”。3.4 模板渲染与交互细节Flask 默认用 Jinja2 渲染。我把公共导航放在 base.html 里每个页面继承它。例如导航栏显示当前登录用户名和注销按钮nav {% if session[username] %} span你好{{ session[username] }}/span a href{{ url_for(logout) }}注销/a {% else %} a href{{ url_for(login) }}登录/a {% endif %} /nav首页任务列表展示也用的是 Jinja2 循环{% for task in tasks %} div classtask-item input typecheckbox nameis_done {% if task[is_done] %}checked{% endif %} onchangethis.form.submit() span class{% if task[is_done] %}done{% endif %}{{ task[title] }}/span a href{{ url_for(edit_task, idtask[id]) }}编辑/a a href{{ url_for(delete_task, idtask[id]) }}删除/a /div {% endfor %}这里有个坑checkbox 放在 form 外面onchange根本不会触发表单提交。我最初的写法就是这样结果勾选完成状态时页面毫无反应。正确做法是给每个任务包一层 form或者单独用一个更新路由接口让前端用链接跳转。考虑到第二次作业的复杂度我给每个任务 item 内嵌了 formcheckbox 的onchange触发this.form.submit()再配合隐藏的 task id。虽然这样每个任务周围都会有点冗余代码但演示时非常直观不会出错。4. 调试、测试与提交把问题消灭在交作业前4.1 常见报错速查表调试期间我整理了下面这张表建议你也一边做一边记。这张表的价值在于你不是每个坑都一定要亲身踩一遍才有经验别人踩过的坑你扫一眼就能避开。报错现象常见原因处理办法jinja2.exceptions.TemplateNotFoundtemplates 目录拼写错误或文件名不对检查项目根目录下是否有 templates 文件夹模板名是否和 render_template 参数一致sqlite3.OperationalError: no such table: task忘了执行 schema.sql执行sqlite3 todo.db schema.sql重启应用RuntimeError: The session is unavailable because no secret key was setapp.secret_key 没设置设置随机字符串正式环境用环境变量sqlite3.IntegrityError: UNIQUE constraint failed注册了重复用户名用 try 捕获并 flash 提示用户页面能开但 CSS 没生效static 目录路径不对在模板里用{{ url_for(static, filenamestyle.css) }}其实很多报错是英语看着吓人翻译过来就是路径或者初始化问题。解决基本原则看完整报错栈不要只看第一行。我见过同学只看到红色字最后一行结果改错地方越改越乱。比如TemplateNotFound报错栈往往还会顺带告诉你它找过哪些路径一看就知道是目录拼错了还是没建目录。4.2 手动测试用例清单模拟老师的行为老师验收作业的时候会怎么操作他会先注册一个用户登录随便输入几个任务然后刷新页面再换个用户登录看看能不能看到别人的任务。还会故意输入空标题、超长内容甚至反斜杠引号。所以你的测试清单要覆盖这些注册空用户名、空密码注册已存在的用户名登录时密码错误未登录直接访问首页应重定向到登录页任务标题为空任务标题含scriptalert(xss)/script修改任务时把 id 改成别人的任务 id删除任务时同样改 id刷新页面后任务仍在前几个是基础第 7 和第 8 就是越权检查。我每轮改完代码都跑一遍这张表虽然很枯燥但交作业前心里有底。尤其是换用户登录看任务隔离那段是全项目最值得炫耀的亮点。XSS 那条也要重视Flask 的 Jinja2 默认会自动转义 HTML所以理论上{{ task.title }}会被显示为纯文本而不是执行脚本但你自己主动测一遍并记住这个特性就能在答辩时回答“你怎么防 XSS 的”。4.3 越权修复一个让老师加分的细节可能有人问为什么需要刻意提越权因为课程作业里很多人默认“用户只能看到自己的任务”是天然成立的。但程序没有这个默认值你不在每个 SQL 里写 user_id它就会把全部人的任务列出来。第二次作业能想到这个点说明你开始有安全边界意识了。具体做法可以加一个辅助函数把所有任务查询都封装起来统一加上当前用户条件def get_own_task(task_id): conn get_db() task conn.execute( SELECT * FROM task WHERE id ? AND user_id ?, (task_id, session[user_id]) ).fetchone() conn.close() return task然后所有编辑、删除路由都只调用这个函数。这样一来万一以后忘了在哪段加条件至少统一入口能挡住错误。我给这个方法单独写了一小段注释并在提交说明里专门写了一句“每个数据操作都校验了当前用户身份”老师看到后直接在群里表扬了。这不是套路是真有用的工程习惯。5. 复盘与延伸第二次作业的价值不止于分数5.1 我的时间分配复盘我这次实际时间花在需求拆解约 30 分钟环境初始化 30 分钟数据库设计 1 小时注册登录 1.5 小时任务 CRUD 2 小时模板和样式 1.5 小时测试和修 Bug 1.5 小时。加起来 8.5 小时。如果当初不拆解需求直接写估计要 12 小时以上而且大概率返工。为什么拆解需求能省时间因为写代码最怕中途发现逻辑不对回头改。比如我一开始就确定了所有 SQL 带 user_id后来写 CRUD 就非常顺要是不事先想清楚后面想到再加每个函数都得翻一遍。还有一个小建议每个功能模块完成之后立刻用一两句话记到文件里。这个记录不会花很多时间但交作业时如果你要写项目说明这些记录就是现成的素材不用临时回忆。5.2 印象最深的三个坑第一个坑密码明文存储。我最初注册功能 10 分钟写完把 password 原样存进数据库自己还很得意。后来一个已经工作两年的学长看了一眼数据库截图问我“你是不是没做哈希”我愣了一下然后赶紧改成 werkzeug 的generate_password_hash。这个坑让我明白能跑和能用是两回事。第二个坑参数化查询。我在拼接 SQL 字符串时想偷懒写了个 f-string结果被同学用一个; DROP TABLE task; --的玩笑演示吓出一身冷汗。从此再也不敢手拼 SQL全部用问号占位符。其实这跟“不推荐加密存储密码”一样不是功能问题而是安全问题。第三个坑全局变量存任务。刚开始我没有数据库只用一个全局 Python 列表存任务以为刷新就丢没什么大不了。真重启后数据消失才意识到老师要求“数据持久化”就是这么来的。换成 SQLite 之后这个问题彻底消失而且我也真正理解了为什么数据库是几乎所有 Web 应用的标配。5.3 后续还能怎么升级第二次作业交了不代表结束。我列了几个可以继续做的方向供你参考注册增加邮箱验证可以在不改表的前提下先做唯一性校验任务支持优先级和标签需要加字段并给列表页增加筛选利用 due_date 做逾期提醒在首页顶部显示过期未完成的任务部署到公网服务器用 gunicorn 加 nginx或者直接用 PythonAnywhere 免费跑起来给老师演示再加一个简单的“忘记密码”功能通过邮件重置链接涉及 session 和邮件库但可以放在下一次作业。我的建议是至少走一遍部署流程。第二次作业如果只在本地跑通演示时万一电脑出了状况就尴尬了。部署到免费平台后把链接发给老师体验完全不一样。部署看起来很吓人其实核心就是把你本地能跑的 Flask 应用放到一个能被公网访问的服务器上数据库可以继续用 SQLite 文件只要配置好路径权限就行。说实话第二次作业能教给你的东西比听十节课都多。我第一次做的时候总想着赶紧写完赶紧交结果每一步都在返工第二次耐下性子把需求拆清楚把坑记下来反而晚上十一点就收工了。写代码这件事就是这样越急越慢越稳越快。如果你现在正卡在第二次作业别慌先画功能脑图再搭骨架再填逻辑最后一轮一轮测。很多你以为的“我不会”其实只是“没拆细”。去写吧代码跑通的那一刻真的很有成就感。
返回列表