简介:这份资源是面向计算机与软件工程专业学生的毕业设计完整方案,围绕基于SpringBoot的人事管理系统展开,适合需要完成毕设或学习企业级Java开发的学习者。系统采用前后端分离架构,后端以Spring Boot为核心,结合Spring Security实现认证与权限控制,MyBatis完成持久层操作,MySQL负责数据存储,整体遵循B/S模式与MVVM设计。功能覆盖员工信息管理、考勤打卡与请假审批、薪资自动计算与个税核算、基于角色的权限控制,以及员工分布、考勤趋势、薪资结构等图表化统计分析,并运用单例、工厂、观察者等设计模式优化代码结构。资源包共305个文件,包含126个Java源码、32个JavaScript脚本、27个XML配置、28张PNG图片及SQL脚本、说明文档、论文文档等,压缩包约90.97MB,源码与论文齐备,涵盖需求分析、系统设计、技术实现与测试部署全过程。目前已有60人学习,下载后可直接部署运行,便于二次开发与定制,是掌握SpringBoot企业级应用开发的实用参考。
1. 从一份能跑起来的 SpringBoot 人事管理系统说起:为什么它成了毕业设计里最稳的选题
每年到了做课程设计或毕业设计的节点,总有一批人卡在选题上。想做点有业务厚度的,怕技术栈太杂收不了尾;想做纯增删改查,又担心答辩时被问“这跟练手项目有什么区别”。SpringBoot 人事管理系统恰好卡在一个很舒服的位置:业务边界清晰,模块划分天然,技术栈主流且资料密集,源码和论文两条线都能落地。它解决的核心问题就一个——把员工从入职到离职的全生命周期数据管起来,同时让考勤、薪资、请假、部门调动这些高频动作有据可查。适合谁?适合已经学过 Java Web、能看懂 Controller-Service-Mapper 三层结构、但还没独立搭过一个完整业务系统的人。也适合那些想拿一份结构规整的源码当底子,再往上叠自己业务理解的人。这个方向不新,但每年都有人靠它把答辩稳稳过掉,原因后面几章会拆开讲。
2. 技术选型与项目骨架:为什么这套组合能扛住答辩追问
2.1 为什么是 SpringBoot 而不是传统 SSM
传统 SSM 要配 web.xml、applicationContext.xml、spring-mvc.xml,光配置文件就能劝退一半人。SpringBoot 的核心价值在于自动配置和起步依赖,把原来散落在多个 XML 里的东西收进一个 application.yml,再通过 starter 把常用依赖打包。对人事管理系统这种模块数量中等、业务逻辑不算复杂的项目来说,SpringBoot 能让开发者把精力放在业务代码上,而不是环境搭建上。
选版本时有个血泪经验:别追最新。热搜里“springboot版本太高”是真实痛点,高版本对 JDK 要求苛刻,部分旧教程里的依赖坐标也对不上。我一般会选 2.7.x 这条线,JDK 用 8 或 11,MyBatis-Plus 用 3.5.x,MySQL 驱动用 8.0.x。这套组合在各类教程、问答和源码里出现频率最高,遇到问题一搜就有答案。如果学校要求 JDK 17,那就上 SpringBoot 3.x,但要注意 javax 包名全部换成了 jakarta,老代码直接拷会编译不过。
2.2 项目结构怎么分层才不乱
一个能讲清楚的人事管理系统,包结构应该让人一眼看懂职责边界。常见做法是按功能模块 + 分层混合切分:
com.hrms ├── common // 通用返回体、异常、工具类 ├── config // 拦截器、跨域、MyBatis-Plus 配置 ├── controller // 接口入口 ├── service │ └── impl ├── mapper // 数据访问 ├── entity // 数据库映射对象 ├── dto // 入参对象 └── vo // 出参对象entity 和数据库表一一对应,dto 接前端传进来的参数,vo 负责返回给前端展示。很多新手直接把 entity 当入参出参用,结果一改表结构前端就崩,答辩时被问“为什么员工密码会返回给前端”就尴尬了。分层不是形式主义,是为了让每层只关心自己的事。
2.3 依赖清单与最小可运行配置
下面这份 pom 依赖是经过多个项目验证的最小集合,能覆盖人事管理系统 90% 的功能:
<dependencies> <!-- Web 层:提供 REST 接口和内嵌 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据访问:MyBatis-Plus 比原生 MyBatis 少写大量 XML --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 简化实体类代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>逻辑说明:web starter 负责接口和容器,mybatis-plus starter 负责数据访问并自带分页插件,validation 负责 @NotNull、@Size 这类注解校验。参数上,MyBatis-Plus 版本要和 SpringBoot 版本匹配,2.7.x 的 SpringBoot 配 3.5.x 的 MyBatis-Plus 是稳的。如果启动报 “Invalid bound statement”,八成是 mapper 扫描路径没配,在启动类上加 @MapperScan(“com.hrms.mapper”) 即可。
2.4 数据库表设计:五张核心表撑起整个系统
人事管理系统的表不用多,但关系要清楚。核心五张表:员工表 employee、部门表 department、考勤表 attendance、请假表 leave_record、薪资表 salary。员工表通过 department_id 关联部门,考勤和请假通过 employee_id 关联员工,薪资通过 employee_id 和月份定位。
CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 1 COMMENT '1男 2女', phone VARCHAR(20), department_id BIGINT, hire_date DATE, status TINYINT DEFAULT 1 COMMENT '1在职 0离职', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '员工表';工号加唯一索引是必须的,否则重复录入查起来很麻烦。status 字段用逻辑删除而不是物理删除,离职员工的历史考勤和薪资记录还要保留,这是人事系统的业务底线。department_id 上建普通索引,按部门筛选员工是高频操作。
3. 核心模块落地:从登录鉴权到考勤统计的完整实现
3.1 登录鉴权:JWT + 拦截器的最小闭环
人事系统的入口是登录,登录之后所有接口都要带身份。常见做法是 JWT + 拦截器,不引入 Spring Security 那套重家伙,对毕业设计来说够用且好讲。
// JwtUtil.java public class JwtUtil { private static final String SECRET = "hrms-secret-key-2024"; private static final long EXPIRE = 24 * 60 * 60 * 1000L; // 生成 token,把用户 id 和角色塞进 payload public static String createToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析 token,失败会抛异常,由拦截器统一处理 public static Claims parse(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }逻辑说明:createToken 把 userId 和 role 写进 payload,前端每次请求放在 Header 的 Authorization 里。parse 方法在拦截器里调用,解析失败直接返回 401。参数上,SECRET 不要硬编码在代码里,放到 application.yml 用 @Value 注入,答辩时被问“密钥泄露怎么办”能答上来。过期时间设 24 小时是折中,太长不安全,太短前端要频繁重登。
拦截器注册时要注意放行登录接口和静态资源:
registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/css/**", "/js/**");3.2 员工管理:分页查询与条件筛选
员工列表是使用频率最高的页面,分页和条件筛选必须做对。MyBatis-Plus 的分页插件能省掉手写 limit 的麻烦。
// EmployeeServiceImpl.java public Page<EmployeeVO> pageQuery(EmployeeQuery query) { Page<Employee> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); // 姓名模糊查,工号精确查,部门精确查 wrapper.like(StringUtils.hasText(query.getName()), Employee::getName, query.getName()) .eq(StringUtils.hasText(query.getEmpNo()), Employee::getEmpNo, query.getEmpNo()) .eq(query.getDepartmentId() != null, Employee::getDepartmentId, query.getDepartmentId()) .eq(Employee::getStatus, 1) .orderByDesc(Employee::getCreateTime); return employeeMapper.selectPage(page, wrapper); }逻辑说明:LambdaQueryWrapper 的条件方法第一个参数是布尔值,只有为 true 时才拼接该条件,这样一条链就能处理“有就查、没有就忽略”的动态查询。参数上,pageNum 从 1 开始,pageSize 默认 10,前端传之前要做边界校验,防止有人传 pageSize=100000 把数据库拖垮。orderByDesc 按创建时间倒序,新入职的员工排前面符合使用习惯。
分页插件需要在配置类里注册,否则 selectPage 不生效,返回的是全量数据:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }3.3 考勤统计:按月汇总的 SQL 与接口设计
考勤模块的难点不在录入,在统计。每个月要算出每个员工的出勤天数、迟到次数、请假天数。用一条 SQL 在数据库层聚合比在 Java 里循环效率高得多。
SELECT e.id, e.name, COUNT(CASE WHEN a.status = 1 THEN 1 END) AS normal_days, COUNT(CASE WHEN a.status = 2 THEN 1 END) AS late_times, COUNT(CASE WHEN a.status = 3 THEN 1 END) AS absent_days FROM employee e LEFT JOIN attendance a ON e.id = a.employee_id AND DATE_FORMAT(a.attend_date, '%Y-%m') = #{month} WHERE e.status = 1 GROUP BY e.id, e.name;逻辑说明:LEFT JOIN 保证没有考勤记录的员工也出现在结果里,否则新员工会被漏掉。DATE_FORMAT 把日期转成年月做匹配,条件写在 ON 里而不是 WHERE 里,这是 LEFT JOIN 的关键区别——写在 WHERE 里会让左连接退化成内连接。参数 month 格式是 “2024-06”,前端传之前用正则校验一下,防止 SQL 注入。
接口层把结果封装成 VO 返回,前端用 ECharts 画柱状图。如果数据量大,可以在 attendance 表的 attend_date 字段上建索引,按月查询走索引扫描。
3.4 请假审批:状态机与并发处理
请假流程是“员工提交 → 主管审批 → 状态更新”,本质是一个状态机。状态值:0 待审批、1 已通过、2 已驳回。审批接口要做两件事:校验当前状态是否允许审批、更新状态并记录审批人和时间。
@Transactional public void approve(Long leaveId, Integer status, Long approverId) { LeaveRecord record = leaveMapper.selectById(leaveId); // 只有待审批状态才能被审批,防止重复操作 if (record == null || record.getStatus() != 0) { throw new BizException("该申请已处理或不存在"); } record.setStatus(status); record.setApproverId(approverId); record.setApproveTime(LocalDateTime.now()); // 乐观锁:带上原状态作为条件,更新行数为 0 说明被并发修改 int rows = leaveMapper.update(record, new LambdaUpdateWrapper<LeaveRecord>() .eq(LeaveRecord::getId, leaveId) .eq(LeaveRecord::getStatus, 0)); if (rows == 0) { throw new BizException("操作冲突,请刷新后重试"); } }逻辑说明:@Transactional 保证状态更新和后续操作在一个事务里。乐观锁通过 update 时带 status=0 条件实现,两个主管同时点审批,只有一个能成功,另一个收到冲突提示。参数上,approverId 从 JWT 里取,不要从前端传,否则可以伪造审批人。这个并发场景在答辩时是加分项,能讲清楚为什么需要乐观锁而不是 synchronized。
4. 避坑与排查:那些让项目跑不起来的常见问题
4.1 启动报 “Failed to configure a DataSource”
现象:项目一启动就报错,提示找不到数据源。原因通常是 application.yml 里数据库配置没写全,或者用了多环境配置但没激活对应 profile。解决:检查 spring.datasource.url、username、password、driver-class-name 四项是否齐全,url 里要带时区和编码参数,比如jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。如果用了 application-dev.yml,确认 spring.profiles.active=dev 已配置。
4.2 前端请求跨域被浏览器拦截
现象:Postman 能调通,浏览器里报 CORS 错误。原因:前后端分离部署时端口不同,浏览器同源策略拦截。解决:加一个全局跨域配置,实现 WebMvcConfigurer 的 addCorsMappings 方法,允许指定来源、方法和头。注意 allowCredentials 为 true 时,allowedOrigins 不能用 “*”,要写具体域名,否则浏览器会拒绝。
4.3 MyBatis-Plus 分页返回全部数据
现象:调分页接口,返回的 total 是对的,但 records 里是全部数据。原因:分页插件没注册,或者注册了但版本和 MyBatis-Plus 不匹配。解决:确认配置类里有 MybatisPlusInterceptor 的 Bean,并且加了 PaginationInnerInterceptor。如果用的是老版本,分页插件类名是 PaginationInterceptor,别搞混。
4.4 时间字段前后端差 8 小时
现象:数据库存的是正确时间,前端显示少了 8 小时。原因:Jackson 序列化时区没配,默认用 UTC。解决:在 application.yml 里加spring.jackson.time-zone=GMT+8和spring.jackson.date-format=yyyy-MM-dd HH:mm:ss。如果用的是 LocalDateTime,还要确认 MySQL 连接串里的 serverTimezone 设对了。
4.5 打包成 jar 后访问 404
现象:IDEA 里跑得好好的,mvn package 之后 java -jar 启动,接口全 404。原因:静态资源没被打进 jar,或者 Controller 没被扫描到。解决:确认静态文件放在 src/main/resources/static 下,启动类在根包下且加了 @SpringBootApplication。如果启动类在子包里,要手动指定 @ComponentScan 的 basePackages。
5. 论文与源码的配合:怎么让答辩老师觉得你真正做了这个系统
5.1 论文框架怎么搭才不空
论文最怕写成产品说明书。常见做法是按“绪论 → 需求分析 → 系统设计 → 系统实现 → 系统测试 → 总结”六章走,但每章要有自己的分析痕迹。需求分析里画用例图,把员工、主管、管理员三个角色的操作边界画清楚;系统设计里画 E-R 图和架构图,说明为什么选三层架构而不是微服务;实现章节贴关键代码,但代码前后要有设计思路的说明,不能只贴代码。测试章节列一张测试用例表,把正常流程和异常流程都覆盖到,比如“输入不存在的工号查询”应该返回什么。
5.2 源码怎么整理才经得起追问
源码不是能跑就行。答辩老师常问的几个点:为什么用逻辑删除、JWT 过期怎么处理、分页插件原理是什么、并发审批怎么保证。整理源码时,把每个模块的核心方法加上注释,注释写“为什么这么做”而不是“这行做了什么”。比如乐观锁那段,注释写“防止两个主管同时审批同一条请假记录”,比写“更新请假状态”有价值得多。另外,把 application.yml 里的敏感信息抽成占位符,展示时用假值,这是基本的安全意识。
5.3 一个能加分的验证技巧:用接口文档反向检查功能覆盖
写完接口后,用 Knife4j 或 Swagger 生成接口文档,然后对着需求分析里的用例逐条核对。比如“员工离职后不能再登录”,就去测登录接口传离职员工账号是否返回错误;“请假天数不能超过年假余额”,就去测边界值。这个动作能发现很多“代码写了但逻辑没闭环”的问题。我一般会在答辩前把接口文档打印出来,每个接口旁边手写测试结果,老师看到这个会认为你是真的跑过而不是抄的。
5.4 我踩过的坑与留下的习惯
第一次做这个系统时,我把所有业务逻辑写在 Controller 里,Service 层只是透传,结果改一个校验规则要翻五个文件。后来养成习惯:Controller 只做参数接收和结果返回,Service 写业务规则,Mapper 只做数据访问。另一个坑是数据库字段用驼峰命名,MySQL 在 Linux 下默认大小写敏感,本地 Windows 跑得好好的,部署到服务器就报表不存在。现在建表一律用下划线命名,实体类用驼峰,靠 MyBatis-Plus 的 map-underscore-to-camel-case 自动映射。这些习惯不写在论文里,但能让代码在答辩现场少出洋相。希望帮到你。
本文还有配套的精品资源,点击获取