简介:本资源是一套面向计算机专业本科生的毕业设计级问卷调查系统源码,基于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给业务部门搭内部工具的人,我看到"基于python+Django的问卷调查系统"这种项目,第一反应不是去下载压缩包,而是先想清楚一件事:这个系统到底是用来做匿名调研、课程反馈、客户满意度收集,还是公司内部的流程审批式问卷?因为不同场景下,权限模型、题型设计、结果统计方式差别非常大。
这篇文章我会从项目落地角度,把一个典型的Python+Django问卷调查系统拆开讲透。覆盖从解压项目后怎么跑起来、数据模型怎么设计、动态表单怎么实现、答卷结果怎么统计、部署上线有哪些坑,以及我在实际项目里迭代问卷功能时踩过的各种细节问题。无论是你刚拿到一份开源问卷系统源码,还是打算从零自己写一个,这篇文章都可以当一份实操参考。
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里依赖包的版本范围。如果一个老项目写的是Django==2.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 四张核心表,先把这个关系理清楚
问卷系统本质上是结构化数据的收集与汇总,所以数据模型设计是整套系统的地基。最经典、也最好用的设计是下面这四张核心表:
| 模型 | 字段示意 | 作用 |
|---|---|---|
| Questionnaire | title, description, created_at, start_time, end_time, status | 问卷本身的基本信息和生命周期 |
| Question | questionnaire(FK), question_type, content, order, is_required | 问卷下的题目,含单选、多选、填空等类型 |
| Option | question(FK), content, order | 选择题下的选项,填空题可以没有选项 |
| Answer | questionnaire(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_delete=models.CASCADE, related_name='questions') question_type = models.CharField(max_length=20, choices=QUESTION_TYPES, default='single') content = models.TextField(verbose_name='题目内容') order = models.IntegerField(default=0, verbose_name='排序') is_required = models.BooleanField(default=True, 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, blank=True)或者JSON数组。
class Answer(models.Model): questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE, related_name='answers') question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers') user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) single_choice_value = models.CharField(max_length=255, blank=True) multi_choice_values = models.JSONField(default=list, blank=True) fill_content = models.TextField(blank=True) score_value = models.IntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True)这个设计的逻辑是:每次问卷提交,系统会为该问卷下的每一道题生成多条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)。状态字段用CharField+choices就够了。
class Questionnaire(models.Model): STATUS_CHOICES = [ ('draft', '草稿'), ('published', '发布中'), ('closed', '已结束'), ] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')这里有个容易忽略的点:如果你希望问卷到了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 = f'question_{q.id}' if q.question_type == 'single': self.fields[field_name] = forms.ChoiceField( choices=[(opt.id, opt.content) for opt in q.options.all()], label=q.content, required=q.is_required, widget=forms.RadioSelect, ) elif q.question_type == 'multiple': self.fields[field_name] = forms.MultipleChoiceField( choices=[(opt.id, opt.content) for opt in q.options.all()], label=q.content, required=q.is_required, widget=forms.CheckboxSelectMultiple, ) elif q.question_type == 'fill': self.fields[field_name] = forms.CharField( label=q.content, required=q.is_required, widget=forms.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 method="post"> {% csrf_token %} {% for field in form %} <div class="form-group"> <label>{{ field.label }}</label> {{ field }} {% for error in field.errors %} <p class="error">{{ error }}</p> {% endfor %} </div> {% endfor %} <button type="submit">提交问卷</button> </form>如果要做成多页问卷(每页5题,分页作答),可以在URL里加?page=1参数。后端从所有题目里切分出当前页应该显示的题目范围。分页的好处一个是减轻用户填写压力,另一个是可以在前端做好进度条,提升填写体验。需要注意的是,分页问卷通常要配合"暂存"功能——也就是每点击下一页就把当前页答案存到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, pk=pk) if not survey.is_active(): return render(request, 'survey/expired.html') questions = survey.questions.all().order_by('order') form = DynamicSurveyForm(request.POST, questions=questions) if form.is_valid(): response = Response.objects.create(questionnaire=survey, user=request.user if request.user.is_authenticated else None) for q in questions: value = form.cleaned_data.get(f'question_{q.id}') Answer.objects.create( response=response, question=q, value=value, ) return redirect('survey:thanks', pk=survey.pk)这里我特意用了一个Response表,把所有答案分成"组"。这种设计和前面的Answer表配合,做"同一份问卷提交结果"的整体查看时非常方便。
5.2 防止重复提交,实战中最实用的三种方案
方案一:数据库唯一约束。如果问卷要求每个登录用户只能提交一次,给Response表加unique_together = ('questionnaire', 'user')。这样就算前端没有控制,数据库层也能兜住。缺点是对匿名用户无能为力,因为匿名用户没有user_id。
方案二:Cookie记录。用户提交成功后,在本地Cookie里写一个survey_answered_<问卷id>=<random_token>。下次再进问卷页时,先检查Cookie,有就直接拦截。这个方案实现简单,但用户清Cookie就能绕过。
方案三:session记录。把"已提交"标记存到request.session里。因为sessionID本身就在Cookie中,所以本质上跟方案二类似,但Django的session可以结合数据库存储,使用体验更好。
我实际落地时,通常把方案一和方案三结合:登录用户靠数据库唯一约束,匿名用户靠session+IP双条件校验。这样一个正常用户很难绕过重复提交控制,同时开发量又不会太大。
5.3 数据处理时最容易掉的坑:空值、多选题的字符串化、非法选项
每次看到问卷里填了"其他:____"的用户,都要做一层数据处理。最容易被忽略的是:多选题的选项顺序和提交顺序不一定一致,统计时必须对选项值做排序或分组,否则同一个选项可能被统计成两组数据。
另一个坑是清理非法选项。如果前端被篡改,用户提交了不存在于Option表里的id,Django表单的ChoiceField会自动校验不通过,但MultipleChoiceField有时会因为clean逻辑没处理好而漏掉。稳妥的做法是在保存前再校验一次:
valid_option_ids = set(q.options.values_list('id', flat=True)) 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(question=q) .values('value') .annotate(count=Count('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, pk=pk) response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="survey_{pk}.csv"' writer = csv.writer(response) # 表头 writer.writerow(['题目', '题型', '答案']) for answer in Answer.objects.filter(questionnaire=survey).select_related('question'): writer.writerow([answer.question.content, answer.question.get_question_type_display(), answer.value]) return response如果要求导出Excel,建议用openpyxl库。在列表里尽量用select_related和prefetch_related避免N+1查询。我在实际项目里就遇到过:问卷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。我在实际项目中发现,新版Django(4.0+)对ALLOWED_HOSTS配置检查更严格,如果漏配了域名或IP,请求会直接返回Bad Request (400)错误。
7.4 DEBUG开关与日志
生产环境必须把DEBUG=False,但很多第一次部署的人会发现,关掉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, }, }, }我见过太多同事把DEBUG=True直接扔到生产环境,结果页面报错时把数据库密码、密钥全部漏出来。如果实在需要查看线上错误,用DEBUG=False+日志文件的方式,比直接开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(initial=draft_data, questions=questions)这里最麻烦的是多选题的初始值结构必须是列表,单选是字符串,开发时一定要在clean方法里做统一转换,否则表单校验会全部失败。
8.3 后续扩展思路
如果你准备把这个系统长期维护下去,后面可以考虑:
- 用
django-import-export库做问卷答案的批量导入导出,不用自己手写CSV的Excel样式。 - 用
celery+redis做定时统计报表,每天晚上汇总当天新增的答卷数据,生成日报邮件。 - 用
django-guardian做细粒度的权限控制,比如"A部门只能看A部门的问卷统计",这个在跨部门共享问卷时尤其有用。
我不建议一开始就把这些全铺上。问卷系统的核心永远是稳定地收集到真实有效的数据。先把表单流程跑顺、统计结果做准确、防重复提交做好,就已经能撑起大部分业务需求了。后面等到确实有用户需求反馈,再逐步加权限、加告警、加定时任务,才是更稳妥的迭代节奏。
最后再分享一个我每次做问卷功能都会执行的"上线检查清单":建一个测试问卷,自己完整填一遍;清空Cookie再填一遍;用另一个浏览器匿名再填一遍;刷新两次确认不能重复提交;导出一次数据,人工核对标题和选项是否串列。这套流程走完,基本能覆盖90%的线上问题。
本文还有配套的精品资源,点击获取