简介:这是一套面向高校计算机相关专业学生的毕业设计完整源码,主题为基于Python+Flask的问卷调查应用,适合正在准备毕设、课程设计或想练手Web全栈开发的学习者。项目采用Flask后端搭配Vue前端,覆盖问卷创建、发布、填写、结果统计等核心业务,可作为同类选题的参考模板。压缩包共48个文件,约36KB,其中14个py文件承载后端逻辑与数据初始化,13个html与5个css构成页面与样式,8个json用于配置与数据存储,另含readme、requirements.txt、Procfile等部署说明文件,目录结构清晰,便于按模块阅读。该资源已获导师认可,答辩评审97分,并在Windows 10/11环境严格调试,下载即可运行,部署教程齐全。目前已有343人学习下载,读者可从中获取完整项目结构、数据库迁移方案与前后端交互思路,快速理解问卷系统的实现路径。
1. 从一份 Flask 问卷项目说起:毕业设计里最容易被低估的"完整闭环"
每年到了毕设季,计算机相关专业的选题里总有一类项目反复出现——基于 Python + Flask 的问卷调查应用。它看起来平平无奇,既没有深度学习那种唬人的名词,也没有知识图谱那种前沿感,但真正动手做过的人会明白,这类项目恰恰是"麻雀虽小五脏俱全"的典型:前端表单渲染、后端路由分发、数据库建模、用户会话管理、数据统计与可视化,一条链路全走一遍。很多同学在选题阶段觉得它简单,做到中期才发现自己连 Flask 的请求上下文都没搞清楚,最后只能去 GitHub 上找一份源码改改交差。
这份"毕业设计源码 + 使用文档"的打包项目,本质上提供的是一套可运行的参考实现。它的价值不在于代码有多复杂,而在于它把问卷系统从创建、发布、填写到统计的完整流程串了起来,让你能对照着理解一个 Web 应用是怎么从零搭起来的。适合的人群很明确:正在做毕设但不知道从哪下手的学生、想用 Flask 练手一个完整项目的初学者,以及需要快速搭一个内部问卷工具的后端开发者。接下来的内容,我会按"环境搭建 → 数据建模 → 核心功能实现 → 部署上线 → 避坑"的顺序,把这类项目从能跑到能讲清楚的全过程拆开讲。
2. 环境准备与项目骨架:把 Flask 问卷应用在本地跑起来
2.1 Python 环境与依赖安装的确定性做法
拿到一份 Flask 项目源码,第一步永远是让它在自己机器上跑起来。很多人卡在这一步不是因为难,而是因为环境不一致导致的玄学报错。我一般会先确认 Python 版本,Flask 2.x 和 3.x 在部分 API 上有差异,问卷项目通常不会用到太新的特性,Python 3.8 到 3.11 都能跑。
# 确认 Python 版本,建议 3.8 以上 python --version # 创建独立虚拟环境,避免污染全局包 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate # 安装依赖,通常项目根目录会有 requirements.txt pip install -r requirements.txt这里的关键点是虚拟环境。我见过太多人直接pip install flask装到全局,结果不同项目之间版本打架,最后连flask run都报模块找不到。虚拟环境不是可选项,是必选项。如果项目没有提供requirements.txt,常见的核心依赖是 Flask、Flask-SQLAlchemy、Flask-Login、WTForms 这几个,可以手动装。
# 没有 requirements.txt 时手动安装核心依赖 pip install flask flask-sqlalchemy flask-login wtforms安装完成后,用pip list确认一下版本,Flask 和 SQLAlchemy 的版本兼容性需要注意:Flask-SQLAlchemy 3.x 要求 Flask 2.2 以上,如果你装的是老版本 Flask 配新版本扩展,启动时会直接抛ImportError。
2.2 项目目录结构与启动入口的定位
Flask 项目的目录结构没有强制标准,但问卷类应用通常遵循一种约定:app.py或run.py作为入口,models.py放数据模型,routes.py或views.py放路由,templates/放 Jinja2 模板,static/放 CSS 和 JS。拿到源码后先别急着改代码,花两分钟把目录树看清楚。
# 查看项目结构(Linux/macOS) find . -maxdepth 2 -type f -name "*.py" | head -20 # 或者直接看目录 ls -la找到入口文件后,重点看三个东西:app = Flask(__name__)在哪里创建的、数据库配置SQLALCHEMY_DATABASE_URI指向什么、if __name__ == '__main__'里的启动参数是什么。有些项目用的是 SQLite,数据库文件就在项目根目录,直接跑就行;有些用的是 MySQL,那就需要你先建库、改配置。
# 典型的入口文件结构(app.py) from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SECRET_KEY'] = 'your-secret-key' # 会话加密用,部署时必须改 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///survey.db' # 默认 SQLite app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) if __name__ == '__main__': with app.app_context(): db.create_all() # 首次运行自动建表 app.run(debug=True, host='0.0.0.0', port=5000)SECRET_KEY这个参数很多人会忽略,它用于签名 session cookie,如果用的是默认值或者硬编码的简单字符串,部署到公网后存在会话伪造风险。毕设演示阶段问题不大,但如果你打算把这个问卷应用真正用起来,这个值必须换成随机字符串。db.create_all()放在app.app_context()里是 Flask-SQLAlchemy 3.x 的要求,老版本不强制,但新版本不加会报RuntimeError: Working outside of application context。
启动命令很简单:
python app.py # 或者 flask run --host=0.0.0.0 --port=5000浏览器访问http://127.0.0.1:5000,能看到首页就说明环境没问题了。如果报ModuleNotFoundError,检查虚拟环境是否激活;如果报数据库连接错误,检查SQLALCHEMY_DATABASE_URI配置;如果页面空白但没报错,大概率是模板路径不对,检查templates/目录是否在 Flask 应用的根路径下。
3. 数据建模与核心功能:问卷系统从建表到提交的完整链路
3.1 问卷、问题、选项、回答四张表的建模思路
问卷系统的数据模型说简单也简单,说复杂也复杂。最核心的实体有四个:问卷(Survey)、问题(Question)、选项(Option)、回答记录(Answer)。它们之间的关系是:一个问卷有多个问题,一个问题有多个选项,一份回答对应一个问卷下的多个问题答案。
# models.py 典型建模 from app import db from datetime import datetime class Survey(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) is_active = db.Column(db.Boolean, default=True) questions = db.relationship('Question', backref='survey', lazy=True) 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(500), nullable=False) q_type = db.Column(db.String(20), default='single') # single / multiple / text options = db.relationship('Option', backref='question', lazy=True) 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(200), 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'), nullable=True) text_value = db.Column(db.Text, nullable=True) # 填空题答案 submitted_at = db.Column(db.DateTime, default=datetime.utcnow)这里有几个设计决策值得展开说。q_type字段用来区分单选、多选和填空题,这决定了前端渲染方式和后端校验逻辑。Answer表里同时保留了option_id和text_value,前者用于选择题,后者用于填空题,这样一张表就能覆盖所有题型,不需要为每种题型单独建表。is_active字段控制问卷是否开放填写,比直接删除问卷更安全。
db.create_all()会自动根据模型创建表,但它不会处理表结构变更。如果你改了模型字段,比如给Survey加了一个deadline字段,create_all()不会自动加列,需要手动迁移或者删库重建。毕设阶段数据不重要,删库重建是最省事的做法;如果已经有数据了,就得用 Flask-Migrate 做迁移。
3.2 问卷填写页面的路由与表单处理
问卷填写是整个应用的核心交互。用户访问/survey/<int:survey_id>时,后端需要查出问卷信息、关联的所有问题和选项,渲染到模板里;用户提交后,后端需要解析表单数据,逐题写入Answer表。
# routes.py 问卷填写与提交 from flask import render_template, request, redirect, url_for, flash from app import app, db from models import Survey, Question, Option, Answer @app.route('/survey/<int:survey_id>', methods=['GET', 'POST']) def survey_detail(survey_id): survey = Survey.query.get_or_404(survey_id) if not survey.is_active: flash('该问卷已关闭') return redirect(url_for('index')) if request.method == 'POST': # 遍历每个问题,从表单中取值 for question in survey.questions: field_name = f'q_{question.id}' if question.q_type == 'text': # 填空题直接取文本 val = request.form.get(field_name, '').strip() if val: ans = Answer(survey_id=survey.id, question_id=question.id, text_value=val) db.session.add(ans) else: # 选择题取 option_id,多选用 getlist if question.q_type == 'multiple': option_ids = request.form.getlist(field_name) else: oid = request.form.get(field_name) option_ids = [oid] if oid else [] for oid in option_ids: ans = Answer(survey_id=survey.id, question_id=question.id, option_id=int(oid)) db.session.add(ans) db.session.commit() flash('提交成功') return redirect(url_for('survey_result', survey_id=survey.id)) return render_template('survey_detail.html', survey=survey)这段代码里有几个容易翻车的点。第一,request.form.getlist()用于多选,request.form.get()用于单选和文本,用错了会导致多选只拿到最后一个值。第二,int(oid)转换前最好做一次校验,如果前端传了非数字字符串会直接抛ValueError,整个提交事务回滚。第三,所有db.session.add()放在循环里,最后统一commit(),这样要么全部成功要么全部失败,不会出现半份问卷入库的情况。
模板端用 Jinja2 循环渲染问题和选项,单选按钮的name统一用q_{{ question.id }},多选用getlist接收,所以name也要一致。填空题用<textarea>或<input type="text">,后端根据q_type判断取值方式。
3.3 统计结果页面的聚合查询与图表渲染
问卷填完之后,用户最想看的是统计结果。最简单的做法是用 SQLAlchemy 的聚合函数按选项分组计数。
# 统计每个选项被选了多少次 from sqlalchemy import func @app.route('/survey/<int:survey_id>/result') def survey_result(survey_id): survey = Survey.query.get_or_404(survey_id) stats = [] for question in survey.questions: if question.q_type == 'text': # 填空题列出所有文本回答 answers = Answer.query.filter_by( survey_id=survey.id, question_id=question.id ).all() stats.append({'question': question, 'texts': [a.text_value for a in answers]}) else: # 选择题按 option_id 分组计数 rows = db.session.query( Answer.option_id, func.count(Answer.id) ).filter_by( survey_id=survey.id, question_id=question.id ).group_by(Answer.option_id).all() counts = {oid: cnt for oid, cnt in rows} stats.append({'question': question, 'counts': counts}) return render_template('result.html', survey=survey, stats=stats)func.count配合group_by是标准的聚合查询写法。返回的rows是一个元组列表,每个元组是(option_id, count)。在模板里遍历question.options,用counts.get(option.id, 0)取对应计数,没有回答的选项显示 0。
如果要在前端画柱状图,常见做法是把统计数据以 JSON 格式传给模板,再用 Chart.js 或 ECharts 渲染。Flask 里可以用json.dumps或者tojson过滤器。
<!-- result.html 中嵌入图表数据 --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <canvas id="chart"></canvas> <script> const ctx = document.getElementById('chart'); new Chart(ctx, { type: 'bar', data: { labels: {{ labels | tojson }}, datasets: [{ label: '选择人数', data: {{ values | tojson }}, backgroundColor: 'rgba(54, 162, 235, 0.6)' }] } }); </script>后端在渲染模板时传入labels和values两个列表即可。注意tojson过滤器会自动处理引号和转义,比手动json.dumps更安全。
4. 部署上线与性能注意:从本机 Flask run 到可访问服务
4.1 用 Gunicorn + Nginx 部署 Flask 问卷应用
flask run只适合开发阶段,它自带的重载和调试功能在生产环境下是安全隐患。正式部署时,常见的组合是 Gunicorn 作为 WSGI 服务器,Nginx 做反向代理和静态文件服务。
# 安装 gunicorn pip install gunicorn # 启动应用,4 个 worker 进程 gunicorn -w 4 -b 127.0.0.1:8000 app:app-w 4表示 4 个 worker 进程,一般设置为 CPU 核心数 × 2 + 1。app:app的意思是app.py文件里的app对象。如果你的入口文件叫run.py,那就是run:app。
Nginx 配置的核心是把外部请求转发到 Gunicorn 监听的端口,同时直接处理static/目录下的静态文件。
server { listen 80; server_name your_domain_or_ip; location /static/ { alias /path/to/your/project/static/; expires 30d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }proxy_set_header这几行不是可选的。没有它们,Flask 里request.remote_addr拿到的永远是127.0.0.1,如果你在问卷里做了 IP 限制或者防重复提交,这个信息就完全失效了。
4.2 数据库选型与并发写入的边界
SQLite 在毕设演示阶段完全够用,但它有一个硬限制:写操作是串行的,同一时刻只允许一个写入者。问卷应用如果同时有几十个人提交,SQLite 会直接报database is locked。如果你的问卷预计并发填写人数超过 20,建议换成 MySQL 或 PostgreSQL。
# MySQL 配置示例 app.config['SQLALCHEMY_DATABASE_URI'] = \ 'mysql+pymysql://user:password@localhost/survey_db?charset=utf8mb4'charset=utf8mb4必须加,否则中文和 emoji 会乱码。pymysql是纯 Python 驱动,安装方便,pip install pymysql即可。换数据库后需要重新db.create_all()建表,SQLite 里的数据不会自动迁移。
另一个容易被忽略的点是db.session的线程安全性。Flask-SQLAlchemy 默认使用 scoped session,每个请求独立,不会跨请求共享。但如果你在后台线程里操作数据库,必须手动管理 session 的生命周期,否则会出现连接泄漏。
5. 避坑与排查:Flask 问卷项目最常见的 5 个翻车现场
5.1 表单提交后 400 错误:CSRF 与字段名不匹配
现象:点击提交按钮后页面跳转到 400 Bad Request,没有任何详细错误信息。
原因:两种可能。一是启用了 Flask-WTF 的 CSRF 保护但模板里没加{{ form.csrf_token }};二是前端表单字段名和后端request.form.get()的 key 不一致,比如前端用question_1而后端取q_1。
解决:先看后端路由里取的字段名是什么,再打开浏览器开发者工具的 Network 面板,看提交的 Form Data 里实际的 key 是什么。两者对齐即可。如果用了 Flask-WTF,确认模板里每个<form>都包含了 CSRF token 隐藏字段。
5.2 中文乱码:数据库编码与响应头双重问题
现象:问卷标题或选项里的中文在页面上显示为????或乱码方块。
原因:SQLite 默认编码是 UTF-8,一般不会出问题;MySQL 如果建库时没指定utf8mb4,或者连接字符串里没加charset=utf8mb4,中文就会乱码。另外 Flask 返回 JSON 时默认ensure_ascii=True,中文会被转义成\uXXXX。
解决:MySQL 建库语句用CREATE DATABASE survey_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,连接字符串加?charset=utf8mb4。Flask 返回 JSON 时设置app.config['JSON_AS_ASCII'] = False(Flask 2.2 之前)或app.json.ensure_ascii = False(Flask 2.2 之后)。
5.3 多选答案只入库了最后一个选项
现象:多选题用户勾了三个选项,数据库里只存了一条记录。
原因:后端用了request.form.get()而不是request.form.getlist()。get()只返回同名参数的第一个值,getlist()才返回列表。
解决:把request.form.get(field_name)改成request.form.getlist(field_name),然后遍历列表逐个入库。同时确认前端多选框的name属性完全相同,不能加[]后缀(Flask 不认 PHP 那套)。
5.4 部署后静态文件 404:Nginx alias 路径末尾的斜杠
现象:本地开发时 CSS 和 JS 正常加载,部署到 Nginx 后页面样式全丢,控制台报 404。
原因:Nginx 的alias指令对路径末尾斜杠敏感。alias /path/to/static;和alias /path/to/static/;行为不同,前者会把location /static/的前缀也拼进去。
解决:location /static/配alias /path/to/static/;,两者末尾都带斜杠,保持一致。或者改用root指令,把静态文件放在root指向的目录下的static/子目录里。
5.5 db.create_all() 不生效:应用上下文缺失
现象:运行db.create_all()后数据库文件生成了,但里面一张表都没有,或者报RuntimeError: Working outside of application context。
原因:Flask-SQLAlchemy 3.x 要求所有数据库操作都在应用上下文内执行。直接在模块顶层调用db.create_all()不会自动创建上下文。
解决:用with app.app_context():包裹db.create_all()。如果是在 Flask shell 里操作,先flask shell进入上下文环境再执行。另外确认模型类已经被导入,create_all()只识别已经加载到db.metadata里的模型,如果models.py没有被 import,表也不会创建。
6. 让问卷项目从"能跑"到"能讲":三个进阶技巧
6.1 用蓝图拆分路由,让代码结构经得起答辩追问
单文件app.py写到底在毕设演示阶段没问题,但答辩老师如果问"如果我要加一个用户管理模块,你打算怎么改",单文件结构就很难回答。Flask 的蓝图(Blueprint)就是为这种场景设计的。
# survey_bp.py from flask import Blueprint, render_template survey_bp = Blueprint('survey', __name__, url_prefix='/survey') @survey_bp.route('/<int:survey_id>') def detail(survey_id): return render_template('survey_detail.html') # app.py 中注册 from survey_bp import survey_bp app.register_blueprint(survey_bp)蓝图的优势在于:每个模块有独立的url_prefix,路由不会冲突;模板可以放在模块自己的templates/子目录下;测试时可以单独注册某个蓝图。把问卷管理、用户认证、数据统计拆成三个蓝图,代码量没增加多少,但结构清晰度提升一个档次。
6.2 用 Flask-Migrate 管理数据库变更,避免删库重建
毕设做到后期改模型是常事,每次改完删库重建虽然简单,但如果已经录入了测试数据就很麻烦。Flask-Migrate 基于 Alembic,可以生成迁移脚本,保留数据的同时更新表结构。
pip install flask-migrate # 初始化迁移仓库(只需一次) flask db init # 生成迁移脚本 flask db migrate -m "add deadline column to survey" # 应用迁移 flask db upgradeflask db migrate会自动对比模型和数据库的差异,生成升级和降级脚本。生成后建议打开脚本看一眼,Alembic 的自动检测不是万能的,字段重命名它可能会识别成删旧列加新列,导致数据丢失。确认无误再执行upgrade。
6.3 问卷结果导出 CSV:一个让答辩加分的小功能
统计页面能看图表,但如果能一键导出 CSV,答辩时的演示效果会好很多。Flask 的Response对象可以直接返回 CSV 内容。
import csv from io import StringIO from flask import Response @app.route('/survey/<int:survey_id>/export') def export_csv(survey_id): survey = Survey.query.get_or_404(survey_id) output = StringIO() writer = csv.writer(output) # 表头 writer.writerow(['提交时间', '问题', '答案']) answers = Answer.query.filter_by(survey_id=survey.id).all() for a in answers: question = Question.query.get(a.question_id) if a.option_id: option = Option.query.get(a.option_id) answer_text = option.label if option else '' else: answer_text = a.text_value or '' writer.writerow([a.submitted_at, question.content, answer_text]) output.seek(0) # 加 BOM 让 Excel 正确识别 UTF-8 return Response( '\ufeff' + output.getvalue(), mimetype='text/csv', headers={'Content-Disposition': f'attachment; filename=survey_{survey_id}.csv'} )\ufeff是 UTF-8 BOM,不加的话 Excel 打开 CSV 中文会乱码。StringIO在内存里拼 CSV 内容,数据量不大时完全够用;如果回答数上万,建议用tempfile写临时文件再流式返回。
这三个技巧的共同点是:它们不会让项目功能变多,但会让代码质量和演示效果明显提升。答辩老师问"你这个项目有什么技术难点"的时候,蓝图拆分、数据库迁移、CSV 导出都是能展开讲五分钟的点。
我做毕设那会儿,最大的教训是前期只顾着堆功能,后期想重构已经来不及了。如果重来一次,我会在写第一行路由之前就把蓝图目录建好,把flask db init跑一遍,哪怕当时用不上。这些基础设施不是锦上添花,是让你在最后两周不至于手忙脚乱的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取