
做了一个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基于角色的访问控制模型落地时守住三条原则角色绑定权限不直接给用户绑菜单或方法。用户只跟角色关联角色再跟权限关联。这样以后新增角色只需分配权限集合不用改用户表。权限控制到视图和按钮级别。视图层用装饰器校验模板层用判断语句控制按钮显隐。后端权限校验绝不能依赖前端隐藏。前端界面可以根据角色隐藏菜单但每个视图都必须做权限验证防止学生直接URL访问教师端接口。Django用户体系扩展主要有两种一种是用Profile模型OneToOne扩展auth.User另一种是重写AbstractUser自定义用户模型。实际开发时我建议直接用第二种在项目创建初期就自定义用户模型因为后期加字段性别、学号、班级外键之类的会比Profile模式更省事。2.2 核心数据表设计一张图看懂表关系班里管理系统的核心数据表我尽量控制在九张以内避免过度设计user继承AbstractUser增加用户类型字段和学号/工号class_info班级信息表班主任外键关联到user表student学生信息表外键关联class_infocourse课程表外键关联教师userscore成绩表外键关联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_length10, choicesUSER_TYPE_CHOICES, defaultstudent) student_no models.CharField(学号, max_length20, blankTrue, nullTrue) phone models.CharField(手机号, max_length11, blankTrue, nullTrue) class Meta: db_table sys_user verbose_name 用户 class ClassInfo(models.Model): name models.CharField(班级名称, max_length50) grade models.CharField(年级, max_length20) head_teacher models.OneToOneField( User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namemanaged_class, verbose_name班主任 ) students models.ManyToManyField(User, throughStudent, related_nameclasses, 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_deletemodels.CASCADE, related_namescores, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namescores, verbose_name课程) exam_batch models.CharField(考试批次, max_length50, default期中考试) score models.DecimalField(成绩, max_digits5, decimal_places2) created_at models.DateTimeField(录入时间, auto_now_addTrue) 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语句帮助你发现查询是否产生了N1问题。我发现很多新手在这里翻车用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, usernameusername, passwordpassword) 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有两种常见写法方法一后端手动设置Cookiefrom django.http import JsonResponse response JsonResponse({code: 0, msg: 登录成功}) response.set_cookie( access_token, token, httponlyTrue, max_age60 * 60 * 24 * 7, samesiteLax, secureFalse # 生产环境开启HTTPS时设为True ) return response方法二使用djangorestframework-simplejwt库自定义登录返回后加Cookie。两种方式都行核心是记得设置httponlyTrue和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: 未登录}, status401) if request.user.user_type not in roles: return JsonResponse({code: 403, msg: 无权限}, status403) 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_deletemodels.CASCADE, related_nameattendances, verbose_name学生) date models.DateField(考勤日期, auto_now_addTrue) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameattendances, verbose_name课程) status models.IntegerField(考勤状态, choicesSTATUS_CHOICES, default1) remark models.CharField(备注, max_length200, blankTrue)打卡逻辑的核心是时间判断。比如上午第一节课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( studentstudent, datetoday, coursecourse, 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_idclass_id) if batch: scores scores.filter(exam_batchbatch) if course_id: scores scores.filter(course_idcourse_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( rankWindow(expressionRank(), partition_by[F(exam_batch), F(course)], order_byF(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_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet) response[Content-Disposition] attachment; filenamescores.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__inmy_classes).exclude( id__inNoticeRead.objects.filter(userrequest.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 fuser_{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_datajson.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)( fuser_{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握手时会自动携带CookieChannels的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(code4001) return6. 部署与上线NginxGunicornDjango的实战配置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 autostarttrue autorestarttrue stderr_logfile/var/log/classmanage/bin/error.logworkers数量一般按CPU核心数*21计算比如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导入如果数据里有中文记着设置连接参数里带上charsetutf8mb4否则容易出字符集错乱。我当初迁移完所有学生姓名变成了问号排查了半天才反应过来是备份时字符集就没对齐。7. 从开发到答辩我踩过的一些坑和建议7.1 Django ORM的N1查询答辩高频翻车点很多人在浏览器里打开“班级学生列表”页面时发现加载很慢根本原因就是N1查询。看这段代码students Student.objects.filter(class_info_id1) for stu in students: print(stu.user.name)每次循环访问stu.user都会触发一次额外SQL查询100个学生就会执行101条SQL。解决办法就是前面提过的select_relatedstudents Student.objects.select_related(user, class_info).filter(class_info_id1)用Django Debug Toolbar查看SQL执行次数是判断N1最直接的手段。装上django-debug-toolbar后页面右侧会显示SQL查询次数和耗时接手别人项目时第一件事就是看这里的恐怖数字。7.2 删除对象时的数据库约束坑热搜词里“django执行查询-删除对象”这个词很精准因为新手执行删除时最容易遇到外键约束报错。Django处理外键删除有三种策略CASCADE删除主表时连带删掉外键表记录PROTECT有外键引用时禁止删除主表抛ProtectedErrorSET_NULL外键字段允许为空时删除主表后把外键置NULL班级表如果被学生表外键引用你直接删除班级默认会连带删除该班级所有学生。如果这不是期望行为必须把外键的on_delete设置为PROTECT或SET_NULL。实践中的建议是凡是涉及业务数据关联的表一律用PROTECT防止误删只有纯记录类表才用CASCADE比如删除一条通知后连带删除它的已读记录。7.3 创建App的目录划分不要一个大app塞到底Django创建App非常简单python manage.py startapp apps但很多新手偷懒只建一个app把所有模型和视图塞在一起到后面想找代码都要翻半天。我的建议是按业务域划分appapp_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的迁移命令跑通剩下的事自然就顺了。