news 2026/9/16 12:44:15

Django学生成绩管理系统:ORM聚合与ECharts数据可视化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django学生成绩管理系统:ORM聚合与ECharts数据可视化实战

简介:一套基于Python与Django实现的学生成绩管理系统,涵盖首页、个人中心、教师管理、学生管理、公告信息、课程类型、课程信息、选课信息与成绩信息等核心模块,并融合数据可视化展示,适合课程设计、毕业设计或Django初学者参考学习。压缩包共360个文件,约8.35MB,内含Python源码(py)、Vue前端组件(vue)、SVG图标、JavaScript脚本、SQL数据库文件、CSS样式及bat启动脚本,另附docx说明文档和部分备份文件,便于恢复与对照调试。目前已有226人学习。通过该系统可掌握Django项目目录结构、多角色权限功能设计、选课与成绩数据管理,以及可视化图表的接入方式;配套的一键安装、运行脚本降低了本地环境配置难度,便于直接启动并查看实际效果,是快速上手同类型管理系统开发的不错范本。

1. Django 学生成绩管理系统:数据可视化不应该是最后才补的功能

学期末教务科最常问的一句话是:哪个班退步了?在 Excel 里,这要透视表加条件格式,换门课再重来一遍;在 Django 学生成绩管理系统里,一次 ORM 聚合加一张 ECharts 图表就够了。分工很明确:Django 负责学生、课程、成绩三张表的增删改查,Python 在视图层把统计口径算好,数据可视化把分数段、班级均分、科目趋势变成浏览器里能交互的图。

后台管理系统要表单、权限、分页,Django 的 Admin 和认证模块能省掉大量脚手架;可视化选 ECharts,浏览器渲染、带 tooltip 和图例开关,交互体验比 Matplotlib 静态图强一个量级。这个组合适合正在做课程设计、毕业设计或校内成绩分析工具的同学,也适合想补全 Django 技术栈的 Web 开发。

下面从建表开始,按一条能跑通的主线把系统搭起来。代码基于 Django 4.x/5.x 和 Python 3.10+,开发用 SQLite,部署时换 MySQL。

2. 用 Django 创建 app 并设计成绩数据模型:Student、Course、Score 三张表

先交代项目层面的选择:这个小系统用一个gradesapp 就够,不需要把学生、成绩拆成独立 app。拆 app 会增加模块间 import 的复杂度,收益却很低;三张表放一起,迁移文件集中,接手的人更容易看懂。搭建 web 项目的第一步,是创建虚拟环境并装好 Django。

2.1 django创建app 前先做这两个准备

独立虚拟环境隔离依赖,避免和系统 Python 打架,后面部署到 Linux 服务器时也能直接在项目目录里复用同一套环境。创建项目的命令按最常见的流程来:

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django==5.0.* django-admin startproject grade_system . python manage.py startapp grades

startproject后面那个点表示在当前目录生成manage.pygrade_system配置包,不额外套一层目录,后续 gunicorn 启动命令的路径更短。startapp grades创建业务 app,学生、课程、成绩都放这里。Django 5.x 的 ORM 行为和 4.2 基本一致,网上大多数 4.x 排错经验可以直接套用。

创建完 app,先注册到INSTALLED_APPS,再改语言和时区:

# grade_system/settings.py INSTALLED_APPS = [ # django 自带 app 省略 'grades', ] LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

LANGUAGE_CODE = 'zh-hans'让 Admin 后台按钮和字段标签显示中文;TIME_ZONE决定auto_now_add这类时间字段写入的本地时区。注意USE_TZ = True时数据库存的是 UTC,模板渲染时会按TIME_ZONE转回本地,排序和比较不受影响,不需要手动改。

2.2 成绩模型字段设计:为什么学号用 CharField、分数用 DecimalField

三张表的关系是成绩表同时外键到学生和课程。字段选择上有几个容易写错的点,直接看代码:

# grades/models.py from django.db import models class Student(models.Model): GENDER_CHOICES = [('M', '男'), ('F', '女')] student_no = models.CharField('学号', max_length=20, unique=True) name = models.CharField('姓名', max_length=50) clazz = models.CharField('班级', max_length=30, db_index=True) gender = models.CharField('性别', max_length=2, choices=GENDER_CHOICES) def __str__(self): return f'{self.student_no} {self.name}' class Course(models.Model): name = models.CharField('课程名', max_length=50, unique=True) credit = models.DecimalField('学分', max_digits=3, decimal_places=1) def __str__(self): return self.name class Score(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='scores') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='scores') score = models.DecimalField('成绩', max_digits=5, decimal_places=1) exam_date = models.DateField('考试日期') class Meta: unique_together = ('student', 'course', 'exam_date') ordering = ['-score']

