Django学生管理系统开发实战:模型、视图与部署全攻略
2026/9/15 15:27:11 网站建设 项目流程

简介:一套基于Python和Django框架的学生管理系统完整源码,适合正在学习Web开发或需要参考完整Django项目的读者,可用于理解模型-视图-模板(MTV)架构、ORM数据操作及项目工程化组织方式。压缩包内共包含6585个文件,整体大小约21.69MB,以Python源码(py、pyc)、HTML模板、JavaScript与CSS样式、PNG/GIF图片资源为主,同时包含po/mo等国际化翻译文件,能够较为完整地覆盖一个Django站点的常用元素。目前已有1897人学习/下载。源码提供了从项目配置、URL路由到视图函数与数据模型、前端页面的完整链路,便于按模块拆解研究。读者可直接运行或在此基础上二次开发,用于学生信息管理相关场景,也可作为课程设计、毕业设计或初学Django的练手项目参考。

1. 学生管理系统:Django 比 Flask 更合适的理由

从 GitHub 或源码包里下载的“学生管理系统”大多是一个 Python + Django 项目,解压后能看到一堆 models.py、views.py、templates 文件,但真正能在本地跑起来的寥寥无几。原因不是代码不行,而是环境配置和 Django 版本兼容问题。这个项目基于 Python 3.7 和 Django 开发,覆盖了学生信息的增删改查、分页搜索、Admin 后台管理等功能,非常适合用来理解 Django 的 MTV 架构,也能作为毕业设计或内部信息管理工具的底座。Django 自带 ORM、Admin、表单验证和 CSRF 防护,比起 Flask 那种需要自己拼装扩展的方式,写这类业务系统能少踩一半的坑。本文会从数据模型、视图路由、分页搜索到部署排错,再把性能和安全上的几个隐蔽问题点透。

2. 模型设计与数据库选型

2.1 学生表结构怎么定义

学生管理系统的核心是 Student 模型,但实际项目中通常还有班级、课程、成绩等关联数据。源码里如果只有一张学生表,迁移后数据库结构往往不够用。我一般会先定义学生基础表,字段包括学号、姓名、性别、出生日期、手机号、邮箱、专业、年级,以及创建和更新时间。学号必须唯一,这比在视图层用if Student.objects.filter(student_no=xxx)判断更可靠,数据库层的唯一约束是并发环境下唯一安全的方案。

# models.py from django.db import models class Student(models.Model): SEX_CHOICES = (('M', '男'), ('F', '女')) student_no = models.CharField('学号', max_length=20, unique=True) name = models.CharField('姓名', max_length=50, db_index=True) sex = models.CharField('性别', max_length=1, choices=SEX_CHOICES, default='M') birth_date = models.DateField('出生日期', null=True, blank=True) phone = models.CharField('手机号', max_length=11, blank=True) email = models.EmailField('邮箱', blank=True) major = models.CharField('专业', max_length=50, default='计算机') grade = models.CharField('年级', max_length=4, default='2024') created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: db_table = 'student' ordering = ['-created_at'] def __str__(self): return f"{self.student_no}-{self.name}"

unique=True会自动创建唯一索引,db_index=True给 name 字段加普通索引,因为列表页经常按姓名模糊搜索,不加索引数据量上去后全表扫描会非常慢。choices限制性别枚举值,比在视图里手工判断更清晰。auto_now_add只在创建时写入时间,auto_now每次保存都会更新,这两个语义不要反了。Meta.ordering默认按创建时间倒序,保证新添加的学生排在列表最前面。

2.2 表关系和外键设计

学生和成绩之间是典型的一对多关系。在 Django 中用ForeignKey表示,外键字段会自动带_id后缀生成数据库列。这里的关键是on_delete参数,CASCADE表示学生删除时成绩也删除,适合成绩没有独立意义的情况;如果业务上要保留历史记录,应该用PROTECTSET_NULL,但SET_NULL要求外键字段null=True

# models.py class Course(models.Model): name = models.CharField('课程名', max_length=100) class Score(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='scores') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='scores') score = models.DecimalField('成绩', max_digits=5, decimal_places=2) class Meta: constraints = [ models.UniqueConstraint(fields=['student', 'course'], name='unique_student_course') ]

