news 2026/10/11 4:32:31

Java田径运动会管理系统实战:从表结构到并发报名与成绩排名

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java田径运动会管理系统实战:从表结构到并发报名与成绩排名

简介:面向Java课程设计、毕业设计及运动会信息化管理学习者,这套完整项目实现了运动员管理、赛事安排、在线报名、成绩录入与排名展示、信息发布和权限控制等功能。系统基于Java与MySQL开发,数据存储与管理由MySQL完成,代码注释清晰,配套SQL初始化脚本、readme说明和数据库课设报告;报告详细描述了设计思路、系统架构、功能模块与实现过程,便于复盘整个开发脉络。资源包共81个文件,压缩包大小2.04MB,其中包含25个java源文件、48个class文件、2个jar依赖包(含MySQL驱动)、1个sql建表脚本和1个pdf课程设计文档,用Eclipse可直接导入运行,目录结构清晰,二次开发也比较方便。已有2168人学习浏览,无论是作为课设参考还是项目实战素材,都能帮助巩固Java面向对象编程、数据库操作及软件工程规范,是一份实用且完整的课程设计参考资料。

1. java田径运动会管理系统在管什么:先从一次运动会组织事故说起

嘴上随便说一个 java田径运动会管理系统,很多人第一反应是“不就是报报名、录成绩吗”。但真经历过一次运动会组织事故的人不会这么想。某高校去年春季运动会,秩序册是手工排的,三个项目共用一块场地还排在同一时间,检录处吵成一团;更麻烦的是成绩统计,裁判手填的纸单传到录入组,再往 Excel 里敲,等敲完已经晚上八点,女子跳远和男子铅球还搞混了两条记录。事后复盘,大家发现真正缺的不是 Excel,是一个能把编排冲突、成绩排名规则、数据校验都提前兜住的系统。

这也是 java田径运动会管理系统 最核心的价值:先用技术把“比赛项目能不能一个人报多个、时间会不会撞、田赛径赛分别怎么排名、成绩修改有没有留痕”这些规则落地,再谈管理后台和数据导出。我见过不少同学把这个项目做成纯 CRUD 的增删改查 Demo,最后交上去只能演示,不敢真用。这篇笔记就围绕真实可落地来写,适合两类人:一类是准备拿 Java 做课设或毕设,想选个不烂大街又不太难的题目;另一类是单位或学校确实要办运动会,需要一个轻量系统的人的。后面所有表结构和代码,都是按“能直接跑起来、能经得住一场 500 人规模的比赛”这个标准来写的。会用 Spring Boot + MyBatis + MySQL,前端不做重渲染页面,后台管理用模板或静态页都行。

2. 表结构与领域模型:把比赛项目、报名和成绩拆成可落地的实体

运动会系统的难点不在页面,在数据模型。项目、报名、成绩这三张主表设计对了,后面编排和排名才有地基。很多人喜欢把所有东西塞进一张大表,运动会项目要分田赛、径赛、团体、男女、预决赛,一张表根本装不下,最后只能用字符串硬凑,查询和统计全是坑。

2.1 比赛项目表:为什么用状态字段而不是直接删除

项目表是最容易设计得过简单的表。很多 Demo 里只有 id、项目名称、比赛时间三个字段,这在实际运动会里完全不够用。我一般会给项目表加上项目类型、性别分组、人数上限、轮次和状态,这样前端可以按条件筛选,编排算法也能直接读字段而不是靠猜。