学号用CharField而不是IntegerField,学号可能带前导零或字母后缀,整数字段会直接丢格式。scoreDecimalField而不是FloatField,浮点累加会出现 73.1999999 这类误差,后续求平均分和分数段统计时误差会被放大。unique_together保证同一个学生同一门课同一次考试只有一条成绩,是防止重复导入的最后一道闸。

on_delete=models.CASCADE表示删除学生或课程时成绩跟着删,与多数人直觉一致,课程设计里够用;生产环境要留痕的话,改成PROTECT并在删除前二次确认。字段选择可以整理成一张决策表,答辩或评审时按表说明理由:

字段推荐类型原因
学号CharField(20)保留前导零与字母,unique 加唯一约束
成绩DecimalField(5,1)避免浮点误差,精确到 0.1 分
班级CharField(30) + db_index班级是最高频的过滤和分组维度
考试日期DateField支持多次考试的时间维度对比

写完后生成迁移并建表,可用 sqlite3 直接看真实建表语句:

python manage.py makemigrations grades python manage.py migrate sqlite3 db.sqlite3 '.schema grades_score'

makemigrations只生成迁移文件不改库,migrate才真正执行建表。.schema grades_score能看出 Django 为外键、unique_together自动加了哪些索引;排查「为什么查询没用上索引」时,先看这里再决定要不要加db_index

2.3 批量灌入模拟数据,让可视化有数据可画

可视化没数据等于白搭。在 Admin 里逐条点 90 个学生太慢,用独立脚本批量写入更符合工程习惯。在项目根目录放一个seed.py

# seed.py import os, random import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'grade_system.settings') django.setup() from grades.models import Student, Course, Score courses = ['高等数学', '大学英语', '数据结构', '操作系统'] for name in courses: Course.objects.get_or_create(name=name, defaults={'credit': 4}) classes = ['计科2401', '计科2402', '软工2401'] students = [] for i, clazz in enumerate(classes): for j in range(30): students.append(Student( student_no=f'2024{i + 1:02d}{j + 1:03d}', name=f'学员{i}-{j}', clazz=clazz, gender=random.choice(['M', 'F']), )) Student.objects.bulk_create(students) for student in Student.objects.all(): for course in Course.objects.all(): Score.objects.create( student=student, course=course, score=round(random.gauss(75, 12), 1), exam_date='2025-01-10', ) print(f'学生 {Student.objects.count()} 人,成绩 {Score.objects.count()} 条')

get_or_create防止脚本重复执行时课程被建两遍;bulk_create一次性插入 90 个学生,比逐个save少 89 次数据库往返。random.gauss(75, 12)用正态分布模拟成绩,均值 75、标准差 12,后面画分数段分布时不会出现全班挤在 90 分的失真情况。跑完python seed.py,库里就有 90 人、4 门课、360 条成绩记录可用于可视化。

3. 用 ECharts 做成绩数据可视化:JSON 接口到图表渲染的完整链路

成绩可视化这一层,先定方案再写代码。接口负责算数,前端负责画图,中间用 JSON 对接。这一章把从 ORM 聚合到图表参数配置的完整链路走一遍。

3.1 可视化方案选型:ECharts 对比 Matplotlib

同样叫数据可视化,Matplotlib 和 ECharts 的目标产物完全不同。成绩分析场景里最常用的是班级平均分柱状图、分数段分布直方图、个人成绩折线图,ECharts 开箱即用,tooltip、图例开关、数据缩放这些交互不用自己写,浏览器里刷新即看,做数据可视化大屏时更是首选。

另一个原因是部署形态。Django 模板直接引 CDN 或本地静态文件,视图层吐 JSON 就行,前后端不分离也能跑得很顺;以后要往企业级数据可视化平台扩展,同一套 ECharts 配置可以平滑迁到 Vue/React 项目。Python 侧的 pandas 和 Matplotlib 适合做离线分析和导出报表,两者不冲突,但 Web 场景优先 ECharts。

3.2 用 ORM 聚合写一个班级平均分 JSON 接口

接口只做一件事:按班级、课程分组算平均分,同时带出最高分、最低分和记录数。先看视图:

