
1. 项目整体设计与技术选型思路拆解花了两周时间给备考公务员的朋友做了一套在线练习系统。后端是Python核心用Django统计模块用Flask前端用了Vue开发环境全程在PyCharm里完成。这套系统从题库管理、随机抽题练习、答题判分到错题收集和学习报告生成基本覆盖了日常刷题的完整流程。如果你是Python刚入门想找一个能真正跑起来练手的Web项目或者已经在用Django想看看怎么把Flask小服务整合进来同时用Vue做一套交互还不错的页面这篇文章都值得看完。我会把项目从头拆到尾把选型理由、关键代码、实操遇到的坑一并整理出来。1.1 需求拆解练习系统需要落地哪些功能开发前先列需求清单这一点比写代码更重要。公务员考试练习系统不是单纯CRUD核心闭环是题库管理、随机抽题、在线答题、自动判分、错题统计、学习报告。把这个闭环走通系统才算可用。每个模块都有细节题库要支持单选、多选、判断练习要支持按科目、按难度筛选答题要有倒计时和交卷错题要进入错题本统计数据要能生成可视化图表。如果一开始就把全部功能都设计进去项目会失控。我建议按“最小可用闭环”来拆分先做完核心功能再迭代。比如第一期只做行政职业能力测验中数量关系和判断推理两类题目跑通之后再扩大到全科目。这个系统的用户实际上有两类一是考生需要顺畅的刷题体验二是内容管理者需要批量维护题库、查看错题数据。需求拆分时我刻意把“用户端”和“管理端”分开。用户端就是Vue单页管理端直接用Django自带Admin。这样可以节省大量重复开发时间也方便非技术运营人员录入题目。第一版上线后朋友导入1200道题全程没有写一行后台代码靠的就是Django Admin自带的批量导入和筛选功能。1.2 后端选型不是“要么Django要么Flask”而是组合使用我在项目标题里把“django-flask”写在一起并不是为了显得花哨而是因为实际项目确实用到了两个框架。主后端选择Django理由很直接Django自带Admin后台、认证系统、强大的ORM做题库管理这类后台管理特别省事。比如题目录入我不用额外写一套管理界面直接用Django Admin就能录入、编辑、批量导入题目。Django的开发体验是“脚手架已经帮你把屋顶盖好了”适合快速搭业务系统。Flask则非常轻适合做单一职责的小服务。在这个项目里学习报告接口正确率、刷题量、知识点掌握度用Flask实现更清爽因为它只需要读库、返回JSON不需要Django那套中间件、模板、权限。两个框架可以共存底层是同一个MySQL数据库Django跑8000端口的业务APIFlask跑5000端口的统计API前端Vue按需请求不同服务。这种组合在Python Web开发中并不少见属于“按需选框架”的务实做法。有人担心两套框架维护成本高实际上只要目录结构划分清楚、职责边界明确维护成本并不会增加多少。真正要避免的是在一个框架里硬塞所有功能比如用Django写一个轻量的数据聚合接口代码量会大很多或者用Flask硬写用户登录、权限、后台管理那才是灾难。1.3 为什么前端用Vue而不是继续用Django模板第一版曾想直接用Django模板加Bootstrap但做到答题交互时发现痛点实时剩余时间、题目切换动画、答案选中状态、提交后立刻显示统计图表这些用服务端模板会频繁刷新页面体验很差。Vue正好在组件化、状态管理上有天然优势它能把“答题页”当做一个单页应用顶部计时器、中间题干、底部选项和进度条每一部分都是独立组件状态变化由Vue统一管理。配合Vue Router多道题目之间的切换、错题本和统计页的跳转都非常自然。PyCharm专业版对Vue有较好的支持代码提示和调试面板都能用所以整个前端工程我直接放在PyCharm里维护。还有一个细节是Vue的构建产物是纯静态文件可以交给Nginx托管后端只需要提供API。这样后续即使要接小程序或移动端接口可以直接复用不需要重写业务逻辑。2. 后端核心细节解析与实操要点2.1 PyCharm里的Django项目初始化先说环境准备Python 3.10以上PyCharm 2023年以后的版本社区版也够用但专业版对Django和Vue集成都更好。打开PyCharm新建一个纯Python项目然后用虚拟环境安装依赖。我的操作步骤python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate pip install django djangorestframework mysqlclient flask flask-cors pymysql创建Django项目和应用django-admin startproject exam_system cd exam_system python manage.py startapp question_bank python manage.py startapp user_center这里需要解释一下为什么要分两个应用。question_bank负责题库、练习、错题user_center负责用户和登录状态职责拆开后面维护舒服。然后在settings.py里注册应用配置数据库连接执行迁移。有一点必须提醒PyCharm里最好手动配置Run Configuration把manage.py设为启动脚本参数填runserver否则你很容易在终端里来回切换虚拟环境还容易把解释器指错。初始化时有一个我自己踩过几次的坑新建项目后直接运行python manage.py runserver结果报django.core.exceptions.ImproperlyConfigured原因是没有设置DJANGO_SETTINGS_MODULE。PyCharm在Run Configuration里会自动帮你设置但如果你是在终端直接跑需要先export DJANGO_SETTINGS_MODULEexam_system.settings或者用python manage.py runserver时确保当前目录在manage.py所在层级。所以我的建议是环境变量统一交给PyCharm管理不要手动在终端折腾。2.2 题库与练习记录的数据模型设计数据模型是整个系统的地基。我设计了精简版模型核心字段如下from django.db import models class Question(models.Model): QUESTION_TYPE ( (single, 单选题), (multi, 多选题), (judge, 判断题), ) type models.CharField(max_length10, choicesQUESTION_TYPE, verbose_name题型) category models.CharField(max_length50, verbose_name科目分类) content models.TextField(verbose_name题干) options models.JSONField(verbose_name选项JSON) # [{key: A, text: ...}] answer models.JSONField(verbose_name正确答案) analysis models.TextField(blankTrue, verbose_name解析) difficulty models.IntegerField(default3, verbose_name难度1-5) is_active models.BooleanField(defaultTrue, verbose_name是否启用) created_at models.DateTimeField(auto_now_addTrue) class ExamRecord(models.Model): user models.ForeignKey(auth.User, on_deletemodels.CASCADE, verbose_name用户) question models.ForeignKey(Question, on_deletemodels.CASCADE, verbose_name题目) user_answer models.JSONField(verbose_name用户答案) is_correct models.BooleanField(verbose_name是否答对) submit_time models.DateTimeField(auto_now_addTrue) class WrongBook(models.Model): user models.ForeignKey(auth.User, on_deletemodels.CASCADE, verbose_name用户) question models.ForeignKey(Question, on_deletemodels.CASCADE, verbose_name题目) wrong_count models.IntegerField(default1, verbose_name错误次数) last_wrong_time models.DateTimeField(auto_nowTrue)设计时的几个考虑选项和答案用JSONField而不是单独建选项表。因为题目选项结构固定用JSON最简单查询的时候不需要额外关联表。正确答案为什么也是JSON因为多选题的答案是多个用列表方便对比。JSONField在MySQL和PostgreSQL里都支持如果你用的是SQLite开发Django同样能处理只是底层转换而已。答题记录用外键关联到用户和题目以后统计每个人每道题的对错情况、计算正确率都很方便。外键的on_delete参数一定要想清楚题目删除后答题记录要不要保留这里考虑到历史统计需要我一开始选了CASCADE后来发现删一道题会连带删除所有答题记录这在统计报告里会丢数据。所以我在实际版本里给Question加了is_active字段做软删除不硬删数据。这样错题本里即使题目已经停用历史记录依然在。2.3 ORM查询与删除对象的那些坑Django ORM用起来顺手但新手最容易在“删除对象”上踩坑。我梳理几个典型场景单个对象删除question.delete()走的是Model.delete()会触发级联删除返回一个元组包含总删除行数和各类型删除行数。查询集批量删除Question.objects.filter(category常识).delete()走的是SQL级DELETE不会调用Model.delete()所以不会触发模型里重写的save或delete方法但会清理外键关联。带条件的删除先qs Question.objects.filter(difficulty__gt4, is_activeTrue)然后qs.delete()。清空全表但保留自增Question.objects.all().delete()。我建议在管理后台或视图里删除前先打印将要删除的数量避免误删。示例qs ExamRecord.objects.filter(submit_time__lt2024-01-01) print(f准备清理 {qs.count()} 条过期记录) # 备份后再删 # qs.delete()另外一个高频问题是查询优化懒加载导致的N1。比如循环展示答题记录时如果没加select_relatedDjango会在访问每条记录的外键时再发一次SQL100条记录就是101次查询。加上一行records ExamRecord.objects.filter(userrequest.user).select_related(question)压力立刻小很多。后面常见问题部分会再展开。3. Flask辅助服务学习统计接口的轻量实现3.1 用Flask拆出统计接口的动机考试系统除了日常刷题还需要一个“学习报告”页面展示最近7天刷题数、各科目正确率、错题数量走势。这类数据聚合查询其实不难但我发现写在Django里很别扭——要配置额外的序列化、URL路由、视图装饰器还要担心和业务代码混在一起。Django是重框架更适合承载完整业务逻辑。而Flask天生就是“微”的对于几个只读统计接口代码量只有Django的三分之一。所以我单独建了一个flask_report目录里面只放统计相关的路由和数据库连接代码独立进程跑在另一个端口。这种做法还有一个额外好处统计接口的压力和业务接口是隔离的。如果将来学习报告功能需要单独扩容可以直接把Flask服务部署到另一台实例而不影响做题服务。3.2 统计API怎么写最省事用Flask写统计API只做三件事连数据库、写SQL或ORM查询、返回JSON。这里直接以MySQL为例用PyMySQL连接同一个数据库from flask import Flask, jsonify import pymysql app Flask(__name__) def get_conn(): return pymysql.connect( host127.0.0.1, userroot, passwordyourpassword, databaseexam_system, charsetutf8mb4 ) app.route(/api/report/daily_count) def daily_count(): conn get_conn() cur conn.cursor() cur.execute( SELECT DATE(submit_time) AS d, COUNT(*) AS cnt FROM exam_system_examrecord WHERE submit_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(submit_time) ) rows cur.fetchall() cur.close() conn.close() return jsonify([{date: str(r[0]), count: r[1]} for r in rows])如果数据量大这种直接SQL比Django ORM的聚合更直白、可控必要时还能加索引。Flask路由写完后在PyCharm里为flask_report/app.py单独配置一个Run Configuration运行在5000端口。启动后打开http://127.0.0.1:5000/api/report/daily_count就能验证。注意这里就不要开Flask自带的debugTrue了生产环境一定用gunicorn或uwsgi跑不然并发一上来直接崩。统计接口里我还会加一个简单的缓存比如同一个用户当天只刷一次同样的统计查询用字典在进程内存里缓存十分钟避免每次访问都跑聚合SQL。Flask进程通常只有一个worker所以进程内缓存是安全的如果后面用了多worker可以换成Redis。3.3 两个框架一起部署时的内存和端口规划很多人一听到“项目里两套Python框架”就觉得重其实讲清楚就不焦虑。Django和Flask都是Python WSGI应用可以用同一个虚拟环境也可以各用各的venv生产环境用gunicorn分别拉起两个进程各监听不同端口Nginx做反向代理和路由转发。我的推荐部署结构客户端 - Nginx(80/443) /api/report/* - Flask(127.0.0.1:5000) /api/* - Django(127.0.0.1:8000) /static/* - Vue构建产物内存方面Django进程大概占80MBFlask大概占30MB一个小型云服务器2G内存完全够。这里有一个容易踩的坑两个服务连接同一个MySQL连接池参数要合理别在每次请求里反复建连接。写Flask统计接口时我用了一个简单的连接复用避免高并发时把数据库连接打满。部署后还要检查CORS前端Vue开发时请求两个端口必须在后端都配置允许跨域。因为开发环境我用了Vite代理生产环境Nginx做了转发所以CORS的坑主要出现在直接访问后端IP调试的时候。建议后端全部加flask-cors和Django的django-cors-headers省去很多联调摩擦。4. Vue前端搭建与考试练习页的完整实现4.1 Vue环境配置与PyCharm联动前端部分我用的是Vue 3加Vite项目管理在PyCharm同一窗口下。先扎扎实实把Vue环境配好npm create vuelatest exam_web cd exam_web npm install npm install axios vue-router pinia npm run devVite把开发服务器跑在5173端口PyCharm需要安装Vue.js插件打开项目后会自动识别package.json可以通过npm脚本一键启动。再提醒一句Node.js版本最好用18以上不然Vite4或Vite5会报兼容性错误。Vue环境配置常见的报错是“vue不是内部或外部命令”这就是没有把node_modules/.bin加入PATH或者没有用npx。我建议所有命令都从PyCharm的Terminal里执行它会自动继承项目环境。如果你用的是PyCharm社区版缺少Vue插件也不用担心。代码提示会弱一些但开发流程不受影响。至少要确保Terminal可执行npm命令这是Vue项目跑起来的前提。Vue工程创建后可以把前后端两个目录放在同一个PyCharm窗口里也可以分开两个窗口。我推荐放在一起因为调试时你能同时看到前后端日志定位问题更快。4.2 路由、请求封装与核心页面Vue路由用vue-router管理。我的路由设计import { createRouter, createWebHistory } from vue-router import ExamPage from /views/ExamPage.vue import WrongBook from /views/WrongBook.vue import ReportPage from /views/ReportPage.vue const routes [ { path: /, component: ExamPage }, { path: /wrong-book, component: WrongBook }, { path: /report, component: ReportPage }, ] const router createRouter({ history: createWebHistory(), routes }) export default router请求封装使用axios实例统一处理baseURL和tokenimport axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) export default request核心页面ExamPage要拆成三块顶栏计时组件、题干组件、选项列表组件。答题区用v-model绑定当前选中答案提交时调用后端判分接口。因为题目类型有多选选项组件需要支持checkbox或radio切换并且把答案格式化成后端约定的JSON结构。这里有一个细节多选题的选项顺序不要打乱因为题目设置的正确答案是按选项key匹配的。体验上用户已经选完一部分选项突然切换题目时选中的状态要用持久化的本地存储恢复不然翻页就丢了。4.3 前后端联调的跨域与鉴权细节开发阶段最常见的联调问题就是跨域。Vue的Vite开发服务器在5173Django在8000Flask在5000三个端口互相访问浏览器直接拦截。我的做法是开发环境用Vite代理不引入复杂的跨域配置// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true }, /report-api: { target: http://127.0.0.1:5000, changeOrigin: true, rewrite: path path.replace(/^\/report-api/, /api) } } } })这样前端代码里统一请求相对路径浏览器看起来是同源的生产环境Nginx也按同样规则转发。鉴权部分我用JWT登录接口由Django处理签发token存到localStorageaxios拦截器自动加上Authorization。Flask统计接口同样校验这个token但因为统计接口只读不写校验逻辑可以更简单用pyjwt把token解析出用户id就行。注意JWT密钥要放在环境变量里不要写死在代码中这是老生常谈但必须说。跨域问题还容易出现在“后端返回大的JSON结构”上。Vue调试时如果发现请求变成了OPTIONS预检通常是后端没有允许请求头Authorization。用django-cors-headers时要把CORS_ALLOW_HEADERS里加上authorization否则前端带token的请求会一直失败。这类问题看浏览器Network面板能很快定位。5. 实战全流程一个“刷题闭环”从0到15.1 在PyCharm里同时启动Django和Flask实战开始前把项目目录结构整理一下exam_system/ django_backend/ # Django项目 flask_report/ # Flask统计服务 exam_web/ # Vue前端在PyCharm底部找到Services面板可以同时添加多个Run ConfigurationDjango runserver 8000、Flask run 5000、Vite dev 5173。这样一次配置后面启动就是点击“启动服务”三个进程全部拉起来。我有一次忘记给Flask服务单独配置工作目录结果它找不到数据库连接配置启动秒报错。所以配置Run Configuration时Working directory一定要选到flask_report目录Environment variables也要填上数据库密码等。还有一个技巧是PyCharm的Services面板支持给三个服务分组命名成“本地开发”以后只需要一次点击。如果某些进程挂了面板会显示红色状态比自己去终端翻日志直观。这套开发环境搭好之后日常开发效率非常高。5.2 实现题库列表、提交答案、错题记录完成环境后我们来走通一个最小的刷题闭环。先定义Django接口# exam_system/question_bank/views.py from rest_framework.decorators import api_view from rest_framework.response import Response from .models import Question, ExamRecord, WrongBook api_view([GET]) def get_question_list(request): qs Question.objects.filter(is_activeTrue).order_by(?)[:10] data [{ id: q.id, type: q.type, category: q.category, content: q.content, options: q.options, } for q in qs] return Response(data) api_view([POST]) def submit_answer(request): question Question.objects.get(pkrequest.data[question_id]) user_answer request.data.get(user_answer, []) correct question.answer user_answer ExamRecord.objects.create( userrequest.user, questionquestion, user_answeruser_answer, is_correctcorrect ) if not correct: obj, created WrongBook.objects.get_or_create( userrequest.user, questionquestion ) if not created: obj.wrong_count 1 obj.save() return Response({correct: correct, analysis: question.analysis})这个接口里有两个值得说的设计。第一个是随机抽题用了order_by(?)在小数据量下没问题如果题库超过十万道这种写法会全表随机排序效率很低建议改成“按最大ID随机取偏移量”或“随机选一批ID后再IN查询”。第二个是答题判分的比较逻辑单选题answer是单个元素列表多选题是多元素列表JSONField取出来是list所以直接question.answer user_answer可以比较。但要注意用户提交的答案也需要是list类型前端不要传字符串否则永远判错。前端拿到题目列表后渲染提交时大体如下async function submit(question, selected) { const res await request.post(/submit_answer/, { question_id: question.id, user_answer: selected }) if (!res.data.correct) { // 弹窗展示解析并把错题写入本地 } nextQuestion() }这个闭环实现了从“随机抽题”到“判分”再到“错题入本”的完整流程所有核心业务就这三块。后面接上用户登录之后request.user才不是匿名用户所以建议先把登录注册模块也做了否则答题记录存不进数据库。5.3 用Vue渲染统计报告接下来把Flask统计接口接到Vue的ReportPage。在报告页里我会拉两个接口日期序列刷题量和各科目正确率。用ECharts还是用简单canvas图表我选择ECharts因为雷达图、柱状图、折线图开箱即用。Vue接入ECharts的方式很简单下载echarts在组件mount时初始化接收接口返回的数据填充option即可。一个注意点图表容器要显式设置宽度和高度否则ECharts渲染出来是空白。我最初就栽在这上面页面里只写了div refchart styleheight: 400px/div忘了宽度结果调试了半天。统计报告页面还可以加一个“薄弱知识点”模块比如根据错题分类统计显示数量最多的三个科目。这个模块在后端Flask里就是一条SQLSELECT category, COUNT(*) cnt FROM exam_system_wrongbook WHERE user_id 1 GROUP BY category ORDER BY cnt DESC LIMIT 3前端渲染成三个彩色标签点击之后跳到对应科目的练习。做这个功能时我体会到了Flask轻量的优势一行SQL加几行Python就能出一个接口如果放在Django里还要思考ORM表达虽然也是几行但总觉得不痛快。6. 常见问题与排查技巧实录6.1 最容易碰到的5个启动报错项目做下来环境类报错占了一大半。我整理几个高频问题报错现象原因解决办法App registry not readysettings.py里INSTALLED_APPS顺序不对或应用未注册检查应用是否在INSTALLED_APPS迁移前先执行makemigrationsTemplateDoesNotExist模板目录设置不正确在settings.py配置DIRS或者在应用内建templates目录django.db.utils.OperationalError: (2002)MySQL连接不上host/port/user错误用命令行先测试mysql连接再检查settings.pyModuleNotFoundError: No module named flask_cors虚拟环境没装依赖安装flask-cors并确认使用的是同一个venvVite启动时显示error: digital envelope routinesNode版本太高和Vite版本不兼容升级Vite或改用Node 18 LTS这五个报错里我遇到最多的是第一个。原因大多是你把应用写好了但忘记在settings.py的INSTALLED_APPS里添加或者添加顺序放到了某些依赖应用前面。Django在启动时会按顺序初始化应用如果你的应用被放在admin之前且定义了和admin有依赖关系的模型就会报这个错。6.2 数据库迁移与中文乱码如果数据库中文出现乱码第一时间检查字符集。MySQL建库时建议CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Django的数据库配置里也要加上OPTIONS: {charset: utf8mb4}否则连接时走的可能是默认latin1。执行迁移时如果模型改了字段先makemigrations再migrate不要手改表结构。Django迁移文件是版本化的团队成员协作时一定把迁移文件一起提交到代码库。另一个容易出问题的点是JSONField里的中文。如果存入JSON时pymysql连接的charset是utf8而不是utf8mb4一些特殊字符会报错。我的经验是统一用utf8mb4不仅数据库建库、连接串要设置Django的DATABASES配置里也要加上DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: exam_system, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }6.3 性能排查N1查询与QuerySet懒加载这个问题前面提到过这里展开再讲一下。Django的QuerySet是懒加载的当你打印、遍历、取值时才会真正执行SQL。所以在遍历答题记录时如果没有select_related每访问一次外键就多发一条查询页面响应会从几十毫秒涨到几秒# 低效 lists ExamRecord.objects.filter(useru) for item in lists: print(item.question.content) # 高效 lists ExamRecord.objects.filter(useru).select_related(question) for item in lists: print(item.question.content)如果只需要查询特定字段还可以用only(question__content)进一步瘦身。另一个提升性能的大招是给数据库加索引比如ExamRecord表的user和submit_time字段分别建索引统计接口的GROUP BY会快很多。数据库索引不是越多越好但高频查询条件和外键字段一定要加。6.4 一个小速查表我把一些常用的运维查询放在这里方便查阅python manage.py check # 检查项目配置 python manage.py showmigrations # 查看迁移状态 python manage.py sqlmigrate question_bank 0001 # 查看迁移的SQL npm run build # 构建Vue生产文件 gunicorn -w 2 -b 127.0.0.1:8000 exam_system.wsgi:application # 启动Django gunicorn -w 1 -b 127.0.0.1:5000 app:app # 启动Flask这里还有一个经验开发时经常遇到端口被占用。Django启动失败不要只盯着报错先看端口是否被上次残留进程占用。Windows下用netstat -ano查端口Linux下用lsof -i:8000找到PID后结束进程再重新启动。7. 实操体会与后续扩展思路这套django-flask前后端分离的练习系统做完之后我对“框架选型”有了一个很实际的体会不要被“一个项目只能用一个框架”的思维框住。Python生态里Django、Flask、FastAPI各有擅长组合使用并不丢人重点是让每一层服务做自己最顺手的事。Django管业务和后台Flask管统计接口Vue管交互这个分工在后来的维护中非常清晰改题库管理不用碰统计接口改前端样式不用动Python代码。后续如果想继续扩展有几个方向非常合适一是把题库接入Excel批量导入配合Django Admin的批量操作团队运营成本会大幅下降二是加一个AI推荐算法基于历史错题和正确率自动推荐薄弱环节的题目三是增加考试模拟计时交卷后展示各知识点的掌握雷达图这部分正好能用上已有的Flask统计接口。如果你正在练手这个项目我建议先把“随机抽题、判分、错题本、统计报告”这条闭环跑通再逐步加功能千万不要一上来就追求大而全那会让项目永远停在“配置环境阶段”。