related_name='scores'让反向查询变成student.scores.all(),而不是默认的student.score_set.all(),语义更直白。UniqueConstraint保证了同一个学生同一门课只能有一条成绩,这是业务规则必须落到数据库层的典型场景。注意,旧项目里可能还在用unique_together,Django 2.2 以后推荐用UniqueConstraint,功能更强,还可以单独指定条件,比如只对未删除的选课记录生效。

2.3 ORM 查询的常见姿势

拿到源码后,最常改的就是查询逻辑。学生列表页用Student.objects.all()是最直接写法,但它会取出所有字段。如果模板里只展示学号、姓名、专业、年级,我一般会用only('id', 'student_no', 'name', 'major', 'grade'),减少数据传输量。但要注意,only()之后如果访问了未加载字段,Django 会立刻发出一条补充查询,反而坏事。另一种方式是values('student_no', 'name', 'major'),直接返回字典列表,模板里一样能取值,性能上更稳定。

删除操作也有讲究。student.delete()删除单个对象,Student.objects.filter(grade='2020').delete()批量删除。批量删除返回一个元组,第一个元素是删除总数。这里特别提醒:删除操作千万不要用 GET 请求触发,必须要用 POST,否则一旦被搜索引擎或爬虫抓到链接,可能批量误删。Django 模板里做删除表单时,必须带上{% csrf_token %},否则会收到 403 响应。

3. 视图、路由与模板渲染的实现

3.1 函数视图和类视图怎么选

学生管理系统的增删改查,源码里可能混用了函数视图和类视图。函数视图适合逻辑简单的动作,比如新增学生的 POST 处理。但列表页要支持分页、搜索、排序,用 Django 内置的ListView能省掉不少样板代码。ListView自动处理 GET 请求、调用 get_queryset、分页,并把上下文传给模板。

# views.py from django.views.generic import ListView from .models import Student class StudentListView(ListView): model = Student template_name = 'students/student_list.html' context_object_name = 'students' paginate_by = 15 def get_queryset(self): qs = super().get_queryset() keyword = self.request.GET.get('keyword', '').strip() if keyword: qs = qs.filter(name__icontains=keyword) return qs

context_object_name如果不设置,模板里默认变量是object_list,很多人在这里踩坑。paginate_by控制每页条数,15 条是比较适合屏幕显示的数量。重写get_queryset是为了接收搜索参数,name__icontains生成LIKE '%keyword%',不区分大小写。如果还要按学号搜索,可以叠加多个filter或者用 Q 对象,下一章会展开。

3.2 URL 路由与反向解析

路由是项目能跑起来的骨架。Django 2.0 之后用path()定义 URL,比正则url()直观得多。路由参数用尖括号,<int:pk>会限定参数为整数,也能自动做类型转换。更重要的是一定要为每个视图指定name,这样模板和视图里统一用reverse{% url %}反向解析。

# urls.py from django.urls import path from . import views urlpatterns = [ path('', views.StudentListView.as_view(), name='student_list'), path('add/', views.student_add, name='student_add'), path('<int:pk>/edit/', views.student_edit, name='student_edit'), path('<int:pk>/delete/', views.student_delete, name='student_delete'), ]

在模板里写{% url 'student_edit' student.pk %},当路由路径变化时,不用手动去改所有模板里的链接。视图里用reverse('student_edit', args=[student.pk])做重定向也是一样的效果。如果项目分了多个 app 且有app_name,那 name 前面要加命名空间,比如app_name:student_edit,否则NoReverseMatch是最常见的报错。

3.3 模板渲染和表单处理

模板文件通常放在templates/students/目录下。列表页渲染要注意空数据的情况,用{% empty %}显示“暂无数据”而不是让表格空着。删除按钮不能是一个简单的<a href>,必须嵌入一个带csrf_token的表单。

<!-- templates/students/student_list.html --> <table> <thead> <tr> <th>学号</th> <th>姓名</th> <th>专业</th> <th>操作</th> </tr> </thead> <tbody> {% for student in students %} <tr> <td>{{ student.student_no }}</td> <td>{{ student.name }}</td> <td>{{ student.major }}</td> <td> <a href="{% url 'student_edit' student.pk %}">编辑</a> <form method="post" action="{% url 'student_delete' student.pk %}" style="display:inline;"> {% csrf_token %} <button type="submit">删除</button> </form> </td> </tr> {% empty %} <tr><td colspan="4">暂无数据</td></tr> {% endfor %} </tbody> </table>