# grades/views.py from django.db.models import Avg, Max, Min, Count from django.http import JsonResponse from .models import Score def class_avg_api(request): rows = ( Score.objects .values('student__clazz', 'course__name') .annotate( avg_score=Avg('score'), max_score=Max('score'), min_score=Min('score'), cnt=Count('id'), ) .order_by('student__clazz', 'course__name') ) return JsonResponse(list(rows), safe=False)

values('student__clazz', 'course__name')生成 SQL 里的GROUP BY clazz, name,按班级和课程两个维度分组;annotate里的四个聚合函数对应平均分、最高分、最低分、条数。cnt是数据质量哨兵:某个班级某门课的条数明显不等于学生数,说明有重复记录或漏录。

list(rows)不能省,QuerySet 不是 JSON 可序列化对象,直接传给JsonResponse会抛 TypeError。safe=False允许响应顶层是数组。挂到 URLconf:

# grade_system/urls.py from django.urls import path from grades.views import class_avg_api urlpatterns = [ path('api/class-avg/', class_avg_api, name='class_avg_api'), ]

响应结构类似:

[ { "student__clazz": "计科2401", "course__name": "高等数学", "avg_score": 76.9, "max_score": 94.0, "min_score": 52.0, "cnt": 30 } ]

返回的键名是 ORM 拼出来的student__clazzcourse__name,前端取字段时容易写错。键名过长影响可读性,视图里加一层映射改成短键名,放在 3.4 的参数说明里细讲。

3.3 Django 模板里接入 ECharts 并配置图表参数

页面用 Django 模板承载,ECharts 从 CDN 引入,数据通过 fetch 拿。重点看setOption的配置方式:

