SpringBoot+Vue3+MyBatis前后端分离人事管理系统开发实战 平时做中小企业的管理系统最常用也最稳妥的一套组合就是 SpringBoot 加 Vue3 加 MyBatis。这套人事管理系统源码前端用 Vue3 配上 Element Plus 做界面后端是 SpringBoot 提供接口MyBatis 负责数据库操作MySQL 存数据。整体采用前后端分离架构。这篇文章就从这个项目的完整设计思路、核心模块实现到实际开发中踩过的坑一次性梳理清楚给准备做类似管理系统的开发者一个可参考的范本。很多刚入行的朋友拿到这类项目源码常常不知道从哪里看起。其实只要抓住几条主线先看数据库表结构再看后端接口设计最后看前端页面如何调用接口。这三条线理清楚了整个系统在你脑子里就立体了。下面我会按实际操作顺序把整个系统的开发心路和关键技术点拆开讲。1. 系统整体设计与技术选型思路1.1 为什么是 SpringBoot Vue3 MyBatis先说技术选型。中小企业的人事管理系统特点是业务逻辑不算复杂但涉及的角色多、状态多、报表多。选 SpringBoot 是因为它足够稳定社区生态成熟招人也容易。SpringBoot 把大量的自动配置做好了开发者能专注写业务代码这对小团队来说是实打实的效率提升。Vue3 是现在前端的主流选择组合式 API 写起来比 Vue2 的选项式 API 更清爽配合 Vite 开发时的热更新速度非常快。Element Plus 是国内做后台管理系统最顺手的组件库表格、表单、弹窗这些现成组件直接拿来用开发效率很高。MyBatis 这个选择看起来有点“老派”但实际用起来非常灵活。人事管理系统的 SQL 往往比较复杂比如考勤统计需要多表关联加条件聚合MyBatis 可以精确控制 SQL排错也直观。有些人会问为什么不用 MyBatis-Plus其实在这个项目里原生 MyBatis 配合 XML 文件更轻SQL 可控性更强写复杂统计的时候心里更有底。当然如果追求开发速度引入 MyBatis-Plus 写单表 CRUD 会更快这两者看个人取舍。1.2 前后端分离架构怎么拆前后端分离不只是把代码分成两个目录那么简单。这个项目的架构划分是这样的前端负责页面渲染、用户交互、表单校验、路由控制后端只提供 JSON 数据接口负责业务逻辑、权限校验、数据持久化两端通过 HTTP 接口通信统一使用 JSON 格式传输数据开发的时候前后端可以并行推进只要提前约定好接口文档。前端用 Mock 数据先开发页面后端把接口逻辑写好最后联调时对接真实数据。值得注意的是前后端分离会带来跨域问题。项目里通过后端配置跨域过滤器解决允许指定来源的请求访问前端开发服务器走代理转发这样能避免浏览器跨域拦截。1.3 系统的模块划分与核心功能人事管理系统按功能边界划分成六大模块每个模块关注一组特定业务。这里有一个模块划分参考表模块名称核心功能涉及角色系统管理用户登录、角色权限、菜单管理、字典管理系统管理员组织架构部门管理、岗位管理系统管理员、人事专员员工管理员工档案、入职登记、调动、离职、转正人事专员考勤管理排班设置、打卡记录、请假审批、考勤统计员工、主管、人事专员薪资管理薪资结构设置、工资计算、工资条查看人事专员、财务招聘管理招聘计划、候选人管理、面试安排、录用通知人事专员、面试官每个模块内部再细分页面和操作按钮权限精细到按钮级别。比如员工管理页面人事专员可以编辑员工信息普通主管只能查看本部门员工普通员工只能看自己的信息。1.4 项目整体目录结构拿到项目源码先看目录结构。前后端分离的项目通常有两个根目录。后端遵循标准的 SpringBoot 工程结构按功能分包src/main/java/com/company/hr ├── controller # 接口层接收前端请求 ├── service # 业务逻辑层接口加实现类 ├── mapper # MyBatis 数据访问层 ├── entity # 数据库实体对象 ├── dto # 数据传输对象接收前端参数 ├── vo # 视图对象返回前端的数据 ├── config # 配置类跨域、拦截器等 ├── common # 公共类统一返回结果、异常处理 └── utils # 工具类前端按照 Vue3 的工程规范组织src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── layout # 布局组件 ├── router # 路由配置 ├── store # Pinia 状态管理 ├── utils # 工具函数 └── views # 页面组件这种分包方式是 Java Web 项目最经典的 MVC 分层。理解它只需要一句话前端请求打到 controllercontroller 叫 service 处理业务service 叫 mapper 查数据库查完的数据再原路返回。2. 登录认证与权限控制的前后端设计2.1 登录流程与 Token 认证机制人事管理系统必须严格区分谁能看什么、谁能改什么所以认证和授权是第一关。项目采用 Token 认证方案整体流程是用户输入账号密码登录后端校验账号密码正确性校验通过后生成一个 Token 字符串返回给前端前端把 Token 存在本地存储每次请求接口时放在请求头里带过去后端拦截器校验 Token 合法性从 Token 中解析出用户身份根据用户身份查询权限信息决定是否允许访问接口为什么不用传统的 Session 方案因为前后端分离后后端服务是无状态的Session 依赖服务器内存不方便水平扩展。Token 方案天然适合这种架构服务器不需要保存会话状态请求里自己带着身份信息。Token 生成的逻辑不算复杂用 UUID 或者自定义规则生成唯一字符串然后把 Token 和用户信息存到 Redis 里设置过期时间。Redis 存储比内存存储更可靠重启后数据不丢失还支持自动过期清理。注意Token 一定要设置过期时间不能永久有效。人事系统里有大量敏感数据长期有效的 Token 一旦泄露后果非常严重。一般设置为 8 小时到 24 小时根据企业使用习惯调整。2.2 自定义注解 AuthCheck 实现接口权限校验这个项目权限控制的核心是一套自定义注解。开发时只需要在接口方法上加个注解就能控制谁能访问这个接口。先看注解的定义Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface AuthCheck { String value() default ; }value 里面填权限标识比如员工管理的编辑权限写employee:edit。在接口上这样使用AuthCheck(employee:add) PostMapping(/employee) public Result addEmployee(RequestBody EmployeeDTO employeeDTO) { return employeeService.addEmployee(employeeDTO); }接口被执行前拦截器会检查当前登录用户是否拥有employee:add这个权限。校验逻辑用 Spring 的拦截器实现Component public class AuthInterceptor implements HandlerInterceptor { Autowired private RedisTemplateString, Object redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 如果不是方法级别的请求直接放行 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; AuthCheck authCheck handlerMethod.getMethodAnnotation(AuthCheck.class); if (authCheck null) { return true; } // 从请求头获取 Token String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { return unauthorized(response); } // 查询权限信息并校验此处省略细节 SetString permissions getPermissionsByToken(token); if (!permissions.contains(authCheck.value())) { return forbidden(response); } return true; } }这套方案的优点是开发效率极高。新增一个接口时只需要想清楚这个操作需要什么权限然后加上对应注解就行不需要到处写校验逻辑。项目里所有权限标识在数据库中有统一管理做到了权限标识的集中配置。2.3 前端路由守卫与按钮级权限控制后端做了接口权限控制前端也要做对应的控制。否则用户虽然调不了接口但页面还是能打开体验不好。前端的权限控制分成两层第一层是路由守卫。用户登录后前端从后端拉取当前用户拥有的菜单和权限标识动态生成可访问的路由。没有权限的页面用户在地址栏输入路径也进不去会被路由守卫拦截并跳转到首页。实现思路是路由表不一次性注册完而是登录后根据权限动态添加。第二层是按钮级控制。页面上“新增”、“编辑”、“删除”这些按钮根据用户权限决定显不显示。项目里封装了一个权限指令// 自定义指令 v-permission app.directive(permission, { mounted(el, binding) { const requiredPermission binding.value; const userPermissions useUserStore().permissions; if (!userPermissions.includes(requiredPermission)) { el.parentNode.removeChild(el); } } });页面上使用只需要这样写el-button v-permissionemployee:add typeprimary新增员工/el-button双重校验下安全性就比较完善了。前端控制了入口后端兜底校验任何绕过前端直接调接口的行为都会被拦截器挡住。2.4 为什么不用 Spring Security 而选择自定义方案这个问题很多人会问。Spring Security 功能强大但学习曲线比较陡配置复杂。中小型人事系统的权限模型相对简单用自定义拦截器加注解这套方案代码量小逻辑透明出了问题也好排查。项目里的角色和权限关系通过数据库管理动态刷新权限也用 Redis 缓存灵活度足够。当然如果项目后续要做 OAuth2 单点登录、精细到数据级别的行级权限控制或者要接入外部身份认证系统那 Spring Security 还是更合适的选择。总之看项目阶段和团队水平来定。3. 核心业务模块的实操拆解3.1 部门管理树形结构与层级展示组织架构模块里最典型的功能是部门管理。部门的层级关系是树形的比如总公司下面有技术部、市场部技术部下面又分前端组、后端组。数据库中的部门表设计如下字段名类型说明idbigint部门IDparent_idbigint上级部门ID顶级为0dept_namevarchar部门名称leader_idbigint部门负责人sort_orderint排序号statustinyint状态 1启用 0停用create_timedatetime创建时间前端展示树形结构时后端返回的是列表数据前端用递归方法转成树function buildTree(list, parentId 0) { const tree []; list.forEach(item { if (item.parentId parentId) { const children buildTree(list, item.id); if (children.length) { item.children children; } tree.push(item); } }); return tree; }这种实现简单直接缺点是如果部门数量很大前端递归会消耗较多性能。人事管理系统通常部门数量在几十到几百之间前端递归问题不大。后端在新增或修改部门时要判断上级部门是否存在、是否形成了循环引用。比如有两个坑把部门A的上级设置成部门B同时部门B的上级设置为部门A会形成死循环删除部门时如果该部门下面还有子部门或者有员工应该阻止删除这些看似不起眼的细节正是实际业务里最容易出问题的地方。3.2 员工管理档案信息与企业字典员工管理是整个人事系统的数据核心其他模块都是围绕员工展开的。员工表字段会相对多一些包括姓名、工号、性别、出生日期、身份证号、手机号、邮箱、学历、入职时间、部门ID、岗位ID、员工状态等。这里有一个经验之谈不要把员工信息全部塞在一张表里应当拆分成员工主表和员工详情表。主表存高频查询的字段如姓名、工号、部门、状态列表页查询和展示都用它。详情表存低频使用的大字段比如紧急联系人、工作经历、教育经历、证件照片等。这样拆分后列表查询不会因为要查大量字段而变慢。员工列表的查询条件通常有姓名、工号、部门、入职时间段、状态。如果部门选择了父级部门应该默认把该部门及其所有子部门的员工都查出来。这个需求在 SQL 里实现有两种思路第一种是递归查询子部门 ID 集合然后WHERE dept_id IN (...)。MySQL 8 支持递归 CTE写起来还挺方便。第二种是在部门表里维护一个paths字段保存从顶层到当前节点的完整路径。比如某子部门路径是0/1/5/9查询时用dept_id 9或者paths LIKE %/5/%。实际项目建议用第二种查询效率高逻辑也简单代价是维护路径字段时需要多做一步更新操作。员工导入导出也是高频功能。导出用 EasyExcel 封装好的工具类实体上注解对应列名几分钟就能搞定。导入时最麻烦的是模板校验比如身份证号格式、手机号格式、必填项是否为空、部门名称能否匹配上这些都要在导入逻辑里逐项判断然后把错误信息收集起来返回给用户查看。3.3 考勤管理排班、打卡与统计规则考勤模块容易写复杂中小型企业需求相对标准化。排班解决的是“几点上班、几点下班、一周上几天”的问题。项目里支持多种班次固定班、排班制、自由打卡。多数中小企业用固定班制也就是周一到周五上班周末双休早上九点到晚上六点。打卡数据的处理是考勤模块的核心算法。员工打卡时间要和班次时间对比判断是否迟到、是否早退。这里有一个细节上下班打卡有弹性时间。比如规定 9 点上班但是 8 点 50 分到 9 点 10 分之间打卡都算正常。这个弹性参数应该做成配置不要写死在代码里。考勤统计页面的表格设计要提前规划好横轴是日期纵轴是员工单元格里显示上班时间、下班时间、异常状态迟到/早退/缺卡。统计时用 SQL 按员工和月份分组外层用 Java 做数据处理生成报表数据。请假审批适合用工作流引擎但小型系统自己写一套状态流转就够了。请假单状态包括待审批 - 主管通过 - 人事确认 - 完成或者审批不通过 - 驳回。用 status 字段维护状态机每个状态的流转条件在代码里明确写出来。不要复杂化。3.4 薪资管理计算规则与数据安全薪资计算是最敏感的功能。先说一下整体思路薪资结构包括基本工资、岗位工资、绩效工资、补贴、扣款、五险一金等。项目里每个员工的薪资结构在一张薪资配置表里工资计算就是把考勤数据和薪资数据结合起来算。工资计算公式示例如下应发工资 基本工资 岗位工资 绩效工资 补贴 实发工资 应发工资 - 五险一金个人部分 - 个税 - 其他扣款具体实现时每个计算项做成独立的计算方法不要把所有逻辑堆在一个大方法里。比如计算考勤扣款时根据迟到次数、请假天数、缺勤天数分别计算每个函数独立测试。这样做的好处是改需求时只动对应的计算方法不会影响其他逻辑。薪资数据的安全性要多说一句工资条查询接口必须限制只能查自己或者有权限的员工的工资。如果接口设计时不小心允许传入任意员工 ID 查询那就严重越权了这是安全事故。实际编码时查询工资条前要校验当前登录用户和目标员工的关系要么是自己要么是有人事权限的管理员。3.5 招聘管理数据流转与看板展示招聘模块相对独立数据流转是候选人 - 简历筛选 - 面试安排 - 面试评价 - 录用审批 - 入职。系统的价值在于把线下混乱的招聘流程管起来。招聘管理里一个好用的功能是招聘看板。用拖拽式的看板展示候选人目前处于哪个阶段面试官可以直接拖拽更新状态。前端实现就是用 Vue3 的拖拽组件后端对应一个状态字段更新接口。招聘模块的员工入职和人事系统的员工模块是联动的。候选人录用审批通过后可以一键创建员工档案并发送入职通知这样两个模块的数据就串起来了。4. 数据库表设计与关键实现技巧4.1 核心表结构总览整个系统的数据库设计遵循几个原则主键统一用雪花 ID、时间字段统一包含创建和更新时间、逻辑删除字段统一叫 deleted、状态字段统一用整数。这样设计的最终效果是所有表结构风格一致开发时不需要翻文档去猜某个字段的含义。以下是核心表的清单表名说明关键字段sys_user系统用户username, password, statussys_role角色表role_name, role_codesys_menu菜单/权限表parent_id, menu_name, permissionsys_user_role用户角色关联user_id, role_idsys_role_menu角色权限关联role_id, menu_iddept部门表parent_id, dept_name, leader_idemployee员工主表emp_no, name, dept_id, statusemployee_detail员工详情表emp_id, education, emergency_contactattendance_record考勤打卡记录emp_id, clock_in, clock_out, work_dateleave_request请假申请emp_id, start_time, end_time, type, statussalary_config薪资配置emp_id, base_salary, post_salary, performance_salarysalary_record工资发放记录emp_id, month, gross_salary, net_salaryrecruit_candidate候选人name, phone, position, stage, status4.2 多对多关系与字典表的处理用户、角色、菜单之间是多对多关系通过关联表连接。查询用户的权限列表时需要多表联查用户表关联用户角色表再关联角色菜单表最后连接菜单表拿到权限标识字符串集合。SELECT DISTINCT m.permission FROM sys_user u LEFT JOIN sys_user_role ur ON u.id ur.user_id LEFT JOIN sys_role r ON ur.role_id r.id LEFT JOIN sys_role_menu rm ON r.id rm.role_id LEFT JOIN sys_menu m ON rm.menu_id m.id WHERE u.username #{username} AND m.permission IS NOT NULL字典表的设计也值得一说。员工性别、学历、婚姻状况、政治面貌这类枚举值项目里统一定义在字典表里。好处是以后新增一个枚举值不需要改代码管理员在系统管理界面就能维护。前端下拉框从后端接口动态加载字典数据代码更通用。4.3 MyBatis XML 中动态 SQL 的使用MyBatis 的 XML 配置文件写复杂查询时动态 SQL 是核心工具。比如员工列表查询搜索条件是可选的用where和if标签组合select idselectEmployeeList resultTypecom.company.hr.vo.EmployeeVO SELECT e.id, e.emp_no, e.name, e.dept_id, d.dept_name, e.position, e.phone, e.status, e.entry_date FROM employee e LEFT JOIN dept d ON e.dept_id d.id where if testname ! null and name ! AND e.name LIKE CONCAT(%, #{name}, %) /if if testempNo ! null and empNo ! AND e.emp_no #{empNo} /if if testdeptId ! null AND e.dept_id IN ( SELECT id FROM dept WHERE paths LIKE CONCAT(%/, #{deptId}, /%) ) /if if teststatus ! null AND e.status #{status} /if if teststartDate ! null and startDate ! AND e.entry_date #{startDate} /if /where ORDER BY e.create_time DESC /selectwhere标签会智能处理条件如果所有条件都没传它不会生成一个裸的 WHERE。这里最值得注意的就是 deptId 的过滤逻辑利用部门表的 paths 字段匹配所有子部门比递归查询效率高得多SQL 也简洁。4.4 MySQL 索引的设计经验写 SQL 查询很慢时先看有没有走索引。用EXPLAIN关键字查看执行计划主要关注 type 和 key 两列。项目的经验是查询频率高的字段像员工表的 emp_no、部门表的 id、考勤记录的 work_date 都要建索引。唯一性约束也不能缺席。比如员工工号必须唯一否则数据错乱后排查起来非常痛苦。在设计表的时候用UNIQUE约束在数据库层面兜底比在应用层判断靠谱。应用层代码判断可能会有并发问题数据库的约束则不存在这种问题。索引不是越多越好。有些字段本身区分度不高比如 status 字段只有几个值建索引反而没用。实际情况是联合索引用在对常见组合查询场景的优化上比如考勤记录表中 (emp_id, work_date) 组成联合索引查询某员工某个时间段的数据就非常高效。5. 典型问题与排查技巧实录5.1 跨域请求被拦截前后端分离开发时前端页面运行在 5173 端口Vite 默认后端接口运行在 8080 端口。浏览器的同源策略会拦截跨端口请求前端控制台报错内容是 “CORS policy” 相关。解决办法是后端配置跨域过滤器Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); // 生产环境应该写具体域名不要用 * config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意setAllowCredentials(true)表示允许携带 Cookie 或认证信息这个和addAllowedOriginPattern(*)搭配使用时不能简单使用addAllowedOrigin(*)否则浏览器会报错。5.2 账号权限数据不刷新、界面缓存问题系统运行一段时间后管理员给某个用户加了权限结果该用户重新登录后还是没有新权限。排查思路是从上到下检查链路。常见原因之一是权限数据被静态存储了登录时把权限列表存在了 CU 端之后不再拉取。解决方案是登录成功后先拉取最新的权限数据再生成前端路由。另一个原因是 Redis 中缓存了权限数据。管理员修改权限后需要主动清除对应用户的缓存。项目里应在用户管理页面提供一个“刷新权限缓存”的操作按钮手动触发缓存清理。这样做虽然多了一步人工操作但很稳妥不会出现权限变更后立即生效又清理不干净的问题。5.3 Long 类型数据返回前端精度丢失数据库表的主键是雪花算法生成的 Long 类型18 位数字。JavaScript 的 Number 类型最大能精确表示的整数是 2 的 53 次方16 位。所以超过 16 位的 Long 类型返回到前端后最后几位数字会变成 0。比如 ID 是 123456789012345678前端拿到的可能是 123456789012345670直接导致后续操作比如编辑、删除找不到对应记录。图解解决方案是使用 Jackson 序列化时把 Long 类型转成字符串返回spring: jackson: generator: write-numbers-as-strings: true这种方式全局生效但会把所有数字都转成字符串严格说不合适。更精准的做法是在实体类的 ID 字段上加上JsonSerialize(using ToStringSerializer.class) private Long id;这个坑太经典了几乎每个前后端分离项目都会碰到一次。5.4 考勤统计报表数据时区问题有一段时间考勤统计的日期总是差一天排查后确认是数据库连接串没有指定时区。MySQL 驱动默认使用的时区可能与服务器本地时间不一致导致日期字段读取时出现了偏移。解决办法是在 JDBC 连接串上加上参数jdbc:mysql://localhost:3306/hr_system?serverTimezoneAsia/Shanghai建议所有项目都显式指定服务器时区不要依赖默认值这样环境切换时不会踩时间坑。5.5 常见问题速查表现象排查方向解决方案前端页面调用接口报 404检查接口路径拼写、Controller 的 RequestMapping核对后端日志确认请求打到哪个地址登录后没有菜单显示用户没有配置角色或角色没有分配菜单系统管理里检查用户角色和角色菜单分配上传文件失败报 413Nginx 上传大小限制修改 client_max_body_size 配置MySQL 连接超时连接池配置过期时间过长调低连接空闲超时时间添加自动重连参数列表查询很慢用 EXPLAIN 查看 SQL 是否走索引根据执行计划添加对应索引前端修改数据后列表不刷新接口返回成功但页面没有调用刷新方法在操作成功后重新调用列表查询方法新增员工时部门下拉为空部门表数据未初始化或接口异常检查后端日志确认部门列表接口是否正常表格里的每一个问题都来自实际开发经历。尤其要提一句遇到问题先看后端日志不要在前端瞎猜。日志是排查问题的最直接入口SpringBoot 的控制台输出加上日志文件的记录能还原大部分问题的真相。6. 部署发布与运维要点6.1 前后端项目的构建打包开发完成后进入部署阶段。后端项目使用 Maven 打包执行以下命令mvn clean package -DskipTests打包后在 target 目录下生成一个 jar 文件这就是一个可独立运行的 SpringBoot 应用。通过java -jar命令启动java -jar hr-system.jar --spring.profiles.activeprod前端项目执行构建命令生成静态文件目录npm run build构建产物在 dist 目录下。把所有静态文件交给 Nginx 托管同时配置 Nginx 将/api开头的请求反向代理到后端服务端口。Nginx 的简单配置示例server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /usr/share/nginx/html; index index.html; 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这行配置很重要它确保前端路由在刷新页面时不会出现 404。6.2 数据库初始化与数据迁移部署到新环境时数据库初始化步骤是先创建数据库再导入 SQL 脚本。项目里应准备一份初始化 SQL 文件包括建库语句、建表语句、初始数据管理员账号、基础菜单、示例字典数据。后续改动表结构时不要直接在原来的建表脚本上改应该新增独立的增量 SQL 脚本sql/ ├── init.sql # 初始化脚本 ├── update_20250110.sql # 2025年1月10日的变更 └── update_20250120.sql # 2025年1月20日的变更这样生产环境升级时只需要按顺序执行新增的增量脚本不容易出错。这个习惯非常值得培养能帮你避免很多“本地没问题、线上报错”的情况。6.3 环境配置与日志管理SpringBoot 使用多环境配置不同环境加载不同的配置文件application-dev.yml 开发环境数据库连接本机日志级别 DEBUGapplication-prod.yml 生产环境正式数据库日志级别 INFOapplication.yml 公共配置含通用设置生产环境的数据库密码、Redis 密码等敏感信息注意不要硬编码在配置文件里而要从环境变量或配置中心读取。日志管理方面建议配置 Logback 按天生成日志文件保留最近 30 天的日志。出现异常时通过日志能定位到具体的接口、参数、SQL 执行情况。项目里要在异常处理类中统一记录异常堆栈这样排查问题会轻松很多。6.4 项目后续可扩展方向如果这个系统要继续演进有几个方向可以参考。第一个是引入工作流引擎让请假审批、报销审批、离职审批这些流程配置化不需要每次加一个流程就写一套代码。第二个是增加 Excel 导出功能的深度优化比如按部门导出花名册、按月份导出工资汇总表这类导出需求在实际使用中非常高频。第三个是增加数据权限控制。目前的权限控制停留在功能权限即能不能访问某个接口。数据权限解决的是“能看到哪些数据”的问题例如事业部负责人只能看到本部门的数据普通员工只能看自己的数据。在复杂组织架构里数据权限的设计需要特殊处理常用的做法是在 SQL 层面追加数据范围的条件或者引入专门的数据权限框架。第四个是接入企业微信或钉钉。人事系统的价值在于流程协同。如果员工的手机号和企业微信打通考勤打卡可以直接用企业微信的数据请假审批也可以在移动端完成。这个方向能让系统从纯后台管理工具变成一个全员使用的工具价值提升会非常明显。实际开发中的几点心得整个项目开发下来我在实际操作中体会最深的一点是人事管理系统看似简单实则细节非常多。如果你刚开始接触这类项目不必追求把所有功能都做到完美先把登录、员工管理、部门管理这三条主线跑通再不断迭代补功能。初期把公共模块做好包括统一返回结果、统一异常处理、分页组件封装、文件上传封装、权限控制框架后面加功能会越来越顺。再分享一个小技巧不要轻易相信前端传过来的任何 ID。不管是员工 ID、部门 ID后端都要校验是否存在且有权限。一个严格的系统不仅要在登录时验证身份还要在每次数据操作时验证操作者与被操作数据的可见范围。做到这一点你的系统就有真正的抗风险能力了。这个项目后续还可以这样扩展把考勤和薪资模块做成可配置化的规则引擎把审批流做成拖拽式设计器把系统管理页面做成更贴合企业实际组织架构的灵活模型。走到那一步系统的能力边界就完全不一样了。但地基要先打好而你现在看到的这套架构就是一个让人踏实的地基。