尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Django学生选课系统:ORM设计、事务与Admin后台实践
简介一个使用 Django 框架开发的学生选课管理系统源码包面向 Python 初学者的 Web 实战项目可帮助掌握 MVC 架构在真实项目中的落地方式。系统包括学生登录认证、课程浏览、选课与退选等基本功能完整涉及模型定义、视图函数、URL 路由、模板渲染和表单处理等 Django 核心知识点。压缩包内共 57 个文件其中 35 个 Python 文件用于业务逻辑和数据模型14 个 HTML 模板负责页面结构8 个 CSS 文件实现界面样式整体约 40KB小巧紧凑目录清晰包含 Django 项目启动入口、核心应用模块、静态资源目录与页面模板目录便于按模块逐个研读。从数据库表结构设计到服务启动再到浏览器页面交互整条开发链路都能在这个精简项目里找到对应实现。目前已有 787 人学习参考。借助该项目可以直观理解 Django 内置用户认证与权限控制、ORM 对象关系映射的增删改查操作、表单数据提交与校验以及基础测试的编写方法同时源码包含完整工程配置与静态文件组织方式可作为课程设计、毕业设计或自学练习的改造起点是学习 Django 开发不可多得的入门素材也是快速搭建管理类系统的参照模板。 用 Python 和 Django 写学生选课管理系统GitHub 上能翻到不少但多数是图省事的单文件 demo。我拆了一份标注 student-management-system-master 的源码包发现它的组织方式很接近真实项目xuanke管配置course与user两个应用分管课程和账号templates里放页面static里连源头 scss 都保留了根目录还丢着constants.py和small.py这类辅助脚本。这个系统能覆盖的正好是 Django 初学者最容易卡住的几件事模型怎么设计选课关系、视图怎么承接选课 POST 请求、模板怎么写课程列表、登录认证和后台管理怎么复用现成组件。适合两类人刚学完 Django 基础想把 MTV 每一层串起来的人和需要一份课程设计交付物的人。2. 数据模型层学生、课程、选课关系的 ORM 设计与迁移2.1 拆模型的思路学生和课程是资源选课是关系先看course/models.py。它把人和课拆成独立资源再用中间表记录二者关系。这个思路对应的是选课业务里最常见的约束一个学生能选多门课一门课能被多个学生选是多对多关系。直接用ManyToManyField可以生成关联表但自动生成的表没有选课时间成绩这类附加字段所以实际项目里更常自己建一张显式的关系表把状态挂在关系上而不是挂在学生或课程上。# course/models.py from django.contrib.auth.models import User from django.db import models class Course(models.Model): name models.CharField(课程名称, max_length128) teacher models.CharField(授课教师, max_length64) capacity models.PositiveIntegerField(容量, default60) selected models.PositiveIntegerField(已选人数, default0) credit models.DecimalField(学分, max_digits3, decimal_places1, default2.0) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return self.name class Enrollment(models.Model): student models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) selected_at models.DateTimeField(auto_now_addTrue, verbose_name选课时间) grade models.CharField(成绩, max_length16, blankTrue, nullTrue) class Meta: unique_together (student, course) verbose_name 选课记录Course上的selected是冗余字段作用是把还剩多少名额这种高频查询做成一次整型读取而不是每打开一次列表页就全表count()一次。Enrollment用unique_together从数据库层保证同一学生对同一课程只有一条记录on_deletemodels.CASCADE表示学生或课程删除时其选课记录一并清除避免残留脏数据。参数方面capacity用PositiveIntegerField限定非负credit用DecimalField存两位小数grade允许为空因为选课前成绩还没有值。注意到Course.__str__返回name后面 Admin 后台会直接拿这个字段做展示不写的话后台列表里全是Course object (1)这类无意义文本。2.2 迁移与在 Python 脚本里直接查数据模型定义完成后要同步到数据库。工程里既有manage.py也有根目录的small.py后者通常用来跑一次性脚本比如批量初始化课程数据。在做这类操作前先执行迁移python manage.py makemigrations course user python manage.py migrate第一步根据模型变化生成迁移文件第二步把迁移应用到数据库。后接的course user是应用名限定只处理这两个应用的模型避免每次改动都重新扫描全项目。如果数据库换成了 MySQL还要在settings.py里改DATABASES配置并安装mysqlclientWindows 下mysqlclient编译需要 Visual C 构建工具用pymysql也能顶但要在__init__.py里执行pymysql.install_as_MySQLdb()否则 Django 会误以为驱动缺失。迁移完成后可以用 Django shell 或写进small.py快速验证数据关系不用频繁启动 dev server# small.py项目根目录执行 python small.py import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, xuanke.settings) django.setup() from django.contrib.auth.models import User from course.models import Course, Enrollment student User.objects.create_user(stu001, password123456) course Course.objects.create(namePython程序设计, teacher张老师, capacity60) Enrollment.objects.create(studentstudent, coursecourse) print(Enrollment.objects.count()) # 1 print(course.selected) # 0冗余字段不会自动 1 Enrollment.objects.filter(coursecourse).delete() # 删除该课程所有选课记录django.setup()之前必须设置DJANGO_SETTINGS_MODULE指向项目的配置模块否则模型加载不到 settings。可以从输出看到course.selected不会因为Enrollment的创建而自动增长所以后面讲选课视图时我会在事务里同时完成插入选课记录和已选人数 1两个动作。filter(...).delete()是 Django 标准的查询删除方式返回一个元组第一位是被删除的记录数调试时靠这个数判断删除是否命中。2.3 冗余字段与关系表字段的取舍方案适用场景代价ManyToManyField直接生成关联表只记录谁选了哪门课无附加字段无法携带成绩、选课时间显式Enrollment表 through需要成绩、时间、退课原因等状态查询多一次 JOIN代码量增加Course.selected冗余存储列表页高频显示剩余名额必须在事务内同步维护否则不一致这个取舍在 Django 面试里常被问到。课程设计阶段保留冗余字段逻辑直观且演示效果好生产环境我更倾向去掉selected在事务里用锁配合count()实时统计把一致性完全交给数据库。3. 视图与 URL 路由选课核心业务逻辑怎么落进代码3.1 从登录到选课一次 POST 请求的完整链路视图可以写成函数式 FBV也可以写成类式 CBV。这个工程登录页偏 CBV选课操作用 FBV 反而更直观。完整链路是学生登录 → 课程列表页显示可选课程 → 点选课时提交POST /course/select/course_id/→ 视图校验登录态、课程存在性、是否重复选课、名额是否足够 → 事务内写入Enrollment并更新selected→ 重定向回课程列表并带上提示消息。# course/views.py from django.contrib.auth.decorators import login_required from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.contrib import messages from .models import Course, Enrollment login_required def course_list(request): courses Course.objects.all() selected_ids Enrollment.objects.filter( studentrequest.user ).values_list(course_id, flatTrue) return render(request, course/index.html, { courses: courses, selected_ids: list(selected_ids), }) login_required transaction.atomic def select_course(request, course_id): course get_object_or_404(Course, pkcourse_id) if Enrollment.objects.filter(studentrequest.user, coursecourse).exists(): messages.warning(request, 你已经选过这门课程) return redirect(reverse(course:list)) if course.selected course.capacity: messages.error(request, 该课程名额已满) return redirect(reverse(course:list)) Enrollment.objects.create(studentrequest.user, coursecourse) course.selected 1 course.save(update_fields[selected]) messages.success(request, f选课成功{course.name}) return redirect(reverse(course:list))login_required是 Django 内置的登录校验装饰器未登录请求会重定向到settings.LOGIN_URLtransaction.atomic把插入选课记录和更新已选人数放进同一个数据库事务任何一个失败都整体回滚这是保证selected与真实记录不脱节的关键。get_object_or_404在课程不存在时返回 404省去手动 try/except。messages是会话消息框架先放到request里下一轮请求由模板渲染。参数course_id由 URL 路由捕获对应关系在urls.py中# course/urls.py from django.urls import path from . import views app_name course urlpatterns [ path(, views.course_list, namelist), path(select/int:course_id/, views.select_course, nameselect), ]int:course_id是路由转换器限定该段是整数并把值作为参数传给select_course。app_name course声明命名空间多个应用同时存在时各自的name不会撞名视图里用reverse(course:list)反解 URL模板里则写{% url course:list %}。这就是django reverse的常态用法不把/course/select/3/这种字面量写死在代码里今后改 URL 结构时只动urls.py。3.2 校验顺序与 PRG 模式为什么选课后要 redirect上面的代码先判是否已选再判名额已满。两个条件谁先跑都行但必须固定因为当名额只剩 1 且当前学生已选时先判容量提示名额已满先判重复则提示已经选过文案直接影响用户体验。constants.py里通常放的就是这类状态码与提示文案集中管理比散落在视图里更容易在验收时改文案。选课成功后我用redirect而不是render直接渲染模板这是 POST-Redirect-GET 模式。如果render返回页面用户按 F5 刷新时浏览器会重新提交上一次的 POST 表单造成重复选课302 之后刷新走的是 GET请求是幂等的不会产生新记录。对选课这种东西这条线必须守住。3.3 并发选课与超卖问题课程设计阶段经常忽略并发两个学生同时提交选课两个请求都读到selected 59各自加 1 后变成 61出现超选。规避办法是在更新前对课程行加锁把select_course里的查询改成select_for_update()login_required transaction.atomic def select_course(request, course_id): course get_object_or_404( Course.objects.select_for_update(), pkcourse_id ) # 后续重复选课、容量判断、写入记录逻辑与上面一致select_for_update会在事务里锁住课程行直到提交前其他事务读同一行会被阻塞配合Enrollment上的unique_together等于上了行锁 唯一约束双保险。注意它只对 InnoDB 这类支持行锁的数据库生效SQLite 默认整库锁效果等同于全局锁。面试聊到选课系统怎么防超卖时能说出这一层比只说我加了判断更有说服力。4. 模板与表单课程列表和选课确认页面的动态渲染4.1 用模板继承复用页面骨架Django 模板语言负责把视图传来的 Python 对象渲染成 HTML。templates目录下通常有一份base.html做骨架其余页面继承它导航、底部版权、公共 CSS 只维护一份。static里有 scss 源码说明前端样式经过编译本地调试DEBUG True时 Django 自动托管静态文件部署到宝塔这类环境要先执行python manage.py collectstatic把分散的静态资源集中到STATIC_ROOT再由 Nginx 直接供给否则页面只有 HTML 没有样式。!-- templates/base.html -- {% load static %} !doctype html html langzh-CN head meta charsetutf-8 title{% block title %}学生选课系统{% endblock %}/title link relstylesheet href{% static css/app.css %} /head body nav {% if user.is_authenticated %} span{{ user.username }}/span a href{% url user:logout %}退出/a {% else %} a href{% url user:login %}登录/a {% endif %} /nav {% if messages %} ul classmessages {% for msg in messages %}li{{ msg }}/li{% endfor %} /ul {% endif %} {% block content %}{% endblock %} /body /html代码里{% load static %}必须先声明后面的{% static css/app.css %}才能解析。user.is_authenticated来自 Django 的上下文处理器判断当前会话是否已登录messages就是上一章视图里messages.success写入的内容渲染成列表展示给用户。{% block content %}是子模板要填充的插槽继承这个骨架的页面只需要重写这一块。4.2 课程列表与三种选课状态首页course/index.html是核心页面需要同时呈现课程信息、剩余名额并区分可选 / 已选 / 已满三种状态。视图里我已经把selected_ids转成列表传给模板模板端判断成本很低!-- templates/course/index.html -- {% extends base.html %} {% block content %} table classtable thead trth课程/thth教师/thth学分/thth名额/thth操作/th/tr /thead tbody {% for course in courses %} tr td{{ course.name }}/td td{{ course.teacher }}/td td{{ course.credit }}/td td{{ course.selected }}/{{ course.capacity }}/td td {% if course.id in selected_ids %} span classbadge已选/span {% elif course.selected course.capacity %} span classbadge已满/span {% else %} form methodpost action{% url course:select course.id %} {% csrf_token %} button typesubmit选这门课/button /form {% endif %} /td /tr {% endfor %} /tbody /table {% endblock %}{% url course:select course.id %}等价于视图里的reverse(course:select, args[course.id])由路由名加参数反解析出真实 URL。{% csrf_token %}是 Django 强制的安全令牌所有 POST 表单缺了它会被CsrfViewMiddleware拒绝并返回 403。选课按钮刻意放在form里而不是用a链接因为选课是改变数据的操作语义上必须走 POST这样浏览器刷新、后退时不会因为 GET 缓存而重复提交选课请求。course.id in selected_ids这个判断之所以能写是视图提前把QuerySet转成了普通列表模板语言里对QuerySet做in判断会触发额外的数据库查询数据量大时有性能坑。4.3 表单类的选择什么时候值得用 Django FormDjango 的forms模块能生成表单并做服务端校验但选课动作只有一个csrf_token和一个提交按钮用Form类反而多余。更合理的做法是字段多、校验规则复杂的场景才用Form。比如学生注册可以直接做UserCreationForm的子类再组合一个记录学号、班级的StudentProfileForm选课动作就用模板手写表单 视图校验代码量更少出问题时定位也更快。这个判断标准不只是写给初学者项目里真正需要Form的往往是注册、资料编辑、退课原因填写这类输入密集的页面而不是所有按钮都要包一层 form class。5. 用户认证复用与 Admin 后台不写独立页面做出管理端5.1 复用内置 User 与登录视图user应用承担认证职责。不少课程设计里登录注册都是自己建 User 表再手工处理密码加密与 session其实django.contrib.auth已经把整套做完了密码默认 PBKDF2 哈希登录视图直接用LoginView覆盖模板即可不需要自己接收 POST、比对密码、写 session。# user/urls.py from django.contrib.auth import views as auth_views from django.urls import path from . import views urlpatterns [ path(login/, auth_views.LoginView.as_view( template_nameuser/login.html, redirect_authenticated_userTrue, ), namelogin), path(logout/, auth_views.LogoutView.as_view(), namelogout), path(register/, views.register, nameregister), ]LoginView.as_view()接收template_name指定登录页模板redirect_authenticated_userTrue表示已登录用户再访问登录页时直接跳走。登录成功后默认跳settings.LOGIN_REDIRECT_URL未登录访问login_required页面时跳settings.LOGIN_URL这两个配置必须在xuanke/settings.py里配对设好否则会出现登录成功回不到课程列表、直接落到 Django 默认首页的问题。5.2 把选课信息做进 Admin 后台管理端是这份源码最容易被低估的部分。python manage.py createsuperuser建管理员后/admin/已经能用但默认只管理 User 和 Group。想直接在后台看课程、看选课名单、按课程筛选只需在course/admin.py注册模型# course/admin.py from django.contrib import admin from .models import Course, Enrollment class EnrollmentInline(admin.TabularInline): model Enrollment extra 0 autocomplete_fields [course] admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (name, teacher, credit, capacity, selected) search_fields (name, teacher) inlines [EnrollmentInline] admin.register(Enrollment) class EnrollmentAdmin(admin.ModelAdmin): list_display (student, course, selected_at) list_filter (course,)autocomplete_fields依赖关联模型配置search_fieldsCourseAdmin里已经声明所以编辑选课记录时课程下拉框可以输入搜索记录量大时比普通下拉框好用得多。list_filter提供左侧筛选验收时可以直接按课程过滤选课名单。这些配置做下来后台界面虽然不是花哨定制但已经足够撑起一个管理端的演示流程想再提升观感可以覆写admin/base_site.html或引入 django-admin-interface那就是纯加分项了。5.3 用 Django TestCase 给选课流程做冒烟验证# course/tests.py from django.test import TestCase from django.contrib.auth.models import User from .models import Course, Enrollment class SelectCourseTests(TestCase): def setUp(self): self.student User.objects.create_user(stu002, passwordp) self.course Course.objects.create(name数据库原理, capacity1) def test_duplicate_select_rejected(self): self.client.login(usernamestu002, passwordp) url f/course/select/{self.course.id}/ self.client.post(url) resp self.client.post(url) self.assertEqual(Enrollment.objects.count(), 1) self.assertEqual(self.course.selected, 1)setUp在每个用例执行前准备独立数据self.client是 Django 内置测试客户端直接发起请求不需要启动服务器。这个用例验证重复选课被拦截第二次 POST 后选课记录仍是 1 条课程selected没有被重复加一。跑用例时注意测试库必须支持事务回滚SQLite 内存库在默认配置下即可换成 MySQL 后需要在测试配置里同时启用事务否则断言里的Enrollment.objects.count()会读到旧快照。先把这个用例跑绿再继续加退课、改选这些分支覆盖。本文还有配套的精品资源点击获取
RELATED

