ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python基于Django的学生教务选课系统设计与实现要点

Python基于Django的学生教务选课系统设计与实现要点

简介:基于Python与Django开发的学生教务选课系统毕业设计源码包,面向计算机相关专业毕业生、课程设计学生及入门Web开发的开发者,可直接用于毕业设计选题或课程项目参考。系统完整覆盖学生、教师、管理员三种角色的认证与权限控制,包含课程增删改查、学生选课、已选课程查看、选课人数统计、课程满意度调查等功能,并借助Django的MTV架构、ORM机制及CSRF/XSS防护体系,清晰展示了从需求分析到部署维护的完整开发流程。压缩包共2000个文件,约5.66MB,以1632个js前端交互脚本、250个html页面、54个css样式表为核心,另有少量Django视图模型py文件、json配置与xml文档,前后端层次明确,适合对照学习或二次改造。目前已有182人浏览学习,既可作为毕业设计选题参考、系统原型搭建模板,也能从中提取用户权限管理、选课业务逻辑与数据统计等模块的实用编码思路,是一份既能动手实践又能帮助梳理Web系统设计脉络的综合性参考。

1. 需求从「排课混乱」开始,选 Django 的三种决策理由

接手「Python基于Django学生教务选课系统」这类毕设项目时,最常见的坑不是写代码,而是根本不知道从哪个模块先动手。之前带过一个模拟项目X的开发,原需求是某高校教务处的选课流程数字化——人工排课总是撞教室、学生选课靠线下签字、成绩登记在 Excel 里来回传。问题看起来是「业务乱」,但落到系统设计上其实就是三件事:课程数据要统一管理、选课过程要不冲突、成绩结果要及时发布。选 Django 来搭这个教务选课系统,不是因为框架热门,而是看中它自带的 Admin 后台能直接管数据、ORM 能把六张表的关联关系说清楚、模板系统能覆盖学生端和教师端的大部分页面。这套方案适合两类人:一类是计算机相关专业的毕业生在准备毕设,需要一套能写进论文、能在答辩时讲清楚的设计;另一类是要给中小型院校做课设或实训项目的工程师。读完后你能拿到完整的设计思路,包括模块划分、数据库结构、核心实现,还有验收答辩的方法。

2. 总体架构设计:MVT 分层与四个核心模块的职责划分

2.1 架构分层:MVT 模式下请求怎么走、代码放哪

Django 的 MVT(Model-View-Template)架构和传统 MVC 的差别在于,Django 的 View 层同时承担了「取数据」和「决定跳转」的职责,而 Template 负责纯展示。在这个教务选课系统里,一个典型的请求链路是这样走的:学生点击「选课」按钮 → URL 路由把请求交给对应的 View 函数 → View 调用 Model 层查询课程余量 → 事务里写入选课记录 → 返回成功页面给 Template 渲染。

代码组织上,项目按功能边界而不是按技术层去建 App,实践下来最顺手的结构是四个 App:

  • accounts:处理登录、注册、会话和角色判断
  • courses:课程信息的增删改查、开课时间管理
  • selection:核心选课逻辑、选课记录、退课和冲突检测
  • grades:成绩录入、发布与统计

这种切法对应着不同角色的操作范围。学生进来看的是「可选课程列表」和「我的课表」,教师进来是「我教的课」和「成绩录入」,管理员在 Django Admin 里维护所有基础数据。每个 App 内部的 models.py、views.py、urls.py 只关注自己的业务,不需要跨模块调用对方的函数。四个 App 职责独立,后续要加功能,比如加一个「课表导出」模块,直接新建 App 而不影响现有代码。

2.2 模块划分:校方端、教师端、学生端、管理员端的边界

用户角色划为三类,再加上后台管理员,共同组成四个边界。

学生端需求最核心的是三个操作:选课、退课、查成绩,选课要看余量、看时间,退课要释放名额,成绩在教师发布后才能看到。教师端则是「我教什么课」和「我录什么分」,录分保存后学生立即可见,但录分窗口要限制在选课结束之后。管理员端主要通过 Django 自带的 Admin 管理课程、教室、教学班,不需要自己写页面,这能省下不少工作量。三个角色共用一套登录认证,一旦登录后通过 request.user 和角色字段判断能访问哪些视图,而不是在模板里隐藏按钮——后端必须拦截,前端只是体验优化。

