Django学生教务选课系统毕设实战:从架构到部署全解析

发布时间:2026/10/23 19:14:27
Django学生教务选课系统毕设实战:从架构到部署全解析 简介面向高校计算机相关专业学生、毕业设计开发者以及对教务系统开发感兴趣的初学者这是一套基于 Python 与 Django 框架构建的学生教务选课系统完整源码案例。系统围绕学生、教师、管理员三类角色设计覆盖身份验证与权限控制、课程信息增删改查、在线选课、已选课程查看、选课人数统计与课表管理等核心模块Django 的 MTV 架构与 ORM 机制让业务逻辑清晰数据库建模、迁移和查询更为高效同时包含 CSRF 保护、XSS 防御等安全机制适合直接作为课程设计或毕业设计参考。压缩包共 2000 个文件以 1632 个 JavaScript 文件、250 个 HTML 页面、54 个 CSS 样式表为主另有少量 Python 程序文件、JSON/XML 配置与 TXT/MD 说明文档整体 5.66MB目录结构规整便于导入运行与按模块查阅。已有 182 人学习下载若正在完成教务选课类课题可借此掌握系统设计思路、数据库建模方法与前后端交互实现快速搭建可演示、可二次开发的项目。1. 学生教务选课系统Django 毕设项目到底该怎么跑、怎么改临近毕业季很多同学拿到的题目都是“基于 Django 的学生教务选课系统”但真正能把选课、退课、成绩查询、管理员审核这一套完整流程跑通的人并不多。这个项目正是一个典型的 Django 毕业设计源码案例——学生登录后能浏览课程列表、选课、退课、查看已选课程和成绩教师能录入成绩、查看自己开设的课程管理员负责审核课程信息、管理学生和教师账号。技术栈就是 Python Django SQLite前端用 Django 模板引擎加 Bootstrap不需要额外搭建前端工程。它适合两类人一类是正在做毕设、想找一个能跑通全流程参考实现的同学另一类是刚学完 Django 基础、想知道 MTV 架构在一个真实系统里怎么落地的开发者。下面我按实际拆项目的顺序把这套系统的架构、核心代码、运行步骤和二次开发技巧一层层剥开。2. 架构设计与数据模型先把 MTV 和表结构理清楚2.1 为什么选 Django 而不是 Flask 或 FastAPI对于这类教务选课系统我一般会优先推荐 Django而不是 Flask 或 FastAPI。核心原因有三个第一Django 自带 Admin 后台。教务系统里“管理员管理课程和学生账号”是个高频需求Django Admin 几乎是零成本实现——注册一下模型就能增删改查比用 Flask 自己写管理页面省下大量时间。第二Django 的 ORM 对学生成绩、选课记录这类关联查询非常友好比如查“某学生所有已选课程的老师是谁”用 select_related 一行就能搞定而 Flask SQLAlchemy 需要手动维护会话。第三Django 的认证体系开箱即用登录、登出、session、权限装饰器都是现成的改改配置就能用。这套项目的整体架构是经典的 Django MTV 模式Model 层负责定义学生、教师、课程、选课记录、成绩这几张核心表。Template 层Django 模板直接渲染 HTMLBootstrap 负责样式。View 层函数视图处理选课、退课、登录等业务逻辑。前后端不分离的架构虽然看着“老派”但对毕设和中小型教务场景最合适——不需要配 Vue 和 REST API服务器直接吐出渲染好的页面部署也简单一台机器加一个 WSGI 服务就能跑。2.2 目录结构一个典型 Django 项目的标准布局拆开这个源码包后目录结构是这样的student_course_system/ ├── manage.py # 项目入口脚本 ├── requirements.txt # 依赖清单 ├── config/ # 项目配置目录 │ ├── __init__.py │ ├── settings.py # 全局配置数据库、应用注册、模板路径 │ ├── urls.py # 根路由 │ └── wsgi.py ├── apps/ # 业务应用目录 │ ├── users/ # 用户模块学生、教师、登录注册 │ ├── courses/ # 课程模块课程管理、选课/退课 │ └── grades/ # 成绩模块成绩录入与查询 ├── templates/ # 全局模板目录 │ ├── base.html # 基础模板导航栏、消息提示 │ ├── login.html │ ├── course_list.html │ └── ... ├── static/ # 静态资源Bootstrap、CSS、JS │ ├── css/ │ ├── js/ │ └── images/ └── db.sqlite3 # SQLite 数据库文件这种拆分方式把用户、课程、成绩三个模块分到独立的 app 里每个 app 都有自己的 models.py、views.py、urls.py。你拿到源码后main.py 可以直接从 config/urls.py 入手看路由分发再跟进到各个 app 的视图函数。2.3 数据模型核心表设计与代码实现选课系统的核心表有 5 张我直接贴出核心的 model 代码# apps/users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): 自定义用户模型扩展 Django 自带 User添加角色字段 ROLE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 管理员), ) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) student_id models.CharField(max_length20, blankTrue, nullTrue, verbose_name学号) class Meta: db_table sys_user verbose_name 用户# apps/courses/models.py from django.db import models from apps.users.models import User class Course(models.Model): 课程表一门课程只有一个老师 name models.CharField(max_length100, verbose_name课程名称) code models.CharField(max_length20, uniqueTrue, verbose_name课程编号) credit models.FloatField(default2.0, verbose_name学分) capacity models.IntegerField(default30, verbose_name容量上限) teacher models.ForeignKey(User, on_deletemodels.CASCADE, limit_choices_to{role: teacher}, verbose_name授课教师) schedule models.CharField(max_length200, blankTrue, verbose_name上课时间) class Meta: db_table course def __str__(self): return f{self.code} {self.name} class CourseRecord(models.Model): 选课记录表学生和课程的多对多关系 student models.ForeignKey(User, on_deletemodels.CASCADE, related_namerecords, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namerecords, verbose_name课程) created_at models.DateTimeField(auto_now_addTrue, verbose_name选课时间) class Meta: db_table course_record unique_together (student, course) # 防止重复选课 class Grade(models.Model): 成绩表一个学生一门课对应一个成绩 record models.OneToOneField(CourseRecord, on_deletemodels.CASCADE, verbose_name选课记录) score models.FloatField(nullTrue, blankTrue, verbose_name成绩) grade_point models.FloatField(default0.0, verbose_name绩点)代码逻辑说明User 继承 AbstractUser这样 Django 自带登录验证和密码哈希直接可用不需要自己写注册逻辑。role 字段区分身份teacher 外键通过 limit_choices_to 限制只能选择角色为老师的用户这招在关联查询里能省掉很多脏数据问题。CourseRecord 里的 unique_together 是防重复选课的关键约束。如果不加这个约束两个并发请求同时提交选课数据库里会出现两条重复记录后面的代码逻辑就要额外做去重判断。Grade 用 OneToOneField 关联选课记录保证一门课一个学生只可能有一条成绩。参数说明credit 是 FloatField 因为很多课有 1.5 学分capacity 是选课容量用于判断课程是否已满schedule 存的是字符串实际项目中建议用 JSONField 存每周节次数组这里为了简单用了文本段。3. 核心模块拆解一次选课请求从进入到返回的完整链路3.1 路由与视图函数映射我先从 routes 入口说起root urls.py 的做法是# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(users/, include(apps.users.urls)), path(courses/, include(apps.courses.urls)), path(grades/, include(apps.grades.urls)), path(, include(apps.courses.urls)), # 首页直接展示课程列表 ]# apps/courses/urls.py from django.urls import path from . import views urlpatterns [ path(, views.course_list, namecourse_list), path(select/int:course_id/, views.select_course, nameselect_course), path(drop/int:record_id/, views.drop_course, namedrop_course), path(my/, views.my_courses, namemy_courses), ]每个 URL 都对应一个函数视图URL 里用int:course_id传递课程主键Django 自动做类型转换校验。下面把选课和退课两个最关键的函数视图拿出来逐段拆。3.2 选课逻辑并发安全的代码实现选课是教务系统最容易出问题的地方——两个学生同时抢最后一门课结果都选上了这就超过容量了。我用 Django 的事务加行锁来解决# apps/courses/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.db import transaction from django.contrib import messages from .models import Course, CourseRecord login_required transaction.atomic def select_course(request, course_id): 选课视图加事务和行锁防止超选 course get_object_or_404(Course, pkcourse_id) # 检查用户身份只有学生才能选课 if request.user.role ! student: messages.error(request, 只有学生可以选课) return redirect(course_list) # 检查是否重复选课 if CourseRecord.objects.filter(studentrequest.user, coursecourse).exists(): messages.warning(request, 你已经选过这门课了) return redirect(course_list) # 关键操作用 select_for_update 锁定课程行防止并发超选 selected_count CourseRecord.objects.filter(coursecourse).count() if selected_count course.capacity: messages.error(request, 课程已满选课失败) return redirect(course_list) CourseRecord.objects.create(studentrequest.user, coursecourse) messages.success(request, f选课成功{course.name}) return redirect(my_courses)这段代码的逻辑说明login_required保证只有登录用户能调用选课接口未登录会跳到登录页。transaction.atomic把整个选课过程包在事务里如果中途出错不会留半截数据。最核心的是select_for_update()——但注意我这里写的是普通 filter count实际在 MySQL 或 PostgreSQL 下应该先Course.objects.select_for_update().get(pkcourse_id)锁定课程行再统计选课人数。SQLite 不支行锁所以毕设里直接用 count 也能工作但如果部署到 MySQL就一定要改成先锁行再 count。我一般在代码里会写成course Course.objects.select_for_update().get(pkcourse_id)。参数说明course_id 来自 URL 的int:course_idDjango 会自动把它转成 int 传入视图request.user 是 Django 的认证中间件注入的当前登录用户对象。3.3 退课逻辑与权限控制login_required def drop_course(request, record_id): 退课视图删除选课记录 record get_object_or_404(CourseRecord, pkrecord_id, studentrequest.user) course_name record.course.name record.delete() messages.success(request, f已退课{course_name}) return redirect(my_courses)退课比选课简单直接——查到自己名下的选课记录删掉就行。有个细节get_object_or_404里带了studentrequest.user这个条件这样学生只能退自己的课就算有人猜到别人的 record_id 也无法操作别人的记录。这是 Django ORM 里常见的权限控制写法不是先查再判断而是在查询条件里直接带上当前用户查不到就 404。3.4 课程列表与我的课程def course_list(request): 课程列表显示所有课程附带已选人数和是否可选状态 if request.user.is_authenticated: user_records CourseRecord.objects.filter(studentrequest.user) selected_ids [r.course_id for r in user_records] else: selected_ids [] courses Course.objects.all().select_related(teacher) course_data [] for c in courses: course_data.append({ course: c, selected_count: c.records.count(), full: c.records.count() c.capacity, selected: c.id in selected_ids, }) return render(request, course_list.html, {course_list: course_data})参数说明c.records.count()利用了 CourseRecord 里 course 外键的 related_namerecords反向查询该课程下所有选课记录的数量。select_related(teacher)是个性能优化玄学——它会在一条 SQL 里用 LEFT JOIN 同时查出课程和老师避免每个课程循环查一遍数据库这就是血泪经验总结出来的列表页一定要用 select_related 或 prefetch_related否则数据一多页面就卡成 PPT。页面模板里用一个循环渲染课程卡片每个卡片上根据 selected 和 full 状态显示不同按钮——已选显示“已选课”满了显示“已满”可选则显示“选课”按钮。这些判断都在视图层算好模板只管展示省去了模板里写复杂逻辑的麻烦。4. 运行与部署从拿到源码到完整跑通的每一个步骤4.1 环境准备拿到项目后建议先去确认 Python 版本这个项目基于 Django 3.x/4.xPython 需要 3.8 以上。我一般习惯先建虚拟环境再装依赖# 进入项目根目录 cd student_course_system # 创建虚拟环境Windows 和 Mac 略有不同 python -m venv venv source venv/bin/activate # Linux / Mac venv\Scripts\activate # Windows # 安装依赖 pip install -r requirements.txtrequirements.txt 里的核心依赖大概有 django、pytz 等如果缺少某个版本直接pip install django装最新版一般也能跑。需要注意的是如果项目原来是用 Django 2.x 写的升级到 3.x 后url()写法会报错改成path()或re_path()即可。这一步翻车率极高先打开项目的 urls.py 看一眼写法再跑最省事。4.2 数据库迁移与初始化数据库迁移是跑通前最容易踩坑的环节# 生成迁移脚本如果源码里没有 migrations 目录的话 python manage.py makemigrations users courses grades # 执行迁移创建数据库表 python manage.py migrate # 创建管理员账号 python manage.py createsuperuser执行 migrate 后会生成 db.sqlite3 文件表结构建好了。如果你用的是项目自带的 db.sqlite3就不需要 makemigrations直接 migrate 即可。注意顺序不能反过来——先 makemigrations 再 migrate少了前者会报No changes detected。4.3 启动开发服务器python manage.py runserver看到Starting development server at http://127.0.0.1:8000/就表示成功了。浏览器打开http://127.0.0.1:8000/会跳转到登录页。用你刚才 createsuperuser 创建的账号就能登录后台http://127.0.0.1:8000/admin/但注意这个超管在系统里默认没有 student 角色去 Admin 后台找到他改一下 role 就能当学生用了。没有课程数据的话先去 Admin 后台手动添加几门课程或者执行一个数据填充脚本——项目里如果有 seed.py 这类文件直接python manage.py shell seed.py跑一遍。往下走到这一步如果页面样式乱了多半是静态文件的问题看下一个小节。4.4 最常碰到的 3 个启动问题问题一TemplateDoesNotExist。原因是模板路径配置不对。看 settings.py 里的TEMPLATES配置DIRS应该指向项目根目录下的 templates 文件夹。如果是[BASE_DIR / templates]这种写法就检查 BASE_DIR 拼接出来的路径对不对。问题二静态文件 404。开发模式下 Django 只处理动态请求静态文件需要配置# settings.py STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]urls.py 里在末尾加上 static(settings.STATIC_URL, document_rootsettings.STATICFILES_DIRS[0])才能看到 Bootstrap 样式。问题三sqlite3.OperationalError: no such table。这通常是最开始没跑 migrate 或 migrate 时报错。回退方法是把 db.sqlite3 删掉重新建。rm db.sqlite3 python manage.py migrate4.5 测试账号与初始数据跑通之后你会需要几个不同身份的账号来测试完整流程管理员一个管理课程和学生、教师一个录成绩、学生两个以上测试选课冲突。可以去 Admin 后台手动创建在 Users 页面新增用户时把 role 选成 teacher 或 student学号字段按规则填。创建完再用这些账号去前台走一遍登录、选课、退课、录成绩的完整链路确认每个环节都符合预期。5. 避坑与常见问题排查5 个高频翻车点的解决方案5.1 并发选课超员两个学生同时选了最后一门课现象两个学生同时提交选课数据库里出现了 3 条记录但课程容量只有 2。原因选课代码先 count 再 insert两个请求同时 count 到 1 一共只剩 1 个名额然后都执行 insert导致超选。解决改为事务 行锁。在 MySQL 下用Course.objects.select_for_update().get(pkcourse_id)锁住课程行再执行 count 和 create。SQLite 不支持行锁毕设可以用transaction.atomic()配合CourseRecord.unique_together保证不超选但严格来说还需要在线程层面加锁。用代码写成这样from django.db import transaction with transaction.atomic(): course Course.objects.select_for_update().get(pkcourse_id) if course.records.count() course.capacity: return redirect(course_list) CourseRecord.objects.create(studentrequest.user, coursecourse)注意 select_for_update 必须放在事务里否则 Django 会报错。5.2 学号重复但 User 表没有唯一约束现象管理员手动在 Admin 后台创建学生时能创建两个学号相同的学生。原因models.py 里 student_id 字段没有加 uniqueTrue导致系统里出现两个同号学生选课和成绩归属全乱。解决迁移时先清理重复数据然后给 student_id 加上唯一约束student_id models.CharField(max_length20, uniqueTrue, blankTrue, nullTrue)5.3 CSRF 验证失败表单提交被拒绝现象登录页输入账号密码后点登录页面报了CSRF verification failed。原因Django 对 POST 请求强制检查 CSRF Token模板里没加{% csrf_token %}。解决在所有的form标签里第一行加上{% csrf_token %}。这是一行代码的问题但每年都能遇到。5.4 静态文件加载后CSS 样式时好时坏现象刷新页面样式时有时无控制台报Not Found: /static/css/bootstrap.min.css。原因开发服务器下静态文件是通过 Django 的 staticfiles 应用处理的如果 settings.py 里DEBUG False则静态文件不再由 Django 处理。解决开发阶段把 settings.py 里的 DEBUG 保持为 True跑通后再研究生产环境的静态文件托管方案。5.5 时区导致成绩时间显示错乱现象成绩录入时间比北京时间快了 8 小时。原因settings.py 中TIME_ZONE默认是 UTCUSE_TZ是 True。解决改成TIME_ZONE Asia/Shanghai和USE_TZ False这样数据库存的就是本地时间模板渲染的时间也是本地时间。如果你保留 USE_TZTrue模板里就要用localtime过滤器去显示不然永远是 UTC 时间。6. 二次开发给选课系统加“课程冲突检测”的实战如果你想拿这个项目做毕设升级又不想只停留在原样我建议第一优先级加“课程冲突检测”。这是一个非常立体、能体现业务理解深度的功能——判断学生选新课的时间是否与已选课程重叠。课程表里 schedule 字段目前是字符串存上课时间比如“周一 3-4 节”要做冲突检测最简单的做法是把它规范化成 JSON# apps/courses/models.py from django.db import models class Course(models.Model): # schedule 改为 JSON 存 [{ day: 1, start: 3, end: 4 }, ...] schedule models.JSONField(defaultlist, blankTrue, verbose_name上课时间)然后在选课视图里加一次冲突检查# apps/courses/views.py def has_conflict(user, new_course): 检查新课程与已选课程是否时间冲突 for record in user.records.select_related(course): old_schedule record.course.schedule new_schedule new_course.schedule for o in old_schedule: for n in new_schedule: if o[day] n[day] and not (n[end] o[start] or n[start] o[end]): return True, record.course.name return False, None调用时把结果返回到前端提示。这样一个功能改动下来架构上不需要动表结构——除了把 schedule 从 CharField 换成了 JSONField。数据库迁移时需要在 Admin 里把旧数据批量转成 JSON 格式。做改造验证时我习惯先写几个单元测试# apps/courses/tests.py from django.test import TestCase from .models import Course, CourseRecord # 测试用例造两个时间错开的课程断言没有冲突再造两个时间重叠的课程断言 has_conflict 返回 True。测试过了再放到页面上联调避免每次手动点页面回归。从那以后我每次拿到新项目都强制走一遍“先跑通、再加功能、再写测试”的顺序改到哪层都不慌。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询