
简介这是一套基于Python Django框架的学生成绩管理系统项目面向计算机相关专业课程设计、毕业设计及Django初学者。系统覆盖首页、个人中心、教师管理、学生管理、公告信息、课程类型、课程信息、选课信息和成绩信息等核心模块并借助数据可视化方式展示成绩统计分析结果适合用来理解后台管理系统的完整开发链路。压缩包共360个文件约8.35MB包含39个Python源码文件、41个Vue前端组件、21个JavaScript脚本、15张PNG和14张JPG图片、2个SQL数据库脚本以及批量安装/运行/打包用的bat脚本svg等图标资源用于后台界面美化。已有226人学习/下载。从压缩包中可获得可运行的项目源码、数据库初始化脚本、前端页面组件和部署启动配置既能作为课程设计或毕业设计的参考模板也能在此基础上扩展功能加深对DjangoVue前后端分离开发模式的理解。1. 为什么学生成绩管理要选 Django 数据可视化一个学生成绩管理系统如果只做增删改查那和 Excel 没有本质区别。真正让系统产生价值的是把成绩数据变成图表和指标比如班级平均分趋势、课程及格率、成绩分布直方图。Python 生态里 Django 的 ORM、Admin 后台和认证体系能让权限、选课、成绩录入这些模块快速成型前端再配合 ECharts 做可视化是目前比较务实的组合。这个项目覆盖了首页、个人中心、教师管理、学生管理、公告信息、课程类型、课程信息、选课信息、成绩信息等功能模块适合两种人一是刚学完 Python 基础想动手做完整项目的初级开发者二是已经用过 Django 但没做过前后端图表联调的后端工程师。我接下来会从数据模型设计讲到可视化接口、权限控制再到一键启动脚本把整个项目拆开看。2. 成绩管理系统的数据模型设计与 Django ORM 查询优化2.1 模型字段与关系设计成绩管理首先要理清关系。课程类型和课程是一对多课程和学生是多对多这个多对多关系我建议通过选课记录来承载而不是 Django 自动生成的中间表。因为成绩必须绑定在某次选课上一条选课记录对应一个成绩这样后续写排名、平均分查询都清晰。from django.db import models from django.contrib.auth.models import User class CourseType(models.Model): name models.CharField(课程类型, max_length32) class Course(models.Model): name models.CharField(课程名称, max_length64) course_type models.ForeignKey(CourseType, on_deletemodels.PROTECT, related_namecourses) teacher models.ForeignKey(User, on_deletemodels.CASCADE, related_namecourses) class Student(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent_profile) student_no models.CharField(学号, max_length20, uniqueTrue) class_name models.CharField(班级, max_length32) class SelectCourse(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_nameselects) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameselects) score models.DecimalField(成绩, max_digits5, decimal_places2, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (student, course) class Announcement(models.Model): title models.CharField(标题, max_length100) content models.TextField(内容) created_at models.DateTimeField(auto_now_addTrue)on_delete参数的选择比很多人想的重要。CourseType被删除时用PROTECT一旦有课程引用这个类型Django 会抛出ProtectedError阻止删除防止课程和成绩变成无关联的孤儿数据。Student.user用OneToOneField扩展 Django 自带的 User这样登录认证逻辑完全复用 Django教师也能在这个基础上加Profile角色字段。Announcement是独立的公告模型不依赖业务表后台只要给出标题和内容的录入界面就能满足公告管理需求。2.2 选课与成绩的 ORM 查询写法2.2.1 按课程统计平均分成绩管理最常见的需求是各课程平均分排名用 Django ORM 的聚合查询可以一次搞定from django.db.models import Avg, Count, Max, Min result SelectCourse.objects.filter(score__isnullFalse) \ .values(course_id, course__name) \ .annotate(avg_scoreAvg(score), max_scoreMax(score), min_scoreMin(score), cntCount(id)) \ .order_by(-avg_score)filter(score__isnullFalse)先过滤掉缺考记录否则Avg会忽略 NULL计算出来的平均分其实是对的但Count(id)会把缺考也计入导致人数和平均分对不上。values(course_id, course__name)指定分组字段annotate生成聚合结果对应 SQL 里的GROUP BY course_id, course_name。order_by(-avg_score)按平均分降序方便排名展示。2.2.2 学生成绩波动查询要看某个学生最近两次考试成绩变化用时间排序后切片records (SelectCourse.objects .filter(student_id1024, score__isnullFalse) .order_by(-created_at)[:2]) if len(records) 2: last, prev records[0], records[1] delta float(last.score) - float(prev.score)order_by(-created_at)按录入时间倒序切片[:2]在 ORM 层面对应 SQL 的LIMIT 2不会把全量记录载入内存。注意DecimalField返回Decimal类型相减结果也是Decimal前端 JSON 序列化时可能报错这里先转成float再抛给接口。2.3 常用查询参数与索引陷阱当选课记录超过一万条很多看起来正常的查询会变慢。下表列出这个系统里容易出问题的查询场景和优化思路。查询场景Django 写法主要风险索引建议按课程查选课成绩filter(course_id5)外键有默认索引但values(course__name)会 JOIN确认外键列已有db_indexTrue按学号查学生filter(student__student_no2024001)跨表过滤产生 JOINStudent.student_no建唯一索引按创建时间排序order_by(-created_at)大表排序走全表扫描给created_at加索引按分数范围过滤filter(score__gte60, score__lt70)范围查询无法命中索引末尾单独建索引或按分数段单独建表调试索引是否生效我一般把 ORM 翻译后的 SQL 拿到 MySQL 里 EXPLAIN。还有一个容易踩的坑select_related必须在values之前调用写成values(course__name)时 ORM 不会自动做 JOIN 优化很多初学者就在这里多出 N1 次查询。删除对象同样有讲究。不要直接把整个 QuerySet 传给delete()先取出 id 列表再确认ids SelectCourse.objects.filter(course_id5).values_list(id, flatTrue) # 业务确认后 deleted SelectCourse.objects.filter(id__inids).delete()values_list(id, flatTrue)返回一维 id 列表delete()返回(total, {app_label.model_name: count})元组可以从中读到每个模型删了多少条用于审计日志。直接filter(...).delete()在 Django 里会级联删除关联对象范围不可控批量操作务必分两步。3. 数据可视化从 QuerySet 到图表打通 Django 与 ECharts3.1 为什么不用 Django Admin 直接展示数据Django Admin 适合录数据不适合做分析。Admin 的list_display只能展示字段值要做成绩分布直方图就得重写模板成本比单独写接口高得多。而且 Admin 的查询集是ModelAdmin.queryset限制较多很难在一个页面上同时展示两个统计维度。实际操作中这个项目采用的是前后端接口分离Django 只负责把聚合后的 JSON 抛给前端ECharts 在浏览器端渲染。这种方式和“djangovue 前后端分离”项目的思路一致只是把 Vue 换成原生 JS 也能跑通。3.2 后端 JSON 接口封装写成绩分布接口时先确定分数段再分别计数from django.http import JsonResponse from django.db.models import Q from .models import SelectCourse def score_distribution_api(request): course_id request.GET.get(course_id, ) if not course_id: return JsonResponse({error: missing course_id}, status400) queryset SelectCourse.objects.filter(course_idcourse_id, score__isnullFalse) ranges [ (0-59, Q(score__lt60)), (60-69, Q(score__gte60, score__lt70)), (70-79, Q(score__gte70, score__lt80)), (80-89, Q(score__gte80, score__lt90)), (90-100, Q(score__gte90)), ] return JsonResponse({label: queryset.filter(cond).count() for label, cond in ranges})同一个Q对象中多个条件默认是 AND 关系Q(score__gte60, score__lt70)生成score 60 AND score 70。循环里执行 5 次 COUNT 查询课程量大时可以用CASE WHEN一次统计但对当前数据量来说清晰比性能更重要。接口收到非法course_id时返回 400前端根据状态码提示用户。3.3 前端 ECharts 绘制成绩分布与趋势3.3.1 成绩分布直方图前端用 fetch 拉取接口再喂给 EChartsfetch(/api/score_distribution/?course_id${courseId}) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(scoreChart)); chart.setOption({ xAxis: { type: category, data: Object.keys(data) }, yAxis: { type: value, name: 人数 }, series: [{ type: bar, data: Object.values(data), barWidth: 40, itemStyle: { color: #5470c6 } }] }); });接口返回的是一个普通对象Object.keys拿到分数段标签Object.values拿到人数。Python 3.7 之后字典保持插入顺序所以标签顺序和接口定义一致。如果接口读的是 Redis 缓存或 MongoDB 聚合结果顺序不保证这时应该让后端返回数组结构[{label:0-59, value: 12}]前端用map解析。3.3.2 班级平均分趋势折线要展示某个班级各门课程的平均分后端这样写def class_trend_api(request): class_name request.GET.get(className, ) data (SelectCourse.objects .filter(student__class_nameclass_name, score__isnullFalse) .values(course__id, course__name) .annotate(avg_scoreAvg(score)) .order_by(course__id)) return JsonResponse(list(data), safeFalse)分组用course__id而不是名称避免同名课程跨学期被合并。接口返回的列表里包含course_id、course_name、avg_score三个字段前端折线图的 X 轴直接取course_nameY 轴取avg_score。3.4 参数说明与常见误区ECharts 配置项不正确图表会白屏或位置错乱。下表列出这个项目里最容易出问题的几个参数。配置项作用错误示例正确设置timeoutfetch请求超时时间不设置默认无限制AbortController或 axios timeout 10sbarWidth柱状图宽度不设小屏挤压设固定像素或百分比yAxis.name纵坐标单位缺失看不出人数/分数添加name: 人数或分数chart.resize()容器尺寸变化重绘弹窗中图表宽度为 0nextTick后调用resize()弹窗或折叠面板里初始化图表经常出现宽度为 0。因为容器在display:none时宽度无法测量ECharts 默认给了 0。解决办法是在容器可见后再初始化或者监听window.resize调chart.resize()。另外接口返回角落别直接信任前端传的course_id后端也要验证课程是否存在避免注入字符把filter(course_id...)变成奇怪的条件。4. 教师端和学生端权限控制用 Django 内置认证还是自定义4.1 用户模型与角色区分在这个系统里教师、学生、管理员能看到的页面和数据范围完全不同。Django 内置的User提供了is_staff和is_superuser但区分教师和学生需要额外字段。常见做法是通过Profile扩展一个role字段而不是换掉AUTH_USER_MODEL因为项目已经跑起来中途换自定义用户模型代价很大。from django.db import models from django.contrib.auth.models import User class Profile(models.Model): ROLE_CHOICES [(teacher, 教师), (student, 学生)] user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) role models.CharField(角色, max_length10, choicesROLE_CHOICES, defaultstudent) teacher_no models.CharField(教师工号, max_length20, blankTrue)角色与可访问接口的关系如下表。角色可以查看可以操作不可访问管理员所有页面教师、学生、课程、公告管理无教师自己授课的课程、选课名单、成绩录入/修改成绩其他教师课程数据学生自己的选课和成绩选课、密码修改他人成绩、所有管理页4.2 登录、注册与权限校验的完整实现登录不用自己写密码比对用 Django 的authenticate和loginfrom django.contrib.auth import authenticate, login def login_view(request): if request.method POST: user authenticate(request, usernamerequest.POST.get(username), passwordrequest.POST.get(password)) if user is not None: login(request, user) return redirect(dashboard) return render(request, login.html, {error: 账号或密码错误}) return render(request, login.html)authenticate内部会用 PBKDF2 校验密码哈希不要在视图里直接比较User.password。登录成功后request.user被写入 session后续视图里用request.user.is_authenticated判断是否登录。注册学生或教师账号时要同时创建 User 和 Profile必须放在同一个事务里from django.db import transaction from django.contrib.auth.models import User transaction.atomic def register_view(request): role request.POST.get(role, student) user User.objects.create_user( usernamerequest.POST.get(username), passwordrequest.POST.get(password), first_namerequest.POST.get(real_name, ), ) Profile.objects.create(useruser, rolerole)create_user会自动调用set_password把明文密码转成加盐哈希。transaction.atomic保证两个写操作同时成功或回滚。如果漏掉事务User 创建成功而 Profile 失败登录会出现RelatedObjectDoesNotExist错误。4.3 防止水平越权查询自己成绩的过滤条件学生登录后请求“我的成绩”最容易犯的错误是让前端传student_id然后直接拿这个参数过滤。这是典型的水平越权漏洞。正确做法是只依赖request.user关联的学生档案def my_scores_api(request): if not request.user.is_authenticated: return JsonResponse({error: unauthorized}, status401) if getattr(request.user, profile, None) is None: return JsonResponse({error: no profile}, status403) student request.user.student_profile scores (SelectCourse.objects .filter(studentstudent, score__isnullFalse) .values(course__name, score, created_at)) return JsonResponse(list(scores), safeFalse)request.user.student_profile是通过Student.user的related_name反向查到的因为当前登录用户就是 Student 的 one-to-one 关联人。这样无论前端传什么参数后端都只查当前登录人的数据。教师查询成绩时要再校验教师与课程的关系def teacher_course_scores_api(request, course_id): course Course.objects.filter( idcourse_id, teacherrequest.user ).first() if not course: return JsonResponse({error: forbidden}, status403) records SelectCourse.objects.filter(coursecourse) # 序列化返回这里用Course.objects.filter(teacherrequest.user)做一次存在性判断而不是只判断role teacher。一个教师账号如果能直接查任意课程的选课学生内部数据就完全暴露了。4.4 权限验证中的坑与调试用login_required装饰器时未登录用户默认被重定向到/accounts/login/。很多项目没有配置这个地址就会 404。在settings.py里设置LOGIN_URL /login/调试权限问题时我一般看 Django 日志里的中间件顺序。如果引入了第三方LoginRequiredMiddleware它默认拦截所有路径静态文件名如/static/admin/css/base.css也可能被拦。需要在中间件配置里加白名单。一个容易忽略的点是request.user是SimpleLazyObject在测试代码里直接调用user.profile.role可能拿到旧缓存值。单元测试要用user.refresh_from_db()或者从数据库重新取一次。5. 系统部署与性能调优Django MySQL 的实战踩坑5.1 环境配置与初始化这个成绩管理系统最终要跑在 MySQL 上。以 Windows 部署为例先装 Python 3.8 以上版本再安装依赖pip install django4.2 mysqlclient mysql -u root -p -e CREATE DATABASE grade_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; python manage.py makemigrations python manage.py migrate python manage.py createsuperusermysqlclient是 Django 连接 MySQL 的常用驱动。utf8mb4必须指定中文课程名和公告内容在排序、比较时如果用到 utf8 会出现字符集不兼容告警。makemigrations先扫描模型差异生成迁移文件migrate再执行。如果模型有外键环迁移顺序可能不好解决初期可以用migrate --run-syncdb强制建表但生产环境仍然要走正式迁移。5.2 静态文件与模板处理开发模式 Django 能自己处理静态文件部署后必须执行python manage.py collectstatic --no-inputcollectstatic会把 App 内所有 static 目录复制到STATIC_ROOT。STATIC_URL需要与 Nginx 的 location 对应。如果前端用 Vue 打包3-build.bat里的npm run build产物要放到 Django 能访问的目录否则页面样式和 JS 全部 404。常见做法是让 Nginx 直接托管distAPI 反向代理到 Django。5.3 查询缓存与数据库连接优化成绩页面的统计接口会被高频访问数据库会被反复聚合查询拖垮。建议用 Redis 做 Django 缓存CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/1, TIMEOUT: 60, } }视图里主动控制缓存from django.core.cache import cache def cached_score_stats(request): course_id request.GET.get(course_id) cache_key fcourse_stats_{course_id} result cache.get(cache_key) if result is None: result compute_stats(course_id) cache.set(cache_key, result, 120) return JsonResponse(result)缓存键必须包含影响结果的全部参数否则不同课程会互相串数据。成绩录入后要主动失效对应键cache.delete(fcourse_stats_{course_id})不然学生改完分数图表还是旧值。缓存过期时间也不能设太长120 秒是一个比较折中的值。5.4 常见异常排查表格报错信息可能原因处理方式django.db.utils.OperationalError: (1045, Access denied)数据库账号密码错误核对DATABASES配置ModuleNotFoundError: No module named django.core.cache.backends.redisDjango 版本低于 4.0升级 Django 或改用django-redisOSError: [Errno 13] Permission denied: /static/静态目录权限设置目录为chmod 755DisallowedHost: Invalid HTTP_HOST headerALLOWED_HOSTS未设置域名在 settings 中加入服务器 IP 或域名如果是在服务器面板这类环境部署需要把runserver换成 gunicorngunicorn grade_system.wsgi:application -w 4 -b 0.0.0.0:8000 --timeout 60-w 4是 worker 数量--timeout 60避免聚合查询耗时过长被强制杀死。wsgi里的application由 Django 项目创建时自动生成不需要手工改动。6. 一键启动脚本背后的细节批量执行与端口占用检测安装包和运行包通常用.bat在 Windows 上完成环境搭建。项目里的1-install.bat、2-run.bat、3-build.bat符合快速交付的习惯。批处理脚本看起来简单但几个细节写不好一键脚本就会变成一键报错。1-install.bat先检查 Python 是否安装再装依赖echo off python --version nul 21 if errorlevel 1 ( echo Python not found, please install Python 3.8 pause exit /b 1 ) pip install -r requirements.txt echo install done pausepython --version nul 21把标准输出和错误输出都丢弃只保留退出码。if errorlevel 1表示若退出码大于等于 1 则执行括号内命令也就是找不到 Python 的情况。pause保留窗口用户才能看到错误信息。2-run.bat启动 Django 开发服务器前先检查 8000 端口是否被占用netstat -ano | findstr :8000 nul if not errorlevel 1 ( echo Port 8000 is already in use. exit /b 1 ) python manage.py runserver 0.0.0.0:8000netstat -ano列出所有端口和进程号findstr :8000做精确匹配。匹配到任何内容时 findstr 返回 0if not errorlevel 1成立说明端口被占用直接退出。这样能避免旧进程残留导致 runserver 起不来。3-build.bat负责前端构建和静态文件拷贝cd frontend npm install npm run build xcopy /E /I /Y dist ..\static_build\xcopy三个参数/E复制所有子目录/I目标目录不存在时自动创建/Y不逐文件确认。构建产物进入static_build后Django 再通过collectstatic合并形成完整发布链。项目里还有像update-password.vue.bak、IndexMain.vue.bak这类备份文件。交付前必须清理否则collectstatic会把.bak也当成静态文件复制到生产目录。Windows 下先列出再确认for /r %i in (*.bak) do echo %i这里%i是循环变量/r递归遍历所有子目录。确认无误后把echo %i替换成del %i再执行。Linux 或 Git Bash 环境则用find . -name *.bak -exec rm -f {} \;执行前先加-print看预览。最后验证一键脚本能否在干净环境跑通要删掉原来的虚拟环境和node_modules重新执行1-install.bat和3-build.bat再启动2-run.bat。重点检查脚本是否写死了绝对路径比如C:\Users\admin\Desktop\...换到别的机器就找不到文件。所有路径都应改成相对路径或者用%~dp0获取脚本所在目录cd /d %~dp0%~dp0在批处理中代表当前脚本所在盘符和路径/d切换盘符。这样用户无论把项目解压到哪个目录一键脚本都能正常工作。本文还有配套的精品资源点击获取