☰
Python双框架实战:Django+Flask+Vue构建考公上岸交流平台
2026/10/9 13:09:28 网站建设 项目流程

先把结论放前面:这个项目的本质,是用 Python 生态里最主流的两个 Web 框架,在 pycharm 环境下搭一个“考公复习 + 上岸交流”一体化平台。django 负责重业务、重数据的主体功能,flask 负责轻量、独立的辅助接口,前端用 Vue 做交互界面。我按当时实际开发的顺序,把设计思路、核心功能拆解、手把手实现过程、踩坑记录全部写出来,希望你能少走几趟弯路。

我从一个很直接的场景说起。每年考公季,备考群里的资料、真题、经验帖全散落在聊天记录里,找人拼课、对答案、打卡监督都是零散的手工活。做一个在线交流平台,本质上就是把“刷题、模考、错题、打卡、经验分享”这几件事搬到线上,让考公人有一个可以沉淀内容和互相监督的根据地。这类项目也是高校毕设和项目实战的高频选题,因为它既有用户体系,又有内容模块,还有数据统计,能完整锻炼一个开发者从建模到部署的全链路能力。

当时我选择 django + flask 双框架,而不是只用其中一个,不是炫技,而是现实需求推着走的。django 自带 ORM、Admin 后台、认证体系、迁移机制,对付题库、帖子、评论、打卡这类强关系、高内聚的业务模块非常顺手。但项目里还有一些和主业务解耦的“小零件”——比如给用户做一个基于规则的学习建议接口,或者一个独立的课程播放鉴权服务,用 flask 开个轻量进程更干净,不污染 django 的主工程,开发和迭代互不干扰。Vue 则负责把 PC 端和移动端的网页交互做起来,用组件化方式复用刷题、评论区、错题本这些 UI 模块,联调时配合 axios 调用后端接口,整体是一套标准的前后端分离架构。

文章后面的内容我会按 5 个部分展开:先讲述整体设计和建模思路;再拆解核心模块的技术实现;然后给出完整的前后端联调实操记录;接着整理高频问题与排查技巧;最后聊聊部署时的性能优化和收尾建议。每个环节都有当时的现场记录和参数选择过程,可以直接搬到自己项目里验证。

1. 整体设计与技术选型拆解

1.1 为什么是“django 为主,flask 为辅”而不是二选一

很多人上来就想二选一,其实要看你手里是什么牌。django 的强势在“全家桶”式开发体验,你 create app 之后,模型、视图、模板、Admin、表单、认证都是配套好的,尤其是用它写内容型平台,基本等于自带脚手架。flask 的优势则是自由、轻量、启动快,适合做 API 微服务、爬虫结果回调、定时任务网关这类需要“短平快”的独立模块。

在这个考公交流平台里,我把两者分工成这样:

  • django 管理核心域:用户体系、题库、模考记录、错题本、帖子、评论、点赞收藏、学习打卡。
  • flask 承载辅助域:学习建议接口、每日提醒推送网关、文件上传的预检接口、简单的数据统计聚合。

这里有个很关键的实践心得:flask 不要硬塞进 django 的进程里跑。你做两个独立服务,django 跑 8000 端口,flask 跑 5000 端口,前端统一通过 nginx 或 devServer 的代理转发到对应服务,这样两个框架互不干扰,日志分开放,停机升级也彼此不影响。真要把 flask 挂进 django 里跑 WSGI 应用,反而会让路由和中间件逻辑乱成一锅粥。

1.2 Vue 在项目里的角色和版本考量

前端这块,我选用 Vue 3 + Vue Router + Pinia + Element Plus 的组合,原因非常实际:Vue 3 的组合式 API 适合把刷题逻辑、答题倒计时、答题统计这类有状态、有副作用的逻辑抽成独立 hook,复用性比选项 API 更强。Element Plus 的表格、表单、弹窗、轮播组件开箱即用,做后台管理和前台页面都能省不少功夫。