表单处理推荐用ModelForm,它可以由模型自动生成字段,并在clean()阶段做类型转换和唯一性校验。视图里拿到StudentForm(request.POST)后,先调is_valid(),再save()。这样代码少,错误信息也统一,不推荐手动把request.POST的每个值赋值给模型字段,字段一多就容易漏。

# forms.py from django import forms from .models import Student class StudentForm(forms.ModelForm): class Meta: model = Student fields = ['student_no', 'name', 'sex', 'birth_date', 'phone', 'email', 'major', 'grade']

4. 分页、搜索与批量操作实战

4.1 用 Paginator 控制列表页

当学生数据超过几百条以后,列表页不能一股脑全渲染。Django 自带的Paginator完全够用,不需要引入第三方分页库。函数视图里这样写:

# views.py from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from .models import Student def student_list(request): student_list = Student.objects.all() paginator = Paginator(student_list, 10) page = request.GET.get('page') try: students = paginator.page(page) except PageNotAnInteger: students = paginator.page(1) except EmptyPage: students = paginator.page(paginator.num_pages) return render(request, 'students/student_list.html', {'students': students})

Paginator第一个参数是查询集,第二个参数是每页条数。注意page参数是从 URL 查询字符串里拿的,比如?page=2PageNotAnInteger处理用户手动输入?page=abc的情况,直接回第一页。EmptyPage处理越界,比如总页数只有 5,用户输入page=99,就返回最后一页。模板里通过students.has_previousstudents.has_next控制上一页/下一页按钮,再用students.numberstudents.paginator.num_pages显示当前页和总页数。

4.2 多字段搜索用 Q 对象

学生管理系统通常有姓名、学号、专业三个搜索条件。三个条件之间是“或”的关系,这时不能连续filter,因为连续filter是“且”的关系。正确做法是用 Django 的Q对象把条件组合起来。

from django.db.models import Q def search_students(request): keyword = request.GET.get('keyword', '').strip() students = Student.objects.none() if keyword: students = Student.objects.filter( Q(name__icontains=keyword) | Q(student_no__icontains=keyword) | Q(major__icontains=keyword) ) return render(request, 'students/search_result.html', {'students': students})

Q对象用|表示 OR,用&表示 AND,还可以在Q前面加~表示 NOT。这里先判空,如果keyword是空字符串,Q(name__icontains='')会匹配所有记录,结果就是返回全表,这个坑要避开。icontains在 SQLite 下对中文匹配没有问题,但如果你用的是 MySQL,要确认数据库表的字符集是 utf8mb4,否则 emoji 或生僻字会丢。

4.3 批量删除与事务处理

批量删除是管理系统的常见需求,前端多个 checkbox 同名提交到后端,视图里用getlist拿到 id 列表,然后filter(id__in=ids).delete()。为了防止删除过程中某个操作失败导致部分数据消失,需要把批量操作包在事务里。

from django.db import transaction @transaction.atomic def bulk_delete(request): if request.method == 'POST': ids = request.POST.getlist('student_ids') if ids: Student.objects.filter(id__in=ids).delete() return redirect('student_list') return redirect('student_list')

transaction.atomic()确保函数内所有数据库操作要么全部提交,要么全部回滚。比如你想删除学生同时删除他的成绩,但在删除成绩时遇到了约束错误,这个装饰器能让学生记录也回滚,避免只剩一半数据。注意getlist拿到的是字符串列表,即使前端值全是数字,也要在查询时让 Django 自动类型转换,好在id__in对字符串数字也能正常处理。批量删除不要循环单删,循环 N 次会产生 N 条 SQL,性能太差。

5. 部署到服务器与常见踩坑

5.1 本地启动与数据库迁移

