第13章 · 智慧星学生管理系统

用 Django 自带的用户/分组/权限系统,几乎不写后台代码,做出"管理员/老师/学生"三种角色的完整管理系统。

🎯 三种角色登录 → 各自看到不一样的后台

项目长啥样:三种角色,一个系统

🛡️ 管理员(superuser)
  • 所有权限
  • 建老师账号
  • 分配权限组
👨‍🏫 老师(is_staff=True)
  • 登录 Django 后台
  • 录入学生
  • 录入成绩
👨‍🎓 学生
  • 登录前台页面
  • 查自己的成绩
💡 这章为什么不重新造轮子
Django 自带完整的用户 / 分组 / 权限系统(auth_user / auth_group / auth_permission),还自带后台 UI。新人项目里"自己写一套"九成会写崩——直接用 Django 自带的,这就是"框架的红利"。本章重点就是怎么用好,而不是怎么从零写。
1 骨架

建项目 + 应用

SHELL
# 1. 建项目根目录
$ cd /home/django
$ django-admin startproject student_system
$ cd student_system

# 2. 在项目里建一个 user 应用:管学生 / 老师 / 成绩三类数据
$ python3 manage.py startapp user

注册应用 + 改基础配置

修改 student_system/settings.py

PYTHON student_system/settings.py(关键改动)
# ① 加 user 应用
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user',                 # ← 新加
]

# ② 课堂演示:允许任何 host 访问
ALLOWED_HOSTS = ['*']

# ③ 中文 + 北京时间,看着舒服
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False             # 关闭时区,时间存什么显示什么
2 模型

三张表:学生 / 老师 / 成绩

💬 关键设计:和 auth_user 挂钩
Django 自带 auth_user 表存"账号 + 密码 + 是否员工"。我们再建 student / teacher 两张表存"学号 / 班级 / 邮箱"等业务字段——两张业务表通过 OneToOneField 连到 auth_user。
这样:登录认证用 Django 自带的(密码哈希、登录拦截全免费),业务字段我们自己存。

修改 user/models.py

PYTHON user/models.py
from django.db import models
from django.contrib.auth.models import User


# =========== 学生表 ===========
# 一个学生 ↔ 一个 auth_user 账号(一对一)
class Student(models.Model):
    # OneToOneField:建立一对一关系
    # 学生删除时,对应的 User 也一起删(CASCADE)
    user = models.OneToOneField(User, on_delete=models.CASCADE,
                                verbose_name='登录账号')
    sno = models.CharField('学号', max_length=20, unique=True)
    name = models.CharField('姓名', max_length=30)
    klass = models.CharField('班级', max_length=30)

    class Meta:
        verbose_name = '学生'
        verbose_name_plural = '学生'     # 后台不显示"Students"

    def __str__(self):
        return f"{self.sno} {self.name}"


# =========== 老师表 ===========
class Teacher(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE,
                                verbose_name='登录账号')
    name = models.CharField('姓名', max_length=30)
    subject = models.CharField('科目', max_length=30)

    class Meta:
        verbose_name = verbose_name_plural = '老师'

    def __str__(self):
        return self.name


# =========== 成绩表 ===========
# 一名学生有多次成绩 → 学生是"一",成绩是"多" → ForeignKey 在成绩表
class Score(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE,
                                verbose_name='学生')
    subject = models.CharField('科目', max_length=30)
    score = models.DecimalField('分数', max_digits=5, decimal_places=1)
    exam_date = models.DateField('考试日期')

    class Meta:
        verbose_name = verbose_name_plural = '成绩'

    def __str__(self):
        return f"{self.student.name} {self.subject} {self.score}"

同步到数据库

SHELL
$ python3 manage.py makemigrations
$ python3 manage.py migrate
💡 verbose_name 的好处
模型字段后面那串中文(如 '学号')是 verbose_name——后台界面里显示成中文。没设的话英文字段名直接当标题,丑且不友好
3 后台

把模型挂到 Django 自带后台

修改 user/admin.py用 ModelAdmin 子类给后台加点料——这是第9章学过的进阶用法:

