ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django学生选课系统开发实战:从数据库设计到项目部署全流程解析

Django学生选课系统开发实战:从数据库设计到项目部署全流程解析 每年毕业季我都能在技术社区里看到一批做Django毕设的同学其中学生选课系统几乎是最高频出现的题目之一。说句实话这类题目不是最炫的但它是少数几个能从数据库设计、业务逻辑、权限控制到部署调试全部揉进去、难度又刚刚好的项目。这篇文章我会从实际做过的项目出发把基于Django的学生选课系统完整拆开讲一遍从需求拆解、数据模型、核心功能、界面呈现到远程调试和上线部署的坑一次性说清楚。你可以把它当作做同类毕设时的路线图也可以当作判断一套Django毕设全套源码文档到底值不值得参考的检查清单。1. 为什么选课系统值得做需求复杂度刚好的毕设黄金区间1.1 从选题逻辑看选课系统恰好覆盖Web开发的完整闭环做毕业设计最怕两件事一是题目太简单需求文档写不满三页答辩时没什么可讲二是题目太复杂做到中期发现自己撑不住数据库关系一团乱麻。学生选课系统恰好落在中间那个舒适区里。它涉及的角色是清晰的学生、教师、管理员天然适合练权限控制它涉及的数据关系是有层次感的学院、专业、班级、课程、教学班、选课记录足够画出漂亮的E-R图它的交互流程是有状态的管理员排课、学生选课、教师录成绩、学生查成绩能把状态流转这个概念讲得很具体。更关键的是它的业务规则里包含了容量限制、时间冲突、重复选课校验这些容易出bug但又能被准确定位修复的逻辑这正是展示你会写业务代码的好素材。我在帮同学调试这类项目时最常说的话是答辩时老师不看你用了多少框架而是看你能不能把为什么这样设计数据表并发选课超员怎么办用户权限怎么拦截这几个问题讲明白。选课系统正好是这些问题的天然载体。1.2 一套完整毕设项目的标准组成和使用顺序市面上挂全套源码文档标签的项目内容质量差异非常大。根据我自己购买和调试过一批项目的经验一份合格的选课系统毕设包至少要包含这几块可运行的Django源码包含完整的app目录、模板、静态资源和requirements.txt依赖清单数据库设计说明最好有SQL文件和模型关系图通常还会附带初始化数据需求分析文档和系统设计文档涵盖用例图、类图、时序图、E-R图、功能模块说明答辩PPT和演示视频用于熟悉系统功能讲解流程远程调试和代码讲解服务遇到环境问题有人能远程帮你定位。拿到这类项目后的正确顺序不是立刻打开编辑器看代码而是先按README把环境跑起来。跑通一遍再关掉自己重新部署一遍然后对照源代码理解每个功能点最后把别人的模型字段改成符合自己学校业务场景的版本。这样一轮下来你才算真正消化了这套源码而不是答辩时被问两句就露馅。2. 业务拆解三种角色、三类操作、一条完整状态链2.1 学生、教师、管理员各自能干什么学生选课系统的业务边界看起来简单但落成代码前必须把角色权限理清楚。我见过不少半成品项目把权限判断写在模板里或者只在Vue前端做拦截后端接口任何人都能调这是典型的安全意识缺失。做这样一套系统后端每一次路由处理都必须校验当前登录用户的身份和角色。管理员维护学生名单、教师名单、学院专业信息创建课程、设置开课学期、设置教学班容量和上课时间重置密码、停用账号、查看全校选课统计。学生浏览本学期开放选课的课程列表选课、退课查看自己的课表查看课程成绩修改个人资料。教师查看自己名下课程的学生选课名单录入课程成绩查看教学班统计信息维护个人资料。这里有个很容易忽略的点同一个用户如果既是教师又在职读研他可能有双重身份。所以设计角色字段时我建议使用多值字段或在UserProfile里做对应的关联表而不是只放一个CharField存student或teacher。不过大多数毕设为了控制复杂度做成单角色字段也完全够用前提是你知道这是一处简化设计而不是完美设计。2.2 核心业务状态流转从排课到成绩落定理解了角色我们就可以梳理一条完整的状态链。这条链几乎是选课系统所有业务逻辑的骨架也是你画功能流程图时的依据。管理员录入课程基础信息并生成一个或多个教学班。每个教学班绑定任课教师、上课时间、上课地点、容量上限。管理员开放选课窗口学生进入选课页面。此时课程状态为选课中。学生提交选课请求系统校验是否冲突通过后生成选课记录课程已选人数加一。学生在选课期内可以退课释放名额课程已选人数减一。选课期结束教师登录系统看到选课名单逐个录入成绩。成绩一旦录入并确认学生端即可查询同时系统可以累计学生的学分绩点。我在给项目写状态设计时习惯在课程表里维护一个status字段取值范围是常量未开放、选课中、已结束然后另用一个字段allow_drop控制是否允许退课。这样扩展补选阶段停选阶段这类规则时只需要加状态值不需要大动表结构。2.3 业务规则里最容易写错的三个点选课系统的业务规则看起来简单实际很容易在细节上写歪。我排一下踩过的高频坑。第一容量校验不能放在前端。前端显示还剩10个名额不代表真的能选上。并发情况下两个学生同时提交必须先走后端校验才能保证不超选。第二时间冲突检测必须覆盖周次维度。很多课程是1-8周和9-16周分开上的简单比较星期几和节次会误判。毕设里可以简化成星期节次唯一但要清楚这个简化的代价。第三成绩录入后的课程不能再退课。这个状态约束要写在后端不能只靠隐藏前端按钮。3. 数据模型设计用Django ORM把关系理清3.1 扩展自带的User模型不要直接改auth_userDjango自带User模型已经包含密码哈希、登录会话等成熟实现我们只需要在它上面扩展业务字段。推荐的做法是新建一个UserProfile用OneToOneField关联到User再在信号里自动创建避免直接改auth_user表结构。# profiles/models.py from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver class UserProfile(models.Model): ROLE_CHOICES [ (student, 学生), (teacher, 教师), (admin, 管理员), ] user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) role models.CharField(max_length20, choicesROLE_CHOICES, defaultstudent) student_no models.CharField(学号, max_length20, blankTrue, nullTrue) teacher_no models.CharField(工号, max_length20, blankTrue, nullTrue) department models.CharField(学院, max_length100, blankTrue) phone models.CharField(手机号, max_length20, blankTrue) receiver(post_save, senderUser) def create_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(userinstance)为什么用OneToOne而不是直接在User上加字段因为Django官方在1.11之后推荐通过替换自定义User模型来做但替换的迁移改造成本较高毕设阶段使用Profile扩展是最稳的折中方案。post_save信号保证你用User.objects.create_user()创建账号时Profile会同步生成不会出现后面取user.profile报DoesNotExist的情况。3.2 核心模型学院、专业班级、课程、教学班、选课记录选课系统的核心数据模型我建议至少包含这几张表学院、专业、班级、学生信息挂在UserProfile上、课程、教学班、选课记录、成绩单。用代码展示会更直观。# courses/models.py from django.db import models from django.contrib.auth.models import User class College(models.Model): name models.CharField(学院名称, max_length100, uniqueTrue) class Major(models.Model): college models.ForeignKey(College, on_deletemodels.CASCADE, related_namemajors) name models.CharField(专业名称, max_length100) class ClassInfo(models.Model): major models.ForeignKey(Major, on_deletemodels.CASCADE, related_nameclasses) name models.CharField(班级名称, max_length100) grade models.CharField(年级, max_length10) class Course(models.Model): code models.CharField(课程编号, max_length20, uniqueTrue) name models.CharField(课程名称, max_length100) credit models.DecimalField(学分, max_digits3, decimal_places1) hours models.IntegerField(学时) # 课程本身不绑定教师和容量由具体的教学班决定这里我把课程和教学班拆开了。很多同学只建一张Course表把任课教师、上课时间、容量全塞进去结果同一门课多个班级时数据冗余严重。拆开后Course是课程基本信息TeachingClass才是这学期某老师某时间的一个班。class TeachingClass(models.Model): course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameclasses) teacher models.ForeignKey(User, on_deletemodels.CASCADE, related_nameteaching_classes) semester models.CharField(学期, max_length20) weekday models.IntegerField(星期几, choices[(i, f星期{i}) for i in range(1, 8)]) start_time models.TimeField(开始时间) end_time models.TimeField(结束时间) capacity models.PositiveIntegerField(容量上限, default60) selected_count models.PositiveIntegerField(已选人数, default0) status models.CharField(状态, max_length20, choices[ (open, 选课中), (pending, 未开放), (closed, 已结束), ], defaultpending) class SelectionRecord(models.Model): student models.ForeignKey(User, on_deletemodels.CASCADE, related_nameselections) teaching_class models.ForeignKey(TeachingClass, on_deletemodels.CASCADE, related_nameselections) selected_at models.DateTimeField(选课时间, auto_now_addTrue) score models.DecimalField(成绩, max_digits4, decimal_places1, nullTrue, blankTrue) class Meta: unique_together (student, teaching_class)unique_together就是同一个学生不能重复选同一教学班的数据库级约束。有些人只写在业务判断里但数据库这一层一定也要加双保险比单保险稳得多。另外TeachingClass.selected_count这个冗余字段是故意设计的目的就是列表页展示剩余名额时不用做聚合查询代价是每次选课/退课时必须在同一个事务里更新这个字段。3.3 外键查询优化select_related和prefetch_related怎么用有了外键关系查询时最忌讳的是在模板里循环查数据库。比如# 错误示范循环中触发N1查询 classes TeachingClass.objects.filter(statusopen) for c in classes: print(c.teacher.username)上面的代码每打印一个teacher都会发出一条SQL。数据量小的时候没感觉演示时学生几百人同时选课就明显卡。正确做法是提前用select_related把外键关联的对象一次性查出来。classes TeachingClass.objects.filter( statusopen ).select_related(course, teacher)如果是多对多或者反向关系比如查一个学生选了哪些课程则用prefetch_related。毕设阶段只要把这两个方法用对性能已经超出大部分同学了。4. 核心功能实现权限控制、选课冲突与事务边界4.1 登录鉴权与角色拦截Django自带的login_required装饰器可以用来拦截未登录用户但角色拦截需要自己写一个装饰器。我建议这样封装# common/decorators.py from functools import wraps from django.http import HttpResponseForbidden def role_required(*roles): def decorator(view_func): wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(login) profile_role getattr(request.user.profile, role, None) if profile_role not in roles: return HttpResponseForbidden(没有权限访问该页面) return view_func(request, *args, **kwargs) return _wrapped_view return decorator使用时就非常清爽学生选课接口加role_required(student)教师录成绩接口加role_required(teacher)管理员接口加role_required(admin)。注意视图函数里的权限判断永远不能省略模板隐藏按钮只是体验优化不是安全控制。4.2 选课冲突检测容量与时间冲突的完整逻辑选课是整套系统的核心入口也是最容易出现并发bug的地方。我直接把一个可用的选课视图逻辑写出来。# courses/views.py from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_POST from common.decorators import role_required require_POST role_required(student) transaction.atomic def enroll_class(request, class_id): student request.user teaching_class TeachingClass.objects.select_for_update().get(pkclass_id) if teaching_class.status ! open: return JsonResponse({success: False, msg: 该教学班未开放选课}) if teaching_class.selected_count teaching_class.capacity: return JsonResponse({success: False, msg: 教学班已满}) if SelectionRecord.objects.filter(studentstudent, teaching_classteaching_class).exists(): return JsonResponse({success: False, msg: 你已经选过这门课}) # 检查上课时间冲突 student_classes TeachingClass.objects.filter( selections__studentstudent, statusopen ).select_related(course) for sc in student_classes: if sc.weekday teaching_class.weekday and not ( teaching_class.end_time sc.start_time or teaching_class.start_time sc.end_time ): return JsonResponse({success: False, msg: f与课程{sc.course.name}时间冲突}) SelectionRecord.objects.create(studentstudent, teaching_classteaching_class) teaching_class.selected_count 1 teaching_class.save(update_fields[selected_count]) return JsonResponse({success: True, msg: 选课成功})这段代码里有几个关键设计要说清楚。第一是select_for_update()。选课时并发请求可能同时读到同一个selected_count如果都不用锁两个请求都会认为还有名额最后超选。加上数据库行级锁后同一时刻只能有一个事务在处理这条教学班的选课超选问题就被原子化解决了。事务装饰器atomic则保证创建选课记录更新已选人数要么一起成功要么一起失败。第二是时间冲突判断。我没有存节次而是存了开始和结束时间判断区间重叠。区间重叠的通用条件是a.start b.end and a.end b.start代码里的写法是等价变体两个时间段不相交则安康否则冲突。第三是返回格式直接用JsonResponse方便前端Ajax处理。毕设里如果不想写前后端分离这样最省事。4.3 Django Admin定制后台为什么选课系统离不开AdminDjango自带的Admin后台非常适合做管理员的排课界面只要注册好模型马上就能增删改查。但直接默认注册会有两个问题一是表单控件太简陋二是缺少业务校验。定制之后可以做到非常好的演示效果。# courses/admin.py from django.contrib import admin from .models import Course, TeachingClass, SelectionRecord admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (code, name, credit, hours) search_fields (code, name) admin.register(TeachingClass) class TeachingClassAdmin(admin.ModelAdmin): list_display (course, teacher, semester, weekday, start_time, end_time, capacity, selected_count, status) list_filter (semester, status, teacher) search_fields (course__name, teacher__username) autocomplete_fields (course, teacher) admin.register(SelectionRecord) class SelectionRecordAdmin(admin.ModelAdmin): list_display (student, teaching_class, selected_at, score) list_filter (teaching_class__semester,)Admin定制好之后管理员排课、查选课名单都不需要额外写页面答辩时也能直接展示管理员如何创建教学班。很多系统把精力全部放在学生端界面上结果管理端页面简陋得没法看其实用Admin就能以极低成本补齐这一块短板。4.4 退课与人数递减的联动退课逻辑和选课逻辑是镜像关系同样要注意事务和状态。退课只允许在允许退课的阶段执行且成绩已经录入就不能退require_POST role_required(student) transaction.atomic def drop_class(request, record_id): try: record SelectionRecord.objects.select_related(teaching_class).get(pkrecord_id, studentrequest.user) except SelectionRecord.DoesNotExist: return JsonResponse({success: False, msg: 选课记录不存在}) tc record.teaching_class if tc.status closed: return JsonResponse({success: False, msg: 选课已结束不能退课}) record.delete() tc.selected_count max(tc.selected_count - 1, 0) tc.save(update_fields[selected_count]) return JsonResponse({success: True, msg: 退课成功})5. 界面呈现模板继承、Bootstrap与Ajax局部更新5.1 模板继承与站点布局学生选课系统是典型的多页面应用直接用Django模板渲染完全没问题没必要强行上前后端分离。模板继承能让公共导航、页脚、用户信息只写一次。base.html里放Bootstrap CDN和导航栏子模板只需要重写content块。!-- templates/base.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title{% block title %}选课系统{% endblock %}/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary a classnavbar-brand href{% url course_list %}学生选课系统/a div classms-auto {% if user.is_authenticated %} span classnavbar-text me-3{{ user.username }}/span a classbtn btn-light btn-sm href{% url logout %}退出/a {% endif %} /div /nav div classcontainer mt-4 {% block content %}{% endblock %} /div /body /html实际项目里建议把Bootstrap文件下载到static目录不要依赖CDN因为毕设演示现场可能没有外网。5.2 用Ajax实现点击选课不整页刷新选课场景里学生点击选课按钮后最好的体验是不刷新页面就看到成功或失败提示。这其实比较简单前端fetch到/course/enroll/1/后端返回JsonResponse前端根据success字段弹提示并更新剩余名额。// static/js/course.js function enrollClass(classId, btn) { fetch(/course/enroll/${classId}/, { method: POST, headers: {X-CSRFToken: getCookie(csrftoken)} }) .then(res res.json()) .then(data { if (data.success) { alert(data.msg); btn.disabled true; btn.textContent 已选; } else { alert(data.msg); } }); }这里要注意CSRF token的处理。Django对POST请求默认开启CSRF校验Ajax请求必须把csrftoken放到请求头里。最稳妥的办法是在模板中渲染{{ csrf_token }}用JavaScript读取cookie。毕设阶段把这两行配置写对会省掉大量调试时间。5.3 分页、搜索和筛选的组合课程列表页如果不做分页几十条数据还勉强一百条以上就开始卡。Django内置的Paginator可以直接用。from django.core.paginator import Paginator from django.shortcuts import render def course_list(request): qs TeachingClass.objects.filter(statusopen).select_related(course, teacher) keyword request.GET.get(keyword, ) if keyword: qs qs.filter(course__name__icontainskeyword) paginator Paginator(qs, 10) page request.GET.get(page, 1) classes paginator.get_page(page) return render(request, courses/course_list.html, {classes: classes})模板里循环classes然后渲染上一页下一页链接时注意把当前搜索参数拼到query string里否则翻页会丢失搜索条件。这个细节也是我在调试时被问过很多次的。6. 远程调试与部署从跑通到交付6.1 本地开发调试runserver、Debug Toolbar与shell_plus选课系统本身的性能瓶颈不在框架而在数据库查询和并发控制。本地调试时我强烈建议装上django-debug-toolbar它会在页面侧边栏显示每次请求的SQL耗时和查询条数。很多N1查询都是靠它一眼看出来的。# requirements.txt Django4.2 django-debug-toolbar4.2 ipython再用python manage.py shell_plus做交互式数据验证比如直接执行批量选课模拟比在页面里点来点去快得多。6.2 远程调试配置VS Code和PyCharm都要会平台写远程调试是这套毕设服务的卖点之一实际上远程调试本质是让本地的IDE连接服务器上的Python进程打断点、看变量。我以最常用的两个IDE为例说明。PyCharm的做法是先在服务器上安装debugpy依赖库然后在代码里加一段调试监听的启动逻辑或者直接在Run/Debug Configuration里配置Python Debug Server。配置好端口后服务器上运行import debugpy # 等待本地IDE接入 debugpy.listen((0.0.0.0, 5678)) print(Waiting for debugger attach...) debugpy.wait_for_client()本地PyCharm里新建一个Python Debug Server配置host填服务器IPport填5678然后点debug启动就能远程打断点。调试完后必须把这段代码从项目里移除否则服务器上的服务会一直等待调试器接入导致正常请求卡住。VS Code的做法类似但更推荐用Python: Remote Attach配置。你需要在.vscode/launch.json里写{ version: 0.2.0, configurations: [ { name: Python: Remote Attach, type: debugpy, request: attach, connect: { host: 服务器IP, port: 5678 }, pathMappings: [ { localRoot: ${workspaceFolder}, remoteRoot: /home/ubuntu/project } ] } ] }pathMappings是最容易漏的配置项它要把服务器上的项目绝对路径映射到本地工作区路径否则断点命中了但定位不到源码文件。6.3 部署上线最容易踩的坑做完毕设总要演示演示现场用runserver是最省事的但如果你想走真实部署流程我这里列几个高频坑。第一DEBUG False之后静态文件全挂。因为Django的开发服务器不会托管静态文件了需要先执行python manage.py collectstatic再用whitenoise中间件托管或者交给Nginx处理。毕设演示建议直接用whitenoise配置最省事。第二ALLOWED_HOSTS没配好导致400错误。部署时至少要写上服务器IP和域名ALLOWED_HOSTS [服务器公网IP, localhost, 127.0.0.1]第三数据库迁移顺序出错。正确流程是先python manage.py makemigrations、再python manage.py migrate但如果从别人那里拷贝了完整SQL文件则不需要重新迁移。这两个方式不要混用否则会出现字段冲突。第四使用Gunicorn启动时runserver里生效的代码热更新不再有改完代码必须重启进程。建议用gunicorn config.wsgi:application --bind 0.0.0.0:8000启动配一个systemd服务或直接用supervisor管理。另外远程调试端口比如5678在防火墙里要放行但生产服务器上建议调试完就关闭端口尽量不要长期暴露避免被扫描工具盯上。7. 这套系统还能怎么扩展从及格到加分项7.1 培养方案校验和学分绩点计算如果想让系统在答辩时多一点亮点可以在现有模型上扩展培养方案概念。学生选课前系统按照专业培养方案检查课程是否属于本专业必修或选修范围超学分上限则拦截。实现起来就是在Major上挂一个required_courses多对多关系然后在选课校验里增加一条口径。毕业设计如果做了这块就可以理直气壮地在答辩PPT里写系统不仅实现了选课还实现了基于专业培养方案的选课合规性校验听起来就比普通选课系统高一级。7.2 选课时间窗口、缓存与异步通知另一个不错的扩展是选课时间窗口。在TeachingClass增加start_select_at和end_select_at两个DateTimeField选课视图增加一个时间判断即可。这个功能演示效果非常直观能让学生看到选课未开始和选课已关闭的状态切换。热门课程选课瞬间的高并发是真实场景毕设里不一定真要扛住几千并发但你可以用Redis给课程列表加缓存减少数据库重复查询。选课成功后向学生发送站内通知则可以用Django的信号机制实现在SelectionRecord创建后给相关用户生成一条Notification记录。这些都是不会破坏原有代码的增量扩展压力不大但对答辩内容丰富度帮助很大。从我个人实际调试这些选课项目的经验看真正让系统拉开差距的不是某个炫酷功能而是细节是否扎实是否使用了事务包裹选课逻辑、是否做了数据库索引、模板有没有大量重复代码、日志能不能定位到出错的请求。把这些做扎实你的选课系统就是能站上台讲的作品。最后再分享一个我自己的处理习惯拿到任何一套Django毕设源码先不要急着改业务代码第一件事是看settings.py里的数据库配置和requirements.txt第二件事是跑通系统并手动走一遍学生选课、教师录成绩、管理员排课的完整流程第三件事才是打开IDE去理解每个视图。这三步走完你已经比大多数只想要能跑就行的同学理解深入得多。这套顺序也建议你用到自己接下来要做的任何一个Django项目中去。
返回列表