
简介这是一份基于PythonDjango2.2PyCharmMySQL5.6开发的学生教务选课系统毕业设计源码包适合计算机相关专业毕业生、Django初学者及需要快速搭建教务管理场景的开发者参考。系统覆盖学生端和管理员端核心功能学生可注册登录、查询课程、在线选课、查看选课和成绩、修改个人资料管理员可管理学生与教师信息、发布维护课程、登记与修改成绩还能发布新闻公告。配套数据库脚本提供了学院、专业、班级、学生等实体ER设计与初始数据便于直接部署调试。压缩包共2000个文件以JS、HTML、CSS为主构成前端交互界面另有Python源码、JSON/XML配置文件及说明文档整体仅5.62MB结构紧凑适合作为课程设计或毕业答辩的项目起点。该资源已有355人学习口碑较佳值得参考。1. “毕业设计基于PythonDjangoPyCharm开发的学生教务选课系统”到底在做什么“教务选课系统”和“毕业设计”“完整源码”“数据库脚本”这几个词放在一起其实是在告诉我们这套项目不是为了让开发者研究算法而是要把一班学生、一批课程和一张选课表用 Python Django 串成一个完整可交付的 Web 应用。它的价值点在于业务足够贴近真实教务场景又不会复杂到一个人做不完学生能选课、退课教师能开课、查名册管理员能维护账号和课程信息剩下的登录鉴权、后台管理和数据库读写全部由 Django 和 MySQL 这套标准组合承载。适合准备毕业设计、拿现成工程练手、以及想快速把 Django 全流程跑通的人。别把它当成 CRUD 博物馆来背重点是建立一整套“数据模型 → 事务 → 界面 → 部署”的闭环以下几章就从这条闭环的每一段讲透。2. 先把理论立住把“教务选课”翻译成 ER 图画得出的四张核心表2.1 角色与权限学生、教师、管理员在 Django 里对应什么写任何代码之前先要把业务对象钉在纸上。教务选课系统的用户一共三类权限边界非常清晰学生只看自己账号下的选课记录教师只看自己开设的课程管理员面对的是全校账号和课程状态。它们落到 Django 里正好对应AbstractUser继承、ForeignKey关联和django.contrib.admin三套机制。角色Django 模型模型能做什么页面入口学生Userrolestudent浏览课程、选课、退课、查看已选课程前台选课页面教师Userroleteacher发布课程、维护开课信息、查看选课名单前台页面 后台教务管理员Userroleadmin管理学生账号、控制选课开关、审核课程状态Django Admin 后台这里有个容易忽略的点Django 自带的User表默认没有“角色”字段所以源码里常见的做法是继承AbstractUser加一个role或者用OneToOneField单独建 StudentProfile / TeacherProfile。两种方案对毕业设计都够用前者更省事因为登录后的request.user.role直接可读后者更接近企业里“用户中心与业务档案分离”的架构论文里也更好写。2.2 课程、选课与用户四张核心表的主键、外键和唯一约束选课系统真正涉及的表远比页面数量少用户表、课程表、选课表加上一个可选的班级表就已经覆盖全部核心业务。课程和用户之间不是简单多对多因为学生和专业、教师和课程之间都带有额外属性所以源码里大多会显式建一个中间表Elective而不是直接挂ManyToManyField。这样可以在选课记录上继续追加created_at、成绩、退课时间等字段。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 管理员), ) role models.CharField(角色, max_length10, choicesROLE_CHOICES, defaultstudent) class Course(models.Model): course_no models.CharField(课程编号, max_length20, uniqueTrue) name models.CharField(课程名称, max_length100) credit models.DecimalField(学分, max_digits3, decimal_places1) capacity models.PositiveIntegerField(容量, default60) selected_count models.PositiveIntegerField(已选人数, default0) teacher models.ForeignKey( User, on_deletemodels.CASCADE, related_namecourses, limit_choices_to{role: teacher} ) class Elective(models.Model): student models.ForeignKey( User, on_deletemodels.CASCADE, related_nameelectives, limit_choices_to{role: student} ) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameelectives) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [created_at] constraints [ models.UniqueConstraint( fields[student, course], nameuq_student_course ) ]这段模型基本是“标配”写法。limit_choices_to{role: teacher}只影响 Django Admin 下拉框里能选到谁不构成数据库层校验真正阻止学生重复选同一门课的是UniqueConstraint它会在 MySQL 里生成唯一索引如果源码里用的还是老式写法等价于unique_together (student, course)。selected_count是为了列表页快速显示余量而冗余存储的它和Elective表中的真实记录数必须保持一致这个一致性就是选课并发时要管的头等大事。数据库脚本文件.sql在源码包里通常就是这几十张表结构外加少量初始化数据。用 MySQL 命令行直接导入后表结构大致是下面这个样子CREATE TABLE elective ( id bigint NOT NULL AUTO_INCREMENT, student_id int NOT NULL, course_id int NOT NULL, created_at datetime(6) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uq_student_course (student_id, course_id), CONSTRAINT fk_elective_student FOREIGN KEY (student_id) REFERENCES user (id) ON DELETE CASCADE, CONSTRAINT fk_elective_course FOREIGN KEY (course_id) REFERENCES course (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个关键点课程表容量是 60已选人数是 0选课表里每插入一条记录已选人数就要加一。这两张表的数据是互相印证的只要一边对不上管理后台里的余量就是假的。2.3 教务选课系统最容易出错的点余量扣减与并发锁网上很多 Django 选课代码的选课逻辑是下面这样的先查出课程判断selected_count capacity然后插入选课记录再把selected_count加一。这段逻辑在单机单用户下完全正确一旦两个学生同时选同一门容量只剩 1 的课两个请求都先读到selected_count59于是都通过了判断最终超卖一人。from django.db import transaction from django.db.models import F class CourseFull(Exception): pass transaction.atomic def enroll(user, course_id): # 1. 锁定课程行直到事务提交 course Course.objects.select_for_update().get(pkcourse_id) # 2. 校验容量这里读到的已经是锁内最新值 if course.selected_count course.capacity: raise CourseFull(course.name) # 3. 插入选课记录 Elective.objects.create(studentuser, coursecourse) # 4. 用 F 表达式原地加一避免“读-改-写”覆盖 Course.objects.filter(pkcourse.pk).update( selected_countF(selected_count) 1 )select_for_update()会翻译成SELECT ... FOR UPDATE锁住这条课程记录事务提交后释放。F(selected_count) 1是让数据库在 SQL 层完成自增而不是把数值查回到 Python 里再写回这两件事配合起来就堵住了超卖。如果中间抛出CourseFulltransaction.atomic会让已经执行的Elective.objects.create一并回滚不需要手工处理事务边界。与之对应的是退课操作。删除选课记录后同样要用F表达式把余量减回来否则就会出现“退了一门课容量没恢复”的 bugfrom django.db.models import F def drop(user, course_id): deleted, _ Elective.objects.filter( studentuser, course_idcourse_id ).delete() if deleted: Course.objects.filter(pkcourse_id).update( selected_countF(selected_count) - 1 )这里的filter(...).delete()返回元组第一个数字是被删除的记录数只有确实删掉了选课记录才应该回补余量。很多人在这一步直接无脑update selected_countF(selected_count) - 1结果退一门不存在的课也会把余量减成负数。3. 在 PyCharm 里用虚拟环境跑通 Django 选课项目的五个关键步骤3.1 先配环境venv、requirements.txt 与 PyCharm 解释器拿到源码包后不要直接双击manage.py先猜一下依赖版本。检查根目录有没有requirements.txt正常情况下里面至少有Django、mysqlclient、pytz这几个包。PyCharm 打开项目后第一件事是新建虚拟环境并指向本机已安装的 Python 解释器。python -m venv venv venv\Scripts\activate pip install -r requirements.txtWindows 下激活命令用venv\Scripts\activatemacOS 或 Linux 用source venv/bin/activate。装完依赖后在 PyCharm 里打开 File → Settings → Project: 项目名 → Python Interpreter添加现有虚拟环境的python.exe路径指向venv\Scripts\python.exe。这一步没做对的话PyCharm 终端里能执行python manage.py但运行配置里的解释器还是全局环境后面会经常出现“终端能跑、点运行按钮却报 ModuleNotFoundError”的怪问题。3.2 改 settings.py数据库连接从 SQLite 切到 MySQL源码包里既然给了数据库脚本说明设计目标是 MySQL。新建数据库后把settings.py里的DATABASES改成下面的样子DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: course_system, USER: course_user, PASSWORD: set-your-pass, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } } LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ TrueNAME是数据库名不是表名HOST写127.0.0.1表示本机OPTIONS里的charsetutf8mb4是为了让中文课程名和 emoji 都能正常存。USE_TZ True时 Django 在数据库里统一存 UTC 时间展示层再转成Asia/Shanghai如果项目里硬编码用datetime.now()生成时间最容易出现晚 8 小时的问题。MySQL 侧还需要给 Django 准备一个账号mysql -u root -p -e CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4; mysql -u root -p -e CREATE USER course_userlocalhost IDENTIFIED BY set-your-pass; mysql -u root -p -e GRANT ALL PRIVILEGES ON course_system.* TO course_userlocalhost; mysql -u root -p -e FLUSH PRIVILEGES;3.3 导入数据库脚本source 与 migrate 的执行顺序数据库脚本这个文件名听起来很专业实际就是一个.sql文件里面通常包含建库、建表、插入初始管理员和课程数据的语句。执行顺序非常重要我的建议是先让 Django 的 migration 状态归零再导入 SQL最后跑增量 migrate。这样能避免“表已经存在”的冲突。mysql -h 127.0.0.1 -P 3306 -u course_user -p course_system course_system.sql python manage.py showmigrations python manage.py migrate python manage.py createsuperuser如果 .sql 文件里没有CREATE DATABASE需要先手动建库再导入。导入完成后showmigrations会显示 Django 认为哪些迁移还没有执行。由于表已经被 SQL 脚本建好了直接migrate大概率会报table already exists此时常见做法是把migrations目录里对应 app 的迁移文件删掉再重新makemigrations和migrate或者用migrate --fake-initial跳过已存在的表。列两个常见做法都行但答辩时要把你选的那一条讲清楚。3.4 启动与验证runserver、后台登录与页面流程都配好之后启动命令依然是最基础的那条python manage.py runserver 127.0.0.1:8000浏览器打开http://127.0.0.1:8000正常情况下能看到登录页或选课大厅。再用createsuperuser建的管理员账号登录/admin/检查两件事左侧菜单能不能看到课程、选课记录选课记录列表里能否看到刚导入的初始数据。如果页面能开但后台是“无样式”状态说明静态文件没配好后面第 4 章会处理。登录进去后走一遍核心流程学生账号选一门课教师账号看名册管理员账号在后台关掉选课开关。三步全通这个项目的运行链路就算真正通了。4. 跑通只是开始Django 选课系统四个必踩的坑与一个回归验证4.1 四个隐蔽坑mysqlclient、admin 无样式、时差八小时、外键删不掉现象原因处理启动时报Error loading MySQLdb module没装 mysqlclient 或已装版本不匹配pip install mysqlclientWindows 下先装 Microsoft C Build Toolsadmin 页面有文字但完全没有 CSSSTATIC_ROOT未配置collectstatic没执行配置STATIC_ROOT后运行python manage.py collectstaticDjango 查出来的时间比本地晚 8 小时TIME_ZONE被注释或USE_TZ配置不一致TIME_ZONE Asia/ShanghaiUSE_TZ True删除课程报外键约束错误Elective表里有记录引用该课程先删选课记录再删课程或改用on_deletemodels.CASCADEmysqlclient是 Windows 上出现频率最高的问题。如果pip install mysqlclient报Microsoft Visual C 14.0 or greater is required说明本机缺少编译工具链。找不到现成 wheel 时优先安装对应 Python 版本的 whl 文件装完后再执行一次pip install mysqlclient。后台样式丢失的问题很多情况下不是代码错了而是DEBUGFalse导致的。Django 只有在DEBUGTrue时才会自动托管静态文件一旦切到生产模式就必须手动执行collectstatic。这里提前说一句后面要部署到 Linux 服务器时常见做法是宝塔面板或 nginx 加 uWSGI启动前同样要跑collectstatic并设置DEBUGFalse否则前端样式会整体丢失。4.2 用 TestCase 给选课和退课做回归验证手工点页面能发现逻辑错误但发现不了并发回归。Django 自带的测试客户端可以直接模拟登录、POST 表单和断言数据库状态不用真正启动 runserver测试时 Django 会为每个 TestCase 自动创建独立测试库。from django.test import TestCase from django.contrib.auth import get_user_model class EnrollTestCase(TestCase): def setUp(self): user get_user_model().objects.create_user( s001, passwordtest123 ) self.client.force_login(user) Course.objects.create( course_noCS101, name数据结构, capacity1 ) def test_enroll_success(self): resp self.client.post( /elective/api/enroll/, {course_id: 1} ) self.assertEqual(resp.status_code, 200) course Course.objects.get(pk1) self.assertEqual(course.selected_count, 1) def test_enroll_course_full(self): self.client.post( /elective/api/enroll/, {course_id: 1} ) resp2 self.client.post( /elective/api/enroll/, {course_id: 1} ) self.assertEqual(resp2.status_code, 400) self.assertEqual(Course.objects.get(pk1).selected_count, 1) def test_drop_restores_capacity(self): self.client.post( /elective/api/enroll/, {course_id: 1} ) self.client.post( /elective/drop/, {course_id: 1} ) course Course.objects.get(pk1) self.assertEqual(course.selected_count, 0)这里请求路径/elective/api/enroll/需要根据源码里实际的路由调整前后端分离项目可能是/api/enroll/传统表单提交可能是/course/enroll/。三个测试分别覆盖正常选课、容量占满后拒绝选课、退课恢复余量三条路径跑完python manage.py test如果显示 OK就说明事务回滚和容量扣减逻辑能经得起基本回归。4.3 答辩前生成演示数据用 shell 一次写 100 门课空数据库演示很难看一门课一门课地在页面上点“新增”也很浪费时间。Django 提供了一条命令行渲染数据的方式直接在 shell 里用bulk_create批量写入python manage.py shell -c from school.models import Course Course.objects.bulk_create([ Course(course_nofC{i:03d}, namef选修课程{i}, capacity50) for i in range(1, 101) ]) bulk_create会把 100 条 INSERT 合并成少量批量语句比循环save()快很多适合用来给选课大厅撑出几页数据。执行时要注意school是应用名忍一下换成你工程里的实际 app 名。course_no有uniqueTrue约束重复跑这段命令会报唯一键冲突所以演示数据脚本最好用幂等写法或者先清空再插入。5. 让选课系统在简历里多一个亮点后台列表优化与并发预检5.1 admin 后台的列表、过滤与搜索配置Django Admin 默认呈现的是所有字段堆在一行选课记录一多就非常难翻。给模型加上一个自定义ModelAdmin能让评审老师打开后台第一眼就看到重点信息而不需要逐个点进详情页。from django.contrib import admin from .models import Course, Elective admin.register(Elective) class ElectiveAdmin(admin.ModelAdmin): list_display (id, student, course, created_at) list_filter (course__credit,) search_fields (student__username, course__name) autocomplete_fields (course,) admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (course_no, name, credit, capacity, selected_count) search_fields (course_no, name)list_filter是按学分筛选search_fields指定搜索学生账号或课程名称autocomplete_fields必须配合关联模型上已配置的search_fields使用否则页面会报ElectiveAdmin.autocomplete_fields指定的外键没有search_fields的错误。CourseAdmin里的capacity和selected_count并排显示容量是否快满一目了然。5.2 一个可落地的并发预检缓存计数只做展示最终一致性交给事务最后一个让项目显得更完整的小技巧是把余量放到缓存里给列表页做轻量预检。数据库行锁能保证最终正确但高并发下每次都锁行会让课程列表页变得很慢反过来如果用缓存先挡一道等真正提交时再用事务锁兜底响应速度和正确性就都有了。from django.core.cache import cache def get_course_left(course_id, capacity): selected cache.get(fcourse_selected_{course_id}) if selected is None: selected Course.objects.get(pkcourse_id).selected_count cache.set(fcourse_selected_{course_id}, selected, 60) return capacity - selected这段代码只负责展示页的预估余量缓存丢失时回源数据库设定 60 秒过期。真正选课成功或退课成功后要把缓存里的数同步加一减一或者直接删掉course_selected_{course_id}让它冷启动回源。注意这个预检不能替代事务里的select_for_update它只是让前端不要频繁打到数据库行锁上。这套“缓存预扣 数据库落账”的思路在抢课系统里很常见毕业设计里能落地这个简化版基本就能把选课系统的技术深度拉开一档。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。