news 2026/10/15 4:22:53

用Flask+SQLite实现待办事项系统:第一次作业全流程拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Flask+SQLite实现待办事项系统:第一次作业全流程拆解

1. 项目整体设计与思路拆解

每年带新人或者回顾自己走过的路,“第一次作业”这四个字都让我感触特别深。很多人觉得第一次作业嘛,无非是把需求实现了就行,但真正动手时才发现,从“知道要做什么”到“代码能跑通”,中间隔着一条巨大的鸿沟。这里我想借一个很典型的案例——某高校软件工程课程的第一次作业“制作一个待办事项管理系统”,来拆解这个完整过程。这个选题特别适合当第一次作业,因为它麻雀虽小五脏俱全,涉及前端页面、后端逻辑、数据存储三个层面,又不会复杂到让人劝退。

先说说为什么第一次作业失败率那么高。我观察到的第一个原因,是需求理解偏差。作业描述里写着“实现待办事项的增删改查”,新手往往直接理解成“写几个按钮能点就行”,但导师真正想要的是数据能持久化,也就是说刷新页面之后数据还在。第二个原因,是急于写代码。很多人拿到作业就打开编辑器开始写,写到一半发现逻辑乱了、代码结构糊成一团,最后只能推倒重来。第三个原因,是环境问题一堆,虚拟环境没激活、依赖没装全、端口被占用,这些乱七八糟的环境问题消耗掉的精力,往往比写代码本身还多。

所以,针对这门课的第一次作业,我当时带的一位新手同学,我们花了整整一晚上把思路整理清楚,最终确定了“后端Flask + 前端Jinja2模板 + SQLite数据库”的技术方案。没选Node.js,也没选前后端分离的架构,就是刻意保持简单。为什么这么选?因为对于第一次作业来说,最大的目标不是炫技,而是完整体验一次“从零到一”的开发流程。Flask足够轻量,一个文件就能跑起服务;Jinja2模板直接在后端渲染页面,不需要考虑跨域问题;SQLite是文件型数据库,不需要额外安装数据库服务,对新手极其友好。

这个方案的优势是:技术栈简单,概念清晰,每一步都能被理解。它能避免什么问题?最大的避免就是“过度设计”——新手一上来就用微服务、消息队列、Redis缓存这些重型技术,完全是在给自己挖坑。我第一次作业时就犯过类似的错误,用了当时觉得特别酷的架构,结果三天没跑通,最后整个推翻。后来我明白了,第一次作业的核心价值在于“跑通全流程”,而不是“证明技术实力”。作业场景里的技术难点应该聚焦在“业务逻辑正确”和“数据能持久化”上,而不是“并发性能”或者“分布式部署”这种远超出能力范围的东西。

再来拆解一下具体的需求。一个标准的待办事项管理系统,核心功能层有这么几块:第一,展示所有待办事项列表;第二,新增一条待办事项;第三,将一条待办事项标记为已完成;第四,删除一条待办事项;第五,编辑一条待办事项的内容。听起来是不是很简单?但每一项背后都有细节。比如“新增”需要处理空内容的情况,“标记完成”需要告知用户操作成功还是失败,“删除”可能需要一个确认过程以防误操作,“编辑”则要考虑是弹窗修改还是跳转页面修改。这些全都是第一次作业中极容易忽略的点。

按照我的经验,第一次作业的思路拆解应该遵循一个原则:先把需求写成用户故事,再转成功能列表,最后再想技术实现。用户故事的角度是“用户想做什么”,功能列表的角度是“系统需要支持什么”,技术实现的角度是“用什么代码完成什么逻辑”。这三层转换做完,作业等于完成了一半。剩下的就是按图索骥,一步步把代码填进去。

1.1 为什么“先定接口”比“先写页面”更重要

这是我想单独拎出来强调的一点。很多新手拿到需求后的第一反应是“先把页面画出来”,但这样做会陷入一个尴尬的境地:页面画得再漂亮,后面接数据时发现接口对不上,又得返工。正确的做法是先定接口。接口是什么?就是前后端交互的契约——前端请求什么URL,传什么参数,后端返回什么格式的JSON或者渲染什么模板。

