☰
数据库课程设计实战:企业员工培训管理系统从ER图到JavaSwing实现
2026/9/26 8:02:48 网站建设 项目流程

简介:面向湖南科技大学数据库系统课程设计任务的企业员工培训管理系统,是一份基于JavaSwing与MySQL的完整数据库应用项目。系统围绕培训计划管理、课程考勤记录、培训资源分配和员工绩效评估四大功能展开,可帮助企业规划课程时间表与内容、追踪员工出勤情况、调配讲师教材等资源,并依据培训表现汇总绩效参考数据。压缩包共67个文件,以49个Java源码为主,辅以课程设计报告PDF、题目要求与指导书、SQL初始数据、Gradle构建配置、E-R图及说明文件等,整体大小约4.06MB,目录结构清晰,便于对照学习和二次开发。已有45人学习下载。除可运行的项目源码外,还附赠详细报告与文档,完整呈现从数据库设计到功能实现的全过程;课程设计报告对模块划分、库表结构及界面实现有具体说明,可直接支撑课程设计答辩或文档撰写,适合作为数据库系统课程设计参考、JavaSwing桌面应用练习或中小型企业培训管理原型搭建,帮助读者快速掌握MySQL数据持久化与图形界面开发的综合技能。

1. 企业员工培训管理系统:为什么它是数据库课程设计的稳妥选题

大学里做数据库系统课程设计,最怕的往往不是不会写代码,而是选了个业务太薄、表太少、演示两分钟就结束的题目。企业员工培训管理系统恰好避开了这个雷:它天然有员工、课程、培训计划、考勤、资源、绩效这几类实体,能撑起七八张表,也能把《数据库系统概论》里反复讲的实体完整性、参照完整性、用户自定义完整性全部展示一遍。再加上 JavaSwing 做桌面端、MySQL 做存储,整个系统单机就能跑起来,不用买服务器也不用配复杂中间件,属于交作业和答辩时性价比很高的一类题目。如果你正在准备数据库课程设计,想要一套能讲清楚“从 ER 图到界面”完整链路的东西,这个方向值得认真做。

2. 从 ER 图到建表脚本:把培训计划、考勤、资源、绩效落进 MySQL

在做任何界面之前,先把数据模型定住。大部分课设翻车都翻在表结构上,后面改表、改查询、改 DAO 的代价远大于一开始多花一小时画 ER 图。这一章按“需求拆解 → 关系模式 → 建表脚本 → 初始化数据”的顺序走完,最后得到一套可以直接拿给 MySQL 执行、也能写进报告数据库设计部分的完整脚本。

2.1 先画 ER 图:四句功能描述怎么拆成七张表

常见的做法是先不写代码,拿出一张纸把实体列出来。这个系统里至少有这些实体:员工、课程、培训计划、培训资源、考勤记录、绩效记录。题目描述里的“培训计划管理”对应培训计划实体,“课程考勤记录”对应考勤实体,“培训资源分配”对应资源实体加一张关联表,“员工绩效评估”对应绩效实体。

真正容易出问题的是资源与计划的关系。一次培训可能要同时占用一间教室、一台投影仪、一套实训设备,这是“计划对资源”的一对多;反过来,同一个教室在不同日期可以被多个培训计划复用,这是“资源对计划”的一对多。合起来就是多对多关系,所以必须引入中间表t_plan_resource,不能简单地在计划表里加一个resource_id字段。很多同学在这里省了一张表,答辩时被老师问“同一个资源能不能被两个计划在不同时间占用”,当场答不上来。

考勤和绩效为什么不直接挂在计划表下面?因为一次培训对应多个员工,每个员工有自己的出勤状态和考核分数,本质上是更细粒度的记录。考勤的粒度是“某个计划 + 某个员工”,绩效的粒度同样是“某个计划 + 某个员工”,这两张表在关系模式上天然需要联合唯一键,恰好可以作为实体完整性在复杂场景下的扩展案例写进报告。

