基于Django开发教务选课系统:从模型设计到并发控制实战
2026/9/8 22:34:03 网站建设 项目流程

简介:基于Python与Django框架开发的学生教务选课系统毕业设计源码案例,面向计算机相关专业学生、毕业设计开发者及初学Web开发的读者。系统覆盖学生、教师、管理员三类角色,包含身份验证与权限控制、课程信息增删改查、学生选课与已选课程查看、选课人数统计及满意度调查等核心模块,完整演示了从数据模型、视图控制到模板渲染的Django MTV开发流程。压缩包共2000个文件,主要以js、html、css等前端资源为主,辅以少量py后端源码、json数据及配置文档,整体约5.66MB,目录结构便于按功能模块检索学习。目前已有182人学习下载,适合作为课程设计参考或毕业设计基础框架,可直接运行调试并二次扩展。

1. 项目整体设计与思路拆解

1.1 为什么选 Django 做教务选课系统

做学生教务选课系统,技术选型上我见过不少卡在第一步的人。有人一上来就抱个 Spring Boot 的大腿,结果环境配了三天,工程还没跑起来;也有人想用 Flask 图轻量,结果写到选课并发那块,发现自己得手动处理数据库事务和迁移,心态直接崩了。我的建议是:如果你用 Python,想快速做出一个结构完整、代码能答辩、还能拿得出手演示的教务系统,Django 是最稳妥的选择,没有之一。

Django 自带的东西实在太多了。Admin 后台可以直接管理课程和教师数据,ORM 让你不用写一行 SQL 就能建表、查询,自带的认证系统(Auth)天然支持用户登录、权限校验,还有模板引擎、表单处理、分页组件这些,几乎是把你毕业设计需要的地基全给你打好了。你只需要把精力放在业务逻辑上——什么叫选课、怎么选课、选课之后怎么办——而不是纠结怎么连接数据库、怎么写登录状态。

还有一点是 Django 的开发模式和教务系统这类业务特别匹配。教务系统的本质,就是围绕"课程、学生、教师、选课记录、成绩"这几张表做增删改查,再加上一点角色权限和业务流程控制。Django 的 MTV 架构(Model-Template-View)恰好跟这个模型一一对应:Model 管数据表,View 写业务逻辑,Template 做页面展示。你甚至可以不用动几行 JavaScript,就能交付一个完整可运行的系统。这在毕设阶段的投入产出比上,绝对划算。

1.2 功能模块如何拆解

我设计这套系统的第一件事,不是写代码,而是把所有页面画在纸上,然后顺着用户角色把功能过一遍。教务选课系统至少要包含三种角色:学生、教师、管理员,每种角色干的事情完全不一样。

  • 学生端:查看可选的课程列表、按学分/时间/教师筛选、提交选课、退课、查看已经选的课、查看最终成绩。
  • 教师端:查看自己教的课程,维护课程信息(比如容量、上课时间),录入学生成绩,查看选课学生名单。
  • 管理员端:管理学生和教师账号,维护学期数据,审核课程开设与下架,处理学生选课异常和冲突记录。

不要一开始就想着把功能做得多复杂,什么课表冲突检测、学分绩点自动计算、消息通知,这些都是加分项。核心闭环是:课程发布 → 学生选课 → 教师录分 → 学生查分。你把这四个环节跑通了,这套系统拿去答辩已经没有任何问题。功能拆解的方法我建议倒着来,先想清楚最后一个页面是什么样子,再反推需要哪些表和字段,这样数据库设计不容易乱。

1.3 这套系统的技术亮点在哪里

拿到手之后你会惊讶,这看起来是个平平无奇的选课系统,但源码里其实藏了不少适合在答辩时讲的亮点

一是用户认证和角色权限的处理。Django 自带的 User 表可以扩展,通过 OneToOne 关联 StudentProfile 和 TeacherProfile,再加上 Django 的装饰器@login_required@user_passes_test实现功能级别的控制。答辩时老师一问"权限怎么做的",你可以直接说出这一整套链路。

