资讯动态

基于Django的选课系统设计与实现:权限、并发与数据库实战解析

发布时间:2026/10/21 14:20:53 来源:尧图企业网站定制
每个学期开学那几天搞选课系统崩溃的新闻总能看到教务处、辅导员、学生三方都在盯着同一个系统并发一上来页面转圈圈数据库锁死甚至选完课一查学分不对整个人都麻了。做过这类系统的人都知道学生选课管理的难点从来不是写个增删改查而是权限怎么分、业务规则怎么落地、并发抢课怎么不超选、数据怎么保证一致。这篇博文要拆的就是一个基于 Python Django 的学生选课管理系统完整包含源码、数据库设计和配套文档。我会从项目角度把核心设计思路、表结构、关键代码实现、实操过程、踩坑实录全部讲透适合正在做课程设计、毕业设计或者想系统学习 Django 实战的新手开发者参考。看完你能明白这类系统为什么这么设计也能直接上手复现和改造。1. 项目核心拆解选课系统到底在解决什么问题很多初学者拿到这种题目上来就建一堆表然后往 views 里写代码结果写到一半发现学生能改别人的课、老师能选课、选课冲突了没人管、退课之后容量不恢复……全是设计阶段没想清楚埋下的坑。选课系统的本质是在学生、课程、教师之间维护一种有约束关系的动态状态核心逻辑其实就三件事谁能做什么、什么操作合法、操作之后怎么保持一致。1.1 角色与权限模型设计代码里能跑通是一回事权限模型清晰是另一回事。这个项目的用户模型基于 Django 自带的auth.User扩展添加role字段区分三种角色学生、教师、管理员。学生浏览课程、选课、退课、查看个人课表教师查看自己讲授的课程、维护课程基本信息、查看选课学生名单管理员管理所有用户、课程、学期处理特殊调课需求这里要重点说的是权限控制不能只靠前端按钮隐藏后端每个接口都必须校验身份。Django 在这方面的标准做法是装饰器 Mixin 结合使用。# views.py 权限控制示例 from django.contrib.auth.decorators import login_required, user_passes_test from django.views.generic import ListView from django.contrib.auth.mixins import UserPassesTestMixin def is_student(user): return user.is_authenticated and user.role student login_required user_passes_test(is_student) def course_select(request, course_id): # 只允许学生角色执行 pass class StudentCourseListView(UserPassesTestMixin, ListView): def test_func(self): return is_student(self.request.user)函数视图用装饰器类视图用 Mixin这是 Django 里比较规整的权限写法。很多项目把权限判断写在视图函数内部用 if 包一层不是说不行但接口一多就很容易漏判真正的项目里还是推荐统一处理。1.2 选课场景的业务逻辑约束选课最核心的业务逻辑是约束——不把约束落进代码里系统就是摆设。我归纳了这套系统必须处理的几条硬性规则同一课程学生不能重复选课已选课程与待选课程不能时间冲突选课人数不得超过课程容量退课后容量立刻释放其他人可补选学分数需在学期可选范围内通常默认上限如 28 学分必修课与选修课分类管理这些约束看起来简单但叠加在一起很容易出 bug。举例来说A 学生选了周一 3-4 节的数据库原理又想选同一时间段的计算机网络后端必须在选课请求处理时检测时间冲突而不是只查这个课满了没有。这个逻辑我在后面的代码解析里会说。2. 表结构设计数据模型是系统的骨架这几张表决定成败Django 项目里数据模型就是models.py表的字段设计直接影响后面所有业务逻辑的实现难度。这套系统的表结构是经过几次重构后稳定下来的核心是 7 张表。2.1 数据表清单与字段设计表名模型类核心字段说明用户表User (扩展)username, password, role, student_id, email继承 AbstractUser课程表Coursecourse_code, name, credit, course_type, capacitycourse_type 区分必修/选修教师表Teacheruser, teacher_no, title, departmentOneToOne 关联用户学生表Studentuser, student_no, major, gradeOneToOne 关联用户开课表CourseSchedulecourse, teacher, semester, time_slot, location, selected_count同一课程不同教师/时段视为不同开课记录选课表Enrollmentstudent, schedule, status, select_time唯一约束防重复选课学期表Semestername, start_date, end_date, is_active判断是否处于选课窗口这是一套典型的课程—开课—选课三层模型。为什么需要开课表CourseSchedule因为同一门课数据库原理可能由两个老师分别开在周一和周三。如果只关联 Course 和 Student根本无法表达同一门课不同的时间、地点、教师会出现容量互相覆盖的问题。2.2 关键取舍为什么继承 AbstractUser 而不是单独建 ProfileDjango 官方文档里给出了扩展用户模型的几种方式AbstractUser继承是其中比较折中、也最适合这种教学系统的方法。直接保持默认 User 不动、再建一个 StudentProfile 表关联也行但每次取学生的学号、专业都要跨表查一次视图代码会啰嗦不少。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (admin, 管理员), (teacher, 教师), (student, 学生), ) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) email models.EmailField(uniqueTrue) class Meta: db_table auth_user注意继承 AbstractUser 必须在第一次migrate之前就配置好否则中途切换会引发数据库迁移错乱的麻烦。这一点我在踩坑部分会具体讲。选课表 Enrollment 上要设置联合唯一约束这是防止重复选课的兜底手段。class Enrollment(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE) schedule models.ForeignKey(CourseSchedule, on_deletemodels.CASCADE) status models.CharField(max_length10, defaultselected) select_time models.DateTimeField(auto_now_addTrue) class Meta: db_table course_enrollment unique_together (student, schedule)unique_together在数据库层面生成联合唯一索引就算代码逻辑万一漏写了判断数据库也会直接拒绝重复插入双保险。3. 核心功能模块设计与关键代码解析设计完表结构接下来是业务代码的编写。不要急着写视图先把核心的服务层逻辑想清楚。我在这个项目里把选课的核心操作单独抽出来放在services.py中不直接写在 views 里这样代码好测试逻辑也好复用。3.1 选课流程事务与并发控制是成败关键选课的流程是前端传 schedule_id 过来后端做一系列校验通过后写入 Enrollment 并给 CourseSchedule 的 selected_count 加一。这个过程必须保证原子性否则多人同时抢最后一个名额就会出现超选。from django.db import transaction from django.core.exceptions import ValidationError transaction.atomic def enroll_course(student, schedule_id): schedule CourseSchedule.objects.select_for_update().get(pkschedule_id) # 校验1是否已选过 if Enrollment.objects.filter(studentstudent, scheduleschedule).exists(): raise ValidationError(你已选择该课程) # 校验2课程容量 if schedule.selected_count schedule.capacity: raise ValidationError(课程容量已满) # 校验3时间冲突 time_conflict Enrollment.objects.filter( studentstudent, schedule__time_slotschedule.time_slot, statusselected ).exists() if time_conflict: raise ValidationError(所选课程时间冲突) # 全部通过后写入 Enrollment.objects.create(studentstudent, scheduleschedule) schedule.selected_count schedule.selected_count 1 schedule.save(update_fields[selected_count])这里有一个值得反复强调的点必须用select_for_update()锁定这条开课记录。普通查询在高并发下会读出同一个旧值然后各自拿着旧值去更新把最终人数覆盖掉。select_for_update()会给这行记录加上行锁其他事务必须等当前事务提交后才能读取有效避免超卖。如果不方便在代码里加事务也可以用 Django 的F()表达式做原子更新配合update()的返回值判断是否更新成功from django.db.models import F # 原子容量扣减返回0表示没扣上容量已满 updated CourseSchedule.objects.filter( pkschedule_id, selected_count__ltF(capacity) ).update(selected_countF(selected_count) 1) if updated 0: raise ValidationError(课程容量已满) # 再插入选课记录这种写法在高并发下更稳但要注意先更新后插入的顺序与失败后的回滚补偿。3.2 退课流程与容量释放退课在业务上比选课简单但有一个常见 bug退课成功后容量释放了但学生课表里还显示这门课。原因往往是退课只删了 Enrollment 记录没有同步更新selected_count。正确做法是用信号signal或者直接在同一个事务内完成删除与更新。transaction.atomic def drop_course(student, schedule_id): deleted, _ Enrollment.objects.filter( studentstudent, schedule_idschedule_id ).delete() if deleted 0: raise ValidationError(未找到选课记录) CourseSchedule.objects.filter(pkschedule_id).update( selected_countF(selected_count) - 1 )退课时的selected_count减一有个前提不能减到负数。实际操作中只要保证删除成功才执行扣减并且扣减操作本身在事务里一般不会出现负数。但极端情况下如果有人并发退同一门课delete()只会有一个人成功另一个 delet 计数为 0 直接抛异常不会影响容量值。3.3 课程时间冲突检测的细节时间冲突检测是选课系统里最容易被低质量实现坑死的点。很多系统只判断同一个星期几、同节次是否重复但现实中还有跨周课程、单双周课程简单判断会误伤。这套系统为了控制复杂度time_slot设计为一个整数字段比如1表示周一 1-2 节2表示周一 3-4 节以此类推。同值即冲突逻辑简单可靠。如果要做更细的周次判断就需要引入周次字符串或星期 开始节次 持续节数的组合字段扩展思路是改CourseSchedule的表结构而不是塞进逻辑代码。时间冲突检测还有一个隐藏需求退课后再选同一时间另一门课系统应该放行。上面的代码里用了statusselected过滤就是避免把已退课程也算进冲突里。3.4 视图层实现Class-Based Views 是省事利器项目里视图尽量用基于类的视图CBV配合 Django 自带的ListView、DetailView、FormView能节省大量重复代码。以学生查看可选课程列表为例from django.views.generic import ListView from .models import CourseSchedule class AvailableCourseListView(ListView): model CourseSchedule template_name course_list.html context_object_name schedules paginate_by 10 def get_queryset(self): # 只显示当前学期的开课记录 qs CourseSchedule.objects.filter(semester__is_activeTrue) # 排除已选课程 student Student.objects.get(userself.request.user) enrolled_ids Enrollment.objects.filter( studentstudent, statusselected ).values_list(schedule_id, flatTrue) return qs.exclude(pk__inenrolled_ids)这个列表页有一个设计巧思默认排除掉自己已经选过的课程。页面呈现上更友好学生不会反复看到自己已选的课也减少了误操作。3.5 信号量的应用选课后自动更新学生学分学分计算属于典型选课成功后要同步做的事我建议放在 Django signal 里负责业务逻辑的代码不动订阅事件自动执行。from django.db.models.signals import post_save from django.dispatch import receiver receiver(post_save, senderEnrollment) def update_student_credits(sender, instance, created, **kwargs): if not created: return student instance.student semester instance.schedule.semester # 计算当前学期总学分简化示例 total_credit Enrollment.objects.filter( studentstudent, schedule__semestersemester, statusselected ).aggregate(totalmodels.Sum(schedule__course__credit))[total] or 0 if total_credit MAX_CREDIT_PER_SEMESTER: # 如果超学分删除该选课记录 instance.delete() raise ValidationError(学期学分超限)信号的好处是解耦但坏处是出问题时排查链路变长。初学者如果把握不好也可以直接把学分计算写进enroll_course服务里逻辑更直观。我的建议是项目规模小优先追求可读性信号谨慎使用。4. 完整实操过程从环境搭建到项目落地光讲设计不给实操过程读者很难真正复现。下面把从零搭建这套系统的每一步写清楚包括环境、配置、命令、数据初始化等关键动作。4.1 环境准备与项目初始化首先保证本地已安装 Python 3.8推荐用虚拟环境隔离依赖执行下面的命令。# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate # 安装 Django pip install django4.2 # 安装数据库驱动以 MySQL 为例也可以先用自带 SQLite pip install mysqlclient # 创建项目和应用 django-admin startproject course_system cd course_system python manage.py startapp course python manage.py startapp users项目结构建议按功能拆分 app不要所有模块堆在一个文件里。这里拆成users用户、权限和course课程、选课两个 app规则清晰。4.2 数据库配置与迁移默认配置使用的是 SQLite零配置、跑起来最省事。如果要切换到 MySQL需要在settings.py中修改数据库配置。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: course_system, USER: root, PASSWORD: yourpassword, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, } } }配置完成后执行迁移命令生成数据表python manage.py makemigrations python manage.py migrate注意makemigrations和migrate是两个不同的命令。前者根据模型生成迁移文件后者将迁移文件实际应用到数据库。很多新手搞混改完模型不执行迁移页面一直报错说 no such column。4.3 数据初始化超级管理员与测试数据系统上线前需要预置超级管理员账户。Django 提供createsuperuser命令在终端中执行后按提示输入用户名、邮箱、密码即可。python manage.py createsuperuser如果不想一步一答也可以写一个数据初始化脚本在开发调试时一键灌入模拟数据。用data_init.py配合python manage.py shell执行from users.models import User, Student, Teacher from course.models import Course, CourseSchedule, Semester # 创建学期 sem Semester.objects.create( name2024-2025-1, start_date2024-09-01, end_date2025-01-15, is_activeTrue ) # 创建学生用户 user User.objects.create_user( username2023001, password123456, emailstu1test.com, rolestudent ) student Student.objects.create(useruser, student_no2023001, major计算机科学) # 创建教师用户 t_user User.objects.create_user( usernamet001, password123456, roleteacher ) teacher Teacher.objects.create(usert_user, teacher_noT001, department信息学院) # 创建课程与开课记录 course Course.objects.create( course_codeCS101, name程序设计基础, credit3.5, course_type必修, capacity40 ) schedule CourseSchedule.objects.create( coursecourse, teacherteacher, semestersem, time_slot1, locationA-101 ) print(初始化完成)有了这些基础数据之前写的列表页、选课接口、管理后台就都有东西可看了。4.4 Django Admin 后台配置Django 自带的后台管理是这类管理系统最不需要写代码的亮点注册模型后在/admin/路径直接管理所有数据。# course/admin.py from django.contrib import admin from .models import Course, CourseSchedule, Enrollment admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display [course_code, name, credit, course_type, capacity] search_fields [name, course_code] admin.register(CourseSchedule) class CourseScheduleAdmin(admin.ModelAdmin): list_display [course, teacher, semester, time_slot, location, selected_count] admin.register(Enrollment) class EnrollmentAdmin(admin.ModelAdmin): list_display [student, schedule, status, select_time] list_filter [status]后台配置的第一个好处是管理员的日常管理可以完全通过界面完成不写一行代码第二个好处是调试阶段数据可视化比如可以直接在后台看某门课的容量和已选人数是否一致。5. 常见问题排查与踩坑实录实操过程中踩过的坑往往比成功路径更有参考价值。下面把这类 Django 选课系统最常见的问题和对应排查方法整理成速查表。5.1 常见问题速查表问题现象可能原因排查思路页面报 no such table未执行migrate检查数据库表是否存在执行迁移字段显示 Unknown column模型改动后未重新迁移重新执行 makemigrations migrate多人同时选课人数超容量未处理并发检查是否使用select_for_update()选了 A 课显示 B 课在课表关联查询写错检查 QuerySet 里 filter/exclude 条件退课后容量不恢复只删选课记录未更新容量检查事务里是否有容量扣减部署后静态文件 404未配置 STATIC_ROOT执行collectstatic并配置 Web 服务器5.2 排课冲突的隐蔽细节排课冲突是个非常细的活很多初学者在这里栽跟头。举例老师 A 在周一 1-2 节教操作系统同时又在周一 1-2 节教计算机组成原理——这在现实中是绝对不允许的但很多系统根本没检查教师的时间冲突导致排课数据本身就有问题。课表数据的源头脏了后面学生选课再检查冲突也无济于事。建议在管理端的开课表单里做教师时间冲突校验创建开课记录时就拦掉冲突数据。from django import forms class CourseScheduleForm(forms.ModelForm): class Meta: model CourseSchedule fields __all__ def clean(self): cleaned_data super().clean() teacher cleaned_data.get(teacher) time_slot cleaned_data.get(time_slot) semester cleaned_data.get(semester) conflict CourseSchedule.objects.filter( teacherteacher, time_slottime_slot, semestersemester ).exclude(pkself.instance.pk if self.instance else None).exists() if conflict: raise forms.ValidationError(该教师在所选时间段已有课程安排) return cleaned_data5.3 管理后台直接改数据带来的脏数据问题Django Admin 默认情况下可以直接编辑关联对象比如管理员在后台把一个课程 Schedule 的容量从 30 改成 50但 Enrollment 表里的记录不会自动做任何同步。如果已经选了 40 个人现在改成 50没问题但改成 20就有 20 条选课数据处于超容量状态。我的经验是管理后台应该在关键模型上覆盖save_model或重写clean方法做保护不允许后台把容量改得比已选人数还小。这算是一个容易被忽略的健壮性设计。5.4 文档编写的实用建议这类系统标配的文档通常包括需求分析、概要设计、数据库设计、接口说明、测试报告、部署说明。很多人在编码完了才开始写文档结果写出来的东西和代码对不上。更省力的做法是在开发过程中同步维护一个简单的 README把最终可用的接口和表结构变更记录下来最后再扩展成完整文档。我建议文档中必须包含一张权限矩阵表列出每个角色能访问的 URL 和能执行的操作。这张表看起来简单但写的时候能逼你把系统的权限边界彻底理清评审的时候也方便。5.5 测试与保命技巧一套课程设计级别的系统不要求 100% 覆盖率但核心流程必须有测试。几个必写的测试用例正常选课流程能成功重复选课被拦截容量已满时选课失败选课时间冲突被拦截退课后容量减少且学生课表消失非学生角色访问选课接口被拒绝# course/tests.py 核心用例示例 from django.test import TestCase from django.core.exceptions import ValidationError from .services import enroll_course, drop_course class EnrollCourseTest(TestCase): def setUp(self): # 准备学生、课程数据 pass def test_enroll_success(self): student self.create_student() schedule self.create_schedule() self.enroll_ok enroll_course(student, schedule.id) self.assertIsNotNone(self.enroll_ok) def test_duplicate_enroll_rejected(self): student self.create_student() schedule self.create_schedule() enroll_course(student, schedule.id) with self.assertRaises(ValidationError): enroll_course(student, schedule.id)测试的意义不光是证明代码能跑更是在后面改造重构时让你敢下手。6. 扩展思路这套系统还能往哪个方向升级做完基础版本之后如果你想让项目更有亮点、或者在答辩时有更多可说的点这几个方向值得考虑。6.1 缓存层与抢课性能优化选课高峰期的性能瓶颈绝大部分在数据库查询和行锁等待上。可以考虑引入 Redis 做两层缓存热门课程列表缓存到 Redis选课计数先写 Redis 再异步落库。更稳妥的架构是Redis 预占名额 消息队列异步写入数据库但这套体系复杂度高很多学习成本不低。实际项目中我推荐一个折中方案热门课程列表用 Django 的 cache 框架加 Redis选课计数保留数据库原子更新。既提升了列表页的响应速度又保证了数据可靠。6.2 选课时间窗口与批次控制真实学校的选课往往分批次比如大四先选、大三第二、新生最后。现有 Semester 模型只能控制整个学期的开闭不能控制细粒度选课窗口。可以在CourseSchedule上增加select_start_time和select_end_time字段enroll_course服务里加入时间判断。from django.utils import timezone now timezone.now() if now schedule.select_start_time or now schedule.select_end_time: raise ValidationError(当前不在选课时间窗口内)这个功能做出来后很实用也是答辩时能主动讲的业务亮点。6.3 成绩管理模块选课系统做完了自然延伸就是成绩管理。给 Enrollment 增加score字段教师可以录入成绩学生可以查看成绩。但要注意成绩录入不能让教师改所有人的要通过teacher__user关联判断这门课是不是该教师教的。def get_teacher_courses(self, teacher_user): return CourseSchedule.objects.filter(teacher__userteacher_user)成绩模块加进来后系统的完整性更强也能顺带把教师角色的权限做深一层。写在最后的一点个人体会这类管理系统做下来我最深的感触是技术上的东西都可以查文档解决真正难的是把业务规则想周全。很多人在选课系统上翻车不是因为不会写 Django 代码而是没想清楚选课的本质是有约束的资源分配问题于是写出来的系统处处是漏洞。建议你在动手写代码之前花大把时间把表结构、状态流转、权限矩阵用一张图画清楚后面会少踩很多坑。选课系统虽然是个老掉牙的题目但把并发控制、权限设计、业务约束这些基本功练扎实了去做任何管理系统、订单系统、预约系统思路都是通的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