运动会成绩管理系统数据库设计:从SQL建表到JDBC增删改查实战 简介这套运动会成绩管理系统是安徽工程大学数据库课程设计的完整项目基于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. 先立住数据库运动会成绩管理系统的三张核心表与初始化 SQL2.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 所属系部 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 运动员表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 比赛项目表 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 当前纪录 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩记录表运动员与项目是多对多成绩表是中间关联表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 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.DriverURL 通常长这样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 ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai useSSLfalse; 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和characterEncodingutf8保证中文按 UTF-8 进出数据库serverTimezoneAsia/Shanghai是因为 MySQL 8 驱动默认要求指定时区不写会报 server time zone 错useSSLfalse则是跳过 SSL 握手本地开发更快。如果你的 MySQL 是 5.7驱动改成com.mysql.jdbc.DriverURL 可以不用 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 ListScoreVO 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; ListScoreVO 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果是全场统一规则可以把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 \) -printunzip -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 里加serverTimezoneAsia/Shanghai。如果你用 Maven在pom.xml里检查 mysql-connector-java 的版本号5.1.x 对应旧类名8.x 对应新类名。改完后 IDEA 的 Maven 面板重新 importEclipse 里右键项目 Maven Update别只改代码不更新依赖。这个坑的迷惑之处在于有时代码新旧混搭lib 里放着 mysql-connector-java 5.1.49代码里却写新类名ClassNotFoundException没跑了。我一般推荐把驱动 jar 统一到 8.0.33 左右然后连接参数按新规范写这样代码和依赖版本一致也方便以后迁移。5.3 中文乱码的完整链路页面、请求、JDBC、表结构现象页面显示运动员姓名变成“???”或“æµè·”在 MySQL 命令行里查表也是乱码。原因中文乱码不是单点问题而是链路问题。只要页面编码、HTTP 请求编码、JDBC 连接编码、表结构 Charset 四个环节里有一个不一致就会出现乱码。最常见的是表建成了 latin1或者 JDBC URL 没有加characterEncodingutf8再或者 JSP 页面没有指定 pageEncoding。解决按链路逐个排查并统一到 UTF-8。第一MySQL 表结构在初始化 SQL 里指定DEFAULT CHARSETutf8mb4已经创建的表可以用ALTER TABLE athlete CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;补救。第二JDBC URL 加useUnicodetruecharacterEncodingutf8。第三JSP 页面头部写% page contentTypetext/html;charsetUTF-8 %如果是 Servlet在 doGet/doPost 开头设置request.setCharacterEncoding(UTF-8)。第四Tomcat 在server.xml的 Connector 里加URIEncodingUTF-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 port8080改成 8081 或 8009注意访问 URL 要同步改。路径含中文的问题把整个项目复制到D:\workspace\sports这样的纯英文目录下再重新导入 IDE。SQL 导入顺序按下层优先class、athlete、sport_item、score如果别人给的脚本没有 DROP TABLE 先清理已存在表导入时会报重复键所以在脚本开头加SET FOREIGN_KEY_CHECKS0;最后设回 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))倒序排名。后来新增一个铅球项目代码又得改。后来我才想明白业务逻辑放进数据字典才是数据库课程设计应该展示的思维方式。希望你动手之前先把里面的字段想清楚再开始写代码。希望帮到你。本文还有配套的精品资源点击获取