资讯动态

SpringBoot+MyBatisPlus田径运动会管理系统设计与实现

发布时间:2026/10/25 11:21:22 来源:尧图企业网站定制
简介这是一个基于Java开发的田径运动会管理系统项目包面向计算机相关专业学生与Java初学者可用于课程设计、毕业设计或理解信息管理系统的完整开发流程。系统覆盖运动员管理、赛事安排、在线报名、成绩录入与排名、信息发布及权限管理等核心模块并采用MySQL数据库存储数据体现了从需求分析到编码实现的软件工程实践。压缩包共81个文件以48个class、25个java源码为主另有数据库脚本taf.sql、项目配置文件及实验报告PDF包体大小约2.04MB。Java源码可直接在Eclipse中打开运行SQL脚本方便初始化数据PDF报告则详细阐述了设计思路与系统架构便于对照学习。目前已有2168人学习下载。借助这份资料读者可以掌握Java Swing/FX界面开发、JDBC数据库操作、分层设计等关键技能同时借鉴其文件组织方式和文档规范提升实际项目的开发与协作能力。1. java田径运动会管理系统一场校运会的“抢票”难题每年校运会报名截止前体育委员们都在做同一件事在 Excel 里来回比对生怕同一个运动员报了三个以上项目、或者某项目人数爆满超了赛道。等比赛当天成绩登记还在用纸质检录单晚上再人工录入名次算错是常事。这套 java田径运动会管理系统本质就是把“报名—编排—成绩—排名”这条链路数字化用 Spring Boot MyBatis-Plus 做后端MySQL 存数据前端用 JSP 或 Vue 都行。它能解决三件事报名不超员、成绩不错排、赛程可追溯。适合毕设、课设也适合学校或单位体委真正拿去用。接下来我把数据模型、并发控制、排名算法和踩过的坑一次讲透你照着做就能跑通。2. 数据模型先行把“人、项目、过程、结果”拆成五张表运动会系统看着业务简单但数据模型一旦偷懒后面写逻辑全是补丁。我一般先把实体理清楚运动员、比赛项目、报名关系、成绩、赛程安排。这五张表各自独立又通过外键和唯一索引互相约束比“一张大宽表”或“一张报名表加几个字段”抗造得多。2.1 运动员表与项目表先定“人”和“事”的边界运动员表存的是“人”的静态信息项目表存的是“事”的可选范围。二者不直接关联中间靠报名表连接。好处是一个运动员可以报多个项目一个项目可以容纳多个运动员这种“多对多”关系如果不用中间表后期查“某个项目有哪些人”“某个人报了哪些项目”都会写得很难受。CREATE TABLE athlete ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 运动员ID, student_no VARCHAR(20) NOT NULL COMMENT 学号/工号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别 0男 1女, class_name VARCHAR(50) COMMENT 班级/部门, phone VARCHAR(20) COMMENT 联系电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT运动员表; CREATE TABLE sport_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 项目ID, item_name VARCHAR(50) NOT NULL COMMENT 项目名称, category TINYINT NOT NULL COMMENT 组别 0男子 1女子 2混合, max_players INT NOT NULL DEFAULT 8 COMMENT 每组最多参赛人数, is_team TINYINT NOT NULL DEFAULT 0 COMMENT 是否团体项目, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_item_name (item_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT比赛项目表;这两张表的字段没有玄学但有三个细节值得注意。student_no加唯一索引是为了防止同一个学生被 Excel 录入两遍形成“幽灵运动员”。max_players放在项目表而不是由着报名接口硬编码是为了让不同项目有不同的报名上限——100 米可能限 8 人趣味接力和拔河这类团体项目可能限 4 队。is_team建议加上因为团体项目的成绩录入逻辑和个人项目不一样团队没有“性别”概念排名也按队伍算。2.2 报名表与成绩表把“过程”和“结果”分开存很多初学者会把“报名状态”和“比赛成绩”混在同一张表里先插入一条记录状态是“已报名”比完赛再 update 成绩。这样做的坑是成绩字段在报名阶段为空业务上说得通但 SQL 里WHERE score IS NOT NULL这类写法会越来越频繁而且一旦误更新很容易把报名信息覆盖掉。我习惯的做法是报名表只负责“谁报了哪个项目”成绩表只负责“这个报名记录跑出了什么结果”。CREATE TABLE sign_up ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 报名ID, athlete_id BIGINT NOT NULL COMMENT 运动员ID, item_id BIGINT NOT NULL COMMENT 项目ID, sign_up_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_athlete_item (athlete_id, item_id), CONSTRAINT fk_sign_athlete FOREIGN KEY (athlete_id) REFERENCES athlete(id), CONSTRAINT fk_sign_item FOREIGN KEY (item_id) REFERENCES sport_item(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报名表; CREATE TABLE result ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 成绩ID, sign_up_id BIGINT NOT NULL COMMENT 报名ID, score DECIMAL(8,2) COMMENT 成绩秒/米/分, rank_no INT COMMENT 名次, is_valid TINYINT DEFAULT 1 COMMENT 成绩是否有效 1有效 0犯规/弃权, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sign_up (sign_up_id), CONSTRAINT fk_result_signup FOREIGN KEY (sign_up_id) REFERENCES sign_up(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;这里最关键的是sign_up表上那个联合唯一索引uk_athlete_item。没有它并发报名时两个人同时提交代码里哪怕先查了“是否已报名”也可能因为查询和插入之间存在时间差而重复报名。数据库的唯一索引是最后一道防线——重复插入直接报DuplicateKeyException比在 Service 层用 synchronized 或分布式锁都更可靠。这个后面第四章还会展开。result表用sign_up_id做唯一键而不是直接存athlete_id item_id是为了保证成绩只能挂在一笔有效报名上。成绩表里的score我用DECIMAL(8,2)而不是FLOAT是因为浮点数比较排名时容易出精度问题尤其是两个选手成绩完全相同的情况。2.3 赛程表让编排和检录都有据可查最后一个核心表是赛程表它记录“哪个项目、第几组、什么时间、在哪个场地”。如果不建这张表运动会当天就只能靠打印的秩序册临时调场时对讲机喊来喊去事后也说不清。赛程表我一般这样设计CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 赛程ID, item_id BIGINT NOT NULL COMMENT 项目ID, group_no INT NOT NULL DEFAULT 1 COMMENT 第几组, start_time DATETIME COMMENT 开赛时间, venue VARCHAR(50) COMMENT 场地, is_finished TINYINT DEFAULT 0 COMMENT 是否已完赛, UNIQUE KEY uk_item_group (item_id, group_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT赛程表;赛程表不是必须的但如果你的系统要支持“按组检录”“批量录入成绩”它就是骨架。uk_item_group保证同一个项目不会开出两个“第一组”。编排时先用一条 SQL 把项目按组展开生成秩序册再在成绩录入页面按组筛选逻辑会顺很多。提示五张表的顺序建议按“运动员→项目→报名→成绩→赛程”创建因为有外键依赖。MySQL 里外键不是性能杀手但查询时记得所有 join 字段都要有索引否则数据量一上来 join 会拖慢。3. Spring Boot MyBatis-Plus 落地从实体类到建表 SQL模型定完下一步是工程落地。项目结构不复杂但依赖、配置和“实体类与表结构怎么对应”是新手最容易卡住的地方。这一章说清楚三件事工程目录怎么摆、实体类和建表 SQL 的关系、以及application.yml里那些容易踩的参数。3.1 工程结构与核心依赖我用的是标准的 Spring Boot 分层结构不搞花活。controller收参数service管事务mapper管 SQL。实体类放entity包和数据库表一一对应。前端可以先用 JSP 或 Thymeleaf 顶着后面换成 Vue 也只是接口对接的事。src/main/java/com/school/sports/ ├── controller/ # 报名、成绩、赛程等接口 ├── service/ # 业务逻辑事务在这里 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 实体类 ├── common/ # 统一返回结果、异常处理 └── config/ # 分页插件等配置pom.xml 里的核心依赖并不复杂。Spring Boot 2.7 以上配上mybatis-plus-boot-starter的 3.5.x 版本再带一个 MySQL 驱动和 Lombok 就能转起来。这里不建议为了图新用 Spring Boot 3因为 javax 到 jakarta 的包名迁移会让很多老教程里的代码直接编译失败对新手不友好。常见做法是dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependencyMyBatis-Plus 这个选型理由很直接单表 CRUD 不用写 XMLBaseMapper直接给你现成的selectById、insert、updateById。运动会这种业务绝大多数操作都是单表读写复杂查询就两三条手写 SQL 完全可控。3.2 实体类不是表但能管住表写一个实体类生成建表语句的小工具有个热词是“mybatisplus根据java实体类生成创建表的sql语句”这里必须先说清楚MyBatis-Plus 本身并不直接提供“根据实体类生成建表 SQL”的功能它只管 ORM 映射。但我们可以利用实体类上的注解信息反推出一份建表语句这在快速初始化项目时非常实用。实体类这样写Data TableName(sign_up) public class SignUp { TableId(type IdType.AUTO) private Long id; TableField(athlete_id) private Long athleteId; TableField(item_id) private Long itemId; TableField(sign_up_time) private LocalDateTime signUpTime; }TableName指定表名TableId标记主键并声明自增TableField把驼峰字段映射到下划线列名。这些是 MyBatis-Plus 的基础操作但真正有价值的是既然实体类已经是表结构的“声明”我们干脆写个小工具解析注解生成CREATE TABLE语句。核心逻辑是遍历实体类的Field[]读取注解里的列名再根据 Java 类型映射到 MySQL 类型。public class DdlGenerator { private static final MapClass?, String TYPE_MAP new HashMap() {{ put(Long.class, BIGINT); put(Integer.class, INT); put(String.class, VARCHAR(50)); put(LocalDateTime.class, DATETIME); put(BigDecimal.class, DECIMAL(8,2)); put(Boolean.class, TINYINT(1)); }}; public static String generate(Class? entityClass) { TableName tableName entityClass.getAnnotation(TableName.class); StringBuilder sb new StringBuilder(CREATE TABLE ) .append(tableName.value()).append( (\n); Field[] fields entityClass.getDeclaredFields(); for (Field field : fields) { TableField tf field.getAnnotation(TableField.class); if (tf null || tf.exist() false) continue; String columnName tf.value().isEmpty() ? camelToUnderscore(field.getName()) : tf.value(); sb.append( ).append(columnName).append( ) .append(TYPE_MAP.getOrDefault(field.getType(), VARCHAR(255))) .append(,\n); } sb.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sb.toString(); } private static String camelToUnderscore(String name) { return name.replaceAll(([a-z])([A-Z]), $1_$2).toLowerCase(); } }这段代码的逻辑说明先通过TableName拿到表名再用 JDK 反射拿到所有字段读取TableField上的列名映射没有显式注解的字段用驼峰转下划线兜底。类型映射放在TYPE_MAP里Java 的Long/String/LocalDateTime分别对应BIGINT/VARCHAR/DATETIME。参数上需要注意的是String我一直先用VARCHAR(50)但项目名称、班级名这种可能超长度的字段建议手动改成VARCHAR(100)或VARCHAR(255)否则建表后插入长字符串会报Data too long。3.3 关键配置时区、驼峰映射、日志与连接池参数application.yml里的配置决定了你少踩多少坑。下面这份是我常用的基础配置每一条都有存在的理由spring: datasource: url: jdbc:mysql://localhost:3306/sports?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autoserverTimezoneAsia/Shanghai是必须写的不然 8.0 驱动连 MySQL 时会报时区错误或者更隐蔽的——时间差 8 小时。map-underscore-to-camel-case和实体类上的TableField是一套组合拳开这个配置后athlete_id能自动映射到athleteId。log-impl开着能看到 MyBatis 实际执行的 SQL排查问题非常有用但线上要关掉否则日志量很大。maximum-pool-size: 20对运动会这种几百人同时报名的场景已经够用不需要调大连接池建太多反而浪费数据库资源。注意MySQL 8.0 驱动类是com.mysql.cj.jdbc.Driver不是 5.x 的com.mysql.jdbc.Driver。用错驱动启动时会报ClassNotFoundException这个问题在热词“java启动失败怎么解决”里出现频率极高先检查这里。4. 报名并发与成绩排名两个需要“写对”的硬核逻辑模型和工程都就绪了现在进入业务核心。运动会系统真正有技术含量的是两个逻辑报名时防止超员和重复报名成绩出来后算出并排名次。这两个逻辑写错了轻则数据不准确重则比赛现场翻车。这章讲清楚并发控制的取舍和排名算法的 SQL 写法。4.1 报名不超员数据库唯一索引比代码锁更靠谱报名接口的逻辑看起来很简单先查这个运动员有没有报过这个项目再查这个项目报名人数有没有满然后插入。但这段“先查后插”的代码在并发场景下就是典型的竞态条件。假设某个项目只剩一个名额两个体育委员同时提交两个请求都查到“名额未满”然后都插入成功最后该项目人数超了一个。解决思路分三个层次。第一层是数据库唯一索引这个已经做过了uk_athlete_item能防住“同一人报同一项目”的重复插入。第二层是控制项目人数上限这一层不能只靠查 count需要在事务里配合锁或数据库原子操作。第三层是接口幂等同一请求重复提交不能重复插入。常见做法是注册一个DefaultLock报名时先给项目加锁保证同一时间只有一个请求在处理同一项目的报名Service public class SignUpService { private final ConcurrentHashMapLong, ReentrantLock projectLocks new ConcurrentHashMap(); Transactional public void signUp(Long athleteId, Long itemId) { ReentrantLock lock projectLocks.computeIfAbsent(itemId, k - new ReentrantLock()); lock.lock(); try { Long count signUpMapper.selectCount( new LambdaQueryWrapperSignUp().eq(SignUp::getItemId, itemId)); SportItem item sportItemMapper.selectById(itemId); if (count item.getMaxPlayers()) { throw new BusinessException(该项目报名人数已满); } signUpMapper.insert(new SignUp(athleteId, itemId)); } finally { lock.unlock(); } } }这里用了ConcurrentHashMapReentrantLock做 JVM 内本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