☰
Django班务管理系统毕业设计实战:从RBAC权限到WebSocket推送
2026/10/13 11:25:46 网站建设 项目流程

做了一个django班务管理系统作为计算机毕业设计的源码项目,前后折腾了大概两个月,从建表到答辩一路踩了不少坑。这个系统表面上看就是学生管理、考勤、成绩、公告这些常见功能,但真正做完之后你会发现,毕业设计能不能拿高分,拼的从来不是功能数量,而是你对框架的理解深度、代码组织是否规范、有没有把非功能需求(权限、推送、部署)真正落地。这篇文章我把整个项目的设计思路、核心模块的实操过程、还有调试和部署阶段的坑都梳理出来,给正在做类似课题的同学一个参考。

先说结论:班务管理系统非常适合作为Django毕业设计课题,因为它业务边界清晰、角色分明、数据实体之间的关联天然存在,既能把CRUD做全,又能往深了做权限控制、实时推送、数据可视化。而且Django本身自带了Admin后台、ORM、认证体系,意味着你不需要从零造轮子,可以把精力放在业务逻辑和框架熟练度上。基础薄弱的同学可以按本文的路线做基础版,基础比较好的可以加上websocket推送、自定义权限、Dashboard统计这些加分项。

1. 项目概述与选题拆解:班务管理系统到底该做什么

1.1 别把班务系统做成“第二个学生管理系统”

很多同学拿到“班务管理系统”这个题目后,第一反应就是抄学生管理系统的作业:学生增删改查、班级增删改查、然后没了。本科毕业设计评阅老师通常不会被这种作品说服,因为这种系统没有体现“班务”的特殊性。班务管理,核心是“班主任工作视角”下的日常事务管理,它和学生管理系统有一个关键区别:使用者不是教务处,而是班主任、班干部,以及学生本人。

所以我在需求分析阶段就把系统划分成四条业务主线:

  • 学生信息与班级组织架构管理(包括班干部、小组之类)
  • 日常考勤与请假事务(迟到、早退、请假审批)
  • 学习与成绩跟踪(考试成绩录入、班级排名、成绩趋势)
  • 班级通知与事务公告(班主任下发通知、学生已读反馈)

这四条主线有一个共同特点:数据之间存在明显的“班级归属”关系。比如一个学生必须属于某个班级,一条考勤记录必须关联某个学生,一个通知必须指定接收班级。这种关联关系用Django的ORM表达起来非常顺手,ForeignKey、ManyToManyField一用就能建出完整的数据关系网。

1.2 为什么选Django而不是Flask或Spring Boot

组里有人提议用Spring Boot,有人建议Flask,最后我还是选了Django。理由很现实:

  • 毕业设计周期有限。Django自带的Admin后台可以让我在开发初期直接获得一套可用的数据管理界面,后台录入班级、课程、教师等基础数据不需要额外写页面。
  • ORM省掉大部分SQL拼接。班务系统的查询大多是关联查询:查考勤要关联学生和班级,查成绩要关联学生、科目、考试批次。Django ORM在复杂关联查询上写起来比手写原生SQL快很多,而且迁移机制让你改完模型后一条命令就同步数据库。
  • 内置安全和认证。Django自带用户认证、Session管理、CSRF防护,这些在毕业设计答辩时都是加分项。如果自己用Flask从头做权限体系,两个月时间可能连权限都做不完。
  • 生态成熟。做实时推送有Channels,做接口有DRF,做后台美化有django-unfold这类现代主题。选题的热度决定了你能搜到的资料量。

如果你是非计算机专业出身,或者Django还处于“会调MVT但不懂原理”的阶段,我也建议选Django——它的目录结构和开发习惯本身就是一套最佳实践,照做就能保证项目不会跑偏。

1.3 这套源码适合谁看

这篇内容面向四类人:

  • 选了班务管理系统或类似班级管理题目的应届毕业生,需要一个完整的参考实现
  • 正在学Django框架,想找一个综合练手项目巩固知识的初学者
  • 需要在项目中引入权限控制、WebSocket推送、多角色登录这些进阶点的开发者
  • 负责指导毕设的老师或学长学姐,想给学生提供一个偏工程的参考框架