2.3 技术栈选型:Django 版本、数据库、前端模板怎么定

技术栈按可控和够用两个原则选。

后端用 Django 3.2 LTS,选 LTS 版本是图省心,安全补丁覆盖时间长,网上资料也全。Python 版本用 3.8 以上即可,不追新,因为某些依赖库可能没跟上最新解释器。

数据库直接用 SQLite 还是换 MySQL,取决于部署环境。毕设演示和课设提交,SQLite 最省事,文件即库,复制即迁移。但答辩时如果被问到「数据量大怎么办」,答「原生支持切换到 MySQL,改 settings 里的 ENGINE 和 NAME 即可」就能说明你考虑过扩展性。模板层面用 Django 自带模板语法,渲染课程列表用 for 循环 + if 判断,不需要引入 Vue,因为需求里没有一个页面复杂到需要前端框架介入。这个系统的大部分页面就是表单提交和数据表格,服务端渲染足以覆盖。

注意:不要把技术栈选型写成「因为 Vue 火所以加 Vue」。毕设答辩时技术选型的理由必须落在需求上,比如「用 Django 自带模板是为了减少前后端联调成本,把工作量集中到业务逻辑上」——这才是能说服评委的理由。

3. 数据库设计:六张核心表与「选课冲突」的数据约束

3.1 实体关系梳理:学生、课程、教师、选课记录的关系

数据库设计是这套系统能不能立住的关键。因为选课系统的核心风险就是「并发选课导致超选」「退课后名额不释放」「成绩和课程对不上」。从实体关系上看,这个系统涉及四类核心实体:学生、教师、课程、选课记录。

教师和课程是「一对多」:一个教师可以教多门课,一门课只有一个主讲教师。课程和选课记录是「一对多」:一门课被多个学生选,一条选课记录只属于一门课。学生和选课记录是「一对多」:一个学生有多条选课记录,每条记录对应一门课,但同一个学生对同一门课只能有一条记录,这是数据层防重的关键。

班级和教师之间的关系不在核心链路里,不需要单独建表,用字段关联即可,避免过度设计。

3.2 核心表结构设计

这套系统的 models.py 核心代码可以这么写:

from django.db import models from django.contrib.auth.models import AbstractUser # 自定义用户表,扩展角色字段,Django 默认的 User 不够用 class User(AbstractUser): ROLE_CHOICES = ( ('student', '学生'), ('teacher', '教师'), ) role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='student') student_id = models.CharField(max_length=20, blank=True, null=True) class Meta: db_table = 'user' # 课程表:容量字段是选课冲突判断的唯一依据 class Course(models.Model): name = models.CharField(max_length=100, verbose_name='课程名称') teacher = models.ForeignKey('Teacher', on_delete=models.CASCADE, verbose_name='授课教师') capacity = models.IntegerField(default=50, verbose_name='课程容量') selected_count = models.IntegerField(default=0, verbose_name='已选人数') credit = models.FloatField(default=2.0, verbose_name='学分') schedule = models.CharField(max_length=100, verbose_name='上课时间') classroom = models.CharField(max_length=50, verbose_name='上课教室') is_open = models.BooleanField(default=True, verbose_name='是否开放选课') class Meta: db_table = 'course' # 选课记录表:unique_together 是防重复选课的关键 class SelectionRecord(models.Model): student = models.ForeignKey(User, 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='选课时间') class Meta: db_table = 'selection_record' unique_together = ('student', 'course') # 同一学生同一课程只能选一次

表设计里有三个细节值得在答辩时展开。第一是 selected_count 字段冗余在 Course 表里,而不靠 count() 实时计算,这是典型的「用空间换时间」,查询余量只需读一个整数字段,不用走聚合函数。第二是 unique_together 在数据库层面直接保证了不重复选课,API 层的判断只是界面友好提示,真正兜底在数据库约束。第三是用户表继承 AbstractUser 而不是用 OneToOne 扩展 Profile,因为角色字段需要直接参与权限判断,继承后 request.user.role 直接可读,不用每次都去查关联表。

