1. 项目概述与方案选型为什么是VueDjango/Flask这套python基于vue的大学生问卷调查系统我拿到手第一反应是典型的课程设计或者毕业设计题目技术栈一眼看穿Python做后端、Vue做前端、PyCharm当开发环境后端框架在Django和Flask之间二选一。说实话这个组合本身没什么稀奇但越是看起来常规的题目越容易翻车因为牵扯到前后端分离、接口设计、数据统计、部署联调一大堆琐碎的环节任何一个环节卡住整个项目都有可能推进不下去。很多同学做这种选题的时候第一个纠结的是Django和Flask我到底选哪个。标题里两个都写了说明在需求分析阶段就摇摆过。这里我给一个非常明确的建议如果是为了快速完成一个有用户管理、有问卷CRUD、有结果统计的完整系统优先选Django因为它自带Admin后台、ORM、认证体系和表单校验省去的开发时间远超你学习它所花的时间。Flask适合做轻量API服务但如果要从零搭用户体系、权限控制、数据库迁移你至少要自己装Flask-SQLAlchemy、Flask-Login、Flask-Migrate折腾一圈下来相当于把Django已经帮你做好的事情重新造了一遍轮子。那Flask有没有优势呢有。如果你打算把整个系统做成微服务架构或者你后续想往FastAPI路线转用Flask练手会灵活一些。但就问卷调查这个场景来说Django的单体架构加DRFDjango REST Framework往外抛接口配合Vue做SPA是我见过最稳、案例最多、出了问题最容易查到解决方案的组合。这篇文章后面所有实操都基于Django但每一步我都会标注Flask里对应的做法方便两个方向都能落地。这套系统的核心价值在于它不是一个纯展示用的静态页面而是真正能跑通创建问卷 → 发布分享 → 学生填写 → 数据回收 → 图表统计的完整闭环。你在答辩或者项目汇报的时候把这条链路现场走一遍比讲十页PPT都管用。2. 需求拆解与架构设计先画清楚再动代码2.1 功能模块的边界要提前划好问卷调查系统的功能看起来就五个字发问卷、收答案但真要落地每个模块都比想象中细碎。我习惯先把角色理清再顺着角色去划功能边界。这个系统有两个天然角色管理员教师/发起方和普通用户学生/填写人。管理员侧的功能登录注册或管理员直接预置账号、问卷的创建/编辑/删除/复制、问卷的发布与关闭、查看每份问卷的回收数量与详细答卷、针对单选/多选/填空类题目查看统计图表、按学院或专业维度筛选数据、导出Excel。学生侧的功能通过问卷链接或列表进入填写页、按题目类型完成作答、提交后看到提交成功回执、查看自己填过的问卷历史如果有需求的话、修改已提交的答案如果管理员允许。功能划完之后我还建议画一张简单的请求路径图Vue页面通过Axios走HTTP请求到Django的APIDjango通过ORM操作SQLite或MySQL数据返回后由Vue动态渲染。这个路径你自己心里清楚就好答辩的时候画在黑板上或者PPT里一眼就能让人看懂你的架构不是纸糊的。2.2 数据库设计是整张答卷质量的分水岭问卷调查系统的数据库设计很多新手喜欢一表走天下所有问卷、题目、答案揉在一张表里最后统计的时候发现SQL写得想哭。规范的思路至少要分四张核心表用户表、问卷表、题目表、选项表再加一张答卷记录表或者明细表用来存每份提交的答案。用户表沿用Django自带的auth_user表就够加一个profile字段区分角色也行不做复杂权限系统的话没必要另起炉灶。问卷表Questionnaire包含标题、描述、创建人、发布时间、截止时间、状态草稿/已发布/已关闭其中状态字段建议用IntegerField映射别用CharField存中文后续筛选比较会方便很多。题目表Question的要点是类型字段单选、多选、填空用数字标识再加一个question_index排序字段——不要依赖数据库的自增ID排序因为你做编辑功能的时候题目的顺序是会被调整的。选项表Option挂在题目下面对应的就是问卷里每个选项的文本和编号。答卷表的核心设计是一张提交记录对应一条明细行统计的时候你只需要按题目聚合明细行就能得到每个选项被选了多少次。这里特别提醒一个容易踩的坑答题明细的存储方式。新手常犯的错误是把一份问卷的答案全部塞进一个JSON字段读起来很爽统计的时候非常痛苦。比如你要统计单选题第3题的A选项有多少人选存JSON你得把每一份的JSON拿出来解析逐条累加一百份答卷还好一千份就明显吃力。正确做法是每条回答建立独立的AnswerRecord行外键指向题目ID和选项ID这样一条SQL就能count出频次。2.3 API接口的分层设计要跟前端页面一一对应Django做后端时我强烈建议用DRFDjango REST Framework而不是手写JSONResponse因为DRF的序列化器Serializer能帮你把ORM对象直接转成前端友好的JSON结构还能顺手做输入校验。API设计上REST风格是稳妥的选择/api/login/、/api/questionnaires/、/api/questionnaires/{id}/、/api/questionnaires/{id}/submit/、/api/questionnaires/{id}/stats/。登录这块我建议用Token或者JWT别用Django默认的Session因为前端是独立部署的Vue项目Session的Cookie处理在跨域场景下会有很多额外配置。用DRF自带的TokenAuthentication最简单的做法是在settings.py里注册authentication_classes然后登录接口返回一个token给前端存到localStorage后续每个请求带上Authorization头就够了。Flask那边的对应方案是PyJWT配合装饰器做鉴权原理完全相同。前端的页面路由也要在动手之前列清楚。问卷调查系统至少需要这几个页面登录页、问卷列表页管理端、问卷编辑页建问卷/改问卷、问卷填写页学生端、数据统计页。这张页面清单直接决定Vue Router的routes怎么配也决定你需要写哪些组件的骨架。3. 实操环节从零到能跑通的全过程3.1 环境准备与PyCharm项目结构规划先把开发环境梳理清楚。Python版本建议3.8到3.11之间太高的版本偶尔会遇到第三方库还没适配的情况。Node.js建议装16.x或18.x的LTS版本Vue CLI或者Vite都能用但如果你不熟悉前端工程的构建配置直接上Vite加Vue3的官方模板是最省心的。PyCharm建议用专业版因为专业版对Django和JavaScript的支持更完善社区版也能用只是缺少数据库可视化面板和部分Web开发辅助功能。PyCharm里创建项目的时候我推荐一个干净的做法新建一个纯Python项目当作后端工作目录再用命令行工具把Vue前端生成在同级目录下。项目结构大概长这样questionnaire_system/ ├── backend/ # Django工程根目录 │ ├── manage.py │ ├── config/ # Django配置文件目录 │ └── apps/ │ ├── users/ # 用户模块 │ └── survey/ # 问卷模块 └── frontend/ # Vue工程根目录 ├── src/ │ ├── api/ # Axios请求封装 │ ├── views/ # 页面组件 │ └── router/ # 路由配置 └── package.json注意一个问题PyCharm打开整个大目录之后Python解释器和Node解释器要分别设置。很多人在同一个窗口里折腾半天发现前端依赖装不上其实是没把运行环境切换对。3.2 Django后端模型、序列化与视图的落地后端的第一步不是写代码而是搭建Django项目骨架。在backend目录下执行conda create -n survey python3.10 conda activate survey pip install django djangorestframework django-cors-headers django-admin startproject config . python manage.py startapp users python manage.py startapp surveysettings.py里需要注册三个关键项rest_framework、corsheaders、以及两个自定义app。corsheaders的中间件要加在CommonMiddleware前面否则请求会被预检挡掉。顺便在INSTALLED_APPS里把corsheaders放在rest_framework前面这些都是DRF加CORS的标准操作。接下来写模型。我用Django自带的User做账户然后在survey这个app里定义问卷和题目的模型from django.db import models from django.contrib.auth.models import User class Questionnaire(models.Model): STATUS_CHOICES [ (0, 草稿), (1, 已发布), (2, 已关闭), ] title models.CharField(max_length200, verbose_name问卷标题) description models.TextField(blankTrue, verbose_name问卷说明) creator models.ForeignKey(User, on_deletemodels.CASCADE, related_namequestionnaires) status models.IntegerField(choicesSTATUS_CHOICES, default0, verbose_name状态) created_at models.DateTimeField(auto_now_addTrue) deadline models.DateTimeField(nullTrue, blankTrue, verbose_name截止时间) class Question(models.Model): TYPE_CHOICES [ (0, 单选), (1, 多选), (2, 填空), ] questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE, related_namequestions) qtype models.IntegerField(choicesTYPE_CHOICES, verbose_name题目类型) text models.TextField(verbose_name题目内容) question_index models.IntegerField(default0, verbose_name排序) is_required models.BooleanField(defaultTrue, verbose_name是否必答) class Option(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE, related_nameoptions) text models.CharField(max_length200, verbose_name选项内容) option_index models.IntegerField(default0, verbose_name选项顺序) class AnswerRecord(models.Model): questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE, related_nameanswers) question models.ForeignKey(Question, on_deletemodels.CASCADE, related_nameanswers) option models.ForeignKey(Option, on_deletemodels.CASCADE, nullTrue, blankTrue) content models.TextField(blankTrue, verbose_name填空内容) user models.ForeignKey(User, on_deletemodels.CASCADE, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue)这里有个细节我单独说一下Questionnaire的创建人如果是管理员教师普通学生只需要填写问卷并不需要登录就能提交所以AnswerRecord的user字段要做成nullTrue。我们实际做的时候发现强制学生登录会让回收率大幅下降因为多一步登录就多一个流失点。如果要记名可以在问卷表加一个anonymous字段不记名的问卷提交时用session生成一个临时标识就够。模型写完之后执行迁移然后注册到Django Admin。Admin面板在开发阶段非常有用你可以直接在里面造数据、改状态比来回调接口方便得多。接下来写序列化器。问卷的嵌套结构比较典型一份问卷包含多个题目每个题目包含多个选项。DRF里用嵌套序列化器表达这个关系创建的时候要重写create方法把嵌套的题目和选项一并保存from rest_framework import serializers from .models import Questionnaire, Question, Option class OptionSerializer(serializers.ModelSerializer): class Meta: model Option fields [id, text] class QuestionSerializer(serializers.ModelSerializer): options OptionSerializer(manyTrue, requiredFalse) class Meta: model Question fields [id, qtype, text, options, is_required] class QuestionnaireSerializer(serializers.ModelSerializer): questions QuestionSerializer(manyTrue) creator_name serializers.CharField(sourcecreator.username, read_onlyTrue) class Meta: model Questionnaire fields [id, title, description, creator_name, status, created_at, deadline, questions] def create(self, validated_data): questions_data validated_data.pop(questions) questionnaire Questionnaire.objects.create(**validated_data) for q_index, q_data in enumerate(questions_data): options_data q_data.pop(options, []) q_data[question_index] q_index question Question.objects.create(questionnairequestionnaire, **q_data) for o_index, o_data in enumerate(options_data): o_data[option_index] o_index Option.objects.create(questionquestion, **o_data) return questionnaire这个create方法很关键因为前端一次性把整份问卷的JSON传给后端后端必须在一次事务里把问卷、题目、选项全部写入数据库。我见过很多人的实现是前端把题目循环调接口创建问卷存了一半后面失败就留下脏数据坑很大。DRF的嵌套序列化里重写create是标准解法逻辑清晰还能保证数据一致性。视图层用ViewSet就行router注册之后自动生成增删改查接口。问卷提交的接口我单独写了一个APIView因为它的业务逻辑跟标准CRUD不一样需要处理校验、去重、统计更新刻意跟ViewSet分开代码更清晰class SubmitAnswerView(APIView): def post(self, request, pk): questionnaire get_object_or_404(Questionnaire, pkpk) data request.data records data.get(answers, []) # 校验问卷是否已关闭 if questionnaire.status 2: return Response({detail: 问卷已关闭}, status400) for item in records: question_id item.get(question_id) option_id item.get(option_id) content item.get(content, ) AnswerRecord.objects.create( questionnairequestionnaire, question_idquestion_id, option_idoption_id, contentcontent, userrequest.user if request.user.is_authenticated else None ) return Response({detail: 提交成功}, status201)统计接口的实现是这类系统最容易马虎的地方。我的做法是写一个聚合查询按题目ID分组统计每个选项被选的次数然后返回给前端做图表。Django ORM里用Count加values就能搞定这类聚合不用写原生SQL。class StatsView(APIView): def get(self, request, pk): questionnaire get_object_or_404(Questionnaire, pkpk) result [] for q in questionnaire.questions.all(): item { question_id: q.id, text: q.text, qtype: q.qtype, total: q.answers.count(), options: [] } if q.qtype 2: # 填空 item[contents] list(q.answers.values_list(content, flatTrue)) else: for opt in q.options.all(): count q.answers.filter(optionopt).count() item[options].append({option: opt.text, count: count}) result.append(item) return Response(result)Flask方向的做法思路完全一样只是把ORM换成了Flask-SQLAlchemy把ViewSet换成了蓝图加装饰器路由。Django里叫序列化器的东西在Flask里用Marshmallow来实现。逻辑上没有本质区别选哪套框架都能完成这套设计。3.3 Vue前端搭建工程、封装请求、写页面前端工程我用Vite创建指令是npm create vuelatest frontend在交互式选项里选择Vue Router、Pinia如果要做状态管理的话、Axios按需引入。Element Plus是问卷调查系统非常合适的组件库它的el-form组件对动态表单的支持做得很好做问卷编辑页面时最顺手。先封装Axios。前端的API请求有一个统一出口方便处理Token注入和错误拦截。我在src/api/request.js里这样写import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.detail || 请求失败) return Promise.reject(error) } )baseURL用了相对路径/api这是配合Vite开发代理的写法。在vite.config.js里设置代理把前端的/api前缀转发到Django的8000端口server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } }这样设置之后开发阶段不会遇到跨域问题。如果你用Flasktarget改成Flask的5000端口就行。跨域问题在生产环节还需要配合Nginx的反向代理解决这里先把开发环境的流程跑通。问卷编辑页面是前端工作量最大的部分。我的做法是用一个数组renderQuestions维护当前问卷的题目列表每个题目都是一个子组件根据qtype动态渲染单选/多选/填空的编辑表单。单选和多选的选项也用一个数组维护支持增删选项。页面底部统一提交把整个问卷对象扔给后端创建接口。问卷填写页的逻辑相对简单难点在于动态校验。el-form对动态字段的校验规则是prop要写成数组下标的形式比如questions[0].options[1]。Element Plus对这个场景支持得不错唯一要注意的是v-model绑定在interface循环里的值要用ref包装否则二维数组的响应式会失效。数据统计页我用ECharts画柱状图和饼图。从stats接口拿到的数据是标准结构化数组直接feed给ECharts的series就好。柱状图适合看多选/单选各选项的分布饼图适合看占比两个图可以共用一个数据源切换图表类型只是改option配置。3.4 前后端联调时的几个关键检查点联调是整个开发过程里真正考验耐心的阶段。我建议按这个顺序检查先确认后端API用Postman单测通过、再确认前端静态页面能独立渲染、最后才是配对联调。配对联调时重点看三个东西跨域配置、登录态传递、表单提交的数据结构。登录态传递是最容易出问题的一环。前端在登录页面拿到token之后存入localStorage后续每个请求自动带着Token头。后端在DRF里配置好TokenAuthentication之后需要把需要鉴权的接口加上permission_classes。我建议问卷创建、编辑、删除走鉴权问卷填写和统计公开因为填写的人可能是匿名用户。表单提交的数据结构是另一个高频翻车点。前端el-form收集到的数据结构可能跟后端序列化器预期的结构不一样比如Element Plus的表单会把数字类型的题目类型变成字符串需要跟前端约定好或者在后端Serializer里加一个to_internal_value做类型转换。检查的时候最实用的方式就是打开浏览器控制台看Network面板里实际发出的Payload长什么样再看后端返回的Validation Error提示一行一行对数据字段名是否匹配。4. 常见问题与排查技巧实录4.1 跨域请求被拦截的解决方案这个坑几乎100%会出现。症状是前端请求发出去控制台报CORS错误后端日志显示请求根本没进视图。原因通常是前端在3000/5173端口、后端在8000端口协议、域名都不同端口也不一样属于典型的跨域场景。开发阶段最简单的方式就是我上面提到的Vite代理。它本质上是用Vite的dev server做了一层转发浏览器里看到的请求是同源的自然没有跨域问题。但如果你非要前端直连后端地址那就要在Django后端配置django-cors-headersCORS_ALLOWED_ORIGINS [ http://localhost:5173, ] CORS_ALLOW_CREDENTIALS TrueFlask方向的对应配置是flask-cors扩展。我实测下来优先推荐代理方案原因只有一个配置代理之后前端请求路径和后端路由路径是解耦的部署阶段你只需要把代理地址从localhost改成服务器域名前端代码一行都不用改。4.2 Django迁移时常见的数据表异常开发过程中频繁修改models是常态但Django的迁移机制在某些改动下会给出让你怀疑人生的报错。最常见的两类加字段时提示需要提供默认值改字段类型时提示迁移无法自动完成。解决思路是先看清楚makemigrations的提示如果要求默认值先给一个临时默认值迁移完再在代码层面对老数据做清洗。另外还有一个小建议如果前期开发阶段数据不重要数据库结构变化太频繁别忍着慢慢迁移直接把SQLite文件删了重新migrate效率最高。答辩前再固定数据结构导入一份完整的演示数据。这个操作前提是你没有依赖真实生产数据毕设项目完全适用。4.3 Vue路由刷新404问题前端路由用了history模式的话刷新页面会遇到404尤其是在部署环境。原因是路由交由前端Router管理后后端服务器并不知道前端路由对应哪个文件所有请求都重新去找index.html找不到就返回404。解决方法是后端或Nginx做rewrite把不存在的路径统一重写到index.html。Django方向可以在urls.py最后加一个catchallFlask方向可以在Flask里配置一个通配路由返回前端模板。如果不想处理这些部署细节最简单的方案是用hash模式路由变成/#/开头刷新永远走index.html不会有404。代价是URL不太美观。做课程设计的话hash模式完全够用。4.4 数据统计的边界情况空指针和零值统计功能做完之后测试时发现了两个很有意思的边界情况。第一个是某道题一个回答都没有前端的图表直接空白因为没有数据给ECharts。处理方式是后端返回total0时前端主动显示一个暂无数据的占位状态。第二个是多选题的统计口径一道多选题一个用户选了三个选项统计结果是三个选项各加一不是这道题算一个回答。这个逻辑在表达层面容易让答辩评委疑惑需要在前端统计页加一个说明注明多选题的人数和选项选择次数的区别。还有一个容易忽略的问题填空题的答案里如果包含Excel特殊字符比如开头的字符串导出Excel时有公式注入风险。处理方式是导出时把所有字段先转成字符串再在单元格里加一个单引号前缀。4.5 PyCharm相关的几个日常坑用PyCharm开发Django项目的时候最常见的坑是解释器配置不对。你conda创建了survey环境但PyCharm右上角还是系统默认Python导致终端里pip install装了包PyCharm运行时报ModuleNotFoundError。解决方法是File → Settings → Project → Python Interpreter里把解释器切换成conda环境的路径然后重启。另一个PyCharm相关的问题是数据库面板。如果你用的SQLitePyCharm专业版的Database面板可以直接看到.db文件的内容调试数据时方便不少。社区版没有这个功能替代方案是装一个SQLite Viewer插件。5. 写在最后的个人经验这套问卷调查系统从需求分析到开发完成我反复做下来最大的体会是技术本身不难难的是把用户要什么翻译成程序怎么做。问卷调查这个场景本质上是数据的收集与结构化整理只要数据库设计不歪后端把CRUD和统计接口写清楚前端老老实实按页面清单做出来系统就不会跑偏。有几个我反复跟新人强调的操作习惯在这里也分享一下。第一每个阶段结束都要提交一次Git尤其不能跳过数据库模型变更前后的提交因为模型改崩了靠Git回滚是最快的方式。第二接口设计完成后先写一份简单的API文档哪怕就用Markdown前端开发的时候对照文档写请求避免来回问后端这里返回什么字段。第三答辩前一天不要再改功能了只做数据准备和流程演练确保登录、建卷、发卷、填卷、出统计这一条链路上没有任何一步是需要现场临场发挥的。如果你拿到的是Flask方向的题目思路完全可以把这套逻辑平移过去把Django的app改成Flask的蓝图把ORM模型改成Flask-SQLAlchemy的模型把DRF序列化器改成Marshmallow Schema前端部分一行都不用变。框架只是工具真正值钱的是对业务逻辑的拆解和对数据流动的理解。把上面这套流程完整走一遍不管答辩还是上项目你都稳得住。