二是选课并发控制。选课最怕的就是两个学生同时抢最后一门课,Django 的 ORM 里用select_for_update()配合事务,可以保证课程余量不会扣成负数。这个点你在论文的需求分析和系统设计里写出来,档次立刻不一样。

三是对 Python 生态的自由调用。Django 只是个框架,你可以在视图函数里随随便便调用其他 Python 库,比如用openpyxl导出学生名单 Excel,用matplotlib生成选课人数统计图,用pandas分析热门课程数据。这些你只要写了,都是加分项。

2. 核心细节解析与实操要点

2.1 数据模型设计得好,后面编程不烦恼

我见过太多人一上来就写 models.py,结果写到一半发现字段对不上,又回去改数据库,越改越乱。先静下心来把 ER 图画明白,再去写代码,比你想象中省时间。

核心模型,至少要包含下面这几张表:

  • Course(课程表):课程代码、名称、学分、授课教师(ForeignKey 关联教师表)、上课时间、上课地点、容量、已选人数、课程状态(可选的 / 不可选的)。
  • Selection(选课记录表):学生(ForeignKey)、课程(ForeignKey)、选课时间、退课状态、成绩(可以是空,等老师录入)。
  • StudentProfile(学生扩展表):OneToOne 关联 User,存学号、班级、入学年份等。
  • TeacherProfile(教师扩展表):职称、所属学院等。

设计的时候注意两点。第一,不用把冗余字段当成洪水猛兽,比如已选人数这个字段,你可以在每次选课成功后F('selected_count') + 1原子更新,查列表页时省掉一次 count() 查询,体验好很多。第二,外键关联时记得设置on_delete=models.CASCADE还是SET_NULL,这影响删数据时会不会报错。我的习惯是业务数据用CASCADE,扩展资料用SET_NULL(关联合法性问题不大,但避免用户删了导致其他记录一起没了)。

2.2 选课这个核心业务,到底应该怎么设计逻辑

选课是整个系统里最容易出 bug 的地方,因为它的业务规则往往是叠加的——这门课是否可选、是否冲突、容量是否满了、是否选过了——任何一个条件不满足都不能让选课成功。

我把选课的完整校验链路给你列出来,你写业务代码的时候照着这个顺序判断就行:

  1. 用户必须登录并且角色是学生。
  2. 课程状态必须是"开放选课"。
  3. 这门课不是自己之前已经选过的(查一下 Selection 表)。
  4. 课程已选人数 < 课程容量。
  5. 上课时间不能和其他已选课程冲突。
  6. 以上全部通过,才允许创建选课记录,并把课程已选人数加 1。

这里第 5 条比较隐蔽。我的方案是把上课时间存成一个字符串约定,比如"周一 3-4 节",存成 JSON 数组格式["1-3-4", "3-7-8"],然后拉出学生已有课程的上课时间做交集判断。你只需要一个列表解析就能完成冲突检测,代码简单又直观。

2.3 权限控制和页面访问,如何做到角色分明

Django 自带 Auth 系统,但默认的 User 表没有角色字段,所以很多人喜欢给 User 表加一个role字段来区分。这是最简单直接的办法,但不够优雅。我的建议是保留 Django 的is_staffis_superuser标志位,再通过UserProfile中的user_type字段做区分。

  • 管理员直接走 Django Admin 后台和自定义管理端。
  • 教师和学生访问的是两个不同的页面入口,各自继承不同的基类模板,在 URLconf 里做分组限制。
  • 视图里用@login_required保证必须登录,再用@user_passes_test(lambda u: u.profile.user_type == 'student')做角色校验。

实际使用的时候你会发现,把页面基类模板分开(学生端是一个 base_student.html,教师端是 base_teacher.html),比在同一个模板里用 if 判断角色再渲染不同内容舒服得多。代码可读性高,也能防止学生 URL 访问到教师路径,直接把列表数据泄露出去。

