
简介基于Python与Django开发的学生成绩管理系统完整源码包面向需要完成课程设计、毕业设计或快速搭建教务管理原型的开发者。系统覆盖首页、个人中心、教师管理、学生管理、公告信息、课程类型、课程信息、选课信息及成绩信息等核心模块并融入数据可视化能力帮助读者理解前后端联调与增删改查逻辑。资源共360个文件压缩包8.35MB以svg图标、vue前端组件、py后端源码及pyc编译文件为主另有js脚本、png/jpg图片、SQL数据库文件、启动与安装批处理和多份vue文件备份可作为项目二次开发或部署调试的参考。当前已有226人学习下载包含完整的项目结构、前端页面与后端接口实现适合初学者对照源码逐模块拆解也适合有Python基础者直接改造复用。1. 为什么把 Django 和数据可视化放在同一个系统里一个学期结束班主任想看看班里数学成绩的分布教务主任想对比两个班级的平均分趋势这些需求用 Excel 能做但数据一多、表一杂手工筛选的效率立刻见底。学生成绩管理系统要解决的正是这个问题把成绩从台账变成可按班级、课程、分数段自由切片的分析结果再用图表直接呈现在浏览器里。Python 负责数据处理和聚合逻辑Django 负责把数据稳定地提供给前端ECharts 负责把 JSON 变成可读的图形三者各司其职整个链路没有多余环节。这套组合适合两类人一类是正在做课程设计或毕业设计的在校生用 Django 快速搭建管理系统是高频选题另一类是工作里接到内部小工具需求的工程师需要在两天内交付一个能录数据、能看趋势的轻量后台。Django 自带 ORM、Admin 后台和模板引擎天然具备“管理后台”基因而 Python 侧的 pandas、aggregate 查询又能把成绩统计这类二维表操作简化到几行代码。数据可视化放在这个场景里不是炫技而是把聚合结果转成折线图、柱状图和饼图让老师不需要看原始数字就能判断教学效果。它的核心价值是用最小的代码量把“录入—存储—查询—展示”串成一条完整链路。2. Django 成绩系统的数据模型与数据源设计2.1 从一张成绩单拆出四张核心表成绩管理系统第一步不是写视图而是设计 models。常见做法是把一张宽表拆成班级、学生、课程、成绩四张关联表避免一门课加一次字段、一个班级复制一批行的尴尬。我在 models.py 里通常这样落from django.db import models class ClassInfo(models.Model): name models.CharField(班级名称, max_length50, uniqueTrue) grade models.CharField(年级, max_length20) def __str__(self): return self.name class Student(models.Model): student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length30) class_info models.ForeignKey(ClassInfo, on_deletemodels.PROTECT, verbose_name班级, related_namestudents) def __str__(self): return f{self.name}({self.student_no}) class Course(models.Model): name models.CharField(课程名, max_length50) credit models.DecimalField(学分, max_digits3, decimal_places1) def __str__(self): return self.name class Score(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name学生, related_namescores) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程, related_namescores) score models.FloatField(成绩, default0.0) exam_date models.DateField(考试日期, auto_now_addTrue) class Meta: unique_together (student, course)这里有几个参数值得说明on_deletemodels.PROTECT用在 ClassInfo 外键上班主任的班级表被学生引用时禁止直接删除防止历史成绩悬空unique_together保证一个学生同一门课只有一条成绩避免录入时产生重复行成绩字段用FloatField而不是DecimalField是为了后续在 ORM 里直接用Avg、Max等聚合函数减少类型转换的麻烦。学分使用DecimalField是因为学分通常只精确到 0.5而且不参与统计运算。2.2 用 Django Admin 把“录成绩”变成“选下拉框”数据模型定好后录入界面直接复用 Django Admin。在 admin.py 里注册四个模型并把常用筛选字段配置好from django.contrib import admin from .models import ClassInfo, Student, Course, Score admin.register(Score) class ScoreAdmin(admin.ModelAdmin): list_display [student, course, score, exam_date] list_filter [course, exam_date] search_fields [student__name, student__student_no] autocomplete_fields [student, course] admin.register(Student) class StudentAdmin(admin.ModelAdmin): search_fields [name, student_no] autocomplete_fields [class_info]list_filter直接生成课程和考试日期的侧边栏筛选器教务老师无需写 SQL 就能只看某次考试的全部成绩search_fields支持跨表搜索student__name的写法告诉 Django 去 Student 表里匹配姓名autocomplete_fields把外键下拉改成可输入的搜索框班级有 30 个学生时体验差异非常明显。Django Admin 的默认界面比较朴素如果项目对外展示可以装一个 simpleui 或者 django-jet 作为后台皮肤。安装后只需在INSTALLED_APPS里把simpleui放在django.contrib.admin之前登录页和列表页会自动替换不需要改业务代码。提示Django Admin 默认对没有 staff 权限的用户是关闭的。给代课老师开账号时只需勾选“职员状态”并把成绩表的“增删改查”权限分配给对应分组不需要开放所有表。2.3 用 manage.py shell 生成可视化需要的预置数据真实项目里成绩可能要从 Excel 导入但开发阶段最快的办法是直接往数据库里写一批模拟数据。我一般会写个独立的 Python 脚本放在项目根目录用python manage.py shell seed_data.py执行import random from datetime import date from app.models import ClassInfo, Student, Course, Score # 先清空再写入保证脚本可重复执行 Score.objects.all().delete() Student.objects.all().delete() Course.objects.all().delete() classes [ClassInfo.objects.create(namef高二({i})班, grade高二) for i in range(1, 4)] courses [Course.objects.create(namename, creditcredit) for name, credit in [(语文, 2.0), (数学, 2.0), (英语, 2.0), (物理, 1.5)]] for c in classes: for i in range(1, 41): # 每班 40 人 stu Student.objects.create( student_nof{c.id}2024{i:03d}, namef学生{c.id}{i:02d}, class_infoc ) for course in courses: Score.objects.create( studentstu, coursecourse, scorerandom.randint(55, 100), exam_datedate(2024, 6, 15) )脚本用objects.create而非save()的目的是让代码更紧凑清空数据时注意先删子表Score再删父表否则外键约束会抛异常。随机分数控制在 55 到 100 之间是为了让可视化图表出现“及格线附近”的分布形态调试饼图和直方图时更容易看出问题。3. 用 Django ORM 做成绩统计输出可视化需要的 JSON3.1 写一个成绩分析 API按班级、课程、分数段聚合图表不是直接把所有成绩全量传给前端而是由后端先用 ORM 完成聚合。最常见的需求有三种各班各科平均分、单科成绩分数段分布、某班多科成绩雷达图。下面这个视图是整套系统的核心它同时返回三类数据import json from django.db.models import Avg, Count, Q from django.http import JsonResponse from django.views.decorators.http import require_GET from .models import Score, ClassInfo, Course require_GET def score_analysis(request): course_id request.GET.get(course_id) class_id request.GET.get(class_id) # 1. 各班各科平均分 avg_data (Score.objects .values(student__class_info__name, course__name) .annotate(avg_scoreAvg(score)) .order_by(student__class_info__name)) # 2. 单科分数段分布用条件聚合一次算完 segment_data [] if course_id: bins [(0, 60, 不及格), (60, 70, 及格), (70, 80, 中等), (80, 90, 良好), (90, 101, 优秀)] course_scores Score.objects.filter(course_idcourse_id) for low, high, label in bins: count course_scores.filter(score__gtelow, score__lthigh).count() segment_data.append({name: label, value: count}) # 3. 指定班级各科平均分用于雷达图 radar_data [] if class_id: radar_data list(Score.objects .filter(student__class_info_idclass_id) .values(course__name) .annotate(avg_scoreAvg(score))) return JsonResponse({ avg: list(avg_data), segment: segment_data, radar: radar_data, })用values(...).annotate(...)生成的 SQL 是GROUP BY两列再取平均值比在 Python 里写双层循环快一个数量级。分数段的统计没有用Case/When而是用filter(score__gtelow, score__lthigh)的五次轻量查询数据量只有两三千行时性能差异可以忽略但代码可读性更好。require_GET装饰器限制请求方法避免前端误用 POST 触发无意义的数据库轮询。3.2 把 ORM 结果转成图表字段的映射关系后端返回的 JSON 结构需要和前端图表字段严格对应。下面这张表是我在对接 ECharts 时整理的映射关系后端字段前端图表位置说明avg[].student__class_info__namexAxis.data柱状图 x 轴班级名avg[].avg_scoreseries[i].data对应课程的平均分序列segment[].nameseries.data[i].name饼图扇区名称segment[].valueseries.data[i].value饼图扇区值radar[].course__nameradar.indicator[i].name雷达图指标名radar[].avg_scoreseries.data.value 中的对应索引需要按 indicator 顺序重排数组注意雷达图的indicator是一个数组而 series 的 data 是一个与指标顺序对应的数值数组。后端radar_data里的字段顺序可能和前端定义不一致稳妥做法是在前端map时按指标名称查找而不是直接push。3.3 最常见的错误在 Python 循环里做统计新手容易写出的版本是这样for cls in ClassInfo.objects.all(): for course in Course.objects.all(): scores Score.objects.filter(student__class_infocls, coursecourse) avg sum(s.score for s in scores) / scores.count()这段代码每查一次平均分就触发一次数据库查询3 个班 × 4 门课就是 12 次查询如果再加学生人数统计查询次数会随数据量线性膨胀。正确思路是把“取数”和“统计”都交给 SQL 层——Django ORM 的annotate让数据库完成分组计算应用服务器只负责把结果序列化成 JSON。用django.db.connection.queries可以查看实际执行的 SQL 条数这是排查性能瓶颈最直接的手段。4. ECharts 数据可视化大屏把 Django 返回的数据画成图表4.1 页面模板引入 ECharts 并请求后端 JSON可视化部分我选择 ECharts 而非 Chart.js 或 Highcharts看中的是它对折线图、饼图、雷达图、热力图都有成熟配置而且 CDN 引入无需构建工具适合 Django 模板直接使用。在 templates 里创建一个analysis.html!DOCTYPE html html head meta charsetUTF-8 title成绩分析大屏/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style .chart { width: 48%; height: 400px; display: inline-block; } /style /head body div classchart idbarChart/div div classchart idpieChart/div div classchart idradarChart/div script const barChart echarts.init(document.getElementById(barChart)); const pieChart echarts.init(document.getElementById(pieChart)); const radarChart echarts.init(document.getElementById(radarChart)); fetch(/api/score-analysis/?course_id1) .then(res res.json()) .then(data { const classes [...new Set(data.avg.map(item item[student__class_info__name]))]; const courses [...new Set(data.avg.map(item item[course__name]))]; const series courses.map(course ({ name: course, type: bar, data: classes.map(cls { const row data.avg.find(item item[student__class_info__name] cls item[course__name] course); return row ? row.avg_score.toFixed(1) : 0; }) })); barChart.setOption({ title: { text: 各班各科平均分对比 }, tooltip: {}, legend: { data: courses }, xAxis: { data: classes }, yAxis: {}, series: series }); pieChart.setOption({ title: { text: 数学成绩分布 }, tooltip: { trigger: item }, series: [{ type: pie, radius: [30%, 60%], data: data.segment }] }); radarChart.setOption({ title: { text: 高二(1)班各科均衡度 }, radar: { indicator: data.radar.map(item ({ name: item[course__name], max: 100 })) }, series: [{ type: radar, data: [{ value: data.radar.map(item item.avg_score.toFixed(1)), name: 平均分 }] }] }); }); /script /body /html这段代码的核心逻辑是把后端返回的扁平数组avg重构成 ECharts 需要的“以课程为系列、以班级为 x 轴”的嵌套结构。find方法用于匹配班级和课程双重条件避免出现undefined导致图表空缺。饼图的radius:[30%,60%]生成环形图比实心饼图更适合展示分数段占比因为内圈留白可以放总数标签。4.2 三个必调的 ECharts 参数开发可视化大屏时我会优先调整这几个参数它们直接决定图表是否“能看”tooltip.trigger设为axis折线图和柱状图鼠标悬停时同时显示所有系列的数据对比多个班级时比默认的item更直观。legend.type设为scroll课程超过 6 门时图例会换行甚至溢出容器滚动图例能保持布局稳定。grid.top和grid.bottom默认值在上方留白过大导致图表主体被压缩。我一般设grid: { top: 60, bottom: 30, left: 50, right: 20 }让绘图区占满容器。4.3 异步加载的时序问题和窗口自适应前端fetch是异步的图表初始化必须放在.then()内部否则setOption执行时数据还是空数组。另一个常见问题是浏览器窗口放大缩小后图表不会自动跟着变需要在页面里补一段自适应逻辑window.addEventListener(resize, () { barChart.resize(); pieChart.resize(); radarChart.resize(); });如果使用了 Tab 切换或折叠面板图表初始化时容器可能是隐藏状态宽度为 0此时init拿到的尺寸不对。解决办法是在容器显示后再调用chart.resize()或者初始化前确认offsetWidth不为 0。Django 模板渲染时图表容器默认显示一般不会遇到这个问题但放在 Bootstrap 折叠面板里就要注意。5. 三个让成绩系统更可靠的优化手段5.1 用 select_related 减少成绩列表页的 SQL 查询Score 列表页如果不做优化每显示一行成绩就要查一次 Student 和 Course 表这是典型的 N1 查询问题。在视图里加一条链式调用即可解决def score_list(request): scores Score.objects.select_related(student__class_info, course).all() return render(request, scores.html, {scores: scores})select_related会生成 LEFT JOIN一次性把 Student、ClassInfo、Course 三张关联表的数据查出来。注意student__class_info这种跨两层外键的写法Django 1.7 之后才支持项目里看到旧教程用select_related(student)只省了一层查询班级名仍会触发额外 SQL。5.2 分数段可视化大屏里的中文字体与坐标轴间隔大屏场景下 ECharts 默认的 12px 字体会显得局促尤其在 1080p 分辨率的展示屏上。全局调整字号的方式是设置textStyletextStyle: { fontSize: 14 }, xAxis: { axisLabel: { interval: 0, rotate: 30 } }interval:0强制所有 x 轴标签都显示不省写rotate:30让班级名斜着放避免三个字的重叠。成绩是整数时不需要设置如果后端返回了小数可以在xAxis.axisLabel.formatter里用函数{ return value 分; }加上单位。5.3 给图表接口加缓存让查询不再重复打数据库成绩数据是一次考试录入后基本不变的静态数据完全可以用 Django 的 cache 框架缓存接口结果from django.core.cache import cache CACHE_KEY fscore_analysis_{course_id}_{class_id} data cache.get(CACHE_KEY) if data is None: # 原有的聚合查询代码 cache.set(CACHE_KEY, json.dumps(result), timeout60 * 30)缓存 key 里带上课程和班级的筛选参数不同组合互不干扰缓存值存 JSON 字符串而非 Python 对象避免 pickle 序列化的兼容问题。timeout1800代表半小时过期学生新成绩录入后最多等半小时图表刷新——如需立即生效在 ScoreAdmin 的save_model里加一句cache.clear()就能解决。本文还有配套的精品资源点击获取