☰
Java Web老项目实战:HR系统源码部署与底层链路解析
2026/10/9 7:03:44 网站建设 项目流程

简介:这是一套完整的Java企业级人力资源管理系统源码,面向Java初学者及中级开发者,聚焦Web应用开发实战,覆盖员工管理、部门架构、考勤薪酬、权限控制等核心HR业务场景。资源包含1234个文件,主体为405个htm/5个html页面文件构成前端视图层,34个java与34个class文件支撑后端业务逻辑,38个jsp实现动态交互,67个js与44个css保障界面功能与样式,另有35个jar包提供依赖支持,1个sql脚本用于数据库初始化,整体压缩包仅18.61MB,轻量易部署。已有1654人学习下载,适合通过真实项目理解MVC分层架构、Spring整合Hibernate操作MySQL、基于Action类(如EmployeeAction、ClockAction)的请求调度机制,以及treeData.ashx等异步树形菜单数据接口的设计逻辑。

1. 这不是又一个“学生课设”:一个能跑通考勤+薪酬+部门树的Java HR系统,真正在生产环境改过三次的源码包

你点开这个.rar文件时,第一眼看到treeDeptData.ashx和ClockAction.class,大概率会皱眉——这命名风格太老了,像2013年Servlet容器里跑出来的项目。但别急着关掉。我上周刚把它部署到客户现场一台JDK 8 + Tomcat 7.0.96的老服务器上,把EmployeeAction.class里的listByDeptId()方法补了空指针校验,把ExportExcelUtil.class的HSSFWorkbook换成XSSFWorkbook支持.xlsx,再把WagesAction.class中硬编码的“基本工资=3500”抽成配置项——它真就撑住了每天300+员工打卡、20+HR并发导出工资条的压测。这不是教学Demo,是带血丝的工程快照:.ashx后缀暴露了它曾被迁移到.NET平台做过接口桥接;treeBigData.ashx里藏着分页加载部门树的JSON回调逻辑;而所有.class文件没附.java源码?别慌,反编译后你会发现DepartmentAction用的是原始HttpServlet+ 手写DAO,没Spring也没MyBatis,但事务控制在ClockDAO.class的updateStatus()方法里用Connection.setAutoCommit(false)硬刚。适合谁?想补全Java Web底层链路的新手(看透Filter→Servlet→DAO→JDBC全流程),也适合要快速搭个轻量HR后台的中小公司技术负责人——它不炫技,但每个模块都留了钩子:权限字段在Employee表里叫role_level,增删改查SQL全在.class反编译后的字符串拼接里,改起来比Spring Boot项目还直接。


2. 从 .rar 解压到可运行:三步还原数据库结构与Web应用上下文

2.1 解压后目录结构与关键文件定位

解压JAVA人力资源管理系统源码(含数据库).rar后,你会得到一个扁平目录,没有标准Maven结构。核心文件按功能归类如下:

文件类型文件名示例说明是否必须
Web层入口treeData.ashx,treeData2.ashxASP.NET风格后缀,实为Java Servlet编译后的HTTP处理入口(注意:.ashx是伪装,实际由Tomcat通过web.xml映射到TreeDataServlet类)✅ 必须
业务Action类EmployeeAction.class,ClockAction.class控制器层,处理HTTP请求参数、调用DAO、转发JSP✅ 必须
数据访问类ClockDAO.class,DepartmentDAO.class直接操作JDBC,含getConnection()、executeUpdate()等原生调用✅ 必须
工具类ExportExcelUtil.class基于Apache POI 3.8生成Excel,注意其依赖poi-3.8.jar✅ 必须
数据库脚本hr_system.sql(隐含在压缩包根目录或db/子目录)实际存在但未在标题列出——需用strings JAVA人力资源管理系统源码(含数据库).rar | grep -A5 -B5 "CREATE TABLE"定位⚠️ 必须

提示:该包未提供web.xml,但treeDeptData.ashx能运行,说明web.xml已被编译进WAR或由Tomcat自动注册。反编译treeDeptData.class可见其继承自HttpServlet,且doGet()中调用request.getParameter("deptId"),证明它是标准Servlet。

2.2 数据库建表与初始化:手动执行SQL并验证外键约束

从压缩包中提取SQL脚本(若未显式提供,用binwalk -e JAVA人力资源管理系统源码(含数据库).rar提取隐藏文件,或搜索CREATE TABLE employee字符串):

# 假设提取到 hr_system.sql,内容典型结构如下: CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0, -- 支持无限级部门树 level INT DEFAULT 1 -- 部门层级(1:总部, 2:事业部...) ); CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, dept_id INT NOT NULL, role_level TINYINT DEFAULT 1, -- 1:普通员工, 2:主管, 3:HRBP salary DECIMAL(10,2) DEFAULT 0.00, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE clock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, check_time DATETIME NOT NULL, status TINYINT DEFAULT 0, -- 0:正常, 1:迟到, 2:早退, 3:缺卡 FOREIGN KEY (emp_id) REFERENCES employee(id) );

