尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java+Swing+Mysql员工工资管理系统实战:从建表到算薪完整教程

Java+Swing+Mysql员工工资管理系统实战:从建表到算薪完整教程 简介这是一套面向Java初学者与课程设计学习者的员工工资管理系统完整源码基于Java Swing桌面端与MySQL数据库开发适合作为毕业设计、课程作业或SwingJDBC综合练习的参考方案。系统分为管理员与普通用户两种角色管理员可对员工信息、部门信息及工资数据进行增删改查与统计普通用户则以匿名方式登录查询个人工资功能覆盖了典型信息管理系统的核心业务闭环。压缩包共133个文件约2.1MB其中109个class为编译产物、16个java为可阅读源码另含2个sql建库脚本、jar依赖及工程配置文件sql脚本位于sql文件夹下导入后即可运行。目前已有3960人学习下载资源经多次测试运行无误读者可借此理解Swing界面布局、事件监听、JDBC连接与数据库表设计的配合方式并在此基础上扩展统计报表、权限控制等功能。1. 从一张工资条说起JavaSwingMysql 员工工资管理系统到底解决什么问题每个月 10 号前后行政把考勤表、绩效表、社保基数表三份 Excel 丢过来让财务在两天内算出全员工资并打印工资条。这个场景里最痛的不是算钱而是三张表口径对不上考勤表里有人请假三天绩效表里却按全勤打了分社保基数调整了Excel 里还是上季度的数。手工核对一遍眼睛都花了还容易出错。用 JavaSwingMysql 做一套员工工资管理系统本质就是把「数据录入、规则计算、结果留痕」这三件事从 Excel 里搬到数据库和桌面程序里让每次算薪都有据可查、可回滚。这套方案适合谁一是计算机相关专业的课程设计Swing 做界面、Mysql 存数据是 java课程设计案例源码里最经典的组合二是中小企业内部工具不需要部署 Web 服务器装个 JDK 和 Mysql 就能跑运维成本低。它不追求高并发也不追求分布式追求的是「一个人两天能搭出能用的版本」。下面我会按「环境怎么搭、表怎么设计、界面怎么分层、算薪逻辑怎么写、坑在哪」的顺序把这条路径完整走一遍。2. 环境搭建与项目骨架JDK、Mysql 和 Swing 怎么串起来2.1 开发环境选型与安装顺序先把工具链定下来避免后面因为版本问题反复折腾。JDK 用 8 或 11 都行Swing 在这两个版本上最稳高版本 JDK 对 Swing 的兼容性没有本质变化但部分 LookAndFeel 在 JDK 17 之后需要额外配置。Mysql 建议用 8.0字符集选 utf8mb4避免中文姓名和部门名出现乱码。IDE 用 IntelliJ IDEA 社区版就够不需要额外插件。安装顺序有讲究先装 JDK配好 JAVA_HOME 和 Path命令行执行java -version能输出版本号再装 Mysql安装时记住 root 密码装完用mysql -u root -p能进命令行最后装 IDEA新建项目时选 Java 项目不要选 Maven 或 Gradle 模板课程设计阶段手动管理依赖更直观。# 验证 JDK 安装 java -version # 输出示例java version 1.8.0_381 # 验证 Mysql 安装并登录 mysql -u root -p # 输入密码后进入 mysql 提示符即成功这两条命令是后面所有操作的前提。如果java -version报「不是内部或外部命令」说明 Path 没配好如果mysql -u root -p报error 2002 (hy000): cant connect to local mysql server through socket说明 Mysql 服务没启动Windows 下用net start mysql80启动Linux 下用systemctl start mysqld。2.2 数据库建库建表员工表、工资项表、工资记录表表结构决定了后面代码怎么写。我一般拆三张核心表employee存员工基本信息salary_item存工资项定义基本工资、绩效、社保、公积金等salary_record存每月算薪结果。这样拆的好处是工资项可以动态增减不用改表结构。-- 创建数据库字符集用 utf8mb4 CREATE DATABASE salary_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE salary_db; -- 员工表 CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, dept_name VARCHAR(50), base_salary DECIMAL(10,2) DEFAULT 0.00, hire_date DATE, status TINYINT DEFAULT 1 -- 1在职 0离职 ); -- 工资项定义表 CREATE TABLE salary_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(50) NOT NULL, item_type TINYINT NOT NULL, -- 1加项 2减项 default_val DECIMAL(10,2) DEFAULT 0.00 ); -- 工资记录表 CREATE TABLE salary_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, salary_month VARCHAR(7) NOT NULL, -- 格式 2025-06 item_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_emp_month (emp_id, salary_month) );employee表里base_salary用DECIMAL(10,2)而不是FLOAT因为工资计算不能有浮点误差DECIMAL是定点数0.10.2 不会变成 0.30000000000000004。salary_record表建了(emp_id, salary_month)联合索引因为查某人某月工资是最高频操作。status字段用TINYINT而不是BOOLEANMysql 里BOOLEAN本质也是TINYINT(1)直接写TINYINT更明确。提示建表时如果提示「Specified key was too long」检查VARCHAR长度和字符集utf8mb4 下索引长度限制是 3072 字节VARCHAR(50)完全够用。2.3 JDBC 连接封装与 Swing 主窗口骨架数据库连上了接下来把 JDBC 连接封装成一个工具类避免每次操作都写一遍DriverManager.getConnection。Swing 这边先搭一个主窗口用JFrameJTabbedPane把员工管理、工资项管理、算薪、查询四个模块分开。// DBUtil.java —— 数据库连接工具类 import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/salary_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PWD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }连接串里useSSLfalse是必须的Mysql 8.0 默认要求 SSL 连接不加这个参数会报mysql ssl连接错误。serverTimezoneAsia/Shanghai解决时区问题不加会报The server time zone value ?D1ú±ê×?ê±?? is unrecognized。characterEncodingutf8保证中文不乱码。// MainFrame.java —— 主窗口骨架 import javax.swing.*; public class MainFrame extends JFrame { public MainFrame() { setTitle(员工工资管理系统); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs new JTabbedPane(); tabs.addTab(员工管理, new EmployeePanel()); tabs.addTab(工资项管理, new SalaryItemPanel()); tabs.addTab(算薪, new CalcPanel()); tabs.addTab(工资查询, new QueryPanel()); add(tabs); } public static void main(String[] args) { SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }SwingUtilities.invokeLater是 Swing 的规矩所有界面创建都放在事件分发线程里否则可能出现界面闪烁或组件不显示。setLocationRelativeTo(null)让窗口居中不加的话窗口默认在屏幕左上角。四个JPanel分别对应四个功能模块后面逐个实现。3. 员工与工资项管理Swing 表格绑定和增删改查怎么写3.1 用 JTable DefaultTableModel 展示员工列表Swing 里展示列表数据最直接的是JTable配DefaultTableModel。DefaultTableModel内部用Vector存数据增删行会自动刷新界面不用手动调repaint。// EmployeePanel.java —— 员工管理面板 import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; public class EmployeePanel extends JPanel { private JTable table; private DefaultTableModel model; public EmployeePanel() { setLayout(new BorderLayout()); // 表头定义 String[] cols {工号, 姓名, 部门, 基本工资, 入职日期, 状态}; model new DefaultTableModel(cols, 0) { Override public boolean isCellEditable(int row, int col) { return false; // 表格只读编辑走弹窗 } }; table new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); // 底部按钮 JPanel btnPanel new JPanel(); JButton addBtn new JButton(新增); JButton editBtn new JButton(编辑); JButton delBtn new JButton(删除); btnPanel.add(addBtn); btnPanel.add(editBtn); btnPanel.add(delBtn); add(btnPanel, BorderLayout.SOUTH); addBtn.addActionListener(e - showEditDialog(null)); editBtn.addActionListener(e - { int row table.getSelectedRow(); if (row -1) { JOptionPane.showMessageDialog(this, 请先选中一行); return; } showEditDialog(row); }); delBtn.addActionListener(e - deleteSelected()); loadData(); } private void loadData() { model.setRowCount(0); // 清空旧数据 String sql SELECT emp_id, emp_name, dept_name, base_salary, hire_date, status FROM employee ORDER BY emp_id; try (Connection conn DBUtil.getConn(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt(emp_id), rs.getString(emp_name), rs.getString(dept_name), rs.getBigDecimal(base_salary), rs.getDate(hire_date), rs.getInt(status) 1 ? 在职 : 离职 }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 加载失败 ex.getMessage()); } } private void deleteSelected() { int row table.getSelectedRow(); if (row -1) { JOptionPane.showMessageDialog(this, 请先选中一行); return; } int empId (int) model.getValueAt(row, 0); int confirm JOptionPane.showConfirmDialog(this, 确认删除工号 empId ); if (confirm ! JOptionPane.YES_OPTION) return; String sql DELETE FROM employee WHERE emp_id ?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, empId); ps.executeUpdate(); loadData(); } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 删除失败 ex.getMessage()); } } private void showEditDialog(Integer row) { // 弹窗逻辑见下一节 } }isCellEditable返回false让表格只读编辑走弹窗这样能统一做数据校验。loadData里先setRowCount(0)清空再重新查比逐行删除简单。删除操作用PreparedStatement而不是拼 SQL 字符串防止 SQL 注入虽然桌面程序注入风险低但养成习惯没坏处。3.2 新增/编辑弹窗与数据校验弹窗用JDialog模态设置成true弹出时主窗口不能操作。字段校验至少做三层非空、格式、范围。// 在 EmployeePanel 里补充 showEditDialog 方法 private void showEditDialog(Integer row) { JDialog dialog new JDialog((Frame) SwingUtilities.getWindowAncestor(this), 员工信息, true); dialog.setSize(400, 320); dialog.setLocationRelativeTo(this); dialog.setLayout(new GridLayout(6, 2, 10, 10)); JTextField nameField new JTextField(); JTextField deptField new JTextField(); JTextField salaryField new JTextField(); JTextField dateField new JTextField(); // 格式 yyyy-MM-dd JComboBoxString statusBox new JComboBox(new String[]{在职, 离职}); if (row ! null) { nameField.setText((String) model.getValueAt(row, 1)); deptField.setText((String) model.getValueAt(row, 2)); salaryField.setText(model.getValueAt(row, 3).toString()); dateField.setText(model.getValueAt(row, 4).toString()); statusBox.setSelectedItem(model.getValueAt(row, 5)); } dialog.add(new JLabel(姓名)); dialog.add(nameField); dialog.add(new JLabel(部门)); dialog.add(deptField); dialog.add(new JLabel(基本工资)); dialog.add(salaryField); dialog.add(new JLabel(入职日期)); dialog.add(dateField); dialog.add(new JLabel(状态)); dialog.add(statusBox); JButton saveBtn new JButton(保存); dialog.add(new JLabel()); dialog.add(saveBtn); saveBtn.addActionListener(e - { String name nameField.getText().trim(); if (name.isEmpty()) { JOptionPane.showMessageDialog(dialog, 姓名不能为空); return; } double salary; try { salary Double.parseDouble(salaryField.getText().trim()); if (salary 0) throw new NumberFormatException(); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(dialog, 基本工资必须是非负数字); return; } // 日期格式校验 if (!dateField.getText().trim().matches(\\d{4}-\\d{2}-\\d{2})) { JOptionPane.showMessageDialog(dialog, 日期格式应为 yyyy-MM-dd); return; } String sql row null ? INSERT INTO employee(emp_name, dept_name, base_salary, hire_date, status) VALUES(?,?,?,?,?) : UPDATE employee SET emp_name?, dept_name?, base_salary?, hire_date?, status? WHERE emp_id?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, name); ps.setString(2, deptField.getText().trim()); ps.setDouble(3, salary); ps.setString(4, dateField.getText().trim()); ps.setInt(5, 在职.equals(statusBox.getSelectedItem()) ? 1 : 0); if (row ! null) ps.setInt(6, (int) model.getValueAt(row, 0)); ps.executeUpdate(); dialog.dispose(); loadData(); } catch (SQLException ex) { JOptionPane.showMessageDialog(dialog, 保存失败 ex.getMessage()); } }); dialog.setVisible(true); }校验顺序是「非空 → 数字格式 → 范围 → 日期格式」每步失败都直接return不继续往下走。日期用正则做初步校验真正入库前如果 Mysql 字段是DATE类型格式不对会抛异常所以正则这层不能省。PreparedStatement的参数下标从 1 开始UPDATE语句里emp_id是第 6 个参数容易数错写的时候对着 SQL 数一遍。3.3 工资项管理动态增减工资项的实现工资项管理比员工管理简单因为字段少。核心是item_type区分加项和减项算薪时加项累加、减项累减。// SalaryItemPanel.java 核心逻辑片段 private void loadItems() { model.setRowCount(0); String sql SELECT item_id, item_name, item_type, default_val FROM salary_item ORDER BY item_id; try (Connection conn DBUtil.getConn(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt(item_id), rs.getString(item_name), rs.getInt(item_type) 1 ? 加项 : 减项, rs.getBigDecimal(default_val) }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 加载失败 ex.getMessage()); } }工资项表里default_val存默认值算薪时如果某员工没有单独设置该项就用默认值。这样设计的好处是新增一个「交通补贴」项只需要在salary_item表插一条记录不用改代码。常见做法是初始化时预置几条基本工资加项、绩效奖金加项、社保扣款减项、公积金扣款减项、个税减项。4. 算薪逻辑与工资查询从考勤数据到工资条的完整链路4.1 算薪规则设计加项减项与个税简化计算算薪的核心是一张「工资项 × 员工」的矩阵。每个员工每月每个工资项有一个金额加项求和减去减项求和就是实发工资。个税按简化累进算不追求和税务系统完全一致课程设计场景够用。// CalcService.java —— 算薪服务类 import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.*; import java.util.*; public class CalcService { // 简化个税起征点 5000超过部分按 3% 到 45% 七级累进 private static BigDecimal calcTax(BigDecimal taxable) { BigDecimal threshold new BigDecimal(5000); if (taxable.compareTo(threshold) 0) return BigDecimal.ZERO; BigDecimal over taxable.subtract(threshold); double[] rates {0.03, 0.10, 0.20, 0.25, 0.30, 0.35, 0.45}; double[] deduct {0, 210, 1410, 2660, 4410, 7160, 15160}; double overD over.doubleValue(); double tax 0; if (overD 3000) tax overD * rates[0] - deduct[0]; else if (overD 12000) tax overD * rates[1] - deduct[1]; else if (overD 25000) tax overD * rates[2] - deduct[2]; else if (overD 35000) tax overD * rates[3] - deduct[3]; else if (overD 55000) tax overD * rates[4] - deduct[4]; else if (overD 80000) tax overD * rates[5] - deduct[5]; else tax overD * rates[6] - deduct[6]; return BigDecimal.valueOf(Math.max(tax, 0)).setScale(2, RoundingMode.HALF_UP); } // 对指定月份所有在职员工算薪 public static int calcMonth(String month) throws SQLException { String empSql SELECT emp_id, base_salary FROM employee WHERE status 1; String itemSql SELECT item_id, item_type, default_val FROM salary_item; String insertSql INSERT INTO salary_record(emp_id, salary_month, item_id, amount) VALUES(?,?,?,?); String deleteSql DELETE FROM salary_record WHERE salary_month ?; try (Connection conn DBUtil.getConn()) { conn.setAutoCommit(false); // 开启事务 try { // 先删掉该月旧记录支持重算 try (PreparedStatement ps conn.prepareStatement(deleteSql)) { ps.setString(1, month); ps.executeUpdate(); } // 加载工资项 Listint[] items new ArrayList(); // [item_id, item_type] ListBigDecimal defaults new ArrayList(); try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(itemSql)) { while (rs.next()) { items.add(new int[]{rs.getInt(item_id), rs.getInt(item_type)}); defaults.add(rs.getBigDecimal(default_val)); } } int count 0; try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(empSql); PreparedStatement insert conn.prepareStatement(insertSql)) { while (rs.next()) { int empId rs.getInt(emp_id); BigDecimal base rs.getBigDecimal(base_salary); BigDecimal gross BigDecimal.ZERO; BigDecimal deduction BigDecimal.ZERO; for (int i 0; i items.size(); i) { int itemId items.get(i)[0]; int type items.get(i)[1]; BigDecimal amount defaults.get(i); // 基本工资项用员工自己的 base_salary if (itemId 1) amount base; insert.setInt(1, empId); insert.setString(2, month); insert.setInt(3, itemId); insert.setBigDecimal(4, amount); insert.addBatch(); if (type 1) gross gross.add(amount); else deduction deduction.add(amount); } // 个税单独算作为减项插入 BigDecimal taxable gross.subtract(deduction); BigDecimal tax calcTax(taxable); insert.setInt(1, empId); insert.setString(2, month); insert.setInt(3, 99); // 99 代表个税项 insert.setBigDecimal(4, tax); insert.addBatch(); count; } insert.executeBatch(); } conn.commit(); return count; } catch (SQLException ex) { conn.rollback(); throw ex; } } } }conn.setAutoCommit(false)开启事务算薪过程中任何一步失败都rollback避免出现「一半员工有工资记录、一半没有」的脏数据。先DELETE再INSERT支持同月重算重算前把旧记录清掉。个税项用item_id 99硬编码实际项目里应该在salary_item表里插一条「个税」记录这里为了简化直接写死。addBatchexecuteBatch批量插入比逐条executeUpdate快一个数量级几百个员工差距不明显几千个员工差距就大了。4.2 工资查询与导出按月份和部门筛选查询面板用两个下拉框选月份和部门点查询后刷新表格。导出功能用 Java POI 写 Excel热词里有人问「java poi word能生成图表吗」POI 对 Word 图表的支持确实有限但生成 Excel 表格完全没问题。// QueryPanel.java 查询逻辑 private void query() { String month (String) monthBox.getSelectedItem(); String dept (String) deptBox.getSelectedItem(); model.setRowCount(0); StringBuilder sql new StringBuilder( SELECT e.emp_id, e.emp_name, e.dept_name, SUM(CASE WHEN i.item_type 1 THEN r.amount ELSE 0 END) AS gross, SUM(CASE WHEN i.item_type 2 THEN r.amount ELSE 0 END) AS deduction FROM salary_record r JOIN employee e ON r.emp_id e.emp_id JOIN salary_item i ON r.item_id i.item_id WHERE r.salary_month ? ); if (!全部.equals(dept)) sql.append(AND e.dept_name ? ); sql.append(GROUP BY e.emp_id, e.emp_name, e.dept_name); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql.toString())) { ps.setString(1, month); if (!全部.equals(dept)) ps.setString(2, dept); ResultSet rs ps.executeQuery(); while (rs.next()) { BigDecimal gross rs.getBigDecimal(gross); BigDecimal deduction rs.getBigDecimal(deduction); BigDecimal net gross.subtract(deduction); model.addRow(new Object[]{ rs.getInt(emp_id), rs.getString(emp_name), rs.getString(dept_name), gross, deduction, net }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 查询失败 ex.getMessage()); } }SQL 里用CASE WHEN把加项和减项分别求和比在 Java 里循环累加快。GROUP BY按员工分组一个员工一行。部门筛选用动态拼 SQL注意dept参数用PreparedStatement占位符不要直接拼字符串。注意个税项item_id 99在salary_item表里不存在上面的 JOIN 会把它过滤掉导致 gross 和 deduction 都不含个税。正确做法是在salary_item表里插一条item_id 99, item_name 个税, item_type 2的记录或者查询时用LEFT JOIN并单独处理。4.3 用 Java POI 导出工资条 Excel导出用 POI 的XSSFWorkbook生成.xlsx文件。依赖只需要poi-ooxmlIDEA 里在项目结构里加 jar 包或者用 Maven 加依赖。// ExcelExporter.java import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import javax.swing.*; import java.io.FileOutputStream; import java.sql.*; public class ExcelExporter { public static void export(String month, String filePath) throws Exception { String sql SELECT e.emp_id, e.emp_name, e.dept_name, SUM(CASE WHEN i.item_type 1 THEN r.amount ELSE 0 END) AS gross, SUM(CASE WHEN i.item_type 2 THEN r.amount ELSE 0 END) AS deduction FROM salary_record r JOIN employee e ON r.emp_id e.emp_id JOIN salary_item i ON r.item_id i.item_id WHERE r.salary_month ? GROUP BY e.emp_id, e.emp_name, e.dept_name; try (Workbook wb new XSSFWorkbook(); Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, month); ResultSet rs ps.executeQuery(); Sheet sheet wb.createSheet(month 工资表); String[] headers {工号, 姓名, 部门, 应发, 扣款, 实发}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowIdx 1; while (rs.next()) { Row row sheet.createRow(rowIdx); row.createCell(0).setCellValue(rs.getInt(emp_id)); row.createCell(1).setCellValue(rs.getString(emp_name)); row.createCell(2).setCellValue(rs.getString(dept_name)); double gross rs.getBigDecimal(gross).doubleValue(); double deduction rs.getBigDecimal(deduction).doubleValue(); row.createCell(3).setCellValue(gross); row.createCell(4).setCellValue(deduction); row.createCell(5).setCellValue(gross - deduction); } try (FileOutputStream fos new FileOutputStream(filePath)) { wb.write(fos); } } } }XSSFWorkbook对应.xlsx格式HSSFWorkbook对应.xls新项目一律用XSSFWorkbook。try-with-resources确保Workbook和FileOutputStream自动关闭不关的话文件可能被占用无法删除。导出路径由用户通过JFileChooser选择不要硬编码到 C 盘根目录权限问题会翻车。5. 避坑与排查这套系统最容易翻车的五个地方5.1 中文乱码从数据库到 Swing 表格全链路排查现象员工姓名在 Mysql 命令行里显示正常但在 Swing 表格里变成「??」或「锟斤拷」。原因乱码可能出现在三个环节——数据库字符集、JDBC 连接串、Swing 组件字体。Mysql 8.0 默认字符集是 utf8mb4但如果建库时没指定可能继承成 latin1。JDBC 连接串没加characterEncodingutf8也会导致传输乱码。Swing 默认字体在某些系统上不支持中文。解决建库时显式指定DEFAULT CHARACTER SET utf8mb4JDBC URL 加characterEncodingutf8Swing 启动时设置全局字体UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, 14))。三步都做基本能根治。5.2 算薪结果对不上DECIMAL 与 DOUBLE 的精度陷阱现象手工算出来实发 8532.47系统算出来 8532.469999999。原因Java 里double是浮点数0.1 0.2 ! 0.3工资项多了之后误差累积。Mysql 里如果字段用FLOAT或DOUBLE同样有精度问题。解决Java 侧金额一律用BigDecimal构造时用new BigDecimal(0.1)而不是new BigDecimal(0.1)后者会把 double 的误差带进来。Mysql 侧金额字段用DECIMAL(10,2)。BigDecimal做除法时指定setScale(2, RoundingMode.HALF_UP)否则除不尽会抛ArithmeticException。5.3 界面卡死Swing 事件线程里做数据库查询现象点「算薪」按钮后界面白屏几秒按钮点不动鼠标变成等待状态。原因Swing 的事件分发线程EDT既负责界面渲染又负责事件处理如果在按钮的actionPerformed里直接做耗时数据库操作EDT 被阻塞界面无法刷新。解决耗时操作放到SwingWorker的doInBackground里done方法里更新界面。算薪这种可能跑几秒的操作必须用SwingWorker查询这种毫秒级的可以偷懒直接做但养成习惯一律用SwingWorker更稳。// 用 SwingWorker 包装算薪操作 new SwingWorkerInteger, Void() { Override protected Integer doInBackground() throws Exception { return CalcService.calcMonth(month); } Override protected void done() { try { int count get(); JOptionPane.showMessageDialog(null, 算薪完成共 count 人); loadData(); } catch (Exception ex) { JOptionPane.showMessageDialog(null, 算薪失败 ex.getMessage()); } } }.execute();5.4 连接泄漏Connection 没关导致 Mysql 连接数耗尽现象程序运行一段时间后报Too many connections或者操作越来越慢。原因每次DBUtil.getConn()都新建一个连接如果某个分支忘了close连接就泄漏了。Mysql 默认最大连接数 151泄漏几十个之后新连接就建不上了。解决所有数据库操作都用try-with-resourcesConnection、Statement、ResultSet都放在 try 的括号里自动关闭。如果项目规模大了引入连接池比如 HikariCP不要自己管理连接。热词里有人搜「mysql的数据库连接池」桌面程序用 HikariCP 有点重但如果是多人共用一个数据库连接池是必须的。5.5 日期格式与月份筛选字符串比较的坑现象查询「2025-6」查不到数据但数据库里明明有 6 月的记录。原因salary_month字段存的是2025-06查询时传2025-6字符串比较不相等。月份下拉框如果从1到12直接拼就会出现2025-6这种格式。解决月份统一格式化为yyyy-MM用String.format(%04d-%02d, year, month)生成。查询时也做同样格式化。如果数据库里已经存了不规范格式用STR_TO_DATE函数转换后再比较但最好从源头统一格式。6. 进阶技巧把算薪规则做成可配置的以及一个验证习惯这套系统写到后面最容易被吐槽的是「加个新工资项就要改代码」。把算薪规则从硬编码改成配置驱动是让它从课程设计变成能实际用的关键一步。具体做法是在salary_item表里加两个字段calc_type计算类型固定值、按基本工资比例、按考勤天数和calc_param计算参数比如比例值 0.1 表示 10%。算薪时根据calc_type走不同分支新增工资项只需要插一条记录不用动 Java 代码。ALTER TABLE salary_item ADD COLUMN calc_type TINYINT DEFAULT 1; -- 1固定值 2比例 3按天 ALTER TABLE salary_item ADD COLUMN calc_param DECIMAL(10,4) DEFAULT 0;对应的 Java 分支逻辑BigDecimal amount; switch (calcType) { case 1: // 固定值 amount defaultValue; break; case 2: // 按基本工资比例 amount base.multiply(calcParam).setScale(2, RoundingMode.HALF_UP); break; case 3: // 按考勤天数需要额外传入出勤天数 amount base.divide(new BigDecimal(21.75), 4, RoundingMode.HALF_UP) .multiply(new BigDecimal(attendDays)) .setScale(2, RoundingMode.HALF_UP); break; default: amount BigDecimal.ZERO; }calc_type 3的按天计算需要考勤数据可以在salary_record表旁边再加一张attendance表存每月出勤天数算薪时 JOIN 进来。这样整套系统的扩展性就上来了从「写死的工资计算器」变成「可配置的算薪引擎」。验证方法上我自己的习惯是每次改完算薪逻辑先拿一个员工手工算一遍和系统结果对对上了再跑全量。手工算的时候用计算器不要心算心算容易漏掉个税那一步。另外算薪前先备份salary_record表CREATE TABLE salary_record_bak AS SELECT * FROM salary_record算错了还能回滚。这个习惯帮我省过好几次后悔药。最后说一个我踩过的坑Swing 的JTable在数据量超过几千行时滚动会卡因为DefaultTableModel每次addRow都触发一次界面刷新。解决办法是批量添加时先model.setRowCount(0)然后用model.fireTableDataChanged()一次性通知或者换TableModel自己实现getValueAt做懒加载。这个优化在员工数几百的时候感觉不到上千之后差距明显。希望帮到你。本文还有配套的精品资源点击获取
返回列表