做毕业设计这几年学科竞赛管理系统基本上每年都会出现在选题清单上。这个题目看起来很标准——发布竞赛、学生报名、评委打分、出成绩听起来就是一套普通的管理系统。但真正动手做下去你会发现这里面涉及角色权限、状态流转、并发报名、文件存储、成绩汇总统计甚至还有一点评委公平性的业务逻辑复杂度远不是增删改查那么简单。这篇文章就把我从零搭建这套系统的完整过程、设计思路、核心代码实现和踩过的坑全部梳理一遍适合正在准备这类毕业设计或者想接手类似高校业务系统的同学参考。系统完全基于常规JavaEE技术栈实现可以直接运行、方便二次扩展。1. 项目概述与核心需求拆解1.1 高校学科竞赛到底在管什么先说清楚业务场景。高校里的学科竞赛种类非常多有校级选拔赛、省级赛、国家级赛还有各类行业学会办的赛事。通常一个学校会同时运行几十个竞赛项目每个竞赛又有自己的报名周期、作品提交截止时间、评审规则和奖项设置。在没有系统之前这些工作大部分靠人工表格和即时通讯软件完成学院发通知、学生填表格汇总、教务处手动核对、评委线下收打分表、最后再由人手工统计排名。这个过程不仅效率低而且很容易出错——漏报、重报、截止时间错过、成绩算错都是常见问题。这个系统的核心目标就是把竞赛从发布到发奖的完整生命周期管理起来。具体来说包括几个关键节点竞赛信息发布、学生在线报名、团队信息维护、作品材料提交、评委在线评审打分、成绩自动汇总、获奖名单公布、数据统计查看。你一定要把系统当做一个业务流程管理工具来设计而不是单纯的信息展示平台这个定位决定了后面的数据表结构和代码设计方向。1.2 角色权限与核心功能场景这类系统最基础也最重要的设计点就是角色权限。我把它分成四个角色每个角色看到和操作的内容完全不一样系统管理员管理所有用户学生、评委、发布和审核竞赛、配置评审规则、处理异常报名、发布公告、查看统计报表。这是权力最大的角色所有关键节点都需要管理员介入。普通学生查看竞赛列表、查看详情、在线报名个人或团队、提交作品材料、查看自己的报名状态和最终成绩/奖项。评委教师查看被分配的作品列表、在线评分、填写评审意见、查看自己评过的分数记录。二级管理员/院系负责人这个角色如果你做的是大型毕设可以加小系统可以先不加。它的作用是为了处理一个竞赛需要各学院先选拔再推送校级的场景。功能场景则是围绕竞赛生命周期展开的。我梳理过核心流程大致如下管理员创建竞赛并发布 → 学生看到通知并报名 → 截止后管理员确认参赛名单 → 学生在期限内提交作品 → 管理员分配评委 → 评委打分并提交 → 系统自动汇总成绩、生成排名 → 管理员确认获奖名单 → 学生查看成绩。每个环节之间都有严格的状态依赖所以你需要让数据库表具备状态字段并且封装好状态变更逻辑。这让我在设计时重点考虑了两件事第一竞赛本身的状态流转第二报名记录的状态流转。每个环节谁可以触发、什么条件下可以触发都必须理清楚。很多同学做的时候想不清楚最后代码写出来要么是状态满天飞要么就是什么状态都没有全凭前端按钮控制一发布就乱了。2. 技术选型与整体架构设计2.1 毕业设计最常见也最稳妥的技术栈技术选型是很多人纠结的第一关。我见过有人选Python Flask、有人选Node.js甚至有人为了炫技上一套微服务加分布式事务结果把自己坑得很惨。做毕设我的原则很简单优先选择自己熟悉且社区资料最全的主流技术保证能按时完成、能顺利演示和答辩。这套系统最终采用的技术栈如下基本是JavaWeb毕设的黄金组合层次技术选择选型理由后端框架Spring Boot 2.7.x自动配置能力强内嵌Tomcat开发效率高Java毕设生态最成熟的框架持久层MyBatis Plus单表CRUD几乎不用写SQL报表统计可以用自定义SQL补充数据库MySQL 8.0免费稳定胜任这种业务量级InnoDB支持事务前端框架Vue 2 Element UI组件化开发适合管理后台类页面标签页表格表单都很现成权限方案Spring Security JWT前后端分离下最标准的组合或简化用JWT拦截器也行文件存储本地磁盘存储毕设没必要上OSS本地存文件简单可控部署时讲得清楚即可说明一下Spring Security 对很多同学来说上手成本偏高。如果时间紧张可以只用JWT加拦截器做登录和权限校验在答辩时说明通过拦截器实现接口级权限控制完全够用。我这里最终用了Spring Security主要是为了权限注解更规范展示起来更有说服力。2.2 项目模块划分与数据结构关系整个系统我从一开始就按模块划分而不是把所有Controller堆在一个包下。后端代码按业务功能拆包com.cms ├── controller # 接口层 │ ├── admin # 管理员相关接口 │ ├── student # 学生相关接口 │ └── judge # 评委相关接口 ├── service # 业务逻辑层 ├── mapper # MyBatis Plus数据访问层 ├── entity # 实体类 ├── dto # 请求/响应DTO ├── common # 通用工具、统一返回结果、异常处理 └── config # 配置类JWT、拦截器、跨域等这种按角色分包、按职责分层的好处是后期维护和答辩讲Project Structure时都特别好讲。前端页面同样按角色拆路由登录页、管理员布局、学生端、评委端。页面组件复用通过通用列表组件和表单弹窗来处理。整个项目的前后端通过JSON交互后端统一返回结果格式{ code: 200, message: 操作成功, data: { } }有了统一返回结构前端处理逻辑就非常简单只需要封装一次axios响应拦截器根据code判断业务是否成功。这样前端不用在每次请求里重复写错误处理代码。3. 数据库设计与核心业务建模3.1 核心数据表结构解析数据库是整个系统最不能省脑子的一环。我核心设计了7张业务表每张表都有它的存在理由。下面挑几张关键的讲用户表userCREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, role VARCHAR(20) NOT NULL COMMENT STUDENT/JUDGE/ADMIN, college VARCHAR(100), major VARCHAR(100), phone VARCHAR(20), email VARCHAR(100), status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );密码字段建议存BCrypt加密后的哈希值不要存明文。虽然只是毕设但答辩老师很可能会问密码安全问题预处理比现场改进容易得多。竞赛表competitionCREATE TABLE competition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, type VARCHAR(50) COMMENT 学科类型如程序设计/数学建模/电子设计, organizer VARCHAR(100) COMMENT 主办方, enroll_start DATETIME NOT NULL, enroll_end DATETIME NOT NULL, work_start DATETIME, work_end DATETIME, status VARCHAR(20) DEFAULT DRAFT COMMENT DRAFT-草稿 REGISTERING-报名中 SUBMITTING-提交中 REVIEWING-评审中 FINISHED-已结束, max_team_members INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里把竞赛拆成三个时间段报名段、作品提交段、评审段是为了让系统对流程的掌控更精确。比如报名结束后学生不能再报名提交结束后评委开始打分。每个阶段对应不同的操作许可。报名表enrollCREATE TABLE enroll ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL, user_id BIGINT NOT NULL COMMENT 队长ID, team_name VARCHAR(100), status VARCHAR(20) DEFAULT REGISTERED COMMENT REGISTERED-已报名 SUBMITTED-已提交作品 AWARDED-已获奖 CANCELLED-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_comp_student (competition_id, user_id) );这张表上我加了**(competition_id, user_id)的唯一索引**这一步非常关键。它可以从数据库层面防止同一个学生重复报名同一个竞赛。很多同学没加唯一索引只靠代码判断结果并发请求下还是插入了重复数据。作品表work、评审表review、奖项表award作品表存提交的文件路径和对应的报名记录ID评审表存评委ID、作品ID、分数和评语奖项表存竞赛ID、获奖等级、获奖学生ID和证书信息。评审表还有一层设计考虑一场竞赛可能多个评委打分所以成绩汇总时要按规则取多个评委的均分或去掉最高最低分。3.2 竞赛状态机与报名流程设计状态机是这个项目里最值得花时间设计的内容。我经历过一开始状态乱写导致前后台逻辑混乱的教训所以后来切换到明确的状态机模型。竞赛状态流转DRAFT(草稿) → REGISTERING(报名中) → SUBMITTING(提交中) → REVIEWING(评审中) → FINISHED(已结束)。不是所有状态都可以随意跳转。比如DRAFT只能由管理员手动发布为REGISTERING同时设置报名截止时间。REGISTERING到SUBMITTING由系统定时任务或管理员手动操作触发触发条件是当前时间大于报名截止时间。SUBMITTING到REVIEWING同理时间到提交截止时自动或手动触发。REVIEWING到FINISHED在评审成绩汇总完成后管理员确认发布结果竞赛结束。每次状态变更我都建议在service层写一个统一方法把状态校验和更新写在事务里。比如Transactional public void changeCompetitionStatus(Long competitionId, String fromStatus, String toStatus) { Competition competition competitionMapper.selectById(competitionId); if (competition null) { throw new BizException(竞赛不存在); } if (!competition.getStatus().equals(fromStatus)) { throw new BizException(当前状态不允许该操作); } competition.setStatus(toStatus); competitionMapper.updateById(competition); }核心思路就是任何状态变更必须显式声明来源状态防止用户绕过流程直接跳转。前端也可以根据后端返回的当前状态来动态渲染按钮。报名流程相对简单但有一个关键点要注意报名插入必须结合竞赛状态校验。学生请求报名时后端要查一次竞赛当前是否处于REGISTERING状态然后加上唯一索引兜底在事务中完成校验与插入。只做前端隐藏按钮而忽略后端校验是这类系统最常见的安全漏洞。3.3 评审打分与成绩汇总逻辑评委评分这部分我采用了单作品多评委的模式。管理员在分配评委后评委登录系统只能看到分配给自己的作品列表每个作品只能评一次分。评审表里把work_id judge_id设成唯一键防止同一评委重复提交。成绩汇总时系统先按作品分组计算均分然后按分数倒序排列生成排名。这里有两个细节值得注意去掉无效分如果竞赛规则是去掉最高分和最低分取平均应该在汇总方法里先排序再排除首尾。我把这条规则写在配置项里答辩时可以现场演示开关效果。评委打分范围一般竞赛是百分制或十分制。前端要做输入校验后端同样要校验否则有人绕过前端直接调用接口就能提交非法分数。public void submitScore(Long reviewId, BigDecimal score, String comment) { if (score null || score.compareTo(BigDecimal.ZERO) 0 || score.compareTo(new BigDecimal(100)) 0) { throw new BizException(分数必须在0-100之间); } // 更新评审记录然后重新计算该作品的平均分 }4. 核心模块实操从0到1搭建关键功能4.1 登录认证与权限拦截这个模块是系统的门面也是无数人的翻车点。我最终采用的技术方案是Spring Security 负责密码加密与认证逻辑JWT 作为无状态令牌通过自定义过滤器解析请求头里的Authorization字段。为了降低复杂度我只保留了UsernamePasswordAuthenticationFilter和OncePerRequestFilter两条链路。核心实现步骤分三步第一步用户登录接口。接收用户名和密码用BCryptPasswordEncoder.matches()比对明文和数据库哈希。比对成功根据用户ID和角色生成JWT令牌。令牌中包含三个核心信息用户ID、用户名、角色。第二步自定义JWT过滤器。拦截所有/api/**请求从Header中摘取Token解析通过后把用户信息放入SecurityContextHolder。没带Token或Token过期直接返回401。第三步接口级权限校验。在Controller方法上使用PreAuthorize(hasRole(ADMIN))对管理员接口做保护学生报名和提交作品接口则校验STUDENT角色。这样即使前端误调用他人接口后端也能拒绝访问。以下是我开发时频繁使用的一个技巧把认证用户信息统一切面注入参数。RestController RequestMapping(/api/enroll) public class EnrollController { PostMapping(/{competitionId}) public Result enroll(PathVariable Long competitionId, AuthenticationPrincipal LoginUser loginUser) { enrollService.enroll(competitionId, loginUser.getId()); return Result.success(); } }这样拿当前登录用户ID非常干净不用每个接口手动解析Token。4.2 竞赛发布与在线报名联调竞赛发布和报名是整个系统演示时的重头戏。管理员发布竞赛后学生端列表应立即出现新条目。这一步我在实现时有一个小坑前后端数据的时间格式化不一致导致前端显示NaN-NaN-NaN看着就像系统坏了。解决方案是统一约定时间传递格式。后端所有时间字段以yyyy-MM-dd HH:mm:ss字符串返回前端解析只用dayjs(字符串)不额外做转换。这个约定从第一个模块开始就写进项目注释里后面再也没有出现过时间显示问题。在线报名还有一件事需要处理——团队报名。很多竞赛允许3人组队。我的做法是队长创建团队并填写团队名称然后将队员的用户名添加进列表。报名表里只存队长ID和团队名称队员关系存在一张team_member表里。这样做的好处是成绩评到团队上奖项可以批量颁发给全体成员。报名联调时最容易出错的是事务边界。比如创建团队时需要同时插入enroll和team_member两张表任何一个失败都应该回滚。我用Transactional把整个方法包住并且抛异常时统一在service层捕获转换成业务异常避免给前端返回一堆堆栈日志。这里有个实际教训有一次创建团队重复调用添加成员方法导致队员关系重复插入后来我在team_member表加了(team_id, student_id)唯一索引同时用insertOrUpdate方式处理才彻底解决。4.3 作品文件上传与下载作品提交是竞赛系统的核心技术点之一涉及文件上传、路径存储、文件类型校验、大小限制、以及后续评委下载查看。我使用Spring Boot的MultipartFile接收文件本地存储到配置指定目录file.upload-dir./upload/ file.max-size50MB上传代码里需要注意几个点随机文件名不要用原始文件名。简单做法UUID.randomUUID()拼接原文件后缀避免中文名和路径穿越问题。扩展名白名单.zip、.rar、.pdf、.docx、.jpg其余一律拒绝。后端按扩展名判断前端选择器只是辅助。限流与容量每个作品目录独立存放路径规则为upload/{competitionId}/{enrollId}/。这样评委下载时通过报名记录就能定位文件管理也清晰。public String storeFile(MultipartFile file, Long competitionId, Long enrollId) { if (file.isEmpty()) { throw new BizException(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!ALLOWED_EXTENSIONS.contains(ext.toLowerCase())) { throw new BizException(不允许的文件类型); } String dir uploadDir File.separator competitionId File.separator enrollId; File dirFile new File(dir); if (!dirFile.exists()) { dirFile.mkdirs(); } String filename UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(dirFile, filename)); return dir File.separator filename; }文件下载时我做了权限控制普通学生只能下载自己团队的作品评委可以下载分配给自己的作品管理员可以下载全部。这个逻辑放在service层判断而不是直接在Controller里把文件流输出给所有人。5. 常见问题与排查实践记录5.1 并发报名导致数据错乱系统开发完成后的压测阶段我用模拟并发请求脚本同时发起了20个报名请求发现最终报名记录数大于预期。排查后发现问题出在先查后插的时序漏洞多个请求同时查到竞赛是REGISTERING状态同时执行插入结果重复报名。解决方案分两层数据库层enroll表的(competition_id, user_id)唯一索引兜底。代码层报名方法加Transactional在校验状态和插入数据之间不留空隙。如果你还想更严谨可以用MySQL的SELECT ... FOR UPDATE锁住竞赛记录保证同时只有一个请求能进入校验逻辑。我在教学演示中特意保留了一版无锁版本的代码对比效果非常直观。5.2 文件上传路径与部署环境差异本地开发文件路径写死了Windows的D:/upload部署到服务器跑起来之后上传文件一直报FileNotFoundException。原因是服务器没有这个绝对路径且服务进程对目标目录没有写权限。这个问题解决有三个关键步骤上传目录放到项目相对路径下可配置不要写死。服务启动时主动检测目录不存在就自动创建。定期备份或同步上传目录因为一旦容器重启或重新部署目录数据不会自动保留。另外还要注意如果用了反向代理或网关文件下载接口的请求大小限制和超时设置也要同步调整否则评委下载大文件时会遇到响应被截断的问题。5.3 评审打分业务规则的边界处理评审模块是一开始最容易忽略细节的地方。我遇到过几个问题评委给分了但作品平均分没变。原因是汇总逻辑只统计了review表里status为SUBMITTED的评价但是提交打分时忘了更新该状态。多个评委同一个作品重复打分。因为评审表没有加唯一索引。后来加上UNIQUE(work_id, judge_id)解决。管理员删除竞赛时关联作品和评审记录没有清理导致统计报表出现孤儿数据。这个通过数据库外键加级联删除或者service层手写事务清理解决。评审公平性也是一个值得讲的设计点。我在系统里控制评委只能看到作品编号和内容看不到学生姓名和学校。这样演示时能拿双盲评审作为一个亮点介绍。具体做法是给作品表增加一个anonymous_code字段平台生成匿名编号评委打分页面统一展示匿名编号而不是报名ID。6. 项目部署与源码运行指南6.1 本地运行环境准备拿到一套源码之后第一步是把环境跑通。整个项目最小的运行环境如下JDK 1.8 或 11MySQL 8.0MySQL 5.7也可以但注意时间字段默认值语法差异Maven 3.6Node.js 14用于前端构建或者前端部分直接集成在后端静态目录启动顺序我推荐创建数据库执行项目根目录下的sql/init.sql完成初始化表结构和测试数据。修改后端application.yml中的数据库账号密码确保和本地一致。启动后端服务等待端口8080正常监听。前端如果独立运行进入前端目录执行npm install和npm run dev如果前端已经打包放入后端static目录直接把前端包放到src/main/resources/static下启动后端后访问http://localhost:8080即可。使用初始化脚本里的管理员账号登录密码通常是admin/123456首次登录建议修改。6.2 顺利运行务必注意的几个关键配置这里单独列一份避坑清单都是实际接手项目时最常见的问题数据库时区问题连接字符串必须带serverTimezoneAsia/Shanghai否则传时间到后端会差8小时。Redis有没有被强制依赖如果项目里设计了验证码或Token黑名单可能依赖Redis。不想用Redis可以直接用内存Cache但部署后重启服务Token会失效这个要在答辩时说明取舍。跨域配置前后端分离模式下前端开发服务器端口通常是8081后端8080。必须配置CorsFilter允许的源、方法和Header否则前端请求全部被浏览器拦截。这里经常出现后端接口测试没问题前端调用就403的怪象根源就在跨域。文件上传大小Spring Boot默认单文件上传限制1MB如果竞赛作品材料超过这个大小必须手动在配置里把spring.servlet.multipart.max-file-size调大。静态资源路径如果上传文件要能通过URL直接访问需要配置资源映射器把/upload/**映射到本地磁盘目录。忘记配置就会出现文件保存成功但页面图片显示不出来的情况。后端启动如果报端口占用用命令查一下是哪个进程占用8080端口直接杀掉或者换端口配置处理掉这个问题后再启动。源码目录结构我建议拿到手后先花20分钟通读一遍不要急着点运行。理清楚每个包是干什么的再对照数据库表过一遍字段含义后面遇到问题定位会快得多。7. 个人实操体验与一点点建议整套系统从建表到前端页面联调完成常规节奏差不多要三周到一个月。我自己的经验是数据库设计阶段多花两天思考状态和唯一索引后面编码、联调阶段能省出一周反过来前期表结构乱后面到处打补丁返工成本极高。在做的过程中我对毕业设计要不要用框架这件事的体会比较深。框架确实是提高效率的工具但它不是万能的。学科竞赛管理系统业务流程相对清晰用Spring Boot这类主流框架能快速搭建骨架但真正的功夫在业务建模和边界条件处理上。比如状态流转、并发控制、权限校验、文件存储这些逻辑和框架无关需要自己一层层想清楚。这些点也恰恰是答辩时老师最愿意追问的地方因为能体现你对一个完整业务系统的理解深度。如果你以后想把这个项目继续扩展有几个方向可以考虑增加竞赛类别与学分认定对接、加入邮箱或短信通知、接一个简单的图表统计页面展示各学院参与率和获奖率、把评审模块改成专家互评模式。这几个方向都能让系统更接近实际生产环境。这套系统的完整源码、数据库脚本和部署文档我整理在附件里项目编号18952。下载后按说明一步步执行基本就能跑起来。如果有哪个模块卡住了对照这篇文章的章节定位问题大部分坑在前面已经帮你趟过了。