2. 系统架构与数据库建模:先把数据关系理清楚,代码就成功了一半

2.1 角色权限模型:RBAC落地的三条原则

班务系统里有三类天然角色:系统管理员(教务/班主任)、学生、班干部(可选)。如果只做国家级管理那么简单,你甚至可以靠Django的is_superuser和is_staff字段蒙混过关。但既然热搜词里有一大堆关于django rbac的内容,说明权限这块确实是很多人头疼的地方。

我采用的是经典RBAC(基于角色的访问控制)模型,落地时守住三条原则:

  1. 角色绑定权限,不直接给用户绑菜单或方法。用户只跟角色关联,角色再跟权限关联。这样以后新增角色只需分配权限集合,不用改用户表。
  2. 权限控制到视图和按钮级别。视图层用装饰器校验,模板层用判断语句控制按钮显隐。
  3. 后端权限校验绝不能依赖前端隐藏。前端界面可以根据角色隐藏菜单,但每个视图都必须做权限验证,防止学生直接URL访问教师端接口。

Django用户体系扩展主要有两种:一种是用Profile模型OneToOne扩展auth.User,另一种是重写AbstractUser自定义用户模型。实际开发时我建议直接用第二种,在项目创建初期就自定义用户模型,因为后期加字段(性别、学号、班级外键之类的)会比Profile模式更省事。

2.2 核心数据表设计:一张图看懂表关系

班里管理系统的核心数据表我尽量控制在九张以内,避免过度设计:

  • user:继承AbstractUser,增加用户类型字段和学号/工号
  • class_info:班级信息表,班主任外键关联到user表
  • student:学生信息表,外键关联class_info
  • course:课程表,外键关联教师user
  • score:成绩表,外键关联student和course,同时存考试批次和分数
  • attendance:考勤表,外键关联student,存打卡日期、状态、备注
  • leave:请假表,外键关联student,存请假类型、起止时间、审批状态
  • notice:通知表,外键关联发布者,多对多关联目标班级
  • notice_read:通知已读记录,外键关联notice和user

设计表关系时值得重点关注的地方是:成绩表要存考试批次字段,而不要直接覆盖历史成绩。之前见过有人直接在student表中加一个“成绩”字段,每次考试都覆盖,导致无法分析成绩趋势。成绩这个业务必须设计成“一条记录对应一个学生某次考试的一门课程”的粒度,这样计算学期平均分、班级排名、成绩曲线时才游刃有余。

2.3 Django模型实例:核心代码长这样

下面给出两个核心模型的参考实现,注意我全部使用自定义用户模型作为外键目标:

