☰
SpringBoot+Vue校园电竞赛事系统:从设计到答辩的完整复盘
2026/9/28 8:10:01 网站建设 项目流程

校园电竞这几年在学校里越来越火,但真正组织过比赛的人都懂,报名靠接龙、分组靠抽签、赛程靠Excel、比分靠截图,一套流程跑下来能把组织者累到怀疑人生。我做这个基于SpringBoot的校园电竞赛事系统,就是要把赛事组织从"手工台账"变成"线上闭环":学生在线报名、管理员审核组队、系统自动生成赛程、裁判录入比分、战绩实时排名,整个流程不需要一张纸质表。如果你是计算机专业准备毕设的同学,或者正在带毕设的老师想找个既实用又有技术含量的题目参考,这篇复盘应该能给你一些真正能落地的思路。

这个项目我前后从需求梳理到答辩准备花了大概两个月,后端用的SpringBoot 2.7,前端配Vue 2和Element UI,数据库MySQL,权限用JWT加拦截器实现。技术栈不算新潮,但每一块都踩在了"够用、能讲清楚、工作量合理"的平衡点上。下面我就按照实战开发的顺序,把设计思路、核心实现、踩过的坑、答辩时被追问最多的问题完整拆开讲。

1. 项目整体设计与思路拆解

1.1 核心需求:先搞清系统到底为谁服务

动工之前,先别急着写代码。我把这个系统的用户角色画了一遍,最终定下三类:学生用户、赛事管理员、系统管理员(通常由学生会或电竞社干事兼任)。三类人的痛点完全不同:学生要的是"报名方便、赛程清晰、战绩有记录";赛事管理员要的是"能快速发布赛事、审核队伍、安排对阵、录入比分";系统管理员要的是"管好用户、管好赛事状态、能看到整体运行数据"。

所以需求本质上可以提炼成一句话:让一场比赛从发布到颁奖的全流程数字化。明确到功能上就是四个核心闭环:赛事发布与报名、组队与审核、赛程生成与比分管理、数据统计与排行。系统还要承担消息触达的职责,比如报名成功通知、赛程变更提醒。这里我建议不要做太重的消息模块,站内信就够了,否则短信、邮件一接入,毕设工作量直接失控。

1.2 功能模块划分:把系统拆成你能写完的样子

我最终把系统拆成了七个模块,每个模块对应一个独立的业务边界:

  • 用户模块:注册登录、个人信息维护、密码修改、角色区分。
  • 赛事模块:赛事创建、赛事详情、赛事状态流转(报名中、进行中、已结束)。
  • 战队模块:学生创建战队、邀请成员、退出战队、战队信息维护。
  • 报名模块:战队报名赛事、管理员审核报名、取消报名。
  • 赛程模块:小组赛/淘汰赛赛程生成、比赛时间场地安排、比分录入、轮次晋级。
  • 排行榜模块:战队积分排行、选手数据统计、赛事数据看板。
  • 系统管理模块:用户管理、赛事管理、公告发布、基础数据配置。

这样的划分有三点好处:第一,每个模块的Controller层职责单一,答辩时按模块讲非常清晰;第二,前后端联调时接口路径好规划,比如/api/match、/api/team一目了然;第三,后期扩展(比如增加直播链接字段)只需要动一个模块,不会牵一发动全身。

1.3 技术选型:为什么没有全家桶,也没有花里胡哨

技术选型是答辩时老师第一眼会看的点。我没有选微服务、没上Redis集群、也没用消息队列,不是不会,而是这些技术在毕设场景里属于"杀鸡用牛刀",你用了却讲不出业务场景,反而是扣分项。这个系统的定位是:单体应用 + 前后端分离 + 标准化的分层架构。

