Java Swing + MySQL 运动会管理系统:从报名到计分的完整实现
2026/9/23 23:47:00 网站建设 项目流程

简介:这是一套面向高校计算机与数据库课程学习者的Java桌面端课程设计资源,以田径运动会管理为业务场景,帮助读者完成从需求分析到编码实现的完整实践。系统基于Java Swing构建图形界面,配合MySQL存储数据,实现运动员、参赛团体、比赛安排与裁判信息的录入和查询,支持按编号或姓名模糊检索,并能统计团体金牌数量、关联查询运动员个人信息与上轮成绩,录入全能赛成绩后自动计算总分并排序;录入环节包含数据完整性与合法性校验,对查询无结果和数据库连接异常均给出提示。压缩包共74个文件,约2.2MB,包含12个java源码、48个class编译文件、5个xml配置、1个sql建库脚本以及课程设计报告docx和必读说明,覆盖源码、数据库脚本与文档三类内容。目前已有188人学习,适合作为课程设计参考或Swing与JDBC综合练习的模板。

1. 运动会管理系统:从手工排表到 Java Swing + MySQL 的落地路径

校运会报名表还在用 Excel 传来传去?裁判手里的成绩单靠微信群喊?这类场景我见过太多,最后都卡在同一个问题上:数据没有统一入口,谁都能改,谁都不敢信。基于 Java Swing + MySQL 的运动会管理系统,本质上是把「报名—编排—计分—公示」这条链路收进一个桌面客户端,用关系型数据库兜住一致性。它适合两类人:一是要做 Java 课程设计、需要一套能跑通、能讲清架构的完整案例;二是小型赛事组织方,想用最低成本把成绩管理从纸面搬到本地。Swing 负责界面,MySQL 负责存储,JDBC 负责连接,这套组合不新,但足够稳,也足够让你把 Java 基础、面向对象、SQL 操作串成一条线。

2. 技术选型与数据库设计:为什么是 Swing + MySQL 而不是别的

2.1 桌面端选 Swing 的现实理由

很多人一听 Swing 就觉得过时,但放到课程设计和小型赛事管理这个场景里,它反而是最省事的选择。Web 方案要配 Tomcat、写前端、处理跨域,一套下来光环境就能劝退一半人。Swing 直接跑在 JVM 上,双击就能启动,不需要额外部署容器。更关键的是,Swing 的组件模型和事件监听机制,天然适合讲清楚「界面—事件—数据」三层怎么分离。

我一般会把界面拆成三类:登录与权限窗口、数据录入窗口、报表展示窗口。每类窗口只做一件事,窗口之间通过控制层传递数据,不直接互相调用。这样做的好处是,后面换数据库或者加导出功能,改动面很小。

// 登录窗口只负责收集输入和触发事件,不处理业务逻辑 public class LoginFrame extends JFrame { private JTextField userField = new JTextField(15); private JPasswordField passField = new JPasswordField(15); private JButton loginBtn = new JButton("登录"); public LoginFrame() { setTitle("运动会管理系统 - 登录"); setLayout(new GridLayout(3, 2, 8, 8)); add(new JLabel("用户名:")); add(userField); add(new JLabel("密码:")); add(passField); add(new JLabel()); add(loginBtn); // 事件交给控制层,窗口本身不查数据库 loginBtn.addActionListener(e -> { String user = userField.getText().trim(); String pass = new String(passField.getPassword()); LoginController.getInstance().doLogin(user, pass, this); }); setDefaultCloseOperation(EXIT_ON_CLOSE); pack(); setLocationRelativeTo(null); } }

这段代码的关键在于:窗口类里没有DriverManager、没有SELECT语句。所有数据库操作都推到LoginController里。参数上,GridLayout(3, 2, 8, 8)表示三行两列、水平和垂直间距各 8 像素,这是为了在不同分辨率下不至于挤在一起。pack()让窗口按内容自适应大小,setLocationRelativeTo(null)保证居中显示。如果你把查询逻辑写进窗口类,后面加一个「记住密码」功能就会牵一发动全身。

2.2 MySQL 表结构:五张核心表撑起整个赛事

运动会管理系统的数据关系并不复杂,但必须提前想清楚。我一般会建五张表:用户表、运动员表、项目表、报名表、成绩表。用户表管权限,运动员表存基本信息,项目表定义比赛项目,报名表关联运动员和项目,成绩表记录最终名次和用时。

表名关键字段说明
t_userid, username, password, rolerole 区分管理员和裁判
t_athleteid, name, gender, dept, phonedept 为院系或班级
t_eventid, event_name, event_type, max_participantsevent_type 区分田赛/径赛
t_registrationid, athlete_id, event_id, reg_time联合唯一索引防重复报名
t_resultid, registration_id, score, rank, record_timerank 为最终名次

建表时有一个容易忽略的点:t_registration上要加UNIQUE KEY uk_athlete_event (athlete_id, event_id)。没有这个约束,同一个人可以反复报名同一个项目,后面统计人数就会翻车。另外t_resultregistration_id外键要设ON DELETE CASCADE,这样删除报名记录时成绩自动清理,不会留下孤儿数据。

