☰
员工培训管理系统:数据库课设中的MySQL与JavaSwing工程实践
2026/10/9 20:05:18 网站建设 项目流程

简介:本资源是湖南科技大学数据库系统课程设计的完整实践项目,面向计算机专业本科生及数据库应用开发初学者,聚焦企业员工培训管理这一典型业务场景,解决培训计划制定、考勤跟踪、资源调度与绩效评估等多环节协同难题。压缩包共67个文件,含49个Java源码(覆盖Swing界面、JDBC连接、业务逻辑与DAO层)、1个初始化SQL脚本、3份PDF文档(含课程设计报告、题目要求与指导书)、1个E-R图.drawio文件及配套说明文件.txt、附赠资源.docx等,整体4.06MB,结构清晰,便于分模块学习与调试。已有45人下载学习,适合开展数据库建模、Java桌面应用开发与MySQL实战训练。读者可直接运行jar包体验系统功能,结合SQL脚本快速部署数据库,参照PDF报告理解设计思路与功能实现逻辑,并通过源码深入掌握Swing事件驱动、表格渲染、CRUD操作及事务处理等核心技能。

1. 为什么一个“员工培训管理系统”能成为数据库课设的硬核试金石?

不是所有课设都配叫“硬核”。湖南科技大学这门数据库系统课程设计里,学生交上来的.zip包里藏着一套完整跑通的JavaSwing+MySQL应用——它不只增删改查,而是把“员工培训计划管理”“课程考勤记录”“培训资源分配”“员工绩效评估”四个强业务耦合模块,用一张张表、一条条外键、一次次事务封装进数据库底层。很多同学卡在“能连上MySQL但一提交就报错”,或“Swing界面点不动,后台SQL明明写对了却查不到数据”,本质是没吃透数据库设计与GUI层的数据流闭环:从JDBC连接池配置、主外键约束如何防止脏数据插入、到Swing事件监听器里怎么安全执行带参数的PreparedStatement,再到事务边界该划在按钮点击前还是保存成功后。这套系统适合两类人:一是刚学完范式理论但还没在真实CRUD中摔过跤的本科生;二是想用最小技术栈(零Spring、零Web容器)验证自己数据库建模能力的自驱型学习者。它不炫技,但每一步都踩在数据库工程落地的筋膜上。


2. 从ER图到MySQL建表:四张核心表的设计逻辑与字段取舍

2.1 培训计划表(training_plan):为什么用复合主键而不是自增ID?

