ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/17 1:09:53 拓冰建站 浏览量
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_deletemodels.CASCADE, related_nameprofile) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) name models.CharField(max_length50, verbose_name姓名) birthday models.DateField(nullTrue, blankTrue) gender models.CharField(max_length10, choices((M, 男), (F, 女)), defaultM) address models.CharField(max_length200, blankTrue) id_card models.CharField(max_length18, blankTrue) banji models.ForeignKey(Banji, nullTrue, blankTrue, on_deletemodels.SET_NULL, verbose_name所属班级)OneToOneField保证一个 User 只对应一份扩展信息。related_nameprofile让你在视图中可以直接写request.user.profile.role不需要再手动查扩展表。我把班级外键放在 Profile 里是为了让学生和教师都能快速按班级筛选。教师也挂班级但教师挂的是「所带班级」与学生的「所属班级」业务含义不同这里靠视图逻辑区分不另建关系表。2.2 班级、专业、课程与成绩的关联方式核心表一共 7 张关系最微妙的是 Score 表。成绩不能直接挂 Student 表否则同一学生同一门课在不同考试批次里会产生多行重复数据。合理设计是把「学生选课关系」抽成StudentCourseScore 再挂在它上面同时挂考试批次。表名关键字段说明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_deletemodels.CASCADE, related_namecourses) course models.ForeignKey(Course, on_deletemodels.CASCADE) class Meta: unique_together (student, course) class Score(models.Model): student_course models.ForeignKey(StudentCourse, on_deletemodels.CASCADE, related_namescores) exam_batch models.ForeignKey(ExamBatch, on_deletemodels.CASCADE) score models.DecimalField(max_digits5, decimal_places2, 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, majormajor) course Course.objects.create(name数据结构, codeCS201) ExamBatch.objects.create(name2025年期中考试) admin_user User.objects.create_user(admin, passwordadmin123) Profile.objects.create(useradmin_user, roleadmin, name系统管理员) stu_user User.objects.create_user(student01, password123456) Profile.objects.create(userstu_user, rolestudent, name张三, banjibanji, genderM)create_user会自动对密码做 PBKDF2 散列不需要手动加密。Profile.objects.create时user必须传 User 实例这里容易犯的错是把username字符串传给外键Django 会直接报ValueError。批量生成学生账号时可以循环创建密码统一用123456正式交给老师前再让学生各自改密。3. 考试批次、成绩录入与按批次检索的实现3.1 考试批次表结构考试批次是教师录入和学生查询的公共维度。批次表除了名称最好加一个exam_date日期字段用于排序。默认按创建时间倒序而不是按 id 倒序。# grades/models.py class ExamBatch(models.Model): name models.CharField(max_length100, uniqueTrue, verbose_name考试名称) exam_date models.DateField(nullTrue, blankTrue, verbose_name考试日期) create_time models.DateTimeField(auto_now_addTrue) class Meta: ordering [-create_time]uniqueTrue防止两个批次同名。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(querysetExamBatch.objects.all(), label考试批次) student forms.ModelChoiceField(querysetProfile.objects.filter(rolestudent), label学生) course forms.ModelChoiceField(querysetCourse.objects.all(), label课程) score forms.DecimalField(max_digits5, decimal_places2, min_value0, max_value100, label成绩) def save(self): data self.cleaned_data student_course, _ StudentCourse.objects.get_or_create( studentdata[student], coursedata[course] ) score, created Score.objects.update_or_create( student_coursestudent_course, exam_batchdata[exam_batch], defaults{score: data[score]} ) return scoreget_or_create负责补齐选课关系update_or_create负责覆盖已有成绩。created可以用于提示「新增成功」或「更新成功」。表单里querysetProfile.objects.filter(rolestudent)是硬性过滤防止教师在下拉框里选到其他角色。视图层接收表单后只需要判断角色和调用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__studentprofile ).select_related(exam_batch, student_course__course) if batch_id: scores scores.filter(exam_batch_idbatch_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_batch和course通过 SQL JOIN 一次性取出来避免每行成绩都发一条查询。模板渲染时下拉框需要回显当前选中的批次。select nameexam_batch classform-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.css、font-awesome.css、bootstrap-theme.css、nav.css、signin.css、commons.css对应着前端目录的静态资源组织方式。Django 里用{% static %}标签引用这些文件模板结构上必须做继承否则每个页面重复写导航栏和 CSS 引用后期改一处要动十几个模板。!-- templates/base.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title{% block title %}学生成绩管理系统{% endblock %}/title link href{% static css/bootstrap.min.css %} relstylesheet link href{% static css/font-awesome.min.css %} relstylesheet link href{% static css/style.css %} relstylesheet {% block extra_css %}{% endblock %} /head body {% include includes/nav.html %} div classcontainer-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 classfa fa-user/i这种写法在nav.css配合下效果干净。注意block extra_css要保留因为各业务页面可能还需要引入 DatePicker 或表格插件。4.2 角色菜单按登录身份动态渲染管理员、教师、学生的菜单在同一个页面模板里渲染通过request.user.profile.role判断。ul classnav navbar-nav {% if request.user.profile.role admin %} lia href{% url admin_exam_list %}i classfa fa-calendar/i考试管理/a/li lia href{% url admin_teacher_list %}i classfa fa-user/i教师管理/a/li lia href{% url admin_class_list %}i classfa fa-building/i班级管理/a/li {% elif request.user.profile.role teacher %} lia href{% url teacher_score_entry %}i classfa fa-edit/i录入成绩/a/li lia href{% url teacher_class_list %}i classfa fa-group/i查看班级/a/li {% elif request.user.profile.role student %} lia href{% url student_score_list %}i classfa fa-list/i我的成绩/a/li lia href{% url student_rank %}i classfa 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异常在 DEBUGTrue 时会显示报错页面生产环境会返回 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 的写法这套系统使用 DjangoMySQL意味着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 migrate5.2 collectstatic 与静态文件路径的坑Django 开发模式下DEBUGTrue静态文件由runserver直接服务。一旦部署DEBUGFalse所有 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 --noinputSTATICFILES_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, usernameusername, passwordpassword) 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 或跳转登录页说明角色装饰器和视图过滤都正常如果直接打开了页面说明你只做了菜单隐藏后端视图没有做角色校验。这个验证点值得放在答辩演示里比展示界面截图更有说服力。本文还有配套的精品资源点击获取