简介:这套基于Python与Django框架实现的学生信息管理系统源码,是针对计算机专业毕业设计需求整理的完整项目包,尤其适合需要快速搭建Web管理系统演示环境的本科生。资源压缩包体积仅3.67MB,内部包含1108个文件,其中Python源码与编译文件负责后台逻辑与数据模型,HTML文件构建前端页面骨架,CSS和JavaScript用于页面样式与交互效果,PNG、GIF等图片丰富界面视觉,同时还集成了Bootstrap前端框架以及时区、地区等配置文件,整体结构清晰、依赖完整。截至目前,已有2241人浏览或下载学习,足以说明其在毕设场景中的实用价值。项目提供学生信息管理相关的核心业务模块,使用者只需在配置文件中修改数据库用户名和密码,执行数据迁移即可在本地快速运行,既可作为毕业设计系统直接提交,也可作为学习Django MVC架构的参考样例,在此基础上进行功能扩展或界面定制,对节省开发时间与理清开发思路都有明显帮助。
1. 从数据库课程设计到可答辩的Web学生信息管理系统
做毕业设计时,很多人第一步就卡在数据库表怎么建。学生信息管理系统恰好把这个问题暴露得很彻底:学生、课程、成绩三张表的关系,以及如何用Django的ORM把关系落进实际项目。这个选题的价值在于覆盖了Web系统最高频的CRUD、搜索、分页与导出,之后无论是转向业务开发还是继续读研,由表驱动页面的思路都能复用。我会按一条可运行的源码路径,从项目初始化讲到数据库模型、视图查询、模板渲染,最后落到答辩现场能拿出手的验证与部署技巧。整个过程只需要Python 3.10及以上、Django 4.2 LTS、一个顺手的数据库,适合正在做数据库课程设计或Web期末作业的同学,也适合快速观摩Django后台系统完整做法的初级开发者。
2. Django项目骨架与数据库模型设计
2.1 环境准备与django创建app
先明确一个原则:数据库模型设计不应该从Navicat画表开始,而应该以Python Model为单位定义,再通过迁移生成表结构。原因是模型代码能被版本控制跟踪,改动差异一目了然;手绘ER图或孤立SQL脚本一旦改动就失去现场,答辩时也说不清演进过程。所以这一节先从项目初始化说起。
我的惯用步骤是这样:
mkdir sims_project && cd sims_project python -m venv venv source venv/bin/activate pip install django==4.2.* mysqlclient==2.2.* django-admin startproject sims_config . python manage.py startapp studentspython -m venv创建独立虚拟环境,避免项目依赖与系统Python打架;source venv/bin/activate在Linux和macOS下生效,Windows下换成venv\Scripts\activate。Django版本锁到4.2 LTS分支,官方支持周期长,换机器重装也不会出现版本差异导致的诡异报错;mysqlclient是连接MySQL的驱动,没装MySQL开发库时可以暂时跳过,先让SQLite把业务流程跑通,最后再切数据库,这样排错面更小。
接下来处理settings.py中最影响本项目的三处。把我这里写进去,和django创建app后默认生成的配置做对比:
# sims_config/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'students', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'sims_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } } LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'charset=utf8mb4主要解决中文和生僻字存储问题,用默认latin1写中文姓名时很容易报Incorrect string value。LANGUAGE_CODE设为zh-hans后,admin后台和表单错误提示变成中文,答辩演示观感更顺;TIME_ZONE影响auto_now_add字段写入的时间是否按本地时区。如果先不接MySQL,把ENGINE改回django.db.backends.sqlite3,把NAME指到BASE_DIR下的db.sqlite3,Django新项目默认就是这个配置。
2.2 学生、课程、成绩三个核心Model
学生信息管理系统的数据核心是三张表,Student存学生静态信息,Course存课程信息,Score存成绩关联。它们的关联是:学生与成绩一对多,课程与成绩一对多,学生与课程通过成绩表形成多对多。下面是我会直接用的Model定义:
# students/models.py from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class Student(models.Model): GENDER_CHOICES = [('M', '男'), ('F', '女')] student_no = models.CharField('学号', max_length=20, unique=True, db_index=True) name = models.CharField('姓名', max_length=50) gender = models.CharField('性别', max_length=1, choices=GENDER_CHOICES, default='M') birth_date = models.DateField('出生日期', null=True, blank=True) phone = models.CharField('手机号', max_length=11, null=True, blank=True) enroll_year = models.PositiveSmallIntegerField('入学年份') created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: ordering = ['enroll_year', 'student_no'] verbose_name = '学生' verbose_name_plural = verbose_name def __str__(self): return f'{self.student_no} {self.name}' class Course(models.Model): course_no = models.CharField('课程号', max_length=10, unique=True) name = models.CharField('课程名', max_length=100) credit = models.DecimalField('学分', max_digits=3, decimal_places=1, default=2.0) class Meta: ordering = ['course_no'] 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=1, validators=[MinValueValidator(0), MaxValueValidator(100)]) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('student', 'course')字段参数里最能影响维护体验的是related_name和on_delete。related_name='scores'让反向查询变成student.scores.all(),不必写Score.objects.filter(student=student)。on_delete=models.CASCADE表示删学生时级联删除成绩,答辩演示最省事;若成绩需要留痕,把外键改成on_delete=models.PROTECT,删除学生时Django会抛ProtectedError,要求你先清理成绩表。
常用字段的参数速查表如下:
| 字段类型 | 常用参数 | 适用位置 |
|---|---|---|
| CharField | max_length、unique、db_index | 学号、姓名、电话 |
| DateField | null=True、blank=True | 出生日期 |
| DecimalField | max_digits、decimal_places、validators | 成绩、学分 |
| ForeignKey | on_delete、related_name | 成绩表指向学生、课程 |
Meta里的ordering决定默认排序,列表页不额外写order_by也能稳定输出;unique_together在数据库层约束同一学生同一课程只有一条成绩,比在视图里if判断更可靠。
2.3 迁移命令与演示数据填充
模型写完,执行迁移。注意每次改models.py后都要重新makemigrations,如果提示No changes detected,说明模型定义和上一次迁移没有差异:
python manage.py makemigrations students python manage.py migrate python manage.py createsuperusercreatesuperuser用于登录admin后台。为了演示分页和筛选,我倾向于再写一个自定义管理命令填充演示数据,避免手动录几十条。下面是seed_demo的完整实现:
# students/management/commands/seed_demo.py import random from django.core.management.base import BaseCommand from django.db import transaction from students.models import Student, Course, Score class Command(BaseCommand): help = '生成100名学生、10门课程、每人5门成绩的演示数据' @transaction.atomic def handle(self, *args, **options): courses = [] for i in range(1, 11): course, _ = Course.objects.get_or_create( course_no=f'C{i:03d}', defaults={'name': f'专业课程{i}', 'credit': 2.0} ) courses.append(course) for i in range(1, 101): student, _ = Student.objects.get_or_create( student_no=f'2024{i:04d}', defaults={ 'name': f'同学{i}', 'gender': random.choice(['M', 'F']), 'enroll_year': 2024, 'phone': f'138{i:08d}', } ) for course in random.sample(courses, k=5): Score.objects.update_or_create( student=student, course=course, defaults={'score': round(random.uniform(60, 100), 1)} ) self.stdout.write(self.style.SUCCESS('seed done'))这段代码的写法是幂等的:重复执行不会产生重复数据,因为get_or_create依赖唯一学号、update_or_create依赖唯一(student, course)组合。100名学生每人5条成绩,已经足够把分页、搜索和报表都演示一遍。
3. 查询、删除与表单校验的系统实现路径
3.1 用ListView接住搜索与筛选参数
Django视图有函数视图和类视图两种写法。对毕业设计这类页面结构稳定的项目,我更愿意用ListView和DeleteView这类通用视图,因为分页、删除确认这些流程已经预置,不必每页重复写request.GET.get这种裸逻辑。
# students/views.py from django.db.models import Q 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 = 20 def get_queryset(self): qs = super().get_queryset() keyword = self.request.GET.get('keyword', '').strip() gender = self.request.GET.get('gender', '').strip() if keyword: qs = qs.filter(Q(name__icontains=keyword) | Q(student_no__icontains=keyword)) if gender: qs = qs.filter(gender=gender) return qsQ(name__icontains=keyword)会转成LIKE '%关键字%'的SQL,对姓名和学号两个字段做OR匹配;gender过滤是简单的等值判断。两个filter条件同时出现时自动用AND连接,空字符串参数不产生过滤条件。这样以参数绑定方式处理请求,比在模板里直接拼参数安全得多,因为它没有把用户输入拼进原生SQL。
| 参数 | 来源 | 数据库操作 |
|---|---|---|
| keyword | request.GET | LIKE '%关键字%',姓名或学号 |
| gender | request.GET | 等值匹配gender字段 |
| paginate_by | ListView属性 | 每页条数 |
模板里翻页时也要带上keyword和gender,否则翻到第二页就丢了筛选结果。具体写法在第4章的分页代码里展示。
3.2 ModelForm校验的边界
新增和编辑表单建议交给ModelForm。它让字段声明和模型保持一致,重复代码变少;遇到学号、电话这种特殊业务规则,再用clean_开头的方法补齐。先给出表单代码:
# students/forms.py from django import forms from .models import Student class StudentForm(forms.ModelForm): class Meta: model = Student fields = ['student_no', 'name', 'gender', 'birth_date', 'phone', 'enroll_year'] widgets = { 'birth_date': forms.DateInput(attrs={'type': 'date'}), } def clean_student_no(self): no = self.cleaned_data['student_no'] if not no.isdigit(): raise forms.ValidationError('学号只能是数字') return no def clean_phone(self): phone = self.cleaned_data.get('phone') if phone and (len(phone) != 11 or not phone.isdigit()): raise forms.ValidationError('手机号需要是11位数字') return phoneclean_student_no这种命名约定会被Django在is_valid()阶段自动调用,抛出的ValidationError会写进表单的errors字典,而不会中断其他字段校验。widgets里的DateInput对应浏览器原生日期选择器,省去自己写JavaScript的麻烦。这里的校验边界在于:ModelForm管的是字段结构和必填,业务规则的学号唯一、手机号格式放Form层,因为Form能拿到用户当前提交的完整上下文,比放Model层的save方法里更容易定位问题。
3.3 django执行查询-删除对象时的级联处理
删除是CRUD里最需要想清楚的环节。django执行查询-删除对象时,如果被删对象被外键引用,必须想明白on_delete策略。我在团队项目里的习惯是:删除学生前先检查是否有成绩记录,有的话在界面上给出提示,而不是直接CASCADE。
# students/views.py from django.http import HttpResponseRedirect from django.urls import reverse_lazy from django.views.generic import DeleteView from django.db import transaction class StudentDeleteView(DeleteView): model = Student success_url = reverse_lazy('student_list') @transaction.atomic def form_valid(self, form): student = self.get_object() student.scores.all().delete() student.delete() return HttpResponseRedirect(self.get_success_url())两个删除动作包在事务里,要么一起成功,要么一起回滚。如果外键用的是PROTECT,必须先清成绩再删学生;如果用的是CASCADE,上面代码其实等价于直接student.delete()。手动写成两步,是为了在删除前能够记录日志,答辩时被问到删了学生成绩怎么办,这段代码就是完整回答。
4. 模板渲染、分页与报表导出的完整闭环
4.1 模板继承与表单组件复用
Django模板系统里,最值得先掌握的是继承和block覆盖。base.html放公共框架,子页面只写各自的content块。对Web期末作业这类前端基础有限的场景,直接引入Bootstrap的CDN即可,不必从零写CSS。
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}学生信息管理系统{% endblock %}</title> <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.3.0/css/bootstrap.min.css"> </head> <body class="bg-light"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand px-3" href="{% url 'student_list' %}">学生信息系统</a> </nav> <main class="container mt-4"> {% block content %}{% endblock %} </main> </body> </html>子模板继承它:
<!-- templates/students/student_list.html --> {% extends 'base.html' %} {% block content %} <div class="card"> <div class="card-body"> <form method="get" class="row g-2 mb-3"> <div class="col-auto"> <input type="text" name="keyword" class="form-control" placeholder="姓名或学号" value="{{ request.GET.keyword }}"> </div> <div class="col-auto"> <select name="gender" class="form-select"> <option value="">全部性别</option> <option value="M" {% if request.GET.gender == 'M' %}selected{% endif %}>男</option> <option value="F" {% if request.GET.gender == 'F' %}selected{% endif %}>女</option> </select> </div> <div class="col-auto"> <button type="submit" class="btn btn-primary">搜索</button> </div> </form> <table class="table table-striped"> <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.get_gender_display }}</td> <td> <a class="btn btn-sm btn-outline-secondary" href="{% url 'student_export' student.pk %}">导出成绩</a> </td> </tr> {% empty %} <tr><td colspan="4" class="text-center">没有匹配结果</td></tr> {% endfor %} </tbody> </table> </div> </div> {% endblock %}get_gender_display输出choices的显示值;模板里直接比较request.GET.gender与选项值,能保留上次选择的筛选条件;empty标签在查询结果为空时展示替代文案,比在视图里硬编码更干净。
4.2 分页组件的参数与模板写法
ListView启用paginate_by后,模板里会出现page_obj和paginator。分页链接必须保留keyword和gender两个参数,这是最容易踩的坑。下面是我确认可用的分页模板写法:
{% if page_obj.paginator.num_pages > 1 %} <nav> <ul class="pagination justify-content-center"> {% if page_obj.has_previous %} <li class="page-item"> <a class="page-link" href="?page={{ page_obj.previous_page_number }}&keyword={{ request.GET.keyword|urlencode }}&gender={{ request.GET.gender|urlencode }}">上一页</a> </li> {% endif %} <li class="page-item disabled"> <span class="page-link">{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span> </li> {% if page_obj.has_next %} <li class="page-item"> <a class="page-link" href="?page={{ page_obj.next_page_number }}&keyword={{ request.GET.keyword|urlencode }}&gender={{ request.GET.gender|urlencode }}">下一页</a> </li> {% endif %} </ul> </nav> {% endif %}| 模板变量 | 含义 |
|---|---|
| page_obj | 当前页对象,含has_previous等方法 |
| page_obj.paginator.num_pages | 总页数 |
| request.GET.keyword | 保留搜索关键字 |
| urlencode过滤器 | 中文与特殊字符安全的URL编码 |
keyword是中文时,|urlencode也会把它转成URL安全的编码,这样粘贴链接和收藏夹都不会出错。
4.3 用openpyxl导出成绩单
导出Excel是答辩里很常见的加分项。openpyxl只处理xlsx,不依赖系统级组件,比weasyprint这类渲染引擎轻得多。下面是按学生维度导出成绩单的视图:
# students/views.py from django.http import HttpResponse from openpyxl import Workbook from openpyxl.styles import Font def export_student_scores(request, pk): student = Student.objects.prefetch_related('scores__course').get(pk=pk) wb = Workbook() ws = wb.active ws.title = f'{student.name}成绩单' headers = ['课程号', '课程名', '学分', '成绩'] ws.append(headers) for col, width in zip('ABCD', [10, 24, 8, 8]): ws.column_dimensions[col].width = width for cell in ws[1]: cell.font = Font(bold=True) for sc in student.scores.all(): ws.append([sc.course.course_no, sc.course.name, float(sc.course.credit), float(sc.score)]) response = HttpResponse( content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = f'attachment; filename=student_{student.student_no}.xlsx' wb.save(response) return responseprefetch_related('scores__course')一次取出成绩和课程数据,避免模板循环里逐条查询产生N+1问题。表头加粗、列宽调整后打印出来更清晰。Content-Disposition设置的filename决定浏览器下载的默认文件名。
5. 答辩现场验证与Django项目部署调试技巧
5.1 用manage.py shell核验业务数据
答辩前留5分钟跑一遍shell验证,比手动点页面快,也更有说服力:
python manage.py shell -c " from students.models import Student, Score from django.db.models import Avg print(Student.objects.count()) print(Score.objects.aggregate(avg=Avg('score'))) print(Student.objects.filter(gender='F').count()) "输出结果和页面展示一致,说明数据模型、视图和模板之间没有断裂。真出现不一致时,优先用ORM确认数据本身,再检查模板渲染,这两步能定位大多数显示异常。
5.2 用django-debug-toolbar定位查询瓶颈
列表页每次刷新出现几十条SQL,通常是模板里逐条访问了外键字段。在settings.py的INSTALLED_APPS中加入debug_toolbar.apps.DebugToolbarConfig,在MIDDLEWARE中加入debug_toolbar.middleware.DebugToolbarMiddleware,并在INTERNAL_IPS写入127.0.0.1,然后打开任意列表页,SQL面板会展示每条查询的语句、耗时和调用栈。把模板里scores的调用换成select_related或prefetch_related后,查询次数往往从几十次降到两三次。
pip install django-debug-toolbar5.3 部署环节的参数速查
演示时把项目放到云服务器上让老师远程访问比较常见,以宝塔部署Django为例,本质是用Python项目管理器创建项目,运行命令指向gunicorn,再由Nginx转发静态资源和动态请求。
pip install gunicorn gunicorn sims_config.wsgi:application -b 0.0.0.0:8000 --workers 2 --timeout 30| 参数 | 作用 | 建议值 |
|---|---|---|
| -b 0.0.0.0:8000 | 监听地址 | 本机调试用127.0.0.1更安全 |
| --workers 2 | Worker进程数 | 2~4,看内存 |
| --timeout 30 | 单请求超时时间 | 30~60 |
| --access-logfile - | 访问日志输出到标准输出 | 排错好用 |
DEBUG=False后要同步配置ALLOWED_HOSTS和STATIC_ROOT,否则静态文件加载不出来。执行顺序也讲究:先python manage.py migrate建表,再python manage.py collectstatic收拢静态文件,最后重启gunicorn,颠倒了就会在第一次请求时报OperationalError。
本文还有配套的精品资源,点击获取