Python+Flask任务清单管理系统:从骨架搭建到部署答辩全攻略
2026/9/15 16:09:26 网站建设 项目流程

简介:基于Python Flask框架设计并实现的任务清单管理系统,是一份经过真实答辩检验的高分毕业设计源码,适合计算机相关专业学生用于毕业设计、课程设计或前后端项目实践。项目目录将应用入口、配置模块、认证逻辑、待办事项业务、单元测试等拆分为独立文件,使用SQLite作为轻量级数据库,并通过requirements.txt管理第三方依赖,结构清晰,便于按功能点阅读和二次开发。压缩包共21个文件,主体为12个Python源码文件,另含6个pyc编译缓存、1个sqlite数据库、1个txt使用说明,整体仅15KB,轻量精简。资源已在Windows 10/11环境下完成调试,下载即可运行,部署文档齐全。目前已有90人学习/下载。该案例获得导师认可,答辩评审分达97分,从项目分层、接口设计到测试用例均有可复用的工程实践价值,尤其适合希望快速掌握Flask开发流程、需要参考高评分毕业设计范式的读者。

1. 为什么毕设课设都绕不开这个 Flask 任务清单

一个任务清单管理系统,如果只做增删改查,很多人两小时就写完了;但拿去答辩,问题往往不在功能,而在“能不能讲清楚、换台机器能不能跑起来”。基于Python+Flask的任务清单管理,正好卡在毕业设计最舒服的位置:Flask 框架足够轻,每个请求怎么进来、模型怎么映射、视图返回什么,都一句话说得明白;任务清单又是典型的业务模型,天然覆盖用户、状态、时间戳这些常规字段。这篇内容面对两类人:一是拿到现成源码想快速改造成自己项目的人,二是想系统做一遍 Flask 管理系统的工程师。下面直接按“骨架—数据—接口—交付”的顺序,把常见做法和该避开的坑一次说清。

2. 搭出 Flask 任务清单的可运行骨架:依赖、应用工厂与启动命令

2.1 依赖先收拢,别让 requirements.txt 形同虚设

很多毕设项目的依赖文件只有 Flask 一行,答辩现场装完才发现少这个包少那个包。一个任务清单管理项目,至少应该把这几项锁在 requirements.txt 里:

依赖包用途少了会怎样
Flask路由、模板、请求响应上下文其他全部无从谈起
Flask-SQLAlchemyORM 与表模型映射手写 SQL 并自行管理连接
Flask-Login登录会话与页面权限标记手写 session 标志和状态保持
Flask-WTF表单解析与 CSRF 防护表单校验和跨站请求防护都要自己做
Flask-Migrate表结构迁移改一次表结构就要删库重建

装环境是老话题了,python 安装认准官方渠道,VSCode 里配置 python 环境后直接pip install -r requirements.txt即可。Flask-SQLAlchemy 用 3.x 时要注意初始化方式从db = SQLAlchemy(app)改成db = SQLAlchemy()db.init_app(app),这是老代码迁移时最常见的编译期不报错、运行时报错的问题。

2.2 应用工厂:把配置收进一个函数

我一般会把应用创建过程写成一个工厂函数,而不是在模块顶层直接app = Flask(__name__)。原因有三个:测试环境可以传不同配置;蓝图注册顺序一目了然;避免 db、migrate 等对象和 app 互相循环导入。

# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_wtf import CSRFProtect from flask_login import LoginManager db = SQLAlchemy() migrate = Migrate() csrf = CSRFProtect() login_manager = LoginManager() def create_app(config_name="default"): app = Flask(__name__, instance_relative_config=True) if config_name == "production": app.config.from_pyfile("config_prod.py", silent=True) else: app.config.from_mapping( SECRET_KEY="dev-secret-change-me", SQLALCHEMY_DATABASE_URI="sqlite:///" + app.instance_path + "/todo.sqlite", SQLALCHEMY_TRACK_MODIFICATIONS=False, ) db.init_app(app) migrate.init_app(app, db) csrf.init_app(app) login_manager.init_app(app) from app.tasks.views import task_bp from app.auth.views import auth_bp app.register_blueprint(auth_bp, url_prefix="/auth") app.register_blueprint(task_bp, url_prefix="/tasks") @app.get("/healthz") def healthz(): return {"code": 0, "message": "ok"} return app

instance_relative_config=True表示实例文件夹和配置文件都放在 instance 目录下,避免开发配置被提交进仓库。from_mapping适合写少量默认值,from_pyfile适合正式环境单独维护一个不进入版本库的配置。蓝图在init_app之后 import,是因为视图文件里会引用dblogin_manager,延迟 import 可以避开循环依赖。

2.3 启动命令参数别只会回车

入口文件 wsgi.py 通常只有两行:

# wsgi.py from app import create_app app = create_app("default")

