ARTICLE DETAIL

资讯详情

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

Python Flask问卷调查系统毕业设计:从零搭建到部署上线

Python Flask问卷调查系统毕业设计:从零搭建到部署上线 简介这是一套基于PythonFlask的问卷调查应用毕业设计源码面向计算机相关专业需要完成毕业设计或课程设计的学生尤其适合希望拿到高分、快速搭建可运行项目的同学。项目已通过导师指导与答辩评审获得97分评价在Windows10/11环境下严格调试下载即可运行。压缩包共48个文件约36KB包含14个Python文件、13个HTML模板、8个JSON配置、5个CSS样式以及README、requirements.txt、Procfile等部署文件覆盖后端逻辑、前端页面、数据模型与迁移脚本目录结构清晰便于按模块阅读与二次开发。资源中附带使用文档与部署教程可帮助读者理解Flask路由、数据库模型、问卷发布与结果统计等核心实现也能作为课程设计参考。目前已有343人学习下载适合需要完整项目方案、排错思路与可运行代码的读者参考借鉴。1. 从一份 Flask 问卷项目说起毕业设计里最容易被低估的“可交付系统”很多同学做毕业设计时第一反应是“找个模板改改”结果改到一半发现路由、模板、数据库全缠在一起连自己都说不清数据从哪来、到哪去。基于 PythonFlask 的问卷调查应用恰好是一个能同时展示后端逻辑、前端交互和数据库设计的典型题目。它不复杂但足够完整用户能创建问卷、填写问卷、查看统计结果管理员能管理问卷状态。这套东西如果按工程化思路做能直接作为计算机毕业设计的核心交付物如果只是拼凑答辩时老师问一句“你的数据校验在哪一层做的”就会卡住。这篇文章面向正在做 Flask 开发、需要一套可运行、可讲解、可扩展的问卷系统的同学把从环境搭建到部署上线的完整路径拆开讲清楚包括每一步的参数含义和容易翻车的地方。2. Flask 问卷系统的骨架怎么搭路由、模型与模板的职责边界2.1 为什么选 Flask 而不是 Django 做问卷类毕设Flask 是微框架没有内置 ORM、表单验证和后台管理这看起来是缺点但对毕业设计反而是优势。问卷系统的业务逻辑集中在“问卷创建—题目配置—答卷提交—结果统计”这条链路上不需要 Django 那种全家桶式的重型结构。用 Flask 可以自己决定用 SQLAlchemy 还是原生 SQL、用 WTForms 还是手写校验答辩时能讲清楚每一层的选型理由而不是“Django 自带的我没改”。常见做法是Flask SQLAlchemy Jinja2 Bootstrap。SQLAlchemy 负责把问卷、题目、选项、答卷这些实体映射成表Jinja2 渲染页面Bootstrap 保证在没前端工程师的情况下页面不丑。这套组合在本地跑通后部署到一台 1 核 2G 的云服务器上完全够用。提示如果学校要求必须用 MySQL把 SQLAlchemy 的连接串从sqlite:///survey.db换成mysqlpymysql://user:passhost/dbname即可其余代码不动。2.2 用 SQLAlchemy 定义问卷、题目、选项和答卷四张核心表问卷系统的数据模型不复杂但关系要理清。一份问卷有多个题目一个题目有多个选项一份答卷对应一份问卷、包含多个答案。下面是最小可用的模型定义from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Survey(db.Model): id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(120), nullableFalse) description db.Column(db.Text) status db.Column(db.String(20), defaultdraft) # draft / published / closed created_at db.Column(db.DateTime, server_defaultdb.func.now()) questions db.relationship(Question, backrefsurvey, cascadeall, delete-orphan) class Question(db.Model): id db.Column(db.Integer, primary_keyTrue) survey_id db.Column(db.Integer, db.ForeignKey(survey.id), nullableFalse) content db.Column(db.String(255), nullableFalse) qtype db.Column(db.String(20), defaultsingle) # single / multiple / text order db.Column(db.Integer, default0) options db.relationship(Option, backrefquestion, cascadeall, delete-orphan) class Option(db.Model): id db.Column(db.Integer, primary_keyTrue) question_id db.Column(db.Integer, db.ForeignKey(question.id), nullableFalse) label db.Column(db.String(120), nullableFalse) class Answer(db.Model): id db.Column(db.Integer, primary_keyTrue) survey_id db.Column(db.Integer, db.ForeignKey(survey.id), nullableFalse) question_id db.Column(db.Integer, db.ForeignKey(question.id), nullableFalse) option_id db.Column(db.Integer, db.ForeignKey(option.id)) text_value db.Column(db.Text) submitted_at db.Column(db.DateTime, server_defaultdb.func.now())这段代码里几个参数需要留意。cascadeall, delete-orphan保证删除问卷时题目和选项一起删不会留下孤儿数据。status字段控制问卷是否对外开放草稿状态的问卷即使有人拿到链接也不能提交。qtype区分单选、多选和文本题统计逻辑会根据这个字段走不同分支。order字段决定题目在页面上的排列顺序不依赖数据库自增 id 排序后期调整题目顺序时更灵活。2.3 路由设计创建、发布、填写、统计四条主链路路由是 Flask 应用的门面问卷系统至少需要四组路由。下面用蓝图拆分避免所有路由堆在app.py里from flask import Blueprint, render_template, request, redirect, url_for, flash from .models import db, Survey, Question, Option, Answer bp Blueprint(survey, __name__) bp.route(/survey/create, methods[GET, POST]) def create_survey(): if request.method POST: title request.form.get(title, ).strip() if not title: flash(问卷标题不能为空) return redirect(url_for(survey.create_survey)) survey Survey(titletitle, descriptionrequest.form.get(description, )) db.session.add(survey) db.session.commit() return redirect(url_for(survey.edit_survey, sidsurvey.id)) return render_template(create_survey.html) bp.route(/survey/int:sid/publish, methods[POST]) def publish_survey(sid): survey Survey.query.get_or_404(sid) if not survey.questions: flash(至少添加一个题目才能发布) return redirect(url_for(survey.edit_survey, sidsid)) survey.status published db.session.commit() return redirect(url_for(survey.fill_survey, sidsid)) bp.route(/survey/int:sid/fill, methods[GET, POST]) def fill_survey(sid): survey Survey.query.get_or_404(sid) if survey.status ! published: flash(该问卷当前不可填写) return redirect(url_for(survey.index)) if request.method POST: for q in survey.questions: if q.qtype text: ans Answer(survey_idsid, question_idq.id, text_valuerequest.form.get(fq_{q.id}, )) else: opt_id request.form.get(fq_{q.id}) ans Answer(survey_idsid, question_idq.id, option_idint(opt_id) if opt_id else None) db.session.add(ans) db.session.commit() flash(提交成功) return redirect(url_for(survey.result, sidsid)) return render_template(fill_survey.html, surveysurvey)创建路由里做了标题非空校验空标题直接打回。发布路由检查是否至少有一个题目防止发布空问卷。填写路由先判断status只有已发布的问卷才渲染表单提交时按题目类型分别处理文本题存text_value选择题存option_id。统计路由可以单独写一个聚合查询按option_id分组计数。注意request.form.get(fq_{q.id})这种命名方式要求模板里每个题目的输入框 name 属性严格对应否则提交后取不到值页面不报错但数据丢失排查起来很费时间。3. 问卷统计与数据校验从提交到结果页的完整链路3.1 统计查询怎么写才不用在 Python 里循环累加很多同学统计结果时先把所有 Answer 查出来然后在 Python 里 for 循环计数。数据量小的时候没问题但答辩演示时如果造了 500 份答卷页面会明显变慢。正确做法是用 SQL 的GROUP BY直接在数据库层聚合from sqlalchemy import func bp.route(/survey/int:sid/result) def result(sid): survey Survey.query.get_or_404(sid) stats [] for q in survey.questions: if q.qtype text: answers Answer.query.filter_by(question_idq.id).all() stats.append({question: q, texts: [a.text_value for a in answers]}) else: rows db.session.query( Option.label, func.count(Answer.id).label(cnt) ).outerjoin(Answer, Answer.option_id Option.id)\ .filter(Option.question_id q.id)\ .group_by(Option.id)\ .all() stats.append({question: q, rows: rows}) return render_template(result.html, surveysurvey, statsstats)这里用outerjoin而不是join是为了保证即使某个选项没人选结果页也能显示出来计数为 0。如果用join零票的选项会直接消失老师一眼就能看出统计逻辑有问题。func.count(Answer.id)统计的是答卷 id 的数量不是选项 id避免选项被重复计数。3.2 服务端校验别让前端校验成为唯一防线前端 HTML 的required属性只能防君子绕过它太容易了。服务端必须对提交数据做二次校验。下面是一个校验函数在fill_survey的 POST 分支里调用def validate_submission(survey, form): errors [] for q in survey.questions: key fq_{q.id} if q.qtype single: val form.get(key) if not val: errors.append(f题目「{q.content}」未作答) elif not Option.query.filter_by(idint(val), question_idq.id).first(): errors.append(f题目「{q.content}」选项非法) elif q.qtype multiple: vals form.getlist(key) if not vals: errors.append(f题目「{q.content}」至少选一项) for v in vals: if not Option.query.filter_by(idint(v), question_idq.id).first(): errors.append(f题目「{q.content}」包含非法选项) return errors这段校验做了两件事检查必填项是否为空检查提交的 option_id 是否真的属于该题目。第二点很关键如果不做有人手动改表单里的 value 就能给别的题目投票统计结果直接失真。校验不通过时把 errors 列表 flash 到页面上让用户逐条看到哪个题没答对。3.3 用 Flask-Migrate 管理数据库变更别手动删表重建开发过程中改模型是常事如果每次改字段都手动删survey.db再重建之前造的测试数据全没了。用 Flask-Migrate 可以生成迁移脚本保留数据的同时更新表结构flask db init flask db migrate -m add status column to survey flask db upgradeflask db init只需执行一次生成migrations目录。migrate根据模型变化自动生成脚本upgrade把变更应用到数据库。注意自动生成的脚本不一定完全正确比如字段重命名它可能识别成删旧建新执行前要打开脚本看一眼。如果学校机房不让装额外包至少也要在app.py里写一个db.create_all()的初始化命令别让老师拿到代码后跑不起来。4. 避坑与排查Flask 问卷项目里最容易翻车的五个地方4.1 现象页面报 500日志显示 “Working outside of application context”原因在 Flask 应用上下文之外调用了db.session或current_app常见于把数据库操作写在了蓝图导入时执行的代码里或者在app Flask(__name__)之前就初始化了 SQLAlchemy。解决确保所有数据库操作都在视图函数或with app.app_context():块内执行。如果用了工厂函数create_app()在create_app内部再调db.init_app(app)不要在外面直接db SQLAlchemy(app)。4.2 现象问卷填写页提交后跳转正常但统计页所有选项都是 0 票原因Answer 表插入时option_id存成了字符串而统计查询用整数比较SQLite 在类型不严格的情况下不报错但匹配不上。解决在插入前显式转int(opt_id)并在模型里把option_id定义为db.Integer。如果已经产生了脏数据写一条 SQL 把option_id字段里非数字的值清掉或修正。4.3 现象多个用户同时提交答卷时偶尔出现数据串行或丢失原因Flask 默认的开发服务器是单线程的但如果在视图里用了全局变量暂存数据多请求会互相覆盖。另外 SQLite 在并发写入时容易锁库。解决不要在视图函数外定义可变全局变量。开发阶段用app.run(threadedTrue)让 Flask 处理并发请求生产环境换 gunicorn 加多 worker。数据库如果继续用 SQLite给连接串加?check_same_threadFalse但更稳妥的是换 MySQL 或 PostgreSQL。4.4 现象模板里url_for(survey.fill_survey, sidsurvey.id)报 BuildError原因蓝图注册时没指定url_prefix或者端点名写错了。蓝图里的端点名格式是蓝图名.视图函数名不是单纯的函数名。解决检查app.register_blueprint(bp, url_prefix/survey)是否执行确认url_for第一个参数是survey.fill_survey而不是fill_survey。如果蓝图名和函数名有改动全局搜一遍url_for调用同步更新。4.5 现象部署到服务器后静态文件 404页面样式全丢原因Flask 默认从static文件夹找静态文件如果用了 Nginx 反代Nginx 的location /static配置指向了错误路径或者 Flask 的static_folder参数被改过。解决确认项目根目录下有static文件夹里面放css、js、img。Nginx 配置里写location /static { alias /path/to/your/app/static; }路径要写绝对路径。如果不想配 Nginx直接用 gunicorn 跑 Flask静态文件由 Flask 自己处理但性能会差一些。5. 让问卷系统在答辩时多拿十分三个进阶技巧与验证方法5.1 给问卷加一个“防重复提交”的轻量机制答辩时老师常问“同一个人反复提交怎么办”。不需要上复杂的用户系统用 Flask 的 session 加一个简单标记就能挡住大部分重复提交from flask import session bp.route(/survey/int:sid/fill, methods[GET, POST]) def fill_survey(sid): survey Survey.query.get_or_404(sid) if survey.status ! published: flash(该问卷当前不可填写) return redirect(url_for(survey.index)) if request.method POST: if session.get(fsubmitted_{sid}): flash(您已经提交过该问卷) return redirect(url_for(survey.result, sidsid)) # ... 原有的保存逻辑 ... session[fsubmitted_{sid}] True db.session.commit() return redirect(url_for(survey.result, sidsid)) return render_template(fill_survey.html, surveysurvey)session默认用签名 cookie 存在客户端用户清掉 cookie 就能再投一次所以这只能防误触不能防恶意刷票。如果要求更严格需要在数据库里记录提交者的 IP 和浏览器指纹但那就超出毕设的合理范围了。答辩时主动说出这个边界反而显得你清楚方案的上限在哪。5.2 用 pytest 写三个最小测试用例证明系统真的能跑很多毕设代码没有任何测试老师想验证功能只能手动点。加三个测试用例成本很低但效果很好import pytest from app import create_app, db from app.models import Survey pytest.fixture def client(): app create_app({TESTING: True, SQLALCHEMY_DATABASE_URI: sqlite:///:memory:}) with app.test_client() as c: with app.app_context(): db.create_all() yield c def test_create_survey(client): resp client.post(/survey/create, data{title: 测试问卷}, follow_redirectsTrue) assert resp.status_code 200 def test_publish_empty_survey_fails(client): client.post(/survey/create, data{title: 空问卷}) resp client.post(/survey/1/publish, follow_redirectsTrue) assert 至少添加一个题目 in resp.get_data(as_textTrue) def test_fill_unpublished_survey(client): resp client.get(/survey/1/fill, follow_redirectsTrue) assert 不可填写 in resp.get_data(as_textTrue)这三个用例分别验证创建问卷、空问卷不能发布、未发布问卷不能填写。跑pytest -v全绿答辩时直接展示测试结果比口头说“功能都实现了”有说服力得多。注意测试用的数据库是内存 SQLite不会污染开发数据。5.3 部署验证从本地flask run到 gunicorn 启动的检查清单本地开发用flask run没问题但答辩演示如果只在本地跑老师可能会问“怎么部署到服务器”。用 gunicorn 启动只需要一条命令gunicorn -w 4 -b 0.0.0.0:8000 app:create_app()-w 4表示 4 个 worker 进程-b指定监听地址和端口。启动后浏览器访问http://服务器IP:8000如果页面正常、静态文件加载正常、提交答卷后统计页数字正确说明部署链路通了。常见问题是 gunicorn 启动后静态文件 404原因是 gunicorn 不负责静态文件需要 Nginx 配合或者用whitenoise中间件。毕设演示如果只是局域网访问直接flask run --host0.0.0.0也能用但要知道生产环境不会这么干。我自己的习惯是每次改完模型或路由先跑一遍 pytest再手动点一遍创建、发布、填写、统计四个页面最后看一遍 gunicorn 的启动日志有没有 warning。这套流程走下来答辩时被问到任何一层都能直接打开对应文件讲而不是靠记忆背代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表