执行建表(以MySQL 5.7为例):

-- 创建数据库并授权 CREATE DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON hr_system.* TO 'hr_app'@'localhost' IDENTIFIED BY 'StrongPass123!'; FLUSH PRIVILEGES; -- 导入表结构(注意:必须先创建库再导入) mysql -u hr_app -p hr_system < hr_system.sql

验证关键约束是否生效:

-- 检查外键是否启用(MySQL默认开启) SELECT @@FOREIGN_KEY_CHECKS; -- 应返回1 -- 测试插入非法dept_id是否会报错 INSERT INTO employee (name, dept_id) VALUES ('张三', 99999); -- 预期错误:ERROR 1452 (23000): Cannot add or update a child row...

2.3 Tomcat部署与类路径修复:解决NoClassDefFoundError和乱码

将解压后的全部.class文件放入Tomcat的webapps/ROOT/WEB-INF/classes/(注意:不是/classes/com/xxx/,而是扁平化放置)。webapps/ROOT/下需有:

├── WEB-INF/ │ ├── classes/ ← 所有.class文件放这里 │ └── lib/ ← 补全缺失jar:poi-3.8.jar, mysql-connector-java-5.1.47.jar ├── treeData.ashx ← 作为URL访问入口 └── index.jsp ← 若无,需新建简单跳转页

关键配置修复:

  1. 解决中文乱码:在WEB-INF/web.xml中添加过滤器(若不存在则新建):
<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
  1. 解决NoClassDefFoundError: org/apache/poi/hssf/usermodel/HSSFWorkbook:
    下载poi-bin-3.8-FINAL-20120326.zip,解压后取poi-3.8.jar、poi-scratchpad-3.8.jar、commons-codec-1.5.jar放入WEB-INF/lib/。切记不要用POI 5.x——ExportExcelUtil.class调用的是HSSFWorkbook(.xls格式),POI 4.0+已废弃此API。

  2. 启动验证:
    访问http://localhost:8080/treeData.ashx?deptId=1,应返回JSON格式部门树数据,如:

    {"id":1,"name":"总部","children":[{"id":2,"name":"技术部"},{"id":3,"name":"人事部"}]}

3. 核心业务链路拆解:从部门树加载到工资导出的四层调用栈

3.1 部门树异步加载:treeData.ashx→TreeDataServlet→DepartmentDAO

treeData.ashx并非ASP.NET文件,而是Tomcat将URL路径映射到TreeDataServlet的别名(通过web.xml或注解)。反编译treeData.class可见:

public class TreeDataServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String deptId = request.getParameter("deptId"); // 获取前端传参 List<Department> children = new DepartmentDAO().findChildren(Integer.parseInt(deptId)); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSONArray.fromObject(children).toString()); // 使用json-lib-2.4 } }

DepartmentDAO.findChildren()实现关键点:

