ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Java Swing与MySQL的员工工资管理系统开发实战

基于Java Swing与MySQL的员工工资管理系统开发实战 简介一套基于Java Swing与MySQL的员工工资管理系统面向Java初学者与课程设计人群主要覆盖员工信息管理、部门信息管理和工资管理三大核心模块并实现匿名登录查询工资功能可直接用于毕业设计或课设参考。压缩包共包含133个文件以16个Java源文件与109个class编译文件为主另附SQL初始化脚本、项目配置文件和可运行jar包整体仅2.1MB轻量简洁便于导入IDE运行与二次开发。从代码结构可看出功能覆盖员工、部门与工资模块的增删改查、设置、查询和统计等完整流程方便按图索骥定位相关实现。系统经多次测试运行稳定下载后即可体验管理员与匿名用户双角色完整流程尤其适合希望快速掌握Swing界面编程与MySQL数据库连接操作的读者。目前已有3961人浏览学习属于同类课设资源中热度较高的选择。1. 员工工资管理系统还在用 Java Swing 做这恰恰是它最合适的时候标题里的技术栈拆开看其实很朴素Java 负责业务逻辑Swing 负责窗口与表格展示MySQL 负责数据长期保存三者串起来就是一个员工工资管理系统的完整闭环。很多团队一听到 Swing 就默认这是毕业设计但在一台没有外网、没有专职 IT 的办公室里一个双击就能打开的桌面工资管理工具比 Web 后台更省事不用部署中间件不用维护前端依赖数据直接落在内网 MySQL 实例上。这个标题能解决的核心问题是工资怎么录、怎么算、怎么查、怎么防重复适合想从零复现完整案例的开发者也适合拿来做企业内的小工具自用。2. 选型与架构Swing 桌面端为什么能扛起工资管理三层职责怎么切2.1 为什么桌面管理系统不选 WebSwing 反而被召回这个方案最容易收到的问题就是现在谁还写 Swing真正做过一次内网办公工具的人会反过来问为什么一定要上 Web工资管理的数据量不大用户无非是财务和行政总共不到十个账号。Web 方案要装 Tomcat 或 Spring Boot 内嵌容器还要考虑端口占用、浏览器兼容、前端构建链路维护成本全部压在一个人身上Swing 双击即可运行升级时换一个 jar 或 exe 就行几乎不需要中间层。这两年一些老团队把 Swing 重新召回桌面办公场景原因非常现实终端电脑配置参差、系统权限受限、又不能把公司数据放到公网服务上。桌面端对网络连接依赖极小MySQL 连不上时能弹出明确的错误对话框而不是像网页一样直接白屏或 500。这不是技术落后是场景匹配。Swing 也有代价界面观感确实停留在早期桌面风格所以使用时要克制不做花哨样式重点保证稳定、可操作、可打印。2.2 三层职责划分UI 只画界面DAO 只碰 SQL这个项目最容易翻车的写法是把 JDBC 代码直接写进按钮监听器里。用户第一次点击能跑第二次换个查询条件就要改界面代码。我一般会把工程拆成四层结构如下com.example.salary ├── Main.java // 程序入口负责拉起登录窗口 ├── ui // Swing 窗口、面板、表格模型 ├── service // 登录校验、工资计算、事务编排 ├── dao // JDBC 数据访问对 service 暴露方法 └── entity // User、Employee、SalaryRecord 实体Main 只做一件事启动登录界面。ui 包里的类不出现任何 SQL 字符串和 Connection 对象只负责收集用户输入、调用 service、刷新界面。service 层处理业务规则比如实发工资计算、月份重复校验dao 层是唯一允许碰 JDBC 的地方方法命名尽量直白比如 findEmployeeByEmpNo、insertSalaryRecord。这样拆的直接好处是排错成本低某个 DAO 查询写错了写个 main 方法就能单独验证不需要把整个界面跑起来点一遍按钮。2.3 开发环境约定安装 MySQL 后先确认四件事开发环境不复杂JDK 8 以上、MySQL 5.7 或 8.0、一个数据库连接驱动 jar、一个 Java IDE。很多人照 MySQL 安装配置教程装完之后急着建表结果连接时报一堆时区、字符集问题。我一般装完先确认四件事MySQL 服务已启动且允许本机连接JAVA_HOME 和 PATH 环境变量正确命令行能敲出 java -version驱动 jar 已放进项目的 lib 目录并加入了编译 classpath创建一个独立库默认字符集设为 utf8mb4。这四项里任何一项没对齐后续所有排查都会往错误方向跑。如果不用 Maven 而是手动编译运行classpath 是最容易踩的一个点。常见做法是用通配符把 lib 目录下所有 jar 一次性加入javac -encoding UTF-8 -cp lib/*:src src/com/example/salary/Main.java -d out java -cp lib/*:out com.example.salary.Main第一行把源码编到 out 目录第二行运行时把 lib 下的驱动和 out 目录放进 classpath。Windows 下路径分隔符是分号Linux 和 macOS 是冒号这行命令直接决定你能不能跑起来。用 IDE 的同学也要注意jar 拷进项目目录不等于加载必须 Add as Library 之后才参与编译和运行。2.4 驱动依赖Maven 坐标与手动 lib 的取舍依赖管理上两种路线都行。用 Maven 就引 mysql-connector-j版本和你本地 MySQL 主版本对应即可不用 Maven就从驱动官网下载 jar 放进 lib 目录。这里有一个踩出来的教训classpath 里绝对不能同时放两个大版本不同的驱动 jar。旧版本驱动的类名是 com.mysql.jdbc.Driver新版本是 com.mysql.cj.jdbc.Driver两者同时存在时注册顺序不受控会出现“有时连得上有时报驱动类找不到”的玄学问题。统一成一个版本是所有驱动类报错的第一条排查规则。3. 数据表与 JDBC 连接decimal 精度、事务边界与三个必调参数3.1 三张表的设计金额一律 decimal防重复靠唯一索引工资管理系统不是复杂业务但数据库设计有几个硬要求金额必须精确、同一个人同一个月不能发两次、员工离职后档案不能被工资记录引用报错。下面这套建表 SQL 是常见做法我一般会在项目开始就把这三张表先落地CREATE DATABASE employee_salary DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT ADMIN, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE t_employee ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 员工工号, name VARCHAR(32) NOT NULL, department VARCHAR(32) NOT NULL, position_title VARCHAR(32) NOT NULL COMMENT 岗位名称, base_salary DECIMAL(10,2) NOT NULL COMMENT 基础工资, entry_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 在职0 离职 ) ENGINEInnoDB; CREATE TABLE t_salary_record ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, salary_month CHAR(7) NOT NULL COMMENT 格式 2025-06, base_salary DECIMAL(10,2) NOT NULL, bonus DECIMAL(10,2) NOT NULL DEFAULT 0.00, overtime DECIMAL(10,2) NOT NULL DEFAULT 0.00, deduction DECIMAL(10,2) NOT NULL DEFAULT 0.00, actual_salary DECIMAL(10,2) NOT NULL COMMENT 实发工资, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_salary_month (emp_id, salary_month), CONSTRAINT fk_salary_emp FOREIGN KEY (emp_id) REFERENCES t_employee(id) ) ENGINEInnoDB;工资字段全部用 DECIMAL(10,2) 而不是 DOUBLE是因为浮点类型在累计加减时会产生精度漂移发工资这种对账敏感的数据经不起一分的误差。t_salary_record 表把基础工资、奖金、加班费、扣款、实发工资都冗余在一行里查询时直接读取实发不用每次临时计算。最关键的约束是唯一索引 uk_salary_month它从数据库层面拦截“同一员工同一个月重复发薪”。外键则保证工资记录不会指向一个不存在的员工。这个冗余和约束的组合是后面所有业务代码的安全底网。3.2 必调连接参数characterEncoding、serverTimezone、useSSL 各管什么JDBC 连接这一步代码本身很短坑全在连接串的参数里。我一般会在一个单独的类里维护 getConnection 方法方便统一改配置public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/employee_salary ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai useSSLfalseallowPublicKeyRetrievaltrue; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, root, 你的密码); } }每个参数都有明确职责缺一个就碰到一类典型问题。把它们整理成一张参数表排查时对着看参数作用不带它时看到的现象useUnicodetruecharacterEncodingutf8中文按 UTF-8 传输表里中文问号界面中文乱码serverTimezoneAsia/Shanghai指定时区报 CST 或 UTC 相关时区异常useSSLfalse关闭 SSL 握手内网连接变慢控制台刷 SSL 警告allowPublicKeyRetrievaltrue允许获取公钥报 Public Key Retrieval is not allowed前两个参数解决的是字符集和时区一致性问题这也是 MySQL 安装配置教程里最容易被忽略的两项。useSSL 在内网场景通常关掉因为工资管理系统走局域网加密意义不大反而白白增加握手开销。allowPublicKeyRetrieval 是 MySQL 8.0 默认认证插件 caching_sha2_password 带来的必配项很多旧项目升级到 8.0 之后连不上就是缺这个参数。3.3 PreparedStatement 与 ORDER BYSQL 写常量值走占位符连接串只是第一关真正每天要写的是查询。查员工列表、按月份查工资记录我全部用 PreparedStatement不用字符串拼接public ListString findDailyIds() { String sql SELECT e.emp_no, e.name, s.salary_month, s.actual_salary FROM t_salary_record s JOIN t_employee e ON s.emp_id e.id WHERE s.salary_month ? ORDER BY e.emp_no; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, month); try (ResultSet rs ps.executeQuery()) { // 逐行封装成 SalaryRecord 对象并返回 } } catch (SQLException e) { throw new RuntimeException(查询工资记录失败, e); } }这里有三点要刻意控制。第一排序尽量放在 SQL 层用 ORDER BY 做让 MySQL 处理排序而不是把数据全部取到内存再手动排。第二ORDER BY 后面的字段不能通过 ? 占位符传入如果列表需要支持用户选择排序字段就用 if 分支检查字段名后拼接白名单。第三ResultSet 遍历完就关闭连接、语句、结果集全部放进 try-with-resources防止连接泄漏把 MySQL 的连接数耗尽。3.4 发薪事务多条记录要么全部生效要么全部回滚工资发放往往不是发一个人而是选中一个部门批量发。批量插入时最怕的情况是前二十条插进去了第二十一条因为重复键失败界面报错退出数据库里却残留了一半数据。所以批量发薪必须放进一个事务里以下是核心控制逻辑public void batchPay(ListSalaryRecord records) { String sql INSERT INTO t_salary_record (emp_id, salary_month, base_salary, bonus, overtime, deduction, actual_salary) VALUES (?, ?, ?, ?, ?, ?, ?); try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (SalaryRecord r : records) { ps.setLong(1, r.getEmpId()); ps.setString(2, r.getSalaryMonth()); ps.setBigDecimal(3, r.getBaseSalary()); ps.setBigDecimal(4, r.getBonus()); ps.setBigDecimal(5, r.getOvertime()); ps.setBigDecimal(6, r.getDeduction()); ps.setBigDecimal(7, r.getActualSalary()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException(批量发薪失败已全部回滚, e); } finally { conn.setAutoCommit(true); } } catch (SQLException e) { throw new RuntimeException(数据库连接失败, e); } }setAutoCommit(false) 是把多条 INSERT 变成一个工作单元executeBatch 统一执行commit 之前任何一条语句抛异常都会走进 catch 里的 rollback。事务边界一定要涵盖整个循环而不是每插一条就自动提交一次。另外唯一索引是防重复的最后一道闸两个窗口同时给同一个人发同一个月工资时MySQL 的 InnoDB 引擎在唯一索引等值插入时会加行锁后到的操作要么等待要么直接报重复键被回滚。MySQL 锁的分类在这里不需要深挖你只需要知道应用层校验和数据库唯一索引是两道互补的防线。4. Swing 界面与业务联动登录鉴权、JTable 刷新与工资录入校验4.1 登录窗口界面不写 SQLService 返回结果是唯一依据登录界面的逻辑很简单输入用户名密码点击登录成功跳主界面失败弹提示。代码实现的重点不在界面布局而在事件监听器里绝对不能写 SQL。我一般把登录动作写成这样btnLogin.addActionListener(e - { String username txtUser.getText().trim(); String password new String(txtPwd.getPassword()); User user authService.login(username, password); if (user ! null) { new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 账号或密码错误); } });JPasswordField 的 getPassword() 返回 char[]不要用 toString 转成 String 留在堆里这是一种习惯性安全写法。authService.login 内部会调用 DAO 查询 t_user 表比对通过后返回包含角色的 User 对象返回 null 就表示账号或密码不匹配。这样做的价值在于界面只依赖 service 的返回值密码怎么加密、SQL 怎么写、角色怎么解析全部和 UI 解耦。演示项目里密码可以明文存储真实上线时建议至少加盐哈希这个改动只影响 service 和 DAO不会动界面代码。4.2 JTable 刷新三部曲清空模型、填充数据、重置排序主界面里最常用的组件是 JTable员工列表、工资记录都靠它展示。新手最容易犯的错是查询完直接 table.setModel导致表头重建、列宽重置、排序器丢失。我一般会固定用一套刷新写法public void refreshEmployeeTable(ListEmployee list) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); // 第一步清空旧数据 for (Employee e : list) { // 第二步逐行填充 model.addRow(new Object[]{ e.getEmpNo(), e.getName(), e.getDepartment(), e.getPositionTitle(), e.getBaseSalary().setScale(2), e.getStatus() }); } table.setModel(model); // 第三步重新套回表格 table.repaint(); }setRowCount(0) 这步很关键不加的话每次查询都会把新数据追加在旧数据后面界面越看越乱。第三步重新 setModel 之后之前设置的 TableRowSorter、列宽、渲染器会被重置所以要么在刷新后重新配置要么只通过更新模型数据而不是换模型来刷新。几十行数据时可以用 TableRowSorter 实现点击列头排序但数据量上千之后排序决定权应该交回 SQL 的 ORDER BYUI 层排序只是视图动作不会改数据源顺序。4.3 工资录入与实发计算BigDecimal 统一算四舍五入到分工资录入表单通常有四个金额输入基础工资、奖金、加班费、扣款。实发工资不要在界面上直接相加减而是由 service 层统一计算。这里唯一正确的方式是 BigDecimalpublic BigDecimal calcActualSalary(String baseStr, String bonusStr, String overtimeStr, String deductionStr) { BigDecimal base new BigDecimal(baseStr.trim()); BigDecimal bonus new BigDecimal(bonusStr.trim()); BigDecimal overtime new BigDecimal(overtimeStr.trim()); BigDecimal deduction new BigDecimal(deductionStr.trim()); BigDecimal actual base.add(bonus).add(overtime).subtract(deduction); return actual.setScale(2, RoundingMode.HALF_UP); }从表单拿到的是字符串用 new BigDecimal(String) 构造不允许用 new BigDecimal(double)那是精度灾难。计算完用 setScale(2, RoundingMode.HALF_UP) 四舍五入到分符合财务习惯。录入校验也要在 service 层完成四个金额都不能为负salary_month 用正则校验必须形如 2025-06员工状态必须是在职。这些校验看起来琐碎但少了任何一条脏数据都会顺着 DAO 落进数据库等对账的时候才暴露。4.4 超过 300ms 的查询交给 SwingWorker别阻塞事件线程登录查询很快可以放在按钮事件里直接执行。但工资记录按月汇总、员工报表这类查询可能涉及多表连接处理时间明显变长。Swing 是单线程模型所有界面重绘都依赖事件分发线程 EDT在这个线程里执行 JDBC 查询窗口会直接卡死。常见做法是用 SwingWorker 把耗时操作挪到后台SwingWorkerListSalaryRecord, Void worker new SwingWorker() { Override protected ListSalaryRecord doInBackground() { return salaryService.queryByMonth(month); } Override protected void done() { try { refreshTable(get()); } catch (ExecutionException e) { JOptionPane.showMessageDialog(frame, 查询失败 e.getCause().getMessage()); } } }; worker.execute();doInBackground 在后台线程执行不会阻塞 EDTdone 方法会自动回到 EDT所以在这里可以安全地更新 JTable。后台线程里绝对不能直接调用 model.addRow 或 setText那会触发跨线程访问 UI 的异常。判断标准很简单数据访问耗时超过 300 毫秒就值得扔进 SwingWorker。等真正遇到界面未响应时再改排查成本比提前封装高得多。5. 避坑记录驱动加载、中文乱码与 Swing 线程阻塞的五处翻车点5.1 驱动类找不到Class.forName 和 jar 没有真正进 classpath现象程序启动后几秒控制台抛出 java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver登录窗口还没弹出来就退出了。这个错误在项目初期出现频率最高也最容易被误导。原因主要有两个第一MySQL 8.x 驱动类的完整类名是 com.mysql.cj.jdbc.Driver而 5.1 时代是 com.mysql.jdbc.Driver写旧类名在 8.x 驱动下必然找不到第二IDE 里能看到 import 语句不报错但运行时 classpath 里根本没有 jar说明驱动只是放在项目目录里没有被标记为 Library。解决方法是只保留一个版本的驱动 jar确认它参与了编译和运行手动运行时检查 java -cp 有没有把 lib 目录带上。如果 classpath 同时存在两个大版本驱动注册顺序不可控会出现“时好时坏”的随机失败。5.2 中文乱码文件编码、连接串、表字符集三处必须一致现象界面表格里中文全是问号或者从 MySQL 命令行查出来的中文变成乱码但英文数字正常。这几乎是所有 Java 桌面项目第一次连接数据库都会撞上的问题。原因是字符集没有对齐三处只要有一处不一致就乱码IDE 保存文件的编码、JDBC 连接串的 characterEncoding、数据库表的默认字符集。解决时按顺序检查这三样IDE 统一 UTF-8连接串加 useUnicodetruecharacterEncodingutf8建表指定 utf8mb4。还有一个隐藏坑手动 java 命令跑在 Windows 控制台时如果控制台代码页是 936日志里的中文会乱码但界面的中文正常这是控制台显示问题不是数据库问题别把它带进代码排查里。5.3 界面卡成白屏在 EDT 里做了 JDBC 耗时操作现象点击“查询全部工资记录”按钮后窗口变成未响应鼠标拖动也没反应过几秒才恢复数据量大的时候干脆假死。原因是 Swing 的事件监听器默认运行在 EDT 上JDBC 查询是网络加磁盘操作耗时远高于界面渲染需求。EDT 被查询阻塞后窗口拖拽、最小化、重绘这些事件全都排队等待界面表现就是卡死。解决方法是按第 4.4 节的做法把耗时查询封装进 SwingWorker在 doInBackground 里访问数据库在 done 里更新表格。记住 Swing 的单线程模型是铁律后台线程直接操作组件不仅会卡死还会抛出 InterruptedException 或线程竞争导致数据错乱。5.4 MySQL 8 连接报错认证插件、公钥获取与时区缺一不可现象本地装的是 MySQL 8.0用老驱动连接报 Unable to load authentication plugin caching_sha2_password或者报 Public Key Retrieval is not allowed还有可能报服务器时区无法识别。原因是 MySQL 8.0 把默认认证插件换成了 caching_sha2_password老版本驱动不认识这个插件allowPublicKeyRetrieval 未开启时驱动又不允许从服务端获取公钥做加密通信。解决方法是把驱动升级到 8.x连接串加上 allowPublicKeyRetrievaltrue。顺带检查 serverTimezoneJava 8 以上版本必须显式指定时区否则驱动读取系统默认时区会得到无法识别的值。我一般统一写成 serverTimezoneAsia/Shanghai不要依赖操作系统时区这样换一台机器部署也不会因为时区不同而翻车。5.5 SQL 拼接与空指针一个引号毁掉查询缓存现象文本框输入1 or 11后查询结果把整个表的数据都带出来或者录入工资时奖金不填保存就报空指针。这就是 SQL 注入的典型表现字符串拼接把用户输入的内容直接变成了 SQL 语义的一部分原来的条件被 or 改写等于没过滤。解决方法是 100% 使用 PreparedStatement所有值都走 ? 占位符哪怕输入内容全是数字也不能拼字符串。涉及 IN 列表时不要试图传一个字符串进去而是循环生成对应数量的占位符。空指针的坑通常藏在金额字段表单里奖金留空代码用 getText() 拿到空字符串直接 new BigDecimal() 必然抛异常。在 service 层做统一的空值判断例如 StringUtils.isBlank 时按 0.00 处理再进入计算能省掉后续大量排查时间。6. 进阶月度工资报表导出与最小闭环验证工资管理系统能正常增删改查之后接下来最重要的能力是把月度工资导成文件。最常见的落地做法是导出 CSV让 Excel 能直接打开。导出时有一个细节中文要正常显示文件必须带 UTF-8 BOM 头否则 Excel 打开 UTF-8 编码的 CSV 会出现中文乱码。核心逻辑如下public void exportMonthly(String month) throws IOException { ListSalaryRecord records salaryService.queryByMonth(month); StringBuilder sb new StringBuilder(\uFEFF); // BOM让 Excel 识别 UTF-8 sb.append(工号,姓名,月份,基础工资,奖金,加班费,扣款,实发工资\n); for (SalaryRecord r : records) { sb.append(r.getEmpNo()).append(,) .append(r.getName()).append(,) .append(r.getSalaryMonth()).append(,) .append(r.getBaseSalary()).append(,) .append(r.getBonus()).append(,) .append(r.getOvertime()).append(,) .append(r.getDeduction()).append(,) .append(r.getActualSalary()).append(\n); } Files.write(Paths.get(salary_ month .csv), sb.toString().getBytes(StandardCharsets.UTF_8)); }BigDecimal 直接 toString 输出不会丢精度也不会有科学计数法隐患。导出之前先跑一遍完整闭环验证建库建表并插入一个 admin 账号新增一条在职员工档案录入一个月的工资执行月度查询导出 CSV 并用 Excel 打开核对实发工资。整个链路走通这个系统才算真正交付。上线前还要做一步备份mysqldump 命令顺手就能完成mysqldump -u root -p employee_salary backup.sql恢复时用 mysql -u root -p employee_salary backup.sql。我自己做这类小系统有个习惯每次改完连接参数先写一条 select 1 的最短查询验证再开始跑业务这能省下大量“怎么连不上”的血泪经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表