
刚带完一届毕业设计好几个学生都选了竞赛管理这个题目。说起来这确实是个经久不衰的选题不管是本科毕设还是课程设计甚至是一些高校的实训项目基于Spring Boot的学生竞赛管理系统出现的频率相当高。后台经常有人私信问我这类项目怎么搭、怎么把功能做完整、评审那块怎么设计才不显得廉价。所以这次干脆把我在实际开发中积累的思路、代码结构和踩坑经验系统整理出来完全按真实项目的做法来拆解希望能帮到正在做类似系统或者准备把这个方向作为毕设选题的朋友。这个系统本质上就是一个典型的业务管理系统核心是解决高校里竞赛信息发布、学生报名、作品提交、评委评审、成绩公示这一整条链路的管理问题。用Spring Boot做后端几乎是这个场景的标准答案生态成熟、上手快、招人也好招更重要的是它那一套自动配置机制能把开发效率拉得很高。下文我就从整体设计、核心功能、技术选型和实际开发中的坑这几个维度展开内容偏实战代码示范基于Spring Boot 2.7.x部分地方会顺带提一下适配3.x版本时需要注意的差异。1. 内容整体设计与思路拆解1.1 核心需求解析竞赛管理系统到底在管什么很多初学者拿到这个题目第一反应是“不就是个CRUD吗”这么想会吃大亏。真去高校里问一圈就知道竞赛管理的痛点从来不在“增删改查”本身而在于业务流程的完整性和不同角色之间的协作。一个合格的学生竞赛管理系统至少要覆盖以下角色和场景学生查看竞赛公告、在线报名、上传参赛作品、查看自己的成绩和获奖情况。评委/教师对分配到的作品进行打分、填写评审意见、提交评审结果。管理员/教务处老师创建竞赛、设置报名时间、管理评委分配、审核作品、发布成绩、导出统计报表。如果只是把学生、竞赛、成绩做成三个孤立的表然后写几个Controller那系统做完也只是一个“数据管理工具”根本谈不上“管理”。真正的难点在于状态流转一个竞赛从“报名中”到“评审中”再到“已结束”不同状态下不同角色能做什么操作数据之间怎么联动这才是设计上的核心。所以我在设计这个系统时第一件事不是写代码而是把状态机画清楚竞赛状态草稿 → 报名中 → 评审中 → 已结束 → 已公示报名状态已提交 → 已通过 → 已驳回作品状态未提交 → 已提交 → 已分配评委 → 评审完成成绩状态未录入 → 已录入 → 已发布有了这一层状态定义后面的接口设计、权限控制、前端页面展示都会清晰很多。这也是我后来带学生做项目时反复强调的一点先梳理业务流程再设计数据库最后才写代码这个顺序不能乱。1.2 技术选型为什么是Spring Boot而不是别的竞赛管理系统这种业务型Web项目技术选型的逻辑其实很固定。Spring Boot之所以是首选核心原因有三个。第一个是起步依赖和自动装配带来的效率优势。如果你用原生的Spring MVC搭建项目光是处理各种XML配置就要折腾一两天而且不同模块之间的版本兼容全靠自己踩坑。Spring Boot通过spring-boot-starter-web、spring-boot-starter-data-jpa这样的起步依赖把常用组件的版本统一管理好项目初始化几分钟就能跑起来这对课程设计和毕设来说太重要了时间应该花在业务逻辑上而不是配置环境上。第二个是内嵌容器简化了部署和测试。Spring Boot默认内嵌Tomcat打包成可执行Jar直接java -jar就能跑不需要额外装一个Tomcat或者配置外部容器的数据源。平时在IDEA里直接跑main方法就能开发调试比传统SSH项目在一个又一个配置文件中挣扎舒服得多。第三个是生态和社区成熟度。不管是集成MyBatis-Plus、Redis、RabbitMQ还是基于Spring Security做权限控制网上都有大量现成的案例和解决方案。就算遇到问题搜索引擎一搜也能找到对应的答案这一点对于新手来说其实是隐形的成本节约。我这个项目里用的技术栈是这样的技术组件用途版本建议Spring Boot基础框架2.7.x可平滑升级到3.xMyBatis-PlusORM框架3.5.xMySQL关系型数据库5.7 / 8.0Redis缓存、验证码存储5.x以上Spring Security JWT认证与授权配合Boot版本选择EasyExcel数据导入导出3.xKnife4j接口文档生成4.x这里提醒一句如果是从零开始做新项目建议直接从Spring Boot 3.x起步毕竟Spring Framework 6和JDK 17的组合以后会是绝对主流。但如果你只是为了完成课程设计且服务器上只有JDK 1.8那老老实实选2.7.x别上来就踩版本坑。1.3 功能模块拆解一个完整的竞赛系统该有哪些页面功能模块的划分直接影响到数据库设计和前后端联调的复杂度。按我实践下来的经验把系统拆成六个模块比较合理用户认证模块、竞赛管理模块、报名管理模块、评审管理模块、成绩管理模块、系统管理模块。用户认证模块负责注册、登录、token签发和权限校验。竞赛管理模块是核心管理端功能管理员在此创建竞赛、配置时间、发布公告。报名管理模块面向学生处理报名申请和作品提交同时管理员可以审核报名信息。评审管理模块是业务复杂度最高的地方涉及评委分配、打分、评审意见录入和进度追踪。成绩管理模块负责成绩汇总、排名计算和公示。系统管理模块则是用户管理、角色管理、日志审计等基础功能。每个模块之间的依赖关系也要理清楚竞赛创建在先报名依赖竞赛存在评审依赖报名审核通过成绩依赖评审完成。模块划分不是给自己看的是给后续数据库设计做铺垫的表结构怎么分、外键怎么关联在这一步就已经能定个七七八八了。2. 核心细节解析与实操要点2.1 用户认证与权限控制Spring Security JWT的组合实践竞赛管理系统涉及到学生、评委、管理员三种角色权限控制是刚需。我在项目里采用的是Spring Security JWT的方案这里展开说一下关键实现。JWT方案的核心思路是用户登录成功后服务端生成一个包含用户ID、角色信息的加密Token返回给前端之后前端每次请求都在请求头里带上这个Token后端通过过滤器解析Token来识别用户身份和权限。这样做的好处是服务端不需要保存Session天然支持水平扩展多个实例部署时不需要考虑Session共享问题。具体的代码结构可以分为三层安全配置层、JWT工具层、自定义过滤器层。安全配置层用SecurityFilterChain配置放行的路径和需要认证的路径注意顺序很重要放行的规则要写在前Configuration EnableWebSecurity EnableGlobalMethodSecurity(prePostEnabled true) public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/**, /doc.html, /webjars/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/judge/**).hasRole(JUDGE) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }过滤器的工作逻辑很直白从请求头Authorization里取出Token解析成功就构造一个UsernamePasswordAuthenticationToken放入SecurityContext表示当前用户已认证后续的权限注解就可以正常使用PreAuthorize做细粒度控制了。这里建议把JWT的过期时间设为2小时结合Redis做刷新处理避免用户用着用着突然掉线。实际开发中还有一个容易被忽略的点**登录接口和验证码接口一定要放行但Token校验过滤器也要拦。**我见过不少项目把过滤器用shouldNotFilter把所有接口全排除了结果后面加上去的接口全都能匿名访问安全配置形同虚设。2.2 文件上传与访问作品提交里的细节坑竞赛系统里学生要上传作品可能是文档、压缩包、图片甚至视频。作品文件一般都不小这里有两个坑是高频踩到的。第一个坑是配置文件的上传大小限制。Spring Boot默认单文件上传大小为1MB超过就报错。需要在application.yml里显式修改spring: servlet: multipart: max-file-size: 100MB max-request-size: 500MB第二个坑是文件存储和访问路径映射。本地开发时把文件存到服务器的某个目录后前端根本访问不到因为那些目录并不在Spring Boot的静态资源映射范围内。解决办法是注册一个资源映射器Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(file: fileUploadProperties.getPath() /); } }如果系统部署在Nginx后面Nginx的client_max_body_size也要同步调整这部分在后面的排查章节会再展开说。2.3 多数据源与缓存策略当项目不再只是一个表小体量的系统单数据源就够了但如果竞赛并发报名量比较大或者业务上希望把系统日志、操作记录单独拆到一个库甚至一台机器上就需要考虑多数据源配置。在Spring Boot里实现多数据源最常用的方案是使用DS注解配合dynamic-datasource-spring-boot-starterspring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://localhost:3306/competition username: root password: 123456 log: url: jdbc:mysql://localhost:3306/competition_log username: root password: 123456然后在Service层用注解切换数据源DS(log) public void writeLog(OperationLog log) { logMapper.insert(log); }缓存层面竞赛公告这类读多写少的数据非常适合用Redis做缓存。用Spring Cache的Cacheable、CachePut、CacheEvict三个注解就能覆盖绝大多数场景不需要自己去封装RedisTemplate的set/get逻辑。特别提醒缓存失效问题一定要考虑比如发布新公告后要主动CacheEvict掉对应的列表缓存否则用户看到的永远是老数据。3. 实操过程与核心环节实现3.1 数据库设计五张核心表的结构与关系在代码开始之前先把数据库设计定下来。竞赛管理系统的核心表可以归纳为五张用户表、竞赛表、报名表、作品表、评审表。再加一些辅助表比如公告表、日志表整体在10张表左右是比较合理的规模。用户表sys_user字段包括id、username、password、real_name、role区分学生/评委/管理员、college、student_no等。密码字段存的是BCrypt加密后的密文绝对不允许明文存储。竞赛表competitionid、title、description、type、status草稿/报名中/评审中/已结束、start_time、end_time、create_by。这里status字段是业务流转的关键建议用int类型存枚举值可以配合一个status_desc注释列方便开发时查看。报名表signupid、competition_id、user_id、status已提交/已通过/已驳回、apply_time、audit_remark。报名表相当于竞赛和学生的关联实体会有大量数据所以一定要给competition_id和user_id建联合索引。作品表workid、signup_id、file_url、file_name、file_size、submit_time、status。每个通过审核的报名记录对应一条作品记录用signup_id做外键关联。评审表reviewid、work_id、judge_id、score、comment、review_time。同一份作品可能分配给多个评委打分取平均分作为最终成绩所以work_id和judge_id的联合唯一索引要考虑好防止同一评委重复打分。竞品系统里常见的“成绩”其实不需要单独建表用视图或者聚合查询从评审表里算出来就行。如果你确实需要保存最终排名和获奖等级可以加一个award表字段为id、competition_id、work_id、rank、award_name。3.2 核心接口设计与状态流转逻辑接口设计直接决定前后端联调的顺畅程度也影响系统的可维护性。我按模块梳理了核心接口清单模块接口方法功能说明认证/api/auth/loginPOST登录换取Token认证/api/auth/registerPOST学生注册竞赛/api/competition/listGET获取竞赛列表支持分页和状态筛选竞赛/api/competition/detail/{id}GET获取竞赛详情竞赛/api/admin/competition/savePOST创建或更新竞赛报名/api/signup/applyPOST学生报名报名/api/signup/audit/{id}PUT管理员审核报名作品/api/work/uploadPOST提交作品文件评审/api/judge/reviewPOST评委提交打分成绩/api/score/rank/{competitionId}GET查看竞赛排名竞赛状态流转的设计是这里面的重点。我在Service层里写了一个checkCompetitionStatus的内部方法所有涉及到竞赛状态的写操作都先走这个校验private void checkCompetitionStatus(Competition competition, Integer expectedStatus, String errorMsg) { if (!expectedStatus.equals(competition.getStatus())) { throw new BizException(errorMsg); } }比如创建一个新竞赛时状态初始为0草稿管理员发布后变为1报名中学生报名时校验竞赛状态必须为1报名截止后管理员手动或者通过定时任务将状态推进为2评审中此时学生不能再报名。状态校验放在Service层而不是Controller层不是为了耍帅而是为了让业务规则在多个入口共用时不至于漏判。3.3 关键代码实现滚动发布成绩与剔除最高最低分竞赛评审里面比较常见的一个业务规则是多个评委评分去掉一个最高分和最低分再求平均。这里给出一个实际可用的实现public BigDecimal calculateFinalScore(ListBigDecimal scores) { if (scores null || scores.size() 3) { throw new BizException(评分数量不足无法计算最终成绩); } ListBigDecimal sorted scores.stream() .sorted() .collect(Collectors.toList()); // 去掉一个最高分和一个最低分 BigDecimal sum BigDecimal.ZERO; for (int i 1; i sorted.size() - 1; i) { sum sum.add(sorted.get(i)); } return sum.divide(BigDecimal.valueOf(sorted.size() - 2), 2, RoundingMode.HALF_UP); }这里要注意的是评委人数不足3人时的兜底处理。我在项目中抽了一个ScoreCalculator组件专门处理这类规则而不是把计算逻辑散落在各个Service里后续如果要支持不同竞赛配置不同的计分规则改起来也会方便很多。成绩发布的逻辑也值得说两句。如果管理员直接更新所有作品的成绩字段高并发下容易出现数据不一致。我的做法是先计算好最终成绩列表写入到临时表然后开启事务批量更新正式成绩表更新完成后再把竞赛状态改成“已公示”整个流程通过Transactional保证原子性。成绩公示状态下学生端可以查看到自己的成绩和获奖等级但是不能修改任何数据。4. 常见问题与排查技巧实录4.1 Spring Boot版本升级从2.x到3.5.x的适配清单这个标题下面涉及到的热词里有不少都在讨论Spring Boot版本太高的问题实际也确实如此。我最早用Spring Boot 2.3.4做完这个系统时一切顺利后来为了适配新服务器上预装的JDK 17不得不把项目升级到3.x那一次踩坑踩得记忆深刻。最大的变化是javax.*包全面迁移为jakarta.*包。原来写的所有javax.servlet、javax.annotation、javax.validation相关代码都要跟着改包名这一步没有捷径只能逐个文件排查替换。Spring Security的配置方式也变了。2.x时代常见的WebSecurityConfigurerAdapter继承方式在Spring Security 5.7之后被标记为过期6.x彻底移除必须改用SecurityFilterChain的Bean方式。如果你的项目里还有很多antMatchers写法升级后也要统一改成requestMatchers。MyBatis-Plus也需要升级到对应版本3.5.x搭配Spring Boot 3.x没有问题但老版本里的分页插件和代码生成器部分API有调整具体参考官方升级文档。还有一个隐蔽的坑是Jackson时间序列化。Spring Boot 3.x默认使用Java Time模块不同版本之间对LocalDateTime的格式化方式有细微差异如果接口返回时间出现8小时时差或者格式不对检查一下spring.jackson.date-format和时区配置。如果你是为了运行老项目而保留JDK 1.8和Spring Boot 2.7IDEA新建项目时选不了JDK 1.8这个问题也很常见。解决方法是手动修改pom.xml里的java.version为1.8并在IDEA的Project Structure里把Project SDK和Module SDK统一设置为本地安装的JDK 1.8。实在不行就绕过Spring Initializr直接在Maven里创建普通项目然后手动加spring-boot-starter-parent依赖这样基本能规避掉初始化模板带来的版本限制。4.2 文件上传大小限制不生效的排查流程这是一个被问烂了但也一直在踩的问题。前端上传大文件时提示“413 Request Entity Too Large”很多人第一反应是改Spring Boot的max-file-size结果改了重启还是不生效。我的排查顺序是这样的先确认Spring Boot配置本身是否有问题application.yml里有没有写对缩进key有没有拼错比如把max-file-size写成了maxFileSize。再检查项目里有没有多个配置文件覆盖有application.yml又有application.properties时后面的会覆盖前面的混淆起来很要命。最后确认系统前面有没有Nginx之类的反向代理。Nginx的client_max_body_size默认只有1MB即使Spring Boot放开了100MBNginx这一层还是会把请求拦掉。Nginx侧的修改点主要是这一个位置server { client_max_body_size 100m; }把这个配置放在http块里是全局生效放在server块里则只对当前站点生效建议放到server块精确控制。4.3 报名高峰期的性能问题Redis缓存与异步处理竞赛报名开放那一刻通常会迎来一波集中的流量。我在压测时发现直接查数据库的报名接口在每秒几百并发时会明显变慢响应时间从几十毫秒飙升到几百毫秒。优化的思路主要落在两层。第一层是缓存竞赛信息。报名页面加载时需要竞赛详情这个数据基本不变用Redis缓存起来可以将耗时压到个位数毫秒级别。用Cacheable注解时建议把缓存key设计成competition:detail:{id}的格式过期时间设10到30分钟就好不需要太长。第二层是异步处理报名后的通知和日志。报名成功后需要发送邮件或者站内消息通知学生这一步在同步逻辑里做会拖慢接口响应用Spring的Async注解丢到线程池异步执行即可。注意Async要配合EnableAsync使用且被调用的方法不能和调用方在同一个类里否则代理模式不生效异步纯属白写。4.4 前后端联调中的CORS跨域问题我用Vue写前端、Spring Boot写后端时本地开发必然遇到跨域问题。解决方案很简单在Spring Boot侧写一个全局跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里注意allowCredentials(true)表示允许携带Cookie但前端一般用的是JWT存在localStorage里所以Cookie相关的跨域配置对这个项目不是必须的。如果上了Spring Security还发现跨域请求被拦检查安全过滤器里有没有处理OPTIONS预检请求通常需要在SecurityConfig里放行所有OPTIONS请求。5. 系统部署与运维实践5.1 打包与部署从Jar到Docker的一键启动开发完项目后部署是绕不开的一环。Spring Boot项目打包默认使用Maven插件mvn clean package -DskipTests打包后在target目录下生成一个可执行Jar直接上传到服务器运行nohup java -jar competition-system.jar --spring.profiles.activeprod app.log 21 如果你用的是Docker可以写一个简单的Dockerfile做镜像化部署FROM openjdk:8-jre-alpine COPY competition-system.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]这里用的是JDK 8基础镜像如果你用的是Spring Boot 3.x请把镜像换成openjdk:17-jdk-slim甚至直接用eclipse-temurin:17-jre。数据库密码、Redis地址这些敏感配置建议用环境变量传给容器不要硬编码在镜像里。5.2 日志与监控别等线上出问题了才看日志系统上线后日志的重要性会瞬间体现出来。我在项目里用Logback做了分环境日志配置开发环境控制台输出DEBUG级别生产环境按天滚动写入文件保留30天。Spring Boot 3.x之后官方推荐用spring-boot-starter-actuator暴露健康检查端点配合/actuator/health做云平台的存活探针。这个依赖非常轻量加上之后对运维会有很大帮助。另外建议接入一个简单的接口耗时日志切面把超过500ms的慢请求单独打WARN日志方便后期做性能优化。针对多实例部署的场景一定要把Redis Session共享方案或者JWT方案落实到位否则用户在A实例登录后请求被负载均衡转发到B实例就直接401了。JWT方案天然没有这个问题这也是我在这类系统里优先推荐JWT而不用Session的原因。6. 项目延展与功能增强建议6.1 消息通知站内信与邮件双通道如果时间充裕我建议给系统加上消息通知功能。学生报名成功、作品审核通过、成绩发布这些关键节点都值得主动通知。最简单的实现是做一个消息表保存站内信数据前端在导航栏轮询未读消息数邮件通道可以用JavaMailSender配合线程池异步发送避免同步阻塞主流程。实际开发中我发现站内信的用户体验比邮件好很多高校场景下学生看邮件频率普遍不高。6.2 数据看板让管理系统有一点“智能”的样子管理员端加一个数据看板展示各竞赛的报名趋势、各院系参赛人数、竞赛类型分布等统计信息视觉效果和实用性都会大幅提升。用ECharts展示图表后端提供聚合查询接口实现成本一天以内但拿出去汇报或者答辩时的效果差距十分明显。这个部分很多同学容易忽略觉得是锦上添花但实际上很多评委老师关心的恰恰是这个。6.3 低代码思维用代码生成器减少重复劳动当你需要开发多个类似的管理页面时纯手工写Controller、Service、Mapper是一件相当枯燥的事情。MyBatis-Plus官方提供的代码生成器可以根据数据库表结构一键生成全套代码虽然生成的代码还需要做一些定制修改但基础CRUD部分能省下不少时间。我还试过用一些开源的快速开发平台做二次开发比如基于Spring Boot的若依框架它对竞赛管理这类“权限管理 业务表单”的项目非常合适。我个人在实际项目迭代中发现很多高校的竞赛管理流程并不完全一致做系统时预留出可配置的流程环节比如是否允许重复报名、是否允许修改作品、评委是否能互相看到评分比把每一条规则都写死在代码里要灵活得多。这也是Spring Boot体系下做业务系统最有价值的部分框架给你的是规范和效率而业务上的灵活度才是真正体现开发水平的地方。做竞赛管理系统这类项目的核心收获其实远不只是学会了Spring Boot的注解怎么用、接口怎么写。通过设计角色权限、梳理状态流转、处理并发与缓存你会对“企业级Web应用”这个概念形成真正的体感。如果你正在做或者准备做类似的系统希望这份实战笔记能帮你少走些弯路。后面有时间的话我还会整理一下Spring Boot整合Redis做验证码登录、以及EasyExcel导出竞赛成绩单的详细过程有需要的朋友可以先关注起来。