这套系统的开发其实并不是从"我要写个网站"开始的。事情起源于教务处那位老师的一句话:评教数据年年收,Excel汇总表一打开几百个文件,谁能帮我们把这些信息真正用起来?那会儿刚好学期末,纸质问卷回收率不到六成,学生填完的表格堆在档案室,督导想查某位老师的历次教学反馈,得翻半天柜子。于是,一个基于 Python、Vue3 和 Django 的高校教学质量评估系统,也就是俗称的学生评教系统,就这么被推到了开发日程上。
市面上评教类的通用问卷产品不少,真正落地到高校场景时却处处别扭:不同学院的评教指标不一样,同一个老师在不同课程里的评分维度也不同,学生要匿名填写但又不能在里面乱刷,教师只能看自己的数据而领导要看全校汇总。这些需求堆在一起,任何一个现成问卷工具都接不住。所以干脆自己造一套,从需求梳理、选型、建模到部署,前后改了四五个大版本,最终的交付成果获得教务和学院的认可。这篇博文就把整个开发过程里我认为最值得参考的部分拆开讲,包括数据模型、匿名评教、防重复提交、权限隔离、前后端联调和生产环境部署的坑。如果你正准备做评教系统、教学质量评估平台或者类似的校园管理类系统,这篇内容能帮你少走好几个月的弯路。
1. 为什么选了这个方向:一套评教系统背后的需求真相
1.1 通用问卷工具解决不了的三个真问题
在动工之前,我花了很长时间跟教务、督导和几个学院的老师聊需求。一开始大家的想法出奇一致:能不能找个现成的问卷工具,每年发个链接就完事?可聊深一点就发现问题远不是"发个链接"这么简单。
第一个问题是匿名评教不等于"不记名"。学生评教最核心的诉求是保护学生身份,让学生敢说真话。但匿名又带来了另一个隐患——怎么防止一个学生给同一门课刷几十个评价?怎么在出现恶意诋毁、严重失实评价的时候,还能让管理员在合规流程下找到责任记录?这里面就存在"匿名保护"和"可审计"之间的矛盾。通用问卷工具通常只做匿名收集,根本没有权限分级和审计设计。
第二个问题是指标体系的灵活性。公共基础课、专业理论课、实验实践课,评教维度完全不是一回事。有的学院想细看教学态度,有的学院更关注课堂互动和课程思政融入情况,还有的学院把"课程达成度"作为重要指标。通用问卷工具做的是固定标题的问卷,管理员想临时加一组指标、调一下权重,往往要重新建问卷、重新发链接,历史数据还不好对比。
第三个问题是多角色报表边界。学生只关心"提交成功了吗",教师关心"我的课堂反馈怎么样",教学院长关心"全院排名和我分管课程的问题分布",教务员关心"总评结果和异常预警"。一个问卷工具给出的统计报表要么太粗,要么把所有数据摆在所有人面前,压根没法做数据隔离。
1.2 评教不是"填表"那么单薄:它是一条五段式数据流水线
把需求聊完以后,我把整个评教流程拆成了五个阶段,每个阶段都有固定的角色、输入输出和系统职责。这张表几乎成了后面所有开发的“地图”。
| 阶段 | 主要操作者 | 核心内容 | 系统职责 |
|---|---|---|---|
| 指标配置 | 教务管理员、学院秘书 | 设置评教批次、选择指标模板、调整权重 | 模板管理、版本留痕 |
| 任务发布 | 教务管理员 | 确定评教学期、评教时间窗口、适用课程范围 | 任务生命周期控制 |
| 学生填写 | 学生 | 对修读课程逐项打分并填写文字评价 | 匿名保护、防重复提交、进度跟踪 |
| 统计分析 | 系统自动执行 | 按权重计算均分、生成班级/课程/教师维度报告 | 权重计算、异常提醒、导出归档 |
| 反馈整改 | 教师、督导 | 教师查看本人数据,督导查看全院情况并跟踪整改 | 数据权限隔离、整改记录关联 |
这个拆分过程让我意识到,评教系统的核心难点并不在"打分页面",而是在任务状态机的管理和数据权限的边界控制上。比如任务发布后,学生端能看到哪些课程、教师端能查到哪些反馈、督导端能不能在学习成绩出来前看到结果,这些都需要一套明确的状态和权限规则来约束。
正因为有这段前期调研,后续的技术选型和数据建模才没有跑偏。很多人一上来就急着写代码,结果发现模型设计得不对,评教任务一多、角色一复杂,整个系统就推倒重来。这个经验我放到最前面,就是想说明:评教系统本质是一个带复杂权限和流程约束的管理系统,不是简单的表单工具。清楚了这一点,后面每一步选型都有了依据。
2. 技术栈取舍:为什么是Django、Python与Vue3的搭配
2.1 后端为什么押注Django而不是造轮子
技术选型是这个项目里最不需要纠结、但又最容易被人问起的一个环节。Flask 足够轻,FastAPI 性能好,Spring Boot 生态庞大,为什么我最终选了 Django?
核心原因是:这个项目的时间窗口很紧,而且需求变更频繁。Django 自带 Admin 后台、ORM 迁移机制、认证系统、权限框架、CSRF 防护和模板系统,等于把一套管理后台的地基直接给你铺好了。教务管理员需要一个后台去配置指标、发布任务、查看报表,Django Admin 稍微定制一下就能承担大量管理工作,这省下了至少两周的前端开发量。
更实在的是 Django ORM 的迁移机制。评教系统的指标权重、课程类型、评分区间这类字段,在开发过程中变动极其频繁。比如一开始评分是 1 到 5 分,后来有的学院希望用百分制,再后来又有人提出把分数和等级同时保留。用python manage.py makemigrations和migrate改起表结构来非常顺滑,比手写 SQL 维护一堆嵌套查询要舒服得多。
当然,FastAPI 在异步和性能上确实有优势,但评教系统的并发量放在高校场景里,单个学期峰值也就是几千人同时提交,Django 同步模型配合数据库连接池完全扛得住。我不止一次见过队友因为追求"高性能异步"反而把简单业务做成性能瓶颈的。在管理类系统里,开发效率、生态成熟度和团队熟练度,往往比框架本身的极限吞吐量更重要。
2.2 前端选Vue3:Composition API适合中后台的繁琐状态
前端部分选择了 Vue3 配合 Vite 构建。对比 React 和 Vue,两者实现评教页面都没问题,但 Vue3 的 Composition API 对"表单+状态管理"这种中后台页面实在太友好了。评教页面有大量动态渲染的指标项,每个指标有分数、权重说明、题型类型,学生填到一半还可能切走、下次要继续,这些都会产生很复杂的状态交互。reactive和ref用起来比 Vue2 的data方案直观,而且配合<script setup>语法,代码量比 Options API 少很多。
组件库选的是 Element Plus,表格、表单、评分、日期选择器这些中后台高频组件全部开箱即用,省下了大量造 UI 轮子的时间。还有一个隐藏好处:Element Plus 对中文文档和中文社区的支持非常完善,团队在遇到组件细节问题的时候能很快找到解决方案。
2.3 中间件与数据库:不做无谓的微服务拆解
数据库选型很多人会纠结 MySQL 和 PostgreSQL,我的建议是看团队运维熟悉度。最终选了 MySQL 8,原因很简单:学校信息中心的运维同事对这个体系更有经验,出问题能快速处理。评审时不需要为了"用 PostgreSQL 更先进"去增加额外的沟通成本。
Redis 在上面的角色首先是缓存,比如评教任务列表、指标模板、热门数据报表,这些热点数据直接用 Redis 缓存可以显著降低数据库压力。此外 Redis 还可以用SETNX做分布式锁,在防重复提交的场景下做一个兜底。
这里我想强调一个原则:不要为了技术炫技做微服务。评教系统整体就是一个 Django 单体应用,内部按 app 拆成users、courses、evaluation、statistics几个模块,单体能承载的复杂度远没到拆服务的程度。拆开以后反而要处理服务间通信、事务一致性和部署运维的三重成本,得不偿失。
3. 数据模型设计:评教系统的地基怎么打
3.1 用户、课程、任务三个层次要分开建模
评教系统里面最容易踩的坑,是把"人"和"角色"混在一个表里,或者把课程直接写死进评教任务。我在早期版本就犯过这个错误,后来重构时才真正把模型分层理顺。
底层是用户体系。我直接复用 Django 自带的AbstractUser做一个扩展,用user_type区分教务管理员、教师、学生,同时通过外键把教师档案和学生档案关联进来。这样既保留了 Django 自带的认证功能,又让角色扩展足够灵活。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPES = [ ('admin', '教务管理员'), ('teacher', '教师'), ('student', '学生'), ('supervisor', '督导'), ] user_type = models.CharField(max_length=20, choices=USER_TYPES, default='student') teacher_profile = models.OneToOneField('TeacherProfile', null=True, blank=True, on_delete=models.SET_NULL) student_profile = models.OneToOneField('StudentProfile', null=True, blank=True, on_delete=models.SET_NULL) class TeacherProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) emp_no = models.CharField(max_length=30, unique=True) department = models.CharField(max_length=100) class StudentProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) student_no = models.CharField(max_length=30, unique=True) department = models.CharField(max_length=100) grade = models.CharField(max_length=20)然后是课程层。课程不是简单一个Course表就完了,评教要精确到"某个学期、某个老师、某个课程班"的教学关系,所以要有Course(课程基础信息)和CourseSchedule(学期开课计划)两层,CourseSchedule里面挂教师和选课学生,评教任务直接面向CourseSchedule展开。
再往上才是评教任务层。EvaluationTask代表一次评教活动,有学期、开始时间、结束时间、状态。一次评教任务可以覆盖多门课程,但一门课程的一次教学安排只能属于一个有效任务。这个约束我直接放在数据库里用唯一索引兜底,避免业务代码漏判。
三层分开以后,整个系统的扩展性就好了很多。比如想发起"只针对新入职教师的评教",只要任务配置里限定teacher_profile__emp_no范围就行,不需要动表结构。
3.2 指标模板:把"指标内容"和"指标权重"分离
评教指标是重头戏。需求调研时教务处要求:每次评教任务可以独立配置指标,但也要能复制上一次的模板。这意味着指标内容不能写死在字段里,而是要建模成可配置的数据。
我的设计思路是三层结构:EvaluationTemplate(模板)、EvaluationItem(指标项)、EvaluationItemOption(选项/打分规则)。模板属于某一类课程场景,指标项挂在模板下,每个指标项有content(描述)、category(分类,如教学态度、教学内容)、score_type(评分题、单选、文本)、weight(权重)。
class EvaluationTemplate(models.Model): name = models.CharField(max_length=100) course_type = models.CharField(max_length=50) # 理论课/实验课/体育课 is_active = models.BooleanField(default=True) class EvaluationItem(models.Model): template = models.ForeignKey(EvaluationTemplate, on_delete=models.CASCADE, related_name='items') category = models.CharField(max_length=50) content = models.TextField() score_type = models.CharField(max_length=20, default='score') # score/radio/text weight = models.DecimalField(max_digits=5, decimal_places=2, default=1.00) sort_order = models.IntegerField(default=0)指标复制功能就很好做了:新任务创建的时候,从模板里把EvaluationItem批量复制到EvaluationTask.item_set,既保留了模板的通用性,也允许某个任务单独修改权重。权重存的不是百分数,而是相对权重,统计时统一做归一化处理。这样教务老师配置指标时不需要算一遍总和,减轻了很多心智负担。
3.3 匿名评教的可审计设计:这个分寸感很难拿捏
匿名评教是最容易做过头或者做漏的一块。如果完全匿名,意味着评教记录和学生ID彻底断绝关系,那么"一个学生能不能重复评"这个去重就失效了。如果保留学生ID关联,学生很容易担心老师能查到自己的评价,导致评教失去真实性。
我采用的方案是:评教提交记录表和评教明细表分离。EvaluationRecord保留student外键,主要用于判重;EvaluationDetail只记录任务、指标、分数和文本,通过一个随机生成的trace_id和提交记录关联,但明细表内不直接存student_id。系统正常业务逻辑只读取EvaluationDetail,只有当发生严重失实评价、需要走合规流程时,管理员才能通过trace_id倒查提交记录,而且这个过程会在后台留存操作日志。
class EvaluationRecord(models.Model): task = models.ForeignKey('EvaluationTask', on_delete=models.CASCADE) course_schedule = models.ForeignKey('CourseSchedule', on_delete=models.CASCADE) student = models.ForeignKey('User', on_delete=models.CASCADE) trace_id = models.CharField(max_length=64, unique=True) status = models.CharField(max_length=20, default='draft') # draft/submitted created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint( fields=['task', 'course_schedule', 'student'], name='uniq_task_course_student' ) ] class EvaluationDetail(models.Model): record = models.ForeignKey(EvaluationRecord, on_delete=models.CASCADE, related_name='details') item = models.ForeignKey(EvaluationItem, on_delete=models.CASCADE) score = models.FloatField(null=True, blank=True) text_content = models.TextField(blank=True, default='')这个"可审计但不直接可见"的分寸感,是教务老师最满意的设计之一。做校园类系统,技术和业务之间经常隔着一层理解,这一层往往要通过好几轮沟通才能完全对齐。
4. 后端接口与权限控制的关键实现
4.1 基于角色的访问控制要写在公共层
评教系统的权限边界特别清晰:学生只能操作自己的评教任务,教师只能查看自己授课学生的反馈汇总,教务管理员可以配置一切,督导可以查看全院汇总但不能看具体学生ID。这些规则如果散落在每个视图函数里,后期维护就是灾难。我用一个自定义的 DRF 权限类统一控制。
from rest_framework.permissions import BasePermission class RolePermission(BasePermission): def has_permission(self, request, view): user = request.user if not user or not user.is_authenticated: return False allowed_roles = getattr(view, 'allowed_roles', []) if user.is_superuser or 'any' in allowed_roles: return True return user.user_type in allowed_roles视图里只要声明一行allowed_roles = ['student'],这个接口就只对学生开放。对象级别的权限(比如教师只能看自己的数据)再在get_queryset里过滤一次,形成双保险:
class TeacherResultViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [RolePermission] allowed_roles = ['teacher', 'supervisor', 'admin'] def get_queryset(self): user = self.request.user qs = CourseSchedule.objects.filter(evaluation_records__isnull=False).distinct() if user.user_type == 'teacher': qs = qs.filter(teacher=user.teacher_profile) return qs这里有一个我在实际开发中学到的点:权限规则宁可写得冗余,也不要在视图里到处留后门。任何"管理员临时豁免"的操作都应该走独立的超级管理员通道,而不是在整个路由里放开某个字段的可写权限。
4.2 评教提交接口:防重复的最后一层不能在代码上
学生评教最容易出现的线上事故是重复提交。有些学生连点两次提交按钮,或者两个浏览器标签页同时操作,后端如果只做了"先查有没有记录,再插入"的普通判断,很容易在同一毫秒内通过两次并发检查,生成两条记录。
常规代码层面可以用事务加select_for_update()来加锁,这个确实要写;但真正的兜底必须放在数据库约束上。我在EvaluationRecord.Meta里加了UniqueConstraint(fields=['task', 'course_schedule', 'student']),哪怕代码层漏了判断,数据库也会直接干翻第二条重复记录。具体实现是在事务里先锁行,再做业务判断:
from django.db import transaction from django.db.models import Q class EvaluationSubmitView(APIView): def post(self, request, task_id): payload = request.data course_id = payload.get('course_schedule_id') items = payload.get('items', []) user = request.user with transaction.atomic(): record, created = EvaluationRecord.objects.select_for_update().get_or_create( task_id=task_id, course_schedule_id=course_id, student=user, defaults={'trace_id': uuid.uuid4().hex} ) if not created and record.status == 'submitted': return Response({'detail': '该门课程已经评教,不能重复提交'}, status=400) # 重置旧明细,写入新明细 record.details.all().delete() for item in items: EvaluationDetail.objects.create( record=record, item_id=item['item_id'], score=item.get('score'), text_content=item.get('text', '') ) record.status = 'submitted' record.save(update_fields=['status']) return Response({'detail': '提交成功'}, status=200)这套逻辑在压测环境下跑了三轮才彻底稳定。第一轮只用前端禁用按钮,直接被并发打穿;第二轮加了代码层判断,并发下仍有概率漏掉;第三轮把数据库唯一约束加上,才算真正堵死。凡是涉及"一人一次"的业务,数据库约束永远是最后一道闸门。
4.3 统计报表:权重归一化与数据隔离
统计模块是整个系统最被"感激"的部分,因为教务处老师终于不用在 Excel 里做加权平均了。计算时要区分单项平均分和综合加权分。单项平均分不复杂,直接用 ORM 聚合;综合分则要把各指标的单项平均分乘上权重再归一化。
def calculate_task_score(task): items = task.items.prefetch_related('details') weighted_sum = 0.0 total_weight = 0.0 category_rows = {} for item in items: avg = item.details.aggregate(avg_score=Avg('score'))['avg_score'] or 0 weighted_score = avg * float(item.weight) weighted_sum += weighted_score total_weight += float(item.weight) category_rows[item.category] += weighted_score final_score = round(weighted_sum / total_weight, 2) if total_weight else 0 return final_score, category_rows这段代码在任务指标很多的时候会有 N+1 查询问题。统计量一大,就要改成一次values().annotate()分组聚合。实际优化时我用了(item__task=task).values('item__category').annotate(Avg('score'))这种写法,把几十次查询压到一两次。
教师的权限隔离在统计接口同样要贯彻。教师端报表接口必须返回CourseSchedule过滤后的结果集,并且序列化时不能输出学生任何身份字段。text 类型的反馈建议展示时统一抹掉可能包含学号、姓名、班级等身份信息的关键词,我这边是维护了一个敏感词列表,在入库和出库时做双层清洗。
5. 前端Vue3落地:从评教表单到可视化报表
5.1 学生端评教表单:要处理"没填完下次继续"的诉求
学生端的核心页面是评教表单,看起来是一个表单,实际状态量不小:任务列表要区分"待评教""已评教""未开始",每个任务进入以后要展示当前课程、授课教师和指标项,用户填到一半离开页面,下次进来还要能恢复草稿。
我用 Pinia 管理评教状态,关键数据先缓存到localStorage,提交成功后再清理。指标项渲染时根据score_type动态绑定:评分题用el-rate,单选用el-radio-group,文本题用el-input。Vue3 的reactive对象用来承载动态表单值,下标对应指标ID,非常直观:
<script setup> import { reactive, onMounted, computed } from 'vue' import { fetchTaskDetail, submitEvaluation, saveDraft } from '@/api/evaluation' const props = defineProps({ taskId: String }) const task = ref(null) const form = reactive({ scores: {}, texts: {} }) const currentIndex = ref(0) onMounted(async () => { task.value = await fetchTaskDetail(props.taskId) const draft = JSON.parse(localStorage.getItem(`eval_draft_${props.taskId}`) || '{}') Object.assign(form.scores, draft.scores || {}) Object.assign(form.texts, draft.texts || {}) }) async function handleSubmit() { const items = task.value.items.map(item => ({ item_id: item.id, score: form.scores[item.id] ?? null, text: form.texts[item.id] || '' })) await submitEvaluation(props.taskId, { course_schedule_id: task.value.course_schedule_id, items }) localStorage.removeItem(`eval_draft_${props.taskId}`) } </script>表单体验上的一个细节:评教任务列表页需要实时显示"已评/待评"状态,学生评完一门课以后列表要立刻更新。这个字段我建议直接由后端接口返回,前端不要自己维护状态,否则刷新页面后状态不一致容易造成焦虑。
5.2 教师端和管理员端:图表不是越花哨越好
教师端报表主要是雷达图和柱状图。雷达图展示教师在"教学态度、教学内容、教学方法、课堂管理、课程思政"等维度的平均得分,颜色点缀即可,不需要搞动画堆叠。柱状图展示同教研室内不同学期的纵向对比,让教师能看出自己这一届和上一届的评价变化。
管理员端更有意思一点,除了常规的表格和排名,还要做"异常预警"。比如某位教师连续两次评教得分落到全院后10%,或者某门课程的文字评价里出现了特定高频负面词,系统就自动标红提醒。这个功能虽然实现不复杂,但非常贴合教务处实际的管理需求。
图表库我用的是 ECharts。Vue3 里有对应的封装库,但直接用原生 ECharts 配合ref拿 DOM 节点也很顺手,更新图表时只要调用setOption就能完成响应式刷新。前端代码保持在"够用不过度设计"的节奏,千万别为了做图表动画无限引入依赖包,页面加载速度会明显变差。
5.3 前后端联调:认证头和错误码要提前约定
联调阶段最大的坑是认证信息不一致。后端发的是 JWT Token,前端要在 axios 请求拦截器里统一加入Authorization头,同时对于 401 响应用户要自动跳转登录页。错误码的设计要区分"参数校验失败""无权限""任务未开始/已结束""已评教不能重复提交"这些常见情况,前端根据不同的code才能给出准确提示,而不是所有错误一律弹出一个"系统错误"。
我在联调前先写了一份简单的接口文档,把所有接口的请求字段、响应结构和错误码列清楚。这份文档后来甚至被拿去给第三方合作团队参考,极大减少了沟通成本。
6. 部署与实测:只有上了生产环境才能遇到的那些坑
6.1 生产部署的标准操作
整个系统用 Nginx + Gunicorn 部署。Gunicorn 跑 Django 应用,Nginx 负责静态文件、反向代理和 HTTPS 终结。Django 的DEBUG = False是底线,生产环境的SECRET_KEY、数据库密码、Redis 连接字符串一律通过.env文件注入,不写死在代码里。
下一步就是python manage.py check --deploy,这个命令会把安全配置的问题列出来,包括 Host 头部校验、CSRF 信任域名、SSL 配置等。全流程里我强烈建议部署前跑一次,能避免很多低级安全漏铜。
数据库和静态文件的备份策略也别忽略。教务数据是学期性资产,评教结果涉及教师期末考核,备份做少了就是事故。我是用系统定时任务每天凌晨自动导出一次 MySQL 全量备份,保留最近 30 天,同时把备份文件同步到另外一台存储机。静态文件和上传文件同理。
6.2 坑一:时区把评教时间窗口算错
第一次测试评教任务时,设置了"11月5日 08:00 开始",结果早上 7 点就有学生发现可以评教了。排查后发现是 Django 的USE_TZ=True配合 MySQL 时区转换的经典问题:代码里用datetime.now()获取的是服务器本地时间,而数据库里存的是带时区的 UTC 时间,两者一比较就产生了偏差。
修正方案是统一用 Django 的timezone模块获取当前时间,并且在网络请求的入口处把用户传来的时间先转成aware的时间对象再入库。这个坑反复出现过两次,最后我在项目里写了一个统一的时间工具函数,禁止所有地方直接调用datetime.now():
from django.utils import timezone def now_local(): return timezone.localtime(timezone.now())6.3 坑二:并发提交重复评教在高并发测试中翻车
前文提过防重复提交的三层设计。这里我想再强调一次实际场景:评教任务开放的第一天上午,全年级几百个学生同时用手机和电脑同时登录系统。第一轮测试时前端按钮确实在点击后立即禁用,但总有几个学生开了两个标签页,或者连续快速点两个按钮,后端接受了两次请求,结果数据表里出现了两条相同课程、相同学生的评教记录。
这个问题的教训很直接:前端永远不能作为数据完整性的最终防线。前端的禁用按钮是为了用户体验,后端的事务锁是为了业务正确,数据库的唯一约束才是数据安全的保底。三层都齐了以后,我在另一台压测环境跑了一遍 500 并发,数据库零重复记录。
6.4 坑三:教师端页面升级后,浏览器缓存把旧资源反复加载
前端用 Vite 打包后默认会给文件名加上 hash 后缀,理论上不会有缓存问题。但有一次发布后发现部分教师打开的还是旧页面,排查到最后是 Nginx 对index.html设置了过长的Cache-Control,导致入口文件被缓存。修复方式是把index.html设置为不缓存或短缓存(比如no-cache),而带 hash 的静态资源可以设置一年的缓存时间。改造完以后每次发版,只要用户刷新一次就能看到新版本,不用再手动清缓存了。
6.5 坑四:安全红线自查清单
除了上面几个功能性坑,安全方面也有一份自查清单:
- CSRF:Django 开启 CSRF 中间件后,Vue 请求要带上
X-CSRFToken请求头。 - XSS:文本评价内容必须做富文本清洗。用户输入的内容里可能有标签、脚本片段,入库前过滤,出库时再转义一次,双保险。
- X-Frame-Options:Django 设置
X_FRAME_OPTIONS = 'DENY',防止页面被嵌入到其他站点。 - HTTPS:生产环境全站启用 HTTPS,Nginx 层做证书配置,Django 侧设置安全相关的响应头。
这套系统后续又加了一些新功能,比如督导巡课记录关联、学生评价申诉通道、评教结果的趋势画像,都是基于第一版稳定的模型骨架扩展出来的。回看整个项目,最有价值的不是代码本身,而是前期需求调研时把"匿名评教的数据边界""多角色报表权限""指标模板化"这几个关键问题想清楚了。如果你正在做类似的高校教学质量评估系统,我建议先花一周时间和使用方把流程走顺,再回来写第一行代码——这个投入绝对值得。
最后分享一个我在实际使用中的体会:无论前端框架怎么换、后端版本怎么升,评教系统这类业务的核心永远是“数据权限”和“流程状态”。把这两条主线把握住,哪怕以后把 Django 换成别的框架、把 Vue3 换成别的前端,系统的骨架都不会散。