3. 实操过程与核心环节实现

3.1 从零初始化项目:环境、依赖与结构

我默认你已经在电脑上装好了 Python 3.8 以上版本,且能用python -m pip正常装包。国内网络环境下,建议先把 pip 源换成清华或者阿里云的镜像源,不然装 Django 时下载速度会让人崩溃。

# 创建并激活虚拟环境(Windows 用 venv,Linux/Mac 也适用) python -m venv venv venv\Scripts\activate # Windows 激活 # source venv/bin/activate # Linux/Mac 激活 # 安装 Django 和第三方依赖 pip install django==4.2 pip install pandas openpyxl # 创建项目和核心应用 django-admin startproject select_system . python manage.py startapp course python manage.py startapp users python manage.py startapp selection

这里的包版本要注意,Django 4.2 是目前最稳的 LTS 版本,适合生产式的慢慢打磨代码。不要追新用 Django 5.x,那些新特性你毕设根本用不上,反而会遇到第三方库不兼容的坑。

项目结构我建议按业务域拆 app,而不是把所有的代码怼在一个 app 里。这样并不是为了炫技,而是后期调试和写论文时按模块讲逻辑更方便。名字不重要,但是course负责课程管理,selection负责选课业务,users负责用户扩展,这个边界要清楚。

3.2 数据表创建与模型迁移

创建好 app 之后,我把核心模型写在course/models.py里。下面是Course模型的核心代码,重点是字段定义和选择状态的写法:

from django.db import models from django.conf import settings class Course(models.Model): COURSE_STATUS = [ ('open', '开放选课'), ('closed', '已关闭'), ('finished', '已结课'), ] code = models.CharField('课程代码', max_length=20, unique=True) name = models.CharField('课程名称', max_length=100) credit = models.DecimalField('学分', max_digits=2, decimal_places=1) teacher = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='teaching_courses', verbose_name='授课教师' ) schedule = models.JSONField('上课时间', default=list) location = models.CharField('上课地点', max_length=100, blank=True) capacity = models.PositiveIntegerField('课程容量', default=30) selected_count = models.PositiveIntegerField('已选人数', default=0) status = models.CharField('课程状态', max_length=10, choices=COURSE_STATUS, default='open') class Meta: verbose_name = '课程' verbose_name_plural = verbose_name ordering = ['code'] def __str__(self): return f'{self.code} {self.name}' @property def is_full(self): return self.selected_count >= self.capacity

设计上要注意,我没有把 capacity 和 selected_count 合并成一个字段。因为容量是管理员设置的固定值,已选人数是随选课行为实时变化的,分开存,查询的时候不必每次做聚合计算,快很多。

然后在selection/models.py里写选课记录表:

from django.conf import settings from django.db import models from course.models import Course class Selection(models.Model): student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='selections') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='selection_records') selected_at = models.DateTimeField('选课时间', auto_now_add=True) is_cancelled = models.BooleanField('是否已退课', default=False) score = models.DecimalField('成绩', max_digits=4, decimal_places=1, null=True, blank=True) class Meta: unique_together = ('student', 'course') verbose_name = '选课记录' verbose_name_plural = verbose_name def __str__(self): return f'{self.student} - {self.course}'

然后执行迁移命令:

python manage.py makemigrations python manage.py migrate python manage.py createsuperuser

这里有个我踩过很多次的坑:架好模型以后,不要在还没建 superuser 之前就开始乱加数据。先在course/admin.py里把课程表注册到 Admin 后台,然后用 Admin 加几门测试课程,后面调试选课逻辑会方便很多。好在 Django 的 Admin 是自动生成后台,这种"白嫖"的开发效率在别的框架里想都不敢想。

3.3 视图与模板:把业务串起来

视图是我花最多时间调试的地方,因为选课逻辑要考虑的边界条件太多了。我建议你在视图函数里不要写大段的嵌套 if,而是用一个事务函数把业务判断都折叠成一个列表推导式,一行一个校验。这样既优雅,也方便在答辩时讲清楚思路。

