基于Django的教务选课系统设计与实现:并发控制与权限管理实战
简介一份基于Django框架的学生教务选课系统毕业设计源码面向计算机相关专业毕业生或需要快速搭建教务选课平台的开发者。系统覆盖学生、教师、管理员三类角色的身份验证与权限控制包含课程信息管理、学生选课、已选课程查看、选课人数统计等功能模块可帮助理解Web系统从需求分析到实现部署的完整流程。资源包共2000个文件以js、html、css、py为主js用于前端交互逻辑html和css搭建页面结构与样式py实现后端业务处理另有少量json、xml及配置文件用于数据交互和环境配置。压缩包整体约5.66MB文件类型多样便于按目录分类检索。目前已有182人学习下载。通过阅读源码与运行调试可以掌握Django的MTV架构、ORM数据库操作、用户认证与权限管理、以及课程信息的增删改查等关键知识点适合作为毕业设计参考或Django入门实战项目。1. 从一次选课崩溃说起为什么需要一个能落地的教务选课系统某个学期选课开放的第一天某高校的教务系统在晚上八点准时卡死A同学刷新了半小时才挤进去结果热门课程名额早就没了而有人用脚本一秒内抢走了三个名额。这件事听起来像运维事故但本质上是业务设计的问题选课系统不只是「把数据存进数据库」它要同时解决高并发下的名额扣减、用户身份权限隔离、学生与教师的信息流转以及管理员对异常数据的兜底处理。很多学生在做这类毕设时会把精力全花在页面上结果做到答辩时发现选课逻辑有并发漏洞、成绩录入和权限控制混在一起、数据库外键删除导致数据错乱。这篇文章要展示的是一套用 Python Django 实现的学生教务选课系统完整方案从用户角色划分、数据库建模、选课/退课/成绩管理核心接口到本地部署和验收答辩要点按一套能直接交付的标准来讲。适合正在做相关课设或毕设的人参考也适合想快速理解 Django 业务系统设计思路的人。这套方案不追求大而全而是把「学生选课、教师录入成绩、管理员管理课程和学生」这条主链路做扎实让你拿着就能讲清楚、能演示、能回答老师追问。2. 总体设计技术选型、角色权限与数据库建模2.1 技术栈选型为什么是 Django 而不是 Flask 或 Spring这套系统选择 Django 3.2 Python 3.8 SQLite 作为基础组合原因有三个一是 Django 自带的 Admin 后台可以直接给管理员用省去单独开发管理端页面的工作量二是 ORM 的对象关系映射能力让选课这种多表关联操作写起来更安全不容易拼错 SQL三是 Django 的用户认证体系内置了 session、权限组和密码哈希直接复用可以避免自己造轮子带来的安全漏洞。如果用 Flask自由度更高但需要自己集成 SQLAlchemy 和 Flask-Login对课设项目来说组装成本偏高。用 Spring 又对 Java 环境有依赖和 Python 生态割裂。Django 属于「框架替你决定了一部分规矩但这些规矩正好匹配教务系统」的选择。数据库默认用 SQLite 方便本地演示生产环境换 MySQL 时只需要修改 settings 里的数据库配置和安装驱动模型代码不用动。实际交付时我一般会建议把 DEBUG 设为 False、关闭 Debug 工具栏并把 SQLite 换成本地 MySQL这样答辩演示时不会因为临时文件锁导致写入失败。2.2 系统角色与权限矩阵系统划分为三种角色学生、教师、管理员。三者对应 Django 内置 User 模型的 is_staff 和 is_superuser 字段再通过 Groups 做细分。学生的权限是浏览课程列表、选课、退课、查看个人已选课程和成绩教师可以查看自己名下的课程、查看选课学生列表、录入和修改成绩管理员通过 Django Admin 管理用户、课程、选课记录和系统公告。权限控制在视图层通过装饰器实现核心原则是学生不能访问教师接口教师不能修改不属于自己的课程数据所有角色都不能直接操作其他人的密码。这里要注意的是不能只在前端隐藏按钮来控制权限因为接口本身是裸露的必须用 Django 的login_required和自定义user_passes_test做后端校验。角色与权限矩阵如下功能模块学生教师管理员浏览课程列表允许允许允许选课/退课允许限本人禁止允许代操作管理课程信息禁止仅限名下课程全部课程录入/修改成绩禁止仅限名下课程全部课程用户管理禁止禁止允许查看选课统计禁止仅限名下课程全部课程这套矩阵会在实现阶段映射为具体的装饰器和查询过滤条件。2.3 数据库表设计与关键字段说明整个系统核心是五张业务表用户表复用 Django 内置 User、学生信息扩展表、教师信息扩展表、课程表、选课记录表。课程表字段包括课程编号、名称、学分、授课教师外键、上课时间、容量上限、已选人数、课程简介。选课记录表是关键表保存选课时间、学生外键、课程外键、成绩字段默认空。选课记录表必须加一个unique_together约束保证同一学生对同一课程只能选一次。课程表的capacity和selected_count是选课并发控制的两个核心字段后面会在实现章节详细处理。数据库模型的关键代码如下class Course(models.Model): course_id models.CharField(max_length20, uniqueTrue, verbose_name课程编号) name models.CharField(max_length100, verbose_name课程名称) credit models.FloatField(verbose_name学分) teacher models.ForeignKey(TeacherProfile, on_deletemodels.CASCADE, verbose_name授课教师) schedule models.CharField(max_length50, verbose_name上课时间) capacity models.IntegerField(default30, verbose_name容量上限) selected_count models.IntegerField(default0, verbose_name已选人数) description models.TextField(blankTrue, verbose_name课程简介) class Meta: db_table course class SelectionRecord(models.Model): student models.ForeignKey(StudentProfile, on_deletemodels.CASCADE, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) selected_at models.DateTimeField(auto_now_addTrue, verbose_name选课时间) score models.FloatField(nullTrue, blankTrue, verbose_name成绩) class Meta: db_table selection_record unique_together (student, course)外键删除策略要特别注意课程被删除时选课记录应该一并删除所以这里用 CASCADE但教师删除时需要保护历史课程数据不应该级联删除课程这时应该用PROTECT或手动改成「停用账号」而不是物理删除。这是答辩时容易暴露的设计盲区。3. 核心功能实现从登录到选课再到成绩录入的完整闭环3.1 用户认证与登录跳转三个入口一个视图登录功能复用 Django 的authenticate和login但跳转逻辑要根据角色区分。实现方式是在login_view里登录成功后读取user.is_staff和user.groups来判断角色然后分别重定向到学生首页、教师工作台或 Admin 后台。学生和教师的资料表通过OneToOneField关联到 User所以登录后要把用户 ID 作为外键去查对应的 Profile 信息。这里有个容易忽略的细节Django 默认只有一张 User 表学生的学号、专业班级信息要放到独立的 StudentProfile 表里不能直接往 User 表加字段。扩展字段的读取有两种方式一种是在视图中手动查询另一种是自定义user_passes_test时联表判断。对于课设来说第一种更直观也方便在模板里直接通过user.studentprofile访问。登录视图核心代码如下from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) if user.is_superuser: return redirect(/admin/) if user.groups.filter(nameteacher).exists(): return redirect(teacher_dashboard) return redirect(student_dashboard) return render(request, login.html, {error: 用户名或密码错误}) return render(request, login.html)这段逻辑的关键在于顺序先判断超级管理员再判断教师组最后默认按学生处理。答辩时如果老师问「为什么不用 is_staff 判断教师」可以回答is_staff 表示能否登录后台不代表教学身份教师既可能是 is_staff 也可能不是用组更符合业务语义。3.2 选课与退课的事务处理并发扣减名额的正确姿势选课接口是这套系统里最容易出 bug 的地方。初学者常见的写法是先查selected_count判断是否小于capacity然后再插入选课记录并将selected_count加一。这种做法在单用户测试时没问题但多浏览器并发选课时会出现超卖现象。要让学生明白这一点答辩时可以演示两个账户同时点击选课或者直接讲解竞态条件的概念。正确做法是使用 Django 的事务原子性和select_for_update在事务内锁住课程行记录再执行查询和更新。这样当两个请求同时到达时第二个请求会等待第一个事务提交后才能读取最新的selected_count从根源上避免超选。完整选课接口代码如下from django.db import transaction from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from .models import Course, SelectionRecord login_required require_POST def select_course(request, course_id): course get_object_or_404(Course, pkcourse_id) student request.user.studentprofile with transaction.atomic(): locked_course Course.objects.select_for_update().get(pkcourse.id) if locked_course.selected_count locked_course.capacity: return JsonResponse({success: False, msg: 课程名额已满}) if SelectionRecord.objects.filter(studentstudent, courselocked_course).exists(): return JsonResponse({success: False, msg: 您已选修该课程}) SelectionRecord.objects.create(studentstudent, courselocked_course) locked_course.selected_count 1 locked_course.save(update_fields[selected_count]) return JsonResponse({success: True, msg: 选课成功})代码里做了三件事锁定课程行、校验容量和重复选课、在同一个事务里插入记录并更新计数。select_for_update要放在事务内部才生效这是很多初学者翻车的地方因为 Django 的 ORM 默认 autocommit脱离事务的锁没有任何意义。退课逻辑则是反向操作删除选课记录前也要锁定课程行然后selected_count减一防止出现负数。退课接口还需要加一个时间窗口判断比如选课后 48 小时内可以退课超过时间需要向管理员申请这部分可以根据课设需求决定加不加。3.3 教师端成绩录入限定数据范围与批量更新教师端不允许教师查看所有学生选课记录只能操作自己名下的课程。视图查询时用request.user.teacherprofile去过滤课程列表再通过课程 ID 查选课记录。成绩录入通常是一个课程对应一个页面页面里用表格列出已选学生和成绩输入框提交时循环更新每条记录。批量更新成绩的代码login_required def save_scores(request, course_id): teacher request.user.teacherprofile course get_object_or_404(Course, pkcourse_id, teacherteacher) if request.method POST: with transaction.atomic(): for record_id, score in request.POST.items(): if record_id.startswith(score_): record_id int(record_id.split(_)[1]) record SelectionRecord.objects.get(pkrecord_id, coursecourse) record.score float(score) if score else None record.save(update_fields[score]) return redirect(course_detail, course_idcourse.id) records SelectionRecord.objects.filter(coursecourse).select_related(student) return render(request, teacher/scores.html, {course: course, records: records})用get_object_or_404(Course, pkcourse_id, teacherteacher)同时做了权限校验如果一个教师尝试修改别的教师的课程Django 会直接返回 404 而不是报权限错误这样更安全。select_related(student)可以避免循环查询学生姓名因为模板里要展示学生学号和姓名这属于基本的 ORM 性能优化答辩时可以主动提到。3.4 学生端课程浏览与选课状态展示学生端首页展示全部课程列表每门课显示课程名、教师、学分、上课时间、剩余名额以及「已选」「可选」「名额已满」三种状态。这个页面需要联表查询当前学生是否已选常规做法是查询该学生的所有选课记录把课程 ID 集合传进模板用if course.id in selected_ids来渲染状态避免为每门课单独执行一次查询。Django 模板中无法直接调用 Python 的集合判断所以要在视图里把selected_ids作为set传入模板上下文。这里有一个细节set在模板里可以通过in操作判断但如果传的是QuerySet模板引擎不会做存在性判断性能也会明显下降。视图里计算一次选课集合配合模板遍历展示是目前最简单且高效的做法。分页功能可以直接用 Django 内置的Paginator每页显示 10 条课程记录课程数量多的时候避免页面太长这个细节在演示时很加分。4. 常见问题与避坑指南并发、数据删除与部署中的翻车现场4.1 选课超卖现象、原因与解决现象两个学生同时点击选课时最后一门课程名额为 1两个人都显示选课成功最终selected_count变成 2超过capacity。原因视图先读selected_count再执行写入两个请求同时读到旧值都判断名额未满于是都插入记录。这是典型的并发竞态条件单机测试很难发现需要模拟并发请求才能触发。解决用transaction.atomic()包裹整个选课逻辑并使用select_for_update()锁定课程行。锁会阻塞第二个事务直到第一个事务提交从而保证读改写操作串行执行。答辩时如果老师问「为什么不用唯一约束」可以回答唯一约束只能防止同一学生重复选课无法解决多个不同学生同时竞争最后一个名额的问题两者是不同的维度。4.2 外键删除导致历史数据丢失现象删除一个教师账号时他教过的所有课程的教师字段变为空或者课程被直接删除选课记录也一起消失成绩数据无法追溯。原因课程表的 teacher 外键和选课记录的 course 外键都使用了on_deletemodels.CASCADE级联删除把相关联的数据全部清理了。解决对于课程表关联教师的外键改用on_deletemodels.PROTECT有课程引用时禁止删除教师账号选课记录保留 CASCADE 是合理的因为课程没了选课记录没有存在意义。更稳妥的做法是在教师模块中提供「停用账号」而不是「删除账号」将账号is_active置为 False 即可。4.3 时区问题导致选课时间错乱现象用户看到的选课时间和服务器时间不一致学生晚上八点点击选课选课记录里的selected_at显示的是中午十二点相差八个小时。原因Django settings 里默认时区是UTC而本机是中国时区auto_now_add写入的是 UTC 时间模板直接渲染会出现偏移。解决在 settings.py 中设置TIME_ZONE Asia/Shanghai和USE_TZ True同时模板渲染datetime对象时 Django 会依据当前时区自动转换。如果要在代码里手动处理时间比较统一使用django.utils.timezone.now()不要使用datetime.datetime.now()。4.4 静态文件与数据库配置在部署阶段翻车现象本地开发一切正常部署到云服务器后页面完全无样式或者打开页面报OperationalError: no such table。原因DEBUG True时 Django 自带静态文件服务关闭后静态文件需要collectstatic收集并交给 Web 服务器处理SQLite 数据库文件的路径没有迁移到服务器或迁移命令没有执行。解决部署在本地或云端时先执行python manage.py collectstatic配置STATIC_ROOT数据库迁移务必执行python manage.py makemigrations和python manage.py migrate。为了方便演示可以把 SQLite 文件路径改成项目根目录下的绝对相对路径避免因为启动目录不同而找不到数据库文件。4.5 表单重复提交与消息提示的体验问题现象学生网络较慢时点击选课按钮没反应再点一次生成了两条选课记录。原因前端的 POST 请求没有做防重复提交后端的选课视图里也没有对重复提交做拦截两次请求在短时间内都能通过校验。解决前端在点击选课后立即禁用按钮并把文案改成「提交中…」后端通过unique_together约束兜底第二次插入时抛出IntegrityError在视图里捕获异常并返回「请勿重复提交」提示。这是前后端双重控制的典型处理方式答辩时可以强调两者的分工。5. 验收与答辩如何把项目从「能跑」讲成「经得起追问」5.1 功能验收清单与演示路径演示顺序建议按照一条业务故事线来走注册登录 → 学生浏览课程 → 选课 → 查看已选课程 → 教师登录录入成绩 → 学生查看成绩 → 管理员后台查看选课统计。这条路径演示完整链路每一步都有数据支撑不会出现「功能都有但演示跳来跳去」的情况。验收清单可以按模块分类验收项操作步骤预期结果学生注册与登录填写注册信息登录能登录并跳转学生首页选课选择一门有空余名额的课程提示选课成功剩余名额减一重复选课再次选择同一门课程提示「您已选修该课程」退课在已选课程中执行退课提示退课成功名额加回教师录入成绩教师登录进入名下课程录入成绩成绩保存成功学生端可见权限隔离教师访问学生选课接口被重定向或返回 403每项功能演示时可以顺带打开 Django Admin 后台展示对应数据库表的实时变化比如选课前后的selected_count字段这会让老师觉得你对数据层是理解的而不只是把页面做出来了。5.2 答辩预判老师最可能追问的三个技术点第一问选课系统如何防止超额选课。回答思路是「数据库行锁 事务 唯一约束三层防线」不要只提一个点。第二问为什么用户表要拆成 User 和 StudentProfile 两张表。回答思路是「Django 内置 User 字段不可扩展拆表可以保证认证数据与业务数据解耦也方便未来接入第三方登录」。第三问系统有没有考虑高并发。回答思路是先承认课设阶段用 select_for_update 保证数据一致性如果要真正支撑全校选课需要引入 Redis 做名额预扣减、消息队列削峰但核心事务仍然要落数据库。这个问题提前想好答辩时就不会卡壳。5.3 交付前的最终检查习惯我在交付这类课设之前有一个习惯把数据库文件删除重新执行一遍迁移命令从零初始化数据确保新环境开箱即用。这个操作模拟了答辩老师拿到项目后第一次运行的真实场景如果这一步不顺畅说明项目里残存着本机环境依赖。然后我会用无痕浏览器分别登录三个角色的账号完整走一遍验收清单确认不存在 session 串号问题。从那以后我每次拿到这类 Django 课设项目都会强制自己走一遍「删库 → 迁移 → 造数据 → 三角色演示」的流程再小的项目也值得花二十分钟做一次干净环境验证。希望这套设计和避坑笔记能帮你在交付或答辩时少遇到几个深夜问题祝顺利。本文还有配套的精品资源点击获取