SpringBoot+Vue+MyBatis+MySQL人事管理系统源码解析与二次开发指南

发布时间:2026/10/10 11:10:51
SpringBoot+Vue+MyBatis+MySQL人事管理系统源码解析与二次开发指南
如果你在 2025 年还在搜“SpringBoot 人事管理系统源码”大概率不是真的想买一套代码而是想找一个能跑起来、能看懂、能改着用的基础盘。这个基于 SpringBoot Vue MyBatis MySQL 的人事系统管理项目核心目标是覆盖中小型公司最常用的员工管理诉求系统登录与权限控制、部门维护、员工档案增删改查、以及后续能继续挂载考勤和薪资的数据库基础。我拿到这套项目源码之后第一反应不是去翻代码而是先把自己代入到“接手同事遗留项目”的场景里把整个项目的启动链路、表结构、前后端拆分方式全部过了一遍。这篇文章就把我看源码、跑源码、改源码过程中整理出来的核心思路和踩坑记录写下来尽量还原一个真实可落地的项目全貌而不是只对着标题夸它“功能齐全”。1. 先搞清楚这套人事系统到底是“管什么”的很多人在选型或者读源码之前喜欢先问“功能全不全”但产品经理和程序员的分歧点从来不在功能数量上而在于边界。人事系统听起来很宽但它和企业微信、钉钉里的审批流、招聘平台里的简历库并不是一回事。拿我看到的这套 SpringBoot Vue 人事系统源码来说它的业务定位集中在“基础人事管理”也就是行政人事部门每天打开后台之后要处理的那部分工作。1.1 最小可用闭环登录、部门、员工、字典系统里最有价值的部分不是新增了多少花哨的表而是把“人”和“组织”的关系落成了数据库结构。一个能直接复用的后勤后台至少要有用户表、部门表、员工表三张核心主表。用户表负责系统登录账号和权限角色部门表负责组织结构上的树形层级员工表负责每位员工的基本档案信息。实际在源码里我看到登录会关联用户表里的账号、密码和角色字段员工表则通过部门 ID 关联部门表。这样设计的好处是前端菜单可以根据角色去渲染后端接口也可以用拦截器校验操作权限员工列表可以按部门树做数据过滤。那套程序里的密码字段用了加密存储不是明文这点对于第一次做管理系统的人来说要注意别顺手把密码直接写进数据库不然上线之后安全隐患没法收场。1.2 业务范围收敛才是能落地的源码很多号称“XX管理系统源码”的项目把考勤、工资、绩效、招聘、培训全堆进去最后每个模块都是半成品接口反而没法用。这套人事系统的优点是范围控制得比较清楚先把登录认证、部门管理、员工管理这几个闭环做扎实考勤和薪资如果还有需要再在现有表结构上继续扩展。对于刚接触 SpringBoot 和 Vue 的学习者来说这样的范围方便你快速梳理整条链路。项目只有四条核心业务线用户登录和角色拦截、部门树维护、员工信息 CRUD、以及围绕 MyBatis 的复杂查询展示。你花一个晚上把所有 Controller、Service、Mapper XML 翻完基本就能理解这套框架的运行方式。对于需要二次开发的开发者来说这个边界也足够清晰不会因为表格过多而导致改一个需求牵连七八张表。2. 技术选型背后为什么是 SpringBoot 3 MyBatis Vue 3每次聊到技术栈总有人会问现在 MyBatis-Plus 那么流行为什么项目还在用纯 MyBatis前端为什么不用 React数据库为什么仍用 MySQL而不是 PostgreSQL这些问题背后其实都有项目背景和团队习惯上的取舍我把这套源码里技术选型的逻辑拆开说。2.1 SpringBoot 3.x 与 JDK 17 的搭配如果你是从老项目切过来的会发现 SpringBoot 3 和 SpringBoot 2 最明显的差异就是底层从 javax 迁移到了 jakarta 命名空间。这意味着以前的 javax.servlet.、javax.annotation.这些包在升级后编译都会报错。这套源码直接用 SpringBoot 3.x搭配 JDK 17我测试下来整体比较顺。需要注意JDK 版本一定要把 17 配好。如果你本机还是 JDK 8SpringBoot 3 是跑不起来的启动阶段就会出现 unsupported class file major version 之类的报错。所以拿到源码第一步检查pom.xml里 spring-boot-starter-parent 的版本号然后再看一下本机 JDK 版本。基础条件匹配了后面的问题都是细节。2.2 MyBatis 在人事系统里的合理性MyBatis 被一些人吐槽的点是复杂 SQL 都要手写但在人事系统这种以查询为主的场景里这反而是优点。部门、员工、角色之间的关联查询通常比较固定通过 Mapper XML 编写 SQL 可以明确控制 JOIN 条件和结果映射又不像 JPA 一样需要生成很多隐式 SQL 影响排查效率。源码里的实现方式也比较传统MapperScan扫描 Mapper 接口Mapper XML 放在resources/mapper/下面application.yml中配置mapper-locations指向对应路径。这里的易错点在于很多人拿到源码后启动报“Invalid bound statement (not found)”就是因为 XML 文件没有正确的 mapper-locations 配置或者 XML 的 namespace 与 Mapper 接口全限定名不匹配。检查顺序先看 namespace再看方法 id最后看 mapper-locations。mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.hr.entity configuration: map-underscore-to-camel-case: true这段配置里我把map-underscore-to-camel-case打开这样数据库里的hire_date字段可以直接映射到实体类中的hireDate属性。没有这个配置查询结果就会出现大量 null 字段你排查半天都想不到是因为驼峰映射没开。2.3 Vue 3 与前端工程化的平衡这套源码前端用了 Vue 3 Element Plus Axios Vue Router是很典型的后台管理系统组合。人事后台界面不追求炫酷动画核心是表格、表单、弹窗、树形控件、分页Element Plus 的组件覆盖度足够二次开发迭代速度也快。Vue 3 里使用组合式 APIComposition API的script setup写法逻辑复用比 Vue 2 的 Options API 更容易组织。你可以在路由守卫里做登录校验在 Axios 拦截器里统一处理 token在页面组件里按模块拆分表格和表单。这套源码在前端拆分上做得比较清楚不是把所有代码堆到一个巨型 Vue 文件里所以拿来练手或者改造结构上不用推倒重来。3. 数据库表设计部门、员工、用户三张核心表怎么联动读源码时要先看数据库脚本这是我最开始建议的方向。很多小伙伴一上来就翻 Controller那样只能看到零散的接口难以理解数据从哪里来、为什么要这样 JOIN。这套人事系统的数据库脚本里三张核心表的设计思路比较典型值得单独拆开分析。3.1 数据库脚本中的核心表结构你可以直接执行项目里附带的 SQL 脚本例如hr_system.sql。初始化完成之后表之间通常是这样一种关系表名关键字段作用sys_userid, username, password, role, status系统登录账号与角色权限deptid, parent_id, name, leader, phone部门树结构支持多级employeeid, dept_id, name, gender, phone, hire_date员工主档关联部门员工表通过dept_id关联到部门表登录账号则通过username定位用户。实际站点中的菜单显示、按钮权限大多依赖role字段判断简单系统用字符串角色就够了更复杂的 RBAC 权限模型需要引入角色表和菜单权限表但那是系统用户可以按需扩展的部分。员工表里别忘记加上id_card、address、email、education这类字段。人事管理系统最大的“隐性需求”是企业后续要做报表导出如果员工表里没有身份证号、学历这些基础字段后面统计就会因为没有原始数据而卡住。源码里有没有每个字段并不重要关键是表设计的扩展思路要学走。3.2 部门表为什么用 parent_id 而不是 varchar 路径部门表里最值得注意的设计是parent_id字段。有些初级系统会把部门层级存成一个字符串路径比如“总公司/技术部/后端组”查询时用 LIKE 去匹配这种做法临时能用但调整部门层级时需要同步改字符串容易产生脏数据。正确做法是用一张自关联表部门表的parent_id指向本表id。前端遍历时先把所有部门查出来再在内存里构造成树形结构传给 Tree 组件。后端返回给前端的结构可以把parent_id一起带出来由前端自行组装 children 数组这样的前后端职责权限分工更清晰。{ id: 2, parentId: 1, name: 技术部, leader: 张三 }如果要做更严格的数据展示也可以让后端在返回层级数据时直接用递归组装树。但考虑到部门数量一般不会特别大更省事的方案是前端拿到扁平数组后自己 for 循环找 children接口简单、逻辑也好调试。3.3 外键到底建不建我对这套源码的印象是它没有在数据库层面大量使用物理外键而是用逻辑外键来维持关联。employee.dept_id并不会强制FOREIGN KEY但删除部门时需要在 Service 层先判断该部门下有没有员工如果没有再允许删除否则需要级联处理或直接拒绝删除。这样的取舍是有道理的。物理外键在数据库层面更安全但后续做数据导入、批量调整、分表分库时会给自己增加阻力也容易因为外键约束产生索引上的额外开销。用逻辑外键加应用层校验是互联网后台和人事实务系统的常见思路。删除部门的校验逻辑大致是这样public boolean deleteDept(Integer deptId) { Long employeeCount employeeMapper.countByDeptId(deptId); if (employeeCount 0) { throw new BusinessException(该部门下存在员工不能删除); } return deptMapper.deleteById(deptId) 0; }不要忽略这个判断。很多人做部门管理接口时只做了简单的 deleteById结果出现前端部门树瞬间消失但员工档案里还残留一个已删除部门ID的尴尬局面。4. 后端实现中最容易卡住的三个环节读完表结构之后接下来要看后端代码。我特意把源码里的后端实现分成三层来看认证链路、多表查询链路、事务和批量操作。这三个地方几乎覆盖了你在运行和调试人事系统时会遇到的主要报错来源。4.1 登录认证的典型写法拦截器 JWT ThreadLocal人事系统的接口不应该裸奔也就是说没有登录的请求不能访问员工列表、部门维护这些数据接口。源码里的认证方式比较常规登录成功后返回 token后续请求在 Header 里带上Authorization: Bearer token后端写一个拦截器统一校验。拦截器在处理请求时需要先放行登录接口、首页等白名单再获取请求头校验 token。校验通过后把用户信息放到ThreadLocal中方便后续 Service 层获取当前登录人。这里有几个顺序问题需要特别注意拦截器里校验 token 失败后必须抛出异常或直接输出 JSON 响应不能注解返回 false 就完事否则前端收到的可能是一段空白页面。ThreadLocal用完之后要在拦截器的afterCompletion中移除不然线程池复用线程时可能导致用户信息串号。token 过期时间和刷新策略需要提前想清楚单纯把过期时间设成一天业务上往往会面临“下午还在登录状态晚上就掉线”的体验问题。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); LoginUser user jwtUtil.parseToken(token); if (user ! null) { UserContext.set(user); return true; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; }顺带一提源码里如果用了 Spring Security要注意过滤器链和这个拦截器的执行顺序否则会出现“明明已经登录但 Security 上下文拿不到用户”的情况。小型项目直接用拦截器其实更简单直观。4.2 多表查询resultMap 和 VO 怎么配合员工列表页通常不会只查员工表本身还需要显示部门名称、岗位、状态等关联信息。如果数据库里员工表的dept_id只存了 ID前端拿不到部门名字就必须 JOIN 部门表。在 MyBatis 里有两种做法一种是在 XML 中定义resultMap配置 association 或者 collection另一种是直接用一个 VO 类去承接多表查询结果避免直接用 Map 接收导致字段含义不清。我推荐大多数场景都要定义清晰的 VO。人事系统里面员工档案展示、导出、详情查看会用到的字段基本是固定的定义一个EmployeeVO里面包含员工基础字段再加一个deptName字段在 XML 里写一条LEFT JOIN dept d ON e.dept_id d.id取数非常直接。select idselectEmployeePage resultTypecom.example.hr.vo.EmployeeVO SELECT e.id, e.name, e.gender, e.hire_date, e.phone, d.name AS deptName FROM employee e LEFT JOIN dept d ON e.dept_id d.id where if testdeptId ! null AND e.dept_id #{deptId} /if if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY e.id DESC /select动态 SQL 是 MyBatis 的核心价值。where配合if可以很方便地拼接查询条件避免写出多条功能重复的 SQL。如果你在源码里看到类似这样的 XML 写法说明作者对 MyBatis 的动态 SQL 使用是比较熟练的。4.3 事务边界批量导入和关联操作不能省 Transactional员工管理很少只是单表 insert。新增员工时可能还要同步创建一个系统登录账号编辑员工信息时需要更新员工表和用户表离职功能则可能需要把用户状态置成禁用。这种跨表操作如果不加事务很容易出现“员工表删了但用户表还在”的半成品状态。源码里正确的做法是将整个操作放到一个 Service 方法中并标注Transactional确保后续出现问题可以回滚。Transactional(rollbackFor Exception.class) public void addEmployee(Employee employee, String username, String password) { employeeMapper.insert(employee); userMapper.insert(new SysUser(username, passwordEncoder.encode(password), employee.getId())); }需要注意的是rollbackFor Exception.class这个参数很多人会漏掉。默认情况下 Spring 事务只会在 RuntimeException 上回滚如果 Service 里抛的是 checked exception不设置 rollbackFor 会导致数据已经插入但异常被吞掉页面报错和实际落库结果不一致排查起来非常浪费精力。分页查询也是人事系统刚需。很多源码直接使用了 PageHelper使用方式是在 Mapper 查询前启动分页PageHelper.startPage(pageNum, pageSize); ListEmployeeVO list employeeMapper.selectEmployeePage(condition); PageInfoEmployeeVO pageInfo new PageInfo(list);分页的隐患主要出现在多表查询和排序字段上。PageHelper 会在执行的 SQL 后面自动拼接 limit如果 SQL 里已经写了 limit分页就会叠加或出错排序字段如果是动态拼接的字符串也容易造成 SQL 注入风险这点在二次开发时不要随意把它拉长到一个前端传参里面。5. Vue 前端部分是如何把后端接口串起来的人事系统的后端接口再完整如果前端组织得像一堆散装页面维护成本依然会很高。这套源码的前端部分使用 Vue 3 和 Element Plus整体结构比较清楚路由负责页面跳转和权限控制Axios 负责接口请求和错误拦截页面组件专注于表格和表单展示。我不打算把每个页面都贴一遍但会把几个关键设计挑出来说因为这些都是实际开发时最容易踩坑的位置。5.1 登录状态管理路由守卫和 localStorage 之间的配合Vue 前端和后端通过 token 维持登录状态。登录成功后前端要把 token 和用户信息存起来通常放在localStorage或sessionStorage。当用户刷新页面时内存中的store会丢失但localStorage里还能读到 token所以刷新后不会被打回登录页。路由守卫中要注意判断逻辑的先后顺序。比较规范的写法是router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } // 可选的进一步校验路由角色 next(); });如果你把 token 校验和角色校验放到同一步且用户角色是一个非空数组那就要小心空数组的情况。很多人登录后跳转首页发现进不去就是因为角色判断里用了role.includes(...)但角色数组为空导致恒为 false。源码里的菜单权限往往是根据后端返回的角色字段动态生成路由的。页面加载完成后通过router.addRoute将符合权限的页面路由加入映射。这里最容易出现的问题是动态加路由之后刷新页面会报“No match for route”的警告解决办法是在路由守卫里重新加一次路由并且把首次动态路由的hasDynamicRoutes标记在 store 或全局变量中记住。5.2 Axios 封装统一 token 和错误提示Axios 在管理后台中最大的作用是避免每个页面重复写请求头和处理错误。比较常见的封装逻辑是request.js文件创建 Axios 实例请求拦截器里添加 header响应拦截器里统一处理 HTTP 状态码和业务状态码。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 code response.data.code if (code 200) { return response.data } if (code 401) { router.push(/login) return Promise.reject(未登录) } ElMessage.error(response.data.msg || 请求失败) return Promise.reject(new Error(response.data.msg)) }, error { ElMessage.error(error.response?.data?.msg || 网络异常) return Promise.reject(error) } )统一封装完成之后页面组件里只需要listApi()...then()就能拿到数据。很多人后端接口明明写的没有任何问题前端页面却反复弹 401 或 500多数就是因为 Axios 拦截器里没有处理 401 跳转或者没有把response.data干净地返回出去。另外要注意上传文件、下载文件时不要走统一的 JSON 响应拦截逻辑。批量导入员工 Excel 时如果后端返回的是文件流而拦截器把它当成 JSON 去解析文件就会损坏。通常做法是在下载接口单独指定responseType: blob拦截器里根据responseType做分支处理。5.3 表格页面的套路搜索表单 分页 删除确认人事系统的前端页面做得再简单最终也会落到一个固定套路搜索表单、数据表格、分页器、新增/编辑弹窗、删除确认。这个设计模式没有什么新意但它很稳定。以员工列表页为例EmployeeList.vue的组件内部一般会维护三个核心数据searchForm、tableData、pagination。搜索按钮会重置页数为 1 再拉取列表分页变化时重新请求后端接口删除操作会先ElMessageBox.confirm确认再调用 delete 接口成功后刷新列表并提示。我在这个页面上提醒大家一个容易忽略的参数pageSize的默认值和后端接口的默认分页大小要保持一致否则会出现“前端明明设置了 pageSize10后端却只返回 5 条”的认知偏差。最佳实践是前端分页组件的 page-size 在初始化时就从后端配置里读取或者至少保证和后端常量一致。6. 从源码到本地可运行环境版本和启动细节写了很多结构分析最终还是要回到“怎么把项目跑起来”这个现实问题上。我拿这套源码跑了不止一次期间遇到的绝大多数启动失败都和版本环境、数据库初始化、配置文件没有对齐有关。这里把一套我自己验证可行的步骤和排查思路整理出来。6.1 环境版本清单与注意事项2025 年的话我建议用这样一套组合来跑 SpringBoot Vue 的人事系统源码组件版本建议说明JDK17SpringBoot 3 必须Maven3.8使用 Maven 管理后端依赖MySQL8.0注意字符集和时区配置Node.js18可以稳定运行 Vue 3 构建npm/yarn8/9安装前端依赖IDEIntelliJ IDEA后端调试比较方便MySQL 安装时可以注意一点如果本机已经存在 MySQL 5.7再装 MySQL 8.0 会有端口或服务冲突。最简单的方式是安装两台 MySQL 用不同端口或者直接把现有的升级。如果你只是为了跑这套源码用 8.0 比较稳因为驱动类名和时区处理都要简单得多。后端application.yml中数据库连接串是整个启动的关键。推荐使用下面的配置spring: datasource: url: jdbc:mysql://localhost:3306/hr_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver字符编码和时间时区是常见坑源。serverTimezone不设置会报 “The server time zone value … is unrecognized” 错误useSSLfalse是为了避免本地开发时出现 ssl 握手的连接错误allowPublicKeyRetrievaltrue是 MySQL 8 使用 caching_sha2_password 认证方式后经常需要的参数不配置时可能出现 Public Key Retrieval is not allowed 的报错。6.2 初始化数据库SQL 脚本执行顺序源码包中一般会有一个 SQL 文件名字可能是db/hr_system.sql或者sql/hr_system.sql。在数据库客户端里新建数据库后直接执行整个 SQL 文件即可。执行之前尽量确认字符集设置正确可以通过下面这条语句查看SHOW CREATE DATABASE hr_system;如果发现数据库不是 utf8mb4可以执行ALTER DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;当表中已经有数据时改字符集可能导致中文乱码所以最稳妥的操作是在执行初始 SQL 前就设置好数据库字符集。初始化脚本一般还会自带一个管理员账号比如admin / admin123。密码字段在数据库里不会以明文保存使用了 BCrypt 加密字符串。如果你登录时发现密码不对第一件事是检查数据库里sys_user表的 password 字段是否被改过或者 SQL 脚本是不是被某次重置覆盖了。6.3 启动后端的排查顺序后端启动报错是很多人会卡住的地方我把常见问题按排查优先级列一下端口被占用检查server.port默认 8080 很可能被其他服务占用改成 8081 或 8082 即可。Maven 依赖没有下载完整第一次启动会拉取大量依赖网络状态不好会出现丢包仓库里可能有.lastUpdated后缀的文件本地仓库里删除重新mvn clean install -U即可。Mapper 找不到检查 XML 路径和 application.yml 中 mapper-locations 是否匹配。数据库连接不上先检查 MySQL 服务是否启动、用户名密码是否与配置一致、数据库名是否写对。mvn spring-boot:run后端看到如下日志代表启动成功Tomcat started on port 8080 (http) with context path / Started HrSystemApplication in 8.321 seconds启动成功后可以先访问http://localhost:8080/api/...测试接口连通性再启动前端项目。前端启动顺序是先安装依赖再启动开发服务器npm install npm run dev如果npm install速度太慢可以切换国内镜像但切换镜像后最好不要和默认源混用否则可能出现 lock 文件不一致的问题。启动成功后访问http://localhost:5173/不同版本端口不一致以 Vite 输出为准用管理员账号登录。如果前端页面白屏打开浏览器开发者工具看 Console。最常见的报错是跨域CORS。解决方法是在后端 Controller 上加CrossOrigin或者在后端配置一个跨域过滤器也可以让前端 dev server 通过 proxy 把请求转发到后端端口这样浏览器看到的请求是同源的。server: proxy: /api: target: http://localhost:8080 changeOrigin: trueVite 配置里这样写前端请求/api/user/list会自动转发到后端的8080端口。后端层面就不需要单独开启 CORS前后端联调时更省心。7. 正式部署前建议你改掉的默认配置本地跑通只是第一步把源码放到服务器上或者交接给别的团队时如果还使用本地开发环境里的默认配置后果一般都比较难看。这里列几个我认为必须在部署阶段处理掉的问题。7.1 默认账号、密码和密钥初始化 SQL 里如果创建了admin / admin123这种账号上线之后第一件事就是把默认密码改掉。同时要检查 JWT 签名密钥很多源码里直接把密钥写成一个常量比如secret: abc123在开发时没问题但生产环境一旦被人逆向拿到密钥就能伪造管理员 token。JWT 配置应该提取到环境变量或者配置文件外置并且用一个足够长的随机字符串。jwt: secret: ${JWT_SECRET:change-me-please-0123456789-abcdef} expire-hours: 8SpringBoot 支持${}占位符可以从环境变量读取值本地不设置环境变量时就使用冒号后面的默认值。这个习惯能让代码在不同环境之间切换时不用改配置文件。7.2 前端打包与后端静态资源匹配Vue 项目开发时用的是 dev server但部署时通常需要把前端构建出来的dist目录放到后端服务的静态资源目录下或者单独部署到 Nginx。源码里如果是把 Vue 打包进 SpringBoot 的方式那么前端构建配置里需要注意assetsDir和publicPath。先执行npm run build然后将生成的dist目录里的文件拷贝到 SpringBoot 的src/main/resources/static/目录下重新打包后端。如果前端用了 Vue Router 的 history 模式后端还要配置对非 API 路径的转发否则刷新/employee页面时会出现 404。如果有单独 Nginx则把dist目录放到 Nginx 的 html 目录并配置反向代理到后端接口同时把try_files $uri $uri/ /index.html;加上解决 history 模式刷新 404 的问题。location /api/ { proxy_pass http://127.0.0.1:8080; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这样只会在 Nginx 层开一个口子把/api转发给 SpringBoot其他前端路由交给 Vue Router 处理。这种部署方式比打进 SpringBoot 静态资源更干净也方便后续独立扩前端资源。7.3 索引、慢查询和缓存人事系统的数据量在中小型企业里通常不会很大但查询条件一多索引设计不当还是会出现慢查询。员工表的dept_id、hire_date、用户表的username这几个字段建议加索引。尤其是登录接口里的username查询几乎是每次登录取主键的前置条件没有索引会导致整个数据库把用户表全表扫描一遍。CREATE INDEX idx_dept_id ON employee(dept_id); CREATE INDEX idx_username ON sys_user(username);MyBatis 自带的二级缓存在这类项目中并不是默认开启的。我一般不建议在人事系统里把二级缓存调得太激进因为员工数据更新频率不高但查询权限相关性强一旦缓存了错误数据用户登录之后看到别人信息的风险比性能收益更严重。如果确实要优化列表查询速度可以从 SQL 层面做例如只查单页数据、减少SELECT *、避免在 WHERE 中对索引列使用函数。比如用户名模糊查询LIKE %xxx%无法命中索引这是业务选择但务必保证精确等值查询能够走索引。8. 拿这套源码接着扩展考勤、薪资和导出报告愿意把源码跑通的人多数不会只满足于员工档案增删改查。现实中人事模块的下一步需求往往围绕考勤、薪资和报表导出展开。这三种功能的实现路径不太一样但都建立在现有表结构可以演进的基础上。8.1 考勤表设计以员工表为主档按天记录状态考勤功能一般需要新增加一张考勤表例如CREATE TABLE attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id BIGINT NOT NULL, work_date DATE NOT NULL, check_in_time DATETIME NULL, check_out_time DATETIME NULL, status TINYINT NOT NULL COMMENT 0-正常 1-迟到 2-早退 3-缺卡, remark VARCHAR(255), UNIQUE KEY uk_emp_workday (emp_id, work_date) )这张表的关联逻辑很简单emp_id指向员工表 IDwork_date记录某一天UNIQUE索引保证一个员工一天最多一条记录。考勤数据如果在每天的上下班打卡后写入前端页面就可以按员工列表加日期维度查询。开发时要考虑的是从打卡设备或第三方系统导入考勤比手工录入更常见。因此扩展方向建议是做一个 Excel 批量导入接口在后端用 EasyExcel 或者 POI 读文件校验员工工号按emp_id work_date作为唯一键做 upsert。这个接口同样需要事务控制最好还要记录导入人和导入时间方便追溯数据问题。8.2 薪资模块不要设计成只有最终工资薪资模块比考勤更敏感。很多初级系统给员工表加一个月薪字段就结束需求这种做法只能满足一个“查询月薪”的小场景真正的薪资管理需要核算基本工资、加班费、绩效、社保扣款、个税等多维数据。合理的设计是新增薪资表CREATE TABLE salary ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id BIGINT NOT NULL, month VARCHAR(7) NOT NULL COMMENT 2025-03, base_salary DECIMAL(10,2) DEFAULT 0.00, performance DECIMAL(10,2) DEFAULT 0.00, allowance DECIMAL(10,2) DEFAULT 0.00, social_security DECIMAL(10,2) DEFAULT 0.00, tax DECIMAL(10,2) DEFAULT 0.00, net_salary DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_month (emp_id, month) )每次核算工资只往这张表里写一行多次生成或者调整会覆盖或标记为历史记录。生产环境上常见的坑是财务要求月底数据不可修改只允许做更正记录。那么还要加一张salary_history表记录每次变更这对权限设计和操作日志要求会变高。薪资导出也是一定会出现的需求。建议在后端使用流式输出 CSV 或 Excel不要把所有数据一次性加载进内存再循环拼接。数据量大时边查边返回到前端下载体验更好。导出的核心字段要和前端表格展示保持一致临时加字段而不改导出配置用户会频繁提“导出和页面不一致”的问题。8.3 操作日志永远不要说“上线再补”无论做考勤还是薪资审计和操作日志的重要性都会被后置。不少人事系统上线三个月后才想起来每个人都想知道“谁的薪资被谁改过改成什么样了”。到那个时候再从头补日志基本意味着要把 Controller 层重写一遍。比较轻量的做法是加一张operation_log表记录操作人、操作方法、请求参数、IP、时间和结果。后端可以写一个 AOP 注解来切面记录控制层接口调用。即便前期只记录员工和部门的增删改也会在后续排查数据异常时节省大量时间。CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64), method VARCHAR(128), params TEXT, ip VARCHAR(64), result TINYINT, error_msg TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP )这个表和业务表同样只通过用户名关联不强制外键因为日志即使放到员工被删除后依然需要保留。9. 最后分享几点我在实际跑这套源码时的心得项目的核心骨架看下来我非常认同“用源码学项目”这件事。它不像只学一个点或只背面试题而是让你看到一个真实系统如何在登录、权限、表格、数据库之间相互配合。但我还是想强调源码只是参考真正值钱的是你从里面提炼出的业务抽象能力和边界意识。跑这套项目时环境的坑其实只占一小部分更多的精力应该花在理解“为什么这样设计”上。比如为什么要在删除部门前检查员工数量为什么员工表要和系统用户表拆开为什么查询列表要用 VO 而不是直接返回 Entity。把这些问题的答案整理成自己的知识结构下次你写任何管理系统都能更快。如果你拿到的源码里没有完整的考勤和薪资模块也不要觉得亏恰恰相反。代码结构清楚的员工管理系统才是你扩展这些功能的好起点。先跑通再修改最后把业务闭环完整落地你会发现整个项目的成就感远大于搜索到一份成品源码本身。如果你在看代码的过程中遇到“Invalid bound statement”“前端白屏”“登录接口 401”这类问题回头再对照我前面说的排查顺序去检查大概率能定位到具体原因。把问题本身当成学习材料这套人事系统才能真正变成你自己的东西。