PYTHON user/admin.py
from django.contrib import admin
from .models import Student, Teacher, Score


# =========== 学生后台 ===========
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
    # list_display:列表页显示哪些列
    list_display = ('sno', 'name', 'klass', 'user')
    # list_filter:右侧筛选侧栏,按班级筛选
    list_filter = ('klass',)
    # search_fields:上方搜索框,按学号 / 姓名搜
    search_fields = ('sno', 'name')


# =========== 老师后台 ===========
@admin.register(Teacher)
class TeacherAdmin(admin.ModelAdmin):
    list_display = ('name', 'subject', 'user')
    list_filter = ('subject',)


# =========== 成绩后台 ===========
@admin.register(Score)
class ScoreAdmin(admin.ModelAdmin):
    list_display = ('student', 'subject', 'score', 'exam_date')
    list_filter = ('subject', 'exam_date')
    # date_hierarchy:上方按日期分级筛选(年/月/日下拉)
    date_hierarchy = 'exam_date'
💬 4 个高频 ModelAdmin 属性
属性作用
list_display列表页要显示的字段
list_filter右侧侧栏筛选项
search_fields上方搜索框搜哪几个字段
date_hierarchy按日期字段分级钻取

建超级管理员 + 启动

SHELL
$ python3 manage.py createsuperuser
# 用户名 admin / 邮箱随便填 / 密码自定(输入时不显示是正常的)

$ python3 manage.py runserver 0.0.0.0:8000

访问 http://IP:8000/admin/,用 admin 登录——能看到学生 / 老师 / 成绩三栏,所有增删改查界面 Django 已经替你画好了

4 权限

给"老师"角色定一组权限

💬 Django 权限系统的三层关系
  • 权限(Permission):每个模型自动生成 4 个,比如 user.add_studentuser.change_studentuser.delete_studentuser.view_student
  • 分组(Group):一组权限的集合。比如建一个"老师组",包含"管学生 + 管成绩"。
  • 用户(User):被归到哪个组,就拥有那个组的全部权限。
整个流程不需要写一行代码,全在后台界面完成。

步骤:在 admin 后台手动操作

  1. 用 admin 登录 /admin/
  2. 左侧找 认证和授权 → 组,点"增加组"
  3. 组名填 老师
  4. 下方"权限"列表里挑选 → 选 user | 学生 | Can add 学生 / Can change 学生 / Can view 学生 / Can change 学生,再选 user | 成绩 | ... 一组(不要给老师 delete 学生权限)
  5. 保存

建一个老师账号

  1. 左侧 认证和授权 → 用户 → "增加用户"
  2. 用户名 teacher1,密码 teacher123,保存
  3. 下一页:勾选 is_staff(员工状态)= True——这样他能登录后台
  4. 下方"组"里把刚建的"老师"组加进去
  5. 保存

🎉 验收:用 teacher1 登录

退出 admin → 用 teacher1 / teacher123 登录 /admin/

会看到只有"学生"和"成绩"两栏,不能操作老师 / 用户 / 组——权限自动起作用

而且学生那栏也只能"添加 / 修改",不能删除(因为我们没给 delete 权限)

🎯 角色 = 权限组合,这就是 RBAC
RBAC = Role-Based Access Control(基于角色的访问控制)。Django 的"组 + 权限"就是教科书级的实现。
不要给某个用户单独配权限——以后他离职、换岗、改职责时,逐个改用户配置就崩了。给"角色"配权限,把人放进角色,一改改一片。
5 前台

学生查成绩页:不用后台,自己写一个

学生不该看到管理后台——他们要的是一个简单的"登录 → 看自己的成绩"页面。

① 路由

修改 student_system/urls.py

PYTHON student_system/urls.py
from django.contrib import admin
from django.urls import path
from user import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),                  # 首页 = 学生登录页
    path('login/', views.student_login),     # 学生登录
    path('scores/', views.my_scores),        # 我的成绩
    path('logout/', views.student_logout),   # 退出
]

② 视图

修改 user/views.py

PYTHON user/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from .models import Student, Score


def index(request):
    return redirect('/login/')


