简介:这是一套基于Python+Flask的问卷调查应用毕业设计源码,面向计算机相关专业需要完成毕业设计或课程设计的学生,尤其适合希望拿到高分、快速搭建可运行项目的同学。项目已通过导师指导与答辩评审,获得97分评价,在Windows10/11环境下严格调试,下载即可运行。压缩包共48个文件,约36KB,包含14个Python文件、13个HTML模板、8个JSON配置、5个CSS样式,以及README、requirements.txt、Procfile等部署文件,覆盖后端逻辑、前端页面、数据模型与迁移脚本,目录结构清晰,便于按模块阅读与二次开发。资源中附带使用文档与部署教程,可帮助读者理解Flask路由、数据库模型、问卷发布与结果统计等核心实现,也能作为课程设计参考。目前已有343人学习下载,适合需要完整项目方案、排错思路与可运行代码的读者参考借鉴。
1. 从一份 Flask 问卷项目说起:毕业设计里最容易被低估的“可交付系统”
很多同学做毕业设计时,第一反应是“找个模板改改”,结果改到一半发现路由、模板、数据库全缠在一起,连自己都说不清数据从哪来、到哪去。基于 Python+Flask 的问卷调查应用,恰好是一个能同时展示后端逻辑、前端交互和数据库设计的典型题目。它不复杂,但足够完整:用户能创建问卷、填写问卷、查看统计结果,管理员能管理问卷状态。这套东西如果按工程化思路做,能直接作为计算机毕业设计的核心交付物;如果只是拼凑,答辩时老师问一句“你的数据校验在哪一层做的”就会卡住。这篇文章面向正在做 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换成mysql+pymysql://user:pass@host/dbname即可,其余代码不动。
2.2 用 SQLAlchemy 定义问卷、题目、选项和答卷四张核心表
问卷系统的数据模型不复杂,但关系要理清。一份问卷有多个题目,一个题目有多个选项,一份答卷对应一份问卷、包含多个答案。下面是最小可用的模型定义:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Survey(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(120), nullable=False) description = db.Column(db.Text) status = db.Column(db.String(20), default='draft') # draft / published / closed created_at = db.Column(db.DateTime, server_default=db.func.now()) questions = db.relationship('Question', backref='survey', cascade='all, delete-orphan') class Question(db.Model): id = db.Column(db.Integer, primary_key=True) survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'), nullable=False) content = db.Column(db.String(255), nullable=False) qtype = db.Column(db.String(20), default='single') # single / multiple / text order = db.Column(db.Integer, default=0) options = db.relationship('Option', backref='question', cascade='all, delete-orphan') class Option(db.Model): id = db.Column(db.Integer, primary_key=True) question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False) label = db.Column(db.String(120), nullable=False) class Answer(db.Model): id = db.Column(db.Integer, primary_key=True) survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'), nullable=False) question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False) option_id = db.Column(db.Integer, db.ForeignKey('option.id')) text_value = db.Column(db.Text) submitted_at = db.Column(db.DateTime, server_default=db.func.now())这段代码里几个参数需要留意。cascade='all, 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(title=title, description=request.form.get('description', '')) db.session.add(survey) db.session.commit() return redirect(url_for('survey.edit_survey', sid=survey.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', sid=sid)) survey.status = 'published' db.session.commit() return redirect(url_for('survey.fill_survey', sid=sid)) @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_id=sid, question_id=q.id, text_value=request.form.get(f'q_{q.id}', '')) else: opt_id = request.form.get(f'q_{q.id}') ans = Answer(survey_id=sid, question_id=q.id, option_id=int(opt_id) if opt_id else None) db.session.add(ans) db.session.commit() flash('提交成功') return redirect(url_for('survey.result', sid=sid)) return render_template('fill_survey.html', survey=survey)创建路由里做了标题非空校验,空标题直接打回。发布路由检查是否至少有一个题目,防止发布空问卷。填写路由先判断status,只有已发布的问卷才渲染表单;提交时按题目类型分别处理,文本题存text_value,选择题存option_id。统计路由可以单独写一个聚合查询,按option_id分组计数。
注意:
request.form.get(f'q_{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_id=q.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', survey=survey, stats=stats)这里用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 = f'q_{q.id}' if q.qtype == 'single': val = form.get(key) if not val: errors.append(f'题目「{q.content}」未作答') elif not Option.query.filter_by(id=int(val), question_id=q.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(id=int(v), question_id=q.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(threaded=True)让 Flask 处理并发请求,生产环境换 gunicorn 加多 worker。数据库如果继续用 SQLite,给连接串加?check_same_thread=False,但更稳妥的是换 MySQL 或 PostgreSQL。
4.4 现象:模板里url_for('survey.fill_survey', sid=survey.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(f'submitted_{sid}'): flash('您已经提交过该问卷') return redirect(url_for('survey.result', sid=sid)) # ... 原有的保存逻辑 ... session[f'submitted_{sid}'] = True db.session.commit() return redirect(url_for('survey.result', sid=sid)) return render_template('fill_survey.html', survey=survey)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_redirects=True) 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_redirects=True) assert '至少添加一个题目' in resp.get_data(as_text=True) def test_fill_unpublished_survey(client): resp = client.get('/survey/1/fill', follow_redirects=True) assert '不可填写' in resp.get_data(as_text=True)这三个用例分别验证创建问卷、空问卷不能发布、未发布问卷不能填写。跑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 --host=0.0.0.0也能用,但要知道生产环境不会这么干。
我自己的习惯是:每次改完模型或路由,先跑一遍 pytest,再手动点一遍创建、发布、填写、统计四个页面,最后看一遍 gunicorn 的启动日志有没有 warning。这套流程走下来,答辩时被问到任何一层都能直接打开对应文件讲,而不是靠记忆背代码。希望帮到你。
本文还有配套的精品资源,点击获取