☰
Java Swing+MySQL员工工资管理系统:从建表到工资核算的完整实现
2026/10/7 14:53:05 网站建设 项目流程

简介:这是一套面向Java初学者与课程设计学习者的员工工资管理系统完整源码,采用Swing做桌面界面、MySQL做数据存储,适合用来练习GUI编程、JDBC操作与基础业务逻辑分层。系统包含管理员对员工信息、部门信息与工资数据的管理,同时支持用户匿名登录后查询工资,功能闭环较为完整,可直接作为毕业设计或实训项目的参考模板。压缩包共133个文件,约2.1MB,其中109个class为编译产物、16个java为可阅读源码,另含2个sql建库脚本、jar依赖与工程配置文件,sql文件夹下可直接导入数据库。内容预览显示涵盖员工增删改、部门维护、工资设置与统计查询等模块,结构清晰,便于按功能定位代码。目前已有3961人学习下载,适合想快速理解Swing+MySQL项目组织方式、需要现成可运行案例来调试与二次开发的读者。

1. 从一份能跑起来的 Java Swing 工资系统说起:它到底解决了谁的痛点

很多做 Java 课程设计或者小企业内训的朋友,都遇到过这种尴尬:想找一个能直接跑、代码结构清晰、数据库脚本齐全的桌面端管理系统参考,结果搜出来的要么是只有截图没有源码,要么是代码里一堆硬编码的数据库密码和绝对路径,换台机器就报ClassNotFoundException或者Access denied for user。这份 Java+Swing+Mysql 实现的员工工资管理系统,定位很明确,就是给需要交课程设计、做内部小工具原型、或者想练手 JDBC 与 Swing 事件监听配合的开发者一个可复现的底本。它不追求 Spring Boot 那种前后端分离的时髦架构,而是用最朴素的 C/S 结构把增删改查、工资核算、部门管理这几件事讲透。如果你正在找 javaweb项目完整案例mysql 的替代方案,或者单纯想看看不用 Web 框架怎么把 mysql事务处理 和 java排序 逻辑塞进桌面程序里,这套东西的参考价值反而比那些过度设计的项目更直接。适合谁?在校生拿来做 java 课程设计,初级 java工程师 用来熟悉 JDBC 原生写法,以及需要快速给行政部门搭一个单机工资录入工具的小团队。

2. 环境搭建与数据库初始化:把 mysql 5.7 和 JDK 配成能跑的状态

2.1 选型理由:为什么是 Swing + 原生 JDBC 而不是 JavaFX + MyBatis

先把这个组合的合理性说清楚,免得你边配环境边怀疑人生。Swing 虽然被一些人嫌弃“丑”,但它的优势在于 JDK 内置,不需要额外引入 UI 库依赖,对于课程设计或者内网工具来说,打包成 jar 之后双击就能跑,不用折腾模块化那套module-info.java。数据库选 MySQL 5.7 而不是 8.0,是因为很多学校的实验环境或者老旧的 Windows 7 机器上,mysql 5.7.44 安装过程详细 教程满地都是,兼容性经过时间检验,驱动包mysql-connector-java-5.1.49.jar也足够稳定。至于为什么不用 MyBatis 或者 MyBatis-Plus,这个项目的体量就几张表,写原生 JDBC 反而能让初学者看清楚PreparedStatement是怎么预编译的、ResultSet是怎么映射成Employee对象的。如果你已经习惯了 mybatisplus根据java实体类生成创建表的sql语句 那种自动化,回头看看手动写 DAO 层,对理解连接池和事务边界有好处。

2.2 数据库建表脚本与初始数据

拿到源码包后,第一件事不是急着打开 IDE,而是先把数据库跑起来。常见的做法是创建一个名为salary_db的数据库,字符集用utf8mb4,排序规则utf8mb4_general_ci。下面这段 SQL 可以直接在 Navicat 或者命令行里执行,包含了部门表、员工表、工资明细表和用户表。注意salary字段我用了DECIMAL(10,2),这是血泪经验,用FLOAT算工资迟早会出现199.9999999这种玄学数字。

