首页 / 资讯中心 / 文章详情

基于Spring Boot与Vue的运动会报名管理系统后端设计与实现

基于Spring Boot与Vue的运动会报名管理系统后端设计与实现 ★ FEATURED ARTICLE
简介这份源码面向正在学习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标配无争议ORMMyBatis-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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 统一返回体和全局异常处理接口返回格式不统一前端对接就是灾难。先定一个ResultTData public class ResultT { private Integer code; // 200成功其他失败 private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T ResultT fail(String msg) { ResultT 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 ResultString 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 步的人数校验在并发下不可靠两个线程同时查到 count9上限 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 ListScheduleDTO接收别一个项目一个请求体育老师会骂人。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 封装要统一带 tokenconst 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 的逻辑删除配置前面已经给了实体类字段加TableLogicTableLogic private Integer deleted;这样deleteById实际执行的是UPDATE ... SET deleted1查询自动过滤deleted0。恢复时手动UPDATE deleted0即可。注意唯一索引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...)那个从应用启动就开始计时重启后时间点会漂移。这套组合下来系统能扛住校运会级别的并发误操作也有后悔药。我自己的习惯是任何带「报名」「审核」字样的系统唯一索引和逻辑删除必须第一天就加上后面补的代价是数据修复。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站