拿到源码后,第一件事不是直接runserver,而是先看环境。项目基于 Python 3.7,本地建议创建虚拟环境:python3.7 -m venv venv,激活后安装依赖。如果源码里没有requirements.txt,就用pip install django装最新兼容版,这个项目的核心代码在 Django 3.2 上也能跑通。

source venv/bin/activate pip install django==3.2 python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000

makemigrations是根据 models.py 的变化生成迁移脚本,migrate才是真正把表建到数据库里。新手最常见的错误是只执行migrate不执行makemigrations,结果被告知No migrations to apply。如果项目使用了 MySQL,还需要先安装mysqlclient,在 Linux 上要提前安装libmysqlclient-dev,Windows 上则下载对应的 whl 文件。settings.py里的DATABASES要确认HOSTPORT都正确,尤其是 MySQL 端口千万不要写成3307之类。

5.2 静态文件与 Admin 后台

本地开发时 Django 能自动处理静态文件,但部署到 Nginx 后样式全丢,几乎每个人都会遇到。原因是DEBUG=False时 Django 不再托管静态文件,必须执行collectstatic把所有 app 的静态文件集中到一个目录,再由 Nginx 别名指向该目录。

# settings.py STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [BASE_DIR / 'static']

STATIC_ROOTcollectstatic的输出目录,STATICFILES_DIRS是额外找静态文件的目录。Django 自带的 admin 后台在部署后看起来非常朴素,可以在urls.py的 admin 路由中设置管理站点的标题和头部,admin.site.site_header = '学生管理后台'admin.site.site_title = '学生管理',这样至少看起来不是默认的“Django administration”。

5.3 高频报错与解决方案

项目跑起来后,最先冒出来的往往是三个问题。第一个是mysqlclient编译失败,Linux 下需要sudo apt-get install default-libmysqlclient-dev,报错信息里会明确提示缺少哪个头文件。第二个是时区问题,settings.pyTIME_ZONE如果还是'UTC',存入数据库的时间会和北京时间差 8 小时,直接把USE_TZ = False并把TIME_ZONE = 'Asia/Shanghai',简单有效。第三个是 CSRF 校验失败,页面没有任何表单,但 POST 请求返回 403,原因多半是模板里没有{% csrf_token %},或者 AJAX 请求没有带上X-CSRFToken请求头。

# settings.py TIME_ZONE = 'Asia/Shanghai' USE_TZ = False ALLOWED_HOSTS = ['your.server.com', 'localhost']

ALLOWED_HOSTS一定不要留空或只用*,这会带来 Host 头攻击风险。内网部署可以填['*']图方便,但只要是生产环境,必须写清楚域名或 IP。

6. 进阶:看一眼就知道要优化的位置

学生管理系统数据量到几千条时,页面响应会肉眼可见地变慢。优化不用盲目上 Redis,先从数据库查询次数下手。ListView如果在外键关联字段上取值,比如显示学生的班级名称,会触发 N+1 查询,每行多出一条 SQL。解决办法是在get_queryset里加select_related

def get_queryset(self): qs = super().get_queryset() qs = qs.select_related('student_class') # 假设有外键 student_class return qs

select_related通过 SQL JOIN 把外键对象一次性取出,把 N+1 压缩成 1 条查询。只对外键和一对一关系有用,多对多用prefetch_related。判断 N+1 的方法很简单,安装django-debug-toolbar,观察 SQL 面板里是否有同一张表的重复查询。

缓存方面,Django 的cache_page装饰器可以直接缓存整个页面响应。对于学生列表这种数据实时性要求不高的场景,缓存 5 分钟足够。

from django.views.decorators.cache import cache_page @cache_page(60 * 5) def student_list(request): ...

缓存的粒度从大到小可以是整页缓存、片段缓存、模板缓存。不要一开始就对视图加缓存,先确认哪些页面访问量大且数据变化少。安全上,除了 Django 默认的 CSRF 和 XSS 过滤,要把 Admin 后台的 URL 改掉,不要用/admin/,改成/manage/或更不显眼的名字。另外检查AUTH_PASSWORD_VALIDATORS是否启用,没有启用的话在settings.py里补上密码强度校验,至少包含最小长度和不能和用户名太相似这两条规则。这样一套下来,学生管理系统无论是自己用还是交作业,都能在功能和健壮性上站得住脚。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询