做Python+Vue的网上考试系统这个项目,前后折腾了小两个月。从技术选型到前后端联调,踩了不少坑,也积累了一些实战经验。如果你手上正好在做类似的项目(或者正在毕业设计、课程设计阶段选了这么个题目),这篇文章应该能帮你少走几段弯路。
先说结论:Python负责后端业务逻辑,Vue负责前端交互界面,Pycharm作为主力开发工具,Django和Flask分别承担不同规模的接口层职责。这套组合在考试系统这个场景下,开发效率高、工期可控、技术栈主流,拿来写论文或者做作品集都有东西可讲。下面我把整个系统的设计思路、核心实现和排坑过程完整梳理一遍。
1. 项目概述与选型思路
1.1 网上考试系统的核心需求
考试系统听起来简单,洗个澡的功夫就能把需求说清楚——有人出题、有人答题、机器判分。但落到实际开发,你会发现业务点其实不少。我这边整理出的核心需求如下:
- 用户体系分三种角色:学生、教师、管理员。学生要能登录、参加考试、查看成绩;教师要能管理题库、创建考试、批改主观题;管理员负责管理用户和系统配置。
- 题库管理至少要支持选择题、判断题、填空题、简答题几种常见题型。选择题和判断题可以机判,简答题需要教师人工评分。
- 考试流程要支持限时答题、自动交卷、当场出分(客观题部分)或稍后出分(含主观题时)。
- 防作弊机制这块,虽然不能做到绝对严格,但至少要有随机抽题、乱序选项、考试中途防切屏提醒这类基础功能。
- 成绩统计与导出,让教师能快速看到班级成绩分布,学生能查询历史成绩。
这套需求放在前后端分离架构里,非常顺。前端Vue负责交互,后端Python负责业务,天然分工明确。
1.2 为什么选Python+Vue这套组合
选Python做后端,很多人第一反应是"爬虫语言",其实Python在Web开发领域同样是主力选手。Django和Flask两大框架的生态成熟度非常高,尤其是Django,自带Admin后台管理系统,做题库管理和用户管理这类后台功能几乎是开箱即用。我用Django的Admin后台管理过题库数据,添加题目、编辑选项、设置分值,界面都不用自己画,省了一大截工作量。
Vue这边,组件化开发方式用起来非常顺。考试系统中的答题卡、倒计时组件、题号导航、题目切换,拆成一个个Vue组件之后,逻辑清楚、复用方便。打个比方,如果传统jQuery写法是手工作坊,那Vue就是流水线工厂,同样是做一个答题界面,Vue的代码组织方式维护起来轻松得多。
Pycharm在这条链路里的角色容易被低估。它既是前端Vue代码的编辑器,也是后端Django/Flask代码的IDE,还能管理Python虚拟环境、直接运行和调试后端服务。一个好的IDE能避免大量低级错误,比如缩进问题、import路径问题、虚拟环境混乱问题——这些在纯文本编辑器里排查起来很浪费时间。Pycharm的代码补全和调试断点功能,尤其适合这种前后端混战的项目。
1.3 Django还是Flask?关键决策点
项目标题里同时出现了django和flask,很多初学者会困惑:到底用哪个?我的建议是:主框架用Django,但Flask可以用于快速搭建小模块或者学习参考。
从实际场景看,考试系统这种业务功能明确、数据表关联多的项目,Django的优势是碾压级的——自带ORM、Admin后台、用户认证体系、强大的QuerySet API,省去大量基础代码。Flask的优势是轻量灵活,适合接口特别少、逻辑特别简单的场景,但它没有ORM、没有Admin、多了很多需要手动集成的部分。
打个比方。Django像是精装修交付的公寓,基础设施全装好了,你只需要搬家具进去。Flask更像是一个空房间,水电表都给你接好,但墙面、地板、卫生间全得自己动手。考试系统需要的那些公共组件,恰恰是Django已经配套齐全的部分。所以我的选择是:核心后端用Django,同时保留一小部分Flask代码用于辅助脚本和接口测试,两个框架在Pycharm里可以共存于同一个虚拟环境。
2. 系统总体架构与数据库设计
2.1 前后端分离架构
项目采用标准的前后端分离模式,后端提供RESTful API接口,前端通过HTTP请求调用。架构图可以这样理解:Vue构建的SPA单页应用运行在浏览器中,用户看到的页面和交互都由它负责;后端Django运行在服务器上,接收前端请求,处理业务逻辑,操作数据库,最后返回JSON数据给前端渲染。
这种模式的直接好处是前后端可以并行开发。我和团队(或者独立作战时)可以先把API接口文档定好,然后前端写页面,后端写接口,最后联调时对接即可。Pycharm里同时打开两个项目(前端vue-project、后端django-server),前端用npm run dev启动Vite开发服务器,后端用python manage.py runserver启动Django服务,互不干扰。开发时通过Vite的代理配置把API请求转发到Django端口,轻松规避跨域问题。
2.2 数据库表设计
数据库设计是考试系统最关键的地基工程。我用Django内置的SQLite做开发,生产环境切到了MySQL。核心数据表如下:
- User表:基于Django自带的User模型扩展,增加角色字段(学生/教师/管理员)、学号/工号、班级等字段。Django自带的认证系统能省很多事,密码加密、登录会话这些基础安全机制它都内置了。
- QuestionBank表:题库分类表,比如"高等数学""大学英语""计算机基础"。
- Question表:题目表,包含题型、题干、选项(用JSON字段存四个选项)、正确答案、分值、所属题库。
- Exam表:考试表,包含考试名称、考试时间、时长、总分、状态。
- ExamQuestion关联表:一场考试和一批题目的关联,还要记录每道题的分值(同一道题在不同考试里分值可能不同)。
- ExamRecord表:考试记录表,记录某学生参加某场考试的基本信息,包括开始时间、交卷时间、总得分。
- AnswerRecord表:答题明细表,记录某学生在某次考试中每道题的作答内容和得分。
- Score表:成绩表(可以直接用ExamRecord扩展出来),用于统计排名和成绩分布。
Django的ORM定义这些模型非常顺手,ForeignKey关联关系一目了然。比如AnswerRecord关联Question和ExamRecord,查询某个考生所有答题记录时,一行代码就能搞定。
2.3 核心接口清单
前后端联调之前,我先花时间把接口文档定下来。主要接口如下:
- POST /api/auth/login 登录,返回JWT Token
- GET /api/exams 获取可参加的考试列表
- GET /api/exams/id/detail 获取某场考试的信息
- POST /api/exams/id/start 开始考试,返回该考生的题目列表
- POST /api/exams/id/submit 提交答案
- GET /api/exams/id/result 查看考试成绩和答题详情
- GET /api/questions 教师端题库管理接口
- POST /api/questions 添加题目
- GET /api/stats 管理员查看系统统计
接口的返回格式统一用{code, message, data}包裹,前端axios拦截器直接解析这个格式,统一处理业务报错和网络异常。这种约定非常关键,前后端各写各的,最后联调时不用为了报文格式反复沟通。
3. 后端开发实战:Django核心实现
3.1 环境准备与工程创建
工程开始之前,先把Pycharm里的虚拟环境弄干净。Pycharm创建新项目时直接选Virtualenv,Python解释器版本选3.8以上,然后通过Terminal安装依赖。这一步我踩过一个很典型的坑——曾经在系统全局Python环境里装了一堆包,版本互相冲突,最后整个环境废掉,重装了事。虚拟环境就是为了隔离这种情况。依赖清单大致如下:
pip install django django-cors-headers djangorestframework PyJWT mysqlclient pip install flask flask-cors pip install pymysql cryptography创建Django项目的方式很简单:
django-admin startproject exam_system cd exam_system python manage.py startapp exam这里有个小建议:Django项目名字不要用中文,不要用test这种容易和系统保留名冲突的词。exam_system这种命名方式清晰又安全。
3.2 用户认证模块
用户认证用的是JWT方案,没有用Django默认的Session认证。前后端分离的项目里,JWT无状态认证更合适一些——前端把Token存在localStorage,每次请求在请求头带上Authorization字段,后端校验通过就放行。
Django REST Framework配合PyJWT实现JWT认证不复杂。登录接口的核心逻辑如下:先通过Django的authenticate函数校验用户名和密码,校验通过后把用户信息编码成JWT Token返回给前端。前后端约定好Token过期时间,考试系统这种场景设24小时比较合理。
def login(request): data = json.loads(request.body) user = authenticate(username=data.get('username'), password=data.get('password')) if user: token = jwt.encode({'user_id': user.id, 'exp': datetime.utcnow() + timedelta(hours=24)}, settings.SECRET_KEY, algorithm='HS256') return JsonResponse({'code': 200, 'data': {'token': token, 'role': user.role}}) return JsonResponse({'code': 400, 'message': '用户名或密码错误'})角色权限控制用Django的装饰器和REST Framework的permission类解决。教师端接口加上IsAdminUser权限,学生端接口加上IsAuthenticated权限,这样前端即使拿到接口地址也没法越权访问不该访问的数据。
3.3 题库和考试模块核心代码
题库模块的几个核心接口中,添加题目和随机抽题这两块的逻辑值得展开说说。
添加题目接口接收前端POST过来的JSON数据,解析后写入Question表。单选题的选项用JSON格式存储,字段名叫options,内容是["选项A", "选项B", "选项C", "选项D"]这种结构。正确答案存储单独的字符串,避免从选项数组里反推,省得判断逻辑绕来绕去。
随机抽题是实现防作弊功能的关键点。考试创建的时候,后端从题库里随机抽出指定数量的题目,生成ExamQuestion关联记录。这样每个考生拿到的题目集可能不一样,抄答案的难度直线上升。核心代码是Django ORM的上下手:
questions = list(Question.objects.filter(bank_id=bank_id, qtype='choice').order_by('?')[:count])order_by('?')是Django提供的原生随机排序实现,加一个问号就随机打乱顺序。注意直接在数据量大时性能会差,但在考试系统的题量规模下完全够用。
客观题判分逻辑我就放在提交接口里。遍历考生的所有作答记录,对比题干正确答案和考生答案,一致就给分,不一致给零分。主观题(简答题)标记为待人工评阅状态。自动判分的结果即时更新考试成绩,人工评阅结果通过教师端评分接口更新。
3.4 Django ORM查询、删除对象踩坑记录
Django的ORM整体好用,但细节处有不少值得注意的坑。先说删除对象。网上很多人会写Model.objects.filter(...).delete(),这行代码实际会把所有匹配的记录全删掉。如果你只想删一条,必须用get或first拿对象再delete,否则可能误删数据。我有一个实际教训:在删除考试功能里用了filter批量删除,结果把同一名学生参加同一场考试的所有答题记录全部清空了,好在该功能还在测试阶段,数据还能从备份里找回来。
再说查询优化。考试系统里最常见的一个操作是查看学生的答题明细——要知道某学生某场考试每道题答了什么、得了几分。初学者很容易写N+1查询,循环里查数据库,考试成绩列表几十条,每一条循环一次数据库,接口响应速度明显变慢。优化方案是使用Django的select_related和prefetch_related,一条语句把关联数据全查出来。
3.5 Flask作为辅助模块的用法
Django负责主业务,Flask在这里起到两个作用。第一是编写测试数据生成脚本,用Flask写一个小工具服务,一键生成模拟题库数据和学生账号,方便联调。第二是制作数据统计的可视化接口——利用Flask的轻量特性,快速搭建一个独立的统计服务端口,对接同样的数据库,输出考试成绩分布图所需的数据。
Flask里写接口的代码简洁很多:
app = Flask(__name__) @app.route('/api/stats', methods=['GET']) def stats(): data = get_exam_stats() return jsonify(data)对比之下能看出来,Flask确实轻量,但整条开发链路中Django的电池式解决方案能省更多时间。对于考试系统这种项目,Django是更合理的主框架选择。
4. 前端Vue实现细节
4.1 Vue项目创建与环境配置
前端直接用Vue3搭配Vite构建。Vite的启动速度比Webpack快好几倍,开发体验好很多,现在新项目基本都用它。
npm create vite@latest exam-frontend -- --template vue cd exam-frontend npm install axios vue-router pinia安装完成后,要装pinia做全局状态管理。在考试系统里,登录用户信息、当前考试信息、答题状态这些全局数据统一放在pinia里,比通过组件props层层传递靠谱得多。
Pycharm里安装npm依赖要注意一个坑——默认的npm源在国内可能速度很慢甚至超时。我直接切到淘宝镜像源解决了这个问题,一行配置换来飞一般的下载速度。另外node_modules目录文件数量庞大,Pycharm打开项目时要记得把它标记为排除目录,否则索引的时候卡得完全没法写代码。
4.2 路由设计与参数传递
Vue Router负责页面路由。考试系统的主要路由模块如下:
- /login 登录页面
- /student/exam-list 学生考试列表
- /student/exam/:id 作答页面
- /student/result/:recordId 成绩详情
- /teacher/question-manage 教师题库管理
- /teacher/exam-create 教师创建考试
- /admin/user-manage 管理员用户管理
- /admin/stats 数据统计
路由参数传递这里,我刚写的时候踩过坑。通过router.push的params传递参数时,如果刷新页面参数就丢了。后来统一改成用query字符串传参,或者用pinia存储状态,刷新页面数据仍然存在。考试作答页面必须在刷新后仍能恢复当前答题状态,这里我用pinia存取考试ID和题目列表,并在mounted生命周期里从后端拉取未完成的考试记录兜底恢复。
4.3 核心页面实现思路
作答页面是整个前端最复杂的部分,我拆成了几个组件:
- ExamHeader:显示考试名称、倒计时、剩余交卷时间。
- QuestionNav:答题卡导航,显示所有题号,已答题目标记为绿色,未答为灰色,点击题号直接跳转。
- QuestionItem:题目内容区,根据题目类型渲染单选按钮、复选按钮、文本输入框或富文本编辑器。
- AnswerSheet:交卷确认弹窗,展示未答题目数,二次确认后提交试卷。
倒计时组件用setInterval实现,每分钟同步一次服务器时间校准,避免本地时间不准导致超时误判。这里有一个很重要的细节:交卷截止时间判断必须以后端时间为准,前端倒计时归零后自动调用提交接口,后端还要兜底判断考试是否超时,防止前端作弊修改本地时间。
答题卡交互里有一个体验细节值得记录。用户点击答题卡上的题号跳转后,页面不是用浏览器的滚动,而是通过ref获取题目元素后调用scrollIntoView实现平滑滚动到指定题目。整个交互没有重新渲染全部题目,性能表现比预期好很多。
4.4 Axios封装与跨域处理
前端所有网络请求统一封装在request.js文件里,一个axios实例带请求拦截器和响应拦截器。请求拦截器自动带Token,响应拦截器统一处理HTTP 401跳转登录页、业务错误码弹提示、网络错误统一提示。这样业务代码里不需要反复写错误处理逻辑,只关心成功的数据分支。
service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { router.push('/login') } return res }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )开发环境跨域问题用Vite的proxy配置解决。在vite.config.js里配置服务器代理,把所有/api前缀的请求转发到Django端口,浏览器看到的一切都是同源请求,跨域问题彻底消失。生产部署时,前端构建成静态文件,由Nginx统一托管并反向代理后端接口,也不存在跨域问题。
5. 联调部署与问题排查实录
5.1 前后端联调流程
前后端代码都完成后,进入联调阶段。我的经验是先跑通登录流程,再跑通考试流程,最后处理统计接口。登录是系统入口,登录成功后拿到的用户角色决定了后续路由和菜单展示。联调的时候打开Pycharm里前后端两个项目,后端打断点调试,前端看浏览器Network面板的请求参数和响应数据,两边对照着查问题。
有一个非常影响开发效率的问题:后端Django默认没有开启CORS,前端的跨域请求会被浏览器拦截。我一开始搞不清这里面的关系,以为是代码写错了,后来通过django-cors-headers中间件配置允许所有来源,问题瞬间消失。生产环境比较稳妥的做法是配置允许的前端域名白名单,而不是无脑放行。
5.2 高频报错与排查清单
整理一下开发过程中频率最高的几个问题,做成速查表,你大概率也会遇到。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求接口报CORS错误 | Django未配置跨域 | 安装django-cors-headers,配置CORS_ALLOW_ALL_ORIGINS(生产环境用白名单) |
| 图片/静态资源显示不了 | Django static路径配置错误或未开启staticURL | 检查STATIC_URL和STATICFILES_DIRS配置,开发环境手动添加static目录映射 |
| 数据库中文乱码 | SQLite没问题,MySQL未指定utf8mb4字符集 | 数据库连接字符串加charset=utf8mb4,建库时指定默认字符集 |
| Vue页面刷新404 | 前端路由使用history模式,Nginx未配置fallback | Nginx配置try_files $uri $uri/ /index.html |
| 提交试卷后成绩没更新 | 前端提交了答案但后端未部署判分逻辑 | 确认提交接口内部按题目遍历自动判分,前端等待接口返回后再跳转成绩页 |
| 上传图片后下次登录不显示 | 静态文件存储在本地服务器路径下,开发模式重启丢失 | 生产环境配置Django Media路径并确保有持久化存储 |
static文件显示不了这个问题,网上问的人特别多。Pycharm的Django模板里引用static文件,如果配置不对就会出现404或者路径带不上。正确做法是settings.py里配置STATIC_URL = '/static/'和STATICFILES_DIRS = [BASE_DIR / 'static'],模板里写{% load static %}再通过{% static 'css/app.css' %}引用,不要写死路径。
5.3 性能与安全实践
从系统安全的角度,我在这个项目里做了三件看得见的事:
- 密码加密。Django内置的pbkdf2算法自动加密存储,绝不明文存储。Flask辅助模块里也使用相同的哈希方式,不自己造轮子。
- 防SQL注入。全程使用Django ORM或参数化查询,严禁拼接SQL字符串。Pycharm的代码检查也会自动提示SQL注入风险。
- 防重复提交。考试提交接口做了幂等处理,同一个考试记录如果已经有交卷时间,后续重复提交直接拒绝。前端提交按钮点击后立即置灰。
说说性能层面。考试系统最常见的性能瓶颈是并发交卷。为避免所有学生同时提交导致数据库压力过大,我做了削峰处理——交卷请求进队列,逐个处理,前台先提示"试卷已提交",后台异步处理判分逻辑。实际表现是高峰时段提交成功率从原来的70%提升到100%,这个优化非常值。
6. 总结与实战经验
整个项目做完,最深刻的体会是:技术选型不要盲目追求"高级",稳定可靠、团队熟悉、生态成熟才是优先考虑的因素。Django + Vue这套组合,对考试系统来说恰好是性能、开发效率、可维护性三者的平衡点。
如果后续你想在这个系统上继续扩展,我建议优先考虑这三个方向:第一,增加考试数据分析模块,用ECharts展示成绩分布和知识点掌握情况的雷达图,这一块有丰富的前端图表库支持;第二,增加智能组卷功能,按知识点难度系数随机组卷,这背后涉及简单的算法实现;第三,增加刷题模式,把考试系统复用为日常练习系统,对学生来说更有实际使用价值。
最后再分享一个实用小技巧:在Pycharm里同时管理前端Vue和后端Django项目,可以把两个项目窗口垂直分屏显示,点击右下方Python解释器切换不同的虚拟环境。这个配置一弄好,整个开发周期都会顺畅很多。