命令行启动时,参数要能说清楚为什么这么写:

python -m venv .venv source .venv/bin/activate pip install -r requirements.txt export FLASK_APP=wsgi.py export FLASK_DEBUG=1 flask run --host 0.0.0.0 --port 8000

FLASK_APP指定应用入口,FLASK_DEBUG=1开启热重载,改完代码不用手动重启。--host 0.0.0.0让局域网内其他机器也能访问,答辩时老师手机直接扫 IP 加端口就能看到页面。--port 8000是为了避开默认的 5000,macOS 上 AirPlay 接收器经常占用 5000 端口,现场改端口非常尴尬。

提示:Flask 2.x 之后,也可以直接flask --app wsgi:app --debug run --host 0.0.0.0 --port 8000,效果相同。

启动前先跑一个健康检查,确认骨架是通的:

curl -s http://127.0.0.1:8000/healthz -o /dev/null -w "%{http_code}\n" # 输出 200 说明应用工厂工作正常

这一步排错通常也就两个方向:模块路径写错,或 instance 目录不存在。前者看报错栈里的 import 行,后者在项目根目录手动建 instance 文件夹即可。

3. 任务清单的数据模型:字段取舍、状态迁移与种子数据

3.1 十个字段怎么定

任务清单的核心不是增删改查,而是“任务”这个实体怎么建模。字段太少,演示时没有可讲的内容;字段太多,答辩时自己都记不住。

字段类型与约束作用
idInteger 主键唯一标识
titleString(120) 非空任务标题
detailText 可空任务详情
statusString(20) 非空pending / doing / done / archived
priorityInteger 默认 00 普通,1-5 依次升高
due_atDateTime 可空截止时间
owner_idInteger 外键 user.id归属用户
completed_atDateTime 可空完成时刻,用于统计
created_atDateTime 默认当前时间创建时间
deleted_atDateTime 可空软删除标记

优先级用整数而不是字符串,因为排序天然按数值走,免去把“高”“中”“低”映射成数字的步骤。completed_at 单独存,而不是认为 status 变成 done 就够,因为“任务完成的时间点”是后续做效率统计的唯一数据来源,靠日志回溯不现实。deleted_at 是软删除标记,给答辩演示“回收站式删除”时很有说服力。

3.2 把模型写成可读性强的代码

# app/models/task.py from datetime import datetime from app import db class Task(db.Model): __tablename__ = "task" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(120), nullable=False) detail = db.Column(db.Text, nullable=True) status = db.Column(db.String(20), nullable=False, default="pending") priority = db.Column(db.Integer, default=0) due_at = db.Column(db.DateTime, nullable=True) owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) completed_at = db.Column(db.DateTime, nullable=True) deleted_at = db.Column(db.DateTime, nullable=True) def to_dict(self): return { "id": self.id, "title": self.title, "status": self.status, "priority": self.priority, "due_at": self.due_at.isoformat() if self.due_at else None, "created_at": self.created_at.isoformat(), "completed_at": self.completed_at.isoformat() if self.completed_at else None, }

nullable=False配合default是两个不同的东西:default 只影响构造时自动填值,数据库层面仍然允许空;要让数据库拒绝空值,必须显式声明nullable=Falsedatetime.utcnow在 Python 3.12 会有弃用警告,如果不想看到警告,改成datetime.now(timezone.utc),但注意后者存入 SQLite 时带时区信息,读取时要保持一致。

to_dict()的价值在接口返回时体现:视图函数只需要return jsonify(task.to_dict()),不用在路由里反复拼字典。这个模式在 Flask 里叫序列化方法,字段多了以后比到处手动构造 JSON 好维护得多。

3.3 状态迁移单独写一张允许表

任务状态如果散落在视图函数里写成if status == "doing": ...,加一个新状态时要翻遍所有路由。我一般会单独建一个状态机常量文件:

# app/tasks/constants.py TASK_STATUS = ("pending", "doing", "done", "archived") ALLOWED_TRANSITIONS = { "pending": ("doing", "done", "archived"), "doing": ("done", "pending"), "done": ("archived", "pending"), "archived": ("pending",), } def can_change_status(current, target): return target in ALLOWED_TRANSITIONS.get(current, ())

规则都集中在这个表里,比如已归档的任务只能重新打开,不能直接跳到完成;已完成的任务不能撤回成待办。改业务规则时只动这个字典,不需要动视图。

3.3.1 用迁移而不是删表重建

很多人开发中改字段后直接db.drop_all()db.create_all(),数据全丢。规范做法是用 Flask-Migrate 生成迁移脚本:

flask --app wsgi:app db init flask --app wsgi:app db migrate -m "create task table" flask --app wsgi:app db upgrade