3.3 约束设计:唯一约束与事务保证

选课冲突有两类:一类是同一个学生选同一门课,由 unique_together 解决;另一类是多个学生同时抢最后一个名额,这要靠事务 + 行级锁解决。

在 Django 里最稳的写法是用 select_for_update 锁住课程行,再判断余量:

from django.db import transaction @transaction.atomic def select_course(request, course_id): # 加锁锁住这条课程记录,其他请求必须等这个事务结束才能操作 course = Course.objects.select_for_update().get(id=course_id) if course.selected_count >= course.capacity: return JsonResponse({'code': 1, 'msg': '课程已满'}) # 同一事务里查重,防止同一学生重复提交 if SelectionRecord.objects.filter(student=request.user, course=course).exists(): return JsonResponse({'code': 1, 'msg': '你已经选过这门课'}) course.selected_count += 1 course.save() SelectionRecord.objects.create(student=request.user, course=course) return JsonResponse({'code': 0, 'msg': '选课成功'})

这里的关键点是必须把「验证余量」和「写入记录」放在同一个事务里,两步之间不允许其他请求插进来。如果先查再写分成两个事务,通常会有超卖漏洞,A 和 B 同时查到余量 1,然后两个都写入成功。select_for_update 是数据库层面的行锁,只有拿到锁的请求才能读这行数据,也就保证了先到先得。用 MySQL 时,整个流程跑在事务里会比 SQLite 稳定得多,因为 MySQL 的行锁机制更成熟。

3.4 核心数据表清单与字段说明

表名关键字段作用
userrole, student_id统一登录,区分学生与教师角色
coursecapacity, selected_count, is_open课程基础信息与选课开关
teachername, title教师基础信息,挂在课程表外键上
selection_recordstudent, course, selected_at选课记录,唯一约束防重复
gradestudent, course, score成绩记录,发布后学生可见

grade 表在选课记录表基础上加了一个 score 字段,外键同样指向学生和课程。表数量控制在五张左右是有意的,毕设论文里的 ER 图画出来不会太复杂,但每一张表的字段都能对应到业务动作,不会让人觉得「设计表只是为了凑字数」。

4. 核心功能实现:选课流程、冲突检测与权限控制

4.1 选课流程拆解:从课程列表到选课成功的四步链路

选课模块是整套系统的核心,流程可以拆成四步:进入选课页面、查看课程余量、提交选课请求、返回选课结果。

第一步要解决的问题是「哪些课可以选」。is_open 字段控制开关,展示时过滤掉未开放的课程,同时区分是全校公选课还是本专业课程。第二步展示课程时,余量实时显示 capacity 减去 selected_count 的结果,这个数据在页面加载时查询一次即可,不需要做实时推送,因为选课的即时冲突发生在提交瞬间。

第三步提交选课请求时,前端用 AJAX 提交比传统表单提交体验好得多,学生选完课不用整页刷新,失败时能直接看到「课程已满」的提示而不是跳转到错误页。第四步返回结果时,明确定义成功和失败的 JSON 结构,成功的数据带课表和上课时间,失败的数据带原因码。

4.2 冲突检测的两种实现:事务加锁与唯一约束兜底

冲突检测在 Django 里有两层,第一层是业务代码里的判断逻辑,第二层是数据库约束。两层的作用不同,不能互相替代。

业务层的代码已经写了,重点解释一下为什么 count+save 是错的。很多初版实现会先查询已选人数,判断小于容量就写入,但这样在并发下必然超卖。改法有两种:第一种是上面代码里的 select_for_update + 事务,适合选课频率不高的场景;第二种是使用 Django 的 F 表达式做原子更新,代码更短但需要在更新后判断返回值。

