☰
基于Django的学生选课系统:从数据库设计到部署答辩全解析
2026/10/4 13:15:53 网站建设 项目流程

1. 毕设选题前,先把“学生选课系统”这事想透

每年到毕设季,都会有学弟学妹来问我选什么题目,我的建议一直是:宁可做一个小而完整、逻辑闭环的系统,也别碰那种看起来高大上、实际上自己根本讲不清楚的大项目。学生选课系统就是一个典型的“小而完整”选题——它在业务上有明确的角色划分,在技术上有清晰的CRUD主线,在展示上又方便演示和截图,应付开题、中期、答辩这三个环节都非常顺手。

先说清楚这一版系统到底做了什么:这是基于Django框架实现的选课系统,角色分为学生、教师和管理员。学生可以浏览课程、选课、退课、查看已选课程和成绩;教师可以发布课程、维护课程信息、录入学生成绩;管理员负责管理学生账号、教师账号、课程审核以及系统基础数据。整体业务不算复杂,但该有的模块都有,而且每一条链路都能跑通。

技术栈用的是Django + SQLite(后续可以无缝切换到MySQL),前端是Django模板引擎配合Bootstrap,没有引入重型前端框架,这样不管是你自己写还是后期讲解,都更容易把逻辑讲清楚。项目结构按照Django的MVT模式组织,models负责数据模型,views负责业务逻辑,templates负责页面渲染,forms负责表单校验,天然就适合用来回答答辩时“你是怎么理解MVC/MVT”这类经典问题。

如果你正准备做毕设,或者只是想通过一个完整的Web项目来练手Django,这篇内容会从数据库设计、核心功能实现、前端页面处理、部署演示、答辩准备五个方面,把这一套选课系统的实现思路和踩坑记录完整拆开讲一遍。文章里的所有方案都基于真实可运行的Django项目实践,不是那种只有空壳代码的“玩具项目”。

2. 数据库建模:一张ER图定下整个系统的地基

2.1 核心实体与字段设计

选课系统的数据库设计其实非常典型,它涉及到用户、课程、选课关系、成绩这几个核心实体,下面我把每个模型的关键字段和设计理由列出来,这也是答辩时老师几乎必问的部分。

User模型(用户):我用的是Django自带的AbstractUser扩展,在原有基础上增加user_type字段,区分学生、教师、管理员三种角色。用AbstractUser而不是自定义全新的用户模型,原因是Django内置的认证系统(登录、session、密码哈希)可以直接复用,省下大量造轮子的工作量,而且后期扩展权限体系也更方便。

from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE_CHOICES = ( (1, '管理员'), (2, '教师'), (3, '学生'), ) user_type = models.IntegerField(choices=USER_TYPE_CHOICES, default=3, verbose_name='用户类型') student_id = models.CharField(max_length=20, blank=True, null=True, verbose_name='学号') teacher_id = models.CharField(max_length=20, blank=True, null=True, verbose_name='工号') class Meta: verbose_name = '用户' verbose_name_plural = verbose_name

这个模型的关键细节是同时预留了student_id和teacher_id两个字段,但用user_type来做判定。这样做的好处是:后面无论是用Django自带的@login_required做登录保护,还是用自定义装饰器做角色权限控制,判断逻辑都非常直观。

Course模型(课程):课程表是整个系统的核心资源,字段包括课程名称、课程代码、授课教师、学分、上课时间、上课地点、容量上限、已选人数、课程简介等。这里有一个很容易被忽略的坑——授课教师字段的外键关联。很多学生会把教师字段直接关联到User表,但没有限制只能关联教师角色,导致数据库层面无法保证数据正确性。

class Course(models.Model): course_code = models.CharField(max_length=20, unique=True, verbose_name='课程代码') name = models.CharField(max_length=100, verbose_name='课程名称') teacher = models.ForeignKey(User, on_delete=models.CASCADE, limit_choices_to={'user_type': 2}, verbose_name='授课教师') credit = models.FloatField(verbose_name='学分') schedule = models.CharField(max_length=200, verbose_name='上课时间') location = models.CharField(max_length=100, verbose_name='上课地点') capacity = models.IntegerField(verbose_name='容量上限') enrolled = models.IntegerField(default=0, verbose_name='已选人数') description = models.TextField(blank=True, verbose_name='课程简介') status = models.IntegerField(default=1, choices=((1, '开放选课'), (0, '已关闭')), verbose_name='选课状态') class Meta: verbose_name = '课程' verbose_name_plural = verbose_name def __str__(self): return f'{self.course_code} - {self.name}'

limit_choices_to={'user_type': 2}是Django ORM提供的外键候选限制,它本身不是数据库级约束,但在后台管理页面和表单校验层能起到极大的便捷作用。当然,业务代码里还是要写一层防御逻辑,我稍后会讲到。

Selection模型(选课记录):这是选课系统的核心关联表,负责记录“哪个学生选了哪门课”。字段包括学生外键、课程外键、选课时间、退课时间(可选)、成绩(允许为空)。这里必须提一个关键设计:成绩字段到底是放在选课记录里,还是单独建一张成绩表?

我最终决定放在选课记录里。原因是:在真实教务系统里,一门课的成绩确实只跟“某个学生选这门课”这个事实绑定,放在Selection里天然就是一一对应的,不需要额外的关联查询。如果单独建成绩表,反而要维护外键关系,增加系统的复杂度,而且对于毕设来说完全没有必要。

class Selection(models.Model): student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='selections', verbose_name='学生') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='selections', verbose_name='课程') selected_at = models.DateTimeField(auto_now_add=True, verbose_name='选课时间') grade = models.FloatField(null=True, blank=True, verbose_name='成绩') class Meta: unique_together = ('student', 'course') verbose_name = '选课记录' verbose_name_plural = verbose_name

unique_together这个约束非常重要,它从数据库层面保证了“同一个学生不能重复选同一门课”。如果没有这一条,仅靠业务代码去查重,在高并发场景下(虽然毕设一般不会有,但你要让老师看到你有这个意识)会出现重复选课的问题。

2.2 业务逻辑中的并发与事务问题

选课系统的经典业务挑战是并发选课下的超额问题。想象一下:课程容量是50人,当前已选49人,两个学生同时在最后一秒提交选课请求。如果没有任何防护,两个人的请求都读到“已选49人”,然后都执行“已选人数+1写回”,最终课程记录变成51人,超载了。

常见解决方案有两种思路:

第一种是通过select_for_update()加行级锁,在开启事务的前提下锁定课程记录,这是最直接的做法:

from django.db import transaction @transaction.atomic def select_course(request, course_id): course = Course.objects.select_for_update().get(pk=course_id) if course.enrolled >= course.capacity: return JsonResponse({'code': 1, 'msg': '课程已满员'}) if Selection.objects.filter(student=request.user, course=course).exists(): return JsonResponse({'code': 2, 'msg': '请勿重复选课'}) Selection.objects.create(student=request.user, course=course) course.enrolled += 1 course.save(update_fields=['enrolled']) return JsonResponse({'code': 0, 'msg': '选课成功'})

第二种是用Django F表达式做原子更新,不需要手动加锁:

from django.db.models import F course = Course.objects.filter(pk=course_id, enrolled__lt=F('capacity')).update(enrolled=F('enrolled') + 1) if course == 0: return JsonResponse({'code': 1, 'msg': '选课失败,课程可能已满员'})

我实际项目里用的是第一种方案,因为select_for_update()的逻辑更容易在答辩时讲清楚:先锁住资源,再检查、再修改,符合传统数据库事务的直觉。而且在MySQL下性能完全够用。要切换到PostgreSQL或MySQL时,需要保证数据库支持行级锁,SQLite在低并发下也能正常工作,但毕设演示完全没问题,这点不用过度担心。

2.3 为什么用Django自带User而不是自定义用户表

很多毕设项目会把用户表设计成自己的独立表,然后所有验证逻辑全部自己写。我的观点是:能用Django自带的AbstractUser就绝不要自己造用户模型。

Django的auth应用已经帮你做好了密码加密(PBKDF2/SHA256)、会话管理(session)、权限系统(permissions)、后台管理(admin),这些功能如果全部自己实现,工作量远超你想象的。而且你在答辩时可以说“密码字段我采用的是Django认证系统的密码哈希算法,数据库里不存明文”,这是一个很加分的表述。

再补充一个细节:Django 5.x开始,部分方法(比如is_authenticated从方法变成了属性)有细微变化,如果你的代码是基于Django 4.x写的,在迁移到Django 5.x时要注意检查。我自己的项目用的是Django 4.2 LTS版本,这是一个长期支持版本,稳定性有保障,建议做毕设的同学直接用4.2 LTS或者更新的稳定版,不要追最新特性而忽视生态兼容性。

3. 核心功能模块:从登录权限到选课退课,每条链路都有细节

3.1 登录、注册与角色权限控制

系统的入口是登录页。我实现了学生和教师共用一套登录入口,用户名密码验证通过后,根据user_type跳转到不同的首页。核心代码如下:

from django.contrib.auth import authenticate, login from django.shortcuts import render, redirect def login_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(username=username, password=password) if user is not None: login(request, user) if user.user_type == 2: return redirect('teacher_dashboard') elif user.user_type == 1: return redirect('admin_index') else: return redirect('student_dashboard') else: return render(request, 'login.html', {'error': '用户名或密码错误'}) return render(request, 'login.html')

这里有一个毕设高频问题:“你是怎么处理权限控制的?”如果你的回答只是“根据用户类型跳转到不同页面”,那显然太单薄了。完整方案需要配合Django的装饰器,对每个视图函数做角色校验:

from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied def student_required(view_func): @login_required def wrapper(request, *args, **kwargs): if request.user.user_type != 3: raise PermissionDenied('仅学生可以访问') return view_func(request, *args, **kwargs) return wrapper

自定义装饰器的好处是,每个业务视图只需要加一行@student_required,整个函数就有了登录校验和角色校验两层保护。模板里做按钮级控制时,配合{% if request.user.user_type == 3 %}判断是否显示“选课”按钮即可,这样即使URL被猜到了,后端的权限拦截也会兜底。

关于注册功能,我建议做一个简化版本:学生可以自助注册,但注册时必须填写学号;教师账号由管理员后台创建。这样一方面控制注册入口的合规性,另一方面也避免答辩时被问到“谁都可以注册教师账号怎么办”这种尴尬问题。

3.2 选课与退课:前端交互和后端校验的配合

选课页面是系统里最核心也最容易被老师反复查看的页面。我设计了两种选课入口:课程列表页直接点击“选课”按钮,以及课程详情页里的大按钮选课。两条入口都走同一个后端接口,只是URL参数不同。

前端按钮的交互要处理好“三种状态”:未选(显示“选课”)、已选(显示“已选”并且禁用)、课程已满(显示“已满”并禁用)。后端渲染时直接把状态字段传给模板:

# views.py def course_list(request): courses = Course.objects.filter(status=1) selected_ids = Selection.objects.filter(student=request.user).values_list('course_id', flat=True) if request.user.user_type == 3 else [] course_data = [] for course in courses: course_data.append({ 'course': course, 'selected': course.id in selected_ids, 'full': course.enrolled >= course.capacity, }) return render(request, 'course_list.html', {'course_data': course_data})

这里提一下一次查询的优化细节——用values_list('course_id', flat=True)把已选课程ID集合一次性取出,然后转换成Python的set做判断,避免在循环里反复查询数据库,也就是避免N+1查询问题。虽然课程量不大时性能差距不明显,但这是答辩老师非常喜欢听的一个性能优化点。

退课功能的逻辑正好反过来:检查选课记录是否存在,存在就删除记录并将课程enrolled减1。在做删除之前,我用了一个transaction.atomic包裹,保证“删除选课记录”和“已选人数减一”这两个操作要么同时成功,要么同时回滚,避免数据不一致。

from django.db import transaction @transaction.atomic def drop_course(request, course_id): course = Course.objects.select_for_update().get(pk=course_id) deleted_count, _ = Selection.objects.filter(student=request.user, course=course).delete() if deleted_count == 0: return JsonResponse({'code': 1, 'msg': '你还没有选这门课'}) course.enrolled = F('enrolled') - 1 course.save(update_fields=['enrolled']) return JsonResponse({'code': 0, 'msg': '退课成功'})

3.3 成绩管理:教师录入与学生查看的权限分隔

教师端成绩录入是很多人会忘记做的模块,但它恰恰是完整业务闭环的重要一环。没有成绩模块的话,系统就只有选课和退课,缺少一条“结课出成绩→学生查看成绩”的后续链路,会在答辩时暴露业务逻辑的不完整性。

教师端成绩录入页面按课程维度展示选课学生列表,每条记录后面有一个分数输入框,支持批量保存。这里要注意:成绩的更新/插入用update_or_create方法比先查后改更能避免脏数据。

if request.method == 'POST': grade_data = request.POST.dict() for key, value in grade_data.items(): if key.startswith('grade_'): selection_id = key.split('_')[1] try: selection = Selection.objects.select_for_update().get( pk=selection_id, course__teacher=request.user ) grade = float(value) if value else None if grade is not None and (grade < 0 or grade > 100): return JsonResponse({'code': 1, 'msg': '成绩必须在0-100之间'}) selection.grade = grade selection.save(update_fields=['grade']) except Selection.DoesNotExist: return JsonResponse({'code': 1, 'msg': '数据异常,请刷新页面重试'})

学生端查看成绩时,只显示自己的课程和分数,用Selection.objects.filter(student=request.user).select_related('course')一次查询拿到关联课程信息,避免循环查询。显示时对成绩做一次“绩点”换算,这种延伸功能虽然不复杂,但在答辩时可以作为“业务拓展点”来讲述,显得你除了完成基本功能还有主动思考。

3.4 后台管理:Django Admin的合理复用与自定义

很多学生不愿意用Django Admin,觉得“这是框架自带的,写出来显得没技术含量”,但我强烈建议一定要用Admin,并且要自定义。

Django Admin的价值在于:你不需要为管理员单独写一套完整的管理前端,注册模型之后,增删改查、搜索、筛选、分页全都有了。你要做的核心工作是:

  • 在admin.py里注册Course、Selection、User模型;
  • 配置list_display、search_fields、list_filter,让列表页更清晰;
  • 对于Selection这种关联表,配置好list_select_related,避免列表页产生大量查询;
  • 用admin的actions实现批量操作,比如批量关闭课程、批量导入学生账号。
@admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ['id', 'course_code', 'name', 'teacher', 'credit', 'capacity', 'enrolled', 'status'] search_fields = ['course_code', 'name', 'teacher__username'] list_filter = ['status', 'credit'] list_editable = ['status'] actions = ['close_courses', 'open_courses'] def close_courses(self, request, queryset): queryset.update(status=0) self.message_user(request, f"已关闭 {queryset.count()} 门课程") close_courses.short_description = '关闭选课'

list_editable允许管理员直接在列表页勾选修改“选课状态”,这一个功能就能在演示时省很多口舌——不用点进详情页,列表页直接改字段,视觉效果好,逻辑也清楚。

4. 前端页面与交互设计:模板引擎、Bootstrap和AJAX

4.1 基础布局与页面骨架

Django的模板继承机制非常适合这个项目。我建了一个base.html作为公共骨架,里面包含导航栏、Bootstrap CSS/JS引用、消息提示区域和内容块{% block content %}。所有子页面只需要继承它并填充自己的内容块即可。这样不但代码复用率高,改导航栏时也只需要改一个文件。

<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}学生选课系统{% endblock %}</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <!-- 导航栏内容 --> </nav> <main class="container mt-4"> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }}">{{ message }}</div> {% endfor %} {% endif %} {% block content %}{% endblock %} </main> </body> </html>

这里补充一个中文界面经常踩的坑:页面显示乱码的地方多半不是Django的问题,而是settings.py里LANGUAGE_CODE和TIME_ZONE没有配置好。建议设置LANGUAGE_CODE = 'zh-hans'、TIME_ZONE = 'Asia/Shanghai'、USE_TZ = True。另外在HTML文件顶部务必加<meta charset="UTF-8">,否则某些浏览器会用默认编码解析导致乱码。

4.2 选课按钮的AJAX交互实现

选课这个动作如果用传统表单提交,每次点击都要刷新整个页面,演示体验比较差。我用了原生JavaScript的fetch发送AJAX请求,后端返回JsonResponse,前端根据返回的code值决定是弹出成功提示还是失败提示。这里用原生JS而非jQuery或框架,是为了让代码逻辑更容易讲解,也不需要额外引用库。

function selectCourse(courseId, btnElement) { fetch(`/student/select/${courseId}/`, { method: 'POST', headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) .then(response => response.json()) .then(data => { if (data.code === 0) { alert(data.msg); btnElement.textContent = '已选'; btnElement.disabled = true; // 更新已选人数显示 document.getElementById(`enrolled_${courseId}`).textContent = parseInt(document.getElementById(`enrolled_${courseId}`).textContent) + 1; } else { alert(data.msg); } }) .catch(error => console.error('Error:', error)); } function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; }

这一段getCookie函数是Django的CSRF防护对应的前端标配。很多新手会在AJAX请求上忘记送X-CSRFToken,结果后端疯狂报403错误。虽然还有更优雅的@csrf_exempt方案,但我不建议为了省事而直接关掉CSRF防护——在答辩时被问到网络安全问题时,保住CSRF防护是加分项,关掉是减分项。

4.3 响应式与美观度处理

毕设系统的前端不需要炫技,但要整体干净、看着舒服。我用的是Bootstrap 5的栅格系统和卡片组件,课程列表以卡片形式展示课程名称、教师、学分、时间地点、容量进度条和选课按钮。Bootstrap的进度条天然适合展示“已选人数/容量”,颜色还可以根据比例变化:低于60%显示蓝色,60%-90%显示黄色,超过90%显示红色。

这里有一个前端开发经验想分享:样式文件不要写太多自定义CSS,尽量复用Bootstrap的utility class,比如text-muted、mt-3、card shadow-sm。自定义CSS越少,后期改样式越轻松,页面也越不容易出现布局塌陷的问题。真正需要自定义的往往只有导航栏颜色、卡片圆角等几个全局变量,改样式时优先去改Bootstrap的CSS变量,而不是写一大堆覆盖样式。

5. 部署与运行:从本地开发到线上演示,一把梭的完整流程

5.1 本地运行环境配置

不管你的系统最终是提交文档、演示视频,还是实际部署到服务器,能在本地一键跑起来是基本要求。我习惯把整个流程写成README.md放在项目根目录,读者拿到代码后按步骤执行即可复现。

第一步是创建虚拟环境并安装依赖:

python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install django==4.2.14 pillow

这里注意:如果你的系统里有图片上传功能(比如课程封面),就必须安装pillow库,否则Django在迁移时使用ImageField会报错。没有图片字段的可以不用装,但装了也无妨,反正体积不大。

第二步是数据库迁移和创建超级管理员:

python manage.py makemigrations python manage.py migrate python manage.py createsuperuser

第三步是准备初始演示数据。为了让答辩演示不出现空荡荡的界面,我写了一个fixtures/initial_data.json文件,内置了几个学生账号、教师账号、十几门课程和若干选课记录,通过一行命令即可导入:

python manage.py loaddata initial_data.json

这里要特别提醒:不要把SECRET_KEY、数据库密码等硬编码进settings.py后提交到Github,虽然毕设项目泄露风险一般可控,但要养成用环境变量或.env文件管理的习惯,把这个作为独立的小亮点写进项目文档里:

import os from dotenv import load_dotenv load_dotenv() SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-default-key') DEBUG = os.getenv('DJANGO_DEBUG', 'True') == 'True'

5.2 线上部署方案:Nginx + Gunicorn + SQLite的保守打法

如果你需要部署到服务器给评委远程演示,我推荐一个最保守、最不容易出问题的方案:Nginx + Gunicorn + SQLite,不引入Docker、不引入MySQL、不引入Redis。理由是:毕设演示环境往往网络和资源有限,组件越多,出问题的概率越高,而SQLite完全能支撑演示级并发。

具体步骤如下:

  1. 在服务器上安装Python环境和Nginx(命令略,常规系统包管理即可);
  2. 把项目代码上传到服务器,创建虚拟环境,安装依赖;
  3. 执行迁移命令,加载初始数据;
  4. 安装Gunicorn并启动:
pip install gunicorn gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --daemon
  1. 配置Nginx反向代理到8000端口,并托管静态文件。

Nginx配置里最容易踩的坑是静态文件路径。Django开发环境下静态文件由runserver直接处理,部署后必须执行python manage.py collectstatic把所有静态文件收集到一个目录,然后在Nginx的location /static/里指向这个目录,否则页面CSS/JS全部丢失。

server { listen 80; server_name your_domain_or_ip; location /static/ { alias /path/to/your/project/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

5.3 部署验证清单

部署完成后不要急着关终端,按以下清单逐项测试:

  • 访问首页,确认登录页正常加载;
  • 学生账号登录,测试选课、退课,确认AJAX请求没有跨域问题或403错误;
  • 教师账号登录,确认课程管理、成绩录入页面渲染正常;
  • 管理员登录Django Admin,确认模型管理页面可以增删改查数据;
  • 直接访问一个不允许学生访问的URL,确认权限拦截生效,返回403页面而不是报错页面;
  • 刷新多次选课请求,确认并发场景下容量数据不会错乱。

这个清单我建议也写进项目文档的“部署测试”小节,答辩时直接说“我是按这个清单完成验证的”,会让老师觉得你的项目工程化程度高,而不只是“能运行”的水平。

6. 代码讲解思路:拿到这份源码后,如何高效地讲清楚自己的项目

6.1 从URL路由反推项目结构,快速建立全局观

很多同学拿到一套源码后,喜欢按文件列表顺序从头看到尾,比如从models.py一直看到views.py,结果看到后面忘了前面。我的做法相反:先从config/urls.py开始看。因为URL配置是整个系统的“入口地图”,它显示了哪些路径由哪个应用、哪个视图函数处理,看完urls就大致知道项目有几个模块、每个模块有哪些功能页面。

以这个选课系统为例,urls.py里可能包含这些路由:

urlpatterns = [ path('admin/', admin.site.urls), path('', views.login_view, name='login'), path('logout/', views.logout_view, name='logout'), path('student/dashboard/', student_views.dashboard, name='student_dashboard'), path('student/courses/', student_views.course_list, name='course_list'), path('student/select/<int:course_id>/', student_views.select_course, name='select_course'), path('student/drop/<int:course_id>/', student_views.drop_course, name='drop_course'), path('student/my_courses/', student_views.my_courses, name='my_courses'), path('student/grades/', student_views.grade_list, name='grade_list'), path('teacher/dashboard/', teacher_views.dashboard, name='teacher_dashboard'), path('teacher/course/manage/', teacher_views.manage_courses, name='manage_courses'), path('teacher/course/create/', teacher_views.create_course, name='create_course'), path('teacher/course/<int:course_id>/grades/', teacher_views.manage_grades, name='manage_grades'), ]

看完URL后,接下来按“一个视图函数对应一个页面”的方式逐个深入:先看views.py里函数怎么取数据,数据传给哪个模板,模板里怎么展示。这样一个闭环就理解了,不用把所有代码读完,只需要对照自己讲解时的重点模块深入即可。

6.2 答辩现场的讲解节奏与演示脚本

答辩演示一般只有5-10分钟,很多同学一上台就想把所有模块都点一遍,反而像走马观花。我的建议是准备一个“演示脚本”,按下面这个节奏走:

  1. 注册/登录(30秒):让学生账号登录,强调登录用的是Django自带认证机制;
  2. 课程列表与选课(2分钟):浏览课程卡片,点击选课,选中后按钮变灰,容量数字变化,顺带提一句“这里用了AJAX请求,页面无刷新更新数据”;
  3. 选课后看我的课表(1分钟):进入“我的课程”页面,展示选课记录与课程信息,然后退掉一门课,再回去看容量下降;
  4. 教师端录成绩(1.5分钟):切换教师账号登录,进入成绩录入页面,给个别学生打分,保存后提醒“等学生端刷新就能看见”;
  5. 学生端看成绩(30秒):回到学生账号,刷新成绩页面,展示刚录入的分数;
  6. 管理员后台(1分钟):登录Admin后台,展示课程列表、筛选、关闭选课状态的操作。

这个节奏的巧妙之处在于:它完整展示了一条业务闭环,从选课到退课到录成绩到查成绩,每一步都有因果关联,评委能快速理解系统的完整逻辑。如果只是零散地展示“这里有选课”“那里有课程管理”,缺乏故事线,效果会大打折扣。

6.3 常见追问与高情商回答模板

答辩会有几个高频问题,提前准备好回答话术比现场临场发挥要稳得多:

  • “为什么用Django不用Flask?”回答思路:选课系统涉及多个业务模块的权限管理和模型关联,Django自带ORM、Admin、认证系统、迁移工具,开箱即用,开发效率更高,更适合一个完整的工程化项目。
  • “课程表的已选人数和选课记录不会不一致吗?”回答思路:我在更新已选人数时使用了select_for_update()行级锁加事务原子性,保证加减操作在同一事务中完成,而且选课记录的增加和已选人数的递增是同步完成的。
  • “如果把数据库换成MySQL,需要改哪些地方?”回答思路:只需要在settings.py中修改数据库配置,ORM层的模型代码基本不用改动,因为Django的ORM在底层已经做了方言适配。这是个标准答案,也是体现ORM优势的最佳回答。
  • “系统有什么不足之处或改进方向?”回答思路:可以说目前缺少基于Redis的缓存层,在大量并发选课场景下可以考虑引入消息队列削峰,另外可以增加课程的选课时间窗口控制。主动承认不足并提供改进方案,比硬着头皮说“这个系统已经很完美了”效果好得多。

7. 从毕设到工程化:这套系统还能延伸出哪些加分玩法

写完一个能运行的选课系统只是第一步,如果你有多余时间(或者想让项目显得更饱满),下面这几个延伸玩法成本不高、但答辩效果很好。

玩法一:增加Redis缓存热点课程。在课程列表页设置一个“热门课程排行榜”,用Redis的Sorted Set存储课程访问热度,每有用户查看课程详情,就给该课程的score加1。代码量很少,但能引出“缓存设计”“热点数据处理”等话题,非常加分。

import redis redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) def course_detail(request, course_id): redis_client.zincrby('hot_courses', 1, course_id) course = get_object_or_404(Course, pk=course_id) return render(request, 'course_detail.html', {'course': course})

玩法二:加入Excel导入导出。教师批量录入学生名单或导入课程表是教务系统的刚需。用openpyxl库实现一个“导入课程Excel文件”的功能,批量创建课程记录;用csv模块实现“导出选课名单”,直接在浏览器下载CSV文件。这个功能只需一个上传表单和一个解析循环,但演示效果很强。

玩法三:仪表盘可视化。用Chart.js在教师端和管理员端做一个简单的统计面板,展示每个院系的选课人数分布、各课程选课率、成绩分布直方图。Chart.js引入极其方便,后端只需要提供一个JSON数据接口,前端渲染图表,代码量在100行以内,但视觉冲击力很大,尤其适合在答辩最后一页PPT切换时展示。

我个人的建议是:不要所有玩法都堆上去,选一到两个和你的开题报告、项目文档里提到的方向一致的功能做延伸就够了。做太多反而增加讲不清楚的风险,记住**“少而精”永远比“多而杂”更安全**。

8. 写在最后的心得

这一套学生选课系统做完,最大的体会是:毕设项目的难点从来不在某一个技术点的炫酷,而在于把整条业务链路做顺、做完整、讲清楚。很多同学一开始痴迷于用什么高深算法、什么分布式架构,最后连基本的登录认证和数据库关系都理不顺。反而是这种踏踏实实把用户、角色、课程、选课、退课、成绩这条主线一步步打磨完整的项目,能让你在答辩时底气十足。

代码本身的量级不算大,但每一块功能背后都有值得深挖的细节:并发选课怎么防超卖、CSRF防护怎么处理、外键字段怎么限制角色、Nginx静态文件怎么配置。把这些细节吃透,你会发现哪怕只是一个小系统,也足够体现一名工程人员在设计、编码、调试、部署全过程中的综合能力。这也是导师和评委真正想看到的——不是“会用框架”,而是“懂设计、能动手、会思考”。

如果你正准备动手做这个题目,我的建议是:先别急着写代码,花两三天把数据库表设计好、把角色和权限梳理清楚、把页面流程画出来,想清楚再动手。好的开始是成功一半,这句话在毕设项目里尤其真实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询