从标题看就是一个很典型的课程设计/毕设选题,但越典型的项目,越值得把细节做扎实。我自己带过几届毕业生,也帮人改过不少作业管理系统,最深的感受是:大部分项目代码能跑,但一深问就说不出所以然,数据库为什么这样设计、权限怎么控制、为什么用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'")错误。
排查步骤:
- 先确认MySQL服务有没有启动。Windows下检查服务,Linux下用
systemctl status mysql。 - 再确认
settings.py里HOST、PORT、USER、PASSWORD是否都正确。很多人喜欢在settings.py里写localhost,但MySQL的localhost和127.0.0.1有时候行为不一样,尤其在带socket时。建议直接用127.0.0.1。 - 测试连接:
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 = TrueUSE_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设计业务系统的手感。