news 2026/9/17 1:09:46

Django学生成绩管理系统开发:角色权限与数据模型设计实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django学生成绩管理系统开发:角色权限与数据模型设计实践

简介:一套基于Django与MySQL开发的学生成绩管理系统,面向Python Web开发学习者、毕业设计或课设使用者。系统按管理员、教师、学生三类角色设计,覆盖考试批次、教师、班级、课程、专业等基础信息管理,以及成绩录入、查看、排名、选课等完整业务流程,适合作为实战练习或项目二次开发的参考。资源共189个文件,rar压缩包仅1.24MB,核心代码以45个pyc、27个py文件为主,另有35个html页面模板,以及scss/less/css样式文件、JavaScript脚本、字体图标与sqlite数据库文件等,结构清楚,便于直接部署或按模块研读。至今已有11854人学习下载,说明项目方案被较多开发者验证过。从内容预览看,前端基于bootstrap与font-awesome搭建,界面干净实用,拿到手后可快速跑通登录、权限控制与成绩管理流程,节省从零搭建的时间。

1. 从登录角色分红聊起 Django 学生成绩管理系统

第一眼看到「三个角色下拉框登录」,以为是普通的课设脚手架,真正拆开才发现,这套 Django 学生成绩管理系统的难点不在增删改查,而是在同一个登录入口下把管理员、教师、学生的数据边界切干净。管理员维护考试批次、班级、教师、学生、课程、专业六类基础数据;教师录入成绩、查看班级专业、改自己密码;学生看成绩、看排名、选课。三层权限互不越界,前端用 Bootstrap 家族那套 CSS 撑住观感。适合正在做 Python 课设、想模仿教务系统权限模型,或者打算把 Django 的 User 扩展机制用熟练的人。下面不按 views.py 的顺序抄,直接从数据模型和角色权限讲起。

2. 三个角色背后的数据模型与权限设计

2.1 用户角色用 Django 内置 User 还是独立表

三张用户表是最容易犯的设计错,因为它们登录校验、密码重置、session 维护都得自己写一遍。这个项目正确做法是复用django.contrib.auth.models.User,再建一张Profile扩展表,用OneToOneField挂到 User 上,通过role字段区分身份。

# accounts/models.py from django.db import models from django.contrib.auth.models import User class Profile(models.Model): ROLE_CHOICES = ( ('admin', '管理员'), ('teacher', '教师'), ('student', '学生'), ) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='student') name = models.CharField(max_length=50, verbose_name='姓名') birthday = models.DateField(null=True, blank=True) gender = models.CharField(max_length=10, choices=(('M', '男'), ('F', '女')), default='M') address = models.CharField(max_length=200, blank=True) id_card = models.CharField(max_length=18, blank=True) banji = models.ForeignKey('Banji', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属班级')

OneToOneField保证一个 User 只对应一份扩展信息。related_name='profile'让你在视图中可以直接写request.user.profile.role,不需要再手动查扩展表。我把班级外键放在 Profile 里,是为了让学生和教师都能快速按班级筛选。教师也挂班级,但教师挂的是「所带班级」,与学生的「所属班级」业务含义不同,这里靠视图逻辑区分,不另建关系表。

2.2 班级、专业、课程与成绩的关联方式

核心表一共 7 张,关系最微妙的是 Score 表。成绩不能直接挂 Student 表,否则同一学生同一门课在不同考试批次里会产生多行重复数据。合理设计是把「学生选课关系」抽成StudentCourse,Score 再挂在它上面,同时挂考试批次。

表名关键字段说明
Majorname专业表
Banjiname、major(FK)班级属于专业
Coursename、code课程表
ExamBatchname、exam_date考试批次
StudentCoursestudent(FK)、course(FK)、unique_together学生选课关系
Scorestudent_course(FK)、exam_batch(FK)、score某次考试成绩
# grades/models.py class StudentCourse(models.Model): student = models.ForeignKey('accounts.Profile', on_delete=models.CASCADE, related_name='courses') course = models.ForeignKey(Course, on_delete=models.CASCADE) class Meta: unique_together = ('student', 'course') class Score(models.Model): student_course = models.ForeignKey(StudentCourse, on_delete=models.CASCADE, related_name='scores') exam_batch = models.ForeignKey(ExamBatch, on_delete=models.CASCADE) score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name='成绩') class Meta: unique_together = ('student_course', 'exam_batch')

