简介基于PythonDjango开发的学生教务选课系统毕业设计资源面向计算机相关专业毕业生、课程设计学生及Django初学者完整覆盖注册登录、课程发布、在线选课、成绩录入、新闻公告管理等教务场景从学生端到管理员端形成功能闭环。压缩包内共2000个文件包含1632个JavaScript、250个HTML、54个CSS等前端资源辅以5个Python源码文件、JSON配置及数据库脚本整体约5.62MB结构符合典型Web项目组织方式。系统涉及学院、专业、班级、学生等实体ER设计内置可运行的Python完整源码与数据库脚本可在PyCharm Django2.2 Python3.6 MySQL5.6环境中直接部署便于二次开发与毕业答辩演示。已有355人学习或浏览适合需要快速搭建同类课题、梳理Django业务开发流程或扩展功能模块的读者参考。1. 为什么这个选课系统值得拆开看一次要说毕业设计里最容易“做完发现什么都没学会”的方向教务选课系统绝对是前几名因为它表面上只有“学生选课、老师录分、管理员发通知”三件事。但真正动笔时你会发现选课是要在多个学生同时操作时保证不超选的成绩是要和学生、课程、教师三方数据正确关联的管理员看的统计页是要跨五张表联查的——这些才是Django开发里真正值钱的部分。这个基于Python3.6Django2.2PyCharm搭建的学生教务选课系统源码和数据库脚本都整理得很完整关键是有学院、专业、班级、学生、课程、选课记录这样完整的ER关系特别适合用来补全“模型关联 事务 权限”这三块实战盲区。不管是拿它做毕业设计底稿还是想快速演练一个多角色业务系统都比空读文档有意义。2. 环境初始化与Django项目骨架版本钉子先钉死2.1 为什么是Python3.6 Django2.2 MySQL5.6这套组合先说环境这不是偏保守而是这套组合在2024年的今天依然有实际理由。Django2.2是最后一个支持Python3.5的LTS版本官方安全支持持续到2022年4月很多老项目至今还在上面跑。Python3.6则保证你在PyCharm里跑旧源码时不会遇到f-string语法不兼容Django2.2的内置代码大量使用f-stringPython3.5以下直接崩。MySQL5.6则是和Django2.2的django.db.backends.mysql适配最顺的版本用全新的MySQL8.0会撞上caching_sha2_password认证插件不兼容问题连接时报Authentication plugin caching_sha2_password cannot be loaded处理起来非常麻烦。环境版本对照如下组件版本说明Python3.6.x源码兼容性的安全线Django2.2.xLTS版ORM和Admin足够成熟PyCharm任意较新版本主要用于调试和虚拟环境管理MySQL5.6 / 5.7避开MySQL8的认证插件问题如果你本机装的是Python3.10以上最稳妥的做法是用pyenv或者PyCharm里配置虚拟环境时手动指定Python3.6解释器路径别硬用高版本Python去跑项目依赖后面迁移模型时会出现一堆TypeError。2.2 从零创建项目和数据库拿到源码后第一步不是直接开跑而是先建立隔离的虚拟环境避免和机器上其他项目的包起冲突。在PyCharm的Terminal里执行# 创建虚拟环境Python3.6解释器路径按实际版本改 python3.6 -m venv venv source venv/bin/activate # 安装依赖 pip install django2.2.28 mysqlclient2.0.3依赖装完后再看项目目录结构。源码包里如果没有manage.py这个文件说明你拿到的可能是纯代码包需要自己用命令补上项目骨架# 创建项目名称按你解压的目录名改 django-admin startproject course_system . # 创建核心应用 python manage.py startapp student python manage.py startapp course python manage.py startapp news上面的结构里student负责学生档案和认证course负责课程和选课事务news负责公告轮播。三应用分离的好处是权限边界清晰可以分别给不同管理员分组授权。创建完应用后编辑course_system/settings.py把三个应用注册进去同时配置数据库连接# course_system/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, student, # 学生档案与扩展 course, # 课程发布与选课 news, # 公告管理 ] # MySQL连接配置 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: course_system, # 数据库名需先在MySQL中创建 USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, # 支持中文和emoji init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }这里有几个坑要重点说出来。第一MySQL里建库时必须指定字符集否则存中文乱码CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二mysqlclient2.0.3在Python3.6下编译时要确保系统已装libmysqlclient-dev否则会报EnvironmentError: mysql_config not foundUbuntu/Debian上执行sudo apt-get install default-libmysqlclient-dev可以解决。第三OPTIONS里的init_command不能省Django2.2在MySQL5.6下不设置sql_mode会在插入数据时静默截断字符串导致数据长度检查失效。2.3 静态文件与前端资源接管源码包里的bootstrap.css、animate.css、font-awesome.css、bootstrap-datetimepicker.css这些前端文件对应的就是bootstrap3风格的模板页面。Django2.2对静态文件的处理方式是每个应用下建static目录顶层的项目也建一个static目录做公共资源池。把解压出来的CSS和JS文件放到course_system/static/下然后在settings.py里补上STATIC_URL /static/ STATICFILES_DIRS [ os.path.join(BASE_DIR, static) ]注意STATICFILES_DIRS是列表多个路径时依次写。模板里引用静态文件的正确姿势是{% load static %} link relstylesheet href{% static css/bootstrap.css %} link relstylesheet href{% static css/bootstrap-datetimepicker.min.css %}别直接用/static/css/bootstrap.css硬编码虽然本地能跑但部署到Nginx后就失效了。前端资源的问题解决掉后面做模型迁移和页面联调才不会分心去排查404。3. 选课核心事务ER模型落成ORM与并发超选控制3.1 从ER实体到Django模型的字段映射这个项目的实体关系比较完整学院College含名称、成立日期、院长、电话专业Major挂在学院下班级ClassInfo挂在专业下学生Student通过外键挂班级。课程和选课记录之间是多对多关系选课记录本身还有成绩字段。这一层映射代码质量直接决定后期联查复杂度。在student/models.py里from django.db import models from django.contrib.auth.models import User class College(models.Model): name models.CharField(学院名称, max_length64, uniqueTrue) established models.DateField(成立日期) dean models.CharField(院长姓名, max_length32) phone models.CharField(联系电话, max_length20) def __str__(self): return self.name class Major(models.Model): name models.CharField(专业名称, max_length64) college models.ForeignKey(College, verbose_name所在学院, on_deletemodels.PROTECT) contact models.CharField(联系人, max_length32) class ClassInfo(models.Model): name models.CharField(班级名称, max_length32) major models.ForeignKey(Major, verbose_name所属专业, on_deletemodels.PROTECT) head_teacher models.CharField(班主任, max_length16) class Student(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) sno models.CharField(学号, max_length20, uniqueTrue) gender models.CharField(性别, max_length2, choices((男,男),(女,女))) klass models.ForeignKey(ClassInfo, verbose_name所在班级, on_deletemodels.PROTECT) phone models.CharField(联系电话, max_length20) email models.EmailField(邮箱)on_deletemodels.PROTECT这个选择值得展开说。Django2.0以后on_delete是必填参数很多人图省事写成CASCADE删除学院时会把专业、班级、学生全带走在真实教务场景里这是严重数据事故。选课系统的数据是终生存档的PROTECT会在删除被关联对象时抛ProtectedError从数据结构层面防住误删。课程和选课记录在course/models.py里class Course(models.Model): name models.CharField(课程名, max_length64) teacher models.CharField(授课教师, max_length32) credit models.DecimalField(学分, max_digits2, decimal_places1) capacity models.PositiveIntegerField(容量, default50) selected models.PositiveIntegerField(已选人数, default0) schedule models.CharField(上课时间, max_length128) is_active models.BooleanField(开放选课, defaultTrue) def __str__(self): return f{self.name} - {self.teacher} class Enrollment(models.Model): student models.ForeignKey(student.Student, on_deletemodels.CASCADE, verbose_name选课学生) course models.ForeignKey(Course, on_deletemodels.PROTECT, verbose_name课程) score models.DecimalField(成绩, max_digits4, decimal_places1, nullTrue, blankTrue) created_at models.DateTimeField(选课时间, auto_now_addTrue) class Meta: unique_together (student, course)unique_together这个联合唯一约束相当于数据库层的最后一道保险即使后面业务代码有bug也没法给同一个学生插入两条相同课程记录。3.2 用事务和行级锁解决超选问题学生选课这个动作放在并发场景下是典型的“检查后写”竞争问题。不加保护的话两个学生同时提交选同一门只剩一个名额的课程两个人查到的selected都是49 50然后各自插入记录最后总人数变成51超选了。解法是事务加select_for_update行锁from django.db import transaction from django.http import JsonResponse from django.contrib.auth.decorators import login_required login_required def enroll_course(request): if request.method ! POST: return JsonResponse({code: 400, msg: 只支持POST请求}) course_id request.POST.get(course_id) student Student.objects.get(userrequest.user) with transaction.atomic(): course Course.objects.select_for_update().get(pkcourse_id) if not course.is_active: return JsonResponse({code: 403, msg: 课程未开放选课}) if course.selected course.capacity: return JsonResponse({code: 403, msg: 课程已满员}) if Enrollment.objects.filter(studentstudent, coursecourse).exists(): return JsonResponse({code: 403, msg: 重复选课}) Enrollment.objects.create(studentstudent, coursecourse) course.selected 1 course.save(update_fields[selected]) return JsonResponse({code: 200, msg: 选课成功})核心逻辑在select_for_update()这一行它会对命中的课程行加写锁直到整个事务结束才释放。当A学生锁住这行时B学生的查询会阻塞等待等A的事务提交后B再读取此时selected已经是更新后的值超选的条件判断自然失效。调用这段视图的模板表单form methodpost action{% url enroll_course %} {% csrf_token %} input typehidden namecourse_id value{{ course.id }} button typesubmit classbtn btn-primary选这门课/button /form3.3 选课页面的数据联查学生登录后看到的是自己的选课和成绩信息这需要跨Enrollment、Course、Student三张表查询from django.shortcuts import render login_required def my_courses(request): student Student.objects.get(userrequest.user) enrollments (Enrollment.objects .filter(studentstudent) .select_related(course) .order_by(-created_at)) context { enrollments: enrollments, student: student, # 统计选课学分 total_credit: sum([e.course.credit for e in enrollments]) } return render(request, student/my_courses.html, context)select_related(course)的意思是让Django在SQL层用JOIN一次性把关联课程取回来而不是先查选课记录再逐条查课程N1查询问题。选课列表页如果不用它30条记录会产生31条SQL页面响应直接飙到几百毫秒。模板里遍历时直接取关联字段{% for e in enrollments %} tr td{{ e.course.name }}/td td{{ e.course.teacher }}/td td{{ e.course.credit }}/td td{% if e.score %}{{ e.score }}{% else %}未出分{% endif %}/td /tr {% endfor %}模板里e.score为空时的分支判断很实用老师没录分之前显示“未出分”录分后自动切换为具体分数这是教务系统的默认行为预期。4. 教务端角色权限与后台数据管理4.1 三级角色的权限边界设计这个系统的角色分三档学生前台注册、教师课程发布与成绩登记、管理员全员信息管理。处理权限最稳妥的方式是复用Django自带的auth.User然后给每个用户分组。具体做法from django.contrib.auth.models import Group, Permission # 创建三个分组 admin_group, _ Group.objects.get_or_create(name教务管理员) teacher_group, _ Group.objects.get_or_create(name教师) student_group, _ Group.objects.get_or_create(name学生)后台管理界面里把课程管理权限分配给教师组# course/admin.py from django.contrib import admin from django.contrib.auth.models import Group from .models import Course, Enrollment class EnrollmentInline(admin.TabularInline): model Enrollment extra 0 readonly_fields (student, score) admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display [name, teacher, credit, capacity, selected, is_active] list_filter [is_active, teacher] search_fields [name, teacher] actions [open_course, close_course] inlines [EnrollmentInline] def open_course(self, request, queryset): queryset.update(is_activeTrue) open_course.short_description 开放选课 def close_course(self, request, queryset): queryset.update(is_activeFalse) close_course.short_description 关闭选课EnrollmentInline放在CourseAdmin里管理员点开设课课程时可以直接看到已选学生列表。actions自定义的“开放选课/关闭选课”批量操作比一个一个点进去改效率高出不少。4.2 学生注册时的密码处理学生在前台注册时最常犯的错误是直接User.objects.create(username..., password...)这样存进去的是明文密码Django自带的登录校验永远无法匹配。正确姿势是调用create_user或者set_passwordfrom django.contrib.auth.models import User from django.shortcuts import redirect, render from .models import Student def register(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) sno request.POST.get(sno) # ... # create_user会对密码做hash加密 user User.objects.create_user(usernameusername, passwordpassword) student Student.objects.create(useruser, snosno, ...) return redirect(login) return render(request, register.html)密码是Django的PBKDF2WithHmacSHA256算法加过盐的Django自身登录视图拿用户输入的明文去做同一个hash运算匹配才放行。4.3 成绩登记的直接提交与防重复老师端的成绩录入可以采用POST直接提交更新但要处理一个边界重复提交时不能新增第二条记录只能更新原记录的成绩字段login_required def save_score(request, enrollment_id): if request.method POST: score request.POST.get(score) try: enrollment Enrollment.objects.select_for_update().get(pkenrollment_id) except Enrollment.DoesNotExist: return JsonResponse({code: 404, msg: 选课记录不存在}) enrollment.score score # 只更新score字段避免覆盖其他字段 enrollment.save(update_fields[score]) return JsonResponse({code: 200, msg: 成绩已保存})update_fields限制只更新score这一列相当于SQL层的UPDATE enrollment SET score ... WHERE id ...避免把created_at等字段意外覆盖。5. 部署与二次开发uWSGI上线和几个值得动手的增强点5.1 用uWSGI Nginx 把系统跑起来开发环境里python manage.py runserver只适合调试并发能力和安全性都不行。部署时按常规做法先用uWSGI托起Django应用# 安装uWSGIPython3.6版本对应 pip install uwsgi2.0.21 # 启动uWSGI监听8000端口 uwsgi --http :8000 \ --chdir /path/to/course_system \ --wsgi-file course_system/wsgi.py \ --master --processes 4 --threads 2 \ --stats :9191 \ --vacuum --max-requests 5000参数说明--processes 4表示开启4个worker进程--threads 2表示每个进程开2个线程--max-requests 5000让worker处理完5000个请求后自动回收防止内存泄漏--vacuum在进程退出时清理unix socket文件。选课高并发场景下这个配置能撑住上百并发。前端静态文件交给Nginx处理效率更高server { listen 80; server_name your.domain.com; location /static/ { alias /path/to/course_system/static/; } location / { uwsgi_pass 127.0.0.1:8000; include /etc/nginx/uwsgi_params; } }5.2 二次开发建议批量导入与公告扩展如果想让系统更接近生产可用最值得动手的两个增强是用openpyxl做学生信息的Excel批量导入以及给新闻公告模型加上cover_image字段显示轮播图。批量导入的代码骨架import openpyxl def import_students(file_path): wb openpyxl.load_workbook(file_path) ws wb.active count 0 for row in ws.iter_rows(min_row2, values_onlyTrue): sno, name, gender, class_name, phone row[:5] # 判断班级是否存在不存在则自动创建 klass, _ ClassInfo.objects.get_or_create(nameclass_name) user User.objects.create_user(usernamesno, passwordsno[-6:]) Student.objects.create(useruser, snosno, namename, gendergender, klassklass, phonephone) count 1 return count5.3 验证并发选课是否真的不超选最后给一个验证手法用abApacheBench工具模拟并发请求测试同一门只剩少量空位的课程。# 并发20个请求选同一门课 ab -n 20 -c 20 -T application/x-www-form-urlencoded \ -p enroll.txt http://127.0.0.1:8000/course/enroll/压测后去MySQL确认SELECT course_id, COUNT(*) FROM course_enrollment GROUP BY course_id; SELECT selected, capacity FROM course_course WHERE id 1;只要selected不超过capacity且enrollment表人数与selected字段一致说明行锁生效。如果出现不一致优先检查视图函数里是否真的用了transaction.atomic()包裹以及select_for_update()是否写在了锁范围内的第一条查询上。本文还有配套的精品资源点击获取