
做人事管理系统这类项目最难的不是某一项技术而是如何把业务逻辑和代码结构结合得恰到好处。Spring Boot Vue 这套组合在目前的中小型企业里几乎成了标配用来做人事管理系统的练习或实际交付都非常合适。这套项目包含源码、数据库脚本和完整文档基本上覆盖了一个企业级管理系统该有的核心模块员工管理、部门管理、考勤打卡、请假审批、薪资记录、公告发布以及基于角色的权限控制。很多人一开始看到“人事管理系统”会觉得这就是一堆增删改查没什么技术含量。但真正动手做的时候才会发现权限控制怎么设计、考勤状态怎么流转、请假审批流程怎么建模、前后端联调跨域怎么处理每一步都有讲究。这篇文章我就从技术选型、数据库建模、后端接口、前端页面、部署排错这几个维度把整个项目的设计思路和实现细节完整拆解一遍顺便把我踩过的一些坑也一并记录下来。1. 系统整体架构与技术选型拆解1.1 为什么选择前后端分离架构早些年的管理系统大多是单体应用页面直接用 JSP 或者 Thymeleaf 在服务端渲染所有前端代码和 Java 业务代码混在同一个工程里。表面上看部署简单但实际维护起来很痛苦前端改一个按钮样式后端就得重新编译打包页面逻辑嵌套在 Java 代码里前端工程师没法独立干活。这个人事管理系统选型时第一个决定就是前后端分离。前端单独维护一个 Vue 工程负责页面渲染、用户交互和路由跳转后端单独提供一个 RESTful API 服务只负责业务逻辑、数据校验和权限校验。两边通过 JSON 格式的数据进行通信。前后端分离带来的直接好处有三点。第一前后端可以并行开发后端把接口定义好前端用 Mock 数据先跑页面不需要等对方完成。第二边界清晰后端不需要关心用户点了什么按钮前端不需要关心数据是怎么查出来的。第三部署灵活前端打包成静态文件扔到 Nginx 即可后端打包成 Jar 包独立运行任何一端出问题不会影响另一端的开发节奏。1.2 后端技术栈的具体选型与理由后端我用了 Spring Boot 2.7.x配合 MySQL 5.7 数据库、MyBatis-Plus 做持久层、Spring Security JWT 做认证授权、Redis 做缓存和验证码存储Maven 管理依赖。这套组合在同类管理系统里是最成熟的路线。Spring Boot 选它核心原因就是自动配置。以前 SSM 时代写一堆 XML 配置现在一个注解加上依赖就能跑起来。MyBatis-Plus 则是帮了大忙单表 CRUD 基本不用写 SQL内置的IService和BaseMapper直接提供常用方法分页插件也很方便。但是注意一旦多表关联查询MyBatis-Plus 的 QueryWrapper 就不够用了复杂 SQL 必须写 XML 自己控制。权限部分我选了 Spring Security 而不是 Shiro。虽然 Shiro 使用上更简单但 Spring Security 和 Spring Boot 的整合更原生社区资料更全而且对于这种前后端分离的项目Spring Security 的过滤器链设计更适合做 Token 校验。JWT 解决了无状态认证的问题用户登录后服务器不再保存 session而是签发一个 Token客户端每次请求带上 Token后端通过签名验签来识别用户身份。这对于前后端分离、后端有多个部署实例的场景非常友好。1.3 前后端工程结构与协作方式后端工程我按标准分层包组织controller、service、mapper、entity、config、common、utils。controller层只做参数接收和结果返回service层写业务逻辑mapper层对数据库操作entity类对应数据库表结构common放统一返回结果和异常处理。这样分包的好处是每个类的职责单一排查问题时能快速定位。前端用的是 Vue 2 搭配 Element UI这套组合虽然不如 Vue 3 Element Plus 新但是稳定成熟网上资料多遇到问题好查。工程结构划分成api、router、store、views、components、utils几块api下按模块封装接口请求router管理路由和动态注册store用 Vuex 管理登录用户信息和权限数据utils里装 axios 封装、token 存取等工具函数。前后端约定统一的接口返回格式后端所有接口返回{ code: 200, msg: 操作成功, data: ... }code 为 200 表示成功其他为失败。前端 axios 拦截器统一判断code 不是 200 就直接弹出错误提示业务代码里不用每个接口都写 try-catch清爽很多。2. 数据库建模人事系统的基础2.1 核心业务表设计与字段规划数据库设计是整个系统的地基业务表之间的关系将直接影响后端代码的复杂度和可维护性。这套人事管理系统我设计了以下几张核心表sys_user系统用户表用来登录系统包含用户名、密码、手机号、状态等字段sys_role角色表包含角色编码和角色名称sys_menu菜单权限表包含菜单名称、父级菜单、路由地址、权限标识等sys_user_role用户角色关联表sys_role_menu角色菜单关联表department部门表支持树形层级结构employee员工表存储员工基本信息与部门关联attendance考勤表记录员工每天的上下班打卡时间leave_request请假申请表包含请假类型、开始时间、结束时间、审批状态salary_record薪资记录表按月记录员工的工资明细员工表是整个系统的核心字段设计上有一个关键点单独设置emp_no员工编号字段而不是直接用自增主键作为员工标识。原因很简单员工编号在业务上要有可读性且保持唯一后续考勤、薪资都与员工编号关联离职后编号也便于追溯。即便员工今天离职了明天的报表里依然能看到他这月产生的记录。所有业务表我都加了逻辑删除字段deleted默认值为 0删除操作走的是逻辑删除而不是物理删除。这一点非常重要人事系统涉及的员工历史信息、考勤记录、审批记录需要长期留存用于审计和追溯真删了数据后面再来查就麻烦了。2.2 薪资与考勤模块的表结构细节薪资没有设计成在员工表里存一个“当前工资”字段而是单独建了salary_record表。工资是会变动的调薪、绩效、补贴都要有历史记录如果用“员工表里的工资字段”那每次调薪就把旧数据覆盖了财务要找历史明细根本找不到。salary_record表的核心字段包括emp_id员工ID、base_salary基本工资、performance绩效考核、bonus奖金、deductions扣款、actual_salary实发工资、salary_date工资月份。这样每个月生成一条记录员工所有人能通过月度记录查看工资变化情况。考勤表的字段设计为employee_id员工ID、work_date日期、checkin_time上班打卡时间、checkout_time下班打卡时间、status考勤状态。status在插入数据时可以按照设定的上下班时间自动判断也可以留到后台定时任务统一计算。我建议在插入打卡记录时只保存真实时间状态用定时任务每天统一计算这样能避免用户补卡时修改状态导致的数据不一致。2.3 建表时的几个实际坑第一表名不要直接用user。MySQL 里user在多处上下文中属于保留字或特殊含义词很容易出现意想不到的问题。我用的是sys_user所有系统级表统一加sys_前缀业务表直接用英文名称。第二所有时间字段必须带默认值。create_time用DEFAULT CURRENT_TIMESTAMPupdate_time使用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这样新增和更新数据时不需要手动维护这两个字段。第三数据库连接串必须指定时区。我遇到过最典型的问题就是时间字段差了 8 个小时原因是 JDBC 连接串没加serverTimezoneAsia/Shanghai。在 MySQL 8 下更是必须配置否则直接报错。第四建表 SQL 脚本里一定要写字段注释。一个团队中期接手项目时最痛点的就是看一张几十个字段的表不知道每个字段是干嘛的。把 COMMENT 写好后面排查问题能省一半时间。3. 后端核心功能实现与关键代码解析3.1 Spring Security JWT 认证登录完整流程认证是整个系统的安全基石。登录流程拆开看有四个环节。第一个环节用户提交用户名和密码到/login接口。后端用AuthenticationManager发起认证这个过程中 Spring Security 会根据UserDetailsService加载对应用户然后用BCryptPasswordEncoder校验密码。为什么用 BCrypt 而不是普通的 MD5 或 SHA256因为 BCrypt 是加盐哈希每次加密结果都不同即使两个用户密码相同也存在数据库里的密文不同能有效对抗彩虹表攻击。第二个环节认证通过后生成 JWT Token。JWT 里负载部分我放了userId、username、loginTime设置有效期 8 小时。Token 生成后返回给前端同时把用户基本信息、权限标识、菜单列表一起返回前端拿到数据存到 Vuex 和 localStorage 里刷新页面后不会被清掉。第三个环节前端在每次请求时通过 axios 拦截器把 Token 放到请求头Authorization: Bearer token里。后端自定义一个 JWT 过滤器继承OncePerRequestFilter在 Spring Security 过滤器链中生效。这个过滤器做的事很简单解析请求头的 Token校验签名和过期时间把解析出来的用户信息加载到 SecurityContext 中。第四个环节是权限校验。在PreAuthorize(hasAuthority(system:employee:add))注解里直接配权限标识Spring Security 会拿着当前用户的权限集合做比较不匹配就直接抛 403。菜单的权限标识如何与用户关联我在 2.2 里说的 RBAC 五表结构就是干这个用的用户登录后查出角色角色关联菜单菜单里的perms字段就是权限标识。3.2 员工管理 CRUD 与 Excel 导入导出实现员工管理模块是系统的重头戏功能包括分页查询、新增员工、编辑员工、删除员工、Excel 批量导入和导出。分页查询的接口参数是pageNum、pageSize、keyword、departmentId。keyword支持按员工姓名、手机号、员工编号模糊查询。在 MyBatis-Plus 里我使用LambdaQueryWrapper动态拼条件注意 keyword 为空字符串时不要拼进查询条件否则 SQL 会变成WHERE name LIKE %%这种条件是永远返回 true 的导致分页数据量不对。新增员工时有三重校验手机号唯一、身份证号唯一、员工编号唯一。这三个字段在数据库层面设置了唯一索引业务代码层也有校验双保险防止并发情况下绕过程序校验直接写入重复数据。校验失败时返回友好的错误信息而不是系统默认的 500 异常。Excel 导入导出用 EasyExcel这是阿里开源的工具底层是 SAX 模式解析几十万行数据也不会内存溢出。导入的流程是前端上传 Excel 文件到后端后端读取每行数据并转换成员工对象逐行校验统计成功条数和失败条数失败数据返回错误文件让用户下载修改。导出流程则用 TableField 一次查全部写 Excel 文件通过 HTTP 响应流下载。3.3 请假审批模块的状态机设计经验请假模块看起来简单做起来有很多细节。一张请假申请表核心字段是leave_type请假类型、start_time开始时间、end_time结束时间、reason请假原因、status审批状态。状态我设计了 0 待审批、1 已通过、2 已驳回三种。一开始有人用布尔值is_approved来表示是否通过这明显不够用因为还有待审批和驳回两种情形。用 int 加字典映射的好处是扩展方便后续如果加“已取消”状态枚举和字典加一个值就行。审批链路是员工提交请假申请状态为待审批该员工的部门负责人登录系统后能看到待办事项通过或驳回状态置为已通过或已驳回同时记录审批人和审批时间。这里有个问题谁有资格审批这个员工我在部门表里设计了leader_id字段指向部门负责人的员工ID。审批权限通过这个字段判断员工发起审批时自动带上部门负责人 ID查询待办时只需要按这个 ID 查找。请假日期与考勤记录的联动也要考虑。审批通过的请假需要回写对应日期的考勤状态标记为请假中避免同一天既显示缺勤又显示请假产生数据矛盾。4. 前端 Vue 工程落地与前后端协作细节4.1 前端动态路由与权限控制实现前端的权限控制分两步走。第一步是登录成功后接口会返回当前用户拥有的菜单列表前端根据这个菜单列表动态注册路由。后端菜单表有对应的路由字段path和组件路径前端把菜单列表转成路由配置用router.addRoutes()添加到 Vue Router 中。这样一来不同角色的用户登录后看到的侧边栏菜单是完全不同的。第二部是路由守卫。在router.beforeEach里判断如果没有 Token一律跳转到登录页如果有 Token 但访问的页面不在当前用户的路由列表中跳转到 404 或提示无权限。这一步是前端拦截解决的是“用户手动改 URL 地址直接访问未授权页面”的问题。这里有一个很典型的坑刷新页面后动态路由丢失。因为 Vuex 里的菜单数据存在内存中页面一刷新就没了。解决办法是在路由守卫里增加判断如果 Vuex 里没有菜单数据但本地有 Token先调用后端接口重新获取用户信息和菜单再放行路由。这个逻辑写不好刷新白屏的问题就会一直困扰你。4.2 axios 封装与统一异常处理机制axios 不封装直接用也能跑但几十个页面反复写重复的请求代码会累死人。我封装了一个request.js核心逻辑包括三块。创建 axios 实例时设置baseURL为/api这样前端代理时只需要将/api前缀的请求转发到后端服务一处配置全局生效。请求拦截器里从 localStorage 取 Token只要存在就放在请求头中。这里要注意Token 为空时不要假设置空字符串否则后端解析空字符串会抛异常。响应拦截器是重中之重。后端返回的 JSON 结构统一是{ code: 200, msg: 操作成功, data: ... }所以前端拦截器拿到响应后先判断业务状态码code 为 200直接返回 data 给调用方业务代码里拿到手就直接是数据不用再解包。code 为 401说明登录失效清掉本地 Token用window.location.href /login跳转登录页。code 为 403提示“没有操作权限”。code 为 500统一提示“服务器异常请稍后重试”避免把后端堆栈信息直接暴露给用户。4.3 表格、弹窗和分页的标准页面实现套路这类系统的前端页面大部分都是“表格 弹窗 表单 分页”的组合。我总结了一套固定写法可以复制到任何页面使用。列表页在created()生命周期里调用查询方法查询参数用reactive或data对象维护包括pageNum、pageSize、keyword。表格用 Element UI 的el-table分页用el-pagination设置current-page、page-size、total属性页码变化和每页条数变化事件触发的回调里重新拉数据。新增和编辑共用一个弹窗表单。这里有一个细节编辑时打开弹窗表单展示的数据应该是源数据的深拷贝绝对不能直接绑定表格里的源对象。否则弹窗里修改一个字段表格数据也跟着变还没点保存就改了页面显示这是新手最容易踩的坑。深拷贝用Object.assign({}, row)或JSON.parse(JSON.stringify(row))都行。表单重置也有讲究。el-form的resetFields重置的是表单初始值如果在编辑回填之后再重置字段值不会完全清空。所以重置的时机应该放在弹窗打开之前先重置、再回填、再展示。校验规则配合prop、rules使用输入框失焦时自动触发校验。删除操作必须有二次确认。用ElMessageBox.confirm弹确认框用户点“确定”后才调删除接口。否则误点删除就整条数据没了虽然做了逻辑删除但页面数据变化还是会让人觉得系统不稳定。5. 部署发布与排查记录5.1 本地开发环境搭建与版本兼容开发环境的关键版本组合如下JDK 8 或 11Maven 3.6MySQL 5.7 或 8.0Redis 6.xNode 14.16前端包管理器 npm 或 yarn。这套组合是我验证过兼容性最高的Spring Boot 2.7.x JDK 8 跑生产环境非常稳定前后端在本地和服务器都不会出现版本冲突问题。MySQL 初始化时执行项目中的sql目录下的脚本即可建议按顺序执行先建库再建表最后插入初始化数据。脚本中包含了管理员账号、演示角色、菜单数据登录系统用的初始密码写在 README 文档里首次登录建议改密。Redis 在这个项目里主要用来存储图形验证码和部分缓存数据。很多人有一个认知误区Spring Security JWT 认证不依赖 Redis。确实不依赖Token 里不存服务端状态。但验证码和频繁查询的部门树数据用 Redis 缓存能明显降低数据库压力。5.2 开发联调中遇到的高频问题记录跨域问题是前后端分离项目无法绕过的坎。本地联调时前端服务跑在 8080 端口后端跑在 9090 端口浏览器默认会拦截跨域请求。我的解决方案是在vue.config.js里配置 devServer proxy将所有/api开头的请求代理到http://localhost:9090这样前端视角里请求同源不触发浏览器跨域拦截。上线后用 Nginx 反向代理同样策略/api前缀转发到后端服务。中文乱码问题也很常见。确保三处配置一致后端application.yml里设置server.servlet.encoding.force-responsetrue字符集用 UTF-8数据库连接串上加characterEncodingutf8前端 HTML 文件声明charsetUTF-8。三处都对了乱码问题基本绝迹。分页插件不生效的问题。MyBatis-Plus 在 3.4.0 版本之后改了分页插件包名旧版是PaginationInterceptor新版改成了MybatisPlusInterceptor配合PaginationInnerInterceptor。如果代码用的是旧包名新依赖下会直接报类型找不到。这个问题的排查方法也很简单看启动日志里有没有分页插件注册成功的提示没有就去 config 配置类里检查。时间格式多了个“T”的问题后端返回的日期字段默认是 ISO 格式形如2025-06-08T10:30:00。前端显示时很别扭。解决办法是实体类时间字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)强制指定 Jackson 的格式化方式。5.3 生产环境的 Nginx 配置与部署要点后端打包用 Maven 的mvn clean package -DskipTests生成的可执行 Jar 包放到服务器上用nohup java -jar拉起服务。前端执行npm run build生成的dist目录拷贝到 Nginx 的html目录。Nginx 配置的关键是 location 规则。主 location 匹配静态资源location /api/反代到后端服务同时要处理 history 路由模式的单页应用刷新 404 问题。Vue Router 如果用了 History 模式需要在 Nginx 配置里加try_files $uri $uri/ /index.html否则用户直接访问或者刷新子路由页面Nginx 找不到对应的静态文件就会报 404。生产环境安全方面还要注意JWT 密钥不要写死在代码里提交到代码仓库应该放到配置文件里并用环境变量覆盖。数据库账号密码同样处理。Nginx 要限制上传文件大小否则 Excel 导入稍大一点就被 Nginx 拦截接口报 413 错误。6. 源码阅读顺序与二次开发扩展思路6.1 拿到源码后建议的阅读顺序很多人拿到项目源码第一时间去看 Controller其实效率很低看半天不知道每个类是干嘛的。我推荐的阅读顺序是先看数据库脚本。SQL 脚本里能看到系统有哪些表、表之间什么关系这是理解业务的捷径。接着看application.yml了解项目配置的数据库、Redis、端口等基本信息。然后看启动类确认包扫描范围。最后顺着一条完整的业务链路走从前端登录接口发起到后端 Token 校验再到菜单权限加载、部门查询、员工分页查询把整条调用链走通整个系统的逻辑就串起来了。读代码时抓住几个关键类统一返回结果类、全局异常处理器、JWT 工具类、过滤器配置类。这几个类基本决定了系统的通用处理逻辑看懂了它们各业务模块的代码就是纯粹的增删改查了。6.2 这个系统还能怎么扩展人事管理系统的可扩展性很强我实际工作中做过的扩展方向有这些。消息通知机制。请假审批通过或者驳回时可以用 WebSocket 给员工推送实时消息替代现在纯粹靠页面刷新查看结果的方式。考勤数据可视化。每月生成考勤统计报表用 ECharts 展示部门出勤率、迟到趋势、请假日历这类图表在中高层管理者的看板视图里很受欢迎。绩效考核模块。给员工设置 KPI月度或季度进行考核评分考核结果关联绩效工资和奖金计算。这是把人事系统从信息管理提升到决策支持的关键模块。钉钉和企业微信集成。将打卡数据对接外部办公平台的考勤记录HR 不用重复录入审批流也可以在办公平台里直接完成。合同电子化。员工劳动合同到期提醒、在线电子签章、合同模板管理这些功能在员工规模几百人以上的公司是刚需。7. 做这类项目的几点真实体会把整套系统从前到后写完最深的体会是人事管理系统真正的门槛不在技术而在对业务的理解和对边界情况的处理。一个员工离职了他的账号要不要禁用他审批过的记录是不是需要保留他上个月的考勤数据还能不能修改这些场景不会出现在技术文档里但真实业务里一定会遇到做的时候要认真想清楚规则再动手否则后期改起来相当痛苦。另一个体会是写这种管理系统代码结构比代码技巧重要得多。Controller 层尽量薄业务逻辑全部下沉到 Service。类似登录用户信息获取、分页对象构造、数据库异常转换这类通用能力尽早抽成公共组件不要在每个模块里复制粘贴。后来维护和扩展的时候你会感谢自己当初没有偷懒。最后想分享一个小建议无论是练习项目还是实际交付把文档写完整。一个清晰的 README、一份数据库设计说明、一份环境搭建文档这些能让你三个月之后再回看自己的代码时快速上手也能让接手的人对你产生信任感。这套人事管理系统本身就是“源码 数据库 文档”的结构按要求把环境跑起来后再逐步去改代码、加功能练习效果会好很多。