ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Web老项目实战:HR系统源码部署与底层链路解析

Java Web老项目实战:HR系统源码部署与底层链路解析 简介这是一套完整的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员工打卡、20HR并发导出工资条的压测。这不是教学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_applocalhost 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/ ← 补全缺失jarpoi-3.8.jar, mysql-connector-java-5.1.47.jar ├── treeData.ashx ← 作为URL访问入口 └── index.jsp ← 若无需新建简单跳转页关键配置修复解决中文乱码在WEB-INF/web.xml中添加过滤器若不存在则新建filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping解决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。启动验证访问http://localhost:8080/treeData.ashx?deptId1应返回JSON格式部门树数据如{id:1,name:总部,children:[{id:2,name:技术部},{id:3,name:人事部}]}3. 核心业务链路拆解从部门树加载到工资导出的四层调用栈3.1 部门树异步加载treeData.ashx→TreeDataServlet→DepartmentDAOtreeData.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); // 获取前端传参 ListDepartment children new DepartmentDAO().findChildren(Integer.parseInt(deptId)); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSONArray.fromObject(children).toString()); // 使用json-lib-2.4 } }DepartmentDAO.findChildren()实现关键点public ListDepartment findChildren(int parentId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListDepartment 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; }参数说明parentId0返回顶级部门如总部parentId1返回其子部门。前端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, ListSalary 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;filenamesalary_ new SimpleDateFormat(yyyyMMdd).format(new Date()) .xls); workbook.write(response.getOutputStream()); // ❌ 未关闭流高并发下OOM }必须修复的三点格式升级将HSSFWorkbook替换为XSSFWorkbook支持.xlsx需引入poi-ooxml-3.8.jar流关闭workbook.write()后必须response.getOutputStream().close()内存保护对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?deptId1返回空白页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参数?useSSLfalseserverTimezoneAsia/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;charsetUTF-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_id1的parent_id1findChildren()递归查询未加深度限制或环检测。解决在SQL中添加AND level 5防止无限递归在DAO方法中加入SetInteger 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自动拼接SQLpublic ListEmployee 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}; } // roleLevel3时whereClause为空查全部 String sql baseSql whereClause; // 使用PreparedStatement执行params传入 }注意currentDeptId参数名易误导实际在EmployeeAction中当roleLevel1时它从session.getAttribute(empId)获取当roleLevel2时从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-namePermissionFilter/filter-name filter-classcom.hr.filter.PermissionFilter/filter-class /filter filter-mapping filter-namePermissionFilter/filter-name url-pattern/EmployeeAction*/url-pattern /filter-mapping5.4 验证行级权限生效用curl模拟不同角色请求准备测试数据INSERT INTO employee (name, dept_id, role_level) VALUES (张三, 2, 1), -- 普通员工技术部 (李四, 2, 2), -- 技术部主管 (王五, 3, 2), -- 人事部主管 (赵六, 1, 3); -- HR管理员发起请求验证# 模拟张三role_level1试图查技术部所有人越权 curl http://localhost:8080/EmployeeAction?actionlistByDeptIddeptId2 \ -H Cookie: JSESSIONIDABC123; empId1; roleLevel1; deptId2 # 预期返回仅张三自己的记录因roleLevel1DAO忽略deptId参数只查empId1 # 模拟李四role_level2查技术部deptId2正确 curl http://localhost:8080/EmployeeAction?actionlistByDeptIddeptId2 \ -H Cookie: JSESSIONIDDEF456; empId2; roleLevel2; deptId2 # 预期返回张三、李四两条记录从那以后我每次接手老Java Web项目都会先用javap -c *.class | grep -A5 -B5 getParameter\|getAttribute扫一遍所有Action类确认权限参数来源——不是从URL拿就是从Session拿绝不会信前端传来的role_level。这套role_level行级控制虽土但胜在透明DAO层一眼看清数据边界不用翻Spring Security配置。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进