unique_together是两层保障:第一层防止学生重复选同一门课,第二层防止同一学生在同一考试批次里同一门课出现两条成绩。没有这两个约束,录入页面快速连点两次提交按钮,就会产生脏数据。后续按学生查成绩时会出现对不上的总分和排名。

2.3 用 seed 脚本快速初始化角色数据

新项目跑起来前,先用独立脚本灌入基础数据,比在 Django Admin 里手动点快得多。我习惯在manage.py同级目录放seed.py,用shell方式执行。

python manage.py shell < seed.py
# seed.py from django.contrib.auth.models import User from accounts.models import Profile from grades.models import Banji, Major, Course, ExamBatch major = Major.objects.create(name='计算机科学与技术') banji = Banji.objects.create(name='计科2201', major=major) course = Course.objects.create(name='数据结构', code='CS201') ExamBatch.objects.create(name='2025年期中考试') admin_user = User.objects.create_user('admin', password='admin123') Profile.objects.create(user=admin_user, role='admin', name='系统管理员') stu_user = User.objects.create_user('student01', password='123456') Profile.objects.create(user=stu_user, role='student', name='张三', banji=banji, gender='M')

create_user会自动对密码做 PBKDF2 散列,不需要手动加密。Profile.objects.createuser必须传 User 实例,这里容易犯的错是把username字符串传给外键,Django 会直接报ValueError。批量生成学生账号时可以循环创建,密码统一用123456,正式交给老师前再让学生各自改密。

3. 考试批次、成绩录入与按批次检索的实现

3.1 考试批次表结构

考试批次是教师录入和学生查询的公共维度。批次表除了名称,最好加一个exam_date日期字段,用于排序。默认按创建时间倒序,而不是按 id 倒序。

# grades/models.py class ExamBatch(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name='考试名称') exam_date = models.DateField(null=True, blank=True, verbose_name='考试日期') create_time = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-create_time']

unique=True防止两个批次同名。ordering定义在 Meta 里之后,所有查询默认按这个排序,视图里不需要再写order_by。如果中间删过批次,再新增时 id 会继续增长,但create_time仍然能给出正确的先后顺序。

3.2 教师录入成绩的视图与表单

录入成绩页面的核心是表单设计。考试批次、学生、课程三个下拉框,一个成绩输入框。保存逻辑不能直接Score.objects.create,否则第二次录入会触碰唯一约束报错。

# grades/forms.py from django import forms from .models import Score, StudentCourse class ScoreForm(forms.Form): exam_batch = forms.ModelChoiceField(queryset=ExamBatch.objects.all(), label='考试批次') student = forms.ModelChoiceField(queryset=Profile.objects.filter(role='student'), label='学生') course = forms.ModelChoiceField(queryset=Course.objects.all(), label='课程') score = forms.DecimalField(max_digits=5, decimal_places=2, min_value=0, max_value=100, label='成绩') def save(self): data = self.cleaned_data student_course, _ = StudentCourse.objects.get_or_create( student=data['student'], course=data['course'] ) score, created = Score.objects.update_or_create( student_course=student_course, exam_batch=data['exam_batch'], defaults={'score': data['score']} ) return score

get_or_create负责补齐选课关系,update_or_create负责覆盖已有成绩。created可以用于提示「新增成功」或「更新成功」。表单里queryset=Profile.objects.filter(role='student')是硬性过滤,防止教师在下拉框里选到其他角色。视图层接收表单后只需要判断角色和调用save()

课程下拉框通常应该只显示该学生已选课程,而不是全部课程。常见做法是在视图里根据 POST 提交的学生 id 重新构造表单:

# grades/views.py @login_required def score_entry(request): form = ScoreForm() if request.method == 'POST': form = ScoreForm(request.POST) if form.is_valid(): form.save() return redirect('score_entry') return render(request, 'grades/score_entry.html', {'form': form})

这个版本把选课过滤交给前端 JS 或后续优化。实际项目中,我会在ScoreForm.__init__中接收student_id,把course字段的 queryset 动态过滤为已选课程。这个细节决定教师能不能录错课。

3.3 学生按考试批次快速检索成绩

学生查看成绩页面要求支持按考试批次筛选。这功能本质是一个GET查询参数过滤器,学生 id 直接从request.user.profile拿。

# grades/views.py @login_required def student_score_list(request): profile = request.user.profile batch_id = request.GET.get('exam_batch', '') scores = Score.objects.filter( student_course__student=profile ).select_related('exam_batch', 'student_course__course') if batch_id: scores = scores.filter(exam_batch_id=batch_id) return render(request, 'grades/student_scores.html', { 'scores': scores, 'batches': ExamBatch.objects.all(), 'current_batch': int(batch_id) if batch_id else '', })

student_course__student是跨关联字段查询,select_related会把exam_batchcourse通过 SQL JOIN 一次性取出来,避免每行成绩都发一条查询。模板渲染时,下拉框需要回显当前选中的批次。

<select name="exam_batch" class="form-control"> <option value="">全部批次</option> {% for b in batches %} <option value="{{ b.id }}" {% if b.id == current_batch %}selected{% endif %}>{{ b.name }}</option> {% endfor %} </select>

如果不回显,用户每次筛选后下拉框都会回到「全部批次」,以为筛选没生效。这是一个体验细节,但也经常成为答辩时的扣分点。

4. 前端界面、模板继承与角色菜单权限控制

4.1 Bootstrap 静态资源与三层模板继承

项目正文里列出的bootstrap.cssfont-awesome.cssbootstrap-theme.cssnav.csssignin.csscommons.css,对应着前端目录的静态资源组织方式。Django 里用{% static %}标签引用这些文件,模板结构上必须做继承,否则每个页面重复写导航栏和 CSS 引用,后期改一处要动十几个模板。

<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>{% block title %}学生成绩管理系统{% endblock %}</title> <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet"> <link href="{% static 'css/style.css' %}" rel="stylesheet"> {% block extra_css %}{% endblock %} </head> <body> {% include 'includes/nav.html' %} <div class="container-fluid"> {% block content %}{% endblock %} </div> <script src="{% static 'js/bootstrap.min.js' %}"></script> {% block extra_js %}{% endblock %} </body> </html>

{% include 'includes/nav.html' %}负责把导航栏抽成独立模板。子页面只需要写{% extends 'base.html' %}{% block content %}font-awesome.css里的图标类名直接用在导航<i>标签上,比如<i class="fa fa-user"></i>,这种写法在nav.css配合下效果干净。注意block extra_css要保留,因为各业务页面可能还需要引入 DatePicker 或表格插件。

4.2 角色菜单按登录身份动态渲染

管理员、教师、学生的菜单在同一个页面模板里渲染,通过request.user.profile.role判断。

<ul class="nav navbar-nav"> {% if request.user.profile.role == 'admin' %} <li><a href="{% url 'admin_exam_list' %}"><i class="fa fa-calendar"></i>考试管理</a></li> <li><a href="{% url 'admin_teacher_list' %}"><i class="fa fa-user"></i>教师管理</a></li> <li><a href="{% url 'admin_class_list' %}"><i class="fa fa-building"></i>班级管理</a></li> {% elif request.user.profile.role == 'teacher' %} <li><a href="{% url 'teacher_score_entry' %}"><i class="fa fa-edit"></i>录入成绩</a></li> <li><a href="{% url 'teacher_class_list' %}"><i class="fa fa-group"></i>查看班级</a></li> {% elif request.user.profile.role == 'student' %} <li><a href="{% url 'student_score_list' %}"><i class="fa fa-list"></i>我的成绩</a></li> <li><a href="{% url 'student_rank' %}"><i class="fa fa-trophy"></i>成绩排名</a></li> {% endif %} </ul>

