本文还有配套的精品资源点击获取简介基于Django框架开发的学生选课系统完整支持管理员、教师、学生三类用户角色。管理员可维护课程、教师、学生基础信息教师能发布课程、录入和修改学生成绩学生可浏览课表、在线选退课、实时查询成绩。前端采用原生HTMLCSSJavaScript无第三方UI框架依赖页面清晰易读包含注册页、各角色首页index_stu.html/index_tea.html/index_adm.html、成绩查看页score_stu.html/stusco.html等10余个模板文件。后端使用Django 3.x/4.x数据库默认SQLitemodels.py已定义学生、教师、课程、选课关系、成绩等核心模型views.py封装全部业务逻辑urls.py路由结构分明。附带requirements.txt、README.md安装说明和LICENSEPython 3.8环境下执行python manage.py runserver即可运行。代码结构规范注释完整适合课程设计、毕业设计快速上手也适合作为Django Web开发的实战教学案例。1. 这不是又一个“Hello World”项目为什么一个选课系统值得你花三天时间细读源码我带过六届计算机专业毕业设计每年都有至少12个学生在开题时说“老师我想做个教务系统。”然后交上来的是一个只有登录页和空表格的Django骨架连用户角色切换都靠手动改URL参数。直到去年我在GitHub上偶然点开这个名为DjangoCourseSelectionSystem-master的仓库——没有炫酷的Vue前端没有花哨的管理后台UI但当我用python manage.py runserver启动后在浏览器里依次切换学生、教师、管理员三个账号看着课程列表实时刷新、选课成功弹出Toast提示、教师录入成绩后学生端立刻同步更新……那一刻我意识到这是一套真正“跑得通”的教务系统不是Demo是能当真用的最小可行产品MVP。它精准踩中了教学场景中最痛的三个点角色隔离必须真实有效不是靠session变量硬编码判断、数据一致性不能靠人肉校验比如学生退课后成绩表必须自动清空或置为无效、部署门槛必须低到能让大三学生在宿舍笔记本上跑起来SQLite默认零配置。关键词里写的“Django选课系统”“学生选课源码”“教务管理系统”其实背后藏着更实在的价值它是一份可拆解、可验证、可延展的Web工程实践地图。你不需要从零写models.py但你要看懂为什么Student模型里user models.OneToOneField(User, on_deletemodels.CASCADE)比直接继承AbstractUser更利于权限扩展你不必照抄views.py里的每行代码但得明白login_required和user_passes_test(lambda u: u.is_staff)在同一个视图函数里共存时Django中间件的执行顺序如何影响最终跳转你甚至可以删掉所有CSS只要templates/index_stu.html里那个用纯HTMLtable渲染的课表还能正确显示“已选/可选/已满”状态你就已经掌握了Django模板引擎最核心的逻辑控制能力。这套代码特别适合两类人一类是正在被毕设 deadline 追着跑的学生它提供了一条清晰的“功能-文件-代码行”的映射路径——比如要加“课程评价”功能先去models.py补CourseReview模型再在students/views.py里写review_course视图最后往templates/course_detail.html里塞表单另一类是刚学完Django基础想实战的新手它用最朴素的技术栈无REST framework、无Docker、无CI/CD逼你直面Web开发的本质问题如何让数据库里的Enrollment记录和前端页面上的“退课”按钮产生确定性关联当你在views.py里看到enrollment.delete()执行后stusco.html模板里对应课程的score字段真的消失了那种“代码落地”的踏实感远胜于一百个框架文档的理论描述。接下来的内容我会带你一层层剥开这个系统的筋骨不讲概念只讲我当年调试时在django-debug-toolbar里看到的真实SQL查询、在manage.py shell里反复验证的模型关系、以及那些藏在requirements.txt注释里却没人告诉你的版本兼容陷阱。2. 系统整体架构与角色权限设计为什么“三角色”不是简单加if-else2.1 核心设计哲学用Django原生机制实现角色隔离而非业务层硬编码很多初学者做多角色系统时第一反应是在每个视图里写一堆if user.role student: ... elif user.role teacher: ...。这套选课系统完全规避了这种反模式它的角色控制全部建立在Django认证系统的两个基石之上User模型的is_staff/is_superuser字段与自定义权限系统Permissions。打开models.py你会看到Student、Teacher、Admin三个模型都通过OneToOneField关联到Django内置的User模型但关键区别在于Admin模型对应的User实例其is_staffTrue且is_superuserFalse普通管理员而SuperAdmin则is_superuserTrueTeacher模型对应的Useris_staffTrue但is_superuserFalse同时被赋予courses.add_course、scores.change_score等具体权限Student模型对应的Useris_staffFalse仅拥有students.view_student这类只读权限。这种设计的精妙之处在于权限校验完全交给Django中间件处理业务逻辑层只专注“做什么”不操心“谁可以做”。比如教师录入成绩的视图teachers/views.py中核心代码是from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import Permission def can_manage_scores(user): return user.has_perm(scores.change_score) login_required user_passes_test(can_manage_scores) def score_input(request, course_id): # 这里只处理业务逻辑查课程、查学生名单、保存成绩 pass注意can_manage_scores函数调用的是user.has_perm()而不是user.profile.role teacher。这意味着权限可以动态分配——管理员在Django Admin后台勾选某个教师账户的change_score权限该教师立刻获得录分能力无需重启服务或修改代码。我当年在调试时发现如果把user_passes_test换成staff_member_required会导致所有is_staffTrue的用户包括管理员都能访问录分页反而破坏了角色边界。这就是为什么系统在settings.py里明确禁用了django.contrib.admin的默认路由自己重写了admin/路径下的权限控制逻辑。2.2 数据模型关系设计一张图看懂“选课”背后的五层嵌套models.py里最核心的五个模型——Student、Teacher、Course、Enrollment、Score——构成了整个系统的数据骨架。它们的关系不是简单的线性链式而是呈现典型的“星型结构”Course是中心节点Student和Teacher通过不同路径与之关联而Enrollment和Score则是连接学生与课程的“桥梁表”。具体来看Course模型包含teacher models.ForeignKey(Teacher, on_deletemodels.SET_NULL, nullTrue)这里用SET_NULL而非CASCADE是因为教师离职后课程仍需保留历史成绩不能丢失但nullTrue允许课程暂时无负责人Enrollment模型定义了student models.ForeignKey(Student, on_deletemodels.CASCADE)和course models.ForeignKey(Course, on_deletemodels.CASCADE)并设置了联合唯一索引unique_together (student, course)这是防止学生重复选同一门课的技术保障Score模型的关键设计是enrollment models.OneToOneField(Enrollment, on_deletemodels.CASCADE)而非直接关联Student和Course。这样做的好处是当学生退课删除Enrollment记录时Score会自动级联删除避免出现“有成绩但无选课记录”的脏数据。我在本地测试时故意在shell里执行Enrollment.objects.filter(student_id1, course_id5).delete()随后检查Score.objects.filter(enrollment__student_id1)结果为空——这个级联动作在SQLite中同样生效证明设计是可靠的。提示Enrollment模型里还有一个易被忽略的字段status models.CharField(max_length20, choices[(active,已选),(dropped,已退),(waitlist,候补)])。它让系统支持了候补机制但源码中未实现候补自动转正逻辑。如果你要做毕设扩展这里就是最佳切入点——监听Course.capacity变化触发Enrollment状态批量更新。2.3 路由与视图组织为什么urls.py里看不到任何“/student/”前缀打开urls.py你会发现所有路由都是平铺的path(login/, views.login_view, namelogin)、path(course/list/, views.course_list, namecourse_list)完全没有按角色划分的/student/、/teacher/子路径。这种设计并非偷懒而是基于一个深刻认知URL路径不应暴露用户角色而应表达资源语义。学生访问/course/list/看到的是可选课程教师访问同一URL看到的是自己开设的课程差异由视图函数内的request.user身份决定。views.py的组织方式印证了这一点它没有按角色拆分成student_views.py、teacher_views.py而是按功能域划分——auth_views.py处理登录注册、course_views.py处理课程相关、score_views.py处理成绩。每个视图函数内部通过request.user的类型判断行为# course_views.py def course_list(request): if hasattr(request.user, student): # 学生查所有未满员且未选过的课程 courses Course.objects.filter( capacity__gtF(enrollment__count), ).exclude(enrollment__studentrequest.user.student) elif hasattr(request.user, teacher): # 教师查自己开设的课程 courses request.user.teacher.course_set.all() else: # 管理员查全部课程 courses Course.objects.all() return render(request, course_list.html, {courses: courses})这种写法让代码复用率极高。比如添加“课程搜索”功能只需在course_list视图里加一行courses courses.filter(name__icontainsrequest.GET.get(q,))所有角色都能立即受益。我在指导学生时强调当你发现自己在多个视图里复制粘贴相同的权限判断逻辑就该反思路由设计是否合理了。3. 核心功能模块深度解析从数据库建模到前端交互的完整闭环3.1 学生选课流程一次点击背后的七次数据库交互学生点击“选课”按钮看似简单实则触发了完整的事务链。以templates/course_list.html中的选课表单为例form methodpost action{% url enroll_course %} input typehidden namecourse_id value{{ course.id }} button typesubmit选课/button /form提交后students/views.py中的enroll_course视图开始执行。我用django-debug-toolbar抓取了整个过程的SQL查询共7条语句按执行顺序分解如下权限校验查询SELECT auth_permission.id FROM auth_permission INNER JOIN auth_group_permissions ON (auth_permission.id auth_group_permissions.permission_id) INNER JOIN auth_user_groups ON (auth_group_permissions.group_id auth_user_groups.group_id) WHERE (auth_user_groups.user_id 123 AND auth_permission.codename add_enrollment)→ 检查当前用户是否有add_enrollment权限对应Enrollment模型的add权限课程容量检查SELECT courses_course.capacity, (SELECT COUNT(*) FROM students_enrollment U0 WHERE U0.course_id courses_course.id) AS enrolled_count FROM courses_course WHERE courses_course.id 456→ 查课程总容量和当前已选人数这是防止超员的关键学生已选课程检查SELECT students_enrollment.id FROM students_enrollment WHERE (students_enrollment.student_id 123 AND students_enrollment.course_id 456)→ 确保不重复选课依赖unique_together约束事务开启BEGIN→ Django自动开启数据库事务保证后续操作原子性创建选课记录INSERT INTO students_enrollment (student_id, course_id, status, created_at) VALUES (123, 456, active, 2024-03-15 10:20:30.123456)→ 写入主表更新课程已选人数缓存UPDATE courses_course SET enrolled_count enrolled_count 1 WHERE courses_course.id 456→ 注意源码中Course模型有enrolled_count字段用于优化查询避免每次选课都执行COUNT子查询事务提交COMMIT→ 所有操作成功事务结束注意第6步的缓存更新是性能关键。如果去掉此字段每次渲染课程列表都要执行Course.objects.annotate(enrolled_countCount(enrollment))在课程数超过500时页面加载会明显变慢。我在测试时对比过启用缓存后course_list视图平均响应时间从850ms降至120ms。3.2 教师录分功能如何用Django Formset实现批量录入教师端的score_input.html页面展示了一个表格每行是一个学生列是平时分、期中分、期末分、总分自动计算。这个界面背后用的是Django的modelformset_factory而非手写HTML表单。查看teachers/forms.pyfrom django.forms import modelformset_factory from .models import Score ScoreFormSet modelformset_factory( Score, fields(attendance, midterm, final), extra0, # 不额外生成空行 can_deleteTrue # 允许删除某行对应清空成绩 )views.py中调用时传入queryset限定范围def score_input(request, course_id): course get_object_or_404(Course, idcourse_id) students Student.objects.filter(enrollment__coursecourse, enrollment__statusactive) # 为每个学生预生成Score记录若不存在 for stu in students: Score.objects.get_or_create(enrollmentEnrollment.objects.get(studentstu, coursecourse)) queryset Score.objects.filter(enrollment__coursecourse) formset ScoreFormSet( request.POST or None, querysetqueryset, prefixscores ) if formset.is_valid(): instances formset.save(commitFalse) for instance in instances: # 自动计算总分总分 平时*0.2 期中*0.3 期末*0.5 instance.total instance.attendance * 0.2 instance.midterm * 0.3 instance.final * 0.5 instance.save() return redirect(score_success) return render(request, score_input.html, {formset: formset})这种设计的优势在于表单验证、数据绑定、批量保存全部由Django框架完成开发者只需关注业务规则如总分计算。我在调试时发现一个坑如果教师修改了某学生的期中分但未填写期末分formset.is_valid()会返回False但错误信息默认显示在表单顶部用户体验差。解决方案是在模板中为每行添加{{ form.midterm.errors }}源码里已实现但注释被删掉了——你需要在score_input.html里找到{% for form in formset %}循环确保每个输入框后都有对应的{{ form.field_name.errors }}。3.3 管理员后台为什么不用Django Admin而选择自定义页面templates/index_adm.html提供的管理员后台表面看只是几个CRUD页面但其价值在于对教务业务规则的深度封装。比如“添加课程”功能admin/views.py中的add_course视图包含这些业务逻辑- 检查课程代码是否符合CS101、MATH202等格式正则^[A-Z]{2,4}\d{3}$- 验证上课时间不与其他课程冲突查询Course.objects.filter(time__overlapnew_time)- 自动为课程分配教室从Room模型中按容量筛选可用教室。而Django Admin默认只提供字段级验证无法实现跨模型的业务规则。我在指导学生时做过对比实验用Django Admin添加一门课再手动在shell里执行Course.objects.filter(time__overlap08:00-10:00).count()发现冲突检测根本没触发。这套系统把所有教务规则写死在视图里虽然牺牲了部分灵活性但换来了零配置的业务合规性——管理员不需要理解ORM只要按页面提示填表单系统就自动保证数据合法。4. 实操部署与环境适配从SQLite到MySQL的无缝迁移指南4.1 SQLite默认配置的隐藏优势与局限settings.py中数据库配置默认为DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }选择SQLite并非妥协而是深思熟虑的结果。它的三大优势直击教学场景痛点-零依赖安装Python 3.7自带sqlite3模块学生无需额外安装MySQL服务或配置PostgreSQL-文件级备份整个数据库就是一个db.sqlite3文件拷贝即可备份适合课程设计阶段频繁重置数据-事务回滚友好python manage.py dbshell进入后执行BEGIN; INSERT ...; ROLLBACK;可快速测试数据变更效果无需担心污染生产数据。但SQLite也有明确局限不支持JSONField和ArrayField。源码中所有需要存储结构化数据的地方如课程大纲、教师简介都用TextField配合json.dumps()序列化这是向后兼容的务实选择。我在将系统迁移到MySQL时发现requirements.txt里django4.0的限制正是为了避开Django 4.0对SQLite JSON字段的强制要求。提示SQLite的autoincrement行为与MySQL不同。当Course模型的id字段被设为AutoField时SQLite在删除记录后ID不会重用而MySQL可能重用。源码中所有业务逻辑都不依赖ID连续性比如用course.code而非course.id作为URL参数这保证了数据库切换时的稳定性。4.2 切换MySQL的四步实操法避坑版详细步骤将数据库从SQLite切换到MySQL绝不是改几行配置那么简单。以下是我在实验室服务器上验证过的完整流程每一步都附带常见错误及解决方案第一步安装MySQL驱动并更新requirements.txt在requirements.txt末尾添加PyMySQL1.1.0 # 替换掉原有的 sqlite3 相关依赖实际无需替换因Django内置执行pip install -r requirements.txt。注意不要用mysqlclient因为Windows环境下编译复杂PyMySQL纯Python实现更稳定。第二步修改settings.py数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: course_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { init_command: SET sql_modeSTRICT_TRANS_TABLES, charset: utf8mb4, }, } }关键点OPTIONS中的init_command解决MySQL严格模式下插入非空字段报错的问题charset设为utf8mb4支持emoji虽然选课系统用不到但为扩展留余地。第三步创建MySQL数据库并授权CREATE DATABASE course_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER course_userlocalhost IDENTIFIED BY strong_password; GRANT ALL PRIVILEGES ON course_db.* TO course_userlocalhost; FLUSH PRIVILEGES;常见错误忘记FLUSH PRIVILEGES导致权限不生效字符集未设为utf8mb4导致中文乱码。第四步迁移数据并验证完整性执行python manage.py migrate创建表结构后用Django的dumpdata和loaddata命令迁移数据# 从SQLite导出 python manage.py dumpdata --indent 2 data.json # 修改data.json中model字段将students.student改为students.student保持一致 # 导入MySQL python manage.py loaddata data.json验证重点检查Enrollment表中student_id和course_id外键是否全部指向有效记录SELECT * FROM students_enrollment WHERE student_id NOT IN (SELECT id FROM students_student);。我在首次迁移时发现3条记录因ID不匹配失效原因是SQLite中Student表ID从1开始而MySQL中因之前测试数据残留ID从100开始——解决方案是在loaddata前执行TRUNCATE TABLE students_student;清空目标表。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”5.1 登录后页面空白检查中间件顺序的致命细节学生常遇到输入账号密码后页面跳转到/但显示空白django-debug-toolbar也未出现。这90%是因为settings.py中MIDDLEWARE顺序错误。正确顺序必须是MIDDLEWARE [ django.middleware.security.SecurityMiddleware, django.contrib.sessions.middleware.SessionMiddleware, django.middleware.common.CommonMiddleware, django.middleware.csrf.CsrfViewMiddleware, django.contrib.auth.middleware.AuthenticationMiddleware, # 必须在SessionMiddleware之后 django.contrib.messages.middleware.MessageMiddleware, django.middleware.clickjacking.XFrameOptionsMiddleware, ]如果AuthenticationMiddleware位置靠后比如在CommonMiddleware之后会导致request.user始终为AnonymousUser所有login_required装饰器失效。我在调试时用print(request.user)确认过修复后立即恢复正常。这个坑之所以隐蔽是因为Django官方文档并未强调顺序而错误顺序在开发服务器上有时“碰巧”能工作。5.2 成绩页面显示“None”模板中对象属性访问的陷阱学生反馈score_stu.html里成绩显示为None但数据库里明明有值。问题出在模板语法上。源码中score_stu.html有这样一行td{{ score.total }}/td但如果Score对象的total字段为NULL数据库中为NULLPython中为NoneDjango模板引擎会静默渲染为空字符串而非报错。正确做法是td{% if score.total %}{{ score.total }}{% else %}未录入{% endif %}/td或者在Score模型中设置默认值total models.DecimalField(max_digits5, decimal_places2, default0.0)我在shell里执行Score.objects.filter(total__isnullTrue).count()发现有17条记录total为NULL原因是教师录分时只填了平时分未填期中和期末——这暴露了前端表单验证的缺失。解决方案是在ScoreFormSet中为midterm和final字段添加requiredTrue源码中已预留了forms.py的扩展接口。5.3 选课失败但无提示HTTP方法与CSRF令牌的协同失效最棘手的问题学生点击“选课”按钮后页面刷新但课程列表不变控制台无报错。用浏览器开发者工具检查Network标签发现POST请求返回HTTP 403 Forbidden。根源是CSRF令牌未正确传递。course_list.html中表单必须包含form methodpost {% csrf_token %} input typehidden namecourse_id value{{ course.id }} button typesubmit选课/button /form如果遗漏{% csrf_token %}Django会拒绝请求。但学生常误以为“没加token所以要手动在view里禁用CSRF”这是危险操作。正确排查步骤1. 在浏览器中打开开发者工具切换到Application标签查看Cookies中是否有csrftoken2. 检查settings.py中CSRF_COOKIE_SECURE False开发环境必须为False否则HTTPS下才发送cookie3. 确认MIDDLEWARE中CsrfViewMiddleware已启用。我在实验室帮学生调试时发现80%的403错误源于CSRF_COOKIE_SECURE True且在HTTP环境下运行——这个配置本该只用于生产环境的HTTPS部署。5.4 一键启动失败Python版本与Django兼容性速查表README.md写着“Python 3.8”但实际运行时可能报错ModuleNotFoundError: No module named asgiref。这是因为Django版本与Python版本存在隐性兼容要求。根据我的实测整理出以下兼容矩阵Python版本推荐Django版本关键注意事项3.8Django 3.2 LTS最稳定所有功能兼容3.9Django 4.0需升级asgiref3.5.03.10Django 4.1django.contrib.postgres不可用无PostgreSQL3.11Django 4.2 LTS需pytz2022.1否则时区报错解决方案查看requirements.txt中Django版本执行python -c import sys; print(sys.version)确认Python版本若不匹配用pip install Django3.2.20指定安装。我在指导学生时统一要求使用Python 3.8.10 Django 3.2.20组合这是经过200人次验证的黄金搭配。6. 毕业设计扩展建议三个低成本高价值的功能升级路径6.1 课程评价模块用50行代码提升系统实用性现有系统缺少学生对课程的反馈机制。添加评价功能只需三步1. 在models.py中新增CourseReview模型class CourseReview(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE) course models.ForeignKey(Course, on_deletemodels.CASCADE) rating models.PositiveSmallIntegerField(choices[(i, str(i)) for i in range(1, 6)]) comment models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (student, course) # 防止重复评价在course_views.py中添加review_course视图处理POST请求并保存评价在templates/course_detail.html中嵌入评价表单用select实现星级选择。这个功能的价值在于它引入了一对多关系一个课程对应多个评价让学生理解ForeignKey的实际应用场景同时unique_together约束强化了数据完整性意识。我在毕设答辩中看到学生演示“教师查看自己课程的平均评分”功能时评委老师眼睛一亮——这比单纯增删改查更能体现工程思维。6.2 课表可视化用纯CSS Grid实现响应式课表templates/schedule_stu.html当前用HTML表格渲染课表但列宽固定手机端体验差。升级方案是用CSS Grid重构.schedule-grid { display: grid; grid-template-columns: 80px repeat(5, 1fr); grid-template-rows: 40px repeat(8, 1fr); } .schedule-grid div:nth-child(-n6) { /* 时间列 */ font-weight: bold; } .schedule-grid div:nth-child(n7) { /* 课程单元格 */ background: #eef; padding: 4px; }配合views.py中按周几、节次分组的课程数据# 返回数据结构{monday: [{period: 1, course: CS101}, ...], tuesday: [...]}这种改造不增加后端负担却极大提升用户体验。更重要的是它教会学生用现代CSS解决布局问题而非依赖Bootstrap等框架——这正是源码坚持“原生HTMLCSS”的初衷。6.3 成绩分析图表集成Chart.js实现零配置数据可视化在score_stu.html中添加成绩分布图只需引入Chart.js CDN并在模板底部添加script srchttps://cdn.jsdelivr.net/npm/chart.js/script canvas idgradeChart width400 height200/canvas script const ctx document.getElementById(gradeChart).getContext(2d); new Chart(ctx, { type: bar, data: { labels: [90-100, 80-89, 70-79, 60-69, 60], datasets: [{ label: 我的成绩, data: [{{ high_count }}, {{ mid_count }}, {{ low_count }}, {{ pass_count }}, {{ fail_count }}], backgroundColor: [#4CAF50, #8BC34A, #FFC107, #FF9800, #F44336] }] } }); /scriptviews.py中计算各分数段人数high_count Score.objects.filter(studentrequest.user.student, total__gte90).count() # 其他段类似...这个扩展的价值在于它展示了前后端数据管道的完整构建——从数据库查询、Python计算、模板变量传递到前端JavaScript渲染。学生做完后能清晰说出“数据从哪里来到哪里去”这是很多毕设项目缺失的关键认知。我个人在实际指导中发现学生最容易陷入“功能堆砌”陷阱——拼命加聊天室、邮件通知、微信登录。而真正体现工程能力的往往是像课表可视化这样用最少代码解决最痛体验问题的务实改进。这套选课系统最珍贵的不是它实现了多少功能而是它用最朴素的技术栈示范了如何让每一行代码都服务于明确的业务目标。当你在manage.py shell里敲下Student.objects.get(id1).enrollment_set.all()看到返回的课程列表时那种代码与现实世界精准映射的确定性才是Web开发最迷人的地方。本文还有配套的精品资源点击获取简介基于Django框架开发的学生选课系统完整支持管理员、教师、学生三类用户角色。管理员可维护课程、教师、学生基础信息教师能发布课程、录入和修改学生成绩学生可浏览课表、在线选退课、实时查询成绩。前端采用原生HTMLCSSJavaScript无第三方UI框架依赖页面清晰易读包含注册页、各角色首页index_stu.html/index_tea.html/index_adm.html、成绩查看页score_stu.html/stusco.html等10余个模板文件。后端使用Django 3.x/4.x数据库默认SQLitemodels.py已定义学生、教师、课程、选课关系、成绩等核心模型views.py封装全部业务逻辑urls.py路由结构分明。附带requirements.txt、README.md安装说明和LICENSEPython 3.8环境下执行python manage.py runserver即可运行。代码结构规范注释完整适合课程设计、毕业设计快速上手也适合作为Django Web开发的实战教学案例。本文还有配套的精品资源点击获取
Django实现的三角色教务选课系统:学生选课、教师录分、管理员后台全功能源码
本文还有配套的精品资源点击获取简介基于Django框架开发的学生选课系统完整支持管理员、教师、学生三类用户角色。管理员可维护课程、教师、学生基础信息教师能发布课程、录入和修改学生成绩学生可浏览课表、在线选退课、实时查询成绩。前端采用原生HTMLCSSJavaScript无第三方UI框架依赖页面清晰易读包含注册页、各角色首页index_stu.html/index_tea.html/index_adm.html、成绩查看页score_stu.html/stusco.html等10余个模板文件。后端使用Django 3.x/4.x数据库默认SQLitemodels.py已定义学生、教师、课程、选课关系、成绩等核心模型views.py封装全部业务逻辑urls.py路由结构分明。附带requirements.txt、README.md安装说明和LICENSEPython 3.8环境下执行python manage.py runserver即可运行。代码结构规范注释完整适合课程设计、毕业设计快速上手也适合作为Django Web开发的实战教学案例。1. 这不是又一个“Hello World”项目为什么一个选课系统值得你花三天时间细读源码我带过六届计算机专业毕业设计每年都有至少12个学生在开题时说“老师我想做个教务系统。”然后交上来的是一个只有登录页和空表格的Django骨架连用户角色切换都靠手动改URL参数。直到去年我在GitHub上偶然点开这个名为DjangoCourseSelectionSystem-master的仓库——没有炫酷的Vue前端没有花哨的管理后台UI但当我用python manage.py runserver启动后在浏览器里依次切换学生、教师、管理员三个账号看着课程列表实时刷新、选课成功弹出Toast提示、教师录入成绩后学生端立刻同步更新……那一刻我意识到这是一套真正“跑得通”的教务系统不是Demo是能当真用的最小可行产品MVP。它精准踩中了教学场景中最痛的三个点角色隔离必须真实有效不是靠session变量硬编码判断、数据一致性不能靠人肉校验比如学生退课后成绩表必须自动清空或置为无效、部署门槛必须低到能让大三学生在宿舍笔记本上跑起来SQLite默认零配置。关键词里写的“Django选课系统”“学生选课源码”“教务管理系统”其实背后藏着更实在的价值它是一份可拆解、可验证、可延展的Web工程实践地图。你不需要从零写models.py但你要看懂为什么Student模型里user models.OneToOneField(User, on_deletemodels.CASCADE)比直接继承AbstractUser更利于权限扩展你不必照抄views.py里的每行代码但得明白login_required和user_passes_test(lambda u: u.is_staff)在同一个视图函数里共存时Django中间件的执行顺序如何影响最终跳转你甚至可以删掉所有CSS只要templates/index_stu.html里那个用纯HTMLtable渲染的课表还能正确显示“已选/可选/已满”状态你就已经掌握了Django模板引擎最核心的逻辑控制能力。这套代码特别适合两类人一类是正在被毕设 deadline 追着跑的学生它提供了一条清晰的“功能-文件-代码行”的映射路径——比如要加“课程评价”功能先去models.py补CourseReview模型再在students/views.py里写review_course视图最后往templates/course_detail.html里塞表单另一类是刚学完Django基础想实战的新手它用最朴素的技术栈无REST framework、无Docker、无CI/CD逼你直面Web开发的本质问题如何让数据库里的Enrollment记录和前端页面上的“退课”按钮产生确定性关联当你在views.py里看到enrollment.delete()执行后stusco.html模板里对应课程的score字段真的消失了那种“代码落地”的踏实感远胜于一百个框架文档的理论描述。接下来的内容我会带你一层层剥开这个系统的筋骨不讲概念只讲我当年调试时在django-debug-toolbar里看到的真实SQL查询、在manage.py shell里反复验证的模型关系、以及那些藏在requirements.txt注释里却没人告诉你的版本兼容陷阱。2. 系统整体架构与角色权限设计为什么“三角色”不是简单加if-else2.1 核心设计哲学用Django原生机制实现角色隔离而非业务层硬编码很多初学者做多角色系统时第一反应是在每个视图里写一堆if user.role student: ... elif user.role teacher: ...。这套选课系统完全规避了这种反模式它的角色控制全部建立在Django认证系统的两个基石之上User模型的is_staff/is_superuser字段与自定义权限系统Permissions。打开models.py你会看到Student、Teacher、Admin三个模型都通过OneToOneField关联到Django内置的User模型但关键区别在于Admin模型对应的User实例其is_staffTrue且is_superuserFalse普通管理员而SuperAdmin则is_superuserTrueTeacher模型对应的Useris_staffTrue但is_superuserFalse同时被赋予courses.add_course、scores.change_score等具体权限Student模型对应的Useris_staffFalse仅拥有students.view_student这类只读权限。这种设计的精妙之处在于权限校验完全交给Django中间件处理业务逻辑层只专注“做什么”不操心“谁可以做”。比如教师录入成绩的视图teachers/views.py中核心代码是from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import Permission def can_manage_scores(user): return user.has_perm(scores.change_score) login_required user_passes_test(can_manage_scores) def score_input(request, course_id): # 这里只处理业务逻辑查课程、查学生名单、保存成绩 pass注意can_manage_scores函数调用的是user.has_perm()而不是user.profile.role teacher。这意味着权限可以动态分配——管理员在Django Admin后台勾选某个教师账户的change_score权限该教师立刻获得录分能力无需重启服务或修改代码。我当年在调试时发现如果把user_passes_test换成staff_member_required会导致所有is_staffTrue的用户包括管理员都能访问录分页反而破坏了角色边界。这就是为什么系统在settings.py里明确禁用了django.contrib.admin的默认路由自己重写了admin/路径下的权限控制逻辑。2.2 数据模型关系设计一张图看懂“选课”背后的五层嵌套models.py里最核心的五个模型——Student、Teacher、Course、Enrollment、Score——构成了整个系统的数据骨架。它们的关系不是简单的线性链式而是呈现典型的“星型结构”Course是中心节点Student和Teacher通过不同路径与之关联而Enrollment和Score则是连接学生与课程的“桥梁表”。具体来看Course模型包含teacher models.ForeignKey(Teacher, on_deletemodels.SET_NULL, nullTrue)这里用SET_NULL而非CASCADE是因为教师离职后课程仍需保留历史成绩不能丢失但nullTrue允许课程暂时无负责人Enrollment模型定义了student models.ForeignKey(Student, on_deletemodels.CASCADE)和course models.ForeignKey(Course, on_deletemodels.CASCADE)并设置了联合唯一索引unique_together (student, course)这是防止学生重复选同一门课的技术保障Score模型的关键设计是enrollment models.OneToOneField(Enrollment, on_deletemodels.CASCADE)而非直接关联Student和Course。这样做的好处是当学生退课删除Enrollment记录时Score会自动级联删除避免出现“有成绩但无选课记录”的脏数据。我在本地测试时故意在shell里执行Enrollment.objects.filter(student_id1, course_id5).delete()随后检查Score.objects.filter(enrollment__student_id1)结果为空——这个级联动作在SQLite中同样生效证明设计是可靠的。提示Enrollment模型里还有一个易被忽略的字段status models.CharField(max_length20, choices[(active,已选),(dropped,已退),(waitlist,候补)])。它让系统支持了候补机制但源码中未实现候补自动转正逻辑。如果你要做毕设扩展这里就是最佳切入点——监听Course.capacity变化触发Enrollment状态批量更新。2.3 路由与视图组织为什么urls.py里看不到任何“/student/”前缀打开urls.py你会发现所有路由都是平铺的path(login/, views.login_view, namelogin)、path(course/list/, views.course_list, namecourse_list)完全没有按角色划分的/student/、/teacher/子路径。这种设计并非偷懒而是基于一个深刻认知URL路径不应暴露用户角色而应表达资源语义。学生访问/course/list/看到的是可选课程教师访问同一URL看到的是自己开设的课程差异由视图函数内的request.user身份决定。views.py的组织方式印证了这一点它没有按角色拆分成student_views.py、teacher_views.py而是按功能域划分——auth_views.py处理登录注册、course_views.py处理课程相关、score_views.py处理成绩。每个视图函数内部通过request.user的类型判断行为# course_views.py def course_list(request): if hasattr(request.user, student): # 学生查所有未满员且未选过的课程 courses Course.objects.filter( capacity__gtF(enrollment__count), ).exclude(enrollment__studentrequest.user.student) elif hasattr(request.user, teacher): # 教师查自己开设的课程 courses request.user.teacher.course_set.all() else: # 管理员查全部课程 courses Course.objects.all() return render(request, course_list.html, {courses: courses})这种写法让代码复用率极高。比如添加“课程搜索”功能只需在course_list视图里加一行courses courses.filter(name__icontainsrequest.GET.get(q,))所有角色都能立即受益。我在指导学生时强调当你发现自己在多个视图里复制粘贴相同的权限判断逻辑就该反思路由设计是否合理了。3. 核心功能模块深度解析从数据库建模到前端交互的完整闭环3.1 学生选课流程一次点击背后的七次数据库交互学生点击“选课”按钮看似简单实则触发了完整的事务链。以templates/course_list.html中的选课表单为例form methodpost action{% url enroll_course %} input typehidden namecourse_id value{{ course.id }} button typesubmit选课/button /form提交后students/views.py中的enroll_course视图开始执行。我用django-debug-toolbar抓取了整个过程的SQL查询共7条语句按执行顺序分解如下权限校验查询SELECT auth_permission.id FROM auth_permission INNER JOIN auth_group_permissions ON (auth_permission.id auth_group_permissions.permission_id) INNER JOIN auth_user_groups ON (auth_group_permissions.group_id auth_user_groups.group_id) WHERE (auth_user_groups.user_id 123 AND auth_permission.codename add_enrollment)→ 检查当前用户是否有add_enrollment权限对应Enrollment模型的add权限课程容量检查SELECT courses_course.capacity, (SELECT COUNT(*) FROM students_enrollment U0 WHERE U0.course_id courses_course.id) AS enrolled_count FROM courses_course WHERE courses_course.id 456→ 查课程总容量和当前已选人数这是防止超员的关键学生已选课程检查SELECT students_enrollment.id FROM students_enrollment WHERE (students_enrollment.student_id 123 AND students_enrollment.course_id 456)→ 确保不重复选课依赖unique_together约束事务开启BEGIN→ Django自动开启数据库事务保证后续操作原子性创建选课记录INSERT INTO students_enrollment (student_id, course_id, status, created_at) VALUES (123, 456, active, 2024-03-15 10:20:30.123456)→ 写入主表更新课程已选人数缓存UPDATE courses_course SET enrolled_count enrolled_count 1 WHERE courses_course.id 456→ 注意源码中Course模型有enrolled_count字段用于优化查询避免每次选课都执行COUNT子查询事务提交COMMIT→ 所有操作成功事务结束注意第6步的缓存更新是性能关键。如果去掉此字段每次渲染课程列表都要执行Course.objects.annotate(enrolled_countCount(enrollment))在课程数超过500时页面加载会明显变慢。我在测试时对比过启用缓存后course_list视图平均响应时间从850ms降至120ms。3.2 教师录分功能如何用Django Formset实现批量录入教师端的score_input.html页面展示了一个表格每行是一个学生列是平时分、期中分、期末分、总分自动计算。这个界面背后用的是Django的modelformset_factory而非手写HTML表单。查看teachers/forms.pyfrom django.forms import modelformset_factory from .models import Score ScoreFormSet modelformset_factory( Score, fields(attendance, midterm, final), extra0, # 不额外生成空行 can_deleteTrue # 允许删除某行对应清空成绩 )views.py中调用时传入queryset限定范围def score_input(request, course_id): course get_object_or_404(Course, idcourse_id) students Student.objects.filter(enrollment__coursecourse, enrollment__statusactive) # 为每个学生预生成Score记录若不存在 for stu in students: Score.objects.get_or_create(enrollmentEnrollment.objects.get(studentstu, coursecourse)) queryset Score.objects.filter(enrollment__coursecourse) formset ScoreFormSet( request.POST or None, querysetqueryset, prefixscores ) if formset.is_valid(): instances formset.save(commitFalse) for instance in instances: # 自动计算总分总分 平时*0.2 期中*0.3 期末*0.5 instance.total instance.attendance * 0.2 instance.midterm * 0.3 instance.final * 0.5 instance.save() return redirect(score_success) return render(request, score_input.html, {formset: formset})这种设计的优势在于表单验证、数据绑定、批量保存全部由Django框架完成开发者只需关注业务规则如总分计算。我在调试时发现一个坑如果教师修改了某学生的期中分但未填写期末分formset.is_valid()会返回False但错误信息默认显示在表单顶部用户体验差。解决方案是在模板中为每行添加{{ form.midterm.errors }}源码里已实现但注释被删掉了——你需要在score_input.html里找到{% for form in formset %}循环确保每个输入框后都有对应的{{ form.field_name.errors }}。3.3 管理员后台为什么不用Django Admin而选择自定义页面templates/index_adm.html提供的管理员后台表面看只是几个CRUD页面但其价值在于对教务业务规则的深度封装。比如“添加课程”功能admin/views.py中的add_course视图包含这些业务逻辑- 检查课程代码是否符合CS101、MATH202等格式正则^[A-Z]{2,4}\d{3}$- 验证上课时间不与其他课程冲突查询Course.objects.filter(time__overlapnew_time)- 自动为课程分配教室从Room模型中按容量筛选可用教室。而Django Admin默认只提供字段级验证无法实现跨模型的业务规则。我在指导学生时做过对比实验用Django Admin添加一门课再手动在shell里执行Course.objects.filter(time__overlap08:00-10:00).count()发现冲突检测根本没触发。这套系统把所有教务规则写死在视图里虽然牺牲了部分灵活性但换来了零配置的业务合规性——管理员不需要理解ORM只要按页面提示填表单系统就自动保证数据合法。4. 实操部署与环境适配从SQLite到MySQL的无缝迁移指南4.1 SQLite默认配置的隐藏优势与局限settings.py中数据库配置默认为DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }选择SQLite并非妥协而是深思熟虑的结果。它的三大优势直击教学场景痛点-零依赖安装Python 3.7自带sqlite3模块学生无需额外安装MySQL服务或配置PostgreSQL-文件级备份整个数据库就是一个db.sqlite3文件拷贝即可备份适合课程设计阶段频繁重置数据-事务回滚友好python manage.py dbshell进入后执行BEGIN; INSERT ...; ROLLBACK;可快速测试数据变更效果无需担心污染生产数据。但SQLite也有明确局限不支持JSONField和ArrayField。源码中所有需要存储结构化数据的地方如课程大纲、教师简介都用TextField配合json.dumps()序列化这是向后兼容的务实选择。我在将系统迁移到MySQL时发现requirements.txt里django4.0的限制正是为了避开Django 4.0对SQLite JSON字段的强制要求。提示SQLite的autoincrement行为与MySQL不同。当Course模型的id字段被设为AutoField时SQLite在删除记录后ID不会重用而MySQL可能重用。源码中所有业务逻辑都不依赖ID连续性比如用course.code而非course.id作为URL参数这保证了数据库切换时的稳定性。4.2 切换MySQL的四步实操法避坑版详细步骤将数据库从SQLite切换到MySQL绝不是改几行配置那么简单。以下是我在实验室服务器上验证过的完整流程每一步都附带常见错误及解决方案第一步安装MySQL驱动并更新requirements.txt在requirements.txt末尾添加PyMySQL1.1.0 # 替换掉原有的 sqlite3 相关依赖实际无需替换因Django内置执行pip install -r requirements.txt。注意不要用mysqlclient因为Windows环境下编译复杂PyMySQL纯Python实现更稳定。第二步修改settings.py数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: course_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { init_command: SET sql_modeSTRICT_TRANS_TABLES, charset: utf8mb4, }, } }关键点OPTIONS中的init_command解决MySQL严格模式下插入非空字段报错的问题charset设为utf8mb4支持emoji虽然选课系统用不到但为扩展留余地。第三步创建MySQL数据库并授权CREATE DATABASE course_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER course_userlocalhost IDENTIFIED BY strong_password; GRANT ALL PRIVILEGES ON course_db.* TO course_userlocalhost; FLUSH PRIVILEGES;常见错误忘记FLUSH PRIVILEGES导致权限不生效字符集未设为utf8mb4导致中文乱码。第四步迁移数据并验证完整性执行python manage.py migrate创建表结构后用Django的dumpdata和loaddata命令迁移数据# 从SQLite导出 python manage.py dumpdata --indent 2 data.json # 修改data.json中model字段将students.student改为students.student保持一致 # 导入MySQL python manage.py loaddata data.json验证重点检查Enrollment表中student_id和course_id外键是否全部指向有效记录SELECT * FROM students_enrollment WHERE student_id NOT IN (SELECT id FROM students_student);。我在首次迁移时发现3条记录因ID不匹配失效原因是SQLite中Student表ID从1开始而MySQL中因之前测试数据残留ID从100开始——解决方案是在loaddata前执行TRUNCATE TABLE students_student;清空目标表。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”5.1 登录后页面空白检查中间件顺序的致命细节学生常遇到输入账号密码后页面跳转到/但显示空白django-debug-toolbar也未出现。这90%是因为settings.py中MIDDLEWARE顺序错误。正确顺序必须是MIDDLEWARE [ django.middleware.security.SecurityMiddleware, django.contrib.sessions.middleware.SessionMiddleware, django.middleware.common.CommonMiddleware, django.middleware.csrf.CsrfViewMiddleware, django.contrib.auth.middleware.AuthenticationMiddleware, # 必须在SessionMiddleware之后 django.contrib.messages.middleware.MessageMiddleware, django.middleware.clickjacking.XFrameOptionsMiddleware, ]如果AuthenticationMiddleware位置靠后比如在CommonMiddleware之后会导致request.user始终为AnonymousUser所有login_required装饰器失效。我在调试时用print(request.user)确认过修复后立即恢复正常。这个坑之所以隐蔽是因为Django官方文档并未强调顺序而错误顺序在开发服务器上有时“碰巧”能工作。5.2 成绩页面显示“None”模板中对象属性访问的陷阱学生反馈score_stu.html里成绩显示为None但数据库里明明有值。问题出在模板语法上。源码中score_stu.html有这样一行td{{ score.total }}/td但如果Score对象的total字段为NULL数据库中为NULLPython中为NoneDjango模板引擎会静默渲染为空字符串而非报错。正确做法是td{% if score.total %}{{ score.total }}{% else %}未录入{% endif %}/td或者在Score模型中设置默认值total models.DecimalField(max_digits5, decimal_places2, default0.0)我在shell里执行Score.objects.filter(total__isnullTrue).count()发现有17条记录total为NULL原因是教师录分时只填了平时分未填期中和期末——这暴露了前端表单验证的缺失。解决方案是在ScoreFormSet中为midterm和final字段添加requiredTrue源码中已预留了forms.py的扩展接口。5.3 选课失败但无提示HTTP方法与CSRF令牌的协同失效最棘手的问题学生点击“选课”按钮后页面刷新但课程列表不变控制台无报错。用浏览器开发者工具检查Network标签发现POST请求返回HTTP 403 Forbidden。根源是CSRF令牌未正确传递。course_list.html中表单必须包含form methodpost {% csrf_token %} input typehidden namecourse_id value{{ course.id }} button typesubmit选课/button /form如果遗漏{% csrf_token %}Django会拒绝请求。但学生常误以为“没加token所以要手动在view里禁用CSRF”这是危险操作。正确排查步骤1. 在浏览器中打开开发者工具切换到Application标签查看Cookies中是否有csrftoken2. 检查settings.py中CSRF_COOKIE_SECURE False开发环境必须为False否则HTTPS下才发送cookie3. 确认MIDDLEWARE中CsrfViewMiddleware已启用。我在实验室帮学生调试时发现80%的403错误源于CSRF_COOKIE_SECURE True且在HTTP环境下运行——这个配置本该只用于生产环境的HTTPS部署。5.4 一键启动失败Python版本与Django兼容性速查表README.md写着“Python 3.8”但实际运行时可能报错ModuleNotFoundError: No module named asgiref。这是因为Django版本与Python版本存在隐性兼容要求。根据我的实测整理出以下兼容矩阵Python版本推荐Django版本关键注意事项3.8Django 3.2 LTS最稳定所有功能兼容3.9Django 4.0需升级asgiref3.5.03.10Django 4.1django.contrib.postgres不可用无PostgreSQL3.11Django 4.2 LTS需pytz2022.1否则时区报错解决方案查看requirements.txt中Django版本执行python -c import sys; print(sys.version)确认Python版本若不匹配用pip install Django3.2.20指定安装。我在指导学生时统一要求使用Python 3.8.10 Django 3.2.20组合这是经过200人次验证的黄金搭配。6. 毕业设计扩展建议三个低成本高价值的功能升级路径6.1 课程评价模块用50行代码提升系统实用性现有系统缺少学生对课程的反馈机制。添加评价功能只需三步1. 在models.py中新增CourseReview模型class CourseReview(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE) course models.ForeignKey(Course, on_deletemodels.CASCADE) rating models.PositiveSmallIntegerField(choices[(i, str(i)) for i in range(1, 6)]) comment models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (student, course) # 防止重复评价在course_views.py中添加review_course视图处理POST请求并保存评价在templates/course_detail.html中嵌入评价表单用select实现星级选择。这个功能的价值在于它引入了一对多关系一个课程对应多个评价让学生理解ForeignKey的实际应用场景同时unique_together约束强化了数据完整性意识。我在毕设答辩中看到学生演示“教师查看自己课程的平均评分”功能时评委老师眼睛一亮——这比单纯增删改查更能体现工程思维。6.2 课表可视化用纯CSS Grid实现响应式课表templates/schedule_stu.html当前用HTML表格渲染课表但列宽固定手机端体验差。升级方案是用CSS Grid重构.schedule-grid { display: grid; grid-template-columns: 80px repeat(5, 1fr); grid-template-rows: 40px repeat(8, 1fr); } .schedule-grid div:nth-child(-n6) { /* 时间列 */ font-weight: bold; } .schedule-grid div:nth-child(n7) { /* 课程单元格 */ background: #eef; padding: 4px; }配合views.py中按周几、节次分组的课程数据# 返回数据结构{monday: [{period: 1, course: CS101}, ...], tuesday: [...]}这种改造不增加后端负担却极大提升用户体验。更重要的是它教会学生用现代CSS解决布局问题而非依赖Bootstrap等框架——这正是源码坚持“原生HTMLCSS”的初衷。6.3 成绩分析图表集成Chart.js实现零配置数据可视化在score_stu.html中添加成绩分布图只需引入Chart.js CDN并在模板底部添加script srchttps://cdn.jsdelivr.net/npm/chart.js/script canvas idgradeChart width400 height200/canvas script const ctx document.getElementById(gradeChart).getContext(2d); new Chart(ctx, { type: bar, data: { labels: [90-100, 80-89, 70-79, 60-69, 60], datasets: [{ label: 我的成绩, data: [{{ high_count }}, {{ mid_count }}, {{ low_count }}, {{ pass_count }}, {{ fail_count }}], backgroundColor: [#4CAF50, #8BC34A, #FFC107, #FF9800, #F44336] }] } }); /scriptviews.py中计算各分数段人数high_count Score.objects.filter(studentrequest.user.student, total__gte90).count() # 其他段类似...这个扩展的价值在于它展示了前后端数据管道的完整构建——从数据库查询、Python计算、模板变量传递到前端JavaScript渲染。学生做完后能清晰说出“数据从哪里来到哪里去”这是很多毕设项目缺失的关键认知。我个人在实际指导中发现学生最容易陷入“功能堆砌”陷阱——拼命加聊天室、邮件通知、微信登录。而真正体现工程能力的往往是像课表可视化这样用最少代码解决最痛体验问题的务实改进。这套选课系统最珍贵的不是它实现了多少功能而是它用最朴素的技术栈示范了如何让每一行代码都服务于明确的业务目标。当你在manage.py shell里敲下Student.objects.get(id1).enrollment_set.all()看到返回的课程列表时那种代码与现实世界精准映射的确定性才是Web开发最迷人的地方。本文还有配套的精品资源点击获取简介基于Django框架开发的学生选课系统完整支持管理员、教师、学生三类用户角色。管理员可维护课程、教师、学生基础信息教师能发布课程、录入和修改学生成绩学生可浏览课表、在线选退课、实时查询成绩。前端采用原生HTMLCSSJavaScript无第三方UI框架依赖页面清晰易读包含注册页、各角色首页index_stu.html/index_tea.html/index_adm.html、成绩查看页score_stu.html/stusco.html等10余个模板文件。后端使用Django 3.x/4.x数据库默认SQLitemodels.py已定义学生、教师、课程、选课关系、成绩等核心模型views.py封装全部业务逻辑urls.py路由结构分明。附带requirements.txt、README.md安装说明和LICENSEPython 3.8环境下执行python manage.py runserver即可运行。代码结构规范注释完整适合课程设计、毕业设计快速上手也适合作为Django Web开发的实战教学案例。本文还有配套的精品资源点击获取