☰
运动会成绩管理系统数据库设计:从SQL建表到JDBC增删改查实战
2026/10/13 18:50:32 网站建设 项目流程

简介:这套运动会成绩管理系统是安徽工程大学数据库课程设计的完整项目,基于C#与SQL Server开发,面向高校数据库课程设计学生以及需要借鉴管理信息系统开发思路的开发者。系统围绕运动会实际工作场景展开,覆盖项目、运动员、成绩的登记管理,支持根据预赛成绩自动生成预(决)赛名单,并具备统计与结果输出功能,可满足单位及个人成绩查询需求。压缩包共72个文件,以C#源码(.cs)、界面资源(.resx、.resources)、SQL Server数据库文件(.mdf、.ldf)及项目解决方案(.sln)为主,另附使用说明Word文档,整体约965KB,结构清晰便于导入。界面文件已注明可用VS2019打开,方便直接运行调试或参考整体设计思路。目前已有1886人学习下载,是理解数据库课程设计流程、练习C#与SQL Server整合开发的实用参考资料。

1. 运动会成绩管理系统是什么:从课程设计标题到可交付工程的解读

拿到“安徽工程大学数据库课程设计运动会成绩管理系统.zip”这个标题,第一反应是:这不就是一个课程设计源码包吗?不过它背后是一个很典型的需求——把一场运动会的运动员、项目、成绩和团体总分用数据库管起来,而不是靠 Excel 登记。对正在做数据库课程设计的人来说,它展示了增删改查怎么落到具体业务里;对想复习 SQL 的人,它是一个现成的练习对象。这套系统通常走 JSP + Servlet + MySQL 路线,核心是几张业务表加一张成绩关联表,再配合统计 SQL。你要想照着自己做一套,从建库到最后部署,这篇都能给你一条能复现的路径。

2. 先立住数据库:运动会成绩管理系统的三张核心表与初始化 SQL

2.1 一张成绩表不够:运动员、项目、成绩之间为什么是多对多

在写 Java 代码前,先把业务画成实体关系。一场运动会里,一个运动员可以报多个项目,一个项目又有多个运动员参加,这是典型的多对多关系。如果只建运动员表、项目表、成绩表,成绩表就是中间表:每条成绩记录包含“哪个运动员、哪个项目、成绩值、名次、得分”。再加一个班级表,是因为团体总分要以班级聚合,否则统计总分只能按人算,后面想加“每班限报两人”这样的限制会很吃力。

字段设计如果只做表面功夫,会留下不少雷。比如成绩值,径赛项目记录的是“秒”,田赛项目记录的是“米/厘米”,计数项目记录的是“次”。如果不加一个“项目类型”或“成绩方向”字段,排名时会出大问题:100 米是数值越小越好,实心球是数值越大越好。我在设计项目表时通常放一个score_direction字段,1 表示越小越好(时间类),2 表示越大越好(距离/计数类)。这是答辩高频问题:你怎么处理不同项目的成绩方向?表里留了这个字段,回答时就能直接指着 SQL 说。

  • 班级表(class):班级名称、所属系部
  • 运动员表(athlete):运动员编号、姓名、性别、班级外键
  • 项目表(sport_item):项目编号、名称、类别、成绩方向、当前纪录
  • 成绩表(score):成绩值、名次、积分、是否破纪录,并外键关联运动员和项目

这样的设计还有一个好处:扩展性。“每个班每个项目最多两人参赛”可以在统计 SQL 里过滤,不需要改表;“破纪录”可以通过对比record_value判断,也不需要额外存。后面做的团体总分,基本就是围绕这些表 JOIN。

索引上,我建议给athlete表的class_id、score表的athlete_id和item_id都建普通索引。外键约束会自动为外键列建索引,但成绩表里按项目查排名的场景特别多,单独建一个(item_id, score_value)的联合索引会让查询排序少走一次文件排序。课程设计要求不高时可能感觉不到,但数据量一旦上 500 条,差别就很明显。

2.2 用 SQL 建库建表:完整初始化脚本与注释

我一般会在源码包的 database 目录放一个sports_game.sql,内容从建库到建表再到插入测试数据。下面这份是去掉测试数据后的核心建表脚本,你可以直接执行。注意统一使用 InnoDB 和 utf8mb4,否则后面中文乱码,外键也不好加。

