☰
SpringBoot+Vue+MyBatis人员管理系统开发实战全解析
2026/10/2 2:41:42 网站建设 项目流程

这段时间后台收到不少私信,都是冲着“人员管理系统”源码来的。仔细翻了下聊天记录,发现大部分朋友的需求其实都差不多:毕业设计要用、公司内部要做个简单的HR管理后台、或者想练手整合一套主流技术栈。这次拿到的是一个很典型的组合——SpringBoot + Vue + MyBatis + MySQL,标题打着“2025最新”,说实话,技术选型本身谈不上新,但胜在经典、稳定、资料多,非常适合快速落地。这篇我就把这个系统的搭建思路、核心代码、以及我实测踩过的坑全部摊开讲,从数据库设计到前端权限控制,再到打包部署,尽量一条龙讲完。

1. 项目整体设计与技术选型思路

1.1 为什么是SpringBoot+Vue这对组合

你要是问现在做管理系统用什么最省心,我大概率还是会推荐这套组合。后端用SpringBoot,本质上是把Spring家族那套复杂的XML配置给收编了,启动就是一个main方法,内嵌Tomcat,打个jar包就能跑,这对新手和需要快速交付的场景来说太友好了。

前端这块选Vue,理由也很直白:组件化开发让页面复用变得特别简单,而且Vue的响应式数据绑定能让我们从繁琐的DOM操作里解脱出来。人事系统这种项目,页面形态高度重复——无非是表格、表单、弹窗、树形部门结构,用Vue的组件机制,封装一个通用的CRUD页面模板,后面加功能模块就是复制粘贴再改改字段的事。

还有一个现实因素:这套技术栈的人才储备最多。将来这个系统要交接、要扩展,不管是找人维护还是招人开发,成本都相对可控。这算是一个隐性的技术选型优势。

1.2 核心功能模块拆解

一个能用的、像样的人事管理系统,不是简单弄个增删改查就行。参考我做过的一些HR项目,核心模块至少得覆盖这么几块:

  • 组织架构管理:部门树形结构,支持多级部门,部门负责人的关联。
  • 员工档案中心:这里不只是姓名电话,还包括入职日期、学历信息、合同信息、紧急联系人、社保基数等等,字段多而杂,所以表单设计上要分组。
  • 考勤与请假:请假单的审批流(发起、主管审批、HR备案)、考勤数据的导入与统计。
  • 薪酬管理:这个模块比较敏感,通常会和员工主数据分开权限管理,导出功能要留操作日志。
  • 系统管理:用户管理、角色管理、菜单权限管理,这是整个系统的地基,基于RBAC模型。

权限这一块,我建议直接用RBAC(基于角色的访问控制)模型,用户关联角色,角色关联菜单和按钮权限。千万别搞用户直接挂权限,后面维护绝对想哭。

1.3 技术栈全览

放一张清单表格,方便你对照自查环境:

技术栈选型补充说明
后端框架Spring Boot 2.7.x不用3.x,避免部分老版本MyBatis兼容问题
持久层MyBatis 3.5.xXML里写SQL,动态SQL非常好用
数据库MySQL 8.0+8.0的窗口函数在报表统计里很香
前端框架Vue 2.7 + Element UI2.7是Vue2的最后一站,稳定又成熟
权限认证JWT(无状态) + Spring Interceptor不引入SpringSecurity,轻量够用
项目管理Maven + Node.js 16+后端依赖和前端构建各管各的
代码生成器MyBatis Generator单表CRUD代码自动生成,效率翻倍

2. 后端核心:SpringBoot+MyBatis工程搭建与分层实现

2.1 项目结构初始化

我习惯按这种包结构组织,清晰且容易扩展:

com.example.hrms ├── HrmsApplication.java // 启动类 ├── common/ // 通用模块(统一返回、异常、分页) ├── config/ // 配置类(跨域、拦截器、MyBatis) ├── controller/ // 控制层 ├── service/ // 业务层(接口+实现) ├── mapper/ // MyBatis的Mapper接口 ├── entity/ // 数据库实体类 ├── dto/ // 前端交互的数据传输对象 └── utils/ // 工具类(JWT、日期处理、Excel导入等)

