十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 django5.0.* django-admin startproject grade_system . python manage.py startapp gradesstartproject后面那个点表示在当前目录生成manage.py和grade_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 TrueLANGUAGE_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_length20, uniqueTrue) name models.CharField(姓名, max_length50) clazz models.CharField(班级, max_length30, db_indexTrue) gender models.CharField(性别, max_length2, choicesGENDER_CHOICES) def __str__(self): return f{self.student_no} {self.name} class Course(models.Model): name models.CharField(课程名, max_length50, uniqueTrue) credit models.DecimalField(学分, max_digits3, decimal_places1) def __str__(self): return self.name class Score(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_namescores) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namescores) score models.DecimalField(成绩, max_digits5, decimal_places1) exam_date models.DateField(考试日期) class Meta: unique_together (student, course, exam_date) ordering [-score]学号用CharField而不是IntegerField学号可能带前导零或字母后缀整数字段会直接丢格式。score用DecimalField而不是FloatField浮点累加会出现 73.1999999 这类误差后续求平均分和分数段统计时误差会被放大。unique_together保证同一个学生同一门课同一次考试只有一条成绩是防止重复导入的最后一道闸。on_deletemodels.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_scoremakemigrations只生成迁移文件不改库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(namename, defaults{credit: 4}) classes [计科2401, 计科2402, 软工2401] students [] for i, clazz in enumerate(classes): for j in range(30): students.append(Student( student_nof2024{i 1:02d}{j 1:03d}, namef学员{i}-{j}, clazzclazz, genderrandom.choice([M, F]), )) Student.objects.bulk_create(students) for student in Student.objects.all(): for course in Course.objects.all(): Score.objects.create( studentstudent, coursecourse, scoreround(random.gauss(75, 12), 1), exam_date2025-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_scoreAvg(score), max_scoreMax(score), min_scoreMin(score), cntCount(id), ) .order_by(student__clazz, course__name) ) return JsonResponse(list(rows), safeFalse)values(student__clazz, course__name)生成 SQL 里的GROUP BY clazz, name按班级和课程两个维度分组annotate里的四个聚合函数对应平均分、最高分、最低分、条数。cnt是数据质量哨兵某个班级某门课的条数明显不等于学生数说明有重复记录或漏录。list(rows)不能省QuerySet 不是 JSON 可序列化对象直接传给JsonResponse会抛 TypeError。safeFalse允许响应顶层是数组。挂到 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, nameclass_avg_api), ]响应结构类似[ { student__clazz: 计科2401, course__name: 高等数学, avg_score: 76.9, max_score: 94.0, min_score: 52.0, cnt: 30 } ]返回的键名是 ORM 拼出来的student__clazz、course__name前端取字段时容易写错。键名过长影响可读性视图里加一层映射改成短键名放在 3.4 的参数说明里细讲。3.3 Django 模板里接入 ECharts 并配置图表参数页面用 Django 模板承载ECharts 从 CDN 引入数据通过 fetch 拿。重点看setOption的配置方式{% load static %} !DOCTYPE html html langzh-hans head meta charsetUTF-8 title成绩可视化看板/title script srchttps://cdn.jsdelivr.net/npm/echarts5.5.0/dist/echarts.min.js/script /head body div idavg-chart stylewidth: 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 %}引入。如果图表白屏先看控制台有没有Cant get DOM width or height报错这是容器没高度的典型问题解决方法是给 div 一个确定高度height: 100%只在父容器有高度时生效。3.4 成绩图表常用的 5 个 ECharts 配置项可视化不只是「画出来」参数直接决定老师能不能一眼看出问题。按成绩场景的优先级整理配置表配置项推荐值作用tooltip.triggeraxis柱状图悬停时整组展示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, safeFalse)这是最常见的「前后端契约整理」动作成本低但能让前端模板和后续接手的人少踩两个字段名拼写的坑。4. 成绩统计查询与 Django Admin聚合、分数段分布、批量删除成绩管理系统的核心不只图表查询和后台操作的效率同样关键。这一章把 ORM 里容易混淆的聚合写法、Admin 配置和删除语义讲透。4.1 aggregate 与 annotate全校均分和班级排名的写法区别aggregate和annotate都做聚合但返回类型完全不同。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_scoreAvg(score), max_scoreMax(score), min_scoreMin(score), ) # 每班数据结构平均分annotate 返回按组展开 by_clazz ( Score.objects .filter(course__name数据结构) .values(student__clazz) .annotate(avg_scoreAvg(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都会发一条 SQL10 条记录变 11 条查询。这个 N1 问题在成绩列表页会被放大成几百条 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(levelCase( When(score__gte90, thenValue(优秀)), When(score__gte80, thenValue(良好)), When(score__gte70, thenValue(中等)), When(score__gte60, thenValue(及格)), defaultValue(不及格), output_fieldCharField(), )) .values(level) .annotate(totalCount(id)) .order_by(level) )Case/When从上到下匹配score__gte90必须放在第一层95 分才会被分到「优秀」而不是落在「良好」顺序不能乱。output_fieldCharField()必须写清楚否则 ORM 不知道这个表达式返回什么类型后续values(level)分组会报错。对比五个filter().count()这条 SQL 只回一趟数据库成绩量到十万级时差距非常明显。4.3 django admin 界面美化的最低成本方案list_display 与 list_filterDjango 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 50list_display控制列表展示哪些列外键字段默认显示其__str__所以 2.2 里给 Student 定义了__str__。list_filter支持外键路径student__clazz可以直接按班级筛选成绩比先选学生再翻成绩快得多。list_editable (score,)让成绩在列表页直接改期末批量录分时能省大量点击但注意list_editable的字段不能是list_display的第一列。真要「界面好看」社区常用 django-simpleui 这类第三方 Admin 主题。但建议先把list_display、list_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()这是最便宜的回滚手段。Score里on_deletemodels.CASCADE是级联开关改成PROTECT后删除学生会抛ProtectedError。宁可不删也不能误删的场景就该用后者。5. 上线前用这三招验证成绩查询质量与缓存效果5.1 用 select_related 把查询次数从 361 压到 1成绩列表页和导出 Excel 场景最常见的性能杀手是 N1。用 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记录本次操作的所有 SQLlen()就是指标。成绩表外键只有 Student 和 Course一条 JOIN 全部解决后面加了多对多选课关系要改用prefetch_related。5.2 给统计接口加 5 分钟缓存班级平均分这类统计接口在成绩未变时结果恒定适合缓存。路由层直接包cache_pagefrom django.views.decorators.cache import cache_page urlpatterns [ path(api/class-avg/, cache_page(300)(class_avg_api), nameclass_avg_api), ]cache_page(300)把整个响应缓存 300 秒需要先在settings.py配缓存后端生产环境用 RedisCACHES { 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 --deploycollectstatic把 Admin 和第三方库的静态文件收到STATIC_ROOT并交给 Nginx 托管--noinput免除确认提示check --deploy会列出当前环境不符合生产配置的警告比如DEBUGTrue、ALLOWED_HOSTS为空、SECRET_KEY未从配置文件剥离是最快的部署自检方式。这两条跑完再用 gunicorn 启动项目成绩管理系统才算真正到了可交付状态。本文还有配套的精品资源点击获取
返回列表