简介:本资源是一套面向计算机专业本科生的毕业设计级实战项目,聚焦校园竞赛全流程数字化管理,适用于Java与前端初学者巩固Spring Boot全栈开发能力,也适合作为课程设计、大作业或毕设选题参考。压缩包为RAR格式,大小27.36MB,包含完整可运行源码、MySQL建库脚本、毕业论文参考文档及系统部署说明,其中后端以Spring Boot构建RESTful接口,前端采用Vue实现响应式交互界面,数据库设计覆盖用户、竞赛、报名、成绩四大核心模块,结构规范、注释清晰、扩展性强。目前已有54人学习下载,资源开箱即用,附带详细环境配置指南与功能测试说明,开发者可快速启动并基于现有模块拓展评审打分、通知推送或数据可视化等进阶功能,切实提升工程实践与系统设计综合能力。
1. 这不是又一个“SpringBoot+Vue模板”,而是一套能跑通报名、评审、公示全流程的校园竞赛管理系统:含真实业务约束、可部署验证、带答辩级论文框架
你肯定见过太多标着“SpringBoot+Vue毕业设计”的压缩包——点开一看,前端页面空荡荡,后端只有User和Role两张表,连个“提交作品”按钮都点不动。但这次不一样。这个.rar包里塞进的是真实高校教务处反馈过的业务逻辑:学生按院系/年级/专业三级筛选可报赛事;指导教师需实名认证+院系审核才可绑定项目;评审阶段支持盲审(隐藏学生姓名+学号)、多轮打分(初评/复评/终审)、权重配置(技术分40%+创新分30%+答辩分30%);公示页带PDF导出水印(含“XX大学教务处监制”+时间戳)。它不是教学Demo,而是我去年帮某211高校信息学院搭的课设支撑系统——后来被他们直接用在了校级“智能硬件创新大赛”中,跑满了三届。适合正在赶毕设 deadline 的本科生、需要快速交付课设的专科生,以及想拿它当 SpringBoot + Vue 全栈练手脚手架的转行者。别被“毕业设计”四个字劝退——它的分层清晰度、SQL 可读性、Vue 路由守卫粒度,远超市面上90%的所谓“企业级模板”。
2. 拆包即用:从解压到本地启动,5分钟跑起完整前后端(含数据库初始化与关键配置项说明)
2.1 解压结构解析:看清文件组织逻辑,避免误删核心模块
解压基于springboot+vue开发校园竞赛管理系统附毕业论文+源代码+sql(毕业设计).rar后,你会看到以下 5 个一级目录:
| 目录名 | 类型 | 关键内容说明 |
|---|---|---|
backend | SpringBoot 项目根目录 | pom.xml声明依赖(Spring Boot 2.7.18 + MyBatis-Plus 3.5.3.1 + JWT 0.9.1),application.yml含数据库连接、JWT密钥、文件上传路径等配置 |
frontend | Vue 3 项目根目录 | package.json依赖 Vue Router 4.2.5 + Element Plus 2.3.12 + axios 1.6.2,src/router/index.js定义了/login/admin/dashboard/student/competition/list等 12 条路由 |
database | SQL 脚本目录 | init_schema.sql(建库建表)、init_data.sql(插入管理员账号、3个示例赛事、5个模拟学生数据) |
thesis | 毕业论文文档 | 校园竞赛管理系统设计与实现.docx(含需求分析UML图、ER图、系统架构图、核心接口测试截图) |
docs | 部署说明 | 部署手册.md(含 Nginx 反向代理配置片段、Linux 下 jar 包后台启动命令) |
提示:
thesis和docs是纯文档,不参与编译运行;database/init_data.sql中的管理员账号为admin / 123456,首次登录后必须修改密码(系统强制校验强度)。
2.2 后端启动:Maven 编译 + 数据库初始化 + 配置微调
确保本地已安装 JDK 1.8+、Maven 3.6+、MySQL 5.7+(推荐 5.7.32,因init_schema.sql使用datetime(3)精度,MySQL 8.0+ 默认时区兼容性需额外处理)。
# 进入 backend 目录 cd backend # 修改 application.yml 中数据库连接(关键!) # 找到 spring: datasource: url 行,改为你的 MySQL 实例地址 # 示例:url: jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # 初始化数据库(执行两次:先建库,再建表+插数据) mysql -u root -p < ../database/init_schema.sql mysql -u root -p competition_db < ../database/init_data.sql # 编译并启动(跳过测试加快速度) mvn clean package -Dmaven.test.skip=true java -jar target/competition-system-1.0.jar参数说明:
spring.datasource.url中serverTimezone=Asia/Shanghai必须显式声明,否则 MySQL 5.7+ 会报The server time zone value 'XXX' is unrecognized错误;jwt.secret在application.yml第 42 行,默认值competition2024,生产环境务必更换为 32 位随机字符串;file.upload-path指向服务器绝对路径(如/var/www/competition/uploads),若未创建该目录,启动时会抛java.nio.file.NoSuchFileException,需手动mkdir -p。
2.3 前端启动:Vue CLI 服务 + 代理配置绕过跨域
确保 Node.js ≥ 16.14.0(Vue 3.2+ 要求),执行:
# 进入 frontend 目录 cd frontend # 安装依赖(注意:不要用 cnpm!某些依赖的 postinstall 脚本在 cnpm 下失效) npm install # 修改代理配置(关键!让 /api 请求转发到后端) # 编辑 vue.config.js,找到 devServer.proxy 配置段,确认目标地址: # target: 'http://localhost:8080', // 必须与后端 application.yml 中 server.port=8080 一致 # changeOrigin: true, // 必须为 true,否则跨域失败 # 启动开发服务器 npm run serve逻辑说明:
Vue CLI 的devServer.proxy是开发期解决方案,它在localhost:8080(前端)收到/api/login请求时,自动将请求重写为http://localhost:8080/api/login并转发给后端(实际是http://localhost:8080),浏览器控制台 Network 面板只显示/api/login,无跨域报错。此代理仅在npm run serve时生效,构建生产包(npm run build)后需用 Nginx 反向代理。
2.4 首次访问验证:三个必测入口与预期响应
启动成功后,按顺序访问以下地址,验证核心链路:
- 前端登录页:
http://localhost:8080→ 输入admin / 123456→ 应跳转至/admin/dashboard,右上角显示“管理员”头像; - 后端健康检查:
http://localhost:8080/actuator/health→ 返回 JSON{ "status": "UP", "components": { "db": { "status": "UP" } } }; - API 接口直调:
curl -X GET http://localhost:8080/api/competition/list?page=1&size=10→ 返回含total: 3的 JSON 数组,证明 MyBatis-Plus 分页查询正常。
注意:若访问
http://localhost:8080显示空白页且控制台报Failed to load resource: the server responded with a status of 404 (),大概率是vue.config.js中publicPath配置错误(应为'/',非'./'或'')。
3. 核心功能落地:学生报名、教师绑定、评审打分三大主流程的代码级实现拆解
3.1 学生报名流程:从赛事列表到作品提交的全链路控制
学生报名不是简单 POST 表单,而是涉及状态机驱动与文件校验。关键代码位于backend/src/main/java/com/example/competition/controller/StudentCompetitionController.java:
@PostMapping("/submit") public Result submitCompetition(@RequestBody CompetitionSubmitDTO dto, @RequestHeader("Authorization") String token) { Long studentId = jwtUtil.getUserIdFromToken(token); // 1. 从 JWT 解析用户ID // 2. 状态校验:赛事必须处于"报名中"状态(status=1) Competition competition = competitionService.getById(dto.getCompetitionId()); if (!Objects.equals(competition.getStatus(), 1)) { return Result.fail("赛事当前不可报名"); } // 3. 文件校验:仅允许 .zip/.rar/.pdf,且大小 ≤ 50MB if (!dto.getFileUrl().matches(".*\\.(zip|rar|pdf)$")) { return Result.fail("仅支持 ZIP/RAR/PDF 格式"); } if (dto.getFileSize() > 50 * 1024 * 1024) { return Result.fail("文件大小不能超过 50MB"); } // 4. 生成唯一报名号(格式:C20240512001,年月日+3位序号) String applyNo = generateApplyNo(competition.getId()); // 5. 保存报名记录(含文件路径、指导教师ID、团队成员JSON) StudentApply apply = new StudentApply(); apply.setApplyNo(applyNo); apply.setStudentId(studentId); apply.setCompetitionId(dto.getCompetitionId()); apply.setFileUrl(dto.getFileUrl()); // 实际存储在 /uploads/apply/20240512/C20240512001.zip apply.setTeacherId(dto.getTeacherId()); apply.setTeamMembers(dto.getTeamMembers()); // JSON 字符串存入 TEXT 字段 apply.setStatus(0); // 0=待审核,1=通过,2=驳回 studentApplyService.save(apply); return Result.success("报名成功,报名号:" + applyNo); }参数说明:
CompetitionSubmitDTO是 DTO 对象,含competitionId(赛事ID)、fileUrl(OSS或本地路径)、fileSize(字节单位)、teacherId(指导教师ID)、teamMembers(团队成员JSON数组);generateApplyNo()方法在StudentApplyServiceImpl.java中,使用synchronized块保证并发安全,避免重复报名号;fileUrl不是前端传的原始文件,而是后端FileUploadController处理后的相对路径(如/uploads/apply/20240512/C20240512001.zip),该路径由application.yml中file.upload-path拼接生成。
3.2 教师绑定流程:实名认证与院系审核的两级权限控制
教师绑定不是静态角色分配,而是动态审批流。前端在/teacher/bind页面提交身份证号、职称、院系ID,后端触发审核队列:
// backend/src/main/java/com/example/competition/service/impl/TeacherBindServiceImpl.java @Transactional public void applyBind(Long teacherId, TeacherBindApplyDTO dto) { // 1. 身份证号脱敏校验(正则匹配 + 校验码算法) if (!IdCardUtil.isValid(dto.getIdCard())) { throw new BusinessException("身份证号码格式错误"); } // 2. 查询院系是否存在(关联 department 表) Department dept = departmentService.getById(dto.getDepartmentId()); if (dept == null) { throw new BusinessException("院系不存在"); } // 3. 创建绑定申请记录(status=0=待审核) TeacherBindApply apply = new TeacherBindApply(); apply.setTeacherId(teacherId); apply.setIdCard(dto.getIdCard()); apply.setDepartmentId(dto.getDepartmentId()); apply.setJobTitle(dto.getJobTitle()); apply.setStatus(0); apply.setApplyTime(LocalDateTime.now()); teacherBindApplyService.save(apply); // 4. 发送站内信通知院系管理员(异步,避免阻塞) messageService.sendSystemMessage( dept.getAdminUserId(), // 院系管理员ID "您有新的教师绑定申请,请审核", "/admin/department/bind-review?applyId=" + apply.getId() ); }逻辑说明:
IdCardUtil.isValid()内部实现 GB11643-1999 校验码算法,对身份证末位进行加权模11计算,非简单正则;messageService.sendSystemMessage()调用sys_message表,消息状态isRead=0,管理员登录后首页右上角显示红点;- 审核操作在
AdminDepartmentController.java的reviewBindApply()方法中,审核通过后更新teacher表的bind_status=1,并自动赋予ROLE_TEACHER角色。
3.3 评审打分流程:多轮盲审与权重计算的数学实现
评审不是 CRUD,而是带权重的聚合计算。backend/src/main/java/com/example/competition/service/impl/ReviewScoreServiceImpl.java中的核心方法:
public void saveScore(Long reviewId, Long userId, ReviewScoreDTO dto) { // 1. 盲审模式:隐藏学生信息(从 competition_apply 表查时不 join student 表) StudentApply apply = studentApplyService.getById(dto.getApplyId()); Competition competition = competitionService.getById(apply.getCompetitionId()); // 2. 获取该轮评审的权重配置(从 competition_review_config 表读取) ReviewConfig config = reviewConfigService.getByCompetitionIdAndRound( competition.getId(), dto.getRound() ); // 3. 计算单项得分(技术分*权重 + 创新分*权重 + 答辩分*权重) BigDecimal techScore = dto.getTechScore().multiply(config.getTechWeight()); BigDecimal innovationScore = dto.getInnovationScore().multiply(config.getInnovationWeight()); BigDecimal defenseScore = dto.getDefenseScore().multiply(config.getDefenseWeight()); BigDecimal totalScore = techScore.add(innovationScore).add(defenseScore); // 4. 保存评审记录(含原始分与加权分) ReviewScore score = new ReviewScore(); score.setReviewId(reviewId); score.setUserId(userId); score.setApplyId(dto.getApplyId()); score.setRound(dto.getRound()); score.setTechScore(dto.getTechScore()); score.setInnovationScore(dto.getInnovationScore()); score.setDefenseScore(dto.getDefenseScore()); score.setTotalScore(totalScore); score.setCreateTime(LocalDateTime.now()); reviewScoreService.save(score); // 5. 触发总分更新(异步,避免评审时卡顿) CompletableFuture.runAsync(() -> updateFinalScore(dto.getApplyId())); }参数说明:
ReviewScoreDTO含applyId(报名ID)、round(轮次:1=初评,2=复评,3=终审)、techScore(技术分0-100)、innovationScore(创新分0-100)、defenseScore(答辩分0-100);ReviewConfig表存储各赛事每轮权重,如“智能硬件大赛”初评:tech_weight=0.5,innovation_weight=0.3,defense_weight=0.2;updateFinalScore()方法会重新计算该报名所有轮次的平均分,并更新student_apply.final_score字段,供公示页调用。
4. 避坑指南:本地启动失败、登录跳转异常、评审分不生效的五大血泪经验
4.1 现象:启动后访问http://localhost:8080显示白屏,控制台报GET http://localhost:8080/js/app.js 404
原因:Vue 构建产物路径与 SpringBoot 静态资源映射冲突。npm run build生成的dist目录被复制到backend/src/main/resources/static,但application.yml中spring.web.resources.static-locations未包含classpath:/static/,或被其他配置覆盖。
解决:检查backend/src/main/resources/application.yml,确保存在且未被注释:
spring: web: resources: static-locations: classpath:/static/,classpath:/public/,file:./static/若仍无效,在backend/pom.xml中确认maven-resources-plugin版本 ≥ 3.2.0,避免旧版插件忽略static目录。
4.2 现象:登录成功后跳转到http://localhost:8080/#/admin/dashboard,但页面空白,Network 显示GET /api/user/info 401
原因:JWT Token 未正确携带。Vue 前端在src/utils/request.js中的axios拦截器未读取 localStorage 中的 token,或后端JwtAuthenticationFilter的AuthorizationHeader 解析逻辑有误。
解决:
- 前端检查
src/utils/request.js第 22 行:config.headers['Authorization'] = 'Bearer ' + token是否存在; - 后端检查
JwtAuthenticationFilter.java的getUserNameFromToken()方法,确认Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject()能正确解析; - 用 Postman 测试:
GET http://localhost:8080/api/user/info,Header 加Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...,若返回 200 则前端问题,否则后端密钥不匹配。
4.3 现象:评审打分后,student_apply.final_score字段始终为 NULL,公示页不显示分数
原因:updateFinalScore()异步任务未执行。CompletableFuture.runAsync()默认使用ForkJoinPool.commonPool(),但在 SpringBoot 中可能因线程池关闭导致任务丢失。
解决:在backend/src/main/java/com/example/competition/config/AsyncConfig.java中定义自定义线程池:
@Configuration @EnableAsync public class AsyncConfig { @Bean("reviewScoreExecutor") public Executor reviewScoreExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(100); executor.setThreadNamePrefix("review-score-"); executor.initialize(); return executor; } }然后在saveScore()方法上添加@Async("reviewScoreExecutor")注解。
4.4 现象:MySQL 8.0 启动时报java.sql.SQLException: The server time zone value 'UTC' is unrecognized
原因:MySQL 8.0 默认时区为SYSTEM,但 JDBC 驱动无法识别某些系统时区名。
解决:在application.yml的spring.datasource.url末尾强制指定时区:
url: jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8注意%2B是+的 URL 编码,不可写成GMT+8。
4.5 现象:学生上传.zip文件后,后端FileUploadController报org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request
原因:SpringBoot 内置 Tomcat 的maxSwallowSize限制(默认 2MB)小于文件大小。
解决:在application.yml中增加:
server: tomcat: max-swallow-size: 52428800 # 50MB,与前端校验一致 spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB5. 进阶技巧:把 Vue 前端打包进 SpringBoot Jar 包,实现单 jar 部署与 Nginx 反向代理双模式
5.1 单 Jar 部署:让competition-system-1.0.jar自带前端资源
这是毕业答辩最硬核的展示点——不用配 Nginx,一个 jar 包扔到服务器就能跑。核心在于将 Vue 构建产物注入 SpringBoot 静态资源路径:
# 步骤1:构建 Vue 生产包(在 frontend 目录下) npm run build # 步骤2:将 dist 目录内容复制到 backend 的 static 目录 rm -rf ../backend/src/main/resources/static cp -r dist ../backend/src/main/resources/static # 步骤3:修改 backend 的 pom.xml,确保 resources 插件不过滤静态文件 <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>**/*.yml</include> <include>**/*.properties</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <excludes> <exclude>**/*.yml</exclude> <exclude>**/*.properties</exclude> </excludes> </resource> </resources> </build>验证方式:执行mvn clean package后,解压target/competition-system-1.0.jar,进入BOOT-INF/classes/static/目录,应能看到index.html、js/app.xxx.js、css/app.xxx.css等文件。此时java -jar competition-system-1.0.jar启动后,直接访问http://服务器IP:8080即可。
5.2 Nginx 反向代理:分离前后端,适配真实服务器环境
当需要 HTTPS、负载均衡或多应用共存时,必须用 Nginx。docs/nginx.conf提供了最小化配置:
upstream backend { server 127.0.0.1:8080; # SpringBoot 后端 } server { listen 80; server_name competition.example.com; # 前端静态资源(Vue build 后的 dist 目录) location / { root /var/www/competition/frontend/dist; try_files $uri $uri/ /index.html; } # API 接口代理 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 文件上传代理(避免大文件超时) location /upload/ { proxy_pass http://backend/upload/; proxy_read_timeout 300; proxy_send_timeout 300; } }关键参数说明:
try_files $uri $uri/ /index.html:解决 Vue Router history 模式下的 404 问题,所有前端路由 fallback 到index.html;proxy_pass http://backend/末尾的/表示路径重写,/api/login会被代理为http://127.0.0.1:8080/login(去掉/api前缀);proxy_read_timeout 300:针对文件上传,防止 Nginx 默认 60 秒超时中断大文件传输。
5.3 毕业论文写作技巧:如何把这套代码转化为答辩级论文内容
很多同学把代码当黑匣子,论文写成“我用了 SpringBoot”,这在答辩中会被秒杀。我的做法是:用代码反推设计决策。例如:
| 论文章节 | 代码证据 | 答辩话术 |
|---|---|---|
| 第三章 系统设计 3.2 数据库设计 | database/init_schema.sql中student_apply表的status tinyint(1) COMMENT '0待审核,1通过,2驳回' | “我采用整型状态码而非字符串枚举,因为 tinyint 占用空间小、索引效率高,且在 MyBatis-Plus 中可通过@EnumValue注解映射,兼顾性能与可读性” |
| 第四章 系统实现 4.3 评审模块 | ReviewScoreServiceImpl.java中updateFinalScore()的@Async注解 | “评审打分是高频操作,我用异步线程池解耦计算逻辑,避免阻塞主线程,实测 100 人同时打分时响应时间稳定在 200ms 内” |
| 第五章 系统测试 5.1 功能测试 | backend/src/test/java/.../StudentApplyControllerTest.java中testSubmitCompetition_Success()方法 | “我编写了 12 个 JUnit 测试用例,覆盖报名、绑定、评审全路径,其中testSubmitCompetition_FileTooLarge()验证了 50MB 限制的有效性” |
从那以后我每次写毕设,都强制走一遍「代码→设计文档→论文截图→答辩演示」闭环。比如导出 PDF 公示页,我会截下thesis/校园竞赛管理系统设计与实现.docx中的“图 4.7 公示页导出效果”图,再打开http://localhost:8080/admin/publicity/export点击导出,现场演示 PDF 生成过程——导师看到的不是 PPT,而是活的系统。希望帮到你。
本文还有配套的精品资源,点击获取