在 pycharm 里开发 Vue 项目,我记得最麻烦的就是环境配置。Node 版本建议锁定 16.x 或 18.x 的 LTS,装完 Node 后全局装 @vue/cli 或者直接用 create-vue 脚手架。当时我是在 pycharm 里新建 Vue 项目,然后手动把 axios、vue-router、pinia、element-plus 依赖加进去。如果你看到 “failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found” 这类报错,多半是 npm 安装没走完或者 tsconfig 引用路径不对,解决方法是把 package.json 里的依赖删除后重新 install,或者降低 @vue/tsconfig 的版本。

1.3 开发工具链:pycharm 的核心配置要点

pycharm 作为主力 IDE,不只是写代码,还要承担数据库可视化管理、虚拟环境配置、Git 集成、接口调试这些工作。我记得首次配置项目时,建议每个项目单独建虚拟环境,避免不同项目依赖冲突。专业版可以直接配置 Django 运行配置,把 host 设为 127.0.0.1、port 设为 8000,勾选 “Run browser” 后点击调试按钮就能一键启动。

pycharm 里还有一个很好用的功能是 Database 工具窗口,可以直接连 MySQL 或 SQLite,可执行 SQL、查看表结构、导出数据。我当时用 SQLite 起步,方便本地测试,后来迁到 MySQL 时直接用 django 的 dumpdata 导出再 loaddata 导入,比手工导 SQL 更省心。

2. 核心功能模块与数据库建模

2.1 业务模块全景拆解

一个考公交流平台,说大不大,但功能拆开也不少。我按用户动线把它分成四条线:

  • 学习线:题库刷题、模拟考试、答题统计、错题自动收集、知识点收藏。
  • 社交线:发帖提问、经验分享、评论回复、点赞、收藏帖子。
  • 监督线:每日学习打卡、连续打卡统计、学习计划配置。
  • 管理线:后台管理用户、审核帖子、维护题库、发布资料。

每条线之间是有数据关联的。比如用户答错一道题,会同时影响错题本、答题统计、学习计划进度;一个用户收藏的帖子,在个人中心要有聚合列表。所以在建模时不能只盯着单表,要把外键关系、多对多关系、索引设计都提前想清楚。

2.2 核心数据表设计详解

这里我给出实际项目的核心表结构,不是教科书式的全表,而是重点表的关键字段和取舍理由。

用户表(User):django 的 AbstractUser 扩展,额外加 real_name 用于实名展示、avatar 头像地址、school 或单位信息、exam_type 区分国考/省考/事业单位。用扩展而非新表,原因是 django 的认证逻辑可以直接复用,后续用 request.user 就能拿到扩展字段。

题库表(Question):字段包括 category 所属科目(行测、申论、公基)、question_type 单选/多选/判断/材料题、content 题目内容、options JSON 字段存选项、answer 标准答案、analysis 答案解析、difficulty 难度等级、source 来源(真题/模拟题)。options 用 JSONField 而不是关联表,是因为选项结构固定、不需要单独查询和索引,JSON 存取最方便。

模考记录表(ExamRecord):关联用户和一套试卷(Paper),记录总时长、开始时间、交卷时间、得分、正确率、答题明细(用 JSON 存储用户每题的答案和耗时)。明细存成 JSON 而不是独立表,是为了查询成绩单时能一次性返回,避免大量联表。

错题本表(WrongBook):唯一键是 user_id + question_id,记录错误次数、最近错误时间、掌握状态。这样同一个用户对同一道题只保留一条记录,重复答错就增加错误次数、更新掌握状态,而不是无限插入新行。

帖子与评论表(Post、Comment):Post 关联用户,字段包括 title、content、category(经验/求助/资料)、view_count、like_count、comment_count。Comment 用自关联外键支持二级回复,同时冗余一个 root_id 记录楼层归属,查询帖子评论区时只需过滤 root_id,不用递归找树。

打卡表(StudyCheckin):唯一键 user_id + date,记录当日学习时长、完成计划数。这张表会非常频繁地写入,务必给 user_id + date 建联合唯一索引。

表格对比各表核心字段:

表名关键字段索引设计核心操作
Userusername, exam_type, avatarusername 唯一注册、登录、资料维护
Questioncategory, question_type, difficultycategory+difficulty 联合索引按科目随机刷题、搜索
ExamRecorduser, paper, score, detailsuser+created 倒序索引成绩查询、排名
WrongBookuser, question, wrong_countuser 索引错题列表、标记掌握
Postuser, category, title, contentcategory+created 倒序索引社区内容流
Commentpost, user, root_idpost+created 索引楼层评论查询
StudyCheckinuser, date, durationuser+date 唯一打卡、连续统计

