SpringBoot+Vue企业人事管理系统设计与实现
发布时间:2026/10/12 8:33:12 锦皓数字建站

基于SpringBootVue的企业人事管理系统LW参考示例在企业级开发这个圈子里SpringBootVue基本已经是前后端分离项目的标准答案了。这套企业人事管理系统是我在带过好几批学生、也自己从头到尾重构过几遍之后沉淀出来的一套最稳妥的落地方案。它要解决的无非是这些事员工档案怎么维护、部门层级怎么设计、考勤和薪资的数据流怎么打通、登录之后的权限怎么控制以及前后端分离在联调和部署阶段那些让人头疼的细节。如果你是正在做毕设或者刚接了一个中小型管理系统的活儿这篇文章可以直接照着落地比你去论坛翻几十篇零散教程要省太多时间。1. 整体设计与技术选型为什么是SpringBootVue1.1 人事管理系统的核心需求拆解很多人一上手就急着建项目、敲代码结果做到一半发现功能之间互相打架。人事管理系统听着简单落到需求上其实牵扯的东西不少。我先说核心功能域。第一个是员工档案管理包括员工基本信息、联系方式、入职日期、岗位、所属部门、在职状态以及附带的增删改查和条件筛选。第二个是部门管理企业里的部门是树形结构比如总公司下面有技术部、市场部技术部下面又有前端组、后端组。第三个是考勤管理记录员工每天上下班时间一个月下来统计出勤天数和迟到早退次数。第四个是薪资管理根据基础工资、岗位工资、绩效和考勤结果算出实发工资。这四块是骨架再加上用户登录、角色权限、操作日志这些公共能力就是一个完整闭环。最容易翻车的地方反而不是功能多复杂而是数据约束没想清楚。比如员工离职不能直接删除记录要改成离职状态删除部门之前必须先确认部门下面没有员工考勤数据一天只能绑定一条记录。这些问题如果没在设计阶段定好后面补逻辑就是在一堆烂代码上打补丁。1.2 前后端分离架构的优势与代价选SpringBootVue本质上就是在走前后端分离路线。它的核心逻辑是后端只负责提供JSON接口不再关心页面长什么样前端只负责页面交互用Axios去拿数据。开发的时候两端可以并行推进后端把接口定义好前端就能用Mock数据先把页面写起来不用互相干等。但分离不是免费的午餐代价主要在跨域和联调这两块。开发环境里前端跑在Vite或Vue CLI的默认端口后端跑在SpringBoot的8080浏览器同源策略会把请求拦下来。解决办法我一般首选前端代理在vue.config.js里改devServer.proxy把/api开头的请求转发到后端地址。这个方案的好处是跨域配置只在开发环境生效生产环境把前后端拆件部署在同一个Nginx域下根本不需要额外处理跨域。联调的代价更隐蔽。原来后端模板渲染时代数据直接灌进页面对不对一眼就能看到。现在前后端各自维护一层接口字段名、日期格式、参数名对不上就能消耗一整天。所以我强烈建议在前端请求封装层就把统一返回格式定死比如{code, message, data}后端所有接口都遵守这个壳联调起码少一半问题。1.3 依赖选型与版本避坑选型这块我踩过的坑很多直接给你一套不会出问题的组合。后端我用SpringBoot 2.7.x搭配MyBatis-Plus 3.5.x。为什么不选最新版Spring Boot 3.x因为3.x是基于Jakarta命名空间很多老教程的代码没法直接跑而且对JDK版本有硬性要求。如果是毕设或者中小型系统2.7.x稳定、教程多、遇到问题容易找到答案。持久层我用MyBatis-Plus它自带分页插件还有BaseMapper的通用CRUD能把最简单的增删改查代码省到极致。但注意复杂的多表查询我不用LambdaQueryWrapper硬拼而是直接在XML里写SQL可读性和维护性都好很多。数据库用MySQL 8.0连接驱动选mysql-connector-java 8.0.33。这里有个老坑如果用5.x时代的com.mysql.jdbc.Driver在MySQL 8下会直接启动失败正确类名是com.mysql.cj.jdbc.Driver而且URL里最好带上serverTimezoneAsia/Shanghai不然日期字段会乱。前端版本如果你之前没用过Vite求稳就选Vue 2.7 Vue CLI 5 Element UI这套组合在中小型项目里非常成熟路由、状态、组件库全齐。想练新技术就Vue 3 Vite Element Plus逻辑大同小异只是Composition API写法上的差异。我见过太多人死磕最新版本结果花了两天解决脚手架兼容问题代码一行没写真的没必要。2. 后端核心设计与实现2.1 数据库表设计与关联关系数据库设计是整个系统的地基表关系搞不清楚后面写接口全是泪。我一般分这几张核心表sys_user用户表、department部门表、employee员工表、attendance考勤表、salary薪资表。表之间的关系大概是这样的sys_user和employee通过工号关联一个员工对应一个登录账号employee表里有department_id指向部门表这就是员工和部门的归属关系attendance通过employee_id关联员工salary也通过employee_id关联员工。全部用逻辑外键不要真在数据库里建外键约束为什么因为公司做数据迁移、批量导入的时候外键约束会让操作顺序变得非常僵硬很多实际项目都放弃物理外键靠应用层保证数据一致性。每个表的字段设计也有讲究。employee表不能只存姓名和手机号还得有身份证号、性别、生日、学历、入职日期、在职状态这些基础字段。state字段建议用int类型1在职、2离职、3试用期比String好维护。attendance表要有date、check_in_time、check_out_time、status注意date要加个唯一索引组合是(employee_id, date)唯一这样才能保证一个人一天只能有一条考勤记录。salary表建议保留base_salary、post_salary、bonus、deduction、actual_salary、month每个月生成一条快照而不是实时去算这样年终统计的时候数据才是稳定的。表名核心作用关键字段关联关系sys_user登录账号username, password, role_id与employee通过work_no关联department部门架构parent_id, name, leader_id自关联实现树形结构employee员工档案name, phone, department_id, state关联departmentattendance考勤记录employee_id, date, check_in, check_out关联employeesalary月度薪资employee_id, month, base_salary, actual_salary关联employee2.2 SpringBoot三层架构与接口分层后端代码我严格按Controller、Service、Mapper三层来分。Controller层只做一件事接收请求参数、调Service、把结果包成统一返回体。Service层写业务逻辑比如考勤打卡的时候要判断该员工当天是否已经打过卡薪资计算要先统计考勤天数。Mapper层干数据访问的活复杂SQL写在XML里。我放一个员工查询的分页接口示例这是整个系统里最有代表性的一个接口RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String name, RequestParam(required false) Integer departmentId) { PageEmployeeVO result employeeService.pageEmployees(page, size, name, departmentId); return Result.ok(result); } }Service里对应的是分页和条件查询逻辑public PageEmployeeVO pageEmployees(int page, int size, String name, Integer departmentId) { PageEmployee p new Page(page, size); LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Employee::getName, name) .eq(departmentId ! null, Employee::getDepartmentId, departmentId) .orderByDesc(Employee::getCreateTime); PageEmployee employeePage employeeMapper.selectPage(p, wrapper); // 把Entity转换成VO补充分组名称等冗余字段 return convertToVO(employeePage); }为什么要用VOView Object而不是直接把Entity返回给前端因为数据库表结构不能完全暴露给页面。比如employee表里如果有身份证号这种敏感字段直接返回Entity就把不该给的数据给出去了。我习惯建一个EmployeeVO只包含前端需要的字段然后在Service层做转换。这个习惯在真实项目里简直是保命级别的。2.3 登录鉴权与权限拦截人事管理系统肯定要有登录功能而且不同角色看到的东西不一样。管理员能看所有模块普通员工只能看自己的信息和考勤。鉴权方案我用JWT整体思路是用户登录成功后后端生成一个Token返回给前端前端存在localStorage里之后每次请求都在Header里带上这个Token。后端写一个拦截器拿到Token就解析出用户ID和角色放行没拿到或者Token过期直接返回401。JWT工具类的核心就两个方法生成和解析public class JwtUtil { private static final String SECRET your-secret-key; public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 12)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器这块有个细节特别容易漏放行登录接口本身。不然你还没登录请求登录接口就被拦截了直接死循环。路径用/api/auth/login和/api/auth/register白名单剩下的都走拦截验证。密码存储绝不能明文用BCrypt加密后再入库Spring Security里自带BCryptPasswordEncoder只引这一个类就够了不用把整个Spring Security拉进来。权限控制我建议做成简单模式用户表里加个role字段1是管理员2是普通员工后端拦截器解析出role之后在接口方法上用注解或者直接判断。比如删除员工这个操作只有admin能做。别一上来就搞RBAC那一套完整的表设计中小型系统用角色字段判断完全够用而且代码好理解太多。2.4 动态SQL与分页查询MyBatis分页插件玩法MyBatis-Plus的分页插件是必配的配置方式很简单写一个分页拦截器的配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配完之后selectPage就是分页查询了MyBatis-Plus自动帮你拼LIMIT语句。但这里注意一个坑分页插件只能拦截单表查询如果你在SQL里写了多表JOIN用自定义XML的selectPage带上Page参数才行。比如员工列表要关联部门表查出部门名称就不能用LambdaQueryWrapper得自己在XML里写select idselectEmployeePage resultTypecom.example.vo.EmployeeVO SELECT e.*, d.name AS department_name FROM employee e LEFT JOIN department d ON e.department_id d.id where if testname ! null and name ! AND e.name LIKE CONCAT(%, #{name}, %) /if if testdepartmentId ! null AND e.department_id #{departmentId} /if /where /select动态SQL的 标签是MyBatis的灵魂。它的语法很直白上面这段代码里name和departmentId传了就拼条件不传就跳过。用这种写法一个接口就能覆盖全部筛选场景不用把page、pageAll、pageByName写好几个方法。这种一个方法处理一类查询的思路在真实开发里太重要了。3. 前端核心实现与页面落地3.1 Vue项目初始化和基础配置前端我从零开始搭。用Vue CLI 5创建项目命令是vue create hr-front然后在交互式选择里勾上Router和Vuex。装完基础依赖再加Element UI和Axiosnpm install element-ui axiosmain.js里做全局注册import Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router import store from ./store Vue.use(ElementUI) Vue.config.productionTip false new Vue({ router, store, render: h h(App) }).$mount(#app)目录结构这块我习惯按模块分目录而不是全堆在views里。我的项目结构大概是这样的views下面按功能模块分子目录比如employee、department、attendance、salarycomponents目录放可复用的通用组件比如一个上传头像组件、一个导出Excel按钮api目录下按模块拆JS文件每个文件里封装对应接口的Axios请求router目录只留一个index.js所有路由集中在这里管理。这种目录的划分方式跟后端Service的分类思路一样就是按业务功能聚合。以后要加一个培训管理模块前端复制一份api/employee.js改成api/training.js视图目录加一个training文件夹路由加几行完事。项目结构一旦清晰开发效率完全是两码事。3.2 路由设计与页面权限控制路由这块我把它分成两大部分静态路由和动态路由。静态路由就是登录页、404页这种不需要权限的动态路由是根据用户角色去过滤出来的菜单和页面。我用一个简单方案来模拟动态路由的效果。先在router/index.js里把需要权限的页面全部定义好每个路由meta里加上roles字段{ path: /employee, name: Employee, component: () import(../views/employee/index.vue), meta: { title: 员工管理, roles: [admin] } }然后在路由全局前置守卫里每次跳转前检查当前用户的角色有没有在该路由的roles里面router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() } else if (!token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(role)) { next(/403) } else { next() } })这个方案的优点是容易理解不需要后端动态返回路由表。真正的动态路由要后端根据角色吐路由配置前端用router.addRoutes动态添加适合大型后台系统。人事管理系统这个规模用meta标记前端判断就绰绰有余。路由懒加载这个点值得提一下。组件用() import(...)这种写法Webpack/Vite就会把每个页面单独打包成一个chunk而不是全部塞进一个bundle.js里。效果就是首屏加载时间能少一半尤其当系统页面多了以后这个优化非常明显。3.3 Axios请求封装与接口对接Axios不能每个页面各写各的必须封装成一个统一请求工具。我在api/request.js里做了三层处理BaseURL配置、请求拦截器、响应拦截器。import axios from axios import { Message } from element-ui import router from ../router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理错误码 service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return res }, error { Message.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } ) export default service有了这层封装后面写接口API就是纯粹的配置工作了。比如api/employee.jsimport request from ../request export function getEmployeePage(params) { return request({ url: /employee/page, method: get, params }) } export function deleteEmployee(id) { return request({ url: /employee/delete/${id}, method: post }) }响应拦截器里把code401做统一跳转处理这是很多项目忽视的关键点。Token过期是必然会发生的事如果不统一处理每个页面都写一遍如果接口返回401就跳登录页的代码代码冗余不说很容易漏掉某个页面导致用户操作半天才发现session过期。3.4 核心功能页面实战员工管理考勤统计落地员工管理页面的实现思路是表格搜索表单对话框弹窗这是中后台页面最常见的组合模式。页面上半部分是搜索区有姓名输入框、部门下拉框、在职状态下拉框下面一个查询按钮和一个重置按钮。下半部分是表格区表格列对应员工所有展示字段操作列放编辑和删除按钮。新增和编辑共用一个Dialog弹窗里面是el-form表单字段校验规则用Element UI自带的那套验证规则就行。考勤统计页面稍微复杂一点。因为考勤数据不是手动录的而是每天打卡进来。我做了两个视图一个是考勤记录表展示每天每个人的打卡情况另一个是月度汇总表按员工月份做group by统计出勤天数、迟到次数、早退次数。月度汇总的数据可以由后端接口实时统计返回不需要单独建表。这里有个小技巧前端表格里展示日期和数字格式最好统一放在一个工具文件里用过滤器处理比如日期格式化成yyyy-MM-dd薪资数字千分位显示。不要在每个页面里复制粘贴同样的格式化代码后期改格式你会想抽自己。4. 联调、打包与上线部署4.1 开发环境与联调经典问题联调阶段是我遇到问题最多的时候但问题类型其实很集中翻来覆去就那几个。第一个是前端拿到的时间字段是一串数字。这是因为后端默认序列化方式是Java时间戳前端拿到的是毫秒数。解决办法有两个一是后端全局配置Jackson格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8二是在实体字段上加JsonFormat注解。我建议全局配置优先因为字段多的时候挨个加注解会烦死人。第二个是Long类型ID精度丢失。如果你的主键用雪花算法生成前端JS的Number类型存不下那么长的整数最后几位会被0替代导致删除或编辑时传错ID。解决办法是后端把Long序列化成String或者干脆前端把ID字段改成el-input时禁用鼠标滚轮数字精确处理。实际项目里我在Jackson配置里统一把Long转成String这个问题一劳永逸。第三个是接口返回结构不统一。有的接口返回的是对象有的是数组有的错误信息塞在data里这会逼着前端写一堆判断分支。我定的规矩是后端所有接口统一返回Result对象data里面具体是什么和前端协商清楚。正是这种约定才让联调效率高起来。4.2 前后端部署方案本地打包到服务器上线部署我一般走两条路各位根据自己的服务器条件选。后端打包比较简单在pom.xml里配好打包插件执行mvn clean package -DskipTests生成jar包然后上传到服务器用nohup启动nohup java -jar hr-server.jar --server.port8081 hr.log 21 前端打包执行npm run build生成dist目录里面是静态文件把这个目录上传到服务器的Nginx静态目录。Nginx里同时配置静态文件服务和/api反向代理server { listen 80; server_name hr.example.com; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/hr/dist; index index.html; try_files $uri $uri/ /index.html; } }try_files那行一定要写因为Vue是单页应用路由切换刷新页面时如果Nginx找不到对应的物理文件就会返回404加了try_files就会自动兜底到index.html。这个坑不亲身体会一遍很难注意到。4.3 参考论文LW撰写时的要点这套系统如果用于毕设那LW论文部分其实占的分量很重。很多同学系统做出来了论文不会写最后反而栽在论文上。我给个通用提纲摘要部分是整个系统的浓缩要交代背景、说清楚系统用什么技术、实现哪些功能、达到什么效果。目录一般从绪论开始讲研究背景和意义然后是国内外研究现状。技术介绍章节把SpringBoot、Vue、MyBatis-Plus的原理写一遍。需求分析章节把功能需求和非功能需求分开描述配用例图。系统设计章节把架构图、功能模块图、数据库ER图放进去表结构截图是少不了的。系统实现章节按功能模块一个模块一个模块写配核心代码和截图。最后是系统测试写测试用例和结果。论文里最忌讳的是堆流水账比如该系统采用SpringBoot框架这样一句话介绍完。正确的写法是解释为什么选、选它解决了什么问题。比如系统采用前后端分离架构前端通过Axios调用后端RESTful接口这解决了传统单体应用前后端耦合度高、无法并行开发的问题这种描述才是有技术含量的。5. 踩坑记录与常见问题速查5.1 高频报错与排查思路我把这两年带学生遇到的高频报错整理成了一张速查表每个错误基本都有一个最常见的诱因。报错现象常见原因解决思路前端请求404后端接口路径和前端不一致先看后端Controller的RequestMapping再看前端api里的url严格对齐前端请求500后端空指针或SQL异常去看后端控制台完整堆栈不要只看浏览器Network里的错误提示登录接口CORS错误跨域配置没生效开发环境用Vue的proxy代理生产环境用Nginx反代数据库中文乱码连接URL缺少characterEncodingURL加characterEncodingutf8serverTimezoneAsia/Shanghai分页查不出数据Page信息没传给Mapper自定义SQL方法第一个参数必须写PageVue页面白屏组件导入路径写错F12看Console具体报错路径注意大小写文件上传失败上传目录权限不够Linux下给文件夹chmod 755或777排查思路上我特别建议养成先看日志再猜原因的习惯。后端启动后把SpringBoot的日志级别调成DEBUGSQL语句和执行参数都能打出来。前端调接口打不开的时候打开浏览器开发者工具Network面板看到底是404、500还是CORS错误对症下药。很多人一报错就盯着代码看却忘了看日志和请求状态这是效率最低的做法。5.2 几个值得养成的开发习惯最后分享几个我从这个项目里提炼出来的习惯可能比功能代码本身更有价值。第一个是接口文档先于代码。前后端约定好URL、请求参数、返回结构再动手我用的是ApiPost这类工具直接在线维护接口文档比动嘴说半天管用。第二个是Git提交要勤不要改完一大坨才提交。每完成一个功能点commit一次备注写清楚是做什么的。真改崩了能回滚这个保障比什么都值钱。第三个是日志要打得有水平不要只打System.out.println统一用Slf4j写logger.info。系统出问题的时候日志是你排查问题的唯一线索打印出关键入参、出参和异常堆栈能帮你省下大把排查时间。第四个习惯是关于学习的方法的。遇到不懂的知识点不要只看文章和视频一定要去翻官方文档。SpringBoot和Vue的官方文档写得都很清楚而且版本更新后文章经常过时文档却是活的。我在排查问题的时候翻官方文档的概率比搜博客高得多。这个项目的整个流程从头到尾走一遍你会发现真正收获不是那几张表和几个接口而是把一套系统从零到一落地的完整认知需求怎么拆、架构怎么选、前后端怎么协作、问题怎么排查、文档怎么沉淀。这套方法论是做一个继续深造的项目还是接私活做外包都能直接用上。我个人在实际带项目过程中最大的感受是很多同学不是学不会技术而是缺少一个完整项目的全局视角。这个系统刚好提供了一个很合适的切入点哪怕你只把它做一遍再去接触更复杂的业务系统心里也会踏实很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。