from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE_CHOICES = ( ('admin', '管理员'), ('teacher', '班主任'), ('student', '学生'), ) user_type = models.CharField('用户类型', max_length=10, choices=USER_TYPE_CHOICES, default='student') student_no = models.CharField('学号', max_length=20, blank=True, null=True) phone = models.CharField('手机号', max_length=11, blank=True, null=True) class Meta: db_table = 'sys_user' verbose_name = '用户' class ClassInfo(models.Model): name = models.CharField('班级名称', max_length=50) grade = models.CharField('年级', max_length=20) head_teacher = models.OneToOneField( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_class', verbose_name='班主任' ) students = models.ManyToManyField(User, through='Student', related_name='classes', verbose_name='学生') class Meta: db_table = 'class_info' verbose_name = '班级'

比较妙的是Student表作为中间表,通过自己的方式扩展了用户和班级的关系,这样可以给每个学生额外保存座位号、宿舍号等属性,同时保证一个学生只能属于一个班级。这里注意related_name一定要写,不然后续queryset反向查询会很难受,而且后期维护看到默认的user_set会让你怀疑人生。

再来看成绩表的设计:

class Score(models.Model): student = models.ForeignKey('Student', on_delete=models.CASCADE, related_name='scores', verbose_name='学生') course = models.ForeignKey('Course', on_delete=models.CASCADE, related_name='scores', verbose_name='课程') exam_batch = models.CharField('考试批次', max_length=50, default='期中考试') score = models.DecimalField('成绩', max_digits=5, decimal_places=2) created_at = models.DateTimeField('录入时间', auto_now_add=True) class Meta: db_table = 'score' unique_together = ('student', 'course', 'exam_batch') verbose_name = '成绩'

unique_together这一行特别关键,它从数据库层面保证同一个学生同一门课同一次考试不会录入两次成绩。当你用ORM做去重统计时,就不会因为脏数据导致排名异常。这个字段在毕业设计答辩时也常被评委问到“如何保证成绩数据一致性”,是很好的提问点。

2.4 通过showmigrations和查询日志验证模型

模型写完后别急着写业务逻辑。先跑两遍关键验证:

  • python manage.py makemigrations && python manage.py migrate,再执行python manage.py showmigrations确认所有迁移已应用
  • 打开数据库,查看每个表的外键关系是否符合预期

然后可以打开Django的shell调试ORM查询:

from django.db import connection students = Student.objects.filter(class_info__name='计科2101班').select_related('user', 'class_info') print(connection.queries)

通过打印connection.queries可以看到实际执行的SQL语句,帮助你发现查询是否产生了N+1问题。我发现很多新手在这里翻车:用Django ORM查询一个班级所有学生的成绩时,如果没有用select_related或prefetch_related,每取一条学生成绩都会多查一次数据库。这就是典型的ORM性能问题,面试和答辩都是必考题。

3. 认证、Cookie与Token:实现多角色登录的关键技术

3.1 Django自带认证体系够用吗

班务管理系统的登录逻辑其实可以拆成两部分:用户身份验证和身份凭证保持。

Django内置的django.contrib.auth已经提供了session认证,对传统MVT页面来说完全够用。登录视图就三行核心逻辑:

from django.contrib.auth import authenticate, login user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('dashboard')

但如果你想把系统同时做成Web接口给小程序端或其他前端调用,就必须考虑token或jwt方案。我在这个项目里选择了JWT。为什么要用JWT不用普通随机token?主要是两个原因:

  • JWT是无状态的,后端不需要把token存数据库,验证签名即可,适合分布式的接口调用
  • 载荷里可以携带用户id和角色信息,前端解析后即可实现菜单权限的显示控制

3.2 Cookie设置Token的正确姿势

搜“django cookie 设置 token”这个热词的应该都是被同一个问题卡的:前端拿到token后怎么存?答案是把它放进HttpOnly的Cookie中,而不是localStorage。

原因很直接:localStorage存的token能被页面里的任何JavaScript读取到,一旦你有XSS漏洞,token就裸奔了。把token设定为httpOnly cookie后,js读不到它的值,只能由浏览器自动携带,这能有效把XSS攻击的伤害面降到最低。

在Django里给前端设置Cookie有两种常见写法:

方法一:后端手动设置Cookie:

from django.http import JsonResponse response = JsonResponse({"code": 0, "msg": "登录成功"}) response.set_cookie( 'access_token', token, httponly=True, max_age=60 * 60 * 24 * 7, samesite='Lax', secure=False # 生产环境开启HTTPS时设为True ) return response

方法二:使用djangorestframework-simplejwt库,自定义登录返回后加Cookie。两种方式都行,核心是记得设置httponly=True和samesite参数。

同时后端写一个登录校验中间件,从Cookie中提取token后验证:

class TokenAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): token = request.COOKIES.get('access_token') if token: try: validated_token = JWTAuthentication().get_validated_token(token) request.user = JWTAuthentication().get_user(validated_token) except Exception: request.user = AnonymousUser() return self.get_response(request)

中间件是一个很讨巧的设计,它可以在所有视图执行之前统一做用户识别,避免每个视图都写一遍解析逻辑。

3.3 RBAC权限校验在视图层的落地

用户登录后,视图函数必须校验角色。我的做法是定义三个装饰器,直接按用户类型判断:

from functools import wraps from django.http import JsonResponse def require_role(*roles): def decorator(view_func): @wraps(view_func) def _wrapped(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({'code': 401, 'msg': '未登录'}, status=401) if request.user.user_type not in roles: return JsonResponse({'code': 403, 'msg': '无权限'}, status=403) return view_func(request, *args, **kwargs) return _wrapped return decorator @require_role('teacher', 'admin') def attendance_manage(request): ...

如果只想在模板层控制菜单显示,则直接在模板里判断:

{% if request.user.user_type == 'teacher' %} <a href="{% url 'score_add' %}">录入成绩</a> {% endif %}

这里务必记住:模板判断只是用户体验,视图层校验才是安全底线。答辩时要能说清楚这两者区别,这是证明你“懂安全”的关键细节。

4. 核心业务模块实现:考勤、成绩、通知的完整链路

4.1 考勤打卡与请假审批的后端逻辑

考勤模块最容易写成“点击打卡按钮,存一条记录”。但真实班主任工作不仅要记录“谁来了”,还要统计“谁迟到了几次”,所以我设计考勤表的一个关键字段是status,用整数表示状态:

class Attendance(models.Model): STATUS_CHOICES = ( (1, '正常'), (2, '迟到'), (3, '早退'), (4, '旷课'), ) student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='attendances', verbose_name='学生') date = models.DateField('考勤日期', auto_now_add=True) course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='attendances', verbose_name='课程') status = models.IntegerField('考勤状态', choices=STATUS_CHOICES, default=1) remark = models.CharField('备注', max_length=200, blank=True)

