
简介这是一套面向高校计算机相关专业学生的毕业设计完整源码主题为基于PythonFlask的问卷调查应用适合正在准备毕设、课程设计或想练手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(debugTrue, host0.0.0.0, port5000)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 --host0.0.0.0 --port5000浏览器访问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_keyTrue) title db.Column(db.String(200), nullableFalse) description db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.utcnow) is_active db.Column(db.Boolean, defaultTrue) questions db.relationship(Question, backrefsurvey, lazyTrue) 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(500), nullableFalse) q_type db.Column(db.String(20), defaultsingle) # single / multiple / text options db.relationship(Option, backrefquestion, lazyTrue) 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(200), 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), nullableTrue) text_value db.Column(db.Text, nullableTrue) # 填空题答案 submitted_at db.Column(db.DateTime, defaultdatetime.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 fq_{question.id} if question.q_type text: # 填空题直接取文本 val request.form.get(field_name, ).strip() if val: ans Answer(survey_idsurvey.id, question_idquestion.id, text_valueval) 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_idsurvey.id, question_idquestion.id, option_idint(oid)) db.session.add(ans) db.session.commit() flash(提交成功) return redirect(url_for(survey_result, survey_idsurvey.id)) return render_template(survey_detail.html, surveysurvey)这段代码里有几个容易翻车的点。第一request.form.getlist()用于多选request.form.get()用于单选和文本用错了会导致多选只拿到最后一个值。第二int(oid)转换前最好做一次校验如果前端传了非数字字符串会直接抛ValueError整个提交事务回滚。第三所有db.session.add()放在循环里最后统一commit()这样要么全部成功要么全部失败不会出现半份问卷入库的情况。模板端用 Jinja2 循环渲染问题和选项单选按钮的name统一用q_{{ question.id }}多选用getlist接收所以name也要一致。填空题用textarea或input typetext后端根据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_idsurvey.id, question_idquestion.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_idsurvey.id, question_idquestion.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, surveysurvey, statsstats)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 srchttps://cdn.jsdelivr.net/npm/chart.js/script canvas idchart/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] \ mysqlpymysql://user:passwordlocalhost/survey_db?charsetutf8mb4charsetutf8mb4必须加否则中文和 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或者连接字符串里没加charsetutf8mb4中文就会乱码。另外 Flask 返回 JSON 时默认ensure_asciiTrue中文会被转义成\uXXXX。解决MySQL 建库语句用CREATE DATABASE survey_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接字符串加?charsetutf8mb4。Flask 返回 JSON 时设置app.config[JSON_AS_ASCII] FalseFlask 2.2 之前或app.json.ensure_ascii FalseFlask 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 部署后静态文件 404Nginx 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_idsurvey.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(), mimetypetext/csv, headers{Content-Disposition: fattachment; filenamesurvey_{survey_id}.csv} )\ufeff是 UTF-8 BOM不加的话 Excel 打开 CSV 中文会乱码。StringIO在内存里拼 CSV 内容数据量不大时完全够用如果回答数上万建议用tempfile写临时文件再流式返回。这三个技巧的共同点是它们不会让项目功能变多但会让代码质量和演示效果明显提升。答辩老师问你这个项目有什么技术难点的时候蓝图拆分、数据库迁移、CSV 导出都是能展开讲五分钟的点。我做毕设那会儿最大的教训是前期只顾着堆功能后期想重构已经来不及了。如果重来一次我会在写第一行路由之前就把蓝图目录建好把flask db init跑一遍哪怕当时用不上。这些基础设施不是锦上添花是让你在最后两周不至于手忙脚乱的后悔药。希望帮到你。本文还有配套的精品资源点击获取