学生作业管理系统这个题目,在课程设计和毕业设计里出现的频率高得吓人。但说实话,我见过太多人卡在同一个地方:代码从网上down下来跑不起来,或者数据库配不明白,又或者逻辑混乱到根本看不懂。我自己早些年也踩过不少坑,后来用Python + Django把这套系统完整重写了一遍,源码、数据库、文档一次配齐。
这套系统解决的核心问题很简单:老师发作业、学生交作业、老师批改打分、成绩统计归档,整个流程全部在线完成,不再靠微信群里刷屏交文件。管理员管老师和学生的账号、课程班级信息;老师可以按课程发布作业、查看提交情况、在线批改评分;学生登录后能看到自己所有课程的待办作业,提交文件或者在线填写内容。权限边界清楚,流程闭环,数据在数据库里一目了然。这次我会把这套系统的设计思路、数据库结构、核心代码逻辑、部署步骤和排坑记录全部拆开讲,适合正在做课设/毕设的同学,也适合想快速搭一个内部小工具的开发者参考,配置过程会非常详细,直接照着做基本不会翻车。
1. 技术选型和整体设计思路
1.1 为什么偏偏是Python + Django
市面上的技术方案很多,PHP、Java SSM、Spring Boot、Vue + Node.js都能做这类系统,但我最终还是选了Python搭配Django来实现。原因很实在:这套系统本质上是业务管理系统,核心价值在于快速把业务逻辑跑通,而不是炫技。
Django自带的东西太适合这种场景了。Admin后台开箱即用,数据库模型靠着ORM直接映射,不用手写SQL建表,用户认证和会话管理也是内置的,CSRF防护默认开启。对于一个学生作业管理系统来说,我需要把精力集中在“作业发布、提交、批改”这条主干上,而不是从零去搭RBAC权限、写登录注册。Django的MTV架构(Model-Template-View)也足够直观,model管数据,view管逻辑,template管页面,新人看代码很容易找到入口。
拿Django的ORM举例,真不是一般地省事。传统JDBC或者原生SQL操作的时候,每写一个查询都要拼接字符串、管理连接、处理结果集,而Django里就是一行Assignment.objects.filter(course__teacher=request.user),链式查询、跨表关联、条件过滤全搞定,开发效率至少快一倍。对于期限紧张的项目,选Django就是选了一条捷径。
1.2 角色权限和功能模块拆解
先把系统要服务的“人”想清楚,功能才能不跑偏。这套系统一共三种角色:
- 管理员:管理教师和学生账号,维护班级、课程信息,查看全局数据。
- 教师:创建课程、发布作业、查看学生提交记录、在线批改打分、发布课程公告。
- 学生:查看作业列表、在线提交作业(文本或附件)、查看批改结果和分数。
权限控制是这类系统最容易被忽略的地方。学生如果直接输入URL就能访问教师后台,那系统就废了。Django自带的认证系统搞定了“登录”这层,我再配合自定义装饰器控制“角色”这层,语法非常简单,后面代码部分会详细贴出来。
模块划分上,我做了认证登录模块、课程班级管理模块、作业发布模块、提交批改模块、公告模块和Admin后台管理模块。六个模块各管一摊,新增功能时只改自己那一块,不会牵一发动全身。比如后面想加一个“成绩排行榜”,只需要在作业模块旁边加个新视图就行,老代码完全不动。
1.3 源码目录结构和MTV分层
这类项目最怕的就是目录乱成一锅粥。我拿到这套源码的第一感受是,它的目录结构一直在遵守Django的“约定优于配置”原则:
student_assignment/ ├── manage.py # Django管理入口 ├── requirements.txt # 环境依赖 ├── db.sqlite3 # 数据库文件(默认SQLite) ├── static/ # 静态文件(CSS/JS/图片) ├── media/ # 用户上传的作业附件 ├── config/ # 项目配置 │ ├── settings.py # 全局配置文件 │ ├── urls.py # 根路由 │ └── wsgi.py └── apps/ ├── users/ # 用户模块 │ ├── models.py # 用户模型 │ ├── views.py # 登录注册视图 │ └── urls.py ├── courses/ # 课程班级模块 │ ├── models.py │ ├── views.py │ └── urls.py ├── assignments/ # 作业模块(核心) │ ├── models.py │ ├── views.py # 发布作业/提交作业/批改 │ └── urls.py ├── students/ # 学生端视图 │ ├── views.py │ └── urls.py ├── teachers/ # 教师端视图 │ ├── views.py │ └── urls.py └── announcements/ # 公告模块 ├── models.py ├── views.py └── urls.py四个子应用各自独立,Model层只管数据结构,View层只管业务逻辑和请求处理,Template只负责把数据渲染到页面上。这种分层最大的好处是排查问题的时候思路清晰:页面显示不对就去查模板,接口报错就去查视图,持久化有问题就去查模型,不用像个无头苍蝇一样瞎猜。
2. 数据库设计和核心模型实现
2.1 数据表关系梳理
数据库设计是整个系统的地基。地基打歪了,后面写多少代码都白搭。这套系统的核心表一共六张:
- 用户表(User):复用Django内置用户表,再加上一个
role字段区分管理员、教师、学生。 - 班级表(ClassInfo):班级名称、所属专业、年级。
- 课程表(Course):课程名称、课程代码、授课教师(外键关联用户表)。
- 作业表(Assignment):作业标题、描述内容、所属课程(外键)、发布教师(外键)、截止时间、附件。
- 提交表(Submission):作业(外键)、学生(外键)、提交内容、附件、提交时间、分数、教师评语、状态。
- 公告表(Announcement):标题、内容、发布人(外键)、发布时间。
表之间的关系说白了就是:一个教师可以教多门课,一门课只有一个授课教师;一门课下面可以发布多个作业,一个作业属于一门课;一个作业可以接收多个学生的提交,一个学生在同一课程下只能提交一次该作业。
这里有一个细节要重点提醒:提交表设立唯一约束,即同一个学生针对同一个作业只能有一条记录。学生第一次提交后如果还没到截止时间,可以更新这条记录,但是不能插入新的一行。这是避免数据重复的关键,很多半吊子系统就是在这里设计了放弃,最后统计成绩的时候数据全乱套了。
2.2 核心模型代码要点
下面贴出作业和提交这两个核心模型,字段设计上做了很多实际考量:
from django.db import models from django.contrib.auth.models import User class Course(models.Model): name = models.CharField(max_length=100, verbose_name="课程名称") code = models.CharField(max_length=20, unique=True, verbose_name="课程代码") teacher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="授课教师") students = models.ManyToManyField(User, related_name="student_courses", verbose_name="选课学生") created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "课程" verbose_name_plural = "课程" def __str__(self): return self.name class Assignment(models.Model): title = models.CharField(max_length=200, verbose_name="作业标题") description = models.TextField(blank=True, verbose_name="作业要求") course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="assignments", verbose_name="所属课程") teacher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="发布教师") due_date = models.DateTimeField(verbose_name="截止时间") attachment = models.FileField(upload_to="assignments/", blank=True, null=True, verbose_name="作业附件") created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "作业" verbose_name_plural = "作业" def __str__(self): return self.title class Submission(models.Model): STATUS_CHOICES = ( ("submitted", "已提交"), ("graded", "已批改"), ("returned", "已退回"), ) assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE, related_name="submissions", verbose_name="作业") student = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="学生") content = models.TextField(blank=True, verbose_name="作业内容") attachment = models.FileField(upload_to="submissions/", blank=True, null=True, verbose_name="提交附件") submitted_at = models.DateTimeField(auto_now_add=True, verbose_name="提交时间") score = models.DecimalField(max_digits=5, decimal_places=1, blank=True, null=True, verbose_name="分数") feedback = models.TextField(blank=True, verbose_name="教师评语") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="submitted", verbose_name="状态") graded_at = models.DateTimeField(blank=True, null=True, verbose_name="批改时间") graded_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name="graded_submissions", verbose_name="批改教师") class Meta: unique_together = ("assignment", "student") verbose_name = "作业提交" verbose_name_plural = "作业提交" def __str__(self): return f"{self.student.username} - {self.assignment.title}"字段设计有几个讲究的地方,我特意解释一下:
score用的是DecimalField(max_digits=5, decimal_places=1),而不是IntegerField。因为成绩经常会出现85.5这样的0.5分,用整型就存不了小数,用浮点型又容易产生精度问题,DecimalField才是正经做法。blank=True, null=True的原因更简单,作业发布时没有成绩,成绩是教师批改的时候才算出来的,所以允许为空。
on_delete参数的选择也很关键。课程删除时,下面的作业、提交记录会变成孤儿数据,所以课程关联用了CASCADE,一键级联删除省得留垃圾。但graded_by指向批改教师,如果教师账号被删除,还想保留历史批改记录,这里用了SET_NULL,从“删干净”变成“保留数据但置空”,这是两类不同业务场景的取舍。
2.3 数据库交付和迁移管理
这套源码的数据库交付方案是SQLite加MySQL两套都兼容。默认配置用的是SQLite,就是项目根目录下的那个db.sqlite3文件,零配置直接用,特别适合课程设计答辩展示。如果你想切到MySQL,只需改settings.py里的数据库配置,后面第4章会详细说。
Django自带了一套“数据库同步”机制,也就是makemigrations和migrate,这一点跟传统课设里手动导入.sql脚本完全不一样。模型改完之后跑一句makemigrations,Django会自动生成迁移脚本;再跑migrate,它就会按顺序把表结构同步到数据库里。不用自己写建表语句,也基本不会出现“开发能跑、部署炸了”的情况,这套迁移机制比手写SQL靠谱太多。
3. 核心功能模块的实操实现
3.1 登录认证和角色权限控制
用户登录这块,直接用Django内置的authenticate和login方法就行。真正需要自己写的是权限校验,我写了一个通用装饰器,判断当前登录用户的角色:
from functools import wraps from django.shortcuts import redirect def role_required(role): def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: return redirect("login") if request.user.profile.role != role: # 角色不匹配时重定向到首页,不直接返回403 return redirect("index") return view_func(request, *args, **kwargs) return _wrapped_view return decorator用法直接往视图函数上一挂就行:
@role_required("teacher") def assignment_create(request, course_id): # 只有教师角色能发布作业 ...这里有个设计细节值得说:角色不匹配时不返回403错误页,而是重定向到首页。理由是403页面对普通用户来说太不友好了,很多学生会以为“系统坏了”,而不是“我没权限”。重定向回首页,再用Django的messages框架弹一个“当前账号无权限访问”的提示,体验好得多。
3.2 作业发布、提交、批改的完整主流程
作业模块是整个系统的核心,我按照“教师的作业管理”和“学生的作业提交”两个视角分开写。
教师发布作业的流程是:教师登录 -> 进入课程详情 -> 点击“发布新作业” -> 填写作业标题、要求、截止时间,可选上传附件 -> 保存。保存逻辑里有个重要的校验点:截止时间必须晚于当前时间,否则直接提示“截止时间不能早于当前时间”。这个校验既在前端页面用JavaScript做了第一层拦截,又在后端视图里做了二次校验,防止有人绕过前端直接POST请求。
学生提交作业的流程是:学生登录后首页展示作业列表,每个作业卡片会显示状态,未开始、已提交、已批改一目了然。点击进入作业详情,如果是“已提交”状态就显示已交内容且给出修改按钮;如果已过截止时间,则限制只能查看不能修改;如果还没提交,显示提交表单。学生可以填写文本内容,也可以上传附件。
教师批改是另一个入口,教师看到某作业的提交列表后,点进某个学生的提交记录,填分数和评语,状态从“已提交”变成“已批改”。这里我额外加了一个“退回”操作。比如学生交上来的附件打不开,或者明显是乱写的,教师点“退回修改”,状态变成“已退回”。退回后学生那边会看到一个“作业被打回”的通知,重新提交后才能再次进入批改队列。
文件下载这里有一个特别容易踩的坑,就是直接拿FileResponse返回文件时,浏览器会乱码或者显示为一串乱码文件名。正确做法是用StreamingHttpResponse并显式设置Content-Type和Content-Disposition:
from django.http import StreamingHttpResponse def download_assignment_file(request, submission_id): submission = Submission.objects.get(id=submission_id) file = submission.attachment response = StreamingHttpResponse( file.chunks(), content_type="application/octet-stream" ) # 处理中文文件名编码,避免浏览器下载时乱码 filename = file.name.split("/")[-1] response["Content-Disposition"] = f"attachment; filename*=UTF-8''{quote(filename)}" return responsefilename*=UTF-8''这个写法是我特意标出来的,直接用filename=文件名遇到中文必定乱码,这个坑我当年踩得很惨,所以特别提醒一下。
3.3 Django Admin后台定制与美化
Django自带的Admin后台是这类项目的杀手锏。默认后台只能看个表格,但我们完全可以改造成一个顺手的运营管理界面。
在admin.py里给每个模型注册一个自定义的Admin类:
from django.contrib import admin from .models import Assignment, Submission @admin.register(Assignment) class AssignmentAdmin(admin.ModelAdmin): list_display = ["title", "course", "teacher", "due_date", "created_at"] list_filter = ["course", "teacher", "due_date"] search_fields = ["title", "course__name"] date_hierarchy = "due_date" ordering = ["-due_date"]list_display控制表格显示哪些列,list_filter让后台可以直接按课程、教师筛选作业,search_fields支持关键字搜索作业标题、课程名,date_hierarchy把截止时间变成日期层级筛选器。整套配置下来,后台的可操作性甚至比一些自己写的前端页面还好用。
再进一步可以整个换掉默认配色和布局,套一个Django SimpleUI的第三方主题,后台立刻变成现代管理系统的样子,在答辩演示时观感提升相当明显。这个库的安装就是pip install django-simpleui,然后在INSTALLED_APPS里把simpleui放到django.contrib.admin前面,刷新页面就有奇效。
4. 从零搭建运行环境的完整步骤
4.1 Python环境安装和虚拟环境创建
源码拿到手,第一件事是把运行环境跑通。这部分我会写得极其详细,照着复制命令就行。
现在Windows和Linux上安装Python都很方便了,官方下载或者包管理器装完记得勾选“Add Python to PATH”。装完之后验证一下:
python --version虚拟环境这一步千万别省。不同项目的依赖版本经常互相打架,直接装到全局会让你后面想哭。创建一个项目专属的虚拟环境:
# 创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # Linux/macOS激活 source venv/bin/activate激活之后命令行前面会出现(venv),说明虚拟环境已经生效。然后安装依赖:
pip install -r requirements.txtrequirements.txt的核心内容就是Django和Pillow(处理上传图片用的),通常还会带上mysqlclient或pymysql等数据库驱动,看你用哪种数据库。装完了用pip list确认一下版本,OK就可以继续了。
4.2 数据库连接配置和mysqlclient安装避坑
默认SQLite的话什么都不用改,直接跳到migrate就行。但如果你想用MySQL,需要在settings.py里改数据库配置:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "student_assignment", "USER": "root", "PASSWORD": "你的密码", "HOST": "127.0.0.1", "PORT": "3306", } }MySQL驱动这里坑特别多,pip install mysqlclient经常因为缺少编译环境直接报错。替代方案是装pymysql,然后在项目的__init__.py里加两行:
import pymysql pymysql.install_as_MySQLdb()就能让Django像操作MySQLdb一样操作MySQL了。实测这个方案在Windows上成功率最高,不需要任何编译器环境。
4.3 数据库初始化和启动项目
环境配好后,第一次启动要按顺序执行这几条命令:
# 1. 根据模型生成迁移脚本 python manage.py makemigrations # 2. 把迁移脚本同步到数据库 python manage.py migrate # 3. 创建超级管理员账号(用于登录Admin后台) python manage.py createsuperuser # 4. 启动开发服务器 python manage.py runserver浏览器打开http://127.0.0.1:8000,先看到的是系统的登录页。登录之后别急着走,建议先去后台http://127.0.0.1:8000/admin用管理员账号登录,然后在后台创建教师账号、学生账号、课程信息和班级信息。这些基础数据一旦就绪,再去前台测试发布作业和提交作业就会顺畅很多,比我直接在页面里点点点手工造数据效率高多了。
5. 常见问题排查和避坑记录
5.1 高频报错速查表
我把这套系统在开发和部署阶段最常见的报错整理成了一张表,方便快速排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError: No module named 'django' | 依赖没装好 | 确认虚拟环境已激活,执行pip install -r requirements.txt |
django.db.utils.OperationalError: no such table | 数据库没迁移 | 执行python manage.py makemigrations和migrate |
| 上传图片到后台一直提示错误 | Pillow库缺失 | 执行pip install pillow |
| 页面CSS丢失,全部是裸HTML | 静态文件加载失败 | 检查settings.py里的STATIC_URL和STATICFILES_DIRS配置 |
| 前端提交作业后,文件上传卡住 | media路由没配 | 在项目urls.py里加上+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |
| 用户登录后跳转不对 | LOGIN_URL或redirect配置问题 | 检查settings.py中登录/登出后的跳转地址 |
5.2 几个典型的“坑”一定要单独说
坑一:删除对象时用错方法是真会删错数据。Django提供get()和filter()两种查询方式,对应的删除行为也不太一样。Assignment.objects.get(id=1).delete()只能删一条;Assignment.objects.filter(course__id=1).delete()会把这个课程下所有作业一次性删掉。后者的杀伤力极大,尤其在有外键关联的情况下,CASCADE会把作业对应的所有提交记录一起删掉,想恢复只能靠数据库备份。所以我给自己定了一条规矩:写删除逻辑的时候,必须先用count()预览一下即将删除的数据量,心里有数再执行。
坑二:重定向后怎么把提示信息带过去。很多时候操作完需要跳转到另一个页面,同时还要告知操作结果。初学者常干的事是把提示信息拼在URL参数里,/list/?msg=success,这种做法既不安全又写起来麻烦。Django里其实有一套现成的解决方案:messages框架。视图里一行messages.success(request, "作业发布成功"),模板里循环读取并渲染,轻松搞定。这个框架底层走的是session,所以选用它来传递一次性提示,刷新页面后消息也会自动消失,不会一直残留。
坑三:中文文件名附件在下载时乱码。这个在前面已经提到过,解决办法是设置Content-Disposition时用filename*=UTF-8''传编码后的文件名。别看只是多写了一行,实测下来这道处理比直接传filename=要稳定得多,尤其在国内环境中文文件名几乎必中招。
5.3 项目维护经验和后续扩展方向
这套系统在实际运行中如果能一直用下去,有几个方向很值得扩展。
一个是作业查重。把学生提交的文本内容做分词和相似度比对,能立刻识别出谁在互相复制粘贴。就算只做一个最简单的Jaccard相似度计算,答辩的时候讲出来也比单纯交作业这个基础功能加分很多。
一个是成绩统计分析。数据库里的成绩数据天然适合做图表展示,用ECharts在前端画一个班级成绩分布柱状图、平均分走势折线图,学生和教师都能直观看到学情趋势。
另一个是对接接口给移动端。Django原生MTV渲染的页面在手机上体验一般,如果后续有小程序或App的需求,可以用Django REST Framework把这套逻辑封装成API,应用层不变,视图层改成APIView,一份逻辑两端复用,扩展成本极低。
最后再分享一个真实的维护经验:这套系统的附件存储走的是本地媒体文件夹,小规模使用完全够,但如果你真的部署到生产环境,建议把文件存储切到OSS或云存储服务上,否则时间一长,服务器磁盘会被学生的作业附件堆满。文件存储路径要做好“按日期分目录”的策略,不然单个目录下文件数量过万后,一些文件系统访问速度会明显变慢。这个教训是我自己跑了大半年后总结出来的,希望你能少走一段弯路。