DROP DATABASE IF EXISTS sports_game; CREATE DATABASE sports_game DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE sports_game; -- 班级/院系表,团体总分按它聚合 CREATE TABLE class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE COMMENT '班级名称', department VARCHAR(50) DEFAULT '' COMMENT '所属系部' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 运动员表 CREATE TABLE athlete ( id INT PRIMARY KEY AUTO_INCREMENT, athlete_no VARCHAR(20) NOT NULL UNIQUE COMMENT '运动员编号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT DEFAULT 1 COMMENT '1男 2女', class_id INT NOT NULL, CONSTRAINT fk_athlete_class FOREIGN KEY (class_id) REFERENCES class (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 比赛项目表 CREATE TABLE sport_item ( id INT PRIMARY KEY AUTO_INCREMENT, item_no VARCHAR(20) NOT NULL UNIQUE COMMENT '项目编号,如M100', item_name VARCHAR(50) NOT NULL COMMENT '项目名称', category VARCHAR(20) DEFAULT '径赛' COMMENT '径赛/田赛', score_direction TINYINT NOT NULL COMMENT '1越小越好 2越大越好', record_value DECIMAL(10,2) DEFAULT NULL COMMENT '当前纪录' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩记录表,运动员与项目是多对多,成绩表是中间关联表 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, athlete_id INT NOT NULL, item_id INT NOT NULL, score_value DECIMAL(10,2) NOT NULL COMMENT '成绩值:秒/米/次', rank_no INT DEFAULT NULL COMMENT '本项目名次', points INT DEFAULT 0 COMMENT '按名次折算积分', is_break_record TINYINT DEFAULT 0 COMMENT '1破纪录 0否', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_score_athlete FOREIGN KEY (athlete_id) REFERENCES athlete (id), CONSTRAINT fk_score_item FOREIGN KEY (item_id) REFERENCES sport_item (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 的重点不是建出几张表,而是把业务规则落到了字段层面。score_direction控制排名方向,points字段先把按名次折算的积分存起来,避免统计团体总分时每次重算。is_break_record在录入成绩后,通过对比score_value和sport_item.record_value刷新,不需要在业务代码里写一堆判断。

参数说明:成绩用DECIMAL(10,2),100 米跑会精确到百分之一秒,实心球用厘米精度也够,不要用 DOUBLE,否则排序时可能出现 8.9999999 这种浮点玄学结果。gender用 TINYINT 而不是 CHAR(1),省空间也好扩展。外键约束在课程设计里是独立的采分点,所以一定要加。注意导入顺序:必须先建班级,再建运动员和项目,最后建成绩表,否则外键会报错。

2.3 不推荐把学号当主键:编码字段与自增主键的取舍

很多第一次做课程设计的同学,会把运动员编号或学号直接设成主键。看起来没问题,但实际会撞墙:编号录错了要改,等于要级联更新所有引用它的成绩记录;而且如果两个班报名编号规则不统一,主键唯一性约束会让导入非常痛苦。我一般单独设一个自增 id 做表主键,把athlete_no做成 UNIQUE 键。UNIQUE 约束仍然能保证编号不重复,但主键和业务编号分开了,以后修改编号规则不需要动外键。

这个取舍同样适用于项目表。item_no比如 M100、LJ,对业务有含义,适合做查询条件,但自增 id 才是成绩表外键引用的对象。如果用业务编号做主键,成绩表的外键字段会很长,索引变大,项目编号规则一变,所有关联外键都要改。自增主键是课程设计里最不花哨但最不容易翻车的选择。

另一个常见的反面做法是,把“运动员姓名 + 项目名称”当唯一约束。真实情况是重名很多,项目名也容易带空格,最后成绩重复插入时无法判断是同一个人还是同名同姓。如果要在成绩表上加唯一约束,应该加(athlete_id, item_id)组合唯一索引,而不是业务字符串。这一点答辩时经常被问到,回答好基本能证明你理解了主键、外键、唯一约束的区别。

还有一点,如果老师不要求建物理外键,你也可以只保留逻辑外键,也就是字段存 id 但不写 CONSTRAINT。优势是导入测试数据快,不会被外键卡顺序,也方便先造一堆垃圾数据再清理。但课程设计建议还是建物理外键,因为评分里数据完整性是明确一块,而且后面做删除运动员时,外键能拦住误删。

3. 用 JDBC 跑通增删改查:运动会成绩管理系统的核心代码与参数说明

3.1 从 DriverManager 到连接池:课程设计阶段最稳的连接方式

运动会成绩管理系统到最终形态,无非是页面提交请求,Servlet 接收参数,DAO 层把参数拼进 SQL,再把结果回传。这里最基础的是数据库连接。课程设计阶段不建议一上来就引 Druid、HikariCP 连接池,虽然它们很成熟,但会引入 jar 包和配置的额外复杂度,老师看代码时也更关注你懂不懂 JDBC 本身。常见做法是写一个 DBUtil 静态工具类,提供getConnection()方法。每次请求创建一个连接,用完就关,虽然频繁创建开销不小,但对课设数据量完全够用。

一个需要特别注意的地方是 JDBC URL 参数。拿 MySQL 8 来说,驱动全类名是com.mysql.cj.jdbc.Driver,URL 通常长这样:

package com.sports.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/sports_game" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // MySQL 8 驱动类名;如果是 MySQL 5.7 改成 com.mysql.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这段代码里必须说清楚三个参数:useUnicode和characterEncoding=utf8保证中文按 UTF-8 进出数据库;serverTimezone=Asia/Shanghai是因为 MySQL 8 驱动默认要求指定时区,不写会报 server time zone 错;useSSL=false则是跳过 SSL 握手,本地开发更快。如果你的 MySQL 是 5.7,驱动改成com.mysql.jdbc.Driver,URL 可以不用 serverTimezone,但加上也无害。

如果你拿到的是别人给的代码,启动就报ClassNotFoundException,先检查 mysql-connector-java 的版本和驱动类名,这是下面第 5 章还会展开的坑。连接放在工具类里静态代码块注册驱动,是为了保证整个程序只加载一次驱动类,不重复注册。注意密码不要直接写死在 Java 里,课设无所谓,但至少把连接参数抽到db.properties,方便不同机器切换。

3.2 成绩录入与修改:PreparedStatement 防注入与参数绑定

运动会成绩管理里最频繁的操作是录入成绩和修改成绩。千万别用 Statement 拼字符串,比如INSERT INTO score VALUES (… + athleteId + …),这只适合在 SQL 命令行里体验,放进 Java 代码就是给 SQL 注入送人头。我一般用 PreparedStatement,既防注入,又让 MySQL 能缓存执行计划。

录入成绩的 DAO 方法如下:

public int insertScore(int athleteId, int itemId, BigDecimal scoreValue) { String sql = "INSERT INTO score (athlete_id, item_id, score_value) VALUES (?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, athleteId); ps.setInt(2, itemId); ps.setBigDecimal(3, scoreValue); int rows = ps.executeUpdate(); // 录入成功后,重新计算该项目排名与积分 if (rows > 0) { updateRankAndPoints(itemId); } return rows; } catch (SQLException e) { e.printStackTrace(); return 0; } }

这里有几个细节:成绩值用 BigDecimal 而不是 double,是为了把数据库里的 DECIMAL 完整接住,避免 float 精度丢失。setInt和setBigDecimal的顺序必须和 SQL 里的问号一一对应,这是新手最容易看花眼的地方。方法最后的updateRankAndPoints是核心业务:录入一条成绩后,成绩表里的rank_no和points必须更新,否则后面统计团体总分时读到的是旧名次。

修改成绩的思路类似,只不过 SQL 变成 UPDATE,并且在更新后也要重算排名。常见问题是只更新score_value,忘了points和rank_no,导致报表里成绩显示变了,积分还是旧的。我的做法是修改后对整个项目重排,而不是只改这条记录。重排时要用项目表的score_direction判断排序方向,这部分放到下一节讲排名 SQL 时一起说。

事务在这里很重要。insertScore里有两个动作:插入成绩、更新排名。如果插入成功但更新排名失败,数据就是脏的。课程设计里最稳的写法是把 Connection 的 autoCommit 关掉,两个操作完成后一起 commit。举个例子:

Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 执行insert // 执行updateRankAndPoints conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); }

看上去多写了几行,但这是“数据一致性”的展示点。老师如果问“你成绩录错了怎么办”,你可以说“修改后重算排名并且在事务里提交”,比只说“再录一次”值钱很多。

3.3 排行榜查询:一次查出成绩、名次和运动员信息

查询场景也比想象的多:按项目查成绩榜、按班级查运动员、按运动员查所有参赛记录。SQL 里离不开 JOIN。这里给出按项目查看成绩单的查询代码:

public List<ScoreVO> listScoreByItem(int itemId) { String sql = "SELECT a.athlete_no, a.name AS athlete_name, c.class_name, " + "s.score_value, s.rank_no, s.points, s.is_break_record " + "FROM score s " + "JOIN athlete a ON s.athlete_id = a.id " + "JOIN class c ON a.class_id = c.id " + "WHERE s.item_id = ? " + "ORDER BY s.rank_no ASC"; List<ScoreVO> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, itemId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ScoreVO vo = new ScoreVO(); vo.setAthleteNo(rs.getString("athlete_no")); vo.setAthleteName(rs.getString("athlete_name")); vo.setClassName(rs.getString("class_name")); vo.setScoreValue(rs.getBigDecimal("score_value")); vo.setRankNo(rs.getInt("rank_no")); vo.setPoints(rs.getInt("points")); vo.setBreakRecord(rs.getInt("is_break_record") == 1); list.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return list; }

这段查询展示了为什么前面要把主键和业务编号分开:成绩表只存 id,查询时通过 JOIN 把编号和姓名带出来,页面显示和数据库存储解耦。如果成绩表里冗余存一个运动员姓名,就会出现改名字后成绩单显示的姓名不同步的问题。

ORDER BY rank_no ASC即按名次升序。注意如果还没有计算 rank_no,这个排序会把所有 NULL 排在最前面,所以录入后重算排名不是可选优化,而是一个必须步骤。我在实际项目里还会在 ScoreVO 里加一个formatScore()方法,把 DECIMAL 按不同项目格式化成“12.50 秒”或“8.65 米”,避免页面直接输出原始数字,不过这属于表现层逻辑,放在工具类里就够了。

4. 成绩统计与团体总分:把计分规则变成 SQL 而不是硬编码

4.1 计分规则表:用配置代替 if-else

运动会排名简单吗?单项目排名简单,难的是把名次折算成积分。常见规则是第一名 7 分、第二名 5 分、第三名 4 分、第四名 3 分、第五名 2 分、第六名 1 分,也可能每个项目取前八名、积分从 9 分往下。如果你在 Java 里写if (rank == 1) return 7; else if (rank == 2) return 5;这种代码,改规则时要改源码重新编译,而且老师要是临时说“今年按项目不同分别记分”,你就很难改。

我一般把计分规则单独建一张score_rule表,结构是 item_id、rank_no、points,也可以不关联 item,表示全场统一的积分规则。更通用的做法:

CREATE TABLE score_rule ( item_id INT NOT NULL, rank_no INT NOT NULL, points INT NOT NULL, PRIMARY KEY (item_id, rank_no), CONSTRAINT fk_rule_item FOREIGN KEY (item_id) REFERENCES sport_item (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

如果是全场统一规则,可以把item_id设为 0,查询时先用实际 item_id 查,查不到再退回去查 0。这样虽然多写几行 SQL,但规则变成了数据而不是代码。在管理系统里加一个“计分规则维护”页面,也不过是对这张表做增删改查,和运动员管理没有本质区别。

积分折算的 SQL 可以用 LEFT JOIN 把 score 表和 score_rule 连起来,score 记录里的 rank_no 作为 JOIN 条件:

UPDATE score s LEFT JOIN score_rule r ON s.item_id = r.item_id AND s.rank_no = r.rank_no SET s.points = COALESCE(r.points, 0) WHERE s.rank_no IS NOT NULL;

这里COALESCE的作用是:如果没找到对应积分,就置 0;如果 rank_no 是 NULL,不更新。要注意这条 UPDATE 会把存在成绩但没有名次的记录也更新成 0,所以前面 WHERE 条件里要加上rank_no IS NOT NULL,具体根据你的业务决定。

4.2 团体总分排名的一条 SQL 与“每班只取前两名”的边界

团体总分是最能体现 SQL 水平的点。基础 SQL 非常简单:班级分组,SUM(points) 降序。

SELECT c.class_name, SUM(s.points) AS total_points FROM score s JOIN athlete a ON s.athlete_id = a.id JOIN class c ON a.class_id = c.id GROUP BY c.id, c.class_name ORDER BY total_points DESC;

但如果规则是“每个项目每个班级最多只能取两个人次计分”,上面的 SUM 会把第三个、第四个人也加进去。这时候必须先在项目内部按班级分组排名,然后只取前两名。如果你用的是 MySQL 8.0,窗口函数能直接解决问题:

SELECT class_id, item_id, athlete_id, score_value, points, rn FROM ( SELECT s.athlete_id, s.item_id, s.score_value, s.points, a.class_id, ROW_NUMBER() OVER ( PARTITION BY s.item_id, a.class_id ORDER BY CASE si.score_direction WHEN 1 THEN s.score_value ELSE -s.score_value END ASC ) AS rn FROM score s JOIN athlete a ON s.athlete_id = a.id JOIN sport_item si ON s.item_id = si.id ) t WHERE rn <= 2;

这段 SQL 最容易在答辩时被深挖。窗口函数PARTITION BY s.item_id, a.class_id先按项目和班级切分组,组内按成绩方向排序。CASE si.score_direction那句是核心:时间类项目(1)成绩值越小越好,直接按 score_value 升序;距离类项目(2)成绩越大越好,取-score_value升序等价于按 score_value 降序。排序后ROW_NUMBER()从 1 编号,最后rn <= 2留下每班前两个。

如果是 MySQL 5.7,不支持窗口函数,就得用用户变量模拟,写法绕但思路一样。我第一次写这种 SQL,纸上画了半天才理顺“PARTITION BY 先切组、ORDER BY 在组内排序”的关系。你现在看这段,只要记住:先切组,再排序,再编号,最后过滤,链条清晰就对了。

得到的子查询结果还要再和score_rule表 JOIN 一次,把积分带出来,然后才是团体总分聚合。可以把这个子查询做成视图,比如view_team_score,这样统计报表的 SQL 可以一层套一层,阅读性好一点。

4.3 破纪录判断与成绩方向:统一排名时最容易翻车的地方

破纪录判断看起来是小事,但实现时容易掉进一个逻辑坑:并不是所有项目都是数值越小越好。如果只写score_value < record_value就是破纪录,那么实心球这种“投得越远越好”的项目就反了。所以在项目表里设计score_direction,就是为了让判断条件也统一。

UPDATE score s JOIN sport_item si ON s.item_id = si.id SET s.is_break_record = CASE WHEN si.score_direction = 1 AND s.score_value < si.record_value THEN 1 WHEN si.score_direction = 2 AND s.score_value > si.record_value THEN 1 ELSE 0 END WHERE si.record_value IS NOT NULL;

另一个容易翻车的地方是:record_value是 NULL。项目没有历史纪录时,上面的 WHEREsi.record_value IS NOT NULL会跳过;如果你不在 UPDATE 里判断,NULL 参与比较返回的是“未知”,is_break_record会被置成 0,也不至于报错,但逻辑上不对。所以要么在录入页面控制“没纪录就不能勾选破纪录”,要么在 SQL 里显式判断,我建议两条都做。

项目排名还有一种做法是放在 Java 里对 List 排序。对于单项目成绩不超过几十条,Java 排序确实简单,但课程设计考察的核心是数据库能力,老师希望看到 SQL 层面的统计。而且 Java 排序需要先把所有成绩查出来,再在内存里排,数据量大时性能差。我在给学弟改代码时,经常看到他们把团体总分算完硬编码成 Map,最后报表一换班级顺序就错。正确的做法是把聚合留给数据库,Java 只负责展示。这样“团体总分”从需求变成 SQL,你才有东西在答辩时讲。

5. 把 zip 变成能运行的系统:导入部署与五个高频坑

5.1 从 zip 包还原工程:先看目录结构和配置文件

假设你手上拿到的“安徽工程大学数据库课程设计运动会成绩管理系统.zip”是别人整理好的源码包。解压后别急着双击运行,先看目录结构。常见套路是:根目录下有 database 或 sql 文件夹,里面是建库脚本;src 目录放 Java 源码;WebRoot 或 webapp 放 JSP 和配置文件;外面有 README 或者课程设计报告文档。如果你发现连 lib 目录都没有,先别慌,没有 MySQL 驱动 jar 包是常事,去把 mysql-connector-java 的 jar 补到 lib 里。

我拿到任何系统包,第一件事是搜索db.properties、jdbc.properties或者DBUtil.java,确认数据库连接参数指向哪里。很多包把 IP、端口、用户名、密码写死在 Java 类里,你需要改成自己的环境。搜索关键词是jdbc:mysql,把所有出现的地方都过一遍,避免只改了 DBUtil 却漏了另一个连接工具类。然后打开 SQL 脚本,确认它是否包含 CREATE DATABASE,如果不包含,需要自己新建数据库再 source。

接下来才是导入 IDE。老一点的项目多半是 Eclipse 工程,目录里有.classpath和.project,用 Eclipse 直接 Import Existing Projects;如果是 Maven 骨架,就用 IDEA 导入 Maven 项目。课程设计包里两种都有可能,不要一看到代码目录就问“为什么打开没有项目”,先看有没有pom.xml或.classpath。这里可以先用命令把关键文件找出来:

unzip sports_game.zip -d sports_game cd sports_game find . \( -name "*.sql" -o -name "*.properties" -o -name "*.java" \) -print

unzip -d sports_game是把压缩包解压到指定目录,避免文件散落。find 命令的括号需要转义,否则某些 shell 会把括号当成语法;列出这三类文件,是为了快速定位建库脚本、连接配置和核心代码。如果你在 Windows 上,没有 unzip 就用右键解压,然后打开资源管理器搜索*.sql,效果一样。

5.2 驱动类名不匹配:MySQL 5 与 8 的 ClassNotFoundException

现象:Tomcat 一启动,日志里出现ClassNotFoundException: com.mysql.jdbc.Driver或者com.mysql.cj.jdbc.Driver,页面直接 500。

原因:MySQL 官方驱动在第 8 版把驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,同时 URL 里多出 serverTimezone 参数。如果代码是按照 MySQL 5 写的,而你本地装的是 8.0;或者相反,代码是按 8 写的,本地是 5.7,就会在Class.forName这一步炸掉。

解决:先确认本机 MySQL 版本,mysql --version一看便知。然后对应改驱动类名和 jar 包。如果是 5.7,用com.mysql.jdbc.Driver;如果是 8.0,用com.mysql.cj.jdbc.Driver,并且 URL 里加serverTimezone=Asia/Shanghai。如果你用 Maven,在pom.xml里检查 mysql-connector-java 的版本号,5.1.x 对应旧类名,8.x 对应新类名。改完后 IDEA 的 Maven 面板重新 import,Eclipse 里右键项目 Maven Update,别只改代码不更新依赖。

这个坑的迷惑之处在于,有时代码新旧混搭:lib 里放着 mysql-connector-java 5.1.49,代码里却写新类名,ClassNotFoundException没跑了。我一般推荐把驱动 jar 统一到 8.0.33 左右,然后连接参数按新规范写,这样代码和依赖版本一致,也方便以后迁移。

5.3 中文乱码的完整链路:页面、请求、JDBC、表结构

现象:页面显示运动员姓名变成“???”或“æµè·”,在 MySQL 命令行里查表也是乱码。

原因:中文乱码不是单点问题,而是链路问题。只要页面编码、HTTP 请求编码、JDBC 连接编码、表结构 Charset 四个环节里有一个不一致,就会出现乱码。最常见的是表建成了 latin1,或者 JDBC URL 没有加characterEncoding=utf8,再或者 JSP 页面没有指定 pageEncoding。

解决:按链路逐个排查并统一到 UTF-8。第一,MySQL 表结构在初始化 SQL 里指定DEFAULT CHARSET=utf8mb4,已经创建的表可以用ALTER TABLE athlete CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;补救。第二,JDBC URL 加useUnicode=true&characterEncoding=utf8。第三,JSP 页面头部写<%@ page contentType="text/html;charset=UTF-8" %>,如果是 Servlet,在 doGet/doPost 开头设置request.setCharacterEncoding("UTF-8")。第四,Tomcat 在server.xml的 Connector 里加URIEncoding="UTF-8",处理 GET 请求参数乱码。

一个很隐蔽的情况是:数据库连接用 utf8,但 MySQL 服务端全局character_set_server还是 latin1,结果表的默认字符集被覆盖。解决方法是连接 URL 里显式指定 characterEncoding,并且在建库语句里不依赖全局变量。还有,如果你用 Navicat/SQLyog 导入 SQL,右下角选字符集时经常默认 UTF-8,但如果 SQL 文件本身是 ANSI 编码,导入后照样乱。这时候把 SQL 文件另存为 UTF-8 无 BOM 再导入,比在数据库里折腾半天更高效。

5.4 成绩录进去却查不到:事务没提交与并发重复提交

现象:在管理系统页面录一条成绩,提示成功,但返回列表查询时看不到;刷新数据库后那条记录可能又出现了。或者录一条成绩,页面卡顿,再点一次提交,数据库里出现两条相同成绩。

原因:第一,代码里关闭了 autoCommit 却没有在最后 commit,或者用了 try-with-resources 但没有显式提交,事务会在连接关闭时回滚。第二,Tomcat 的服务器线程并发执行时,同一个表单被重复提交,前端按钮没有禁用,后端也没有做唯一校验。

解决:事务部分,在 finally 里保证 commit/rollback 成对出现。我习惯写成:

Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 执行成绩插入和排名更新 conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); }

用 try-with-resources 时如果 commit 前抛异常,连接关闭时会自动回滚,但没有显式 rollback 可能不好排查。更稳妥的是把 conn 声明在外面,catch 里调用 rollback。重复提交的解决分两层:前端提交按钮设置 disabled,后端在 score 表上建(athlete_id, item_id)唯一索引,插入重复时捕获 DuplicateKeyException 并提示“该运动员已录过此项目成绩”。从数据库层面兜底,比只靠前端稳妥得多。

5.5 端口占用、路径含中文、SQL 导入顺序:三个隐藏雷

现象:Tomcat 启动失败,报Address already in use: JVM_Bind 8080;页面能打开但 CSS 全丢;SQL 脚本导入一半报外键错误。

原因:8080 端口被其他程序占用;Tomcat 的 web 应用部署路径或项目物理路径含中文/空格,导致静态资源 URL 解析异常;SQL 脚本里先建成绩表后建运动员表,外键找不到父表。

解决:端口占用可以在 Tomcat 安装目录 conf/server.xml 里把<Connector port="8080"改成 8081 或 8009,注意访问 URL 要同步改。路径含中文的问题,把整个项目复制到D:\workspace\sports这样的纯英文目录下,再重新导入 IDE。SQL 导入顺序按下层优先:class、athlete、sport_item、score,如果别人给的脚本没有 DROP TABLE 先清理已存在表,导入时会报重复键,所以在脚本开头加SET FOREIGN_KEY_CHECKS=0;,最后设回 1。

这三个雷很基础,但在课程设计提交前最容易连环炸。尤其是路径含中文,很多同学的桌面上用户名是中文,zip 解压到桌面后,Tomcat 部署到带中文的目录,图片和 CSS 加载失败,页面变成纯文本。遇到这种情况不要怀疑代码,先看地址栏里的 URL 是不是被转义成了一大串百分号,是的话就把项目挪到英文路径。

6. 让课程设计从能跑变成能答辩:验证清单、计分规则配置和一点经验

如果你已经把上面的库、代码和部署跑通,接下来最值钱的一步是“按评分点自查”。课程设计不是上线生产系统,老师通常看重三件事:数据库设计是否合理、增删改查是否完整、统计 SQL 是否体现了一点难度。我给自己的项目做答辩前自测时,会先执行下面这条 SQL,确认各表数据能被正确 JOIN 出来:

SELECT si.item_name, a.name AS athlete_name, c.class_name, s.score_value, s.rank_no, s.points, s.is_break_record FROM score s JOIN athlete a ON s.athlete_id = a.id JOIN class c ON a.class_id = c.id JOIN sport_item si ON s.item_id = si.id ORDER BY si.item_no, s.rank_no;

能一次查出所有成绩明细,说明外键、JOIN、成绩字段都没问题。然后你可以把计分规则做成配置表,在系统里加一个“规则维护”页面,这样答辩现场老师让你改积分规则,你只要在页面改一条记录,再跑一遍统计,总分立刻更新,比改代码重新部署有说服力得多。这套数据库设计思路也不只绑定 JSP,后面想换成 Spring Boot + Vue3 后台管理系统,Dao 层换成 MyBatis,核心的统计 SQL 基本不用动。

最后说一点经验:我大学时做过一个类似系统,当时图省事,成绩表里没有放score_direction,结果处理“跳远”项目时我硬在 Java 里写if (itemNo.equals("LJ"))倒序排名。后来新增一个铅球项目,代码又得改。后来我才想明白,业务逻辑放进数据字典,才是数据库课程设计应该展示的思维方式。希望你动手之前,先把里面的字段想清楚,再开始写代码。希望帮到你。

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

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

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

立即咨询