简介这是一套面向高校计算机与数据库课程学习者的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_result的registration_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;utf8mb4是必须的不然运动员姓名里如果有生僻字就会变成问号。ENGINEInnoDB支持事务和外键MyISAM 虽然快但不支持外键成绩录入这种需要一致性的场景不能用。2.3 JDBC 连接与连接池别每次操作都重新连新手最常见的写法是每个方法里DriverManager.getConnection()跑起来没问题但报名高峰期一卡一卡的。原因是每次建立 TCP 连接、验证权限、分配资源开销不小。我一般会引入一个轻量连接池比如 HikariCP或者自己写一个简单的静态连接管理类。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/sports_meet?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4; 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(); } }useSSLfalse在本地开发时避免证书警告serverTimezoneAsia/Shanghai解决时区差 8 小时的问题characterEncodingutf8mb4保证中文不乱码。maximumPoolSize10对课程设计足够connectionTimeout3000表示 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 { ListScheduleItem arrange(int eventId, ListInteger athleteIds); } public class TrackScheduleStrategy implements ScheduleStrategy { Override public ListScheduleItem arrange(int eventId, ListInteger athleteIds) { // 径赛随机分道每道一人 Collections.shuffle(athleteIds); ListScheduleItem 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 ListScheduleItem arrange(int eventId, ListInteger athleteIds) { // 田赛按报名顺序编号轮次后续再排 ListScheduleItem items new ArrayList(); for (int i 0; i athleteIds.size(); i) { items.add(new ScheduleItem(athleteIds.get(i), 0, i 1)); } return items; } }ScheduleItem里lane表示道次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 没加characterEncodingutf8mb4或者数据库、表、列的字符集不统一。解决先确认SHOW VARIABLES LIKE character%输出都是utf8mb4再检查连接串。如果还不行在 Swing 的JTable里设置table.setFont(new Font(微软雅黑, Font.PLAIN, 14))有些默认字体不支持中文。4.2 连接泄漏界面卡死但没报错现象操作几次后界面无响应任务管理器里 Java 进程内存一直涨。原因Connection、Statement、ResultSet没有关闭。解决全部用 try-with-resources或者确保finally块里close()。连接池的maximumPoolSize设了 10泄漏 10 次之后就拿不到连接了表现就是界面卡死。我一般会在DBUtil里加一个getActiveConnections()方法方便调试时看连接数。4.3 时区差 8 小时报名时间对不上现象数据库里reg_time比实际时间早 8 小时。原因JDBC URL 没指定serverTimezoneMySQL 驱动默认用 UTC。解决URL 加serverTimezoneAsia/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 SwingWorkerListScheduleItem, Void() { Override protected ListScheduleItem doInBackground() { return scheduleService.generate(eventId); } Override protected void done() { try { ListScheduleItem 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_time和update_time用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动维护。希望帮到你。本文还有配套的精品资源点击获取