def student_login(request):
    if request.method == 'POST':
        sno = request.POST.get('sno')
        password = request.POST.get('password')

        # 关键设计:学号当 username 用
        # authenticate 是 Django 自带的密码校验函数
        # 它内部会处理"哈希后比较"——比自己写的安全得多
        user = authenticate(request, username=sno, password=password)

        if user is not None:
            login(request, user)            # Django 帮你写 session
            return redirect('/scores/')
        else:
            return render(request, 'login.html',
                          {'error': '学号或密码错误'})

    return render(request, 'login.html')


def my_scores(request):
    # request.user 是当前登录的 User 对象
    # 没登录时 request.user.is_authenticated 是 False
    if not request.user.is_authenticated:
        return redirect('/login/')

    # 通过 user 反查 Student(OneToOne 反向访问)
    try:
        student = request.user.student      # ↑ 注意小写
    except Student.DoesNotExist:
        return render(request, 'login.html',
                      {'error': '此账号不是学生账号'})

    # 这个学生的所有成绩
    scores = Score.objects.filter(student=student).order_by('-exam_date')
    return render(request, 'scores.html',
                  {'student': student, 'scores': scores})


def student_logout(request):
    logout(request)
    return redirect('/login/')
💡 用 Django 自带的 authenticate / login
第9章我们手写过登录——课堂演示直观,但只能记得"用户名"。Django 自带的 authenticate + login
  • 密码自动哈希比较(不存明文)
  • 登录态自动写入 session(处理后续请求时通过 request.user 直接拿到)
  • 支持"记住我"、密码重置、邮箱激活等扩展
能用框架自带的,就别自己造

③ 两个模板

新建 user/templates/login.html(在 user 应用里建 templates 目录):

HTML user/templates/login.html
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>智慧星 · 学生登录</title></head>
<body style="font-family:sans-serif; max-width:360px; margin:80px auto;">
    <h2>🎓 智慧星 · 学生登录</h2>
    <form method="post">
        {% csrf_token %}
        <input name="sno" placeholder="学号"
               style="width:100%; padding:8px; margin:5px 0;">
        <input name="password" type="password" placeholder="密码"
               style="width:100%; padding:8px; margin:5px 0;">
        <button style="width:100%; padding:10px; background:#3B6B9A; color:#fff; border:none;">登录</button>
    </form>
    {% if error %}<p style="color:#B83B2E">❌ {{ error }}</p>{% endif %}
</body>
</html>

新建 user/templates/scores.html

HTML user/templates/scores.html
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>我的成绩</title></head>
<body style="font-family:sans-serif; max-width:720px; margin:30px auto;">

    <h2>👋 {{ student.name }} ({{ student.sno }}) 的成绩</h2>
    <p>班级:{{ student.klass }} ·
       <a href="/logout/">退出</a></p>

    <table border="1" cellpadding="6"
           style="border-collapse:collapse; width:100%;">
        <tr><th>科目</th><th>成绩</th><th>考试日期</th></tr>
        {% for s in scores %}
            <tr>
                <td>{{ s.subject }}</td>
                <td>{{ s.score }}</td>
                <td>{{ s.exam_date }}</td>
            </tr>
        {% empty %}
            <tr><td colspan="3">暂无成绩</td></tr>
        {% endfor %}
    </table>
</body>
</html>

④ 建测试学生 + 跑通

注意:学生账号要在 admin 后台建——先建一个 User(用户名 = 学号,比如 2026001,密码自定),再回到 user → 学生 里建一条 Student 关联到这个 User。

然后浏览器访问 http://IP:8000/login/,用学号 + 密码登录,看到自己的成绩列表。

📋 本节小结

项目模块归属

谁用访问入口怎么实现
管理员 / 老师/admin/Django 自带后台 + ModelAdmin 配置
学生/login/自己写视图 + 模板(用 authenticate / login)

本章 4 个核心收获

  1. OneToOneField 把业务表挂到 auth_user 上——不要自己重新写用户表
  2. ModelAdmin 子类给后台加列表 / 筛选 / 搜索,几行配置就能用
  3. Group + Permission = RBAC,给"角色"配权限而不是给"人"
  4. authenticate / login / logout / request.user——Django 自带的认证 4 件套

