如果你接过高校学科竞赛管理的需求,大概见过这种画面:通知在好几个群里来回转发,学生报名信息散落在不同Excel里,团队成员学号、学院得分头核对,评委打分表格式各写各的,最后汇总奖项还要人工排序确认。这个“企业级高校学科竞赛平台管理系统”的源码,要解决的就是这一整条流程里最耗人力、最容易出错的环节——把比赛从发布、报名、评审到公示的每一步,都变成系统里可追踪、可校验的状态。
这套源码走的是Java领域最稳的组合:SpringBoot做后端接口、Vue做前端页面、MyBatis负责数据库操作、MySQL做数据存储。选这套技术栈不是因为它时髦,而是因为它成熟、资料多、接手的人好找,更重要的是结构清晰的话,后续无论加证书打印、接短信通知、对接学校统一身份认证,都不会把项目改崩。这篇内容我不打算逐行念代码,而是从拿到这套源码之后你真正会做的几件事出发:理解项目结构、摸清业务闭环、跑通部署、应对常见问题。把这几个点吃透,你就具备把这套系统用在真实竞赛周里的能力。
1. 项目定位与架构选型的现实逻辑
1.1 学科竞赛平台到底管什么
高校学科竞赛管理,表面看是“发通知、收报名、给成绩”三件事,实际拆开全是细节。一个竞赛可能有个人赛和团队赛,团队赛允许跨学院组队,队长和队员的角色得理清楚;报名期有截止时间,过期之后系统要自动关闭入口;评委可能来自企业、其他高校,不一定是系统里的教师账户,要给他们单独开评审权限;评分环节有的规则是去掉一个最高分、去掉一个最低分再取平均;获奖名单要公示,公示期间学生可能还要申诉。
这些需求决定了系统不是几个增删改查接口拼在一起,而是要有完整的业务状态流转。竞赛有“报名中、评审中、已公示、已结束”这些阶段,报名记录有“待审核、通过、拒绝、已退回”这些状态。状态流转做清楚了,系统才算真正可用,这也就是“企业级”和学生作业之间最明显的分野。
1.2 为什么 SpringBoot+Vue+MyBatis+MySQL 是稳妥答案
很多人看到这套组合觉得不够“高大上”,但实际做过校园项目的都会认同它的合理性,原因可以总结成四点:
- SpringBoot 生态成熟,遇到问题搜索一下就有答案。内置Tomcat、自动配置,部署成本非常低,一个jar包就能跑。
- Vue 对渐进式开发友好,页面组件复用度高。校园系统里学生端、教师端、管理员端大量复用表格和表单,组件化之后开发效率翻倍。
- MyBatis 的优势是SQL可控。竞赛系统里复杂的多条件筛选、按院系统计人数这类需求,直接手写SQL比用ORM拼出来的更直观,也更好优化。
- MySQL 在学校场景里几乎是标配,运维不陌生,数据量在竞赛场景下撑死百万级,完全够用。
这套组合还有一个隐藏优势:找人接手不愁。后做毕设的学生、新入职的实习生都能快速上手,对学校的长期维护来说是非常现实的好处。
2. 后端实现拆解:从登录到颁奖的完整闭环
2.1 项目结构与统一接口设计
拿到源码先看包结构。正常情况下在 com.xxx.contest 下面会分 controller、service、mapper、entity、common、config 这些包。这个分包方式看着普通,但胜在约定俗成,新接手的人不用猜业务代码在哪。
有两个东西是拆包时我会优先找出来看的:统一返回结果和全局异常处理。如果每个接口都自己拼Map返回,前端联调时痛苦不堪。规范的做法是定义一个 Result 类,包含 code、message、data 三个字段,成功时 code 固定为200,失败时用自定义错误码。全局异常处理通过 @RestControllerAdvice 捕获业务异常和SQL异常,避免把整个异常栈直接甩给前端,也让接口的返回格式始终一致。
这个设计看上去是基本功,但实际影响很大。前端只要根据 code 判断一次,就能统一处理所有接口的错误弹窗,不用每个页面写一遍。
2.2 JWT 登录与角色权限的双层控制
校园系统最常见的登录逻辑是:用户表里存 username、password、role 字段,密码用 BCrypt 或 MD5 加密存储,role 区分 student、teacher、admin。登录成功之后后端签发 JWT,前端存在 localStorage 里,每次请求通过拦截器带上 Authorization 请求头,后端用拦截器校验 token 并解析出用户身份。
角色权限要分两层做。第一层是后端接口校验,管理员调用的竞赛管理接口,学生角色访问时直接返回403;第二层是前端按角色渲染菜单,学生看不到管理入口。前端的控制做得再花哨,后端的校验不能省,两层都有才算闭合。这也是那种“页面隐藏了但直接请求接口还能操作”的漏洞的解法。
拿到源码之后第一件事,我建议先改默认管理员密码。很多项目初始数据里躺着 admin/admin123 这种账户,上生产环境之前不处理,整个系统的门就等于敞着。
2.3 报名审核与数据事务
报名这个环节最能看出源码的成色。一套合格的实现,会先检查竞赛状态是不是“报名中”,再查当前用户是不是已经报过名,然后判断报名人数有没有超过上限,最后才插入报名记录并把状态置为待审核。这几个判断看起来简单,顺序稍有不对就可能出现重复报名或者把报名量顶爆。
团队赛报名涉及两张表:enroll_record 保存团队主信息,enroll_member 表保存每个成员的信息,一个队对应多条成员记录。这里一定要用 @Transactional 把主表和子表的写入绑在一起,否则主队插入成功、成员插入失败,数据就脏了。我见过不少源码在这里省了事务注解,报名高峰期一出现并发,队伍名单就对不上,后面审核环节全是坑。
2.4 评分排名与计分规则
评委评分这块,常规设计是 score 表存竞赛ID、选手ID、评委ID、分数、评语,一行就是一位评委给一个选手的一次打分。最终排名的计算通常是先按规则处理评分——比如去掉一个最高分和一个最低分再取平均,然后按平均分降序排列。
真正要注意的是计分规则不能写死在代码里。不同竞赛的计分方式可能不同,有的用均值,有的去掉极端值,有的各维度加权。成熟的做法是把计分规则配置到竞赛表里,比如 score_rule 字段存规则编码,后端根据编码动态计算。如果源码里直接硬编码了某个算法,后续每接一个比赛就要改一次代码,后期维护非常被动。
2.5 数据统计与名单导出
管理员后台还有一个很常见的需求:统计各学院报名人数、各竞赛参赛趋势、导出报名名单和获奖名单。统计接口一般返回前端 ECharts 用的数据结构,后端写聚合 SQL 按学院、按竞赛分组统计。导出功能则用 EasyExcel 或 POI 生成 xlsx 文件。
这块如果源码里做了,能省非常多力。实际竞赛周里,学校经常要求“今天下班前交一份各学院报名汇总表”,有导出功能点一下就好,没有就得折腾半天下载再加工。拆源码时留意一下导出接口是不是流式返回,避免数据量大时内存撑爆。
3. 数据库设计与 MyBatis 实践:地基决定上层
3.1 核心表结构长什么样
根据业务闭环,核心表大致可以分成用户权限、竞赛信息、报名过程、成绩结果、内容发布五个组,用一张表说明:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| user | id, username, password, real_name, role, college, student_no, phone, email | 用户主表,角色区分为学生、教师、管理员 |
| competition | id, name, category, level, organizer, enroll_start, enroll_end, status, score_rule, description | 竞赛基本信息,status控制生命周期 |
| enroll_record | id, competition_id, user_id, team_name, status | 报名主记录,status表示待审核、通过、拒绝 |
| enroll_member | id, enroll_id, user_id, role_in_team | 团队成员,个人赛只保留一条 |
| score | id, competition_id, enroll_id, judge_id, score, comment | 评分记录,一位评委一条 |
| notice | id, title, content, publish_time | 公告与通知 |
| attachment | id, biz_type, biz_id, file_name, file_url | 报名材料、赛题文件统一存储 |
这套结构基本覆盖了常见竞赛管理场景。关于外键,多说一句:很多学生项目为了省事不加外键,删数据时全链路崩;但外键过多,批量导入时也会拖慢速度。企业级场景更常见的做法是在物理表上不加外键,靠索引、唯一约束和业务逻辑保证数据一致,这需要源码在删除报名、删除竞赛时做足够的关联检查。
3.2 字符集、索引与排序的坑
数据库初始化第一件事,是把字符集统一成 utf8mb4。很多老项目还在用 utf8,学生提交的 emoji、生僻字一入库直接报错或变问号。排序规则用 utf8mb4_general_ci 或 utf8mb4_unicode_ci 都行,但涉及到按中文拼音排序的名单,MySQL 8.0 下的排序行为和 5.7 会有差别,做参赛名单排序时提前测一下,别到了要出公示名单的时候才发现顺序不对。
索引设计是另一门功夫。报名表最常做的查询是“某竞赛下所有报名记录”和“某用户报过的所有竞赛”,前者要建 competition_id 索引,后者要建 user_id 索引。score 表查询基本围绕竞赛和评委,联合索引 (competition_id, judge_id) 就够用。索引不是越多越好,每加一个索引,写入性能都会下降,竞赛报名高峰时影响尤其明显。
3.3 动态SQL、结果映射与缓存取舍
MyBatis 在竞赛系统里最值钱的能力是动态SQL。竞赛列表往往支持按名称模糊查询、按分类筛选、按状态筛选,如果三个条件都写死,接口要么参数冗余,要么写好几套SQL。用 XML 里的 和 标签,一组SQL就能通吃所有筛选组合,这也是标题里强调 MyBatis 架构的原因——在这类管理系统中,SQL的灵活可控比全自动ORM更有优势。
结果映射也要重视。报名列表联查竞赛名称、团队人数、状态名称之后,返回字段已经不是单张表结构。规范的做法是定义 VO,用 resultMap 做多表 JOIN 的嵌套映射,避免在 Service 层用循环手工组装数据。源码如果大量出现 for 循环里查数据库,性能基本不能看。
最后说缓存。MyBatis 一级缓存默认开启,作用域是 SqlSession,这层问题不大;二级缓存跨 SqlSession 共享,但竞赛列表这种频繁变化的业务数据如果开了二级缓存,管理员更新竞赛信息后用户端很容易读到旧数据。我的习惯是业务数据不开二级缓存,只对字典表这类低频变动数据使用,并用 flushCache 严格控制失效时机。
4. 前端 Vue 工程:三种角色的三种工作台
4.1 目录结构、路由与角色菜单
前端项目拿到手先看 src 目录,核心就分 api、utils、router、store、views、components 几类。views 下面通常会按角色分目录,student、teacher、admin 各有主页,后台管理页再按业务拆 competition、user、score 等子目录。
路由层面,最简单的是在 router.beforeEach 里判断有没有 token,没有就跳去登录页。更进一步的是动态路由:后端登录接口返回当前用户的角色和权限菜单,前端根据菜单动态生成路由表。这个做法的好处是学生登录后完全看不到管理路由,菜单干净,权限边界也更清晰。热词里有人专门搜 Vue 动态路由,竞赛系统正是动态路由非常典型的落地场景。
4.2 Axios 封装与视频附件场景
Axios 封装属于前端工程的必修课。源码里一般有个 request.js,统一设置 baseURL、从 localStorage 取 token 放进请求头,在响应拦截器里统一处理 code 不等于 200 的错误,401 时清空登录态并跳回登录页。这套东西做好之后,业务页面里的接口调用写起来非常清爽,不用每个页面重复处理错误。
竞赛系统经常涉及附件和视频场景。报名时要传作品文档、承诺书,赛后要传答辩视频。视频文件如果直接放 mp4,大文件加载和断点续播体验都很差。现在比较常见的方案是转成 m3u8 切片后,前端用 hls.js 或 video.js 播放,后端存储可以用 MinIO 这类对象存储,SpringBoot 侧把 MinIO 的客户端封装好,上传下载都很方便。源码里如果已经预留了存储抽象接口,后续接 MinIO 会省很多事。
4.3 打包部署:单 jar 还是 Nginx 托管
开发阶段前后端分离,前端跑在 8000 或 8080,后端跑在 9090,通过 vue-cli 或 vite 的 proxy 把接口请求代理到后端,绕开跨域。
生产部署有两种常见路径。一种是把前端 build 出来的 dist 文件放在 Nginx 下,Nginx 反向代理接口到 SpringBoot;另一种是把 dist 文件拷进 SpringBoot 的 src/main/resources/static 目录,重新打包成单个 jar,一条命令启动。第二种方案在校园服务器资源紧张时非常实用,一台机器一个进程全部搞定。但用第二种方案时要注意,前端路由如果用 history 模式,后端需要做 404 回退处理,否则用户刷新页面就会白屏。这是单 jar 部署最容易踩的坑。
5. 本地部署完整实操:让源码在你电脑上跑起来
5.1 环境准备与镜像配置
想把这套源码跑通,先确认四件事:JDK 是 1.8 还是 11,SpringBoot 2.x 用 8 就行,如果是 SpringBoot 3.x 就必须 JDK 17 以上;MySQL 建议 8.0,5.7 也能跑但部分SQL写法要兼容;Maven 3.6 以上,Node.js 14 以上。
Maven 和 npm 的镜像源一定要提前配好。Maven 在 settings.xml 里配阿里云镜像,npm 执行 npm config set registry 指向 npmmirror,不然等依赖下载的时间比写代码还长。这一项不做好,很多人在部署第一步就劝退了。
5.2 数据库初始化与后端启动
后端启动步骤按顺序来:
- 创建数据库,字符集指定 utf8mb4:CREATE DATABASE contest DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci。
- 导入源码带的 SQL 文件。数据库表结构、初始管理员数据都在这个文件里,别用图形工具手工建表,容易漏初始数据。
- 修改 application.yml 里的数据源配置,MySQL 8.0 的驱动类是 com.mysql.cj.jdbc.Driver,连接 URL 推荐带完整参数:useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。
- 如果本地是 MySQL 5.7,驱动类要相应调整。常见报错集中在驱动类名不存在和时区问题。
这几步里最值钱的就是 serverTimezone=Asia/Shanghai。不加这个参数,很多环境会出现“时间差8小时”的诡异现象,因为 MySQL 驱动默认拿的 UTC 时间和北京时间对不上,排错能排一下午。
后端启动方式两种:IDEA 里直接运行主类的 main 方法,或者命令行执行 mvn spring-boot:run。日志里出现 Tomcat started on port(s): 8080 就说明后端起来了。
5.3 前端启动与前后端联调
前端命令看着简单,坑都在细节。npm install 如果报错,先怀疑 node-sass 这类需要编译的依赖,Node 版本一高它就罢工;Vue2 老项目常见这个坑,解决方案是换 node-sass 版本或者改用 sass 包。依赖装完后 npm run dev 启动开发服务器。
联调时重点看登录接口通不通。页面能打开但登录一直转圈,大概率是接口代理有问题。确认 Vue 开发服务器的 proxy 配置里 target 指向了后端实际端口,并且前后端端口不能互相占用。如果页面能出数据但浏览器控制台报跨域,检查后端有没有配 CORS,或者前端 proxy 是否真正生效。
6. 源码使用中的高发问题与排查心得
6.1 环境与版本类问题
第一类是 MySQL 连接异常。最常见的报错是 Communications link failure,听上去很吓人,实际多半是驱动版本和 MySQL 版本不匹配、useSSL 参数没生效、或者 allowPublicKeyRetrieval 没有开启。MySQL 8.0 客户端连接时,URL 里建议加上 allowPublicKeyRetrieval=true,否则某些认证方式下会报 public key retrieval 错误。
第二类是 SpringBoot 版本太高导致编译失败。很多人拿到源码后不看 pom 就升级 JDK,结果 SpringBoot 3.x 的项目在 JDK 8 下编译直接报“程序包不存在”。记住 SpringBoot 3 是基于 Jakarta EE 的,javax 开头的包全部换成了 jakarta,老代码没改造就不能在 JDK 8 跑。先看 spring-boot-starter-parent 版本再决定 JDK 版本,这个顺序不能反。
第三类是端口被占用。后端 8080 被其他程序占着,启动时直接报端口冲突。Linux 用 lsof -i:8080 查占用进程,Windows 用 netstat -ano | findstr 8080,找到 PID 之后决定是换端口还是清进程。
6.2 框架配置类问题
MyBatis 报错里最常见的是 Invalid bound statement (not found)。这个错翻译成人话就是:接口方法找到了,但对应的 SQL 语句没找到。排查顺序固定是三个:MapperScan 注解指定的包路径对不对;mapper XML 文件的 namespace 是不是和接口全限定名一致;target/classes 里有没有编译进去这个 XML。第三个问题尤其容易踩,如果源码把 XML 放在了 src/main/java 目录下,而 Maven 的 resources 配置没包含 xml 后缀,运行时就找不到了。
第二类是缓存问题。前面提过的 MyBatis 二级缓存,如果竞赛列表数据更新后不生效,就检查是不是二级缓存拦截了查询。最直接的排查方法是在 SQL 日志里看同一 SQL 是否被重复执行,如果只执行一次且改了数据后结果没变,基本就是缓存问题。处理方式有两种:在更新语句上配置 flushCache 强制刷新,或者把这部分业务数据的缓存直接关掉。
6.3 业务逻辑中的隐藏坑
环境问题都好解决,业务逻辑上的坑才真的是源码质量试金石。最典型的坑是报名截止时间的判断。有的源码只是前端倒计时归零后隐藏报名按钮,但后端接口没有校验当前时间,用户手动调接口照样能报名成功。规则类校验必须放后端,前端隐藏按钮只是改善体验,不是权限控制。
第二类是获奖名单的并列问题。用平均分排序时,如果两支队伍总分相同,源码如果没有处理并列规则,奖项就会出现顺序错乱。合理的处理是先按平均分降序,再按去掉极端值后的评分一致性或提交作品时间作为次排序条件。这些细节没处理,竞赛周的负责人就会拿着一份“看起来不对劲”的名单来找你理论,那种场面经历过一次就够了。
第三类是统计口径问题。按学院统计参赛人数时,个人赛和团队赛的计算方式不一样,团队赛是按队伍数算还是按成员人数算,各地各校规则不同。源码里如果统计逻辑写死了,后期做报表会非常痛苦。这类扩展点最好设计成配置文件或后端字典,改口径时不动代码。
结尾
这套源码拿下来之后,我的建议是先别急着改功能,把登录、报名、评分、获奖这条主链路完整跑一遍,亲手感受状态是怎么流转的。学校项目的复杂度通常不高,但对流程完整性要求很高,你完整走通一条真实竞赛流程,对这套系统的理解会比看十遍代码都深。后面如果要做扩展,优先从附件管理、消息通知、证书打印这几个方向入手,改动范围小、容易出效果,也最容易让学校老师直接感受到系统带来的变化。真到了竞赛周,系统稳定跑完一整场比赛,那种踏实感才是最值得的回报。