一个内部管理系统的选型从来不是越高级越好,而是在团队接受度、维护成本和业务复杂度之间找到一个平衡点。之前有位做生产的客户,内部人事数据一直用几份Excel来回传,考勤统计靠手工加总,每个月末就是人事部门的“灾难日”。后来我帮他们整理了一套基于Spring Boot + Vue的人事管理系统,前后端分离,带完整的MySQL数据库脚本和部署文档。下面把整个项目的选型思路、表结构设计、后端接口、前端页面以及联调部署的坑都摊开聊一聊,希望对你搭建类似系统有实际参考价值。
1. 这套系统的定位:一个能直接跑起来的人事管理原型
做管理系统最怕的就是“需求无限蔓延”。人事系统的核心价值归根结底是三类:员工信息能存能查、考勤薪资能算能导出、权限角色能分能管。围绕这三个核心诉求,我把项目收敛成六大模块,不做大而全,但每个模块都能独立使用。
1.1 六大功能模块与实际场景
模块划分直接影响数据库设计和接口数量,这里列出我最终确定的清单:
- 员工档案管理:员工基本信息的新增、编辑、查询、离职操作,支持照片上传和附件归档。
- 部门与岗位管理:部门以树形结构维护,岗位与部门关联,员工挂在具体部门下。
- 考勤管理:记录每日打卡或者补卡数据,支持按月汇总出勤天数。
- 薪资管理:根据基础工资、岗位工资、考勤扣款和补贴生成月度薪资单。
- 系统用户与权限:登录用户、角色、菜单权限三层模型,不同角色看到不同菜单和操作按钮。
- 数据统计与导出:部门人数分布、员工状态统计,以及员工花名册Excel导出。
上面这套功能覆盖了绝大多数中小型团队人事管理的日常场景。而对于一些诸如招聘流程、培训记录、绩效考核之类的延伸功能,我建议在原型跑通之后再迭代。如果你第一次就尝试把所有模块塞进去,大概率会在数据库设计阶段就陷入僵局。
1.2 这套源码适合谁参考
从实际受众来看,这套系统有三类人会比较适用。第一类是刚接触前后端分离项目的学生朋友,用它当课程设计或毕业设计的底子,结构清晰,文档齐全。第二类是需要快速搭建内部管理工具的小微企业技术人员,给非技术团队做试用版选型参考。第三类是自己接外包的小团队,拿这套系统改改就能交付给客户,省去从零建表的成本。
总的来说,这套系统的定位是一个“高完成度的半成品”:数据库脚本给你列好了,接口文档给你写清楚了,前端页面能直接登录操作,剩下的业务细节完全可以按自己的需求再加。
2. 技术栈看着很常见,为什么最终选了Spring Boot + Vue
关于技术选型,很多文章喜欢罗列一堆理由,但落到实际项目里,核心其实就几个问题:这个技术大家熟不熟、部署维护难不难、出了问题有没有人能接盘。
2.1 后端选型对比
我在做这个项目时,后端候选方案主要有三个:Spring Boot、Node.js Express、还有传统PHP。三者各有优势,但结合人事管理系统的特点,我倾向Spring Boot。
Spring Boot的生态非常成熟,尤其是Spring Security、MyBatis Plus这些组件,用起来省掉大量重复代码。而且Java后端在中小型系统里最不缺的就是维护者,哪怕你交接给另一个团队,也能很快接手。相比之下,Node.js在开发速度上有优势,但工程化管理和权限体系要自己拼装;PHP上手门槛低,但前后端分离模式下的接口开发和鉴权实现反而更折腾。
在Spring Boot版本选择上,我建议用Spring Boot 2.7.x配合JDK 1.8或11。不是3.x不好,而是2.x在兼容性上更稳,网上资料也多,尤其是对接各种中间件时踩坑成本低。如果你的环境允许使用较新版本,那Spring Boot 3.x配JDK 17是趋势,但一定要提前确认所有依赖都适配了,否则很容易卡在版本陷阱里。
| 对比维度 | Spring Boot | Node.js | PHP |
|---|---|---|---|
| 团队上手难度 | 中等 | 中等 | 低 |
| 生态成熟度 | 高 | 中高 | 高 |
| 前后端分离开发体验 | 好 | 好 | 一般 |
| 长期维护成本 | 低 | 中 | 中 |
| 毕业设计/面试加分 | 高 | 中 | 低 |
2.2 前端技术栈的取舍
前端的选型相对明确,Vue加Element UI组件库是当前国内做后台管理系统的经典组合。我这个项目里用的Vue版本是2.6,配Element UI 2.15。有人会问都202X年了怎么不用Vue3加Element Plus,我的理由很简单:开源生态里Vue2的存量项目和使用人群依旧庞大,各种踩坑方案都能搜到。对于毕业生或者外包项目,技术新颖不一定等于好交付,稳定可查才是第一位的。
当然,如果你完全从零开始,没有历史包袱,Vue3加Element Plus也完全可行。这套系统的接口设计和页面逻辑是前后端分离通用的,换成Vue3后只需要重写组件引入方式,接口层不用动。
2.3 中间件与构建工具的搭配
数据库我用的是MySQL 5.7,主要考虑到客户服务器上已经装了这个版本,迁移成本为零。如果你手上是MySQL 8.0,脚本一样能跑,只需要留意驱动版本和时区配置差异。ORM框架选了MyBatis Plus,它的分页插件和代码生成器能让开发效率提升一半,尤其在后端接口多、表单多的管理系统里,这种半自动ORM比原生MyBatis舒服很多。
构建工具方面,后端用Maven,前端用npm加vue-cli,都是最主流的方案,没有任何学习门槛。整套技术栈总结成一句话:不是最新最炫的,但一定是最容易找到资料、最容易让人接手继续改的。
3. 数据库设计的关键细节:部门树、员工状态、逻辑删除
说实话,人事系统真正的核心不在前端页面长什么样,而是在数据库设计。后面接口写得顺不顺畅,报表统计能不能做出来,全看这一层。
3.1 核心表结构一览
整个数据库我拆成近二十张表,但核心的也就下面几张,先看表名和作用。
- sys_user:系统登录账号,字段包括id、username、password、nickname、avatar、status、dept_id。
- sys_role:角色表,定义管理员、人事专员、普通员工等角色。
- sys_user_role:用户和角色的关联表,用户与角色是多对多关系。
- sys_menu:菜单权限表,前端动态菜单和后端接口权限都依赖这张表。
- dept:部门表,用parent_id实现层级关系,比如公司>生产部>车间一组。
- position:岗位表,和部门是弱关联,一个部门可以有多个岗位。
- employee:员工档案主表,字段最多的一张表,下面会重点展开。
- attendance:考勤表,记录员工每天的上、下班时间。
- salary:薪资表,按月存储每个员工的薪资明细。
sys_user和employee是分开的。很多初学者容易混,其实逻辑很简单:admin账号对应系统登录身份,employee记录的是员工真实信息,两者通过user_id字段关联。一个员工未必有登录账号,一个登录账号也不一定对应在职员工。
3.2 员工表字段设计的讲究
employee表是整个系统的数据枢纽,字段看似多,但每一个都有明确用途。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| emp_no | varchar | 员工编号,唯一索引 |
| name | varchar | 姓名 |
| gender | tinyint | 性别 |
| id_card | varchar | 身份证号 |
| phone | varchar | 手机号 |
| dept_id | bigint | 所属部门 |
| position_id | bigint | 岗位 |
| hire_date | date | 入职日期 |
| status | tinyint | 员工状态:1在职 2试用 3离职 |
| avatar | varchar | 照片地址 |
| deleted | tinyint | 逻辑删除标记 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
这里有几个设计细节特别注意。
第一,emp_no必须加唯一索引。员工编号是业务上识别员工的关键字段,一旦重复,后面考勤、薪资关联数据全乱。并且这个编号最好在新增员工时由系统自动生成,比如“EMP”加年月日加三位流水号,而不是让用户手输,这样能极大减少脏数据。
第二,status只做单字段状态管理。有些系统喜欢用“在职/离职”两个表来区分,查询时就必须JOIN两张表,非常别扭。用一个status字段加一个离职时间remark,既能满足列表筛选,也能在后续做统计分析时直接分组。
第三,deleted逻辑删除是必选项。人事系统里,员工档案是非常敏感的数据,如果误删再也找不回来就麻烦了。所以表里统一加deleted字段,删除操作只是把deleted改成1,查询时自动过滤。这样既保证数据不丢,又不影响列表查询的性能。
3.3 部门树的设计与查询思路
部门表用自关联的方式维护层级结构,核心字段就是parent_id。
建树的方式有两种。一种是在SQL里递归查询,MySQL 8.0以后支持递归公共表表达式,写起来很简洁;MySQL 5.7则要写自定义函数或者循环查。另一种是把所有部门一次性查出来,在Java代码里根据parent_id构建成嵌套对象,前端直接渲染树组件。我这里用的是第二种,因为部门数量一般不会超过几百个,一次查完内存构建更简单,数据库压力也小。
构建树的核心逻辑可以这样理解:先查全部部门列表,再遍历一遍,把每个节点的子节点列表填充进去。这就是最常见的“遍历建树”操作,代码量不大,但能让你避开反复查库的性能陷阱。
4. 后端实现:认证、统一响应、员工档案和Excel导入导出
后端模块我按功能切分成三块来写:基础架构、权限认证、业务接口。基础架构决定了你后续写接口的舒服程度,权限认证决定了系统的安全性,而业务接口则是用户每天都要使用的核心功能。
4.1 统一响应与全局异常:后端的地基
好的后端项目首先需要一个统一的JSON响应结构。我内部定义的格式是:
{ "code": 200, "message": "操作成功", "data": {} }所有接口都返回这个格式,前端通过code来判断业务是否成功,通过message来提示用户。这个统一结构的好处越到后期越明显。比如登录失败返回401、权限不足返回403、业务校验不通过返回500,前端只需要写一个拦截器就能统一处理所有异常情况,不需要每个接口单独判断。
全局异常处理同样重要。使用Spring的@RestControllerAdvice注解统一兜底,可以避免出现异常时返回Spring默认的错误页面。我这里定义了针对参数校验异常、业务异常、未知异常的三层处理逻辑:参数校验异常提示用户具体字段错了请修改,业务异常提示操作请重试或检查状态,未知异常才记录完整错误日志。
4.2 JWT认证与权限控制的实现思路
登录鉴权用的是JWT加拦截器方案,整体流程是这样的。用户首次登录时,后端校验用户名密码,通过后生成一个token返回给前端,前端存到localStorage里。之后每次请求都在Header里带上Authorization字段,后端拦截器解析token,校验通过则放行。
具体落地我分了两个层次。第一层是登录拦截器,校验token是否存在且有效;第二层是基于菜单权限的接口权限控制,当前用户拥有的菜单编号如果不在允许列表内,接口直接返回403。
JWT实现本身并不复杂,关键是要注意token过期时间和用户状态变更的问题。我设置的token有效期是24小时,前端在收到401响应时自动跳转登录页。另外一个容易被忽略的问题是,当用户被管理员禁用后,如果token还没过期,他依然能访问系统。所以我在拦截器里除了验证token,还会每次查一下用户状态,发现被禁用就立即失效。
4.3 员工档案接口的具体设计
员工管理接口是整个系统最关键的CRUD模块,我把它分成几个接口来设计。
- GET /api/employee/page:分页查询员工列表,支持姓名、部门、状态、入职日期区间筛选。
- GET /api/employee/{id}:查询员工详情。
- POST /api/employee:新增员工。
- PUT /api/employee:修改员工信息。
- DELETE /api/employee/{id}:逻辑删除员工。
- GET /api/employee/export:导出员工花名册Excel。
分页查询用的是MyBatis Plus的分页插件。前端传入current和size两个参数,后端返回total和records两个字段。前端表格组件只需要把这两个字段对应上即可实现分页。查询条件的处理上,我建议用条件构造器而不是自己拼SQL,这样代码整洁,也避免SQL注入风险。
新增和修改员工时,后端必须做字段校验。比如手机号是否符合格式、入职日期不能早于出生日期、员工编号是否重复,这些校验规则写在服务层,而不是前端。后端校验的意义在于,哪怕有人绕过前端直接调接口,数据也一样是干净的。
4.4 使用POI实现员工花名册导出
导出Excel在做管理系统的项目里是非常加分的功能。员工花名册导出用的是Apache POI库,流程大概是:创建Workbook,创建Sheet,设置表头,逐行填充员工数据,最后通过HttpServletResponse把文件流写回前端。
导出的细节处理也很有讲究。表头建议先加粗并设置背景色,列宽按内容长度自适应。身份证号和手机号必须设置成文本格式,否则在Excel里会因为超过一定长度被科学计数法显示。一个很典型的坑就是,明明是15位身份证号,导出后显示为1.1101E+14,所以要用CellStyle设置数据格式为文本。
注意:Excel导出接口一旦数据量大,就不能同步导出,否则接口超时是必然的。超过两万条数据建议加一层异步导出机制:前端发起导出请求,后端生成文件后存入临时目录,然后返回文件下载链接。这套系统目前的数据量还是同步导出,但留了异步接口的扩展位。
4.5 部门树与统计接口的实现
部门树接口返回的是嵌套结构,Java里用List类型的DeptVO,DeptVO再包含一个List类型的下children。这样前端拿到后直接绑定Element UI的树组件就能完美展示。统计接口则用SQL的GROUP BY加COUNT查出每个部门的人数,前端用饼图展示。
这两个接口写的顺不顺全看数据库设计。部门表有parent_id,员工表有dept_id,统计查询只需要一条SQL就能完成,不需要在业务代码里做二次聚合。
5. Vue前端搭建:动态路由、权限按钮和表单校验
前端部分我也可以唠唠实际经验。很多人觉得后台管理页面就是表格加表单,没什么技术含量,但真做起来,路由守卫、状态管理、组件封装这些都直接影响使用体验。
5.1 前端工程结构与依赖安装
前端项目基于vue-cli创建,目录结构大概如下。
hrms-web/ ├── src/ │ ├── api/ # 接口请求模块 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件 │ ├── layout/ # 整体布局:侧边栏、顶栏、主内容区 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── utils/ # 工具方法,如request.js │ ├── views/ # 页面视图 │ ├── App.vue │ └── main.js依赖方面重点装了四个库:axios、vue-router、vuex、element-ui。element-ui按需引入可以缩小打包体积,但为了方便后期维护,我直接全量引入,系统部署在内网,体积对访问速度影响不大。
5.2 路由守卫:没有token就跳登录
路由守卫是前端安全的第一道门。我在全局前置守卫里做了三件事:检查本地有没有token,没有token的强制跳转登录页;有token但访问的是登录页,则跳转首页;进入每个页面时再根据菜单权限判断用户是否被允许访问,不允许就跳转403页面。
这里有一个容易踩的坑:动态路由和刷新页面时的状态丢失。如果你的菜单权限是根据用户角色动态生成的,那么刷新页面时Vuex里的菜单状态会清空,路由守卫会认为用户没有权限而跳转到403。我当时的解决方法是,路由守卫里先异步请求后端获取当前用户的权限菜单,再动态添加路由。也就是说刷新页面后,要等菜单权限加载完成再放行。
5.3 基于角色的按钮权限控制
后端接口肯定要做权限控制,但前端也要配合控制用户能看到哪些操作按钮。我的方案是基于Vue自定义指令实现一个v-permission,指令的值是一个权限标识,如果当前用户没有这个标识,对应按钮直接移除。
举个例子。
<el-button v-permission="'employee:add'">新增员工</el-button>这个指令的实现核心是查询当前用户权限列表是否包含employee:add,不包含就把DOM删掉。这样普通员工登录后根本看不到新增、删除按钮,从交互层面减少误操作。
菜单权限的数据源是sys_menu表。后端提供一个GET /api/auth/menus接口,根据当前用户角色返回菜单列表,前端渲染成侧边栏。这样不同的角色登录系统,看到的菜单本身就不同。
5.4 表格、搜索和表单弹窗的通用模式
员工管理页面是标准的“搜索区加表格区加弹窗表单”结构。我在组件设计上做了个封装,把所有弹窗表单统一成一个通用的DialogForm组件,内部处理打开关闭、表单校验、提交逻辑。每个业务页面只需要传一个表单配置文件,比如有哪些字段、字段类型是输入框还是下拉框,组件就自动渲染出来表单。
这种做法一开始有点费工夫,但后面新增业务页面时几乎就是复制粘贴改配置,效率翻倍。不过要提醒一下,过度封装也可能导致代码难维护,特别是项目只有两三个人开发时,简单直接的页面反而更舒服。所以封装程度要视团队规模而定,技术不是目的,好用才是。
5.5 Axios请求封装与常见错误处理
request.js是整个前端和后台交互的枢纽。我封装的逻辑包括:给所有请求自动加token请求头;响应拦截器里判断code字段,如果返回401就清除本地登录状态并跳转登录页;403则统一提示没有权限;其他业务错误直接弹出message提示。
这里有一个处理细节值得提一下:后端返回的文件流和普通JSON需要使用不同的解析方式。例如导出接口返回的是二进制数据,如果统一走JSON解析就会乱码。解决办法是在axios封装里加一个responseType参数,导出时手动设置成blob,然后通过实例化FileReader或URL.createObjectURL来触发下载。
6. 从联调到部署:跨域、Nginx和最容易翻车的几处配置
前后端分离的项目,本地跑通只是第一步,真正让系统交付给客户使用,还包括跨域配置、生产环境打包、Nginx反向代理部署这几个环节。这里面任何一个细节出问题,前端页面都会白屏或者接口全部报错。
6.1 前端跨域与代理配置
开发环境下,前端在localhost:8080,后端在localhost:8081,Ajax请求默认是跨域的。后端虽然可以通过配置@CrossOrigin注解来解决,但生产环境走Nginx时,这个配置又可能冲突,所以我更推荐前端用vue-cli的proxy来解决开发环境跨域。
在vue.config.js里配置:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };这样前端请求/api/employee/page会被自动转发到后端8081端口,浏览器看到的是同源请求,不需要后端开启跨域。代价是后端接口需要统一加/api前缀,或者通过context-path设置。我这里的做法是后端所有接口统一访问路径前缀为/api,生产环境Nginx再把/api转发给后端服务。
6.2 Nginx部署与静态资源处理
生产环境部署时,前端的npm run build会生成dist目录,把dist部署到Nginx里,配置一个server块,root指向dist目录。然后还需要加一条location规则,将/api路径代理到后端服务。
location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }另外一个最关键的就是前端路由的history模式需要配置try_files,否则刷新某个子页面时nginx会报404。正确配置是:
location / { root /opt/hrms-web/dist; index index.html; try_files $uri $uri/ /index.html; }这句try_files的含义是:如果请求的URL对应不上真实文件,就统一返回index.html,由前端路由接管页面渲染。没有这一行,你会遇到一种特别诡异的情况:从首页点进去操作一切正常,一刷新子页面就404。
6.3 数据库连接和时区问题
后端application.yml里配置数据库连接时,最容易遇到时区和SSL问题。MySQL 8.0以后,连接串建议写成:
jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/ShanghaiserverTimezone必须显式指定,不然MySQL和Java的默认时区可能不同,查出来的时间字段会差八个小时,尤其是考勤和薪资这种对时间敏感的业务,数据错乱会让用户直接抓狂。
6.4 常见联调问题排查思路
联调阶段我总结了三类高频问题,可以当排错清单来用。第一类是登录接口通了,但其他接口全部401。优先检查前端有没有把token放进请求头,以及后端拦截器解析token的加密秘钥是否一致。第二类是页面能访问但接口报504,一般情况下是后端服务没启动成功,或者Nginx代理的target地址打到错误端口。第三类是前端能登录但是页面空白,查看浏览器控制台,排查是不是动态路由加载顺序有问题,或者路由守卫把页面全部拦截了。
这三类问题覆盖了我实际项目中遇到的大部分场景,每次排查都遵循一个原则:先前端控制台看请求是否发出,再看后端日志看异常堆栈,最后看Nginx错误日志验证代理规则。一条链路从上到下排查,基本不会漏。
7. 源码、数据库脚本和文档:拿到的项目里都有什么
这部分主要说说交付内容的结构。拿到这套项目的源码包之后,你不会面对一个不知所措的文件夹,所有东西都按后端、前端、数据库、文档四块组织好。
7.1 后端工程结构说明
后端是一个标准的Maven工程,核心目录如下。
backend/ ├── sql/ # 数据库初始化脚本 ├── src/main/java/ │ ├── com.hrms.common/ # 统一响应、异常处理、工具类 │ ├── com.hrms.config/ # 配置类,包括拦截器、跨域、文件上传 │ ├── com.hrms.controller/ # 接口控制器 │ ├── com.hrms.entity/ # 实体类 │ ├── com.hrms.mapper/ # MyBatis Plus Mapper │ ├── com.hrms.service/ # 业务逻辑层 │ └── com.hrms.security/ # JWT与权限相关如果要做二次开发,大部分情况下你只需要改service层和新增controller,新建一张表就对应一个新实体的编写。这种规范的分层结构也方便导师或者团队负责人代码评审,一眼就能看清代码属于哪个层次。
7.2 数据库脚本的使用方式
sql文件夹里有两个文件,一个是hrms_schema.sql创建数据库和所有表的脚本,另一个是hrms_data.sql初始化的基础数据,包含默认管理员账号、菜单权限数据、示例部门岗位和少量员工信息。
导入数据库的步骤很简单,先创建数据库,然后依次执行两个脚本。
mysql -u root -p create database hrms default character set utf8mb4;然后使用source命令导入两个SQL文件。这里要重点提醒,数据字典菜单权限这部分初始数据必须导入,否则admin账号登录后看不到任何菜单,前端动态路由加载会直接报错。我遇到过多次这种问题,大多都是只导了schema没导data。
7.3 快速启动流程
后端启动前需要改application.yml里的数据库用户名和密码,确保能连上MySQL。然后通过Maven执行打包命令,再用java -jar启动。前端则是先npm install,再npm run dev启动开发模式,浏览器访问localhost:8080。
默认管理员账号密码一般是admin和admin123,首次登录后建议立即修改密码。文档里面写了一份接口文档,每个接口的请求方式、参数说明、返回示例都有标注,让接手的人不用去翻源码也能理清接口逻辑。
文档中还包含一份部署笔记,内容包括Linux环境的JDK和MySQL安装、Nginx配置、防火墙端口开放等,基本照着走就能把系统部署到一台干净的服务器上面。
8. 个人体会:给打算复刻这个项目的人几个实在建议
最后聊几个我在开发这个项目过程中体会最深的地方,希望能帮后面的人少走弯路。
第一,不要一开始就追求“全”。我最初设计功能的时候也想把绩效、招聘、培训全部做上,但工期和复杂度都不允许。实际落地的项目中,员工档案加考勤薪资已经能解决80%的问题,把核心链路做稳定远比铺开一堆半成品功能更重要。第二,数据库设计一定要花足够的时间推演,这个真不能省。我见过太多临时拼凑的系统到后期加一个字段就要改一堆SQL,原因就是前期没想清楚关系模型。哪怕后面要改,先把核心表的一对多、多对多关系理清楚,后面改起来也会轻松很多。
第三,前端代码尽量抽出公共组件,但别过度设计。像员工管理、部门管理这种页面,结构高度相似,抽一个通用查询表单组件确实能节省很多时间。但是组件之间的参数传递复杂起来后,调试成本也会上升。我的建议是,先复制粘贴实现两三个页面,等确认页面结构稳定后再回过来抽组件,这样组件的API设计会更贴合业务,而不是凭空拍脑袋。
根据我的使用感受,这套人事管理系统说到底是一个基础框架加一套可运行的实现。你自己落地的时候,可以拿它当骨架,需要二次开发的部分很清晰,不需要的部分删掉也不影响整体结构。如果你正好要做类似的东西,现在就可以动手,把数据库初始化好,然后从登录接口一路调到员工管理页面,相信很快就能跑通整个流程。