打卡逻辑的核心是时间判断。比如上午第一节课8:00开始,我设置一个deadline配置项,8:00之前打卡为正常,8:00之后到8:15之间为迟到,8:15之后为旷课。这个判断直接写成一个函数:

from datetime import time import zoneinfo def calc_attendance_status(checkin_time: time, deadline: time, grace: int = 15): if checkin_time <= deadline: return 1 # 计算迟到分钟数 late_minutes = (checkin_time.hour - deadline.hour) * 60 + (checkin_time.minute - deadline.minute) if late_minutes <= grace: return 2 return 4

提交打卡时同时要防止重复打卡。这里用ORM的get_or_create会不规范,因为同一个人同一节课只能有一条考勤记录:

attendance, created = Attendance.objects.get_or_create( student=student, date=today, course=course, defaults={'status': calc_attendance_status(...)} ) if not created: return JsonResponse({'code': 1, 'msg': '今日该课程已打卡'})

get_or_create不是并发安全的,真要并发打卡场景下会有竞态,但作为毕业设计已经足够,答辩时候可以顺势说一句“如果需要更严格可以去重,我们会引入数据库唯一约束”,这会给评委一种“懂并发边界”的印象。

请假审批的流程则是经典的状态机。请假表加一个status字段:0待审批、1通过、2拒绝。学生提交请假申请后,班主任在view里修改状态。审批通过后,我们可以直接创建一条请假状态的考勤记录,这样就不会误判成旷课。

4.2 成绩录入、统计排名与成绩单导出

成绩模块有两个比较容易出彩的功能点:多条件查询汇总和导出Excel。

多条件查询是评委经常现场点开使用的功能:“我要查看某个班级、某次考试、某门课的成绩列表”,如果你只能筛选其中一个,现场会很尴尬。我的建议是使用Django的链式筛选:

def score_list(request): class_id = request.GET.get('class_id') batch = request.GET.get('exam_batch') course_id = request.GET.get('course_id') scores = Score.objects.select_related('student', 'student__class_info', 'course').all() if class_id: scores = scores.filter(student__class_info_id=class_id) if batch: scores = scores.filter(exam_batch=batch) if course_id: scores = scores.filter(course_id=course_id)

记住,Django ORM的QuerySet是惰性的,你只管不停地链式filter,最后实际访问数据时才执行SQL。这样的函数写起来非常优雅,也是体现“Pythonic”的绝佳例子。

统计排名时使用窗口函数比较直接,Django 2.0以上支持在ORM里用Window表达式:

from django.db.models import F, Window from django.db.models.functions import Rank ranked_scores = Score.objects.annotate( rank=Window(expression=Rank(), partition_by=[F('exam_batch'), F('course')], order_by=F('score').desc()) )

用Rank窗口函数后,每一行成绩都会自动附带该次考试内的排名,一张查询就搞定排名功能,不依赖反复的for循环排序。

导出Excel不需要重造轮子,直接使用openpyxl库。核心代码大概是:

