☰
基于Django的学生作业管理系统开发实践:从数据库设计到部署
2026/10/9 6:58:37 网站建设 项目流程

从标题看就是一个很典型的课程设计/毕设选题,但越典型的项目,越值得把细节做扎实。我自己带过几届毕业生,也帮人改过不少作业管理系统,最深的感受是:大部分项目代码能跑,但一深问就说不出所以然,数据库为什么这样设计、权限怎么控制、为什么用Django自带User表,这些问题才是面试官和老师真正关心的点。这篇就按我实际开发这套学生作业管理系统的经验,从需求拆解、数据库设计、核心功能实现到部署踩坑,完整捋一遍,代码可以直接拿去用,更重要的是搞懂每一步的为什么。

1. 项目概览:学生作业管理系统到底解决什么问题

1.1 为什么选择Python + Django这套组合

先说选型。作业管理系统不是只有Django能做,Spring Boot、Flask、甚至PHP都能写,但Python+Django在几个核心点上特别适合这个场景:

  • 开发效率高:Django自带Admin后台、ORM、认证系统、表单处理,一个作业管理系统最核心的"谁登录、谁提交、谁批改"逻辑,用Django的全家桶可以少写一半代码。
  • 模板系统方便:Django的DTL模板语法简单,配合Bootstrap,前端页面很快就能搭出来,不需要单独前后端分离。
  • 生态成熟:文件上传、分页、消息提示这些功能都有现成方案,踩坑少。
  • 学习成本适中:对于学生项目或个人练手,Django比Spring Boot更容易上手,又能学到完整的Web开发流程。

我见过不少用Flask写的管理系统,Flask灵活但很多功能要自己拼,比如Admin、认证、ORM都要额外找库。作业管理系统虽然小,但五脏俱全,用Django这种"大而全"的框架反而省心。

我个人在实际开发中的习惯是:**项目的技术选型不是越炫越好,而是看团队和场景。**这个系统如果是作为毕设,用Django既展示了对Web框架的理解,又不会在环境搭建上卡太久;如果是企业内小工具,Django的稳定性也能扛住几百人的使用量。

1.2 系统的核心模块与整体架构

学生作业管理系统的核心需求很明确,无非是:

  • 老师:创建课程、发布作业、查看提交、批改打分、导出成绩。
  • 学生:查看作业、提交作业(文件或文本)、查看分数和评语。
  • 管理员:管理用户、管理课程、系统配置。

再往后想一步,我们还需要班级的概念。因为国内高校的课程通常和组织架构挂钩,学生属于某个班级,老师教某个班级,这样作业发布和统计会更方便。

系统整体架构是标准的Django MTV模式:

  • Model层:用户、班级、课程、作业、提交记录、附件、通知等数据模型。
  • Template层:基于Bootstrap的响应式页面,教师端和学生端分开。
  • View层:基于函数的视图(FBV)和基于类的视图(CBV)混用,登录、权限、业务逻辑都在这一层。

部署上就是经典的Nginx + Gunicorn + MySQL + Django,开发环境用Django自带服务器 + SQLite也完全没问题。我后文会把两种方式的细节都讲清楚。

2. 数据库设计:作业系统的地基

2.1 实体识别与关系建模

数据库设计是整个系统里最不值得偷懒的部分。我见过太多人一上来就用一个"作业表"搞定所有字段,结果后面加需求时各种返工。先花20分钟把实体关系和字段想清楚,后面能省一周的时间。

这套系统的核心实体有7个:

  • 用户(扩展Django自带User)
  • 班级表(Class)
  • 课程表(Course)
  • 作业表(Assignment)
  • 提交表(Submission)
  • 附件表(Attachment)
  • 通知表(Notification)

关系如下:

  • 班级和学生是多对多。一个班级有多个学生,但一个学生可能属于多个班级吗?现实中可能,但为了简化,通常是班级一对多学生,即每个学生属于一个班级。
  • 课程和班级是多对多:一个课程可以被多个班级使用,一个班级有多个课程。
  • 老师和课程是多对多:一个老师可以教多门课,一门课也可以有多个老师(但通常为一对多,可以按需设计)。
  • 作业属于一门课程(外键),一门课程有多个作业。
  • 提交记录属于一个作业和一个学生(外键联合唯一,确保一个学生对一个作业只能有一次最终提交)。
  • 一个提交记录可以关联多个附件(文件,一对多)。

