2026/10/25 11:20:47

Java田径运动会管理系统:报名编排与成绩统计全解析

Java田径运动会管理系统:报名编排与成绩统计全解析 简介这是一份基于Java的田径运动会管理系统完整项目资源面向需要完成课程设计、毕业设计或想学习Java信息管理系统开发的在校生和初级开发者。系统围绕运动会组织流程设计提供运动员信息添加/删除/修改、比赛项目创建与赛事安排、在线报名、成绩录入与自动排名、公告发布及角色权限管理等功能模块可有效提升赛事组织效率和数据处理准确性。资源共81个文件其中25个Java源代码和48个class编译文件构成完整工程主体另含数据库驱动jar依赖、SQL初始化脚本、readme使用说明和数据库课设报告PDF压缩包大小为2.04MB目录结构清晰可通过Eclipse直接打开运行。实验报告详细说明了系统架构、数据库设计、功能实现逻辑SQL脚本便于快速初始化数据适合对照源码学习Java GUI界面开发、JDBC数据库操作及软件工程文档规范。目前已有2168人学习下载整体可作为Java课程设计、实训项目的优秀参照能帮助开发者快速理解管理信息系统的完整开发流程。1. 田径运动会管理系统报名、编排、计分一条线通吃的 Java 信息系统每年校运会体育组老师最头疼的往往不是比赛本身而是赛前的报名登记和赛后的成绩统计。一个班几十个学生、十几个项目纸质报名单来回传手工录入成绩再按班级汇总名次稍有差池就要翻半天纸。这套 Java 田径运动会管理系统核心就是把这些手工活换成结构化数据流运动员信息管理、项目报名、成绩录入、名次判定、团体总分统计全部落到数据库里用 Spring Boot MyBatis-Plus 把业务串起来。适合正在做课程设计、毕业设计的在校生也适合想快速搭一套中小型赛事管理后台的 Java 工程师。它能解决的实际问题很具体报名截止后一键锁定名单、成绩录入后实时刷新排名、团体总分不再靠计算器。因为底层是标准 Java Web 项目改造成本低接 MySQL、换前端主题都不费劲。2. 数据模型先行从业务实体到建表 SQL 的落地思路2.1 六个核心业务实体谁在参赛、比什么、成绩怎么存运动会管理系统本质上是一个「人员—项目—成绩」三要素系统。运动员在哪个班级、性别、参赛号码、报了什么项目、每个项目的成绩和名次、最后折算成班级多少分这六件事对应六张表班级表class_info、运动员表athlete、项目表event、报名表entry、成绩表result、系统用户表sys_user。其中最容易设计错的是 entry 表。很多初学者会把报名记录直接塞进 athlete 表里加几个字段结果一个运动员报三个项目就变成三行运动员记录后面统计参赛人数时全是重复数据。正确做法是把报名拆成独立表entry 表只存 athlete_id 和 event_id 的关联关系加上报名状态字段。CREATE TABLE entry ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 报名ID, athlete_id BIGINT NOT NULL COMMENT 运动员ID, event_id BIGINT NOT NULL COMMENT 项目ID, entry_status TINYINT DEFAULT 0 COMMENT 报名状态0待审核1通过2驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_athlete_event (athlete_id, event_id) ) COMMENT 运动员报名关联表;注意这里有个容易被忽略的约束UNIQUE (athlete_id, event_id)。没有它同一个运动员同一天下午能被勾三次同一个项目。我在实际项目里吃过这个亏报名页面多提交一次数据库就多了条重复记录最后成绩表里一个人两个成绩排名直接出错。2.2 成绩表设计不同赛项的存储策略田径项目分两大类田赛和径赛。径赛计时以秒为单位100 米、200 米、4×100 接力田赛是测距跳远、铅球、实心球。存储上建议统一用一个成绩值字段存数值另外存 score_type 区分单位。秒数存 DECIMAL(5,2)比如 11.23 秒距离存 DECIMAL(5,2)比如 5.68 米。单位靠项目表里的 unit 字段区分不要在成绩表里把单位拼接进字符串——否则排序时 9.8 米会排在 10.2 米前面因为字符串排序按字符位比较。CREATE TABLE result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL COMMENT 报名记录ID关联entry表, score DECIMAL(6,3) COMMENT 成绩值径赛单位秒田赛单位米, score_unit VARCHAR(10) COMMENT 成绩单位冗余自event表, rank_no INT COMMENT 名次, is_broken_record TINYINT DEFAULT 0 COMMENT 是否破纪录1是0否, record_by VARCHAR(20) COMMENT 录入人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 成绩表;冗余 score_unit 字段是我比较推荐的做法。虽然项目表里已经有单位了但成绩报表导出时经常要单独查成绩表少一次 JOIN 就少一分出错可能。rank_no 在录入阶段先留空只有裁判确认成绩后才回填。is_broken_record 这个字段看着不起眼却是运动会赛后写奖状的数据来源。2.3 建表顺序与外键约束先班级后运动员建表顺序有讲究。我一般按依赖关系从底往上建先建 class_info再建 athlete运动员所属班级然后是 event、entry、result。外键建议在应用层维护数据库层面只建索引不建物理外键。原因很简单运动会报名期间经常要批量导入运动员数据物理外键在导入时一遇到临时不存在的班级 ID 就会整批失败而业务上我们更希望先把数据导进来再回头排查脏数据。MyBatis-Plus 的田赛/径赛分页查询建议直接用 LambdaQueryWrapper。比如按项目查成绩排行LambdaQueryWrapperResult wrapper Wrappers.lambdaQuery(); wrapper.eq(Result::getEventId, eventId) .isNotNull(Result::getScore) .orderByAsc(Result::getScore); // 径赛秒数越小越好这段代码有两个关键点。一是只查 score 不为空的数据避免报名了但没有成绩的缺赛记录挤在排行榜前面。二是 orderByAsc 只适用于径赛田赛要改成 orderByDesc因为跳得远、扔得远才是第一。很多人在同一个查询里混用这两种排序成绩排行就变成了「用时最长的人排第一」——血泪教训。3. 编排与报名业务锁名单、防重复、批量导入的完整链路3.1 赛程编排的冲突检测一个运动员可以报多个项目但两个项目如果比赛时间重叠就会造成检录冲突。赛程编排这一环我用的是「先查后插」策略拿到运动员的所有报名项目再和这场赛事的其他已排项目比对时间。常见做法是在编排接口写一个时间冲突校验方法。传入的比赛时间字段是 start_time 和 end_time两个项目冲突的判定条件是时间区间有交集。下面这段是我的复现代码假设 event 表有这两个时间字段public boolean checkScheduleConflict(Long athleteId, LocalDateTime start, LocalDateTime end) { ListEntry entries entryMapper.selectList( Wrappers.EntrylambdaQuery().eq(Entry::getAthleteId, athleteId) ); ListLong eventIds entries.stream().map(Entry::getEventId).collect(Collectors.toList()); if (eventIds.isEmpty()) return false; ListEvent events eventMapper.selectList( Wrappers.EventlambdaQuery() .in(Event::getId, eventIds) .le(Event::getStartTime, end) // 已有项目开始时间不晚于新项目结束时间 .ge(Event::getEndTime, start) // 已有项目结束时间不早于新项目开始时间 ); return !events.isEmpty(); }这段冲突检测用的区间重叠判断是两条条件新项目开始时已有项目还没结束新项目结束时已有项目已经开始了。两个条件同时满足才算冲突。很多人只查 le 一条导致「新项目开始前旧项目刚好结束」这种假冲突永远存在。selectList 查出来的是与请求时间段有交集的所有已有项目只要集合非空就说明撞了。3.2 报名截止状态机前端按钮禁用不够后端也要挡实际比赛中经常出现的场景是报名截止日在周三周二晚上还有班主任打电话要加人。这时候前端把报名按钮置灰没用直接调接口照样能录入。我习惯在报名接口里做一层状态校验用 project_stage 字段标记当前赛事的阶段1 报名中、2 编排完成、3 比赛中、4 已结束。if (projectStage.getStageCode() ! 1) { throw new BizException(当前赛事已截止报名); }这个判断放在 Service 层而不是 Controller 层。因为后端接口可能被多个入口调用比如管理端手动添加、Excel 批量导入、学生自助报名校验放在 Service 层才能保证所有入口都被同一套规则拦住。3.3 Excel 批量导入用 EasyExcel 兜住 500 条报名数据一个年级十几个班每班运动员少说二十人手敲录入不现实。项目里我用 EasyExcel 做批量导入模板列设计是班级名称、学号、姓名、性别、报名项目 1、报名项目 2、报名项目 3。性别和项目名称都用中文上传后先翻译成 ID 再落库。PostMapping(/import) public R importAthletes(RequestParam MultipartFile file) { ListAthleteImportDTO list EasyExcel.read(file.getInputStream()) .head(AthleteImportDTO.class) .sheet() .doReadSync(); for (AthleteImportDTO dto : list) { String classId classService.getIdByName(dto.getClassName()); if (classId null) { throw new BizException(班级不存在 dto.getClassName()); } athleteService.saveAthleteWithEntry(classId, dto); } return R.ok(成功导入 list.size() 条报名数据); }这里有三个坑值得提前避开。第一EasyExcel 的 doReadSync 返回的 List 要判空空表直接读会返回空集合后续循环不报错但用户以为成功了。第二班级名称转 ID 时如果查不到要立刻抛异常并提示是哪一行哪个班级名称而不是跳过——跳过会导致班主任事后发现少人又得查半天。第三导出的模板表头必须和 AthleteImportDTO 字段顺序完全对应否则 EasyExcel 按 index 映射会串列。4. 成绩判定与排名算法秒数和米数混排的 Java 实现4.1 田赛取最优成绩径赛取历史最佳径赛通常一个运动员要跑两轮预赛和决赛取最好成绩。田赛则每人跳三次取最远一次。成绩表里每轮都插入记录最后排名的数据源是「每人每项目的最优成绩」。实现上用 SQL 的 MAX/MIN 分组不如在 Java 里做更能控制逻辑。因为径赛取的是 MIN时间越小越好田赛取的是 MAX距离越大越好两条 SQL 逻辑完全相反拼在一起反而难维护。public ListRankItemVO calcRank(Long eventId, String eventType) { ListResult results resultMapper.selectList( Wrappers.ResultlambdaQuery().eq(Result::getEventId, eventId) ); // 每个运动员取最优成绩田赛取最大值径赛取最小值 MapLong, Result bestMap new HashMap(); for (Result r : results) { Result best bestMap.get(r.getEntryId()); if (best null) { bestMap.put(r.getEntryId(), r); } else { boolean better 田赛.equals(eventType) ? r.getScore().compareTo(best.getScore()) 0 : r.getScore().compareTo(best.getScore()) 0; if (better) bestMap.put(r.getEntryId(), r); } } // 按成绩排序田赛降序径赛升序 ComparatorResult byScore (田赛.equals(eventType)) ? Comparator.comparing(Result::getScore).reversed() : Comparator.comparing(Result::getScore); return bestMap.values().stream() .sorted(byScore) .map(...) .collect(Collectors.toList()); }这段代码的核心是把「取最优」和「排序」拆成两个阶段。取最优阶段用 HashMap 按 entryId 分组entryId 对应的是报名记录一个运动员同一个项目只有一条报名记录所以不会丢人。比较成绩时用 compareTo而不是直接相减比大小——因为 BigDecimal 或 DECIMAL 类型在 Java 里相减可能触发精度问题compareTo 是安全做法。4.2 名次并列规则第 2 名空缺还是补位运动会排名有个体育界通行的规则同分同名次下一名空缺。比如两个并列第二那么第三名不存在下一个直接是第四。这套规则在代码里实现起来有一个很妙的循环写法int currentRank 1; for (int i 0; i resultList.size(); i) { if (i 0 resultList.get(i).getScore() .compareTo(resultList.get(i - 1).getScore()) ! 0) { currentRank i 1; // 成绩不同名次等于当前位置1天然实现空缺 } resultList.get(i).setRankNo(currentRank); }这个算法的妙处在于不单独计算并列人数而是靠「不一致时名次跳到当前位置」来实现空缺逻辑。比如第 0、1 名成绩一样都排第 1第 2 名成绩不一样currentRank 变成 3于是第二名空缺第三个人直接排第 3 名。这个处理比「每遇到不同成绩就 count 1」的方法更简洁且天然符合体育竞赛规则。4.3 团体总分积分表用 Map 做班级维度的累加团体分规则一般是每个项目取前六名积分为 9、7、6、5、4、3、2、1看学校规则。按班级累加的时候最直接的做法是查出所有已产生名次的成绩循环里给对应班级加分。我一般会把积分规则做成一个静态数组// 前八名积分规则下标 0 对应第 1 名 private static final int[] SCORE_TABLE {9, 7, 6, 5, 4, 3, 2, 1}; public MapString, Integer scoreByClass(Integer eventId) { ListResult ranked resultMapper.selectList(...按rank_no升序...); MapString, Integer classScoreMap new HashMap(); for (Result r : ranked) { if (r.getRankNo() null || r.getRankNo() SCORE_TABLE.length) continue; String className athleteMapper.getClassByEntryId(r.getEntryId()); classScoreMap.merge(className, SCORE_TABLE[r.getRankNo() - 1], Integer::sum); } return classScoreMap; }merge 方法是这里的关键。第一个参数是 key第二个是新值第三个是合并函数 Integer::sum。循环里每加一次分如果 className 已经存在就把旧值和新值相加如果不存在merge 会创建新 key。这比先 get 再判断 null 再 put 的写法清晰得多也不会漏掉第一个板块的班级。5. 输出到报表MySQL 与 Java 在统计上的分工边界5.1 用 LambdaQueryWrapper 做条件筛选而非手写 XML成绩报表的典型场景是查看某个比赛项目、某个年级、某个班级的成绩明细。这种查询条件组合多变手写 XML 的 标签虽然也不难但 MyBatis-Plus 的 LambdaQueryWrapper 在条件动态拼接上更省事。public PageResultVO pageResult(ResultQueryDTO dto) { LambdaQueryWrapperResult wrapper Wrappers.lambdaQuery(); wrapper.eq(dto.getEventId() ! null, Result::getEventId, dto.getEventId()) .eq(dto.getClassId() ! null, Result::getClassId, dto.getClassId()) .eq(StringUtils.hasText(dto.getGender()), Result::getGender, dto.getGender()) .orderByAsc(Result::getEventId) .orderByAsc(Result::getRankNo); return resultMapper.selectPage(new Page(dto.getPage(), dto.getSize()), wrapper); }注意 eq 方法的第一传参是 boolean 条件只有当条件成立时这条 SQL 片段才会拼接。这是预防 SQL 注入之外最重要的习惯不判断 null 就直接把参数丢进去会导致查询条件永远是「event_id null」结果集为空还查不出原因。5.2 名次与积分的报表导出避免循环查库最影响性能的写法是在循环里调 mapper 查询。比如逐条成绩去查运动员姓名、班级名500 条成绩会触发 500 次 SQL。正确做法是一次查出所有成绩对应的报名记录和运动员然后用 Map 聚合重放。ListResult results resultMapper.selectList(...); ListLong entryIds results.stream().map(Result::getEntryId).collect(Collectors.toList()); MapLong, AthleteVO athleteMap athleteMapper.selectBatchIds(entryIds) .stream().collect(Collectors.toMap(Athlete::getId, a - a));先查出全部 athlete 再转 Map后面填充报表列时走 map.get(entryId) 变成纯内存操作。数据量在几千条以内这个方案的性能是最稳妥的不要为了「省代码」把统计扔给 Java 循环一层一层嵌套。5.3 导出 Excel 报表时表头与数据分离用 EasyExcel 导出成绩报表时我习惯把表头做成 List 数据做成 ListListListListObject rows resultVOList.stream().map(vo - { ListObject row new ArrayList(); row.add(vo.getAthleteNo()); row.add(vo.getAthleteName()); row.add(vo.getClassName()); row.add(vo.getGender()); row.add(vo.getEventName()); row.add(vo.getScore() vo.getScoreUnit()); row.add(vo.getRankNo()); row.add(calcScore(vo.getRankNo())); row.add(vo.getBrokenRecord() 1 ? 是 : 否); return row; }).collect(Collectors.toList()); EasyExcel.write(outputStream).sheet(成绩汇总).head(headers).doWrite(rows);这里有个很实用的小技巧成绩列输出时拼接了单位和分数比如 11.24秒 和 5.68米让看表格的班主任不用猜。但要注意这个字段只能在导出时拼数据库里仍然存纯数值否则排序就废了。6. 实战避坑运动会管理系统里的七条典型翻车记录6.1 重复报名怎么都拦不住现象同一个运动员被多次录入同一个项目成绩统计出来一个人占两个名次团体分虚高。 原因报名接口没有做唯一约束数据库层也没建联合唯一索引前端多次提交或者多端同时操作产生重复数据。 解决entry 表建 UNIQUE (athlete_id, event_id) 唯一索引。代码里插入前先查一次合并成「先查后插」捕获 DuplicateKeyException 后统一返回友好提示。从那以后我所有涉及关联关系的表都先建唯一索引这是成本最低的兜底手段。6.2 男子组出现了女运动员成绩现象男子 100 米项目成绩列表中混入女生数据裁判录入页面看性别标识不明显导致录错。 原因录入成绩接口只校验了 event_id没有校验运动员的性别和该项目分组是否匹配。 解决成绩录入接口里加一道「报名记录级」校验查出 entry 关联的 athlete.gender 和 event.gender_group不一致直接拒绝录入并提示。特别强调不要在页面下拉框里做限制因为后端接口绕开页面还能提交。6.3 径赛时间成绩排序错乱现象100 米成绩排序出现 11.5 秒排在 9.8 秒前面的情况。 原因score 字段类型设成了 VARCHAR数据库排序时按字典序排9.8 以 9 开头排在了 1 开头的 11.5 后面。 解决成绩字段统一 DECIMAL(6,3)排序在 SQL 和 Java 层都按数值处理。已经在跑的项目改字段类型前先备份数据MySQL 改 VARCHAR 到 DECIMAL 时如果原数据格式不干净容易失败。6.4 成绩区间的误判现象录入跳远成绩 5.68 米时被系统提示成绩超出范围但运动员明明有这个水平。 原因项目表里的成绩上限写成了 5.60后来调整规则时只改了文档没改数据库。 解决把项目规则最小成绩、最大成绩统一维护在 event 表后端做范围校验时读库里的配置而不是在代码里写死常量。日常维护时如果改了项目规则用 UPDATE 语句同步数据库别只改代码不碰数据。6.5 报表导出格式全乱现象导出 Excel 后中文姓名乱码或者「名次」列变成科学计数法。 原因EasyExcel 导出时没有指定字符集或者名次字段用了 Excel 默认的数字格式导致超宽列被缩列。 解决调用 EasyExcel.write 时指定 .charset(StandardCharsets.UTF_8)文件名用 URLEncoder.encode 处理后再拼 Content-Disposition。名次列如果要用文本格式就用 CellData 或者自定义 Converter 强制字符串。6.6 并发报名导致成绩被覆盖现象两个裁判同时给同一个运动员录入成绩后录入的直接覆盖先录入的好成绩丢了。 原因成绩录入走的是 UPDATE result SET score ? WHERE id ?没有版本号最后写库的赢。 解决result 表加 version 字段UPDATE 时带上 WHERE version ?影响行数为 0 就提醒「数据已被他人修改请刷新后再录」。这个乐观锁方案比锁表要好用得多也不会阻塞正常录入流程。6.7 团体总分雷同但冠亚军要判先后现象两个班团体总分完全相同但系统显示并列第一学校要求必须决出先后。 原因团体总分只按分数排序没有排序的二次依据。 解决在团体总分排序里加两个条件先比分再比破纪录次数再比金牌数。破纪录次数专门统计 is_broken_record 1 的数量金牌数是 rank_no 1 的数量。这三层排序条件在 SQL ORDER BY 里写清楚即可。7. 验证与交付一套提前演练过的数据流才算真正跑通系统开发完不要急着打包上线我习惯先灌入一份模拟比赛的数据走完整流程建班级、批量导入运动员、关闭报名、编排赛程、录入成绩到最后导出团体总分表全链路跑通一遍才算数。# 一路跑通完整业务链路核心看控制台日志是否有异常堆栈 mvn spring-boot:run # 快速验证成绩排名接口的返回结构 curl http://localhost:8080/api/result/rank?eventId1 # 打包生产环境 jar mvn clean package -DskipTests生产环境部署时我强烈建议把 application.yml 里的数据库连接、文件上传路径、报名截止开关全部外置到 application-prod.yml用--spring.profiles.activeprod启动。这样赛前一天要调整报名截止时间只需要改配置文件重启一次不用重新打包。最后保留一个我自己的交付习惯每次给使用方提供部署包前强制跑一遍「报名—编排—录入—统计」的仿真数据流程把题目数据写成 SQL 脚本留在项目 resources 目录下。这么做一方面是为了确认代码没有低级逻辑错误另一方面是给接手的人留一份活的样例数据——从运动员、报名到成绩都是齐全的别人看代码的时候对着数据理解业务比干看实体类快得多。从那以后我每交付一个 Java 信息系统都强制走一遍这条验证路径省掉了太多赛后现场的道歉电话。希望这份复盘对你有用照着这份流程把系统跑通一遍这套运动会管理系统就真正长在你手里了。本文还有配套的精品资源点击获取