简介:基于 Python 与 Django 开发的教师教学质量评价系统,是一套面向高校毕业设计、课程设计及期末大作业的完整源码包。系统按照学生评价、教师自评、数据统计与结果展示等环节设计前后端结构,适合希望借助完整项目快速理解 Django 分层思路或直接复用代码的同学。压缩包共 134 个文件,整体大小 3.78MB;其中 45 个 HTML 模板与 CSS、JavaScript 文件负责页面交互,41 个 Python 文件承载配置和业务逻辑,SQLite3/SQL 文件则提供现成数据库表及样例数据,导入后即可开展后续开发。前端部分使用了 Bootstrap 组件与自订样式,页面风格统一,后端按 Django 常规模式组织,模块边界清晰;项目经老师指导并作为高分毕业设计完成,源码与数据库齐全,可参考其评价流程设计、字段结构或作为二次开发底座。该资源在 CSDN 上已有 264 人学习,体量适中,对刚接触 Django 的初学者也较为友好。
1. 教师教学质量评价系统:一份能直接跑的 Django 毕业设计,先解决答辩和复现两件事
每年到毕业设计答辩季,总能见到这样的场景:PPT 里挂着系统截图,演示的时候打开浏览器,Django 开发服务器却起不来——不是缺依赖就是数据库没初始化,最后只能尴尬地翻源码里的 README。这份基于 Python 和 Django 的教师教学质量评价系统源码加数据库,恰好是把「能跑通」这件事放在了第一位。它不是一个只写了增删改查的壳子,而是把学生评教、教师查看结果、管理员管理评价维度和权重这三类角色完整串起来的管理系统,数据表里还给你备好了模拟的用户和评价记录,拉起服务就能看到效果。适合三类人:一是做毕业设计需要一套可以答辩、可以讲清楚业务逻辑的 Django 项目;二是想学 Django 但缺一个完整业务闭环的初学者;三是企业或学校内部想快速搭一个评教工具的开发者。本文会从数据建模、核心功能、统计可视化一路拆到部署和踩坑,目标是让你拿到压缩包后,按步骤半小时内跑起来,并且能讲明白每一行关键代码为什么这么写。
2. Django MTV 架构下的评价业务建模:从数据表设计到 ORM 映射
2.1 项目目录结构与 MTV 分层
拿到源码后,第一步不是急着runserver,而是先看清整个 Django 项目的物理结构。最常见的毕业设计 Django 项目布局是这样的:
TeacherEvaluation/ ├── manage.py ├── db.sqlite3 ├── evaluation/ # 主项目配置包 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── quality/ # 评价业务 app │ ├── models.py │ ├── views.py │ ├── admin.py │ ├── forms.py │ ├── migrations/ │ └── templates/quality/ ├── static/ │ ├── css/ │ └── js/ └── templates/ └── base.html这个结构里,manage.py是 Django 的命令行入口,所有迁移、启动、创建 app 的操作都经它执行;evaluation/是项目配置包,负责全局路由和设置;quality/是这个系统的核心业务 app,教学评价的一切逻辑都在这里。
Django 的 MTV(Model-Template-View)模式跟 MVC 的对应关系,在答辩时经常被问到:Model 对应数据表,Template 对应视图层(HTML 渲染),View 对应控制器逻辑。这个项目里,评价表的数据读写全在quality/models.py,页面渲染在templates/quality/下,路由和业务处理在views.py和urls.py。理解这个分层,比单纯背概念更能应对追问——比如评委问「查询评价结果时为什么不直接在模板里写 SQL」,你就可以回答「ORM 把数据库操作封装成了对象方法,模板只负责展示,数据在视图层准备好再传给模板」。
2.2 核心模型:用户角色、课程与评价记录的字段设计
评价系统绕不开四张核心表:用户表、课程表、评教记录表、评价维度表。Django 自带auth.User作为用户基础表,但学生、教师、管理员需要在基础用户上扩展角色,常见的做法是建一张Profile表,用OneToOneField跟User关联。
下面这段models.py是这类项目里最典型的写法:
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): """用户扩展表:通过 OneToOne 关联 Django 内置 User,区分学生和教师角色""" ROLE_CHOICES = ( ('student', '学生'), ('teacher', '教师'), ('admin', '管理员'), ) user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name='关联用户') role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='student', verbose_name='角色') student_id = models.CharField(max_length=20, blank=True, null=True, verbose_name='学号') def __str__(self): return f"{self.user.username} ({self.get_role_display()})" class Course(models.Model): """课程表:一门课由一位教师负责""" name = models.CharField(max_length=100, verbose_name='课程名称') teacher = models.ForeignKey(User, on_delete=models.CASCADE, related_name='courses', verbose_name='任课教师') semester = models.CharField(max_length=20, verbose_name='学期', default='2024-2025-1') class Meta: verbose_name = '课程' verbose_name_plural = '课程' class EvaluationDimension(models.Model): """评价维度:比如教学态度、课堂互动、作业批改等打分项""" name = models.CharField(max_length=50, verbose_name='维度名称') weight = models.DecimalField(max_digits=4, decimal_places=2, default=1.00, verbose_name='权重') description = models.TextField(blank=True, verbose_name='维度说明') class EvaluationRecord(models.Model): """评教记录:学生给某门课的一位教师打分,一个学生同一门课只能评一次""" student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='given_evaluations', verbose_name='评教学生') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='evaluations', verbose_name='被评课程') dimension = models.ForeignKey(EvaluationDimension, on_delete=models.CASCADE, null=True, blank=True, verbose_name='评价维度') score = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)], verbose_name='评分(1-5)') comment = models.TextField(blank=True, verbose_name='评语') created_at = models.DateTimeField(auto_now_add=True, verbose_name='评教时间') class Meta: unique_together = ('student', 'course') verbose_name = '评价记录' verbose_name_plural = '评价记录'逻辑说明:Profile不新建用户表,而是用OneToOneField扩展User,这样auth.authenticate登录验证可以直接用,不用重写认证逻辑;EvaluationRecord里的ForeignKey指向课程和用户,保证了评价的对象边界。重点是unique_together = ('student', 'course'),这行在数据库层面就杜绝了一个学生对同一门课重复评教——答辩时一定要能说出这行的作用,这是评委提问的高频点。
参数说明:score用了IntegerField加choices限制在 1 到 5 分,比用FloatField更干净,前端渲染时对应单选按钮,后端校验也不可能出现 5.5 分这种脏数据。weight用DecimalField而不是FloatField,是为了做加权平均时不产生二进制浮点误差。
2.3 数据库初始化:迁移、超级管理员与初始数据
模型定义好了,要让表真真实实出现在数据库里,必须走迁移流程。db.sqlite3文件在仓库里即使已经存在,也可能跟当前代码的模型不同步——这是源码包最常见的坑,后面避坑章会细说。正确的初始化顺序如下:
# 1. 进入项目根目录,激活虚拟环境(如果有) cd TeacherEvaluation pip install -r requirements.txt # 2. 把 models.py 的变更生成迁移文件 python manage.py makemigrations quality # 3. 执行迁移,创建数据表 python manage.py migrate # 4. 创建管理员账号 python manage.py createsuperuser # 5. 如果需要默认课程和维度数据,导入项目自带的 initial_data.json python manage.py loaddata initial_data.json # 6. 启动开发服务器 python manage.py runserver 0.0.0.0:8000逻辑说明:makemigrations是把模型的字段变更记录成迁移脚本,migrate才真正把脚本应用到数据库。如果源码包里的db.sqlite3和models.py不一致,直接runserver虽然不报错,但访问页面时会出现「no such column」——所以强烈建议拿到源码后删掉旧的db.sqlite3,从零迁移一遍。
参数说明:runserver 0.0.0.0:8000里的0.0.0.0是让开发服务器监听所有网卡,方便局域网内其他设备访问,比如答辩时用投影仪连接自己的电脑。如果只想本机访问,python manage.py runserver就够用。注意 Django 开发服务器的runserver只适合本地调试,并发能力很弱,正式部署得上 gunicorn 或 uWSGI,这个后面部署章节展开。
3. 核心功能模块拆解:评教流程、权限控制与后台管理
3.1 登录鉴权与角色分发:为什么用装饰器而不是手写 session 判断
评价系统有三个角色,但 Django 的login_required装饰器只区分「登录」和「未登录」,不区分具体角色。所以需要在登录后把用户角色写进 session 或每次从Profile表查询。
登录视图的views.py片段:
from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect def user_login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) # 登录成功后根据角色跳转到不同首页 profile = Profile.objects.get(user=user) if profile.role == 'teacher': return redirect('teacher_dashboard') elif profile.role == 'admin': return redirect('admin_dashboard') else: return redirect('student_dashboard') else: return render(request, 'quality/login.html', {'error': '用户名或密码错误'}) return render(request, 'quality/login.html') @login_required def student_dashboard(request): """学生首页:列出当前学期所有未评教的课程""" profile = Profile.objects.get(user=request.user) if profile.role != 'student': return redirect('user_login') courses = Course.objects.filter(semester='2024-2025-1') # 过滤掉已经评过的课程 rated_course_ids = EvaluationRecord.objects.filter( student=request.user ).values_list('course_id', flat=True) pending_courses = courses.exclude(id__in=rated_course_ids) return render(request, 'quality/student_dashboard.html', { 'courses': pending_courses, 'rated_count': len(rated_course_ids), })逻辑说明:authenticate做用户名密码校验,通过后login(request, user)写入 session。角色判断不能只靠前端隐藏按钮——后端每次请求都要查Profile确认角色,否则学生改一下 URL 就能访问教师的评分汇总页面。这里exclude(id__in=rated_course_ids)是「待评教列表」的核心逻辑,把已评过的课程 ID 集合取出来,从全部课程里排除,剩下的才是学生要评的。
参数说明:values_list('course_id', flat=True)返回一个由 course_id 组成的扁平列表,flat=True表示不要元组形式的[(1,), (2,)],直接得到[1, 2]。这个写法比all()再遍历取 ID 高效,数据量大时差距明显。login_required装饰器在未登录时会自动跳转到settings.LOGIN_URL指定的地址,这个配置在settings.py里设置成'/login/'即可。
3.2 打分表单与后端校验收口:拒绝绕过页面的脏数据
评教页面的表单是 DjangoForm类,不是手写 HTML 后自己解析 POST。用 Form 类有两个好处:渲染字段时可以生成统一 HTML,校验时自动执行字段规则。
forms.py片段:
from django import forms from .models import EvaluationRecord class EvaluationForm(forms.Form): """评教表单:用 Form 而不是 ModelForm,便于校验多维度打分""" # 每个维度一个评分字段,动态生成 score_teaching_attitude = forms.IntegerField( min_value=1, max_value=5, widget=forms.RadioSelect(choices=[(i, f'{i}分') for i in range(1, 6)]), label='教学态度' ) score_class_interaction = forms.IntegerField( min_value=1, max_value=5, widget=forms.RadioSelect(choices=[(i, f'{i}分') for i in range(1, 6)]), label='课堂互动' ) score_homework_review = forms.IntegerField( min_value=1, max_value=5, widget=forms.RadioSelect(choices=[(i, f'{i}分') for i in range(1, 6)]), label='作业批改' ) comment = forms.CharField(widget=forms.Textarea, required=False, label='评语')当min_value=1, max_value=5写在IntegerField里时,Django 在is_valid()阶段就会自动拦截 0 分或 6 分的异常请求,不需要在视图层再手写 if 判断。这样前端即便有人用 Postman 直接提交score_teaching_attitude=100,在后端也会被 Form 的校验器挡掉。
对应的视图处理,注意这里要拿着course_id检查这个学生是否真的能评这门课:
from django.shortcuts import get_object_or_404 from .models import Course, EvaluationRecord, Profile from .forms import EvaluationForm @login_required def submit_evaluation(request, course_id): profile = Profile.objects.get(user=request.user) if profile.role != 'student': return redirect('user_login') course = get_object_or_404(Course, id=course_id) # 检查是否重复评教 if EvaluationRecord.objects.filter(student=request.user, course=course).exists(): return render(request, 'quality/error.html', {'message': '你已经评过这门课了'}) if request.method == 'POST': form = EvaluationForm(request.POST) if form.is_valid(): # 将表单字段写入评价记录表 for dimension_name, score in [ ('教学态度', form.cleaned_data['score_teaching_attitude']), ('课堂互动', form.cleaned_data['score_class_interaction']), ('作业批改', form.cleaned_data['score_homework_review']), ]: dimension, _ = EvaluationDimension.objects.get_or_create(name=dimension_name) EvaluationRecord.objects.create( student=request.user, course=course, dimension=dimension, score=score, comment=form.cleaned_data['comment'], ) return redirect('student_dashboard') else: form = EvaluationForm() return render(request, 'quality/evaluate.html', {'form': form, 'course': course})这里有一个容易被答辩老师抓住的点:上面的写法把「一次评教」拆成了「按维度插入多条EvaluationRecord」,但模型里unique_together = ('student', 'course')会阻止第二次插入——因为维度不同但学生和课程相同,唯一约束直接报错。所以真正稳妥的做法是:评价记录只存一条,用单独的表存各维度的分数,或者在unique_together里加上dimension。见下面的避坑章,这是这个项目里最常见的逻辑翻车点。
3.3 Admin 后台:白嫖 Django 内置管理界面做数据维护
Django 自带的后台不算「开发」功能,但毕业设计里很讨巧——用最小代码实现教务处管理员的课程和教师管理。
admin.py配置:
from django.contrib import admin from .models import Profile, Course, EvaluationDimension, EvaluationRecord @admin.register(Profile) class ProfileAdmin(admin.ModelAdmin): list_display = ('user', 'role', 'student_id') search_fields = ('user__username',) @admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ('name', 'teacher', 'semester') search_fields = ('name', 'teacher__username') list_filter = ('semester',) @admin.register(EvaluationDimension) class EvaluationDimensionAdmin(admin.ModelAdmin): list_display = ('name', 'weight', 'description') @admin.register(EvaluationRecord) class EvaluationRecordAdmin(admin.ModelAdmin): list_display = ('student', 'course', 'dimension', 'score', 'created_at') list_filter = ('course__semester', 'score')逻辑说明:@admin.register装饰器替代了旧的admin.site.register写法,让代码更整洁。search_fields里user__username的双下划线表示跨表查询,学生姓名搜索会 join 到 User 表的 username 字段。后台默认 URL 是/admin/,用第 2 章创建的超级管理员登录即可看到全部数据表。
这块内容的答辩价值在于:你不需要跟评委说自己实现了教务处管理界面,而是说「利用 Django Admin 自动生成后台做数据维护,自定义了列表筛选和搜索字段」,这是 Django 实战中非常合理的取舍,比硬写一个残缺的后台管理页更有说服力。
4. 评价结果统计与可视化:聚合查询、加权平均与导出报表
4.1 用 aggregate 和 annotate 算平均分,别在 Python 里手算
评教系统的核心价值不在录入,而在统计。管理员页面需要看到每门课、每位教师的平均分,用aggregate做整表聚合、annotate按分组聚合。
教师端查看自己课程得分的视图:
from django.db.models import Avg, Count, F from django.db.models.functions import Coalesce @login_required def teacher_dashboard(request): profile = Profile.objects.get(user=request.user) if profile.role != 'teacher': return redirect('user_login') courses = Course.objects.filter(teacher=request.user) stats = [] for course in courses: # 对每门课计算平均分和评教人数 agg = EvaluationRecord.objects.filter(course=course).aggregate( avg_score=Coalesce(Avg('score'), 0.0), eval_count=Count('id'), ) stats.append({ 'course': course, 'avg_score': round(agg['avg_score'], 2), 'eval_count': agg['eval_count'], }) return render(request, 'quality/teacher_dashboard.html', {'stats': stats})逻辑说明:Avg('score')是在数据库层面执行的AVG()聚合,比把记录全查出来在 Python 里sum/len快得多。Coalesce是数据库函数,当没有评教记录时Avg返回NULL,Coalesce(..., 0.0)把NULL变成 0,避免模板里报错。
参数说明:annotate和aggregate的区别经常被问——aggregate返回一个字典,是整个表的聚合结果;annotate是给每个分组生成一个聚合列,返回一个 QuerySet。上面代码用的是循环里aggregate,数据量小时没问题,但如果课程多,更好的写法是Course.objects.filter(teacher=request.user).annotate(avg_score=Avg('evaluations__score')),一次查询拿到所有课程的平均分,避免 N+1 查询问题。答辩时主动提一句「这里可以优化成 annotate 减少查询次数」,能明显加分。
4.2 加权平均:处理不同维度权重不同的事项
如果评价维度表里有weight字段,教学态度权重 0.4、课堂互动 0.3、作业批改 0.3,那平均分不能简单加和除以维度数。加权平均的逻辑如下:
from django.db.models import F, Sum def calculate_weighted_score(course): """计算某门课的加权平均分:总分 = SUM(score * weight) / SUM(weight)""" records = EvaluationRecord.objects.filter(course=course).select_related('dimension') weight_sum = 0.0 score_weight_sum = 0.0 for record in records: w = float(record.dimension.weight) weight_sum += w score_weight_sum += record.score * w return round(score_weight_sum / weight_sum, 2) if weight_sum > 0 else 0.0逻辑说明:这里必须用select_related('dimension')预取外层关联,否则每次record.dimension都会触发一次数据库查询——一门课被评 50 次就有 51 条 SQL,这是个典型的 N+1 性能反例。加上了select_related后,Django 用一条LEFT JOIN把维度数据带回来。
血泪经验:权重字段如果用FloatField,计算 0.4 + 0.3 + 0.3 这类小数,Python 侧会得到 0.9999999999999999,最终加权分数差 0.01。所以在模型层用DecimalField,计算时就稳定了。如果数据库里已经是FloatField,计算前先用round(w, 4)修正,或者用Decimal(str(w))转换。
4.3 把统计结果可视化:折线趋势和横向对比
毕业设计里不引入 ECharts 全家桶也能做出饼图和柱状图——Django 模板里渲染数据,交给前端一个轻量图表库即可。
模板里使用 Chart.js 的典型写法:
{% if stats %} <div style="width: 600px; height: 400px;"> <canvas id="scoreChart"></canvas> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <script> const ctx = document.getElementById('scoreChart').getContext('2d'); const scoreData = { labels: {{ course_names|safe }}, // ['高等数学', '大学英语', ...] datasets: [{ label: '平均分', data: {{ avg_scores|safe }}, // [4.2, 3.8, ...] backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }; new Chart(ctx, { type: 'bar', data: scoreData, options: { scales: { y: { min: 0, max: 5, ticks: { stepSize: 1 } } } } }); </script> {% endif %}逻辑说明:{{ course_names|safe }}和{{ avg_scores|safe }}是视图传过来的 JSON 数组,但 Django 模板会自动转义法字符,导致[变成[,所以必须加|safe过滤器告诉系统「这段内容已经是可信任的 JS 字面量」。视图里需要先用json.dumps把 Python 列表转成 JSON 字符串再传给模板——直接传列表,模板渲染出来的是['高等数学', '大学英语'],JS 不认这个语法。
参数说明:scale里max: 5是因为评价分数是 1 到 5,图表纵轴跟业务分数对齐,评委看图的时不会误读。stepSize: 1强制刻度每次递增 1,防止 Chart.js 自动生成 0.5 步长显得不专业。
4.4 导出 Excel 报表:答辩加分的小工具
统计看完还要能下载,csv模块是 Django 里导出报表最轻量的方式,不需要安装openpyxl或pandas:
import csv from django.http import HttpResponse @login_required def export_course_report(request, course_id): course = Course.objects.get(id=course_id) response = HttpResponse(content_type='text/csv; charset=utf-8') response['Content-Disposition'] = f'attachment; filename="course_{course_id}_report.csv"' writer = csv.writer(response) writer.writerow(['学生用户名', '课程', '维度', '分数', '评语', '时间']) records = EvaluationRecord.objects.filter(course=course).select_related('student', 'dimension') for record in records: writer.writerow([ record.student.username, course.name, record.dimension.name if record.dimension else '整体评价', record.score, record.comment, record.created_at.strftime('%Y-%m-%d %H:%M'), ]) return response逻辑说明:content_type='text/csv; charset=utf-8'是防止 Excel 打开 CSV 时中文乱码的关键——光有utf-8不够,Excel 识别 UTF-8 CSV 需要文件头带 BOM,更稳妥的做法是response.write('\ufeff')加 BOM 头。CSV 导出比 Excel 二进制格式好维护,答辩演示时用 Excel 打开 CSV 无压力,而且不依赖第三方库。
5. 从源码到运行环境:依赖安装、数据库同步与五个高频避坑点
5.1 环境一致性检查:requirements.txt 与虚拟环境是第一条命
很多人拿到源码后直接python manage.py runserver,然后撞上一堆ModuleNotFoundError。根因几乎都是全局 Python 环境里没装对依赖,或者 Django 版本跟代码语法不兼容。
先看requirements.txt里典型的三行:
Django>=3.2,<4.0 mysqlclient==2.1.1 Pillow==9.4.0逻辑说明:Django>=3.2,<4.0限制了主版本,因为 Django 4.x 修改了部分urls.py的语法和USE_L10N等配置项,如果代码是照着 3.2 写的,直接装 4.2 可能会报错。mysqlclient是 MySQL 驱动,如果你用默认的 SQLite,这一行可以去掉,但留着也不影响。Pillow 是处理用户头像或图片验证码的依赖,不是每个项目都用。
参数说明:版本号里>=3.2,<4.0是冒号左边的精确约束,pip install -r requirements.txt会根据这个范围选择最新符合的版本,而不是随便装一个最新版。如果你拿到源码后不确定依赖版本是否兼容,最快的自检命令是python -m django --version和pip list | grep -i -E "django|mysql"。
避坑建议:永远用虚拟环境。python -m venv venv创建,source venv/bin/activate激活(Windows 是venv\Scripts\activate),然后在虚拟环境里装依赖。这样即使系统 Python 里装了其他版本的 Django,也不会互相污染——这个问题在毕业设计评审现场出现过太多次了。
5.2 避坑清单:五条让源码跑不起来的经典问题
坑一:数据库表结构和代码不同步
现象:runserver能启动,但访问学生首页时报OperationalError: no such column: quality_evaluationrecord.comment,或者后台看到的数据表对不上。
原因:源码包里的db.sqlite3是作者写好时的状态,但它只对应作者电脑上当时的models.py。你拿到的代码可能经历了多次改动,迁移文件版本错位,导致表缺列或多列。
解决:不要尝试保留旧数据,直接删掉数据库文件从零迁移。rm db.sqlite3后重新makemigrations+migrate。如果还有问题,把quality/migrations/目录里的迁移文件全部删掉,保留__init__.py,再重新生成一套干净迁移。
坑二:CSRF 验证失败
现象:提交评教表单时页面报CSRF verification failed,或者显示403 Forbidden。
原因:Django 默认开启 CSRF 中间件,所有 POST 请求的模板必须包含{% csrf_token %}。手写 HTML 表单时经常漏掉这行。
解决:在evaluate.html的<form>标签内、所有表单字段之前加上{% csrf_token %}。特别注意,fetch或axios发 POST 时需要在请求头里带 CSRF Token,从 cookie 里取csrftoken加上去,不能只在模板里放 token 就不管 AJAX 请求。
坑三:unique_together约束导致评教时插入报错
现象:学生提交评教后页面报IntegrityError: UNIQUE constraint failed: quality_evaluationrecord.student_id, quality_evaluationrecord.course_id。
原因:这是第 3 章埋的伏笔——如果视图逻辑按维度拆多条记录插入,但唯一约束是「学生 + 课程」,第二次插入直接违例。
解决:两种改法。第一种是unique_together改为('student', 'course', 'dimension'),同一门课允许不同维度各评一次;第二种(我更推荐这个项目的做法)是把维度分数存成 JSON 字段或单独建EvaluationScore表,EvaluationRecord只存学生、课程、评语、总体分,唯一约束保留('student', 'course')。具体改法取决于原始models.py怎么设计的,这一步不预判,拿到源码先跑一次评教流程看报错再改。
坑四:静态文件 404,CSS/JS 全部丢失
现象:登录页面纯 HTML 没有样式,浏览器控制台一堆GET /static/css/style.css 404。
原因:settings.py里DEBUG = False时 Django 不再自动伺服静态文件,runserver也不会帮你找static/目录。
解决:在settings.py设置STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]、STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles'),然后执行python manage.py collectstatic。答辩演示时如果不想配置这些,可以直接把DEBUG = True打开,开发模式下静态文件会自动伺服,但一定要知道这不是正式部署的做法。
坑五:中文数据乱码或数据库操作报字符集错误
现象:在 admin 后台输入中文课程名保存无问题,但通过脚本写数据或用 MySQL 时存入的中文变成???或直接报Incorrect string value。
原因:SQLite 默认 UTF-8 基本没事,但 MySQL 建库时如果没指定utf8mb4,中文会丢。Django 的 MySQL 连接也需要在settings.py里设置OPTIONS: {'charset': 'utf8mb4'}。
解决:如果是 MySQL,建库语句写CREATE DATABASE evaluation CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,同时在DATABASES配置里加'OPTIONS': {'charset': 'utf8mb4'}和'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"。SQLite 很少遇到这个坑,遇到也是因为读取了非 UTF-8 编码的旧数据文件。
5.3 MySQL 切换:把 SQLite 换成项目要求的数据库
很多学校毕业设计会要求用 MySQL,数据库配置切换是个必经步骤。
settings.py里的数据库配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'evaluation_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } }逻辑说明:ENGINE从django.db.backends.sqlite3换到mysql时,NAME不再是文件路径而是数据库名,HOST和PORT指向 MySQL 服务。跑迁移前必须先手动创建好evaluation_db这个库,否则migrate会报Unknown database。
参数说明:init_command里设STRICT_TRANS_TABLES是让 MySQL 执行严格模式,插入超长字符串或非法日期时直接报错而不是静默截断,这能帮你提前发现脏数据写入的问题,而不是答辩的时候数据莫名其妙少了几条。
6. 拿到源码先干这件事:用 Django 的测试客户端跑一遍评教闭环
不要依赖肉眼打开浏览器点来点去验证功能——写两个简单的测试用例,把「学生登录 → 选课 → 打分 → 教师看到平均分」这条链路自动跑通,比任何手动测试都可靠。Django 自带测试客户端,不需要额外装 selenium。
在quality/tests.py里写一个最小闭环测试:
from django.test import TestCase from django.contrib.auth.models import User from .models import Profile, Course, EvaluationRecord, EvaluationDimension class EvaluationFlowTest(TestCase): def setUp(self): # 创建学生、教师、课程和评价维度 self.student_user = User.objects.create_user(username='stu001', password='pass123') Profile.objects.create(user=self.student_user, role='student', student_id='2021001') self.teacher_user = User.objects.create_user(username='teacher01', password='pass123') Profile.objects.create(user=self.teacher_user, role='teacher') self.course = Course.objects.create( name='高等数学', teacher=self.teacher_user, semester='2024-2025-1' ) self.dim = EvaluationDimension.objects.create(name='教学态度', weight='0.40') def test_student_rates_course(self): # 学生登录 self.client.login(username='stu001', password='pass123') # 提交评教表单 response = self.client.post(f'/evaluate/{self.course.id}/', { 'score_teaching_attitude': 5, 'score_class_interaction': 4, 'score_homework_review': 5, 'comment': '讲得很清楚', }) self.assertEqual(response.status_code, 302) # 重定向到学生首页 # 验证评价记录写入 record_exists = EvaluationRecord.objects.filter( student=self.student_user, course=self.course ).exists() self.assertTrue(record_exists) # 重复提交应失败 response2 = self.client.post(f'/evaluate/{self.course.id}/', { 'score_teaching_attitude': 3, 'score_class_interaction': 3, 'score_homework_review': 3, 'comment': '重复评教', }) self.assertNotEqual(response2.status_code, 302)跑测试的命令一行:
python manage.py test quality逻辑说明:setUp在每个测试方法执行前重新创建干净数据,保证测试之间互不干扰。self.client.login是 Django 测试客户端的登录方法,模拟的是浏览器 session,不走真实密码加密延迟。POST 提交后断言302是重定向到学生首页的正常状态码;如果视图里的重复检查生效,第二次 POST 应该是 200(渲染错误页)。
参数说明:测试用的 URL 是/evaluate/{course_id}/,这跟urls.py里的path('evaluate/<int:course_id>/', ...)对应,写死进断言里没关系,因为这是测试当前项目,不是测试通用库。
这个闭环测试覆盖了最核心的业务逻辑:细审之后我可以很有底气地说——系统跑通不是靠「感觉没问题」,而是靠测试脚本告诉我「评教流程 OK」。从那以后我每次拿到新的 Django 毕业设计源码,都强制先写一遍这个闭环测试再考虑改功能,改完代码跑一次能省掉至少两小时的纯手工点按验证,这个习惯比任何调试技巧都实在。希望帮到你。
本文还有配套的精品资源,点击获取