简介:本资源是一套基于Java Swing与MySQL开发的员工工资管理系统完整实现,面向计算机专业本科生及Java初学者,适用于Java课程设计、期末大作业等实践教学场景。系统功能覆盖员工信息管理、部门维护、工资核算、查询统计与密码修改等核心模块,代码结构清晰、注释规范,已通过本地编译验证并获助教评审98分,具备良好的学习参考价值与工程可复现性。压缩包共47个文件,含12个核心Java源码(如UI界面类、业务逻辑类、数据库操作类)、10个XML配置文件(含IDEA项目配置与UI Designer布局)、5个SQL脚本(含初始化、备份及多版本数据表结构)、13张功能界面截图(add.png、query.png、modify.png等),以及jar依赖库和README说明文档,整体大小为2.99MB。目前已有153人下载学习,配套文档详述部署步骤、数据库配置与运行要点,便于快速上手与二次开发。
1. 用 Java + Swing + MySQL 搭建员工工资管理系统:不是“写个界面+连个库”就完事,而是要理清三层职责边界
很多刚学完 JDBC 和 Swing 的开发者,拿到“Java+Swing+MySQL实现员工工资管理系统”这个标题时,第一反应是:不就是拖几个 JTextField、写几条 INSERT/SELECT、再套个 JFrame 吗?但实际交付时才发现——新增一个“按部门统计平均工资”功能,要改 7 个类;修改工资计算逻辑后,测试发现社保扣款没同步更新;导出 Excel 时中文乱码,查了三小时才发现 ResultSetMetaData 的字段名编码没统一。这不是代码量问题,而是架构意识缺失:Swing 不只是 UI 容器,它天然承载 MVC 中的 View 和部分 Controller 职责;MySQL 不仅存数据,它的约束、索引、事务边界直接决定业务逻辑能否可靠落地;而 Java 作为胶水层,必须明确划分 DAO 层封装 SQL 细节、Service 层编排业务规则、UI 层响应用户意图。本文面向已掌握 Java 基础语法、能手写 JDBC 连接但尚未系统实践过桌面应用分层的开发者,从零构建一个可维护、可扩展、能通过基础单元测试的工资管理系统——重点不在“跑起来”,而在“改起来不崩”。
2. 为什么选 Swing 而非 JavaFX 或 Web?从真实部署场景倒推技术选型
2.1 企业内网桌面端的不可替代性:离线可用、权限可控、无浏览器兼容风险
当前仍有大量制造业、政务窗口、医院后勤等场景依赖 Windows 7/10 环境下的本地桌面应用。这些系统往往部署在无外网、无统一身份认证、甚至禁止安装 Chrome 的封闭网络中。Swing 的优势在此刻凸显:JRE 8u291 仍被广泛预装,无需额外安装运行时;所有组件渲染由 JVM 自主完成,不依赖系统原生控件(避免 Win11 下 JavaFX 的 WebView 渲染异常);打包成单个 JAR 后,双击即用,运维只需下发一个文件。对比之下,JavaFX 需单独打包 runtime(增加 50MB+),Web 方案则面临 IE 兼容性、HTTPS 证书配置、跨域调试等额外成本。某市社保局 2023 年上线的工资核对终端,明确要求“不依赖浏览器、不调用外部 API、所有操作日志本地落盘”,最终采用 Swing + HSQLDB 嵌入式方案——这正是本项目选用 Swing 的现实依据。
2.2 MySQL 作为后端存储的合理性:事务强一致性与结构化查询能力不可替代
工资计算涉及多表关联(员工、部门、岗位、考勤、绩效)、并发更新(多人同时提交工资调整)、数据一致性校验(实发工资 = 应发 - 社保 - 公积金 - 个税)。MySQL 的 InnoDB 引擎提供 ACID 事务保障,支持行级锁避免更新冲突;其 DATE、DECIMAL 类型精准表达入职日期、薪资金额;内置函数如ROUND(salary * 0.12, 2)可直接在 SQL 层完成社保计算,减少 Java 层数据搬运。虽然 SQLite 更轻量,但缺乏用户权限管理,无法满足“HR 可读写、部门经理只读本部门”的分级控制需求;而 PostgreSQL 虽功能更强,但企业内网环境常限制安装非标数据库。因此,MySQL 5.7+(兼容性最佳)成为本项目的存储底座,且必须启用innodb_strict_mode=ON防止隐式类型转换导致的数据截断。
2.3 分层设计决策:Swing UI 层不掺杂 SQL,DAO 层不暴露 Connection
常见错误是把Connection conn = DriverManager.getConnection(...)写在 JButton 的actionPerformed()里,导致连接泄漏、SQL 注入、UI 线程阻塞。正确做法是:
- UI 层(Swing):只负责事件监听、数据展示、输入校验(如身份证号格式、薪资范围 3000–99999),通过
ActionListener触发业务方法,绝不触碰数据库; - Service 层(Java):定义
SalaryService.calculateMonthlySalary(employeeId)接口,封装工资计算规则(含个税速算扣除数查表、社保基数浮动逻辑),调用 DAO 获取原始数据; - DAO 层(JDBC):使用
PreparedStatement参数化查询,try-with-resources确保ResultSet/Statement/Connection自动关闭,返回 POJO(如Employee、SalaryRecord),不返回ResultSet。
提示:Swing 的 EDT(Event Dispatch Thread)严禁执行耗时操作。所有数据库访问必须在
SwingWorker中异步执行,否则界面会卡死。例如点击“加载全部员工”按钮时,需启动后台线程查询,完成后用publish()/process()更新表格模型。
3. 数据库建模与 Java 实体映射:用 ER 图约束业务语义,而非堆字段
3.1 核心表结构设计:聚焦工资管理本质,拒绝过度泛化
根据《企业工资支付暂行规定》及常见 HR 流程,最小可行表结构包含 4 张表,字段严格遵循第三范式:
| 表名 | 字段(关键) | 类型 | 约束 | 说明 |
|---|---|---|---|---|
employee | id,name,id_card,entry_date,dept_id,position_id | INT PK, VARCHAR(50), CHAR(18), DATE, INT FK, INT FK | NOT NULL, UNIQUE(id_card) | 员工主表,身份证号唯一 |
department | id,name,manager_id | INT PK, VARCHAR(30), INT FK | — | 部门表,manager_id 指向 employee.id |
salary_config | id,base_salary,social_security_rate,housing_fund_rate,tax_threshold | INT PK, DECIMAL(10,2), DECIMAL(5,3), DECIMAL(5,3), DECIMAL(10,2) | DEFAULT 0.12, 0.08 | 工资配置表,按岗位/职级关联 |
salary_record | id,employee_id,month,base_salary,bonus,deduction,actual_salary,created_at | INT PK, INT FK, CHAR(6), DECIMAL(10,2), DECIMAL(10,2), DECIMAL(10,2), DECIMAL(10,2), DATETIME | INDEX(month, employee_id) | 工资记录表,month 格式为 '202403' |
注意:
salary_record.month使用 CHAR(6) 存储 '202403' 而非 DATE 类型,避免跨月查询时的时区转换问题,且便于按年月快速分区(如WHERE month BETWEEN '202401' AND '202412')。
3.2 Java 实体类与 JDBC 映射:用 POJO 承载业务语义,而非字段搬运工
实体类需体现领域概念,而非简单复制表结构。例如SalaryRecord不应只有getActualSalary(),而应提供业务方法:
public class SalaryRecord { private Long id; private Integer employeeId; private String month; // "202403" private BigDecimal baseSalary; private BigDecimal bonus; private BigDecimal deduction; private BigDecimal actualSalary; // 构造函数省略 public BigDecimal getTax() { BigDecimal taxableIncome = this.baseSalary.add(this.bonus).subtract(this.deduction); if (taxableIncome.compareTo(BigDecimal.ZERO) <= 0) return BigDecimal.ZERO; // 简化个税计算:起征点5000,税率10%,速算扣除数210 BigDecimal threshold = new BigDecimal("5000"); BigDecimal rate = new BigDecimal("0.1"); BigDecimal quickDeduction = new BigDecimal("210"); return taxableIncome.subtract(threshold) .multiply(rate) .subtract(quickDeduction) .max(BigDecimal.ZERO); } public boolean isOverThreshold() { return this.actualSalary.compareTo(new BigDecimal("10000")) > 0; } }DAO 层使用ResultSet映射时,必须显式指定列名,避免因 SQL 字段顺序变动导致赋值错位:
// 正确:按列名取值,与 SELECT 字段顺序无关 while (rs.next()) { SalaryRecord record = new SalaryRecord(); record.setId(rs.getLong("id")); record.setEmployeeId(rs.getInt("employee_id")); record.setMonth(rs.getString("month")); // 注意:CHAR(6) 直接转 String record.setBaseSalary(rs.getBigDecimal("base_salary")); // ... 其他字段 }3.3 初始化脚本与连接池配置:HikariCP 是 Swing 应用的稳定选择
Swing 应用启动时需初始化数据库连接。推荐使用 HikariCP(轻量、高性能、默认配置即安全):
<!-- pom.xml --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>// DatabaseConnection.java public class DatabaseConnection { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/salary_db?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("your_password"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(10); // Swing 应用并发低,10 足够 config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }提示:MySQL 连接 URL 必须添加
serverTimezone=Asia/Shanghai,否则entry_date等 DATE 字段可能因时区差异写入错误日期。useSSL=false仅用于开发环境;生产环境需配置 SSL 证书。
4. Swing 界面实现:用 JTable + TableModel 封装数据逻辑,而非手动拼接字符串
4.1 主窗口布局:BorderLayout + CardLayout 实现模块切换
主界面采用BorderLayout,顶部放菜单栏(File、Edit、Report),中心区域用CardLayout切换不同功能面板(员工管理、工资录入、报表查询):
public class MainFrame extends JFrame { private JPanel cardPanel; private CardLayout cardLayout; private EmployeePanel employeePanel; private SalaryPanel salaryPanel; private ReportPanel reportPanel; public MainFrame() { setTitle("员工工资管理系统 v1.0"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 菜单栏 JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> System.exit(0)); fileMenu.add(exitItem); menuBar.add(fileMenu); setJMenuBar(menuBar); // 卡片面板 cardPanel = new JPanel(); cardLayout = new CardLayout(); cardPanel.setLayout(cardLayout); employeePanel = new EmployeePanel(); salaryPanel = new SalaryPanel(); reportPanel = new ReportPanel(); cardPanel.add(employeePanel, "employee"); cardPanel.add(salaryPanel, "salary"); cardPanel.add(reportPanel, "report"); add(cardPanel, BorderLayout.CENTER); pack(); setLocationRelativeTo(null); } public void showPanel(String panelName) { cardLayout.show(cardPanel, panelName); } }4.2 员工表格:自定义 TableModel 实现实时过滤与排序
Swing 的DefaultTableModel无法动态响应数据变更。应继承AbstractTableModel,封装List<Employee>并提供fireTableDataChanged()通知机制:
public class EmployeeTableModel extends AbstractTableModel { private List<Employee> data; private String[] columnNames = {"ID", "姓名", "身份证号", "入职日期", "部门", "岗位"}; public EmployeeTableModel(List<Employee> employees) { this.data = new ArrayList<>(employees); } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Employee emp = data.get(rowIndex); switch (columnIndex) { case 0: return emp.getId(); case 1: return emp.getName(); case 2: return emp.getIdCard(); case 3: return emp.getEntryDate(); // java.time.LocalDate 自动格式化 case 4: return emp.getDeptName(); // 关联查询结果缓存 case 5: return emp.getPositionName(); default: return null; } } @Override public String getColumnName(int column) { return columnNames[column]; } // 提供刷新方法 public void refreshData(List<Employee> newData) { this.data = new ArrayList<>(newData); fireTableDataChanged(); } // 提供按姓名模糊搜索 public void filterByName(String keyword) { if (keyword == null || keyword.trim().isEmpty()) { refreshData(EmployeeDAO.findAll()); } else { List<Employee> filtered = EmployeeDAO.findByName(keyword.trim()); refreshData(filtered); } } }在EmployeePanel中绑定表格:
JTable table = new JTable(new EmployeeTableModel(EmployeeDAO.findAll())); JScrollPane scrollPane = new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); // 添加搜索框 JTextField searchField = new JTextField(20); searchField.addActionListener(e -> { String keyword = searchField.getText(); ((EmployeeTableModel) table.getModel()).filterByName(keyword); });4.3 表单验证:用 InputVerifier 阻断非法输入,而非弹窗提示
Swing 表单需在用户输入时即时拦截错误,而非提交后才报错。例如薪资输入框必须限制为数字且范围合理:
JTextField salaryField = new JTextField(); salaryField.setInputVerifier(new InputVerifier() { @Override public boolean verify(JComponent input) { JTextField field = (JTextField) input; String text = field.getText().trim(); if (text.isEmpty()) { JOptionPane.showMessageDialog(null, "薪资不能为空"); return false; } try { BigDecimal salary = new BigDecimal(text); if (salary.compareTo(new BigDecimal("3000")) < 0 || salary.compareTo(new BigDecimal("99999")) > 0) { JOptionPane.showMessageDialog(null, "薪资应在 3000–99999 之间"); return false; } return true; } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, "请输入有效数字"); return false; } } });提示:
InputVerifier在焦点离开组件时触发,比DocumentListener更符合表单验证语义。身份证号验证可复用 Apache Commons Validator 的IdCardValidator。
5. 工资计算与报表导出:将业务规则固化到 Service 层,避免 UI 层硬编码
5.1 工资计算 Service:解耦规则与界面,支持未来政策调整
工资计算逻辑必须独立于 Swing 组件,便于单元测试和政策变更:
public class SalaryService { // 从数据库读取配置 public SalaryConfig getConfigByEmployeeId(Integer employeeId) { return SalaryConfigDAO.findByEmployeeId(employeeId); } // 计算单月工资 public SalaryRecord calculateSalary(Integer employeeId, String month) { Employee emp = EmployeeDAO.findById(employeeId); SalaryConfig config = getConfigByEmployeeId(employeeId); BigDecimal base = config.getBaseSalary(); BigDecimal bonus = BonusDAO.findByEmployeeAndMonth(employeeId, month).orElse(BigDecimal.ZERO); BigDecimal deduction = calculateDeduction(emp, config, month); BigDecimal actual = base.add(bonus).subtract(deduction); return new SalaryRecord(null, employeeId, month, base, bonus, deduction, actual); } private BigDecimal calculateDeduction(Employee emp, SalaryConfig config, String month) { // 社保 = 基础工资 × 比例 BigDecimal socialSecurity = config.getBaseSalary() .multiply(config.getSocialSecurityRate()); // 公积金 = 基础工资 × 比例 BigDecimal housingFund = config.getBaseSalary() .multiply(config.getHousingFundRate()); // 个税 = (应纳税所得额 - 起征点)× 税率 - 速算扣除数 BigDecimal taxable = config.getBaseSalary().add(bonus).subtract(socialSecurity).subtract(housingFund); BigDecimal tax = calculateTax(taxable, config.getTaxThreshold()); return socialSecurity.add(housingFund).add(tax); } private BigDecimal calculateTax(BigDecimal taxableIncome, BigDecimal threshold) { if (taxableIncome.compareTo(threshold) <= 0) return BigDecimal.ZERO; BigDecimal excess = taxableIncome.subtract(threshold); // 简化:统一税率10%,速算扣除数210 return excess.multiply(new BigDecimal("0.1")).subtract(new BigDecimal("210")) .max(BigDecimal.ZERO); } }5.2 报表导出:用 Apache POI 生成 Excel,解决中文乱码与样式问题
Swing 应用导出 Excel 需处理字体、编码、单元格格式:
public class ExcelExporter { public static void exportToExcel(List<SalaryRecord> records, String filePath) throws IOException { Workbook workbook = new XSSFWorkbook(); // .xlsx 格式 Sheet sheet = workbook.createSheet("工资报表"); // 设置表头字体(解决中文乱码) Font headerFont = workbook.createFont(); headerFont.setFontName("微软雅黑"); headerFont.setBold(true); headerFont.setFontHeightInPoints((short) 12); CellStyle headerStyle = workbook.createCellStyle(); headerStyle.setFont(headerFont); headerStyle.setAlignment(HorizontalAlignment.CENTER); // 写入表头 Row headerRow = sheet.createRow(0); String[] headers = {"员工ID", "姓名", "月份", "基本工资", "奖金", "扣款", "实发工资"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 写入数据 for (int i = 0; i < records.size(); i++) { Row row = sheet.createRow(i + 1); SalaryRecord r = records.get(i); row.createCell(0).setCellValue(r.getEmployeeId()); row.createCell(1).setCellValue(EmployeeDAO.findById(r.getEmployeeId()).getName()); row.createCell(2).setCellValue(r.getMonth()); row.createCell(3).setCellValue(r.getBaseSalary().doubleValue()); row.createCell(4).setCellValue(r.getBonus().doubleValue()); row.createCell(5).setCellValue(r.getDeduction().doubleValue()); row.createCell(6).setCellValue(r.getActualSalary().doubleValue()); } // 自动列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } try (FileOutputStream fileOut = new FileOutputStream(filePath)) { workbook.write(fileOut); } workbook.close(); } }在 Swing 按钮事件中调用:
JButton exportBtn = new JButton("导出 Excel"); exportBtn.addActionListener(e -> { try { List<SalaryRecord> data = SalaryDAO.findByMonth("202403"); JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("保存 Excel 文件"); chooser.setSelectedFile(new File("工资报表_202403.xlsx")); if (chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { String path = chooser.getSelectedFile().getAbsolutePath(); if (!path.toLowerCase().endsWith(".xlsx")) { path += ".xlsx"; } ExcelExporter.exportToExcel(data, path); JOptionPane.showMessageDialog(null, "导出成功!"); } } catch (Exception ex) { JOptionPane.showMessageDialog(null, "导出失败:" + ex.getMessage()); } });提示:POI 5.2.4+ 默认使用
XSSFWorkbook(.xlsx),兼容性优于HSSFWorkbook(.xls)。若需支持旧版 Excel,需添加poi-ooxml依赖并确保xmlbeans版本匹配。
6. 部署与调试技巧:用 JAR 打包、日志隔离、SQL 日志追踪真实瓶颈
6.1 Maven 打包:生成带依赖的 Fat JAR,规避 ClassPath 问题
Swing 应用分发时,必须将所有依赖(MySQL Connector/J、HikariCP、POI)打包进单个 JAR:
<!-- pom.xml --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.salary.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>执行mvn clean package后,target/salary-system-1.0.jar可直接双击运行或java -jar salary-system-1.0.jar启动。
6.2 日志配置:用 SLF4J + Logback 分离 UI 日志与数据库日志
Swing 应用需区分两类日志:用户操作日志(如“张三修改了李四的工资”)和系统日志(如“SQL 执行超时”)。Logback 配置如下:
<!-- src/main/resources/logback.xml --> <configuration> <!-- 控制台输出(仅 DEBUG 级别) --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 文件输出:操作日志 --> <appender name="OPERATION_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/operation.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/operation.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %msg%n</pattern> </encoder> </appender> <!-- 文件输出:SQL 日志 --> <appender name="SQL_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/sql.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/sql.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>5MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} %msg%n</pattern> </encoder> </appender> <logger name="com.example.salary.ui" level="INFO" additivity="false"> <appender-ref ref="OPERATION_LOG"/> </logger> <logger name="com.zaxxer.hikari" level="DEBUG" additivity="false"> <appender-ref ref="SQL_LOG"/> </logger> <root level="WARN"> <appender-ref ref="CONSOLE"/> </root> </configuration>在 UI 层记录操作:
private static final Logger OPERATION_LOGGER = LoggerFactory.getLogger("com.example.salary.ui"); // ... OPERATION_LOGGER.info("用户 {} 执行了工资导出操作,月份:{}", currentUser, month);6.3 SQL 性能诊断:用慢查询日志定位真实瓶颈
当报表查询变慢时,不要盲目优化 Java 代码。先开启 MySQL 慢查询日志:
-- 登录 MySQL SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; -- 超过 1 秒记为慢查询 SET GLOBAL log_output = 'FILE'; -- 日志输出到文件 SHOW VARIABLES LIKE 'slow_query_log_file'; -- 查看日志路径分析日志发现SELECT * FROM salary_record WHERE month = '202403'未走索引,立即添加复合索引:
ALTER TABLE salary_record ADD INDEX idx_month_emp (month, employee_id);提示:Swing 应用的性能瓶颈 80% 出现在数据库层。务必在
salary_record表上为高频查询字段(month、employee_id)建立索引,并用EXPLAIN验证执行计划。
本文还有配套的精品资源,点击获取