migrate会根据模型和当前库的差异自动生成脚本,upgrade把脚本应用到数据库。之后改了字段,重复执行 migrate 和 upgrade 就行。毕设里这样做,答辩时可以说“数据库结构可版本化”,这个点比功能本身更让老师眼前一亮。

提示:db.create_all() 只能建表不能改表,适合本地快速验证;正式提交代码时,启动文档里应写 migrate 流程。

3.4 种子数据让演示不用现场敲键盘

# scripts/seed.py from datetime import datetime from app import create_app, db from app.models.task import Task def seed(): app = create_app("default") with app.app_context(): if Task.query.first(): print("已有数据,跳过") return now = datetime.utcnow() db.session.add_all([ Task(title="写开题报告", status="done", due_at=now, priority=3), Task(title="搭Flask骨架", status="doing", due_at=now, priority=1), Task(title="录演示视频", status="pending", due_at=now, priority=2), ]) db.session.commit() print("种子数据写入完成") if __name__ == "__main__": seed()

三种状态各一条,演示筛选时点一下按钮就有区分度。开头的if Task.query.first()是幂等检查,重复执行不会插入重复数据。执行命令是python scripts/seed.py,注意一定在app.app_context()里操作数据库,否则会报“Working outside of application context”的错误。

4. 业务接口这样写:登录、CRUD、筛选分页一个不少

4.1 蓝图按资源拆,不按功能拆

任务清单最常见的接口划分:

方法路径视图函数是否登录
POST/auth/loginauth.login
POST/auth/logoutauth.logout
GET/taskstask.list_tasks
POST/taskstask.create_task
GET/tasks/ int:task_idtask.get_task
PUT/tasks/ int:task_idtask.update_task
DELETE/tasks/ int:task_idtask.delete_task

资源名放在路径中,动作通过 HTTP 方法表达,这是 REST 风格的基本约定。Flask 的@auth_bp.post("/login")这种写法从 2.0 开始支持,比@auth_bp.route("/login", methods=["POST"])短一截,也更好读。

4.2 登录会话:毕设场景别上 JWT

任务清单是典型的第一方应用,页面和接口同源部署,用 Flask-Login 的 session 认证就够了。JWT 适合多端分离的场景,放在这里只会增加答辩时被追问的风险。

# app/auth/views.py from flask import Blueprint, request, jsonify from flask_login import login_user, logout_user, login_required from app.models.user import User auth_bp = Blueprint("auth_bp", __name__) @auth_bp.post("/login") def login(): data = request.get_json(silent=True) or request.form user = User.query.filter_by(username=data.get("username")).first() if user is None or not user.check_password(data.get("password", "")): return jsonify(message="用户名或密码错误"), 401 login_user(user, remember=bool(data.get("remember"))) return jsonify(message="登录成功") @auth_bp.post("/logout") @login_required def logout(): logout_user() return jsonify(message="已退出")

request.get_json(silent=True) or request.form的写法让接口同时接受 JSON 和表单两种提交方式,Postman 测试和页面表单都能用。login_userremember参数对应 Flask-Login 的记住我功能,实现原理是往 session 里写一个带过期时间的 cookie,不需要自己维护 token 表。check_password是 User 模型里的方法,用werkzeug.security.generate_password_hashcheck_password_hash实现,不要存明文密码。

开了 CSRFProtect 后,表单提交需要在模板里加{{ csrf_token() }},纯 JSON 请求需要在请求头里带X-CSRFToken。这是 Flask-WTF 默认的校验机制,接口测试时容易踩坑。

4.3 列表接口用查询参数做筛选与分页

列表页是任务清单最核心的接口,参数设计直接决定前端好不好写:

# app/tasks/views.py from flask import Blueprint, request, jsonify from flask_login import login_required, current_user from app import db from app.models.task import Task task_bp = Blueprint("task_bp", __name__) SORT_COLUMNS = { "priority": Task.priority, "due_at": Task.due_at, "created_at": Task.created_at, } @task_bp.get("/") @login_required def list_tasks(): q = Task.query.filter( Task.owner_id == current_user.id, Task.deleted_at.is_(None), ) status = request.args.get("status") if status: q = q.filter_by(status=status) keyword = request.args.get("keyword") if keyword: q = q.filter(Task.title.like(f"%{keyword}%")) page = request.args.get("page", 1, type=int) per_page = min(request.args.get("per_page", 10, type=int), 50) sort = request.args.get("sort", "created_at") q = q.order_by(SORT_COLUMNS.get(sort, Task.created_at).desc()) p = q.paginate(page=page, per_page=per_page, error_out=False) return jsonify( items=[t.to_dict() for t in p.items], page=p.page, pages=p.pages, total=p.total, )