CREATE TABLE sports_event ( id BIGINT AUTO_INCREMENT PRIMARY KEY, event_code VARCHAR(20) NOT NULL COMMENT '项目编号,如A001', event_name VARCHAR(50) NOT NULL COMMENT '项目名称,如男子100米', event_type TINYINT NOT NULL COMMENT '1-径赛 2-田赛 3-团体', gender TINYINT NOT NULL COMMENT '1-男子 2-女子 3-混合', max_participants INT NOT NULL DEFAULT 8 COMMENT '个人项目人数上限,团体项目为每队人数', rounds TINYINT NOT NULL DEFAULT 1 COMMENT '1-预决赛合并 2-预赛+决赛', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-报名中 2-比赛中 3-已结束 4-停用', sort_order INT DEFAULT 0 COMMENT '秩序册排序权重,越小越靠前', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

这里最关键的不是 id 和名称,而是 event_type、rounds 和 status 三个字段。event_type 决定了后面成绩排名走哪条规则:径赛比时间,数值越小越好;田赛比距离或高度,数值越大越好。rounds 决定了这个项目需要录几轮成绩,预赛+决赛就是两轮,而不是一组成绩打天下。status 字段则是一个被很多人忽略的设计:删除项目不要用 DELETE,而是把 status 置为 4,因为历史成绩还要引用这个项目,真删了会导致成绩表变成孤儿数据。这个习惯在运动会系统里尤其重要,比完赛后还要出成绩册,数据要能追溯。

参数上需要注意的是 max_participants:径赛一般取 8 人(一道一人),田赛可以设 12 人,预赛后取前 8 进决赛。如果做团体项目,这个值指的是每队的人数上限,报名时要和队伍数量区分开,别混用。

2.2 报名表:并发报名时怎么用条件更新挡住超员

项目表定好以后,报名表是第一个容易出现并发问题的表。运动会报名是典型的短时高并发场景:通知一发出,几百个学生同时登录报名,如果代码写成“先检查人数再插入”,那并发请求打过来时大概率超员。这个问题后面避坑章节会展开讲,但表结构这里就要先留好伏笔。

CREATE TABLE enrollment ( id BIGINT AUTO_INCREMENT PRIMARY KEY, athlete_id BIGINT NOT NULL COMMENT '运动员ID', event_id BIGINT NOT NULL COMMENT '比赛项目ID', group_code VARCHAR(20) DEFAULT NULL COMMENT '预赛分组编号,由编排算法生成', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-已报名 2-已退赛', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_athlete_event (athlete_id, event_id), KEY idx_event_status (event_id, status) );

报名表唯一索引 uk_athlete_event 是硬约束,同一个运动员同一个项目只能报一次,这个必须由数据库兜底,不能只靠 Java 代码判断。索引 idx_event_status 是给管理端“查某个项目报名了多少人”用的,没有这个索引,随着报名数据增长,后台查询会越来越慢。

运动员信息建议单独建 athlete 表,包括姓名、性别、学号、院系、手机号,不要塞进 user 表。原因有两个:一是运动员可能没有登录账号,是报名负责人代录的;二是 user 表将来要接权限,职责分离更清晰。字段上注意学号要加唯一索引,成绩册打印时要按学号排序。

2.3 成绩表:复合主键与唯一索引的设计细节

成绩表是整个系统里最容易返工的表。见过有人把“一次比赛成绩”设计成一行,包含第一次成绩、第二次成绩、决赛成绩,这是把 Excel 思维带进了数据库。田径比赛每个项目可能有预赛、决赛,田赛还有多次试跳、试投,正确做法是每条成绩单独一行,用轮次和 attempt_no 区分。

CREATE TABLE score_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, enrollment_id BIGINT NOT NULL COMMENT '报名记录ID', event_id BIGINT NOT NULL COMMENT '项目ID', athlete_id BIGINT NOT NULL COMMENT '运动员ID', round_no TINYINT NOT NULL DEFAULT 1 COMMENT '轮次:1-预赛/第一轮 2-决赛/第二轮', attempt_no TINYINT NOT NULL DEFAULT 1 COMMENT '田赛第几次试跳/试投;径赛固定为1', raw_result VARCHAR(20) NOT NULL COMMENT '原始成绩字符串,如11.23秒 / 6.58米', score_value DECIMAL(10,3) NOT NULL COMMENT '可比较数值,径赛越小越好,田赛越大越好', rank_no INT DEFAULT NULL COMMENT '本组名次,由排名算法回填', is_valid TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效 0-犯规', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_enrollment_round_attempt (enrollment_id, round_no, attempt_no) );

这里有两个字段容易被忽视。第一个是 score_value,它把成绩统一转成可比较的数值,径赛直接存秒数,田赛存米数,这样排名时不需要解析字符串。很多系统直接用 raw_result 排序,最后排出来“10.2秒”比“9.8秒”大,看着没问题,但一旦出现“11.1”和“9.90”这种,字符串排序就会翻车。第二是 uk_enrollment_round_attempt 唯一索引,它保证同一个人、同一轮、同一次尝试不会重复录入。裁判重复提交时,数据库会直接报 Duplicate 错误,而不是生成两条互相矛盾的成绩。

3. 编排与成绩处理:从赛程冲突检测到田赛径赛排名规则

表结构准备好之后,接下来是系统的核心业务逻辑:批量导入报名、编排赛程、统计成绩。这一章不写页面,只写服务层和算法,因为这几个环节最容易出逻辑错误。

3.1 批量导入报名的检验顺序:先查项目再插数据

运动会报名经常由院系负责人统一提交 Excel。批量导入最容易踩的坑是一次性把几千行插进去,遇到一条错误数据整个事务回滚,好的全没了,又得重传。我更推荐逐行校验、逐行插入,把每行的错误原因记录下来返回给用户。

public ImportResult importEnrollments(MultipartFile file) { List<EnrollmentError> errors = new ArrayList<>(); int successCount = 0; List<EnrollmentExcelRow> rows = ExcelReader.read(file); for (int i = 0; i < rows.size(); i++) { EnrollmentExcelRow row = rows.get(i); try { SportsEvent event = eventMapper.selectByCode(row.getEventCode()); if (event == null) { errors.add(new EnrollmentError(i + 1, "项目编号不存在:" + row.getEventCode())); continue; } if (event.getStatus() != 1) { errors.add(new EnrollmentError(i + 1, "项目当前不在报名期:" + event.getEventName())); continue; } int count = enrollmentMapper.countByEventId(event.getId()); if (count >= event.getMaxParticipants()) { errors.add(new EnrollmentError(i + 1, "报名人数已满:" + event.getEventName())); continue; } enrollmentMapper.insert(buildEnrollment(row, event)); successCount++; } catch (DuplicateKeyException e) { errors.add(new EnrollmentError(i + 1, "该运动员已报名此项目")); } } return new ImportResult(successCount, errors); }

这段代码的关键是校验顺序。先查项目是否存在,再查状态,再查人数,最后才插入。如果把人数校验放在项目校验前面,项目编号写错了也会去 count 一次,浪费查询不说,错误提示还不准确。逐行 try-catch 的作用是让单条错误不影响整批数据,前面成功的行照样入库,返回结果里带上 Excel 行号,用户改错时能直接定位。

批量导入的 Excel 模板列顺序建议固定为:学号、姓名、性别、项目编号、备注。性别不要用“男/女”,用“1/2”更不容易出编码问题。项目编号由系统生成,和项目名称一一对应,这样即使项目名称改了也不影响历史导入数据。

3.2 赛程冲突检测:时间区段重叠判断的边界处理

编排赛程是运动会系统里最体现“管理”价值的功能,也是很多人会直接跳过的功能。做过真实运动会的人都深有体会:一个人报了三四个项目,如果两个项目的时间段重叠,检录时就得二选一,赛后又来找裁判长理论。常见做法是编排时先列出所有运动员的报名项目,然后依次给每个项目分配时间段,分配前检测该运动员是否已有冲突。

public boolean hasTimeConflict(LocalDateTime startTime, LocalDateTime endTime, List<ScheduleItem> existingSchedules) { for (ScheduleItem item : existingSchedules) { LocalDateTime existingStart = item.getStartTime(); LocalDateTime existingEnd = item.getEndTime(); if (startTime.isBefore(existingEnd) && endTime.isAfter(existingStart)) { return true; } } return false; }

这个重叠判断的写法要注意两个边界:一是两个时间段首尾相接的情况,比如 9:00-9:30 和 9:30-10:00,按上面的逻辑,startTime.isBefore(existingEnd) 在 9:30 时返回 false(因为不早于),所以不认为冲突,这符合运动会实际——前一个项目结束后运动员可以赶去下一个检录,只要留出从场地到检录处的移动时间即可。二是完全相等的时间段,isBefore 和 isAfter 同时为 true,能正确识别冲突。

编排算法我通常会按场地分开处理。径赛项目按时间线排,田赛项目单独排,因为不同场地可以并行比赛。如果两个径赛项目重叠,系统会在编排阶段就给出提示,不会等到比赛当天才发现。参数上,相邻径赛项目之间的时间间隔至少要留 10 分钟,用于检录和疏散;同一运动员的两个项目如果只有 10 分钟间隔,系统应该给出黄色警告,而不是直接拦截,因为有些短距离项目确实能跑完。

3.3 成绩排名:田赛取最优、径赛取最短的规则实现

成绩排名看起来简单,实际上是最容易写错规则的地方。径赛是预赛和决赛分开排名,决赛成绩直接决定名次;田赛要把每个运动员所有试跳、试投的最优成绩取出来,再按最优成绩排名。如果代码里只写一个 sort,那田径规则就全乱了。

public List<ScoreRankVO> rankEvent(Long eventId) { SportsEvent event = eventMapper.selectById(eventId); List<ScoreRecord> records = scoreMapper.selectValidByEventId(eventId); Map<Long, List<ScoreRecord>> groupedByAthlete = records.stream() .collect(Collectors.groupingBy(ScoreRecord::getAthleteId)); List<ScoreRankVO> ranks = new ArrayList<>(); groupedByAthlete.forEach((athleteId, recordList) -> { if (event.getEventType() == 2) { // 田赛:取最优成绩,数值越大越好 recordList.sort(Comparator.comparing(ScoreRecord::getScoreValue).reversed()); } else { // 径赛:取最短时间,数值越小越好 recordList.sort(Comparator.comparing(ScoreRecord::getScoreValue)); } ScoreRecord best = recordList.get(0); ranks.add(new ScoreRankVO(athleteId, best.getScoreValue(), best.getRawResult())); }); if (event.getEventType() == 2) { ranks.sort(Comparator.comparing(ScoreRankVO::getScoreValue).reversed()); } else { ranks.sort(Comparator.comparing(ScoreRankVO::getScoreValue)); } ranks.forEach(r -> r.setRank(ranks.indexOf(r) + 1)); return ranks; }

这段代码的关键是 score_value 的可比较性。田赛里“6.58米”存成 6.580,径赛里“11.23秒”存成 11.230,排序时直接用数值比较,完全不需要解析字符串。注意字段类型选了 DECIMAL(10,3),是因为田径成绩要精确到百分之一秒或厘米,float 和 double 存在精度问题,稍微多一点数据就会出现 11.229999 这种诡异结果。

团体项目的排名规则不一样,它不直接比较个人成绩,而是要把个人名次换算成分数,比如第一名 9 分、第二名 7 分,再按总分排团队名次。这个逻辑不适合塞在 score_record 表里,我一般单独建积分规则表,把“名次-分数”的映射存进去,方便不同单位按照自己的规则调整,而不是写死在代码里。

4. 权限与审计:三类角色如何共用一套成绩修改流程

运动会系统不需要很复杂的权限体系,但也不能做成谁都能改成绩。常见做法是分管理员、裁判、普通用户三类角色。管理员管项目和用户,裁判只能录成绩和改自己负责的项目,普通用户只能看秩序册和成绩。这里最大的坑是权限控制不到位,导致裁判能改其他裁判负责的项目成绩,出问题后还查不到是谁改的。

4.1 三张权限表:为什么角色权限要拆开而不是写死字段

很多入门项目会在 sys_user 表里加一个 role_type 字段,1 是管理员,2 是裁判,3 是普通用户,简单直接。这个做法在用户量只有几个人的系统里没问题,但一旦出现“裁判长”这种半个管理员角色,就得改表结构。我宁可一开始就多建两张表,把用户和角色拆开,后面加角色不用动表结构。

CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-启用 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sys_role ( id BIGINT AUTO_INCREMENT PRIMARY KEY, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT 'ADMIN/JUDGE/ATHLETE', role_name VARCHAR(50) NOT NULL ); CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) );

这里没有做细粒度的 permission 表,因为运动会的操作就那么几类,角色足够覆盖。但是 user 和 role 拆分后,将来如果要加权限,只需要再建 role_permission 表,不影响现有表结构。用户表里 status 字段是给“该用户被禁用”留后门的,比如裁判离职或学生毕业,不用删账号,把状态改成 0 就行,历史操作记录还能追溯。

一个小提示:如果运动会规模特别小,比如只有三五个人管理,三张表确实有点重,可以在 sys_user 里直接加 role_type。但既然要做成可复用的系统,拆开更稳妥。我见过的翻车案例里,至少有一半是因为图省事把权限写死,后来需求一变就得重构。

4.2 URL级权限:用 Spring Security 控制接口访问范围

表设计好后,接口权限控制建议直接交给 Spring Security,而不是自己在每个 Controller 里判断当前用户角色。Spring Security 的过滤器链配置能统一拦截,方法级别还能用注解精确控制。下面这段配置适合当前场景。

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/judge/**").hasAnyRole("ADMIN", "JUDGE") .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(ex -> ex .authenticationEntryPoint((req, resp, e) -> { resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); })); return http.build(); } }

需要注意 hasRole 里面的角色名会自动加上“ROLE_”前缀,如果你的角色编码是 ADMIN,那这里直接写 hasRole("ADMIN") 就行。关于裁判只能改自己负责的项目这种数据权限,URL 级别控制不了,需要在 Service 层加判断。常见做法是给裁判分配项目权限表,或者把裁判和项目的关联关系放在内存缓存里,修改成绩时先校验当前裁判是否有该项目的操作权限。

还有一个细节是登录方式。运动会系统通常是内部使用,做成无状态 JWT 会更方便,前端拿到 token 后每次请求带上。无状态会话配置如果省略,Spring Security 默认启用 session,前后端不分离时能用,分离了就经常出现 403。上面加了 STATELESS,整体逻辑更清晰。

4.3 成绩修改审计:把操作记录和成绩快照一起存下来

成绩修改必须有留痕,这个是血泪经验换来的。比赛结束后一旦有人质疑成绩,你得能查出来“谁在什么时间把 11.23 改成了 11.43”。只记“某某修改了成绩”是不够的,要把旧值和新值都存下来,最好连修改原因一起存。

CREATE TABLE score_audit_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, score_record_id BIGINT NOT NULL COMMENT '成绩记录ID', operator_id BIGINT NOT NULL COMMENT '操作人用户ID', old_value VARCHAR(20) NOT NULL COMMENT '修改前原始成绩', new_value VARCHAR(20) NOT NULL COMMENT '修改后原始成绩', old_score_value DECIMAL(10,3) NOT NULL COMMENT '修改前可比较数值', new_score_value DECIMAL(10,3) NOT NULL COMMENT '修改后可比较数值', reason VARCHAR(200) DEFAULT NULL COMMENT '修改原因', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

审计日志这块,常见的落地方式有两种。一种是在 Service 方法里动手写,成绩更新前先查旧值,再插入一条日志,放在同一个事务里,简单可靠。另一种是用 AOP 切面拦截 update 方法,自动记录旧值和新值,代码侵入小,但是调试起来比较绕。我更推荐前者,因为成绩修改频率不高,多写一行代码不算负担,事务保证也不会丢日志。要注意的是,日志表里的 old_value 和 new_value 一定要存字符串形式的原始成绩,因为最终打印成绩册和成绩公告时,展示的是“11.23秒”而不是数值,只存数值会让你回溯时还得重新格式化。

5. 部署与避坑:运动会系统常见的 5 个踩坑现场

这一章从把项目跑起来到真正上线,挑 5 个我见过最多、也最容易让新手崩溃的问题。每条按现象、原因、解决写,可以直接对照着排查。

5.1 翻车现场:Maven 依赖冲突,Spring Boot 一启动就报 ClassNotFound

现象:项目在本地 IDE 里跑得好好的,用mvn spring-boot:run启动时突然报NoClassDefFoundError,或者提示BeanCreationException: Error creating bean with name 'xxx'。很多人第一反应是去清 IDE 缓存,折腾半天发现没用。

原因:最常见的是 POI 和 Spring Boot 自带依赖的版本冲突。导入 Excel 用的 POI 依赖poi-ooxml会传递引入commons-compress、xmlbeans等库,和 Spring Boot 的 starter-web 里某个传递依赖版本不匹配。Maven 默认采用“就近原则”选版本,但冲突时不会报错,只在运行时类加载阶段才炸出来。

解决:先跑一下mvn dependency:tree -Dincludes=org.apache.poi看 POI 到底依赖了哪些库、版本是多少,再和 Spring Boot 管理的版本对比。最简单的处理是用exclusion排除冲突库,让 Spring Boot 的版本生效。比如排除poi-ooxml自带的xmlbeans,改成显式声明一个和 Spring Boot 兼容的版本。这个问题的通用排查思路是看清谁引起的 Caused by,maven dependency:tree 是最靠谱的工具,别靠猜。

5.2 踩坑:中文文件名导致上传的 Excel 读不出来

现象:本地 Windows 开发环境下,上传“运动会报名表.xlsx”一点问题没有,部署到 Linux 服务器之后,同一个文件上传就报FileNotFoundException或者java.nio.file.InvalidPathException。

原因:Windows 本地文件系统用 GBK 编码,Linux 用 UTF-8,而 MultipartFile 拿到原始文件名后如果没有做统一编码处理,直接拼到路径上就会乱码。更隐蔽的是,有些前端上传组件会对文件名做 URL 编码,后端拿到的名字和被编码过的名字对不上,导致文件找不着。

解决:不管原始文件名叫什么,保存到服务器时统一用 UUID 重命名,把后缀保留下来。原始文件名存到数据库字段里用于回显,文件在磁盘上的名字和用户看到的名称彻底分开,这就从根上避开了中文编码问题。代码上就是String newFileName = UUID.randomUUID().toString() + "." + suffix;那一行的事,但很多人就是在这行上省事才掉的坑。

5.3 踩坑:并发报名时人数超限,MyBatis 先查再插的经典翻车

现象:报名截止前最后 10 分钟,大量学生同时提交报名,后台查出某个项目报名人数只有 7 人,但实际插入成功了好几条,最终人数超出 8 人上限。很多人以为是数据写重复了,其实不是。

原因:count(*)查询和insert之间不是原子操作。两个请求同时查到 count=7,都判断没满,然后各自插入,最终就变成了 9 人。数据库自带唯一索引只能拦住“同一个人重复报名”,拦不住“不同人同时超员”。

解决:有两个靠谱方案。一是给 enrollment 表加一个selected_count字段放在 sports_event 表里,更新时用UPDATE sports_event SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < max_participants,受影响行数为 0 说明满员,事务回滚。二是用SELECT ... FOR UPDATE锁住项目行,让并发请求串行化。运动会系统并发量不算大,条件更新方案更简单,不需要锁事务的复杂处理。

5.4 踩坑:导出成绩册时没参赛的人被静默过滤

现象:导出运动会成绩册时发现人数少了一截,尤其是报了名但没来参赛的运动员,整个人的记录从成绩册里消失了。看起来像是数据被误删。

原因:查询成绩时很多人习惯直接查 score_record 表,条件是is_valid = 1。未参赛的人根本没有成绩记录,自然查不出来。但从管理角度讲,成绩册应该列出所有报名运动员,未参赛的显示“缺赛”或“无成绩”,而不是直接不出现。

解决:导出成绩册时以enrollment表为主表,LEFT JOIN score_record,这样即使没有成绩也能保留下这行记录。代码上注意 join 条件要带上轮次和项目 ID,避免一个运动员多条成绩记录时把数据拆散成多行。

5.5 踩坑:上传文件超过 1MB 被拒,Excel 模板还没读就返回 500

现象:上传一个 2MB 的 Excel 文件,接口直接报MaxUploadSizeExceededException,前端提示 500。模板里只要放一张 Logo 图片就必中招。

原因:Spring Boot 的 multipart 默认最大文件大小是 1MB,超过就抛异常。项目里配了spring.servlet.multipart.enabled,但忘记调大小。

解决:在 application.yml 里把限制放开。

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB

这里 max-file-size 是单个文件大小,max-request-size 是一次请求所有文件的总大小。如果前端一次只传一个文件,两个都设置成 10MB 就行。要注意的是,这两个参数只对 Servlet 标准的 MultipartFile 生效,如果用了第三方组件,可能需要另外配置,落地前先用一个 5MB 的测试文件验证一下。

6. 验证与进阶用法:把系统从“能交差”做到“真有用”

系统写完不等于能用,尤其运动会系统这种“平时不用、用一次就必须不能掉链子”的场景。我的习惯是上线前按下面这张清单过一遍,缺哪补哪。

验证项检查方法通过标准
并发报名用 JMeter 或 ab 模拟 50 个线程同时报名同一项目人数不超过上限,无 500 错误
编排冲突造一个运动员报两个时间重叠的项目系统给出冲突提示,不生成秩序册
成绩排名分别录入径赛和田赛数据,检查排序结果径赛最小值为第一名,田赛最大值为第一名
裁判越权用普通裁判账号尝试修改非负责项目成绩接口返回无权限
审计留痕修改一条成绩,查询 score_audit_log新旧值、操作人、时间完整

最后说一个我自己的实战教训。当初给某单位做类似系统时,我自认为功能都测过了,结果比赛当天上午报名并发一冲,才发现数据库连接池默认才 10 个连接,大量请求排队等连接,页面转圈卡死。后来在配置里把连接池调成 20,并加了 HikariCP 的 maximum-pool-size 参数才稳住。这件事之后我养成了习惯:凡是这种“短时高并发、之后长时间闲置”的系统,一定要在交付前做一次并发冒烟测试,不要等现场翻车。

进阶用法上,如果想把系统做得更贴近真实赛事,可以把历届运动会成绩单独建一张 history 表,比赛结束后把本届成绩快照存进去,下次办赛时可以拉出来做校记录对比。这不需要改现有表结构,只要加一个定时任务或赛后归档按钮,纯粹是数据利用率的问题。还有秩序册生成,可以预留一个模板接口,把编排好的项目、时间、分组信息灌进 Word 或 PDF 模板,省去手动排版。这两件事都不难,但能把系统从“管理工具”变成“赛事数据资产”。希望这篇 java田径运动会管理系统 的实战笔记能帮到你,下次办赛别再熬夜手排秩序册。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 4:32:28

用C#实现OPC UA客户端并存入SQL Server的完整指南

简介&#xff1a;OPC UA客户端与SQL Server数据落地的C#实践项目&#xff0c;面向工业自动化、物联网数据采集及企业级数据集成的开发者&#xff0c;也可作为高校自动化专业项目的参考。项目借助OpcUaHelper开源库简化OPC UA协议交互&#xff0c;完整演示从服务器读取数据、以“…

作者头像 李华
网站建设 2026/10/11 4:32:26

基于NSGA-Ⅲ的梯级水电火电联合调度多目标优化及Matlab实现

在电力系统优化调度这块待久了&#xff0c;会经常看到一类需求&#xff1a;把梯级水电和火电机组放在同一个模型里做联合调度。水电站之间有上下游水力联系&#xff0c;火电这边又有煤耗、排放、爬坡约束&#xff0c;再加上负荷平衡和水库库容限制&#xff0c;模型一搭就是多目…

作者头像 李华
网站建设 2026/10/11 4:31:55

SCUC安全约束机组组合建模与求解:从MILP原理到YALMIP+Gurobi实践

简介&#xff1a;考虑安全约束机组组合的电力系统机组组合&#xff08;含经济调度&#xff09;优化资源包&#xff0c;面向电力系统优化研究人员与电气工程专业学生&#xff0c;基于IEEE 30节点测试系统&#xff0c;结合MATLAB与CPLEX求解含安全约束的机组组合与经济调度问题&a…

作者头像 李华
网站建设 2026/10/11 4:31:51

grep匹配不到返回码为1?详解Shell脚本set -e与管道退出码陷阱

1. 问题现象复盘&#xff1a;一条看似“报错”的 Shell 脚本先还原一下我当时的场景。我记得是在处理一个日志清洗的流程&#xff0c;脚本里有一段逻辑&#xff1a;从一堆访问日志里用 grep 过滤出包含特定用户标识的行&#xff0c;然后做后续统计。当时图省事&#xff0c;写完…

作者头像 李华
网站建设 2026/10/11 4:31:10

Android Gradle下载编译失败全链路排错指南:从环境到缓存

你正兴高采烈地打开一个刚拉回来的 Android 项目&#xff0c;IDE 还在转圈导入&#xff0c;Gradle sync 的进度条就停在某个百分数不动了。几秒后&#xff0c;Event Log 里多了一大串红色报错&#xff0c;有 Could not resolve&#xff0c;有 Connection refused&#xff0c;还…

作者头像 李华