CREATE TABLE t_registration ( id INT PRIMARY KEY AUTO_INCREMENT, athlete_id INT NOT NULL, event_id INT NOT NULL, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_athlete_event (athlete_id, event_id), FOREIGN KEY (athlete_id) REFERENCES t_athlete(id), FOREIGN KEY (event_id) REFERENCES t_event(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

utf8mb4是必须的,不然运动员姓名里如果有生僻字就会变成问号。ENGINE=InnoDB支持事务和外键,MyISAM 虽然快但不支持外键,成绩录入这种需要一致性的场景不能用。

2.3 JDBC 连接与连接池:别每次操作都重新连

新手最常见的写法是每个方法里DriverManager.getConnection(),跑起来没问题,但报名高峰期一卡一卡的。原因是每次建立 TCP 连接、验证权限、分配资源,开销不小。我一般会引入一个轻量连接池,比如 HikariCP,或者自己写一个简单的静态连接管理类。

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/sports_meet?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl(URL); config.setUsername(USER); config.setPassword(PASSWORD); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

useSSL=false在本地开发时避免证书警告,serverTimezone=Asia/Shanghai解决时区差 8 小时的问题,characterEncoding=utf8mb4保证中文不乱码。maximumPoolSize=10对课程设计足够,connectionTimeout=3000表示 3 秒拿不到连接就报错,避免界面卡死。注意,连接用完必须close(),但连接池的close()是归还连接而不是真正关闭,所以可以放心用 try-with-resources。

3. 核心功能实现:报名、编排、计分三条链路怎么串

3.1 报名模块:防重复与人数上限的原子操作

报名看起来简单,但有两个坑:一是同一个人重复报名,二是项目人数超限。这两个问题必须在数据库层面解决,不能只靠界面判断。界面判断只能防君子,防不了并发。

public boolean register(int athleteId, int eventId) { String checkSql = "SELECT COUNT(*) FROM t_registration WHERE athlete_id=? AND event_id=?"; String countSql = "SELECT COUNT(*) FROM t_registration WHERE event_id=?"; String maxSql = "SELECT max_participants FROM t_event WHERE id=?"; String insertSql = "INSERT INTO t_registration(athlete_id, event_id) VALUES(?, ?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 = conn.prepareStatement(checkSql); PreparedStatement ps2 = conn.prepareStatement(countSql); PreparedStatement ps3 = conn.prepareStatement(maxSql); PreparedStatement ps4 = conn.prepareStatement(insertSql)) { ps1.setInt(1, athleteId); ps1.setInt(2, eventId); ResultSet rs1 = ps1.executeQuery(); rs1.next(); if (rs1.getInt(1) > 0) { conn.rollback(); return false; // 已报名 } ps3.setInt(1, eventId); ResultSet rs3 = ps3.executeQuery(); rs3.next(); int max = rs3.getInt(1); ps2.setInt(1, eventId); ResultSet rs2 = ps2.executeQuery(); rs2.next(); if (rs2.getInt(1) >= max) { conn.rollback(); return false; // 人数已满 } ps4.setInt(1, athleteId); ps4.setInt(2, eventId); ps4.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }

这里用事务把「查重—查人数—插入」包在一起,setAutoCommit(false)之后,任何一步失败都回滚。参数上,PreparedStatement?占位符不仅防 SQL 注入,还能让 MySQL 复用执行计划。注意rs.next()一定要调用,不然游标不在第一行,getInt(1)会抛异常。如果并发量真的很大,还可以在t_registration上加唯一索引,让数据库直接拒绝重复插入,代码里捕获SQLIntegrityConstraintViolationException即可。

3.2 赛程编排:用策略模式处理田赛和径赛的不同规则

田赛和径赛的编排逻辑不一样。径赛要分道次、按成绩排序;田赛按轮次、取最好成绩。如果写成一堆 if-else,后面加一个「趣味项目」就会很乱。我一般用策略模式,把编排逻辑抽成接口。

public interface ScheduleStrategy { List<ScheduleItem> arrange(int eventId, List<Integer> athleteIds); } public class TrackScheduleStrategy implements ScheduleStrategy { @Override public List<ScheduleItem> arrange(int eventId, List<Integer> athleteIds) { // 径赛:随机分道,每道一人 Collections.shuffle(athleteIds); List<ScheduleItem> items = new ArrayList<>(); for (int i = 0; i < athleteIds.size(); i++) { items.add(new ScheduleItem(athleteIds.get(i), i + 1, 0)); } return items; } } public class FieldScheduleStrategy implements ScheduleStrategy { @Override public List<ScheduleItem> arrange(int eventId, List<Integer> athleteIds) { // 田赛:按报名顺序编号,轮次后续再排 List<ScheduleItem> items = new ArrayList<>(); for (int i = 0; i < athleteIds.size(); i++) { items.add(new ScheduleItem(athleteIds.get(i), 0, i + 1)); } return items; } }

ScheduleItemlane表示道次,order表示出场顺序。径赛用Collections.shuffle打乱后分配道次,田赛按报名顺序给轮次。策略模式的好处是,新增项目类型时只需要加一个实现类,不用改原有代码。这也是 Java 课程报告里可以重点写的设计模式应用点。

3.3 成绩录入与名次计算:一条 SQL 搞定排名

成绩录入后要算名次。很多人会查出来在 Java 里排序,但数据量大时不如直接用 SQL 窗口函数。MySQL 8.0 支持RANK(),一条语句就能出结果。

SELECT r.id, a.name, r.score, RANK() OVER (ORDER BY r.score DESC) AS rank FROM t_result r JOIN t_registration reg ON r.registration_id = reg.id JOIN t_athlete a ON reg.athlete_id = a.id WHERE reg.event_id = ? ORDER BY rank;

RANK()遇到相同成绩会跳号,比如两个第一,下一个就是第三。如果规则是并列不跳号,用DENSE_RANK()。参数?是项目 ID。注意,如果成绩是时间类(径赛),排序要改成ASC,因为时间越短越好。这个细节在课程报告里可以单独写一段,说明不同项目类型的排序方向差异。

4. 避坑与排查:那些让我熬夜的报错和玄学问题

4.1 中文乱码:从数据库到界面全链路排查

现象:运动员姓名在数据库里正常,但 Swing 表格里显示问号。原因通常是 JDBC URL 没加characterEncoding=utf8mb4,或者数据库、表、列的字符集不统一。解决:先确认SHOW VARIABLES LIKE 'character%'输出都是utf8mb4,再检查连接串。如果还不行,在 Swing 的JTable里设置table.setFont(new Font("微软雅黑", Font.PLAIN, 14)),有些默认字体不支持中文。

4.2 连接泄漏:界面卡死但没报错

现象:操作几次后界面无响应,任务管理器里 Java 进程内存一直涨。原因:ConnectionStatementResultSet没有关闭。解决:全部用 try-with-resources,或者确保finally块里close()。连接池的maximumPoolSize设了 10,泄漏 10 次之后就拿不到连接了,表现就是界面卡死。我一般会在DBUtil里加一个getActiveConnections()方法,方便调试时看连接数。

4.3 时区差 8 小时:报名时间对不上

现象:数据库里reg_time比实际时间早 8 小时。原因:JDBC URL 没指定serverTimezone,MySQL 驱动默认用 UTC。解决:URL 加serverTimezone=Asia/Shanghai。如果已经建表,DATETIME字段存的是字面量,不会自动转换,所以改连接串后新数据正常,旧数据需要手动UPDATE修正。

4.4 外键约束导致删除失败

现象:删除运动员时报Cannot delete or update a parent row。原因:t_registration有外键指向t_athlete,且没有设级联删除。解决:要么先删报名记录再删运动员,要么建表时加ON DELETE CASCADE。我一般建议加级联,但要在课程报告里说明这是业务取舍——删除运动员意味着放弃他的所有报名和成绩。

4.5 Swing 事件线程阻塞:点击按钮后界面假死

现象:点击「生成赛程」后界面卡住,直到处理完才恢复。原因:耗时操作写在了ActionListener里,而ActionListener运行在 EDT(事件调度线程)上。解决:用SwingWorker把耗时逻辑放到后台线程,完成后通过done()回调更新界面。

new SwingWorker<List<ScheduleItem>, Void>() { @Override protected List<ScheduleItem> doInBackground() { return scheduleService.generate(eventId); } @Override protected void done() { try { List<ScheduleItem> items = get(); tableModel.setData(items); } catch (Exception e) { JOptionPane.showMessageDialog(null, "生成失败:" + e.getMessage()); } } }.execute();

doInBackground()在后台线程跑,done()在 EDT 跑,这样界面不会卡。参数上,SwingWorker的泛型第一个是返回值类型,第二个是中间结果类型,这里不需要中间结果所以用Void

5. 课程报告怎么写才不像流水账:把设计决策讲成故事

课程报告最容易写成「第一章 需求分析,第二章 数据库设计」这种流水账。我一般会换一个结构:先讲一个具体问题,再讲为什么选这个方案,最后讲怎么验证。比如报名防重复,不要只写「加了唯一索引」,而是写「最初没加索引,测试时同一个人报了三次,后来在数据库层加了联合唯一索引,并在代码里捕获异常返回友好提示」。这样报告有血有肉,老师也能看出你真的跑过。

验证方法上,我习惯用三个层次:单元测试测 DAO 层,界面测试测事件响应,集成测试测完整流程。DAO 层用 JUnit 加内存数据库 H2 跑,不依赖本地 MySQL。界面测试手动点,但要把每个按钮的预期结果写清楚。集成测试从登录开始,走一遍报名、编排、录成绩、查排名,确认数据一致。

一个具体技巧:在t_result表里加一个record_time字段,记录成绩录入的时间戳。这样如果后面发现名次算错了,可以追溯是哪次操作导致的。这个字段在界面上不显示,只在排查时查。我吃过亏,有一次成绩被覆盖,没有时间戳根本不知道是谁改的。后来养成习惯,关键操作表都加create_timeupdate_time,用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动维护。

希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询