SpringBoot+Vue+MyBatis+MySQL打造前后端分离人事管理系统

SpringBoot+Vue+MyBatis+MySQL打造前后端分离人事管理系统
前后端分离这几年基本成了中小型企业内部系统的事实标准而人事管理系统又是刚需中的刚需。我接手过不少类似的单体老项目也帮人把SSH那套旧代码迁移到SpringBootVue上来前后端分离这个架构在中型企业内部系统里的优势非常明显前端只管交互后端专注业务和数据团队协作边界清晰部署也能各自独立伸缩。如果你正打算自己写一套人事管理系统或者想把学校/公司的课设项目升级成能说服面试官的作品这套基于SpringBootVueMyBatisMySQL的前后端分离实现方案值得你从头到尾走一遍。这个项目覆盖了中小企业人事管理的核心场景员工档案管理、部门维护、考勤打卡记录、请假审批流、薪资信息管理外加基于JWT的登录鉴权和基于Vue Router的前端路由守卫。相比网上那些只做了单表CRUD的“玩具项目”这套系统的业务完整度、代码结构、部署方式都更贴近真实企业项目可以直接作为课程设计、毕业设计或者入职前练手的主力项目。1. 项目到底做了什么核心模块与技术选型思路1.1 人事管理系统需要覆盖哪些业务很多新手上来就急着写代码结果做着做着发现业务边界根本收不住。我建议先盘一下中小企业人事管理的真实诉求再决定做哪些功能。实际调研下来一套能落地的人事系统至少要包含员工管理、部门管理、考勤管理、请假管理和薪资管理这几块。员工管理是核心所有其他模块几乎都围绕员工展开。一张员工主表要能覆盖基本信息姓名、性别、出生日期、身份证号、手机号、邮箱、岗位信息所属部门、职位、职级、入职时间、转正时间和状态信息在职、试用、离职。这些字段在设计表结构时就要想清楚不然后期加字段非常被动。部门管理看起来简单但要有树形结构。中小企业虽然部门不多但层级是存在的技术部下面还要分前端组、后端组所以部门表必须支持无限层级用parent_id做自关联。考勤和请假是关联性很强的两块。考勤记录每天产生一次请假审批通过后要能影响考勤结果。这个逻辑在中小企业的规则可以简化但流程必须完整——员工提交申请、直属主管审批、人事归档。薪资管理比较敏感一般人事实操中薪资数据只有HR和管理层能看到所以权限控制在这里要比其他模块严格前端要控制按钮级权限后端也要做角色校验不能只靠隐藏按钮。1.2 为什么选SpringBootVue这套组合前后端分离方案现在是主流但具体选型各有说法。我见过用若依框架快速生成的也见过用微服务硬套的对中小企业人事系统来说SpringBootVueMyBatisMySQL是投资回报率最高的组合没有之一。SpringBoot在Java后端领域的统治力不用多说。它解决的问题是Spring时代繁琐的XML配置内嵌Tomcat让部署变成一条java -jar命令。对中小企业来说团队里招Java的人远比招其他语言的人容易生态成熟度也摆在那里。Vue在中小型管理系统前端方案里的优势是渐进式、上手快、中文文档完善。虽然React也很好但在国内中小企业里Vue的普及率是真的高Element UI组件库又是为后台管理系统量身定做的表格、表单、弹窗、树形控件全是现成的能省掉大量重复造轮子的时间。数据库选MySQL是稳的选择社区版免费、性能足够、运维资料满天飞。人事管理系统的数据量在中小企业的量级下MySQL完全轻松应对再加上事务支持、外键约束等能力数据一致性能得到保障。1.3 为什么用原生MyBatis而不是MyBatis-Plus这个选择值得单独拿出来讲。现在很多人一上来就推MyBatis-Plus确实它的BaseMapper封装了单表CRUD代码量少了很多。但在这个项目里我用的是原生MyBatis而且我建议你在这个项目里也用原生MyBatis。原因是人事管理系统的查询逻辑复杂经常是多表关联查询单表CRUD的便利性派不上多少用场反而MyBatis-Plus的逻辑删除、乐观锁这些特性在复杂SQL面前有时候还会碍事。热搜词里就有一条“mybatis plus 查询 禁用逻辑删除”说明很多人被这个坑过。原生MyBatis的XML SQL是明明白白写出来的SQL执行效果一眼就能看穿排查问题非常直接。另外如果你是从零开始学习框架底层原理手写XML能让你更理解MyBatis的参数映射、动态SQL和结果集映射机制。等你把原生MyBatis玩透了再看MyBatis-Plus的封装就觉得非常简单。注意如果你确实想用MyBatis-Plus记得在关联查询时要搞清楚逻辑删除的影响范围避免出现“明明删掉了还能查出来”或者“关联表条件被自动注入导致查不到”这类诡异问题。2. 后端从零搭建SpringBootMyBatisMySQL的实现细节2.1 项目结构设计与数据库表结构规划后端项目我强烈建议用标准的分层架构不要图省事把代码全塞在Controller里。一个清晰的结构能让你在代码量大起来之后依然保持清醒com.example.hrms ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config ├── common │ ├── result │ ├── exception │ └── utils └── interceptorentity对应数据库表一个字段名都不能马虎dto接收前端参数做数据校验vo返回给前端控制接口不能把敏感字段比如密码哈希暴露出去。这套分层在面试里也是高频考点值得认真对待。数据库表的设计我列一下核心几张表员工档案表employeeid、emp_no工号、name、gender、birth_date、id_card、phone、email、department_id、position、hire_date、status、created_time、updated_time。部门表departmentid、parent_id、name、leader_id、created_time。考勤表attendanceid、emp_id、attendance_date、check_in_time、check_out_time、status正常、迟到、早退、缺卡。请假申请表leave_requestid、emp_id、leave_type年假、事假、病假、start_time、end_time、reason、status待审批、通过、驳回、approver_id、approve_time。薪资表salaryid、emp_id、basic_salary、performance_bonus、allowance、social_security_fund、month、actual_salary。所有表都加created_time和updated_time是通用好习惯排序和排查问题都方便。工号和身份证号要建唯一索引因为这是业务上必须保证唯一的字段。2.2 MyBatis配置与SQL编写要点SpringBoot集成MyBatis的配置非常简洁但有几个点很容易栽跟头。spring.datasource配置里时区参数要写对推荐使用serverTimezoneAsia/Shanghai否则数据库连接池会报时区错误。还有url里的参数useSSLfalse要加上本地开发环境不需要SSL加密。mapper-locations要注意配置正确如果XML放到了resources目录下的mapper文件夹记得在application.yml里写清楚mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.hrms.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必须开启这样数据库的created_time才能自动映射到实体字段createdTime不然你就要在ResultMap里逐个字段手动映射累且容易出错。日志打印这个问题也值得说两句热词里有“mybatis配置打印”这个需求很真实。开发阶段一定要有SQL日志否则报错时根本不知道SQL执行到哪一步。上面的log-impl配置成StdOutImpl就可以在控制台直接看到完整SQL和参数值。复杂多表查询建议写在XML里比如查询员工列表需要关联部门名称这时候在Mapper接口上写一个方法ListEmployeeVO selectEmployeePage(Param(offset) int offset, Param(size) int size, Param(keyword) String keyword);XML里写动态SQL注意员工姓名、工号、部门这三个维度的条件组合select idselectEmployeePage resultTypecom.example.hrms.vo.EmployeeVO SELECT e.*, d.name AS department_name FROM employee e LEFT JOIN department d ON e.department_id d.id where if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.emp_no LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY e.create_time DESC LIMIT #{offset}, #{size} /select这里有个细节动态SQL里的if判断MyBatis的OGNL表达式判断字符串为空要用! null and ! 不能只判断null这是初用MyBatis的人最容易漏的。另外建议用CONCAT拼接模糊查询而不是直接在#{}里写%${keyword}%后者有SQL注入风险。2.3 统一响应体、全局异常处理与登录鉴权写接口的时候如果每个接口的返回格式都自己定前端就会被你坑惨。统一的响应结构非常关键我在项目里是这么封装的public class ResultT { private Integer code; // 200成功其他失败 private String message; private T data; // 静态方法 success(), error() 省略 }所有Controller的返回值都包一层Result前端Axios拦截器就可以统一拦截处理登录失效返回401时统一跳转到登录页业务异常返回对应错误码时统一弹提示。这样代码整洁度提升一个档次。全局异常处理用SpringBoot的RestControllerAdvice就能搞定。要处理的异常包括参数校验异常MethodArgumentNotValidException、业务异常自定义BusinessException、运行时异常Exception兜底。这里有个坑参数校验异常如果不用Validated注解配合Valid使用前端传错参数时后端返回的message会很底层不友好。登录鉴权这个项目用JWT实现。用户登录成功后后端生成一个token返回给前端前端把token存在localStorage里每次请求在请求头里带Authorization: Bearer xxx。后端写一个拦截器对需要登录的接口进行token校验。拦截器的核心逻辑从请求头取出token解析JWT得到用户ID和角色放入ThreadLocal或者request attribute里方便后续业务代码取用。判断接口是否需要鉴权可以用注解方式也可以在配置里放白名单。这个项目里登录接口、静态资源不走拦截器其余全部拦截。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; // 预检请求直接放行 } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } // 解析token、校验、存入上下文 Long userId JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(userId); return true; }注意OPTIONS请求必须放行否则前后端分离部署时浏览器跨域预检请求会全部被拦截前端会报一堆看似莫名其妙的CORS错误。2.4 角色权限控制的设计人事系统的权限控制和普通系统不太一样员工自己只能看到自己的档案部门主管能审批本部门的请假申请HR能管理所有员工信息管理员还能配置部门。这个在RBAC基于角色的访问控制模型里可以落地。数据量小的话不用搞太复杂的五表RBAC三张表就够——用户表和角色表关联角色和权限点关联。我在这个项目里做的是用户在员工表上加了role字段分ADMIN、HR、MANAGER、EMPLOYEE四种角色然后写一个权限注解拦截器的方法Retention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface RequireRole { String[] value(); }在需要权限控制的接口上标注比如只有ADMIN和HR才能操作薪资数据RequireRole({ADMIN, HR}) PostMapping(/salary) public Result? saveSalary(RequestBody SalaryDTO dto) { ... }拦截器里通过反射拿到方法上的RequireRole注解判断当前用户角色是否在允许列表里。这套方案比起Spring Security要轻量得多对中小企业系统完全够用面试的时候也能讲清楚设计思路。3. 前端Vue实现从环境配置到页面落地3.1 Vue环境准备和工程初始化前端这块第一步就是环境这一步卡住了不少新手。首先要装Node.js版本别太新也别太旧建议用16.x或者18.x的LTS版本。Node版本太新可能导致node-sass这类依赖装不上太老又跑不动新版的Vue CLI。npm源建议切换到国内镜像不然装依赖能等到怀疑人生npm config set registry https://registry.npmmirror.com然后创建Vue项目。如果你用的是Vue CLI 5.x版本创建命令是vue create hrms-web手动选择功能勾选Router、Vuex、AxiosAxios在插件里选或者后续单独安装。项目创建完成后安装Element UInpm install element-ui --saveVue 2项目用Element UIVue 3项目要用Element Plus这个别搞混了。开发调试强烈建议装Vue Devtools浏览器插件。装了它之后你能在浏览器里直观看到组件的data、computed、props的值也能在Vuex面板里看到状态的变化路径排查问题效率提升一半以上。3.2 路由设计、Axios封装与权限控制前端路由的设计要跟后端菜单逻辑对应。人事管理系统的路由结构大概是这样的const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: Dashboard }, { path: employee, name: EmployeeList, component: EmployeeList, meta: { roles: [ADMIN, HR] } }, { path: attendance, name: Attendance, component: Attendance }, { path: leave, name: LeaveRequest, component: LeaveRequest }, { path: salary, name: Salary, component: Salary, meta: { roles: [ADMIN, HR] } } ] } ]路由守卫实现登录验证和角色拦截。beforeEach钩子里先判断有没有token没token一律踢回登录页有token再判断meta里的roles当前用户角色不在列表中就直接拒绝访问。axios封装这块请求和响应拦截器一个都不能少。请求拦截器往headers里塞token响应拦截器做统一错误处理——code为401时清空登录状态跳转登录页code不为200时ElMessage直接弹错误提示。这样业务代码里就不用每个请求都写了一堆重复的try-catch了。Vue Router的传参问题也常被问到。跳转编辑员工页面时要带员工ID过去this.$router.push({ path: /employee/edit, query: { id: row.id } })是简单的方案但刷新页面后参数还在。用params传参刷新会丢所以要慎用。在人事系统里建议用query或者把ID放在路由的参数位上保证刷新后还能拿到。3.3 核心页面实现员工列表的完整交互员工列表是人事系统的门面页面也是前后端交互最复杂的页面。它包含搜索区域、按钮区域新增、编辑、删除、导出、表格区域和分页区域。表格区域我用的是Element UI的el-table列配置里要注意formatter格式化日期和状态的显示。比如状态字段在数据库里是0/1/2试用/在职/离职直接显示数字肯定不行要用formatter函数翻译成中文再配合el-tag的type属性用不同颜色区分。新增和编辑共用一个弹窗对话框这是写表单页面时最高频的需求。做法是弹窗里放一个el-formopen弹窗时根据是否有id决定是新增还是编辑。新增时清空表单编辑时调用后端接口获取详情回填表单。保存按钮的loading状态一定要加防止用户重复提交。分页这块要注意el-pagination组件的current-page和page-size要和后端接口的参数一一对应搜索条件变化后要把当前页重置为1不然会出现搜出来第5页数据这种反人类体验。handleSearch() { this.queryParams.pageNum 1; // 搜索必须重置页码 this.loadData(); }, handlePageChange(page) { this.queryParams.pageNum page; this.loadData(); }这个重置页码的细节很容易被忽略但用户体验上非常重要。4. 完整部署流程从本地联调到服务器上线4.1 MySQL安装与数据库初始化数据库这块的坑大部分人都在MySQL安装环节踩过。Windows下安装MySQL 8.x要注意新版是MSI安装包一路Next的时候注意选对MySQL Server版本和端口默认端口3306不要和已有服务冲突。MySQL 8.x默认的认证插件是caching_sha2_password如果你的JDBC驱动版本太老连不上数据库会报Authentication plugin错误。解决办法是换MySQL Connector/J 8.x版本驱动或者创建用户时指定mysql_native_password。数据库初始化工作的核心是执行SQL脚本。我习惯把建库建表、初始数据分两个脚本文件一个schema.sql、一个data.sql。初始数据里必须有一条管理员账号密码存BCrypt加密后的哈希值不能存明文。Docker环境下安装MySQL则更简单跑一条命令就完事docker run -d --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -e MYSQL_DATABASEhrms mysql:8.0能用Docker就尽量用Docker省去环境安装的时间而且以后要换服务器容器迁移起来也方便。4.2 后端打包与启动后端打包前一定要确认application.yml里的环境配置。生产环境的数据库地址、账号、密码要改对不能拿着本地数据库配置去部署服务器。打包命令很简单mvn clean package -DskipTests跳过测试的-DskipTests参数务必加上不然如果项目里写了单元测试会因为环境问题导致打包失败。打包完成后target目录下会生成jar文件。启动命令我推荐用nohup放到后台执行并且指定JVM参数nohup java -jar -Xms512m -Xmx1024m hrms-server.jar --spring.profiles.activeprod startup.log 21 进程日志输出到startup.log之后排查问题就看这个日志文件。生产环境建议再配合--spring.config.location指定外部配置文件这样改配置不用重新打jar包。4.3 前端打包与Nginx部署前端打包前需要改API请求的baseURL让请求指向后端服务的真实地址。这里要说明一下“反向代理”的概念——前端通过Nginx转发API请求到后端解决跨域问题这是生产环境的标准做法。server { listen 80; server_name your.domain.com; root /opt/hrms-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files那行是Vue路由history模式部署的命根子。Vue Router用了history模式的话浏览器直接访问某个子路由路径会404必须配置try_files把请求指向index.html由前端路由接管。如果你实在不想配置Nginx也可以走hash模式URL上会多一个#号但好处是部署时不用额外配置。前端打包命令npm run build打包产物在dist目录把dist里的文件上传到服务器上的/opt/hrms-web/dist目录就可以了。4.4 跨域问题的本质与解决思路前后端联调时跨域问题是绕不开的。浏览器同源策略规定不同端口、不同域名、不同协议的请求都算跨域。开发环境最容易撞到这个前端跑在8080端口后端跑在8081端口两个端口就不算同源。开发环境解决方案是配置Vue CLI的devServer代理devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这样前端请求/api/xxx会被Vite或者webpack-dev-server转发到后端浏览器看到的都是同源的。生产环境则用Nginx的proxy_pass解决。后端方面也要配合配置跨域我推荐写一个全局的CorsConfig类实现WebMvcConfigurer接口addCorsMappings里允许来源、方法和请求头。前后端同时配置好联调才能顺畅。5. 实操中踩过的坑常见问题与排查技巧5.1 SpringBoot版本太高引发的连环坑这个点必须重点说。现在新项目一创建SpringBoot很容易拉到2.7.x甚至3.x版本。如果你跟着老教程做很多配置都对不上报错信息又看不懂。SpringBoot 3.x要求JDK 17以上如果你机器上装的是JDK 8直接跑不起来。Spring Boot 3.x的javax.servlet包也换成了jakarta.servlet导致很多第三方库的兼容性问题。热词里“springboot版本太高”这条搜索热度不是没有原因的我建议这个项目用SpringBoot 2.7.x加上JDK 8的经典组合网上资料最全踩坑最少。进到2.7.x版本内部也有一个坑SpringBoot 2.4版本开始spring.config位置和profile配置规则变了src/main/resources下的application.properties和application.yml如果同时存在会有加载优先级问题。解决办法是只用application.yml不用properties避免混乱。5.2 MyBatis相关的几个高频报错MyBatis的报错大多数都集中在XML映射文件和注解不匹配上。错误一Invalid bound statement (not found)。这个报错的意思是Mapper接口的方法没有找到对应的SQL语句。排查思路检查XML文件的namespace是否等于Mapper接口的全限定名检查方法名和XML里的id是否一致检查target/classes里有没有编译进去XML文件。最后一个原因很隐蔽pom.xml里如果没配置resources包含XMLMaven打包时会把XML文件排除掉。错误二TooManyResultsException。一条查询返回了多行数据但Mapper方法定义的返回值是单个对象。这种一般是SQL条件写窄了加了联表后产生了重复数据。解决方法是查数据确认结果集或者用LIMIT 1限制。错误三MyBatis第一级缓存导致的数据不同步问题。Spring环境下每执行一个SQL会清掉一级缓存基本不会踩到但如果你在同一个事务里先查了员工列表然后别的手段改了数据再查还是旧数据。遇到过几次之后我建议复杂查询场景直接关闭一级缓存或使用二级缓存时特别小心。5.3 MySQL连接和时区问题排查MySQL连接数据库时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized是经典报错原因就是JDBC连接串里没设置serverTimezone。解决办法url里加serverTimezoneAsia/Shanghai同时确认MySQL时区设置正确。还有一种是Public Key Retrieval is not allowed错误。MySQL 8.x默认配置下JDBC连接时如果使用caching_sha2_password认证插件需要显式允许获取公钥。在JDBC url里加上allowPublicKeyRetrievaltrue可以解决。连接池连接超时问题也很常见。SpringBoot默认的HikariCP有个maximumPoolSize默认是10如果连接数不够并发一上来就会报HikariPool-1 - Connection is not available, request timed out after 30000ms。可以调大maximumPoolSize或者检查数据库的max_connections。不过对人事系统这种轻量系统默认配置一般也够用了。5.4 Vue前端开发中的高频问题Vue项目常见的坑集中在依赖安装、路由和打包三块。npm install报错十有八九是node-sass和老版本Node的兼容性炸了。这个项目里我建议直接用sassdart-sass替代node-sass装起来更省心。如果项目已经用了node-sass尝试删除node_modules和package-lock.json后重新安装还是不行的就把Node切到和项目匹配的版本。前端请求接口时网络层报404或者502先别急着看后端打开浏览器控制台Network面板看看实际请求的URL是不是带上了正确的前缀。开发环境最常见的问题是/api路径拼错了导致请求发到了前端服务器而不是后端代理。Vue项目build后部署到服务器页面白屏。这个大概率是静态资源路径问题。在vue.config.js里设置publicPath为相对路径module.exports { publicPath: ./ }这样打包后的index.html里引用的js/css路径就是相对路径部署到任意子目录下都能正常访问不用非得放到域名根目录。5.5 部署环节的零碎问题服务器上部署时端口占用是高频问题。SpringBoot默认8080端口如果服务器上已经有别的服务占用了8080启动直接报Web server failed to start. Port 8080 was already in use.。解决办法是换端口或者杀掉占用进程也可以用server.port配置项改成别的端口。防火墙问题容易被忽略。云服务器上安全组规则没放行端口导致浏览器访问不到服务。这个问题最坑的地方在于服务器上一切正常curl localhost也正常就是你从浏览器远程访问不通——基本就是防火墙和安全组的锅。还有一个是前后端时间格式不一致。后端返回的LocalDateTime默认序列化格式是数组前端收到看着像天书。解决方法是配置Jackson的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这个配置不改的话前端那边要花不少力气处理时间展示问题。我在实际做这个项目的过程中最大的感受是考勤和请假这种带审批流的业务比员工CRUD难做得多。牵扯到状态流转、角色校验、数据联动每一步都要考虑边界情况。你有时间的话可以在这个基础上继续扩展招聘管理和培训管理模块把系统的业务闭环做得更完整。另外既然前后端都写完了我强烈建议你抽时间把测试用例补上至少给Service层的关键业务逻辑写一些单元测试再把Dockerfile和docker-compose.yml写好。把部署做到一条命令拉起整个环境这套系统的完整度才真正够看。

最新新闻

日新闻

周新闻

月新闻