from openpyxl import Workbook from django.http import HttpResponse def export_scores(request): wb = Workbook() ws = wb.active ws.append(['学生姓名', '班级', '课程', '考试批次', '成绩']) for s in queryset: ws.append([s.student.user.name, s.student.class_info.name, s.course.name, s.exam_batch, s.score]) response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') response['Content-Disposition'] = 'attachment; filename=scores.xlsx' wb.save(response) return response

实际开发时我发现很多同学在导出时不会设置Column Dimensions,导致导出的Excel列宽很别扭。顺手加一行ws.column_dimensions['A'].width = 20观感会好很多,这种小细节很能给答辩加分。

4.3 班级通知的“已读未读”设计

通知模块怎么体现“交互感”?答案是已读回执。

Django实现已读功能的常见方案是建一个中间表NoticeRead记录每个用户对每篇通知的已读状态。发布通知时,只创建notice记录;学生点击通知后,在NoticeRead中插入一条已读记录。查询未读数量:

unread_count = Notice.objects.filter(target_classes__in=my_classes).exclude( id__in=NoticeRead.objects.filter(user=request.user).values('notice_id') ).distinct().count()

这个查询的核心点是:notices排除掉我已经读过的那批,剩下的就是未读。另一个容易被忽略的点是ManyToManyField的target_classes过滤后会产生重复记录,因此最后必须加.distinct()。

5. 用Django Channels做WebSocket实时推送

5.1 什么时候需要WebSocket,什么时候不要硬上

热搜词里“python django websocket实现后台有数据前端推送”被大量搜索,说明很多人的毕设需求里都有“后台一旦有新数据,前端页面要立刻刷新”。合理。但我必须先泼一盆冷水:WebSocket不是万能的。

如果推送场景只是“学生提交请假申请,5分钟内班主任看到就行”,你完全可以用前端每10秒轮询一次接口来实现;但如果场景是“老师正在上课点名,每个学生手机同时收到考勤通知”,或者“班级公告下发后,学生端立即弹出提醒”,这种强实时场景才需要WebSocket。前者省事,后者体验好,毕业设计优先保证稳定,其次才是炫技。

5.2 实现WebSocket必备的Channels配置

Django原生不支持WebSocket是基于HTTP请求/响应的模型,所以要做实时推送必须引入channels库。整合步骤总共有四步:

第一步:安装

pip install channels daphne

安装完成后一定要把项目里INSTALLED_APPS的第一位改成daphne,放在django.contrib.admin前面。这个顺序非常关键,daphne必须接管ASGI入口,否则启动服务时还是走HTTP。

第二步:在asgi.py里配置协议路由器

import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from channels.security.websocket import AllowedHostsOriginValidator from apps.notify import routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'classmanage.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AllowedHostsOriginValidator( AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)) ), })

第三步:定义路由和消费器。

# routing.py from django.urls import path from apps.notify.consumers import NoticeConsumer websocket_urlpatterns = [ path('ws/notice/', NoticeConsumer.as_asgi()), ]
# consumers.py from channels.generic.websocket import AsyncWebsocketConsumer import json class NoticeConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name = f'user_{self.scope["user"].id}' await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def send_notice(self, event): await self.send(text_data=json.dumps(event['data']))

注意写WebSocket consumer时是async语法,所以不能用Django ORM的同步查询,要么用database_sync_to_async包装,要么在连接时只做轻量操作。

第四步:在业务视图里通过channel layer推送消息。

from asgiref.sync import async_to_sync from channels.layers import get_channel_layer channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( f'user_{student.user_id}', {"type": "send_notice", "data": {"title": notice.title, "msg": "新公告已发布"}} )

注意group_send的第一个参数必须精确匹配前端订阅的组名。我在开发时踩过的坑是:用户id在消费器里取到的是字符串,在视图里却传了整数,两边拼接出来的组名对不上,测试了半天才发现是类型不一致。

5.3 前端连接与WebSocket安全

前端使用WebSocket原生API即可:

const socket = new WebSocket(`ws://${location.host}/ws/notice/`); socket.onmessage = function(event) { const data = JSON.parse(event.data); alert(data.msg); };