相关推荐

嵌入式开发强度真相:C语言、单片机与RTOS的物理级工程实践

嵌入式开发强度真相:C语言、单片机与RTOS的物理级工程实践

1. 这不是劝退帖,是26年嵌入式老兵掏心窝子的“强度说明书”“实话难听”这四个字,我写在标题里,不是为了制造焦虑,而是怕你花三年时间学完C语言、单片机、RTOS,最后发现连一个能稳定跑三天不崩的Modbus从机驱动都调不…

📅 2026/9/13 20:35:10
RAG技术演进:四大优化方向解析与实践

RAG技术演进:四大优化方向解析与实践

1. RAG技术演进与优化需求在大模型应用开发领域,检索增强生成(Retrieval-Augmented Generation)已经成为解决大模型幻觉问题和知识更新的关键技术。传统RAG采用"检索-生成"的线性流程,但在实际应用中暴露出三个典型问题…

📅 2026/9/13 20:30:09
梯度下降原理与实战:从数学直觉到工业级调优

梯度下降原理与实战:从数学直觉到工业级调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/13 20:30:09
MORE NEWS

更多资讯

📰

LeetCode-Go 动态规划实战:用 Go 实现带障碍物的路径计数(Unique Paths II,第 63 题)

LeetCode-Go 动态规划实战:用 Go 实现带障碍物的路径计数(Unique Paths II,第 63 题) 【免费下载链接】LeetCode-Go ✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解 项目地址: https…