{% load static %} <!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="UTF-8"> <title>成绩可视化看板</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script> </head> <body> <div id="avg-chart" style="width: 100%; height: 480px;"></div> <script> async function loadClassAvg() { const resp = await fetch('/api/class-avg/'); const data = await resp.json(); const chart = echarts.init(document.getElementById('avg-chart')); // 用 Set 去重拿到班级和课程两个维度 const clazzes = [...new Set(data.map(d => d.student__clazz))]; const courses = [...new Set(data.map(d => d.course__name))]; const series = courses.map(course => ({ name: course, type: 'bar', data: clazzes.map(clazz => { const row = data.find( d => d.student__clazz === clazz && d.course__name === course ); return row ? row.avg_score : null; }) })); chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: courses }, xAxis: { type: 'category', data: clazzes }, yAxis: { type: 'value', name: '平均分', min: 60 }, series: series }); } loadClassAvg(); </script> </body> </html>

[...new Set(...)]去重后得到班级和课程两个维度列表,避免把班级名硬编码在 JS 里。series由课程数组映射而来,每门课一根柱,柱的位置按clazzes数组下标对齐。tooltip.trigger: 'axis'让悬停时某个班级的四门课同时展示,横向对比效率高;yAxis.min: 60把刻度下限定在 60,平均分 70 到 90 的差异在图上明显很多。

注意:CDN 引入的前提是部署环境能访问外网;内网部署时把echarts.min.js下载到static/目录,用{% static 'echarts.min.js' %}引入。

如果图表白屏,先看控制台有没有Can't get DOM width or height报错,这是容器没高度的典型问题,解决方法是给 div 一个确定高度,height: 100%只在父容器有高度时生效。

3.4 成绩图表常用的 5 个 ECharts 配置项

可视化不只是「画出来」,参数直接决定老师能不能一眼看出问题。按成绩场景的优先级整理配置表:

配置项推荐值作用
tooltip.trigger'axis'柱状图悬停时整组展示
yAxis.min60放大 60~100 分区间的差异
dataZoom{type:'inside'}课程多、柱体密时滚动查看
series.barMaxWidth32班级少时防止柱体过宽占满
grid{containLabel: true}让坐标轴标签不被裁掉

containLabel是新手最容易漏的:x 轴班级名太长时会被 grid 裁掉一部分,加上grid: {containLabel: true}后标签自动撑开间距。视图层键名映射在这里一起处理,返回前加一行列表推导把键改短:

payload = [ {'clazz': r['student__clazz'], 'course': r['course__name'], 'avg': r['avg_score']} for r in rows ] return JsonResponse(payload, safe=False)

这是最常见的「前后端契约整理」动作,成本低,但能让前端模板和后续接手的人少踩两个字段名拼写的坑。

4. 成绩统计查询与 Django Admin:聚合、分数段分布、批量删除

成绩管理系统的核心不只图表,查询和后台操作的效率同样关键。这一章把 ORM 里容易混淆的聚合写法、Admin 配置和删除语义讲透。

4.1 aggregate 与 annotate:全校均分和班级排名的写法区别

aggregateannotate都做聚合,但返回类型完全不同。aggregate返回字典且只有一行;annotate配合values返回按组展开的 QuerySet。同一条统计分别用两种写法:

from django.db.models import Avg, Max, Min from grades.models import Score # 全校数据结构平均分:aggregate 返回单行字典 overall = Score.objects.filter(course__name='数据结构').aggregate( avg_score=Avg('score'), max_score=Max('score'), min_score=Min('score'), ) # 每班数据结构平均分:annotate 返回按组展开 by_clazz = ( Score.objects .filter(course__name='数据结构') .values('student__clazz') .annotate(avg_score=Avg('score')) .order_by('-avg_score') )

aggregate不写values时是对全表聚合,性能最好,返回{'avg_score': 74.8, 'max_score': 96.0, 'min_score': 41.0}这样一行。annotate的核心是和values组合,GROUP BY的维度就写在values里;.order_by('-avg_score')让班级按平均分倒序,这就是「哪个班退步了」这个问题的答案——排最后的班级就是要关注的。

取单科前 10 名时注意关联查询:

top10 = ( Score.objects .filter(course__name='数据结构') .select_related('student') .order_by('-score')[:10] ) for s in top10: print(s.student.name, s.score)

去掉select_related('student')的话,循环里每次访问s.student都会发一条 SQL,10 条记录变 11 条查询。这个 N+1 问题在成绩列表页会被放大成几百条 SQL,第 5 章用connection.queries验证。

4.2 用 Case/When 一次查出分数段分布

分数段分布是成绩管理里最常用的直方图数据。新手容易写成五个 count 查询,更优的做法是用Case/When在数据库里分桶,一趟 SQL 查出五个区间:

from django.db.models import Case, When, Value, CharField, Count buckets = ( Score.objects .annotate(level=Case( When(score__gte=90, then=Value('优秀')), When(score__gte=80, then=Value('良好')), When(score__gte=70, then=Value('中等')), When(score__gte=60, then=Value('及格')), default=Value('不及格'), output_field=CharField(), )) .values('level') .annotate(total=Count('id')) .order_by('level') )

Case/When从上到下匹配,score__gte=90必须放在第一层,95 分才会被分到「优秀」而不是落在「良好」,顺序不能乱。output_field=CharField()必须写清楚,否则 ORM 不知道这个表达式返回什么类型,后续values('level')分组会报错。对比五个filter().count(),这条 SQL 只回一趟数据库,成绩量到十万级时差距非常明显。

4.3 django admin 界面美化的最低成本方案:list_display 与 list_filter

Django Admin 默认界面不算好看,但美化的第一步不是换主题,而是先让信息密度和筛选效率提上来。ModelAdmin的几个属性就是成本最低的优化:

# grades/admin.py from django.contrib import admin from .models import Student, Course, Score @admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display = ('student_no', 'name', 'clazz', 'gender') list_filter = ('clazz',) search_fields = ('student_no', 'name') @admin.register(Score) class ScoreAdmin(admin.ModelAdmin): list_display = ('student', 'course', 'score', 'exam_date') list_filter = ('course', 'exam_date', 'student__clazz') search_fields = ('student__name', 'student__student_no') list_editable = ('score',) list_per_page = 50

list_display控制列表展示哪些列,外键字段默认显示其__str__,所以 2.2 里给 Student 定义了__str__list_filter支持外键路径,student__clazz可以直接按班级筛选成绩,比先选学生再翻成绩快得多。list_editable = ('score',)让成绩在列表页直接改,期末批量录分时能省大量点击,但注意list_editable的字段不能是list_display的第一列。

真要「界面好看」,社区常用 django-simpleui 这类第三方 Admin 主题。但建议先把list_displaylist_filter配好再考虑主题,因为 90% 的后台效率问题出在字段配置,不在皮相。

4.4 django执行查询-删除对象时的级联行为与返回值

删除是高风险操作,先把 ORM 的删除语义讲清楚。批量删除返回一个二元组,靠它确认实际影响范围:

# 删除某门课的全部成绩 result = Score.objects.filter(course__name='高等数学').delete() # (120, {'grades.Score': 120}) # 删除一个班所有学生,级联删掉该班全部成绩 result = Student.objects.filter(clazz='软工2401').delete() # (150, {'grades.Score': 120, 'grades.Student': 30})

delete()返回(总删除行数, {模型: 行数}),第二项会列出级联触发的所有模型。打印返回值能第一时间发现「我只删 30 个学生,怎么连带了 120 条成绩」。批量删除只在 QuerySet 上调用,走的是数据库 DELETE,不会经过模型实例上可能重写过的delete()方法,也没有 Admin 里的校验逻辑。

提示:批量删除前先执行一次filter(...).count()确认行数,再执行delete(),这是最便宜的回滚手段。

Scoreon_delete=models.CASCADE是级联开关,改成PROTECT后删除学生会抛ProtectedError。宁可不删也不能误删的场景,就该用后者。

5. 上线前用这三招验证成绩查询质量与缓存效果

5.1 用 select_related 把查询次数从 361 压到 1

成绩列表页和导出 Excel 场景最常见的性能杀手是 N+1。用 Django 自带的connection.queries验证:

from django.db import connection, reset_queries from grades.models import Score reset_queries() scores = Score.objects.select_related('student', 'course')[:100] for s in scores: print(s.student.name, s.course.name, s.score) print(len(connection.queries)) # 输出 1

select_related去掉,这串循环的查询次数就是 101。reset_queries()清零计数,connection.queries记录本次操作的所有 SQL,len()就是指标。成绩表外键只有 Student 和 Course,一条 JOIN 全部解决;后面加了多对多选课关系,要改用prefetch_related

5.2 给统计接口加 5 分钟缓存

班级平均分这类统计接口在成绩未变时结果恒定,适合缓存。路由层直接包cache_page

from django.views.decorators.cache import cache_page urlpatterns = [ path('api/class-avg/', cache_page(300)(class_avg_api), name='class_avg_api'), ]

cache_page(300)把整个响应缓存 300 秒,需要先在settings.py配缓存后端,生产环境用 Redis:

CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/1', 'TIMEOUT': 300, } }

缓存键默认包含完整请求路径,不同课程参数的请求各自缓存,不会串数据。代价是成绩更新后最多 5 分钟内图表是旧数据;不能接受的话,在写成绩的操作后执行cache.clear(),简单但有效。

5.3 宝塔部署 Django 前必跑的两条命令

宝塔面板部署 Django 是常见的生产路径,面板负责 Nginx 和进程管理,Django 侧有三件事必须在部署前做:收集静态文件、迁移数据库、部署项检查。其中两条命令可以直接当验收标准:

python manage.py collectstatic --noinput python manage.py check --deploy

collectstatic把 Admin 和第三方库的静态文件收到STATIC_ROOT并交给 Nginx 托管,--noinput免除确认提示;check --deploy会列出当前环境不符合生产配置的警告,比如DEBUG=TrueALLOWED_HOSTS为空、SECRET_KEY未从配置文件剥离,是最快的部署自检方式。这两条跑完,再用 gunicorn 启动项目,成绩管理系统才算真正到了可交付状态。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 12:43:17

低温环境下微电网储能电池优化调度与Matlab实现

1. 项目背景与核心挑战在极寒地区或冬季低温环境下&#xff0c;微电网系统的储能电池性能会显著下降。这不仅仅是容量衰减的问题&#xff0c;更关键的是低温会导致电池内阻急剧增加、充放电效率降低&#xff0c;甚至引发不可逆的晶体结构损坏。我们团队在内蒙古-25℃的实地测试…

作者头像 李华
网站建设 2026/9/16 12:41:04

基于Spring Cloud的高并发电影票务系统设计与实践

1. 项目背景与核心价值电影票务系统在数字化时代早已不是简单的购票工具&#xff0c;它需要整合在线选座、动态定价、社交化评分、团购营销等复合功能。传统影院管理系统往往只关注票务核销&#xff0c;而现代Java技术栈能够构建高并发、高可用的全流程解决方案。我去年为本地连…

作者头像 李华
网站建设 2026/9/16 12:39:23

C语言基础数据类型与内存操作15个关键点解析

1. 项目背景与核心价值"Back to Base-ics"这个标题乍看像文字游戏&#xff0c;实则精准概括了C语言开发中一个关键痛点——基础数据类型和内存操作的精准控制。在嵌入式开发、系统编程和高性能计算领域&#xff0c;程序员经常需要处理原始字节流、内存对齐和二进制协…

作者头像 李华
网站建设 2026/9/16 12:36:45

Playnite 游戏库便携同步:3 个高频目标一次配齐的实操教程

Playnite 游戏库便携同步&#xff1a;3 个高频目标一次配齐的实操教程 【免费下载链接】Playnite Video game library manager with support for wide range of 3rd party libraries and game emulation support, providing one unified interface for your games. 项目地址:…

作者头像 李华