简介湖南科技大学数据库系统课程设计项目《企业员工培训管理系统》是一套基于JavaSwing与MySQL的完整数据库应用面向计算机相关专业学生和开发人员解决企业在员工培训计划管理、课程考勤记录、培训资源分配和员工绩效评估方面的核心需求。压缩包共包含67个文件包括49个Java源码文件、3份PDF文档课程设计报告、题目要求、指导书、初始数据SQL脚本、E-R图文件、README说明等整体大小约4.06MB。目前已有45人学习适合作为课程设计参考或项目复现的蓝本。内容提供完整的JavaSwing界面代码与MySQL后端逻辑附有数据库初始数据和Gradle构建配置课程设计报告详述功能说明与设计思路可帮助读者理解从需求分析、E-R建模到系统实现的全过程。文档另附题目要求与指导书便于对照逐项检查项目的完整度与规范性。1. 课程设计为何总选JavaSwingMySQL这个员工培训系统到底解决什么每年到了数据库系统课程设计选题的时候总能看到一批图书管理、学生选课、超市收银系统。这几个系统上一届的学长已经写烂了演示时老师提问的粒度也会越来越细。我见过不少同学把时间花在界面美化上最后被一条“这张表满足第几范式”问住说明业务和数据库模型没咬合。企业员工培训管理系统恰好能把这个短板补上员工、培训计划、课程、考勤、资源、绩效多个实体天然形成一条从“安排培训”到“验证效果”的数据流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、positiontraining_plan培训计划主表plan_no唯一start_date/end_datestatuscourse课程明细挂在计划下plan_id外键course_date/class_hourstraining_resource资源台账resource_nametotal_countavailable_countresource_allocation课程与资源的分配关系course_id/resource_id唯一组合use_countattendance员工课程考勤employee_idcourse_id唯一组合attend_statusabsence_countexam_scoreperformance员工在计划下的绩效employee_idplan_id唯一attendance_rateexam_scoregrade这段设计的主线是计划下有课程课程要分配资源员工参加课程产生考勤考勤汇总成绩效。课程设计最忌讳把员工培训记录全部塞进一张大宽表那样展示不了范式分析。像《数据库系统概论》里讲的把绩效表单独抽出来避免员工表里出现对培训计划字段的部分函数依赖这就是一个现成的范式分析素材。2.2 从ER图到建表SQLWorkbench 导出后再改的完整脚本我经常用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 ) ENGINEInnoDB; -- 培训计划表 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 ) ENGINEInnoDB; -- 课程表 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 ) ENGINEInnoDB; -- 培训资源表 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 ) ENGINEInnoDB; -- 资源分配记录表 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) ) ENGINEInnoDB; -- 考勤记录表 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) ) ENGINEInnoDB; -- 绩效评估表 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) ) ENGINEInnoDB;这段脚本里有几个参数值得在报告里展开。ENGINEInnoDB是为了支持外键和事务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_TIMESTAMPupdate_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 ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 allowPublicKeyRetrievaltrue; 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); } }这段代码里的参数每一个都有明确作用。useSSLfalse表示关闭SSL握手本地学习环境不需要加密可以消除一堆告警。serverTimezoneAsia/Shanghai解决MySQL 8.0的时区报错这是最常见的连接失败原因。characterEncodingutf8和数据库的utf8mb4对应保证中文写入不乱码。allowPublicKeyRetrievaltrue解决MySQL 8.0默认caching_sha2_password认证插件在非SSL连接下报“Public Key Retrieval is not allowed”的问题。驱动类名也要注意mysql-connector-java 5.x写com.mysql.jdbc.Driver8.x改成了com.mysql.cj.jdbc.Driver。网上大量旧博客写的还是5.x的叫法如果你下载的是8.x驱动这里会直接ClassNotFoundException。其实JDBC 4.0之后可以不写Class.forNameSPI机制会自动加载驱动但课程设计里写上这一句老师问“驱动怎么加载”时更容易答。连接池要不要上课程设计规模不大DriverManager每次getConnection的开销可以接受。但如果你界面读一次列表就开一次连接而且不关闭性能会越来越差。更规范的做法是引入HikariCP或Druid连接池配一个最小连接数和最大连接数。我一般不会在课程设计里强行上连接池因为答辩容易被追问“连接池参数依据是什么”。用DriverManager但保证连接在finally里关闭信息已经足够。3.2 写一个员工DAOPreparedStatement与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 ListEmployee findAll() throws SQLException { String sql SELECT id, emp_no, emp_name, department, position, phone FROM employee ORDER BY department, emp_no; ListEmployee 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() { DefaultComboBoxModelEmployee 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()否则下拉框会显示Employee1a2b3c这种对象地址老师看到会皱眉这是典型的“黑匣子”表现。每次打开窗体都要重新加载下拉框不要在类里用静态初始化员工列表。因为另一个窗口可能已经新增了员工下拉框里的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字段用TINYINT0未开始、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和courseNameJTable直接绑定。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)处理NULLSUM对没有记录的结果为NULL页面显示就会空白。出勤率公式是出勤次数 / 记录次数 * 100其中attend_status ! 0表示出勤、迟到、早退都算到场如果你想把迟到算缺勤把条件改成 1即可。这个统计口径要在报告的“统计规则”里写清楚。GROUP BY e.id, e.emp_no, e.emp_name是因为MySQL默认开启了only_full_group_bySELECT里的非聚合列必须都出现在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. JavaSwingMySQL课程设计避坑我看过的翻车现场与排查步骤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.Driver8.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加serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。然后检查服务状态Windows用net start mysqlLinux用systemctl status mysql。如果数据库装在远程服务器授权时不要只授权localhost课程设计连本机即可。如果改了my.ini端口URL里的3306要同步改。MySQL安装配置教程里通常会把MySQL注册成Windows服务装完后先确认服务启动再跑Java不要直接跳到写代码。5.3 现象数据库和界面都是中文问号新增员工“张三”写入后变成“???”。原因非常集中建库字符集不是utf8mb4JDBC URL没指定characterEncoding或者Java源文件编译编码不对。这三处只要有一处不一致中文就会出问题。解决方法是建库SQL加DEFAULT CHARACTER SET utf8mb4URL加characterEncodingutf8IDEA把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。SwingWorkerListPerformance, Void worker new SwingWorker() { Override protected ListPerformance doInBackground() throws Exception { try (Connection conn DBUtil.getConnection()) { return new PerformanceDAO(conn).queryByPlan(planId); } } Override protected void done() { try { ListPerformance 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读ExcelJDBC批量addBatch一次导入几百人界面加一个进度条。这个扩展不需要改数据库表只新增导入逻辑但演示效果很直观也顺带复习了文件解析和批量操作。6.3 提交前自查清单检查项看什么常见翻车点JDBC URLuseSSLfalse、serverTimezone、characterEncoding时区、乱码驱动jar在项目lib里类名com.mysql.cj.jdbc.DriverClassNotFoundException字符集建库utf8mb4Java源文件UTF-8中文问号外键与唯一索引考勤、资源分配、绩效是否防重复重复数据UPDATE语句是否带WHERE整表数据被改连接释放Connection、Statement、ResultSet是否关闭界面卡死提交前我习惯把数据库初始化脚本从头执行一遍再把项目从零跑一遍模拟老师拿到zip后的第一反应。课程设计的代码不一定要花哨但每一步操作都有对应数据反馈你讲起来也有底气。希望这些踩坑记录帮到你至少不要在第一轮连接数据库时就卡住。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Hadoop集群部署避坑实录:从环境准备到配置文件详解 开局先把话说清楚:Hadoop生态部署这件事,网上教程多如牛毛,但真正能让你少掉头发的,永远是那些“教程永远不会写”的坑。我前后在虚拟机、物理机、Docker环境里折腾过Hadoop 2.x和3.x,陪跑过伪分布式、完全分布式&… · 2026/9/26 5:05:31
Hadoop部署踩坑全指南:从环境准备到高可用集群的排查思路 如果你决定自己从零部署一套Hadoop生态,那么先恭喜你,你马上要进入一段被配置文件、端口号和各种诡异报错支配的时光。这话不是吓唬人,网上的教程大多止步于“安装成功”那一刻,很少有人认真记录“装完之后为什么起不来”。我这些… · 2026/9/26 5:05:31
后端自建Excel注入工具类:从POI到流式写入的实战指南 做后端这几年,Excel导出这个需求几乎每个项目都绕不过去。你要说它难,确实不难,无非是把数据一行行塞进单元格;但你要说它简单,谁做谁知道——样式不统一、日期格式乱、数据量一大就内存溢出、表头改了十几次才满意&am… · 2026/9/26 5:05:31
Pulsar key_shared消息不消费排查与消息中间件选型实践 1. 从开源年会看 Pulsar Developer Day 的定位COSCon‘25 的消息一出来,我第一反应是去看同场活动的完整排期。作为一个天天跟消息中间件打交道的后端开发者,这几年我对 Pulsar 的关注度其实是持续上升的。今年的 Pulsar Developer Day 作为 COSCon‘25 … · 2026/9/26 5:44:14
复杂网络分析实战:从课件到可复现的度分布拟合与小世界效应量化 简介:这份PPT面向复杂网络与交通网络方向的初学者及研究人员,系统梳理了复杂网络的基础理论与经典模型。内容从网络概念切入,讲解节点与边的构成、拓扑结构,并重点剖析小世界效应与无标度特征两大统计量,进而延伸至规则… · 2026/9/26 5:44:14
MySQL连接查询优化:从嵌套循环到哈希连接,慢JOIN排查实战 做后端开发的人应该都有过这种体验:单表查询飞快,一上JOIN就像踩了棉花,数据量不大但就是跑不动。MySQL的连接查询优化算法确实很容易成为性能瓶颈,而且它不像单表慢查询那样好排查——一条SQL的执行计划里,藏着优化器… · 2026/9/26 5:44:13
AI自动化识别需求文档生成测试用例:从PRD解析到用例导出的完整链路 简介:Req2TestCase 是一款面向测试人员、产品经理、业务分析与实施人员的 Windows 桌面工具,用于将 PRD、自然语言需求、Office/PDF 文档及图片需求自动转换为功能测试用例,解决手工编写用例耗时、覆盖不全的痛点。资源包为 1 个 docx 操作手… · 2026/9/26 5:44:13
大模型安全评估实战:从威胁建模到回归测试的工程化落地 简介:这份由安全牛发布的《AI大模型安全评估与防护技术应用指南》,面向大模型研发、安全合规与运维人员,系统解决从研发到迭代全生命周期的安全风险识别、评估与防护落地问题。文档围绕数据、算法、模型、服务、应用五层展开,梳理… · 2026/9/26 5:44:13
Spring Boot项目Lombok编译报错HandleData failed的排查与解决 如果你在用 Spring Boot 开发,同时又习惯用 Lombok 减少样板代码,大概率会在某次编译时碰到这样一行刺眼的错误:Lombok annotation handler class lombok.javac.handlers.HandleData failed on Dxx.java我第一次见到它的时候一脸懵࿰… · 2026/9/26 5:44:07
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46