FEATURED · 精选文章

Django 3.2 + Vue 前后端分离问卷系统:从数据模型到部署落地

发布时间 / 2026/8/30 8:49:25
来源 / 创域科博编辑部
栏目 / 资讯中心
Django 3.2 + Vue 前后端分离问卷系统:从数据模型到部署落地 简介这是一套基于Django 3.2与Vue开发的完整问卷调查系统源码面向计算机专业本科生及初阶Web开发者专为毕业设计、课程实训与轻量级教学评估场景打造。系统实现学生作答、教师阅卷、管理员全流程管控三大角色闭环涵盖用户权限分级、问卷动态配置、结果可视化统计与CSV批量导出等核心功能具备良好的可扩展性与MySQL迁移能力。资源包共95个文件含58个Python后端模块覆盖models、views、admin等标准Django结构、24个HTML前端模板、3个CSV用户导入模板及1个SQLite数据库文件整体仅132KB结构清晰、模块解耦度高便于快速理解MVT架构与前后端协作逻辑。目前已有1551人学习下载配套提供完整测试账号、用户导入规范说明及典型部署路径提示是掌握Django权限控制、Vue基础集成与教育类SaaS系统设计思路的优质实践样本。 做内部工具时总会碰到一类需求要一份能把数据攥在自己手里的问卷系统。商用问卷平台功能多但到了数据导出、题型定制、和内部系统打通这些环节处处是限制。所以我把这套东西做成了一个完整可运行的项目——Django 3.2 提供后端 APIVue 负责前端交互打包成django-question-master.zip这种解压就能跑的状态。这篇文章会把技术选型、数据库设计、前后端实现、部署上线到实际踩坑完整过一遍适合三类人想完整走一遍前后端分离项目的人、需要在内部快速部署问卷系统的团队、以及拿到源码包后想二次开发的同学。1. 为什么这个场景非 Django 3.2 Vue 不可1.1 问卷系统天然是两端形态一个问卷系统不管表面多花哨底层一定是两条独立链路管理端创建和答题端填写。管理端要处理的是复杂表单创建问卷、添加题目、配置选项、设置题目顺序、在线预览、看统计报表。这属于典型的中后台场景页面多、状态多、操作频繁。答题端则是另一回事它面向的是不特定的受访者可能拿手机打开链接就填追求的是加载快、操作顺、误触少。两条链路对技术栈的要求几乎是相反的。Django 自带 admin 后台可以快速支撑管理端的雏形Vue 单页应用则能把答题端的体验做得很流畅。django-question-master.zip里前后端分离本质就是顺应这个两端天然不同的结构。1.2 Django 3.2 在这个项目里扮演什么角色Django 3.2 是一个长期支持版本LTS支持周期到 2024 年 4 月这意味着在项目上线后的很长一段时间里不需要被版本升级追着跑。问卷系统本质上是一个数据密集型应用创建问卷、存储答卷、聚合统计每一步都围绕数据转。Django 在这类场景下的优势非常明显。ORM 直接操作数据库问卷、题目、答卷这些结构天然适合用关系模型表达自带 admin 后台可以在开发早期快速看到一个可用的 CRUD 界面验证业务逻辑迁移工具能轻松改表结构。还有一个容易忽略的点Django 内置的表单系统和校验机制对问卷这类需要严格校验输入的场景很有用。提交答卷时每道题是否必填、选项值是否合法、填空题的长度是否超限这些用 Django 的 form 或者 DRF 的 serializer 都能规范地处理。很多人觉得 Django 重、慢那是不会用。问卷系统的请求量级和电商秒杀完全不是一个量级Django 的成熟度和生态带来的开发效率提升远比性能这个标签重要。1.3 Vue 的取舍逻辑前端选 Vue核心原因就一句话声明式渲染让交互逻辑简单直接。答题页里选了一个选项后突然冒出追问这是典型的条件渲染Vue 的v-if和计算属性处理这种场景非常自然。管理端里表格、弹窗、表单校验用 Vue 配合组件库能大幅缩短开发时间。另外要照顾到现实情况Vue 的中文社区活跃遇到问题很容易搜到解决方案做后台系统和中小型项目的人多数是从 Vue 入手的团队协作时沟通成本低。React 当然也能做但在这个项目的体量下Vue 的生态和学习曲线更适合快速交付、稳定运行的目标。需要说明的是django-question-master.zip里的前端整体上保留了 Vue 2 时代的经典写法组件选项式 API、vue-router 3、vuex这对现在拿着源码包动手改造的人来说反而友好——网上 Vue 2 的资料和踩坑帖子非常全。如果你想迁移到 Vue 3思路也是通的组件拆分和状态管理逻辑基本不变后面我会单独说迁移要注意什么。2. 问卷数据模型把一套问卷拆成四张表2.1 核心表结构设计问卷系统最简单的存储方案是把整份问卷塞进一个 JSON 字段。这个方案在原型阶段很爽但一旦要改局部题目、做统计分析、查某道题的作答情况就会非常痛苦。所以我坚持用关系模型最终落成四张核心表。# backend/apps/survey/models.py from django.db import models class Survey(models.Model): STATUS_CHOICES ( (draft, 草稿), (published, 发布中), (closed, 已截止), ) title models.CharField(问卷标题, max_length200) description models.TextField(问卷说明, blankTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultdraft) start_time models.DateTimeField(开始时间, nullTrue, blankTrue) end_time models.DateTimeField(截止时间, nullTrue, blankTrue) created_by models.ForeignKey(auth.User, on_deletemodels.CASCADE, verbose_name创建人) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Question(models.Model): TYPE_CHOICES ( (single, 单选题), (multiple, 多选题), (text, 填空题), (score, 评分题), ) survey models.ForeignKey(Survey, related_namequestions, on_deletemodels.CASCADE) order models.IntegerField(排序, default0) type models.CharField(题型, max_length20, choicesTYPE_CHOICES) stem models.TextField(题干) required models.BooleanField(是否必填, defaultTrue) options models.JSONField(选项列表, defaultlist, blankTrue) branch models.JSONField(条件逻辑, defaultdict, blankTrue) class Answer(models.Model): survey models.ForeignKey(Survey, related_nameanswers, on_deletemodels.CASCADE) user_identifier models.CharField(答题人标识, max_length128, blankTrue) ip models.GenericIPAddressField(IP地址, nullTrue, blankTrue) submitted_at models.DateTimeField(提交时间, auto_now_addTrue) class AnswerDetail(models.Model): answer models.ForeignKey(Answer, related_namedetails, on_deletemodels.CASCADE) question models.ForeignKey(Question, on_deletemodels.CASCADE) content models.JSONField(作答内容)这套模型的关键点在于问卷和题目是一对多题目用order控制排序而不是依赖自增 id 的顺序选项直接用 JSON 数组存储因为单道题的选项不具备独立的业务实体答卷和答卷详情分开因为一次提交会包含多道题的答案。user_identifier字段用来做防重复提交的标识可以是用户 id也可以是浏览器生成的匿名 ID。2.2 JSONField 是怎么帮你省事的Django 3.2 的JSONField已经非常稳定底层在 PostgreSQL 上是原生的 jsonb 类型查询和索引都比早期版本成熟。在这个项目里两个地方用 JSONField 很值。第一个是题目的选项。单选题的四个选项本质上只是渲染数据不需要单独建一张选项表。把[A. 很好, B. 一般, C. 较差]直接存进options字段编辑题目的时侯取出数组整体替换序列化时直接透传给前端不需要多表联查。第二个是条件逻辑。比如选 A 则跳转到第 5 题否则显示第 6 题这种分支逻辑在传统外键模型里非常难表达。用 JSONField 存一份分支规则就简单了branch { type: jump, condition: {question_id: 3, operator: eq, value: A}, target_question_id: 6, }前端渲染题目时遍历题目的branch配置判断是否显示或跳转。后端统计时不需要解析这个字段只有渲染时才用。这种写入一次、读取频繁的场景JSONField 成本低、灵活度高值得用。2.3 外键和 JSON 的边界在哪里JSONField 虽好但别什么都往里塞。题目和问卷之间的关系必须用外键因为统计时要Question.objects.filter(surveysurvey).count()、要按问卷聚合。答卷数据也必须结构化因为统计第三题选 B 的人数这种需求靠 JSON 全文搜索数据库会很难受。我的实践原则是需要单独做条件查询、聚合、关联的数据一定要建模成字段只是跟着某条主记录一起存一起取的静态展示数据才考虑 JSON。这条边界划清楚后面的统计功能会省很多事。3. 后端 API问卷从草稿到统计的完整生命周期3.1 接口规划后端基于 Django REST FrameworkDRF提供接口。问卷系统的接口不多但每个都要想清楚权限和场景。方法路径用途权限GET/api/surveys/问卷列表需登录POST/api/surveys/创建问卷需登录GET/api/surveys/{id}/问卷详情含题目需登录PUT/api/surveys/{id}/更新问卷需登录DELETE/api/surveys/{id}/删除问卷需登录POST/api/surveys/{id}/publish/发布问卷需登录POST/api/surveys/{id}/close/截止回收需登录GET/api/surveys/{id}/fill/答题页数据公开POST/api/answers/提交答卷公开GET/api/surveys/{id}/statistics/统计数据需登录GET/api/surveys/{id}/answers/答卷明细需登录管理端接口统一要求登录认证答题接口完全公开。fill接口和detail接口返回的数据结构不一样fill只返回当前状态下需要展示的题目不返回统计数据detail则包含问卷的全部信息供编辑器使用。3.2 发布与截止的状态机问卷有草稿、发布中、已截止三种状态。状态切换不是简单地改个字段而是有约束的class SurveyActionView(APIView): def post(self, request, pk): survey get_object_or_404(Survey, pkpk) action request.query_params.get(action) if action publish: if survey.status published: return Response({detail: 问卷已发布}, status400) if not survey.questions.exists(): return Response({detail: 问卷至少需要一道题目}, status400) survey.status published survey.start_time timezone.now() elif action close: if survey.status closed: return Response({detail: 问卷已截止}, status400) survey.status closed survey.end_time timezone.now() survey.save() return Response(SurveySerializer(survey).data)发布前校验题目数量防止空问卷上线。截止后答题接口要拒绝提交。时间窗口也可以叠加控制——start_time和end_time同时存在时答题接口要判断当前时间是否在窗口内。实际项目里用户更容易理解的是直接点发布/截止按钮时间窗口作为补充限制。3.3 提交答卷的原子性和防重复提交答卷是整个系统里对数据一致性要求最高的接口。一次提交可能包含几十道题的答案要么全部写入要么全部不写不能出现一半的情况。用数据库事务包住整个写入过程from django.db import transaction from rest_framework import status class AnswerCreateView(APIView): authentication_classes [] permission_classes [] transaction.atomic def post(self, request): survey_id request.data.get(survey_id) answers request.data.get(answers) survey get_object_or_404(Survey, idsurvey_id) if survey.status ! published: return Response({detail: 问卷当前不可填写}, status400) answer Answer.objects.create( surveysurvey, user_identifierrequest.data.get(user_identifier, ), iprequest.META.get(REMOTE_ADDR, None), ) for item in answers: question Question.objects.filter(iditem[question_id], surveysurvey).first() if not question: continue AnswerDetail.objects.create( answeranswer, questionquestion, contentitem[content], ) return Response({id: answer.id}, statusstatus.HTTP_201_CREATED)防重复提交这个需求很容易被忽略但问卷一旦在群里传播重复提交的数据会直接把统计结果搞脏。项目里用的是服务端记录 前端浏览器标识的方案前端首次加载答题页时生成一个 UUID 存到 localStorage提交时带上这个 UUID后端在Answer表上给(survey, user_identifier)建唯一约束重复提交直接抛 IntegrityError接口捕获后返回您已经填写过这份问卷。这个方案在不需要用户登录的场景下足够用。3.4 统计聚合的查询写法统计功能是问卷系统的价值所在。每道题的选项分布、填空答案列表、评分题的平均分全部用 ORM 聚合就能完成。from django.db.models import Count, Avg def get_statistics(survey): stats {} for question in survey.questions.all(): if question.type in (single, multiple): # 统计每个选项被选中的次数 counts {opt: 0 for opt in question.options} details AnswerDetail.objects.filter(questionquestion) for d in details: content d.content if isinstance(content, list): # 多选 for c in content: if c in counts: counts[c] 1 else: # 单选 if content in counts: counts[content] 1 stats[question.id] counts elif question.type score: avg AnswerDetail.objects.filter(questionquestion).aggregate(Avg(content)) stats[question.id] avg[content__avg] else: values AnswerDetail.objects.filter(questionquestion).values_list(content, flatTrue) stats[question.id] list(values) return stats多选的统计逻辑稍微绕一点因为 content 是数组需要展开后逐个累加。数据量大的时候这个写法能顶住数万份答卷再大的量就该考虑用 Redis 做计数器或直接把统计结果缓存到问卷表上了。导出 CSV 时用 Python 的 csv 模块加 StreamingHttpResponse 流式输出避免一次性把所有答卷数据加载进内存。4. Vue 前端的核心实现答题页与后台管理页的分工4.1 路由设计前端路由分成两个完全独立的模块/fill/:surveyId 答题页公开 /admin/login 后台登录 /admin/surveys 问卷列表 /admin/surveys/new 新建问卷 /admin/surveys/:id/edit 编辑问卷 /admin/surveys/:id/statistics 统计页答题页和管理页共用同一个 Vue 应用但通过路由守卫彻底隔离。/admin下的所有路由都挂在meta: { requiresAuth: true }下面全局前置守卫里检查 token 是否存在不存在就跳登录页。这里有个容易踩的细节token 过期后调接口返回 401不能只靠路由守卫判断还需要在 axios 响应拦截器里做统一跳转。4.2 答题页的组件化拆解答题页是受访者直接接触的界面核心要求是加载快、操作直观、不会因为误触丢失已填内容。组件拆成三层FillPage.vue页面容器负责拉取问卷详情、维护答题状态、控制进度QuestionPanel.vue单题组件根据题目 type 渲染不同控件ProgressBar.vue进度组件展示已答题数/总题数答题状态用answers对象保存key 是 question idvalue 是用户输入。配合 Vue 的v-model双向绑定交互层代码非常简洁template div v-forquestion in visibleQuestions :keyquestion.id QuestionPanel :questionquestion v-modelanswers[question.id] / /div /template script export default { data() { return { answers: {}, formData: null, }; }, computed: { visibleQuestions() { if (!this.formData) return []; return this.formData.questions.filter(q this.isQuestionVisible(q)); }, isQuestionVisible() { return (question) { if (!question.branch || !question.branch.condition) return true; const cond question.branch.condition; return this.answers[cond.question_id] cond.value; }; } } }; /scriptvisibleQuestions这个计算属性是整个答题页的核心。条件逻辑由前端的计算属性实时更新用户选了某个选项后续的追问题立即出现或消失不需要后端参与。这种交互用 Vue 做非常顺。4.3 必填校验与题型扩展提交前校验分两层纯前端校验和提交后后端校验。前端校验覆盖体验后端校验保证数据可靠。前端在提交时遍历visibleQuestions对必填题检查answers[question.id]是否有有效值对手机号、邮箱这类题型做正则校验对评分题检查范围。校验失败时定位到第一道未通过的题并滚动到对应位置。后端 serializer 里再做同样的校验防止有人绕过前端直接调接口。题型扩展是这套系统最常遇到的需求。代码里默认有单选、多选、填空、评分四种模板题、矩阵题基本是基于这四类的组合。扩展新题型时后端只需要在Question.TYPE_CHOICES里加类型前端在QuestionPanel里加一个对应的渲染分支不需要动路由和状态管理。4.4 管理端的列表、编辑器和统计可视化管理端用的组件库是 Element UI。问卷编辑器是管理端最复杂的页面左侧题目列表中间是预览区右侧是当前题目的属性配置。题目拖动排序用的是vuedraggable拖拽结束后把新顺序写回题目数组保存时按 order 提交。统计页用的是 ECharts饼图展示单选分布柱状图展示多选分布表格列出填空答案。整体复杂度不高但要注意一个细节ECharts 实例在chart数据更新后需要手动setOption而且要在beforeDestroy里销毁实例否则路由切换频繁会内存泄漏。axios 封装在src/utils/request.js里统一处理 baseURL、请求头、token 注入和错误提示。开发环境走 webpack 的 devServer 代理生产环境由 Nginx 反向代理到 Django接口地址不需要硬编码。5. 前后端联调与上线跨域问题躲不掉5.1 CORS 到底怎么配前后端分离后第一个撞上的问题就是跨域。Django 端装django-cors-headers是标准做法# settings.py INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 注意 CorsMiddleware 要放在 CommonMiddleware 前面 # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, https://survey.example.com, ] CORS_ALLOW_CREDENTIALS True开发环境里前端跑在 8080后端跑在 8000必须让 Django 接受来自 8080 的跨域请求。生产环境里我建议做同域部署——前端打包后的静态文件交给 NginxAPI 路径用反向代理转发到后端的 gunicorn这样浏览器看到的只有同源请求CORS 完全不参与。跨域配置在生产环境里打开多个域名入口就是给自己挖坑。5.2 开发环境的代理配置开发阶段更流畅的方案是前端脚手架自带的代理功能让跨域问题在浏览器层面都不出现// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, } } } };前端请求/api/surveys/时webpack-dev-server 会转发到http://127.0.0.1:8000/api/surveys/。这套配置下request.js里的 baseURL 直接写/api就行不需要写死域名。5.3 Nginx 部署的核心配置生产环境部署时前端执行npm run build生成 dist 目录后端用 gunicorn 跑 Django。Nginx 配置两个 blockserver { listen 80; server_name survey.example.com; # 前端静态文件 location / { root /var/www/django-question-master/frontend/dist; try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { 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; } # Django admin 静态文件 location /static/ { alias /var/www/django-question-master/backend/staticfiles/; } }注意try_files那行Vue Router 默认用 history 模式刷新页面时 Nginx 要能正确回退到 index.html否则刷新/admin/surveys会 404。Django 侧还需要配置ALLOWED_HOSTS把域名加进去并在settings.py里设置STATIC_ROOT和STATIC_URL然后执行python manage.py collectstatic。这里有个 Django 安全相关的细节生产环境必须把DEBUG设为 FalseSECRET_KEY不能出现在代码仓库里建议通过环境变量注入。项目里给了一份.env.example把SECRET_KEY、DB_NAME、DB_USER、DB_PASSWORD这类敏感项都列出来复制成.env填好之后settings.py里用os.environ.get()读取。6. 解压 django-question-master.zip 之后怎么跑起来6.1 目录结构速览这个压缩包解压后目录结构大致是这样django-question-master/ ├── backend/ │ ├── manage.py │ ├── config/ # 项目配置settings、urls、wsgi │ ├── apps/ # 业务应用survey 等 │ │ └── survey/ │ │ ├── models.py │ │ ├── serializers.py │ │ ├── views.py │ │ └── urls.py │ ├── requirements.txt │ └── .env.example ├── frontend/ │ ├── public/ │ ├── src/ │ │ ├── api/ # axios 请求封装 │ │ ├── router/ # vue-router 路由配置 │ │ ├── views/ # 页面组件 │ │ ├── components/ # 通用组件 │ │ ├── store/ # vuex 状态管理 │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vue.config.js ├── docs/ │ └── API.md └── README.mdbackend/config是 Django 项目根配置backend/apps下面是业务应用。这套结构参考了大多数 Django 项目的标准组织方式后端新增业务模块时直接在apps/下新建应用即可。6.2 后端启动步骤后端启动非常简单前提是机器上有 Python 3.6 以上版本cd backend python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # 然后填入实际配置 python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000requirements.txt里锁定了 Django3.2.*、djangorestframework、django-cors-headers、gunicorn、python-dotenv 这些核心依赖。数据库默认是 SQLite零配置直接跑要切 MySQL 或 PostgreSQL 时改settings.py里的DATABASES配置再pip install对应的驱动即可。SQLite 在小规模内部问卷场景下完全够用数据量到十万份答卷再考虑迁移。6.3 前端启动步骤cd frontend npm install npm run serve浏览器访问http://localhost:8080就能看到答题页入口。npm install如果因为网络问题装得很慢可以切到国内镜像源npm config set registry https://registry.npmmirror.com。这是我在新环境搭建项目时首选的配置方式实测速度提升明显。6.4 初始化一份示例问卷压缩包里带了一个fixtures/demo_survey.json里面包含一份完整的示例问卷覆盖四种题型和条件逻辑cd backend python manage.py loaddata fixtures/demo_survey.json加载完访问/fill/1就能看到一份能真实提交的问卷。这份 fixture 是我建议保留的二次开发时每改一次前端交互都能用它快速回归测试。7. 实际项目里踩过的坑从滚动的表格到冒烟的 node_modules7.1 表格滚动位置丢失keep-alive 和 el-table 的恩怨做管理后台时用el-table展示答卷列表同时用includekeep-alive缓存了统计页和列表页。结果发现从列表页切到统计页再切回来表格滚动条回到了顶部已经勾选的复选框也全丢了。这个现象非常迷惑因为直觉上 keep-alive 已经把组件实例缓存住了。排查链路是这样的第一步怀疑 el-table 自身的滚动容器问题。给 el-table 加了 ref在activated钩子里打印this.$refs.table.$el.querySelector(.el-table__body-wrapper).scrollTop发现拿到的值固定是 0说明组件恢复时滚动容器是全新的。第二步怀疑 keep-alive 没有生效。在deactivated和activated钩子里分别打 log确认两个钩子都触发了组件实例是同一个。第三步查到根源el-table 在数据重新赋值时会销毁并重建内部的滚动容器所以即使组件实例被缓存了滚动位置也被重建过程清掉了。解决方式是绕过 el-table 内部的滚动状态自己在组件层面保存data() { return { scrollPosition: 0, }; }, deactivated() { const wrapper this.$refs.table.$el.querySelector(.el-table__body-wrapper); if (wrapper) { this.scrollPosition wrapper.scrollTop; } }, activated() { this.$nextTick(() { const wrapper this.$refs.table.$el.querySelector(.el-table__body-wrapper); if (wrapper) { wrapper.scrollTop this.scrollPosition; } }); }这个坑的核心教训是keep-alive 缓存的是组件实例组件实例内部某些框架组件的 DOM 状态并不会被自动保留。保存前先在deactivated里读出状态恢复时在activated里写回去这是最可靠的做法。7.2 node_modules 说没就没前端依赖安装的三个教训django-question-master.zip本身不包含 node_modules前端依赖需要自己装。但有几次在别的机器上解压运行时报错The project can not found node_modules命令行提示npm install -g vue/cli。这种提示有误导性——它让人以为全局装脚手架能解决问题实际上缺失的是项目本地依赖。排查思路先确认 Node 和 npm 版本。这个项目要求 Node 12 以上老版本机器装完依赖后运行时报各种兼容错。npm install报错时优先npm cache verify清理缓存再删掉 node_modules 和 package-lock.json 重新装解决绝大多数网络或缓存问题。依赖装完后如果 cli 命令找不到把./node_modules/.bin加到 PATH或者直接用npx vue-cli-service serve启动。这是排在最前面的检查项不要一上来就全局装依赖。换机器时最容易忽略的是 Node 版本差异。曾经有一台机器的 Node 版本是 10npm install能成功但npm run serve启动后页面白屏控制台报错指向 Vue 语法高亮相关的包实际上是新版依赖对低版本 Node 不兼容。项目里 README 写了建议的 Node 版本范围就是从这个经验来的。7.3 问卷里嵌视频题m3u8 流播放不能靠 video 标签硬来有个真实需求是让受访者先看一段视频再回答问题。视频服务商给的是 m3u8 地址前端用原生video标签播放Safari 下正常Chrome 和 Firefox 下直接黑屏控制台报MEDIA_ERR_SRC_NOT_SUPPORTED。原因很清楚m3u8 是 HLS 协议原生支持它的是 SafariChrome 和 Firefox 需要靠 MSEMedia Source Extensions来转码播放。hls.js这个库就是干这个事的import Hls from hls.js; export function initHls(videoElement, src) { if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 videoElement.src src; } else if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(src); hls.attachMedia(videoElement); } else { // 兜底提示 videoElement.innerHTML 当前浏览器暂不支持视频播放; } }这个逻辑我封装在src/utils/video.js里视频题的QuestionPanel渲染时调用。处理完 Chrome 和 Firefox 的播放问题后还要留意跨域视频资源在 CDN 上时服务端必须返回正确的 CORS 头否则 hls.js 拉切片会被浏览器拦截。这个坑隐蔽排查时容易误以为代码写错了。7.4 导出 PDF 时结果页被截断管理端有个功能是把统计结果导出成 PDF。最初用jspdfhtml2canvas实现页面长的时候 PDF 最后一页要么空白要么只截出半张图。调查后发现html2canvas对超过视口高度的内容会丢内容需要通过windowHeight和scrollY逐屏截取再拼接到 PDF 里。后来改用html2pdf.js底层做了分页处理但依然要显式设置scale和pagebreakimport html2pdf from html2pdf.js; export function exportToPdf(el, filename) { const opt { margin: [10, 10, 10, 10], filename: filename, image: { type: jpeg, quality: 0.95 }, html2canvas: { scale: 2, useCORS: true, // 关键限制截图高度避免长页面截断 windowHeight: document.documentElement.scrollHeight, onclone: (doc) { const cloned doc.getElementById(el.id); if (cloned) cloned.style.backgroundColor #ffffff; } }, jsPDF: { unit: mm, format: a4, orientation: portrait }, pagebreak: { mode: [avoid-all, css, legacy] }, }; html2pdf().set(opt).from(el).save(); }scale: 2是给高清屏用的否则导出图片文字发虚useCORS: true是为了让统计图里的跨域图片能正常绘制。这两个参数缺一不可不然出来的 PDF 要么模糊要么白图。统计页的图表如果有动态渲染的数据导出前要先确保图表已经渲染完成通常用setTimeout或者图表库的rendered事件兜底。8. 把这套系统往深处改的一点建议8.1 从问卷模板走向考试模式问卷和考试在结构上高度相似区别主要在判分逻辑和结果展示。想在这套系统上加考试模式可以给Question增加一个answer字段存储正确选项Survey增加is_exam标识。提交答卷时如果问卷是考试模式后端在AnswerDetail里记录判分结果统计接口里加一个分数分布聚合。这个扩展不改表结构体系只加字段和校验逻辑整体改动量在两天左右。8.2 从匿名问卷接入登录体系系统默认不要求应答者登录user_identifier给了扩展空间。想接入公司内部账号体系只需在后端AnswerCreateView里认证方式改为读取 JWT token 中的用户 id写入user_identifier前端答题页挂载时校验登录态未登录先跳登录页。这个改动绕开了问卷的匿名属性适用于内部测评、培训考核等场景。8.3 Vue 3 迁移提醒把前端迁移到 Vue 3最需要注意的是几个生态包的版本变化vue-router从 3 升到 4vuex从 3 升到 4或者换 PiniaElement UI 要换成 Element Plus。组件选项式 API 写法可以直接迁移不用改成 Composition API只是main.js里createApp的挂载方式和路由初始化写法变了。后端完全不用动因为接口层面没有变化。8.4 Redis 该不该引入问卷系统现阶段没有用 Redis。它依赖的只是数据库事务和普通表索引。如果未来要做高并发抢答类问卷、实时答题人数统计、短时间大量提交的削峰再考虑引入 Redis。做缓存和计数器很合适但不要为了用而用——引入一个中间件部署和运维成本立刻上升。我自己在这套系统上踩过最大的坑不是技术问题而是想当然地以为问卷系统简单、不需要仔细设计数据结构。结果在加了条件逻辑和统计功能之后才发现早期 JSON 一把梭的设计根本撑不住只能回头重构。改成现在这种关系模型为主、JSON 只做灵活补充的方案后后面加题型、加逻辑、加导出功能都很顺畅。代码从来不说谎数据结构设计得是否合理在加功能的那一天就会见分晓。如果你打算在这个 zip 基础上做二次开发我建议先完整看一遍models.py和API.md再动手改任何一行代码。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