前端隐藏菜单只是体验层面,真正的权限控制必须在视图上。如果不加保护,学生登录后手工输入教师录入成绩的 URL,就能直接访问到录分页面。自定义一个角色装饰器是最直接的方案:

# accounts/decorators.py from functools import wraps from django.core.exceptions import PermissionDenied def role_required(*roles): def decorator(view_func): @wraps(view_func) def _wrapped(request, *args, **kwargs): if not request.user.is_authenticated: return redirect('login') if request.user.profile.role not in roles: raise PermissionDenied return view_func(request, *args, **kwargs) return _wrapped return decorator

使用方式就是@role_required('teacher')。注意PermissionDenied异常在 DEBUG=True 时会显示报错页面,生产环境会返回 403。如果你希望越权访问时直接重定向到登录页而不是 403,可以改成return redirect('login')

4.3 修改密码与个人信息编辑的安全处理

密码修改最容易踩的坑是直接给 User 的password字段赋值明文,这样数据库里存的不是散列值,导致后续登录时authenticate()永远失败。正确做法是set_password(),并且改完后调update_session_auth_hash()保持当前登录态。

# accounts/views.py from django.contrib.auth import update_session_auth_hash from django.contrib.auth.decorators import login_required @login_required def change_password(request): if request.method == 'POST': old_password = request.POST.get('old_password') new_password = request.POST.get('new_password') if request.user.check_password(old_password): request.user.set_password(new_password) request.user.save() update_session_auth_hash(request, request.user) return redirect('login') return render(request, 'accounts/change_password.html', {'error': '原密码错误'}) return render(request, 'accounts/change_password.html')

check_password内部会把输入的明文和数据库散列做比对,不需要自己重新散列。update_session_auth_hash的作用是刷新 session 中的认证哈希,否则改密后当前 session 失效,用户被直接踢回登录页。这个函数平时不起眼,但漏掉它体验就很糟糕。

个人信息编辑页,教师和学生只能编辑自己的信息。视图里取用户时用request.user.profile而不是根据 URL 参数用户 id 去查。管理员编辑任意用户时,要判断一下目标用户是否存在于对应角色表中,否则会出现「管理员把学生资料改成了教师但角色没变」的错乱。

5. 把系统跑起来:MySQL 配置、静态文件收集与三个坑

5.1 settings.py 连接 MySQL 的写法

这套系统使用 Django+MySQL,意味着settings.py里的数据库配置不能沿用默认的 sqlite3。修改DATABASES后,还需要安装 MySQL 驱动。Python 3.8 以上版本,mysqlclient在 Windows 上经常编译失败,常见替代方案是在项目__init__.py中强制使用pymysql伪装。

# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'student_grade_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }

utf8mb4是必须的,否则中文姓名和考试名称插入时可能报Incorrect string value。执行迁移命令时,要让 App 内的模型先被识别:

python manage.py makemigrations accounts grades python manage.py migrate

5.2 collectstatic 与静态文件路径的坑

Django 开发模式下DEBUG=True,静态文件由runserver直接服务。一旦部署,DEBUG=False,所有 CSS 都会失效。你需要先定义收集目录,再执行收集命令。

STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] STATIC_ROOT = os.path.join(BASE_DIR, 'collected_static')

然后执行:

python manage.py collectstatic --noinput

STATICFILES_DIRS是源目录,STATIC_ROOT是收集目标。很多人把两者设成同一个目录,collectstatic会报错。收集完成后,部署层将collected_static映射到 URL 路径/static/即可。如果实在没配好,生产环境页面会出现只有文字没有样式的裸样式,这是项目交付时最尴尬的场面。

5.3 登录身份校验与部署前三件事

最后把系统收口:登录视图要校验角色下拉框与实际用户角色是否一致。