CREATE TABLE training_plan ( plan_id VARCHAR(20) NOT NULL, employee_id VARCHAR(15) NOT NULL, course_id VARCHAR(15) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, status ENUM('pending', 'ongoing', 'completed', 'canceled') DEFAULT 'pending', PRIMARY KEY (plan_id), FOREIGN KEY (employee_id) REFERENCES employee(employee_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT );

提示:plan_id设为VARCHAR(20)而非INT AUTO_INCREMENT,是因为业务要求计划编号需含年份+部门缩写+序号(如TP2024-HR-001),便于人工追溯和审计。若用自增ID,后续导出报表时还得关联其他表拼接,徒增复杂度。

关键字段说明:

  • status用ENUM而非TINYINT,直接语义化状态值,避免代码层硬编码数字映射(如1→pending),Swing下拉框可直取枚举值渲染;
  • ON DELETE CASCADE对employee_id生效:员工离职时,其所有未完成的培训计划自动清除,符合HR流程;
  • ON DELETE RESTRICT对course_id生效:课程被删除前必须先清空所有关联计划,防止培训资源“悬空”。

2.2 考勤记录表(attendance):时间戳精度与索引策略的实战权衡

CREATE TABLE attendance ( record_id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id VARCHAR(20) NOT NULL, check_in DATETIME NOT NULL, check_out DATETIME NULL, is_late BOOLEAN DEFAULT FALSE, remark TEXT, FOREIGN KEY (plan_id) REFERENCES training_plan(plan_id) ON DELETE CASCADE, INDEX idx_plan_time (plan_id, check_in) );

注意:check_in和check_out用DATETIME(精度秒级)而非TIMESTAMP,因TIMESTAMP受时区影响,而考勤数据必须绝对时间锚定;idx_plan_time复合索引覆盖高频查询场景——“查某员工某次培训的所有考勤记录”,避免全表扫描。

字段设计血泪经验:

  • record_id用BIGINT:单次培训可能有上百人签到,INT溢出风险高;
  • is_late单独布尔字段:比在应用层计算check_in > scheduled_start更快,且支持WHERE is_late = TRUE精准过滤;
  • remark用TEXT:允许HR填写手写原因(如“设备故障延迟15分钟”),VARCHAR(255)显然不够。

2.3 培训资源表(training_resource):一对多关系的三种实现方式对比

业务需求:一门课程可关联多个资源(PPT、视频、PDF),一个资源仅属一门课程。

方案表结构优点缺点本项目选择
方案A:资源表含course_id外键resource_id,course_id,file_name,file_path,upload_time查询简单,SELECT * FROM resource WHERE course_id=?直接命中若课程删除,资源文件物理存在但逻辑失效✅ 采用
方案B:中间表resource_courseresource_id,course_id支持资源复用(同一PPT用于多门课)本项目无复用需求,徒增JOIN❌ 放弃
方案C:JSON字段存资源列表course_id,resources JSON灵活,免建新表无法索引单个资源,LIKE模糊查性能差❌ 放弃

最终建表语句:

CREATE TABLE training_resource ( resource_id INT PRIMARY KEY AUTO_INCREMENT, course_id VARCHAR(15) NOT NULL, file_name VARCHAR(100) NOT NULL, file_path VARCHAR(255) NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE );

玄学细节:file_path存相对路径(如/resources/2024/hr_onboarding_ppt.pdf),而非绝对路径。Swing客户端通过getClass().getResourceAsStream()加载,避免硬编码盘符导致跨机器部署失败。

2.4 绩效评估表(performance_eval):如何用CHECK约束保证业务规则落地

CREATE TABLE performance_eval ( eval_id INT PRIMARY KEY AUTO_INCREMENT, plan_id VARCHAR(20) NOT NULL, evaluator_id VARCHAR(15) NOT NULL, score TINYINT CHECK (score BETWEEN 0 AND 100), feedback TEXT, eval_date DATE NOT NULL DEFAULT (CURRENT_DATE), FOREIGN KEY (plan_id) REFERENCES training_plan(plan_id) ON DELETE CASCADE, FOREIGN KEY (evaluator_id) REFERENCES employee(employee_id) ON DELETE RESTRICT, UNIQUE KEY uk_plan_evaluator (plan_id, evaluator_id) );

关键约束解析:

  • CHECK (score BETWEEN 0 AND 100):强制分数合法,比在Java层校验更可靠(防止绕过Swing界面直接SQL注入);
  • UNIQUE KEY uk_plan_evaluator:确保同一培训计划不被同一评估人重复打分,避免数据污染;
  • eval_date DEFAULT (CURRENT_DATE):MySQL 8.0+ 支持函数默认值,省去Java层手动设日期。

3. JavaSwing层的数据绑定:JTable与ResultSet的零拷贝同步技巧

3.1 用AbstractTableModel封装查询结果,避免List<Object[]>硬编码

传统做法:ResultSet→ 遍历 →List<String[]>→JTable,字段顺序靠注释维护,极易错位。本项目改用泛型化模型:

public class TrainingPlanTableModel extends AbstractTableModel { private List<TrainingPlan> data = new ArrayList<>(); private final String[] columnNames = {"计划ID", "员工姓名", "课程名称", "开始日期", "结束日期", "状态"}; @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { TrainingPlan plan = data.get(rowIndex); return switch (columnIndex) { case 0 -> plan.getPlanId(); case 1 -> plan.getEmployeeName(); // 关联查询已join employee表 case 2 -> plan.getCourseName(); // 关联查询已join course表 case 3 -> plan.getStartDate(); case 4 -> plan.getEndDate(); case 5 -> plan.getStatus(); default -> null; }; } @Override public String getColumnName(int column) { return columnNames[column]; } // 提供刷新方法,Swing按钮点击后调用 public void refreshData(List<TrainingPlan> newData) { this.data = newData; fireTableDataChanged(); // 通知JTable重绘 } }

逻辑说明:TrainingPlan是POJO,字段与数据库列一一映射,getValueAt中的switch语句明确绑定列序,比data.get(rowIndex)[2]可读性高10倍;fireTableDataChanged()是Swing线程安全更新的核心,漏掉会导致界面不刷新。

3.2 PreparedStatement参数化防注入:Swing组件值到SQL的精准映射

考勤记录查询界面含两个日期选择器(JDateChooser)和一个员工下拉框(JComboBox)。错误写法:

// ❌ 危险!字符串拼接 String sql = "SELECT * FROM attendance WHERE check_in BETWEEN '" + startDate + "' AND '" + endDate + "' AND plan_id IN (SELECT plan_id FROM training_plan WHERE employee_id = '" + empId + "')";

正确写法(带事务):

public List<Attendance> searchAttendance(Date startDate, Date endDate, String empId) { String sql = """ SELECT a.* FROM attendance a JOIN training_plan tp ON a.plan_id = tp.plan_id WHERE tp.employee_id = ? AND a.check_in BETWEEN ? AND ? ORDER BY a.check_in DESC """; List<Attendance> results = new ArrayList<>(); try (Connection conn = DataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empId); // 参数1:员工ID ps.setTimestamp(2, new Timestamp(startDate.getTime())); // 参数2:起始时间 ps.setTimestamp(3, new Timestamp(endDate.getTime())); // 参数3:结束时间 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { results.add(new Attendance( rs.getLong("record_id"), rs.getString("plan_id"), rs.getTimestamp("check_in"), rs.getTimestamp("check_out"), rs.getBoolean("is_late"), rs.getString("remark") )); } } } catch (SQLException e) { e.printStackTrace(); // 实际项目应转为自定义异常 } return results; }

参数说明:setTimestamp比setDate更精确,保留时分秒;ps.setString(1, empId)的1是占位符序号,与SQL中?从左到右顺序严格对应,错一位则数据错乱。

3.3 Swing事件驱动下的事务边界:一个按钮=一次原子操作

新增培训计划时,需同时插入training_plan表和初始化一条attendance记录(签到状态为pending)。若分两次executeUpdate(),中间崩溃会导致数据不一致。

public boolean createTrainingPlan(TrainingPlan plan, Attendance initAttendance) { String planSql = "INSERT INTO training_plan VALUES (?, ?, ?, ?, ?, ?)"; String attendSql = "INSERT INTO attendance (plan_id, check_in, is_late) VALUES (?, ?, ?)"; try (Connection conn = DataSource.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交 try (PreparedStatement psPlan = conn.prepareStatement(planSql); PreparedStatement psAttend = conn.prepareStatement(attendSql)) { // 设置培训计划参数 psPlan.setString(1, plan.getPlanId()); psPlan.setString(2, plan.getEmployeeId()); psPlan.setString(3, plan.getCourseId()); psPlan.setDate(4, new java.sql.Date(plan.getStartDate().getTime())); psPlan.setDate(5, new java.sql.Date(plan.getEndDate().getTime())); psPlan.setString(6, plan.getStatus()); psPlan.executeUpdate(); // 设置考勤参数(plan_id复用) psAttend.setString(1, plan.getPlanId()); psAttend.setTimestamp(2, new Timestamp(System.currentTimeMillis())); psAttend.setBoolean(3, false); psAttend.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { conn.rollback(); // 任一失败则回滚 throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }

避坑重点:conn.setAutoCommit(false)必须在try-with-resources外层设置,否则资源关闭时自动提交;rollback()后必须throw e,让上层Swing知道操作失败并弹窗提示。


4. 避坑指南:五个让课设答辩当场卡壳的致命问题

4.1 现象:Swing界面启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:MySQL Connector/J 驱动JAR未加入项目Classpath,或版本与JDK不兼容(如JDK17用mysql-connector-java-5.1.x)。
解决:下载mysql-connector-j-8.0.33.jar(适配JDK8+),右键项目 → Build Path → Add External Archives,确认Driver类全路径为com.mysql.cj.jdbc.Driver(注意cj,非旧版jdbc)。

4.2 现象:插入中文数据到MySQL显示问号(???)

原因:MySQL服务端、数据库、表、连接URL四层字符集未统一为utf8mb4。
解决:

  1. MySQL服务端配置my.cnf:[mysqld] default-character-set = utf8mb4;
  2. 创建数据库时指定:CREATE DATABASE hr_training DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
  3. JDBC URL加参数:jdbc:mysql://localhost:3306/hr_training?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai;
  4. 表建表语句末尾加DEFAULT CHARSET=utf8mb4。

4.3 现象:JTable双击某行编辑时,修改后点击“保存”无反应

原因:Swing默认JTable不可编辑,或TableModel未重写isCellEditable()和setValueAt()方法。
解决:在TrainingPlanTableModel中添加:

@Override public boolean isCellEditable(int rowIndex, int columnIndex) { return columnIndex == 5; // 仅“状态”列可编辑 } @Override public void setValueAt(Object value, int rowIndex, int columnIndex) { if (columnIndex == 5 && value instanceof String) { data.get(rowIndex).setStatus((String) value); fireTableCellUpdated(rowIndex, columnIndex); } }

4.4 现象:考勤记录按日期查询时,BETWEEN '2024-01-01' AND '2024-01-31'漏掉1月31日全天数据

原因:BETWEEN对DATETIME字段默认匹配到00:00:00,'2024-01-31'等价于'2024-01-31 00:00:00',导致2024-01-31 14:30:00被排除。
解决:将结束时间设为下一日00:00:00,SQL改为:

WHERE a.check_in >= ? AND a.check_in < ? // Java层传参:startDate, new Date(endDate.getTime() + 24*60*60*1000)

4.5 现象:导出Excel报表时,ResultSet已关闭,抛SQLException: Operation not allowed after ResultSet closed

原因:ResultSet依赖Statement生命周期,Statement关闭后ResultSet自动失效。
解决:将数据一次性读入内存再关闭资源:

List<Attendance> list = new ArrayList<>(); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(new Attendance(...)); // 构造对象 } } // rs在此关闭,但list已持有全部数据 return list; // 返回list,而非rs

5. 性能压测与边界验证:用10万条模拟数据检验系统健壮性

5.1 生成10万条考勤记录的Python脚本(离线生成,避免Swing卡死)

当课设进入答辩阶段,老师常会问:“如果公司有5000名员工,每人每年参加20次培训,系统能扛住吗?” 此时现场演示比讲原理更有说服力。我们用Python快速生成10万条考勤数据(约等效2000员工×50次培训),直接INSERT到MySQL:

# generate_attendance.py import mysql.connector from datetime import datetime, timedelta import random conn = mysql.connector.connect( host='localhost', user='root', password='123456', database='hr_training' ) cursor = conn.cursor() # 预先获取所有plan_id cursor.execute("SELECT plan_id FROM training_plan") plan_ids = [row[0] for row in cursor.fetchall()] # 生成10万条记录 for i in range(100000): plan_id = random.choice(plan_ids) # 随机生成check_in时间(过去一年内) base_date = datetime.now() - timedelta(days=random.randint(0, 365)) check_in = base_date + timedelta(hours=random.randint(8, 18), minutes=random.randint(0, 59)) # 50%概率有check_out check_out = check_in + timedelta(minutes=random.randint(30, 180)) if random.random() > 0.5 else None is_late = random.random() < 0.05 # 5%迟到率 cursor.execute( "INSERT INTO attendance (plan_id, check_in, check_out, is_late) VALUES (%s, %s, %s, %s)", (plan_id, check_in, check_out, is_late) ) if i % 10000 == 0: print(f"Inserted {i} records...") conn.commit() conn.commit() cursor.close() conn.close() print("Done.")

执行要点:脚本在MySQL命令行外运行,避免Swing主线程阻塞;每万条commit()一次,防止事务过大OOM;生成数据后执行ANALYZE TABLE attendance更新统计信息,让优化器选对索引。

5.2 Swing界面响应时间实测:三类操作的毫秒级真相

用System.nanoTime()在关键方法前后打点,测试10万数据下的真实耗时(i5-10210U / 16GB / MySQL 8.0.33):

操作类型SQL语句平均耗时是否触发索引优化建议
查某员工全部考勤SELECT * FROM attendance a JOIN training_plan tp ON a.plan_id=tp.plan_id WHERE tp.employee_id='EMP001'128ms✅idx_plan_time(plan_id已覆盖)无需优化
查某日全部签到SELECT * FROM attendance WHERE DATE(check_in) = '2024-06-01'2150ms❌DATE()函数导致索引失效改用check_in >= '2024-06-01' AND check_in < '2024-06-02',降至 89ms
批量更新状态UPDATE attendance SET is_late=TRUE WHERE check_in < '2024-01-01 09:00:00'3400ms❌ 无索引在check_in上建单独索引,降至 210ms

表格结论:DATE()函数是索引杀手,必须规避;check_in字段高频用于范围查询,单独索引收益巨大;Swing界面上“批量更新”按钮背后,应预判用户操作习惯(如按日期筛选后更新),提前建好对应索引。

5.3 连接池的隐形成本:HikariCP vs 原生DriverManager

课设初期常用DriverManager.getConnection(),但10万数据下频繁创建连接会拖垮性能。对比测试:

方式100次并发查询平均耗时连接创建开销是否复用连接推荐度
DriverManager420ms每次新建,约80ms/次❌⚠️ 仅限课设演示
HikariCP(max=20)95ms首次创建后复用✅✅ 生产级必选

集成HikariCP只需3步:

  1. Maven引入:<dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>5.0.1</version></dependency>;
  2. 初始化连接池(单例):
public class DataSource { private static HikariDataSource ds; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/hr_training?..."); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); ds = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }
  1. Swing所有DAO方法改用DataSource.getConnection()替代DriverManager.getConnection()。

我的习惯:课设第一版用DriverManager快速验证逻辑,第二版立刻切HikariCP——因为答辩时老师很可能问“如果并发100人同时查考勤,系统会怎样?”,而你已经测过数据。

希望帮到你。

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

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

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

立即咨询