这里最需要注意的关键设计是提交表和作业表的关系。很多新手会把提交内容直接作为作业表的一个字段,导致一个学生多次提交时只能覆盖,没有历史记录,老师也没法看到多次提交的过程。正确的做法是单独建提交记录表,每一次提交都是一条新记录,或者对同一学生同一作业的提交保留版本历史。

核心表结构建议如下:

Django内置User表扩展:

  • user_type:用户类型(教师/学生/管理员),Django没有现成字段,我们可以加一个user_type,或者用Group来区分。我用的是自定义用户模型,直接继承AbstractUser,加一个user_type字段和所属班级外键。

班级表(SchoolClass):

  • name:班级名称
  • grade:年级
  • head_teacher:班主任(可选)

课程表(Course):

  • name:课程名称
  • code:课程代码
  • teacher:外键到User(教师)
  • semester:学期
  • students:多对多到User(选课学生)

作业表(Assignment):

  • title:作业标题
  • description:作业描述(富文本或纯文本)
  • course:外键到Course
  • created_by:外键到User(教师)
  • deadline:截止时间(非常重要,必须用DateTimeField)
  • total_score:总分
  • created_at、updated_at:时间戳

提交表(Submission):

  • assignment:外键到Assignment
  • student:外键到User
  • content:文本内容(可空)
  • submit_time:提交时间
  • status:状态(已提交/已批改/被打回)
  • score:分数(可空)
  • feedback:教师评语(可空)
  • is_late:是否迟交(这个可以在save时自动判断)

附件表(Attachment):

  • submission:外键到Submission,也可以设计为FileField挂在Submission上。如果一个提交包含多个文件,建议用附件表;如果只提交一个文件,直接在Submission加一个file字段即可。为了通用,我用Attachment表,但更多时候直接在Submission里放file字段更简单。

通知表(Notification):

  • user:发给谁
  • content:通知内容
  • is_read:是否已读
  • created_at

这部分设计好,基本系统就完成一半了。因为我们优先把关系捋清楚,而不是急着写代码。

2.2 模型定义与字段选择

以下是我实际开发中核心模型的一个代码节选(仅示意核心结构,完整源码可以看文末):

# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): class UserType(models.IntegerChoices): ADMIN = 0, "管理员" TEACHER = 1, "教师" STUDENT = 2, "学生" user_type = models.IntegerField(choices=UserType.choices, default=UserType.STUDENT) # 学生可能属于一个班级 class_room = models.ForeignKey('SchoolClass', null=True, blank=True, on_delete=models.SET_NULL, related_name='students') def __str__(self): return f"{self.username}({self.get_user_type_display()})" class SchoolClass(models.Model): name = models.CharField(max_length=100, unique=True) grade = models.CharField(max_length=20, blank=True) def __str__(self): return self.name class Course(models.Model): name = models.CharField(max_length=100) code = models.CharField(max_length=20, unique=True) teacher = models.ForeignKey(User, on_delete=models.CASCADE, related_name='teaching_courses', limit_choices_to={'user_type': 1}) semester = models.CharField(max_length=20, blank=True) students = models.ManyToManyField(User, related_name='learning_courses', blank=True) def __str__(self): return f"{self.name} ({self.code})" class Assignment(models.Model): title = models.CharField(max_length=200) description = models.TextField(blank=True) course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='assignments') created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_assignments') deadline = models.DateTimeField() total_score = models.DecimalField(max_digits=5, decimal_places=1, default=100) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): # 字段逻辑:可限制只有教师能创建作业 super().save(*args, **kwargs) def __str__(self): return f"{self.course.name} - {self.title}" class Submission(models.Model): assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name='submissions') student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') content = models.TextField(blank=True) # 如果说提交是一个文件,用FileField即可,但如果是多个文件可以建附件表 file = models.FileField(upload_to='submissions/%Y/%m/', null=True, blank=True) submit_time = models.DateTimeField(auto_now_add=True) score = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) feedback = models.TextField(blank=True) status = models.CharField(max_length=20, choices=[('submitted', '已提交'), ('graded', '已批改'), ('rejected', '被打回')], default='submitted') class Meta: # 确保一个学生一个作业只有一条有效记录(虽然可以多次提交,但只保留一条,覆盖更新) unique_together = ('assignment', 'student') @property def is_late(self): return self.submit_time > self.assignment.deadline def __str__(self): return f"{self.student.username} - {self.assignment.title}"

