简介:湖南科技大学数据库系统课程设计项目《企业员工培训管理系统》是一套基于JavaSwing与MySQL的完整数据库应用,面向计算机相关专业学生和开发人员,解决企业在员工培训计划管理、课程考勤记录、培训资源分配和员工绩效评估方面的核心需求。压缩包共包含67个文件,包括49个Java源码文件、3份PDF文档(课程设计报告、题目要求、指导书)、初始数据SQL脚本、E-R图文件、README说明等,整体大小约4.06MB。目前已有45人学习,适合作为课程设计参考或项目复现的蓝本。内容提供完整的JavaSwing界面代码与MySQL后端逻辑,附有数据库初始数据和Gradle构建配置,课程设计报告详述功能说明与设计思路,可帮助读者理解从需求分析、E-R建模到系统实现的全过程。文档另附题目要求与指导书,便于对照逐项检查项目的完整度与规范性。
1. 课程设计为何总选JavaSwing+MySQL:这个员工培训系统到底解决什么
每年到了数据库系统课程设计选题的时候,总能看到一批图书管理、学生选课、超市收银系统。这几个系统上一届的学长已经写烂了,演示时老师提问的粒度也会越来越细。我见过不少同学把时间花在界面美化上,最后被一条“这张表满足第几范式”问住,说明业务和数据库模型没咬合。企业员工培训管理系统恰好能把这个短板补上:员工、培训计划、课程、考勤、资源、绩效,多个实体天然形成一条从“安排培训”到“验证效果”的数据流,JavaSwing做桌面端,MySQL存数据,正好覆盖数据库系统课程设计要求的表结构设计、关系模式、外键约束、统计查询和存储过程。
这套方案适合正在做数据库系统课程设计、Java课程设计案例源码找不到头绪的同学,也适合想快速把JDBC、JTable、DAO这些知识点串成完整项目的人。我一般把项目拆成四层:先写建库脚本,再写数据库工具类,然后按实体写DAO,最后把Swing的窗口和按钮接上去,流程不复杂,但每一步都能在报告里写清楚。
2. 先把数据模型立住:企业培训系统的表设计与ER图
2.1 七张表的职责拆分与字段命名
在写任何Java代码之前,我先把ER图画清楚。《数据库系统概论》里强调“概念结构设计是先于逻辑结构设计的”,对课程设计来说,ER图既是报告的第一张图,也是后面所有SQL的依据。这个项目里的实体有:员工employee、培训计划training_plan、课程course、考勤记录attendance、培训资源training_resource、资源分配resource_allocation、绩效评估performance。员工和培训计划不直接关联,而是通过课程和考勤表间接关联,因为一个员工可以参加多个计划,一个计划下有多门课程,课程与员工是多对多关系,考勤记录就是中间的关联表,也是绩效统计的数据源。
字段命名我统一用一套规则:主键都用id INT AUTO_INCREMENT,业务编号单独用emp_no、plan_no这样的字段并加唯一索引,外键一律叫xxx_id,公共时间字段叫create_time和update_time。不要直接用员工姓名当主键,重名和改名都会导致级联修改,答辩时不好讲。金额、成绩、出勤率这类带小数的字段用DECIMAL,不要用FLOAT,避免精度问题。下面是七张表的职责划分。
| 表名 | 职责 | 关键字段 |
|---|---|---|
| employee | 员工基础信息 | emp_no唯一,department、position |
| training_plan | 培训计划主表 | plan_no唯一,start_date/end_date,status |
| course | 课程明细,挂在计划下 | plan_id外键,course_date/class_hours |
| training_resource | 资源台账 | resource_name,total_count,available_count |
| resource_allocation | 课程与资源的分配关系 | course_id/resource_id唯一组合,use_count |
| attendance | 员工课程考勤 | employee_id+course_id唯一组合,attend_status,absence_count,exam_score |
| performance | 员工在计划下的绩效 | employee_id+plan_id唯一,attendance_rate,exam_score,grade |
这段设计的主线是:计划下有课程,课程要分配资源,员工参加课程产生考勤,考勤汇总成绩效。课程设计最忌讳把员工培训记录全部塞进一张大宽表,那样展示不了范式分析。像《数据库系统概论》里讲的,把绩效表单独抽出来,避免员工表里出现对培训计划字段的部分函数依赖,这就是一个现成的范式分析素材。
2.2 从ER图到建表SQL:Workbench 导出后再改的完整脚本
我经常用MySQL Workbench的EER Model画ER图。如果你按照mysql workbench使用教程里的“Create EER Model from Database”反过来生成图也可以,但自动生成的表脚本会带一堆反引号和命名前缀。课程设计建议自己手写建表脚本,这样每个约束都能讲清楚。下面是一套可以直接复制执行的初始化脚本。
-- 建库:字符集统一 utf8mb4,避免中文乱码 CREATE DATABASE IF NOT EXISTS training_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE training_db; -- 员工表 CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '员工工号', emp_name VARCHAR(30) NOT NULL COMMENT '员工姓名', department VARCHAR(50) COMMENT '部门', position VARCHAR(30) COMMENT '岗位', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 培训计划表 CREATE TABLE training_plan ( id INT PRIMARY KEY AUTO_INCREMENT, plan_no VARCHAR(20) NOT NULL UNIQUE, plan_name VARCHAR(100) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, responsible_person VARCHAR(30), status TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, plan_id INT NOT NULL, course_name VARCHAR(100) NOT NULL, lecturer VARCHAR(30), class_hours INT DEFAULT 1, course_date DATETIME, location VARCHAR(50), CONSTRAINT fk_course_plan FOREIGN KEY (plan_id) REFERENCES training_plan(id) ON DELETE CASCADE ) ENGINE=InnoDB; -- 培训资源表 CREATE TABLE training_resource ( id INT PRIMARY KEY AUTO_INCREMENT, resource_name VARCHAR(50) NOT NULL, resource_type VARCHAR(20) COMMENT '教室/投影/教材', total_count INT DEFAULT 0, available_count INT DEFAULT 0 ) ENGINE=InnoDB; -- 资源分配记录表 CREATE TABLE resource_allocation ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, resource_id INT NOT NULL, use_count INT DEFAULT 1, CONSTRAINT fk_alloc_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, CONSTRAINT fk_alloc_resource FOREIGN KEY (resource_id) REFERENCES training_resource(id) ON DELETE CASCADE, UNIQUE KEY uk_course_resource (course_id, resource_id) ) ENGINE=InnoDB; -- 考勤记录表 CREATE TABLE attendance ( id INT PRIMARY KEY AUTO_INCREMENT, employee_id INT NOT NULL, course_id INT NOT NULL, attend_status TINYINT DEFAULT 0 COMMENT '0缺勤 1出勤 2迟到 3早退', absence_count TINYINT DEFAULT 0 COMMENT '缺勤次数默认0', exam_score DECIMAL(5,2) DEFAULT NULL COMMENT '课程考核分', remark VARCHAR(255), CONSTRAINT fk_att_emp FOREIGN KEY (employee_id) REFERENCES employee(id) ON DELETE CASCADE, CONSTRAINT fk_att_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, UNIQUE KEY uk_emp_course (employee_id, course_id) ) ENGINE=InnoDB; -- 绩效评估表 CREATE TABLE performance ( id INT PRIMARY KEY AUTO_INCREMENT, employee_id INT NOT NULL, plan_id INT NOT NULL, total_hours INT DEFAULT 0, attendance_rate DECIMAL(5,2) DEFAULT 0.00, exam_score DECIMAL(5,2) DEFAULT 0.00, grade VARCHAR(5) DEFAULT 'C', CONSTRAINT fk_perf_emp FOREIGN KEY (employee_id) REFERENCES employee(id) ON DELETE CASCADE, CONSTRAINT fk_perf_plan FOREIGN KEY (plan_id) REFERENCES training_plan(id) ON DELETE CASCADE, UNIQUE KEY uk_emp_plan (employee_id, plan_id) ) ENGINE=InnoDB;这段脚本里有几个参数值得在报告里展开。ENGINE=InnoDB是为了支持外键和事务,MyISAM虽然读起来快,但不支持外键约束,课程设计老师看到MyISAM基本会追问。ON DELETE CASCADE表示删除课程后,该课程下的考勤和资源分配自动清理;删除员工后,其考勤和绩效也一并清理。这里要能讲清楚为什么用级联删除:员工离职后保留其培训记录没有实际意义,而且会造成“孤儿数据”。UNIQUE KEY是防重复的最佳手段,attendance表对(employee_id, course_id)加唯一索引,用户连点两次出勤按钮,不会生成两行记录。
字符集方面,建库时用utf8mb4而不是utf8,因为utf8mb4支持完整的Unicode,包括emoji。DATETIME和TIMESTAMP的区别也要知道:course_date用DATETIME,不随数据库时区变化;create_time用DEFAULT CURRENT_TIMESTAMP,update_time加ON UPDATE CURRENT_TIMESTAMP,后续改数据时这一列会自动刷新,省掉Java端手动维护时间。
2.3 初始化数据与三个让评委点赞的约束
写完表之后,一定要塞几行演示数据,否则界面和SQL调试都会卡在“没有数据”的状态。初始化数据可以写成脚本,也可以直接在Workbench里执行。我建议保留脚本文件,因为课程设计报告里应当有“数据初始化”这一节。下面是一份适合直接粘贴的初始数据。
INSERT INTO employee (emp_no, emp_name, department, position) VALUES ('E001', '张三', '技术部', '后端工程师'), ('E002', '李四', '技术部', '测试工程师'), ('E003', '王五', '市场部', '运营专员'); INSERT INTO training_plan (plan_no, plan_name, start_date, end_date, responsible_person, status) VALUES ('P20250301', '新人入职培训计划', '2025-03-01', '2025-03-20', '赵六', 0); INSERT INTO course (plan_id, course_name, lecturer, class_hours, course_date, location) VALUES (1, '企业文化与制度', '赵六', 2, '2025-03-03 09:00:00', 'A101'), (1, 'Java基础与数据库', '刘工', 8, '2025-03-05 09:00:00', '机房B204'); INSERT INTO training_resource (resource_name, resource_type, total_count, available_count) VALUES ('A101教室', '教室', 50, 50), ('投影仪', '设备', 5, 5), ('内部培训教材', '教材', 30, 30); INSERT INTO resource_allocation (course_id, resource_id, use_count) VALUES (1, 1, 1), (2, 2, 1), (2, 3, 30); INSERT INTO attendance (employee_id, course_id, attend_status, exam_score) VALUES (1, 1, 1, 85.00), (2, 1, 1, 90.00), (3, 1, 0, NULL);这段初始化数据里体现三个加分点。第一,唯一约束:resource_allocation有(course_id, resource_id),attendance有(employee_id, course_id),performance有(employee_id, plan_id),答辩时可以讲“用数据库约束防重复,而不是在Java里做判断”。第二,默认值:attendance.absence_count和attend_status都用DEFAULT 0,这就是“mysql设置默认值为0”的实际场景,录入考勤时只要给出员工、课程、状态,缺勤次数自动从0开始。第三,外键约束:没有课程id插不了考勤,没有员工id插不了考勤,数据库从源头挡住脏数据。
如果你用的MySQL是8.0以上,可以给performance表加CHECK (attendance_rate BETWEEN 0 AND 100)这类约束,8.0会真正生效。5.7版本会忽略CHECK,需要在Java端或触发器中校验。这个细节可以作为“版本差异”放在报告里,但不要为了一个约束去改MySQL版本,连接不上时排查版本反而是麻烦事。
3. 用JDBC把JavaSwing和MySQL连起来:DAO层与连接池的取舍
3.1 驱动加载与URL参数:连接MySQL 8 的正确打开方式
JavaSwing和MySQL之间的桥梁是JDBC。课程设计里最常翻车的就是连接这一步,很多同学把数据库管理工具能连上当成Java能连上,结果在DriverManager.getConnection处抛异常。先上一个最小可用的DBUtil类。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/training_db" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // mysql-connector-java 8.x 使用 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("缺少 MySQL JDBC 驱动", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里的参数每一个都有明确作用。useSSL=false表示关闭SSL握手,本地学习环境不需要加密,可以消除一堆告警。serverTimezone=Asia/Shanghai解决MySQL 8.0的时区报错,这是最常见的连接失败原因。characterEncoding=utf8和数据库的utf8mb4对应,保证中文写入不乱码。allowPublicKeyRetrieval=true解决MySQL 8.0默认caching_sha2_password认证插件在非SSL连接下报“Public Key Retrieval is not allowed”的问题。
驱动类名也要注意:mysql-connector-java 5.x写com.mysql.jdbc.Driver,8.x改成了com.mysql.cj.jdbc.Driver。网上大量旧博客写的还是5.x的叫法,如果你下载的是8.x驱动,这里会直接ClassNotFoundException。其实JDBC 4.0之后可以不写Class.forName,SPI机制会自动加载驱动,但课程设计里写上这一句,老师问“驱动怎么加载”时更容易答。
连接池要不要上?课程设计规模不大,DriverManager每次getConnection的开销可以接受。但如果你界面读一次列表就开一次连接,而且不关闭,性能会越来越差。更规范的做法是引入HikariCP或Druid连接池,配一个最小连接数和最大连接数。我一般不会在课程设计里强行上连接池,因为答辩容易被追问“连接池参数依据是什么”。用DriverManager但保证连接在finally里关闭,信息已经足够。
3.2 写一个员工DAO:PreparedStatement与ResultSet映射
连接有了,下一步把数据库表映射成Java对象。常见做法是给每个实体写一个DAO类,方法名和业务动作对齐:insert、update、delete、findById、findAll。下面用EmployeeDAO说明最常用的两个方法。
import java.sql.*; import java.util.ArrayList; import java.util.List; public class EmployeeDAO { private Connection conn; public EmployeeDAO(Connection conn) { this.conn = conn; } // 新增员工,返回自增主键 public int insert(Employee emp) throws SQLException { String sql = "INSERT INTO employee(emp_no, emp_name, department, position, phone) " + "VALUES(?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, emp.getEmpNo()); ps.setString(2, emp.getEmpName()); ps.setString(3, emp.getDepartment()); ps.setString(4, emp.getPosition()); ps.setString(5, emp.getPhone()); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { return rs.getInt(1); } } } return 0; } // 查询全部员工,按部门再按工号排序 public List<Employee> findAll() throws SQLException { String sql = "SELECT id, emp_no, emp_name, department, position, phone " + "FROM employee ORDER BY department, emp_no"; List<Employee> list = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Employee e = new Employee(); e.setId(rs.getInt("id")); e.setEmpNo(rs.getString("emp_no")); e.setEmpName(rs.getString("emp_name")); e.setDepartment(rs.getString("department")); e.setPosition(rs.getString("position")); e.setPhone(rs.getString("phone")); list.add(e); } } return list; } }这里有两个关键点。第一,PreparedStatement的占位符从1开始,按SQL里的?顺序赋值,不要用字符串拼接SQL,否则会出现SQL注入,也会在字段值里带中文引号时翻车。第二,Statement.RETURN_GENERATED_KEYS用于插入后拿回自增id,这个id后续要给课程表当plan_id、给考勤表当employee_id,拿不到id,所有子表都插不进去。
ResultSet映射时,列名直接用rs.getInt("id")这种方式,比按下标rs.getInt(1)可读性好。如果SQL里写了JOIN且有同名列,要使用表前缀或AS别名,避免取值错位。try-with-resources会自动关闭ResultSet和Statement,这一点很关键,不关闭的话长时间运行会把数据库连接数耗尽。
为什么DAO层要单独存在?因为界面不直接写SQL,后期换成SQLite或者改成MyBatis时只需要改DAO实现。这个解释虽然不是课程设计的硬性要求,但报告里写“表现层与数据访问层分离”是加分项。实际操作中,我给DAO方法统一加throws SQLException,把异常抛给上层统一处理,避免在每个方法里try-catch把错误吞掉。
3.3 用JTable和JComboBox填充数据:下拉框不是写死的
Swing里最常用的两个数据组件是JTable和JComboBox。它们的数据应该来自数据库,而不是界面里硬编码的数组。下面这段代码把员工列表刷到表格和下拉框。
private void loadEmployeeTable() { DefaultTableModel model = new DefaultTableModel( new String[]{"工号", "姓名", "部门", "岗位", "电话"}, 0); try (Connection conn = DBUtil.getConnection()) { EmployeeDAO dao = new EmployeeDAO(conn); for (Employee e : dao.findAll()) { model.addRow(new Object[]{ e.getEmpNo(), e.getEmpName(), e.getDepartment(), e.getPosition(), e.getPhone() }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "加载员工失败:" + ex.getMessage()); return; } employeeTable.setModel(model); } private void loadEmployeeCombo() { DefaultComboBoxModel<Employee> comboModel = new DefaultComboBoxModel<>(); try (Connection conn = DBUtil.getConnection()) { for (Employee e : new EmployeeDAO(conn).findAll()) { comboModel.addElement(e); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "员工下拉框加载失败:" + ex.getMessage()); } empCombo.setModel(comboModel); }DefaultTableModel的第二个参数是列名数组,第三个参数0表示初始零行,后面用addRow逐行填充。这样调整列顺序时不需要改遍历逻辑。loadEmployeeCombo里的Employee对象必须重写toString(),否则下拉框会显示Employee@1a2b3c这种对象地址,老师看到会皱眉,这是典型的“黑匣子”表现。
每次打开窗体都要重新加载下拉框,不要在类里用静态初始化员工列表。因为另一个窗口可能已经新增了员工,下拉框里的id和数据表不一致,后面选考勤时外键就会失败。这段代码仍然在Swing事件线程里执行,教学场景数据量小没问题,如果查询超过了事秒级,就要考虑SwingWorker,第5章会展开。
4. 从培训计划到绩效评估:四个模块的落地代码
4.1 培训计划管理:日期校验与课程表CRUD
培训计划是整个系统的入口。计划管理界面的核心操作是新增计划和在计划下添加课程,常见错误是不管日期直接插入,等统计时发现结束日期小于开始日期,数据一团糟。所以我在DAO层就做一次校验。
public boolean addTrainingPlan(TrainingPlan plan) throws SQLException { if (plan.getEndDate().before(plan.getStartDate())) { throw new IllegalArgumentException("结束日期不能早于开始日期"); } String sql = "INSERT INTO training_plan(plan_no, plan_name, start_date, end_date, responsible_person, status) " + "VALUES(?, ?, ?, ?, ?, 0)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, plan.getPlanNo()); ps.setString(2, plan.getPlanName()); ps.setDate(3, new java.sql.Date(plan.getStartDate().getTime())); ps.setDate(4, new java.sql.Date(plan.getEndDate().getTime())); ps.setString(5, plan.getResponsiblePerson()); return ps.executeUpdate() == 1; } }校验放在DAO层,界面按钮也做一个if判断,双层防护。如果只有界面判断,绕过界面直接调DAO时依旧会把坏数据写进库。new java.sql.Date(plan.getStartDate().getTime())是常见坑,直接把java.util.Date传给MySQL 8驱动,部分版本会报DataTruncation或日期变成零点,稳妥做法是显示转换。
status字段用TINYINT,0未开始、1进行中、2已完成。查询列表时用ORDER BY start_date, plan_no,这是mysql排序的一个简单应用,能让计划按开始日期排好,界面展示更自然。课程表是计划的子表,添加课程时plan_id由当前选中计划决定,SQL结构和addTrainingPlan几乎一样,只是多一个外键字段。我一般把课程管理做成计划界面的Tab页,选中计划后下方显示该计划课程表,再点新增课程。
修改和删除计划时要考虑子表。如果培训计划已经产生考勤记录,删除计划会通过外键级联删除考勤,需要在删除前弹窗提示“将删除该计划下所有课程与考勤”。这个业务规则可以写在DAO层,也可以写在界面的确认对话框里,课程设计更看重后者的用户体验。
4.2 课程考勤记录:唯一索引、缺省值0与ON DUPLICATE KEY UPDATE
考勤是绩效统计的原料,也是最容易出现脏数据的模块。一个员工在同一门课程里只应该有一条考勤记录,状态可能是出勤、缺勤、迟到或早退。我先在表上建了唯一索引,再用MySQL的ON DUPLICATE KEY UPDATE让它具备“重复点击就更新”的能力。
public boolean recordAttendance(int employeeId, int courseId, int status) throws SQLException { String insert = "INSERT INTO attendance(employee_id, course_id, attend_status) VALUES(?, ?, ?) " + "ON DUPLICATE KEY UPDATE attend_status = VALUES(attend_status)"; try (PreparedStatement ps = conn.prepareStatement(insert)) { ps.setInt(1, employeeId); ps.setInt(2, courseId); ps.setInt(3, status); ps.executeUpdate(); if (status == 0) { try (PreparedStatement upd = conn.prepareStatement( "UPDATE attendance SET absence_count = 1 " + "WHERE employee_id = ? AND course_id = ? AND attend_status = 0")) { upd.setInt(1, employeeId); upd.setInt(2, courseId); upd.executeUpdate(); } } return true; } }ON DUPLICATE KEY UPDATE是MySQL专有语法,命中唯一冲突时不再插入新行,而是更新状态。这对连点两次“出勤”的场景很有用。如果是通用数据库,需要先select再insert,但课程设计用MySQL就可以展示这个技巧。attendance.absence_count建表时DEFAULT 0,只有当attend_status = 0时才更新为1,这就是“mysql设置默认值为0”的意义:不手动去设,它也不会误记。
考勤列表展示时用JOIN查询员工姓名和课程名,否则JTable里显示id很难看。常见写法如下,放到AttendanceDAO里形成一个findVOList(),返回的VO对象带empName和courseName,JTable直接绑定。
SELECT a.id, e.emp_name, c.course_name, a.attend_status, a.exam_score FROM attendance a JOIN employee e ON a.employee_id = e.id JOIN course c ON a.course_id = c.id ORDER BY c.course_date, e.emp_no;这里再提一次JOIN同名列的问题:employee和course都有id,如果SELECT写*,rs.getInt("id")取到的可能是employee.id,一定要用表别名加列名。考勤状态在界面显示时,用Map或switch把0转成“缺勤”、1转成“出勤”、2转成“迟到”、3转成“早退”,不要在JTable里直接显示数字。
4.3 培训资源分配:关联查询、事务与占用检查
培训资源分配这个模块在课本里容易被做成简单的资源列表CRUD,实际上它是最能体现关联查询和事务的模块。课程需要教室、投影、教材,分配时不仅要记录使用数量,还要刷新资源的可用数。下面先做资源占用检查。
-- 分配前检查:同一时间段该资源是否已被占用 SELECT COUNT(*) FROM resource_allocation a JOIN course c ON a.course_id = c.id WHERE a.resource_id = ? AND c.course_date = ?; -- 关联查询:展示课程的资源分配情况 SELECT c.course_name, r.resource_name, r.resource_type, a.use_count FROM resource_allocation a JOIN course c ON a.course_id = c.id JOIN training_resource r ON a.resource_id = r.id ORDER BY c.course_date, r.resource_type;分配界面左侧显示课程列表,右侧显示资源列表,选中资源填数量后点“分配”。插入前先SELECT ... FOR UPDATE锁住资源行,防止两个课程同时把同一台投影仪分出去。这在单机课程设计里很少触发,但事务与锁是《数据库系统概念》里的考点,值得写进报告。
扣减available_count和插入resource_allocation必须放在一个事务里:关闭自动提交,成功后commit,异常时rollback。不放事务的话,一旦插入失败,可用数已经被扣了,数据就不一致。UNIQUE KEY uk_course_resource保证同一课程不会重复分配同一资源,如果重复分配,第二次应该走更新数量而不是新增。
也可以不做实时扣减,而是用“该课程在某个时间段占用了哪些资源”的冲突查询,检查course.course_date相同段的资源重复。两种方案都能跑,但后者更贴近“资源分配冲突”的真实业务。课程设计建议选一种在报告里重点展开,不要两种都讲得含糊。我一般选扣减方式,操作直观,答辩也容易演示。
4.4 员工绩效评估:用统计SQL把培训数据变成绩效
绩效评估是系统的收尾模块,也是最容易让老师眼前一亮的模块。它在计划结束后统计每名员工的总课时、出勤率、平均成绩,并生成评级。数据全部来自前面四张表,不需要额外录入,这就是数据库一致性带来的好处。
SELECT e.emp_no, e.emp_name, COALESCE(SUM(c.class_hours), 0) AS total_hours, ROUND(SUM(CASE WHEN a.attend_status != 0 THEN 1 ELSE 0 END) / COUNT(a.id) * 100, 2) AS attendance_rate, ROUND(COALESCE(AVG(a.exam_score), 0), 2) AS avg_score FROM employee e LEFT JOIN attendance a ON e.id = a.employee_id LEFT JOIN course c ON a.course_id = c.id AND c.plan_id = ? GROUP BY e.id, e.emp_no, e.emp_name;LEFT JOIN保证没有选课的员工也出现在列表里,否则统计会漏人。COALESCE(..., 0)处理NULL,SUM对没有记录的结果为NULL,页面显示就会空白。出勤率公式是出勤次数 / 记录次数 * 100,其中attend_status != 0表示出勤、迟到、早退都算到场;如果你想把迟到算缺勤,把条件改成= 1即可。这个统计口径要在报告的“统计规则”里写清楚。
GROUP BY e.id, e.emp_no, e.emp_name是因为MySQL默认开启了only_full_group_by,SELECT里的非聚合列必须都出现在GROUP BY中,只按e.id分组在5.7就会报错。生成performance表时,先DELETE FROM performance WHERE plan_id = ?再插入,避免多次点击生成导致重复累积。这一步正好用到mysql update语法,所有UPDATE都要带WHERE,否则会把所有员工的等级一起改掉。
public void generatePerformance(int planId) throws SQLException { String select = "SELECT e.id, e.emp_no, e.emp_name, " + "COALESCE(SUM(c.class_hours), 0) AS total_hours, " + "ROUND(SUM(CASE WHEN a.attend_status != 0 THEN 1 ELSE 0 END) / COUNT(a.id) * 100, 2) AS attendance_rate, " + "ROUND(COALESCE(AVG(a.exam_score), 0), 2) AS avg_score " + "FROM employee e " + "LEFT JOIN attendance a ON e.id = a.employee_id " + "LEFT JOIN course c ON a.course_id = c.id AND c.plan_id = ? " + "GROUP BY e.id, e.emp_no, e.emp_name"; conn.setAutoCommit(false); try (PreparedStatement del = conn.prepareStatement("DELETE FROM performance WHERE plan_id = ?")) { del.setInt(1, planId); del.executeUpdate(); try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO performance(employee_id, plan_id, total_hours, attendance_rate, exam_score) " + "VALUES(?, ?, ?, ?, ?)"); PreparedStatement query = conn.prepareStatement(select)) { query.setInt(1, planId); try (ResultSet rs = query.executeQuery()) { while (rs.next()) { ps.setInt(1, rs.getInt("id")); ps.setInt(2, planId); ps.setInt(3, rs.getInt("total_hours")); ps.setBigDecimal(4, rs.getBigDecimal("attendance_rate")); ps.setBigDecimal(5, rs.getBigDecimal("avg_score")); ps.addBatch(); } ps.executeBatch(); } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }这段Java代码先删旧数据,再统计员工表并批量插入绩效表,整个过程放在一个事务里。ps.addBatch()和ps.executeBatch()做批量插入,比逐条executeUpdate快很多。绩效评级也可以在SQL里用CASE WHEN一步完成,比如出勤率大于等于90且平均分大于等于80给A,大于等于80给B,否则C,这样JTable直接显示评级,不需要Java再算一遍。
5. JavaSwing+MySQL课程设计避坑:我看过的翻车现场与排查步骤
5.1 现象:ClassNotFoundException: com.mysql.jdbc.Driver
双击jar或者从旧博客复制代码,运行程序第一句就抛出java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因几乎只有一个:驱动jar不在编译classpath里,或者驱动类名写成了老版本。mysql-connector-java 5.x的包名是com.mysql.jdbc.Driver,8.x变成了com.mysql.cj.jdbc.Driver,课程设计里很多同学拿的是8.x驱动,却抄了5.x代码。
解决方法是先从mysql下载官网或IDE的Maven仓库下载对应驱动jar,放到项目根目录的lib目录,在IDEA里选中jar右键“Add as Library”。代码里统一写com.mysql.cj.jdbc.Driver。还要确认jar有没有真正进入编译输出目录,我遇到过Build后lib没打包,运行脚本时找不到驱动的情况,可以在class目录搜索Driver.class确认。这个排查顺序能省半小时。
5.2 现象:Communications link failure 和时区报错
点按钮后弹The server time zone value '�й��� ��ʱ' is unrecognized,或者Communications link failure。原因是MySQL 8驱动要求URL里带serverTimezone,否则拿不到连接;Communications link failure则大多是MySQL服务没启动、端口不是3306、用户名密码不对。
解决方法是URL加serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。然后检查服务状态:Windows用net start mysql,Linux用systemctl status mysql。如果数据库装在远程服务器,授权时不要只授权localhost,课程设计连本机即可。如果改了my.ini端口,URL里的3306要同步改。MySQL安装配置教程里通常会把MySQL注册成Windows服务,装完后先确认服务启动,再跑Java,不要直接跳到写代码。
5.3 现象:数据库和界面都是中文问号
新增员工“张三”写入后变成“???”。原因非常集中:建库字符集不是utf8mb4,JDBC URL没指定characterEncoding,或者Java源文件编译编码不对。这三处只要有一处不一致,中文就会出问题。
解决方法是建库SQL加DEFAULT CHARACTER SET utf8mb4,URL加characterEncoding=utf8,IDEA把Project Encoding和File Encoding设为UTF-8。如果旧数据已经乱了,可以ALTER TABLE employee CONVERT TO CHARACTER SET utf8mb4,但已经被问号破坏的数据大概率救不回来,所以要在建库第一步做对。出现乱码别急着改代码,先用Workbench直接执行INSERT看库里是否正常,分清是MySQL侧还是Java侧。
5.4 现象:点“生成绩效”后窗口卡死,标题栏出现“未响应”
数据量不大,但每次点击按钮后整个窗口白屏,过几秒才恢复。原因是统计SQL在Swing的EDT线程执行,界面刷新被阻塞;如果还开着连接不关闭,连接用满也会拖慢。解决方法是把耗时操作放到SwingWorker后台线程,doInBackground里执行SQL和数据遍历,done里更新JTable。
SwingWorker<List<Performance>, Void> worker = new SwingWorker<>() { @Override protected List<Performance> doInBackground() throws Exception { try (Connection conn = DBUtil.getConnection()) { return new PerformanceDAO(conn).queryByPlan(planId); } } @Override protected void done() { try { List<Performance> list = get(); // 在这里更新 JTable 模型 } catch (Exception e) { JOptionPane.showMessageDialog(frame, "统计失败:" + e.getMessage()); } } }; worker.execute();在doInBackground里碰任何Swing组件都会引发线程安全问题,表现为随机闪一下或偶尔卡死,这是Swing多线程的黑匣子。如果不想上SwingWorker,至少先要把查询结果放进List,再一次性构造TableModel,不要在addRow里执行查询。
5.5 现象:外键插入失败、考勤重复、UPDATE把整表改了
添加考勤时明明选了员工,插入报Cannot add or update a child row: a foreign key constraint fails,或者同一员工同一课程录了两遍,或者想改一个人考勤,update之后全表状态都变了。原因分别是下拉框里的id是旧数据、表上没加唯一约束、UPDATE漏了WHERE。
解决方法是插入前用SELECT COUNT(*) FROM employee WHERE id = ?校验,或者捕获SQLIntegrityConstraintViolationException后弹“员工/课程已失效,请刷新列表”。重复考勤靠唯一索引加ON DUPLICATE KEY UPDATE,不要在Java里先select再insert,两个窗口同时操作时还是会重复。UPDATE语句先写WHERE再写SET,用真实数据测试:
UPDATE attendance SET attend_status = 1 WHERE employee_id = 1 AND course_id = 1;我见过的血泪经验是,在MySQL客户端验证时忘了写WHERE,三屏数据全被改成出勤状态,只能从备份恢复。课程设计一定要在初始化脚本里保留INSERT数据,给自己留后悔药。
如果在答辩现场翻车,我的排查顺序是:先看MySQL服务端口,再看URL参数,再看驱动jar,再看字符集,最后才是业务SQL。前面四个现象有一半是环境问题,不是代码问题,把这个顺序写在项目README里,比临场刷新有用。
6. 答辩验收与后续可做的扩展:让课程设计从“能跑”到“能讲”
6.1 演示时的数据准备与操作顺序
答辩演示的顺序比功能多少更重要。我每次演示都按一条主线走:先在员工管理里新增一名员工,再建一个培训计划,给计划添加两门课程,课程分配资源,切到考勤界面给员工打考勤,最后点“生成绩效”,让页面上出现总课时和出勤率。这条线从无到有,每一步都依赖上一步入库的数据,比分别展示四个窗口更清晰。
演示前把测试数据恢复到初始化状态,不要带着以前考勤过的数据直接点生成。老师如果问“这个绩效是怎么来的”,你按顺序操作,他可以顺着看见数据流向;如果提前数据太多,回答时会绕圈子。
6.2 三个低成本可加分扩展
第一个扩展是把绩效生成SQL包成MySQL存储过程sp_generate_performance(plan_id),Java端只调CALL sp_generate_performance(?)。课程设计报告里多了“存储过程”一节,实际操作不一定要用存储过程,但教学评分标准里这是加分大项。
第二个扩展是考勤状态细化。把attend_status改成1出勤、2迟到、3早退、0缺勤,统计时按course.course_date和当前时间差计算迟到,在JTable上显示中文状态。成本只有一个状态字段和一个时间比较函数,却能体现你考虑了真实考勤场景。
第三个扩展是把Excel员工名单批量导入。用POI读Excel,JDBC批量addBatch,一次导入几百人,界面加一个进度条。这个扩展不需要改数据库表,只新增导入逻辑,但演示效果很直观,也顺带复习了文件解析和批量操作。
6.3 提交前自查清单
| 检查项 | 看什么 | 常见翻车点 |
|---|---|---|
| JDBC URL | useSSL=false、serverTimezone、characterEncoding | 时区、乱码 |
| 驱动jar | 在项目lib里,类名com.mysql.cj.jdbc.Driver | ClassNotFoundException |
| 字符集 | 建库utf8mb4,Java源文件UTF-8 | 中文问号 |
| 外键与唯一索引 | 考勤、资源分配、绩效是否防重复 | 重复数据 |
| UPDATE语句 | 是否带WHERE | 整表数据被改 |
| 连接释放 | Connection、Statement、ResultSet是否关闭 | 界面卡死 |
提交前我习惯把数据库初始化脚本从头执行一遍,再把项目从零跑一遍,模拟老师拿到zip后的第一反应。课程设计的代码不一定要花哨,但每一步操作都有对应数据反馈,你讲起来也有底气。希望这些踩坑记录帮到你,至少不要在第一轮连接数据库时就卡住。
本文还有配套的精品资源,点击获取