pom.xml里的核心依赖就是spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java(新版是com.mysql:mysql-connector-j)。这里要提醒一句,8.0的驱动类名是com.mysql.cj.jdbc.Driver,时区参数serverTimezone=Asia/Shanghai必须加,不然连接会报时区错误。

2.2 MyBatis的Java配置与Mapper扫描

很多人喜欢在启动类上加@MapperScan,这是最省事的做法。我个人习惯更进一步,在config包里单独建一个MyBatisConfig类,把驼峰映射、分页插件、SQL执行日志这些全放在一起配置,这样后期调整不用翻启动类:

@Configuration @MapperScan("com.example.hrms.mapper") public class MyBatisConfig { @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> { // 下划线转驼峰:数据库字段user_name -> java属性userName configuration.setMapUnderscoreToCamelCase(true); // 打印SQL到控制台,开发阶段一定要开 configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); }; } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // 这里用的MyBatis-Plus的分页插件,如果纯MyBatis就配PageHelper MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

Column:开发阶段开启StdOutImpl打印SQL非常有必要,我见过太多人上来就问“为什么我的SQL不生效”,结果控制台一片空白,连个错误日志都没有。先把日志打开,很多问题一眼就能看清。

2.3 统一返回体与全局异常处理

前端跟后端交互,需要一个固定的数据格式契约。我定义了一个Result类,泛型设计:

public class Result<T> { private Integer code; // 200成功,500失败,401未授权 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

全局异常处理器用@RestControllerAdvice,把业务异常、参数校验异常、兜底异常分开处理,避免异常堆栈直接抛给前端,也算是一道安全防线。

2.4 JWT登录认证与拦截器链路

管理系统的认证,我推荐JWT+拦截器这种轻量方案,不做SpringSecurity是觉得它太重,学习成本也高,对HR这种业务复杂度来说有点杀鸡用牛刀。

JWT工具类我就不贴全代码了,核心就三步:登录成功后签发token(把userId和username塞进claims)、拦截器里解析token、校验失败返回401。

拦截器注册的时候要注意excludePathPatterns,登录接口、验证码接口一定要放行。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求,跨域场景必须写 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } // 解析token,解析失败抛异常交给全局异常处理 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); return true; } }

2.5 MyBatis中容易忽视的TypeHandler与缓存

热搜词里有mybatis中typehandler的工作流程图和mybatis缓存,这块确实容易让人困惑。TypeHandler简单理解就是JDBC类型和Java类型之间的转换桥。比如我们的员工表里有个状态字段,存的是int(1在职,0离职),但实体类里想用枚举,这时候自定义一个枚举TypeHandler就特别方便。

@MappedTypes(EmployeeStatus.class) @MappedJdbcTypes(JdbcType.INTEGER) public class EmployeeStatusHandler extends BaseTypeHandler<EmployeeStatus> { @Override public void setNonNullParameter(PreparedStatement ps, int i, EmployeeStatus parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public EmployeeStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { return EmployeeStatus.of(rs.getInt(columnName)); } // 另外两个重载方法:getNullableResult(rs, columnIndex) 和 getNullableResult(rs, columnName) }

至于缓存,MyBatis一级缓存是SqlSession级别的,默认开启,生命周期极短,基本不用管。二级缓存是namespace级别的,我建议在管理类系统里关闭二级缓存。HR系统的数据时效性要求高,而且跨namespace缓存同步容易出坑,你查出来的员工信息可能是几分钟前的,这种问题排查起来特别拧巴。

3. 前端核心:Vue+Element的动态路由与权限控制

3.1 前端工程搭建与目录规范

前端我用Vue2.7 + Element UI,目录结构这么设计:

src ├── api/ // 接口请求封装,按模块拆分 ├── assets/ // 静态资源 ├── components/ // 通用组件(UploadExcel、SearchBar等) ├── layout/ // 主布局(侧边栏、顶栏) ├── router/ // 路由配置 + 动态路由生成 ├── store/ // Vuex状态管理 ├── utils/ // axios封装、工具函数 └── views/ // 页面视图

axios封装这块有个细节:请求拦截器里统一带token,响应拦截器里统一处理code:200放行,401强制跳登录页,500弹出后端返回的message。不要在每个页面里重复写错误处理,纯属浪费时间。

3.2 动态路由的实现原理

管理系统的菜单权限,我的做法是:用户登录成功后,后端返回该用户的菜单树和按钮权限标识列表,前端拿到这些数据动态生成路由和菜单。

动态路由的核心逻辑在store/modules/permission.js里:

// 将后端返回的菜单数据转换为路由组件 function buildRoutes(menus) { const routes = []; menus.forEach(menu => { const route = { path: menu.path, name: menu.name, component: loadView(menu.component), // 通过组件路径映射 meta: { title: menu.title, icon: menu.icon }, children: menu.children ? buildRoutes(menu.children) : [] }; routes.push(route); }); return routes; }

这里有个关键点:组件映射必须用require.context或者import.meta.glob(Vite),把views目录下的.vue文件一次性导入,然后通过路径字符串去匹配。不接受动态加载组件的话,路由生成的页面永远是空白。

3.3 路由守卫与按钮级权限

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token) { if (to.path === '/login') { next(); } else { next(`/login?redirect=${to.fullPath}`); } return; } // 已登录且访问登录页,直接跳首页 if (to.path === '/login') { next('/'); return; } // 首次进入,拉取用户信息和菜单 if (!store.state.permission.menusLoaded) { store.dispatch('permission/generateRoutes').then(() => { next({ ...to, replace: true }); }); return; } next(); });

按钮级的权限控制,用自定义指令v-permission。指令里判断当前用户是否拥有该按钮的权限标识,没有就直接把按钮DOM删掉。这个实现思路很简单,但效果很好,后端接口层也要做二次校验,前端控制只是交互层面的优化,真正的权限边界必须由后端守住。

3.4 Vue打包发布到SpringBoot的两种姿势

热词里有人问vue打包放进springboot中,这确实是把前后端合并部署的常见需求。第一种方式,也是我推荐的方式:前端执行npm run build生成dist目录,然后在SpringBoot里配置:

spring: web: resources: static-locations: classpath:/static/,classpath:/public/,file:./dist/

把dist下的文件复制到src/main/resources/static/目录下,打包进jar。要注意Vue路由的history模式需要配置前端路由转发,否则刷新页面就404。在SpringBoot里加一个转发控制器:

@Controller public class ForwardController { // 所有非API路径都转发到index.html,交给前端路由处理 @RequestMapping(value = {"/", "/{path:[^\\.]*}", "/{path:^(?!api).*}/**"}) public String forward() { return "forward:/index.html"; } }

第二种方式是用Nginx部署,前端一个nginx,后端一个jar,用/api前缀做反向代理。这种方式前后端边界清晰,正式环境我会优先用这个。开发环境就直接用Vue的proxyTable代理,把/api开头的请求转发到后端8080端口,跨域问题在开发阶段就这么轻松解决。

4. 数据库设计与核心表结构落地

4.1 三大核心表的设计逻辑

先看部门表(sys_dept),我用的是经典的parent_id方案,外加一个ancestors字段存祖先链,比如0,1,3,,查某个部门下的所有子部门时用ancestors like '0,1,%'即可,比递归查询高效得多。

员工表(emp_employee)要特别留意冗余设计。部门名称、岗位名称这种字段,有人喜欢用关联表查询,我推荐在员工表冗余一个部门名称字段。管理系统的列表页几乎都要按部门筛选、展示部门名,每次join一张大表,性能不划算,而且这个字段变更频率并不高,部门改名时用批量更新语句同步一下就行。

用户表(sys_user)是登录的凭证,密码存储建议用BCrypt加密,不能明文,这个属于底线问题。

4.2 考勤与请假流程的表结构

考勤表(att_record)我的设计思路是:每个员工每天一条记录,字段包括上班打卡时间、下班打卡时间、考勤状态(正常/迟到/早退/缺卡)。为了快速判断状态,我会在写入时直接算好状态字段,查询报表时直接按状态分组统计,比在SQL里用case when现算现排快得多。

请假单表(oa_leave)要带上审批流字段:当前审批节点、审批状态。比较接地气的做法是状态机:0草稿、1待主管审批、2待HR确认、3已通过、4已驳回。每步审批都在日志表(oa_leave_log)里留下一行记录,包括审批人、审批时间、审批意见。这个日志表非常重要,员工经常会对审批过程有异议,没有日志就是死无对证。

4.3 常用SQL编写技巧

员工列表的动态查询SQL是XML里的重头戏,where标签结合if标签来做多条件筛选:

<select id="selectEmployeePage" resultType="com.example.hrms.entity.Employee"> SELECT e.*, d.dept_name FROM emp_employee e LEFT JOIN sys_dept d ON e.dept_id = d.dept_id <where> <if test="query.deptId != null"> AND e.dept_id IN (SELECT dept_id FROM sys_dept WHERE ancestors LIKE CONCAT('%', #{query.deptId}, '%')) </if> <if test="query.keyword != null and query.keyword != ''"> AND (e.name LIKE CONCAT('%', #{query.keyword}, '%') OR e.mobile LIKE CONCAT('%', #{query.keyword}, '%')) </if> <if test="query.status != null"> AND e.status = #{query.status} </if> </where> ORDER BY e.create_time DESC </select>

Column:子查询查部门树这种方式,在数据量不大的管理系统里(比如几千人)性能完全没问题,比在Java代码里过滤要简洁清晰得多。当然,等数据量真到十万级了,再考虑用更专业的方案也不迟,届时用部门路径冗余的方式即可。

MySQL排序这块,热词里有mysql排序,提个最容易踩的坑:如果按中文字段排序,直接用ORDER BY name是按字符编码顺序排的,不是拼音序。要按拼音排得用:

ORDER BY CONVERT(name USING gbk) ASC

但gbk转码排序会放弃索引,数据量大的场景要谨慎。我的实践是,员工姓名这种字段不用刻意拼音排序,输入关键词查询是主要场景。

5. 部署联调与常见问题排查实录

5.1 从零到一的启动清单

后端启动步骤:

  1. 检查MySQL版本,建议8.0及以上,utf8mb4字符集。
  2. 执行项目里的schema.sql和data.sql脚本,初始化表结构和基础数据(管理员账号)。
  3. 修改application.yml里的数据库连接、redis地址(如果有session共享需求)。
  4. 启动HrmsApplication,看到控制台打印启动成功且无红色报错。
  5. 用Postman先测登录接口,拿到token后测员工列表接口,验证鉴权。

前端启动步骤:

  1. 确保Node.js版本≥16,建议用nvm管理版本,避免多个项目版本冲突。
  2. npm install安装依赖,如果慢要用国内镜像:npm config set registry https://registry.npmmirror.com。
  3. npm run dev启动开发服务器,默认端口9528(Vue CLI模板默认端口)。
  4. 改.env.development文件里的VUE_APP_BASE_API,指向后端地址。

5.2 高频异常与排查方法速查表

问题现象排查思路
前端请求后端接口CORS报错后端加跨域配置,或者前端用proxy代理,优先推荐proxy
SQL执行报Unknown column实体类和表字段没对应上,检查驼峰映射配置
查询数据时日期显示少8小时连接串加serverTimezone=Asia/Shanghai,Jackson设置GMT+8
前端菜单一直不显示检查动态路由生成的component字段,大概率是路径映射失败
接口能通但页面空白用浏览器DevTools看Network,通常是JS报错阻断渲染
Maven打包冲突执行mvn dependency:tree定位冲突版本,排除即可
打包jar后前端页面404一定是history路由模式没转发,检查我上面的ForwardController
MySQL连接数爆了连接池别开太大,HikariCP默认10就够了,检查是否有连接泄露

5.3 Maven构建SpringBoot项目的细节

热词里有javamaven项目构建方法springboot,这块确实有讲究。Maven构建SpringBoot项目,最常用的命令是:

mvn clean package -DskipTests

-DskipTests是跳过测试用例,不要写成-Dmaven.test.skip=true,前者编译测试代码但跳过执行,后者直接不编译测试代码,功能相同但语义不同。

构建成功后jar包在target目录。启动:

java -jar hrms-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

生产环境的配置我用application-prod.yml单独维护,数据库连接、日志级别都在这份配置里,用--spring.profiles.active去切换,不要把生产库的密码写在application.yml里打包进镜像,这是安全红线。

5.4 MyBatis打印SQL与MyBatis-Plus批量操作

开发阶段在application.yml配:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样每条SQL执行日志会打印到控制台,包括参数值和影响行数。排查动态SQL拼装问题的时候,这个日志就是救命稻草。

批量插入这个问题,热词里是java mybatis mybatis-plus 批量。批量插入别用for循环单条insert,性能差得离谱。用MyBatis的foreach标签:

<insert id="batchInsert" parameterType="list"> INSERT INTO emp_employee (name, dept_id, mobile, status) VALUES <foreach collection="list" item="emp" separator=","> (#{emp.name}, #{emp.deptId}, #{emp.mobile}, #{emp.status}) </foreach> </insert>

注意batch的大小,我习惯每批500条,超过1000条在MySQL 8.0下会受max_allowed_packet限制报错。如果数据量非常大,用批次切分循环提交。

5.5 MyBatis-Plus是加分项

如果让我给这个项目做一次升级,我会把MyBatis替换成MyBatis-Plus。它的优势是内置BaseMapper支持单表CRUD、分页插件、条件构造器(QueryWrapper),开发速度和代码整洁度都会有明显提升。它跟原生MyBatis的兼容性极好,你手写的XML和Mapper可以原封不动保留。

不过我要说一句:不要无脑依赖MyBatis-Plus。把复杂SQL全堆在注解里,或者滥用LambdaQueryWrapper导致SQL复杂度失控,性能问题后面会找你算账的。我的原则是:单表操作用MyBatis-Plus,多表关联和复杂动态查询用原生XML,两边各干各最擅长的活。

6. 关于这个项目的后续扩展方向

把完整系统搭起来之后,有几个方向值得继续折腾:

集成MinIO做文件存储,这个在热词里出现了minio加入到springboot,确实是个常见的基建需求,跟人事系统的场景结合非常自然:员工头像、身份证扫描件、合同PDF存档、批量导入的Excel模板,都需要一个统一的对象存储服务。MinIO Docker一键拉起,本地开发环境做一个轻量级的MinioService封装,上传、下载、生成预签名URL,代码量不多但能实打实提升系统完成度。可以给用户管理模块加上头图上传能力,就能体现这套能力。

日常用法就是:

// 封装好的MinioService里两个核心方法 public String uploadFile(MultipartFile file, String bucketName) {} public String getPresignedUrl(String bucketName, String objectName) {}

这样无论是员工头像还是部门文档,都用一套服务统一管理。

让系统集成在线播放视频能力,这个方向也挺有意思,热词里有vue播放m3u8免安装。比如企业培训模块要上传培训视频,或者员工自助查看入职指引视频。视频统一转成m3u8切片格式,前端用hls.js播放,浏览器原生支持度好,不用装额外插件。后端只需要部署一个简单的流媒体服务(Nginx配置一下也能出HLS切片),前端引入hls.js即可。

引入HanLP做简历解析和人才分析,这是另一个合理的技术延伸。既然系统沉淀了不少员工信息,就可以在招聘模块上传PDF简历时,用HanLP做关键词抽取(姓名、学校、专业、工作经历),字段自动回填到简历表单。这个功能做起来有技术含量,而且能解决HR人员的真实痛点,做出来会有很强的实用感。

接入Prometheus做应用监控,如果系统将来要长期维护,上线后不可能每天手动看日志。用Spring Boot Actuator暴露指标端点,Prometheus定时抓取,再配一个Grafana看板,服务的CPU、内存、接口调用量、平均响应时间一目了然。对团队来说,这是运维成熟度的标志之一。

我个人建议是有余力先把MinIO和文件系统这块做了,它换来的系统完整度提升几乎是最明显的,其他方向可以按兴趣和实际业务需求来。


最后再分享一个小技巧,这个项目我前后测了不少轮,调试阶段最折磨人的不是业务逻辑,而是各种环境问题。强烈建议把本地开发环境的MySQL、Redis、MinIO都通过Docker Compose一键管理,写一个docker-compose.yml,一条命令全部拉起来。不管是换电脑还是带新人,五分钟就能从零把环境还原,省下来的时间睡个午觉不香吗?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询