这一步完成后的关系模式是:

  • 员工表 t_emp:emp_id, emp_no, emp_name, dept, job_title, phone, entry_date, status
  • 课程表 t_course:course_id, course_name, category, credit_hours, teacher
  • 培训计划表 t_plan:plan_id, plan_no, course_id, teacher, plan_date, start_time, end_time, status
  • 资源表 t_resource:resource_id, res_name, res_type, capacity, res_status
  • 计划资源分配表 t_plan_resource:id, plan_id, resource_id, alloc_date
  • 考勤表 t_attendance:atd_id, plan_id, emp_id, attend_status, remark
  • 绩效评估表 t_performance:perf_id, plan_id, emp_id, score, evaluate

课程设计报告里,把 ER 图画完以后,下一步就是“ER 图向关系模式转换”,学校常用的教材里对这个转换规则讲得很细,照着实体和关系直接映射即可。千万别跳过这一步直接建表,后面所有 SQL 都要依赖这个模式。

2.2 建表脚本:三类完整性约束怎么写才不会被答辩老师抓

MySQL 8.0 环境下,一套可以直接执行的建表脚本如下。为了让报告里能写清楚每种约束的意义,脚本里刻意把实体完整性、参照完整性、自定义完整性分开注释出来。

-- 员工表:emp_no 唯一,对应实体完整性 CREATE TABLE t_emp ( emp_id INT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '员工工号', emp_name VARCHAR(50) NOT NULL, pwd VARCHAR(64) DEFAULT '123456', dept VARCHAR(50), job_title VARCHAR(50), phone VARCHAR(20), entry_date DATE, status TINYINT DEFAULT 1 COMMENT '1在职 0离职' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE t_course ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, category VARCHAR(50) COMMENT '技术/管理/安全等', credit_hours INT DEFAULT 8, teacher VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 培训计划表:外键指向课程,对应参照完整性 CREATE TABLE t_plan ( plan_id INT AUTO_INCREMENT PRIMARY KEY, plan_no VARCHAR(30) NOT NULL UNIQUE COMMENT '计划编号,如PX2025001', course_id INT NOT NULL, teacher VARCHAR(50), plan_date DATE NOT NULL, start_time TIME, end_time TIME, status TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已结束', CONSTRAINT fk_plan_course FOREIGN KEY (course_id) REFERENCES t_course(course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 资源表:res_status 表示空闲或占用 CREATE TABLE t_resource ( resource_id INT AUTO_INCREMENT PRIMARY KEY, res_name VARCHAR(100) NOT NULL, res_type VARCHAR(30) COMMENT '教室/投影/设备', capacity INT DEFAULT 30, res_status TINYINT DEFAULT 0 COMMENT '0空闲 1占用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 计划资源分配表:多对多中间表 CREATE TABLE t_plan_resource ( id INT AUTO_INCREMENT PRIMARY KEY, plan_id INT NOT NULL, resource_id INT NOT NULL, alloc_date DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_plan_res (plan_id, resource_id), CONSTRAINT fk_pr_plan FOREIGN KEY (plan_id) REFERENCES t_plan(plan_id), CONSTRAINT fk_pr_resource FOREIGN KEY (resource_id) REFERENCES t_resource(resource_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 考勤表:联合唯一键防止同一员工同一计划重复录入 CREATE TABLE t_attendance ( atd_id INT AUTO_INCREMENT PRIMARY KEY, plan_id INT NOT NULL, emp_id INT NOT NULL, attend_status TINYINT DEFAULT 0 COMMENT '0缺勤 1出勤 2请假', remark VARCHAR(200), UNIQUE KEY uk_plan_emp (plan_id, emp_id), CONSTRAINT fk_atd_plan FOREIGN KEY (plan_id) REFERENCES t_plan(plan_id), CONSTRAINT fk_atd_emp FOREIGN KEY (emp_id) REFERENCES t_emp(emp_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 绩效评估表:分数范围由 CHECK 约束和 DAO 层双重保证 CREATE TABLE t_performance ( perf_id INT AUTO_INCREMENT PRIMARY KEY, plan_id INT NOT NULL, emp_id INT NOT NULL, score DECIMAL(5,1), evaluate VARCHAR(500), UNIQUE KEY uk_perf_plan_emp (plan_id, emp_id), CONSTRAINT fk_perf_plan FOREIGN KEY (plan_id) REFERENCES t_plan(plan_id), CONSTRAINT fk_perf_emp FOREIGN KEY (emp_id) REFERENCES t_emp(emp_id), CONSTRAINT chk_score CHECK (score BETWEEN 0 AND 100) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个参数要说明。工号emp_no用 NOT NULL 加 UNIQUE,这是实体完整性的直接体现,答辩时老师问“工号为什么不直接做主键”,要能答出来:自增主键用于内部关联,工号是业务编号,两张表之间的外键关系不应该依赖可能变更的业务编号。考勤表和绩效表的联合唯一键uk_plan_emp、uk_perf_plan_emp是为了保证“一个员工在一次培训里只有一条考勤、一条绩效”,这是最常见的重复数据来源,不加这个唯一键,演示时连续点两次“签到”就会插出两条记录。

check 约束要留意:MySQL 8.0.16 之前会静默忽略CHECK (score BETWEEN 0 AND 100),如果你用的是 5.7,这条约束不生效,分数校验必须在 DAO 层补上。这也是课程设计里一个很值得写进报告的“版本差异”踩坑点。

外键为什么不加ON DELETE CASCADE?培训计划一旦被删除,对应的考勤和绩效记录就失去了语义上下文,直接级联删除会把历史数据抹掉。课程设计系统我一般用逻辑删除替代物理删除,员工的status字段就是这个目的。把员工状态置为 0 比DELETE FROM t_emp安全得多,也方便在报告里写“为什么不用级联删除”的取舍。

2.3 造数脚本:让页面一打开就有能演示的数据

建完表后最怕的事情是界面上空空荡荡,演示的时候现场录入来不及。常见做法是写一段初始化 SQL,手工造几条业务数据。人体工效上,不建议一条条 INSERT,可以用INSERT ... SELECT做批量生成,比如给某个培训计划批量生成全部员工的考勤记录:

-- 给计划编号 PX2025001 的所有参训员工生成考勤记录,默认缺勤 INSERT INTO t_attendance (plan_id, emp_id, attend_status) SELECT (SELECT plan_id FROM t_plan WHERE plan_no = 'PX2025001'), emp_id, 0 FROM t_emp WHERE status = 1;

这段脚本把“查询计划编号对应的 plan_id”和“取所有在职员工”合并到一次 INSERT 里,避免在 Java 代码里写循环。造数时给员工表准备 20 到 30 条数据,课程 5 条左右,培训计划覆盖“已结束、进行中、未开始”三种状态,考勤和绩效各造 30 到 50 条,演示时随便点开一个计划都有内容可看。

初始化数据这件事别拖到最后一天做。先造数据,再做界面,每个功能开发完立刻能看到对应数据的变化,开发体验会顺很多。

3. JavaSwing 接 MySQL:JDBC 连接、登录窗口与三层结构

很多做课设的同学把 JavaSwing 当纯画界面的工具,JButton 里直接写 SQL,最后代码乱成一团。正确的姿势是把它当成一个标准的桌面应用来组织:界面层只负责收集输入和展示结果,业务逻辑放在 Service 层,数据操作放在 DAO 层。这一章先搭一个最小可运行的三层骨架,再把登录窗口和列表加载跑通。

3.1 三层结构:实体类、DAO、界面层各管各的事

常见的课程设计代码组织方式是entity、dao、ui三个包:

  • entity放和表字段一一对应的类,比如Emp、Course、Plan
  • dao放所有 JDBC 操作,方法名直接对应用例,比如findByEmpNo、insertPlan
  • ui放 JFrame、JPanel、JTable 等界面代码

这样做的好处是答辩时老师问“我要改个查询条件,改哪里”,你能直接说“只改 DAO 层的 SQL,不动界面”。一个员工实体类大概长这样:

public class Emp { private Integer empId; private String empNo; private String empName; private String dept; private String jobTitle; private String phone; private java.sql.Date entryDate; private Integer status; // getter / setter 省略 }

实体类里日期字段用java.sql.Date,不要用java.util.Date。虽然java.util.Date在界面里处理起来方便,但PreparedStatement.setDate接收的是java.sql.Date,混着用每次都要手动转换,早晚漏一次。先把字段类型定成java.sql.Date,界面里需要显示时再在工具类里转格式,这样 DAO 层最干净。

3.2 JDBC 连接工具类:URL 参数里的时区、SSL、编码

连接参数是玄学重灾区。同一个 jar 包,换个环境就报错,多半是 URL 参数没对齐。我的连接工具类固定写法如下:

import java.sql.*; public class DBUtil { // 完整连接串:时区、SSL、编码全部显式声明 private static final String URL = "jdbc:mysql://127.0.0.1:3306/training_db" + "?serverTimezone=Asia/Shanghai" + "&useSSL=false" + "&characterEncoding=utf8" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASS = "123456"; static { try { // MySQL 8.x 必须用 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASS); } public static void close(Connection conn, Statement st, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (st != null) { try { st.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

这几个参数分别解决不同问题。serverTimezone=Asia/Shanghai解决 MySQL 8 的时区报错,不写的话会看到类似于The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized的乱码报错。useSSL=false关掉 SSL 握手警告,本机开发没必要加密。characterEncoding=utf8配合建表时的utf8mb4字符集,解决中文写入后变问号的问题。allowPublicKeyRetrieval=true是 MySQL 8 用 caching_sha2_password 加密插件时需要的参数,很多教程没提这个,导致驱动连接时报Public Key Retrieval is not allowed。

如果 jar 包用的是老版本mysql-connector-java 5.1.x,驱动类名要改成com.mysql.jdbc.Driver,也就是没有cj的那一段。这个差别在避坑章还会单独讲一遍,因为报错信息特别容易误导人。

3.3 登录窗口:用 PreparedStatement 而不是拼 SQL

登录模块看起来简单,但它最适合在报告里展示安全意识。常见做法是把 SQL 写死成字符串拼接的形式,这种演示型写法一旦被问到“登录框里输入 1' OR '1'='1 会怎么样”,整个答辩就停在这个问题上了。用PreparedStatement可以从根源上避免:

public Emp login(String empNo, String pwd) { String sql = "SELECT emp_id, emp_name, job_title, status " + "FROM t_emp WHERE emp_no = ? AND pwd = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empNo); ps.setString(2, pwd); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Emp emp = new Emp(); emp.setEmpId(rs.getInt("emp_id")); emp.setEmpName(rs.getString("emp_name")); emp.setJobTitle(rs.getString("job_title")); emp.setStatus(rs.getInt("status")); return emp; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

参数占位符把用户输入当成数据而不是 SQL 片段传进去,这是PreparedStatement的核心价值。需要注意密码的存储:这里为了课设演示用了明文默认密码,报告里要写清楚生产环境应该用BCrypt之类的摘要算法,明文存储只是简化实现。

登录窗口本身的代码比较模板化,一个JTextField、一个JPasswordField、一个“登录”按钮。按钮事件里调用login方法,返回 null 就弹JOptionPane.showMessageDialog,非 null 就跳到主窗体。建议在登录成功的返回对象里带上empName和jobTitle,主窗体的状态栏能直接显示“当前用户:张三(培训专员)”,演示效果比只显示工号好得多。

3.4 主界面加载列表:JTable 与 DefaultTableModel 的配合

主窗体一般用JTabbedPane放四个功能标签页,每个标签页里放一个JTable和若干操作按钮。JTable 不直接绑定ResultSet,正确姿势是先把数据装进DefaultTableModel,再setModel刷新:

public void loadPlanTable(JTable table) { String sql = "SELECT p.plan_no, c.course_name, p.teacher, " + "p.plan_date, p.start_time, p.end_time, p.status " + "FROM t_plan p JOIN t_course c ON p.course_id = c.course_id " + "ORDER BY p.plan_date DESC"; DefaultTableModel model = new DefaultTableModel( new String[]{"计划编号", "课程", "讲师", "日期", "开始", "结束", "状态"}, 0); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getString("plan_no"), rs.getString("course_name"), rs.getString("teacher"), rs.getString("plan_date"), rs.getString("start_time"), rs.getString("end_time"), planStatusText(rs.getInt("status")) }); } } catch (SQLException e) { e.printStackTrace(); } table.setModel(model); }

要注意DefaultTableModel的构造方法,第二个参数传0表示初始行数,随后用addRow逐行添加。每次点击查询都重新创建一个model再setModel,是最稳妥的刷新方式,不用去记fireTableDataChanged()这些细节。planStatusText是个小的转换方法,把注意 0、1、2 翻译成“未开始、进行中、已结束”,界面不需要直接暴露编码值。

4. 四个核心功能模块:培训计划、考勤、资源分配、绩效评估怎么串起来

表结构和连接层就绪后,接下来把四个功能模块逐个实现。这一章的顺序就是演示时的操作顺序:先建计划,再分配资源,然后考勤,最后录绩效。每个模块都给出一段可复用的核心代码,并按课程设计可以接受的复杂度来组织。

4.1 培训计划管理:先校验后插入,业务规则写在 DAO 层

新增培训计划时,界面要提供课程下拉框、日期选择、时间输入。这里的业务规则是:课程必须存在、日期不能早于今天、计划编号不能重复。把规则写在 DAO 层而不是界面层,界面只管传参。

public boolean insertPlan(Plan plan) { String checkCourse = "SELECT COUNT(*) FROM t_course WHERE course_id = ?"; String insert = "INSERT INTO t_plan (plan_no, course_id, teacher, plan_date, start_time, end_time, status) " + "VALUES (?, ?, ?, ?, ?, ?, 0)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement psCheck = conn.prepareStatement(checkCourse)) { psCheck.setInt(1, plan.getCourseId()); ResultSet rs = psCheck.executeQuery(); rs.next(); if (rs.getInt(1) == 0) { return false; // 课程不存在,拒绝插入 } try (PreparedStatement ps = conn.prepareStatement(insert)) { ps.setString(1, plan.getPlanNo()); ps.setInt(2, plan.getCourseId()); ps.setString(3, plan.getTeacher()); ps.setDate(4, plan.getPlanDate()); ps.setTime(5, plan.getStartTime()); ps.setTime(6, plan.getEndTime()); return ps.executeUpdate() == 1; } } catch (SQLException e) { e.printStackTrace(); return false; } }

先查再插不是绝对安全,两个操作之间课程可能被删,但课设场景下足够。真正要注意的是ps.setDate和ps.setTime,日期用java.sql.Date,时间用java.sql.Time,这两个类型都来自java.sql包。计划编号建议在界面层生成,比如PX加年月日再加三位序号,展示出来比自增主键有业务感。MySQL 里plan_no有唯一约束,重复插入会抛Duplicate entry异常,捕获后给用户提示“计划编号已存在”即可。

4.2 课程考勤:批量插入与 ON DUPLICATE KEY UPDATE 的配合

考勤模块的典型交互是:选中一个培训计划,页面列出该计划涉及的员工列表,每个员工有一个下拉框标记出勤、缺勤或请假,点保存时一次性提交。性能不是问题,但逻辑上有个容易翻车的点:重复点击保存会插入重复记录。

用一个 UPSERT 风格的 SQL 就能同时处理“插入”和“重复时更新”两种场景:

public void saveAttendance(List<Attendance> list) { String sql = "INSERT INTO t_attendance (plan_id, emp_id, attend_status, remark) " + "VALUES (?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE " + "attend_status = VALUES(attend_status), remark = VALUES(remark)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Attendance a : list) { ps.setInt(1, a.getPlanId()); ps.setInt(2, a.getEmpId()); ps.setInt(3, a.getAttendStatus()); ps.setString(4, a.getRemark()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { e.printStackTrace(); } }

ON DUPLICATE KEY UPDATE依赖第 2 章建表时定义的联合唯一键uk_plan_emp,这就是当时留这个约束的原因。addBatch加executeBatch把多条插入合并成一次网络往返,比单条循环提交快很多,上百人的考勤瞬间完成。

事务在这里是必要的:如果第 10 个人的插入失败,前面 9 条已经被executeBatch写进库了,没有事务会出现“一半成功一半失败”的状态。setAutoCommit(false)后手动 commit 和 rollback,是课程设计里能写进报告的一个加分设计。

4.3 培训资源分配:事务保证“占用标记”和“分配记录”同步更新

资源分配的逻辑是:给培训计划选资源时,只能选res_status = 0的空闲资源;分配成功后,资源表里这条资源的res_status变成 1。如果界面上有“释放资源”按钮,则把占用状态改回 0,同时删除或标记中间表记录。

分配资源要更新两张表,必须用事务:

public boolean allocateResource(int planId, int resourceId) { String insertAlloc = "INSERT INTO t_plan_resource (plan_id, resource_id) VALUES (?, ?)"; String updateRes = "UPDATE t_resource SET res_status = 1 WHERE resource_id = ? AND res_status = 0"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(insertAlloc); PreparedStatement ps2 = conn.prepareStatement(updateRes)) { ps1.setInt(1, planId); ps1.setInt(2, resourceId); int allocResult = ps1.executeUpdate(); ps2.setInt(1, resourceId); int updateResult = ps2.executeUpdate(); if (allocResult == 1 && updateResult == 1) { conn.commit(); return true; } conn.rollback(); return false; } catch (SQLException e) { conn.rollback(); return false; } } catch (SQLException e) { e.printStackTrace(); return false; } }

这段代码的关键在UPDATE t_resource SET res_status = 1 WHERE resource_id = ? AND res_status = 0。把“当前是否空闲”放在 WHERE 条件里,而不是先查再改,防止两个人同时抢同一资源。如果updateResult是 0,说明资源已经被占走,此时分配失败并回滚。这种写法在报告里叫“乐观锁思路”,虽然没引入版本号字段,但判断条件本身就带了并发保护的含义。释放资源时把res_status改回 0,同时删除t_plan_resource里的对应记录,同样用事务包起来。

4.4 员工绩效评估:多表联查把分数和课程对上

绩效录入界面上要能同时看到员工姓名、课程名称和对应计划,不能用数字编号去面对用户。因此查询必须走多表JOIN:

SELECT e.emp_no, e.emp_name, c.course_name, p.plan_date, p.perf_id, p.score, p.evaluate FROM t_performance p JOIN t_emp e ON p.emp_id = e.emp_id JOIN t_plan tp ON p.plan_id = tp.plan_id JOIN t_course c ON tp.course_id = c.course_id WHERE tp.plan_id = ? ORDER BY p.score DESC;

这条 SQL 把绩效表的plan_id先连到培训计划,再通过培训计划连到课程,总共跨四张表。写报告时把查询树画出来,老师看到多表联查是加分的。录入绩效时,沿用考勤模块的 UPSERT 方式,联合唯一键uk_perf_plan_emp保证一个员工在一次培训里只有一条绩效记录。

分数校验要做两层。数据库里CHECK (score BETWEEN 0 AND 100)在 MySQL 8.0.16 之后的版本才强制生效,旧版本会忽略,所以 DAO 层也要判断:

double score = performance.getScore(); if (score < 0 || score > 100) { throw new IllegalArgumentException("成绩必须在0到100之间"); }

界面层用JSpinner或JTextField加格式化校验,三层校验虽然冗余,但课程设计报告里能写出一小节“数据完整性的多重保障”,属于投入产出比很高的设计。

5. 避坑:课程设计里最常见的五个翻车点与排查步骤

这一章写的是我见过踩得最多、且每个都能在答辩现场救命的坑。现象描述按实际报错来,解决步骤按最快能跑通的方式给,不做理论展开。

5.1 中文乱码:数据库、连接串、界面三处必须对齐

现象:往 MySQL 里插入“张三”后变成“???”;或者 JTable 里显示的中文来自数据库时正常,但直接在 Swing 文本框里输入再查询,查不到结果。

原因:字符集没有层层统一。MySQL 服务端默认字符集不是 utf8mb4,建表时没写DEFAULT CHARSET=utf8mb4,连接串少了characterEncoding=utf8,三个环节只要有一个不对,中文就会在某一层变乱码。

解决:按顺序检查。先看建表 SQL 有没有CHARSET=utf8mb4,再看连接 URL 有没有characterEncoding=utf8,最后在 Java 代码里确认文件编译字符集是 UTF-8。IDE 里Settings > File Encodings全改成 UTF-8 再重新编译。这三处对齐后,乱码基本消失。课程设计演示时如果现场出了一次乱码,重新启动前先检查 MySQL 命令行下SHOW CREATE TABLE t_emp的输出,字符集不对立刻手动ALTER TABLE t_emp CONVERT TO CHARACTER SET utf8mb4,这是最快的后悔药。

5.2 error 2002:MySQL 服务没启动或者地址选错

现象:一跑程序就报java.sql.SQLException: Cannot connect to MySQL Server on 'localhost'或者ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。

原因:这种报错大多数人第一时间怀疑驱动和代码,实际上最常见原因是 MySQL 服务根本没启动。其次是在 Linux 或 macOS 上,localhost会被解析成 Unix socket 连接,而 Java 的 JDBC 驱动走的是 TCP 协议,两者接不上。

解决:先确认服务状态。Windows 下打开服务管理器看 MySQL 服务是否运行;Linux 下执行systemctl status mysqld。不行就把连接串里localhost改成127.0.0.1,强制走 TCP 端口 3306,这一步能解决大部分“代码没问题但连不上”的诡异情况。端口被占时用netstat -ano \| findstr 3306查一下,确认没有第二个实例占用。

5.3 日期转换:setDate 参数类型不匹配

现象:代码运行到ps.setDate这一行直接编译报错,提示找不到符号或者类型不兼容;有时界面上选的日期存进数据库后整体少了或多了 14 个小时。

原因:界面用的java.util.Date,而PreparedStatement.setDate要的是java.sql.Date,两者虽然有继承关系但.setDate方法不认父类。时间偏移则多半是时区参数没加。

解决:在实体类里统一用java.sql.Date,界面层拿到日期字符串后按yyyy-MM-dd解析,再转成java.sql.Date。如果确实需要在界面用java.util.Date,转换写法是new java.sql.Date(utilDate.getTime())。时区问题回到第 3 章的serverTimezone=Asia/Shanghai,加了这个参数后偏移消失。

5.4 外键约束挡住删除:删不掉是设计问题不是 SQL 问题

现象:执行DELETE FROM t_emp WHERE emp_id = 5时报错,错误码 1451,信息里有cannot delete or update a parent row: a foreign key constraint fails。

原因:t_attendance 或 t_performance 里还有记录引用了这个员工。第 2 章建表时没有用ON DELETE CASCADE,MySQL 宁可让你删不动,也不会静默丢数据。

解决:这其实不是 bug,是约束生效的表现。课程设计系统采用逻辑删除更合适,把员工 status 置 0,而不是物理删除。如果一定要物理删除,顺序是先删除子表引用记录,再删父表记录,两步包进一个事务。报告里解释清楚“为什么不用级联删除”,比演示级联删除更能体现设计能力。

5.5 驱动类找不到:jar 包没引入或驱动类名写错

现象:程序启动时抛ClassNotFoundException: com.mysql.jdbc.Driver,或者运行时No suitable driver found for jdbc:mysql...。

原因:mysql-connector 8.x 的驱动类名改成了com.mysql.cj.jdbc.Driver,老教程里写的是com.mysql.jdbc.Driver,直接复制旧代码就会报类找不到。另外很多时候是 jar 包根本不在 classpath 里,IDE 里能看到代码高亮,但运行时找不到。

解决:先确认引入的 connector 版本。打开项目的Build Path,看 jar 包版本是 5.x 还是 8.x,对应修改Class.forName里的类名。顺手确认 jar 在Order and Export标签页里被勾选,有时候代码不报错但运行报找不到驱动,就是勾选框没打上。

6. 答辩前最后一天:按这个顺序把演示和几张核心报表跑顺

演示环节最容易翻车的不是功能缺失,而是操作顺序不合理。我的习惯是把演示脚本固定下来,每个操作之间留出老师看效果的停顿,全程控制在五分钟以内。

第一遍演示走主链路:登录 → 新建培训计划 → 给计划分配资源 → 批量考勤签到 → 录入绩效 → 查看联查报表。这条链路覆盖了系统名称里企业员工培训管理系统相关的全部功能,每一步都能在前一步的数据上继续,老师会看到数据在表与表之间流动。

第二遍演示带一点边界:重复签到同一员工,展示联合唯一键拦截;输入 101 分的成绩,展示 CHECK 约束或 DAO 层校验;删除一个已分配资源的计划,展示外键约束不放行。这些操作叫“主动演示约束”,专门给老师准备,说明你不是只会写简单的增删改查,而是在设计时考虑了数据完整性。

建议在答辩前一天用下面这个验证清单过一遍系统。每个功能点都准备一条可复现、能截图的数据,报告里的结果截图也按这个顺序截,省得到时候在报告和系统之间来回翻。

验证项操作方式预期结果
登录校验输入错误密码弹出提示,不进入主界面
计划编号唯一重复新增同一编号数据库报 Duplicate 或前端提示已存在
考勤防重同一计划同一员工重复保存记录数不增加,状态被更新
资源占用已占用资源不出现在下拉框再次分配返回失败并回滚
绩效分数范围输入 101 分被 DAO 层拦截或 CHECK 约束拦截
外键保护删除有考勤记录的员工删除被拒,提示外键约束失败

答辩前一天的下午,把这几条全部跑一遍,每个结果截一张图,放进报告对应位置。晚上只做一件事:再跑一次完整主链路演示,确保关闭电脑再开机后依然能复现。

如果还有余力,可以加一个数据库端的存储过程,比如统计某次培训的出勤率,SELECT 出勤人数 / 总人数算出来再返回。这属于 MySQL 存储过程结合业务场景的典型案例,写在报告“数据库对象设计”一节里,比重写一个 Java 方法有用得多。给查询频繁的t_attendance(plan_id)加一个普通索引,把执行计划EXPLAIN的结果也放进去,是报告里“性能优化”部分能站住脚的内容。

我第一次做这类系统时,把考勤和绩效直接挂在培训计划表上,用一个List<String>存员工编号,删除计划时顺带把所有子记录删掉,结果演示时误删了整批考勤数据,当场翻车。后来才意识到数据库课程设计的重点不是界面多炫,而是让数据在约束下稳定流动。把这一步想明白,你的系统已经超过大半的课程设计作品了,希望帮到你。

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

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

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

立即咨询