仍以待办事项管理系统为例,我们定下这么几个接口:GET /todos 获取全部待办列表;POST /todos 新增一条待办;PUT /todos/ /done 标记某条待办为已完成;PUT /todos/ /edit 编辑某条待办内容;DELETE /todos/ 删除某条待办。注意这里的 /todos/ 是一种路径参数写法,尖括号里的 id 会作为变量传给视图函数。为什么先定这个?因为在Flask里,路由和视图函数是成对出现的,路由定了,视图函数的参数和返回值也就基本定下来了。前端页面无论怎么改,只要确保表单提交到这些路由上就行。

我见过很多反面案例。某个作业是做一个博客系统,他先写了漂亮的HTML页面,然后把“发表文章”表单的action直接写成“submit”。结果后端怎么都接收不到数据,排查了半天才发现action路径错了。这就是典型的没有先定接口导致的问题。接口就是在需求和技术实现之间架一座桥,先把桥搭好,两岸怎么施工心里都有数。

另外一个原因是,先定接口方便并行开发。如果是分组作业,一个人负责前端模板,另一个人负责后端逻辑,接口约定好了,两边可以同时动工。虽然第一次作业通常不需要分工,但养成这个习惯非常有用。等到了团队项目阶段,接口设计文档就是团队的“宪法”,谁不遵守都得返工。

1.2 选型里的“克制”是一种能力

技术选型这部分我想多写几句。第一次作业用到的技术栈,我的核心建议是:用你最熟悉的东西。如果上课教的是Python,那就用Python的Web框架;如果上课教的是Java,那就用Servlet或者Spring Boot的入门版;如果完全自学,那就选社区文档最全的。对于绝大多数第一次作业来说,Flask或者Django的入门模式都是合适的选择。

我自己在指导过程中,有个新手非要挑战自我,选了一个很小众的异步Web框架,理由是“听说性能很好”。结果遇到问题去搜资料,搜出来的信息少得可怜,连官方文档的示例都是残缺的,卡了三个小时寸步难行。后来我帮他换回Flask,同样的功能半小时就实现了。这不是说小众框架不好,而是说第一次作业的试错成本太高了——你本来应该把精力花在理解增删改查的逻辑上,结果全被环境问题消耗掉了。

选型时的另一个“克制”是不要一上来就用数据库抽象层(ORM)去屏蔽SQL。我知道很多教程会推荐SQLAlchemy,说它简洁优雅。但对于第一次作业,我反而建议直接用SQLite自带的sqlite3模块,手写SQL语句。为什么?因为第一次作业的意义不是“用最少的代码实现功能”,而是“理解数据到底是怎么存进去、怎么取出来的”。手写SQL能让你清晰地知道INSERT、UPDATE、DELETE、SELECT这四类操作对应到代码里的哪一行,以后再用ORM时,你会知道它在背后默默帮你做了什么,遇到诡异问题时不至于完全抓瞎。

我印象很深,第一次作业时我用ORM,增删改查写得很爽。直到有一次数据莫名其妙丢了,我去看日志,才发现ORM生成的SQL语句里少了一个WHERE条件,导致批量更新把一个表的数据全改了。如果当时能读懂SQL,这个问题一眼就能看出来。所以我说,第一次作业不要过度依赖高级封装,该手写的地方就手写。这不是走回头路,而是在打地基。

2. 核心细节解析与实操要点

现在进入具体的核心细节环节。这里我把第一次作业里最容易出问题、也最值得讲透的几个点拿出来单独拆解。

2.1 环境准备:虚拟环境救了你无数次

先说环境。很多人第一次作业的第一道坎,不是代码,而是环境。我们假设你用的是Python,第一步绝对是创建虚拟环境。为什么需要虚拟环境?因为不同项目依赖的包版本可能是冲突的——某个项目需要Flask 2.0,另一个项目需要Flask 1.1,如果全装到全局环境里,升级一个项目时另一个就挂了。虚拟环境相当于给每个项目单独隔了一间房子,里面装什么都不会影响其他项目。

操作命令并不复杂:

# 创建名为 venv 的虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS / Linux) source venv/bin/activate # 安装依赖 pip install flask

激活之后,命令行前面会出现一个 (venv) 前缀,看到这个就说明你已经在虚拟环境里了。这里有一个新手都踩过的坑:忘了激活虚拟环境,直接pip install,装到了全局;然后运行代码时又用的是虚拟环境,模块找不到,报 ModuleNotFoundError。怎么排查?运行 pip list 看看当前环境里有没有Flask,再运行 which python(macOS/Linux)或 where python(Windows)看看当前用的是哪个Python解释器。

