简介:一套基于JavaScript、Java、HTML和CSS构建的员工工资管理系统设计源码,面向高校课程设计、毕业设计及Web全栈开发学习者,旨在实现工资数据的自动化处理、智能计算与高效查询,提升企业人力资源管理效能。资源包共340个文件,压缩包大小16.66MB,其中包含65个JavaScript文件、63个Java文件、30个HTML文件、30个CSS文件,另有PNG/GIF图片素材、SCSS/LESS样式扩展、字体图标、SQL数据库脚本及Maven配置等,类型覆盖前端交互、后端逻辑、界面样式与数据持久化各层。系统前端通过JavaScript完成动态交互与即时响应,后端以Java实现工资计算规则、数据存储与接口服务,HTML结合CSS构建清晰友好的操作界面,整体目录结构按功能模块划分,便于检索和二次开发。目前已有286人学习下载。完整源码附带配置文件、说明文档和数据库脚本,可帮助读者快速理解前后端协作流程,也可作为课程设计参考模板或企业级管理系统的开发起点。
1. 为什么工资管理系统要选 Java + JavaScript + HTML + CSS 这套技术栈
对于员工工资管理系统这类内部系统,最稳妥的落地方式不是一上来就上微服务,而是把 Java 用作后端计算和数据访问主力,用 HTML、CSS、JavaScript 把页面交互做干净,最后打成 war 包或 jar 包直接运行。这套组合在课设、毕设和中小公司内部项目里出现频率很高,因为它的源码能按 MVC 分层逐行读下去,不会隔三差五冒出一个三千行的类。工资系统的重心不在页面多炫,而在每笔金额算得准、查得快、能按月追溯。从数据库表到 Servlet 接口再到浏览器端渲染,每一层都能单独替换,正是这套源码设计最值钱的地方。
2. 数据约束先行:工资系统的表设计与初始化 SQL
工资系统对数据一致性要求高,先定表结构再写代码,能避免后面反复改接口。从员工入职、调薪到每月发薪,整个流程可以抽象成三张核心表:员工表、工资配置表、月度工资记录表。
2.1 从工资发放流程拆出三张核心表
工资发放通常要经过三个环节:确认员工基本信息、维护长期工资项目、按月份生成发放记录。员工表保存不会频繁变化的自然属性,比如部门、岗位、入职日期;工资配置表保存基本工资、岗位工资、各项补贴和社保公积金基数的长期值;月度工资记录表保存每个员工某个月的应发、个税、实发以及加班费、奖金等发生额。这样拆分以后,调薪只改配置表,历史记录不受影响;某个月算错只改记录表,不用回头动员工档案。把社保、公积金直接放配置表,适合演示和小型系统;如果公司有独立薪资平台,这些字段应改成引用外部档位 ID,这一版为了控制源码复杂度选择了前者。
2.2 三张表的 DDL 与关键设计点
CREATE DATABASE IF NOT EXISTS salary_db DEFAULT CHARACTER SET utf8mb4; USE salary_db; -- 员工表 CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE, emp_name VARCHAR(30) NOT NULL, dept_name VARCHAR(50) NOT NULL, position VARCHAR(50), hire_date DATE, status TINYINT NOT NULL DEFAULT 1 COMMENT '1在职 0离职', INDEX idx_dept (dept_name) ) ENGINE=InnoDB; -- 工资配置表 CREATE TABLE salary_config ( config_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, base_salary DECIMAL(10,2) NOT NULL DEFAULT 0.00, post_salary DECIMAL(10,2) NOT NULL DEFAULT 0.00, allowance DECIMAL(10,2) NOT NULL DEFAULT 0.00, social_security DECIMAL(10,2) NOT NULL DEFAULT 0.00, housing_fund DECIMAL(10,2) NOT NULL DEFAULT 0.00, FOREIGN KEY (emp_id) REFERENCES employee(emp_id), UNIQUE KEY uk_emp_config (emp_id) ) ENGINE=InnoDB; -- 月度工资记录表 CREATE TABLE salary_record ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, record_month CHAR(7) NOT NULL COMMENT '格式2025-01', before_tax DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '应发工资', tax DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '个税', actual_salary DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资', bonus DECIMAL(10,2) DEFAULT 0.00, overtime_pay DECIMAL(10,2) DEFAULT 0.00, deduction DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_month_emp (record_month, emp_id), FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINE=InnoDB;金额字段全部用DECIMAL(10,2),不要用FLOAT或DOUBLE,否则累加会出现 0.1+0.2 不等于 0.3 的误差。record_month用CHAR(7)存2025-01这样的值,正好和 HTML 原生type="month"提交的格式一致。salary_record上(record_month, emp_id)的唯一索引很关键,它保证同一员工同一月份只有一条记录,重复导入时可以用INSERT ... ON DUPLICATE KEY UPDATE做覆盖更新,而不是生成两条数据。
2.3 初始化数据时最容易踩的外键顺序问题
插入数据必须先插employee,再插salary_config和salary_record,否则外键直接报错。员工编号建议用固定前缀加递增序号,和登录账号区分开。下面是初始化示例:
INSERT INTO employee (emp_no, emp_name, dept_name, position, hire_date) VALUES ('E001', '张倩', '技术部', 'Java开发工程师', '2021-03-15'), ('E002', '李明', '财务部', '会计', '2020-08-01'); INSERT INTO salary_config (emp_id, base_salary, post_salary, allowance, social_security, housing_fund) VALUES (1, 8000.00, 2000.00, 500.00, 800.00, 1000.00), (2, 6000.00, 1200.00, 300.00, 600.00, 800.00); -- 手工插入一条2月历史记录 INSERT INTO salary_record (emp_id, record_month, before_tax, tax, actual_salary, bonus, overtime_pay, deduction) VALUES (1, '2025-02', 10500.00, 300.00, 8400.00, 0.00, 0.00, 0.00);这里salary_record中的数据是之前算好的结果,实际系统里应由 Java 后端计算后写入。测试数据不要塞太多,够联调就行。如果后续要增加新的工资项,比如高温补贴,最简单的做法是在salary_config加字段;如果项目多到十几个,就该拆salary_item子表按员工和月份存储,避免每一列都有大量空值。这个取舍要提前和财务确认。
数据库确认无误后,下一章就用 Java 把这些表变成接口。
3. 用 Java Servlet + JDBC 实现工资查询与计算接口
后端不使用 Spring Boot,是因为 Servlet + JDBC 的源码对课程设计和中小型系统来说更透明:每个类职责单一,业务逻辑从入口到数据库只有两三层,非常适合作为设计源码阅读和二次修改。
3.1 源码角色按 controller / service / dao 三层拆开
在src/main/java/com/demo/salary下建立四个包:
com.demo.salary ├── entity 员工、工资配置、工资记录实体 ├── dao JDBC 查询与写入 ├── service 工资计算、校验规则 └── servlet 接收 HTTP 请求、返回 JSONservlet层不做业务判断,只解析参数、调用 service、写 JSON 响应;service层放个人所得税计算、实发工资计算;dao层封装所有PreparedStatement。这样后续换数据库驱动或者把servlet换成 Spring 的@Controller,业务代码都不需要重写。源码好不好维护,看的不是注释量,而是每一层是否只干一件事。
3.2 工资计算核心:先算应发,再算个税
实发工资的计算路径是:应发 = 基本工资 + 岗位工资 + 补贴 + 加班费 + 奖金;实发 = 应发 - 社保 - 公积金 - 个人所得税 - 其他扣款。个税部分用简化的月度税率表,实际生产中需要改成累计预扣法。
public class SalaryService { /** * 计算个税,按照月度综合所得税率表(简化版) */ private double calcTax(double taxableIncome) { if (taxableIncome <= 0) { return 0; } double tax = 0; if (taxableIncome <= 3000) { tax = taxableIncome * 0.03; } else if (taxableIncome <= 12000) { tax = taxableIncome * 0.10 - 210; } else if (taxableIncome <= 25000) { tax = taxableIncome * 0.20 - 1410; } return Math.max(0, tax); } public double calcActualSalary(double baseSalary, double postSalary, double allowance, double overtimePay, double bonus, double socialSecurity, double housingFund, double deduction) { double beforeTax = baseSalary + postSalary + allowance + overtimePay + bonus; double taxableIncome = beforeTax - socialSecurity - housingFund - 5000; double tax = calcTax(taxableIncome); return beforeTax - socialSecurity - housingFund - tax - deduction; } }参数说明:overtimePay由调用方按加班小时数乘小时工资算好传入,不在这个方法里重新算;taxableIncome是扣除社保公积金和 5000 元起征点后的金额;速算扣除数 210、1410 是税率表常数,去掉后税额会偏大。调用前要确保每个参数不是null,最好在 Servlet 层统一转成double。
3.3 DAO 层用 PreparedStatement 防注入
很多初版源码喜欢拼接 SQL:"SELECT * FROM employee WHERE emp_no = '" + empNo + "'",这是工资系统的安全大忌。员工编号一旦被注入' OR '1'='1,整个工资表都可能被拉走。正确做法是始终使用占位符:
public List<Map<String, Object>> findByMonth(String month, String empNo) { StringBuilder sql = new StringBuilder( "SELECT e.emp_no, e.emp_name, e.dept_name, r.before_tax, r.tax, r.actual_salary " + "FROM salary_record r JOIN employee e ON r.emp_id = e.emp_id " + "WHERE r.record_month = ?"); if (empNo != null && !empNo.isEmpty()) { sql.append(" AND e.emp_no = ?"); } List<Map<String, Object>> result = new ArrayList<>(); try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { ps.setString(1, month); if (empNo != null && !empNo.isEmpty()) { ps.setString(2, empNo); } ResultSet rs = ps.executeQuery(); while (rs.next()) { Map<String, Object> row = new HashMap<>(); row.put("emp_no", rs.getString("emp_no")); row.put("emp_name", rs.getString("emp_name")); row.put("dept_name", rs.getString("dept_name")); row.put("before_tax", rs.getBigDecimal("before_tax")); row.put("tax", rs.getBigDecimal("tax")); row.put("actual_salary", rs.getBigDecimal("actual_salary")); result.add(row); } } catch (SQLException e) { e.printStackTrace(); } return result; }这里try-with-resources会自动关闭Connection、PreparedStatement和ResultSet,避免连接泄漏。setString会把参数内容转义后拼进 SQL,从源头杜绝注入。注意empNo为空时查询该月全部员工,不为空时多追加一个条件,占位符索引从 1 开始连续编号。
3.4 浏览器请求入口:Servlet 只做参数校验和 JSON 序列化
提供一个/api/querySalary接口,前端传month和可选的empNo:
@WebServlet("/api/querySalary") public class QuerySalaryServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=UTF-8"); String month = req.getParameter("month"); if (month == null || !month.matches("\\d{4}-\\d{2}")) { resp.getWriter().write("{\"code\":400,\"msg\":\"month格式需为yyyy-MM\"}"); return; } String empNo = req.getParameter("empNo"); SalaryRecordDao dao = new SalaryRecordDao(); List<Map<String, Object>> data = dao.findByMonth(month, empNo); resp.getWriter().write(buildJson(data)); } }接口参数表:
| 接口路径 | 方法 | 参数 | 返回体 |
|---|---|---|---|
| /api/querySalary | GET | month(必填,yyyy-MM),empNo(可选) | code、data 数组 |
| /api/saveSalary | POST | empId、month、bonus、overtimePay、deduction | code、msg |
month的正则校验放在 Servlet 入口,即使前端漏掉校验,非法值也进不了 SQL。返回值统一带code字段,前端根据它对业务是否成功做分支,和 HTTP 状态码区分开,这样接口可以被其他客户端复用,不绑定浏览器页面。
3.5 保存工资记录时如何处理重复提交
月度工资不能同一员工同一月份出现两条,除了数据库唯一索引,后端保存时也要用upsert逻辑。最简单的是INSERT ... ON DUPLICATE KEY UPDATE,让新数据覆盖旧数据:
public void upsertRecord(int empId, String month, double beforeTax, double tax, double actualSalary, double bonus, double overtimePay, double deduction) throws SQLException { String sql = "INSERT INTO salary_record " + "(emp_id, record_month, before_tax, tax, actual_salary, bonus, overtime_pay, deduction) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE " + "before_tax = VALUES(before_tax), tax = VALUES(tax), " + "actual_salary = VALUES(actual_salary), bonus = VALUES(bonus), " + "overtime_pay = VALUES(overtime_pay), deduction = VALUES(deduction)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, empId); ps.setString(2, month); ps.setDouble(3, beforeTax); ps.setDouble(4, tax); ps.setDouble(5, actualSalary); ps.setDouble(6, bonus); ps.setDouble(7, overtimePay); ps.setDouble(8, deduction); ps.executeUpdate(); } }这里依赖第 2 章建的唯一索引uk_month_emp,没有这个索引,ON DUPLICATE KEY UPDATE就不生效。VALUES()函数在 MySQL 8.0.20 后已标记为废弃,生产环境可以改用别名语法INSERT INTO ... AS new ON DUPLICATE KEY UPDATE actual_salary = new.actual_salary,课设环境用老写法不影响。
4. 前端源码:HTML 页面结构、CSS 布局与 JavaScript 异步请求
页面不需要复杂动画,重点是“输入、查询、结果展示”这条链路顺畅。HTML 给结构,CSS 给间距和反馈,JavaScript 把表单变成请求并把返回 JSON 渲染成表格。
4.1 用 HTML 搭出工资查询页面骨架
一个页面内放查询表单和结果表格。月份用原生type="month"控件,员工编号用普通文本输入框,查询按钮用type="submit",这样按回车也能触发提交。
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>员工工资查询</title> <link rel="stylesheet" href="css/salary.css"> </head> <body> <main class="container"> <h2>员工月度工资查询</h2> <form id="queryForm" class="query-form"> <label for="empNo">员工编号</label> <input type="text" id="empNo" name="empNo" placeholder="例如 E001"> <label for="month">工资月份</label> <input type="month" id="month" name="month" required> <button type="submit" id="searchBtn">查询</button> </form> <table id="salaryTable"> <thead> <tr> <th>员工编号</th> <th>姓名</th> <th>部门</th> <th>应发工资</th> <th>个税</th> <th>实发工资</th> </tr> </thead> <tbody></tbody> </table> </main> <script src="js/salary.js"></script> </body> </html>label的for属性关联输入框 id,点击文字就能聚焦输入框,这个是 HTML 表单可访问性的基础。table用thead和tbody分开,后续 JavaScript 只更新tbody,表头不用重复渲染。
4.2 CSS:用伪类选择器处理鼠标移入和焦点反馈
CSS 部分重点在三个伪类选择器::hover让表格行在鼠标移入时变色,:focus让输入框获得焦点时有蓝色边框,:disabled让按钮在请求中变成灰色不可点。这些交互反馈不需要 JavaScript,CSS 原生就能完成。
.container { max-width: 920px; margin: 30px auto; padding: 24px; background-color: #f7f9fc; border-radius: 8px; } .query-form { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 18px; } .query-form input, .query-form button { padding: 7px 12px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 14px; background-color: #ffffff; } .query-form input:focus { border-color: #3b82f6; outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } #salaryTable { width: 100%; border-collapse: collapse; background: white; } #salaryTable th, #salaryTable td { border: 1px solid #e2e8f0; padding: 9px 12px; text-align: left; font-size: 14px; } #salaryTable tbody tr:hover { background-color: #f1f5f9; } #searchBtn:disabled { background-color: #e2e8f0; cursor: not-allowed; }:hover和:focus是 CSS 伪类选择器最常用的两个场景,注意它们和伪元素::before、::after的区别,后者是插入内容,前者是状态匹配。border-collapse: collapse去掉表格双线框,:disabled与按钮的disabled属性配合,防止请求期间重复点击。
4.3 JavaScript 用 fetch 请求后端并渲染表格
前端核心逻辑是监听submit事件,读取表单值,构造请求参数,用async/await等待返回,最后把 JSON 映射成表格行。
const form = document.getElementById('queryForm'); const tbody = document.querySelector('#salaryTable tbody'); const searchBtn = document.getElementById('searchBtn'); form.addEventListener('submit', async (event) => { event.preventDefault(); const empNo = document.getElementById('empNo').value.trim(); const month = document.getElementById('month').value; if (!month) { alert('请选择工资月份'); return; } const params = new URLSearchParams({ month }); if (empNo) { params.append('empNo', empNo); } searchBtn.disabled = true; searchBtn.textContent = '查询中...'; try { const response = await fetch('/api/querySalary?' + params.toString()); const result = await response.json(); if (result.code !== 200) { alert('查询失败:' + result.msg); return; } renderRows(result.data); } catch (error) { console.error('请求异常', error); } finally { searchBtn.disabled = false; searchBtn.textContent = '查询'; } }); function renderRows(rows) { tbody.innerHTML = rows.map(row => `<tr> <td>${row.emp_no}</td> <td>${row.emp_name}</td> <td>${row.dept_name}</td> <td>${row.before_tax}</td> <td>${row.tax}</td> <td>${row.actual_salary}</td> </tr>` ).join(''); }event.preventDefault()阻止表单整页刷新,这是单页交互的关键。URLSearchParams负责把参数编码,即使员工编号里带空格或中文也不会拼出非法 URL。finally里恢复按钮状态,保证请求报错后按钮不会被锁死。真实项目中还要处理result.data为null的情况,比如后端返回了空数组,此时应显示“暂无数据”,而不是一张空表格。
4.4 调试前后端联调时的常见问题
联调阶段最常遇到三类报错。
第一,Failed to fetch,一般是部署地址不对或后端没启动。打开浏览器开发者工具的 Network 面板,看请求是http://localhost:8080/...还是file:///...,如果是后者,把 HTML 放进webapp目录重新访问。
第二,JSON.parse报错,通常是返回的 Content-Type 不是application/json,或者后端在 JSON 前面输出了日志。处理方法是先看 Response 原文,再去掉多余的System.out.println。
第三,中文乱码。HTML 文件、Java 文件、数据库连接 URL 三处的编码都要统一为 UTF-8。JDBC 连接串加上useUnicode=true&characterEncoding=utf8,页面 meta 里的charset="utf-8"不能少。
如果你用 HBuilderX 内置浏览器直接打开这个 HTML,fetch请求会以file://协议发起,浏览器会拦截跨域请求。常见做法是让前端页面放在 Web 应用的webapp目录下,和后端同源部署,这样既不配置 CORS,也避免调试时的协议不一致问题。
5. 从开发到验收:接口自测、权限控制和工资数据不可抵赖
最后收尾不聊框架,聊三个上线前必须做的事:接口自测、登录会话统一校验、工资数据核对。这些细节决定了这个系统能不能从演示变成真正给财务使用。
5.1 用 curl 快速验证工资查询接口
Tomcat 启动后,在命令行执行:
curl "http://localhost:8080/salary/api/querySalary?month=2025-02&empNo=E001"返回体里code应为 200,data数组包含一条记录。如果返回 400,先检查month是否为yyyy-MM格式;如果连接不上,看 war 包是否已部署到 webapps 目录,以及/api/querySalary是否被 Filter 拦了登录。
5.2 登录校验放在 Filter,而不是每个 Servlet 重复写
工资数据不能裸奔。在@WebFilter("/api/*")里统一检查 session:
@WebFilter("/api/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object userId = request.getSession().getAttribute("userId"); if (userId == null) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return; } chain.doFilter(req, resp); } }配一个 Filter,所有/api/*接口都先过登录校验,比每个 Servlet 里复制粘贴判断语句可靠得多。前端登录后,浏览器会自动在同源请求里携带sessionId,不需要在前端存 token。
5.3 用一条核对 SQL 验证实发工资是否算错
写一条 SQL 把actual_salary和手工推导结果对比:
SELECT e.emp_no, r.before_tax, r.tax, r.actual_salary, (r.before_tax - r.tax - c.social_security - c.housing_fund - r.deduction) AS expected FROM salary_record r JOIN employee e ON r.emp_id = e.emp_id JOIN salary_config c ON r.emp_id = c.emp_id WHERE r.record_month = '2025-02';如果actual_salary和expected不一致,说明写入记录时用的个税版本、社保金额或扣款项与配置表不对齐。这个反查技巧在交接时特别好用,能让接手的人十分钟内判断出工资逻辑是否有漏洞,而不是翻几百行 Java 代码。
本文还有配套的精品资源,点击获取