后端SpringBoot负责业务逻辑和接口,MyBatis Plus负责数据库操作(自带的分页插件能少写很多样板代码),MySQL存业务数据,Redis我只在Session共享和排行榜缓存的地方用到,可选但用上能让系统性能上有话讲。前端Vue 2 + Element UI + Axios,为什么不用Vue 3?因为Element UI对Vue 2的支持最成熟,毕设讲究的是稳定和快速出界面,不是追新。构建工具用Maven而不是Gradle,理由是网上SpringBoot + Maven的资料最多,遇到问题搜解决方案快。

2. 数据库设计:一张表结构图背后的博弈

2.1 核心表结构与字段设计

数据库是这类管理系统最见功底的地方。我设计核心表时遵循一条原则:一个业务闭环对应一组关联表,尽量减少表之间的循环依赖。最终核心表一共8张:用户表、赛事表、战队表、战队成员表、报名表、赛程表、比分表、公告表。

以最关键的几张表为例:

  • 用户表(user):id、username、password(BCrypt加密存储)、nickname、role(0学生 1管理员)、avatar、phone、create_time。这里我建议role字段不搞复杂的权限表,毕设场景用整型字段区分角色足够,也方便答辩解释。
  • 赛事表(match_event):id、event_name、event_type(1单人赛 2团体赛)、game_type(游戏项目)、报名开始/结束时间、比赛开始时间、报名人数上限、当前报名队伍数、状态status(0报名中 1进行中 2已结束)、创建人ID。这个表是所有业务的"主心骨",有一半的接口都围绕它转。
  • 战队表(team):id、team_name、leader_id、team_logo、team_intro、win_count、lose_count、create_time。战队表加冗余的win_count和lose_count是刻意为之,排行榜查询时不需要再现场统计,效率高,答辩时被问到"为什么冗余"也能解释得清楚。
  • 赛程表(match_schedule):id、event_id、round_number(第几轮)、match_index、team_a_id、team_b_id、winner_id、match_time、match_location、status(0待比赛 1已结束)。这张表设计时最关键的是team_a和team_b不能为空,但为了支持轮空场景,我单独设计了is_bye字段,而不是把team_b写死。

2.2 状态机设计:避免业务逻辑乱成一锅粥

赛事系统的核心复杂度在于状态流转。我全部用整型状态值来表示,并在代码里用常量类统一管理,绝不裸写数字。典型的状态流转有两条线:

  • 赛事状态:草稿(0) → 报名中(1) → 已截止(2) → 进行中(3) → 已结束(4)。
  • 报名状态:待审核(0) → 已通过(1) → 已拒绝(2) → 已取消(3)。

这些状态在接口层必须做前置校验。举个例子,用户在赛事报名截止后还能不能报名?不能。管理员在赛事"已结束"状态还能不能录入比分?不能。我一开始没做状态校验,导致测试时出现了"比赛结束了还能改比分"的尴尬,后来在Service层统一加了一个checkEventStatus()方法,每个写操作前先过一遍状态,这个问题才算根治。

另外有一个细节容易被忽略:数据库唯一约束。报名表里我对(event_id, team_id)加了联合唯一索引,防止同一个战队重复报名同一赛事。很多人会漏掉这一层,只靠代码判断,结果并发请求下插入了两条重复记录。数据库约束永远是你防御逻辑的最后一道防线,必须加上。

3. 核心功能模块实现:从报名到晋级全流程

3.1 用户登录与权限控制:JWT的落地姿势

我用的方案是JWT + 拦截器,没有引入Spring Security。为什么?Spring Security虽然是标准方案,但配置复杂、概念多,对于毕设这种角色只有两种的系统,拦截器加JWT更轻量,也更容易在答辩时讲清楚。

具体实现分三步:

  1. 登录接口:用户提交用户名密码,Service层用BCryptPasswordEncoder校验密码,成功后用io.jsonwebtoken生成Token,Token里放userId和role,有效期设24小时,返回给前端。
  2. 拦截器:写一个AuthInterceptor实现HandlerInterceptor,在preHandle里从请求头拿token,解析失败则返回401,成功就把userId放到ThreadLocal或Request attribute里供后续使用。
  3. 角色鉴权:在拦截器基础上加一个角色判断,比如创建赛事、审核报名这类接口要求role为1,如果用户是普通学生直接返回403。