另外一个细节,如果你用的是PyCharm这类IDE,它其实已经帮你做了很多环境管理的工作。新建项目时可以选择虚拟环境,IDE会自动创建并激活。但从命令行的角度理解这个过程依然是重要的,因为有时候IDE抽风,你还是要回到命令行来排查问题。

我把环境相关的几个常见问题整理成了一个表格,第一次作业前对照着检查一下能省很多时间:

问题现象大概率原因快速排查方法
ModuleNotFoundError: No module named 'flask'虚拟环境未激活或未安装运行 pip list 确认;激活环境后重新 pip install
命令找不到 python环境变量未配置用 python3(macOS/Linux)或重新安装并勾选 Add to PATH
端口被占用无法启动上一次运行的服务未关闭换端口,如 app.run(port=5001),或查找并结束占用进程
运行报 Permission denied权限问题macOS/Linux 用 sudo,但更推荐换用户目录或调整文件夹权限

2.2 数据库设计:一张表就够,但字段要合理

第一次作业的数据库设计,应该用最简模型。待办事项系统只需要一张表,叫 todos。字段怎么定?我给出一个基础版本:

  • id:主键,自增整数。每条待办事项的唯一标识。
  • title:字符串,待办内容。不能为空。
  • completed:整数或布尔值,0表示未完成,1表示已完成。
  • created_at:时间戳,记录创建时间。

在实际建表时,SQL语句长这样:

CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, completed INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

这个设计里,为什么 completed 用 INTEGER 而不是 BOOLEAN?这是SQLite的特性和历史原因,SQLite没有独立的布尔类型,通常用0和1代替。你在操作时把它当成布尔值理解就行:读出来时做 if completed 判断,写进去时传1或0。

为什么需要 created_at?因为一个待办事项系统如果只能看到一串文字,没有时间概念,体验会差很多。加了创建时间之后,列表页可以按时间倒序排列,最新创建的显示在最上面,这属于用户可感知的细节提升。不用多想,直接使用 DEFAULT CURRENT_TIMESTAMP 就能在插入时自动写入当前时间。

还有一个细节,字段类型尽量用TEXT而不是VARCHAR,不是说不可以用VARCHAR,而是SQLite对VARCHAR的长度限制处理比较宽松,如果后来想扩展字段长度,TEXT更方便。第一次作业不需要纠结那么深,但少走弯路总是好的。表结构定了之后,初始化数据库的代码可以写在单独的文件里,比如 init_db.py,执行一次就好。运行时不需要每次重新建表,用 IF NOT EXISTS 就是防止重复建表覆盖数据的。

2.3 后端路由:视图函数的“三件套”

Flask后端开发的核心是写视图函数。一个视图函数通常包含三件套:接收请求参数、处理业务逻辑、返回响应。仍以待办系统为例,我们看几个核心路由的实现。

首先是获取列表:

@app.route('/todos') def list_todos(): conn = sqlite3.connect('todo.db') cur = conn.cursor() cur.execute('SELECT id, title, completed FROM todos ORDER BY id DESC') todos = cur.fetchall() conn.close() return render_template('index.html', todos=todos)

这里有几个新手容易忽略的点:第一,用完连接要关闭,虽然Python的垃圾回收机制会在一定程度上帮你收拾,但在连接池场景下不显式关闭会积累大量未释放连接,迟早出问题。第二,render_template 自动去 templates 目录找模板文件,如果你不小心把模板放在了 templates 之外的目录,会报 TemplateNotFound。第三,查询结果是一堆元组,如果在模板中直接用 todos[0] 这种下标方式取数据,可读性很差,所以后面我会提到在查询时加上一个转换步骤。

然后是新增待办:

@app.route('/todos', methods=['POST']) def add_todo(): title = request.form.get('title') if not title or not title.strip(): flash('待办内容不能为空') return redirect(url_for('list_todos')) conn = sqlite3.connect('todo.db') cur = conn.cursor() cur.execute('INSERT INTO todos (title) VALUES (?)', (title.strip(),)) conn.commit() conn.close() return redirect(url_for('list_todos'))

