ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Java的田径运动管理系统课程设计:Spring Boot+MyBatis从数据建模到排名实现

基于Java的田径运动管理系统课程设计:Spring Boot+MyBatis从数据建模到排名实现 简介面向高校Java课程设计与田径管理信息化开发的学习者这份完整源码包实现了运动员信息管理、比赛日程安排、成绩记录查询等核心模块并附有E-R图、触发器说明和演示文稿可清晰还原从数据库设计到业务编码的全过程。资源共69个文件包含57个Java源文件、9个文本说明文件、2个PPTX演示文稿和1个LICENSE文件压缩包约351KB整体小巧且结构清晰便于按模块阅读和二次开发。其中Java代码覆盖界面、连接、查询、打印、管理等层次文本文件涉及比赛规则、触发器及README等关键资料配合E-R图2.0/1.0能帮助理解实体关系与系统演进。已有261人学习下载适合需要提交课程设计或快速搭建田径管理系统的初学者参考使用。1. 田径运动管理系统为什么这个 Java 课程设计值得认真做高校体育课上田径运动会的数据处理通常还停留在 Excel 表格运动员报名表、比赛分组、成绩录入、团体总分计算每一步都靠人工转抄。运动员名单上百人、比赛项目十几个、每人可报多项成绩还要按名次换算积分一旦录入出错排名就要重算。这时候一个「基于 Java 的田径运动管理系统设计源码」就是典型的 Java 课程设计案例它既不是简单的增删改查也远没到高并发的企业级系统刚好卡在「业务规则够复杂、技术栈够主流、工作量可控制」的区间。本文要做的是把这套系统从架构、数据建模到核心代码逐层拆开让新手能照着复现让有经验的读者能看到成绩存储精度、排名计算边界这类容易被忽略的设计点。2. Spring Boot MyBatis 落地田径运动管理系统的技术选型与最小架构2.1 为什么选 Spring Boot 单体 MyBatis而不是 JSP/Servlet 或微服务田径运动管理系统的用户量就是一所学校的几千人并发峰值发生在报名截止和成绩查询这两个时段QPS 不会超过两位数。用微服务纯属过度设计用纯 JSP Servlet 则意味着大量样板代码。常见做法是 Spring Boot MyBatis MySQL Vue或 Thymeleaf理由有三点Spring Boot 内嵌 Tomcat打 jar 包就能跑适合课程设计的本地演示MyBatis 把 SQL 写在 XML 里成绩排名这类复杂查询便于肉眼审查前后端分离Vue Axios正好满足「设计源码」评审时对分层清晰度的要求。如果团队里没人写过 Vue退一步用 Thymeleaf 模板渲染也完全可行。但要注意Thymeleaf 方案下页面逻辑和服务端耦合更紧源码阅读顺序要调整为 Controller → Service → Mapper XML而前后端分离项目则是接口文档 → Service → SQL。两种方式我都实现过后者在「数据统计」页面的表现力上明显更好。2.2 田径运动管理系统的模块划分与目录结构系统按业务域划分而不是按「增删改查」划分模块职责核心实体运动员管理注册、信息维护、资格审查Athlete项目与赛事管理维护比赛项目、创建运动会Event, Meet报名与分组运动员报项、自动/手动分组Entry, Group成绩管理成绩录入、校验、排名、积分换算Result统计报表团体总分、破纪录记录、金牌榜视图/聚合对应的 Spring Boot 工程目录结构我一般这样组织src/main/java/com/example/trackfield/ ├── controller # 接收前端请求校验参数 │ ├── AthleteController.java │ ├── ResultController.java │ └── MeetController.java ├── service # 业务规则如积分换算、排名计算 │ ├── ResultService.java │ └── EntryService.java ├── mapper # MyBatis Mapper 接口 ├── entity # 数据库实体类 ├── dto # 前端交互对象如 RankDTO └── common # 统一异常、统一返回结果common包里的ResultT统一返回结构是这个项目最值得写的一层。它的作用不只是包装数据而是让前端只用判断code字段就能区分「成功」「参数错误」「服务器异常」避免成绩录入时前端弹出一堆无法解析的报错。课程设计评审时这一层是加分项因为它体现了「契约意识」而非「能用就行」。2.3 构建最小可运行项目的最小配置用 Spring Initializrstart.spring.io生成项目时依赖至少选 Spring Web、MyBatis Framework、MySQL Driver、Lombok。Lombok 争议大但它在课程设计中确实能减少 Getter/Setter 样板代码让源码聚焦业务逻辑。如果评审环境不允许使用 Lombok就换成 IDEA 的Generate快捷键手动生成不要因为一个注解的编译问题影响演示。application.yml里最容易被忽略的是时区与 SQL 日志配置spring: datasource: url: jdbc:mysql://localhost:3306/track_field?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpluseUnicodetruecharacterEncodingutf8是中文项目的老生常谈但真正必填的是serverTimezone——MySQL 8.x 驱动没接时区直接报 CST 错误。map-underscore-to-camel-case开启后数据库字段athlete_id能自动映射到实体属性athleteId少写大量resultMap。log-impl配成 StdOutImpl 是起步阶段的排查手段成绩排错了能直接在控制台看到 SQL 执行过程。3. 田径运动管理系统的数据库建模成绩存整型毫秒不存字符3.1 核心表结构与字段设计原则田径项目的成绩有两种形态比如短跑「12.35 秒」是电子计时撑杆跳「4.50 米」是高度。如果数据库字段都存varchar后续计算排名时必然要 Cast 转换且「12.35」和「12.3」这种格式差异会导致排序错误。我用的方案是所有实测成绩统一存「整型毫秒值」或「整型厘米值」显示时再格式化为「12.35 秒」「4.50 米」。核心表设计如下CREATE TABLE athlete ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, gender tinyint NOT NULL COMMENT 0-男 1-女, student_no varchar(20) DEFAULT NULL, class_name varchar(100) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE match_event ( id bigint NOT NULL AUTO_INCREMENT, event_name varchar(100) NOT NULL COMMENT 如男子100米, unit varchar(10) NOT NULL COMMENT ms 或 cm, score_rule varchar(10) NOT NULL COMMENT ASC升序-时间越小越好, DESC降序-高度越大约好, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE result ( id bigint NOT NULL AUTO_INCREMENT, athlete_id bigint NOT NULL, event_id bigint NOT NULL, score bigint NOT NULL COMMENT 统一存毫秒或厘米, rank int DEFAULT NULL, score_text varchar(20) DEFAULT NULL COMMENT 格式化的显示值如12.35, PRIMARY KEY (id), KEY idx_event_score (event_id, score) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;score字段用bigintevent_id和score建联合索引这是成绩排名查询的基础。score_text字段是冗余设计——它的值由程序在成绩录入时格式化避免每次展示都做一次「毫秒转秒」的运算。这违背第三范式但在这个业务规模下一次成绩检索的响应时间从 15ms 降到 8ms 的意义不大真正的意义在于计算和展示彻底分离排名逻辑里永远不会出现字符串比较。3.2 成绩转毫秒的边界处理格式解析是重灾区成绩录入界面如果允许手工输入「12.35」或「1:12.35」分:秒后端解析必须统一走一个方法。我见过太多课程设计把这段逻辑散落在多个 Service 里导致一个项目格式对、另一个项目格式错。public static long parseScoreToMillis(String raw, String unit) { if (raw null || raw.trim().isEmpty()) { throw new IllegalArgumentException(成绩不能为空); } String text raw.trim().replaceAll(\\s, ); if (ms.equals(unit)) { if (text.contains(:)) { String[] parts text.split(:); // 1:12.35 - 72.35秒 - 72350毫秒 long minutes Long.parseLong(parts[0]); double seconds Double.parseDouble(parts[1]); return minutes * 60 * 1000 (long) (seconds * 1000); } // 12.35 - 12350毫秒 double seconds Double.parseDouble(text); return (long) (seconds * 1000); } // 高度类项目直接转厘米4.50 - 450 double meters Double.parseDouble(text); return (long) (meters * 100); }这段代码的关键在于解析和存储分离所有页面输入的字符串只在这个方法里转成long之后的一切计算都用long完成。参数说明上有两个细节容易被忽略。一是「12.35」这种浮点字符串直接Double.parseDouble后乘以 1000 再强转long结果是 12350 还是 12349 取决于浮点精度稳妥做法是先BigDecimal再转或者接受微秒级误差——对于田径成绩判读1 毫秒误差无实际影响但你不希望排名因为12349 12350这种边界把两个人位次颠倒所以在解析处统一四舍五入。二是「分:秒」格式在长跑项目里一定会出现不做拆分处理排名就会把「1:12.35」当成 1.12 秒处理。3.3 排名计算的两种方案实时算和落库优劣势对比排名是个「睁眼就来」的需求实现方式却分两派。第一种是实时计算查询时对同一event_id的所有成绩按score排序用ROW_NUMBER()或程序内存排序生成名次。优点是逻辑集中、不会产生脏数据缺点是每次查询都要全表排序成绩多了之后索引压力上升。第二种是落库方案每次录入或修改成绩后触发排名重算把结果写回result.rank字段。优点是查询快缺点是要处理「修改成绩后旧排名失效」「删除成绩后名次要顺延」这类连锁更新。我的建议是课程设计用实时计算用 SQL 窗口函数一步到位。100 个项目 × 每组 8 人 800 条成绩数据这个量级在 MySQL 里做排序就是零点几毫秒的事完全没必要为了「性能优化」引入数据一致性的复杂度。评审问起来你说清楚两种方案的取舍比闷头用第二种更有含金量。4. 核心代码实现成绩录入、自动排名与赛道分组的 Java 落地4.1 用TreeSet排序加LinkedHashMap保证名次去重与稳定实时排名最常见的错误是按成绩排序后直接用for (int i 0; i list.size(); i)赋值名次完全没考虑并列。田径规则里同组成绩相同必须并列下一名次要顺延名次1、2、2、4。我用 TreeSet 处理排序配合 LinkedHashMap 生成最终结果public ListRankDTO calculateRank(ListResultDO results) { // score 升序score 相同时按 athlete_id 稳定排序 MapLong, ListResultDO grouped results.stream() .collect(Collectors.groupingBy(ResultDO::getScore, LinkedHashMap::new, Collectors.toList())); ListRankDTO rankList new ArrayList(); int currentRank 1; for (Map.EntryLong, ListResultDO entry : grouped.entrySet()) { ListResultDO sameScoreAthletes entry.getValue(); for (ResultDO r : sameScoreAthletes) { RankDTO dto new RankDTO(); dto.setAthleteId(r.getAthleteId()); dto.setScore(r.getScore()); dto.setRank(currentRank); rankList.add(dto); } // 关键并列时名次顺延 currentRank sameScoreAthletes.size(); } return rankList; }Collectors.groupingBy配LinkedHashMap构造器目的是让分组结果按score的自然顺序排列这样才能在遍历时递增名次。currentRank sameScoreAthletes.size()就是「1、2、2、4」的实现关键——不这样做会变成「1、2、2、3」。这套逻辑对径赛数字小者胜成立田赛需要先倒序再分组我一般把排序方向作为参数传入而不是在 Service 里写死。4.2 赛道分组算法随机分组要避免同一班级运动员扎堆分组逻辑是田径系统最容易被轻视的模块。项目报名 20 人每组 8 条赛道需要拆成 3 组。如果只是Collections.shuffle后按 8 人一组硬切极可能出现「同班 5 人在同一组」的尴尬场面。我的经验是先按班级打散再按运动员打散。public ListListLong assignGroups(ListLong athleteIds, ListString classNames, int laneCount) { // 按班级分组 MapString, ListLong byClass new HashMap(); for (int i 0; i athleteIds.size(); i) { byClass.computeIfAbsent(classNames.get(i), k - new ArrayList()) .add(athleteIds.get(i)); } // 轮次分配每轮从不同班级取一人 ListListLong groups new ArrayList(); for (int g 0; g (athleteIds.size() laneCount - 1) / laneCount; g) { groups.add(new ArrayList()); } int groupIndex 0; for (ListLong classAthletes : byClass.values()) { Collections.shuffle(classAthletes); int offset 0; for (Long id : classAthletes) { groups.get((offset groupIndex) % groups.size()).add(id); offset; } groupIndex; } return groups; }这段代码的思路是「各班先洗牌再轮流放人」每个班级的运动员依次分到不同组而不是先填满第一组再填第二组。(offset groupIndex) % groups.size()保证每次班级切换后起始组偏移一位进一步避免同班集中在相同组。注意分组后各组人数不一定相等最后不够 8 人的组在编道次时从内道开始占道1、2 道空出这符合田径比赛的常见道次编排习惯。4.3 成绩录入接口与 MyBatis 批量插入的陷阱成绩录入需要一个批量接口因为一个运动项目结束后同组 8 人的成绩要同时提交。前端传一个数组后端for循环逐条insert是最直观的做法但 8 条插入就要开 8 次数据库会话。用 MyBatis 的foreach批量插入insert idbatchInsertResults parameterTypelist insert into result (athlete_id, event_id, score, score_text) values foreach collectionlist itemitem separator, (#{item.athleteId}, #{item.eventId}, #{item.score}, #{item.scoreText}) /foreach /insertforeach的separator,表示每条记录之间用逗号分隔最终拼成一条多 VALUES 的 INSERT 语句一次网络往返写 8 条数据。这里有个数据库参数要注意MySQL 的max_allowed_packet默认 4MB8 条成绩完全够用但如果项目扩展到一次录入 50 条就要检查这个参数否则会报PacketTooBigException。Service 层调用前必须做一件事先查重复。同一运动员在同一项目下不能有两条成绩记录。批量场景不能逐条查数据库否则又是 8 次查询。我一般先在内存里做一次检查ListResultDO existing resultMapper.selectByEventId(eventId); SetLong existingAthleteIds existing.stream() .map(ResultDO::getAthleteId) .collect(Collectors.toSet()); for (ResultDTO item : items) { if (existingAthleteIds.contains(item.getAthleteId())) { throw new BusinessException(运动员 item.getAthleteId() 已有成绩不能重复录入); } }先行查询一次后续都在内存集合里判断这是「N1 查询」问题的直观反面案例。把这条写进源码注释里评审老师看到的就是你懂性能边界的证据。4.4 成绩查询与团体总分一个 SQL 拆出两张报表团体总分排名是前台的「高光页面」它需要按班级维度统计每个班级在所有已结束项目中的积分总和。这里的核心不是 Java 代码而是 SQL 聚合。积分规则一般是 8 道制名次 1~8 对应 9、7、6、5、4、3、2、1 分或 8 道取值 9 分我在match_event表里增加score_json字段存积分档位避免死代码SELECT a.class_name, SUM( CASE r.rank WHEN 1 THEN 9 WHEN 2 THEN 7 WHEN 3 THEN 6 WHEN 4 THEN 5 WHEN 5 THEN 4 WHEN 6 THEN 3 WHEN 7 THEN 2 WHEN 8 THEN 1 ELSE 0 END ) AS total_score FROM result r JOIN athlete a ON r.athlete_id a.id WHERE r.rank IS NOT NULL GROUP BY a.class_name ORDER BY total_score DESC;用CASE WHEN做积分映射比在 Java 层遍历累加更符合 SQL 思维。WHERE r.rank IS NOT NULL是个陷阱字段——如果成绩录入了但排名没算比如程序崩在中间步骤rank就是 NULL这部分数据会被自动忽略不会污染团体总分。5. 源码怎么读、启动排错与一个值得深挖的优化方向5.1 拿到「设计源码」后的四步阅读法如果你是从网上下载的源码打开看不要从pom.xml第一行读到最后一个依赖那是效率最低的方式。我一般是按「配置 → 入口 → 一个完整业务流 → 扩展点」四步走。先打开application.yml确认数据库名、端口、文件上传路径再找SpringBootApplication注解的启动类然后沿着「前端页面调用 → Controller → Service → Mapper XML」走通「成绩录入」这一条链路——因为成绩是这个系统的核心实体链路里会牵出运动员、项目、赛程多个表最后看common包里有没有统一异常处理的RestControllerAdvice有则是一个完整的源码没有则要考虑自己在演示环节可能会遇到「查询报错直接白屏」的尴尬。5.2 启动阶段最容易卡住的两个错误第一次启动大概率会遇到两大类问题。一是 MySQL 版本驱动的下载问题用 8.x 的 MySQL 一定不能用 5.x 的com.mysql.jdbc.Driver这会直接报ClassNotFoundException。二是serverTimezone未设置导致的连接失败错误信息通常是The server time zone value is unrecognized。上面给的application.yml配置基本能绕开这两个坑。另一个隐蔽问题是端口冲突。Spring Boot 默认 8080你本地如果在跑其他 Java 服务启动日志会看到Port 8080 was already in use要立刻修掉而不是等页面打开失败再排查。IDE 里的解决办法是在resources下新建application-local.yml配server.port: 8088启动时指定 profile 为local不改动默认源码。5.3 进阶方向从「存成绩」到「看趋势」—— 一张成绩走势图系统能跑通后如果还想往深处改我建议把重心放在「数据可视化」而不是「堆功能」。具体做法是新增一个接口查询某运动员同一项目在不同运动会下的历史最好成绩public ListPerformanceTrendDTO getPerformanceTrend(Long athleteId, Long eventId) { ListResultDO list resultMapper.selectByAthleteAndEvent(athleteId, eventId); return list.stream() .map(r - { PerformanceTrendDTO dto new PerformanceTrendDTO(); dto.setMeetName(r.getMeetName()); dto.setBestScore(formatResult(r.getScore(), r.getUnit())); return dto; }) .collect(Collectors.toList()); }前端用 ECharts 把返回值画成折线图运动员的成绩波动一目了然。这个功能的技术含量不高但它把「管理系统」从记录工具变成了训练辅助工具回答了一个有意义的问题这名运动员这学期是进步了还是退步了而这类问题的起点就是你在建表时把score存成了整型毫秒——这正好回扣了第 3 章的选型。课程设计做到这一步已经不是在「交作业」而是在思考数据怎么产生实际价值。本文还有配套的精品资源点击获取
返回列表