这里有几个坑我必须提一下:第一,JWT的密钥不要写在代码里,放在application.yml配置文件中;第二,拦截器要放行登录、注册、获取赛事列表这些公共接口,我一开始忘记放行,导致前端页面死活调不通接口;第三,Token过期时间不要太长也不要太短,24小时对这个场景足够。我见过有人设置7天,答辩时老师追问"Token泄露了怎么办"直接答不上来。

3.2 报名审核:业务闭环的第一步

报名流程是:学生在赛事详情页点击"报名"→ 后台判断赛事状态是否报名中、战队是否已报名 → 生成一条待审核记录 → 管理员在后台审核通过或拒绝 → 通过后赛事表的当前报名队伍数加1。

这个流程看起来简单,但有一个核心设计决策:单人赛和团体赛的报名逻辑完全不同。单人赛报名的是"个人",团体赛报名的是"战队"。我的解决方案是:

  • 单人赛直接在user级别生成报名记录,不需要战队存在。
  • 团体赛要求用户必须先加入一个战队,以战队为单位报名。

这个区分很重要,如果混在一起设计,后续赛程生成的逻辑会非常痛苦。我最初做的是全部以"队伍"为单位,结果测试单人赛时发现一个学生报完名,赛程表里队伍关联的是战队,算积分时算到了战队头上,完全对不上。后来拆开处理,才把逻辑理顺。

3.3 赛程生成:淘汰赛算法与轮空处理

赛程生成是这个系统最容易被问"含金量"的地方。我的实现支持两种赛制:小组循环赛和单败淘汰赛。淘汰赛的赛程生成逻辑是:

  1. 获取所有已通过审核的参赛队伍(数量假设为n)。
  2. 计算第一轮实际参赛队伍数:找到小于等于n的最大2的幂次方,记作p,如果n刚好等于p,则所有队伍直接参赛;如果n不等于p,则产生n - p个队伍在第一轮轮空。
  3. 生成第一轮对阵表,轮空队伍自动进入下一轮。
  4. 后续轮次按上一轮胜者 + 轮空队伍重新配对,直到决出冠军。

核心代码片段如下:

public List<MatchSchedule> generateKnockoutSchedule(Long eventId, List<Long> teamIds, LocalDateTime startTime) { int n = teamIds.size(); int power = Integer.highestOneBit(n); List<Long> byeTeams = new ArrayList<>(); List<List<Long>> rounds = new ArrayList<>(); List<Long> currentTeams = new ArrayList<>(teamIds); if (n != power) { int byeCount = n - power; byeTeams = currentTeams.subList(0, byeCount); currentTeams = currentTeams.subList(byeCount, currentTeams.size()); } // 第一轮 rounds.add(buildFirstRound(currentTeams, byeTeams, startTime)); // 后续轮次按胜者推进 return buildSubsequentRounds(rounds); }

这里的轮空处理是最容易出错的地方。我的经验是把轮空队伍和正常比赛分开处理:轮空队伍不生成对阵记录,而是直接标记为晋级,进入下一轮待配对列表。如果你强行给轮空队伍分配一个空的对手,后期胜者计算时会出现大量空指针异常。另外,对阵表的洗牌要在生成时用Collections.shuffle()打乱顺序,否则每次都是第一队打第二队,体验非常差。

3.4 比分录入与胜者判定:事务和数据一致性

比分录入的场景是:裁判(管理员)在赛程详情页录入A队和B队的比分,系统根据比分自动判定胜负,把胜者写入winner_id字段,同时更新赛程状态为"已结束",然后自动把胜者推送到下一轮的待参赛列表。

这里最容易出问题的是事务边界。更新赛程状态、更新战队胜负场次、生成下一轮对阵,这三个操作必须处于同一个事务中。我一开始没注意,写完发现比赛结束但是下一轮赛程没有生成,排查半天发现是先更新了状态、再生成赛程,中间接口报错导致数据不一致。后来在Service方法上加了@Transactional,问题解决。

还有一个细节:比分校验。FPS类游戏比分是单局数值,MOBA类可能是BO3、BO5的多局累计。为了通用性,我设计了score_a和score_b两个字段,录入时不校验具体数值大小,只校验两者不相等——因为如果相等就是平局,淘汰赛不能出现平局。答辩时老师问"如果比赛真的平了怎么办",我的方案是支持"加赛"配置,即管理员可以把平局的两支队伍重新生成一场附加赛,这个功能在赛程表加一个is_rematch字段就能实现。

4. 前端设计与前后端联调要点

4.1 页面清单与路由设计

前端我用Vue 2和Element UI,页面一共10个左右。核心页面包括:登录注册页、赛事列表页、赛事详情页、战队管理页、赛程对阵页、比赛数据看板、后台管理页(赛事审核、队伍审核、用户管理、公告管理)。路由设计上我采用了嵌套路由,后台管理单独挂在/admin下面,通过路由守卫校验角色,非管理员访问直接跳回首页。

这里有一个经验:路由守卫只在页面跳转时拦截,接口层面的鉴权必须靠后端拦截器。前端的路由守卫是用户体验上的优化,不是安全屏障。如果你只做了前端隐藏按钮而忽略了后端鉴权,答辩时被老师用Postman直接调接口打穿系统,那场面相当尴尬。

4.2 核心交互:赛程对阵图怎么画

对阵图是前端交互细节最高的地方。Element UI没有现成的组件,我是用Card组件嵌套实现的:每一轮用一个纵向排列的卡片组,轮与轮之间横向排列。每张卡片里显示两个队的名称和比分,胜者加粗显示,轮空队伍显示"轮空"标识。

实现上有两个坑:第一,后端返回赛程数据时要按round_number分组返回,前端不要自己筛数据;第二,对阵图的连线效果不要用SVG硬画,太复杂,直接用Element UI的el-steps组件模拟轮次递进,效果不错而且代码量少。如果你想做得更炫酷,可以用ECharts的树图来展示淘汰赛过程,但考虑工作量,我建议先把基本版做出来,学有余力再上ECharts。

4.3 前后端联调:Axios封装与错误处理

联调阶段最容易耗时间的是接口格式不一致。我在项目里统一定义了返回格式:

{ "code": 200, "message": "success", "data": {} }

Axios放在独立的request.js中统一封装,拦截器里做三件事:请求头自动带Token、响应code非200时全局弹出错误提示、401时自动跳转登录页。这个是所有毕设前端都应该有的基础设施,能省掉后面大量的重复代码。

我还建议在联调时启用前端代理而不是开启后端跨域。具体在Vue的vue.config.js里配置devServer.proxy,把/api前缀代理到localhost:8080。这样既避免了跨域问题,又不需要在后端写@CrossOrigin。等到部署时再用Nginx统一处理代理,前后端各司其职。

5. 部署上线与答辩准备:从能跑到能讲

5.1 本地运行与打包部署

毕业设计最尴尬的场景是答辩现场演示时项目起不来。本地开发时我用IDEA直接启动SpringBoot,前端用npm run dev跑在8081端口。但现场答辩建议提前打包成可执行文件:后端用Maven的package命令打成Jar包,前端用npm run build生成dist目录,然后放到Nginx的html目录下,配置反向代理把/api转发到后端的8080端口。

我踩过的一个大坑是前端打包后路由刷新404。这是Vue的history模式引起的,Nginx需要配置try_files:

location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }

如果不加这个配置,现场演示时按F5刷新页面直接白屏,特别影响观感。

5.2 答辩演示时的高频问题清单

