教学质量评价系统这个名字,放在校园项目里几乎每年都会出现,但真正把它做成一套能跑起来、能录入数据、能出统计结果的完整系统,并不是写几个页面那么简单。我手头这个项目就是用Python做后端、Vue做前端,在PyCharm里从空目录一步步搭起来的。整个过程用到了Django,也对比试过Flask,最终选择了Django作为主框架。如果你正准备做类似的管理系统,或者想搞清楚Python+Vue这条技术路线到底怎么落地,这篇博文应该能帮你少走不少弯路。我会把需求拆解、技术选型、环境搭建、后端接口、前端页面、联调部署这几个环节全部过一遍,中间穿插实际踩过的坑和最终采用的写法。
1. 从需求出发:教学质量评价系统要管哪些人和哪几件事
动代码之前,我习惯先把业务盘清楚。教学质量评价,表面上看就是“学生给老师打分”,但真拆开来看,至少涉及三类角色、四个核心流程,任何一个环节理解偏了,后端模型和前端页面都会返工。
1.1 三类用户,三种权限边界
第一个角色是学生,他关心的是“我能不能方便地给这学期教我的老师打分、提交文字建议”,而且要让学生相信评价是匿名的,否则很多人不敢说真话。第二个角色是教师,他看到的是自己的最终评分、排名区间、以及匿名文字反馈,但看不到“具体是哪个学生打了分”。第三个角色是管理员,负责配置评价任务、管理课程与教师关系、查看全院的统计报表。
这三类角色看起来很常规,但权限边界一定要在需求阶段固定下来。我见过不少项目做到一半,教师突然问“为什么我不能看到每个学生的打分明细”,这就是一开始没把匿名原则约束清楚。系统设计时,教师端所有接口只能返回聚合数据,不能返回明细记录,哪怕管理员也只能按课程维度去查看原始评价,而不是按学生姓名去筛选。
1.2 评价数据流:从指标配置到统计报表
整套系统跑起来之后,数据流向大致是这样的:
- 管理员创建评价任务,设置评价指标(比如教学态度、内容讲解、课堂互动、作业批改),每个指标对应一个分值区间。
- 管理员将评价任务关联到具体的课程和任课教师。
- 学生登录后看到“待评价课程列表”,对每个课程逐项打分并填写文字意见。
- 系统将每条评价记录落库,打上课程、教师、指标类型等维度。
- 教师登录后读取自己的聚合结果;管理员按学期、学院、课程做多维统计。
这个流程直接决定了数据库模型怎么设计。评价任务和课程是多对多关系,指标项和评价记录是一对多关系,教师和课程又是一对多关系。如果一开始就把这些关系理顺,后面写ORM模型会非常轻松;反过来,如果一上来就建一张宽表,把所有字段都塞进去,统计维度会瞬间变复杂。
1.3 先用原型对齐字段,再写后端模型
我在这类项目上的一个经验是:不要急着建表,先用前端页面原型把字段全部列出来。比如评价页面到底需要哪些字段:课程名称、教师姓名、指标评分(1到5分)、评语输入框、提交按钮,这些字段确认以后,后端模型几乎可以直接对应着写。
我吃过一次亏:最初做模型时,我把指标和评价记录合并成了一张表,每一条记录就是一行带十几个字段的冗余数据。等做到统计页面时,发现“按指标维度做平均”要写一堆条件聚合,代码又长又难维护。后来改成“评价任务表 + 指标表 + 评价记录表 + 记录明细表”的结构,统计查询才变得干净。所以说,需求梳理阶段花一天画原型、列字段,比后期重构省三天不止。
2. 技术栈磨合:Django与Vue如何分工,Flask为什么被“降级”为备选
标题里既写了Django又写了Flask,确实很多人会在两个框架之间纠结。我两个框架都写过,这个项目也分别做了技术验证,最终选了Django。这不是说Flask不好,而是这个项目的特性更适合Django。
2.1 Django与Flask的取舍对比
先看一张我从实战角度做的对比:
| 维度 | Django | Flask |
|---|---|---|
| ORM | 自带强大ORM,迁移、查询、关联都顺手 | 默认无ORM,通常要接SQLAlchemy |
| Admin后台 | 自带Admin,配置一下就出现管理界面 | 需要额外集成Flask-Admin |
| 用户认证 | 自带User模型、session、权限体系 | 需要自己搭或用扩展插件 |
| 项目成熟度 | 重量级,适合业务复杂的系统 | 轻量级,适合单一接口或微服务 |
| 学习曲线 | 概念多,前期理解成本高 | 上手快,但后期要自己组装 |
对这个教学质量评价系统来说,最要命的是用户认证和权限。Django内置的User模型和Permission机制,能直接支撑学生、教师、管理员三种角色;而用Flask的话,光用户角色控制就要花不少时间自己去拼。另一个点是ORM,评价相关的数据模型关系复杂,Django的ORM在跨表查询、annotate聚合统计时非常顺滑,Flask接SQLAlchemy虽然也能写,但多一层依赖,出错排查时又多一个变量。
2.2 前后端分离的架构价值
项目选择Python+Vue这套组合,本质上是把系统拆成两个独立工程:后端Django只提供JSON接口,前端Vue负责渲染页面和交互。这样做的好处有三个。
第一,前后端可以并行开发。我这边写Django接口的同时,另一头Vue页面可以先写假数据,最后联调时再替换成真实请求,整体开发周期明显缩短。第二,后续维护边界清晰。前端改样式、后端该逻辑,互不干扰;如果以后要出小程序端,直接复用同一套后端API。第三,部署灵活。Django跑在服务器上,Vue构建后的静态文件交给Nginx托管,前后端分离部署,抗压能力也更好。
2.3 数据库和IDE的配套选择
数据库方面,开发环境我用SQLite就够了,但最终交付我切到了MySQL。原因很简单:SQLite在并发写入和权限控制上行,官方推荐生产环境使用MySQL或PostgreSQL。如果你的项目是直接用SQLite一路写完,建议预留出数据库切换的配置入口,Django的settings里把数据库连接配置独立出来,换库时只改一处。
IDE选PyCharm,倒不是因为它有多神,而是在Python项目的虚拟环境管理、Django命令行集成、调试模式上做得足够顺手。尤其是PyCharm Professional版自带的Django支持,可以直接建Django项目、启动runserver、debug断点,对刚入门的人来说省去了很多命令行的繁琐。
3. PyCharm环境搭建:从Python解释器到Vue脚手架
环境搭建是整个项目最容易卡住的地方,尤其是很多同学第一次在PyCharm里同时跑两套项目,经常一头雾水。我把整个流程拆成了三个步骤:先配Python后端,再建Vue前端,最后统一调版本兼容。
3.1 在PyCharm中创建虚拟环境并安装依赖
打开PyCharm新建项目时,我建议直接选择虚拟环境,Python版本用3.10或3.11都行。项目创建好后,打开Terminal,执行虚拟环境激活和依赖安装:
python -m venv venv # Windows下激活虚拟环境 venv\Scripts\activate # Mac/Linux下激活虚拟环境 source venv/bin/activate pip install django djangorestframework django-cors-headers PyJWT这几个包的作用分别是:Django框架本身、Django REST Framework(简称DRF)用来快速写REST API、django-cors-headers解决前后端分离时的跨域问题、PyJWT用来做JWT登录鉴权。
安装完成后,创建Django项目和应用:
django-admin startproject qa_project cd qa_project python manage.py startapp evaluation这里有个小建议:不要把业务代码全塞在一个“主应用”里,我习惯把评价相关逻辑单独放一个app叫evaluation,用户认证逻辑放users,这样后期代码组织更清晰。
3.2 初始化Vue项目并调整目录结构
前端部分,我使用的是Vue 3 + Vite。命令行执行:
npm create vue@latest脚手架会让你选择是否安装TypeScript、Pinia、Vue Router等依赖,根据自己的需求勾选即可。项目生成后,进入项目目录安装依赖并启动开发服务:
npm install npm run devVite默认的端口是5173,而后端Django默认跑在8000端口,所以前后端联调时必须解决跨域问题,这一步我放在第六部分细说。
Vue项目目录我一般会做两个调整:
- 把src/router里放路由配置,用于定义登录页、学生端、教师端、管理端的页面路径;
- 在src/api下新建request.js统一封装axios请求,把后端API地址和请求拦截器集中管理。
3.3 版本兼容这个隐形坑
版本兼容是新手最容易忽略的问题。Django 5.0发布后,很多人直接pip install django,然后发现项目跑起来一堆弃用警告,甚至某些第三方库还没适配。我实测下来,Django 4.2 + DRF 3.14是一个很稳的组合,Python版本用3.10或3.11都不要紧。
前端方面,Node.js版本最好用18以上,Vite 5要求Node 18+,否则装依赖时可能会出现ES模块解析报错。如果不确定本机Node版本,可以先执行node -v检查,低于18就先升级Node环境再创建Vue项目。还有一个坑是npm install报错,多半是由于网络环境导致的,可以临时切镜像源再重试。
4. 后端核心实现:模型关系设计、JWT登录与评价接口的完整链路
后端是这套系统的大脑,把所有业务规则落实到数据模型和接口逻辑上。我按模型、接口、鉴权三个维度来说。
4.1 数据模型:四张核心表把业务关系串起来
先放最终采用的模型结构:
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES = ( ('student', '学生'), ('teacher', '教师'), ('admin', '管理员'), ) role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='student') real_name = models.CharField(max_length=50, verbose_name='姓名') teacher = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='students') class Course(models.Model): name = models.CharField(max_length=100) code = models.CharField(max_length=20, unique=True) teacher = models.ForeignKey(User, on_delete=models.CASCADE, related_name='courses') class EvaluationTask(models.Model): title = models.CharField(max_length=100) start_time = models.DateTimeField() end_time = models.DateTimeField() courses = models.ManyToManyField(Course, related_name='evaluation_tasks') class Indicator(models.Model): task = models.ForeignKey(EvaluationTask, on_delete=models.CASCADE, related_name='indicators') name = models.CharField(max_length=100) full_score = models.IntegerField(default=5) class EvaluationRecord(models.Model): task = models.ForeignKey(EvaluationTask, on_delete=models.CASCADE, related_name='records') course = models.ForeignKey(Course, on_delete=models.CASCADE) student = models.ForeignKey(User, on_delete=models.CASCADE) comment = models.TextField(blank=True) created_time = models.DateTimeField(auto_now_add=True) class Score(models.Model): record = models.ForeignKey(EvaluationRecord, on_delete=models.CASCADE, related_name='scores') indicator = models.ForeignKey(Indicator, on_delete=models.CASCADE) value = models.IntegerField()这套模型的核心思路是:评价任务(EvaluationTask)挂多个课程,每个课程由某个教师教学;学生提交一条评价记录(EvaluationRecord),记录下面有多条指标得分(Score)和一段文字评价。这样拆开以后,“查某位教师某门课的平均分”就是一次简单的跨表聚合。
4.2 用DRF实现接口:从queryset到viewset的简化写法
DRF最大的价值在于,常规的增删改查逻辑几乎不用手写。以评价任务接口为例:
from rest_framework import serializers, viewsets class IndicatorSerializer(serializers.ModelSerializer): class Meta: model = Indicator fields = ['id', 'name', 'full_score'] class EvaluationTaskSerializer(serializers.ModelSerializer): indicators = IndicatorSerializer(many=True, read_only=True) class Meta: model = EvaluationTask fields = ['id', 'title', 'start_time', 'end_time', 'indicators'] class EvaluationTaskViewSet(viewsets.ModelViewSet): queryset = EvaluationTask.objects.all().prefetch_related('indicators') serializer_class = EvaluationTaskSerializerModelViewSet自动帮我把列表、详情、创建、更新、删除的路由都生成了。省略了手写一堆重复的if request.method == POST之类的判断。URL配置也很简单:
from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register('tasks', EvaluationTaskViewSet) urlpatterns = router.urls4.3 JWT登录鉴权
用户体系这块,我没有用Session,而是选了JWT。因为前后端分离部署之后,前端在浏览器里访问Django接口,如果还用Session,CSRF校验和Cookie传递会比较麻烦。JWT的方式是:登录成功后后端返回一个Token,前端把Token存起来,后续请求带上Authorization头即可。
登录接口的核心逻辑很简单:
import jwt from django.contrib.auth import authenticate from rest_framework.views import APIView from rest_framework.response import Response SECRET_KEY = 'your-secret-key' class LoginView(APIView): def post(self, request): username = request.data.get('username') password = request.data.get('password') user = authenticate(username=username, password=password) if not user: return Response({'error': '用户名或密码错误'}, status=400) token = jwt.encode( {'user_id': user.id, 'role': user.role}, SECRET_KEY, algorithm='HS256' ) return Response({'token': token, 'role': user.role, 'name': user.real_name})前端拿到Token后,每次请求都在拦截器里自动带上。后端再写一个解析Token的装饰器,就能实现接口权限控制。这里要特别提醒:JWT密钥一定要放在Django的settings里,并且生产环境不要硬编码在代码里,最好用环境变量读取。
5. 前端页面落地:从评价表单到统计图表的Vue组件写法
前端Vue部分,我最想分享的是路由设计、评价表单的交互写法,以及权限控制三个点。
5.1 路由守卫:按角色控制页面访问
Vue Router可以通过路由守卫拦截未登录或无权访问的页面。我的做法是在路由规则里给每个路由加一个meta字段:
{ path: '/student/evaluate', component: StudentEvaluate, meta: { requiresAuth: true, role: ['student', 'admin'] } }然后全局注册一个路由守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role && !to.meta.role.includes(role)) { next('/403') } else { next() } })这样,教师登录后想去学生评价页面,会被直接重定向到403页,从入口上杜绝越权操作。
5.2 评价页面核心交互:动态指标列表与提交校验
评价页面是学生端最核心的界面。它的特点是:每门课的指标数量不一定相同,所以指标列表必须由后端返回,前端动态渲染。
用Vue 3的组合式API写起来很干净:
<script setup> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import api from '@/api/request' const route = useRoute() const taskId = route.params.taskId const indicators = ref([]) const scores = ref({}) const comment = ref('') onMounted(async () => { const res = await api.get(`/api/tasks/${taskId}/indicators/`) indicators.value = res.data }) async function submit() { const missing = indicators.value.some(item => !scores.value[item.id]) if (missing) { alert('请完成所有指标评分') return } await api.post(`/api/tasks/${taskId}/evaluate/`, { scores: scores.value, comment: comment.value }) } </script> <template> <div v-for="item in indicators" :key="item.id"> {{ item.name }} <select v-model.number="scores[item.id]"> <option v-for="n in item.full_score" :value="n">{{ n }}</option> </select> </div> <textarea v-model="comment"></textarea> <button @click="submit">提交评价</button> </template>这段逻辑看起来简单,但包含一个很重要的设计:提交前必须校验是否全部评分,否则漏掉某项指标会导致统计结果失真。mock阶段我曾经跳过校验,结果教师端查总评时发现少了一门课的平均分,排查半天才发现是有学生提交时漏选了评分项。
5.3 教师端统计图表:按课程和指标维度聚合
教师端统计功能,我选择引入ECharts来展示。后端需要提供一个聚合接口,比如返回某教师各门课程在不同指标上的平均分:
from django.db.models import Avg from rest_framework.views import APIView class TeacherCourseScoreView(APIView): def get(self, request): teacher = request.user courses = Course.objects.filter(teacher=teacher) result = [] for course in courses: row = {'course': course.name} records = EvaluationRecord.objects.filter(course=course) indicators = Indicator.objects.filter(task__courses=course) for ind in indicators: avg = Score.objects.filter( record__course=course, indicator=ind ).aggregate(avg_value=Avg('value'))['avg_value'] row[ind.name] = round(avg or 0, 2) result.append(row) return Response(result)前端拿到数据后,用ECharts的折线图或柱状图渲染即可。一个经验之谈是:饼图和柱状图尽量按“课程”分组,而不是把多个指标塞进一堆看似酷炫的仪表盘里,因为教学管理者最关心的永远是不同课程之间的横向对比。
6. 联调、部署与坑后反思:CORS、CSRF与静态文件
系统开发完成后,联调阶段往往比写代码更折磨人。我在这部分总结三个最经典的问题和解决办法。
6.1 CORS跨域:前端访问不了后端接口怎么办
前后端分离开发时,Vite跑在5173端口,Django跑在8000端口,浏览器会认为这两个端口属于不同源,默认不允许前端发起跨域请求。解决方式是安装django-cors-headers:
INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:5173', ]配置好之后,前端就能正常请求后端了。这里要注意:生产环境千万不要用CORS_ALLOW_ALL_ORIGINS = True,否则任何网站都能向你的接口发起请求,安全隐患非常大。
6.2 CSRF校验:POST请求被403拦截
这个问题只在没有启用JWT、使用Session时会遇到。如果坚持用Django自带的登录系统,所有POST请求都需要带上CSRF Token。我建议既然选了前后端分离,就统一用JWT方案,登录接口用APIView的authentication_classes指定为空,其他接口统一校验JWT,这样就不会和CSRF机制纠缠不清。
如果确实要用Django的登录页面,那前端要在请求头中加入X-CSRFToken字段,并且从Cookie中读取csrftoken。网上关于这个报错的解决方案很多,但大多是拆东墙补西墙,最干净的方案还是换JWT。
6.3 部署:Nginx + uWSGI + Vue静态文件
部署方案我最终选了Nginx + uWSGI的组合。流程是:
1.后端代码打包放到服务器,创建虚拟环境,安装依赖。 2.执行Django的collectstatic,把静态文件收集到指定目录。 3.用uWSGI启动Django服务。 4.前端执行npm run build,生成dist目录。 5.Nginx配置里,把/api请求转发到uWSGI,把其他路径指向前端dist目录。
一个参考版的Nginx配置:
server { listen 80; server_name your-domain.com; location /api/ { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; } location / { root /var/www/qa_frontend/dist; try_files $uri $uri/ /index.html; } }部署中最容易漏掉的一步是:Vue是单页应用,前端路由必须由Nginx做try_files回退到index.html,否则刷新页面会直接404。我在测试阶段就栽过一次,学生端提交完评价后刷新,页面直接白屏,排查了很久才发现是Nginx配置里少了try_files这一行。
6.4 最后一个值得强调的教训
整个项目做完,我最大的体会是:教学质量评价系统的价值不在“会打分”这个功能点上,而在“统计结果可信”这件事上。为了这个目标,前端必须有完整的评分校验,后端必须在保存评价记录时做事务处理,防止一半成功一半失败;数据库层面对同一学生同一门课只允许提交一次,这些约束都比页面样式重要得多。
如果你正准备照着这个方向做毕业设计或小项目,我建议你先把第1部分的角色和数据流想透,再动手写代码。一个清晰的模型设计,能让你后半程少花一半精力去改表、改接口。最后分享一个小技巧:开发过程中所有耗时超过100ms的接口,可以在Django里加一个简单的耗时日志中间件,随时监控接口性能。评价统计接口如果后期数据量上来了,记得给EvaluationRecord的task和course字段加上联合索引,聚合查询的提升会非常明显。