资讯动态

Flask毕业设计任务清单:从蓝图拆分到安全部署的高分项目实践

发布时间:2026/9/15 19:40:26 来源:尧图企业网站定制
简介面向计算机专业毕业设计的任务清单管理项目基于PythonFlask实现后端并结合Vue技术栈已在Windows10/11环境完成调试答辩评审分达97分可下载即用。资源包共21个文件其中12个为Python源码另含6个编译缓存、SQLite数据库、配置文件和说明文档整体体积仅15KB结构紧凑。目前已有90人学习/下载。项目涵盖应用入口、待办事项与用户认证等模块并配有测试用例代码划分清晰随包附带使用文档和部署教程既可直接用于毕业设计或课程设计参考也适合Flask初学者对照学习任务增删改查、数据库交互及单元测试的完整流程具备较好的二次开发空间。1. 毕业设计里Flask任务清单为什么能成为“高分安全牌”一个常见误区是Flask太轻了拿来当毕设会被导师质疑“工作量不够”。实际上在课程设计和毕业设计场景里Flask恰恰比Django更占优势——项目结构一目了然、运行链路短、每一条路由和每次数据库操作都能在答辩时讲清楚这对评委来说比“用框架自动生成了一个大项目”更有说服力。任务清单管理Todo List是所有Web框架的经典练手项目功能边界清晰天然覆盖增删改查、表单提交、登录态、数据持久化这几个必考点非常适合作为Flask入门的完整收尾项目。这篇博文不针对某个具体压缩包而是从零讲清楚一个“高分优秀项目”应该具备的完整形态工厂模式组织应用、蓝图拆分业务、SQLite做持久化、Werkzeug做密码散列、会话管理、CSRF防护再到pytest测试和交付文档怎么写。这套流程跑通之后你拿到的任何一份Flask任务清单源码都能看懂结构也能自己把它从“能跑”改到“能答辩”。新手可以照着章节顺序一步步复现有经验的读者可以重点看参数选型和那些你在教程里不容易查到的边界细节。2. Flask项目骨架与蓝图拆分模块而不是把代码堆进一个py文件打开很多课程设计源码最常见的扣分点是app.py一个文件写了全部路由、数据库操作和模板渲染。一旦加用户认证文件超过500行就谁也讲不清了。高分项目的第一步是把工程按职责拆开这一章解决的是“目录结构、工厂模式、蓝图注册”这三个基础问题。2.1 用工厂函数创建Flask应用实例而不是直接全局app Flask(__name__)全局app对象写起来简单但测试时不方便替换数据库配置也无法在同一个进程里创建多个应用实例。工厂模式的核心是一个create_app()函数它接收配置对象、创建app、注册扩展和蓝图最后返回这个实例。初次接触这个模式的人容易把扩展对象放在模块顶层但注意在工厂模式下db、csrf这些扩展对象要在模块顶层创建然后在工厂里init_app或者直接放进工厂函数内部创建。前者是主流做法。# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() def create_app(test_configNone): app Flask(__name__, instance_relative_configTrue) # 默认配置写在代码里实例目录下的config.py可用于覆盖 app.config.from_mapping( SECRET_KEYdev-only-key, SQLALCHEMY_DATABASE_URIsqlite:/// os.path.join(app.instance_path, todo.sqlite), SQLALCHEMY_TRACK_MODIFICATIONSFalse, ) if test_config is not None: app.config.from_mapping(test_config) db.init_app(app) from .routes import task_bp, auth_bp app.register_blueprint(auth_bp) app.register_blueprint(task_bp, url_prefix/tasks) # 建表flask shell里执行 db.create_all() 更推荐 return app逻辑说明instance_relative_configTrue让应用能找到instance/目录下的配置文件这个目录默认不受版本控制影响适合放本地密钥或部署时的覆盖配置。test_config参数是给pytest传临时内存数据库用的这是工厂模式在测试里的最大价值——生产用文件数据库测试用内存数据库代码不用改一行。参数方面SECRET_KEY不要写死成dev-only-key。生产环境从环境变量读取常见做法是os.environ.get(SECRET_KEY, os.urandom(16))。SQLALCHEMY_TRACK_MODIFICATIONSFalse是必须关掉的否则每个操作都会多一层对象追踪浪费内存且没有任何业务价值。from .routes import这里的相对导入要求路由文件里用蓝图实例而非app这也间接强制了代码分层。2.2 蓝图拆分任务模块和用户模块的边界蓝图是Flask里组织一组路由的载体。任务清单项目至少拆成两个蓝图auth_bp管注册登录登出task_bp管任务的增删改查和列表展示。每个蓝图有独立的url_prefix路由内部则只关注业务本身。# app/routes/task.py from flask import Blueprint, render_template, redirect, url_for, request, abort from app.models import Task from app import db task_bp Blueprint(task, __name__) task_bp.route(/) def index(): tasks db.session.scalars(db.select(Task).order_by(Task.created_at.desc())).all() return render_template(index.html, taskstasks) task_bp.route(/int:task_id/toggle, methods[POST]) def toggle(task_id): task db.session.get(Task, task_id) if task is None: abort(404) task.completed not task.completed db.session.commit() return redirect(url_for(task.index))逻辑说明db.session.scalars(...)返回的是Task对象列表而不是Row元组配合db.select()比旧的Task.query写法更贴近SQLAlchemy 2.x的推荐风格。url_for(task.index)里的task是蓝图注册时第一个参数指定的名字不是文件名改蓝图文件名不会影响这里。注意toggle路由用POST而不是GET因为修改状态是有副作用的操作GET请求会被浏览器预取或爬虫触发导致误修改这也是答辩时经常被问到的点。注册蓝图时的url_prefix/tasks会让task_bp下的/实际访问路径变成/tasks/。如果你希望首页直接展示任务列表可以单独写一个app.route(/)分配到index视图或者把url_prefix去掉。成功登录后跳转到哪、未登录怎么拦截这些交给认证蓝图配合session处理第4章展开。2.3 配置分层的三个必调参数项目答辩时“配置为什么这么写”几乎是必问题。Flask的配置本质就是一个字典开发、测试、生产三种场景要有不同的值。用config.py的类来组织是最通用的方案不需要引入额外依赖。配置项开发环境测试环境生产环境作用SQLALCHEMY_DATABASE_URIsqlite:///todo.sqlitesqlite:///:memory:PostgreSQL连接串数据库连接SECRET_KEY写死的随机串test-key环境变量读取session签名SESSION_COOKIE_HTTPONLYTrueTrueTrue禁止JS读取cookieSESSION_COOKIE_SAMESITELaxLaxStrictCSRF辅助防护TESTINGFalseTrueFalse测试时改变错误处理.env文件管理密钥时注意python-dotenv只在本地开发时用生产环境用系统环境变量不要把这个机制反向推广。一个容易被忽略的参数是SESSION_COOKIE_SECURE开发环境设置False是因为本地HTTP没有证书生产开了HTTPS才能置True否则session会直接失效这是部署后“登录就掉线”的头号原因。3. SQLite任务表设计状态字段别用字符串CRUD要挡住非法日期任务清单的业务核心是四个操作列表、添加、标记完成、删除。这个项目里数据表字段设计的合理程度直接决定答辩时能不能讲出深度。别小看这张表——索引、默认值、约束、时间处理都在这里体现。3.1 任务表字段completed用布尔值due_date可空# app/models.py from app import db from datetime import datetime class Task(db.Model): __tablename__ tasks id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(120), nullableFalse) completed db.Column(db.Boolean, nullableFalse, defaultFalse) due_date db.Column(db.Date, nullableTrue) created_at db.Column(db.DateTime, nullableFalse, defaultdatetime.utcnow) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse)字段设计逻辑title用String(120)而不是Text因为任务标题是短文本在列表页展示时无需分页截断。due_date允许为空代表“不设截止日期”的待办这在语义上比强制填一个日期更合理。completed之所以用布尔值而不是done/undone字符串是因为SQLite没有原生布尔类型但SQLAlchemy会把Boolean映射为整型0/1排序、过滤、统计都要方便得多。索引设计上user_id建外键约束虽然保证引用完整性但查询“某个用户的所有任务”时它不会自动拥有索引。追加一个db.Index(ix_tasks_user_completed, user_id, completed)可以覆盖列表页最频繁的查询组合。另外created_at用datetime.utcnow还是datetime.now是经典坑前者存UTC时间模板渲染时再转本地时区后者直接存服务器本地时间服务器换时区后历史数据全乱。项目里推荐存UTC。3.2 用Flask-SQLAlchemy还是原生sqlite3毕设场景二选一的原则这是一个在选题阶段就必须想清楚的问题。原生sqlite3模块的优点是零依赖、代码直观、答辩时一句“我没有用ORM底层SQL都看得懂”就能讲清。但同时你要自己处理连接管理、字段类型转换、SQL注入风险代码量至少多A4纸三页。Flask-SQLAlchemy的优点是开发速度快、模型定义即文档、事务管理统一但答辩时如果被追问“ORM生成了什么SQL”容易卡壳。通常更推荐Flask-SQLAlchemy理由是这个框架本身就在Flask生态里db.session帮你管好了事务边界db.create_all()可以直接根据模型建表省掉手写建表脚本。而且SQLAlchemy的声明式模型可读性很好就算没学过SQL也能看懂字段含义。要写进源码的建表脚本可以用下面的等价SQL理解底层发生了什么CREATE TABLE tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(120) NOT NULL, completed BOOLEAN NOT NULL DEFAULT 0, due_date DATE, created_at DATETIME NOT NULL, user_id INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE INDEX ix_tasks_user_completed ON tasks (user_id, completed);AUTOINCREMENT在SQLite里只在希望id永不复用时才需要普通自增主键直接写INTEGER PRIMARY KEY就行不加AUTOINCREMENT反而性能更好。日期类型在SQLite中实际以文本存储格式必须严格是YYYY-MM-DD否则排序会出错——这是SQLite一个很隐蔽的怪癖。3.3 任务新增与编辑的视图函数非法日期必须在模型层拦截表单提交的数据默认全是字符串due_date如果直接赋给Date字段SQLAlchemy会在写入时报错但此时已经浪费了一次数据库往返。正确做法是在视图函数里用datetime.strptime提前解析失败就回传错误信息而不是让500页面吞掉异常。from datetime import datetime task_bp.route(/create, methods[POST]) def create(): title request.form.get(title, ).strip() due_date_str request.form.get(due_date, ).strip() if not title: flash(标题不能为空, error) return redirect(url_for(task.index)) if len(title) 120: flash(标题不能超过120个字符, error) return redirect(url_for(task.index)) due_date None if due_date_str: try: due_date datetime.strptime(due_date_str, %Y-%m-%d).date() except ValueError: flash(日期格式应为 YYYY-MM-DD, error) return redirect(url_for(task.index)) task Task(titletitle, due_datedue_date, user_idcurrent_user_id()) db.session.add(task) db.session.commit() return redirect(url_for(task.index))参数说明request.form.get第二个参数设置默认值比直接request.form[title]安全后者在字段缺失时会抛BadRequestKeyError。current_user_id()在这里是简化写法实际项目中从session取值见第4章。校验顺序上先查空再查长度两个错误提示分开写因为用户看到的反馈需要足够具体。flash消息在模板里要配合get_flashed_messages()显示否则用户完全不知道发生了什么。删除任务时有个传统坑用GET请求调删除接口。只要攻击者构造一个img src/tasks/3/delete用户浏览任意页面就会触发删除。所以删除也必须是POST并且要在模板里用表单包裹提交按钮而不是一个简单的a链接。这个细节在很多开源项目里都存在答辩提一句“删除和状态修改都用了POST防止CSRF和越权”立刻能加分。4. 用户认证与登录态不要用sha256存密码用Werkzeug的散列函数任务清单如果只有单机功能不需要用户系统但毕业设计要展示的完整业务闭环通常需要“注册-登录-操作-登出”。这一章是全项目最容易被挑毛病的地方——很多源码里密码直接用sha256(password)存储这在今天已经是必须要解释清楚的安全漏洞。4.1 Werkzeug散列原理salt和迭代次数的意义werkzeug.security.generate_password_hash是Flask内置依赖就有的密码散列函数不需要额外安装。它的默认算法是scrypt默认方法在Werkzeug 2.3之后是scrypt之前是pbkdf2:sha256。它做的事情可以理解为生成随机salt把它和密码拼在一起做多次哈希迭代最终输出格式为方法$盐$哈希值的字符串。from werkzeug.security import generate_password_hash, check_password_hash hash_value generate_password_hash(my_password) print(hash_value) # 形如 scrypt:32768:8:1$abc123...$........ check_password_hash(hash_value, my_password) # True check_password_hash(hash_value, wrong_password) # False为什么不能自己拼一个salt secrets.token_hex(8); sha256(salt password)因为Werkzeug不只是做一次哈希它还控制迭代成本让暴力破解的代价成倍增加。scrypt方法的三个数字32768:8:1分别对应内存成本、块大小、并行度参数。这些参数存在散列字符串里意味着未来硬件变快后可以安全地升级参数旧密码仍能校验通过——这正是“自研加密”最容易遗漏的设计点。登录视图的校验逻辑必须是根据用户名查用户→用户不存在也走一遍check_password_hash防止通过响应时间差探测用户名是否存在→校验通过把user_id写进session。时间差攻击这个细节足够在答辩时展示你对安全的理解。4.2 session管理为什么服务端session方案比把用户信息存localStorage可靠很多前后端分离的教学项目会把登录后的用户ID放localStorage但Flask的session是基于签名的cookie用户看不到也不能篡改内容。session[user_id] user.id后客户端拿到的只是一串签名数据Flask用SECRET_KEY验证签名有效性任何对内容的改动都会导致校验失败。from flask import session, redirect, url_for, request from app.models import User auth_bp.route(/login, methods[POST]) def login(): username request.form.get(username, ) password request.form.get(password, ) user db.session.scalar(db.select(User).where(User.username username)) if user is None or not check_password_hash(user.password_hash, password): flash(用户名或密码错误, error) return redirect(url_for(auth.login)) session.clear() session[user_id] user.id session.permanent True return redirect(url_for(task.index))session.permanent True的含义是告诉Flask这个session的生命周期受PERMANENT_SESSION_LIFETIME控制默认31天单位是datetime.timedelta。不设这行的话session会在浏览器关闭时失效。登出视图就是session.clear()然后重定向到首页不需要像Token方案那样处理服务端注销。要控制“未登录不能访问任务页”写一个before_app_request钩子做白名单判断。注意白名单里要放auth.login和静态文件路由否则会出现无限重定向auth_bp.before_app_request def require_login(): allowed [auth.login, auth.register, static] if request.endpoint not in allowed and user_id not in session: return redirect(url_for(auth.login))逻辑说明request.endpoint是蓝图名.视图函数名的格式。把登录和注册页放进白名单是必须的否则它们自己也触发未登录检查导致重定向循环。这个钩子放在auth_bp里通过before_app_request对全局生效而不是只在认证蓝图内生效——这正是before_app_request和before_request的关键区别后者只作用于所属蓝图。4.3 Flask-WTF的CSRF防护和表单写法CSRF跨站请求伪造在Flask表单项目里的攻击场景是用户在别的网站提交一个隐藏表单向你的/tasks/create发送POST请求带着同源cookie自动完成提交。防护的核心是给每个表单生成一个随机的csrf_token服务器校验这个token和session里存储的是否一致。Cookie本来就是浏览器自动带的攻击者无法读取同源响应所以伪造不出合法token。使用Flask-WTF后全局开启CSRF防护的方式是from flask_wtf.csrf import CSRFProtect csrf CSRFProtect() def create_app(): app Flask(__name__) csrf.init_app(app) return app模板里的每个表单要加一行form methodpost action{{ url_for(task.create) }} input typehidden namecsrf_token value{{ csrf_token() }} input typetext nametitle button typesubmit添加/button /form注意csrf_token()是Flask-WTF注入到模板全局上下文中的函数不是视图函数传过去的变量。如果你的项目没使用Flask-WTF只在自己的表单里加一个input name_csrf_token value...手工校验也能实现但Flask-WTF帮处理了错误响应和字段生成省掉这部分代码。这里要特别提醒CSRFProtect开启后所有POST/PUT/DELETE请求都会强制校验token如果你用curl直接调API测试接口会全部403这是Flask-WTF最常见的“为什么接口突然全挂了”原因测试时需要在create_app里给测试配置关掉或带上token。5. pytest覆盖关键链路与交付文档的四个必写模块一个“高分优秀项目”和“能跑的项目”之间隔着一份可复现的运行说明和几条自动化测试。最后一章聚焦两件事用pytest让你的登录和任务操作可以一键回归以及项目包里必须有哪些文档才能让人不看代码就装起来跑通。5.1 用pytest的fixture做内存数据库测试测试Flask应用最方便的是SQLite内存数据库——每个测试用例结束自动销毁不会污染开发数据。fixture的典型写法import pytest from app import create_app, db pytest.fixture() def app(): app create_app({ TESTING: True, SQLALCHEMY_DATABASE_URI: sqlite:///:memory:, SECRET_KEY: test-secret, WTF_CSRF_ENABLED: False, }) with app.app_context(): db.create_all() yield app pytest.fixture() def client(app): return app.test_client() def test_home_redirects_to_login(client): resp client.get(/tasks/) assert resp.status_code 302 assert /auth/login in resp.headers[Location]逻辑说明create_app里的test_config在这里发挥了作用测试配置把数据库指向内存同时关掉CSRF避免每个测试都得先取token。test_home_redirects_to_login验证了第4章白名单拦截是否生效。写测试时assert resp.status_code 302比断言响应内容更稳定因为重定向页面本身的信息业务价值不大真正要测的是访问控制逻辑。再补一条测试走通“注册→登录→创建任务→列表出现”基本就覆盖了项目核心价值不至于答辩时被问“怎么保证代码没回归”而答不上来。5.2requirements.txt固定版本还是放宽版本用pip freeze requirements.txt生成的文件会把所有传递依赖也锁定版本这能保证复现环境但也会带来另一个问题锁定版本过旧Python 3.12用户装不上过新和项目的代码不兼容。折中方案是只列直接依赖并放宽次要版本Flask3.0,4 Flask-SQLAlchemy3.1,4 Flask-WTF1.2,2 pytest8.0,9说明4这种上界是为了防止主版本更新引入破坏性API变更。注意不要手动把Werkzeug写进这个文件里它是Flask的依赖pip会自动安装手动指定一个过高或过低的版本反而可能和Flask产生兼容性问题。5.3 使用文档里最容易被忽略的“环境差异”说明毕业设计的说明书或README里很多人会写“使用Python 3.8运行”但完全没提数据库怎么初始化、Flask怎么启动。一份能让别人零障碍跑通的使用文档至少要包含这四个模块模块内容常见失败点环境准备Python版本、pip换源命令忘记激活虚拟环境依赖安装pip install -r requirements.txt网络原因要用国内镜像数据库初始化flask shell里执行db.create_all()没有app_context启动方式flask --app run.py run --debug老命令python app.py已过时第3项“没有app_context”是新手重灾区。在flask shell交互环境里db.create_all()是安全的因为shell自动推入了应用上下文。但如果你写一个独立的init_db.py脚本里面直接from app import db; db.create_all()必然报RuntimeError: Working outside of application context。常见做法是在脚本里手动包一层from app import create_app, db app create_app() with app.app_context(): db.create_all()这也是为什么前面推荐用flask shell而不是自己写脚本的原因。最后核对一遍生产部署时把app.run(debugTrue)换成waitress-serve --call app:create_app并确保SECRET_KEY来自环境变量、SESSION_COOKIE_SECURETrue把这个检查项写进文档的部署小节就可以从容去答辩了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价