Spring Boot人事管理系统开发实战:从架构设计到安全落地

发布时间:2026/9/17 3:43:49
Spring Boot人事管理系统开发实战:从架构设计到安全落地 简介一份面向高校毕设与Java开发学习者的人事管理系统完整方案涵盖论文、源码、答辩PPT及部署配置。系统基于Spring Boot与Vue.js前后端分离架构实现员工资料管理、人事信息入库分析、薪资套账、在线通信、统计分析及RBAC权限控制等模块并集成Spring Security安全框架与MySQL数据库适合作为毕业设计参考或中小型人事管理项目实战练习。资源包共319个文件约92.56MB。其中126个Java文件对应后端核心逻辑32个JS与17个CSS为前端Vue.js页面组件另有SQL数据库脚本、Dockerfile、Maven封装脚本及论文/PPT文稿覆盖从环境搭建到部署运行的关键环节。文件类型涉及源码、脚本、文档与图标资源整体结构较清晰便于按模块查阅。目前已有2294人浏览学习。获取后可得到完整论文正文、可运行前后端代码、答辩演示PPT及部署说明适合需要快速搭建人事管理系统原型或参照完整项目流程完成毕设任务的读者。1. 基于 Spring Boot 的人事管理系统选型和落地都围绕业务边界展开基于 Spring Boot 的人事管理系统业务边界拆开就是员工档案、部门组织、考勤、薪资、请假审批五块。这类系统并发量不高几十个人同时操作就算高峰但数据敏感、权限严格、状态流转复杂比如请假要经过主管和 HR 两级审批薪资要按考勤数据自动核算。用 Spring Boot 来做比 SSM 少写大量 XML 和配置比微服务方案少了注册中心和分布式事务的负担是本科毕设和中小企业内部系统里最常见的选型。这篇文章的读者要么是正在做毕设需要完整落地方案要么是公司要上内部人事系统但不想过度设计。下面这套从架构到代码的路径可以直接照着改造。2. Spring Boot 人事系统的分层架构与项目目录规范2.1 四层架构里的 Manager 层解决事务边界问题网上搜 spring boot 四层架构最常见的划分是 Controller、Service、Mapper 加 Entity。放到人事管理系统里我建议在 Service 和 Mapper 之间再拆一层 Manager原因很具体考勤和薪资都要跨多张表操作直接堆在 Service 里会导致事务边界模糊一旦薪资计算失败考勤状态已经被改掉回滚逻辑非常难写。标准目录规范如下com.example.hrms ├── controller # 接口层只做参数接收和响应封装 ├── service # 业务层事务边界在这里 │ └── impl ├── manager # 跨表聚合层 ├── mapper # MyBatis 接口层 ├── entity # 数据库实体 ├── dto # 请求和响应对象 ├── config # 配置类 └── common # 统一返回、异常、工具类Controller 层只做参数接收和响应封装Service 层承载业务规则和事务Manager 层负责跨表聚合Mapper 层只做单表 SQL。人事系统的特殊性在于考勤统计、薪资核算、审批流转三个模块都是跨表操作如果是经典三层架构面试官追问事务边界时很难解释清楚。加上 Manager 层后Service 的方法保持单一职责事务注解细化到具体业务方法上代码评审时一眼能看清每个方法操作哪些表。2.2 Spring Boot 版本选型与 MyBatis-Plus 依赖版本判断的标准很简单新起的项目直接用 Spring Boot 3.x 配 Java 17已有 2.x 基础上的毕业设计改造保持 2.7.x 也够。Spring Boot 3.x 基于 Jakarta EE 9javax 包全部换成 jakarta实体类上的Entity、Table注解 import 路径全部要改。组件Spring Boot 2.7.xSpring Boot 3.2.xJDK 基线8/1117持久层包名javax.persistencejakarta.persistenceMyBatis 整合包mybatis-spring-boot-starter 2.xmybatis-spring-boot-starter 3.xWeb 容器Tomcat 9Tomcat 10.1人事系统里有大量 CRUDjava mybatis 和 Spring Boot 框架搭配时我更倾向 MyBatis-Plus 而不是 JPA。JPA 的关联查询在复杂报表场景下容易产生 N1 问题而 MyBatis-Plus 的 LambdaQueryWrapper 直接在 Service 层拼条件分页插件一次配置全局生效。依赖这样加dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意 Spring Boot 3.x 必须用mybatis-plus-spring-boot3-starter这个 artifactId老的mybatis-plus-boot-starter是针对 2.x 的混用在启动阶段不会报错但分页插件和条件构造器的自动配置不会加载。MySQL 8 以上驱动类名是com.mysql.cj.jdbc.DriverJDBC URL 里必须带serverTimezoneAsia/Shanghai否则时间字段差 8 小时考勤和薪资模块直接算错。2.3 目录规范之外的 application.yml 必写配置spring: datasource: url: jdbc:mysql://localhost:3306/hrms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.hrms.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case让数据库字段emp_name自动映射到实体属性empName员工表字段多手写 resultMap 效率太低。jackson.time-zone不写接口返回的时间会统一少 8 小时这在请假审批时间、薪资发放时间的展示上是硬伤。mapper-locations指向classpath:mapper/*.xml复杂 SQL 用 XML 写简单条件用注解项目里不要混用两种风格。3. 员工档案与考勤模块MyBatis-Plus 分页和事务落地3.1 核心表结构与实体设计人事管理系统的数据库设计核心是员工表、部门表、考勤表外加薪资表和请假申请表。员工表和部门表多对一考勤表按天记录每个员工的打卡结果薪资表通过emp_id和月份的唯一索引约束重复计算。CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, name VARCHAR(50) NOT NULL, dept_id BIGINT NOT NULL, position_name VARCHAR(50), hire_date DATE, status TINYINT DEFAULT 1 COMMENT 1在职 0离职 ); CREATE TABLE attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id BIGINT NOT NULL, work_date DATE NOT NULL, check_in TIME, check_out TIME, status TINYINT COMMENT 1正常 2迟到 3早退 4缺勤, UNIQUE KEY uk_emp_date (emp_id, work_date) );emp_no的工号唯一索引是业务刚需人事系统里工号一旦下发就不能重复。attendance表用(emp_id, work_date)联合唯一索引保证同一个人同一天只有一条考勤记录重复导入不会产生脏数据。实体类用 Lombok 的Data标注字段类型与 MySQL 对应DATE对LocalDateTIME对LocalTimeDATETIME对LocalDateTime。不要用java.util.Date序列化格式难统一不说前端处理时也容易出时区偏移。3.2 Service 层事务考勤汇总写入薪资表薪资计算是最能体现事务边界的地方。考勤汇总要查出迟到、早退、缺勤天数按规则扣减再把结果写入薪资表任何一个环节异常整个月的薪资数据都会错位必须加Transactional。Service public class SalaryServiceImpl implements SalaryService { Autowired private EmployeeMapper employeeMapper; Autowired private AttendanceMapper attendanceMapper; Autowired private SalaryMapper salaryMapper; Override Transactional(rollbackFor Exception.class) public void calculateMonthlySalary(Long deptId, int month) { ListEmployee employees employeeMapper.selectByDeptId(deptId); for (Employee emp : employees) { AttendanceStat stat attendanceMapper.selectMonthStat(emp.getId(), month); BigDecimal salary baseSalary(emp) .subtract(deductForLate(stat.getLateCount())) .subtract(deductForAbsent(stat.getAbsentCount())); salaryMapper.insertOrUpdate( new Salary(emp.getId(), month, salary)); } } }rollbackFor Exception.class必须写。Spring 默认只对 RuntimeException 回滚如果薪资计算里调用外部接口抛出受检异常不指定这个参数就会数据半提交。insertOrUpdate是 MyBatis-Plus 提供的合并写入先查主键再决定 insert 还是 update保证同一人同月薪资不重复插入。事务加在 Service 实现类的方法上不要加在 Controller 里Controller 层的异常处理应交给全局RestControllerAdvice。提示事务方法在类内部用 this 调用会绕过 AOP 增强Transactional不生效。薪资计算的入口要放在独立的 Service Bean 里跨类调用才能被事务管理器拦截。3.3 分页查询插件配置与条件构造器员工列表是人事系统里被调用最多的接口分页方案直接影响前端体验。MyBatis-Plus 的分页插件注册成一个Bean之后所有 Page 查询自动生效。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Controller 层配合 DTO 接收查询参数不要直接把前端传的 Map 透传给 Mapper否则排序字段会成为 SQL 注入点。GetMapping(/page) public ResultPageResultEmployeeVO page(EmployeeQueryDTO dto) { PageEmployee page new Page(dto.getCurrent(), dto.getSize()); LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(dto.getName()), Employee::getName, dto.getName()) .eq(Objects.nonNull(dto.getDeptId()), Employee::getDeptId, dto.getDeptId()) .orderByDesc(Employee::getHireDate); PageEmployee result employeeService.page(page, wrapper); return Result.success(convertToPageResult(result)); }like和eq的第一个参数是 condition为 false 时该条件不拼进 SQL比用if判空再调and(...)干净得多。排序用orderByDesc写死列名不允许前端传orderBy参数这是人事系统这类数据敏感系统的底线。EmployeeQueryDTO里current和size要有默认值和上限避免前端传size10000把全表拖出来。4. 认证权限与文件上传Spring Boot 里最容易漏的两个口子4.1 权限框架选型Spring Security、Sa-Token、Shiro 对比人事系统的数据敏感程度比普通业务系统高薪资、身份证号、家庭住址属于敏感字段权限控制要管到接口和字段级别。三个可选方案各有利弊框架学习成本注解支持活跃度适合场景Spring Security高需配置注解开关高大厂标准体系Sa-Token低开箱即用高中小系统、毕设Shiro中有低遗留项目我一般用 Sa-Token。登录、权限校验、踢人下线都是几行代码和 Spring Boot 集成不需要写过滤器链对并发不高的内部人事系统足够。密码加密用 BCrypt不要用 MD5人事系统密码一旦脱库MD5 撞库成本太低。权限数据模型是三张主表两张中间表user、role、permission用户角色、角色权限分别用中间表关联。Configuration public class SaTokenConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - { StpUtil.checkLogin(); })).addPathPatterns(/**) .excludePathPatterns(/login, /actuator/health); } }这个拦截器把所有非登录接口保护起来/login放行Actuator 健康检查端点也要放行否则运维探活会把系统判定为不健康。接口级权限用SaCheckPermission(employee:export)标注导出花名册这种操作单独授权避免所有 HR 账号都能导出全量数据。4.2 文件上传头像与证件附件的落盘策略人事系统必然涉及文件上传员工头像、身份证扫描件、简历 PDF。上传接口要处理三件事文件类型白名单、大小限制、存储路径。PostMapping(/upload/avatar) public ResultString uploadAvatar(RequestParam(file) MultipartFile file) { String original file.getOriginalFilename(); if (original null || original.lastIndexOf(.) 0) { throw new BusinessException(文件名为空或缺少扩展名); } String suffix original.substring(original.lastIndexOf(.)).toLowerCase(); if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(suffix)) { throw new BusinessException(仅支持 jpg/png/gif 图片格式); } if (file.getSize() 5 * 1024 * 1024) { throw new BusinessException(头像文件不能超过 5MB); } String path /uploads/avatar/ LocalDate.now() / UUID.randomUUID().toString().replace(-, ) suffix; file.transferTo(new File(basePath path)); return Result.success(path); }类型校验不能只依赖文件后缀MultipartFile.getContentType()可以伪造但后缀白名单是第一道防线。文件名必须重命名为 UUID用户原始文件名可能是中文、可能包含../路径穿越字符直接transferTo会覆盖磁盘目录。按日期分目录存放避免单目录文件数过多影响文件系统查找效率。数据库里只存相对路径前端展示时由拦截器拼接完整访问 URL。注意MultipartFile.getOriginalFilename()在部分浏览器下可能返回 null做后缀截取之前必须先判空否则一次空上传就让接口返回 500。spring.servlet.multipart的参数写在 application.ymlspring: servlet: multipart: max-file-size: 5MB max-request-size: 20MBmax-file-size控制单文件上限max-request-size控制一次请求多个文件的总大小后者默认只有 10MB批量上传员工证件照片时会踩坑。超出限制时Spring 抛出的MaxUploadSizeExceededException不捕获会返回 500需要在全局异常处理器里单独处理并给出友好提示。4.3 Actuator 未授权访问的加固方案Actuator 默认只暴露 health 端点但开发中为了方便会配置成include: *导致未授权访问漏洞。/actuator/env会把数据库密码、Redis 密码直接打印在响应体里/actuator/heapdump能导出 JVM 堆内存里面可能包含运行期内存中的敏感数据。毕设答辩和等保测评都会查这个点。management: endpoints: web: exposure: include: health, info endpoint: health: show-details: never只暴露 health 和 infoshow-details: never关闭健康检查详情避免数据库连接信息外泄。如果确实需要 metrics 监控把管理端口拆到单独的 8081不对外网开放再结合防火墙 IP 白名单。这一步做完安全评审里「Actuator 未授权访问」这一条就能直接勾掉。5. 报表导出、WebSocket 推送与 Spring Boot 4.x 迁移注意点5.1 EasyExcel 导出员工花名册人事部门每月固定导出花名册POI 的 XSSFWorkbook 会把整个工作簿载入内存五万条员工数据就可能 OOM。EasyExcel 基于 SAX 逐行解析和写入内存占用保持在较低水平是这类导出功能的标准做法。GetMapping(/export) public void export(HttpServletResponse response) throws IOException { ListEmployeeExcel data employeeService.listForExport(); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); response.setHeader(Content-Disposition, attachment;filenameemployees.xlsx); EasyExcel.write(response.getOutputStream(), EmployeeExcel.class) .sheet(员工花名册) .doWrite(data); }Content-Disposition必须是attachment写成inline时浏览器会在页面里渲染二进制内容。EmployeeExcel里的日期字段加DateTimeFormat(yyyy-MM-dd)导出的入职日期才能被 Excel 识别为日期而不是文本。5.2 请假审批的 WebSocket 实时通知员工提交请假申请后审批人需要立刻收到提醒。轮询数据库的方式太浪费WebSocket 推送是更合理的方案。内部人事系统在线人数通常只有几百直接用 Spring 自带的 WebSocket 加 STOMP 协议即可不必引入 Netty 这类重型框架。注意连接握手时要从 Header 里取 Token 做身份校验否则任何人都能连上推送连接等于开了一个未授权访问的口子。5.3 Spring Boot 4.x 升级的排查路径Spring Boot 4.0 发布后3.x 项目升级最集中的三个报错点DataSourceAutoConfiguration的包路径变了需要按新包的 autoconfigure 模块找对应类Jackson 的JSONMapper$Builder链式调用有调整序列化LocalDate要显式注册JavaTimeModuleSpring Framework 7 要求 Java 17 以上JDK 得一起升。排查升级问题最有效的工具是条件装配报告启动参数加--debug日志里的Positive matches和Negative matches会列出哪些自动配置生效、哪些被跳过。数据库连不上、配置没生效、MyBatis 插件不拦截都能从这份报告里找到根因比逐行加日志快得多。本文还有配套的精品资源点击获取