这里有几个很容易踩的坑:

  • 关联字段的on_delete:学生删除时,他的提交记录怎么办?如果直接CASCADE删除,那就连老师的批改记录也一起没了,这通常不是我们想要的。建议对Submission的student外键用on_delete=models.CASCADE,但保留历史成绩可以设SET_NULL,取决于业务。我的处理方式是:Student对应的User不允许删除,只做停用,所以CASCADE无关紧要。
  • limit_choices_to:给课程表的外键加上limit_choices_to={'user_type': 1},这样后台选老师时,只会列教师用户,不显示学生和管理员。这是Django后台的一个好用但容易被忽略的参数。
  • unique_together:这一个约束可以防止一个学生给同一个作业提交多条记录。如果题目要求允许多次提交并记录历史,就要去掉这个约束,加上一个version字段。我自己的设计是允许覆盖提交,每次保存都新建一条,但保留历史版本,这里为了演示简单采用了唯一约束,业务上需要时可以改成version + assignment + student联合唯一。

2.3 数据迁移与初始数据准备

数据库模型定义完成后,别急着写视图,先做数据迁移。命令很简单:

python manage.py makemigrations python manage.py migrate

但这里有一个大坑:如果你在一开始就自定义了User模型,必须在第一次migrate前完成设置,否则会报错。因为Django的迁移系统会自动创建默认的auth_user表,如果你中途改User模型,迁移会非常痛苦。

标准的做法是在创建项目后立刻在settings.py中加上:

AUTH_USER_MODEL = 'core.User'

其中core是你存放User模型的app名称。之后再运行makemigrations就不会有问题。

另外,初始数据(管理员账号、基础老师账号、测试班级)可以通过python manage.py shell或者写fixtures。更省事的办法是创建一个名为init_data.py的自定义管理命令,放在core/management/commands/目录下,里面创建超级管理员和一些示例数据,这样别人拿到源码也能快速初始化。

我用过的初始数据脚本长这样(简单示意):

# core/management/commands/init_data.py from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model User = get_user_model() class Command(BaseCommand): help = '初始化基础数据' def handle(self, *args, **options): if not User.objects.filter(username='admin').exists(): User.objects.create_superuser('admin', 'admin@example.com', 'admin123') self.stdout.write(self.style.SUCCESS('管理员创建成功'))

执行:

python manage.py init_data

这样项目一clone下来就能快速跑起来,不用手动去后台敲用户。好的项目文档里,把初始化步骤写清楚,这比代码本身更能体现工程素养。

3. 核心功能实现:从登录到作业闭环

3.1 用户认证与角色权限控制

Django自带非常完整的登录认证体系,但直接用它自带的登录页会没有样式且不够灵活。我们在项目里实际使用的是django.contrib.auth自带的login和login_required装饰器,再加一个简单的登录视图。

登录视图示例:

# views.py from django.contrib.auth import login, authenticate from django.shortcuts import render, redirect def login_view(request): if request.user.is_authenticated: return redirect('dashboard') if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user: login(request, user) return redirect('dashboard') else: return render(request, 'core/login.html', {'error': '用户名或密码错误'}) return render(request, 'core/login.html')

登录之后,我们需要根据user.user_type来分流到不同的首页:教师看到课程和作业管理,学生看到待提交作业列表,管理员看到用户统计。

权限控制我用了两层:

  • 第一层:login_required,确保用户必须登录。
  • 第二层:user_passes_test,确保用户是老师才能访问老师视图。

示例:

from django.contrib.auth.decorators import login_required, user_passes_test def teacher_required(user): return user.user_type == 1 @login_required @user_passes_test(teacher_required) def teacher_dashboard(request): ...

user_passes_test虽然好用,但有一个问题是它只能判断request.user,不能拿到当前对象(比如想判断“当前课程是否属于当前老师”),这种情况我一般直接在视图里做查询和判断,而不是依赖装饰器。

3.2 作业发布与更新

教师创建作业的核心视图比较简单,重点在于判断教师只给自己教的课程发布作业。创建作业时,course下拉框应该只显示当前教师任教的课程。

创建作业视图(简版):

@login_required @user_passes_test(teacher_required) def create_assignment(request): courses = Course.objects.filter(teacher=request.user) if request.method == 'POST': title = request.POST.get('title') description = request.POST.get('description') course_id = request.POST.get('course') deadline = request.POST.get('deadline') total_score = request.POST.get('total_score') # 验证字段 if not all([title, course_id, deadline]): return render(request, 'core/assignment_form.html', {'error': '必填项不能为空'}) course = Course.objects.get(id=course_id, teacher=request.user) # 确保是本人的课程 Assignment.objects.create( title=title, description=description, course=course, created_by=request.user, deadline=deadline, total_score=total_score ) return redirect('assignment_list') return render(request, 'core/assignment_form.html', {'courses': courses})

这里有一个细节:Course.objects.get(id=course_id, teacher=request.user),查询条件里带teacher=request.user是防越权的最简单手段。很多学生项目直接Course.objects.get(id=course_id),如果传入一个别人的course_id就会越权创建作业。这一点面试时经常被问到,建议写进代码里。

3.3 作业提交与截止时间管理

学生提交作业的视图,需要考虑几个边界情况:

  • 截止时间过了还能不能提交?业务上可以允许迟交,但系统要标记is_late。我通常设置:截止后不禁止提交,但会在作业详情页显示“迟交”标签,老师能看到。
  • 已经批改过的作业,学生还能重新提交吗?正常逻辑是:一旦老师批改打分,学生就不能再改了。实现方式是在提交视图里检查submission.status != 'graded'。
  • 文件上传:Django的FileField会自动处理文件存储,但需要配置MEDIA_ROOT和MEDIA_URL。

作业提交的典型视图逻辑:

@login_required @user_passes_test(student_required) def submit_assignment(request, assignment_id): assignment = get_object_or_404(Assignment, id=assignment_id) if not assignment.course.students.filter(id=request.user.id).exists(): return redirect('dashboard') # 不是选课学生不能提交 # 检查是否已有提交 submission = Submission.objects.filter(assignment=assignment, student=request.user).first() if request.method == 'POST': content = request.POST.get('content') file = request.FILES.get('file') if submission: if submission.status == 'graded': return render(request, 'core/error.html', {'msg': '作业已被批改,无法修改'}) submission.content = content if file: submission.file = file submission.save() else: Submission.objects.create(assignment=assignment, student=request.user, content=content, file=file) return redirect('assignment_detail', assignment_id=assignment.id) return render(request, 'core/submit_form.html', {'assignment': assignment, 'submission': submission})

这段代码里最关键的一个点,是assignment.course.students.filter(id=request.user.id).exists(),这行决定了不是选课学生就不能乱提交作业。很多系统漏掉这层校验,结果任何人都能向任意课程提交无限份作业,这是严重的权限漏洞。

关于文件上传,还要在settings.py里配置:

MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'

然后在项目的urls.py中加一段debug模式下的静态媒体文件映射:

if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

注意:这段只有在DEBUG=True时生效,生产环境用Nginx托管media目录,不能让Django直接处理静态文件,否则性能和安全性都不行。

3.4 批改与成绩管理

老师批改作业的核心视图,需要把提交记录按学生列出,教师给分数和评语。这里的细节是分页。一个50人的班级一份作业就50条记录,如果列表不写分页,后期会越来越卡。

我习惯用Django自带的分页器:

from django.core.paginator import Paginator @login_required @user_passes_test(teacher_required) def grade_submissions(request, assignment_id): assignment = get_object_or_404(Assignment, id=assignment_id, course__teacher=request.user) submissions = Submission.objects.filter(assignment=assignment).order_by('student__username') paginator = Paginator(submissions, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'core/grade_list.html', {'assignment': assignment, 'page_obj': page_obj})

批改时提交表单:

@login_required @user_passes_test(teacher_required) def save_grade(request, submission_id): submission = get_object_or_404(Submission, id=submission_id, assignment__course__teacher=request.user) if request.method == 'POST': score = request.POST.get('score') feedback = request.POST.get('feedback') if score is not None: submission.score = score submission.feedback = feedback submission.status = 'graded' submission.save() return redirect('grade_submissions', assignment_id=submission.assignment_id)

在成绩管理页面,还可以一键查看谁的作业还没交、谁迟交了、平均分是多少。这些统计可以通过Django的聚合函数实现,比如平均分:

from django.db.models import Avg avg_score = submission.aggregate(avg=Avg('score'))['avg']

这个调用非常常用,比手动遍历要高效很多。学会annotate和aggregate是Django进阶的一个重要标志,值得去官网查一查。

4. 实操过程与踩坑记录:从源码到成功运行

4.1 环境准备与项目初始化

拿到源码后,第一步是搭Python环境。我的建议是使用virtualenv或者conda创建独立虚拟环境,不要用系统全局的Python环境,因为pip的包很容易互相冲突。

基本流程:

# 创建虚拟环境 python -m venv venv # 激活(Windows) venv\Scripts\activate # 激活(Linux/Mac) source venv/bin/activate # 安装依赖 pip install -r requirements.txt

很多项目都会在README里列一堆依赖,但最规范的做法是导出requirements.txt:

pip freeze > requirements.txt

不过pip freeze会把所有子依赖也列出来,再次安装时可能版本不兼容。更优雅的方式是用pip-tools或者只手动列出顶层依赖。对于课程设计来说,pip freeze足够了,但记得加上说明。

然后是创建项目里的settings.py配置文件。通常项目的settings.py已经写好了,但你需要改几个东西:

  • SECRET_KEY:不能用默认值,换一个你自己的随机字符串。
  • DEBUG:部署时改成False。
  • ALLOWED_HOSTS:调试期间可以设为['*'],上生产环境要改。

4.2 配置数据库与静态文件

开发环境用SQLite最简单,不需要额外安装数据库服务,零配置。如果正式一点,可以切到MySQL:

pip install mysqlclient # 或 pymysql

然后在settings.py中配置数据库连接:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'homework_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', } }

MySQL和SQLite最大的区别在于并发能力和支持的数据类型上。Django的ORM封装了一层,所以代码里很少需要改动。但有几个小坑:

  • MySQL的DateTimeField会自动变成datetime(6),和SQLite的行为略有差异。
  • DecimalField在MySQL里是decimal,查询数值类型时要注意类型转换。
  • 字符集建议用utf8mb4,支持emoji等特殊字符。

配置完数据库后,先跑一次migrate:

python manage.py migrate

你会看到一连串的migration操作,如果报错就停下来排查。常见的错误是Table 'xxx' already exists,这通常是因为之前跑过migrate,或者数据库不是空的。可以用python manage.py migrate --fake-initial来跳过,但如果不是很熟悉,别乱用。

4.3 创建应用与注册路由

在Django项目里,应用的创建很有讲究。通常一个业务模块对应一个app,比如我们核心业务是作业管理,可以建一个app叫core,如果用户和班级管理也放在core里,那系统管理功能可以单独建dashboard或accounts。

创建app的命令:

python manage.py startapp core

不要把所有代码都堆在项目根目录里,更不要每个功能建一个app然后互相乱引用。一个中小项目的合理布局是:

project/ ├── manage.py ├── project/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── core/ # 核心业务:用户、班级、课程、作业、提交 │ ├── accounts/ # 登录注册相关(可以并入core) │ └── tools/ # 一些工具函数

注册app后,还要在总urls.py里把子路由include进来:

from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('apps.core.urls')), path('accounts/', include('apps.accounts.urls')), ]

这个顺序很重要:urls.py的匹配是从上到下的,如果先写了path('', include('core.urls')),而没有指定更精确的路由,某些情况下可能出现匹配冲突。Django的URL解析是正则式的,但新版Django推荐用path()和re_path()区分。

4.4 前端页面与模板渲染

Django模板系统的核心就是变量替换和循环,比如作业列表页面:

{% for assignment in assignments %} <tr> <td>{{ assignment.title }}</td> <td>{{ assignment.course.name }}</td> <td>{{ assignment.deadline|date:"Y-m-d H:i" }}</td> <td>{{ assignment.total_score }}分</td> </tr> {% empty %} <tr><td colspan="4">暂无作业</td></tr> {% endfor %}

模板过滤器比如date、default、yesno非常实用。我建议各位写项目时多掌握几个:

  • {{ obj.date|date:"Y-m-d" }}格式化日期
  • {{ text|linebreaksbr }}把换行转成<br>
  • {{ file.url }}获取上传文件的URL
  • {% url 'name' param %}反向解析路由,避免硬编码URL

在前端样式上,我一般直接用Bootstrap 4/5的CDN,不建议花时间手写复杂CSS。管理系统最重要的是清晰、整齐、不容易点错按钮。

模板继承是必须要用的,否则每个页面都复制HTML头尾,后期改个导航就要翻十几页。基础的做法是:

<!-- base.html --> <html> <head>... Bootstrap CSS ...</head> <body> {% include 'core/navbar.html' %} <div class="container"> {% block content %}{% endblock %} </div> </body> </html> <!-- assignment_list.html --> {% extends 'base.html' %} {% block content %} ... {% endblock %}

用block和include组合,能让模板复用率大幅提升。这也是Django官方推荐的做法。

5. 常见问题与排查技巧实录

5.1 数据库连接失败问题

症状:跑migrate或访问页面时,报类似于django.db.utils.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'")错误。

排查步骤:

  1. 先确认MySQL服务有没有启动。Windows下检查服务,Linux下用systemctl status mysql。
  2. 再确认settings.py里HOST、PORT、USER、PASSWORD是否都正确。很多人喜欢在settings.py里写localhost,但MySQL的localhost和127.0.0.1有时候行为不一样,尤其在带socket时。建议直接用127.0.0.1。
  3. 测试连接:mysql -u root -p -h 127.0.0.1,如果能连但Django连不上,看看项目的用户权限。GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';这种操作要谨慎,但本地调试无妨。

5.2 循环导入错误

Django项目中很常见的坑,比如在models.py里要用到另一个app的模型,在views.py里又要引用models.py中的某个函数,容易形成循环依赖。

症状:运行时会报类似ImportError: cannot import name 'Course' from 'core.models'或circular import。

解决方案:

  • 用django.apps的apps.get_model('app_name', 'ModelName')延迟加载。
  • 或把公共的功能抽到独立的util模块,避免相互依赖。
  • 或修改导入位置,把其中一个导入移到函数内部。

我自己常把工具函数放到core/utils.py,models.py少写函数,尽量避免模型层直接引用视图层的东西。

5.3 静态文件404

症状:页面能访问,但CSS、图片、JS全部404,样式整个乱了。

原因:未配置STATIC_URL和STATICFILES_DIRS,或模板中用了{% load static %}但实际文件目录不对。

解决方案:

# settings.py STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]
{% load static %} <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">

注意,Django只在DEBUG=True时能直接提供STATIC_ROOT下的静态文件,生产环境还是要Nginx处理。如果改了静态文件不生效,可以先停Django再启动,还是不行就删掉浏览器缓存或重启开发服务器。

5.4 时区与截止时间判断

这个坑非常隐蔽。Django默认的TIME_ZONE是UTC,如果你直接在本机创建作业设置截止时间,提交的时候用datetime.now()判断是否迟到,往往会差了8小时。

我之前就遇到过:老师设置截止时间是中午12点,学生过了晚上8点还能提交,后台显示的提交时间却还是当天,其实已经过了截止时间,因为数据库里存的是UTC,页面显示时也不转换。

解决方案很简单:

# settings.py TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

USE_TZ=True表示使用Django的时区系统,数据库存UTC时间,渲染时自动转换。这本身没问题,但关键是代码中所有取当前时间的地方,都要用timezone.now(),而不是datetime.now()。

