ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Django的教务选课系统设计与实现:并发控制与权限管理实战

基于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_length=20, unique=True, verbose_name="课程编号") name = models.CharField(max_length=100, verbose_name="课程名称") credit = models.FloatField(verbose_name="学分") teacher = models.ForeignKey(TeacherProfile, on_delete=models.CASCADE, verbose_name="授课教师") schedule = models.CharField(max_length=50, verbose_name="上课时间") capacity = models.IntegerField(default=30, verbose_name="容量上限") selected_count = models.IntegerField(default=0, verbose_name="已选人数") description = models.TextField(blank=True, verbose_name="课程简介") class Meta: db_table = "course" class SelectionRecord(models.Model): student = models.ForeignKey(StudentProfile, on_delete=models.CASCADE, verbose_name="学生") course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name="课程") selected_at = models.DateTimeField(auto_now_add=True, verbose_name="选课时间") score = models.FloatField(null=True, blank=True, 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, username=username, password=password) if user is not None: login(request, user) if user.is_superuser: return redirect("/admin/") if user.groups.filter(name="teacher").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, pk=course_id) student = request.user.studentprofile with transaction.atomic(): locked_course = Course.objects.select_for_update().get(pk=course.id) if locked_course.selected_count >= locked_course.capacity: return JsonResponse({"success": False, "msg": "课程名额已满"}) if SelectionRecord.objects.filter(student=student, course=locked_course).exists(): return JsonResponse({"success": False, "msg": "您已选修该课程"}) SelectionRecord.objects.create(student=student, course=locked_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, pk=course_id, teacher=teacher) 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(pk=record_id, course=course) record.score = float(score) if score else None record.save(update_fields=["score"]) return redirect("course_detail", course_id=course.id) records = SelectionRecord.objects.filter(course=course).select_related("student") return render(request, "teacher/scores.html", {"course": course, "records": records})

用get_object_or_404(Course, pk=course_id, teacher=teacher)同时做了权限校验,如果一个教师尝试修改别的教师的课程,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_delete=models.CASCADE,级联删除把相关联的数据全部清理了。

解决:对于课程表关联教师的外键,改用on_delete=models.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 课设项目,都会强制自己走一遍「删库 → 迁移 → 造数据 → 三角色演示」的流程,再小的项目也值得花二十分钟做一次干净环境验证。希望这套设计和避坑笔记能帮你在交付或答辩时少遇到几个深夜问题,祝顺利。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进