# accounts/views.py from django.contrib.auth import authenticate, login def login_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') role = request.POST.get('role') user = authenticate(request, username=username, password=password) if user is not None and user.profile.role == role: login(request, user) return redirect('dashboard_home') elif user is not None: return render(request, 'accounts/login.html', {'error': '身份与账号角色不符'}) else: return render(request, 'accounts/login.html', {'error': '用户名或密码错误'}) return render(request, 'accounts/login.html')

角色不符时不要login(request, user),直接返回错误提示。因为登录之后页面会按角色渲染菜单,如果角色不匹配,一个学生可能看到教师菜单并访问到录分页面,即使后端有装饰器拦截,体验也已经乱了。这里redirect('dashboard_home')会调用一个决定性的视图,根据角色跳转到各自的首页。

生产部署前还有三件事必须改。第一,DEBUG设置为False,否则任何报错都会显示堆栈和数据库连接信息。第二,ALLOWED_HOSTS配置为实际域名或 IP,形如['your-domain.com']。第三,确认模板里的每个<form>都有{% csrf_token %},并且没有在中间件里误删django.middleware.csrf.CsrfViewMiddleware。CSRF 被挡的表现是 POST 提交返回 403,排查时先检查这两处。

验证权限是否生效的简单方法:用一个学生账号登录,然后手工在浏览器地址栏输入教师录入成绩的 URL。如果系统返回 403 或跳转登录页,说明角色装饰器和视图过滤都正常;如果直接打开了页面,说明你只做了菜单隐藏,后端视图没有做角色校验。这个验证点值得放在答辩演示里,比展示界面截图更有说服力。

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

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

GitKraKen:可视化理解Git工作区/暂存区/本地仓库模型

1. 项目概述&#xff1a;GitKraKen不是Git&#xff0c;也不是Kragen&#xff0c;更不是拼写错误“GitKraKen”这个名称一出现&#xff0c;我身边好几个刚接触版本控制的新手第一反应都是&#xff1a;“这是Git的新UI&#xff1f;还是某个开源社区搞的KubernetesGit混合体&#…

作者头像 李华
网站建设 2026/9/17 1:06:03

基于SpringBoot+Vue的足球俱乐部管理系统开发实战

简介&#xff1a;这是一份基于SpringBoot与Vue的足球俱乐部管理系统完整项目&#xff0c;主要面向Java学习者、毕业设计或课程设计者&#xff0c;也可供需要快速搭建俱乐部信息化管理原型的人员参考。系统按用户、教练、管理员三个角色划分权限&#xff0c;覆盖公告、赛事、球员…

作者头像 李华
网站建设 2026/9/17 1:04:11

2026爆火个人AI助手:本地化运行与隐私保护技术解析

1. 项目概述&#xff1a;26年爆火的个人AI助手2026年最火的个人AI助手项目在GitHub上已经狂揽5万Star&#xff0c;这个数字还在持续增长。作为一个长期关注AI领域的开发者&#xff0c;我第一时间下载并深度体验了这个项目。它之所以能引发如此大的关注&#xff0c;核心在于实现…

作者头像 李华
网站建设 2026/9/17 1:03:24

VSCode下载失败/缓慢怎么办?从安装到C/C++、Python环境配置完整指南

不少刚接触VScode的朋友&#xff0c;第一个拦路虎其实不是写代码&#xff0c;而是下载和安装。我见过太多次这样的情况&#xff1a;电脑配置不错&#xff0c;网络也显示“连接正常”&#xff0c;结果打开VScode官网&#xff0c;下载按钮点下去&#xff0c;进度条要么纹丝不动&a…

作者头像 李华
网站建设 2026/9/17 1:00:39

因果表示学习:让机器理解数据背后的因果结构

因果表示学习最近在机器学习圈子里讨论度一直往上走&#xff0c;很多做视觉、RL、医疗AI的朋友都在问这到底是个什么方向&#xff0c;和普通的表示学习有什么区别。简单说&#xff0c;它要解决的问题是&#xff1a;模型学到的特征向量到底“代表”了什么&#xff1f;如果特征里…

作者头像 李华
网站建设 2026/9/17 0:58:00

授权码换 token 报 invalid_grant?TaoToken 这样改 Codex 通道。

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华