课堂练习

两组共 12 题:第一组业务模型 / 后台,第二组权限和认证。

第一组

模型与后台(6 题)

Q1单选

为什么本章不直接 username = CharField 加在 Student 模型里,而要 OneToOneField 关联到 User?

A因为这样数据更小
B这样可以复用 Django 自带的密码哈希、登录、权限等机制
C因为 Django 不支持 CharField
D语法要求
解析:auth_user 不是普通表——它后面挂着完整的认证 / 会话 / 权限基础设施。挂上去全免费。
Q2单选

"一个学生有多次成绩"在模型里用什么字段实现?

AOneToOneField
B在 Score 表里加 ForeignKey 指向 Student
CManyToManyField
D把所有成绩拼成 CharField
解析:"一对多"的外键定义在"多"的那一方。一个学生 → 多条成绩,所以 Score 加外键。
Q3单选

想让后台列表页显示某些列,应该设置哪个 ModelAdmin 属性?

Afields
Blist_display
Clist_filter
Dshow_columns
解析:list_display 控制列表页的列;fields 控制表单页(编辑/添加)的字段。
Q4单选

verbose_name = '学生' 起什么作用?

A变成数据库表名
B密码加盐
C在后台界面显示成中文(不显示英文类名 Student)
D给字段加索引
解析:verbose_name 是"展示用名字"——后台 UI、错误提示等都用它。Meta 类里设的是模型名,字段构造里设的是字段名。
Q5判断

第一次跑 migrate 之前可以先 createsuperuser,效果一样。

解析:createsuperuser 要往 auth_user 表里写——必须 migrate 之后表才存在。顺序:先 migrate,再 createsuperuser
Q6判断

本章 14 张表里,auth_* / django_* 开头的表都是 Django 自动建的,不用我们写代码。

解析:它们随 django.contrib.auth / contenttypes / sessions 等内置应用一起装好。我们只需写自己的业务模型(Student / Teacher / Score)。
第二组

权限与认证(6 题)

Q1单选

Django 后台的 RBAC 模型由哪三层构成?

A用户 / 角色 / 资源
B用户 / 邮箱 / 密码
C用户 / 组(Group) / 权限(Permission)
D会话 / Cookie / Token
解析:Django 用 Group 做角色,里面装 Permission,把 User 加到 Group 就有了那组权限。
Q2单选

老师能登录 /admin/ 后台,关键勾选项是?

Ais_superuser
Bis_staff(员工状态)
Cis_active
Dis_admin
解析:is_staff = True 是登录后台的最低门槛;is_superuser = True 才是管理员(拥有全部权限)。
Q3单选

"老师不该能删除学生"——怎么实现?

A在 ModelAdmin 里写 if 判断
B给"老师"组的权限里只勾 view/add/change,不勾 delete
C把数据库 user_student 表改成只读
D让前端隐藏删除按钮
解析:权限系统是声明式的——勾哪个就有哪个,没勾就不能做。Django 后台 UI 会自动根据权限隐藏对应按钮。
Q4单选

视图里 authenticate(request, username=..., password=...) 干了什么?

A把用户写进 session
B建新用户
C校验账号密码(比较哈希),返回 User 对象或 None
D修改密码
解析:校验完得到 User 对象,再用 login(request, user) 把它写进 session。两步分离的设计很关键——你能在校验通过但其它条件不满足时不登录。
Q5单选

登录之后,视图里怎么拿当前用户?

Asession.user
Brequest.user
CUser.current()
Dget_current_user()
解析:Django 中间件会自动从 session 把 user 挂到 request 上。没登录时是 AnonymousUser(is_authenticated 为 False)。
Q6判断

学生模型用 request.user.student 反查——这是 OneToOneField 的反向访问,由 Django 自动生成。

解析:定义了 Student.user = OneToOneField(User) 后,从 User 反向访问 Student 用类名小写user.student)。如果学生不存在,会抛 Student.DoesNotExist。