
简介这是一套面向Java初学者与课程设计学习者的员工工资管理系统完整源码采用Swing做桌面界面、MySQL做数据存储适合用来练习GUI编程、JDBC操作与基础业务逻辑分层。系统包含管理员对员工信息、部门信息与工资数据的管理同时支持用户匿名登录后查询工资功能闭环较为完整可直接作为毕业设计或实训项目的参考模板。压缩包共133个文件约2.1MB其中109个class为编译产物、16个java为可阅读源码另含2个sql建库脚本、jar依赖与工程配置文件sql文件夹下可直接导入数据库。内容预览显示涵盖员工增删改、部门维护、工资设置与统计查询等模块结构清晰便于按功能定位代码。目前已有3961人学习下载适合想快速理解SwingMySQL项目组织方式、需要现成可运行案例来调试与二次开发的读者。1. 从一份能跑起来的 Java Swing 工资系统说起它到底解决了谁的痛点很多做 Java 课程设计或者小企业内训的朋友都遇到过这种尴尬想找一个能直接跑、代码结构清晰、数据库脚本齐全的桌面端管理系统参考结果搜出来的要么是只有截图没有源码要么是代码里一堆硬编码的数据库密码和绝对路径换台机器就报ClassNotFoundException或者Access denied for user。这份 JavaSwingMysql 实现的员工工资管理系统定位很明确就是给需要交课程设计、做内部小工具原型、或者想练手 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?useUnicodetruecharacterEncodingutf8useSSLfalse; 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每次调用返回一个新连接。参数说明useUnicodetruecharacterEncodingutf8是为了防止往数据库存中文姓名时出现问号useSSLfalse在本地开发时关掉省去证书配置的麻烦。如果你在 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 对应 username2 对应 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()) { VectorObject 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 rootlocalhost原因数据库密码不对或者 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 rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;把认证插件改回旧版。4.2 现象Swing 界面中文显示成方块或者乱码原因IDE 的默认编码是 GBK而源码文件是 UTF-8或者数据库连接 URL 里没加characterEncodingutf8。 解决在 IDEA 或 Eclipse 的设置里把文件编码统一改成 UTF-8。数据库连接 URL 补上?useUnicodetruecharacterEncodingutf8。如果建表时没指定字符集执行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 里用CallableStatementCallableStatement 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;。这三条语句能覆盖大部分数据一致性问题从那以后我每次改完工资核算逻辑都强制走一遍这三条校验比在界面上点半天靠谱得多。希望帮到你。本文还有配套的精品资源点击获取