
简介本资源是一套面向计算机专业本科生的毕业设计级问卷调查系统源码基于Python与Django框架开发完整覆盖用户管理、问卷CRUD、多题型答题、结果统计分析及报表导出等核心功能适用于毕设开发、课程设计与Web全栈能力实训。压缩包共46个文件含28个Python后端逻辑文件models/views/urls等、7个HTML模板页、5个JavaScript交互脚本、1个CSS样式文件及README.md等辅助文档总大小787KB结构清晰模块划分明确如api/web/surveySystem等子应用便于理解MVT架构与工程组织方式。已有288人学习下载资源提供开箱即用的可运行项目包含数据库迁移脚本、requirements依赖清单、基础测试用例及部署说明助读者快速掌握Django ORM建模、表单处理、模板渲染与安全防护实践。 作为一个经常用Django给业务部门搭内部工具的人我看到基于pythonDjango的问卷调查系统这种项目第一反应不是去下载压缩包而是先想清楚一件事这个系统到底是用来做匿名调研、课程反馈、客户满意度收集还是公司内部的流程审批式问卷因为不同场景下权限模型、题型设计、结果统计方式差别非常大。这篇文章我会从项目落地角度把一个典型的PythonDjango问卷调查系统拆开讲透。覆盖从解压项目后怎么跑起来、数据模型怎么设计、动态表单怎么实现、答卷结果怎么统计、部署上线有哪些坑以及我在实际项目里迭代问卷功能时踩过的各种细节问题。无论是你刚拿到一份开源问卷系统源码还是打算从零自己写一个这篇文章都可以当一份实操参考。1. 解压之后别急着跑先把Django项目骨架和本地启动逻辑理清楚1.1 一个问卷系统压缩包里最常见的工程结构拿到这样一个ZIP包打开后通常会看到下面这种典型的Django工程布局survey_project/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置目录有的叫 project/ 或 mysite/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ ├── asgi.py ├── apps/ # 应用目录 │ ├── questionnaire/ │ │ ├── models.py │ │ ├── views.py │ │ ├── forms.py │ │ ├── admin.py │ │ ├── migrations/ │ │ ├── templates/ │ └── user/ # 用户相关扩展不一定有 ├── static/ # 静态文件 ├── media/ # 上传文件目录 ├── templates/ # 项目级模板 └── db.sqlite3 # 默认数据库不同作者习惯不同有的是单应用一个survey应用包所有有的是多应用拆分。但不管怎么拆你最先要找到的永远是manage.py和requirements.txt这两个文件。前者是Django项目的操作入口后者决定了项目依赖了哪些版本的包。1.2 本地启动前必须检查的三件事很多同学下载完项目直接python manage.py runserver结果报一堆红色错误。我建议按下面顺序来第一建立独立的虚拟环境。这一步别偷懒。问卷系统为了兼容不同Django版本经常会出现依赖冲突。用venv隔离环境是最省心的cd survey_project python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate第二看requirements.txt里依赖包的版本范围。如果一个老项目写的是Django2.2.*而你本机装的是Python 3.11甚至3.12那大概率会出兼容性问题因为旧版本Django不支持新版本Python。遇到这种情况我的经验是先别急着升级到Django 4.x或5.x优先用项目锁定的版本。除非你对项目代码有十足的把握否则顺手升级Django大版本通常会带出模板语法、路由写法、中间件配置等一堆连锁问题。第三执行迁移再跑服务。pip install -r requirements.txt python manage.py migrate python manage.py runserver 127.0.0.1:8000为什么要先跑migrate因为Django内置的auth、session、admin应用都需要建数据表不迁移直接启动八成会在访问后台或登录时白屏报错。如果项目根目录下有db.sqlite3文件说明作者把本地数据库一起打包了这种情况可以跳过migrate直接启动先进后台看看效果。但如果是MySQL或PostgreSQL的配置只给了.sql导出文件那就需要自己在本地先建好数据库再导入。2. 问卷系统的数据模型核心是一张问卷-题目-选项-答卷的关系网2.1 四张核心表先把这个关系理清楚问卷系统本质上是结构化数据的收集与汇总所以数据模型设计是整套系统的地基。最经典、也最好用的设计是下面这四张核心表模型字段示意作用Questionnairetitle, description, created_at, start_time, end_time, status问卷本身的基本信息和生命周期Questionquestionnaire(FK), question_type, content, order, is_required问卷下的题目含单选、多选、填空等类型Optionquestion(FK), content, order选择题下的选项填空题可以没有选项Answerquestionnaire(FK), question(FK), user, content, created_at每道题的答题结果这是标准的关系型建模思路。问卷Questionnaire是一对多题目Question题目Question是一对多选项Option而用户提交的每个答案Answer记录的是哪份问卷、哪道题、什么内容。有人会问为什么不把一次提交的所有答案打包成一条记录如果你只是做个量级很小的表单收集把答案放进JSONField里确实省事。Django 3.1自带models.JSONField存JSON数组轻轻松松。但一旦你需要做统计比如这道单选ABCD各有多少人选了JSON字段的查询代价就会明显上升得把每条数据取出来再在代码里循环数据量大了以后相当痛苦。老老实实用关系表存后面对接ECharts做图表、导出Excel、做交叉分析都会很顺畅。2.2 题型抽象用整数映射还是用多表继承调研问卷最常见的题型包括单选、多选、填空、下拉、量表评分比如NPS 0-10分、排序题。不同的题型存储和处理逻辑差别很大。一个常见做法是用question_type字段做类型区分class Question(models.Model): QUESTION_TYPES [ (single, 单选题), (multiple, 多选题), (fill, 填空题), (score, 量表题), (sort, 排序题), ] questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE, related_namequestions) question_type models.CharField(max_length20, choicesQUESTION_TYPES, defaultsingle) content models.TextField(verbose_name题目内容) order models.IntegerField(default0, verbose_name排序) is_required models.BooleanField(defaultTrue, verbose_name是否必答)这种设计上手快也够用。但如果你想在这个项目上做二次开发让不同题型拥有不同字段比如打分题要有min_score和max_score排序题要有max_options之类的约束那进一步的做法是用多表继承class QuestionBase(models.Model): # 公共字段 class Meta: abstract True class SingleChoiceQuestion(QuestionBase): options models.ManyToManyField(Option) ...对于大多数中小型问卷项目我建议别过度建模。我见过很多人一上来就抽象了五六张表结果后台管理界面变得复杂无比问卷编辑者也用不明白。现实里很多需求其实用Question一张表 Option一张表 前端根据question_type动态渲染就能解决。等确实遇到题型差异化字段需求时再加extra_config models.JSONField()把是否需要限制选项数量最低分最高分这类配置塞进JSON字段灵活性和开发效率都能兼顾。2.3 答案落库一份提交对应多条Answer记录我之前见过有人在Answer表里只存题目id和用户选的选项id列表用逗号拼接比如3,5,9。这种设计在导出Excel时看似方便但统计时反而不方便。更常见的做法是每道题单独一行记录多选题用selected_options models.ManyToManyField(Option, blankTrue)或者JSON数组。class Answer(models.Model): questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE, related_nameanswers) question models.ForeignKey(Question, on_deletemodels.CASCADE, related_nameanswers) user models.ForeignKey(settings.AUTH_USER_MODEL, nullTrue, blankTrue, on_deletemodels.SET_NULL) single_choice_value models.CharField(max_length255, blankTrue) multi_choice_values models.JSONField(defaultlist, blankTrue) fill_content models.TextField(blankTrue) score_value models.IntegerField(nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue)这个设计的逻辑是每次问卷提交系统会为该问卷下的每一道题生成多条Answer记录。这样就保证了后面无论是按题统计、按用户统计还是按时间维度分析都能灵活应对。代价是数据表膨胀得比较快但问卷系统本身量级一般不会像订单系统那样大所以完全能接受。另外一定要加一个response_id之类的字段来标记同一次提交否则你想获取某个人某次填写的完整问卷结果时就只能靠时间段和user_id来拼很容易出错。3. 后台编辑与发布问卷管理页到底该做成什么样3.1 别把所有期望都压在Django Admin上Django自带Admin很适合快速后台但问卷系统的编辑流程比较复杂——编辑者需要动态增删题目、配置选项、拖拽排序这些操作在原生Admin里体验并不好。我的实际经验是Admin可以做但只适合做数据管理的兜底不适合作为业务主界面。如果打算用Admin快速搭建管理后台建议至少做这样几个优化在admin.py里注册QuestionnaireInline让Question内联在问卷详情页里避免来回跳转class QuestionInline(admin.TabularInline): model Question extra 1 ordering (order,) admin.register(Questionnaire) class QuestionnaireAdmin(admin.ModelAdmin): inlines [QuestionInline] list_display (title, status, start_time, end_time, created_at)自定义Admin里的按钮来支持复制问卷。很多内部调研场景下每季度的满意度问卷题目基本一样只是时间变了。没有复制功能管理效率低到让人抓狂。Django Admin可以通过自定义actions实现批量复制或者在前端框架里放一个复制为草稿按钮。3.2 发布状态和有效期一种常见的三段式设计问卷的状态我建议设计成这三个草稿draft、发布中published、已结束closed。状态字段用CharFieldchoices就够了。class Questionnaire(models.Model): STATUS_CHOICES [ (draft, 草稿), (published, 发布中), (closed, 已结束), ] status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft)这里有个容易忽略的点如果你希望问卷到了end_time就自动截止单靠状态字段不行因为数据库不会定时把状态从published改成closed。应该在做逻辑判断时同时考虑状态和时间def is_active(self): from django.utils import timezone now timezone.now() if self.status ! published: return False if self.start_time and now self.start_time: return False if self.end_time and now self.end_time: return False return True等于说能否填写这个动作是通过状态 时间段两个条件共同决定的而不是依赖某个后台任务定时改状态。这种设计能避免因为Celery定时任务没跑或者服务器时区不对导致问卷提前/延后关闭的问题。4. 动态表单实现一道题一套渲染规则前端页面才能灵活4.1 视图层怎么把模型数据变成可交互的表单问卷系统的难点不在CRUD而在动态。同一个模板要能渲染单选、多选、填空、评分等完全不同的组件。实现方式有两条路线路线一用Django Form来动态生成表单字段。根据Question表的题型在视图里动态填充forms.Form的字段。如果是单选就添加一个forms.ChoiceField多选就forms.MultipleChoiceField填空就是forms.CharField。from django import forms class DynamicSurveyForm(forms.Form): def __init__(self, *args, **kwargs): questions kwargs.pop(questions) super().__init__(*args, **kwargs) for q in questions: field_name fquestion_{q.id} if q.question_type single: self.fields[field_name] forms.ChoiceField( choices[(opt.id, opt.content) for opt in q.options.all()], labelq.content, requiredq.is_required, widgetforms.RadioSelect, ) elif q.question_type multiple: self.fields[field_name] forms.MultipleChoiceField( choices[(opt.id, opt.content) for opt in q.options.all()], labelq.content, requiredq.is_required, widgetforms.CheckboxSelectMultiple, ) elif q.question_type fill: self.fields[field_name] forms.CharField( labelq.content, requiredq.is_required, widgetforms.Textarea(attrs{rows: 3}), )路线二前端用Vue/React这类框架后端起一个JSON接口让前端根据题型配置动态渲染组件。这是现在前后端分离项目的主流写法。后端只负责返回{ id: 12, question_type: single, content: 你对目前的工作环境满意吗, options: [ {id: 1, content: 非常满意}, {id: 2, content: 满意}, {id: 3, content: 一般}, {id: 4, content: 不满意} ] }前端拿到数据是单选就渲染el-radio-group是多选就渲染el-checkbox-group是评分就渲染el-rate。两种方式哪种好如果你只是交作业或做内部小工具我强烈推荐路线一Django Form自带验证、CSRF、字段错误信息几乎不用额外写前端逻辑。如果你打算做成长期维护的产品前端交互复杂、要拖动排序、要做条件逻辑选了否就跳到下一题那路线二更适合。条件逻辑控制在纯后端模板里会写得非常痛苦我试过代码可读性差到后来自己都不想维护。4.2 模板里怎么循环输出题目区块用Django动态表单时模板可以很简洁form methodpost {% csrf_token %} {% for field in form %} div classform-group label{{ field.label }}/label {{ field }} {% for error in field.errors %} p classerror{{ error }}/p {% endfor %} /div {% endfor %} button typesubmit提交问卷/button /form如果要做成多页问卷每页5题分页作答可以在URL里加?page1参数。后端从所有题目里切分出当前页应该显示的题目范围。分页的好处一个是减轻用户填写压力另一个是可以在前端做好进度条提升填写体验。需要注意的是分页问卷通常要配合暂存功能——也就是每点击下一页就把当前页答案存到session或数据库草稿否则用户填到第5页不小心刷新一下就前功尽弃了。4.3 匿名作答与登录用户作答问卷调查系统通常有匿名和登录两种模式。纯粹的内部员工调研一般走登录模式这样方便后面统计部门维度、职级维度的差异。对外的满意度调查则基本是匿名模式用户只需要填写即可。匿名模式下要注意防刷。最简单的方案是记录IP 浏览器指纹24小时内限制同一IP只能填一次。再严格一点的方案是发一次性链接token这个token是一次性的使用过就失效。Django里做这个token一般用secrets.token_urlsafe(32)存在问卷记录表里。对于需要发邮件给指定人填写的场景一次性链接比登录验证更快落地用户也不会因为忘记密码而被卡在填写流程外。5. 提交答卷后的处理逻辑清洗数据、防止重复、记录流水5.1 一次提交动作里的完整请求链路用户在答题页点了提交接下来后端至少要做以下几步校验问卷状态是否仍在有效期内。校验CSRF token和表单数据Django的Form组件自动完成。校验是否已提交过如果项目要求不重复提交。遍历用户提交的每个字段逐题生成Answer记录。保存本次提交的Response记录用于关联同一批答案。返回感谢页或已提交的JSON结果。def submit_survey(request, pk): survey get_object_or_404(Questionnaire, pkpk) if not survey.is_active(): return render(request, survey/expired.html) questions survey.questions.all().order_by(order) form DynamicSurveyForm(request.POST, questionsquestions) if form.is_valid(): response Response.objects.create(questionnairesurvey, userrequest.user if request.user.is_authenticated else None) for q in questions: value form.cleaned_data.get(fquestion_{q.id}) Answer.objects.create( responseresponse, questionq, valuevalue, ) return redirect(survey:thanks, pksurvey.pk)这里我特意用了一个Response表把所有答案分成组。这种设计和前面的Answer表配合做同一份问卷提交结果的整体查看时非常方便。5.2 防止重复提交实战中最实用的三种方案方案一数据库唯一约束。如果问卷要求每个登录用户只能提交一次给Response表加unique_together (questionnaire, user)。这样就算前端没有控制数据库层也能兜住。缺点是对匿名用户无能为力因为匿名用户没有user_id。方案二Cookie记录。用户提交成功后在本地Cookie里写一个survey_answered_问卷idrandom_token。下次再进问卷页时先检查Cookie有就直接拦截。这个方案实现简单但用户清Cookie就能绕过。方案三session记录。把已提交标记存到request.session里。因为sessionID本身就在Cookie中所以本质上跟方案二类似但Django的session可以结合数据库存储使用体验更好。我实际落地时通常把方案一和方案三结合登录用户靠数据库唯一约束匿名用户靠sessionIP双条件校验。这样一个正常用户很难绕过重复提交控制同时开发量又不会太大。5.3 数据处理时最容易掉的坑空值、多选题的字符串化、非法选项每次看到问卷里填了其他____的用户都要做一层数据处理。最容易被忽略的是多选题的选项顺序和提交顺序不一定一致统计时必须对选项值做排序或分组否则同一个选项可能被统计成两组数据。另一个坑是清理非法选项。如果前端被篡改用户提交了不存在于Option表里的idDjango表单的ChoiceField会自动校验不通过但MultipleChoiceField有时会因为clean逻辑没处理好而漏掉。稳妥的做法是在保存前再校验一次valid_option_ids set(q.options.values_list(id, flatTrue)) submitted_ids set(int(x) for x in value) if not submitted_ids.issubset(valid_option_ids): raise forms.ValidationError(包含无效的选项)6. 统计与导出问卷系统的价值释放点6.1 不同的题型统计口径完全不同做问卷系统的关键不只是收集数据而是让管理员能看出趋势和差异。统计模块建议按题型拆单选/下拉统计每个选项被选择的次数算百分比用饼图或柱状图展示。多选统计每个选项出现次数但分母是用户数而不是次数还要注意多选题百分比加起来会超过100%展示时要说清楚。填空直接导出为文本列表再做关键词词频分析。量表/评分题计算平均分、中位数、标准差甚至按部门/时间段交叉对比。我写过这样一个聚合逻辑from django.db.models import Count from django.db.models.functions import Cast, StrIndex # 单选统计 data ( Answer.objects.filter(questionq) .values(value) .annotate(countCount(id)) .order_by(-count) )如果value直接存的是选项ID聚合会非常快如果存的是选项文本统计结果一样但如果你中间改过选项文字历史数据就会出现对不上号的情况。所以我建议Answer.value字段存选项ID而不是选项文本导出时再关联Option做一层中文映射。6.2 用Django视图导出Excel/CSV导出功能几乎是问卷系统的标配。Django导出CSV很简单用内置的csv模块配合StreamingHttpResponse即可import csv from django.http import HttpResponse def export_survey_results(request, pk): survey get_object_or_404(Questionnaire, pkpk) response HttpResponse(content_typetext/csv) response[Content-Disposition] fattachment; filenamesurvey_{pk}.csv writer csv.writer(response) # 表头 writer.writerow([题目, 题型, 答案]) for answer in Answer.objects.filter(questionnairesurvey).select_related(question): writer.writerow([answer.question.content, answer.question.get_question_type_display(), answer.value]) return response如果要求导出Excel建议用openpyxl库。在列表里尽量用select_related和prefetch_related避免N1查询。我在实际项目里就遇到过问卷20题、2000人提交没加优化前导出接口直接超时后来给Answer查询加了select_related(question, response__user)并改成一边查一边写Excel速度翻了好几倍。6.3 图表展示的最小实现如果不引入重型前段框架用JQuery ECharts是最省事的方案。后端返回聚合好的JSON数据前端绑定一个折线图或饼图就可以。需要注意ECharts一旦遇到像单选这种分类数据要用{name: A选项, value: 100}这种格式传给pie系列不要直接传数组。更省力的方案是直接在Django Admin的change_form里通过TabularInline展示结果汇总表下面再用自制的模板标签渲染一个简单的HTML表格和CSS柱状条。我做过一个内部满意度问卷完全没用图表库只是用CSSwidth: 百分比实现了一个横向条形图同样效果好而且零依赖、加载极快。7. 项目上线前后必须处理的细节数据库、静态文件、部署环境7.1 从SQLite切到MySQL/PostgreSQL开发阶段用SQLite省事但生产环境建议迁到PostgreSQL或MySQL。切换时最常见的坑是SQLite对日期处理比较宽松PostgreSQL却对时区敏感过去时间相关的数据可能会出现8小时的偏移。迁移步骤一般是pip install psycopg2-binary或pymysql。修改settings.py的DATABASES配置。python manage.py makemigrations和migrate。用dumpdata/loaddata迁移数据或者用pgloader直接把SQLite转成PostgreSQL。7.2 静态文件与媒体文件的坑本地项目里static/目录下通常有CSS、JS文件media/目录存放上传文件。部署到服务器时必须python manage.py collectstaticcollectstatic会把所有App和项目里的静态文件汇总到STATIC_ROOT配置的目录里让Nginx统一托管。如果忘了执行页面会出现所有样式全部丢失的现象。媒体文件则要确保Nginx配置了对应路径的别名例如location /media/ { alias /var/www/survey_project/media/; }7.3 部署时常见的依赖与换行符问题如果项目是从Windows开发机上直接打包到Linux服务器部署还要注意三个细节requirements.txt里最好锁定精确版本而不是避免生产环境拉取到不兼容的新版本。数据库迁移文件必须完整提交不要靠生产环境重新makemigrations。迁移文件是Django历史的记录缺了后面对模型的修改可能生成错误的迁移。如果服务器是麒麟或其他基于Linux的国产系统某些安全策略会比较严格。部署时注意不要用root直接跑服务runserver只用于开发调试生产环境建议用gunicorn或uwsgi配合Nginx。我在实际项目中发现新版Django4.0对ALLOWED_HOSTS配置检查更严格如果漏配了域名或IP请求会直接返回Bad Request (400)错误。7.4 DEBUG开关与日志生产环境必须把DEBUGFalse但很多第一次部署的人会发现关掉DEBUG后500错误的页面变成了一行Internal Server Error完全看不到具体报错。这时候就要配合日志系统LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: DEBUG, class: logging.FileHandler, filename: /var/log/survey/debug.log, }, }, loggers: { django: { handlers: [file], level: DEBUG, propagate: True, }, }, }我见过太多同事把DEBUGTrue直接扔到生产环境结果页面报错时把数据库密码、密钥全部漏出来。如果实在需要查看线上错误用DEBUGFalse日志文件的方式比直接开DEBUG安全得多。也可以接入Sentry这样的错误监控平台出了异常自动发通知省得有人反馈问题后你才后知后觉。8. 二次开发时常见的小需求与修改思路8.1 问卷逻辑跳转现在很多问卷要求如果选了选项A则跳到第5题否则跳到第8题。用Django后端实现这个功能最清晰的做法是在Option表里加一个skip_to_question外键字段。前端判断题目标题时发现选择了带跳转逻辑的选项就自动隐藏后续的某些题。后端校验时也应该检查跳转后的必填逻辑避免用户绕开必填题直接提交。这个功能难点不在Django本身而在前端交互。用前端框架处理会比较自然纯Django模板渲染时则要写不少自定义JavaScript代码容易乱。我的建议是如果只是内部使用的问卷尽量别做复杂跳转用分页问卷代替把不同题目分组放在不同页面里用户不会感受到逻辑跳转的突兀。8.2 问卷草稿与暂存答卷人填到一半临时有事想保存草稿下次继续填这是很常见的需求。实现方式就两步点击暂存时把当前已填内容序列化成JSON存到ResponseDraft表关联user和问卷。下次打开问卷时如果查询到草稿就把JSON解析后回填到表单项。用Django Form动态表单时回填相对复杂一些需要在生成表单时把初始化值传进去form DynamicSurveyForm(initialdraft_data, questionsquestions)这里最麻烦的是多选题的初始值结构必须是列表单选是字符串开发时一定要在clean方法里做统一转换否则表单校验会全部失败。8.3 后续扩展思路如果你准备把这个系统长期维护下去后面可以考虑用django-import-export库做问卷答案的批量导入导出不用自己手写CSV的Excel样式。用celeryredis做定时统计报表每天晚上汇总当天新增的答卷数据生成日报邮件。用django-guardian做细粒度的权限控制比如A部门只能看A部门的问卷统计这个在跨部门共享问卷时尤其有用。我不建议一开始就把这些全铺上。问卷系统的核心永远是稳定地收集到真实有效的数据。先把表单流程跑顺、统计结果做准确、防重复提交做好就已经能撑起大部分业务需求了。后面等到确实有用户需求反馈再逐步加权限、加告警、加定时任务才是更稳妥的迭代节奏。最后再分享一个我每次做问卷功能都会执行的上线检查清单建一个测试问卷自己完整填一遍清空Cookie再填一遍用另一个浏览器匿名再填一遍刷新两次确认不能重复提交导出一次数据人工核对标题和选项是否串列。这套流程走完基本能覆盖90%的线上问题。本文还有配套的精品资源点击获取