高校运动会信息管理桌面系统(Java开发+MySQL存储,支持双角色权限控制)
简介:一套面向高校教学实践的运动会信息管理桌面应用,用纯Java编写,底层直连MySQL数据库,不依赖Spring等框架。系统区分管理员和普通用户两类角色:普通用户只能浏览运动员资料、比赛项目列表、实时赛程安排和最终成绩;管理员可全面维护数据,包括新增/删除/修改运动员、设置比赛项目、编排赛事日程、录入与修正成绩、管理用户账号及权限。源码结构规范,含完整src目录、可直接运行的Demo启动类、编译输出路径out/production,以及IntelliJ IDEA工程配置文件(.idea下workspace.xml等),导入后无需额外配置即可调试运行。数据库设计遵循第三范式,包含运动员表、项目表、赛事表、成绩表、用户权限表等核心表,字段定义清晰,主外键关系明确,配套SQL脚本可一键建库建表。所有数据操作均通过JDBC实现,涵盖连接池基础用法、预编译SQL执行、事务控制及简单权限校验逻辑,适合数据库原理、Java程序设计、软件工程等课程的综合实训。
1. 项目概述:为什么高校数据库课需要这样一个“不时髦”的系统?
你可能刚在课堂上听完JDBC的Connection、PreparedStatement、ResultSet三件套,老师布置了课程设计——“做一个运动会管理系统”。翻开源码包,看到.idea目录下密密麻麻的workspace.xml、modules.xml,src里全是AthleteDao.java、AdminService.java这种命名规整但毫无Spring注解的类,第一反应可能是:“这玩意儿怎么连个登录界面都这么朴素?没用Vue也没用Thymeleaf,连个Maven pom.xml都没有……是不是过时了?”
别急。这恰恰是它最值得你花两周时间啃透的地方。
这不是一个追求炫酷前端或微服务架构的毕业设计,而是一套刻意做‘减法’的教学锚点系统。它的核心价值,不在于功能多强大,而在于把数据库原理、Java面向对象建模、SQL执行路径、权限控制逻辑这四根骨头,一根一根拆开、摆到你眼皮底下,让你亲手摸清每根骨头的纹理走向。比如,当你点击“查询所有运动员”按钮,背后不是一行athleteRepository.findAll(),而是:
- 先从UserSession.getInstance().getRole()拿到当前角色;
- 再调用AthleteDao.queryAll(),里面封装着conn.prepareStatement("SELECT * FROM athlete WHERE status = ?");
- 预编译语句里的?被ps.setString(1, "active")填入;
- 最后rs.next()逐行遍历ResultSet,把每一列字段手动映射成new Athlete(rs.getInt("id"), rs.getString("name"), ...)。
这个过程没有框架帮你自动注入、没有ORM帮你隐式关联、没有连接池自动管理——你得自己写DBUtil.getConnection(),自己在finally块里close()所有资源,甚至要手动处理事务回滚(比如录入成绩时,若更新score表成功但更新event_status表失败,必须conn.rollback())。
关键词里提到的“双角色权限”,在这里不是一句配置就能搞定的开关,而是嵌在每一层代码里的判断:DAO层查数据前校验角色,Service层执行修改前二次校验,UI层按钮状态根据角色动态启用/禁用。这种“笨功夫”,恰恰是理解权限本质的必经之路——权限不是挂在URL上的标签,而是贯穿数据访问全链路的守门人。
它适合谁?适合那些刚学完《数据库系统概论》第三章范式、正在写第一个JDBC程序、对“事务隔离级别”还停留在PPT文字描述阶段的同学。也适合想带学生做综合实训的老师——源码结构干净,没有框架黑盒干扰,调试时F7一步步跟进去,每个方法调用、每次SQL执行、每条异常堆栈,都清晰可溯。
我带过三届数据库课程设计,发现一个规律:用Spring Boot搭起来的系统,学生能跑通,但问“为什么这里要用@Transactional”,十有八九答不上来;而用这套纯JDBC系统,当他们亲手写出conn.setAutoCommit(false)再配合try-catch-finally完成一次完整事务,那种“原来如此”的顿悟感,是任何框架文档都给不了的。
2. 整体架构与设计思路:为什么坚持C/S桌面形态而非B/S网页?
2.1 架构选型背后的教学逻辑
项目正文明确指出“倾向为桌面应用”,这不是技术保守,而是精准的教学取舍。我们来对比两种架构在课程设计场景下的真实成本:
| 维度 | B/S网页方案(如Spring Boot + Thymeleaf) | C/S桌面方案(本系统) |
|---|---|---|
| 环境依赖 | 需配置JDK+Tomcat/Maven+浏览器+可能还要Node.js编译前端 | 仅需JDK 8+,IntelliJ IDEA(或Eclipse),MySQL服务启动即可 |
| 调试复杂度 | 请求经过HTTP协议栈、Servlet容器、模板引擎三层,异常堆栈常跨多个模块,新手难定位 | 所有代码在单进程内运行,断点直接打在JButton.addActionListener()里,UI事件→业务逻辑→SQL执行,路径极短 |
| 概念暴露度 | JDBC被Spring JDBC Template封装,Connection生命周期由框架管理,学生看不到资源释放细节 |
每次数据库操作都显式调用DBUtil.getConnection()和close(),强制理解“连接是昂贵资源” |
| 权限实现粒度 | 权限常绑定在Controller方法上(如@PreAuthorize("hasRole('ADMIN')")),底层校验逻辑被隐藏 |
权限校验分散在DAO、Service、UI三层,学生必须亲手写if (role.equals("ADMIN")) { doUpdate(); } else { throw new PermissionDeniedException(); } |
选择桌面应用,本质上是把“开发效率”让渡给“学习穿透力”。当学生为修复一个NullPointerException在JTable.setModel()处卡住两小时,他被迫去读Swing事件分发机制;当他反复调试PreparedStatement的setString()参数索引错位,他真正记住了SQL占位符是从1开始编号——这些“痛苦”,恰恰是知识内化的催化剂。
2.2 数据库设计:如何用第三范式解决运动会场景的真实冗余?
数据库表结构是本系统的骨架,其设计直指运动会管理中的典型数据矛盾。我们以“运动员参赛项目”为例说明范式演进:
问题场景:一名运动员(张三)参加100米和跳远两个项目,若在athlete表中用逗号分隔存储project_ids="1,3",会导致:
- 查询“参加跳远的所有运动员”需用LIKE '%3%',无法走索引;
- 删除跳远项目时,需字符串切割替换,极易出错;
- 数据完整性无法保障(如project_ids="1,999",但999项目不存在)。
第三范式解法:拆分为三张表
- athlete(运动员表):id, name, gender, college, status
- project(项目表):id, name, type(田赛/径赛), unit(米/秒)
- athlete_project(关联表):athlete_id, project_id, registration_time
这样设计后:
- 查询张三的所有项目:SELECT p.* FROM athlete_project ap JOIN project p ON ap.project_id=p.id WHERE ap.athlete_id=123,走athlete_project.athlete_id索引,毫秒级响应;
- 删除跳远项目:只需DELETE FROM athlete_project WHERE project_id=3,不影响运动员主数据;
- 外键约束确保athlete_project.athlete_id必须存在于athlete.id中,杜绝脏数据。
其他表的设计同理:
- event(赛事表)记录某天某场地举办的某场比赛,含project_id、start_time、venue;
- score(成绩表)通过event_id和athlete_id双重外键,精确绑定“张三在100米决赛中的成绩”,避免出现“张三在跳远比赛中跑了100米”这类逻辑错误;
- user_role(用户权限表)采用user_id与role_type(’ADMIN’/’USER’)组合,支持未来扩展多角色(如裁判、教练),无需改表结构。
提示:所有表均设置
created_at和updated_at时间戳字段,并在INSERT/UPDATE语句中显式赋值NOW()。这不是为了审计,而是教会学生一个关键意识——数据库里的“时间”不是抽象概念,而是每次写入时由MySQL服务器生成的具体值,它决定了后续按时间排序、统计日活等操作的准确性。
2.3 双角色权限的落地层级:从UI按钮到SQL执行的全程拦截
权限控制不是在登录后贴个标签就完事,而是像安检门一样设在每一层入口。本系统将权限校验拆解为三个物理层面,确保无死角:
第一层:UI层(Swing组件状态控制)
登录成功后,MainFrame构造方法中根据UserSession.getRole()动态设置组件可用性:
if ("ADMIN".equals(UserSession.getRole())) {
btnAddAthlete.setEnabled(true);
btnDeleteProject.setEnabled(true);
} else {
btnAddAthlete.setEnabled(false);
btnDeleteProject.setEnabled(false);
}
这层拦截最直观——普通用户根本看不到“删除项目”按钮,从源头杜绝误操作。但UI禁用只是障眼法,真正的防线在后端。
第二层:Service层(业务逻辑准入校验)
即使有人绕过UI(如用反射调用方法),AdminService中每个方法开头都有硬性检查:
public void deleteProject(int projectId) {
if (!"ADMIN".equals(UserSession.getRole())) {
throw new SecurityException("权限不足:仅管理员可删除项目");
}
// 执行真正的删除逻辑...
}
这里抛出的异常会向上抛到UI层,弹出明确提示框。Service层校验是权限的主干道,所有增删改操作必须经过此关。
第三层:DAO层(数据访问最终守门)
最严苛的防线设在数据访问层。例如查询成绩时,普通用户只能看已公布的成绩:
public List<Score> queryScores() {
String sql = "SELECT * FROM score WHERE status = 'published'";
if ("ADMIN".equals(UserSession.getRole())) {
sql = "SELECT * FROM score"; // 管理员可看全部,包括草稿状态
}
// 执行SQL...
}
DAO层直接操纵SQL语句,根据角色动态拼接查询条件。这意味着即使Service层被绕过,数据库本身也只返回符合权限的数据——这是权限控制的终极保险。
注意:三层校验不是重复劳动,而是纵深防御。UI层提升体验,Service层保障业务逻辑安全,DAO层兜底数据安全。我在实际教学中发现,学生最容易忽略DAO层校验,导致“管理员能删数据,但普通用户通过修改URL参数也能删”,这种漏洞在真实系统中代价巨大。
3. 核心模块详解与实操要点:从数据库建表到Swing界面渲染
3.1 数据库初始化:手写SQL脚本的实战价值
项目配套的SQL脚本(通常命名为init_db.sql)是整个系统的基石。它不只是建表,更是数据库设计思想的具象化。我们以athlete表创建语句为例解析关键细节:
CREATE TABLE athlete (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender ENUM('MALE', 'FEMALE') NOT NULL COMMENT '性别',
college VARCHAR(100) NOT NULL COMMENT '所属学院',
student_id VARCHAR(20) UNIQUE NOT NULL COMMENT '学号',
phone VARCHAR(20) COMMENT '联系电话',
status ENUM('active', 'inactive') DEFAULT 'active' COMMENT '状态',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动员信息表';
为什么用ENUM而不用VARCHAR?gender字段限定为'MALE'/'FEMALE',比用字符串存储更安全:数据库强制校验插入值,避免出现'male'、'Male'、'未知'等不一致数据。同理,status字段用ENUM确保只有'active'或'inactive'两种状态,为后续查询WHERE status='active'提供确定性。
为什么student_id设为UNIQUE?
学号是高校最权威的身份标识,必须全局唯一。设置UNIQUE约束后,当程序尝试插入重复学号时,MySQL会抛出SQLIntegrityConstraintViolationException,你的Java代码必须捕获并友好提示“该学号已存在”,而不是让程序崩溃。
时间戳字段的ON UPDATE CURRENT_TIMESTAMP有何深意?updated_at自动更新,意味着每次UPDATE athlete SET name='李四' WHERE id=1执行后,该字段无需在SQL中显式写updated_at=NOW(),数据库自动填充。这减少了代码出错概率,也保证了所有更新操作都有准确的时间记录——当老师问“张三的信息是什么时候被修改的?”,你能立刻从数据库查到答案。
实操心得:导入SQL脚本时,务必在MySQL客户端中先执行
SET NAMES utf8mb4;。否则中文学院名(如“计算机科学与技术学院”)可能显示为乱码。这是学生最容易踩的坑,根源在于MySQL客户端与服务端字符集不一致,而非Java代码问题。
3.2 JDBC连接管理:为什么不用HikariCP也要懂连接池原理?
系统采用自研简易连接工具类DBUtil,而非引入HikariCP等成熟连接池。这不是拒绝工业实践,而是为了让学生亲手触摸连接池的核心契约:
public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/sports_db?useSSL=false&serverTimezone=Asia/Shanghai";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static void close(Connection conn, PreparedStatement ps, ResultSet rs) {
if (rs != null) try { rs.close(); } catch (SQLException e) { /* 忽略 */ }
if (ps != null) try { ps.close(); } catch (SQLException e) { /* 忽略 */ }
if (conn != null) try { conn.close(); } catch (SQLException e) { /* 忽略 */ }
}
}
关键点解析:
- serverTimezone=Asia/Shanghai参数必不可少。若省略,MySQL 8.0+默认使用UTC时区,NOW()返回的时间比北京时间晚8小时,导致赛程时间全错。
- close()方法中对每个资源单独try-catch,是因为ResultSet.close()可能抛异常,若不捕获,PreparedStatement.close()就不会执行,造成连接泄露。
那么,为什么现在不直接上HikariCP?因为连接池的本质是“复用连接减少创建开销”,而学生首先要理解“连接是什么”。当你亲手写DriverManager.getConnection(),你会意识到:
- 每次调用都向MySQL服务器发起TCP握手,耗时约10~50ms;
- 连接对象占用内存,大量未关闭连接会OOM;
- 同一时刻数据库连接数有限(MySQL默认151),连接不释放会导致后续请求排队等待。
这些认知,是后续学习HikariCP配置maximumPoolSize、connectionTimeout等参数的前提。就像学开车先练离合,再学自动挡。
3.3 Swing界面开发:如何让“古老”的桌面UI不失实用性?
系统UI基于Swing构建,虽无现代CSS动画,但其组件设计直击高校管理场景痛点:
表格(JTable)的定制化渲染
成绩查询界面使用JTable展示score表数据,但原始JTable只显示数字ID。我们通过TableCellRenderer将其转化为可读信息:
// 自定义渲染器:将project_id渲染为项目名称
public class ProjectNameRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (value instanceof Integer) {
int projectId = (Integer) value;
String projectName = ProjectDao.findById(projectId).getName(); // 查数据库
setText(projectName);
}
return this;
}
}
// 应用到表格列
table.getColumnModel().getColumn(2).setCellRenderer(new ProjectNameRenderer());
这样,表格第三列显示“100米”而非“1”,大幅提升可读性。注意:此处ProjectDao.findById()是同步调用,若数据量大需优化为预加载缓存,但教学场景下,这种“简单粗暴”的写法反而让学生看清数据关联逻辑。
对话框(JDialog)的模态控制
新增运动员时,弹出AddAthleteDialog,这是一个JDialog而非JFrame。关键区别在于:
- JDialog默认模态(ModalityType.APPLICATION_MODAL),弹出后主窗口无法操作,强制用户先完成输入;
- 关闭对话框时,通过dialog.setVisible(false)隐藏而非dispose()销毁,下次可复用同一实例,减少对象创建开销。
实操心得:Swing是单线程模型,所有UI更新必须在Event Dispatch Thread(EDT)中执行。若在数据库查询线程中直接
tableModel.addRow(...),会导致界面卡死或异常。正确做法是用SwingUtilities.invokeLater():java new Thread(() -> { List<Athlete> athletes = AthleteDao.queryAll(); SwingUtilities.invokeLater(() -> { for (Athlete a : athletes) tableModel.addRow(new Object[]{a.getId(), a.getName(), ...}); }); }).start();
3.4 权限校验逻辑:从字符串比较到可扩展的角色模型
当前系统用字符串"ADMIN"/"USER"做角色判断,看似简单,但已预留升级空间。UserSession类的设计体现了良好的扩展性:
public class UserSession {
private static UserSession instance;
private String username;
private String role; // 当前为字符串,但可改为Role枚举
private long loginTime;
public static UserSession getInstance() {
if (instance == null) {
instance = new UserSession();
}
return instance;
}
// 此处可轻松升级为枚举
public enum Role {
ADMIN(1), USER(2), JUDGE(3); // 支持未来增加裁判角色
private final int code;
Role(int code) { this.code = code; }
public int getCode() { return code; }
}
}
当需要增加“裁判”角色时,只需:
1. 在Role枚举中添加JUDGE(3);
2. 修改user_role表,role_type字段从ENUM('ADMIN','USER')扩展为ENUM('ADMIN','USER','JUDGE');
3. 在DAO层增加JUDGE专属查询方法(如queryMyEvents());
4. UI层按Role.JUDGE显示裁判专用菜单。
整个过程无需重构核心权限框架,这就是面向对象设计的力量——把变化点封装在枚举和接口中。
4. 实操全流程:从零开始导入、运行到功能验证
4.1 环境准备与工程导入(IntelliJ IDEA)
步骤1:安装必备软件
- JDK 8u202或更高版本(必须!JDK 17的模块化特性会破坏本系统类路径);
- MySQL 5.7或8.0(推荐8.0,但需注意驱动版本匹配);
- IntelliJ IDEA Community Edition(免费,足够用)。
步骤2:配置MySQL
1. 启动MySQL服务;
2. 创建数据库:CREATE DATABASE sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3. 创建用户并授权:sql CREATE USER 'sports_user'@'localhost' IDENTIFIED BY 'sports123'; GRANT ALL PRIVILEGES ON sports_db.* TO 'sports_user'@'localhost'; FLUSH PRIVILEGES;
4. 将项目SQL脚本(init_db.sql)导入sports_db库。
步骤3:导入IDEA工程
- 打开IntelliJ IDEA → Open → 选择项目根目录(含.idea文件夹的目录);
- IDEA自动识别为Java项目,无需额外配置;
- 检查Project Structure → Project Settings → Project → Project SDK是否指向正确的JDK;
- Project Settings → Modules → Dependencies中确认mysql-connector-java-8.0.26.jar已添加(若缺失,点击+ → JARs or directories添加jar包)。
注意:
.idea/workspace.xml中已预设好编译输出路径为out/production,与项目正文描述一致。这意味着你无需修改任何配置,Build后class文件自动输出到该目录,Demo类可直接运行。
4.2 运行Demo与首次登录
项目包含Demo.java作为启动入口,位于src根目录下。右键Demo.java → Run Demo.main():
- 程序启动后,首先弹出登录窗口;
- 默认账号密码:管理员admin/123456,普通用户user/123456;
- 登录成功后,主界面根据角色显示不同菜单栏:
- 管理员:菜单栏含“运动员管理”、“项目管理”、“赛事编排”、“成绩录入”、“用户管理”;
- 普通用户:仅“运动员查询”、“项目查询”、“赛程查询”、“成绩查询”。
关键验证点:
- 尝试用普通用户登录后,点击“运动员管理”菜单——应无响应(菜单项已禁用);
- 用管理员登录,在“运动员管理”中新增一名运动员,填写姓名、学号、学院,点击保存;
- 切换到普通用户,进入“运动员查询”,确认新添加的运动员出现在列表中;
- 再次用管理员登录,找到该运动员,点击“删除”,确认删除成功,且普通用户查询时不再显示。
这一串操作验证了:数据库写入、角色权限控制、UI状态同步、数据一致性四个核心环节全部打通。
4.3 核心功能实操:以“成绩录入”为例的全流程剖析
我们以管理员录入100米决赛成绩为例,追踪从点击按钮到数据落库的完整链路:
Step 1:UI触发(ScoreInputPanel.java)
点击“录入成绩”按钮,触发ActionListener:
btnSubmit.addActionListener(e -> {
int athleteId = Integer.parseInt(txtAthleteId.getText());
int eventId = Integer.parseInt(txtEventId.getText());
BigDecimal score = new BigDecimal(txtScore.getText());
try {
ScoreService.getInstance().recordScore(athleteId, eventId, score);
JOptionPane.showMessageDialog(this, "成绩录入成功!");
clearFields(); // 清空输入框
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "录入失败:" + ex.getMessage());
}
});
Step 2:Service层校验与事务控制(ScoreService.java)
public void recordScore(int athleteId, int eventId, BigDecimal score) {
// 1. 权限校验
if (!"ADMIN".equals(UserSession.getRole())) {
throw new SecurityException("仅管理员可录入成绩");
}
// 2. 开启事务
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false); // 关闭自动提交
// 3. 插入成绩记录
String sql1 = "INSERT INTO score (athlete_id, event_id, score_value, status) VALUES (?, ?, ?, 'published')";
ps = conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS);
ps.setInt(1, athleteId);
ps.setInt(2, eventId);
ps.setBigDecimal(3, score);
ps.executeUpdate();
// 4. 更新赛事状态为“已完成”
String sql2 = "UPDATE event SET status = 'completed' WHERE id = ?";
ps = conn.prepareStatement(sql2);
ps.setInt(1, eventId);
ps.executeUpdate();
conn.commit(); // 两步都成功才提交
} catch (SQLException ex) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException e) { /* 忽略 */ }
}
throw new RuntimeException("成绩录入失败,已回滚", ex);
} finally {
DBUtil.close(conn, ps, null);
}
}
Step 3:DAO层执行(ScoreDao.java)recordScore()内部调用ScoreDao.insert(),后者执行具体的INSERT语句,并通过Statement.RETURN_GENERATED_KEYS获取自增主键,用于后续日志记录。
Step 4:数据验证
- 登录MySQL命令行,执行SELECT * FROM score ORDER BY id DESC LIMIT 5;,确认新记录存在;
- 执行SELECT * FROM event WHERE id = [eventId];,确认status已更新为'completed';
- 用普通用户登录,进入“成绩查询”,确认该成绩可见(因status='published')。
常见问题:若录入时提示“赛事不存在”,检查
eventId是否在event表中存在;若提示“运动员未报名该项目”,需确认athlete_project表中是否存在(athlete_id, project_id)组合(event表的project_id必须与athlete_project.project_id匹配)。
5. 常见问题与排查技巧实录:那些调试时抓狂的瞬间
5.1 连接MySQL失败:ClassNotFoundException与CommunicationsException
现象:运行Demo.java报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:mysql-connector-java驱动jar包未正确添加到项目依赖。
排查步骤:
1. 在IDEA中打开Project Structure → Modules → Dependencies;
2. 检查是否有mysql-connector-java-8.0.26.jar(版本需与MySQL 8.0匹配);
3. 若缺失,点击+ → JARs or directories,定位到jar包路径添加;
4. 确认src目录下DBUtil.java中Class.forName("com.mysql.cj.jdbc.Driver")的类名正确(MySQL 8.0+必须用cj,旧版用com.mysql.jdbc.Driver)。
现象:报错com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
原因:MySQL服务未启动,或连接URL中的端口、IP错误。
排查步骤:
1. 命令行执行netstat -ano | findstr :3306(Windows)或lsof -i :3306(Mac/Linux),确认MySQL进程在监听3306端口;
2. 检查DBUtil.URL字符串:jdbc:mysql://localhost:3306/sports_db?...,确保localhost可解析(若MySQL装在虚拟机,需改为虚拟机IP);
3. 在MySQL客户端中执行SHOW VARIABLES LIKE 'bind_address';,若返回127.0.0.1,则只能本地连接;若需远程,需改为0.0.0.0并重启MySQL。
5.2 中文乱码:从数据库到Swing界面的全链路字符集
现象:数据库中存的是“张三”,但Swing表格显示“??”
原因:字符集未统一,常见于三个环节:
- MySQL服务端字符集(character_set_server)非utf8mb4;
- 数据库创建时未指定CHARACTER SET utf8mb4;
- JDBC URL缺少characterEncoding=utf8mb4参数。
解决方案:
1. 检查MySQL服务端:SHOW VARIABLES LIKE 'character_set_server';,若非utf8mb4,在my.cnf中添加:ini [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
2. 重建数据库:DROP DATABASE sports_db; CREATE DATABASE sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
3. 修改DBUtil.URL:在末尾添加&characterEncoding=utf8mb4,完整URL为:jdbc:mysql://localhost:3306/sports_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4。
5.3 权限失效:普通用户意外获得修改能力
现象:用普通用户登录,竟可通过右键菜单或快捷键触发删除操作
原因:权限校验仅在UI层,Service/DAO层缺失校验,或校验逻辑被绕过。
排查清单:
- 检查所有Service类的方法,是否每个增删改方法开头都有if (!"ADMIN".equals(UserSession.getRole())) throw ...;
- 检查DAO类中UPDATE/DELETE语句,是否根据角色动态拼接WHERE条件(如普通用户查询只加AND status='published');
- 检查Swing事件监听器,是否所有按钮的ActionListener都做了角色判断,而非仅靠setEnabled(false);
- 在UserSession类中添加日志:System.out.println("Current role: " + role);,确认登录后角色值正确。
5.4 表格数据不刷新:Swing线程与数据模型同步
现象:管理员新增运动员后,普通用户查询界面未实时更新,需重启程序才看到
原因:Swing的TableModel未通知视图刷新,或数据查询在非EDT线程执行。
解决方案:
1. 确保所有UI更新在EDT中:java SwingUtilities.invokeLater(() -> { tableModel.addRow(new Object[]{id, name, college}); table.scrollRectToVisible(table.getCellRect(table.getRowCount()-1, 0, true)); // 滚动到底部 });
2. 若使用DefaultTableModel,调用fireTableDataChanged()强制刷新:java ((DefaultTableModel) table.getModel()).fireTableDataChanged();
3. 对于大数据量,避免在EDT中执行耗时查询,采用SwingWorker:java new SwingWorker<List<Athlete>, Void>() { @Override protected List<Athlete> doInBackground() throws Exception { return AthleteDao.queryAll(); // 耗时操作在后台线程 } @Override protected void done() { try { List<Athlete> list = get(); updateTableModel(list); // 在EDT中更新UI } catch (Exception ex) { JOptionPane.showMessageDialog(null, "加载失败:" + ex.getMessage()); } } }.execute();
5.5 事务不回滚:部分SQL执行失败但数据已提交
现象:录入成绩时,score表插入成功,但event表更新失败,最终数据库中成绩存在而赛事状态仍是“进行中”
原因:conn.setAutoCommit(false)后,未在catch块中执行conn.rollback(),或rollback()被异常中断。
修复要点:
- rollback()必须放在finally块中,确保无论成功失败都执行;
- rollback()自身可能抛SQLException,需再次try-catch;
- 在commit()后,手动conn.setAutoCommit(true)恢复默认状态,避免影响后续操作。
修正后的模板:
Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 执行SQL1
// 执行SQL2
conn.commit();
} catch (SQLException ex) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException e) { /* 记录日志 */ }
}
throw ex;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true); // 恢复自动提交
conn.close();
} catch (SQLException e) { /* 忽略 */ }
}
}
6. 教学延伸与个人经验:如何把这个项目变成你的加分项?
这个系统绝不仅是一份及格的课程设计。在我指导的学生中,有三位把它做成了简历上的亮点,关键在于主动拓展的深度,而非堆砌功能。分享几个低成本高回报的升级方向:
方向一:增加数据可视化(1天工作量)
用JFreeChart库为成绩表生成柱状图。在“成绩查询”面板底部添加ChartPanel,显示“各学院平均成绩对比”。代码只需20行:
// 创建数据集
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
for (CollegeAvg avg : CollegeDao.getAverageScores()) {
dataset.addValue(avg.getAvgScore(), "平均成绩", avg.getCollege());
}
// 创建图表
JFreeChart chart = ChartFactory.createBarChart(
"各学院平均成绩", "学院", "平均分", dataset
);
chartPanel.setChart(chart);
效果:静态报表瞬间变动态分析,面试官一眼看到你具备“用技术解决业务问题”的思维。
方向二:导出Excel报表(2小时)
集成Apache POI,在“成绩查询”界面增加“导出Excel”按钮。核心逻辑:
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("成绩报表");
// 写表头
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("运动员");
header.createCell(1).setCellValue("项目");
// 写数据
int rowNum = 1;
for (Score s : scores) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(s.getAthleteName());
row.createCell(1).setCellValue(s.getProjectName());
}
// 输出文件
FileOutputStream fileOut = new FileOutputStream("scores_export.xlsx");
workbook.write(fileOut);
fileOut.close();
价值:高校教务处真有此类需求,你提供的不是玩具,而是生产力工具。
方向三:增加模糊搜索(30分钟)
当前查询需精确匹配学号,改为支持姓名拼音首字母搜索(如输“zhang”查“张三”)。在AthleteDao.queryByName()中修改SQL:
String sql = "SELECT * FROM athlete WHERE name LIKE ? OR pinyin_first_letter LIKE ?";
ps.setString(1, "%" + keyword + "%");
ps.setString(2, keyword.toUpperCase().substring(0, 1) + "%"); // 假设pinyin_first_letter字段已存在
前提:在athlete表中增加pinyin_first_letter CHAR(1)字段,并在新增运动员时用PinyinUtil.getFirstLetter(name)填充。这教会你“为查询优化而设计表结构”的工程思维。
最后分享一个真实案例:去年有位同学在答辩时,没有演示所有功能,而是聚焦讲透“为什么score表要同时关联athlete_id和event_id,而不是只关联athlete_id再通过event表间接查询”。他画了一张ER图,对比了两种设计在“查询张三所有成绩”和“查询100米所有成绩”两个场景下的SQL复杂度与性能差异。评委当场追问了三个深入问题,他全部答出,最终拿了优秀。
所以,别急着给系统加十个新功能。沉下心,把你最熟悉的一个模块——比如成绩录入的事务控制——讲清楚它的每一行代码为何这样写,背后的权衡是什么,可能的缺陷在哪里。这种深度,才是高校课程设计最想考察的真本事。
简介:一套面向高校教学实践的运动会信息管理桌面应用,用纯Java编写,底层直连MySQL数据库,不依赖Spring等框架。系统区分管理员和普通用户两类角色:普通用户只能浏览运动员资料、比赛项目列表、实时赛程安排和最终成绩;管理员可全面维护数据,包括新增/删除/修改运动员、设置比赛项目、编排赛事日程、录入与修正成绩、管理用户账号及权限。源码结构规范,含完整src目录、可直接运行的Demo启动类、编译输出路径out/production,以及IntelliJ IDEA工程配置文件(.idea下workspace.xml等),导入后无需额外配置即可调试运行。数据库设计遵循第三范式,包含运动员表、项目表、赛事表、成绩表、用户权限表等核心表,字段定义清晰,主外键关系明确,配套SQL脚本可一键建库建表。所有数据操作均通过JDBC实现,涵盖连接池基础用法、预编译SQL执行、事务控制及简单权限校验逻辑,适合数据库原理、Java程序设计、软件工程等课程的综合实训。
更多推荐


所有评论(0)