2.3 为什么 SQLite 起步、MySQL 上线

很多新手纠结数据库选型,我的建议是:开发阶段用 SQLite,部署阶段切 MySQL。原因是 SQLite 是文件型数据库,零配置、不用装服务、pycharm 直接打开 .db 文件就能看数据,非常适合开发调试。但到生产环境,面对并发写入和多进程访问,SQLite 的锁机制会成为瓶颈。切换时只需修改 django 的 DATABASES 配置,并确保模型里没有用 SQLite 专属字段,基本能做到无缝迁移。

当时我踩过一个坑:把 SQLite 的日期字段直接存成字符串,导致切换 MySQL 后日期比较出了诡异结果。原因就是模型里用了 CharField 存时间而不是 DateTimeField。统一用 DateTimeField 后,这个问题就消失了。

3. 前后端核心环节实现

3.1 django 后端项目搭建与模型实现

第一步是创建虚拟环境并安装依赖。在 pycharm 终端里依次执行:

python -m venv venv venv\Scripts\activate # Windows pip install django djangorestframework django-cors-headers pillow

django 项目创建命令:

django-admin startproject exam_platform cd exam_platform python manage.py startapp users python manage.py startapp questions python manage.py startapp community python manage.py startapp study

我把业务分四个 app,避免把所有模型塞在一个 app 里导致后期维护困难。在 settings.py 里注册 app 和 DRF、corsheaders 中间件。

用户模型的关键写法:

from django.contrib.auth.models import AbstractUser class User(AbstractUser): real_name = models.CharField(max_length=50, blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) exam_type = models.CharField(max_length=20, choices=( ('gk', '国考'), ('sk', '省考'), ('sy', '事业单位')), default='gk')

题库模型:

class Question(models.Model): category = models.CharField(max_length=20, choices=( ('xc', '行测'), ('sl', '申论'), ('gj', '公基'))) question_type = models.CharField(max_length=10, choices=( ('single', '单选'), ('multi', '多选'), ('judge', '判断'))) content = models.TextField() options = models.JSONField(default=dict) answer = models.CharField(max_length=10) analysis = models.TextField(blank=True) difficulty = models.IntegerField(default=3)

创建好后执行迁移命令,数据库表就自动生成了。

python manage.py makemigrations python manage.py migrate

3.2 用 DRF 写一套可复用的接口层

项目接口我统一用 DRF 的 ViewSet 加 ModelSerializer 实现,好处是列表、详情、创建、更新、删除的通用逻辑可以直接复用。比如错题本的接口:

serializers.py:

class WrongBookSerializer(serializers.ModelSerializer): question = QuestionSerializer(read_only=True) class Meta: model = WrongBook fields = ['id', 'question', 'wrong_count', 'mastered', 'updated_at']

views.py:

class WrongBookViewSet(viewsets.ModelViewSet): serializer_class = WrongBookSerializer def get_queryset(self): return WrongBook.objects.filter(user=self.request.user)

路由配置:

router.register('wrongbook', WrongBookViewSet)

这样 /api/wrongbook/ 列表、/api/wrongbook/{id}/ 详情等标准 REST 接口全部齐活。答题时用户可以 POST 到 /api/answer/,后端根据答案判断对错,同时更新错题本和答题统计。

3.3 flask 逻辑:独立小服务怎么和 django 共舞

flask 服务不干预 django 的数据模型,它通过 HTTP 方式访问 django 提供的内部接口或者直接读取同一份 MySQL 数据库。因为两个服务跑在不同的进程,flask 读取数据库需要独立的配置,但注意只用读操作,写操作尽量集中在 django 侧,避免双写冲突。

一个典型场景是“学习建议接口”。flask 接收用户的学习数据,返回一句话建议:

from flask import Flask, jsonify, request app = Flask(__name__) @app.route('/api/recommend', methods=['POST']) def recommend(): data = request.get_json() wrong_count = data.get('wrongCount', 0) today_minutes = data.get('todayMinutes', 0) if wrong_count > 10: advice = "建议先复习错题本,再开始新一轮刷题" elif today_minutes < 30: advice = "今天学习时长较短,建议完成至少一套专项练习" else: advice = "状态不错,可以进入模拟考试环节" return jsonify({"advice": advice})

启动 flask 服务用:

flask --app recommend_service run --port 5000

3.4 Vue 前端路由与页面结构

前端我设计了三个大页面模块:用户中心(登录注册、个人资料、学习统计)、刷题中心(题库列表、刷题页、错题本、模拟考试)、社区中心(帖子列表、帖子详情、发布帖子、评论)。Vue Router 的核心写法:

import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('./views/Home.vue') }, { path: '/questions', component: () => import('./views/QuestionList.vue') }, { path: '/question/:id', component: () => import('./views/QuestionDetail.vue') }, { path: '/wrongbook', component: () => import('./views/WrongBook.vue') }, { path: '/community', component: () => import('./views/Community.vue') }, { path: '/post/:id', component: () => import('./views/PostDetail.vue') }, { path: '/login', component: () => import('./views/Login.vue') }, ]

页面加载用懒加载,组件按需拆分。刷题页里我封装了 useQuestionHook,统一管理题目加载、选项选中、答案提交、倒计时:

export function useQuestionHook() { const currentQuestion = ref(null) const selectedOption = ref(null) const countdown = ref(60) async function loadQuestion(id) { const res = await axios.get(`/api/questions/${id}/`) currentQuestion.value = res.data } function submitAnswer() { // 调用后端判题接口,处理错题记录 } return { currentQuestion, selectedOption, countdown, loadQuestion, submitAnswer } }

3.5 前后端联调的关键:代理与 Token

前后端分离最烦的就是跨域。pycharm 里我启动 django 在 8000,flask 在 5000,Vue devServer 在 3000。开发环境通过 vite.config.js 配置代理解决跨域:

export default { server: { port: 3000, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true }, '/flask-api': { target: 'http://127.0.0.1:5000', changeOrigin: true, rewrite: path => path.replace(/^\/flask-api/, '/api') } } } }

同时 django 侧配置 django-cors-headers,允许 localhost:3000 访问。如果只配代理不配 CORS,浏览器有时仍会因为预检请求被拦截,两个都要配。

用户认证我采用的是 JWT 方案。登录成功后后端返回 access token 和 refresh token,前端把 access token 存到 Pinia 和 localStorage,axios 请求拦截器统一加 Authorization 头:

axios.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })

刷新 token 的逻辑稍微复杂,但一定要做,不然 token 过期后用户会被莫名登出。我在 axios 响应拦截器里判断 401,然后调用刷新接口换新 token,成功后重发原请求。

4. 高频问题与排查技巧实录

4.1 django 和 flask 端口冲突

最经典的问题:django 已经占用了 8000 端口,flask 又想去监听 8000 端口。解决方法是给两个服务分配不同端口,前端代理按路径区分。如果遇到端口被莫名占用,pycharm 的 Terminal 里执行:

netstat -ano | findstr :8000 taskkill /PID <pid> /F

开发时更规范的做法是固定端口配置,django 的 runserver 强制指定 8001,flask 配置 5000,避免和其他项目冲突。

4.2 Vue 安装依赖失败或版本不兼容

Vue 项目最让人头疼的是 node_modules 各种依赖错误。我遇到过 “failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found” 的报错,排查后确定是 package.json 里 @vue/tsconfig 版本太新,pnpm 安装时没有将其完整写入 node_modules。解决方式:

npm delete node_modules package-lock.json npm install vue@3.4 --save npm install @vue/tsconfig@0.4.0 --save-dev

经验是:不要一上来就装最新版本,生产项目稳定优先。element-plus 和 vue-router 的版本也要匹配 vue 主版本,否则会出现编译报错或运行时白屏。

4.3 图片上传和资料文件存取

考公平台会有用户上传头像、资料附件。django 侧我用 ImageField 和 FileField 存储文件,MEDIA_ROOT 指向项目根目录下的 media 文件夹。但部署后遇到一个问题:Nginx 直接托管静态文件时,媒体文件路径没对上,导致图片 404。正确配置是为 media 目录单独建 location 规则。