-- 创建数据库,指定字符集防止中文乱码 CREATE DATABASE IF NOT EXISTS salary_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE salary_db; -- 部门表:存储部门名称和备注 CREATE TABLE tb_dept ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, dept_remark VARCHAR(200) ); -- 员工表:关联部门,存储基本工资和入职日期 CREATE TABLE tb_employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, emp_sex CHAR(2), emp_birthday DATE, dept_id INT, emp_salary DECIMAL(10,2), emp_entry_date DATE, FOREIGN KEY (dept_id) REFERENCES tb_dept(dept_id) ); -- 工资发放记录表:记录每月实发工资 CREATE TABLE tb_salary_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT, record_month VARCHAR(7), -- 格式如 '2025-03' base_salary DECIMAL(10,2), bonus DECIMAL(10,2) DEFAULT 0.00, deduction DECIMAL(10,2) DEFAULT 0.00, real_salary DECIMAL(10,2), FOREIGN KEY (emp_id) REFERENCES tb_employee(emp_id) ); -- 系统用户表:用于登录验证 CREATE TABLE tb_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(50) NOT NULL ); -- 插入默认管理员账号,密码明文仅用于演示,实际项目务必加密 INSERT INTO tb_user (username, password) VALUES ('admin', '123456');

执行完建表语句后,往tb_dept里插两条测试数据,比如“技术部”和“财务部”,再往tb_employee里插几个员工,这样启动程序后界面不至于空空如也。这里有个参数要留意:record_month我用了VARCHAR(7)而不是DATE,因为工资是按月发放的,存'2025-03'这种字符串在查询和展示时更直观,不用做日期格式化转换。

2.3 JDBC 连接配置与驱动加载的坑

数据库建好后,打开源码找到db.properties或者DBUtil.java这类文件。通常项目会把连接信息抽到一个属性文件里,方便修改。如果你拿到的版本是硬编码在 Java 文件里的,建议手动抽出来,不然换台电脑就得重新编译。下面是一个标准的DBUtil写法,注意Class.forName那行,MySQL 8.0 驱动类名是com.mysql.cj.jdbc.Driver,而 5.7 是com.mysql.jdbc.Driver,写错了直接抛ClassNotFoundException。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 数据库连接参数,根据实际环境修改 private static final String URL = "jdbc:mysql://localhost:3306/salary_db?useUnicode=true&characterEncoding=utf8&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; static { try { // MySQL 5.7 驱动加载,8.0 请改为 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException("数据库驱动加载失败,请检查 mysql-connector jar 包是否导入"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这段代码的逻辑很简单:静态代码块在类加载时执行一次驱动注册,getConnection每次调用返回一个新连接。参数说明:useUnicode=true&characterEncoding=utf8是为了防止往数据库存中文姓名时出现问号;useSSL=false在本地开发时关掉,省去证书配置的麻烦。如果你在 Windows 10 上按照 mysql安装教程8.0 装的数据库,端口默认 3306,服务名可能是MySQL80,用net start mysql启动时如果报错,先去服务列表里确认服务名到底是mysql还是mysql80。

3. 核心功能模块拆解:登录、员工管理与工资核算的代码实现

3.1 登录模块:从密码校验到主界面跳转

登录界面通常是用户接触的第一个 Swing 窗口,代码结构一般是LoginFrame继承JFrame,里面放两个JTextField和一个JPasswordField,再加一个“登录”按钮。按钮的ActionListener里做两件事:查数据库验证账号密码,验证通过就dispose()当前窗口并new MainFrame().setVisible(true)。这里有个容易翻车的地方:JPasswordField取出来的char[]不能直接用toString()转字符串,那样得到的是数组地址,必须用new String(passwordField.getPassword())。

// 登录按钮的事件监听逻辑 loginBtn.addActionListener(e -> { String username = usernameField.getText().trim(); // 注意:getPassword() 返回 char[],不能直接 toString String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名或密码不能为空"); return; } String sql = "SELECT * FROM tb_user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { // 登录成功,关闭登录窗,打开主界面 this.dispose(); new MainFrame().setVisible(true); } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "数据库连接异常,请检查服务是否启动"); } });

参数说明:PreparedStatement的?占位符从 1 开始计数,这里 1 对应 username,2 对应 password。用try-with-resources语法可以自动关闭Connection和PreparedStatement,避免忘记关连接导致 MySQL 报Too many connections。如果你在调试时发现点击登录没反应,先看控制台有没有抛异常,大概率是数据库服务没启动或者db.properties里的密码填错了。

3.2 员工管理:表格渲染与增删改查的联动

主界面一般用JTabbedPane分页,员工管理页放一个JTable展示数据,下面配几个按钮。JTable的数据来源通常是DefaultTableModel,从数据库查出来的ResultSet逐行addRow进去。这里有个细节:JTable默认是可直接编辑单元格的,但工资系统里通常不允许直接在表格里改数据,否则用户误操作后很难追踪。所以初始化表格时要调用table.setDefaultEditor(Object.class, null)来禁用编辑。

