简介:这是一套完整的JavaWeb人力资源管理系统源码,面向Java初学者与Web开发入门者,适用于课程设计、毕业设计及中小型企业HR模块快速原型开发。系统涵盖员工管理、招聘、考勤、薪资、培训、简历审核等核心业务功能,采用JSP+Servlet+JDBC经典三层架构,便于理解MVC模式与Web应用开发全流程。压缩包共268个文件,含71个Java源文件、71个编译后Class文件、40个JSP页面、38个XML配置文件(含web.xml与数据库映射)、22个Jar依赖库,辅以CSS、JS、图片及SQL建表脚本等资源,整体大小24MB,结构清晰、模块划分明确。已有4763人学习下载,源码可直接导入Eclipse或MyEclipse运行,配套SQL脚本支持MySQL快速部署,适合边学边练、调试分析与二次开发。
1. 项目概述:从零到一构建一个企业级JavaWeb人力资源管理系统
最近在整理硬盘,翻出来一个几年前做过的企业级人力资源管理系统(HRM)的源码。这个项目当时是为一家中型制造业公司定制的,从需求对接到最终上线,前前后后折腾了小半年。今天借着这个机会,把整个系统的设计思路、技术选型、核心模块的实现,还有那些年踩过的坑,系统地梳理一遍。如果你正在学习JavaWeb,或者正打算自己动手做一个类似的管理系统来练手、甚至作为毕业设计,那这篇内容应该能给你提供一条清晰的路径和不少实用的参考。
简单来说,这个系统就是一个基于B/S架构,使用Java技术栈开发的,用于企业人力资源数字化管理的Web应用。它要解决的核心问题,就是把传统企业里那些散落在Excel表格、纸质档案甚至员工脑子里的“人”的信息和流程,搬到线上来,实现规范化、流程化和数据化。核心用户包括HR部门、各部门经理以及普通员工,覆盖了从员工入职到离职的全生命周期管理。下面,我们就一层层剥开这个系统的“外壳”,看看里面到底是怎么运作的。
2. 系统整体架构与技术选型背后的思考
做一个管理系统,技术选型是第一步,也是最容易让人纠结的一步。当时市面上可选的技术非常多,我们的选择是基于几个核心考量:团队技术栈的熟悉度、社区的活跃度与生态成熟度、项目的可维护性以及未来的扩展性。最终敲定的是一套非常经典且稳健的JavaWeb“全家桶”。
2.1 后端技术栈:为什么是Spring Boot + MyBatis?
后端我们选择了Spring Boot 2.x作为核心框架。没选传统的SSH(Struts2 + Spring + Hibernate)或者SSM(SpringMVC + Spring + MyBatis)的零散整合方式,直接上Spring Boot,图的就是一个“开箱即用”。它内嵌了Tomcat,约定大于配置,能让我们快速搭建起项目骨架,把精力集中在业务逻辑上,而不是没完没了的XML配置。这对于中小型团队或者个人开发者来说,效率提升是巨大的。
数据持久层,我们放弃了重量级的Hibernate,选择了MyBatis。这里有个很重要的权衡:HR系统的业务表结构相对固定,但查询需求极其复杂多变。比如,HR可能需要根据“入职时间在2023年、部门为技术部、学历为本科及以上、并且上月绩效考核为A的员工”这样的组合条件进行筛选。Hibernate的HQL或者Criteria API在处理这种动态多条件查询时,代码会显得比较臃肿。而MyBatis的XML映射文件,可以非常灵活地编写动态SQL(通过<if>,<choose>等标签),SQL的可读性和可控性更强,也方便我们针对复杂查询进行SQL优化。毕竟,对于管理系统,复杂列表查询的性能是用户体验的关键。
注意:选择MyBatis意味着你需要自己编写和维护更多的SQL,这对开发者的SQL功底有一定要求。但换来的是极致的灵活性和在复杂业务场景下更优的性能表现。如果你的业务非常简单,且团队更熟悉JPA的面向对象思维,那么Spring Data JPA也是一个很好的选择。
2.2 前端技术栈:从JSP到前后端分离的演进
这个项目最初期用的是JSP + JSTL + Bootstrap这套经典组合。JSP直接在后端渲染页面,开发速度快,对于表单提交、页面跳转这类传统操作非常顺手。Bootstrap则能快速搭建出风格统一、响应式的管理界面,大大减少了前端UI的工作量。
但是,随着模块增多,尤其是涉及到更多动态交互(如无刷新数据加载、复杂表单验证)时,JSP混合Java代码和HTML的模式就显得有些力不从心,代码可维护性下降。因此,在项目后期重构部分模块时,我们引入了Vue.js作为前端框架,尝试了前后端分离的架构。后端Spring Boot只提供RESTful API,前端通过Vue进行组件化开发。这样做的好处是前后端职责清晰,并行开发效率高,前端用户体验也更流畅。不过,这也带来了额外的学习成本和部署复杂度。
对于初学者或毕业设计,我建议可以从JSP+Bootstrap开始,先把核心业务跑通,理解整个MVC的流程。有精力再去探索前后端分离,这是一个很自然的技术演进路径。
2.3 其他关键组件
- 权限控制:Apache Shiro。相比Spring Security,Shiro的API更直观易懂,配置也相对简单。对于HRM这种权限模型(用户-角色-权限)清晰的项目,Shiro完全够用,可以精细控制到菜单、按钮甚至API接口的访问。
- 数据库:MySQL 5.7。开源、稳定、生态完善,是中型管理系统的首选。我们使用了InnoDB存储引擎,并对核心表(如员工表、考勤记录表)建立了合适的索引。
- 项目构建与管理:Maven。依赖管理、项目构建、打包部署,一条龙服务。
- 缓存:Redis。用于缓存一些不常变动但访问频繁的数据,如部门树形结构、系统配置项、用户会话信息等,显著减轻数据库压力。
3. 核心功能模块设计与实现拆解
一个完整的HRMS,其核心是围绕“人”的“进、管、出”流程。我们的系统主要包含了以下六大模块,每个模块的设计都有其特定的业务考量。
3.1 组织架构与员工信息管理:系统的基石
这是整个系统的数据核心,设计的好坏直接影响到其他所有模块。
数据库设计要点: 员工表(employee)的设计至关重要。除了基本信息(工号、姓名、性别、身份证号、联系方式),关键字段包括:
dept_id(部门ID):关联部门表。position_id(岗位ID):关联岗位表。这里将部门和岗位分开,是因为一个部门下可能有多个岗位,且员工岗位调动可能不涉及部门变动。hire_date(入职日期)、status(员工状态:试用、正式、离职等)。status字段用于软删除和状态流转控制。- 敏感信息如身份证号、银行卡号,在数据库存储时应进行加密处理。
部门树形结构实现: 部门通常具有层级关系(如公司->事业部->部门->小组)。我们在部门表(department)中设计了一个parent_id字段指向父部门ID,顶级部门的parent_id为0或NULL。在Java后端,我们通过递归或一次查询后内存组装的方式,构建出完整的部门树形结构,提供给前端以树形控件展示。
实操心得:处理树形数据时,避免在循环中频繁查询数据库。最佳实践是一次性查询出所有部门数据到内存(List),然后通过Map(以id为key,部门对象为value)和递归算法在内存中构建树。这样可以减少数据库查询次数,性能提升非常明显。
3.2 招聘管理:流程化与状态驱动
招聘模块本质上是一个状态机驱动的流程管理系统。我们设计了一个招聘需求表(recruitment_request)和一个候选人表(candidate)。
候选人有一个stage字段,表示当前所处阶段,如:“简历筛选”、“初试”、“复试”、“HR面试”、“发放Offer”、“已入职”、“已淘汰”。每个状态的变更,都通过一个独立的操作(如“安排面试”、“录入面试结果”、“发送Offer”)来触发,并记录操作人和时间到流程日志表。
关键实现:我们使用了枚举(Enum)来定义所有可能的状态和操作,并在服务层编写状态转换的逻辑。例如,从“初试”到“复试”,必须满足“初试结果”为“通过”且由有权限的人执行操作。这种设计保证了流程的规范性和数据的可追溯性。
3.3 考勤与薪酬核算:规则与计算的复杂性
这是业务逻辑最复杂的模块之一。
考勤子系统:
- 规则定义:需要设计考勤规则表,定义上班时间、下班时间、迟到早退的容忍分钟数、是否弹性工作制等。
- 数据采集:与考勤机对接(通过TCP/IP或数据库直连),定时任务每天同步原始打卡记录到
attendance_record表。 - 异常处理:系统根据规则自动判断每条记录的异常状态(正常、迟到、早退、缺卡)。对于“缺卡”,需要提供补卡申请流程,由上级审批后更新状态。
- 统计报表:按月统计每个员工的出勤天数、各种异常次数,为薪酬核算提供依据。
薪酬核算子系统:
- 薪资结构:设计薪资项表(
salary_item),如“基本工资”、“岗位津贴”、“绩效奖金”、“社保个人部分”、“个税”等,并标识其为“加项”或“减项”。 - 薪资模板:为每个岗位或员工绑定一个薪资模板,模板定义了其包含的薪资项及计算规则(固定值、公式计算等)。
- 核算流程:每月在固定时间点(如5号),系统启动核算任务。任务会:
- 获取员工当月考勤统计结果。
- 获取员工的薪资模板。
- 根据模板中的公式(如
基本工资 - (迟到次数 * 罚款单价))动态计算每个薪资项。 - 调用税务计算接口(或内置规则)计算个人所得税。
- 汇总所有加项和减项,生成最终应发工资和实发工资,存入
payroll表。
- 审批与发放:生成的工资单需要经过财务或HR负责人审批确认后,才能锁定数据,并进入银行代发流程。
踩坑记录:薪资计算涉及钱,必须保证幂等性。即同一员工、同一月份,多次触发核算,结果必须一致且不会重复生成记录。我们的做法是在核算前检查
payroll表是否已存在该员工该月的记录,如果存在且已锁定,则跳过或报错。计算过程所有用到的变量(如考勤数据)必须在计算开始时一次性取好,避免在计算过程中数据被修改。
3.4 绩效管理:目标与评估的闭环
我们采用相对常见的KPI(关键绩效指标)模式。
- 周期与模板:定义绩效周期(季度、年度),并为不同岗位设置绩效评估模板,模板中包含若干考核项(KPI)及其权重、评分标准。
- 目标设定(Goal Setting):周期初,员工与上级共同在系统中设定本期的绩效目标(关联到具体的KPI项)。
- 自评与上级评:周期末,员工先进行自评,然后上级进行评分,并填写评语。系统支持多级评审(如上级的上级确认)。
- 结果校准与反馈:HR或部门负责人可能对评分进行校准(如强制分布)。最终结果与员工确认后,归档并作为奖金、晋升的依据。
技术实现关键点:这个模块涉及大量的表单动态渲染和审批流。我们使用了一个工作流引擎的轻量级实现(也可以自己基于状态机设计),来驱动“目标制定->提交->自评->上级评->校准->确认”这个流程。每个节点的操作权限和可见字段都需要精细控制。
3.5 培训与发展与员工自助
- 培训管理:管理培训课程、讲师、资源,员工可报名参加,系统记录培训结果。
- 员工自助平台:这是提升员工体验的关键。员工可以在此查看自己的档案、工资条、考勤明细,提交请假、报销、补卡等各类申请,查看公司公告。这极大地减少了HR的日常事务性工作。
请假流程示例:
- 员工填写请假单(类型、起止时间、事由)。
- 提交后,根据请假天数和公司规则,自动路由给相应审批人(如直接上级、部门总监)。
- 审批人收到待办任务(系统消息或邮件通知),进行审批。
- 审批通过后,状态更新,并自动同步到考勤预期数据中。
- 员工和HR均可查看流程状态。
4. 数据库设计与核心表结构解析
数据库设计是系统的骨架。这里列举几个最核心的表及其关联,让你感受一下设计思路。
表关系核心思想:员工(employee)是中心实体,通过外键与部门(department)、岗位(position)关联。各种业务单据(如请假单leave_application、报销单expense_report)都以employee_id关联到具体员工,并包含自己的业务状态和审批流信息。
部分核心表字段说明:
| 表名 | 关键字段 | 说明与设计理由 |
|---|---|---|
employee | id,emp_no(工号),name,dept_id,position_id,hire_date,status | emp_no唯一索引。status用于软删除(0离职/1在职)。 |
department | id,name,parent_id,leader_id(负责人) | parent_id构建树形结构。leader_id关联employee.id。 |
attendance_record | id,employee_id,check_date,check_in_time,check_out_time,status | 以(employee_id, check_date)建立联合唯一索引,防止重复打卡记录。 |
leave_application | id,employee_id,leave_type,start_time,end_time,duration,status,approver_id | status跟踪流程(提交中/审批中/已批准/已驳回)。duration单位可以是天或小时,需统一。 |
payroll | id,employee_id,pay_period(发薪月份),base_salary,bonus,tax,net_salary,is_confirmed | (employee_id, pay_period)唯一索引,确保幂等性。is_confirmed标记是否已审批锁定。 |
索引策略:
- 主键:所有表使用自增整数
id作为主键。 - 外键索引:在所有作为外键的字段上建立索引,如
employee.dept_id,leave_application.employee_id。 - 查询优化索引:在经常用于
WHERE条件、ORDER BY或JOIN的字段上建立索引。例如,attendance_record表的(employee_id, check_date)联合索引,对于查询某个员工某个月的考勤记录速度极快。 - 唯一索引:保证业务唯一性的字段组合,如
employee.emp_no,payroll.(employee_id, pay_period)。
5. 权限系统设计与Shiro整合实战
没有权限控制的管理系统就是“裸奔”。我们采用经典的RBAC(基于角色的访问控制)模型:用户 -> 角色 -> 权限。
数据库设计:
sys_user: 系统用户表,与employee表一对一或关联。sys_role: 角色表。sys_permission: 权限表,存储权限标识符(如employee:view,salary:approve)和对应的资源(菜单URL、按钮ID、API路径)。user_role,role_permission: 用户-角色、角色-权限的关联表。
与Shiro整合的关键步骤:
- 自定义Realm:这是Shiro的核心,你需要继承
AuthorizingRealm类,重写doGetAuthenticationInfo(认证)和doGetAuthorizationInfo(授权)方法。在这里,你从数据库中查询用户的角色和权限信息,并封装成Shiro能识别的对象(如SimpleAuthenticationInfo,SimpleAuthorizationInfo)。 - 权限标识符:我们将权限细化为三级:
模块:操作:资源,例如hrm:employee:view(查看员工)、hrm:salary:export(导出工资单)。这样在控制层方法或页面上,可以使用注解@RequiresPermissions("hrm:employee:view")进行声明式控制。 - 动态菜单:用户登录后,根据其拥有的权限标识符,过滤出有权限访问的菜单项,动态生成左侧导航菜单。这通常在前端或后端模板中完成。
- 按钮级控制:在JSP或Thymeleaf模板中,可以使用Shiro标签
<shiro:hasPermission name="hrm:salary:export">来控制一个导出按钮是否显示。
注意事项:Shiro的权限缓存非常重要。每次授权都查数据库是无法接受的。务必在Realm中配置合理的缓存策略(如使用Ehcache或Redis)。同时,当用户的权限被管理员修改后,需要手动清除该用户的授权缓存,否则会出现权限更新延迟的问题。
6. 典型业务场景代码实现剖析
光讲理论不够,我们看两个典型业务的后端代码实现片段。
6.1 复杂条件分页查询员工列表
这是HR最常用的功能。前端传递一堆可能为空的查询条件(姓名、部门、状态等),后端需要动态拼接SQL。
Controller层:
@RestController @RequestMapping("/api/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; @GetMapping("/list") public PageResult<EmployeeVO> listEmployees(EmployeeQueryDTO queryDTO) { // queryDTO 包含了 pageNum, pageSize, name, deptId, status 等字段 return employeeService.queryEmployeeList(queryDTO); } }Service层与Mapper动态SQL: 在EmployeeMapper.xml中,我们利用MyBatis的动态SQL功能:
<select id="selectEmployeeList" parameterType="EmployeeQueryDTO" resultMap="EmployeeResult"> SELECT e.*, d.name as dept_name, p.name as position_name FROM employee e LEFT JOIN department d ON e.dept_id = d.id LEFT JOIN position p ON e.position_id = p.id WHERE e.status != 'DELETED' <!-- 软删除过滤 --> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> <if test="status != null and status != ''"> AND e.status = #{status} </if> <!-- 可以添加更多条件,如入职时间范围 --> <if test="hireDateStart != null"> AND e.hire_date >= #{hireDateStart} </if> <if test="hireDateEnd != null"> AND e.hire_date <= #{hireDateEnd} </if> ORDER BY e.id DESC </select>PageHelper是一个常用的分页插件,在Service方法中调用PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize())后,下一次的Mapper查询就会自动进行物理分页。
6.2 发起请假申请与审批流转
这是一个典型的工作流场景。
1. 创建请假单(Controller):
@PostMapping("/leave/apply") public Result applyLeave(@RequestBody LeaveApplicationDTO dto, @CurrentUser SysUser user) { // 1. 数据校验(时间是否合法、假期余额是否足够等) // 2. 设置申请人ID、初始状态为“提交中” dto.setApplicantId(user.getEmployeeId()); dto.setStatus(LeaveStatus.SUBMITTED.getCode()); // 3. 调用服务层 leaveApplicationService.createApplication(dto); return Result.success("请假申请提交成功,等待审批"); }2. 服务层创建并启动流程(Service):
@Override @Transactional // 保证创建单据和启动流程在一个事务里 public void createApplication(LeaveApplicationDTO dto) { // 1. DTO 转 Entity,并保存到数据库 LeaveApplication entity = convertToEntity(dto); leaveApplicationMapper.insert(entity); // 2. 根据请假规则,确定审批人(可能是直接上级,或更高级别领导) Long approverId = findApproverByRule(entity.getApplicantId(), entity.getDuration()); // 3. 更新单据的当前审批人,状态改为“审批中” entity.setApproverId(approverId); entity.setStatus(LeaveStatus.UNDER_REVIEW.getCode()); updateById(entity); // 4. 发送通知(系统消息、邮件、钉钉/企业微信)给审批人 notificationService.sendLeaveApprovalNotification(approverId, entity.getId()); }3. 审批操作(Service):
@Override @Transactional public void approveApplication(Long applicationId, Long approverId, boolean approved, String comment) { LeaveApplication application = getById(applicationId); // 1. 校验:单据是否存在、状态是否为“审批中”、当前用户是否为审批人 validateApprovalPermission(application, approverId); // 2. 更新单据状态和审批意见 if (approved) { application.setStatus(LeaveStatus.APPROVED.getCode()); // 可能触发后续动作:扣减假期余额、同步至考勤预期 deductLeaveBalance(application.getApplicantId(), application.getLeaveType(), application.getDuration()); syncToAttendance(application); } else { application.setStatus(LeaveStatus.REJECTED.getCode()); } application.setApprovalComment(comment); application.setApprovalTime(new Date()); updateById(application); // 3. 通知申请人审批结果 notificationService.sendLeaveResultNotification(application.getApplicantId(), approved); }7. 部署、运维与性能优化经验谈
项目开发完,部署上线才是真正的开始。
7.1 部署实践
我们采用最经典的Linux + Nginx + Tomcat + MySQL架构。
- Linux服务器:使用CentOS 7,配置好防火墙、时区、Java环境。
- Nginx:作为反向代理和静态资源服务器。将前端的HTML/CSS/JS文件放在Nginx下,提高访问速度。同时,将
/api/路径的请求代理到后端的Tomcat。Nginx还能轻松配置负载均衡、SSL证书(HTTPS)、压缩传输等。 - Tomcat:使用Spring Boot内嵌的Tomcat,通过
java -jar your-application.jar --server.port=8080启动。生产环境建议使用nohup或配置为systemd服务,保证进程稳定运行。 - MySQL:单独部署在一台服务器上,定期进行备份(mysqldump + binlog)。务必修改默认端口和root密码,创建专属的、权限受限的数据库用户给应用使用。
7.2 性能优化点
- 数据库层面:
- 索引:如前所述,分析慢查询日志,为高频查询条件建立合适的索引。但也要避免过度索引,影响写性能。
- SQL优化:避免
SELECT *,只取需要的字段。多表关联时,注意关联字段是否有索引。复杂查询考虑是否能用子查询或JOIN优化。 - 连接池:使用HikariCP或Druid等高性能连接池,并合理配置最大最小连接数、超时时间。
- 应用层面:
- 缓存:使用Redis缓存热点数据。例如,部门树、数据字典(如请假类型)、用户权限信息。注意设置合理的过期时间,并处理好缓存与数据库的一致性。
- 异步处理:对于耗时的操作,如发送批量邮件、生成复杂的统计报表,不要阻塞主请求线程。可以使用Spring的
@Async注解,或者集成消息队列(如RabbitMQ)进行异步解耦。 - JVM调优:根据服务器内存大小,调整Spring Boot应用的JVM参数,如堆内存大小(
-Xms,-Xmx)、垃圾回收器等。
- 前端层面:
- 压缩JS、CSS、图片资源。
- 利用浏览器缓存。
- 对于数据量大的表格,采用分页或虚拟滚动,不要一次性加载所有数据。
7.3 常见问题与排查技巧
应用启动失败,端口被占用:
netstat -tlnp | grep 8080查找占用8080端口的进程。kill -9 <PID>结束该进程,或修改应用的server.port配置。
数据库连接池耗尽:
- 现象:应用日志中出现
Cannot get connection from pool或大量超时错误。 - 排查:首先检查数据库服务是否正常,网络是否通畅。然后,检查应用连接池配置是否合理(最大连接数是否太小)。最后,通过Druid监控或日志,分析是否有SQL执行过慢导致连接被长时间占用,或者代码中是否有忘记关闭数据库连接的情况(如未在finally块中关闭ResultSet, Statement, Connection)。
- 现象:应用日志中出现
前端页面访问慢:
- 打开浏览器开发者工具(F12)的Network面板,查看哪个资源加载耗时最长。
- 如果是API接口慢,查看后端应用日志和数据库慢查询日志。
- 如果是静态资源(如图片、JS)慢,检查Nginx配置和服务器带宽。
权限配置了但不生效:
- 检查Shiro的过滤器链配置是否正确,请求路径是否被正确拦截。
- 检查自定义Realm中,用户的权限信息是否被正确查询和返回。
- 最重要的一点:检查Shiro的权限缓存。修改用户权限后,尝试退出重新登录,或者手动清除该用户的授权缓存(
securityManager.logout()或直接操作缓存管理器)。
这个项目做下来,最大的体会是,开发一个管理系统,技术实现只是基础,更重要的是对业务逻辑的理解和抽象能力。如何将HR繁杂的线下流程,精准地转化为清晰的数据模型和状态机,是成败的关键。代码的健壮性、异常处理、日志记录,这些在练手时容易忽略的细节,在生产环境中却至关重要。建议你在实现每个功能时,多思考边界情况:如果员工离职了,他的历史数据怎么处理?如果审批人不在岗,流程如何流转?这些思考,会让你的系统从一个“玩具”升级为一个真正可用的“工具”。
本文还有配套的精品资源,点击获取