尧图精选

中小企业人事系统实战:SpringBoot 2.7搭建轻量级HR管理系统

🕒 发布时间:2026/9/11 23:12:41 📁 来源:尧图网络
简介本资源是一套基于Spring Boot与Vue技术栈开发的中小企业人事管理系统完整源码项目面向计算机专业本科生、Java后端及全栈初学者适用于课程设计、毕业设计与中小型企业轻量级HR系统原型开发。项目采用B/S架构后端以JavaSpring BootMyBatisPlus构建前端使用VueElementUIAjax实现响应式界面配套MySQL数据库脚本与详细文档含绪论、技术选型、系统分析、功能设计及数据库ER图等覆盖用户管理、素材上传等核心模块。压缩包共407个文件包含114个Java业务逻辑类、44个Vue组件、161个SVG图标资源、15个XML配置及SQL映射文件辅以JS、YML、CSS等支撑文件整体8.73MB结构清晰、模块解耦度高便于学习分层架构与前后端联调。目前已有111人下载学习可直接导入IDE运行附带install/run/build三阶段批处理脚本显著降低环境搭建门槛。1. 为什么中小企业人事管理系统不能直接套用大厂SaaSJavaSpringBoot是更可控的落地选择很多刚接手HR信息化建设的中小企业技术负责人第一反应是“买个钉钉或企业微信的HR模块就行”。但实际跑起来才发现考勤规则要按本地工时制定制、社保公积金基数每年要手动调、员工异动审批流得嵌入公司红头文件编号逻辑、离职交接清单必须关联IT资产回收状态——这些细节通用SaaS要么不开放配置要么要加价买定制包。而用JavaSpringBoot从零搭一套轻量级人事系统反而能在3周内交付可运行版本核心模块员工档案、组织架构、入转调离、考勤统计全部自主可控数据库字段、校验规则、导出Excel模板全在自己代码里连SQL慢查询都能直接加索引优化。本文面向两类人一是计算机专业应届生做毕设需要能通过答辩、有完整源码结构、能演示增删改查权限控制的真实系统二是中小企业的兼职IT或外包开发者手头只有1台4核8G服务器要求系统启动快、部署简单、后续能自己加字段修Bug。所有代码基于SpringBoot 2.7.x兼容JDK 8避开SpringBoot 3.x的Jakarta EE迁移坑关键配置项和数据库建表语句全部给出不是教你怎么学SpringBoot而是告诉你怎么把这套人事系统真正跑起来。2. 用SpringBoot快速搭建人事系统骨架从Maven依赖到基础CRUD接口2.1 选型依据为什么不用SpringBoot 3.x而坚持2.7.x当前2024年中中小企业开发环境仍以JDK 8为主流尤其老旧ERP或财务系统尚未升级。SpringBoot 3.x强制要求JDK 17且将Servlet API从javax.迁移到jakarta.导致MyBatis-Plus 3.5.x、PageHelper等常用分页插件需同步升级而这些插件在中小项目中往往绑定着大量自定义SQL。实测发现将现有MyBatis XML映射文件中的select标签内#{}参数引用迁移到jakarta命名空间平均每个Mapper需修改12处以上且部分动态SQL的if testxxx ! null逻辑因EL表达式解析器变更出现空指针。因此本系统采用SpringBoot 2.7.182024年6月最新维护版配套MyBatis-Plus 3.5.3.1确保与JDK 8完全兼容同时保留Spring Security 5.7.x的XML配置兼容性——这对需要对接老OA单点登录的客户至关重要。提示若你已用JDK 17可将spring-boot-starter-parent版本改为3.1.12但必须同步升级MyBatis-Plus至3.5.5并将所有javax.annotation注解替换为jakarta.annotation否则TableField等注解会失效。2.2 Maven核心依赖配置与作用说明以下为pom.xml中必须声明的依赖块删除任何一项都会导致启动失败或功能缺失dependencies !-- SpringBoot Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency !-- MyBatis-Plus ORM框架 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL驱动适配8.0 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope version8.0.33/version /dependency !-- Lombok简化POJO -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 阿里Druid连接池比HikariCP更易监控 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.18/version /dependency !-- Spring Security基础认证 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId version2.7.18/version /dependency !-- Apache POI Excel读写 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version4.1.2/version /dependency /dependencies关键参数说明druid-spring-boot-starter提供/druid/login.html监控页面可实时查看SQL执行耗时、连接池活跃数避免线上出现“连接数打满”问题poi-ooxml 4.1.2是最后一个支持JDK 8的稳定版高版本POI在JDK 8下会抛出java.lang.NoClassDefFoundError: org/apache/commons/collections4/map/LRUMap所有依赖版本号必须严格匹配SpringBoot 2.7.x与MyBatis-Plus 3.5.x存在强绑定关系混用3.4.x会导致LambdaQueryWrapper构造器报NoSuchMethodError。2.3 员工档案模块的最小可行CRUD实现以Employee实体为例其数据库表需包含基础字段及业务强相关字段CREATE TABLE employee ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(50) NOT NULL COMMENT 姓名, id_card char(18) DEFAULT NULL COMMENT 身份证号, phone varchar(11) NOT NULL COMMENT 手机号, entry_date date NOT NULL COMMENT 入职日期, dept_id bigint NOT NULL COMMENT 部门ID, position varchar(30) DEFAULT NULL COMMENT 职位, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-在职2-离职3-试用期, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_dept_status (dept_id,status) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工档案表;对应Java实体类使用Lombok减少样板代码import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; Data EqualsAndHashCode(callSuper false) TableName(employee) public class Employee { TableId(type IdType.AUTO) private Long id; private String name; TableField(id_card) private String idCard; TableField(phone) private String phone; TableField(entry_date) private LocalDate entryDate; // 使用LocalDate而非Date避免时区问题 TableField(dept_id) private Long deptId; private String position; private Integer status; // 状态枚举值非字符串存储 TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }Service层实现分页查询含状态过滤Service public class EmployeeServiceImpl extends ServiceImplEmployeeMapper, Employee implements EmployeeService { Override public PageEmployee pageByStatus(PageEmployee page, Integer status) { // 构造查询条件只查在职/离职员工避免全表扫描 QueryWrapperEmployee wrapper new QueryWrapper(); if (status ! null status 0) { wrapper.eq(status, status); } // 按入职日期倒序新员工优先显示 wrapper.orderByDesc(entry_date); return this.page(page, wrapper); } }Controller暴露REST接口RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public ResultPageEmployee page( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Integer status) { PageEmployee page new Page(current, size); PageEmployee result employeeService.pageByStatus(page, status); return Result.success(result); // Result为统一响应包装类 } }逻辑说明TableField(fill FieldFill.INSERT_UPDATE)自动填充updateTime避免手动赋值出错PageEmployee由MyBatis-Plus内置分页插件生成无需手写LIMIT语句且自动识别MySQL方言Result.success()封装了code、msg、data三字段前端可统一处理错误码如code401表示未登录RequestParam(required false)允许前端不传status参数此时查询全部员工符合管理后台筛选需求。3. 实现真实业务场景组织架构树形展示与入转调离审批流3.1 部门树形结构的递归查询与前端渲染中小企业常有“总部-区域分公司-项目部”三级架构且需支持部门合并、拆分。传统LEFT JOIN自关联查询在部门层级超过5层时性能急剧下降。本系统采用两次查询法先查出所有部门再在Java内存中构建树既避免N1查询又规避了MySQL 5.7对WITH RECURSIVE的支持限制。数据库表设计departmentCREATE TABLE department ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, parent_id bigint DEFAULT 0 COMMENT 上级部门ID0表示根节点, sort_order int NOT NULL DEFAULT 0 COMMENT 同级排序序号, status tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;Service层构建树形结构Service public class DepartmentService extends ServiceImplDepartmentMapper, Department { public ListDepartmentTreeVO buildTree() { // 1. 一次性查出所有有效部门 ListDepartment allDepts this.list(new QueryWrapperDepartment().eq(status, 1)); // 2. 转为Map便于O(1)查找父节点 MapLong, Department deptMap allDepts.stream() .collect(Collectors.toMap(Department::getId, dept - dept)); // 3. 找出所有根节点parent_id0 ListDepartmentTreeVO roots new ArrayList(); for (Department dept : allDepts) { if (dept.getParentId().equals(0L)) { roots.add(convertToVO(dept, deptMap)); } } return roots; } private DepartmentTreeVO convertToVO(Department dept, MapLong, Department deptMap) { DepartmentTreeVO vo new DepartmentTreeVO(); vo.setId(dept.getId()); vo.setName(dept.getName()); vo.setSortOrder(dept.getSortOrder()); // 递归查找子部门 ListDepartment children allDepts.stream() .filter(d - d.getParentId().equals(dept.getId())) .sorted(Comparator.comparingInt(Department::getSortOrder)) .collect(Collectors.toList()); vo.setChildren(children.stream() .map(child - convertToVO(child, deptMap)) .collect(Collectors.toList())); return vo; } }前端Vue组件接收JSON数据后用el-tree直接渲染el-tree :datadeptTree node-keyid :props{ label: name, children: children } default-expand-all :expand-on-click-nodefalse /注意convertToVO方法中children的排序必须在递归前完成否则子节点顺序会乱。sort_order字段用于控制同级部门显示顺序比单纯按ID排序更符合管理习惯。3.2 入转调离审批流的状态机设计人事异动不是简单状态切换而是带审批节点的流程。例如“调岗”需经过员工提交→部门负责人审批→HR复核→系统自动更新岗位信息。本系统不引入Activiti等重型工作流引擎而是用状态操作类型双字段控制降低复杂度ALTER TABLE employee ADD COLUMN approval_status tinyint DEFAULT 0 COMMENT 审批状态0-无审批1-待审批2-已通过3-已驳回, ADD COLUMN approval_type tinyint DEFAULT 0 COMMENT 审批类型1-入职2-转正3-调岗4-离职;审批操作ControllerPostMapping(/approval/{id}) public ResultString handleApproval( PathVariable Long id, RequestParam Integer approvalType, RequestParam Integer status, // 1通过3驳回 RequestBody ApprovalComment comment) { Employee employee employeeService.getById(id); if (employee null) { return Result.fail(员工不存在); } // 校验当前状态是否允许审批如离职申请已被驳回则不能再批 if (!canApprove(employee, approvalType, status)) { return Result.fail(当前状态不允许此操作); } // 更新员工状态与审批状态 employee.setStatus(getNewStatus(approvalType, status)); employee.setApprovalStatus(status); employee.setUpdateTime(LocalDateTime.now()); // 记录审批意见单独表存储便于审计 ApprovalRecord record new ApprovalRecord(); record.setEmployeeId(id); record.setApprovalType(approvalType); record.setOperatorId(getCurrentUserId()); // 从SecurityContext获取当前登录人 record.setComment(comment.getContent()); record.setCreateTime(LocalDateTime.now()); approvalRecordService.save(record); employeeService.updateById(employee); return Result.success(审批成功); } private boolean canApprove(Employee emp, Integer type, Integer status) { // 规则调岗审批中员工状态必须为在职1且审批状态为待审批1 if (type 3 emp.getStatus() 1 emp.getApprovalStatus() 1) { return true; } // 其他类型类似校验... return false; }关键设计点approval_status与status分离避免审批中状态被误读如审批中员工仍是“在职”但不能发起新调岗ApprovalRecord独立建表字段含operator_id、create_time、comment满足《劳动合同法》第50条关于“书面记录”的合规要求canApprove()方法集中校验业务规则后续新增“试用期转正”审批时只需扩展此方法无需改Controller。4. 毕设与生产环境的双重适配权限控制、Excel导入导出与部署优化4.1 基于角色的细粒度权限控制RBAC实现中小企业不需要OAuth2或JWT的复杂鉴权但必须区分“HR专员”“部门经理”“CEO”三类角色。本系统采用Spring Security 自定义Filter的方式避免XML配置的冗余Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() // 毕设项目可关闭CSRF生产环境需启用 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz - authz .requestMatchers(/api/login, /api/public/**).permitAll() .requestMatchers(/api/employee/**).hasAnyRole(HR, MANAGER) .requestMatchers(/api/dept/**).hasRole(HR) .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .formLogin().disable() .httpBasic().disable(); return http.build(); } }角色与菜单权限绑定表role_menurole_idmenu_codepermission1emp_listREAD1emp_editWRITE2emp_listREAD2emp_editNONE前端按钮级权限控制Vue指令!-- 只有HR角色才能看到编辑按钮 -- el-button v-has-permissionemp_edit:WRITE clickeditEmployee编辑/el-button // 自定义指令 Vue.directive(has-permission, { inserted(el, binding) { const { value } binding; const permissions store.state.user.permissions; // 从Vuex获取用户权限列表 if (!permissions.includes(value)) { el.style.display none; } } });提示v-has-permission指令中value格式为菜单编码:操作如emp_edit:WRITE避免前端硬编码v-ifuser.role HR方便后期权限调整。4.2 Excel导入导出的防错与性能优化人事系统高频操作是批量导入员工信息。直接用POI逐行读取1000行Excel在JDK 8下会触发Full GC。本系统采用事件模型SXSSF 字段校验前置PostMapping(/import) public ResultString importEmployees(RequestParam MultipartFile file) { try { // 1. 用SXSSFWorkbook流式读取内存占用恒定 SXSSFWorkbook workbook new SXSSFWorkbook(new XSSFWorkbook(file.getInputStream())); Sheet sheet workbook.getSheetAt(0); // 2. 提取表头建立列名到字段的映射 Row headerRow sheet.getRow(0); MapString, Integer columnMap new HashMap(); for (int i 0; i headerRow.getLastCellNum(); i) { String cellValue getCellValue(headerRow.getCell(i)); columnMap.put(cellValue, i); } // 3. 逐行解析校验必填字段 ListEmployee employees new ArrayList(); for (int rowNum 1; rowNum sheet.getLastRowNum(); rowNum) { Row row sheet.getRow(rowNum); if (row null) continue; Employee emp new Employee(); emp.setName(getCellValue(row.getCell(columnMap.get(姓名)))); emp.setPhone(getCellValue(row.getCell(columnMap.get(手机号)))); emp.setIdCard(getCellValue(row.getCell(columnMap.get(身份证号)))); // 关键校验手机号格式、身份证号长度 if (!PhoneUtil.isValid(emp.getPhone())) { return Result.fail(第 (rowNum 1) 行手机号格式错误); } if (emp.getIdCard() ! null emp.getIdCard().length() ! 18) { return Result.fail(第 (rowNum 1) 行身份证号长度应为18位); } employees.add(emp); } // 4. 批量插入MyBatis-Plus内置batchInsert employeeService.saveBatch(employees); workbook.dispose(); // 必须调用释放临时文件 return Result.success(成功导入 employees.size() 条员工数据); } catch (Exception e) { return Result.fail(导入失败 e.getMessage()); } }getCellValue()工具方法处理Excel单元格类型private static String getCellValue(Cell cell) { if (cell null) return ; switch (cell.getCellType()) { case STRING: return cell.getStringCellValue().trim(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { return DateUtil.getJavaDate(cell.getNumericCellValue()).toString(); } else { return String.valueOf((long) cell.getNumericCellValue()); } default: return ; } }4.3 生产部署的3个关键参数调优单台4核8G服务器部署时需调整以下JVM与SpringBoot参数避免OOM参数推荐值说明-Xms/-Xmx2g初始与最大堆内存设为2GB留2GB给操作系统及线程栈-XX:MaxMetaspaceSize256m防止动态代理类过多导致元空间溢出spring.servlet.context-path/hr避免根路径冲突Nginx反向代理时更安全server.tomcat.max-connections500Tomcat最大连接数匹配Druid连接池的maxActive20application-prod.yml配置示例server: port: 8080 servlet: context-path: /hr spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 # 必须≤max-connections的1/25防止连接池耗尽 stat-view-servlet: enabled: true login-username: admin login-password: hr123456 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 生产环境关闭验证部署是否成功访问http://your-server:8080/hr/druid/login.html输入账号密码进入Druid监控页查看DataSource面板中ActiveCount峰值不超过20执行jstat -gc pid观察S0C/S1C总和稳定在200MB左右无频繁GC。5. 毕设答辩与上线前的5个硬核检查点5.1 数据库字段命名与注释的合规性检查中小企业人事系统涉及《个人信息保护法》数据库字段必须明确标注用途。以下为必须添加注释的字段清单MySQL 5.7-- 员工表 ALTER TABLE employee MODIFY COLUMN id_card char(18) COMMENT 身份证号加密存储仅HR可见, MODIFY COLUMN phone varchar(11) COMMENT 手机号脱敏显示138****1234; -- 薪资表独立schema权限隔离 CREATE TABLE salary ( id bigint NOT NULL, employee_id bigint NOT NULL COMMENT 关联员工ID, base_salary decimal(10,2) COMMENT 基本工资, bonus decimal(10,2) COMMENT 绩效奖金, created_by bigint COMMENT 创建人IDHR专员, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT薪资明细表敏感数据需单独授权访问;提示答辩时评委常问“如何保护员工隐私”回答要点是①身份证号用AES-128加密存库②手机号在Controller层用PhoneUtil.mask(phone)脱敏③薪资表不与员工表JOIN前端请求/salary/{id}需二次权限校验。5.2 毕设代码结构的可答辩性增强评审老师关注代码是否体现“工程能力”而非仅功能实现。建议在src/main/java下按以下包结构组织com.example.hr ├── config // WebMvcConfigurer、Druid配置 ├── controller // REST接口命名含模块名如EmployeeController ├── entity // TableName实体不含业务逻辑 ├── mapper // MyBatis-Plus Mapper接口 ├── service // 接口ImplImpl类名含ServiceImpl后缀 ├── dto // 数据传输对象如EmployeeImportDTO ├── vo // 视图对象如DepartmentTreeVO ├── exception // 自定义异常如BusinessException └── util // 工具类PhoneUtil、AesUtil关键加分点dto包中EmployeeImportDTO用NotBlank、Pattern注解声明校验规则Controller层加Valid触发exception包中GlobalExceptionHandler捕获MethodArgumentNotValidException统一返回Result.fail(姓名不能为空)util包中AesUtil实现AES加密密钥从application.yml读取避免硬编码。5.3 一个能现场演示的“高光功能”离职交接清单自动生成毕设答辩时演示一个自动化程度高的功能最能体现价值。离职交接清单需关联多个系统状态本系统用定时任务状态监听实现Component public class ResignationHandler { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void generateHandoverList() { // 查找状态为“待离职”且离职日期≤今天的所有员工 ListEmployee pendingResigns employeeService.list(new QueryWrapperEmployee() .eq(status, 4) // 4-待离职 .le(resign_date, LocalDate.now())); for (Employee emp : pendingResigns) { // 1. 生成交接清单PDF用iText7 HandoverPdfGenerator.generate(emp.getId()); // 2. 发送邮件通知直属领导用JavaMailSender mailService.sendHandoverNotice(emp.getManagerEmail(), emp.getName()); // 3. 更新员工状态为“离职中” emp.setStatus(5); // 5-离职中 employeeService.updateById(emp); } } }演示话术“点击‘发起离职’后系统自动生成含IT资产、办公用品、客户资料三类交接项的PDF清单并邮件发送给部门负责人。您看这是昨晚2点生成的样例打开PDF每项都带二维码扫码可确认归还状态——这解决了中小企业HR手工催办的痛点。”5.4 面试官最爱问的3个SpringBoot问题及答案结合热词“springboot面试题”预判答辩/面试高频问题问题回答要点代码佐证SpringBoot自动配置原理是什么EnableAutoConfiguration导入spring.factories中声明的AutoConfiguration类通过ConditionalOnClass等条件注解判断是否生效。本系统DruidDataSourceAutoConfigure生效因pom.xml引入了druid-spring-boot-starter。spring-boot-autoconfigure-2.7.18.jar!/META-INF/spring.factories中含com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigureMyBatis-Plus的LambdaQueryWrapper为什么能避免字段名写错编译期检查eq(User::getName, 张三)中User::getName是方法引用IDE可跳转若字段改名则编译失败。而eq(name, 张三)是字符串运行时才报错。QueryWrapperEmployee w new QueryWrapper(); w.eq(name, 张三); // 易错vsLambdaQueryWrapperEmployee w new LambdaQueryWrapper(); w.eq(Employee::getName, 张三); // 安全如何解决SpringBoot项目启动慢①禁用无用starter如spring-boot-starter-cache②SpringBootApplication(exclude {DataSourceAutoConfiguration.class})排除自动配置③本系统用MapperScan(com.example.hr.mapper)替代Mapper减少类扫描。SpringBootApplication(exclude {DataSourceAutoConfiguration.class, RedisAutoConfiguration.class})最后一步打包前执行mvn clean compile -Dmaven.test.skiptrue生成target/hr-0.0.1-SNAPSHOT.jar。上传服务器后用nohup java -Xms2g -Xmx2g -jar hr-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod 启动访问http://服务器IP:8080/hr/swagger-ui.html即可调试所有接口——这才是中小企业真正能落地的人事系统。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →