资讯动态

Spring Boot+Vue高校竞赛管理系统:从业务建模到部署上线全指南

发布时间:2026/10/17 11:19:14 来源:尧图企业网站定制
在学校里待过一段时间的人都知道竞赛管理这件事看着简单真做起来全是细节。我之前帮人梳理过好几版类似的“高校竞赛管理系统”也经常看到有同学拿着Spring Boot加Vue的方案来问为什么我的项目跑通了但一换真实场景就各种报错为什么明明表结构建好了报名功能还是乱成一团这篇文章我想把基于Spring Boot加Vue的高校竞赛管理系统从零到上线的完整思路讲清楚从业务拆解、数据库设计、权限模型、核心接口到前端页面联动、部署避坑尽量把我踩过的和看别人踩过的坑都列出来。适合准备做课程设计、毕业设计或者想在企业里搭一套内部竞赛管理工具的同学参考也适合刚接触前后端分离开发的人拿来练手。这类系统的核心其实不是“技术有多难”而是“业务状态有多乱”。报名、审核、提交作品、评审、公示成绩每个环节都有状态流转每个角色都有不同诉求。把这些东西理清楚代码只是顺带的事。1. 竞赛管理系统到底在管什么业务拆解与需求来源1.1 为什么“竞赛报名”是系统的核心矛盾我观察过不少学校里的竞赛组织流程最常见的一种状态是管理员在群里发通知、学生用在线表格填报名信息、赛事负责人每天手动整理名单、评委拿Excel打分、最后把结果打印出来公示。听起来没什么大问题但一旦竞赛数量多起来、参与人数过千这套手工流程就开始失控。比如同一个学生同时报了三个团队比如提交作品的截止时间到了还有人在改文件比如评委打分口径不一致导致公示后被质疑。这些问题的根源都在于“报名”这个入口没有做规则约束。所以设计竞赛管理系统时我建议把报名流程当成整个系统的心脏来对待而不是把它当成一个普通增删改查页面。围绕报名这个核心系统的业务流程大概是这样管理员发布竞赛并设置报名起止时间学生查看竞赛详情并组队报名管理员或负责老师审核报名资格审核通过后学生在规定时间内提交作品评委在评审窗口内打分最后系统汇总成绩并支持公示。这个链路里每一次状态变化都要有记录都要有权限控制否则后期很难追溯。1.2 四类用户和他们的真实操作路径竞赛管理系统最常见的角色可以压缩成四类管理员、学生、评审教师、普通游客或者说未登录用户。每类人在这套系统里的操作路径完全不同做功能设计时一定要先以角色视角列一遍需求而不是上来就画页面。用表格表达会比较直观角色核心操作关注的数据典型页面管理员发布竞赛、审核报名、管理用户、发布公告竞赛数量、报名人数、审核状态后台看板、竞赛管理、用户管理学生浏览竞赛、组队报名、提交作品、查看成绩报名是否通过、作品提交截止时间、最终分数竞赛列表、报名页、作品上传评审教师查看待评作品、打分、填写评语待评数量、已完成评审、成绩分布评审工作台未登录用户浏览竞赛公告、查看竞赛介绍竞赛时间、参赛要求首页、竞赛详情这个表不是让你直接拿去做数据库表而是提醒你一切功能设计都要回到“某个角色在某个阶段做什么事、有什么数据诉求”上。很多项目做到一半发现页面很多但不知道数据从哪来就是因为一开始没有按角色把页面和数据字段对齐。1.3 功能边界哪些该做哪些不该做高校竞赛管理系统很容易被做成“大杂烩”。有人加在线答题有人加聊天室有人加复杂的数据大屏。我的建议是除非你的场景和题目里明确要求否则这些附加功能都别放在第一版。第一版只要做清爽的五块竞赛管理发布、编辑、状态控制、报名管理组队、审核、名单导出、作品管理上传、下载、截止时间控制、评审管理分配评委、打分、汇总排名、公告与用户管理。等核心链路跑通再考虑扩展数据统计、消息通知、导出成绩单这些提升体验的功能。边界清楚的好处是数据库表不会失控权限模型不会混乱整篇文章后续讲到的状态机和接口设计才能在一个可控的范围内展开。2. 后端设计Spring Boot的表结构、权限模型与状态机2.1 数据库表清单从用户登录到成绩发布的完整链路我用的是经典的Spring Boot 2.7搭配MyBatis-Plus数据库MySQL 8。这套组合在网上资料最多遇到问题很容易搜到解决方案对学生项目或者中小型内部系统来说完全够用。表结构我建议按业务实体拆成这几张核心表。别一开始就设计二十张表先把主链路跑通再逐步拆子表。表名作用关键字段sys_user用户表存登录账号和基本信息id, username, password, real_name, role, college, phonecompetition竞赛表id, name, type, level, organizer, description, enroll_start, enroll_end, statusteam团队表对应一次报名的队伍id, competition_id, captain_id, team_name, member_countteam_member团队成员关联表id, team_id, user_idwork作品表id, competition_id, team_id, title, file_url, submit_timereview评审表id, work_id, reviewer_id, score, comment, review_timeannouncement公告表id, title, content, create_time这里有个细节用户表我习惯命名为sys_user而不是user因为user在MySQL里虽然不算特别严格的保留字但直接用作表名容易在复杂查询、框架生成SQL时踩到意想不到的坑。换一个前缀成本极低后面省心很多。团队和团队成员为什么要拆两张表因为一个团队有多个人而团队本身要存队长、团队名称这些独立信息。如果不拆要么只能一个人一个团队要么就得冗余存字段。竞赛系统里最典型的场景是一个队长报名时把所有成员姓名、学号一起填进去这个场景用team加team_member两张表是最自然的建模方式。2.2 RBAC权限模型为什么不用简单的role字段加if判断很多新手做权限时喜欢在接口里写“if (user.getRole().equals(admin))”看起来简单直接但角色一多就崩了。比如评审教师既能看到评审工作台也能看到自己负责的竞赛列表管理员能管所有模块。如果权限判断散落在各个接口里改一个角色的权限就要翻遍全部Controller。推荐的做法是RBAC思路建sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu五张表把角色和菜单/接口权限做成对应关系。对毕业设计这个体量的系统不一定页面精度到按钮级别但至少做到“角色控制菜单可见性、后端拦截接口访问权限”。具体的后端实现方式也很标准按用户查出角色按角色查出权限标识集合在Spring Boot里用自定义注解加拦截器校验。比如定义一个RequirePermission(competition:add)注解拦截器里判断当前用户是否有这个权限标识没有就直接返回403。这样权限逻辑集中在拦截器里业务代码里不用到处写角色判断。当然如果项目时间确实紧张也可以做一个简化版用户表加role字段前端根据角色动态显示菜单后端做一个全局的角色匹配拦截只区分“管理员/教师/学生”三类。我知道很多能跑通的方案都是这么做的但一旦遇到“某个老师既是评审又是竞赛负责人”这种特殊人物就得回来补RBAC所以建议一步到位。2.3 状态机让报名、作品、评审不再混乱竞赛管理系统里最容易被忽略的就是状态字段的设计。我见过很多项目的竞赛表里只有一个status0表示未开始1表示进行中2表示已结束。看起来没问题实际上处理“报名截止了但作品还在提交”这种情况时就说不清了。我习惯把状态拆成两张表各自管理竞赛状态和报名/作品状态。竞赛本身的状态流转可以设计成草稿 - 报名中 - 评审中 - 已结束。管理员发布竞赛时系统自动根据当前时间和报名起止时间计算应该处于什么状态。这里不需要定时任务去改状态只要在查询接口里实时计算即可展示时再转换成对应的文字。报名状态则是待审核 - 已通过 - 已拒绝或者直接报名成功后进入待提交作品状态。作品状态则是未提交 - 已提交 - 已评审。把状态拆细以后前端页面就能根据状态决定按钮的禁用和跳转后端也能根据状态拒绝非法操作比如报名时间已过就不允许再insert报名记录。可以用一张简表感受一下状态机设计前后对比场景简单status的方案分状态机的方案报名截止、作品提交已开始status2无法区分competition_statusREVIEWING, work_statusNOT_SUBMITTED学生撤销报名不能准确判断是否允许报名在待审核状态可撤销评委重复打分可能覆盖原记录review表对(work_id, reviewer_id)加唯一约束统计各竞赛报名人数要额外写逻辑直接按报名状态count2.4 关键接口设计与防重复操作后端接口设计遵循一个原则一个页面一个聚合接口一个动作一个提交接口。不要为了接口数量好看而拆得零碎。登录认证用JWT加Spring Security或者拦截器都行。我比较推荐轻量方案登录接口校验用户名密码后生成JWT前端把token存在localStorage里请求时放到Authorization头后端拦截器解析token并放入ThreadLocal业务代码里通过一个CurrentUserHolder取当前登录用户。这样很轻量也好理解。密码存储必须用BCrypt加密这一点没有商量余地。明文密码在任何项目里都是红线数据库一旦泄露明文密码就是灾难。防重复报名是竞赛系统里最常见的并发问题同一个学生短时间连点两次报名按钮可能产生两条报名记录。解决的办法有两个层面。数据库层面给team_member表加(team_id, user_id)唯一索引给team表加(competition_id, captain_id)唯一索引应用层面报名接口启动事务插入前先查一次是否已存在。唯一索引是最后一道防线事务加前置查询是常规拦截两者配合基本不会出错。文件上传接口倒是简单但要特别注意两点文件名一定要重命名不要使用用户原始文件名直接落盘否则中文名、特殊字符很容易出问题上传后的文件路径要存在数据库里做访问时通过映射路径读取而不是直接把真实磁盘路径暴露出去。3. 前端实现Vue组件化开发与页面联动3.1 技术选型Vue2加Element UI为什么最适合这类项目很多人在Vue2和Vue3之间纠结我的建议很实际Vue2加Element UI依然是这个项目的最稳妥组合。Element UI的组件文档极其完整网上案例多到数不清菜单、表格、表单、弹窗、分页这些常见功能开箱即用。Vue3对应的Element Plus也成熟了如果是从零开始学用Vue3也不是不行但遇到版本兼容问题时踩坑概率更高。组件库选好以后页面结构推荐这样组织views下面按模块建目录比如views/competition、views/enroll、views/work、views/review、views/user每个模块一个列表页加一个表单页。公共组件放在components比如PageHeader、StatusTag、FileUpload这类复用性高的组件。这样代码结构清晰后期扩展也方便。3.2 路由权限与菜单的动态生成前端权限控制最简单的实现是登录时后端返回当前用户的角色和权限列表前端根据角色生成菜单。后端sys_menu表里存菜单名称、路径、图标、权限标识通过角色关联查询出当前用户可见菜单登录成功以后一次返回给前端。路由守卫是必须加的。在router.beforeEach里判断是否已登录没有token就跳登录页有token但是首次进入就拉取用户信息并动态添加路由。这样刷新页面也不会丢失权限因为每次刷新后都会重新执行一次路由初始化。有一点我特别提醒前端路由守卫只是用户体验层面的过滤真正的安全校验一定在后端接口上。前端跳进一个页面不代表能调接口每个接口都要通过后端的权限断言。3.3 报名、作品、成绩三种页面的状态处理逻辑竞赛列表页是学生用户的第一站。这个页面的核心是“根据竞赛状态展示不同操作按钮”报名中显示“去报名”报名已截止显示“已截止”评审中显示“查看作品”已结束显示“查看成绩”。这个逻辑很简单但一定要把后端返回的status和前端展示文字映射清楚。报名页面有不少细节。学生进入报名页时先要判断自己是否已经报名过这场竞赛如果已经报名且审核通过按钮应该显示“已报名”如果是待审核状态显示“审核中”如果被拒绝显示原因并且允许修改后重新提交。团队成员的输入建议做成动态增删行每个成员行包含姓名、学号、学院等字段提交时和后端team_member表一一对应。作品提交页面要注意截止时间的判断前端拿到竞赛的work_end_time在倒计时结束前允许上传结束后禁用上传按钮同时后端接口也要校验当前时间。前端校验只是提示后端校验才是强制规则。成绩公布页面则要区分“可查看”和“不可查看”两种状态。管理员没有公布成绩之前学生只能看到“评审中”的占位文案公布之后才显示分数和排名。我记得有同学直接把后端数据库里的成绩字段一下发到前端结果学生通过网络请求直接看到了未公布的分数这种低级错误一定不要犯。3.4 axios封装与文件上传细节axios封装算是老生常谈但每次都能发现有人犯同样的错误。请求拦截器里从localStorage取token并添加到header响应拦截器里统一处理业务码业务码为零表示成功非零弹错误信息HTTP 401统一跳登录页。这套东西一定要在一开始就封装好否则后面每个页面都在复制代码。文件上传用el-upload组件时组件默认走的是action属性指定的地址但我们的项目需要带token所以要在headers里动态设置Authorization。上传成功的回调里拿到后端返回的文件URL把URL放到表单里一并提交不要边传边提交避免上传还没结束表单就提交了。预览下载功能也不能忘。后端返回的文件URL如果是相对路径前端拼接时注意不要重复加斜杠如果文件名是中文下载时要注意URL编码否则浏览器可能解密失败。4. 项目落地过程中绕不开的坑权限、时间与文件4.1 时间错乱的八小时之谜前后端分离项目里时间问题是我见过出现频率最高的问题。后端往前端返回LocalDateTime时默认序列化可能带T或者出现前端显示的时间比实际时间早八个小时的情况。这个问题的根源是时区。MySQL连接串里要加serverTimezoneAsia/ShanghaiSpring Boot的application.yml里用jackson.date-format指定格式同时设置time-zone: GMT8。如果用了Fastjson也要检查全局序列化配置是否指定了时区。还有一种情况是前端拿到时间字符串以后直接用JavaScript的new Date()解析在不同浏览器里解析结果可能有细微差异。最稳妥的做法是前后端统一用yyyy-MM-dd HH:mm:ss字符串传递时间字段需要计算倒计时时再由前端转换成时间戳。4.2 Long类型ID在前端丢精度这个坑极其隐蔽。数据库主键用的是bigint自增IDMyBatis-Plus默认雪花算法生成的ID是19位Long类型传到前端以后JavaScript的Number类型最多安全表示2的53次方超出部分精度丢失表现在页面上就是最后一个或两位数字变成0。结果操作记录时发现ID对不上数据窜了。解决办法是在后端实体类的ID字段上加JsonSerialize(using ToStringSerializer.class)把Long序列化成字符串前端拿到字符串类型的ID既不丢精度也不影响后续请求。这个小配置虽然不起眼但几乎所有项目里都会需要。4.3 文件上传的边界大小、类型与路径文件上传相关的配置如果不在工程里提前设置等到部署到服务器再发现就晚了。Spring Boot里要配置的内容包括spring.servlet.multipart.max-file-size和spring.servlet.multipart.max-request-size分别控制单个文件大小和单次请求总大小。如果前端用了el-upload组件里也要设置limit和文件类型accept两个层面一起限制才能防住大部分异常文件。文件存储路径建议配在application.yml里用一个自定义属性如file.upload-path并设置成绝对路径。项目打包成jar运行以后相对路径会因为工作目录不同而飘忽不定一旦用相对路径存文件重启以后文件访问404是常见结果。如果文件是要公开访问的比如竞赛附件、公告附件后端要配置一个静态资源映射把/files/**映射到上传路径目录。如果是登录后才能看的作品文件就不能简单做静态映射要通过下载接口做权限判断后再输出文件流。4.4 并发报名场景下的脏数据问题真实竞赛场景里报名往往集中在截止前的最后半小时。那段时间用户疯狂点击如果系统没有并发保护很容易出现报名人数超限、同一成员重复加入多队、同一个队伍被提交多次的问题。我建议做两层保护。第一层是数据库唯一约束这个前面已经说过team_member表对(team_id, user_id)建唯一索引team表对(competition_id, captain_id)建唯一索引。第二层是报名接口的事务控制用Transactional把“检查是否已报名”和“插入报名记录”包在一个事务里避免检查之后插入之前被别人抢先。如果是高要求的场景还可以在报名接口入口加一个synchronized或者分布式锁锁的粒度精确到竞赛ID加用户ID。对普通校内竞赛来说数据库唯一索引加事务基本足够不需要引入Redis做分布式锁。4.5 Excel批量导入的学生名单有些管理员希望一步导入整个学院的学生用户而不是一个一个手动新增。这个功能不建议用POI的原始API硬写直接用阿里EasyExcel它的读写API很简洁内存占用也低。导入时最容易出的问题有两个一是模板里的列头和学生上传的表头不一致导致解析错位二是重复用户名的处理策略不明确。我的做法是先给管理员提供一个固定的Excel模板下载后端解析时用监听器逐行读取遇到同名的用户默认更新已有记录的手机号和学院信息不新增重复账号。解析完生成导入结果摘要显示“新增X人更新Y人失败Z条及原因”。5. 部署上线从开发环境到服务器的一次完整旅程5.1 本地调试环境的版本搭配前后端版本搭配建议用一个表锁死不要随便升级组件推荐版本说明JDK8 或 11Spring Boot 2.7 兼容性最好Spring Boot2.7.x稳定且资料多不要轻易用3.0以上改坑MyBatis-Plus3.5.x自带分页插件和代码生成器MySQL5.7 或 8.08.0注意驱动名和时区配置差异Node.js14 或 16对应Vue2生态最稳Vue2.6.x配合Element UI 2.15.x这个组合不是说绝对最新而是“一切以文档多、社区答案多、踩坑成本低为优先”。等你把项目吃透再往新版本迁移也不迟。5.2 前后端分离部署Nginx反向代理与接口代理打包部署的流程是这样的前端在项目目录下执行npm run build生成dist目录后端执行mvn clean package生成jar包。服务器上如果只有一个Nginx就把dist里的静态文件放到Nginx的HTML目录然后配置一个location /api/把请求代理到本地的Spring Boot端口。一个典型配置片段大致如下server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /files/ { proxy_pass http://127.0.0.1:8080/files/; } }这里有几个容易踩的点try_files那行一定要加上否则前端路由一刷新就404proxy_pass后面是否带/api/会影响实际转发路径一定要仔细对一遍如果文件上传也要走Nginx记得在http块里调整client_max_body_size默认1MB会让你上传作品直接报413。后端服务的启动命令建议写到启动脚本里比如nohup java -jar competition-system.jar --spring.profiles.activeprod app.log 21 。配置文件区分开发和生产环境生产环境里数据库密码和JWT密钥通过外部配置或环境变量注入。5.3 上线前必须检查的配置清单每次上线前我都会按下面这个清单过一遍避免“本地能跑、服务器跑不了”的尴尬。一是数据库确认表结构已初始化账号密码正确编码用utf8mb4连接串带时区参数。二是配置文件检查上传路径是否有读写权限JWT密钥是否换成足够长的随机串是否关闭了Spring Boot的DevTools自动重启。三是Nginx检查client_max_body_size检查静态资源是否带try_files检查代理路径是否对。四是防火墙与安全组确认只有80、443和SSH端口对外开放后端8080端口不要暴露到公网。五是备份数据库定时备份哪怕用一个简单的crontab加mysqldump也是一层保障。这些检查项都不复杂但它们决定了一个项目能不能从“我的电脑上能跑”变成“服务器上能稳定用”。做了这么多套类似项目我最大的感受是所谓“系统”不是比谁用了多新的框架而是比谁把业务状态理得足够清楚。一个报名状态字段的定义方式就能决定这个系统后面是越用越顺还是越改越烂。如果你正在做自己的竞赛管理系统我建议先把角色和状态流转在纸上画清楚再动手写第一行代码那些看似不起眼的唯一索引、时区配置、权限注解才是撑起一个系统稳定运行的地基。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