☰
SpringBoot+Vue+MyBatis+MySQL人事管理系统源码解析与二次开发指南
2026/10/10 11:10:46 网站建设 项目流程

如果你在 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 8,SpringBoot 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 里使用组合式 API(Composition 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;charset=UTF-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 id="selectEmployeePage" resultType="com.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 test="deptId != null"> AND e.dept_id = #{deptId} </if> <if test="keyword != 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); List<EmployeeVO> list = employeeMapper.selectEmployeePage(condition); PageInfo<EmployeeVO> 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的默认值和后端接口的默认分页大小要保持一致,否则会出现“前端明明设置了 pageSize=10,后端却只返回 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

字符编码和时间时区是常见坑源。serverTimezone不设置会报 “The server time zone value … is unrecognized” 错误;useSSL=false是为了避免本地开发时出现 ssl 握手的连接错误;allowPublicKeyRetrieval=true是 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 启动后端的排查顺序

后端启动报错是很多人会卡住的地方,我把常见问题按排查优先级列一下:

  1. 端口被占用:检查server.port,默认 8080 很可能被其他服务占用,改成 8081 或 8082 即可。
  2. Maven 依赖没有下载完整:第一次启动会拉取大量依赖,网络状态不好会出现丢包,仓库里可能有.lastUpdated后缀的文件,本地仓库里删除重新mvn clean install -U即可。
  3. Mapper 找不到:检查 XML 路径和 application.yml 中 mapper-locations 是否匹配。
  4. 数据库连接不上:先检查 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: true

Vite 配置里这样写,前端请求/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: 8

SpringBoot 支持${}占位符,可以从环境变量读取值,本地不设置环境变量时就使用冒号后面的默认值。这个习惯能让代码在不同环境之间切换时不用改配置文件。

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指向员工表 ID,work_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”这类问题,回头再对照我前面说的排查顺序去检查,大概率能定位到具体原因。把问题本身当成学习材料,这套人事系统才能真正变成你自己的东西。

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

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

立即咨询