这里的问号占位符是一种参数化查询的写法,它的作用有两个:一是防SQL注入,二是不用手动拼字符串。新手经常图省事写成 f"INSERT INTO todos (title) VALUES ('{title}')",如果用户输入一条内容是 1'); DROP TABLE todos;-- 的待办,数据库表就直接没了。这不是危言耸听,这是教科书级别的安全漏洞。所以从第一次作业开始,就养成用 ? 占位符的习惯。

请求方式为什么要用POST而不是GET?这是HTTP协议的基本语义。GET请求是获取数据,不应该对服务器上的数据产生额外影响;POST是提交数据,适合创建新资源。如果全用GET,用户通过一个链接就能直接新增待办,谁都会觉得不对。我见过有新手用GET实现删除功能,结果搜索引擎爬虫顺着链接把所有数据都删了。这不是段子,是真实发生过的翻车事件。所以,会修改数据的操作一律用POST、PUT、DELETE,只有读取操作才用GET。

2.4 前端模板:Jinja2的几个“语法糖”

后端返回页面时用到Jinja2模板,第一次作业能用到的高级语法其实就那么几个:变量输出、for循环、if判断、url反转。大部分逻辑都在后端处理,模板只负责展示。

模板里最常用的循环长这样:

<ul id="todo-list"> {% for todo in todos %} <li> <span class="{% if todo[2] %}done{% endif %}">{{ todo[1] }}</span> {% if not todo[2] %} <a href="{{ url_for('mark_done', todo_id=todo[0]) }}">完成</a> {% endif %} <a href="{{ url_for('delete_todo', todo_id=todo[0]) }}">删除</a> </li> {% endfor %} </ul>

有人可能会注意到我前面提到查询返回的是元组,在模板里用 todo[0]、todo[1] 这种下标访问很丑,而且一旦字段顺序变了,所有下标都要跟着变。怎么优化?有一个简单办法:把返回结果转成字典。改动很小,在查询后加一步处理:

def dict_from_row(row): return {'id': row[0], 'title': row[1], 'completed': row[2]} todos = [dict_from_row(row) for row in cur.fetchall()]

这样模板里就能写成 todo.title、todo.completed,可读性好很多。第一次作业可能体会不到这个好处,等你做第二个项目时,模板里写满了魔法数字一样的下标,就会想起这里的转换步骤。

url_for 是另一个值得养成的习惯。不要在前端模板里硬编码 /todos/{{ todo[0] }}/delete,而是用 url_for('delete_todo', todo_id=todo['id'])。硬编码的问题在于:如果后端的路由路径改了,前端所有写死的链接全都要手动改,漏掉一个就出404。url_for 是根据视图函数名动态生成URL,改了路由路径,只要没改函数名,模板里的链接自动更新,省心许多。

3. 实操过程与核心环节实现

这一部分带着大家把整个流程走一遍。我会从项目结构开始,一步一步展示如何从零把第一次作业做出来。即使你是第一次写Web应用,按着这份步骤走,也能在半小时内看到页面跑起来。

3.1 从空白文件夹到“hello world”

先建立一个项目目录。我习惯是这么组织:

todo_app/ ├── app.py # Flask主应用,路由和业务逻辑都写这里 ├── init_db.py # 数据库初始化脚本,执行一次即可 ├── todo.db # SQLite数据库文件(首次运行 init_db.py 后生成) └── templates/ └── index.html # 待办事项页面模板

新建文件后,先不要急着写业务代码。第一步是让Flask先跑起来,哪怕只输出一个hello world页面。为什么?因为环境问题都要在最早阶段暴露出来。如果这时app.run能成功启动,说明虚拟环境没问题、Flask装好了、端口没被占用,后续可以集中精力写业务逻辑。

from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello, 第一次作业!' if __name__ == '__main__': app.run(debug=True)

运行 python app.py 后,浏览器打开 http://127.0.0.1:5000/ ,看到Hello world页面就说明整个链路是通的。这里 debug=True 打开了调试模式,代码改动后服务器会自动重启,省去了手动重启的繁琐。但注意,调试模式只建议在开发环境使用,部署上线时绝对要关掉。

3.2 初始化数据库并验证表结构

环境通了之后,写数据库初始化脚本。这个脚本可以单独运行,也可以和主应用合并,我建议单独放一个文件,因为它的职责就是“初始化”,不需要每次启动应用都执行。初始化脚本里,先删除旧表再建新表,这样能保证每次跑这个脚本时得到一个干净的状态,适合开发阶段反复测试。