但WebSocket的鉴权是个隐藏大坑。因为浏览器WebSocket API没法主动携带Cookie头之外的token,所以我的方案是让用户先走HTTP登录,登录成功后种下httpOnly Cookie。WebSocket握手时会自动携带Cookie,Channels的AuthMiddlewareStack会自动解析到request.user。这就回到了前面花了那么大力气做Cookie token的意义,把推送给鉴权串成了一条线。

这里提醒一句:Channels的AuthMiddlewareStack默认只会把已认证用户设置为request.user,如果没登录,scope里就是AnonymousUser。所以consumer里的connect一定要加判断:

async def connect(self): if self.scope["user"].is_anonymous: await self.close(code=4001) return

6. 部署与上线:Nginx+Gunicorn+Django的实战配置

6.1 部署架构:四层结构别搞混

本地开发用的runserver只适合调试,真正要跑在服务器上必须用标准部署架构:

  • Nginx:接收用户请求,处理静态文件和反向代理
  • Gunicorn:运行Python代码,处理Django动态请求
  • Django应用:业务逻辑主体
  • PostgreSQL/MySQL:数据存储

这样做卡主的流量不会直接打到Python进程,静态资源由Nginx直接回,压缩和缓存也都在这一层完成。

6.2 Gunicorn的基本配置

我用Supervisor管理Gunicorn进程,配置文件大概是:

[program:classmanage] command=/home/user/.virtualenvs/classmanage/bin/gunicorn classmanage.wsgi:application --bind 127.0.0.1:8001 --workers 3 directory=/home/user/apps/classmanage autostart=true autorestart=true stderr_logfile=/var/log/classmanage/bin/error.log

workers数量一般按CPU核心数*2+1计算,比如2核机器就开5个worker。开多了反而会增加内存压力和进程切换开销。如果某个worker发生内存泄漏,supervisor会自动重启,保证服务的稳定性。

6.3 静态文件与部署后刷新不出来的问题

部署后最容易出现的白屏场景是:静态文件路径不对。本地开发时Django能自动为你托管静态文件,但生产环境下必须由Nginx托管。

我处理static的方法:

  • 把STATIC_ROOT设置为项目外的/var/www/classmanage/static
  • 执行python manage.py collectstatic把所有静态文件汇总到该目录
  • 在Nginx配置里把该目录映射为/static/路径
location /static/ { alias /var/www/classmanage/static/; expires 30d; }

另一个常见坑是STATIC_URL和STATIC_ROOT搞混。STATIC_URL是浏览器端访问静态文件的URL前缀,STATIC_ROOT是服务器端的物理目录,两个必须同时配置好。Media上传文件则用MEDIA_URL和MEDIA_ROOT单独管理,Nginx里再加一个location映射。

6.4 部署时settings的关键调整

本地能跑,部署后各种403,排查半天发现是ALLOWED_HOSTS没配。上线前请务必检查这几个配置项:

DEBUG = False ALLOWED_HOSTS = ['yourdomain.com', '你的服务器IP'] CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com']

如果Cookie要跨域,还要配置CORS。Django处理跨域,我建议直接使用django-cors-headers库:

INSTALLED_APPS += ['corsheaders'] MIDDLEWARE += ['corsheaders.middleware.CorsMiddleware'] CORS_ALLOWED_ORIGINS = ['https://yourdomain.com'] CORS_ALLOW_CREDENTIALS = True

把CORS_ALLOW_CREDENTIALS打开,前端带Cookie跨域访问接口时才不会被浏览器拦截。

6.5 数据库从SQLite迁移到MySQL

毕设早期的SQLite确实方便,但答辩时如果用的是本机SQLite,很容易被一句话问倒:“如果数据量增大,SQLite还撑得住吗?”所以最后阶段我迁到了MySQL。迁移过程有几个重点:

  • 安装mysqlclient或用PyMySQL作为驱动
  • 修改DATABASES配置的ENGINE为django.db.backends.mysql
  • 原有SQLite数据使用manage.py dumpdata导出,再在MySQL下用loaddata导入

如果数据里有中文,记着设置连接参数里带上charset='utf8mb4',否则容易出字符集错乱。我当初迁移完所有学生姓名变成了问号,排查了半天才反应过来是备份时字符集就没对齐。

