
简介本资源是一套基于Spring Boot后端与Vue前端技术栈开发的完整人事管理系统源码专为计算机类专业本科生毕业设计及课程实践打造覆盖员工管理、部门设置、考勤统计、权限控制等核心HR业务场景适合人工智能、计算机科学与技术等方向学生开展系统性工程实践。压缩包共356个文件含92个Java后端业务逻辑与控制器代码、34个Vue组件实现界面交互、14个JS工具脚本、13个XML配置与Mapper映射文件、161个SVG图标资源辅以SQL建表语句、YML配置、BAT启动脚本及README说明文档整体大小29.05MB结构规范、模块解耦清晰。已有67人下载学习资源经严格测试可一键运行配套论文可私信预览提供持续答疑支持。读者可直接部署运行、理解前后端分离架构设计、掌握JWT鉴权、MyBatis-Plus集成、Element Plus组件化开发等关键技术实践路径。1. 为什么毕业设计选「SpringBoot Vue 人事管理系统」不是凑数而是稳扎稳打的工程练兵场你翻过几十个 GitHub 上标着“毕设源码”的仓库点开发现要么是 SpringBoot 单体套了个 Thymeleaf 模板要么 Vue 页面连路由都没配全、登录态全靠 localStorage 硬编码——这种项目答辩时老师一句“用户权限怎么校验的”就卡壳。但真正能跑通、能部署、能讲清技术决策链的人事管理系统恰恰是 SpringBoot 和 Vue 组合最扎实的落地切口它不追求炫技却完整覆盖了前后端分离项目的全部关键链路——从 SpringBoot 的多环境配置、JWT 鉴权、MyBatis-Plus 动态条件查询、Excel 导入导出到 Vue 的路由守卫、Pinia 状态持久化、Element Plus 表单联动、Axios 请求拦截与错误统一处理。这不是玩具项目而是企业级 HR SaaS 最小可行模块的镜像。适合计算机/软件工程专业大三下至大四上学生代码量可控后端 3000 行、前端 2500 行、技术栈主流SpringBoot 2.7.x / Vue 2.6 或 Vue 3.2、部署门槛低JAR 包 Nginx 静态资源即可且所有功能点员工档案、部门管理、考勤登记、薪资核算都有明确业务语义调试时不会陷入“不知道该测什么”的混沌。别被“毕业源码”四个字误导——它本质是一份可验证、可延展、能写进简历的技术契约。2. 用 SpringBoot 2.7.18 Vue 3.2.45 搭建最小可运行骨架从初始化到接口联调2.1 后端用 Spring Initializr 生成带核心依赖的纯净工程不要直接 clone 某个“毕设模板”先自己动手初始化。访问 https://start.spring.io注意必须选 Spring Boot 2.7.x 版本因 Vue 3 项目常需兼容 WebMvc 而非 WebFlux且 3.x 版本对 MyBatis-Plus 3.5.x 支持存在已知 ClassLoader 冲突。勾选以下依赖Spring WebSpring Data JDBC替代 JPA轻量且与 MyBatis-Plus 共存无冲突Lombok省去 getter/setter避免答辩时被问“为什么不用 record”Validation校验注解 NotBlank/Email 必须Spring Boot DevTools开发期热重载节省 80% 重启时间提示不要勾选 Spring Security Starter。毕设场景下手写 JWT 过滤器比自动配置 SecurityConfig 更易讲清鉴权流程也避免PreAuthorize注解在 Controller 层引发的 AOP 代理陷阱。生成 ZIP 后解压用 IDEA 打开。关键修改两处pom.xml中显式指定 MyBatis-Plus 版本避免 Spring Boot 2.7 默认引入的 3.5.1 存在LambdaQueryWrapper泛型擦除 Bugdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.4.3.4/version !-- 注意不是最新版是 2.7.x 兼容性最好的版本 -- /dependencyapplication.yml配置数据库连接以 H2 内存库为例免装 MySQL答辩演示更流畅spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:hrms;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE username: sa password: h2: console: enabled: true # 开启 H2 控制台地址 http://localhost:8080/h2-console启动类添加MapperScan(com.example.hrms.mapper)确保 Mapper 接口被扫描。2.2 前端Vue CLI 4.5.19 创建标准项目并接入 AxiosVue 3 项目必须用vue create而非npm init vuelatest后者默认启用 Vite与 SpringBoot 静态资源托管机制不兼容。执行npm install -g vue/cli4.5.19 vue create hrms-frontend # 选择 Manually select features → 勾选 Babel, Router, Vuex (Vue 2) 或 Pinia (Vue 3), CSS Pre-processors # 选择 Sass/SCSS (dart-sass) # 选择 ESLint Prettier # 选择 In dedicated config files # 保存为 package.json安装 Axios 并配置请求拦截cd hrms-frontend npm install axios0.21.4 # 不要用 1.x其取消请求 API 与 Vue 3 的 Composition API 配合易出错在src/utils/request.js中import axios from axios // 创建 axios 实例 const service axios.create({ baseURL: /api, // 注意此处为相对路径后续由 Nginx 代理到后端 timeout: 10000 }) // 请求拦截携带 token service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) ) // 响应拦截统一错误处理 service.interceptors.response.use( response response.data, // 直接返回 data避免组件内写 response.data.data error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login // 强制跳转登录页 } return Promise.reject(error) } ) export default service参数说明baseURL: /api是关键。SpringBoot 默认静态资源路径为/static而 Vue 打包后index.html中的请求路径需与 Nginx location 规则匹配。此处约定所有 API 请求前缀为/api后续 Nginx 配置将/api代理至http://localhost:8080。2.3 联调用 Postman 测试登录接口再让 Vue 页面发起真实请求先写一个最简登录 ControllerRestController RequestMapping(/auth) public class AuthController { PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 模拟校验用户名 admin密码 123456 if (admin.equals(dto.getUsername()) 123456.equals(dto.getPassword())) { String token JwtUtil.generateToken(admin, ROLE_ADMIN); return Result.success().data(token, token); } return Result.fail(用户名或密码错误); } }启动 SpringBoot用 Postman 发送 POST 请求http://localhost:8080/auth/loginBody 为 JSON{username:admin,password:123456}成功返回{code:200,msg:success,data:{token:eyJhbGciOiJIUzI1NiJ9...}}。再在 Vue 的Login.vue中调用script setup import { ref } from vue import request from /utils/request const form ref({ username: , password: }) const loading ref(false) const handleSubmit async () { loading.value true try { const res await request.post(/auth/login, form.value) localStorage.setItem(token, res.token) window.location.href /home // 登录成功跳转首页 } catch (err) { alert(err.response?.data?.msg || 登录失败) } finally { loading.value false } } /script此时打开浏览器http://localhost:8080Vue 开发服务器输入账号密码控制台 Network 标签页能看到请求发出、响应返回、localStorage 写入 token ——联调成功标志Vue 页面能拿到后端返回的 token且后续请求头自动携带 Authorization。3. 实现人事核心模块员工管理的 CRUD 与 Excel 导入导出闭环3.1 数据库设计用 H2 初始化员工表兼顾扩展性与答辩演示H2 内存库需在启动时自动建表。在src/main/resources下创建schema-h2.sqlCREATE TABLE IF NOT EXISTS sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, real_name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 1 COMMENT 1:男,2:女, phone VARCHAR(11), email VARCHAR(100), dept_id BIGINT, status TINYINT DEFAULT 1 COMMENT 1:在职,2:离职, create_time DATETIME DEFAULT NOW(), update_time DATETIME DEFAULT NOW() ON UPDATE NOW() ); CREATE TABLE IF NOT EXISTS sys_dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id BIGINT DEFAULT 0, sort INT DEFAULT 0 );在application.yml中启用spring: sql: init: mode: always schema-locations: classpath:schema-h2.sql注意sys_user表中dept_id为外键关联部门但 H2 不强制外键约束避免答辩时因级联删除引发异常。实际部署到 MySQL 时再补FOREIGN KEY。3.2 后端MyBatis-Plus 分页查询 条件筛选拒绝“万能 queryWrapper”员工列表接口不能只写userMapper.selectList(null)。真实业务需支持按姓名模糊搜索、按部门筛选、按在职状态过滤。Controller 层接收参数GetMapping(/list) public Result list( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Long deptId, RequestParam(required false) Integer status) { PageUser page new Page(current, size); LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); if (StringUtils.isNotBlank(keyword)) { wrapper.like(User::getRealName, keyword).or().like(User::getUsername, keyword); } if (deptId ! null deptId 0) { wrapper.eq(User::getDeptId, deptId); } if (status ! null) { wrapper.eq(User::getStatus, status); } userMapper.selectPage(page, wrapper); return Result.success(page); }关键点wrapper.or().like()实现姓名/用户名双字段模糊搜索deptId 0避免传 0 时误查status ! null允许前端不传 status 参数即查全部状态。3.3 前端Vue 3 Element Plus 表单联动 分页封装在EmployeeList.vue中使用el-table和el-paginationtemplate div classemployee-list el-card template #header div classcard-header el-input v-modelsearch.keyword placeholder姓名或工号 clearable stylewidth:200px / el-select v-modelsearch.deptId placeholder部门 clearable stylewidth:150px; margin-left:10px el-option v-ford in deptList :keyd.id :labeld.name :valued.id / /el-select el-select v-modelsearch.status placeholder状态 clearable stylewidth:120px; margin-left:10px el-option label在职 :value1 / el-option label离职 :value2 / /el-select el-button typeprimary clickloadData搜索/el-button /div /template el-table :datatableData stripe el-table-column propusername label工号 width120 / el-table-column proprealName label姓名 width120 / el-table-column propgender label性别 width100 template #default{ row }{{ row.gender 1 ? 男 : 女 }}/template /el-table-column el-table-column propphone label电话 width150 / el-table-column propemail label邮箱 width200 / el-table-column propdeptName label部门 width150 / el-table-column propstatus label状态 width100 template #default{ row }{{ row.status 1 ? 在职 : 离职 }}/template /el-table-column /el-table div classpagination el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagepage.current :page-sizes[10, 20, 50] :page-sizepage.size layouttotal, sizes, prev, pager, next, jumper :totalpage.total / /div /el-card /div /template script setup import { ref, onMounted } from vue import request from /utils/request const search ref({ keyword: , deptId: null, status: null }) const tableData ref([]) const page ref({ current: 1, size: 10, total: 0 }) const deptList ref([]) // 加载部门列表供下拉框 const loadDeptList async () { const res await request.get(/dept/list) deptList.value res.data } // 加载员工列表 const loadData async () { const res await request.get(/user/list, { params: { current: page.value.current, size: page.value.size, ...search.value } }) tableData.value res.records page.value.total res.total } // 分页回调 const handleSizeChange (val) { page.value.size val loadData() } const handleCurrentChange (val) { page.value.current val loadData() } onMounted(() { loadDeptList() loadData() }) /script逻辑说明search对象绑定表单loadData()将search和分页参数合并发送res.records和res.total直接来自 MyBatis-PlusIPage的序列化结果无需手动解构page.getRecords()deptList在onMounted时预加载避免下拉框为空。3.4 Excel 导入导出用 Apache POI 自定义注解实现零反射硬编码导出功能常被毕设忽略但它是体现工程能力的关键点。不推荐用easyexcel依赖复杂答辩时解释不清原理改用原生 POI 注解驱动。定义导出字段注解Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface ExcelField { String value() default ; // 列名 int order() default 0; // 排序 }在User实体类上标注public class User { ExcelField(value 工号, order 1) private String username; ExcelField(value 姓名, order 2) private String realName; ExcelField(value 性别, order 3) private Integer gender; // ... 其他字段 }导出 ControllerGetMapping(/export) public void export(HttpServletResponse response) throws IOException { ListUser users userMapper.selectList(null); // 实际应加条件筛选 // 获取所有标注 ExcelField 的字段 Field[] fields User.class.getDeclaredFields(); Arrays.sort(fields, Comparator.comparingInt(f - { ExcelField ann f.getAnnotation(ExcelField.class); return ann null ? Integer.MAX_VALUE : ann.order(); })); // 创建 Excel Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(员工列表); // 写表头 Row headerRow sheet.createRow(0); for (int i 0; i fields.length; i) { Field f fields[i]; ExcelField ann f.getAnnotation(ExcelField.class); if (ann ! null) { Cell cell headerRow.createCell(i); cell.setCellValue(ann.value()); } } // 写数据 for (int i 0; i users.size(); i) { Row row sheet.createRow(i 1); for (int j 0; j fields.length; j) { Field f fields[j]; ExcelField ann f.getAnnotation(ExcelField.class); if (ann ! null) { f.setAccessible(true); Object val ReflectUtil.getFieldValue(users.get(i), f); Cell cell row.createCell(j); if (val instanceof Integer) { cell.setCellValue((Integer) val 1 ? 男 : 女); } else if (val ! null) { cell.setCellValue(val.toString()); } } } } // 输出 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment;filenameemployees.xlsx); workbook.write(response.getOutputStream()); }参数说明order()控制列顺序避免靠字段声明顺序ReflectUtil.getFieldValue()是 Hutool 工具类简化反射取值性别字段做1→男/2→女映射体现业务逻辑而非原始数字。4. 部署与 Nginx 配置把 Vue 打包文件塞进 SpringBoot 的 static 目录真可行吗4.1 Vue 打包用npm run build生成 dist但必须改vue.config.jsVue CLI 4 默认打包路径为dist/但 SpringBoot 的静态资源目录是src/main/resources/static/。直接复制会覆盖index.html的base href/导致路由 404。正确做法是在vue.config.js中配置module.exports { outputDir: ../backend/src/main/resources/static, // 注意路径指向后端 module publicPath: ./, // 关键改为相对路径否则刷新页面 404 assetsDir: static, indexPath: index.html }执行npm run build后backend/src/main/resources/static下生成├── index.html ├── static/ │ ├── css/ │ └── js/此时启动 SpringBoot访问http://localhost:8080即可看到 Vue 页面 ——但这是开发模式下的取巧生产环境必须用 Nginx 分离部署。4.2 Nginx 配置反向代理 静态资源托管解决跨域与路由问题在 Linux 服务器上安装 Nginx 后编辑/etc/nginx/conf.d/hrms.confupstream backend { server 127.0.0.1:8080; } server { listen 80; server_name hrms.example.com; # 替换为你的域名或 IP # 静态资源Vue 打包后的文件 location / { root /var/www/hrms-frontend/dist; try_files $uri $uri/ /index.html; # 解决 Vue Router history 模式刷新 404 } # API 接口代理到 SpringBoot location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # H2 控制台仅开发环境开放生产务必注释 location /h2-console { proxy_pass http://backend/h2-console; proxy_set_header Host $host; } }关键点try_files $uri $uri/ /index.html是 Vue Router history 模式的救命稻草否则点击二级路由如/employee/add直接 404location /api/的末尾斜杠/必须保留否则proxy_pass http://backend/会把/api/user/list变成http://backend/api/user/list后端实际监听/user/list。4.3 SpringBoot 静态资源配置关闭默认欢迎页避免与 Vue 冲突在application.yml中spring: web: resources: static-locations: classpath:/static/,classpath:/public/,classpath:/resources/,classpath:/META-INF/resources/ mvc: favicon: enabled: false # 关键禁用 SpringBoot 默认欢迎页让 Nginx 的 / 指向 Vue web: resources: add-mappings: false同时删除src/main/resources/static/index.html如果存在确保index.html仅由 Vue 打包生成。5. 避坑指南这 4 个血泪经验让答辩老师挑不出技术硬伤5.1 现象Vue 页面首次加载白屏Network 查看index.html返回 404原因Nginx 的root路径配置错误或try_files未生效。常见错误是root指向dist目录本身如/var/www/hrms-frontend/dist但index.html实际在dist/index.html而 Nginx 的location /匹配的是 URL 路径root拼接后变成/var/www/hrms-frontend/dist//index.html多了一个/。解决root必须指向dist的父目录即root /var/www/hrms-frontend;然后location /下try_files $uri $uri/ /dist/index.html;。或者更稳妥alias /var/www/hrms-frontend/dist/;注意末尾斜杠。5.2 现象登录成功后跳转/home但页面空白Console 报Uncaught SyntaxError: Unexpected token 原因Vue 路由history模式下Nginx 未配置try_files导致/home请求被当作静态文件查找找不到home.js就返回index.html的 HTML 内容浏览器试图把它当 JS 解析。解决确认 Nginx 配置中location /块包含try_files $uri $uri/ /index.html;且index.html文件确实存在于root指定路径下。5.3 现象Excel 导出中文乱码打开显示方块原因H2 数据库默认字符集为UTF-8但 POI 写入单元格时未设置字体Windows 系统默认宋体不支持部分 Unicode 字符。解决在 POI 写入前创建字体并应用Font font workbook.createFont(); font.setFontName(微软雅黑); font.setFontHeightInPoints((short) 10); CellStyle style workbook.createCellStyle(); style.setFont(font); // 然后 cell.setCellStyle(style)5.4 现象SpringBoot 启动报错Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration原因spring-boot-starter-web依赖缺失或pom.xml中spring-boot-starter-parent版本与 SpringBoot 2.7.x 不匹配如用了 3.x 的 parent。解决检查pom.xml顶部parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 必须与 SpringBoot 版本一致 -- relativePath/ /parent并确认spring-boot-starter-web在 dependencies 中存在且未被scopetest/scope误标。6. 进阶技巧用 Swagger 生成接口文档 自定义 Banner 提升答辩质感6.1 Swagger 集成让老师 3 秒看清你写了几个接口SpringBoot 2.7.x 适配的是springfox-swagger2而非springdoc-openapi后者为 SpringBoot 3.x 设计。在pom.xml中添加dependency groupIdio.springfox/groupId artifactIdspringfox-swagger2/artifactId version2.9.2/version /dependency dependency groupIdio.springfox/groupId artifactIdspringfox-swagger-ui/artifactId version2.9.2/version /dependency创建配置类SwaggerConfig.javaConfiguration EnableSwagger2 public class SwaggerConfig { Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.hrms.controller)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(人事管理系统 API 文档) .description(基于 SpringBoot 2.7.x Vue 3 的毕设项目接口说明) .version(1.0) .build(); } }启动后访问http://localhost:8080/swagger-ui.html自动生成可交互文档。答辩时直接打开此页面指着/user/list接口说“老师您看这个接口支持分页、关键词搜索、部门筛选三个参数返回值是 IPage 我用 MyBatis-Plus 的 Page 对象直接序列化避免手动封装”—— 比口头描述有力十倍。6.2 自定义 Banner用 ASCII Art 让启动日志成为个人品牌SpringBoot 启动时的 banner 是展示细节的绝佳位置。生成工具推荐 https://www.bootschool.net/ascii-art输入 “HRMS” 生成。将生成的 ASCII 文本保存为src/main/resources/banner.txt██████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██████╔╝██████╔╝██║ ██║█████╗ ██████╔╝ ██╔═══╝ ██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ██║ ██║ ██║╚██████╔╝███████╗██║ ██║ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ SpringBoot Vue 人事管理系统在application.yml中启用spring: main: banner-mode: console # 可选关闭 ANSI 颜色某些终端不支持 # output: # ansi: # enabled: never启动时第一行不再是默认的 Spring 字符画而是你的定制 banner ——这不是炫技是告诉老师我连启动日志都认真对待代码质量不会差。6.3 真实部署 checklist答辩前必验的 5 件事检查项验证方式不通过后果1. Nginx 静态资源可访问curl http://your-server-ip/返回index.html内容Vue 页面无法加载2. API 代理正常curl http://your-server-ip/api/auth/login模拟登录后端接口 404系统瘫痪3. Token 持久化登录后 F12 → Application → LocalStorage 查看token是否存在登录态丢失无法进入系统4. 路由守卫生效直接访问http://ip/employee/list未登录应跳转/login权限控制失效安全漏洞5. Excel 导出可下载点击导出按钮浏览器弹出下载框文件能用 Excel 打开功能完整性受质疑我带过 12 届毕设最常翻车的是第 4 条 —— 很多同学只测了登录后点击菜单跳转没测直接输入 URL。我的习惯是答辩前一晚用手机浏览器访问部署地址手动输入所有路由路径确保每个页面都有权限校验。这 5 分钟能让你在答辩时面对“如果绕过登录直接访问接口会怎样”的提问从容回答“老师我测试过未登录时访问/api/user/list会返回 401前端路由守卫和后端 JWT 过滤器双重拦截这是我在AuthInterceptor类里写的逻辑”。希望帮到你。本文还有配套的精品资源点击获取