下面是用transaction.atomic()实现安全选课的核心代码:

from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.contrib.auth.decorators import login_required from django.contrib import messages from course.models import Course from selection.models import Selection @login_required def select_course(request, course_id): course = get_object_or_404(Course, pk=course_id) if request.method == 'POST': with transaction.atomic(): # 锁住当前课程行,防止并发把容量扣成负数 course = Course.objects.select_for_update().get(pk=course_id) # 1. 课程必须开放选课 if course.status != 'open': messages.error(request, '该课程当前不可选') return redirect('course_list') # 2. 不能重复选课 if Selection.objects.filter(student=request.user, course=course, is_cancelled=False).exists(): messages.error(request, '你已经选过这门课程了') return redirect('course_list') # 3. 检查容量 if course.is_full: messages.error(request, '这门课已经选满了') return redirect('course_list') # 4. 时间冲突检测(schedule 为 JSON 列表,如 ["1-3-4", "3-7-8"]) existing_courses = Course.objects.filter( selection_records__student=request.user, selection_records__is_cancelled=False ) new_schedule = course.schedule for ec in existing_courses: if set(ec.schedule) & set(new_schedule): messages.error(request, f'与课程 {ec.name} 上课时间冲突') return redirect('course_list') # 5. 全部校验通过,创建选课记录并更新人数 Selection.objects.create(student=request.user, course=course) course.selected_count += 1 course.save(update_fields=['selected_count']) messages.success(request, f'选课成功:{course.name}') return redirect('my_courses') return render(request, 'course/course_detail.html', {'course': course})

有几个地方我想专门提一下。

加锁是并发控制的关键。如果没有select_for_update(),两个学生同时点选课,可能两个请求都读到selected_count=29,容量为 30,都放行,最后导致选课人数变成 31,超出容量。加了行锁之后,第二个请求会等第一个事务提交后,再读取最新值,就能正确判断容量是否满了。这段代码你在论文里写上,老师一眼就知道你认真考虑了并发问题。

时间冲突检测我这里用了集合取交集set(ec.schedule) & set(new_schedule)如果非空,说明有重复的时间段,就直接拦截。这个方案的前提是你存储 schedule 时格式保持一致,比如统一是"星期几-第几节-第几节"的三段式字符串。你在适配别的项目时,只要保证存储格式统一,这套思路就能直接复用。

3.4 前端页面的组织:如何做得像模像样

我强烈建议你在页面上引入一个现成的前端模板,而不是自己从零写 CSS。像 Bootstrap 官网上就有很多免费的 Admin 模板,比如 SB Admin、AdminLTE(国内访问可能有些慢,可以直接去 gitee 上找搬运仓库)。把静态文件放到static/目录后,在settings.py里配置 STATIC_URL 和 STATICFILES_DIRS,模板中引入即可。

STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]

大概用户看到的页面是这么几类,你照着写准没错:

  • 课程列表页:一张大表格,显示课程代码、名称、学分、教师、容量、已选人数、上课时间,后端用 Django 内置分页器Paginator做分页,每页 10 条,需要引入django.core.paginator包。
  • 课程详情页:显示课程介绍,按钮是"立即选课",提交后 POST 到选课视图。
  • 我的课表页:把你已选的课程按星期横轴、节次纵轴渲染成一个课表,周一到周五、第 1-10 节,这能让答辩老师眼前一亮。
  • 成绩查询页:显示每个学期选过的课和对应成绩。

模板引擎的 for 循环和 if 判断很好用,像{% for course in page_obj %} {% endfor %}这种写法,你多看几遍官方文档就会了。初学容易踩的坑是模板里忘记写{% csrf_token %},导致所有 POST 表单提交都会 403 报错。这个点我在下面"常见问题"里还会再说一次。

4. 常见问题与排查技巧实录

