做这类“复习备考平台”项目最怕的不是功能写不出来而是技术栈没想清楚就动手。我复盘的这个项目标题里同时挂着Flask、Django、Vue和PyCharm第一眼看上去像是技术杂烩实际上是一个很典型的“双后端前端分离”落地案例。项目面向前端开发者和Python全栈方向的同学场景是给应届生提供公务员考试题库练习、模拟考试、错题回顾、学习计划跟踪和学情分析。这篇文章会把选型原因、数据库设计、Django与Flask各自负责什么、Vue页面怎么对接、上线踩了哪些坑完整过一遍给正在做考试类系统或前后端分离项目的朋友做个参考。1. 公务员应届生备考平台的技术选型复盘为什么是FlaskDjangoVue1.1 平台要解决的备考场景痛点应届生备考公务员最典型的痛点不是“没有题目”而是“题目太多、太散不知道从哪刷起”。很多人买了一堆资料今天看行测明天翻申论最后什么都没吃透。所以这个平台在设计之初就确定了三个核心目标第一把题目按考试类别、章节、知识点分层组织让刷题有路径第二通过答题记录自动生成错题本让复习有反馈第三通过统计数据识别薄弱章节让备考有优先级。围绕这三个目标功能上拆成了几个大模块题库管理、在线刷题、模拟考试、错题本、学习计划、个人中心外加一个学情分析面板。用户角色也很清楚学生端是日常刷题和看数据管理端是维护题库和查看整体学习情况。考虑到时间成本我建议项目第一期重点做学生端管理端只要能维护题目即可别一上来就把后台做成“功能大而全”的重型系统。这个定位直接影响后面的技术选型。因为核心是“题目做题记录统计分析”所以后端需要成熟的ORM、权限体系和快速开发能力而“学情分析”这种服务比较独立适合拆成一个小服务单独跑。于是就有了那套争议很大的组合Django管主系统Flask跑分析服务Vue做前端PyCharm统一开发调试。1.2 Flask与Django同台按边界分工而不重复造轮子先聊一个很多人会问的问题一个项目里同时出现Flask和Django是不是为了炫技真不是。Django是一个“全家桶”式框架自带了ORM、Admin后台、认证体系、表单处理、迁移工具。做题库管理、用户体系、答题记录这种强业务逻辑的模块Django能省掉大量重复劳动。尤其是Admin后台稍微配几行代码就能让管理员维护题目对这类内容后台需求多的项目非常友好。Flask则是一个“轻量级微框架”路由简单、启动快、依赖少。它在这个项目里承担的是一个相对独立的任务根据考生最近的答题记录生成薄弱点分析和复习建议。这个功能不需要Django的ORM也不需要Admin后台它只需要接收一段JSON数据经过计算后返回一段建议文本。把这种低频的、计算逻辑独立的接口塞进Django里也能做但会污染主工程的目录结构也会拖累主服务启动。所以我的划分原则是强业务逻辑、需要数据建模的部分全部交给Django轻量、可独立部署的服务用Flask单独起一个进程。两边通过HTTP接口通信互不干扰。回头看这个决策在后期联调时帮了大忙。Django那边频繁改模型、跑迁移Flask服务完全不受影响反过来Flask接口调整算法逻辑时也不会导致整个平台重启。如果你把一个分析功能硬塞进Django每次改动都要走一遍完整测试流程成本高得多。1.3 PyCharm下搭建项目骨架与虚拟环境开发工具选择PyCharm主要看重的是它的调试体验和代码提示。项目结构上我用了典型的“一主一辅”布局主目录是Django项目里面有一个独立目录放Flask服务。结构大致是这样的exam_platform/ ├── config/ # Django主配置 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ # Django业务应用 │ ├── users/ │ ├── question_bank/ │ └── study/ ├── analyzer/ # Flask独立服务 │ ├── app.py │ └── requirements.txt ├── frontend/ # Vue前端工程 │ └── src/ ├── manage.py └── requirements.txt第一步是创建虚拟环境。为了避免全局pip污染我推荐直接用PyCharm内置的Virtualenv功能。选中Python 3.9解释器然后逐个安装依赖pip install django djangorestframework django-cors-headers flask requests gunicorn把Django项目初始化出来之后前端我用的是Vue 3加Vite直接初始化和PyCharm里的后端目录并列。Vite比Webpack快很多尤其是在项目中期频繁刷新页面时热更新体验差距非常大。还有一个细节建议从一开始就要把数据库连接信息、调试开关、跨域白名单写进配置文件的本地版本里不要提交到版本库。这个项目后面联调时我用一个本地config副本节省了大量环境切换时间。2. 从需求到表结构核心业务模块与数据库建模2.1 核心业务模块题库、刷题、模拟考、错题本、学习计划数据库设计是这类平台最关键的部分。题目表设计得不好后面做随机组卷、错题统计、成绩分析都会很痛苦。我先把核心实体理了一遍用户、知识点、题目、试卷、答题记录、错题本、学习计划。用户表直接继承Django的AbstractUser这样可以省去重写登录认证的功夫只需要增加几个业务字段考试日期、目标分数、性别、所在地区等。知识点表用自关联结构支持二级甚至三级分类这样题目既能挂在“行测-言语理解”这种大类下也能精确定位到“逻辑填空-语境分析”这种细节点。题目表是所有模块的地基。字段包括题干、选项、答案、解析、题型、难度、知识点外键、来源。其中选项和答案我用的是JSONField因为选择题的选项数量不固定某些题还有多选或不定项选择JSON存储让后端代码更灵活。答题记录表解决的是“考生做过什么、做得怎么样”的问题。每次刷题提交后都会插入一条记录记录题目ID、考生ID、是否答对、所选答案、用时等。错题本不是一张独立的静态表而是基于答题记录动态生成的。什么时候把某道题放进错题本、什么时候算“已掌握”都可以由这套元数据计算出来。学习计划表相对简单就是给用户按天生成任务关联知识点、题目数量、完成状态。数据看板的统计结果也是基于这些表的聚合查询得出的。2.2 Django ORM模型的关键实现我把几个核心模型写出来大家可以直接对照落地from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): GENDER_CHOICES ( (male, 男), (female, 女), ) gender models.CharField(max_length10, choicesGENDER_CHOICES, blankTrue) exam_date models.DateField(nullTrue, blankTrue) target_score models.IntegerField(default0) class KnowledgePoint(models.Model): name models.CharField(max_length100) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE) class Question(models.Model): TYPE_CHOICES ( (single, 单选题), (multiple, 多选题), (judge, 判断题), (case, 案例分析), ) DIFFICULTY_CHOICES ( (easy, 简单), (mid, 中等), (hard, 困难), ) content models.TextField() options models.JSONField(defaultdict) answer models.JSONField() analysis models.TextField() qtype models.CharField(max_length10, choicesTYPE_CHOICES) point models.ForeignKey(KnowledgePoint, on_deletemodels.PROTECT) difficulty models.CharField(max_length10, choicesDIFFICULTY_CHOICES, defaultmid) source models.CharField(max_length100, blankTrue) class AnswerRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) question models.ForeignKey(Question, on_deletemodels.CASCADE) selected models.JSONField() is_correct models.BooleanField() cost_time models.IntegerField(help_text用时单位秒) created_at models.DateTimeField(auto_now_addTrue) class StudyPlan(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) date models.DateField() point models.ForeignKey(KnowledgePoint, nullTrue, on_deletemodels.SET_NULL) question_count models.IntegerField(default20) is_finished models.BooleanField(defaultFalse)几个字段上的取舍值得说一下。答案字段也用JSON是为了支持多选题和判断、案例混合题型题干如果放HTML可以存富文本但安全上要注意转义避免XSS风险。options用defaultdict保证Django迁移时不会有空值报错。2.3 建模时容易踩的坑题目重复、答案存储、统计性能第一个坑是题目重复。题库录入时很容易因为复制粘贴造成同一道题存在多份。我在模型上给content加了唯一约束同时在管理端加了一个“查重”入口。从数据源导入题目时也做了内容哈希比对。第二个坑是答案校验的位置。用户在刷题时选了A、B后端接收到的数据是[A,B]。校验逻辑尽量在序列化层做不要让前端先算好对错再传回来。等于是后端根据正确答案重新比较一次防止有人直接改请求体刷成绩。第三个坑是统计性能。答题记录表一旦数据量上来按用户聚合会变慢。我的做法不是实时去全表统计而是用户每次做完一组题后把知识点正确率增量更新到用户知识点统计表里。这样打开数据看板时只需要查少量汇总行而不是扫描上万条答题记录。这个设计在最开始就要想好不然后期加汇总表会面临历史数据回填的麻烦。3. 接口层实现Django支撑核心APIFlask跑独立学习分析服务3.1 Django REST framework认证、权限与公共接口接口层我用Django REST framework实现。JWT认证用djangorestframework-simplejwt登录后前端拿token请求受保护资源。settings里需要配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }然后把跨域配置好。后端装django-cors-headers在settings里加入APP并配置中间件INSTALLED_APPS [ corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]我写接口时会尽量用ViewSet而不是一个方法写一个函数视图。ViewSet配合Router可以自动生成标准REST风格的路由代码量少很多。以刷题接口为例核心是“按知识点取随机题”。这个接口不接收前端传入的“题号列表”而是接收知识点ID和数量由后端做随机抽取class QuestionViewSet(viewsets.ReadOnlyModelViewSet): serializer_class QuestionSerializer def get_queryset(self): point_id self.request.query_params.get(point_id) if point_id: return Question.objects.filter(point_idpoint_id) return Question.objects.all() action(detailFalse, methods[get]) def random_paper(self, request): point_id request.query_params.get(point_id) count int(request.query_params.get(count, 10)) qs Question.objects.filter(point_idpoint_id) selected random.sample(list(qs), min(count, qs.count())) return Response(self.get_serializer(selected, manyTrue).data)这样前端只需要拿到一组题目渲染即可不需要关心组卷逻辑。3.2 刷题与成绩分析接口的关键细节提交答题记录不能一条条插库性能太差。我设计成批量提交一次传一个题目列表里面每一项包含题目ID、所选答案、用时后端统一循环校验、计算正确率并更新错题本和知识点统计。实现大致如下class AnswerRecordViewSet(viewsets.ModelViewSet): serializer_class AnswerRecordSerializer def create(self, request): data request.data.get(answers, []) results [] for item in data: q Question.objects.get(iditem[question_id]) correct self._check_answer(q, item[selected]) record AnswerRecord.objects.create( userrequest.user, questionq, selecteditem[selected], is_correctcorrect, cost_timeitem.get(cost_time, 0), ) results.append({question_id: q.id, is_correct: correct, correct_answer: q.answer}) self._update_user_point_stats(request.user, results) return Response({results: results}, status201)这里有个容易被忽略的点错题本不是每次答错就机械加入。如果同一道题错第三次我会把它的优先级调高而不是再塞一条重复记录。错题本表的字段可以设置成“最近错误时间”和“错误次数”页面展示时按这两个字段排序真正把错题本变成“复习队列”。3.3 Flask分析服务怎么和Django主系统联合协作Flask在整个项目里扮演“学情分析师”。Django提供聚合好的答题统计数据Flask基于这个数据计算薄弱知识点并返回复习建议文本。例如Flask接口from flask import Flask, request, jsonify app Flask(__name__) app.route(/analyze, methods[POST]) def analyze(): data request.get_json() stats data.get(stats, []) if not stats: return jsonify({suggestion: 暂无足够做题数据请先完成基础练习}) weakest min(stats, keylambda x: x[accuracy]) strongest max(stats, keylambda x: x[accuracy]) return jsonify({ weak_point: weakest[point_name], accuracy: weakest[accuracy], strong_point: strongest[point_name], suggestion: ( f你的薄弱点是{weakest[point_name]}正确率仅{weakest[accuracy]:.0%}。 f建议本周优先安排该知识点每日20题专项训练。 ) }) if __name__ __main__: app.run(port5000)Django这边通过requests库调用它def get_study_suggestion(request): uid request.user.id stats get_point_accuracy(uid) # 内部聚合逻辑 try: resp requests.post(http://127.0.0.1:5000/analyze, json{stats: stats}, timeout3) return JsonResponse(resp.json()) except requests.exceptions.RequestException: return JsonResponse({suggestion: 分析服务暂时不可用})它独立进程跑Django挂了它还能单独测试Flask挂了也不影响主体业务前端收到提示后会降级显示“暂无分析结果”。看起来多了一层服务但实际部署时就是多一个进程而已收益远大于成本。4. Vue前端联调实录登录页、刷题页、数据看板4.1 Vue工程初始化与请求层封装前端用Vue 3 Vite Pinia Element Plus。初始化命令是npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios pinia element-plus echarts开发过程中最大的痛点是跨域。本地前端端口是5173后端Django端口是8000。直接在axios里写http://localhost:8000/api浏览器会报CORS错误。我用的是Vite代理配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样前端请求/api/xxx时Vite服务器会转发到8000端口浏览器完全感知不到跨域。axios请求封装统一放在request.js里import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) config.headers.Authorization Bearer ${token} return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token) window.location.href /login } return Promise.reject(error) } )权限控制放在路由守卫里没有token就跳到登录页。登录成功后缓存用户信息和token刷新页面也能保持会话。4.2 刷题页面核心交互与答题回传刷题页是整个平台的核心页面交互上要求“快速作答、即时反馈、并不打断刷题节奏”。组件结构我分成三个部分题目渲染区、答题卡、结果弹窗。题目渲染区根据题型动态渲染单选题用单选按钮多选题用复选框判断题用两个大按钮案例分析用textarea。刷题流程是这样的用户从知识点列表进入刷题页。页面加载时请求随机题组接口拿到题目数组。每次做完一题点击“下一题”前端把本题答案暂存在一个数组中。做完一组后点击“提交本轮”前端把所有暂存答案一次性POST到后端。用暂存而不是逐题提交好处是用户中途可以回退修改答案。数据上报时附带每道题的耗时这个字段对后面学情分析很有用。代码核心片段function nextQuestion(selectedAnswer, timeUsed) { answerList.value.push({ question_id: currentQuestion.value.id, selected: selectedAnswer, cost_time: timeUsed }) if (currentIndex.value questionList.value.length - 1) { currentIndex.value timer.start() } else { submitRound() } } async function submitRound() { const res await answerApi.submit({ answers: answerList.value }) // 根据返回结果显示正确率弹窗 showResult.value true resultData.value res.results // 请求最新错题本 await loadMistakeList() }正确率弹窗里不仅仅是“答对多少题”还会列出每道错题的正确答案和解析并提供“加入错题重练”的按钮。4.3 ECharts数据看板与跨域联调数据看板是给用户“看到努力成果”的地方也是学情服务价值最直观的出口。我用ECharts做三个可视化模块每题正确率趋势折线图、各知识点正确率雷达图、每日刷题数量柱状图。雷达图的数据来源是知识点正确率聚合。前端请求/api/study/radar拿到的数据格式大致是[ {name: 言语理解, accuracy: 0.72}, {name: 判断推理, accuracy: 0.64}, {name: 数量关系, accuracy: 0.38} ]前端渲染雷达图const radarOption computed(() ({ radar: { indicator: radarData.value.map(item ({ name: item.name, max: 100 })) }, series: [{ type: radar, data: [{ value: radarData.value.map(item Math.round(item.accuracy * 100)) }] }] }))看板页面还有一个“AI复习建议”卡片它调用的就是前文写的Django接口Django再去请求Flask分析服务。调通这个联动链路后整个项目的核心价值就闭环了刷题产生数据数据驱动分析分析反哺复习计划。联调时最容易出的问题就是数据格式不一致。Django返回的是蛇形命名snake_case前端如果习惯用驼峰命名的接口常会出现字段对不上的问题。我的建议是统一全部用蛇形命名前端代码里也保持蛇形省去一层转换逻辑。5. 上线部署与常见问题排查实录5.1 本地到生产nginxgunicornDjangoFlask部署流程部署部分我踩的坑比开发部分还多。最开始只会在本地跑runserver上线时发现runserver是单进程开发服务器稍微多点并发就卡。Django侧的部署命令建议用gunicornpip install gunicorn python manage.py collectstatic --noinput gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000Flask作为独立服务也用gunicorn单跑一个进程gunicorn -w 2 -b 127.0.0.1:5000 analyzer.app:appNginx负责前端静态文件和反向代理server { listen 80; server_name your_domain; root /opt/exam_platform/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有一个容易忽略的问题Flask服务只允许本机访问就够了不要暴露公网端口。Nginx不需要对外开5000端口Django内部通过127.0.0.1:5000访问即可。安全边界上一定要收敛。部署完成后记得测试三条链路登录是否正常、刷题提交是否正常、看板能否拿到Flask分析结果。只要有一条链路不通优先看对应进程日志。5.2 开发期高频报错速查表我整理了项目开发过程中出现频率最高的几个问题和解决办法可以直接当作速查表用现象原因处理方式前端请求/api返回404Vite代理没生效或者后端路由前缀不匹配检查vite.config.js代理配置确认Django的urls里包含api路径登录成功但刷题接口返回401JWT token没放在Authorization请求头里在axios请求拦截器里统一设置token提交答题记录后错题本没变化错题本更新逻辑放在事务外的异步任务里被吞掉检查更新逻辑是否成功执行建议先同步更新再异步统计随机组卷接口返回重复题目没有做去重或者sample方法传的count大于总量抽题前先去重数量超限时做截断数据看板接口响应慢查询没有走索引或者N1查询严重使用select_related和prefetch_related提前加载外键Flask分析接口超时分析逻辑处理慢或网络隔离问题增加requests超时时间并把分析服务改成异步任务上传图片或资料在Nginx下404没配置media目录的alias在Nginx配置中增加media路由映射Vue的热更新经常失效项目文件权限或Node版本过高用稳定的Node LTS版本清理依赖缓存这些问题都不是什么高深技术但每一个卡住都够折腾两三个小时。如果你是自己一个人开发整套系统建议把日志从第一天就打印规范Django用模块级loggerFlask用统一的请求日志前端在axios拦截器里记录请求路径和响应状态。这样排查问题时不用到处翻。5.3 性能优化、安全加固与后续扩展思路性能层面我最有体感的优化是给项目加缓存。题库是读多写少的数据每次进刷题页都查数据库很浪费。我用Redis缓存了热点知识点的题目列表缓存键设计成question:list:{point_id}:{difficulty}组卷时先查缓存没有再查库回填。安全层面Django的settings里要做几件事把SECRET_KEY从代码里抽出来放到环境变量Debug关掉ALLOWED_HOSTS限定实际域名管理后台路径不要用默认的/admin换成自定义路径上传文件强制校验类型。还有一点用户提交的内容如果是富文本一定要做转义处理否则会埋XSS隐患。这个项目后续的可扩展空间其实挺大。现在题库刷题、错题本、看板已经跑通接下来可以加模拟考试计时模式、申论在线批改、社区经验分享、按用户画像推送题目。核心表设计已经留好了扩展位知识点继续往深做细分不会影响现有结构。整个平台做完后我最大的体会是不要害怕在一个项目里用多个框架关键是边界要清楚。Django管业务、Flask管独立分析、Vue管交互每个组件都只解决一类问题而不是所有逻辑都堆在一个工程里。技术上所有复杂的东西拆开后都不复杂复杂的是那些想清楚却不敢动手的决策。希望这篇复盘能让你在做类似考试平台或复习系统时少走几天弯路。