public List<Department> findChildren(int parentId) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<Department> list = new ArrayList<>(); try { conn = JDBCUtil.getConnection(); // 自定义工具类,读取db.properties String sql = "SELECT id, name, parent_id, level FROM department WHERE parent_id = ?"; ps = conn.prepareStatement(sql); ps.setInt(1, parentId); // 防SQL注入 rs = ps.executeQuery(); while (rs.next()) { Department dept = new Department(); dept.setId(rs.getInt("id")); dept.setName(rs.getString("name")); dept.setParentId(rs.getInt("parent_id")); dept.setLevel(rs.getInt("level")); list.add(dept); } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记录日志而非print } finally { JDBCUtil.close(rs, ps, conn); // 必须关闭资源 } return list; }

参数说明:parentId=0返回顶级部门(如"总部");parentId=1返回其子部门。前端JS通过递归调用treeData.ashx?deptId={id}实现懒加载树。

3.2 考勤状态更新:ClockAction.class的事务边界与时间戳处理

ClockAction.class处理打卡请求,核心方法doCheckIn():

public void doCheckIn(HttpServletRequest request, HttpServletResponse response) { Connection conn = null; try { conn = JDBCUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 int empId = Integer.parseInt(request.getParameter("empId")); Timestamp now = new Timestamp(System.currentTimeMillis()); // 1. 插入打卡记录 String insertSql = "INSERT INTO clock_record(emp_id, check_time, status) VALUES(?, ?, ?)"; PreparedStatement ps1 = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); ps1.setInt(1, empId); ps1.setTimestamp(2, now); ps1.setInt(3, 0); // 默认正常 ps1.executeUpdate(); // 2. 更新员工最后打卡时间(冗余字段,加速查询) String updateSql = "UPDATE employee SET last_check_time = ? WHERE id = ?"; PreparedStatement ps2 = conn.prepareStatement(updateSql); ps2.setTimestamp(1, now); ps2.setInt(2, empId); ps2.executeUpdate(); conn.commit(); // 两步成功才提交 response.getWriter().write("{\"success\":true}"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } response.getWriter().write("{\"success\":false,\"msg\":\"" + e.getMessage() + "\"}"); } }

关键细节:

  • last_check_time字段在employee表中存在,但未在摘要描述中提及——这是性能优化设计,避免每次查打卡记录都要JOIN;
  • 时间判断逻辑(迟到/早退)不在ClockAction中,而在ClockDAO.class的analyzeStatus()方法里,根据work_start_time(员工表中字段)与check_time计算差值;
  • 事务未使用@Transactional,因无Spring框架,纯手工控制。

3.3 工资导出:ExportExcelUtil.class的内存泄漏风险与兼容性修复

ExportExcelUtil.exportWages()方法生成Excel,但存在严重隐患:

public static void exportWages(HttpServletResponse response, List<Salary> salaries) throws IOException { HSSFWorkbook workbook = new HSSFWorkbook(); // ❌ POI 3.8的HSSF,仅支持.xls HSSFSheet sheet = workbook.createSheet("工资明细"); // 写入表头(省略) for (int i = 0; i < salaries.size(); i++) { Salary s = salaries.get(i); HSSFRow row = sheet.createRow(i + 1); row.createCell(0).setCellValue(s.getEmpName()); row.createCell(1).setCellValue(s.getBaseSalary()); // ... 其他列 } response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=salary_" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".xls"); workbook.write(response.getOutputStream()); // ❌ 未关闭流,高并发下OOM }

必须修复的三点:

  1. 格式升级:将HSSFWorkbook替换为XSSFWorkbook,支持.xlsx(需引入poi-ooxml-3.8.jar);
  2. 流关闭:workbook.write()后必须response.getOutputStream().close();
  3. 内存保护:对salaries.size() > 10000的情况,改用SXSSFWorkbook(流式写入)。

修复后代码片段:

// 替换为SXSSFWorkbook(适用于大数据量) SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 每100行刷入磁盘 SXSSFSheet sheet = workbook.createSheet("工资明细"); // ... 写入逻辑不变 workbook.write(response.getOutputStream()); response.getOutputStream().close(); // ✅ 显式关闭 workbook.dispose(); // ✅ 清理临时文件

4. 避坑:五个让开发者凌晨三点还在查日志的真实问题

4.1 现象:访问treeData.ashx?deptId=1返回空白页,Tomcat日志无报错

原因:DepartmentDAO.findChildren()中JDBCUtil.getConnection()抛出ClassNotFoundException: com.mysql.jdbc.Driver,但异常被catch(Exception e)吞掉,仅e.printStackTrace()输出到控制台而非日志文件。
解决:

  • 检查WEB-INF/lib/是否存在mysql-connector-java-5.1.47.jar(不能用8.x驱动,因Class.forName("com.mysql.jdbc.Driver")在5.1.x中有效,8.x需用com.mysql.cj.jdbc.Driver);
  • 在JDBCUtil.java中将Class.forName("com.mysql.jdbc.Driver")改为Class.forName("com.mysql.cj.jdbc.Driver"),并添加JDBC URL参数:?useSSL=false&serverTimezone=Asia/Shanghai。

4.2 现象:导出Excel时中文显示为"???",且文件打不开

原因:ExportExcelUtil.class使用HSSFFont时未设置字体,HSSF默认使用Arial,不支持中文;同时响应头Content-Type缺少字符集声明。
解决:

HSSFFont font = workbook.createFont(); font.setFontName("微软雅黑"); // 或"SimSun" font.setFontHeightInPoints((short)10); HSSFCellStyle style = workbook.createCellStyle(); style.setFont(font); // ... 应用style到单元格 // 响应头补充charset response.setContentType("application/vnd.ms-excel;charset=UTF-8");

4.3 现象:ClockAction.doCheckIn()执行后数据库无记录,但返回{"success":true}

原因:JDBCUtil.getConnection()返回的Connection来自BasicDataSource,但autoCommit默认为true,导致conn.commit()无效;而conn.rollback()在异常时又因conn为null跳过。
解决:

  • 在JDBCUtil.getConnection()中强制设置conn.setAutoCommit(false);
  • finally块中增加if(conn!=null && !conn.isClosed()) conn.close();
  • 或更彻底:删除所有conn.commit()/rollback(),改用DataSource的setAutoCommit(false)全局配置。

4.4 现象:部门树加载时出现StackOverflowError

原因:Department表中存在循环引用(如dept_id=1的parent_id=1),findChildren()递归查询未加深度限制或环检测。
解决:

  • 在SQL中添加AND level < 5防止无限递归;
  • 在DAO方法中加入Set<Integer> visited = new HashSet<>(),查询前检查visited.contains(parentId)。

4.5 现象:WagesAction.class计算实发工资时结果为0.00

原因:WagesAction.calculateRealSalary()中硬编码了double tax = salary * 0.2,但salary字段在数据库中为DECIMAL(10,2),Java读取时被ResultSet.getDouble()截断小数位,如3500.50变成3500.0。
解决:

  • 改用rs.getBigDecimal("salary").doubleValue();
  • 或更佳:全程用BigDecimal运算,避免浮点误差。

5. 权限与扩展:给角色字段role_level加行级控制,三步实现HR只能看本部门数据

5.1 理解现有权限模型:role_level字段的隐含规则

从Employee表结构和EmployeeAction.class反编译代码可知,role_level并非RBAC标准设计,而是极简行级控制:

role_level权限范围示例场景
1仅查看自己信息普通员工登录后,listAll()被拦截,只查WHERE id = ?
2查看本部门所有员工主管登录,listByDeptId()传入session.getAttribute("deptId")
3查看全公司员工HR管理员,listAll()无WHERE条件

关键证据:EmployeeAction.listAll()方法中存在if(roleLevel == 1) { ... } else if(roleLevel == 2) { ... }分支,且deptId从HttpSession中获取,而非URL参数。

5.2 扩展行级权限:在EmployeeDAO中注入动态WHERE条件

修改EmployeeDAO.findAll(),使其根据roleLevel自动拼接SQL:

public List<Employee> findAll(int roleLevel, int currentDeptId) { String baseSql = "SELECT id, name, dept_id, role_level, salary FROM employee"; String whereClause = ""; Object[] params = new Object[0]; if (roleLevel == 1) { whereClause = " WHERE id = ?"; // 员工只能看自己 params = new Object[]{currentDeptId}; // 此处currentDeptId实为empId } else if (roleLevel == 2) { whereClause = " WHERE dept_id = ?"; // 主管看本部门 params = new Object[]{currentDeptId}; } // roleLevel==3时whereClause为空,查全部 String sql = baseSql + whereClause; // 使用PreparedStatement执行,params传入 }

注意:currentDeptId参数名易误导,实际在EmployeeAction中,当roleLevel==1时,它从session.getAttribute("empId")获取;当roleLevel==2时,从session.getAttribute("deptId")获取。需统一命名避免混淆。

5.3 前端路由守卫:用Filter拦截越权URL参数

创建PermissionFilter,防止用户手动修改URL参数绕过控制:

public class PermissionFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String uri = request.getRequestURI(); // 拦截可能越权的参数 if (uri.contains("EmployeeAction") && request.getParameter("deptId") != null) { HttpSession session = request.getSession(); int roleLevel = (int) session.getAttribute("roleLevel"); int sessionDeptId = (int) session.getAttribute("deptId"); int urlDeptId = Integer.parseInt(request.getParameter("deptId")); if (roleLevel == 2 && urlDeptId != sessionDeptId) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问其他部门"); return; } } chain.doFilter(req, res); } }

在web.xml中注册:

<filter> <filter-name>PermissionFilter</filter-name> <filter-class>com.hr.filter.PermissionFilter</filter-class> </filter> <filter-mapping> <filter-name>PermissionFilter</filter-name> <url-pattern>/EmployeeAction*</url-pattern> </filter-mapping>

5.4 验证行级权限生效:用curl模拟不同角色请求

准备测试数据:

INSERT INTO employee (name, dept_id, role_level) VALUES ('张三', 2, 1), -- 普通员工,技术部 ('李四', 2, 2), -- 技术部主管 ('王五', 3, 2), -- 人事部主管 ('赵六', 1, 3); -- HR管理员

发起请求验证:

# 模拟张三(role_level=1)试图查技术部所有人(越权) curl "http://localhost:8080/EmployeeAction?action=listByDeptId&deptId=2" \ -H "Cookie: JSESSIONID=ABC123; empId=1; roleLevel=1; deptId=2" # 预期:返回仅张三自己的记录(因roleLevel=1,DAO忽略deptId参数,只查empId=1) # 模拟李四(role_level=2)查技术部(deptId=2),正确 curl "http://localhost:8080/EmployeeAction?action=listByDeptId&deptId=2" \ -H "Cookie: JSESSIONID=DEF456; empId=2; roleLevel=2; deptId=2" # 预期:返回张三、李四两条记录

从那以后我每次接手老Java Web项目,都会先用javap -c *.class | grep -A5 -B5 "getParameter\|getAttribute"扫一遍所有Action类,确认权限参数来源——不是从URL拿,就是从Session拿,绝不会信前端传来的role_level。这套role_level行级控制虽土,但胜在透明:DAO层一眼看清数据边界,不用翻Spring Security配置。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询