ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot的竞赛管理系统开发实战:从需求到部署

基于SpringBoot的竞赛管理系统开发实战:从需求到部署 如果你点进这篇文章大概率是在准备做一个大学生科技竞赛管理系统或者在找JavaSpringBootSSM方向的项目参考。这个标题我太熟了很多同学拿到题目后的第一反应是“这不就是个后台管理CRUD吗”但实际动手就会发现竞赛管理系统比普通管理系统要复杂不少难点集中在几个地方赛事状态怎么流转、多人组队报名时如何保证数据一致、多评委打分怎么统计排名、不同角色的权限边界如何控制。这篇文章我就围绕这套系统把从需求拆解到数据库设计再到核心模块代码实现和项目部署交付的完整过程整理出来全部是一个可复现项目的真实经验。我默认你已经有Java基础和基本的SpringBoot使用经验但即使你刚学完Spring这篇文章里涉及的知识点也足够你照着一路写下来。我讲的每一个坑都是实际开发中真真切切遇到的不是从教程里抄来的。1. 项目到底在做什么需求拆解与整体设计1.1 业务场景与功能全景大学生科技竞赛管理系统核心不是“展示赛事”而是把赛事从发布到结束的全过程搬到线上。实际校园场景里一个竞赛的完整链路大概是这样的教务处或学院发通知学生组织队伍报名队伍提交作品老师担任评委打分最后管理员公示成绩和获奖名单。传统做法是微信通知群加Excel表格加邮件收作品信息零散、容易出错、统计也慢。系统的价值就是把这些动作全部集中到一个平台里每个角色只看到和自己相关的内容。按角色来拆功能大致是管理员用户管理、赛事创建与发布、赛事审核、公告管理、数据统计学生浏览赛事、在线报名、创建或加入队伍、上传作品、查看成绩评委/教师查看分配给我的评审任务、对作品在线打分、填写评语只看这一层还不够。我建议你按“赛事生命周期”来重新组织需求这对后面的表结构和代码设计帮助非常大。一个赛事从无到有会经历以下阶段赛事创建管理员录入赛事名称、主题、报名时间、作品截止时间、参赛限制赛事发布审核通过后状态变为报名中学生端可见报名组队学生选择赛事创建队伍或加入已有队伍队长提交报名作品提交报名审核通过后队伍在截止时间前上传作品评审打分管理员分配评委评委在线打分并给出评语结果公示系统根据评分规则计算成绩管理员设置公示状态生命周期是数据库状态机的设计依据也是答辩时最能讲出东西的部分。很多同学的项目做完只能说“登录、增删改查”但你如果能把这个生命周期讲清楚整个项目的档次一下就上来了。1.2 为什么选择SpringBootSSM这套技术组合项目标题里的技术栈是JavaSpringBootSSM。有人会问SSM不是指SpringSpringMVCMyBatis吗SpringBoot已经是整合框架了为什么还要强调SSM这里要解释清楚也是答辩时容易被问到的问题。这套组合的实际含义是SpringBoot负责自动装配和快速启动SpringMVC负责Web层接口处理MyBatis负责持久层SQL操作。也就是说项目整体跑在SpringBoot容器里但内部的分层架构仍然沿用SpringMVCMyBatis的经典模式。这种组合在高校课程和中小型项目里非常常见你后面接手的人也好理解。选型理由我再展开说说学习成本低。SSM本身是计算机专业课程体系里的主流内容源码给学弟学妹维护或者你自己过几个月再回来看都不会有陌生感。SQL可控性强。竞赛系统里大量涉及多表查询和聚合统计比如按赛事统计报名人数、按评委分组算平均分、去掉最高分最低分这种排名计算。用MyBatis写SQL非常直观如果用JPA的Criteria查询复杂报表会让你头疼到怀疑人生。生态成熟。无论你用的是IDEA、Maven、MySQL还是最后部署到服务器这套技术栈的资料和技术帖子都多到看不完遇到问题容易排查。我印象很深的是有一次带队伍做比赛管理系统队友一开始强烈推荐JPA理由是实体类写得爽、自动建表方便。结果做到评审排名那一步要按赛事分组统计、去掉最高最低、算平均分还要处理并列条件JPA的查找方案写出来又长又难调。最后我们还是把持久层换回了MyBatis。这不是说JPA不好而是团队要选择自己最可控的技术。提示毕设项目选型求稳优先。你如果非要尝试WebFlux、JPA或其它非主流组合不是不行而是答辩时必须额外解释技术差异这部分工作量其实是没必要的。1.3 分层架构与项目包结构项目不要所有代码堆在一个包下分层清晰是代码质量的第一道底线。按SSM的惯例工程建议拆成下面这些包src/main/java └── com.example.contest ├── config // 配置类拦截器、跨域、文件上传映射等 ├── controller // 接口层只做参数接收和统一返回 ├── service // 业务层事务和核心逻辑都在这层 ├── dao // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互所需的数据对象 ├── vo // 视图对象/统计结果对象 └── common // 常量、统一返回、异常类、工具类分包的核心原则就是controller层只做参数接收和校验service层负责业务逻辑和事务管理dao层只做数据库访问。很多同学图省事把SQL和业务判断全写在controller里开发前期确实很爽但后面一旦要加权限、加日志、加多角色判断就会变成一团乱麻你自己都不敢去改。2. 数据库设计与核心表结构决定项目上限2.1 核心数据表与字段设计数据库设计是整个系统的地基。我见过太多项目做到一半推倒重来几乎都是因为表结构没想清楚。竞赛管理系统核心表不算多但每张表都有它需要注意的设计细节。核心表可以分成下面几张用户表 user主键、用户名、密码、姓名、学号/工号、角色类型学生/教师/管理员、院系、电话、创建时间赛事表 competition主键、赛事名称、简介、赛事类型挑战杯/互联网/专业赛、报名开始时间、报名截止时间、作品截止时间、状态、队伍人数上限、创建人团队表 team主键、赛事ID、队名、队长ID、创建时间团队成员表 team_member主键、团队ID、成员ID、加入时间、成员角色报名表 registration主键、团队ID、赛事ID、提交时间、审核状态、审核意见作品表 work主键、团队ID、赛事ID、作品名称、简介、文件路径、状态评审表 review主键、作品ID、评委ID、分数、评语、评审时间公告表 notice主键、标题、内容、发布人、发布时间这里有四个设计错误的坑要专门提醒角色字段不要用字符串。很多表里会出现“管理员”“admin”“ADMIN”三种写法联调时一查一个准。建议用数字0/1/2然后在Java常量类里统一管理。状态字段必须加注释。比如赛事表status0代表草稿、1代表报名中、2代表评审中、3代表已结束。加了注释前后端联调时沟通成本会低很多。不要乱加物理外键。表之间用逻辑关联字段存ID就够了别真去加FOREIGN KEY约束。竞赛系统里删除和更新场景很多物理外键经常导致删不动、更新报错。每张表都要有create_time和update_time条件允许就加逻辑删除字段deleted。逻辑删除在数据统计时很有用哪怕业务上不要求写代码时留着也能防止误删数据没法恢复。2.2 示例DDL一张关键表的建表思路这里给出竞赛系统最重要的一张表——赛事表的MySQL建表DDL后续表结构可以参考这个思路去写CREATE TABLE competition ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 赛事名称, description text COMMENT 赛事介绍, type varchar(50) DEFAULT NULL COMMENT 赛事类型挑战杯/互联网/专业赛等, signup_start_time datetime DEFAULT NULL COMMENT 报名开始时间, signup_end_time datetime DEFAULT NULL COMMENT 报名截止时间, work_deadline datetime DEFAULT NULL COMMENT 作品提交截止时间, max_member int(11) DEFAULT 5 COMMENT 队伍人数上限, status tinyint(4) DEFAULT 0 COMMENT 状态0草稿 1报名中 2评审中 3已结束, create_by bigint(20) DEFAULT NULL COMMENT 创建人ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(4) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT赛事表;字段选择上有一个经验可以分享类型type为什么用varchar而不用int因为赛事类型后续很可能要扩展名称用int你还得维护一张字典表对毕设来说是过度设计。而状态status为什么用tinyint而不是varchar因为状态是定死的枚举用数字在代码里比较高效也不怕大小写问题。这种选择没有绝对的对错关键是全项目的字段规范要统一。2.3 赛事状态机与时间校验逻辑有了表结构接下来是业务层的核心状态机。竞赛系统的所有操作都应该围绕状态流转而不是“想在哪操作就在哪操作”。定义清楚状态机0草稿管理员创建后未发布学生不可见1报名中赛事已发布学生可报名组队但还不可提交作品2评审中报名截止、作品提交截止评委开始打分3已结束评审完成结果公示系统进入只读阶段。写业务接口时必须先判断状态报名接口只能在status1时执行提交作品接口要保证当前时间早于work_deadline评审接口只能在status2时调用。为什么状态和截止时间都要判断因为状态是管理员手动改的可能存在时间差而截止时间是硬性边界规则上用时间判断永不失效。我见过一个经典bug某同学在赛事状态从“报名中”切到“评审中”后还能通过直接改前端URL调后端提交作品接口。原因是代码只做了前端按钮隐藏完全没有在service层校验赛事状态。这是一个典型的“后端信任前端”的错误。所有重要操作都必须在后端接口里做二次校验身份校验、状态校验、时间校验一个都不能少。在并发场景下还要考虑两个学生同时报名同一个赛事都通过了“未报名”校验结果各建了一支队伍、各提交了一条报名记录。解决办法可以是给team表的competition_id和leader_id加唯一索引或者对赛事状态字段做乐观锁控制。毕设做到唯一索引这一层基本就够用了但如果答辩时能说出索引兜底和乐观锁、悲观锁的区别这一个点就值得写进论文里。3. 核心功能模块的代码实现细节3.1 登录认证与角色权限控制系统里有三类角色权限控制如果做不好学生就能改别人的作品、评委就能看别人的打分、普通用户能进管理后台。这个项目的权限方案我不建议直接上Spring Security虽然它是标准方案但配置复杂度高对毕设来说可解释性差。我推荐用“登录Token 拦截器 自定义注解”的轻量方案关键在于你能把原理讲清楚。流程是这样的用户登录成功后用一个UUID生成token以token为keyuserId为value存入Redis并设置过期时间前端把token放请求头后端写一个全局拦截器统一校验token是否存在、是否过期再根据接口上的角色注解判断当前用户是否有权限。拦截器核心代码类似这样Component public class AuthInterceptor implements HandlerInterceptor { Autowired private RedisTemplateString, Object redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { throw new BusinessException(401, 未登录); } Object userId redisTemplate.opsForValue().get(login_token_ token); if (userId null) { throw new BusinessException(401, 登录已过期); } // 校验角色 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { User user userService.getById((Long) userId); if (!Arrays.asList(requireRole.value()).contains(user.getRole())) { throw new BusinessException(403, 无权限访问); } } request.setAttribute(currentUserId, userId); } return true; } }注意一个小细节如果Redis里存的token过期时间是30分钟用户连续操作到第29分钟时token刚过期下一秒请求就会401体验很糟糕。正确做法是在拦截器里统一做“续期”每次请求都会刷新剩余时间用户只要在用就不会莫名掉线。密码存储必须用BCrypt加密不要用MD5。MD5虽然能跑但答辩时被问“密码安全怎么做”就非常尴尬。SpringBoot自带spring-security-crypto单独引入后可以直接使用BCryptPasswordEncoder加盐逻辑都是封装好的调用简单。3.2 赛事报名与组队流程的事务处理报名是竞赛系统里写操作最密集的模块。一个学生报名参加团队赛后端要同时做三件事创建队伍、把当前用户插进成员表、写入报名记录。任何一个步骤失败其他步骤都要回滚否则会出现“有队伍没队长”或者“报名记录残缺”的脏数据。解决办法很简单在service方法上标注Transactional。但这里有个很多人不知道的坑Transactional默认只在RuntimeException时回滚如果你自己在业务里throw new Exception(xx)事务不会回滚。所以项目里一定要自定义一个BusinessException继承RuntimeException所有业务异常都抛它这样才能保证事务及时回滚。核心报名逻辑示例Transactional(rollbackFor Exception.class) public void signUp(Long competitionId, Long userId, String teamName) { // 1. 校验赛事状态 Competition competition competitionMapper.selectById(competitionId); if (competition null || competition.getStatus() ! 1) { throw new BusinessException(当前赛事不在报名时间内); } // 2. 防止重复报名数据库唯一索引兜底 if (registrationMapper.existsByUserAndCompetition(userId, competitionId)) { throw new BusinessException(请勿重复报名); } // 3. 创建团队 Team team new Team(); team.setCompetitionId(competitionId); team.setName(teamName); team.setLeaderId(userId); teamMapper.insert(team); // 4. 队长也是团队成员 TeamMember member new TeamMember(); member.setTeamId(team.getId()); member.setUserId(userId); teamMemberMapper.insert(member); // 5. 写入报名记录 Registration registration new Registration(); registration.setTeamId(team.getId()); registration.setCompetitionId(competitionId); registration.setStatus(0); registrationMapper.insert(registration); }并发情况下两个请求同时走到第2步数据库层面并没有阻止它们都会创建队伍并生成报名记录。杜绝这个问题最简单的方式是给team表加唯一索引比如competition_id和leader_id的联合唯一索引如果你的系统还要求一个赛事有队伍数量上限那就需要更细的并发控制比如对赛事行加乐观锁版本号更新时校验版本号。3.3 文件上传与作品提交作品提交这块最麻烦的是文件上传。学生上传的可能是一个压缩包、PDF或视频文件大小从几百KB到上百MB都有可能。如果你的系统前后端分离部署还会有跨域限制问题。建议做一个独立的上传接口统一接收multipart文件校验后保存到本地磁盘或对象存储。如果是毕设存本地磁盘完全够用。文件名不要用用户原始文件名容易中文乱码重名建议用UUID重命名再把原始文件名记进数据库。SpringBoot默认单文件上传上限是1MB必须重新配置spring: servlet: multipart: max-file-size: 200MB max-request-size: 210MB文件类型不能只看后缀名最好用MIME类型做白名单至少后端要拦掉exe、sh这类可执行文件。保存目录也不要放到项目源码目录里否则打包后运行会找不到路径。我习惯在配置类里做静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadDir /); } }文件上传还有一个隐蔽的问题如果文件先落盘、后写数据库数据库写失败磁盘上就留下一个孤儿文件。规范做法是在service里try-catch数据库写失败时立刻删除已保存文件或者用定时任务定期清理未关联的文件。对于毕设前者实现简单也不容易出问题。3.4 多评委打分与成绩统计评审打分是系统里另一个业务难点。一个作品通常由多个评委共同评分最终成绩不是简单平均而是有规则的。常见规则是去掉一个最高分和一个最低分再取平均分。不同赛事还可能有评委数量要求、权重分配差异。评审表设计上每个评委对同一个作品只允许有一条记录用(work_id, judge_id)做唯一索引防止同一评委重复打分。统计最终成绩时我用下面这条SQLSELECT work_id, CASE WHEN COUNT(*) 2 THEN AVG(score) ELSE (SUM(score) - MAX(score) - MIN(score)) / (COUNT(*) - 2) END AS final_score FROM review WHERE deleted 0 GROUP BY work_id;注意如果作品只有一位评委打分去掉最高最低后分母会变成0SQL直接报错。所以统计前必须查一次评审数少于最低评委数的作品要标记为“待评审”不参与排名。排名展示还有一个让人摸不清的坑当两个作品最终得分相同页面顺序会来回跳动。究其原因是没有定义并列时的排序规则。系统里必须补上二级排序条件比如作品提交时间更早的优先、作品编号更小的优先否则用户刷新两次结果看到排名变了会质疑系统的正确性。4. 开发与联调中最容易踩的坑4.1 SpringBoot版本与依赖版本匹配SpringBoot版本不要太激进。现在最新的3.x版本要求JDK17及以上而很多高校毕设环境还是JDK8团队其他人用的工具链也未必跟上。我强烈建议就用SpringBoot 2.7.x JDK8 MyBatis MySQL 5.7/8.0这套组合网上资料多问题少稳定。版本选择不当最典型的表现是引入mybatis-spring-boot-starter后启动直接报ClassNotFoundExceptionDruid连接池在JDK17下反射异常javax.servlet.* 变成 jakarta.servlet.*网上教程里的代码复制过来编译不过。一句话能用稳定版就不用最新版。技术选型的首要任务不是追新而是降低风险。4.2 MyBatis XML与Mapper扫描问题MyBatis实现SQL有两种方式注解和XML文件。我的建议是全部统一用XML复杂动态SQL在注解里写会让代码膨胀到没法看。最频繁出现的报错是“Invalid bound statement (not found)”。碰见这个先按顺序排查以下四个点application.yml里mybatis.mapper-locations是否配置为classpath:mapper/*.xmlXML文件是否真的放在了src/main/resources/mapper目录下XML文件的namespace是不是和Mapper接口全限定名一致XML里每条语句的id是否和接口方法名一致。这个报错在SSM开发里的出现率真的可以排第一你只要遇到就可以直接按这个顺序过一遍基本都能解决。4.3 事务不生效与自调用问题Transactional不生效最常见原因是同一个Service类里内部方法互相调用。Spring事务基于AOP代理实现内部调用走的是this.method()不走代理注解自然失效。解决方式有两种把需要事务管理的方法放到另一个Service类中由外部类调用简单粗暴但有效自己类里注入自己的代理不推荐但能解决问题。还有一个隐蔽的场景在事务方法里catch住异常后返回false事务感知不到任何异常没有回滚。记住事务方法里能不在catch中吞异常就尽量不吞如果要catch必须手动抛出RuntimeException。4.4 跨域配置与时间序列化前端用Vue开发、后端跑8080端口的话势必遇到跨域问题。SpringBoot里的核心配置是先允许跨域请求再设置允许携带凭证Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }设置allowCredentials(true)之后allowedOrigin不能直接写“*”只能用allowedOriginPattern否则前端会直接报错。这个细节踩坑率极高。另一个容易被忽略的是时间字段的序列化问题。后端的LocalDateTime返回JSON后可能是“2024-07-15T10:30:00”这种ISO格式前端显示不好看。统一在配置里处理spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果你实体用的是LocalDateTime而不是java.util.Date上面的date-format不生效需要额外加Jackson序列化配置或者干脆统一用Date类型。联调阶段做好这步能省很多沟通时间。4.5 常见问题速查表我把开发中常遇到的问题整理成了一个速查表建议收藏现象可能原因解决方案登录后请求接口报401token过期或Redis中无记录检查拦截器续期逻辑、Redis连接中文文件名上传后乱码未对原始文件名做处理用UUID重命名保存原始名存数据库接口响应慢、日志打印SQL查询未走索引explain查看执行计划状态字段加索引工程启动后静态资源404静态资源映射配置错误检查addResourceHandlers配置前后端联调一直CORS报错允许凭证后仍使用通配符域名改用allowedOriginPattern打包后页面显示Whitelabel Error前端打包产物未放对目录将dist目录内容复制到resources/static5. 打包部署与项目交付的完整经验5.1 Maven打包与生产环境部署开发完毕后用Maven打包成jar包是SpringBoot单应用最标准的部署方式。IDEA里先执行clean再执行package就能在target目录下拿到一个可执行的jar包。生产环境部署时我用的是下面这条命令nohup java -jar contest-system.jar --spring.profiles.activeprod app.log 21 两个建议配置必须分环境。开发环境application-dev.yml与生产环境application-prod.yml分开数据库地址、密码、Redis连接不要混在一起防止误连测试库。如果前端用了Nginx反向代理大文件上传除了Spring的multipart限制还要修改Nginx的client_max_body_size这一项经常漏。数据库初始化用SQL脚本一次性执行不要依赖MyBatis自动建表。自动建表虽然省事但字段注释、索引、唯一约束完全不受控后期调整表结构非常痛苦。SQL脚本跟着代码走这才是可复现的项目交付方式。5.2 源码LW调试文档讲解的交付物搭配如果这个项目是毕业设计或类似的课程设计交付物绝不只是“能跑的代码”。一个完整的交付物应该由四部分组成源码代码要有必要的注释能独立跑起来关键模块不要写得太散LW论文/设计报告至少包含系统设计、表结构设计、核心流程图、界面截图、测试结果五章。论文不是越厚越好而是要能讲清楚“为什么这么设计”调试文档环境要求、启动步骤、端口配置、默认账号权限、常见错误处理。这份文档其实是写给你自己的答辩现场环境出问题时能快速恢复讲解准备能对着页面把核心表结构、状态流程、一个核心接口的代码逻辑讲明白。提前准备一套演示数据包含几个不同状态的赛事、若干支队伍、已有的评审分数避免现场临时造数据。我在交付前会坚持做一次“全流程实测”用一个新账号把赛事发布、报名、组队、作品提交、评审、结果公示完整走一遍每一步都截图留存测试数据。这件事做一遍等到写论文或者答辩时你就能感到奇迹般的轻松。5.3 项目目录结构与文档组织建议为了让接手的人评审老师、学弟学妹快速理解项目源码里建议保留一个README并把目录结构规范化contest-system ├── src/main/java │ └── com/example/contest │ ├── config │ ├── controller │ ├── service │ ├── dao │ ├── entity │ ├── dto │ └── common ├── src/main/resources │ ├── mapper // MyBatis XML │ ├── static // 前端打包文件 │ └── application.yml ├── sql │ └── contest.sql // 数据库初始化脚本 └── README.md为什么单独把sql独立出来放在一个目录因为很多同学建表习惯直接在Navicat里手工创建最后交代码时连初始化脚本都没有换一台电脑就很难快速还原整个环境。这个习惯一定要改所有DDL都写进SQL文件跟着代码一起走项目才谈得上“可交付”。带过不少同学做这类竞赛管理系统我发现大部分人的问题不在技术难度而在需求理解不透就开始写代码最后东一改西一改越改越乱。你如果能先把角色、状态、流程这三件事想清楚工程结构按照分层的思路搭好再动手去写具体功能整套开发节奏就会顺畅很多。系统本身不复杂但把细节处理好它就是一个能拿得出手的完整项目。
返回列表