7. 从开发到答辩:我踩过的一些坑和建议

7.1 Django ORM的N+1查询,答辩高频翻车点

很多人在浏览器里打开“班级学生列表”页面时,发现加载很慢,根本原因就是N+1查询。看这段代码:

students = Student.objects.filter(class_info_id=1) for stu in students: print(stu.user.name)

每次循环访问stu.user都会触发一次额外SQL查询,100个学生就会执行101条SQL。解决办法就是前面提过的select_related:

students = Student.objects.select_related('user', 'class_info').filter(class_info_id=1)

用Django Debug Toolbar查看SQL执行次数是判断N+1最直接的手段。装上django-debug-toolbar后,页面右侧会显示SQL查询次数和耗时,接手别人项目时第一件事就是看这里的恐怖数字。

7.2 删除对象时的数据库约束坑

热搜词里“django执行查询-删除对象”这个词很精准,因为新手执行删除时最容易遇到外键约束报错。Django处理外键删除有三种策略:

  • CASCADE:删除主表时连带删掉外键表记录
  • PROTECT:有外键引用时禁止删除主表,抛ProtectedError
  • SET_NULL:外键字段允许为空时,删除主表后把外键置NULL

班级表如果被学生表外键引用,你直接删除班级,默认会连带删除该班级所有学生。如果这不是期望行为,必须把外键的on_delete设置为PROTECT或SET_NULL。实践中的建议是:凡是涉及业务数据关联的表,一律用PROTECT,防止误删;只有纯记录类表才用CASCADE,比如删除一条通知后连带删除它的已读记录。

7.3 创建App的目录划分,不要一个大app塞到底

Django创建App非常简单:

python manage.py startapp apps

但很多新手偷懒只建一个app,把所有模型和视图塞在一起,到后面想找代码都要翻半天。我的建议是按业务域划分app:

  • app_student:学生信息管理
  • app_attendance:考勤请假
  • app_score:成绩管理
  • app_notice:通知公告
  • app_manage:后台管理类页面

每个app的模型职责单一,视图文件按逻辑拆成views.py、forms.py、apis.py。这个习惯会让代码结构变成答辩时的展示亮点,评委会觉得你确实按工程化标准在开发。

7.4 django-unfold这类现代后台值不值得用

django-unfold是一个基于新版Django Admin的美化方案,用了它之后后台也不会再是那种古板的django默认样式。我的建议:如果毕设有单独的后台管理界面需求,可以引入unfold,因为它不需要大量改代码,只需替换相应的Admin类配置即可。但要小心版本兼容问题,新版unfold对Django版本有要求。如果导师更看重务实风格,原版django admin也已经足够,你可以把省下来的时间投入核心业务模块。

7.5 答辩时准备几个“延伸扩展”话题

评委对毕业设计的提问往往不局限于已实现功能,他更喜欢问“如果要做成产品,你下一步会优化什么”。你可以提前准备这些话题:

  • 系统可以加入班级量化考核功能,根据考勤、纪律、卫生等维度自动生成评分
  • 成绩分析模块可以扩展可视化图表,例如用ECharts展示班级成绩分布与趋势
  • 学生端可以增加个人学习档案,记录每次成绩变化曲线
  • 考勤打卡功能可以对接校园一卡通设备
  • 消息推送可以引入企业微信或钉钉机器人webhook,真正落地到日常班主任工作场景

这些话题体现的是项目延展性,不是空口说大话,而是基于你现有代码结构确实能平滑扩展的方向。准备三四条就足够。

7.6 最后分享一点实际体会

做完这个项目我最大的感受是:毕业设计出彩的关键,不是功能有多花哨,而是你有没有把“用户、权限、数据关系”这三条线彻底想清楚。班务管理系统做出来容易,做深很不容易。你愿意在权限模型上多花三天,愿意在ORM查询优化上较真,愿意把WebSocket部署链路真正跑通,这些东西才会在答辩现场变成你的底气。如果你正在啃这个题目,别慌,把数据库设计画清楚,再把Django的迁移命令跑通,剩下的事自然就顺了。

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

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

立即咨询