
前阵子接手了一个模拟项目X题目就叫“Python基于Vue的线上考试系统的设计与实现”。这标题看着常规实际动手时才发现坑不少Django 和 Flask 到底选哪个、PyCharm 里怎么同时管前后端两个工程、Vue 的答题倒计时跟后端时间不同步怎么办。这篇文章就把我这一版从零到可运行的全部过程拆开讲包含选型理由、建表逻辑、接口设计、Vue 页面实现以及部署时容易忽略的安全细节。适合正在做课程设计、毕业设计或者想用前后端分离技术栈练手的朋友参考。我用的组合是 Django Vue PyCharm但文章里也会交代清楚同样场景下 Flask 该怎么取舍避免你辛辛苦苦搭完环境才发现选错框架。1. 项目定位与技术选型拆解1.1 线上考试系统到底要解决什么先说需求。一个能真正用起来的线上考试系统核心角色有三个管理员、教师、学生。管理员负责系统配置和账号管理教师负责题库维护、组卷、发布考试和批改主观题学生负责登录、考试、交卷、查分。如果只是做一个“能答题”的 Demo其实半天就能憋出来但要做到能实际使用至少要拆出下面几条线题库管理题目增删改查、按知识点和难度分类、支持单选多选判断填空等常见题型。组卷与发布手动选题或者按规则随机抽题生成试卷设置考试时间、时长、及格线然后发布给指定班级或所有人。在线考试考生进入考试页后只能看到自己被分配的那份试卷答题过程中需要倒计时提示时间到了必须强制交卷交卷后客观题自动判分。成绩管理查询成绩、统计分数分布、导出成绩单主观题走人工批改流程。这些需求基本决定了后端要管哪些数据、要写哪些接口。我做设计的时候把每条线都画成了独立的模块Django 里的 app 也按这个边界来切后面扩展权限、加功能都不至于牵一发动全身。1.2 Django 和 Flask这个场景为什么我选 Django这个项目标题里同时出现了 django 和 flask说明不少人在选型时是摇摆的。我自己的判断标准很简单看项目的关联复杂度和功能完整度别只看“重不重”。Flask 确实轻一个文件就能起服务适合接口特别少、逻辑特别简单的应用。但线上考试系统需要用户角色、题库、试卷、考试记录、答题明细这些表互相关联还要做权限控制、事务处理、后台管理。这些在 Flask 里不是不能做而是很多轮子要自己找、自己拼开发周期会拉长。Django 的优势在于自带 Admin、ORM、认证体系和中间件机制。Admin 后台可以让我在开发阶段几乎零成本地管理题库和用户数据自带的 User 模型配合扩展字段就能满足角色需求ORM 处理多表关联非常直接。配合 Django REST Framework写考试相关的接口也很快。如果让我给一个更具体的参考如果题目是“自测题库小程序”或者“几十道题的测验页”Flask 完全够用写起来还更顺手但如果是“线上考试系统”这种带完整业务流程的我建议直接上 Django。这里放一张对比表方便你对照对比维度DjangoFlask内置后台自带 Admin可快速管理数据无需要另外开发或接入第三方ORM成熟且内置关系映射方便需接入 SQLAlchemy 之类认证授权自带 User/Permission扩展方便需自己实现或接扩展社区生态DRF 专为接口场景服务组合自由度大但选择成本高学习曲线偏陡但成套平缓但模块要自己组装适合场景中大型业务系统、多角色应用轻量接口、小工具、快速原型我最终选了 Django还有一个实际原因PyCharm 对 Django 提供了专门支持创建项目、模板调试、manage.py 命令都有图形化入口开发体验顺很多。这点我在下一节细说。1.3 PyCharm 下前后端分离工程的目录规划既然定了 Django Vue 的前后端分离方案PyCharm 里就要同时管理两个工程。我的做法是建一个总目录后端和前端分开放总目录本身不当作项目根目录避免 PyCharm 把 Django 和 Vue 的依赖搞混。目录结构大概是这样的exam_system/ ├── backend/ # Django 工程 │ ├── manage.py │ ├── config/ # 项目配置settings/urls/wsgi/asgi │ ├── apps/ │ │ ├── users/ # 用户与认证 │ │ ├── exam/ # 题库、试卷、考试记录 │ │ └── result/ # 成绩、批改 │ └── requirements.txt ├── frontend/ # Vue 工程 │ ├── package.json │ ├── vite.config.js │ └── src/ │ ├── api/ # 接口封装 │ ├── views/ # 页面组件 │ └── store/ # 全局状态 └── README.mdPyCharm 的打开方式是直接打开总目录 exam_system然后把 Python 解释器指向 backend 的虚拟环境前端的 Node 解释器指向系统 Node 即可。我在开发时给 Django 分配了 8000 端口给 Vue 的 dev server 分配了 5173 端口前端通过代理把 /api 开头的请求转发到 8000这样既能避开跨域问题又贴近生产环境的部署形态。有一个容易被忽略的小细节PyCharm 里同时跑 npm run dev 和 Django runserver需要给两个任务配置不同的 Python/Node 解释器不然会出现终端里命令找不到的情况。第一次配环境时我踩过这个坑后文第 5 节会专门展开。2. 后端核心模块设计与实现2.1 用户认证与多角色权限考试系统的用户角色很明确学生、教师、管理员。Django 自带的 User 模型虽然能用但字段是通用的直接往里塞角色标识不太优雅。我的方案是保留默认 User 作为账号主体再建一个 Profile 模型用 OneToOne 关联存放角色、学号/工号、班级等业务字段。这样做的原因有两个第一Django 默认用户模型和认证逻辑不需要改用起来最稳第二业务扩展字段和认证核心字段解耦以后想接第三方登录、加人脸校验都不需要动整张用户表。接口认证我用的是 JWT配合 Django REST Framework 的权限类做角色控制。JWT 的好处是前端拿到 token 后请求只要在 Header 里带上就能识别身份服务和前端分离时非常方便不需要依赖 session 的 cookie 机制避免了跨域携带 cookie 的麻烦。简单说登录成功后后端返回 access token前端把它存在内存或 localStorage 里每次请求带在 Authorization 头里。权限控制上我写了一个 BasePermission 子类判断当前用户的 profile.role 是否在允许的列表里from rest_framework.permissions import BasePermission class RolePermission(BasePermission): allowed_roles [] def has_permission(self, request, view): user request.user if not user or not user.is_authenticated: return False profile getattr(user, profile, None) if not profile: return False return profile.role in self.allowed_roles实际使用时视图里直接声明 permission_classes [RolePermission]并把 allowed_roles 设成 [teacher, admin]就能挡住学生访问管理接口。这个思路很简单但很实用比在每个视图里手写 if 判断清爽得多。2.2 题库设计与随机组卷逻辑题库是考试系统的地基。设计题库表的时候我用了三个核心表题目表、知识点表、题目与知识点关联表。题目表存题干、题型、选项、答案、难度、解析知识点可以看作一个标签方便组卷时按范围筛选。单选题的选项我用 JSON 字段存一个列表比如 [A. xxx, B. xxx, C. xxx]答案字段存正确选项的字母。多选题的答案用逗号分隔的字符串比如 A,C判断题答案存 T 或 F。用统一字段的好处是题型扩展时不用改表结构缺点是查询时不能直接按某个选项值过滤但在考试系统里我们几乎不会按选项内容查题所以完全可接受。组卷逻辑分两种手动组卷和随机组卷。手动组卷适合教师指定题目随机组卷适合大题库按规则抽题。随机组卷我是在后端做的不是在前端做的不然题目分布和防作弊都没法控制。核心思路是先按条件过滤题目再 shuffle 后取指定数量from django.db.models import Count import random def generate_paper(pool_rules): questions [] for rule in pool_rules: # rule: {knowledge: Python基础, type: single, count: 10, difficulty: easy} qs Question.objects.filter( knowledge_points__namerule[knowledge], question_typerule[type], difficultyrule[difficulty] ) candidates list(qs.values_list(id, flatTrue)) if len(candidates) rule[count]: raise ValueError(f题目数量不足: {rule[knowledge]} - {rule[type]}) random.shuffle(candidates) questions.extend(candidates[:rule[count]]) return questions这里有个非常重要的细节组卷时必须把试卷和题目通过一张中间表关联起来记录每道题在当前试卷里的分数、顺序。绝对不能直接把题目列表塞进试卷表里否则后面改题、调分、做成绩统计都会非常痛苦。中间表里还要额外存一份题目快照比如题干和选项因为题目可能在未来被编辑但已经发布的试卷应该保持原样避免考完试发现题目变了。2.3 在线考试流程与自动阅卷在线考试的核心流程是考生进入考试页时后端生成一份“考试记录”同时把试卷关联的题目生成一份“答题卡”返回给前端。前端拿到题目后开始计时考生每答一道题前端就把答案暂存到本地状态里交卷时统一提交也可以做成实时保存到后端。我建议的关键点是考试结束时间以服务端时间为准前端倒计时只做展示和提醒后端收到交卷请求时再校验时间防止考生改本机时间作弊。自动阅卷只处理客观题。交卷后后端遍历答题卡里的每一题和试卷快照里的标准答案比对。单选题直接比选项字母多选题需要判断选项集合是否完全一致判断题比较 T/F。完全一致的得分不一致的记零分。这里要注意多选题的判分规则要和业务确认清楚是“少选不给分”还是“少选得部分分”我用的是完全匹配因为规则明确、实现简单。整个过程我用了一个事务包裹交卷、判分、更新考试记录状态在同一个事务里完成避免出现答案提交成功但成绩没有落库的尴尬情况。具体代码就是一个简单的遍历和比对但事务和幂等处理很重要后面第 4 节会专门说。3. Vue 前端与接口联调实战3.1 前端工程搭建与核心页面规划前端我用 Vue 3 加 Vite 搭建UI 组件库选了 Element Plus后端接口通信用 Axios。整体页面不算多但每个页面都有各自的职责登录页账号密码登录成功后存 token。考试列表页学生查看自己可参加的考试显示考试时间、时长、状态。答题页核心页面展示题目、答题、倒计时、试题导航、交卷。成绩页考试后查看成绩、答案解析。管理端页面题库管理、试卷管理、成绩管理。页面级别用 Vue Router 控制通过路由守卫判断登录状态和角色。没有 token 直接跳登录页角色不是教师或管理员时禁止访问管理端路由。这个做法在前后端分离项目里是标配但要注意路由守卫只是体验层面的保护真正的权限校验必须依赖后端否则别人拿个 token 直接调接口就能绕过前端。3.2 答题页的状态管理与倒计时实现答题页是整个前端工程里最容易写乱的页面。原因在于它需要同时管理试卷信息、当前索引、每一题的答案、倒计时剩余时间、已经标记的题目状态这些状态之间还有联动。如果全堆在组件里用不了多久就会变得乱七八糟。我是用 Pinia 管理答题状态的。进入考试页时先在 store 里初始化题目列表和答案数组答案数组用题目 id 做 key值存用户的选项。切换题目只是改 currentIndex不会触发重渲染导致答案丢失。标记待确定题目、清除答案都通过 store 的 action 完成组件层只负责渲染和事件分发。倒计时我单独讲一下因为这里坑最多。倒计时不能只靠 setInterval 累减页面切后台时浏览器会挂起定时器导致回来时时间不准。我的做法是每次 tick 时不是对剩余秒数减 1而是用结束时间戳减去当前时间戳const remaining Math.max(0, Math.floor((endTime - Date.now()) / 1000))这样即使定时器被挂起恢复后也能立刻计算准确剩余值。同时在前端倒计时归零时立即禁用答题并触发交卷请求后端也会在收到交卷请求时再次校验截止时间双保险。3.3 与 Django 接口的统一封装和错误处理前端所有接口调用我都统一封装在一个 api 模块里而不是在每个页面组件里直接写 axios 请求。这样做的好处是错误处理和 token 携带逻辑只需要写一份。我在请求拦截器里统一从 store 取 token 并加到 Authorization 头在响应拦截器里统一处理后端返回的错误码遇到 401 就清除本地登录状态并跳转登录页。还需要注意一件事前后端接口的字段风格。Django 和 DRF 默认习惯用 snake_case前端 JavaScript 更习惯 camelCase。我不建议在接口里做大量字段转换尽量保持后端字段命名风格统一前端直接使用即可。如果团队里有代码规范偏好可以在序列化器里设置字段别名但不要两边各转一次容易出 bug。4. 数据库设计与关键细节4.1 核心表结构数据库设计直接决定后面开发是否顺畅。我用的核心表有这些表名主要字段说明auth_userDjango内置username、password、email账号主体user_profileuser_id、role、student_no、class_name用户业务信息questionquestion_type、stem、options、answer、difficulty、analysis题库knowledge_pointname知识点标签question_knowledgequestion_id、knowledge_id多对多关联papertitle、duration、start_time、end_time、total_score、pass_score试卷paper_questionpaper_id、question_id、score、order_no、snapshot试卷题目快照exam_recordpaper_id、student_id、start_time、submit_time、status、score考试记录answer_detailrecord_id、question_id、user_answer、is_correct、score答题详情这套表结构基本上能覆盖前文说的所有功能点。有几个关键点要说第一paper_question 里的 snapshot 字段非常重要它保存发布试卷时题目的题干、选项和答案快照。这样即使题库里的题之后被老师改了已发布的试卷也不会被动成绩统计才稳定。第二exam_record 是考试会话的核心学生每次进入考试都先创建一条记录重复进入时判断状态是进行中还是已交卷避免一个人交完卷还能再进。4.2 多对多与一对多关系落地题库和知识点是多对多关系试卷和题目是多对多关系。Django ORM 里用 ManyToManyField 可以快速创建中间表但对试卷和题目这种需要携带额外字段的中间表我用显式中间模型因为需要在中间表里存分数、顺序和快照。这和知识点的纯标签关系不一样要注意区分。一套试卷对应多个考试记录是一对多考试记录对应多个答题详情是一对多。在做班级或者学生维度的统计时直接从 exam_record 聚合即可。Django 的 ORM 支持跨表聚合写起来很简洁。比如统计某个学生所有考试的平均分一句 annotate 就能搞定。4.3 并发防重与事务边界在线考试最怕出现的情况是学生在交卷那一刻网络抖动前端重试又发了一次交卷请求导致出现两条考试记录。我在这里做了两道防线第一道是数据库层的唯一约束规定同一 student_id 和 paper_id 只能有一条有效交卷记录。第二道是事务内先查后写如果发现已有交卷状态直接返回原记录不再重复判分。判分流程本身用事务包裹任何一步失败就整体回滚。这里的经验是事务越短越好不要在事务里做耗时的文件写入或者外部请求。判分是纯内存比较速度快所以放在事务里完全没有问题。5. 实际开发中踩过的坑与排查记录5.1 跨域问题不是所有接口都要手动开 CORS前后端分离开发时默认会遇到跨域。我第一版直接用了 django-cors-headers把 CORS 全部放开开发很爽但一想部署后不能这么干于是改成了两个方案并行开发环境用 Vite 的代理转发生产环境用 Nginx 反向代理这样前后端同域根本不需要 CORS。Django 侧我只保留了允许的 Host 配置反而更安全。如果你非要直接跨域访问接口CORS 配置也要限制在白名单里不要用 allow_origin_all。这个原则适用于所有前后端分离项目。5.2 答题页刷新后状态丢失第一版答题页的所有答案都存在 Pinia 里一刷新就全部消失这对考试系统来说是致命的。后来我改成把答案实时存到 localStorage并定期同步到后端。刷新后先从后端恢复未提交的考试记录再把本地答案合并进去。考试记录接口返回进行中的状态时会携带已暂存的答题数据。这个操作看起来简单但涉及到一个敏感点如果后端已经把答案存库前端和数据库的“暂存数据”要以哪边为准我的处理是后端为准前端只是缓存每次刷新时拉取后端最新暂存数据覆盖本地。5.3 倒计时和交卷时间不一致踩过的另一个坑是前端倒计时结束了才发交卷请求但后端因为网络延迟收到请求时时间已经超了这时到底算不算有效交卷我的处理是考生在截止时间前点击交卷但因为网络原因后端收到时已经超时仍然算有效如果考生是在截止时间之后才点击直接拒绝。实现方式是在交卷接口里同时提交前端记录的点击时间后端校验时判断“点击时间是否在截止时间内”而不是用“后端收到时间”一刀切。5.4 PyCharm 调试与虚拟环境问题PyCharm 里调试 Django 是挺顺手的但有几个点要提前配好第一虚拟环境一定要用项目专属的 venv不要图方便用全局 Python不然依赖版本冲突能让你怀疑人生。第二Django 的 runserver 配置里把环境变量指向本地的 settings 文件方便区分开发和生产配置。第三调试接口时可以直接用 PyCharm 的 HTTP Client 工具不需要打开 Postman请求记录还能跟着项目走。如果在 PyCharm 的终端里执行 python manage.py 提示找不到模块基本就是解释器没选中 venv 的 python 路径。这个检查顺序是项目解释器 → 终端解释器 → 依赖是否安装齐全。按照经验十次有八次都是解释器路径的问题。6. 部署上线与安全加固建议6.1 本地可运行后的部署流程开发完不是结束能部署才是真的能用。我的部署方案是后端用 Nginx uWSGI 跑 Django前端用 npm run build 打包出静态文件由 Nginx 直接托管。Nginx 配置里有两条路由规则/ 开头和 /assets 开头的请求走前端静态文件/api 开头的请求反代到 Django 的 socket。这样用户访问的是同一个域名既解决了跨域也方便后面的 HTTPS 配置。部署到服务器之前需要把 Django 的 DEBUG 设为 False把 ALLOWED_HOSTS 设置成实际域名把数据库从 SQLite 切到 MySQL 或 PostgreSQL。SQLite 在本地开发很方便但线上并发一高就顶不住我在模拟项目X 部署时深有体会。一个简化版的 Nginx 核心配置片段server { listen 80; server_name exam.example.com; location / { root /opt/exam_system/frontend/dist; try_files $uri $uri/ /index.html; } location /api { include uwsgi_params; uwsgi_pass unix:/tmp/exam_system.sock; } }6.2 静态资源与图片上传处理考试系统的图片不多但如果你打算在题干里放图片就要处理上传。我的建议是上传的文件统一放到单独的 media 目录Nginx 里单独配一个 /media 路由不要让 Django 处理静态文件的请求。Django 处理文件请求是很快但高并发下容易拖累进程Nginx 直接托管文件效率高得多。6.3 安全加固不能省线上考试系统牵涉学生数据安全这关不能马虎。我重点做了这些第一密码字段用 Django 自带的 PBKDF2 加密绝不存明文。第二所有接口都用 JWT 保护管理端接口额外校验角色。第三数据库操作全部走 ORM禁止拼接 SQL。第四Nginx 层限制单 IP 的请求频率防止接口被刷。第五答题时间相关的校验以服务端为准防止绕过前端作弊。还有一个细节JWT 的过期时间不能太长。我设置 access token 有效期 2 小时refresh token 有效期 7 天前端在 token 快过期时静默刷新。这样既能保证体验又降低了 token 泄露带来的风险。这套系统做完再回头看我最大的感受是线上考试系统的难点并不在“会不会写代码”而在流程设计和边界处理。题库、试卷、考试、成绩这条链路只要有一个环节没想清楚后面就会连环报错。特别是时间控制、并发交卷、题目快照这些细节看着不起眼决定了系统到底能不能真实用起来。如果你也准备动手做类似项目我的建议是先别急着写代码把角色、流程、表结构画清楚再用 Django 把后端接口一个个跑通最后才写 Vue 页面。顺序反了会非常痛苦。开发过程中把 PyCharm 的调试、Vite 的代理、Django 的 Admin 充分利用起来这几样配合好效率会明显提升。最后分享一个小技巧不要把“能运行”当成目标而是把“连续考三场不出问题”当成验收标准。模拟一次并发交卷、模拟一次断网重试、模拟一次改本机时间你就会发现系统里还有很多可以加固的地方。把这些小问题解决掉这个项目才算真正完成。