from django.db.models import F from django.db import transaction @transaction.atomic def select_course_fast(request, course_id): course_qs = Course.objects.filter(id=course_id, selected_count__lt=F('capacity')) # 原子更新,数据库层面只允许一个请求把 selected_count 加 1 updated = course_qs.update(selected_count=F('selected_count') + 1) if not updated: return JsonResponse({'code': 1, 'msg': '课程已满'}) SelectionRecord.objects.create(student=request.user, course_id=course_id) return JsonResponse({'code': 0, 'msg': '选课成功'})

这种写法的原理是先构造一个条件:已选人数小于容量,然后直接在 SQL 更新里把人数加一。如果条件不成立,update 返回 0,就说明没抢到。它的优势是只有一条 SQL 语句,天然原子性,代码也简洁。但要注意后续要加「同一个学生重复选课」的判断,还需要在创建记录前再查一次唯一约束,或者依赖数据库抛异常兜底。所以实际项目中不能只靠 F 表达式把问题解决完,数据完整性必须靠 unique_together 的数据库约束来保证。如果唯一约束被触发,Django 会抛出 IntegrityError,需要在事务里捕获这个异常,然后给用户返回「你已经选过这门课」的友好提示。

4.3 权限控制:登录、角色判断与页面入口管理

权限控制是评委最喜欢深入问的模块之一,因为这里能区分「会写业务代码」和「有安全意识」的两种水平。

最基础的层面是登录拦截:未登录用户访问任何功能页面都跳转到登录页,这个在视图函数上加 @login_required 装饰器即可。

第二层是角色判断:

from django.contrib.auth.decorators import login_required from django.http import JsonResponse def teacher_required(view_func): @login_required def wrapper(request, *args, **kwargs): if request.user.role != 'teacher': return JsonResponse({'code': 403, 'msg': '无权限访问'}, status=403) return view_func(request, *args, **kwargs) return wrapper

这个装饰器确保了请求到达视图业务代码之前,角色已经校验通过。注意这里的校验必须在后端做,不能相信前端隐藏按钮或者模板里的 if 判断——前端只是体验优化,任何人都可以直接构造 POST 请求打到后端接口,后端不拦截就等于裸奔。页面入口用模板语法控制,是产品层面的体验设计,后端拦截是安全防线,两者结合才能通过安全审查。

4.4 成绩录入与统计:教师发布后学生可见的流程设计

成绩模块要设计一个「发布」状态,不发布则学生看不到,这是教务系统里一个容易被忽略的细节。教师录入成绩后只保存 draft 状态,确认无误后再批量发布。

表设计上,成绩表在选课记录上增加 score 字段和 is_published 字段。教师的成绩录入页面按课程显示选课学生列表,逐行输入分数,保存时校验成绩范围 0~100。发布后学生端「成绩查询」页面才能查到对应课程的成绩,没发布的课程显示「成绩尚未公布」。

统计功能放在课程详情里:一门课的最高分、最低分、平均分和及格率。这部分用 Django ORM 的 aggregate 函数,代码量不大,但能给系统增加「数据分析」的亮点,答辩时可以主动展示。

5. 常见问题排查:上线前最容易翻车的五个点

5.1 表单提交 403:CSRF 验证失败的典型症状

现象是页面上能看到课程列表,但点选课提交后直接报错,错误信息显示 CSRF verification failed。

原因是在模板的 form 标签里漏写了{% csrf_token %}。Django 默认开启 CSRF 中间件,所有 POST 请求必须带 token。如果前端用的 AJAX,还需要在请求头里带上 X-CSRFToken。

解决方法是模板表单里加 csrf_token;AJAX 场景在页面加载时读取 cookie 中的 csrftoken,每次请求把它放进 header。遇到 403 时先看请求的 header 里有没有带 token,而不是怀疑中间件坏了。这个问题排错五分钟能解决,但新手往往折腾半小时。

5.2 选课超卖:count+save 组合的并发漏洞

现象是课程容量 50,选课成功后人数超过了 50。

原因是视图里先count()判断小于容量,再save()写入,两个步骤之间的间隙被并发请求趁虚而入。

解决方法是统一用事务 + select_for_update,或者 F 表达式原子更新,同时必须依靠数据库唯一约束兜底,不允许学生重复选课。排查的时候打开两个浏览器窗口无痕模式,同时点提交,能稳定复现。

