
简介一套基于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_noxxx)判断更可靠数据库层的唯一约束是并发环境下唯一安全的方案。# models.py from django.db import models class Student(models.Model): SEX_CHOICES ((M, 男), (F, 女)) student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50, db_indexTrue) sex models.CharField(性别, max_length1, choicesSEX_CHOICES, defaultM) birth_date models.DateField(出生日期, nullTrue, blankTrue) phone models.CharField(手机号, max_length11, blankTrue) email models.EmailField(邮箱, blankTrue) major models.CharField(专业, max_length50, default计算机) grade models.CharField(年级, max_length4, default2024) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table student ordering [-created_at] def __str__(self): return f{self.student_no}-{self.name}uniqueTrue会自动创建唯一索引db_indexTrue给 name 字段加普通索引因为列表页经常按姓名模糊搜索不加索引数据量上去后全表扫描会非常慢。choices限制性别枚举值比在视图里手工判断更清晰。auto_now_add只在创建时写入时间auto_now每次保存都会更新这两个语义不要反了。Meta.ordering默认按创建时间倒序保证新添加的学生排在列表最前面。2.2 表关系和外键设计学生和成绩之间是典型的一对多关系。在 Django 中用ForeignKey表示外键字段会自动带_id后缀生成数据库列。这里的关键是on_delete参数CASCADE表示学生删除时成绩也删除适合成绩没有独立意义的情况如果业务上要保留历史记录应该用PROTECT或SET_NULL但SET_NULL要求外键字段nullTrue。# models.py class Course(models.Model): name models.CharField(课程名, max_length100) class Score(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_namescores) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namescores) score models.DecimalField(成绩, max_digits5, decimal_places2) class Meta: constraints [ models.UniqueConstraint(fields[student, course], nameunique_student_course) ]related_namescores让反向查询变成student.scores.all()而不是默认的student.score_set.all()语义更直白。UniqueConstraint保证了同一个学生同一门课只能有一条成绩这是业务规则必须落到数据库层的典型场景。注意旧项目里可能还在用unique_togetherDjango 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(grade2020).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__icontainskeyword) return qscontext_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(), namestudent_list), path(add/, views.student_add, namestudent_add), path(int:pk/edit/, views.student_edit, namestudent_edit), path(int:pk/delete/, views.student_delete, namestudent_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 methodpost action{% url student_delete student.pk %} styledisplay:inline; {% csrf_token %} button typesubmit删除/button /form /td /tr {% empty %} trtd colspan4暂无数据/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 查询字符串里拿的比如?page2。PageNotAnInteger处理用户手动输入?pageabc的情况直接回第一页。EmptyPage处理越界比如总页数只有 5用户输入page99就返回最后一页。模板里通过students.has_previous、students.has_next控制上一页/下一页按钮再用students.number和students.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__icontainskeyword) | Q(student_no__icontainskeyword) | Q(major__icontainskeyword) ) 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__inids).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__inids).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 django3.2 python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000makemigrations是根据 models.py 的变化生成迁移脚本migrate才是真正把表建到数据库里。新手最常见的错误是只执行migrate不执行makemigrations结果被告知No migrations to apply。如果项目使用了 MySQL还需要先安装mysqlclient在 Linux 上要提前安装libmysqlclient-devWindows 上则下载对应的 whl 文件。settings.py里的DATABASES要确认HOST和PORT都正确尤其是 MySQL 端口千万不要写成3307之类。5.2 静态文件与 Admin 后台本地开发时 Django 能自动处理静态文件但部署到 Nginx 后样式全丢几乎每个人都会遇到。原因是DEBUGFalse时 Django 不再托管静态文件必须执行collectstatic把所有 app 的静态文件集中到一个目录再由 Nginx 别名指向该目录。# settings.py STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles STATICFILES_DIRS [BASE_DIR / static]STATIC_ROOT是collectstatic的输出目录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.py里TIME_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如果在外键关联字段上取值比如显示学生的班级名称会触发 N1 查询每行多出一条 SQL。解决办法是在get_queryset里加select_relateddef get_queryset(self): qs super().get_queryset() qs qs.select_related(student_class) # 假设有外键 student_class return qsselect_related通过 SQL JOIN 把外键对象一次性取出把 N1 压缩成 1 条查询。只对外键和一对一关系有用多对多用prefetch_related。判断 N1 的方法很简单安装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里补上密码强度校验至少包含最小长度和不能和用户名太相似这两条规则。这样一套下来学生管理系统无论是自己用还是交作业都能在功能和健壮性上站得住脚。本文还有配套的精品资源点击获取