4.1 Django 版本和数据库的那点事

做这个项目时,身边同学问得最多的就是数据库选择问题。Django 默认用的是 SQLite,对毕设来说足够用,没有任何配置成本。如果你的数据量不大,几千条以内,SQLite 完全扛得住。答辩演示时也不用额外装 MySQL,少一步环境配置就少一个翻车的可能。

但如果你想在论文里写"系统支持防并发选课",用的是 MySQL,最好在settings.py里把数据库配置改掉:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'select_system', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', } }

改完别忘了再执行一次makemigrationsmigrate,以及安装pymysql并在__init__.py中注册pymysql.install_as_MySQLdb()。这一步忘掉的后果就是启动时直接报ModuleNotFoundError: No module named 'MySQLdb',我第一次遇到时愣是花了 20 分钟才反应过来。

4.2 表单提交 403 报错,模板没有带 CSRF Token

这个问题的本质是 Django 默认要求所有 POST 请求都带上 CSRF Token 做安全校验。模板里的 form 如果在中间忘记加{% csrf_token %},一提交就 403 Forbidden。

我自己的排查习惯是:先看模板里是否写了{% csrf_token %},再确认settings.pyMIDDLEWARE是否有CsrfViewMiddleware。正常情况下不要移除这个中间件,不然你的系统在安全性和毕设评分上都会打折扣。

4.3 迁移记录混乱的"救急大法":清库重来

开发过程中,改 models.py 十次很正常。有时候你会发现无论怎么makemigrations,数据库里的表就是跟模型对不上。或者migrate时提示一下No migrations to apply,但明明表不存在。

遇到这种情况最省事的操作是:删掉每个 app 下 migrations 目录里除了__init__.py之外的所有文件,然后删掉数据库文件(db.sqlite3 直接删),重新执行makemigrationsmigrate反正开发阶段没有真实业务数据,这种"重开"成本极低,别浪费时间在修复老迁移上。如果是 MySQL,则要手动 DROP 掉相关表再重新 migrate。

4.4 页面样式"裸奔",静态文件加载不出来

做完项目检查时最尴尬的,就是代码逻辑全对,结果页面纯 HTML 无样式,像刚出土的网页。Django 默认在生产模式(DEBUG=False)下不帮你托管静态文件,所以部署前你会觉得一切正常,runserver一跑样式全在。等你部署到宝塔或云服务器上,DEBUG 一关,所有 CSS 全丢了。

解决方法是老老实实收集静态文件:

python manage.py collectstatic

然后在settings.py里配置STATIC_ROOT。部署到 Nginx 或 Apache 时,把STATIC_ROOT目录指给 Web 服务器托管即可。你在用宝塔部署时,在网站设置里把静态目录指向这个路径,样式就出来了。这个细节,80% 用 Django 部署项目的人都会遇到。

4.5 选课并发测试怎么演示

最后再分享一个很涨面子的小技巧。答辩时想演示并发选课,不用写什么高深代码,你是可以自己模拟的:开两个浏览器窗口(或者用浏览器的无痕模式),同时用两个不同的学生账号,对准同一门只剩 1 个名额的课程,连续点击选课按钮。你会发现最终只有一个学生能选上,另一个提示"课程已满"。能稳定复现这个结果,说明系统并发控制有效,这个演示比背一百行代码都让人信服。

5. 部署上线:从本地到服务器的一步之遥

5.1 用宝塔部署 Django 项目

很多人的毕设项目做到本地跑起来就停了,其实"部署上线"是简历上和答辩中非常加分的经历。用宝塔面板部署 Django 不复杂,流程也比较成熟。

先说整体思路:Django 是一个 WSGI 应用,需要有个东西跑 Python 代码,然后 Nginx 反向代理把 HTTP 请求转发给 Django 的进程。

第一步,把项目传到服务器,然后在服务器上创建 Python 虚拟环境,安装requirements.txt里的依赖:

python3 -m venv venv source venv/bin/activate pip install -r requirements.txt

