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端口的业务API,Flask跑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_MODULE=exam_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_length=10, choices=QUESTION_TYPE, verbose_name='题型') category = models.CharField(max_length=50, verbose_name='科目分类') content = models.TextField(verbose_name='题干') options = models.JSONField(verbose_name='选项JSON') # [{"key": "A", "text": "..."}] answer = models.JSONField(verbose_name='正确答案') analysis = models.TextField(blank=True, verbose_name='解析') difficulty = models.IntegerField(default=3, verbose_name='难度1-5') is_active = models.BooleanField(default=True, verbose_name='是否启用') created_at = models.DateTimeField(auto_now_add=True) class ExamRecord(models.Model): user = models.ForeignKey('auth.User', on_delete=models.CASCADE, verbose_name='用户') question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name='题目') user_answer = models.JSONField(verbose_name='用户答案') is_correct = models.BooleanField(verbose_name='是否答对') submit_time = models.DateTimeField(auto_now_add=True) class WrongBook(models.Model): user = models.ForeignKey('auth.User', on_delete=models.CASCADE, verbose_name='用户') question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name='题目') wrong_count = models.IntegerField(default=1, verbose_name='错误次数') last_wrong_time = models.DateTimeField(auto_now=True)设计时的几个考虑:选项和答案用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__gt=4, is_active=True),然后qs.delete()。清空全表但保留自增:Question.objects.all().delete()。
我建议在管理后台或视图里删除前先打印将要删除的数量,避免误删。示例:
qs = ExamRecord.objects.filter(submit_time__lt='2024-01-01') print(f"准备清理 {qs.count()} 条过期记录") # 备份后再删 # qs.delete()另外一个高频问题是查询优化懒加载导致的N+1。比如循环展示答题记录时,如果没加select_related,Django会在访问每条记录的外键时再发一次SQL,100条记录就是101次查询。加上一行:
records = ExamRecord.objects.filter(user=request.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( host='127.0.0.1', user='root', password='yourpassword', database='exam_system', charset='utf8mb4' ) @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自带的debug=True了,生产环境一定用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进程大概占80MB,Flask大概占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 vue@latest 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和token:
import 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开发服务器在5173,Django在8000,Flask在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存到localStorage,axios拦截器自动加上Authorization。Flask统计接口同样校验这个token,但因为统计接口只读不写,校验逻辑可以更简单,用pyjwt把token解析出用户id就行。注意JWT密钥要放在环境变量里,不要写死在代码中,这是老生常谈但必须说。
跨域问题还容易出现在“后端返回大的JSON结构”上。Vue调试时如果发现请求变成了OPTIONS预检,通常是后端没有允许请求头Authorization。用django-cors-headers时,要把CORS_ALLOW_HEADERS里加上authorization,否则前端带token的请求会一直失败。这类问题看浏览器Network面板能很快定位。
5. 实战全流程:一个“刷题闭环”从0到1
5.1 在PyCharm里同时启动Django和Flask
实战开始前,把项目目录结构整理一下:
exam_system/ django_backend/ # Django项目 flask_report/ # Flask统计服务 exam_web/ # Vue前端在PyCharm底部找到Services面板,可以同时添加多个Run Configuration:Django 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_active=True).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(pk=request.data['question_id']) user_answer = request.data.get('user_answer', []) correct = question.answer == user_answer ExamRecord.objects.create( user=request.user, question=question, user_answer=user_answer, is_correct=correct ) if not correct: obj, created = WrongBook.objects.get_or_create( user=request.user, question=question ) 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 ref="chart" style="height: 400px"></div>,忘了宽度,结果调试了半天。
统计报告页面还可以加一个“薄弱知识点”模块,比如根据错题分类统计,显示数量最多的三个科目。这个模块在后端Flask里就是一条SQL:
SELECT 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 ready | settings.py里INSTALLED_APPS顺序不对或应用未注册 | 检查应用是否在INSTALLED_APPS,迁移前先执行makemigrations |
TemplateDoesNotExist | 模板目录设置不正确 | 在settings.py配置DIRS,或者在应用内建templates目录 |
django.db.utils.OperationalError: (2002) | MySQL连接不上,host/port/user错误 | 用命令行先测试mysql连接,再检查settings.py |
ModuleNotFoundError: No module named 'flask_cors' | 虚拟环境没装依赖 | 安装flask-cors,并确认使用的是同一个venv |
Vite启动时显示error: digital envelope routines | Node版本太高和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 性能排查:N+1查询与QuerySet懒加载
这个问题前面提到过,这里展开再讲一下。Django的QuerySet是懒加载的,当你打印、遍历、取值时才会真正执行SQL。所以在遍历答题记录时,如果没有select_related,每访问一次外键就多发一条查询,页面响应会从几十毫秒涨到几秒:
# 低效 lists = ExamRecord.objects.filter(user=u) for item in lists: print(item.question.content) # 高效 lists = ExamRecord.objects.filter(user=u).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统计接口。如果你正在练手这个项目,我建议先把“随机抽题、判分、错题本、统计报告”这条闭环跑通,再逐步加功能,千万不要一上来就追求大而全,那会让项目永远停在“配置环境阶段”。