简介这是一份面向Java方向毕业设计、课程设计与期末大作业场景的SpringBootVue前后端分离人力资源管理系统源码包。系统覆盖员工信息、考勤、薪资等常见模块代码注释完整新手也能上手部署适合需要完整项目参考或快速交付的高校学生。压缩包共909个文件大小约31.51MB以java/vue源码、svg图标、js/css前端资源、xml配置和sql数据库脚本为主另附启动脚本与部署说明便于本地环境一键运行建议配合MySQL 5.7与Tomcat 7/8使用。目前已有1195人学习下载项目经过调试确保可运行作为毕设或课设可直接修改扩展。内含前后端完整代码、数据库脚本及相关工具目录结构清晰能节省从零搭建系统的时间具有较高的实际参考价值。1. 这个毕设题目到底要交付什么先看清HR系统的边界学生选题时看到“基于SpringBootVue的现代企业人力资源管理系统设计与实现”第一反应往往是一套增删改查。方向没错但这类题目真正拉开差距的地方在于数据模型是否完整、前后端交互是否规范、答辩时能不能拿出验证数据。系统通常覆盖组织架构、员工档案、考勤、薪资、用户登录等模块全部是管理后台形态没有C端复杂交互。从数据库设计到后端接口再到前端页面和联调排错每个环节都有能直接抄作业的落地细节适合正在做Java毕业设计、需要快速跑通一套全栈项目的读者。2. 从数据库到后端接口用SpringBoot把HR系统的主干搭出来2.1 数据库表设计部门、员工、考勤、薪资四张表怎么建模HR系统的业务核心在组织架构和人员数据。多数毕设都会选MySQL因为资料多、环境好配而且mybatis的分页插件在springboot框架里整合得最顺。表设计不需要一步到位做成企业级规范但至少要满足三个要求字段命名统一、外键关系清晰、预留状态字段方便后续扩展。以员工表为例一个能支撑查询、统计、联动的建表方案如下CREATE TABLE department ( id BIGINT AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL, parent_id BIGINT DEFAULT 0 COMMENT 上级部门ID0表示根部门, sort_order INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号手动或规则生成, name VARCHAR(30) NOT NULL, gender TINYINT COMMENT 1男 0女, phone VARCHAR(20), dept_id BIGINT NOT NULL, position VARCHAR(50), status TINYINT DEFAULT 1 COMMENT 1在职 0离职, hire_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_dept_id (dept_id), CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的关键参数是parent_id和dept_id。department表用parent_id表达树形结构前端渲染成级联下拉或树控件都很直接比单独建一张层级表省事employee的dept_id关联部门表查询员工时一条join SQL就能拿到部门名。工号emp_no建议加唯一索引因为后续考勤、薪资都要拿它做关联键重复会产生脏数据。status用TINYINT而不是VARCHAR是为了让扩展枚举值更方便例如1在职、0离职、2停薪留职等状态都可以按数字维护。考勤表和薪资表的设计思路类似考勤按员工日期做唯一键薪资按员工月份做唯一键防止重复录入CREATE TABLE attendance ( id BIGINT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL, work_date DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT 1正常 0迟到 2早退 3缺勤, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_date (emp_no, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE salary ( id BIGINT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL, month VARCHAR(7) NOT NULL COMMENT 格式 2024-06, base_salary DECIMAL(10,2), bonus DECIMAL(10,2), deduct DECIMAL(10,2), actual_salary DECIMAL(10,2), UNIQUE KEY uk_emp_month (emp_no, month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我一般会建议再补一张sys_user表做登录账号通过emp_no与employee表关联而不是直接在员工表上塞password字段。理由是账号权限和人员档案应当分开管理答辩时可以多讲一层“权限解耦”的设计意图。对于毕设来说多一张表多一个标准功能点性价比很高。假如压缩包里的数据库脚本只有四张核心表这也是一个合理的扩展方向把用户表补上即可。2.2 SpringBoot工程结构配置项与CRUD代码怎么组织后端工程建议用SpringBoot MyBatis-Plus而不是原生MyBatis。毕设的核心工作量在业务场景设计单表CRUD用MyBatis-Plus的IService和LambdaQueryWrapper可以省掉大量样板代码分页、条件构造都是现成的。前提是springboot配置写对尤其是数据源和驼峰映射server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hr_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 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 configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: autoserverTimezone必须指定为Asia/Shanghai否则MySQL 8.x启动或首次连接会报时区错误。map-underscore-to-camel-case设为true之后数据库列dept_id直接映射到实体属性deptId不用在每个字段上写TableField。jackson的date-format只对java.util.Date类型生效如果实体用了LocalDateTime要么依赖jsr310模块要么给字段加JsonFormat注解这个坑在第四章详细展开。另外SpringBoot 2.x默认用HikariCP作为数据库连接池毕设一般不用调参数但论文里可以提一句maximum-pool-size默认值演示并发请求时注意连接数上限。Controller层要统一返回结构不要散装返回Map否则前端每个页面都要单独处理错误码。常见做法是自定义Result 包含code、message、data三个字段业务成功一律返回Result.success(data)。查询接口用MyBatis-Plus的Page对象接收分页参数RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public ResultIPageEmployee page(RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword) { PageEmployee page new Page(current, size); LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Employee::getName, keyword) .or().like(Employee::getEmpNo, keyword)); } wrapper.orderByDesc(Employee::getCreateTime); return Result.success(employeeService.page(page, wrapper)); } }这段代码里最容易抄错的是LambdaQueryWrapper的条件分组。写成wrapper.like(...).or().like(...)时如果后面还有其它条件MyBatis-Plus生成的SQL会破坏优先级正确做法是像上面这样用wrapper.and(w - w.like(...).or().like(...))包一层保证关键字搜索的括号语义正确。current和size用defaultValue兜底前端漏传参数时也能正常分页。2.3 登录鉴权用JWT保护薪资和考勤接口HR系统里的薪资、考勤属于敏感数据裸奔访问会被答辩老师直接质疑。毕设规模适合用JWT配合拦截器实现不建议引入Spring Security全家桶因为它的过滤器链和UserDetailsService对小项目来说太沉重调试成本高。登录接口校验用户名密码通过后签发一个带过期时间的token核心逻辑如下RestController RequestMapping(/api/auth) public class AuthController { Value(${jwt.secret}) private String jwtSecret; Autowired private EmployeeService employeeService; PostMapping(/login) public ResultMapString, Object login(RequestBody LoginParam param) { Employee emp employeeService.checkLogin(param.getUsername(), param.getPassword()); String token Jwts.builder() .setSubject(emp.getEmpNo()) .claim(empNo, emp.getEmpNo()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); return Result.success(Map.of(token, token, empNo, emp.getEmpNo())); } }签发token时不建议把密码、部门这类敏感字段塞进ClaimsJWT默认只做Base64编码不是加密放在里面的信息谁都能解出来。过期时间按24小时设置比较适合演示场景太短会导致答辩现场突然掉登录太长又体现不出安全的严谨性。token校验用一个HandlerInterceptor实现比Filter更好用因为preHandle方法能拿到HandlerMethod参数方便后续做角色级别的细粒度校验Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String jwtSecret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } try { Claims claims Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(authHeader.substring(7)) .getBody(); request.setAttribute(empNo, claims.get(empNo)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token无效或已过期\}); return false; } } }首先判断handler是否为HandlerMethod是为了放行静态资源和浏览器预检请求。拦截器注册在WebConfig里addPathPatterns用/api/**登录接口单独放行Configuration public class WebConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login); } }excludePathPatterns漏写静态资源路径是常见失误表现是启动后页面加载不出来接口能通但页面空白。放行写法建议精确到接口层不要为了省事直接放行/api/**那等于白做鉴权。3. Vue前端工程环境配置、组件封装与路由守卫3.1 Vue环境与工程初始化从Node、Vite到Element Plus前端技术栈建议Vue3 Vite Element Plus比Vue2加vue-cli的组合更能体现“现代企业”的题目定位。Vue3的Composition API在毕设答辩时也更好讲路由用vue-router 4状态管理用pinia。如果指导老师指定用vue-cli也可以按vue-cli流程初始化核心逻辑完全一样。先确认Node版本Vue3的工程要求Node 16以上用node -v检查。然后用vite创建项目npm create vitelatest hr-frontend -- --template vue cd hr-frontend npm install npm install axios element-plus vue-router4 pinia npm run devvue安装依赖这一步经常因为网络问题卡住npm默认源慢时可以临时切换镜像再装装完把依赖信息都留在package.json里方便换机器复现。开发过程中建议装一个Vue Devtools插件排查组件props传递和路由跳转异常会轻松很多。新建工程后main.js里全局注册Element Plus注意补上中文语言包import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/es/locale/lang/zh-cn import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(ElementPlus, { locale: zhCn }) app.use(router) app.use(createPinia()) app.mount(#app)Element Plus的locale参数不要漏很多人只装了组件库打开页面发现日期选择器和分页器全是英文就是这个原因。中文语言包用element-plus/es/locale/lang/zh-cn引入注册组件时附带配置即可。3.2 Axios请求封装拦截器、token注入与401跳转前端直接在每个页面里调axios也能跑但后端返回401时每个页面都要写跳转逻辑token也需要手动塞进Header。正规做法是把axios实例封装成一个request模块两个拦截器分别处理请求和响应import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(hr_token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message || Error)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(hr_token) router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default requesttoken键名统一用hr_token别在localStorage里同时存好几个变量名会导致排查问题时不知道读的是哪一份。baseURL写成/api是有意为之Vite的devServer把/api代理到后端8080端口这样代码里只有相对路径打包部署后切换域名只改一处代理配置。响应拦截器里判断res.code业务失败统一弹Message401时清掉本地token跳回登录页前端代码里不会再出现散装的错误处理。对应的vite代理配置export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })changeOrigin必须设为true否则代理转发后请求头里的Origin仍旧是前端端口后端一旦开启CORS白名单校验就会拦截。如果后端接口路径没有/api前缀还需要用rewrite把前缀去掉这一项在4.1里展开。3.3 路由与页面骨架登录守卫和菜单权限的两种写法vue-router 4的路由配置可以拆成静态路由加动态路由。静态路由指登录页、首页框架这类所有角色都能访问的页面动态路由是在登录后用router.addRoute按角色追加。毕设里大多数场景用静态路由加meta角色标记就够了演示效果和动态路由差不多代码量少一半。登录守卫直接写在beforeEach里const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/MainLayout.vue), redirect: /dashboard, children: [ { path: employee, component: () import(/views/employee/EmployeeList.vue), meta: { title: 员工管理 } }, { path: attendance, component: () import(/views/attendance/AttendanceList.vue), meta: { title: 考勤管理 } }, { path: salary, component: () import(/views/salary/SalaryList.vue), meta: { title: 薪资管理 } } ] } ] router.beforeEach((to, from, next) { const token localStorage.getItem(hr_token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })这段守卫只校验登录态适合单角色演示。如果你做了管理员和普通HR两种角色可以在meta里声明requiredRole再在守卫里读取pinia里保存的角色值做比对。动态路由方案需要处理刷新丢路由的问题页面一刷新addRoute注册的路由会丢失结果跳到404需要在路由守卫里做一次“当前路由不存在且有权限”时的补救注册这也是答辩高频追问点。员工列表页是典型的CRUD页面用el-table加el-pagination加el-dialog组合。后端返回的分页数据结构是两层包装Result里的data字段才是分页对象分页对象里又有records和total前端取值要写成res.data.records而不是res.records。这种字段错位在前后端联调期出现频率极高提前在request封装层统一处理可以省不少事。4. 联调与部署排错SpringBootVue的5个高频坑4.1 联调前必查的三项配置端口、代理、数据源前后端各写各的、一联调就崩是毕设阶段最磨人的环节。动手联调之前先把三处配置过一遍能消掉八成的问题。第一是端口。后端8080、前端3000确认vite proxy的target端口和springboot的server.port一致。很多同学改了application.yml但没同步vite的target页面一直报网络错误就是这个原因。第二是代理路径。vite的proxy按前缀匹配/api开头的请求会转发给后端。后端接口的RequestMapping也带/api前缀时不需要重写路径如果后端接口是/employee/list需要配置rewrite把/api去掉否则会404。第三是数据源。本地MySQL的root密码是否和配置一致、数据库脚本是否已执行这两个没对齐时后端可能依然启动成功但一请求业务接口就报表不存在。建议启动后先调一个最简单的接口验证数据库连通再开始写页面。注意联调阶段优先用代理解决跨域后端不要再同时开CORS否则一次请求要经历两次预检报错信息相互掩盖反而更难排查。4.2 五个高频坑现象、定位与修复坑1跨域请求被浏览器拦截。现象浏览器控制台报Access-Control-Allow-Origin缺失F12 Network里请求连后端都没到。原因前端3000端口直接请求8080跨端口触发同源策略。解决按3.2的配置使用vite proxy。需要后端开放接口给第三方调用时再加CORS注意SpringBoot 2.4要用allowedOriginPatterns配合allowCredentials旧版allowedOrigins(*)在credentials开启时会被拒绝。坑2MySQL 8连接报时区错误。现象启动报“The server time zone value ... is unrecognized”或首个请求超时。原因MySQL 8.x默认时区不是UTC驱动要求显式声明serverTimezone。驱动类名也从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver。解决数据源配置写成2.2节的写法driver-class-name用com.mysql.cj.jdbc.Driverurl末尾追加serverTimezoneAsia/Shanghai和characterEncodingutf8。坑3jjwt版本API差异导致编译失败。现象setSigningKey或parseClaimsJws方法找不到或者参数类型报错。原因jjwt 0.11.x的API比0.9.x改动很大0.11.x要求用SecretKey表示密钥Claims解析参数也变了。网上解决方案新旧混杂照抄容易翻车。解决pom锁定0.9.1版本并补jaxb-api这是最容易一次编译通过的组合dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency如果已经有代码基于0.11.x就把setSigningKey改成Keys.hmacShaKeyFor(secret.getBytes())签名验证方法也跟着新API调整千万别两个版本的写法混在一个项目里。坑4MyBatis-Plus分页返回total始终为0。现象page接口有数据但total0或只有第一页记录返回。原因MyBatis-Plus 3.4默认不启用物理分页没注入PaginationInnerInterceptor时Page参数不会拼进SQL。解决项目里新增配置类注入分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType必须和实际数据库类型一致MySQL写DbType.MYSQLPostgreSQL写PG写错类型分页SQL生成会异常。这个配置类放在当前启动类能扫描到的包路径下否则不生效。坑5时间字段渲染成ISO格式。现象前端表格显示2024-06-01T16:00:00.00000:00。原因Jackson对LocalDateTime默认用ISO-8601序列化且不带时区。解决字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)或者在配置类里统一注册LocalDateTimeSerializer。两种方式选一种全局配置更省事字段注解适合只有个别地方需要特殊格式的场景。5. 答辩前做三件事把系统从“能跑”变成“经得起问”第一件事是接入OpenAPI/Swagger。Spring Boot工程里加springdoc-openapi依赖启动后就能看到接口文档。它不只是给答辩老师演示用的更重要的是强迫你补齐每个接口的参数说明和返回结构说明——写文档的过程往往能暴露隐藏的字段错位问题。第二件事是录制一套核心流程的操作验证。登录、新增员工、录考勤、算薪资这四个动作串起来就是完整的业务闭环。用Postman保存成集合每次改动后跑一遍回归比在页面上手动点鼠标高效得多。答辩前把关键接口的响应截图放进论文比放一堆代码截图有说服力。第三件事是补一个全局异常处理。Controller里只写业务逻辑异常统一抛给RestControllerAdvice前端拿到的是友好的中文提示而不是堆栈。测试时故意输入错误密码、删除不存在的数据观察系统是否稳定返回统一错误格式这几乎是答辩老师必问场景。这套系统做完最大的收获不是那几百行CRUD代码而是完整走了一遍全栈链路——从建表字段怎么选到前后端字段名对不上怎么排查再到部署时环境配置怎么改。见过不少同学拿到毕设源码不知道从哪下手其实按“先导入数据库脚本、再启动后端、最后启动前端”的顺序走问题都能逐层定位。我自己做项目时也常遇到配置看着没问题就是不通的玄学时刻后来养成了先看日志再动手的习惯联调时间压缩了一半。希望这篇拆解能帮到你少走我走过的那段弯路。本文还有配套的精品资源点击获取