答辩老师们最常问的问题其实来来去去就那么几个,提前准备好就能从容应对:

  • 为什么选SpringBoot不用SSM?重点回答自动配置和快速开发,强调这是一套"更现代的Java Web解决方案"。
  • MyBatis Plus和MyBatis的区别?重点说BaseMapper内置了单表CRUD方法,分页插件可以免手写Limit语句。
  • 并发报名如何处理?回答数据库唯一约束兜底 + Service层同步锁 + 必要时Redis分布式锁,层层递进。
  • 密码安全性怎么保证?回答BCrypt加密存储,登录时用加密算法校验,不能明文存储。
  • 如果比赛队伍数到几百个,系统性能瓶颈在哪?回答数据库索引优化、排行榜加Redis缓存、静态资源走CDN。

每次答辩都有同学在"并发"这个问题上卡壳。我的建议是:哪怕你的系统没有真正的并发场景,也要把设计思路讲出来,因为老师想考察的是你有没有主动考虑过这个问题,而不是你的系统真的能扛住十万并发。

6. 常见问题与排查技巧实录

6.1 开发期高频踩坑记录

这个项目从零写到能演示,我记录了不少问题,整理成了一份速查表:

现象根因解决方案
前端请求接口404后端Controller路径或拦截器未放行检查@RequestMapping路径,确认拦截器exclude列表
登录后接口全部401Token过期或请求头未携带检查Axios拦截器是否配置了Authorization头
数据库中文乱码连接参数缺少编码配置url增加characterEncoding=utf8并设置serverTimezone
报名成功但列表查不到分页参数写错页码从1开始PageHelper和MyBatis Plus的current从1开始
日期前后相差8小时时区问题,MySQL和JVM时区不一致url配置serverTimezone=Asia/Shanghai
打包后前端白屏静态资源路径错误vue.config.js设置publicPath: './'

其中日期8小时的问题我印象特别深,当时数据库存的时间明明是下午三点,查到前端显示凌晨三点,排查了好久才发现是JVM默认时区用了UTC,MySQL连接串加serverTimezone之后瞬间解决。

6.2 数据库设计与查询性能优化心得

毕设评审老师也喜欢问查询性能。这个系统的核心查询是赛事列表和排行榜。我的优化思路是三层:

第一层,SQL层面。赛事列表查询只查需要展示的字段,不用SELECT *,该用索引的字段(如status、event_name)全部加上普通索引。报名表、赛程表经常按event_id查询,也要建索引。

第二层,缓存层面。排行榜数据是高频读、低频写的典型场景,我加了一层Redis缓存,管理员录入比分后主动删除排行榜缓存,下次查询重新加载,实现简单且效果立竿见影。

第三层,代码层面。避免在循环中查数据库,比如查询赛程列表时需要带出队伍信息,我一次性查出所有队伍ID再in查询,不在大循环里逐条selectById。

6.3 项目演示前的最终检查清单

最后分享一个我每次做项目演示前都会过的检查清单,能帮你少出糗:

  • 数据库服务是否启动,账号密码是否可用。
  • 后端接口是否都能连通,先用Swagger或Postman跑一遍主要接口。
  • 前端是否已构建最新代码,路由刷新是否正常。
  • 是否准备好了2-3个测试账号,分别覆盖学生和管理员角色。
  • 核心演示数据是否提前造好,比如有正在报名的赛事、已生成的赛程、有比分记录的比赛。
  • 是否预演过一遍完整业务流:注册/登录 → 创建战队 → 报名比赛 → 管理员审核 → 生成赛程 → 录比分 → 看排行榜。

最后再分享一点个人心得

做完这套系统,我心里最大的感受是:毕设项目不在于技术堆得有多高,而在于逻辑闭环和细节完整。评审老师看重的不是一个"花里胡哨但跑不通"的东西,而是一个"朴实无华但每个按钮都能点通、每个状态都有交代"的系统。做一个校园电竞赛事系统,技术点本身没什么神秘,但当你把报名状态、赛程轮次、比分胜负、排行榜积分这一条主线彻底跑通,把踩过的坑一个个补齐,答辩的时候你自然有底气讲清楚每一个设计决策背后的取舍——而这种"能讲清楚"的能力,恰恰是毕业设计真正要训练的东西。如果你正在做类似的管理系统,希望这篇复盘能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询