简介:这份源码面向正在学习Springboot与Vue前后端分离开发的学生和初级开发者,提供了一套运动会报名管理系统的后端实现,可用于课程设计、毕业设计或技术练手。压缩包共112个文件、约425KB,以80个Java源代码文件为核心,覆盖用户、竞赛、报名、草稿等业务控制器;另有14个XML配置、1个SQL脚本、1个yml与properties配置、1个jar依赖包,以及少量图片和说明文档,结构完整便于快速导入运行。系统采用Springboot、Mybatis-Plus、Easypoi与Sa-Token技术栈,支持文件导入和MySQL数据库,能帮助读者理解权限校验、数据导出与接口分层设计。目前已有407人学习下载,适合对照源码梳理后端模块划分、接口编写与数据库交互思路,作为二次开发或功能扩展的参考基础。代码仅供学习使用,未经授权禁止出售。
1. 运动会报名管理系统后端:从赛程表到接口清单的落地拆解
校运会报名最怕什么?不是项目多,是报名表收上来之后没人知道谁跟谁重了、哪个项目还差人、秩序册怎么排。基于 Spring Boot 和 Vue 的运动会报名管理系统,本质就是把「纸质报名表 + 微信群接龙」这套流程搬到线上,后端负责管住项目、运动员、报名记录和赛程这几张核心表,前端负责让体育老师点得动。这套东西适合谁?学校信息化岗的开发者、接毕设或课设的学生、想练手前后端分离的中级新手。它不复杂,但坑集中在数据模型和并发报名上,下面按我实际搭过的顺序拆开讲。
2. 后端骨架怎么搭:Spring Boot 项目结构与依赖选型
2.1 为什么用 Spring Boot 3 + MyBatis-Plus 这套组合
运动会报名系统的业务量不大,一个学校撑死几千人,但接口要稳、要快出活。Spring Boot 3 是目前主流选择,JDK 17 起步,配合 MyBatis-Plus 能省掉大量单表 CRUD 的样板代码。热词里有人问「springboot版本太高」怎么办,我的建议是:如果学校服务器还在 JDK 8,就退到 Spring Boot 2.7.x,别硬上 3.x,否则javax换jakarta那一堆包名能让你改到怀疑人生。
选型上,我一般这样定:
| 组件 | 选择 | 理由 |
|---|---|---|
| Web 框架 | Spring Boot Web | 标配,无争议 |
| ORM | MyBatis-Plus | 单表零 SQL,复杂查询手写 |
| 数据库 | MySQL 8 | 学校环境通用 |
| 鉴权 | JWT + 拦截器 | 轻量,不引 Spring Security |
| 参数校验 | Validation | 报名表单字段多,必须校验 |
不引 Spring Security 是因为这套系统的权限就两级:管理员和普通用户(运动员/领队)。用 JWT 加一个拦截器足够,引了 Security 反而配置成本高。
2.2 项目目录结构与最小可运行配置
目录按标准分层来,别搞花活:
src/main/java/com/sports/registration/ ├── controller/ // 接口层 ├── service/ // 业务层 │ └── impl/ ├── mapper/ // 数据访问层 ├── entity/ // 数据库实体 ├── dto/ // 请求/响应对象 ├── config/ // 配置类 ├── interceptor/ // JWT 拦截器 └── common/ // 统一返回、异常处理application.yml最小配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sports_meet?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case必须开,否则athlete_name映射不到athleteName,查出来全是 null,这个坑我踩过不止一次。逻辑删除字段建议一开始就加上,报名记录删了之后赛程统计还要用,物理删除会让你后悔。
2.3 统一返回体和全局异常处理
接口返回格式不统一,前端对接就是灾难。先定一个Result<T>:
@Data public class Result<T> { private Integer code; // 200成功,其他失败 private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } }再配一个@RestControllerAdvice全局异常处理器,把MethodArgumentNotValidException(参数校验失败)和自定义业务异常统一转成Result.fail。这样前端永远只处理一种结构,省掉大量 if-else。参数说明:code用 200/500 两档就够,别学大厂搞一堆错误码,这套系统用不上。
3. 核心数据模型:运动员、项目、报名记录三张表怎么设计
3.1 表结构设计与字段说明
整个系统的数据关系就三条线:一个运动员可以报多个项目,一个项目可以有多人报名,报名记录是中间表但带了额外状态。建表 SQL:
-- 运动员表 CREATE TABLE athlete ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, gender TINYINT NOT NULL COMMENT '1男 2女', class_name VARCHAR(50), phone VARCHAR(20), deleted TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 比赛项目表 CREATE TABLE sport_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(50) NOT NULL, item_type VARCHAR(20) COMMENT '田径/球类/趣味', gender_limit TINYINT DEFAULT 0 COMMENT '0不限 1男 2女', max_participants INT DEFAULT 0 COMMENT '0表示不限', start_time DATETIME, deleted TINYINT DEFAULT 0 ); -- 报名记录表 CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, athlete_id BIGINT NOT NULL, item_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待审核 1通过 2驳回', remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_athlete_item (athlete_id, item_id) );registration表上的uk_athlete_item唯一索引是关键,它从数据库层面挡住了同一个人重复报同一个项目。别指望在 Java 代码里用if判断,并发下必翻车。
3.2 报名接口的实现与并发控制
报名接口是整套系统压力最大的地方,尤其是报名截止前那几分钟。核心逻辑:
@Transactional(rollbackFor = Exception.class) public Result<String> register(Long athleteId, Long itemId) { // 1. 校验项目是否存在、是否已截止 SportItem item = sportItemMapper.selectById(itemId); if (item == null || item.getStartTime().before(new Date())) { return Result.fail("项目不存在或已截止"); } // 2. 校验性别限制 Athlete athlete = athleteMapper.selectById(athleteId); if (item.getGenderLimit() != 0 && !item.getGenderLimit().equals(athlete.getGender())) { return Result.fail("性别不符合项目要求"); } // 3. 校验人数上限(用行锁避免超报) if (item.getMaxParticipants() > 0) { int count = registrationMapper.countByItemId(itemId); if (count >= item.getMaxParticipants()) { return Result.fail("报名人数已满"); } } // 4. 插入,唯一索引兜底 try { Registration reg = new Registration(); reg.setAthleteId(athleteId); reg.setItemId(itemId); registrationMapper.insert(reg); } catch (DuplicateKeyException e) { return Result.fail("请勿重复报名"); } return Result.ok("报名成功"); }逻辑说明:第 3 步的人数校验在并发下不可靠,两个线程同时查到 count=9(上限 10)都会放行。真正的兜底是第 4 步的唯一索引和数据库行锁。如果对人数上限要求严格,把countByItemId换成SELECT ... FOR UPDATE锁住项目行,或者用 Redis 原子计数。参数上max_participants设 0 表示不限,这个约定要在前端也同步,否则前端显示「剩余 0 人」会闹笑话。
3.3 赛程编排与成绩录入的接口设计
赛程编排本质是给sport_item排start_time,再按时间生成秩序册。成绩录入需要单独一张result表,字段包括registration_id、score、ranking。这里有个容易忽略的点:成绩录入后要回写报名记录的status,否则运动员查不到自己成绩。接口设计上,编排用PUT /item/schedule,成绩用POST /result/entry,都走管理员权限。批量编排时用@RequestBody List<ScheduleDTO>接收,别一个项目一个请求,体育老师会骂人。
4. 前后端联调:Vue 对接 Spring Boot 的跨域与鉴权
4.1 跨域配置与 axios 封装
前后端分离第一个拦路虎就是跨域。后端加配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns而不是allowedOrigins,后者在allowCredentials(true)时会报错。前端 axios 封装要统一带 token:
const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = token return config }) request.interceptors.response.use( res => res.data, err => { if (err.response?.status === 401) router.push('/login'); return Promise.reject(err) } )响应拦截器里直接返回res.data,因为后端统一包了Result,前端拿到的就是{code, msg, data},业务层再判断code。
4.2 JWT 拦截器与放行规则
拦截器里做三件事:取 token、验 token、放行白名单。白名单至少包括/login、/register(运动员注册)、静态资源。token 过期时间设 2 小时够用,运动会期间不会有人连续操作两小时。验签失败直接返回 401,前端拦截器跳登录页。这里有个细节:拦截器里不要抛异常,直接response.setStatus(401)然后 return false,抛异常会被全局异常处理器吞掉,前端收到的是 200 加错误信息,排查起来很费劲。
4.3 报名页面的表单校验与提交
前端报名表单要做的校验比后端还多:必填、学号格式、性别与项目匹配、已报项目去重。用 Vue 的 computed 实时算「当前运动员已报项目列表」,提交前再查一遍。提交用POST /registration,成功后刷新列表。别在前端做人数上限判断,那个数据实时变,前端判断只会误导用户。
5. 避坑与排查:报名系统上线后最容易翻车的 5 个点
5.1 现象:同一运动员重复报名成功
原因:只在 Java 层做了select判断,没加唯一索引,并发下两个请求都通过判断。解决:registration表加uk_athlete_item唯一索引,代码里捕获DuplicateKeyException返回友好提示。
5.2 现象:项目人数超上限
原因:count查询和insert之间没有锁,并发窗口内多个请求都看到未满。解决:对sport_item行加FOR UPDATE,或用 RedisINCR原子计数,超限则DECR回滚。
5.3 现象:前端拿到的字段全是 null
原因:application.yml没开map-underscore-to-camel-case,或者实体类字段名和数据库列名对不上。解决:开启驼峰映射,实体类用@TableField显式指定列名,别偷懒。
5.4 现象:跨域配置后仍然报 CORS 错误
原因:用了allowedOrigins("*")同时allowCredentials(true),浏览器拒绝。解决:改用allowedOriginPatterns("*"),或者明确写前端域名。
5.5 现象:报名截止后仍能提交
原因:截止判断用的是start_time,但项目可能提前截止报名。解决:sport_item加registration_deadline字段,报名接口校验这个字段,别复用start_time。
6. 进阶技巧:用逻辑删除和操作日志把系统做成「有后悔药」的
6.1 逻辑删除让误删可恢复
运动会期间管理员误删报名记录是高频事故。MyBatis-Plus 的逻辑删除配置前面已经给了,实体类字段加@TableLogic:
@TableLogic private Integer deleted;这样deleteById实际执行的是UPDATE ... SET deleted=1,查询自动过滤deleted=0。恢复时手动UPDATE deleted=0即可。注意:唯一索引uk_athlete_item在逻辑删除下会失效——删了再报同一项目,唯一索引冲突。解决办法是把唯一索引改成(athlete_id, item_id, deleted)三列联合,或者逻辑删除时把deleted设为id值而非 1。
6.2 操作日志表记录关键动作
再建一张operation_log,记录谁在什么时候改了哪条报名记录:
CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, operator_id BIGINT, action VARCHAR(50), target_type VARCHAR(30), target_id BIGINT, detail VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );在报名审核、成绩录入、赛程编排三个接口里埋点写入。出问题时查这张表,比翻服务器日志快得多。日志表数据量大就按月分表,运动会场景一年就几次,单表足够。
6.3 用定时任务清理过期报名
报名截止后,未审核的报名记录应该自动标记为驳回。用 Spring 的@Scheduled每天凌晨跑一次:
@Scheduled(cron = "0 0 2 * * ?") public void cleanExpiredRegistration() { registrationMapper.rejectExpired(new Date()); }cron表达式0 0 2 * * ?表示每天凌晨 2 点。别用@Scheduled(fixedRate=...),那个从应用启动就开始计时,重启后时间点会漂移。这套组合下来,系统能扛住校运会级别的并发,误操作也有后悔药。我自己的习惯是:任何带「报名」「审核」字样的系统,唯一索引和逻辑删除必须第一天就加上,后面补的代价是数据修复。希望帮到你。
本文还有配套的精品资源,点击获取