资讯动态

Django学生教务选课系统实战:事务并发与权限设计全解析

发布时间:2026/10/13 12:24:55 来源:尧图企业网站定制
简介这是一份基于Python与Django框架开发的学生教务选课系统毕业设计源码案例面向计算机专业毕业生、课程设计学生以及希望深入学习Django的初级开发者。系统完整覆盖了学生、教师、管理员三类角色的身份认证与权限控制并实现了课程信息管理、学生选课、已选课程查看、选课人数统计等典型教务业务模块有助于理解选课系统的整体架构与开发流程。资源包共包含2000个文件压缩包大小约5.66MB其中绝大多数为前端资源包括1632个JavaScript文件、250个HTML页面、54个CSS样式文件另含5个Python文件作为Django后端核心代码以及少量JSON、XML、TXT和Markdown说明文档目录划分清晰便于按模块对照学习。目前已有182人学习或下载。通过该案例能够快速掌握Django的MTV架构、ORM数据库操作、用户认证与权限控制、表单处理与安全机制等关键知识点同时获得一套可直接运行或二次开发的前后端完整代码可作为毕业设计选题方案或Django综合性实战练习的参考模板。1. 学生教务选课系统一个翻车率最低但最容易写坏的毕设方向拿到“Python基于Django学生教务选课系统”这个题目的人多半觉得它简单无非是学生登录、浏览课程、点一下选课、后台管理几条数据。但实际上我见过不少在这个题目上翻车的同学问题不出在功能做不完而出在业务逻辑写得太想当然——选课超员、重复选课、权限混乱、前后端数据对不上答辩时被老师连着追问几个“如果同时有100个人选最后一门课怎么办”场面就绷不住了。这个题目真正的价值在于功能量适中却能把 ORM、事务、鉴权、数据关系这些 Django 的核心机制全部覆盖一遍是一个投入产出比很高的毕业源码设计方向。本文就围绕这个项目从模型设计、选课事务、权限控制到避坑排查给出一套能直接照做的完整路径。2. 从选型到模型设计先回答“为什么是 Django”再写实体表2.1 Django 还是 Flask选课系统对框架的四个硬性要求选课系统这类教务项目本质上是一个典型的“数据密集型 CRUD 少量高并发写”场景。别急着选框架先列出业务对框架的硬性要求第一需要完善的对象关系映射课程、学生、选课记录之间的关系要能清晰表达第二需要开箱即用的后台管理界面方便教务人员维护课程数据不需要自己写一堆重复的增删改查页面第三需要内置的认证系统学生、教师、教务三类角色的登录和权限区分不能靠手写会话来实现第四需要能承载事务操作的数据库能力选课时的容量扣减必须保证一致性。对照这四点答案就很明确了Flask 确实灵活但它的 ORM通常搭配 SQLAlchemy和认证体系都要自己组装对于毕业设计来说组装成本高且容易在关键地方漏配置。Django 的 ORM、Admin 后台、内置 User 模型和中间件机制正好一一对应上面四个需求尤其是它的 ORM 自带的事务支持和行锁语法能直接用最少的代码把并发选课的问题处理掉这是 Flask 生态里需要额外绕路的点。所以我的建议是除非导师明确规定不允许使用重型框架否则这个题目就用 Django把精力省下来留给业务实现。2.2 三张核心表学生、课程和选课记录的模型长什么样选课系统的数据模型最忌讳的就是一张表搞定所有字段把学生信息、课程信息全部塞进 User 表里。常见且可靠的拆法是一张基础用户表加三张业务表User 由 Django 内置提供存放账号密码和角色标记Student 和 Teacher 分别通过一对一关联补充扩展字段Course 存课程本身信息Enrollment 存选课记录。这样拆的好处是解耦清晰答辩时也能讲明白为什么用外键而不是冗余字段。from django.db import models from django.contrib.auth.models import User class Student(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent) student_no models.CharField(max_length20, uniqueTrue, verbose_name学号) grade models.IntegerField(default2025, verbose_name入学年级) class Teacher(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameteacher) emp_no models.CharField(max_length20, uniqueTrue, verbose_name工号) title models.CharField(max_length20, blankTrue, verbose_name职称) class Course(models.Model): name models.CharField(max_length80, verbose_name课程名) teacher models.ForeignKey(Teacher, on_deletemodels.PROTECT, related_namecourses) credit models.DecimalField(max_digits3, decimal_places1, verbose_name学分) capacity models.PositiveIntegerField(default50, verbose_name容量) enrolled models.PositiveIntegerField(default0, verbose_name已选人数) schedule models.CharField(max_length50, verbose_name上课时间) class Enrollment(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_nameenrollments) course models.ForeignKey(Course, on_deletemodels.PROTECT, related_nameenrollment_set) created_at models.DateTimeField(auto_now_addTrue, verbose_name选课时间) class Meta: unique_together ((student, course),) verbose_name 选课记录这段模型里有几个细节值得展开说。Student 和 Teacher 都用 OneToOneField 关联 User而不是直接在 User 上追加字段。原因在于 Django 的 User 表是框架核心直接改它会让后续升级和迁移变得很被动用扩展表是官方推荐的姿势。Enrollment 表里的unique_together非常关键它让数据库层面就堵死了同一学生重复选同一门课。仅靠代码判断会出现并发漏洞这个唯一约束是兜底防线。2.3 外键与级联删除选课记录不该跟着课程一起消失外键的on_delete行为是答辩高频考点也是很多人在模型设计里最容易随手一写就出错的地方。先看两个反例第一个是把 Enrollment 指向 Course 的外键写成on_deletemodels.CASCADE意味着一旦删除了课程所有选课记录会被连带删除这在真实教务中是不可接受的学生的成绩和选课历史属于重要档案。第二个反例是把 Student 指向 User 的外键也做成 CASCADE看似合理但实际场景中学生转学或毕业账号注销不该把选课历史抹掉更合理的做法是软删除。所以在这个项目里推荐的做法是Enrollment 指向 Course 和 Student 时用on_deletemodels.PROTECT只要有选课记录存在对应的课程就不允许直接删除如果确实要下架课程应该通过添加状态字段如is_active做软处理。Course 指向 Teacher 的外键同样用 PROTECT避免教师被删后课程悬空。五张表之间的关系里唯一放心用 CASCADE 的是 Student 和 Teacher 对 User 的扩展关联因为扩展记录本身就是依附于账号存在的账号没了扩展信息没有意义。这样设计之后模型层面的数据完整性就有保证了。3. 把选课写进事务容量校验、冲突检测与六行核心代码3.1 事务处理为什么是关键两个用户抢最后一个名额先还原一个真实场景某门课容量还剩最后 1 个名额学生 A 和学生 B 同时点了选课按钮。如果代码写成“先查询已选人数判断是否小于容量再插入选课记录”那么 A 和 B 的请求可能同时读到“已选人数为 49”两个都通过了校验然后各自插入一条记录已选人数最终变成 51。这就是经典的超卖问题。原因在于“判断”和“写入”之间不是原子的两步之间存在时间窗口而这个窗口在并发场景下被两个请求同时踩进去了。解决思路非常明确必须把“检查容量 - 写入选课记录 - 更新已选人数”这三步放进同一个事务里并且在读取课程行时锁住它让第二个请求等第一个请求提交后再读。Django 的 ORM 正好提供了一个简单可用的语法select_for_update()配合transaction.atomic()就能实现行级锁。这是整个选课系统里技术含量最高的一个点也是答辩时最能讲清楚的地方。3.2 六行核心代码条件更新与 F() 表达式守住超限直接把这个选课逻辑写成一个视图函数。这里采用了一个更紧凑的方案不用先取出 ORM 对象再save()而是用filter(...).update()配合 F() 表达式做条件更新一步完成校验和自增。from django.db import transaction, IntegrityError from django.db.models import F from django.http import JsonResponse from django.views.decorators.http import require_POST require_POST transaction.atomic def enroll_course(request, course_id): user request.user if not hasattr(user, student): return JsonResponse({code: 403, msg: 非学生账号不能选课}) # 行级锁锁住课程行防止并发下容量判断失效 try: course Course.objects.select_for_update().get(pkcourse_id, is_activeTrue) except Course.DoesNotExist: return JsonResponse({code: 404, msg: 课程不存在或已下架}) # 条件更新容量未满才执行自增返回受影响行数 updated Course.objects.filter( pkcourse_id, capacity__gtF(enrolled) ).update(enrolledF(enrolled) 1) if updated 0: return JsonResponse({code: 400, msg: 课程已满员}) try: Enrollment.objects.create(studentuser.student, coursecourse) except IntegrityError: # 唯一约束兜底同一学生重复选课回滚人数自增 transaction.set_rollback(True) return JsonResponse({code: 400, msg: 请勿重复选课}) return JsonResponse({code: 200, msg: 选课成功})这里最关键的三行代码是select_for_update()、capacity__gtF(enrolled)和set_rollback(True)。select_for_update()锁住的是课程这一行数据在事务提交前其他事务对该行的更新操作会被阻塞保证后面的容量判断和人数自增是连续完成的。capacity__gtF(enrolled)意思是在数据库层执行“容量大于已选人数”这个条件判断而不是先把数据取回 Python 层再判断这样就避免了读到过期数据。最后一步重复选课触发了 IntegrityError此时必须显式回滚否则前面已经执行成功的update会把人数加 1 但选课记录没插入出现人数和记录对不上的脏数据。它的整体设计思路值得注意把最核心的容量守护放到了数据库条件更新上而不是依靠 Python 的 if 判断。这是一个重要的思维转变——在并发场景下应用层的“先查后判”只是建议数据库层的“条件更新”才是最终裁决。3.3 下课表冲突与外键约束前端校验只是第一道门容量问题解决了下一个常见需求是冲突检测。比如学生 A 选了周一 1-2 节的“高等数学”就不能再选同一时段的“大学物理”。很多初学者倾向于在前端页面做这个判断选课前把已选课程的 schedule 字段拉出来比对。前端校验当然可以提升体验但它不是可靠边界——用户直接构造请求绕过页面或者多标签页同时操作前端就形同虚设。可靠的方案是把这个校验放到事务内完成。在创建 Enrollment 之前先查询该学生已选课程中是否存在 schedule 冲突的记录。但这时仍然有个细节要注意冲突查询必须在同一个事务内执行否则事务 A 查询时没看到事务 B 尚未提交的选课记录冲突检测就会漏判。实际操作中还需要给 Enrollment 表增加一个 schedule 字段的冗余存储或者在 Course 表里用更规范的“星期 节次”JSON 字段这样冲突查询可以直接用exclude(pk__in...)配合 JSON 字段条件在数据库里完成。我的经验是如果代码里if 周一 in ...这种字符串截取逻辑大量出现说明表结构还需要再设计一次。从成本角度看这个环节用“唯一约束 事务内查询 数据库条件更新”三层组合才算把业务规则真正落到数据层。4. 登录鉴权与权限分层同一个系统里的学生、教师和教务4.1 用 Django 自带的 auth 搭双角色登录很多同学一上来就想自己写登录逻辑用 session 存用户 ID然后每个请求去查数据库判断身份。这在 Django 项目里是典型的重造轮子。Django 自带了一整套认证系统但针对这个项目要解决一个实际的问题登录接口需要区分学生和教师同时要往 User 表里填充这两类角色。这里推荐的做法是扩展一个统一登录视图认证成功后判断关联表类型再写入 session。from django.contrib.auth import authenticate, login from django.http import JsonResponse def login_view(request): username request.POST.get(username) password request.POST.get(password) # authenticate 会自动做密码哈希校验 user authenticate(request, usernameusername, passwordpassword) if not user: return JsonResponse({code: 400, msg: 用户名或密码错误}) # 登录成功后判断身份类型 role None if hasattr(user, student): role student elif hasattr(user, teacher): role teacher elif user.is_superuser: role admin else: return JsonResponse({code: 403, msg: 账号未分配角色}) login(request, user) return JsonResponse({code: 200, msg: 登录成功, role: role})这段代码里有两个必须注意的细节。authenticate这一步不只是查用户名密码它会校验用户是否启用is_active被禁用的账号在这里会被直接拦下来。hasattr(user, student)是利用 Django 的反向关系判断该 User 是否关联了 Student 表这个写法比user.student直接调用更安全因为直接调用在关联不存在时会抛异常而这里只是做一个存在性检查。登录接口本身不需要在 User 表上额外加字段。角色信息已经隐含在“是否有关联的学生/教师扩展表”以及“是否超级管理员”里这样后续增加新角色也不用改 User 表结构。前端拿到 role 后跳转到不同首页后端每次请求时通过中间件或装饰器再做一次权限校验。4.2 视图级权限控制教务该看什么学生又该碰什么登录只是第一层视图层面的权限控制才是核心。这个系统里至少有三类操作教务可以创建和上下架课程、查看所有选课记录教师可以查看自己课程的选课名单和人数学生只能选课、退课和查看自己的课表。如果没有权限控制任何登录用户直接请求课程管理接口系统就失控了。Django 提供了permission_required和user_passes_test两类方式但具体到这个项目访问权限不完全取决于权限码还需要判断当前用户的身份类型。建议写一个可复用的装饰器from functools import wraps from django.http import JsonResponse def student_required(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({code: 401, msg: 请先登录}) if not hasattr(request.user, student): return JsonResponse({code: 403, msg: 仅学生可操作}) return view_func(request, *args, **kwargs) return wrapper def teacher_required(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({code: 401, msg: 请先登录}) if not hasattr(request.user, teacher): return JsonResponse({code: 403, msg: 仅教师可操作}) return view_func(request, *args, **kwargs) return wrapper使用方式就是在视图函数上方加一行student_required或teacher_required。这些装饰器要放在require_POST这类条件装饰器下方确保请求方式验证先执行再进入身份校验。这里有一个常见坑装饰器的顺序搞反后未登录用户发起的 GET 请求可能会被require_POST拦截返回 405但日志里却看不到具体原因。可以在装饰器内部加一段print(request.user)辅助排查。4.3 会话过期与重复登录两个容易被答辩老师追问的点选课系统虽然只是进行内部业务但答辩环节老师喜欢追问两个细节会话过期后的前端表现、同一账号多处登录怎么处理。会话过期的问题可以这样处理在前端 Axios 请求中做统一拦截当接口返回 401 时清除本地存储并强制跳转登录页。Django 的 Session 默认有效期是两周设置SESSION_COOKIE_AGE可以调整时长的同时还要留意后端接口返回的响应头里设置的 cookie。如果用户不活跃session 过期是自动的前端拿着过期 cookie 访问接口会被拒这是预期行为。重复登录的处理则选择面很多常见做法是先踢掉旧会话之后新用户才能登录。实现上需要给 User 表或扩展表加一个session_key字段登录时生成新 session 后把旧 session 的 Session 记录删除。代码示例如下from django.contrib.sessions.models import Session def force_login(request, user): # 清理该用户历史会话 old_key Student.objects.filter(useruser).first().session_key if old_key: Session.objects.filter(session_keyold_key).delete() login(request, user) # 记录新会话 Student.objects.filter(useruser).update(session_keyrequest.session.session_key)这是一个可行的但不算最简单的思路。比较简单可靠的替代方案是在登录时直接把上一次登录时间写入扩展表前端在用户长时间未操作时主动触发刷新 token 校验。答辩时把这个方案讲清楚再加上“多端登录可能导致选课记录错乱”的理由会比泛泛而谈安全性更有说服力。5. 避坑指南选课系统最常见的五个翻车现场5.1 现象选同一门课两个人都成功了这是并发选课的第一大坑。第 3 章已经给了解决方案但这里必须再补充一种更隐蔽的触发方式两个请求进入同一个事务一个读取课程时没有加锁另一个先提交前者 commit 时覆盖了后者的更新。具体表现就是 assume 并发测试跑不通。原因select_for_update()必须配合事务使用且在 MySQL 默认的 InnoDB 引擎下才生效。如果代码里没有用transaction.atomic包裹视图函数或者数据库表的存储引擎是 MyISAM锁就会失效。解决确认 settings.py 里数据库引擎是 InnoDB视图函数加transaction.atomic并确认select_for_update()在事务内且不是commit_on_success模式下执行。可以写一个并发测试用两个线程同时发起选课请求再加一层断言验证选课人数不超过容量。提示调试并发问题时不要手动刷新页面去试写一段脚本同时发请求才是可复现的办法。5.2 现象事务没生效超卖问题照旧有时候代码加了transaction.atomic但测试发现超卖依旧。原因设置文件里配置了ATOMIC_REQUESTS或AUTOCOMMIT被设为 False导致事务的行为和预期不一致或者在transaction.atomic里捕获了异常但没有手动回滚导致之前已经执行的 SQL 没有正确回滚。解决把update和create的顺序调整为 create 成功后再 update如果 create 抛异常就抛到最外层让事务回滚而不是在内部 try/except 吃掉异常。里面set_rollback(True)只回滚了标记但外层如果不重新抛出异常事务提交时还是可能生效。最稳妥的方式是让 IntegrityError 直接抛出视图由 transaction 包裹层统一回滚并返回错误。5.3 现象CSRF 验证失败POST 请求被 403用 Ajax 提交数据时最容易踩中这个。Django 默认开启了 CSRF 防护但前端没有把 token 放进请求头。原因Django 在渲染模板时会生成一个csrf_token正常的表单通过{% csrf_token %}自动提交。但纯 Ajax 请求不会自动带 token必须在请求头里手动加。解决最标准的方式从 cookie 中取出 csrftoken写入请求头function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) { xhr.setRequestHeader(X-CSRFToken, getCookie(csrftoken)); } } });这里有个细节值得注意如果视图函数使用了csrf_exempt虽然能快速绕过去但强烈不建议在正式项目里用。答辩时提到“我通过设置请求头加入 token”会比“我干脆禁用了”显得专业得多。5.4 现象迁移文件冲突导致 makemigrations 报错多人开发或者自己复制了备份代码以后执行python manage.py makemigrations会出现“conflicting migrations detected”之类的报错。最常见的原因是两个分支同时修改了同一个模型并各自生成了迁移文件合并代码时迁移文件冲突。原因Django 的迁移是链式的新迁移文件依赖上一个迁移文件的名字。多个分支各自生成了基于同一个父迁移的子迁移形成分叉。解决最简单的做法在确保数据库里有对应数据表的前提下用python manage.py migrate --fake把当前分支的迁移状态标记为已应用再删除多余的分叉迁移文件。但是要注意--fake只适用于你已经确定表结构和迁移文件内容一致的情况否则会出现“迁移已应用但字段不存在”的隐性错误。如果不想这么激进就手动编辑迁移文件的dependencies列表把分叉迁移合并成一个线性链然后makemigrations --merge自动合并。5.5 现象时区设置导致日期统计差一天课程表里记录了选课时间后台统计“今天选课人数”时发现从上午 8 点到中午 12 点的选课记录被算到了昨天或者明天。这实际上是时区问题。原因MySQL 的 DATETIME 类型本身不包含时区信息Django 如果设置了USE_TZ True存进数据库的时间会被转换成 UTC 时间而从数据库读出的时间又被当成 UTC 处理最终展示时和本地时间相差 8 小时。解决如果项目只是单纯给国内使用最省心的方式是设置USE_TZ False同时把TIME_ZONE Asia/Shanghai这样 Django 存取的 datetime 都是本地时间。但如果想保留时区能力就保持USE_TZ True在使用时显式转为本地时间本地展示localtime(record.created_at)统计日期用datetime.date.today()配合__gte和__lt条件做区间查询而不是直接用__date比较。后者在数据库层会因为时区转换而出现偏差。6. 从“能跑”到“好讲”答辩前值得做的三个进阶动作6.1 用 Django Debug Toolbar 定位 N1 查询毕设做到后期课程列表页可能出现打开缓慢的情况原因通常是 N1 查询列表加载 50 门课程又对每门课程额外查询一次教师姓名和已选人数共执行了 51 条 SQL。装上django-debug-toolbar后直接看右侧面板中 SQL 查询数量如果数量是“课程数 1”就说明存在 N1。修复方式在于Course.objects.select_related(teacher).prefetch_related(enrollment_set)。select_related 解决外键prefetch_related 解决反向外键集合这是答辩时可以主动提出来的优化点。6.2 把选课规则抽成配置改参数不用改代码一个容易被忽视但很加分的做法把选课起止时间、每人每学期选课上限、是否需要先修课程数量限制等规则从代码里抽离到一个SystemConfig模型里或者至少放到settings.py中作为常量。比如MAX_COURSES_PER_STUDENT 6在视图逻辑里引用。答辩老师问“你们系统的规则好不好改”时直接展示配置项比现场改代码从容得多。配合 Django 的缓存框架这些配置读取还能进一步优化但这属于加分项不需要纠结。6.3 写一份准备回答的“为什么不用…”清单答辩阶段最大的风险不是项目做不出来而是项目做出来后面对备选方案的追问。提前准备三组问题就够了第一为什么用 Django 自带的 User 而不自己设计用户表——答案是安全性和开箱即用扩展表已经有 Student 和 Teacher 承接个性化字段第二为什么不用 JWT 而用 Session——答案是内部急切的可信环境Session 配合 CSRF 对浏览器场景更简单安全JWT 在登出和会话撤方面反而是劣势第三为什么不用 Redis 做缓存——答案是这个项目的数据量级用数据库直接读完全够用引入缓存是过度设计。这三组问题问答有理有据远比临时解释更有说服力。我自己的习惯是先把选课的核心事务跑通再去补齐装饰器和前端页面最后把 Debug Toolbar 打开检查查询数量。每个项目交付前重新跑一遍并发脚本确认不会超卖然后我才会安心提交。做毕业设计这件事最怕的不是功能少而是答辩时说不清楚每个细节为什么这样设计。希望这篇笔记能让你在验证选课逻辑和梳理参数时少绕几个弯也希望能帮到你顺利走完 1.1 到 6.3 的每一步。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