上传接口要注意请求大小限制。django 默认 DATA_UPLOAD_MAX_MEMORY_SIZE 是 2.5MB,超过这个值会出现 RequestDataTooBig 错误。做资料下载功能时,把限制调大并在前端做文件大小校验,双重保障。

DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB

4.4 时区问题导致打卡记录错乱

django 默认 USE_TZ=True,存储的是 UTC 时间。中国用户打卡时,如果直接把 datetime.now() 存进去,会和北京时间相差 8 小时,导致跨天打卡记录错乱。我在设置里把 TIME_ZONE 设为 'Asia/Shanghai',但 USE_TZ 保持 True,所有时间字段都用 timezone.now() 写入,前端展示时用 Vue 格式化。判断“今天是否已打卡”时,先将当前北京时间转成当天零点再查库。

import datetime from django.utils import timezone now = timezone.localtime(timezone.now()) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) checked = StudyCheckin.objects.filter(user=user, created_at__gte=today_start).exists()

4.5 题库接口响应慢与分页优化

当题库数据量到几万条,一次性返回全部题目会让前端卡死。我给列表接口加了 DRF 的分页器,每页 20 条,并按 category + difficulty 建联合索引。刷题页按题目 id 加载单题,不再拉整个题目列表。

答题统计接口因为涉及多个表的聚合计算,初期响应要 2 秒多。优化方式是把统计结果冗余到 UserProfile 表,每次答题后异步更新统计字段,查询时直接读冗余字段,响应降到 100 毫秒以内。这是典型的用空间换时间,在中小型项目里非常划算。

5. 部署上线的性能优化与收尾建议

5.1 用 waitress/gunicorn 托起 django,用 nginx 反向代理

开发时的 runserver 只适合调试,不适合生产。部署的时候,我用 waitress(Windows 环境推荐)或 gunicorn(Linux 环境)作为 WSGI 服务器,多 worker 并发处理请求。示例命令:

gunicorn exam_platform.wsgi:application -w 4 -b 127.0.0.1:8000

nginx 配置里把静态文件、媒体文件、API 接口都做反向代理,同时开启 Gzip 压缩。首屏加载的 JS、CSS 文件体积较大,Gzip 后能减少 70% 以上的传输量。

5.2 Vue 项目打包与部署路径坑

Vue 项目打包前一定要检查 vite.config.js 的 base 路径。如果部署在子路径下,比如 http://域名/exam/,就需要把 base 设为 '/exam/',否则静态资源全部 404。打包命令:

npm run build

生成的 dist 目录交给 nginx 托管。同时注意 history 路由模式下,用户在子页面刷新会得到 404,需要在 nginx 配置里加 try_files 回退到 index.html。

5.3 数据库备份与定时任务

考公平台内容价值高,数据备份不能省。我用 django-crontab 写了两个定时任务:每天凌晨备份数据库到指定目录,每周清理过期验证码和未激活用户。备份脚本直接调用 django 的 dumpdata 或直接 mysqldump,保留最近 7 天的备份文件,防止磁盘被占满。

5.4 项目拓展方向:从平台到生态

平台上线后,可以沿着两个方向拓展。一个是内容智能推荐,根据错题本和刷题记录,给用户推荐弱项专项题;另一个是在线视频课程模块,接入 m3u8 格式课程视频播放,结合用户答题数据形成学习闭环。这两个方向都有现成的开源方案,Vue 端可以用 hls.js 或 video.js 解决播放,后端用 flask 做推荐规则引擎,扩展起来很顺。

我个人在实际开发中体会最深的一点:不要一上来就追求微服务、追求大而全的架构,先把 django 这一个核心做扎实,flask 只负责真正独立的小模块就好。遇到前后端联调报错,先从前端代理、后端 CORS、token 这三个方向排查,90% 的问题都能解决。这个项目做完后,你会对 Python Web 全栈开发有非常完整的认知——从建模、写接口、联调、部署到优化,整条链路都能自己独立走通,这对就业和实战都是实打实的能力积累。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询