
学科竞赛管理系统我这两年带过的毕设项目里出现频率相当高。很多同学一看到“管理系统”四个字就觉得是简单的CRUD但实际上学科竞赛这个业务领域要做扎实、做成能拿得出手的毕设还真有不少门道。这周正好又有一个学弟拿着这个题目来找我聊了一下午今天就干脆把这个项目的完整设计思路和实现细节整理出来。这个项目适合计算机相关专业做毕业设计也适合想系统学习Django项目实战的开发者。无论你是零基础想快速搞定毕设还是有一定基础想把这个题目做得有深度这篇文章都能给你一条清晰完整的路径。1. 项目整体设计与业务需求拆解1.1 学科竞赛管理的核心痛点在哪里很多人一说到学科竞赛管理系统第一反应就是“不就是发布竞赛信息、学生报名、老师评分吗”。真做过需求分析你会发现这里面涉及的角色和流程比想象中复杂得多。一个完整的学科竞赛管理流程通常涉及四类角色管理员通常是学院教务或团委老师、竞赛发布者各专业教研室或竞赛组织方、参赛学生、评审专家。整个流程大致是竞赛通知发布、学生在线报名、报名资格审核、竞赛作品或论文提交、专家评审打分、成绩公示与奖项评定、证书管理、最终的数据统计上报。这里面最核心的痛点是信息不对称和流程碎片化。传统模式下竞赛通知通过QQ群或邮件层层转发报名用Excel表格收集作品提交靠U盘拷贝评审打分又换一套表格最后统计还要手动汇总。这个过程不仅效率低而且很容易出错——漏掉某个学生的报名表、成绩汇总算错一位小数、证书信息张冠李戴这些都是真实发生过的事。1.2 为什么用Django而不是其他框架选型是启动项目的第一步也是很多同学纠结的地方。这个题目用Django做我认为有三个核心考虑。第一开发效率高。Django自带Admin后台、ORM、认证系统、表单处理、模板引擎这些内置功能对管理系统类项目来说简直是量身定做的。比如用户认证Django内置的User模型加全程cookie和session处理你不需要从头写登录注册。再比如Admin后台在开发调试阶段可以直接用它将就着管理数据极大缩短了开发周期。第二你的毕设答辩有东西讲。Django的MTV架构、ORM映射机制、中间件原理、模板继承机制这些都是答辩时老师喜欢深挖的点。用Flask写个简单系统固然轻巧但描述工作量和深度时明显不够看。用Spring Boot固然能体现Java功底但对Python基础的同学来说学习成本陡增。第三部署和演示方便。Django项目在本地跑起来简单部署到云服务器也就几步操作配合Gunicorn加Nginx就能稳定运行。毕设答辩时不管是现场演示还是录屏展示流程都顺畅。1.3 系统模块划分与功能架构基于上面提到的痛点我把系统划分为六个核心功能模块用户管理模块学生、教师评审专家、管理员三类角色的注册、登录、个人信息维护、密码修改。学生信息需要关联学号、专业、年级教师信息关联工号、所属教研室、研究方向。竞赛信息管理模块发布竞赛通知、维护竞赛分类A/B/C类对应不同级别、设置报名时间窗口、上传竞赛附件、修改和撤销已发布通知。报名管理模块学生在线报名选择竞赛项目填写报名信息团队参赛需要添加团队成员上传报名材料。审核状态实时可见待审核、已通过、已驳回附驳回原因。作品提交与管理模块初赛和决赛对应的作品上传、修改、删除文件类型和大小校验提交截止时间控制。评审打分模块评审专家在分配的评分区间内对参赛作品打分支持分项评分和总分汇总自动去掉最高分最低分多评委场景成绩反查与异议处理。公示与统计模块竞赛结果公示获奖名单生成各类别竞赛数据的统计图表展示按专业、年级维度的参赛情况汇总。这些模块放在一起才勉强算一个“系统”。如果只做前三个模块说实话工作量偏少答辩容易被问倒。2. 数据库设计与核心模型实现2.1 数据库选型与模型关系梳理数据库层面Django默认配置的是SQLite开发阶段完全够用。但如果想体现项目规范性建议直接把数据库切到MySQL。SQLite和MySQL在Django里的切换非常简单改一下settings.py里的DATABASES配置即可。毕设答辩时强调生产环境用MySQL、开发环境用SQLite的兼容方案是个加分项。模型关系上我是这样设计的User用Django内置的AbstractUser做扩展增加user_type字段区分角色。Competition竞赛关联Category竞赛分类一个分类下可以有多个竞赛。Enrollment报名记录关联Competition和User一个学生可以报名多个竞赛一个竞赛允许大量学生报名多对多关系通过报名记录表体现额外带上团队名称、成员数量、审核状态等字段。Work作品关联Enrollment一对一每个通过审核的报名记录对应一个作品提交入口。Review评审记录关联Work和User评审专家一个作品允许多个专家打分一个专家分配多个作品多对多关系。2.2 核心模型代码实现这部分直接给出核心模型的代码我用的是Django 4.x的写法Python 3.10环境。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 管理员), ) user_type models.CharField(max_length20, choicesUSER_TYPE_CHOICES, defaultstudent, verbose_name用户类型) student_id models.CharField(max_length20, blankTrue, nullTrue, verbose_name学号) teacher_id models.CharField(max_length20, blankTrue, nullTrue, verbose_name工号) major models.CharField(max_length100, blankTrue, nullTrue, verbose_name专业) grade models.CharField(max_length20, blankTrue, nullTrue, verbose_name年级) phone models.CharField(max_length11, blankTrue, nullTrue, verbose_name手机号) class Meta: verbose_name 用户信息 verbose_name_plural verbose_name def __str__(self): return self.usernameclass Competition(models.Model): LEVEL_CHOICES ( (A, A类国家级), (B, B类省部级), (C, C类校级), ) file_type_choices models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name竞赛分类) title models.CharField(max_length200, verbose_name竞赛名称) level models.CharField(max_length10, choicesLEVEL_CHOICES, defaultC, verbose_name竞赛级别) organizer models.CharField(max_length200, blankTrue, verbose_name主办单位) description models.TextField(blankTrue, verbose_name竞赛简介) registration_start models.DateTimeField(verbose_name报名开始时间) registration_end models.DateTimeField(verbose_name报名结束时间) file models.FileField(upload_tocompetition_files/, blankTrue, nullTrue, verbose_name附件材料) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 竞赛信息 ordering [-created_at] def __str__(self): return self.titleclass Enrollment(models.Model): STATUS_CHOICES ( (pending, 待审核), (approved, 已通过), (rejected, 已驳回), ) competition models.ForeignKey(Competition, on_deletemodels.CASCADE, verbose_name竞赛项目) student models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name报名学生, related_nameenrollments) team_name models.CharField(max_length100, blankTrue, verbose_name团队名称) members models.TextField(blankTrue, verbose_name团队成员) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending, verbose_name审核状态) reject_reason models.CharField(max_length255, blankTrue, verbose_name驳回原因) created_at models.DateTimeField(auto_now_addTrue, verbose_name报名时间) class Meta: verbose_name 报名记录 unique_together (competition, student) def __str__(self): return f{self.student.username} - {self.competition.title}class Work(models.Model): enrollment models.ForeignKey(Enrollment, on_deletemodels.CASCADE, verbose_name报名记录) title models.CharField(max_length200, verbose_name作品名称) summary models.TextField(blankTrue, verbose_name作品简介) file models.FileField(upload_toworks/, verbose_name作品文件) submit_time models.DateTimeField(auto_now_addTrue, verbose_name提交时间) is_final models.BooleanField(defaultFalse, verbose_name是否为决赛作品) class Meta: verbose_name 参赛作品 ordering [-submit_time] def __str__(self): return self.titleclass Review(models.Model): work models.ForeignKey(Work, on_deletemodels.CASCADE, verbose_name参赛作品, related_namereviews) expert models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name评审专家, related_namereviews) score_creativity models.DecimalField(max_digits5, decimal_places2, default0, verbose_name创新性得分) score_completeness models.DecimalField(max_digits5, decimal_places2, default0, verbose_name完整性得分) score_practicality models.DecimalField(max_digits5, decimal_places2, default0, verbose_name实用性得分) comment models.TextField(blankTrue, verbose_name评审意见) created_at models.DateTimeField(auto_now_addTrue, verbose_name评审时间) class Meta: verbose_name 评审记录 unique_together (work, expert) property def total_score(self): return self.score_creativity self.score_completeness self.score_practicality2.3 数据库设计的心得与注意点设计这套模型的时候有几个细节值得琢磨。关于unique_together (competition, student)这是为了禁止同一学生对同一竞赛重复报名。但需要注意如果后续需求扩展了“同一竞赛允许学生先个人报名再补团队报名”这种场景这个唯一约束就会变成限制。我当时做的时候根据业务规则做了取舍毕设场景下这个约束是合理的但在真实业务里要谨慎用。关于外键的on_delete行为Competition关联Category我用了PROTECT这是因为分类被竞赛引用时不允许直接删除避免出现孤儿数据。而报名记录关联竞赛和用户用的是CASCADE因为竞赛删除时它的所有报名记录和作品都应该一并清理否则会产生大量脏数据。文件上传这块Django的FileField底层会自动处理文件存储路径和数据库字段的映射但要注意MEDIA_ROOT和MEDIA_URL的配置。我在settings.py里单独加了配置上传目录按类型区分competition_files/和works/避免文件混在一起。3. 核心功能模块的实现过程3.1 用户注册登录与多角色权限控制Django内置的认证系统非常强大但默认的User模型不够用所以我继承了AbstractUser做了扩展。注册功能我用的UserCreationForm作为基类加上了学校相关的字段。登录直接使用authenticate()和login()方法。权限控制是三块视图级别用login_required配合user.user_type判断URL级别通过分组配置路由模板级别用{% if user.user_type teacher %}控制菜单显隐。这三层在我平时带项目时都会强调它是Django权限体系里最实用的组合。举一个具体的视图例子管理员发布竞赛from django.contrib.auth.decorators import login_required, user_passes_test from django.shortcuts import render, redirect from .forms import CompetitionForm def is_admin(user): return user.is_authenticated and user.user_type admin login_required user_passes_test(is_admin) def competition_create(request): if request.method POST: form CompetitionForm(request.POST, request.FILES) if form.is_valid(): form.save() return redirect(competition_list) else: form CompetitionForm() return render(request, competition/competition_form.html, {form: form})这段代码里user_passes_test(is_admin)的作用非常直观不是管理员就进不来login_required保证未登录用户先跳登录页。3.2 报名流程设计与状态流转报名模块是学生最常用的功能也是整个系统的核心流程之一。我设计的报名页是这样一个流程学生进入竞赛详情页 → 查看报名起止时间 → 点击报名 → 填写个人信息和团队信息 → 提交报名 → 等待管理员审核。这里有一个非常关键的判断报名表放在竞赛详情页里还是独立的报名页。我实际开发时用的是独立报名页因为报名信息字段比详情展示复杂得多混在详情页会让页面过于臃肿。详情页只需要一个“立即报名”的按钮入口。状态流转我用的是审核字段加一个状态更新逻辑没有引入Django的状态机库那对毕设来说过度设计。管理员在后台看到报名列表每条记录上有“通过”和“驳回”按钮驳回时弹出模态框填写原因。这样实现起来代码量少数据库改动少逻辑也清晰。审核后的操作有个容易遗漏的细节审核通过后系统需要自动更新竞赛的当前报名人数。这里有两种做法——每次查询时用.count()实时统计或者用一个IntegerField冗余存储。我选择了实时统计牺牲一点查询性能换来数据一致性对毕设规模的数据量完全够用也少了维护冗余字段的负担。3.3 作品提交与文件上传的重点难点作品文件上传是整个系统里最容易出问题的模块。问题通常集中在三方面文件大小超限、文件类型不合法、文件名冲突。Django的FileField支持validators校验我用FileExtensionValidator限定了扩展名配合一个自定义的文件大小校验器。from django.core.validators import FileExtensionValidator from django.db import models import os def validate_file_size(value): limit_mb 50 if value.size limit_mb * 1024 * 1024: raise ValidationError(f文件大小不能超过{limit_mb}MB) class Work(models.Model): # 前面省略 file models.FileField(upload_toworks/, validators[validate_file_size, FileExtensionValidator(allowed_extensions[zip, rar, pdf, docx, pptx])], verbose_name作品文件)文件名冲突这个坑属于不做不知道、做了吓一跳的类型。Django默认保存上传文件时如果目标位置已存在同名文件它会在文件名末尾自动加随机字符串。但对用户来说他上传final.zip下载下来可能是final_abc123.zip体验很差。我在实际项目里做了重命名处理用“学号竞赛ID时间戳”作为新文件名既保证了唯一性又保留了可追溯性。def generate_work_filename(instance, filename): ext filename.rsplit(., 1)[-1].lower() student instance.enrollment.student comp_id instance.enrollment.competition_id sub_type final if instance.is_final else round1 new_name f{student.student_id}_{comp_id}_{sub_type}_{int(time.time())}.{ext} return os.path.join(works/, new_name)这种上传路径函数在定义FileField时传给upload_to即可文件保存时会自动调用。3.4 评审打分与成绩计算逻辑评审模块是拉开普通项目和高质量项目差距的地方。简单做法是每个专家看到所有作品然后自己打分。但这个系统里我做了两层优化第一评分维度拆分。总分不是专家直接填一个数而是拆成创新性、完整性、实用性三个维度每个维度10分系统自动累加。这既符合真实评审场景的评分表结构答辩时也更有说服力——说明你考虑过评分标准的可量化问题。第二评委隔离。每个专家只能看到分配给自己的作品而不是所有作品。分配逻辑由管理员在后台操作一个作品可以分配给三个专家。这样分布式打分比较公平单人作弊影响不了全局。成绩计算上多评委场景我做了一个汇总视图对每个作品的多个评审记录取平均分但如果评委里有人给了明显不合理的低分就要能剔除。我做了“去掉最高分和最低分再取平均”的算法并保留原始数据答辩的时候可以现场演示这个逻辑如何影响最终排序。4. 前端页面与用户体验优化4.1 模板继承与基础布局Django的模板继承机制是提高前端开发效率的关键。我搭建了一个base.html作为整个前端页面的骨架里面包含导航栏、侧边栏、主内容区和底部页脚。子模板只需要写{% extends base.html %}和{% block content %}占据的页面主体部分。侧边栏根据用户角色动态渲染菜单管理员的菜单包括用户管理、竞赛管理、报名审核、评审分配、成绩汇总、数据统计。教师的菜单是评审任务、历史评审记录。学生只有竞赛列表、我的报名、我的作品、我的成绩。这样每个角色打开系统看到的界面不一样权限感和专业性一下就出来了。前端框架用的是Bootstrap 5 少量自定义CSS。选Bootstrap而不是Element UI或Ant Design是因为它对Django模板的配合度最高直接CDN引入不需要前端构建工具毕设演示时不会因为npm包安装失败而翻车。4.2 列表页的搜索、筛选与分页管理系统离不开列表页一个优秀的列表页必须具备三个能力搜索、筛选、分页。Django的Paginator类处理分页非常方便但需要配合模板里的页码组件一起用。from django.core.paginator import Paginator def competition_list(request): competitions Competition.objects.all() keyword request.GET.get(keyword, ) level request.GET.get(level, ) if keyword: competitions competitions.filter(title__icontainskeyword) if level: competitions competitions.filter(levellevel) paginator Paginator(competitions, 10) page_number request.GET.get(page) competitions paginator.get_page(page_number) return render(request, competition/competition_list.html, {competitions: competitions})这块有个小技巧分页后做筛选时翻页会导致筛选条件丢失。比如我在第1页搜索“电子设计”点第2页后搜索条件没了。解决方式是在模板的分页链接后面带上查询参数?page{{ 下一页 }}keyword{{ request.GET.keyword }}。这个细节虽然小但在答辩演示时评委往往能注意到这种交互细节。4.3 消息提示与表单验证反馈Django的messages框架非常实用我把它用在所有表单提交后的反馈里报名成功、审核通过、作品上传失败、密码修改成功等。消息渲染放在base.html的全局位置用Bootstrap的alert组件展示。表单验证方面Django自带forms的后端验证但前端体验欠佳。我给关键表单加了基础的HTML5required属性和maxlength限制并用Django表单的error_messages参数自定义错误提示文案保证用户看到的是人话而不是英文报错。5. 遇到的坑与排查经验总结5.1 Django ORM查询优化N1问题列表页展示竞赛列表时如果同时想显示每个竞赛的报名人数最简单的做法是循环里调用count()for comp in competitions: comp.enrollment_count comp.enrollment_set.count()一次性查50条数据会触发50条额外的SQL查询。这就是经典的N1问题。正确的做法是用annotate()一条SQL搞定from django.db.models import Count competitions Competition.objects.annotate(enrollment_countCount(enrollment))这样每个竞赛对象上都自动多了enrollment_count属性模板里直接{{ comp.enrollment_count }}输出不产生额外查询。这个优化点虽然简单但答辩时主动提出来说明你对ORM底层原理有认识属于展示基本功的加分操作。5.2 时区问题导致的时间显示错乱Django默认USE_TZ True配合TIME_ZONE Asia/Shanghai在日志记录和时间显示上常常差8个小时。因为数据库里存的是UTC时间模板渲染时会转换成本地时间但有些场景下比如接口返回JSON会拿到UTC原始时间。我的解决方案是明确区分存储和展示。数据库统一存UTC接口返回前统一转成Asia/Shanghai字符串模板渲染依赖Django的时区转换机制。另外报名截止时间判断时用timezone.now()而不是datetime.now()这两个在USE_TZ True模式下是不同的用错了会导致截止时间提前8小时或延后8小时。5.3 部署阶段的常见错误Django项目部署到云服务器时最常见的问题是static文件找不到。开发模式下Django自动处理静态文件但部署模式下需要执行python manage.py collectstatic把所有app的静态文件收集到一个指定目录然后让Nginx去托管这个目录。我用的部署方案是Gunicorn Nginx MySQL生产用。Gunicorn负责跑Django应用Nginx负责反向代理和托管静态文件。整个过程有两个容易踩的坑一是务必关闭DEBUG False否则访问任意不存在的URL都会暴露详细的错误堆栈信息这是安全隐患。我的做法是设置系统环境变量控制DEBUG开发环境默认为True部署时通过环境变量置为False避免代码在仓库和服务器上来回改。二是文件上传权限问题。MEDIA_ROOT目录如果权限不对用户上传文件会报Permission denied。部署时记得给www-data用户Nginx默认用户对媒体目录的读写权限。5.4 数据库备份与恢复实操数据库备份在毕设里不一定要求但我会给每个项目都加上。SQLite版本直接拷贝db文件即可MySQL版本用mysqldump命令mysqldump -u root -p competition_db backup_$(date %Y%m%d).sql恢复的话mysql -u root -p competition_db backup_20250601.sql我通常会写一个shell脚本配合crontab每天凌晨自动备份备份文件保留最近7天。这篇内容放到系统中也是一个加分项因为真实的管理系统必须考虑数据安全和灾难恢复。6. 从毕设到作品集如何让它为你加分6.1 功能之外还要展示哪些能力很多同学做完系统就结束了功能全部实现、测试通过、论文交上去。但如果你想让这个项目在求职或升学的作品集里发挥更大价值我建议再做三件事。第一写一份高质量的项目README。不是简单地罗列“这是什么系统”而是从背景、痛点、方案、技术架构、核心难点、效果呈现这几个角度来写。招聘方和技术面试官看你的代码仓库时README就是你的第一印象。第二录一个5分钟功能演示视频。操作路径按真实用户故事走管理员登录→发布竞赛→创建评委账号→学生注册→报名→上传作品→管理员分配评审→评委打分→成绩公示。这个视频可以直接放在简历附件的链接里。第三把项目的架构图画出来。可以在文档中用图片形式放一张完整的功能架构图、一张数据表关系图。这不需要多么精美的设计工具PowerPoint或draw.io随手画出来即可。但有了它面试时讲项目逻辑会清晰很多也显得思路成熟。6.2 如何按毕设要求扩展这个系统如果指导老师要求功能更多一些或者你想把项目做得更完整可以从以下几个方向扩展数据可视化大屏使用ECharts展示全校各专业的参赛率、获奖率、竞赛成绩变化趋势结合Django的JsonResponse提供数据接口。采用Django REST Framework重构API前后端分离使用Vue或React做前端增加项目技术广度。消息通知机制报名审核结果短信通知或邮件通知需要对接第三方短信平台或使用Celery异步任务。证书在线生成与下载基于模板生成带有学生姓名和奖项的电子证书可用openpyxl或weasyprint实现PDF导出。多学院多组织架构为每个学院单独配置管理员数据按学院隔离。6.3 答辩时的常见提问与应答思路毕设答辩时评委最喜欢问的问题集中在几个方向“为什么选这个题目”——结合学科竞赛的实际管理痛点回答说明你观察到了真实需求而不是凭空造轮子。“数据库为什么这么设计”——从数据一致性、查询性能、扩展性三个角度解释说明每个表的存在都有业务依据。“你们系统的安全性怎么考虑”——至少能说出密码哈希存储Django默认PBKDF2、CSRF防护、XSS过滤模板自动转义、登录状态用session管理、文件上传类型校验、权限控制用装饰器拦截。能回答这几点答辩就稳了。“系统上线后如何保证稳定运行”——提到日志记录Django logging、数据库定时备份、异常捕获与告警机制哪怕只是邮件通知管理员就能体现工程化意识。最后再分享一点实际带项目的体会这个题目我前前后后带了不下十次每次都会遇到不同的问题但有一条经验始终不变先把核心业务规则想清楚再动手写代码。很多同学一上来就建项目、建app、写模型结果写到报名审核那一步发现业务逻辑变了回改模型代价极高。我的习惯是先用半天时间把所有角色的操作路径画一遍甚至用纸笔写用户故事把“谁在什么时间做什么事、产生什么数据、影响什么状态”全部理清然后才开始编码。另外针对这个题目我最推荐的开发路径是先做用户认证和角色权限再做竞赛信息模块然后报名流程接着作品上传最后评审和统计。每个模块都是上一步的延伸每一步做完都能独立演示不会出现做了两周还不知道系统长什么样的焦虑感。学科竞赛管理系统确实不是多新颖的题目但它麻雀虽小五脏俱全几乎覆盖了Web应用开发的所有基础知识点。一个学生如果真能把这个系统从头到尾独立做出来并且能说清楚每个设计决策的理由那他的工程能力已经超过大部分同龄人了。这个题目本身可能不够酷炫但把它做透就是很好的作品。