import sqlite3 conn = sqlite3.connect('todo.db') cur = conn.cursor() cur.execute('DROP TABLE IF EXISTS todos') cur.execute(''' CREATE TABLE todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, completed INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() print('数据库初始化完成')

运行之后,如果你已经装了一些数据库查看工具(比如可视化工具DB Browser for SQLite),可以直接打开 todo.db 看看表结构对不对。没有可视化工具的也没关系,可以用命令行验证:

sqlite3 todo.db .tables .schema todos

看到 todos 表以及字段定义,说明数据库这步过了。这里有一个细节:执行完数据库操作后,提交(commit)了没有?新手经常忘记 commit,结果数据没写进去。SQLite里不显式commit,数据只存在于事务缓存中,程序崩溃或连接关闭时数据就丢了。每次执行增删改之后都要 commit,这点非常重要。

3.3 把四个核心操作做成可点击页面

数据库和初始路由都就绪后,接下来才是真正的作业主体部分。主应用 app.py 里总共完成四个核心操作:读取列表、新增、标记完成、删除。我把完整代码写出来,这个版本经过数次简化,每一行都是有必要的:

import sqlite3 from flask import Flask, request, redirect, url_for, render_template app = Flask(__name__) app.secret_key = 'some_secret_key' # flash 消息需要用到 DB_NAME = 'todo.db' def get_db(): conn = sqlite3.connect(DB_NAME) conn.row_factory = sqlite3.Row return conn def dicts_from_rows(rows): return [dict(row) for row in rows] @app.route('/') def index(): conn = get_db() cur = conn.cursor() cur.execute('SELECT id, title, completed FROM todos ORDER BY created_at DESC') todos = dicts_from_rows(cur.fetchall()) conn.close() return render_template('index.html', todos=todos) @app.route('/todos', methods=['POST']) def add_todo(): title = request.form.get('title', '').strip() if not title: return redirect(url_for('index')) conn = get_db() cur = conn.cursor() cur.execute('INSERT INTO todos (title) VALUES (?)', (title,)) conn.commit() conn.close() return redirect(url_for('index')) @app.route('/todos/<int:todo_id>/done') def mark_done(todo_id): conn = get_db() cur = conn.cursor() cur.execute('UPDATE todos SET completed = 1 WHERE id = ?', (todo_id,)) conn.commit() conn.close() return redirect(url_for('index')) @app.route('/todos/<int:todo_id>/delete') def delete_todo(todo_id): conn = get_db() cur = conn.cursor() cur.execute('DELETE FROM todos WHERE id = ?', (todo_id,)) conn.commit() conn.close() return redirect(url_for('index')) if __name__ == '__main__': app.run(debug=True)

注意这里我用到了 conn.row_factory = sqlite3.Row,再配合 dict(row) 转成字典,模板里就能用 todo.title 这种属性风格了。另外,mark_done 和 delete_todo 用的是GET请求,我前面提到修改数据应该用POST,但在这个简化项目里,为了减少前端表单的复杂度,我故意用了GET链接。等你学到后面,可以改成更规范的POST表单或者AJAX请求。第一次作业不用一头扎进最佳实践,先把流程跑通。

模板文件如下:

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>待办事项</title> <style> body { font-family: sans-serif; max-width: 600px; margin: 50px auto; } .done { text-decoration: line-through; color: #999; } li { margin: 8px 0; } </style> </head> <body> <h1>我的待办</h1> <form action="{{ url_for('add_todo') }}" method="post"> <input type="text" name="title" placeholder="输入新待办" required> <button type="submit">新增</button> </form> <ul> {% for todo in todos %} <li> <span class="{% if todo.completed %}done{% endif %}">{{ todo.title }}</span> {% if not todo.completed %} <a href="{{ url_for('mark_done', todo_id=todo.id) }}">完成</a> {% endif %} <a href="{{ url_for('delete_todo', todo_id=todo.id) }}">删除</a> </li> {% else %} <li>暂无待办,快添加一条吧</li> {% endfor %} </ul> </body> </html>

这版页面很朴素,但功能完整。把 app.py 和 index.html 都保存好,重新刷新浏览器,现在应该能看到新增、完成、删除三个操作都能生效。跑通的那一刻,那种成就感是真的不一样——第一次作业的完成感,不是简单一句话能说出来的。

3.4 从“能跑”到“自己看着舒服”

功能完整之后,如果学有余力,可以给页面做一点美化。我第一次作业时给列表加了优先级标记,用红色标签表示高优先级、黄色表示中优先级、绿色表示低优先级,然后把完成项置灰。这种小改进不需要引入任何前端框架,纯CSS就能搞定。加了之后页面立刻活了起来,作业展示时也更有说头。

更关键的,如果时间充裕,给新增操作加上一个校验失败的提示。比如输入框为空时点击新增,刷新页面闪一下,但其实什么都没加,用户会觉得很困惑。Flask的 flash 消息机制就是干这个的。在视图函数里做判断,然后模板里显示提示:

from flask import flash # 在 add_todo 里 if not title: flash('待办内容不能为空') return redirect(url_for('index'))

模板里加一段:

{% with messages = get_flashed_messages() %} {% if messages %} <ul class="flash"> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %}

这就要用到 app.secret_key,不然flash会报错。这只是一个小例子,说明第一次作业不应该止步于“能用”,而应该尽量考虑用户在使用过程中会遇到的边界情况。你做得越细,锻炼越大。

4. 常见问题与排查技巧实录

这一部分把我见到的、自己和身边人踩过最多的坑集中整理出来。如果你在做第一次作业时遇到问题,先对照这个清单排查,大概率能省下找导师或翻文档的时间。

4.1 环境与启动阶段的玄学问题

  • 端口被占用。启动时报错“Address already in use”,说明5000端口被之前的进程占用了。最简单的方法是换端口,app.run(port=5001)。查找占用端口的进程,macOS/Linux用 lsof -i :5000,Windows用 netstat -ano | findstr 5000。开发阶段换端口就好,不用刻意去纠结“杀掉进程”。

  • 浏览器缓存旧页面。改了代码但页面还是旧的,这可能是浏览器缓存导致。按Ctrl+Shift+R强制刷新(macOS是Cmd+Shift+R),或者打开调试模式后点击Flask日志里提示的“Restarting with reloader”。大多数时候,代码没生效都是浏览器缓存和服务器没重启共同作用的后果。

  • 模板修改不生效。Jinja2会缓存模板,但在debug模式下Flask会定期检查模板文件的变化并自动重载。如果明明改了模板却看不到变化,尝试关闭浏览器标签页重新打开,或者把模板里的一个空格去掉保存再恢复保存来触发重载。

  • 中文乱码。HTML文件编码问题。在模板头部加上 ,同时确保编辑器保存文件时编码是UTF-8而不是GBK。这个问题在Windows上特别常见,有时候页面能显示但中文是乱码,十有八九是文件保存编码的问题。

4.2 数据操作中的问题

这一部分是数据相关的高频踩坑点。

  • 数据没写进数据库。最大的可能是没有调用 conn.commit()。SQLite支持事务,不提交就看不到数据变化。我在代码里每次增删改后都写了commit,但如果你自己另写代码,很容易漏掉这一步。

  • 数据能写入但列表显示空白。可能是查询时字段名对不上。如果你在模板里写了 todo.title,但查询返回的是元组而非字典,模板里就取不到值。解决办法是用 row_factory = sqlite3.Row,或者手动把元组转成字典。

  • 删除后列表不刷新。这与数据写入无关,大概率是你在删除后忘了 return redirect(url_for('index')),导致页面停留在空白。视图函数要有明确的返回,无论成功或失败,重定向到列表页才能让用户看到最新状态。

  • 反转URL报错。url_for('delete_todo', todo_id=1) 报 BuildError,说明视图函数名拼错了,或者该函数名不存在。检查 app.py 里对应的 @app.route 函数,确认 def delete_todo 的名字与 url_for 里用的字符串一致。

  • 表格里的特殊字符导致异常。比如用户输入了引号、尖括号,在页面上如果直接输出,可能被解析成HTML标签,造成样式错乱,这被称为XSS漏洞。安全的做法是在模板中使用自动转义,Jinja2默认会转义 {{ todo.title }} 中的HTML字符,但千万不要用 |safe 过滤器,除非你明确知道内容是安全的。第一次作业可能不会遇到攻击,但这个意识要建立起来。

4.3 逻辑与流程的坑

  • 新增空内容。输入框里只敲几个空格,后端接收到的内容看起来非空,但实际上全是空白字符。处理办法是 title.strip() 去掉首尾空格后再判断。这个细节很小,但我在实际使用中发现非常多第一次作业的代码都会忽略它。

  • 完成状态无法切换回来。我的设计里,待办标记为完成后不再提供“撤销”链接。如果你想支持取消完成,只需要把 mark_done 改成 toggle 操作,根据当前状态置0或置1。第一次作业可不是不能做这个功能,只是范围需要控制好。如果作业描述中没有明确要求支持“取消完成”,可以先不做,但你自己要明白怎么扩展。

  • 没有做异常处理。数据库文件被误删、数据库被外部锁定(比如用可视化工具打开着)、磁盘空间不足,这些情况都会让程序直接抛异常。第一次作业可以先不处理所有异常,但至少要清楚:如果代码没有 try/except,出问题时控制台会打印一条红色Traceback,阅读Traceback的能力是写代码的基础功。报错信息再长,核心就是最后一行 Exception: 后面的内容,从那里开始分析。

以上是我在第一次作业中积累下来的经验。其实说到底,第一次作业最宝贵的收获不是做出一个多炫酷的功能,而是完整地走完一个项目从需求分析、技术选型、编码实现到调试交付的整个流程。很多人第一次接触全流程时会觉得繁琐,但恰恰是这份繁琐,让你真正理解了软件开发不是“写代码”三个字可以概括的。根据我个人的体会,做完第一次作业之后,再去做第二个项目时,你的心里会非常有底,因为你已经知道了“路是怎么走的”。

如果你此刻正卡在自己的第一次作业上,我希望这份记录能帮你少走一点弯路。如果时间有限,记住三个要点:第一,先定接口再写页面;第二,动手前把环境问题全部解决,不要让环境白费你两个小时;第三,每一段代码都要自己读一遍,理解它在做什么,而不是盲目复制。这三点做到了,你的第一次作业就已经赢过了很多同龄人。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 4:22:13

Windows远程桌面文件传输:驱动器映射实现双向拷贝

Windows远程桌面文件传输教程&#xff08;驱动器映射传输&#xff09;每次远程到服务器或者同事的电脑处理问题&#xff0c;文件来回拷都是最头疼的事。以前我习惯顺手开个网盘或者临时搭个FTP&#xff0c;后来发现折腾半天不如直接用远程桌面自带的驱动器映射来得干脆。这功能…

作者头像 李华
网站建设 2026/10/15 4:22:00

Flutter迁移OpenHarmony:启动、列表与图片内存性能优化

做内容型App&#xff0c;尤其是三国杀武将攻略、卡牌图鉴这类“高频滑动多图列表详情页穿梭”的场景&#xff0c;性能问题总是比功能需求更早爆发。我前阵子把一个原本跑在Android和iOS上的Flutter攻略应用&#xff0c;整体迁移到Flutter for OpenHarmony环境里&#xff0c;期间…

作者头像 李华
网站建设 2026/10/15 4:22:00

AI写专著不靠万能对话框:五类工具链搭建与长程一致性实战

这两年我越来越频繁地被同一个问题找上门&#xff1a;AI到底能不能帮我写一本书&#xff1f;每次我都会先反问一句——你说的“帮”&#xff0c;是帮你打字&#xff0c;还是帮你把整本书组织起来&#xff1f;如果是后者&#xff0c;那答案就远不是“选哪一款AI”这么简单&#…

作者头像 李华
网站建设 2026/10/15 4:21:34

正态随机过程仿真:三种工程化生成方法与验证指南

简介&#xff1a;本资源是湖南大学《随机信号分析与处理》课程配套实验报告&#xff0c;面向电子信息、通信工程等专业本科生及信号处理初学者&#xff0c;聚焦正态随机过程的建模仿真与统计特性验证。报告完整覆盖实验目的、要求、MATLAB实现代码&#xff08;含均匀/正态序列生…

作者头像 李华
网站建设 2026/10/15 4:21:04

企业门户网站开发实战:Django + Vue 前后端分离与部署指南

说起来&#xff0c;企业门户网站算是 Web 开发里“看着简单、做起来碎活儿一堆”的项目类型。无非是首页、新闻、产品、关于我们、联系页面&#xff0c;再加一个能维护内容的后台。可真等你去设计数据库表、处理图片上传、调跨域、考虑部署环境的时候&#xff0c;就会发现每一环…

作者头像 李华