Task.deleted_at.is_(None)是软删除查询的固定写法,把已删除的数据挡在结果集外。per_pagemin(..., 50)钳位,防止有人传per_page=10000一次性拉全表。sort参数必须走SORT_COLUMNS白名单,不能直接拼进order_by,否则会被 SQL 注入利用。paginate(error_out=False)表示页码超出范围时返回空页而不是抛 404,前端翻页体验更平滑。

创建任务的接口同样要校验入参:

@task_bp.post("/") @login_required def create_task(): data = request.get_json(silent=True) or request.form title = (data.get("title") or "").strip() if not title or len(title) > 120: return jsonify(message="标题不能为空且不超过120字"), 400 task = Task( title=title, status="pending", priority=int(data.get("priority", 0) or 0), owner_id=current_user.id, ) db.session.add(task) db.session.commit() return jsonify(task.to_dict()), 201

str.strip()去掉首尾空格,避免“标题只有空格”这种脏数据入库。int(data.get("priority", 0) or 0)兼容了 priority 传空字符串的情况,Python 类型转换在这里要防御一下,不能直接int(data.get("priority"))然后等异常。

4.4 更新和删除的边界控制

@task_bp.put("/<int:task_id>") @login_required def update_task(task_id): task = db.get_or_404(Task, task_id) if task.owner_id != current_user.id: return jsonify(message="无权操作"), 403 data = request.get_json(silent=True) or request.form new_status = data.get("status") if new_status and not can_change_status(task.status, new_status): return jsonify(message="非法状态流转"), 400 if new_status == "done" and task.completed_at is None: task.completed_at = datetime.utcnow() task.title = (data.get("title") or task.title).strip() task.status = new_status or task.status db.session.commit() return jsonify(task.to_dict()) @task_bp.delete("/<int:task_id>") @login_required def delete_task(task_id): task = db.get_or_404(Task, task_id) if task.owner_id != current_user.id: return jsonify(message="无权操作"), 403 task.deleted_at = datetime.utcnow() db.session.commit() return jsonify(message="已删除"), 200

db.get_or_404是 Flask-SQLAlchemy 3.x 提供的快捷方法,查不到直接返回 404,不用自己 try except。删除走软删除而不是db.session.delete(task),保证了演示时误删数据还能从数据库恢复。completed_at只在状态变成 done 时写一次,避免重复更新。注意datetime.utcnow在 Python 3.12 有弃用警告,可以统一封装一个now()工具函数,后续更换时只动一处。

5. 让“高分毕设”经得起验收:文档、部署与自动化验证

5.1 README 的启动文档写什么

源码能不能在陌生环境跑起来,是毕设评分最直接的加分项。README 里至少要有一节完整的启动命令序列,按“python 安装 → 虚拟环境 → 依赖 → 数据库 → 种子数据 → 启动”的顺序写:

python -m venv .venv source .venv/bin/activate pip install -r requirements.txt flask --app wsgi:app db upgrade python scripts/seed.py export SECRET_KEY=your-random-secret flask --app wsgi:app run --host 0.0.0.0 --port 8000

这段内容对应三件事:让人能跑起来、让人能拿到演示数据、让人知道正式运行时该设什么环境变量。VSCode 用户打开项目时会自动识别.venv,在状态栏切换解释器即可,不需要额外写进文档。

5.2 换一个 WSGI 服务器跑给老师看

flask run自带的是开发服务器,换到别的机器或局域网演示时并发一高就卡。交付前我会改用 waitress 把项目变成正式服务:

pip install waitress waitress-serve --listen=0.0.0.0:8080 --threads=4 wsgi:app

--listen=0.0.0.0:8080监听所有网卡,局域网内可通过宿主机 IP 访问;--threads=4开四个工作线程,撑住几十个人的访问压力没有问题。Windows 上 waitress 是首选,Linux 环境则更常见用gunicorn -w 2 -b 0.0.0.0:8080 wsgi:app,参数含义相同。

5.3 用 pytest 验一条完整链路

接口写完了不能只靠浏览器手点。写一个冒烟测试,验证健康检查和创建任务两个关键路径:

# pytest_smoke.py import pytest from wsgi import app @pytest.fixture def client(): app.config["TESTING"] = True app.config["WTF_CSRF_ENABLED"] = False return app.test_client() def test_healthz(client): rv = client.get("/healthz") assert rv.status_code == 200 assert rv.json["code"] == 0 def test_create_task_requires_login(client): rv = client.post("/tasks", json={"title": "测试任务"}) assert rv.status_code == 401

测试里把 CSRF 关掉,因为测试模拟的是接口调用而不是浏览器页面。test_create_task_requires_login验证了未登录用户不能创建任务,这条断言保证了登录装饰器是真的生效了,而不是形同虚设。运行命令收尾:

python -m pytest pytest_smoke.py -v

把这条命令写进 README 的“验收方式”一节,答辩时先跑测试再演示功能,比口头解释“代码能跑”有说服力得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询