资讯详情

资讯详情

Django学生成绩管理系统:从数据模型到ORM查询与部署全解析

简介基于Python与Django的学生成绩管理系统完整项目包专注于学生信息管理、成绩录入、查询与统计分析等核心业务面向毕业设计学生、Python Web开发者也适合教育机构用于教务管理场景。系统后端采用Django框架前端包含Vue组件与静态资源数据库脚本采用SQL形式整体遵循MVC设计模式结构分层清晰便于二次扩展与维护。整个资源包共362个文件压缩后10.53MB主要文件类型涵盖Python源码、Vue组件、JavaScript与CSS、SVG图标、SQL数据库脚本、JSON/YML配置以及docx/ppt说明文档和用于安装、运行、构建的bat批处理脚本目录结构完整可快速定位所需模块。目前已有995人学习下载。项目包不仅提供可运行的完整系统还包含数据库设计文档、开发说明和环境配置指引可帮助理解数据表结构、字段约束以及Django模型-视图-模板的实现方式。数据库模型设计覆盖数据完整性、一致性与安全性要求源码注重模块化与注释规范适合作为毕业设计参考也可按需扩展教师、课程、班级等模块整体具备较好的学习与实用价值。1. 学生成绩管理系统的核心定位为什么是 pythondjango 这一套数据库课程设计里「学生成绩管理系统」是出现频率最高的题目之一。很多人第一反应是 Java 或 PHP但换成 pythondjango 后开发节奏完全不同ORM 把建表和增删改查从手写 SQL 里解放出来自带 Admin 后台让人不写前端就能维护数据模板渲染成绩表只需几行循环。网上这类源码也大多是一个 zipdjango 项目源码、SQLite 数据库文件和课程设计文档。难点不在功能而在数据模型规不规范、查询统计顺不顺手、部署时会不会被 mysqlclient 这类环境问题卡住。下面按最直接的从业方案拆解数据模型与数据库配置、Admin 与批量导入、ORM 查询统计、本地跑通与部署排错。适合正在做数据库课程设计的学生、想快速搭内部小工具的开发者以及想把 Django CRUD 与聚合查询理清的人。2. 拆解成绩管理系统的 Django 数据模型与数据库设计2.1 三张核心表学生、课程、成绩为什么必须拆开一个学生选多门课一门课被多个学生选成绩就是这层多对多关系上的属性所以必须拆成 Student、Course、Grade 三张表。如果把分数塞进学生表一个学生每学期十几门课就会出现大量冗余列新增一门课还得改表结构反过来塞进课程表也一样。这是数据库课程设计里范式考点最集中的地方答辩时基本必问。设计细节上有三个约定俗成的做法。学号设 unique 但继续用 Django 默认自增 id 做主键因为学号规则可能被学校调整unique 约束比主键好改。学分、成绩这类数值用 DecimalField 而不是 FloatField浮点类型在平均分计算里会出 0.30000000000000004 这种问题。Grade 表只放学生、课程的外键和分数、学期、考试性质不冗余存姓名或课程名否则更新学生姓名时成绩表也要跟着改。2.2 用 django 创建 app 并落地 models.py字段参数逐个说明先建 apppython manage.py startapp grader创建完必须把 grader 加进 settings.py 的 INSTALLED_APPS否则 makemigrations 根本不扫描这个 app 的模型。漏掉这一步的表现是 migrate 时没有反应系统目录也找不到原因这是 django 新人最高频的翻车点之一。models.py 的完整写法from django.db import models class Student(models.Model): student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50) gender models.CharField(性别, max_length1, choices[(M, 男), (F, 女)], defaultM) class_name models.CharField(班级, max_length50, db_indexTrue) enroll_year models.PositiveIntegerField(入学年份, default2024) def __str__(self): return f{self.student_no} {self.name} class Course(models.Model): course_no models.CharField(课程号, max_length20, uniqueTrue) name models.CharField(课程名, max_length100) credit models.DecimalField(学分, max_digits3, decimal_places1) teacher models.CharField(授课教师, max_length50, blankTrue) def __str__(self): return f{self.course_no} {self.name} class Grade(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_namegrades) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namegrades) score models.DecimalField(成绩, max_digits5, decimal_places2) semester models.CharField(学期, max_length20) exam_type models.CharField(考试性质, max_length10, choices[(normal, 正常), (makeup, 补考)], defaultnormal) class Meta: constraints [ models.UniqueConstraint( fields[student, course, semester, exam_type], nameunique_grade_record) ] ordering [student__student_no] def __str__(self): return f{self.student} - {self.course} - {self.score}字段参数的用途对照表参数作用本系统中的落点max_lengthCharField 必填限制字符串长度学号、姓名、学期unique数据库层唯一约束防重复数据学号、课程号choices限定可选值Admin 里渲染成下拉框性别、考试性质db_index给列建索引加速 WHERE 过滤班级按班查成绩是高频操作related_name指定反向查询名student.grades.all()on_delete外键指向的记录删除时的策略CASCADE / PROTECTblank表单层面允许不填授课教师这种可空字段default不传值时的默认值入学年份、考试性质外键删除策略单独说一句。成绩表里用 on_deletemodels.CASCADE意味着删除学生时他的成绩一起删如果改成 PROTECT那么存在成绩的学生不允许删除防止误删后成绩单对不上。教务系统我一般建议 PROTECT单纯交课程设计用 CASCADE 最快答辩时能把区别讲清楚就是加分项。执行迁移python manage.py makemigrations grader python manage.py migrate python manage.py createsuperusermakemigrations 生成 migrations 目录下的迁移文件migrate 才真正在数据库里建表createsuperuser 建后台管理员。三步缺一不可很多人只跑了一步就急着去数据库里找表。2.3 数据库选型与 settings.py 配置SQLite 还是 MySQLzip 包里最常见的「数据库」就是项目自带的 SQLite 的 .db 文件本机演示、交作业用它零成本。但 SQLite 是整库级写锁多个人同时录成绩会明显卡顿正式部署建议切成 MySQL。对比项SQLiteMySQL安装成本Django 内置驱动零配置需装服务端加 mysqlclient 驱动并发写入整库锁多人录入会卡行级锁适合多人同时操作备份迁移拷贝 .db 文件即可需要 mysqldump 导出 SQL适用场景演示、作业、单机小工具正式部署、并发录入settings.py 切到 MySQL 的写法DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: grade_system, USER: grade_user, PASSWORD: Grade2024, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, }, }参数含义ENGINE 指定 Django 用哪套数据库后端NAME 是库名需要先在 MySQL 里执行CREATE DATABASE grade_system DEFAULT CHARSET utf8mb4;migrate 不会帮你建库HOST 写 127.0.0.1 表示连本机 MySQLcharset 用 utf8mb4 是为了中文和生僻字不乱码utf8 在 MySQL 里其实存不了四字节字符。切库后先装驱动pip install mysqlclientmysqlclient 在 Windows 上经常因为缺编译环境安装失败常见做法是装预编译的 wheel或者改用 pymysql 并在项目__init__.py里执行pymysql.install_as_MySQLdb()。pymysql 能跑通但性能不如 mysqlclient正式环境不推荐长期用。提示MySQL 8.0 默认的 caching_sha2_password 认证插件会让老版本驱动报Authentication plugin ... cannot be loaded把账号改成 mysql_native_password 即可绕开。3. 把成绩数据灌进数据库Admin 注册、fixtures 与批量导入脚本3.1 注册 Django Admin 与列表页参数list_display、list_filter、search_fieldsDjango 自带 Admin 是这类成绩管理系统最省事的维护入口不用写一个前端页面就能完成学生、课程、成绩的增删改查。但默认注册只是「能看」把列表配好才算好用这也是 django admin界面美化 话题里最常被提到的部分。所谓美化核心是把列表字段、筛选器、搜索框配到位。admin.py 的推荐写法from django.contrib import admin from .models import Student, Course, Grade admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display (student_no, name, gender, class_name, enroll_year) list_filter (class_name, gender) search_fields (student_no, name) ordering (student_no,) list_per_page 20 admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (course_no, name, credit, teacher) search_fields (course_no, name) admin.register(Grade) class GradeAdmin(admin.ModelAdmin): list_display (student, course, score, semester, exam_type) list_filter (semester, exam_type, course) search_fields (student__student_no, student__name, course__name) list_select_related (student, course) list_per_page 50参数说明list_display 决定列表页显示哪些列外键字段直接写student会显示其str结果list_filter 在右侧生成筛选面板成绩表按 semester 和 exam_type 筛是使用频率最高的操作search_fields 里用双下划线跨表搜索student__student_no表示按关联学生的学号搜list_select_related 解决列表页对成绩表逐行反查学生和课程造成的 N1 查询数据量一大这个参数能让页面快一个数量级。如果嫌原生界面丑可以在 INSTALLED_APPS 里把 django-simpleui 放在 django.contrib.admin 前面刷新后就是一套现代化皮肤字段配置完全不用改。这类第三方库按需使用不改变上面这些参数的含义。3.2 造演示数据的三种方式dumpdata、shell 脚本、CSV 导入zip 里的数据库文件可能自带一批数据也可能需要自己生成。三种常见做法的取舍方式命令或手段适合场景fixtures 导入dumpdata 导出再 loaddata 导入把已有系统的数据原样搬走shell 脚本manage.py shell 里批量创建生成随机成绩、造演示数据CSV 导入csv 模块加 get_or_create接教务导出的 Excel 数据fixtures 的方式python manage.py dumpdata grader --indent 2 grader_fixture.json python manage.py loaddata grader_fixture.jsondumpdata 把 grader 这个 app 三个表的数据序列化成 JSONloaddata 再原样写回数据库。注意 fixtures 里带的是自增主键和外键 id导入到已经有过数据的库里可能主键冲突干净库或清空表后再导入最稳妥。shell 批量造数据的脚本import random from grader.models import Student, Course, Grade course Course.objects.create( course_noCS101, namePython 程序设计, credit3.0, teacher张老师) for i in range(1, 31): s, created Student.objects.get_or_create( student_nof2024{i:04d}, defaults{name: f学生{i}, class_name: 计科2401}) Grade.objects.create( students, coursecourse, scorerandom.randint(55, 100) random.choice([0, 0.5]), semester2024-2025-1)get_or_create 返回的是一个二元组第一个元素是对象第二个是布尔值表示是否新建。这里学号唯一保证脚本可以重复执行重复跑不会堆出重复学生。成绩用 randint 加 0.5 的随机尾数是为了后面验证 Decimal 精确计算的表现。3.3 CSV 导入编码与 get_or_create 的去重细节教务系统导出的通常是 Excel转成 CSV 后字段顺序和命名五花八门导入脚本最稳的写法是按列名取数而不是按位置import csv from decimal import Decimal from grader.models import Student, Course, Grade with open(scores.csv, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: s, _ Student.objects.get_or_create( student_norow[学号].strip(), defaults{name: row[姓名].strip(), class_name: row[班级].strip()}) c, _ Course.objects.get_or_create( course_norow[课程号].strip(), defaults{name: row[课程名].strip(), credit: Decimal(row[学分].strip())}) Grade.objects.get_or_create( students, coursec, semesterrow[学期].strip(), defaults{score: Decimal(row[成绩].strip())})编码用 utf-8-sig 而不是 utf-8Excel 保存的 CSV 带 BOM用 utf-8 读会把\ufeff留在第一列的第一个字段里学号立刻匹配不上。每列都 strip 一次是为了去掉单元格里的空格和换行。成绩转 Decimal 时如果文件里有空单元格这里会抛 InvalidOperation处理方式是先判断if row[成绩].strip():再转换空值跳过或记到日志里。几千行以内的数据逐条 get_or_create 完全够用几万行就要换成 bulk_create 分批写否则每行一次 INSERT 会把数据库拖慢。bulk_create 不做唯一约束去重导入前先按学号做一次查询或先清空目标表。注意如果 CSV 来自 WPS列名可能带空格或全角字符先用文本编辑器预览表头再写 row 的取值键否则 KeyError 会让人误以为是编码问题。4. 用 ORM 做成绩增删改查、聚合统计与排名从查询到页面渲染4.1 成绩 CRUD 的标准写法与常见误用成绩管理系统本质上就是一张成绩表的增删改查加上统计。先看四个标准动作from grader.models import Grade # 新增create 一步完成 Grade.objects.create( student_id1, course_id1, score88.5, semester2024-2025-1) # 修改queryset.update 一次 UPDATE比 save 循环快得多 Grade.objects.filter(exam_typemakeup, score__gte60).update(exam_typenormal) # 删除返回元组(总删除数, 各表删除明细) Grade.objects.filter(semester2023-2024-2).delete() # 查询单条get 必须恰好一条否则抛异常 g Grade.objects.get(student_id1, course_id1)这四段代码覆盖了 django 执行查询-删除对象 的标准写法但有三个容易踩的边界。第一get 查不到或查到多条会分别抛 DoesNotExist 和 MultipleObjectsReturned如果只是取一条做展示用filter(...).first()更安全查不到返回 None。第二批量更新一定用 queryset.update不要写for g in qs: g.score xxx; g.save()那会变成 N 条 UPDATEupdate 直接生成一条 UPDATE 语句。第三delete 返回的是元组不是删除条数取数要用deleted[0]这个返回值经常让人误解。反向查询也要用起来。视图里已经拿到 Student 对象时s.grades.all()比Grade.objects.filter(students)更贴近模型语义还能配合 related_name 控制命名。related_name 没指定时反向查询名是grade_set不写容易和正向外键搞混。4.2 聚合查询平均分、及格率与按课程分组统计统计功能靠 aggregate 和 annotate 两组方法。aggregate 返回一个字典只算一行结果annotate 配合 values 可以在每一行分组上追加统计列。from django.db.models import Avg, Count, Max, Min, Q # 单门课程的成绩统计 stats Grade.objects.filter(course_id1).aggregate( avg_scoreAvg(score), high_scoreMax(score), low_scoreMin(score), totalCount(id), ) # 按班级和课程分组统计平均分与不及格人数 report ( Grade.objects.values(student__class_name, course__name) .annotate( avg_scoreAvg(score), fail_countCount(id, filterQ(score__lt60)), totalCount(id), ) .filter(total__gte5) .order_by(-avg_score) )values 里写什么字段group by 就按什么字段分。Django 2.0 以后 Count、Sum 这些聚合函数支持 filter 参数Count(id, filterQ(score__lt60))直接在分组内统计不及格人数不需要先过滤再分组。.filter(total__gte5)是对分组结果做过滤语义上相当于 SQL 的 HAVING用来排除只有一两个人考试的课程避免平均分失真。常用聚合函数对照表函数作用参数写法Avg平均值Avg(score)Count计数可带条件Count(id, filterQ(score__lt60))Max / Min最高 / 最低分Max(score)Sum求和比如总学分Sum(course__credit)4.2.1 及格率怎么算才不出错及格率在 Python 侧算最直接pass_rate pass_count / total * 100其中 pass_count 和 total 从同一个分组结果里取保证分母一致。不要用两个独立查询再除数据变动时两个结果对不上排错成本很高。及格线不是写死 60有的课程按 5 分制或 150 分制建议在 Course 表上加一个 pass_line 字段统计时统一读这个字段而不是散落在各处代码里。4.3 排名查询与模板渲染Window、annotate 与视图函数排名在 MySQL 5.7 以下版本要写一堆用户变量Django 2.0 之后可以直接用 Window 函数在 ORM 层解决from django.shortcuts import render from django.db.models import F, Window from django.db.models.functions import RowNumber def course_rank(request, course_id): grades ( Grade.objects.filter(course_idcourse_id) .select_related(student) .annotate(rankWindow( expressionRowNumber(), order_byF(score).desc())) .order_by(rank) ) return render(request, grader/rank.html, {grades: grades})Window 的 expression 决定排名算法RowNumber 按成绩降序逐行编号同分也会分出名次。如果要求并列名次换成 Rank同分占同一个名次后续跳号或 DenseRank同分同号且不跳号。教务场景一般用 Rank因为第二名并列两个人时后面直接是第四名符合奖学金排名习惯。视图返回的是 queryset模板里直接用就行table trth名次/thth学号/thth姓名/thth成绩/th/tr {% for g in grades %} tr td{{ g.rank }}/td td{{ g.student.student_no }}/td td{{ g.student.name }}/td td{{ g.score }}/td /tr {% endfor %} /tableselect_related(student) 在这是必须的没有它模板里每访问一次 g.student 就发一条 SQL 查学生表30 行成绩变成 30 条额外查询这就是 N1 问题。加入 select_related 后成绩和学生用一条 JOIN 拿回来。URL 路由挂到 viewsfrom django.urls import path from grader import views urlpatterns [ path(course/int:course_id/rank/, views.course_rank, namecourse_rank), ]int:course_id是路径转换器只匹配整数Django 会把值转成 int 传给视图函数。这套模板渲染的形态不做前后端分离如果要求提供接口给前端把 render 换成 JsonResponse 返回 rank、student_no、score 的列表即可查询逻辑一行不用改。5. 本地跑通与宝塔部署 Django 成绩管理系统的排错清单5.1 从 zip 源码到本地运行的最小命令序列解压后不要急着双击运行按顺序来cd grade_system python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt # 没有则 pip install django mysqlclient python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000venv 隔离依赖是第一件事zip 包自带 SQLite 库时先备份再 migrate旧表结构和当前代码版本不一定匹配报错就从这里查。跑起来后先访问 /admin 登录确认成绩表里有数据再改代码。5.2 gunicorn nginx 与宝塔面板部署的配置要点本地 runserver 只能用于开发上线用 gunicorn 跑 WSGI 应用nginx 负责静态文件和反向代理pip install gunicorn python manage.py collectstatic --noinput gunicorn grade_system.wsgi:application -b 127.0.0.1:8000 --workers 2worker 数按 CPU 核数的两倍取整即可小项目 2 个够用调高反而抢内存。nginx 配置server { listen 80; server_name your_domain_or_ip; location /static/ { alias /opt/grade_system/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }静态文件必须交给 nginx 的 alias 指向 collectstatic 的输出目录否则页面有 HTML 没样式。宝塔面板里常见做法是用「Python 项目管理器」导入项目、启动方式选 gunicorn再配置反向代理原理与上面这段 nginx 配置一致。5.3 上线前用 check --deploy 做一轮安全检查上线前跑一个经常被忽略的内置检查python manage.py check --deploy这个命令会列出所有不安全配置常见输出SECRET_KEY 硬编码在 settings 里、DEBUG 没设为 False、ALLOWED_HOSTS 为空。逐条处理后500 报错时先看 gunicorn 日志而不是反复刷新页面。现象原因处理DisallowedHost / 400ALLOWED_HOSTS 没配加上域名或 IP502 Bad Gatewaygunicorn 没起或端口不一致确认 8000 端口有进程监听403 CSRF verification failed模板表单缺 csrf_tokenform 里加 {% csrf_token %}ModuleNotFoundError: MySQLdbmysqlclient 未安装pip install mysqlclient登录后样式全丢静态文件没收集或没指向重跑 collectstatic 并检查 alias最后留一个验证技巧部署完成后用一条 shell 命令核对数据完整性python manage.py shell -c from grader.models import Student, Course, Grade; print(Student.objects.count(), Course.objects.count(), Grade.objects.count())三个数分别对上学号、课程、成绩的记录量和 zip 文档或原系统导出数一致说明迁移没丢数据之后再写统计和排名时排查方向就不会往「数据不齐」上偏。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →