说实话,最初想搞这个公务员考试练习系统,纯粹是被备考过程里那些烂体验逼出来的。市面上的刷题App要么弹广告,要么题库分类乱得离谱,想整理错题还得开会员,更别说自己导出一份符合复习节奏的章节练习了。我本身是做Python后端开发的,那段时间正好想系统练一遍前后端分离的技术栈,于是干脆自己动手写了一个。项目主线是Python的Web框架(Django和Flask我都测试过)+ Vue前端 + PyCharm开发调试,整个项目前后花了大概三周业余时间,从数据库建模到组卷、判分、错题闭环,再到部署上线,基本把一套完整的Web应用开发流程走了一遍。
这篇文章我会把项目从选题动机到技术选型、从表结构设计到核心业务逻辑、从踩坑修复到部署运维,完完整整地拆开讲。适合两类人参考:一类是想拿真实业务场景练习Python Web开发的读者,另一类是公考备考者,想给自己或身边人做一个干净、可控、可定制的练习工具。哪怕你目前只学过Django基础或者只写过几个VueDemo,按这篇文章的路径走下来,也能跑通一个能用的系统。
1. 项目缘起:不是拍脑袋,是被备考体验逼出来的
1.1 市面上的练习工具到底缺什么
先说我观察到的几个痛点,这也是我决定自己动手的直接原因。
第一,题库不透明。很多App宣传几十万道题,但你不知道它的题库来源、更新频率,也不知道每个模块的题量分布。备考刷题最怕的是练了很久发现某类题型重复率极高,真正的薄弱模块反而没练到。
第二,错题整理成本高。不少工具把错题本功能放在付费墙后面,普通用户只能自己截图或手抄。可公务员考试的题量一上来,靠手抄错题根本不现实。
第三,学习数据没有沉淀。我想要的不是只刷题,而是能看到每个模块的正确率变化、练习量趋势、薄弱点在哪。这些东西本质上就是数据分析,市面工具做得好的不多。
第四,功能定制不自由。比如我想做"资料分析限时专项练习",每次随机抽5道题,倒计时8分钟,练完自动判分并记录用时——这类个性化练习模式,现成App很少能精确配置。
所以我自己动手做了这个系统。核心需求拆开就四条:一套可控的题库管理后台、一套随机可靠的组卷逻辑、一套自动判分和错题收集机制、一组能辅助复习的数据统计。
1.2 这个项目适合谁来参考
如果你是Python Web开发者,这个项目最大的价值在于把后端框架、数据库设计、REST接口、前端调用串成一条完整链路。它不是教科书里那种零散的Demo,而是有业务规则、有边界条件、有数据一致性的真实系统。
如果你是备考者,又想学点技术,那这个项目就更对口了。你可以把题库存成JSON或者SQLite文件,随时导入导出,在电脑上或者局域网里跑一套自己的练习环境,数据完全掌握在自己手里。
我认识的一位备考朋友,后来我直接把项目给他部署到家里一台旧笔记本上,他把行测5000多道题按模块分好类,考前一个月天天用这个系统刷题和看错题统计。他说最有用的反而是"正确率曲线"——能直观看到哪个模块从60%涨到80%,哪个模块一周都没动弹,复习计划调整起来不再靠感觉。
2. 技术选型实测:Django和Flask的取舍,以及Vue在这里的位置
2.1 Django还是Flask,我最后怎么定的
标题里写了"django-flask",因为很多人在选Python Web框架时都会摇摆。我把两个框架在这套系统里的表现做个对比,都是实测感受,不搞信仰之争。
| 对比维度 | Django | Flask |
|---|---|---|
| 自带Admin后台 | 有,开箱即用,适合管理题库 | 无,需要另外接第三方后台 |
| ORM成熟度 | 丰富且顺手,迁移数据方便 | 要用SQLAlchemy另外配 |
| REST API支持 | DRF非常完善,序列化器好用 | Flask-RESTful相对基础 |
| 项目结构 | 按app组织,适合业务模块较多的系统 | 灵活自由,但规模大后要自己约束 |
| 学习成本 | 概念多,上手略慢 | 上手快,但很多功能要自己拼 |
| 适合本项目的理由 | 题库、用户、练习记录、错题本天然适合多app拆分 | 更轻,但管理后台这块要费不少劲 |
这套系统有几个客观需求:需要一个顺手的管理后台,用来批量导入题目、调整分类、修改题目内容;需要一套带权限校验的API;需要处理多个业务模块之间的外键关系、数据一致性。这些正好都是Django的强项。
我最终选了Django。但我也实际用Flask搭了一版简化原型,验证了组卷和判分逻辑。结论是:如果你只写纯API服务、不要可视化后台、管理端用现成的客户端工具连数据库,Flask也完全能做,代码量还少一些。但如果想要一个完整的、可交付别人使用的系统,Django的Admin和DRF能省下至少一周时间。
2.2 Vue在前端扮演的角色
练习系统的前端有一个明显特征:不是纯展示型页面,而是高交互、多状态的。用户答题时要做选择、切换下一题、标记不确定、查看答题卡、倒计时提醒、交卷后瞬间显示判分结果——这些交互如果用传统的多页面同步渲染,体验会非常差。
Vue的组件化在这里帮了大忙。我把答题页拆成几个组件,练习界面、模拟考试界面、错题本界面、数据看板都各自独立,组件之间通过Vuex或Pinia共用用户信息和练习状态,数据流清晰很多。
配合Element Plus用来做界面组件库,表格、表单、按钮、分页、弹窗都有了现成的,省掉大量手写CSS的时间。图表部分接的是ECharts,用来渲染模块正确率、练习量趋势、题型分布这些统计图。
开发工具我用的是PyCharm。专业版对Django的支持比较深入,可以直接跑Django服务器、调试断点、管理虚拟环境;对前端也能识别Vue文件,通过npm脚本运行dev服务。如果用的是社区版,功能会少一些,但通过扩展插件也能达到差不多效果。
2.3 前后端分离架构怎么落地
这套系统采用前后端分离模式,后端只提供JSON API,前端独立运行。
后端API设计示例:
POST /api/auth/login/用户登录,返回JWT TokenGET /api/categories/获取题库模块分类GET /api/questions/?category_id=1&limit=10获取指定分类的题目(不包含答案)POST /api/exams/创建一次练习/模拟考试记录POST /api/exams/{id}/submit/提交答卷,返回判分结果与答案解析GET /api/wrong-book/获取错题本列表GET /api/stats/overview/获取统计数据
前端通过axios请求这些API,本地开发时前端跑在Vite默认的5173端口,后端跑在8000端口,通过代理转发解决跨域。生产环境则把前端打包成静态文件交给Nginx,Nginx把/api前缀的请求反向代理到Django的Gunicorn服务。
3. 先把地基打牢:题库与练习记录的表结构设计
3.1 核心表怎么拆
练习系统的业务核心是题库和练习记录,表结构设计直接决定后面组卷、判分、统计好不好写。我最终定下来的表结构大致如下。
用户表直接用Django自带的User,扩展一个Profile表存备考类型(行测、申论等)和每日目标题数。
题目分类表,字段包括:分类名称、上级分类(用于二级归类,比如"言语理解-片段阅读")、排序权重。分类不能做得太粗也不能太细,建议两级,一级是行测的五大模块,二级是具体题型,这样后期统计才能精确到细颗粒度。
题目表,这是最重要的一张表,字段包括:所属分类、题型标识、题干内容、选项(我用JSON存,格式如{"A":"文字内容","B":"文字内容","C":"文字内容","D":"文字内容"})、正确答案、解析、难度等级(1到5整数)、是否启用、题目来源、创建时间。其中正确答案和解析这两个字段,必须和后端序列化器配合好,不能直接暴露给题目列表接口。
练习/考试记录表,字段包括:用户、练习模式(顺序练习、随机练习、模拟考试)、题目范围分类、设定题数、开始时间、结束时间、总得分、总用时。这张表负责承载每一次练习的元信息。
答题明细表,字段包括:用户、所属练习记录、对应题目、用户提交的答案、判分结果(正确/错误/未作答)、用时。这张表是判分和统计的数据来源。
错题本表,字段包括:用户、题目、首次答错时间、最近答错时间、累计答错次数、连续答对次数、状态(练习中/已掌握)。错题本的"掌握程度"是靠这张表维护的。
3.2 几个容易踩坑的设计细节
关于正确答案字段,我强调一下:答案的数据类型不能用题目文本里带"答案:C"这种形式,一定要拆成一个独立结构化的字段。因为判分时需要批量比对,如果答案埋在文本里,每次都要解析,性能和可靠性都很差。我的做法是单选题答案存"B",多选题存["A","C"],判断题存"正确",统一在模型里定义类型。
关于题目列表接口和答题接口,接口返回的题目字段要分开设计。题目列表接口不能带正确答案和解析,否则用户用浏览器开发者工具一翻就能看到答案,练习就失去意义了。我的方案是序列化器里写两个版式:列表版不含答扩展字段,判分返回的题目快照里才有正确答案。这个设计在项目一开始就要定好,后面返工很麻烦。
还有一个容易被忽略的是题目的随机性。如果组卷只是简单地从所有题目里随机抽,用户练两三次就发现题目重复了。我在题目表里加了一个last_shown_at时间戳,组卷时优先抽最近7天没出现过的题,才能保证练习的覆盖度。
3.3 批量导入题目:效率翻倍的操作
手工在Admin后台录入几百道题不现实,我写了一个Django management command,支持从CSV或JSON批量导入题目。CSV的列设计和题目表的字段一一对应,中文表头用utf-8-sig编码,避免Windows下直接用utf-8读取出现乱码。
导入命令我加了两层校验:第一层格式校验,检查选项和答案字段是否完整;第二层逻辑校验,比如多选题的答案数组里是否有不存在的选项索引。任何一道题校验失败,整批导入就会回滚,保证数据坏不到数据库里。我在项目里放了一个示例题库,包含行测五个模块各50道代表性题目,用于开发和演示。真实使用的时候,可以把自己手里整理好的题库按同样的CSV格式导进来,几千道题也就几秒钟的事。
4. 练习系统的核心逻辑:随机组卷、判分与错题闭环
4.1 随机组卷:要随机,更要有比例
组卷算法是练习系统的灵魂。很多人想到随机抽题就是一句ORDER BY RAND() LIMIT n,但直接这么做有三层问题:性能(题目多时会变慢)、重复度(同一个用户连续两次抽到同样的题)、不公平(不同分类的题量占比不稳定)。
我的做法是分三层:
第一层,按分类算配额。前端提交练习配置时带上分类和题数,后端先把用户设定范围内所有启用中的题目按分类统计数量,然后按比例分配每个分类抽几道题。比如用户设定30题,如果资料分析只占总题量的8%,那么只会抽2到3道题,而不是把整次练习变成资料分析的专场。
第二层,分类内做"排除最近出现"的随机。用last_shown_at做条件,优先抽最近7天没出现过的题。如果不够抽,按last_shown_at升序补足。我用一个游标遍历的方式,不依赖MySQL或SQLite的分页,性能稳定。
第三层,对二次抽题做排除。用户在重复练习时,后端会记录用户已经做过的题目ID,组卷时把它们排除掉,除非该分类已经没题了,才允许复用。
这样组出来的卷子,覆盖面、新鲜度和比例感都比较接近真实练习场景,模拟考试模式还会额外按难度比例抽题(难题占20%、中等占60%、基础占20%),更贴近真实试卷的梯度结构。
4.2 答题、交卷与超时强制交卷
答题的核心交互是"计时 + 逐题作答 + 最后交卷"。
前端用Vue维护当前题号和答题卡数组,用户每选择一道题就把答案写入内存状态。倒计时我不用前端本地时间做截止,而是由后端在创建练习记录时把服务器时间返回给前端,前端拿到截止时间戳做倒计时。这样用户改了本地系统时间也不影响考试计时。
到时间后的处理逻辑:前端倒计时归零自动触发交卷,同时后端也会在提交接口里校验当前时间是否晚于截止时间。如果网络抖动导致提交晚了,后端先按正常判分逻辑处理,但会把答题明细里所有时效性检查置为"过期交卷",不参与正常成绩统计。这套逻辑不算复杂,但能防止两种作弊方式,一种是改本地时间,另一种是卡在截止瞬间再提交。
4.3 判分与解析返回
判分逻辑本身很简单:遍历答题明细,逐条比对提交答案和正确答案,判断为正确、错误、未作答三种状态,最后汇总总分和正确率。
但有几个边界情况要处理:
- 多选题:只要答案数组和正确答案不一致(哪怕少选或多选一项)都判错。具体扣分规则可以设置,但默认整题零分。
- 判断题:用户提交"正确"和"错误",和标准答案比对,没有中间态。
- 案例分析题(主观题):客观题判分完成后,主观题标记为"待批改",后台管理员或用户自己可以在管理端给分。我做的版本里主观题默认记录答案内容,不自动判分,而是提示用户手动对照参考答案。
交卷接口返回的数据结构里,除了总分和正确率,还包括一个题目列表,每道题目带上了用户答案、正确答案、判分结果、题目解析。前端的答题卡页面会按"正确/错误/未答"染成不同颜色,点击题目格子能看到详细的解析面板。
4.4 错题闭环:自动进错题本,而不是手动收集
答错的题会自动进入错题本。错题本不是简单地把题堆在一起,它要解决的是"重新练习和记忆掌握"的问题。
我的策略是:每道错题有一个"连续答对次数"字段。用户做错题练习时,答对一次就+1,答错一次就清零。连续答对三次,这道题就判定为已掌握,从错题本移除,进入"已掌握"归档列表。这样错题本里的题量会越来越小,用户也直观地看到自己正在攻克薄弱点。
错题练习模式也比正常练习严格一些:重做错题时,优先抽取最近答错次数最多、连续答对次数最少的题。这套排序逻辑让我想起当年用单词App背单词时,算法总会把最陌生的词排在最前面。练习系统同理,用户没掌握的内容应该被更频繁地推出来。
4.5 数据统计:正确率、模块分布与每日练习量
练习系统如果只有刷题和错题本,还缺闭环的最后一环——数据统计。我在后端写了一个统计接口,一次性返回这几项数据:
- 总练习量和总正确率
- 按模块(分类)统计的题目数量、正确率、平均用时
- 近14天每日练习量和正确率趋势
- 最薄弱模块Top3(正确率最低的三个分类)
- 错题本当前数量和近7天新增错题数
前端用ECharts把趋势图画出来,柱状图展示每日练习量,折线图展示正确率变化,饼图展示各模块做题占比。用户最关心的其实是"哪个模块拖了我的分",这个统计页面直接回答这个问题,复习的时候也就知道该往哪使劲了。
5. PyCharm开发与前后端联调:踩过的坑逐个说
5.1 跨域和Token认证:联调第一道坎
前后端分离开发,第一天遇到的就是跨域报错。前端在5173端口访问后端8000端口,浏览器默认不允许。解决办法是后端安装django-cors-headers,在settings.py里配置允许的来源列表。本地开发我直接允许http://localhost:5173和http://127.0.0.1:5173,生产环境则只允许线上域名。
认证部分我用的是djangorestframework-simplejwt。登录成功后拿到access token和refresh token,前端把access token存在内存或localStorage,每次请求在Authorization: Bearer <token>头里带上。为了省事,我在axios封装里做了拦截器,遇到401就自动用refresh token刷新access token,再重发原请求。这个逻辑看着简单,但实际联调时我踩了一个坑:刷新token的请求本身如果也走拦截器,会导致死循环。解决办法是给刷新token的请求加一个skipAuthRefresh标记,不让它再进入重发逻辑。
5.2 时区问题:奇怪的"答题超出时长"Bug
有一阵子用户反馈,明明刚答几道题就提示"练习已超时"。排查半天发现是时区问题。Django默认的TIME_ZONE是UTC,如果创建练习记录时存的时间是UTC时间,前端拿到的截止时间戳也基于UTC,但浏览器里用户看到的本地时间会差8个小时。前端倒计时用的是本地时间换算,结果就是练习还没开始就显示超时。
解决方案:后端创建练习记录时,时间统一用UTC存储(USE_TZ=True),但API返回给前端的开始时间和截止时间都转成ISO 8601字符串并带上时区偏移,前端用dayjs解析时间戳后,统一转为本地时间轴。这样不管用户在哪个时区,倒计时都是准的。
5.3 答案字段泄漏:差点把题目的底裤都露了
这个坑我印象最深。第一次联调答题页时,我直接把ORM模型序列化输出,结果前端网络面板里能看到每道题的正确答案和解析。当时心里一凉,这哪是练习系统,简直是开卷考试。
解决方案就是前面说的,同一道题在列表接口、判分接口、错题本接口里用不同的序列化器:列表版没有答案和解析;判分完成后返回的题目快照里带上答案和解析;错题本详情页因为用户已经答过了,所以允许显示答案。排查这类问题还有一个技巧:前端开发者工具的网络面板本地数据直接打开看字段,任何不该出现的字段一眼就能发现。
5.4 Vue打包部署后的API路径问题
本地开发一切正常,前端打包部署到Nginx之后,所有API请求全变成了404。原因是前端打包后的静态页面被Nginx托管,但API请求的路径仍然指向/api/xxx,Nginx没有把这个前缀反向代理到Django。
Nginx配置核心部分长这样:
server { listen 80; server_name your_domain_or_ip; root /path/to/vue_dist; index index.html; location / { try_files $uri $uri/ /index.html; } 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; } location /admin/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; } }Nginx还有一个坑:Vue使用history模式路由时,刷新某个二级页面会404。location /里的try_files $uri $uri/ /index.html就是用来解决这个问题的。如果你怕麻烦,本地开发可以直接用hash模式,但生产环境我建议用history模式配Nginx,URL干净不少。
5.5 PyCharm配置项目的小技巧
用PyCharm开发Django+Vue项目,有几个小设置能省很多事。
第一,虚拟环境。PyCharm创建项目时选择已有的虚拟环境,或者新建一个。后端的依赖全部通过requirements.txt管理,新机器上一条pip install -r requirements.txt就能装齐。
第二,运行配置。我习惯在PyCharm里加两个运行配置:一个是Django Server,跑8000端口;一个是npm脚本(dev),跑Vite前端服务。两个配置可以同时启动,修改代码热加载都很及时。
第三,断点调试。PyCharm专业版支持Django请求直接打断点,接口报错或者数据异常时,F8逐步走一遍,定位问题比看日志高效太多。
第四,数据库管理。PyCharm自带的数据库工具可以直接连项目里的SQLite文件,查看表数据、执行SQL都很方便。调试统计逻辑时我经常直接查表验证,少写很多临时脚本。
6. 部署上线与长期维护的一些补充建议
6.1 数据库选型:练习系统用SQLite还是MySQL
我在开发时先用SQLite,因为它零配置、文件式、适合开发调试。生产环境如果只是自己用或者小规模给备考小组用,SQLite完全够用。但如果是多人同时在线练习,建议迁移到MySQL或PostgreSQL。主要原因是SQLite并发写的能力有限,多人同时提交答卷时可能出现锁等待。如果只是偶尔几个人用,SQLite的真实瓶颈你大概率遇不到。
从SQLite迁移数据到MySQL,我直接用Django的dumpdata导出JSON,再在目标库上loaddata导入,数据模型不变的情况下非常省事。题目标题里含图片的话,需要用外链或base64存储,这要提前规划好。
6.2 备份与题库维护
练习系统的数据安全主要在题库和练习记录。题库是最核心的资产,必须定期备份。我写了一个导出命令,把全部启用状态下的题目按CSV格式导出,包括分类、题型、题干、选项、答案、解析、难度、来源。每个月手动跑一次导出,把CSV存到网盘或本地。
练习记录和答题明细这类用户数据,我用Django自带的后台按周导出汇总,主要看每周练习量和正确率变化趋势,用于了解用户群体的复习进度,也用于观察系统本身是否存在性能瓶颈。
6.3 部署方案的最终选择
我的线上方案是:一台普通服务器,Nginx托管前端静态文件,Gunicorn跑Django服务,SQLite数据库文件放在本地磁盘。系统本身并不吃资源,2核2G内存的服务器带几十个并发完全没问题。
部署步骤大致是:
- 服务器装好Python 3.10+、Node.js、Nginx
- 拉取项目代码,创建虚拟环境,安装后端依赖
- 前端
npm run build生成dist,上传到服务器Nginx root目录 - 后端用Gunicorn启动,绑定
127.0.0.1:8000 - Nginx配置好反向代理和静态文件路径,重启Nginx
- 首次用
python manage.py migrate初始化数据库,创建管理员账号,导入题库
整个过程跟着文档走,一套下来大约半小时。我在项目里还放了一个deploy.sh脚本,把前端打包、后端重启、Nginx reload这些操作合并成一条命令,日常改代码更新时一条命令就生效。
结语:这套系统下一步还能怎么扩展
项目做完了,但我个人体会最深的一点是:练习系统的难点从来不在写代码,而在把业务规则想透。答错进错题本、答对三次移出、按模块比例抽题、超时强制交卷、答案不回传列表接口——这些规则看起来都是小细节,但每一个都直接影响用户的练习体验。代码框架再新、技术栈再时髦,规则没想清楚,做出来也就是个花架子。
如果你也想做类似的东西,我建议不要一开始就追求功能大而全。先把题库导入、随机练习、判分、错题本这四个最小闭环跑通,再逐步加模拟考试、统计图表、用户系统。每一步都是完整可用的状态,越往后迭代越有底气。
最后分享一个小技巧:题库数据不要只放在数据库里,我习惯同时维护一份纯文本Markdown格式的题目备份。数据库表结构升级或者数据损坏时,纯文本资料反而最容易恢复。这个习惯帮我在一次数据库文件损坏的意外里救回了接近两千道题,也算是血泪教训换来的经验了。