第二步,在宝塔软件商店安装 Python 项目管理器(或者直接用 gunicorn/uwsgi),配置启动命令。我最常用的组合是gunicornbind 127.0.0.1:8000

gunicorn select_system.wsgi:application --bind 127.0.0.1:8000 --workers 2

第三步,在宝塔网站里新建一个静态站点,配置反向代理到127.0.0.1:8000,再把STATIC_ROOT目录作为静态文件目录指定给站点。完成这三步后,你的系统就从本地跑到了公网服务器上,手机在流量状态下也能访问,等于一个完整的云上软件。

5.2 环境变量与密钥管理

有一点必须提醒:不要把 Django 的SECRET_KEY和数据库密码明文写在settings.py里,然后把自己的项目打包扔到公开仓库。虽然这基本是每个新手都会犯的错,但你不是为了应对毕业才写纯代码,你的系统是需要讲究安全的。解决办法很简单,把项目里面的关键配置抽到一个.env文件,开发时用python-dotenv读取:

import os from dotenv import load_dotenv load_dotenv() SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'

然后写一个.gitignore,把.envdb.sqlite3venv/全部忽略掉。这样不管你是部署到服务器还是在 GitHub 上开源,都不会把敏感的密钥信息暴露出去。这套习惯在校招面试时问到"你对安全有没有概念",也可以自然地讲出来,比背面试题强多了。

5.3 DEBUG=False 之后页面 500 错怎么排查

部署后经常出现的一个尴尬情况是:DEBUG=False后再访问某些页面直接 500,但本地开发时一切正常。原因大多是静态文件挂载失败、ALLOWED_HOSTS 没配好,或代码里某些不该在线上执行的操作被触发。

我的排查步骤是:

  1. 先看 Nginx 错误日志,路径在宝塔面板的网站日志里,常见的是404 Not Found或者端口连接失败。
  2. 打开 Django 的日志,如果没配置日志文件,就先在settings.py里临时把DEBUG改回 True,看看报错页面(注意别在公网上手贱放着不关)。
  3. 确认ALLOWED_HOSTS配置了你的服务器 IP 或域名,比如ALLOWED_HOSTS = ['your-server-ip'],不配的话 Django 直接拒绝请求。

部署这一步虽然有点折磨人,但说白了就是把开发和生产的差异一点点补平。多部署几次,你就知道为什么网上招聘天天要求"熟悉 Linux 和服务器部署"。不踩过这些坑,永远不算真正掌握了 Django 开发。

6. 经验漫谈:我怎么看待这套系统的价值和边界

说实话,学生教务选课系统算是 Python/Django 方向里最经典的毕业设计题之一,但这不代表它是"烂大街"的题。正因为它经典,网上能找到大量资料和学习路径,反而降低了上手门槛,让你有精力去打磨自己的业务逻辑和交互设计。我更建议你在拿到这套源码后,无论如何自己重新打一遍核心的选课视图和模型代码,然后在此基础上做两件事:一是把课表渲染成可视化组件,二是把选课数据用图表展示出来。这两点做好了,你的毕设就已经开始从"会跑"走向"能用"。

最后我想说,写代码只是毕设的一半,另一半是能不能把思路讲清楚。建议你在答辩前一定要用自己的话,把每个核心模块的"为什么这么写"顺一遍。比如为什么选课要加事务锁,为什么用户角色要拆表扩展,为什么把时间冲突检测放在最后一步。这套"思维体操"练习下来,你会发现答辩不仅不紧张,甚至还有点享受。

这个项目后续可以扩展的方向其实很多——移动端 API 接口、成绩统计分析、消息通知系统、甚至对接校园一卡通,只要你掌握了这套 Django 的全栈流程,换一层皮就是另一个系统。这也是我想传达给你们的核心观点:技术栈本身不稀奇,稀奇的是你能不能在正确的地方做出合理的选择。这大概才是做这个项目,最大的收获。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询