判断是否迟交:

from django.utils import timezone if submission.submit_time > assignment.deadline: # 迟交

如果硬编码datetime.now(),比较时会因为时区问题得到错误结果。这个细节在面试里很容易被问到,值得特别记一下。

5.5 上传文件类型与大小限制

默认Django没有限制上传文件大小,这可能导致服务器被大文件撑爆。建议在settings.py中限制:

# 限制请求体大小(单位字节),这里限制为10MB # 需要在中间件层面处理,Django本身没有直接配置项? # 可以自己写一个简单的中间件,或者在前端限制,同时后端也要校验

我的简化做法是,在提交视图里判断:

MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB if file and file.size > MAX_FILE_SIZE: return render(request, 'core/error.html', {'msg': '文件大小不能超过10MB'})

还可以通过设置FILE_UPLOAD_MAX_MEMORY_SIZE和FILE_UPLOAD_HANDLERS来控制文件保存策略,但对于作业管理系统,一个简单的size判断就足够满足需求了。

6. 项目优化与扩展建议

6.1 性能优化

作业管理系统规模不大,但不是说就不用关心性能。几个成本低收益高的优化点:

  • 查询优化:用django-debug-toolbar观察SQL查询次数。例如作业列表页面,如果N+1问题严重,每显示10个作业都会多查10次课程表。解决办法是使用select_related和prefetch_related。
assignments = Assignment.objects.select_related('course', 'created_by').all()
  • 数据库索引:频繁查询的字段可以加db_index=True,比如Submission里assignment和student已经是联合唯一的,索引已建好。Assignment.deadline和Course.semester也可以加索引。
  • 分页:所有列表页面都要分页,虽然数据量小的时候无所谓,但写清楚这个习惯,对以后做更大项目有好处。
  • 文件缓存:Django的模板和静态文件在生产环境中会自己缓存,但MEDIA文件不会。如果部署在Nginx上,可以给/static/和/media/路径加简单缓存。

6.2 功能扩展方向

一个基础的作业管理系统,可以扩展的方向其实很多。如果项目想做得更完整,可以考虑:

  • 作业重提交与版本管理:记录每次提交的文件和时间,教师能看完整历史。
  • 互评系统:学生之间可以互相打分,老师给最终意见。
  • 成绩单导出:用openpyxl或pandas导出Excel成绩单,给老师省大量时间。
  • 通知系统:作业即将截止前,自动通过邮件/站内信通知未提交学生。
  • 数据可视化:用pyecharts或前端图表库展示成绩分布。
  • 消息队列:如果提交量大,可以用Celery异步处理文件转码或通知发送。不过对于课程设计级别,这个属于高配了。

6.3 代码规范与文档维护

代码是给人看的,尤其这套系统如果作为毕设,老师一定会打开代码看。我在写完项目后,会花一小时做这几件事:

  • 将所有视图函数加上docstring,说明参数、返回值的含义。
  • 把关键查询和复杂逻辑写注释,不写废话注释(比如# 遍历列表这种就没意义)。
  • 在项目根目录编写一份简洁的README,内容包括:项目简介、技术栈、目录结构、安装步骤、初始账号、核心功能说明。
  • 提供一个docs/目录放置设计说明、数据库ER图和操作手册截图,这对答辩或验收帮助巨大。

这些动作不需要太多时间,但会让你的项目看起来有一个“专业人士”的完成度。我见过太多功能都能跑但代码一塌糊涂的项目,反而不如一个结构干净、文档完整的小系统得分高。

最后再分享一个我自己的体会:Django项目调试时,一定要习惯看控制台日志和django-debug-toolbar页面的SQL,而不是只盯着页面报错。很多数据库相关的坑,页面提示不一定清晰,但日志里会明显告诉你哪一行SQL有问题。把日志和数据库客户端配合着用,排查效率会高很多。这套学生作业管理系统你完全可以照着代码敲一遍,遇到问题多想一想“这行查询能不能少一次”“这个权限校验是不是缺了一环”,能想清楚这几层,你收获的就不只是代码,而是用Django设计业务系统的手感。

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

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

立即咨询