Java田径运动会管理系统:报名、计时、排名、PDF导出一体化
简介:本资源是一个面向高校数据库与Java课程设计的田径运动会信息管理系统,专为计算机专业学生完成课设、课程实验及小型赛事管理实践而开发。系统基于Java语言实现,涵盖运动员管理、赛事编排、成绩录入、排名展示与权限控制等核心功能,采用Swing或JavaFX构建GUI界面,配合MySQL数据库(含taf.sql脚本)实现数据持久化,完整体现面向对象设计与软件工程规范。压缩包共81个文件,含25个可读性良好的Java源码、48个编译后class文件、2个关键jar包(含mysql-connector)、1份PDF课设报告、1个SQL建库脚本及readme说明,结构清晰,便于理解模块划分与运行流程。目前已有2171人学习下载,读者可直接导入Eclipse运行调试,获取完整可执行项目、详细设计文档、数据库初始化方案及系统部署要点,是掌握Java桌面应用开发与数据库集成实践的优质课设范例。
1. 一个跑完800米就自动算出名次、生成成绩单、还能导出PDF的Java后台系统
你有没有试过在高校体育部临时顶班——运动会前两天,Excel手录300+学生报名信息,手动对表查重,计时员用秒表报成绩后,再切回表格敲数据、排序、复制粘贴到Word做奖状?等你熬到凌晨三点导出第7版“最终版”成绩单,发现跳远项目漏了某学院的替补选手……这种靠人力硬扛的田径赛事管理,在2024年真不该再发生了。这个「Java田径运动会管理系统」就是为这类场景而生:它不是教学Demo,而是能真实支撑校级运动会全流程的轻量级信息系统——从运动员报名、项目编组、分道安排、成绩录入,到实时排名、名次判定(含同分处理规则)、PDF成绩单批量生成、Excel导出,全部闭环落地。技术栈干净利落:Spring Boot 2.7 + MyBatis Plus + Thymeleaf + Apache POI + iText7,无前端框架包袱,纯JSP/Thymeleaf服务端渲染,部署即用。适合高校体育教师、院系行政人员、学生会体育部成员,也适合Java初学者拿来做课程设计或毕设原型——代码结构清晰、模块边界明确、数据库设计符合赛事逻辑,改个字段就能上线。
2. 系统架构与核心模块拆解:从数据库ER图到Controller层链路
2.1 数据库设计:紧扣田径赛事业务逻辑的5张核心表
系统采用MySQL 5.7+,共12张表,但真正驱动业务的是以下5张主表。它们不是泛泛的“用户-订单-商品”,而是为田径场景定制的实体关系:
t_athlete:运动员表,含student_id(学号)、name、gender、college_id(学院ID)、class_name(班级),关键字段是is_valid TINYINT(1)——用于标记资格审核状态(0=待审,1=通过,2=取消),避免已退赛选手仍出现在检录名单。t_event:项目表,含event_code(如"100M"、"4x100M"、"JAVELIN")、event_type(0=径赛,1=田赛)、is_team TINYINT(1)(是否团体项目),注意max_participants字段:径赛项目控制每组最多8人,田赛项目则按器械轮次限制人数。t_registration:报名关联表,联合主键(athlete_id,event_id),带status(0=报名中,1=已确认,2=弃权),唯一约束防止重复报名同一项目。t_competition_session:赛程编排表,含session_date、start_time、end_time、venue(场地编号),关键字段session_order INT:用于生成“第1场-男子100米预赛”、“第2场-女子跳高决赛”这类可读性强的场次编号。t_result:成绩表,含registration_id、final_time(秒数,精度0.01)、distance_cm(跳远/铅球单位厘米)、rank、is_valid TINYINT(1)(成绩是否有效,如抢跑、超线判无效),特别设计score_point INT字段:用于后续积分榜计算(如第一名10分,第二名8分),不依赖实时计算。
提示:建表SQL中所有时间字段均使用
DATETIME而非TIMESTAMP,规避MySQL时区自动转换导致的赛程时间错乱问题;所有外键均显式声明ON DELETE RESTRICT,防止误删学院导致运动员记录孤儿化。
2.2 后端分层结构:Controller → Service → Mapper 的职责铁律
项目采用标准Spring Boot分层,但每层都嵌入了田径业务判断:
// com.example.sports.controller.ResultController.java @PostMapping("/result/save") @ResponseBody public ResultVO saveResult(@RequestBody ResultSaveDTO dto) { // 1. 校验:该运动员是否已在此项目中完成检录?(查t_checkin表) if (!checkInService.isCheckedIn(dto.getRegistrationId())) { return ResultVO.fail("未检录,不可录入成绩"); } // 2. 业务校验:径赛成绩不能为负,田赛距离不能为0 if (dto.getEventType() == 0 && dto.getFinalTime() < 0) { return ResultVO.fail("径赛成绩不能为负数"); } if (dto.getEventType() == 1 && dto.getDistanceCm() <= 0) { return ResultVO.fail("田赛成绩必须大于0厘米"); } // 3. 调用Service执行保存+自动排名 resultService.saveAndRank(dto); return ResultVO.success(); }这段代码体现了三层协作的关键点:
- Controller只做参数接收、基础校验、调用入口,绝不碰DAO;
- Service层封装完整业务事务:
saveAndRank()方法内开启@Transactional,先存t_result,再调用rankingService.calculateRanking(dto.getEventId())重新计算该项目所有有效成绩的名次(含同分并列逻辑); - Mapper层严格遵循MyBatis Plus规范:所有查询均用
QueryWrapper构造,避免SQL注入;更新操作使用LambdaUpdateWrapper,字段名由IDE自动补全,杜绝手写字符串字段名导致的运行时异常。
2.3 成绩自动排名算法:处理并列、弃权、无效成绩的三重过滤
名次计算不是简单ORDER BY final_time ASC。系统在RankingServiceImpl.calculateRanking()中实现分步过滤:
// com.example.sports.service.impl.RankingServiceImpl.java public void calculateRanking(Long eventId) { // Step 1: 获取该项目所有【有效】且【非弃权】的成绩(排除is_valid=0和status=2) List<Result> validResults = resultMapper.selectList( new QueryWrapper<Result>() .eq("event_id", eventId) .eq("is_valid", 1) // 成绩有效 .ne("status", 2) // 非弃权状态 ); // Step 2: 按项目类型分组排序逻辑 if (isTrackEvent(eventId)) { validResults.sort(Comparator.comparingDouble(Result::getFinalTime)); } else { validResults.sort(Comparator.comparingInt(Result::getDistanceCm).reversed()); } // Step 3: 手动赋名次(核心:处理并列!) int currentRank = 1; double lastTime = -1.0; // 径赛用 int lastDistance = -1; // 田赛用 for (int i = 0; i < validResults.size(); i++) { Result r = validResults.get(i); if (isTrackEvent(eventId)) { if (i == 0 || Math.abs(r.getFinalTime() - lastTime) > 0.005) { currentRank = i + 1; } r.setRank(currentRank); lastTime = r.getFinalTime(); } else { if (i == 0 || r.getDistanceCm() != lastDistance) { currentRank = i + 1; } r.setRank(currentRank); lastDistance = r.getDistanceCm(); } resultMapper.updateById(r); // 批量更新性能差,此处单条更新确保原子性 } }参数说明:
0.005是径赛并列阈值(5毫秒内视为并列),符合国际田联电子计时精度惯例;reversed()确保跳远/铅球距离越大名次越靠前;setRank()后立即updateById(),避免多线程下排名错乱——这是血泪经验:曾有并发录入导致两人同得“第1名”却无“第2名”。
3. 运行部署实操指南:从JDK配置到Nginx反向代理
3.1 环境准备:JDK 11 + MySQL 5.7 + Maven 3.6(最低可行组合)
系统基于Spring Boot 2.7构建,强制要求JDK 11(非8或17)。原因:iText7 PDF生成模块在JDK 17下存在字体渲染兼容性问题,而JDK 8缺少var关键字导致部分工具类编译失败。验证方式:
# 终端执行 java -version # 输出必须为: # openjdk version "11.0.22" 2024-01-16 # OpenJDK Runtime Environment (build 11.0.22+7-post-Ubuntu-0ubuntu2~22.04.1)MySQL需启用innodb_file_per_table=ON(默认开启),并创建专用数据库:
CREATE DATABASE sports_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'sports_user'@'localhost' IDENTIFIED BY 'StrongPass2024!'; GRANT ALL PRIVILEGES ON sports_system.* TO 'sports_user'@'localhost'; FLUSH PRIVILEGES;注意:
utf8mb4是硬性要求,否则运动员姓名含emoji(如某些社团昵称)会插入失败。
3.2 项目启动:application.yml关键配置项详解
src/main/resources/application.yml中以下配置决定系统能否正确运行:
spring: datasource: url: jdbc:mysql://localhost:3306/sports_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: sports_user password: StrongPass2024! # 必须关闭JPA自动建表,否则会清空你的历史数据! jpa: hibernate: ddl-auto: validate # 仅校验,不创建/更新/删除表 mybatis-plus: configuration: # 开启下划线转驼峰,否则t_athlete_id无法映射到athleteId map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id # 使用雪花算法生成ID,避免自增ID暴露数据量 # 文件上传路径(成绩导入/照片上传) file: upload-path: /opt/sports/uploads/ max-size: 10MB # PDF导出字体路径(Linux环境) pdf: font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc关键点说明:
serverTimezone=Asia/Shanghai:解决MySQL时区与Java时区不一致导致的赛程时间偏移8小时;ddl-auto: validate:开发时可设为update,但生产环境必须为validate,否则一次配置失误可能清空整个数据库;wqy-microhei.ttc:文泉驿微米黑字体,Linux服务器需提前安装(sudo apt install fonts-wqy-microhei),否则PDF中文显示为方块。
3.3 前端访问与初始账号:无需npm build,Thymeleaf直出页面
系统前端完全基于Thymeleaf模板引擎,无需Node.js、无需npm install、无需mvn compile前端资源。所有HTML文件位于src/main/resources/templates/,如:
login.html:登录页,含验证码(基于Spring Security集成)admin/athlete-list.html:运动员管理页,含Excel导入按钮judge/result-input.html:裁判端成绩录入页,按场次动态加载项目
首次启动后访问http://localhost:8080即可看到登录页。默认账号密码:
| 角色 | 账号 | 密码 | 权限说明 |
|---|---|---|---|
| 系统管理员 | admin | admin123 | 可管理所有学院、项目、用户权限 |
| 裁判员 | judge001 | judge2024 | 仅能录入成绩、查看本场次结果 |
| 学院管理员 | col_phy | colphy2024 | 仅能管理本学院运动员报名 |
提示:密码在
UserServiceImpl.initDefaultUsers()中硬编码,首次启动时自动插入。生产环境务必在启动后立即修改!
4. 常见问题排查:那些让你重启三次才找到原因的坑
4.1 现象:登录成功后跳转到空白页,浏览器控制台报Thymeleaf template not found
原因:Thymeleaf默认查找templates/xxx.html,但开发者误将HTML文件放在static/目录下(这是放JS/CSS的),或文件名大小写错误(如Login.htmlvslogin.html)。Linux服务器对大小写敏感,Windows开发机不报错,一上生产就挂。
解决:
- 确认文件路径为
src/main/resources/templates/login.html(全小写); - 检查
pom.xml中thymeleaf-spring5版本是否为3.0.15.RELEASE(与Spring Boot 2.7匹配),旧版本不支持th:block语法; - 清理IDE缓存(IntelliJ:File → Invalidate Caches and Restart)。
4.2 现象:成绩录入后名次始终为0,数据库t_result.rank字段未更新
原因:RankingServiceImpl.calculateRanking()方法未被事务管理器代理。常见于:
- 该方法被本类其他Service方法直接调用(如
this.calculateRanking()),绕过Spring AOP代理; @Service类未被Spring容器扫描到(包路径不在@SpringBootApplication同级或子包)。
解决:
- 在
ResultServiceImpl.saveAndRank()中,改为通过ApplicationContext获取Bean调用:
@Autowired private ApplicationContext context; public void saveAndRank(ResultSaveDTO dto) { // ... 保存成绩 RankingService rankingService = context.getBean(RankingService.class); rankingService.calculateRanking(dto.getEventId()); }- 检查
com.example.sports包结构,确保RankingServiceImpl在com.example.sports.service.impl下,且启动类在com.example.sports包下。
4.3 现象:导出PDF成绩单时中文乱码,显示为口口口或方框
原因:iText7未正确加载中文字体。pdf.font-path配置路径错误,或字体文件权限不足(Linux下/usr/share/fonts/需root权限写入,但Java进程以普通用户运行)。
解决:
- 将字体文件复制到项目资源目录:
src/main/resources/fonts/wqy-microhei.ttc; - 修改配置:
pdf: font-path: classpath:fonts/wqy-microhei.ttc- 在PDF生成工具类中强制指定字体:
PdfFont font = PdfFontFactory.createFont( this.getClass().getClassLoader().getResourceAsStream("fonts/wqy-microhei.ttc"), PdfEncodings.IDENTITY_H, true );4.4 现象:Excel导入运动员信息时,提示“学号格式错误”,但Excel里明明是纯数字
原因:Excel单元格格式为“文本”,但Apache POI读取时自动转为Double(如学号20220001读成2.0220001E7),再转String变成2.0220001E7。
解决:
在AthleteImportService.readExcel()中,对学号列强制按字符串读取:
Cell cell = row.getCell(0); // 第1列是学号 String studentId = ""; if (cell != null) { if (cell.getCellType() == CellType.NUMERIC) { // 强制按文本读取数字型单元格 DataFormatter formatter = new DataFormatter(Locale.CHINA); studentId = formatter.formatCellValue(cell); } else { studentId = cell.getStringCellValue(); } }5. 二次开发实战:3个高频改造需求与代码级实现方案
5.1 需求:增加“电子检录”功能,扫码确认运动员到场
当前系统依赖人工点名,大型运动会易出错。需在运动员列表页增加二维码,裁判扫码后自动标记is_checked_in=1。
改造步骤:
- 数据库新增字段:
ALTER TABLE t_athlete ADD COLUMN is_checked_in TINYINT(1) DEFAULT 0 COMMENT '是否已检录:0-未检录,1-已检录';- 后端新增Controller接口(支持微信扫码):
// com.example.sports.controller.CheckInController.java @GetMapping("/checkin/qr/{athleteId}") public void generateQRCode(@PathVariable Long athleteId, HttpServletResponse response) throws Exception { // 生成带签名的短链接,防恶意刷检录 String sign = DigestUtils.md5Hex(athleteId + "CHECKIN_SALT_2024"); String shortUrl = "http://your-domain.com/checkin/do?aid=" + athleteId + "&s=" + sign; // 使用ZXing生成二维码 BitMatrix bitMatrix = new MultiFormatWriter().encode( shortUrl, BarcodeFormat.QR_CODE, 300, 300 ); MatrixToImageWriter.writeToStream(bitMatrix, "png", response.getOutputStream()); }- 前端增加检录状态列:在
athlete-list.html中添加:
<td th:if="${athlete.is_checked_in == 1}"> <span class="badge bg-success">已检录</span> </td> <td th:if="${athlete.is_checked_in == 0}"> <span class="badge bg-warning">未检录</span> <a th:href="@{'/checkin/qr/' + ${athlete.id}}" target="_blank"> <i class="bi bi-qr-code"></i> </a> </td>关键细节:
CHECKIN_SALT_2024是硬编码盐值,生产环境应替换为配置中心密钥;二维码尺寸300×300像素,适配手机扫码。
5.2 需求:为团体项目(如4×100米)增加“接力棒次”录入
当前t_result表只存一个成绩,无法记录四棒各自用时。需扩展成绩结构。
改造方案:
- 不修改原
t_result表(避免影响现有逻辑),新增t_relay_leg表:
CREATE TABLE t_relay_leg ( id BIGINT PRIMARY KEY AUTO_INCREMENT, result_id BIGINT NOT NULL COMMENT '关联t_result.id', leg_order TINYINT NOT NULL COMMENT '棒次:1/2/3/4', leg_time DECIMAL(5,2) NOT NULL COMMENT '本棒用时(秒)', athlete_id BIGINT COMMENT '本棒运动员ID', INDEX idx_result_id (result_id), FOREIGN KEY (result_id) REFERENCES t_result(id) ON DELETE CASCADE );- 成绩录入页动态渲染:在
result-input.html中,当选择项目为4x100M时,显示4个时间输入框:
<div th:if="${event.eventCode == '4x100M'}"> <div class="mb-3" th:each="leg,iterStat : ${#numbers.sequence(1,4)}"> <label th:text="'第' + ${leg} + '棒用时(秒)'"></label> <input type="number" step="0.01" class="form-control" th:name="'legTime[' + ${iterStat.index} + ']'"/> </div> </div>- Service层保存逻辑:在
ResultService.saveAndRank()中,检测到团体项目则批量插入t_relay_leg:
if (isRelayEvent(dto.getEventId())) { List<RelayLeg> legs = new ArrayList<>(); for (int i = 0; i < dto.getLegTimes().size(); i++) { RelayLeg leg = new RelayLeg(); leg.setResultId(savedResult.getId()); leg.setLegOrder((byte)(i + 1)); leg.setLegTime(dto.getLegTimes().get(i)); legs.add(leg); } relayLegMapper.insertBatchSomeColumn(legs); // 使用MyBatis Plus批量插入 }5.3 需求:导出“学院积分榜”,按总分降序排列,同分按金牌数排序
当前系统只有个人名次,缺乏团体激励维度。需新增统计接口。
实现要点:
- SQL层面用GROUP BY + ORDER BY多级排序,避免Java内存排序(万级数据OOM):
SELECT c.college_name, SUM(r.score_point) as total_score, COUNT(CASE WHEN r.rank = 1 THEN 1 END) as gold_count FROM t_result r JOIN t_athlete a ON r.athlete_id = a.id JOIN t_college c ON a.college_id = c.id WHERE r.is_valid = 1 AND r.status = 1 GROUP BY c.id, c.college_name ORDER BY total_score DESC, gold_count DESC;- Controller返回JSON供前端渲染:
@GetMapping("/report/college-rank") @ResponseBody public ResultVO<List<CollegeRankVO>> getCollegeRank() { List<CollegeRankVO> ranks = reportService.getCollegeRank(); return ResultVO.success(ranks); }- VO对象定义(避免Map泛型丢失类型安全):
@Data public class CollegeRankVO { private String collegeName; private Integer totalScore; private Integer goldCount; private Integer rank; // 计算后的名次 }从那以后我每次接到“加个新功能”的需求,第一件事就是打开ER图.md和application.yml,先看数据要不要加字段、配置要不要改、SQL能不能推给数据库算——而不是一头扎进Controller写逻辑。因为田径比赛的规则不会变,但人写的代码会,让数据库干它擅长的事,Java只做胶水和校验,系统才能稳如跑道上的起跑器。希望帮到你。
本文还有配套的精品资源,点击获取