📰

OpenAI Agents SDK 怎么改默认模型并调整 GPT-5 推理参数

OpenAI Agents SDK 怎么改默认模型并调整 GPT-5 推理参数 【免费下载链接】openai-agents-python A lightweight, powerful framework for multi-agent workflows 项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python 在 openai-agents-python&…

📰

Ruby 语法级计算器:TRICK 2025 获奖作品「纯 Ruby 语法加法」原理与玩法

Ruby 语法级计算器:TRICK 2025 获奖作品「纯 Ruby 语法加法」原理与玩法 【免费下载链接】ruby The Ruby Programming Language 项目地址: https://gitcode.com/GitHub_Trending/ru/ruby 本文基于 Ruby 仓库中 TRICK 2025(第 5 届 Transcendental…

📰

CAMEL Loaders 模块完全指南:从文档解析、网页抓取到 RAG 数据接入

CAMEL Loaders 模块完全指南:从文档解析、网页抓取到 RAG 数据接入 【免费下载链接】camel 🐫 CAMEL: The first and the best multi-agent framework. Finding the Scaling Law of Agents. https://www.camel-ai.org 项目地址: https://gitcode.com/G…

📰

3条命令跑通 kohya_ss Docker 部署:从克隆到训练界面

3条命令跑通 kohya_ss Docker 部署:从克隆到训练界面 【免费下载链接】kohya_ss 项目地址: https://gitcode.com/GitHub_Trending/ko/kohya_ss 第三次在 pip install torch 的 CUDA 版本报错里打转时,可以停下了:kohya_ss 是 Stable …

📰

Lithe-IDEA:Rust+JavaFX打造的轻量开源Java IDE

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