// 加载员工数据到表格 public void loadEmployeeData() { // 清空现有行 model.setRowCount(0); String sql = "SELECT e.emp_id, e.emp_name, e.emp_sex, d.dept_name, e.emp_salary, e.emp_entry_date " + "FROM tb_employee e LEFT JOIN tb_dept d ON e.dept_id = d.dept_id"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { Vector<Object> row = new Vector<>(); row.add(rs.getInt("emp_id")); row.add(rs.getString("emp_name")); row.add(rs.getString("emp_sex")); row.add(rs.getString("dept_name")); row.add(rs.getBigDecimal("emp_salary")); row.add(rs.getDate("emp_entry_date")); model.addRow(row); } } catch (SQLException e) { e.printStackTrace(); } }

这段代码用了LEFT JOIN把部门名称关联出来,这样表格里显示的是“技术部”而不是部门 ID。getBigDecimal对应数据库的DECIMAL类型,比getDouble精度更可靠。删除员工时,不能只删tb_employee表,如果该员工在tb_salary_record里有工资记录,直接删会触发外键约束报错。常见做法是弹窗提示“该员工存在工资记录,是否级联删除”,确认后先删子表记录再删主表记录,或者干脆在数据库层面设置ON DELETE CASCADE。

3.3 工资核算:把应发、扣款和实发算明白

工资核算模块是整个系统的业务核心。通常界面会让用户选择月份,然后列出所有员工的基本工资,手动录入奖金和扣款,点击“计算”按钮后自动算出实发工资并保存。实发工资的公式很简单:实发 = 基本工资 + 奖金 - 扣款。但坑在于,如果用户没选月份就直接点保存,数据库里会出现一堆record_month为空的脏数据。所以保存前必须校验月份格式,用正则^\d{4}-\d{2}$卡一下。

// 计算并保存单条工资记录 public boolean saveSalaryRecord(int empId, String month, BigDecimal base, BigDecimal bonus, BigDecimal deduction) { // 校验月份格式,防止脏数据入库 if (!month.matches("^\\d{4}-\\d{2}$")) { JOptionPane.showMessageDialog(null, "月份格式错误,应为 yyyy-MM"); return false; } BigDecimal realSalary = base.add(bonus).subtract(deduction); String sql = "INSERT INTO tb_salary_record (emp_id, record_month, base_salary, bonus, deduction, real_salary) " + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, empId); pstmt.setString(2, month); pstmt.setBigDecimal(3, base); pstmt.setBigDecimal(4, bonus); pstmt.setBigDecimal(5, deduction); pstmt.setBigDecimal(6, realSalary); return pstmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }

参数说明:BigDecimal的add、subtract方法返回新对象,不会修改原值,所以可以放心链式调用。executeUpdate返回受影响行数,大于 0 表示插入成功。如果你在测试时发现保存后查询不到数据,先检查是不是忘了conn.commit()——默认 JDBC 是自动提交的,但如果你手动开了事务conn.setAutoCommit(false),就必须显式提交,否则连接关闭时数据回滚,这就是典型的“黑匣子”问题。

4. 避坑与排查:从环境配置到代码逻辑的五个常见翻车点

4.1 现象:启动报错Access denied for user 'root'@'localhost'

原因:数据库密码不对,或者 MySQL 8.0 的root用户默认使用了caching_sha2_password认证插件,而老版本的mysql-connector-java-5.1.x驱动不支持这种认证方式。 解决:先确认DBUtil里的密码和安装 MySQL 时设置的一致。如果用的是 8.0 版本,要么把驱动换成mysql-connector-java-8.0.xx,要么在 MySQL 命令行里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';把认证插件改回旧版。

4.2 现象:Swing 界面中文显示成方块或者乱码

原因:IDE 的默认编码是 GBK,而源码文件是 UTF-8,或者数据库连接 URL 里没加characterEncoding=utf8。 解决:在 IDEA 或 Eclipse 的设置里把文件编码统一改成 UTF-8。数据库连接 URL 补上?useUnicode=true&characterEncoding=utf8。如果建表时没指定字符集,执行ALTER DATABASE salary_db CHARACTER SET utf8mb4;补救。

4.3 现象:点击按钮没反应,控制台也不报错

原因:ActionListener没注册成功,或者按钮被其他组件遮挡了,还有一种可能是事件分发线程(EDT)里执行了耗时操作导致界面卡死。 解决:检查button.addActionListener(this)是否漏写。如果是在actionPerformed里直接查数据库,数据量大时界面会假死,常见做法是把数据库操作放到SwingWorker的doInBackground里执行,完成后在done方法里更新 UI。