5.3 时区导致的选课时间判断错误

现象是课程设置了 10:00 开放选课,但 10:00 整页面仍然显示未开放。

原因是 Django 的 TIME_ZONE 默认是 UTC,USE_TZ 为 True 时,写入的时间戳存 UTC 格式,前端显示时没做时区转换,导致本地时间和服务器时间对不上。

解决方法是设置TIME_ZONE = 'Asia/Shanghai',USE_TZ = False。对于毕设级别不涉及多时区用户场景的项目,直接关掉 USE_TZ 是最省心的做法。开课时间字段用 DateTimeField 存本地时间,显示和判断就都不容易出错。

5.4 外键删除导致课程被连带删除

现象是删除一个教师账号,结果他教的课程也全没了。

原因是外键用的是on_delete=models.CASCADE,删除主表记录时自动删除关联表数据。

解决方法是根据业务语义选择合适的删除策略。教师离职时课程应该保留,所以on_delete=models.SET_NULL加null=True更合理;选课记录是课程的学生选课历史,不能随着课程删除而丢失,所以也要避免 CASCADE。在答辩准备阶段把这个点准备好,回答外键策略问题会很有说服力。

5.5 静态文件在部署后全部 404

现象是本地开发一切正常,部署到服务器后 CSS、JS 全加载不出来。

原因是 DEBUG=False 时 Django 不再自动提供静态文件服务,必须靠 collectstatic 收集文件,再由服务器配置静态目录。

解决方法是部署流程固定成三步:python manage.py collectstatic收集文件到 STATIC_ROOT,然后在 Nginx 里配置 location /static/ 指向这个目录,最后确认 STATIC_URL 路径与 Nginx 配置一致。开发环境配置一个 local 静态目录就够了,生产环境不走 Django 的静态文件服务而是让 Nginx 接管。踩过这个坑之后就明白了,本地开发正常不等于项目能上线,部署验证必须走完整流程才能放心。

6. 验收与答辩:从功能演示到用几张截图讲清设计

6.1 功能验收清单与演示路线

验收阶段先自己按照一条完整的演示路线走一遍:管理员登录 → 添加学生账户和教师账户 → 创建课程并开放选课 → 学生选课成功 → 查看课表 → 教师录入成绩并发布 → 学生查看成绩。

这条路线覆盖了系统内所有核心角色和业务动作,答辩时按这个顺序演示,过程流畅且逻辑连贯。建议提前准备三张截图:一张是课程列表页显示余量、一张是选课成功后的个人课表、一张是成绩统计的聚合结果。截图放在 PPT 里,答辩现场如果网络或环境出问题,还能拿截图说。

每种角色至少准备一个极端用例。学生端最典型的提问是「如果选课时课程满了怎么办」,教师端是「成绩发布后发现录错了能不能改」,管理员端是「怎么把一门课从可选变成不可选」。这些操作都要能流畅展示,说明你真正理解系统的边界和异常处理逻辑。

6.2 答辩陈述的因果链条:从需求到实现的逻辑闭环

答辩时最容易翻车的情况,是你坐在台上按代码文件挨个讲功能,评委听着听着就失去兴趣了。有说服力的陈述方式是构建「需求→设计→实现→验证」的因果链。

比如讲到选课模块,你要回答的是:

  • 问题是什么:学生同时抢最后一门课的余量,如何不超卖
  • 方案是什么:事务加行锁保证写操作串行
  • 验证是什么:两个浏览器同时提交,最终人数不超过容量

比你优秀 | 他说:『我就是把每一步都验证过了,才敢把代码交上去。』

厉害。这个习惯我印象太深了,从那以后我每次做类似的毕业设计或课程设计项目,都会强制自己走一遍「自己当验收员」的测试流程,从新注册一个学生账号开始,模拟最完整的业务路径,不带任何侥幸心理。这套学生教务选课系统也一样,跑通主流程后再跑异常分支,全部通过才交付。这个习惯虽然简单,但真的让很多不必要的答辩惨案没有发生过。希望帮到你,也祝你项目顺利通过。

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

RELATED READING

延伸阅读

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