做学生选课管理系统这个选题,我算是见过太多版本了:有用Excel硬撑的,有用PHP写完了交差的,也有前后端分离搞到一半做不下去的。今天想拆解的这套基于Python + Django的学生选课管理系统,属于“源码+数据库+文档”三件套齐全的项目,核心逻辑放在Django后端,前端用模板渲染,后台依赖Django Admin,整体非常适合拿来当课程设计、毕业设计底座,也适合想系统性走一遍Web开发流程的新手。
这套系统解决的是高校选课场景里最实际的几个痛点:学生不知道有什么课可选、选了课之后和已有课程时间冲突、热门课被抢光后全靠手动补退选、教务老师统计选课人数费时费力。把这些痛点落到Django项目里,就会牵扯出自定义用户模型、中间表设计、事务与并发控制、Admin后台定制这些绕不开的技术点。下面我把整个项目的设计思路、数据库模型、核心功能实现、部署跑通方法以及常见坑位一次讲透。
1. 学生选课管理系统到底在管理什么
1.1 业务角色与核心场景
先别急着看代码,做管理系统第一件事是盘角色和场景。选课系统里至少有三类角色:学生、教师、教务管理员。学生要能浏览课程、筛选课程、选课、退课、查看自己的课表;教师要能查看自己授课课程的学生名单,部分版本还会加成绩录入功能;教务管理员则是最高权限角色,负责维护课程信息、学期设置、教师和学生的基础数据,以及查看全校选课统计。
这三个角色对应到系统里就是三种不同的权限边界。学生不应该能修改课程容量,教师不应该能替学生选课,管理员也不应该需要走学生选课的完整流程。很多Django初学者喜欢把权限判断写散在各个视图里,结果改一个页面就要动一堆代码。更稳妥的方式是先用Django自带User模型区分角色,再通过装饰器或中间件统一控制访问入口。
1.2 功能需求清单:选课系统不只是增删改查
从业务角度拆解,一个合格的学生选课管理系统至少包含四类核心需求。
第一类是课程基础数据管理。课程编号、课程名称、学分、授课教师、上课时间、上课地点、课程容量、适用年级/专业,这些字段决定了学生搜索课程时的筛选维度和展示信息。第二类是选课规则控制。同一学生不能重复选择同一门课,两门课不能在同一时间段冲突,已选人数不能超过课程容量。第三类是数据统计与展示。学生端要显示“已选学分”和“本学期课表”,教师端要能按课程导出学生名单,教务端要能看到每门课的热门程度。第四类是权限隔离。这是前面提到的角色边界,必须保证每个页面只能被合法角色访问。
1.3 为什么这类业务特别适合Django来落地
选课系统属于典型的表格密集、规则清晰、页面数量有限的Web应用。Django在这个场景下有几个天然优势:ORM直接把数据模型映射成数据库表,不用自己写SQL和连接管理;自带Admin后台可以秒变教务管理系统;内置的用户认证体系解决了登录、登出、会话管理;模板引擎加上Bootstrap就能做出够用的前端页面。
我自己对比过Flask方案,Flask虽然轻量,但所有模块都要手动组装,做完用户认证和Admin后台约等于重写半个Django。Spring Boot在企业级项目里很强,但Java环境和技术门槛对课程设计场景来说太重了。所以这套项目选用Django,不是因为它最潮,而是因为它和业务形态的匹配度最高。
2. 技术选型与项目结构设计
2.1 Django版本与依赖选择
这套系统在开发环境里我建议用Python 3.8以上搭配Django 3.2或4.x。Django 3.2属于LTS长期支持版本,网上资料多,遇到的坑都能搜到解决方案;Django 4.x在新项目里也没问题,但要注意第三方插件的兼容性。如果项目自带requirements.txt,直接按那个安装最省事。
依赖层面核心就是Django本身,其余根据需求加。比如引入django-crispy-forms可以把表单样式做得规整,引入django-filter可以让课程筛选代码更简洁。但如果你只想要一套不折腾的课程设计,我建议依赖越少越好,纯Django自带模块完全能实现全部功能。
2.2 数据库:开发用SQLite,部署再换MySQL
很多拿到了“源码+数据库”的同学,拿到手先找数据库文件在哪。这个项目通常会附带一个SQLite文件,也就是db.sqlite3,它的好处是零配置、单文件、随项目走,非常适合演示和交作业。SQLite对学生选课这种几千条数据量的小系统完全够用,不需要单独装数据库服务。
但如果你要部署到服务器上,或者后续接着开发多人并发版本,SQLite的写锁问题就会暴露出来。选课高峰期多个学生同时提交选课,SQLite会串行处理事务,性能下降明显。这时候建议切换到MySQL。切换方式在Django里很简单,改settings.py里的DATABASES配置,安装mysqlclient,执行迁移即可。核心代码不需要变化,因为ORM层把数据库细节挡住了一部分。
| 对比项 | SQLite | MySQL |
|---|---|---|
| 适用阶段 | 开发、演示、课程设计 | 服务器部署、并发较高场景 |
| 配置成本 | 零配置 | 需要安装数据库服务 |
| 并发能力 | 写操作串行,易锁库 | 支持并发事务 |
| 数据文件 | 单文件db.sqlite3 | 独立数据目录 |
| 迁移难度 | 低 | 需要配置连接参数 |
2.3 前端模板:服务端渲染为什么够用
Django项目里最传统的做法是MTV架构,视图函数返回渲染好的HTML页面。这个方案对选课系统来说完全够用,因为页面交互并不复杂:课程列表、详情页、我的课表、登录注册,最多加上一些Ajax做局部刷新。不需要Vue/React,也不需要REST API层,把这个项目的技术栈复杂度降到了最低。
好处是明显的:你只需要掌握Django模板语法和少量JavaScript,就能把页面做出来。问题排查时也不存在跨域、Token鉴权这类前后端分离才有的麻烦。如果后续你要把它升级成前后端分离项目,Django也能通过django-rest-framework另起一套接口层,但那是另外一个工作量了,不建议在课程设计阶段强行上。
3. 数据库设计与数据模型细节
3.1 核心模型:五个表撑起整个系统
选课系统的基础数据结构其实不复杂,我习惯拆成五个模型:用户模型、课程模型、学生信息模型、教师信息模型、选课记录模型。用户模型直接继承Django的AbstractUser,新增role字段区分角色;学生和教师信息可以做成一对一扩展表,也可以把班级、学院、工号这些字段直接塞进用户模型。考虑到课程设计阶段要控制复杂度,我更推荐直接塞进用户模型,减少联表查询。
课程模型是核心,包含course_code课程编号、name名称、credit学分、teacher外键指向教师、week_day星期几上课、start_period开始节次、end_period结束节次、classroom上课地点、capacity容量、semester学期字段。选课记录模型则记录哪个学生选了哪门课、选课时间、课程状态,后期有需要还可以加成绩字段。
3.2 选课记录表:为什么不能只用ManyToManyField
很多初学者第一个版本会写成Course模型里加一个students = ManyToManyField(User),Django会自动生成中间表,这样确实能实现“学生和课程多对多”的关系。但问题在于,这种自动中间表只记录外键关系,没法存“选课时间”“状态”“成绩”。如果学生退课了,记录直接删除,历史数据没了;如果教务管理员想看某个时间段的选课日志,完全做不到。
所以正确做法是显式定义Selection模型,里面放student外键、course外键、selected_at选课时间、status状态、score成绩。然后在Course里用through参数指定这个中间模型:
class Course(models.Model): students = models.ManyToManyField( settings.AUTH_USER_MODEL, through='Selection', related_name='courses' )这样既保留了ManyToMany的查询语法,又能给中间表扩展业务字段。选课时创建Selection记录,代码比直接操作ManyToMany更清晰。
3.3 防止重复选课的数据库约束
同一学生同一门课程只能选一次,这个约束必须在数据库层面体现,不能只靠视图逻辑判断。数据库唯一约束是最后的防线,即使代码出了并发问题,数据库也会拒绝重复数据。在Selection模型中写:
class Meta: constraints = [ models.UniqueConstraint( fields=['student', 'course'], name='unique_student_course' ) ]加了这条约束之后,如果程序尝试插入重复选课记录,Django会抛出IntegrityError。视图逻辑里也要先做一次友好判断,返回“你已经选过这门课”的提示,而不是让用户看到500错误页面。
3.4 课程时间冲突检测的字段设计
课程冲突检测是选课系统里最容易设计错的地方。很多人只存储一个“上课时间”字符串,比如“周一 3-4节”,然后在前端用字符串比较,这样做既没法做规则判断,也无法应对跨周次排课。
我推荐把时间拆成三个字段:week_day存星期几,start_period存开始节次,end_period存结束节次。检测冲突时,查询学生已选的所有课程,看是否有同一week_day且节次区间有交集的课程。区间重叠的判断条件是:
new_start <= existing_end and new_end >= existing_start这个判断逻辑写在视图层,数据模型不需要用数据库约束来做区间重叠限制,因为SQLite和MySQL默认都不支持这种业务级规则。把逻辑放在事务里执行,配合课程行锁,就能在绝大多数情况下保证正确性。
4. 核心功能实现:从登录到选课全链路
4.1 自定义用户模型与登录注册
如果你在项目一开始就用自定义用户模型,后面会省下大量麻烦。继承AbstractUser后追加role、student_id、name、department这些字段。注意AUTH_USER_MODEL的设置要在第一次migrate之前配置好,否则中途切换用户模型会出现外键关联错误。
登录逻辑可以直接用Django自带的login和authenticate,注册逻辑就是创建一个用户并设置密码。给不同角色写不同的登录后跳转:学生跳到课程列表页,教师跳到授课课程页,管理员跳到Django Admin后台。有基础的还可以加login_required装饰器保护所有需要登录的视图。
4.2 课程列表与综合筛选
课程列表是学生最常看到的页面,需要支持按课程名称、课程编号、授课教师、学分等条件筛选。Django的Q对象非常适合这种多条件叠加查询:
def course_list(request): keyword = request.GET.get('keyword', '') credit = request.GET.get('credit', '') courses = Course.objects.filter(semester='2025-2026-1') if keyword: courses = courses.filter( Q(name__icontains=keyword) | Q(course_code__icontains=keyword) | Q(teacher__name__icontains=keyword) ) if credit: courses = courses.filter(credit=credit) paginator = Paginator(courses, 10) page_obj = paginator.get_page(request.GET.get('page')) return render(request, 'course_list.html', {'page_obj': page_obj})筛选参数从URL的query string里读取,表单提交后返回到同一个页面,模板里回填条件值。分页使用Paginator,10条一页比较合适,当前页超过总页数时get_page不会抛异常,这个细节对用户体验很关键。
4.3 选课事务:容量检查和冲突检测必须放一起
选课是写操作,涉及多条数据变更:插入Selection记录、增加课程的已选人数。这两步必须放在同一个数据库事务里,要么全部成功,要么全部回滚。Django里用transaction.atomic()包裹。
下面是选课视图的核心逻辑:
from django.db import transaction from django.db.models import F @login_required @transaction.atomic def enroll_course(request, course_id): course = Course.objects.select_for_update().get(pk=course_id) student = request.user if Selection.objects.filter(student=student, course=course).exists(): return JsonResponse({'ok': False, 'msg': '你已经选过这门课'}) if course.current_count >= course.capacity: return JsonResponse({'ok': False, 'msg': '课程容量已满'}) # 冲突检测 selected_courses = course.objects.filter(selections__student=student) for c in selected_courses: if has_time_conflict(c, course): return JsonResponse({'ok': False, 'msg': f'与课程{c.name}时间冲突'}) Selection.objects.create(student=student, course=course, status=1) Course.objects.filter(pk=course.pk).update(current_count=F('current_count') + 1) return JsonResponse({'ok': True, 'msg': '选课成功'})关键在于select_for_update():它会对选中course这条记录加数据库行锁,事务提交前其他线程无法更新同一行。这样两个学生同时抢同一门最后剩余名额时,第二个事务必须等第一个提交后才能读取最新current_count,从根源上避免了超卖。F('current_count') + 1则保证自增操作是原子SQL更新,而不是“先查出来再写回去”两步走。
退课逻辑恰好相反:删除Selection记录,同时把课程当前人数减一。同样要放在事务里,并且用F表达式保证数量准确。
5. 管理后台、初始数据与文档组织
5.1 定制Django Admin,让教务管理开箱即用
Django自带Admin是这套系统的一大加分项。你只需要在admin.py里注册模型并配置展示字段,就能得到一个可用的后台管理界面。教务管理员登录/admin/后可以维护课程、查看选课名单、管理用户,几乎不需要额外写页面。
我对常用配置做过整理:
@admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ('course_code', 'name', 'credit', 'teacher', 'week_day', 'start_period', 'capacity', 'current_count') search_fields = ('course_code', 'name', 'teacher__name') list_filter = ('semester', 'week_day', 'credit')search_fields里可以用双下划线跨表查询,比如teacher__name。list_filter让管理员按学期、星期、学分快速筛选课程。选课记录模型里,建议把selected_at设置为readonly_fields,避免管理员无意中修改选课时间。
5.2 初始数据与演示账号
项目交付时应该带一份可直接演示的初始数据,里面包含若干学生账号、教师账号、课程数据和选课记录。Django里可以用fixture导出和导入数据:
python manage.py dumpdata auth.User course.Course selection.Selection --indent 2 > initial_data.json python manage.py loaddata initial_data.json演示账号通常包含三类角色:管理员账号admin、学生账号student01、教师账号teacher01。密码在文档里统一写清楚,例如都是123456或Admin@123。用真实初始数据来演示,能给别人最直观的印象,比空着一张表强得多。
5.3 文档里必须写清楚的部分
这个项目的卖点之一是“源码+数据库+文档”,文档质量直接影响别人能否把项目跑起来。一份合格的项目文档至少包含六个部分:项目介绍与功能清单、环境依赖与安装步骤、数据库表结构说明、核心功能实现说明、部署手册(含开发服务器和正式服务器两套方案)、常见问题列表。如果系统里有演示账号,也要单独列一个表格写清楚。
我见过太多源码项目,代码没问题但文档只写了一句“运行python manage.py runserver”,这对零基础的人完全不够。所以我在组织文档时会把每一步命令、每一步预期结果、浏览器访问地址都写进去,相当于一份手把手的操作指南。
6. 拿到源码后,怎么把项目跑起来
6.1 环境准备与虚拟环境
先确认本机Python版本,建议3.8到3.11之间。Django 3.2不支持Python 3.12以上的版本,这一点容易踩坑。然后创建并激活虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里只需列出Django和必须的插件。如果项目附带了db.sqlite3数据库文件,直接使用即可;如果没有,就需要执行迁移命令。
6.2 settings.py里的关键配置
不管项目是自带数据库还是从零开始,都需要检查settings.py里的几项配置:
INSTALLED_APPS里是否包含项目自己的app,比如course、users。AUTH_USER_MODEL是否指向自定义用户模型,例如users.User。LANGUAGE_CODE设置为'zh-hans',TIME_ZONE设置为'Asia/Shanghai'。DATABASES配置是否和项目数据库一致。
如果要从SQLite切换到MySQL,数据库配置大致如下:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'course_system', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }切换后执行迁移和数据导入,项目代码不需要改动。
6.3 迁移、导入数据与启动命令
如果项目数据库文件缺失,执行下面这套命令完成初始化:
python manage.py makemigrations python manage.py migrate python manage.py loaddata initial_data.json python manage.py createsuperuser python manage.py runserver访问http://127.0.0.1:8000/就能看到系统首页,访问http://127.0.0.1:8000/admin/进入后台。注意,如果项目自带的db.sqlite3已经包含数据,就不要随便执行migrate,否则可能因为模型和数据库迁移记录不一致而出错。这时候直接runserver是最省事的。
6.4 浏览器访问与联动流程
跑起来后,建议按这个顺序过一遍完整流程:先注册一个学生账号,登录后浏览课程列表;筛选一门可选课程,进入详情页选课;再到“我的课表”里确认是否出现课程;尝试选一门时间冲突的课程,确认系统会给出提示;最后使用管理员账号登录后台,查看选课记录和课程人数统计。这套流程走通,说明系统功能正常。
7. 常见问题与排查技巧实录
7.1 数据库迁移显示No migrations to apply
这个问题的原因通常是项目里已经存在db.sqlite3,并且里面已经记录了迁移历史,而你的模型没有新增变化。如果数据库文件是从别人那边拷来的,直接运行即可;如果数据库是空的新文件,却提示没有迁移,多半是INSTALLED_APPS里没有注册对应的app,Django找不到迁移文件。先检查course等app是否在INSTALLED_APPS中,再检查app目录下是否有migrations文件夹。
7.2 选课并发导致人数超卖
如果选课人数莫名超过容量,核心原因八成是没用事务加行锁。我之前接手过一个项目,选课逻辑是“先查剩余人数小于容量,然后插入选课记录,最后更新人数”,这段代码在高并发下必然出错。两个请求同时查到剩余1个名额,同时通过判断,同时插入记录,最后人数变成2。解决办法就是前面写的select_for_update锁住课程行,配合唯一约束兜底。测试时可以用多个线程同时请求选课接口,观察最终人数是否超过容量。
7.3 中文乱码和时区问题
中文乱码绝大多数出在数据库连接配置上。SQLite一般不会乱码,MySQL则要确认库表使用utf8mb4,数据库连接加上charset='utf8mb4'。时区问题表现为课程时间显示差8小时或者Admin后台时间与本地不一致,这是因为Django默认TIME_ZONE='UTC'。如果只在本地演示,把TIME_ZONE改为'Asia/Shanghai',并且把USE_TZ设为False最简单;如果后续要部署到服务器,建议USE_TZ=True,在前端模板里使用localtime过滤器转换。
7.4 静态文件加载失败
Django Admin页面样式丢失、登录页没有CSS,基本就是静态文件配置问题。开发模式DEBUG=True时,Django能自动处理静态文件;一旦改成DEBUG=False,就需要配置STATIC_ROOT并执行:
python manage.py collectstatic如果项目部署在Nginx后面,还要在Nginx配置里把/static/路径指向收集好的静态文件目录。课程设计阶段通常会忽略这个细节,但答辩演示有意想不到的风险。
7.5 管理员密码忘记与权限验证失败
忘记管理员密码不需要重新创建用户,用Django shell重置即可:
python manage.py shellfrom django.contrib.auth import get_user_model User = get_user_model() u = User.objects.get(username='admin') u.set_password('new_password') u.save()如果登录后访问某些页面提示403或需要更高权限,检查视图里的user.is_authenticated和角色判断逻辑,再看@login_required装饰器有没有加对位置。这类问题多出现在自定义用户模型后,原生Django的is_staff字段没有同步设置,导致管理员无法进入后台。解决办法是在创建管理员时设置is_staff=True和is_superuser=True。
选课管理系统这套项目里,我个人体会最深的一点是:业务规则一定要在数据库和事务层面守住底线。前端按钮可以做成灰色,但后端必须同样做校验;容量判断可以写得很直观,但并发环境下必须用行锁和唯一约束兜底。很多人做课程设计时喜欢堆页面,结果核心选课逻辑反而没做扎实,答辩时被一个问题问倒,非常可惜。如果你打算基于这个项目继续扩展,下一步可以加选课时间窗口、课程评价、教师录入成绩、选课结果导出这些功能,数据模型和现有代码都不会被推翻。先把最小闭环跑通,再一点点往上加东西,才是做这类系统最稳的路线。