
简介本资源为学科竞赛管理系统毕业设计完整项目包面向计算机相关专业需要完成毕设的学生及指导教师。项目基于Python语言采用Django后端框架、Vue前端框架与MySQL数据库开发针对高校竞赛活动逐年增多、传统管理方式效率低下的问题实现了赛项信息、报名与奖项统计的数字化管理。系统分为管理员与学生两大模块管理员可管理个人中心、学生、教师、赛项信息、赛项报名、奖项统计及系统设置学生注册登录后可浏览首页、赛项信息与新闻资讯并进行个人中心维护和赛项报名。压缩包共744个文件约42.82MB包含47个py源码、38个vue组件、31个html页面、46个css样式、155个js脚本及2个sql数据库文件另附毕业论文与视频演示。已有411人学习下载适合作为毕设参考、课程设计或DjangoVue全栈学习案例。1. 从一份能跑起来的学科竞赛管理系统说起每年到了毕业设计选题季计算机相关专业里总有一批人会盯上「学科竞赛管理系统」这个题目。原因很直接业务场景清晰角色分明功能模块够多但不至于失控答辩时也好讲。但真正动手之后翻车的人不在少数——环境装不上、Django 和 Vue 联调跨域、MySQL 8.0 认证插件报错、打包部署白屏这些问题一个接一个。这篇笔记面向的是准备用 Python Django Vue MySQL 做一套学科竞赛管理系统的人不管你是刚入门想照着走一遍还是已经写了一部分卡在某个环节。我会把技术选型的理由、环境搭建的具体命令、核心模块的表结构和接口设计、前后端联调的配置、以及部署时最容易踩的坑按实际开发顺序讲清楚。整套方案的目标是本地能跑通功能能演示代码结构经得起答辩追问。2. 技术选型为什么是 Django Vue MySQL 这套组合2.1 Django 做后端的三个实际理由选 Django 不是因为它是「最流行」的 Python Web 框架而是因为学科竞赛管理系统这个场景和 Django 的强项高度匹配。第一自带 Admin 后台。竞赛管理涉及大量的数据录入和审核工作——管理员要发布竞赛、审核报名、录入成绩。Django 的 Admin 几乎零代码就能生成一套可用的管理界面省掉大量重复的前端开发。你只需要在admin.py里注册模型设置list_display和search_fields一个功能完整的管理后台就出来了。第二ORM 成熟。竞赛系统里「学生-竞赛-报名-成绩」之间的关系是典型的多对多和一对多Django ORM 处理这些关系很自然。比如查询某个学生参加过的所有竞赛及其成绩用CompetitionRegistration.objects.filter(studentstudent).select_related(competition)一行就搞定不需要手写 JOIN。第三认证和权限体系开箱即用。系统里至少有三类角色管理员、教师、学生。Django 内置的User模型加上Group和Permission配合自定义的UserProfile可以很干净地实现角色区分。# models.py 中扩展用户角色 from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (admin, 管理员), (teacher, 教师), (student, 学生), ) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) student_id models.CharField(max_length20, blankTrue, nullTrue, verbose_name学号) department models.CharField(max_length50, blankTrue, nullTrue, verbose_name院系) class Meta: db_table user verbose_name 用户这里继承AbstractUser而不是新建一张关联表好处是认证逻辑完全复用 Django 内置的authenticate()和login()直接能用。role字段用来做权限判断student_id和department是学生特有的信息教师和管理员留空即可。2.2 Vue 做前端的取舍前端选 Vue 而不是 React 或纯模板渲染主要考虑两点一是 Vue 的学习曲线对毕业设计这个场景更友好模板语法接近 HTML上手快二是前后端分离的架构在答辩时更容易讲清楚「你做了什么」。但这里有个常见的误判很多人觉得前后端分离就是前端发请求、后端返回 JSON然后各写各的。实际上接口约定才是最容易出问题的地方。我的做法是先定接口文档再写代码。比如竞赛列表接口字段类型说明idint竞赛 IDnamestring竞赛名称categorystring类别学科/文体/创新创业levelstring级别校级/省级/国家级registration_deadlinedatetime报名截止时间statusstring状态报名中/进行中/已结束participant_countint已报名人数前端按这个结构写v-for后端按这个结构序列化联调时基本不会因为字段名对不上而返工。Vue 这边用 Vue CLI 创建项目路由用 Vue Router状态管理看项目复杂度决定要不要上 Vuex。学科竞赛管理系统的状态不算复杂用户登录信息用 localStorage 存 token全局状态用 Vuex 管一下用户角色就够了。2.3 MySQL 版本选择和字符集设置MySQL 用 8.0 还是 5.7我的建议是 8.0。原因不是性能而是 8.0 的默认字符集是utf8mb4直接支持 emoji 和生僻字不需要像 5.7 那样手动改配置。竞赛名称里出现特殊字符的概率不低用 5.7 的话容易在插入时报Incorrect string value错误。安装 MySQL 8.0 时有一个坑默认的认证插件是caching_sha2_password而 Django 的mysqlclient在旧版本里不支持这个插件。解决办法有两个要么升级mysqlclient到最新版要么在 MySQL 里把用户认证方式改成mysql_native_password。-- 创建数据库时指定字符集 CREATE DATABASE competition_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 如果遇到认证插件问题修改用户认证方式 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;Django 的settings.py里数据库配置对应写DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: competition_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }注意HOST写127.0.0.1而不是localhost避免在某些系统上走 socket 连接导致权限问题。3. 环境搭建从零到跑通的最小步骤3.1 Python 和 Django 的安装与项目初始化Python 版本选 3.10 或 3.11 都行不建议用 3.12部分依赖库的兼容性还没跟上。安装完 Python 后先建虚拟环境这是好习惯避免全局包污染。# 创建项目目录 mkdir competition-system cd competition-system # 创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装 Django 和数据库驱动 pip install django mysqlclient djangorestframework django-cors-headers # 创建 Django 项目 django-admin startproject backend . # 创建核心应用 python manage.py startapp competition这里装了四个包django是框架本体mysqlclient是 MySQL 驱动djangorestframework用来写 REST 接口django-cors-headers解决前后端分离的跨域问题。startproject后面的.表示在当前目录创建项目这样目录结构更扁平。创建完项目后在settings.py里注册应用INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, competition, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 放在最前面 django.middleware.security.SecurityMiddleware, # ... 其他中间件 ] # 跨域配置 CORS_ALLOWED_ORIGINS [ http://localhost:8080, # Vue 开发服务器地址 ]CorsMiddleware必须放在CommonMiddleware之前否则跨域请求会被拦截。CORS_ALLOWED_ORIGINS里写 Vue 开发服务器的地址默认是localhost:8080。3.2 Vue 项目的创建和依赖安装Vue 这边用 Vue CLI 创建项目虽然 Vite 更快但 Vue CLI 的配置更稳定适合毕业设计这种不需要极致构建速度的场景。# 安装 Vue CLI如果还没装 npm install -g vue/cli # 创建 Vue 项目 vue create frontend # 进入项目目录 cd frontend # 安装 axios 和 vue-router npm install axios vue-router4创建项目时选择手动配置勾选 Router 和 Vuex。创建完成后在vue.config.js里配置代理解决开发环境的跨域module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, // Django 开发服务器 changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这样前端发/api/competitions/请求时会被代理到 Django 的8000端口浏览器不会报跨域错误。生产环境不需要这个代理用 Nginx 做反向代理即可。3.3 数据库迁移和初始数据Django 的迁移系统是它最好用的功能之一。定义完模型后两条命令就能建表# 生成迁移文件 python manage.py makemigrations # 执行迁移 python manage.py migrate # 创建超级管理员 python manage.py createsuperuser但这里有个细节migrate之前要确保数据库已经创建好了。Django 不会自动建库只会建表。所以先在 MySQL 里执行CREATE DATABASE competition_db ...再跑迁移。初始数据方面竞赛类别、院系这些固定选项可以用 Django 的fixtures或者数据迁移来预置。我一般写一个管理命令# competition/management/commands/init_data.py from django.core.management.base import BaseCommand from competition.models import Category, Department class Command(BaseCommand): help 初始化基础数据 def handle(self, *args, **options): categories [学科竞赛, 文体竞赛, 创新创业] for name in categories: Category.objects.get_or_create(namename) departments [计算机学院, 数学学院, 外国语学院] for name in departments: Department.objects.get_or_create(namename) self.stdout.write(self.style.SUCCESS(基础数据初始化完成))执行python manage.py init_data就能一键导入。这种方式比手动在 Admin 里一条条加要可靠得多也方便在换环境时快速重建。4. 核心模块的表结构设计与接口实现4.1 竞赛、报名、成绩三张核心表的关系学科竞赛管理系统的数据模型可以抽象成三个核心实体竞赛Competition、报名记录Registration、成绩Score。它们之间的关系是一个竞赛有多条报名记录一条报名记录对应一条成绩或者没有如果还没出成绩。# competition/models.py from django.db import models from django.conf import settings class Competition(models.Model): name models.CharField(max_length200, verbose_name竞赛名称) category models.ForeignKey(Category, on_deletemodels.CASCADE, verbose_name类别) level models.CharField(max_length20, choices[ (school, 校级), (province, 省级), (national, 国家级) ], verbose_name级别) description models.TextField(blankTrue, verbose_name竞赛描述) registration_start models.DateTimeField(verbose_name报名开始时间) registration_end models.DateTimeField(verbose_name报名截止时间) competition_date models.DateField(verbose_name比赛日期) max_participants models.IntegerField(default0, verbose_name人数上限) created_by models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue, related_namecreated_competitions) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table competition ordering [-created_at] class Registration(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 已通过), (rejected, 已拒绝) ] competition models.ForeignKey(Competition, on_deletemodels.CASCADE, related_nameregistrations) student models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameregistrations) team_name models.CharField(max_length100, blankTrue, verbose_name队伍名称) status models.CharField(max_length10, choicesSTATUS_CHOICES, defaultpending) registered_at models.DateTimeField(auto_now_addTrue) class Meta: db_table registration unique_together (competition, student) # 同一竞赛不能重复报名 class Score(models.Model): registration models.OneToOneField(Registration, on_deletemodels.CASCADE, related_namescore) score models.DecimalField(max_digits6, decimal_places2, verbose_name得分) rank models.IntegerField(nullTrue, blankTrue, verbose_name排名) award models.CharField(max_length50, blankTrue, verbose_name奖项) remark models.TextField(blankTrue, verbose_name备注) class Meta: db_table score几个设计要点unique_together保证同一个学生不能对同一竞赛重复报名这个约束放在数据库层比放在应用层更可靠。Score用OneToOneField关联Registration因为一条报名记录只对应一个成绩。on_delete的选择上竞赛删除时报名记录跟着删CASCADE但创建者删除时竞赛保留SET_NULL。4.2 用 DRF 写竞赛列表和报名接口接口用 Django REST Framework 来写视图集ViewSet比逐个写视图函数更简洁。# competition/serializers.py from rest_framework import serializers from .models import Competition, Registration, Score class CompetitionSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) participant_count serializers.SerializerMethodField() class Meta: model Competition fields [id, name, category, category_name, level, registration_start, registration_end, competition_date, max_participants, participant_count] def get_participant_count(self, obj): return obj.registrations.filter(statusapproved).count()SerializerMethodField用来计算已通过审核的报名人数这个字段在数据库里没有对应列是动态算出来的。sourcecategory.name直接把外键关联的类别名称平铺到返回结果里前端不需要再发一次请求查类别。# competition/views.py from rest_framework import viewsets, permissions from rest_framework.decorators import action from rest_framework.response import Response from .models import Competition, Registration from .serializers import CompetitionSerializer, RegistrationSerializer class CompetitionViewSet(viewsets.ModelViewSet): queryset Competition.objects.all() serializer_class CompetitionSerializer permission_classes [permissions.IsAuthenticatedOrReadOnly] action(detailTrue, methods[post], permission_classes[permissions.IsAuthenticated]) def register(self, request, pkNone): competition self.get_object() if Registration.objects.filter(competitioncompetition, studentrequest.user).exists(): return Response({error: 您已报名该竞赛}, status400) if competition.registrations.filter(statusapproved).count() competition.max_participants: return Response({error: 报名人数已满}, status400) registration Registration.objects.create( competitioncompetition, studentrequest.user ) return Response(RegistrationSerializer(registration).data, status201)action装饰器把自定义的register方法挂到路由上访问POST /api/competitions/{id}/register/就能触发报名逻辑。这里做了两个检查重复报名和人数上限。返回 400 状态码让前端能区分「请求成功但业务失败」和「请求本身有问题」。路由注册# backend/urls.py from rest_framework.routers import DefaultRouter from competition.views import CompetitionViewSet, RegistrationViewSet router DefaultRouter() router.register(rcompetitions, CompetitionViewSet) router.register(rregistrations, RegistrationViewSet) urlpatterns [ path(admin/, admin.site.urls), path(api/, include(router.urls)), ]4.3 前端页面的数据请求和状态处理Vue 这边用 axios 发请求封装一个统一的请求模块// src/utils/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000, }) // 请求拦截器自动带上 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request竞赛列表页面的核心逻辑// src/views/CompetitionList.vue 的 script 部分 import request from /utils/request export default { data() { return { competitions: [], loading: false, currentPage: 1, total: 0 } }, created() { this.fetchCompetitions() }, methods: { async fetchCompetitions() { this.loading true try { const res await request.get(/competitions/, { params: { page: this.currentPage } }) this.competitions res.results this.total res.count } catch (err) { this.$message.error(获取竞赛列表失败) } finally { this.loading false } }, async handleRegister(competitionId) { try { await request.post(/competitions/${competitionId}/register/) this.$message.success(报名成功等待审核) this.fetchCompetitions() } catch (err) { this.$message.error(err.response?.data?.error || 报名失败) } } } }这里的关键点是错误处理。后端返回的{error: 您已报名该竞赛}会被 axios 放在err.response.data里前端直接取出来展示给用户。如果不做这个处理用户只会看到一个笼统的「报名失败」体验很差。5. 避坑指南部署和联调中最容易翻车的五个问题5.1 跨域配置写了但还是报 CORS 错误现象settings.py里已经加了corsheaders中间件和CORS_ALLOWED_ORIGINS但浏览器控制台还是报Access-Control-Allow-Origin缺失。原因通常有两个一是CorsMiddleware的位置不对必须放在CommonMiddleware之前否则响应头还没加上就被处理了二是前端请求的Origin不在CORS_ALLOWED_ORIGINS列表里比如前端跑在localhost:8081但配置里只写了8080。解决检查中间件顺序确认CorsMiddleware在列表最前面。开发阶段可以临时用CORS_ALLOW_ALL_ORIGINS True排查确认是配置问题后再改回白名单。5.2 MySQL 8.0 连接报 Authentication plugin 错误现象Django 启动时报django.db.utils.OperationalError: (2059, Authentication plugin caching_sha2_password cannot be loaded)。原因MySQL 8.0 默认用caching_sha2_password认证插件旧版本的mysqlclient不支持。解决升级mysqlclient到 2.1 以上版本或者在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY password;把认证方式改回去。推荐前者后者只是绕过问题。5.3 Vue 打包后部署到 Django 静态目录白屏现象npm run build生成的dist目录放到 Django 的static目录下访问页面白屏控制台报 404。原因Vue 打包默认用绝对路径/static/引用资源但 Django 的静态文件路径可能不一样。另外 Vue Router 的 history 模式需要服务端配置 fallback。解决在vue.config.js里设置publicPath: /static/确保资源路径正确。Router 用 hash 模式createWebHashHistory可以避免服务端配置但 URL 里会带#。如果坚持用 history 模式需要在 Django 的urls.py里加一条 catch-all 规则把所有非 API 请求都指向index.html。5.4 数据库迁移时字段类型不匹配现象makemigrations后migrate报错提示某个字段的类型和数据库现有数据冲突。原因常见于修改了模型的字段类型比如把CharField改成了IntegerField但表里已经有数据了MySQL 无法自动转换。解决开发阶段直接删库重建DROP DATABASE再CREATE DATABASE然后重新migrate。生产环境需要写数据迁移脚本先加新字段、迁移数据、再删旧字段分三步走。5.5 前端 token 过期后页面卡死现象用户登录后放置一段时间再操作时页面一直 loading没有任何提示。原因token 过期后后端返回 401但前端没有统一处理请求一直 pending 或者静默失败。解决在 axios 的响应拦截器里统一处理 401清除本地 token 并跳转到登录页。这个逻辑我在 4.3 节的request.js里已经写了关键是error.response.status 401这个判断不能漏。6. 让系统经得起答辩追问的两个进阶技巧6.1 用数据库索引优化竞赛列表查询答辩时老师很可能会问「如果竞赛数据量大了你的查询性能怎么保证」这时候如果你能说出索引的设计思路会加分不少。竞赛列表最常见的查询是按状态和截止时间筛选。比如「查出所有报名中的竞赛按截止时间排序」-- 在 Competition 表的 status 和 registration_end 上建联合索引 CREATE INDEX idx_status_deadline ON competition (status, registration_end);但注意Django 的Meta里也可以定义索引这样迁移时会自动创建class Competition(models.Model): # ... 字段定义 class Meta: db_table competition indexes [ models.Index(fields[level, registration_end]), models.Index(fields[created_at]), ]索引不是越多越好每个索引都会增加写入的开销。竞赛表的写入频率不高读多写少所以加两三个索引是合理的。报名表Registration的写入频率高索引要克制只在competition和student外键上建索引就够了。6.2 用 Django 的信号机制解耦报名通知逻辑报名成功后的通知逻辑发邮件、站内信如果直接写在视图里会让视图越来越臃肿。用 Django 的post_save信号可以把这部分逻辑解耦出去# competition/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Registration receiver(post_save, senderRegistration) def notify_on_registration(sender, instance, created, **kwargs): if created: # 新报名创建时触发通知 from .tasks import send_notification send_notification.delay(instance.id)然后在apps.py的ready()方法里导入信号模块class CompetitionConfig(AppConfig): default_auto_field django.db.models.BigAutoField name competition def ready(self): import competition.signals # noqa这样视图里只需要Registration.objects.create(...)通知逻辑自动触发。答辩时如果被问到「你的系统怎么保证可扩展性」这就是一个很好的例子——新增一种通知方式比如短信只需要改信号处理函数不需要动视图代码。我自己的习惯是每次写完一个功能模块先不急着写下一个而是花十分钟想想「如果数据量翻十倍哪里会先崩」。这个习惯帮我提前发现过好几次 N1 查询的问题。学科竞赛管理系统虽然数据量不会真的很大但答辩时能说出这些思考比单纯展示功能要有说服力得多。希望帮到你。本文还有配套的精品资源点击获取