尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue校园电竞赛事系统:从设计到答辩的完整复盘

SpringBoot+Vue校园电竞赛事系统:从设计到答辩的完整复盘 校园电竞这几年在学校里越来越火但真正组织过比赛的人都懂报名靠接龙、分组靠抽签、赛程靠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张用户表、赛事表、战队表、战队成员表、报名表、赛程表、比分表、公告表。以最关键的几张表为例用户表userid、username、passwordBCrypt加密存储、nickname、role0学生 1管理员、avatar、phone、create_time。这里我建议role字段不搞复杂的权限表毕设场景用整型字段区分角色足够也方便答辩解释。赛事表match_eventid、event_name、event_type1单人赛 2团体赛、game_type游戏项目、报名开始/结束时间、比赛开始时间、报名人数上限、当前报名队伍数、状态status0报名中 1进行中 2已结束、创建人ID。这个表是所有业务的主心骨有一半的接口都围绕它转。战队表teamid、team_name、leader_id、team_logo、team_intro、win_count、lose_count、create_time。战队表加冗余的win_count和lose_count是刻意为之排行榜查询时不需要再现场统计效率高答辩时被问到为什么冗余也能解释得清楚。赛程表match_scheduleid、event_id、round_number第几轮、match_index、team_a_id、team_b_id、winner_id、match_time、match_location、status0待比赛 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更轻量也更容易在答辩时讲清楚。具体实现分三步登录接口用户提交用户名密码Service层用BCryptPasswordEncoder校验密码成功后用io.jsonwebtoken生成TokenToken里放userId和role有效期设24小时返回给前端。拦截器写一个AuthInterceptor实现HandlerInterceptor在preHandle里从请求头拿token解析失败则返回401成功就把userId放到ThreadLocal或Request attribute里供后续使用。角色鉴权在拦截器基础上加一个角色判断比如创建赛事、审核报名这类接口要求role为1如果用户是普通学生直接返回403。这里有几个坑我必须提一下第一JWT的密钥不要写在代码里放在application.yml配置文件中第二拦截器要放行登录、注册、获取赛事列表这些公共接口我一开始忘记放行导致前端页面死活调不通接口第三Token过期时间不要太长也不要太短24小时对这个场景足够。我见过有人设置7天答辩时老师追问Token泄露了怎么办直接答不上来。3.2 报名审核业务闭环的第一步报名流程是学生在赛事详情页点击报名→ 后台判断赛事状态是否报名中、战队是否已报名 → 生成一条待审核记录 → 管理员在后台审核通过或拒绝 → 通过后赛事表的当前报名队伍数加1。这个流程看起来简单但有一个核心设计决策单人赛和团体赛的报名逻辑完全不同。单人赛报名的是个人团体赛报名的是战队。我的解决方案是单人赛直接在user级别生成报名记录不需要战队存在。团体赛要求用户必须先加入一个战队以战队为单位报名。这个区分很重要如果混在一起设计后续赛程生成的逻辑会非常痛苦。我最初做的是全部以队伍为单位结果测试单人赛时发现一个学生报完名赛程表里队伍关联的是战队算积分时算到了战队头上完全对不上。后来拆开处理才把逻辑理顺。3.3 赛程生成淘汰赛算法与轮空处理赛程生成是这个系统最容易被问含金量的地方。我的实现支持两种赛制小组循环赛和单败淘汰赛。淘汰赛的赛程生成逻辑是获取所有已通过审核的参赛队伍数量假设为n。计算第一轮实际参赛队伍数找到小于等于n的最大2的幂次方记作p如果n刚好等于p则所有队伍直接参赛如果n不等于p则产生n - p个队伍在第一轮轮空。生成第一轮对阵表轮空队伍自动进入下一轮。后续轮次按上一轮胜者 轮空队伍重新配对直到决出冠军。核心代码片段如下public ListMatchSchedule generateKnockoutSchedule(Long eventId, ListLong teamIds, LocalDateTime startTime) { int n teamIds.size(); int power Integer.highestOneBit(n); ListLong byeTeams new ArrayList(); ListListLong rounds new ArrayList(); ListLong 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_fileslocation / { 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增加characterEncodingutf8并设置serverTimezone报名成功但列表查不到分页参数写错页码从1开始PageHelper和MyBatis Plus的current从1开始日期前后相差8小时时区问题MySQL和JVM时区不一致url配置serverTimezoneAsia/Shanghai打包后前端白屏静态资源路径错误vue.config.js设置publicPath: ./其中日期8小时的问题我印象特别深当时数据库存的时间明明是下午三点查到前端显示凌晨三点排查了好久才发现是JVM默认时区用了UTCMySQL连接串加serverTimezone之后瞬间解决。6.2 数据库设计与查询性能优化心得毕设评审老师也喜欢问查询性能。这个系统的核心查询是赛事列表和排行榜。我的优化思路是三层第一层SQL层面。赛事列表查询只查需要展示的字段不用SELECT *该用索引的字段如status、event_name全部加上普通索引。报名表、赛程表经常按event_id查询也要建索引。第二层缓存层面。排行榜数据是高频读、低频写的典型场景我加了一层Redis缓存管理员录入比分后主动删除排行榜缓存下次查询重新加载实现简单且效果立竿见影。第三层代码层面。避免在循环中查数据库比如查询赛程列表时需要带出队伍信息我一次性查出所有队伍ID再in查询不在大循环里逐条selectById。6.3 项目演示前的最终检查清单最后分享一个我每次做项目演示前都会过的检查清单能帮你少出糗数据库服务是否启动账号密码是否可用。后端接口是否都能连通先用Swagger或Postman跑一遍主要接口。前端是否已构建最新代码路由刷新是否正常。是否准备好了2-3个测试账号分别覆盖学生和管理员角色。核心演示数据是否提前造好比如有正在报名的赛事、已生成的赛程、有比分记录的比赛。是否预演过一遍完整业务流注册/登录 → 创建战队 → 报名比赛 → 管理员审核 → 生成赛程 → 录比分 → 看排行榜。最后再分享一点个人心得做完这套系统我心里最大的感受是毕设项目不在于技术堆得有多高而在于逻辑闭环和细节完整。评审老师看重的不是一个花里胡哨但跑不通的东西而是一个朴实无华但每个按钮都能点通、每个状态都有交代的系统。做一个校园电竞赛事系统技术点本身没什么神秘但当你把报名状态、赛程轮次、比分胜负、排行榜积分这一条主线彻底跑通把踩过的坑一个个补齐答辩的时候你自然有底气讲清楚每一个设计决策背后的取舍——而这种能讲清楚的能力恰恰是毕业设计真正要训练的东西。如果你正在做类似的管理系统希望这篇复盘能帮你少走一些弯路。
返回列表