4.4 现象:删除部门时提示外键约束失败

原因:tb_employee表里有该部门的员工记录,直接删tb_dept会违反外键约束。 解决:先查询该部门下是否有员工,如果有,弹窗提示用户先转移或删除员工。或者在建表时把外键写成ON DELETE SET NULL,这样删除部门后员工的dept_id变成 NULL,不会阻塞删除操作,但后续查询要处理 NULL 值。

4.5 现象:打包成 jar 后双击运行提示“找不到主类”或“没有主清单属性”

原因:META-INF/MANIFEST.MF里没有指定Main-Class,或者依赖的mysql-connectorjar 没有一起打进去。 解决:用 Maven 的maven-shade-plugin或者maven-assembly-plugin打一个包含所有依赖的 fat jar,并在插件配置里指定<mainClass>。如果手动打包,解压 jar 后手动编辑MANIFEST.MF,加上Main-Class: com.yourpackage.LoginFrame和Class-Path: lib/mysql-connector-java-5.1.49.jar。

5. 进阶技巧:用存储过程做月度工资汇总与数据校验

5.1 为什么要把汇总逻辑下沉到 MySQL 存储过程

当员工数量超过几十人,每个月做工资汇总时,如果在 Java 层循环查询每个员工的记录再累加,网络往返次数会很多,效率低。把汇总逻辑写成 MySQL 存储过程,一次调用就能返回结果集,减少 JDBC 交互次数。而且存储过程在数据库端预编译,执行计划复用,对于固定的月度统计场景很合适。下面这个存储过程接收月份参数,返回该月所有员工的实发工资合计和平均工资。

DELIMITER // CREATE PROCEDURE proc_monthly_salary_summary(IN p_month VARCHAR(7)) BEGIN -- 汇总指定月份的工资数据 SELECT COUNT(*) AS emp_count, SUM(real_salary) AS total_salary, AVG(real_salary) AS avg_salary, MAX(real_salary) AS max_salary, MIN(real_salary) AS min_salary FROM tb_salary_record WHERE record_month = p_month; END // DELIMITER ;

调用时在 Java 里用CallableStatement:CallableStatement cs = conn.prepareCall("{call proc_monthly_salary_summary(?)}"); cs.setString(1, "2025-03"); ResultSet rs = cs.executeQuery();。参数说明:IN p_month是输入参数,调用前必须setString赋值。DELIMITER //是为了让 MySQL 客户端把;当成存储过程内部语句的结束符,而不是整个语句的结束,在 Navicat 里执行时如果报语法错误,检查是不是漏了DELIMITER这一行。

5.2 用触发器做数据校验:防止实发工资为负数

工资核算里有个边界情况:如果扣款金额填得比基本工资加奖金还大,实发工资会变成负数。虽然业务上不合理,但代码不拦的话数据库照样存进去。与其在 Java 层到处写 if-else,不如在数据库层加一个BEFORE INSERT触发器,发现real_salary < 0就抛异常回滚。

DELIMITER // CREATE TRIGGER trg_check_salary_before_insert BEFORE INSERT ON tb_salary_record FOR EACH ROW BEGIN IF NEW.real_salary < 0 THEN -- 抛出异常,阻止插入 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '实发工资不能为负数,请检查扣款金额'; END IF; END // DELIMITER ;

这个触发器的逻辑是:在插入新记录之前,检查NEW.real_salary的值,如果小于 0 就用SIGNAL抛出一个自定义错误。Java 层捕获SQLException后,e.getMessage()里会包含“实发工资不能为负数”这段文字,直接弹窗展示给用户即可。注意SIGNAL SQLSTATE '45000'是用户自定义异常的通用写法,45000表示未处理的用户定义异常。

5.3 验证方法:用几条 SQL 确认数据一致性

功能写完后,别急着点界面按钮,先在数据库里跑几条校验 SQL。第一,查有没有员工的部门 ID 在部门表里不存在:SELECT * FROM tb_employee WHERE dept_id NOT IN (SELECT dept_id FROM tb_dept);。第二,查工资记录里的实发是否等于基本加奖金减扣款:SELECT * FROM tb_salary_record WHERE real_salary != base_salary + bonus - deduction;。第三,查有没有重复的月度记录:SELECT emp_id, record_month, COUNT(*) FROM tb_salary_record GROUP BY emp_id, record_month HAVING COUNT(*) > 1;。这三条语句能覆盖大部分数据一致性问题,从那以后我每次改完工资核算逻辑,都强制走一遍这三条校验,比在界面上点半天靠谱得多。

希望帮到你。

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

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

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

立即咨询