资讯动态

SpringBoot+Vue+MyBatis+MySQL实战:中小企业人事管理系统完整开发

发布时间:2026/10/15 7:28:20 来源:尧图企业网站定制
做管理系统的朋友应该都遇到过这种甲方一开始说就做一个简单的人事系统结果需求拆着拆着员工档案、组织架构、考勤、请假审批、薪资报表全堆过来了。而SpringBoot Vue MyBatis MySQL这四件套恰恰是目前中小企业人事管理系统里最主流、也最稳妥的一套搭配。前端Vue负责界面交互后端SpringBoot提供接口服务MyBatis控制SQL层MySQL存数据前后端彻底分离。这套方案既有大厂惯用的分层思路又不至于把架构搞得让一个人维护不动。这篇文章我按实际做项目的节奏来写先说清楚为什么这么选型、后台数据怎么设计然后拆登录鉴权、员工模块这些核心功能的落地细节再完整走一遍打包部署流程最后把联调和部署阶段我踩过的坑整理成速查表。无论你是想给公司做一套内部系统还是刚接触前后端分离想找个完整项目练手照着这条路子走都能少走很多弯路。1. 项目整体设计与技术选型背后的事1.1 为什么是这四件套而不是微服务或者其他组合先说点实在的。中小企业的人事系统真实业务量摆在那里几十人、几百人的公司考勤记录一天撑死几万条数据库层面加几个普通索引就完全扛得住。这种场景下如果我用微服务那一套——拆服务、搞注册中心、做分布式事务、上消息队列最终结果就是系统复杂度远超业务复杂度一个人开发光维护就累死。单体应用加前后端分离是这个量级业务的最优解。SpringBoot的核心价值不是高科技是省事。它内嵌了Tomcat不用再单独部署一个web容器打包成一个可执行的jar直接java -jar就能跑起来。这对没有专业运维的小公司尤其友好服务器上装一个JDK剩下的全交给SpringBoot。Vue则是中后台系统的效率利器。人事管理这种系统80%的页面就是表格、表单、弹窗、树形结构Vue生态里的Element UI/Element Plus把这些组件全做完了。相比ReactVue的上手曲线更平缓招人容易初级的开发也能快速产出页面。MyBatis的价值在于SQL可控。人事系统里最烦人的不是CRUD是那一堆多表关联的报表查询——比如按部门统计人数、按月汇总考勤、生成工资表。用MyBatis我可以直接手写SQL该怎么join就怎么join执行计划有问题时也能精准优化。如果换成JPA/Hibernate那种以实体对象为核心的思路复杂查询反而被对象映射束缚住改起SQL特别别扭。MySQL就更不用多说了普及率高、云数据库几乎都兼容、遇到问题一搜一大把解决方案。对一个预算有限的中小企业来说MySQL的成本和生态都是无可挑剔的。这套组合最忌讳的就是有人拿高并发海量数据来吓唬你。做人事实时统计系统本来就不面对这种场景选型时明确业务边界比盲目追求新技术重要得多。1.2 功能模块拆解与数据库表设计思路人事管理系统的核心模块无论需求方怎么变底子上跑不出这几块系统管理用户、角色、权限、组织架构部门、职位、员工管理档案、入职离职调动、考勤管理打卡、请假、加班、薪资管理工资项、工资记录。招聘模块看需求要求不严的时候可以后用Excel顶。数据库设计是这种系统最见功夫的地方。我见过好多半路出家的项目上来就建一张巨大的员工表所有字段往里塞结果部门调整、职位变动全没地方记录。正确做法是拆表加关联。部门表必须有parent_id字段靠这个字段做树形结构。员工表不直接存部门名称而是存department_id外键这样部门改名只需要改一行记录员工表完全不受影响。职位同理单独一张职位表。用户、角色、权限这三张表是权限模型的地基。简化的RBAC模型是三张表sys_user、sys_role、sys_user_role。更完整一点还可以加sys_menu表做菜单权限控制但中小企业内部系统往往连菜单权限都懒得细分一般做到一个角色一套菜单按钮级权限只给少数管理功能用。员工表的核心字段我列一下你们做的时候可以直接参考字段说明备注id主键自增或雪花IDemployee_no员工编号唯一索引系统自动生成name姓名索引查询高频gender性别字典值birth_date出生日期用于年龄统计department_id所属部门外键position_id职位外键entry_date入职日期离职等状态判断依据status在职状态1在职 2离职 3休假phone / email联系方式注意脱敏展示deleted逻辑删除标记强烈建议必加这里有两条设计经验值得单独说。第一不要物理删除员工数据用deleted字段做逻辑删除。人事数据涉及历史记录和薪资追溯一旦物理删了就再也查不到了。第二每张表都加上create_time、update_time、create_by、update_by这组审计字段出了数据问题能顺着查是谁在什么时候改的。考勤表按员工每天一条记录来设计字段包括employee_id、attendance_date、上班打卡时间、下班打卡时间、考勤状态。请假表则要有leave_type事假、病假、年假、开始时间、结束时间、请假原因、审批状态。审批状态保留一个待审批、通过、驳回的三态流程别一开始就想做复杂的审批流引擎。2. 核心细节解析与实操要点2.1 接口规范、统一返回体与跨域联调前后端分离项目最先要定死的不是哪个页面怎么写而是接口规范。前后端各做各的没有统一约定联调阶段就是灾难现场。我习惯先写一个统一返回体所有接口一律返回这个结构public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }code的约定要全团队统一200代表成功401表示未登录或token失效403是已登录但没有操作权限500是服务器异常。前端axios拦截器里统一判断这个code不要每个页面单独处理错误提示。接口路径走RESTful风格。员工模块大概长这样GET /api/employee/page分页查询员工列表GET /api/employee/{id}查询员工详情POST /api/employee新增员工PUT /api/employee修改员工DELETE /api/employee/{id}逻辑删除员工这里有个细节分页接口我故意不叫GET /api/employee/list而叫page因为接口返回结构本身就是分页对象total、records、current、size名字起成page前端看到就知道是分页结构。跨域是前后端分离躲不开的问题。前端跑在localhost:8081后端跑在localhost:8080浏览器同源策略直接拦截请求。解决方式常用的有三种后端加CORS配置、前端开发环境配proxy代理、生产用Nginx反向代理。我的建议是**后端就把CORS配置写好前端开发时也用proxy转发生产环境则用Nginx统一入口。**三管齐下哪个环境都不出问题。后端CORS配置最简单的方式是加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }前端开发环境的proxy配置Vue 2的vue.config.js里这么写devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/employee/page时开发服务器会自动转发到后端的8080端口浏览器看到的是同源请求跨域问题变成不存在。2.2 登录鉴权和权限控制落地人事系统的数据太敏感了员工工资、身份证号、手机号都在里面权限设计是安全底线。我用的是JWT加拦截器的方式。流程不复杂但每一步都有值得注意的地方。用户提交用户名密码后后端校验通过签发一个JWT返回给前端。JWT里只放必要信息用户ID、用户名、角色编码别的敏感数据一概不放。token过期时间定成12小时比较合适太短用户老是重新登录太长又有安全隐患。密钥要单独配置在配置文件里坚决不写死在代码中更不能提交到代码仓库。前端拿到token后放在localStorage里axios请求拦截器统一在请求头加Authorization: Bearer xxx。后端这边写一个拦截器拦截所有/api/**请求先从请求头取token校验签名和有效期再把用户信息放到ThreadLocal里供后续使用。这里有个新手容易忽略的点/api/login这个接口本身不能被拦截否则用户还没登录就被拦住了。权限控制这块我强烈推荐先把RBAC做对。用户在sys_user表里角色在sys_role表里用户和角色通过sys_user_role中间表关联。每个角色对应一组权限标识。后端接口上可以配注解比如员工新增功能标注PreAuthorize(hasAuthority(employee:add))用户没有这个权限Spring Security会直接拒绝。如果项目一开始没接Spring Security也可以用拦截器加注解扫描的方式自己实现一套轻量鉴权不过说实话只要项目不是特别赶直接上Spring Security更稳妥省得后面返工。前端这边配合动态路由登录成功后接口返回当前用户的菜单列表和按钮权限集合前端用router.addRoutes动态挂载路由再用自定义指令控制按钮的显示隐藏。没有权限的菜单直接不渲染比渲染出来再置灰要清爽得多。还有个细节密码存储必须用BCrypt加密不能是MD5。MD5虽然也算哈希但彩虹表一查就没了。Spring Security自带的BCryptPasswordEncoder直接用就行。2.3 MyBatis 实战三个高频场景MyBatis用起来有几个地方几乎每个项目都会踩到我单独拎出来讲。第一是分页。分页插件PageHelper是真方便但有一个铁律PageHelper.startPage(pageNum, pageSize)之后必须紧跟真正要分页的那条查询语句中间不能插入别的查询或耗时操作。我曾经在一个方法里先查了一个部门列表再查员工分页结果分页插件作用到了部门查询上返回的数据完全错乱。排查了好久才意识到是这个顺序问题。第二是动态SQL。员工列表的筛选条件不固定有时按部门查有时按关键字查有时按状态查。这种场景用where标签加if标签组合最清晰select idselectEmployeePage resultTypemap SELECT e.*, d.name AS department_name, p.name AS position_name FROM employee e LEFT JOIN department d ON e.department_id d.id LEFT JOIN position p ON e.position_id p.id where if testdepartmentId ! null and departmentId ! AND e.department_id #{departmentId} /if if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.employee_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null and status ! AND e.status #{status} /if AND e.deleted 0 /where ORDER BY e.create_time DESC /select注意模糊查询一定要用CONCAT(%, #{keyword}, %)来拼接不要直接用${keyword}拼接字符串。前者是预编译安全后者是拼接SQL存在注入风险。第三是像部门树这种树形数据。SQL里递归查询写起来费劲数据量和层级都不深的情况下我一般先查出全量部门列表然后在Java代码里递归组装成树。代码好维护逻辑也直观性能完全够用。批量插入考勤记录时用foreach标签一次insert多条比循环单条插入性能好很多倍。3. 实操过程与核心环节实现3.1 开发环境搭建与工程结构规划先列一套我亲测稳定的版本组合JDK 8 或 11建议 11企业里 8 也完全没问题Maven 3.6 以上MySQL 5.7 或 8.0建议 8.0驱动类名和时区配置有变化Node.js 14 以上Vue 2.7 或 Vue 3 Element Plus 都可以中小系统选哪个看团队熟悉度IDEA 写后端、VSCode 写前端或者一个IDEA全搞定数据库初始化直接用Navicat或命令行导入预先写好的hrms.sql脚本注意建库时数据库字符集用utf8mb4比utf8更能兼容emoji和生僻字。后端工程结构我习惯按这种分包方式src/main/java/com/example/hrms/ ├── controller/ # 接口层只负责参数接收和Result返回 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis的Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 接口出入参对象 ├── config/ # 配置类CORS、JWT等 ├── common/ # 统一返回体、异常处理、常量 ├── utils/ # 工具类 └── HrmsApplication.java前端Vue项目标准结构src/ ├── api/ # 按模块封装的请求方法 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── store/ # Vuex/Pinia状态管理 ├── views/ # 页面视图 │ ├── login/ │ ├── dashboard/ │ ├── employee/ │ ├── department/ │ └── system/ ├── utils/ # request封装、校验工具 ├── App.vue └── main.js这种结构最大的好处是职责清晰后端一个接口对应Controller一个方法前端一个页面对应views目录下一个文件夹找人找代码都不费劲。3.2 员工管理模块从后端到前端完整落地员工管理是人事系统的核心模块我用它把一条完整链路走一遍。后端部分首先在实体类里建好Employee字段和表结构一一对应。然后写Mapper接口Mapper public interface EmployeeMapper { ListEmployeeVO selectEmployeePage(PageParam param); Employee selectById(Long id); int insert(Employee employee); int update(Employee employee); int logicalDelete(Long id); }Service层处理业务逻辑比如员工编号自动生成Service public class EmployeeServiceImpl implements EmployeeService { Autowired private EmployeeMapper employeeMapper; Override Transactional public void addEmployee(Employee employee) { // 生成员工编号EMP 时间戳后8位或者查库最大编号1 String maxNo employeeMapper.selectMaxEmployeeNo(); employee.setEmployeeNo(generateEmployeeNo(maxNo)); employeeMapper.insert(employee); } }Controller层保持薄弱只做参数接收和转发RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public ResultPageResultEmployeeVO page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long departmentId, RequestParam(required false) String keyword) { return Result.success(employeeService.pageQuery(pageNum, pageSize, departmentId, keyword)); } }前端部分先在api/employee.js里封装请求import request from /utils/request export function getEmployeePage(params) { return request({ url: /employee/page, method: get, params }) } export function addEmployee(data) { return request({ url: /employee, method: post, data }) }然后在views/employee/index.vue里做列表页。页面骨架是顶部搜索栏中间表格底部弹窗表单。搜索栏包含部门下拉框、关键字输入框和查询/重置按钮表格列出来员工编号、姓名、部门、职位、入职日期、状态弹窗表单用动态表单校验。表单校验有个实用技巧新增时手机号必填且校验格式编辑时手机号可以留空。把校验规则做成变量在新增和编辑两个方法里分别赋值const rules { name: [{ required: true, message: 姓名不能为空, trigger: blur }], phone: [ { required: isAdd.value, message: 手机号不能为空, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ] }这样一套走完增删改查、搜索、分页、表单校验就全齐了。其他模块比如部门管理、职位管理无非是把列表和表单换一换骨架完全复用。3.3 前后端打包与部署全流程系统开发完部署环节决定能不能真正上线。我按最省钱的单机部署方案来讲一台云服务器就够。后端打包在项目根目录执行mvn clean package -DskipTests打包完成后target目录下生成一个可执行jar包。启动前准备好生产环境的application-prod.yml把数据库连接、JWT密钥改成正式的。启动命令nohup java -jar hrms.jar --spring.profiles.activeprod /opt/hrms/hrms.log 21 想让服务开机自启且崩溃能自动拉起用systemd写一个服务配置比裸跑nohup靠谱得多。前端打包在项目根目录执行npm run build构建产物会输出到dist目录。把这个目录上传到服务器/opt/hrms/dist下然后用Nginx做静态托管和API反向代理。关键的Nginx配置如下server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/hrms/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 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 $uri $uri/ /index.html;。Vue如果用history模式不带#的路由刷新页面时Nginx会去找对应的静态路径找不到就404加上这个配置会把所有未匹配路径回退到index.html由前端路由接管问题立刻消失。/api/反向代理到8080端口这样浏览器访问的就是同一个域名跨域问题在生产环境彻底规避。如果服务器装了Docker更优雅的方式是三个容器MySQL、后端应用、前端Nginx用docker-compose编排。不过这块看个人习惯在我服务的中小企业里大部分还是习惯直接跑jar包加Nginx运维简单出问题也好排查。4. 常见问题与排查技巧实录4.1 联调阶段高频报错速查联调就像两个齿轮咬合齿对不上就咔咔响。这里把最常见的几类问题整理成速查表报错现象根本原因解决方案浏览器报CORS跨域错误前后端端口不一致未配置跨域后端加CORS配置或前端devServer配proxy请求返回404接口路径拼错或Controller类没被扫描到确认Controller所在包在主启动类包路径下请求返回401token未携带、过期或签名不匹配检查axios拦截器是否加Authorization头查看token过期时间后端返回Long类型ID前端后几位变成0JavaScript精度只能处理到Number.MAX_SAFE_INTEGER实体类ID或返参上用JsonSerialize(using ToStringSerializer.class)转成字符串前端查到的时间比数据库少了8小时时区配置不对JDBC URL加serverTimezoneAsia/ShanghaiJackson配置UTC转GMT8前端明明收到了数据列表渲染空白字段名对不上或返回结构不是预期格式浏览器Network里看响应体和后端实体字段逐个核对时间时区这个坑十个项目至少五个会踩。后端写得快的人经常忽略结果一上线发现所有时间显示都是昨天。建议项目初始化时就把spring.jackson.date-format和time-zone配好别等联调再修。4.2 部署环境中的几个实战坑部署阶段的问题跟联调不一样操作环境变了新问题就冒出来。MySQL 8.0 之后数据库驱动类名变成了com.mysql.cj.jdbc.Driver老的com.mysql.jdbc.Driver虽然能兼容但会报警告。连接串里的时间时区参数也必须带上否则启动直接报The server time zone value ... is unrecognized。服务器内存不够是常事。一台2G内存的云服务器MySQL一启动就占掉几百兆再跑java jar很容易被系统OOM杀掉。给JVM限个内存池启动参数加-Xms256m -Xmx512m够用就行别让SpringBoot默认把内存夯满。端口占用也是高频问题。一台服务器上可能同时部署了多个服务8080被占了不知道后端起不来还老怀疑自己代码写错了。排查命令记牢netstat -tlnp | grep 8080看到占用进程的PID再ps -ef | grep PID看清楚是什么进程。还有前端部署目录权限问题。Nginx默认以www-data或nobody用户运行如果dist目录是用root上传的权限不对就会403。上传完记得chown -R www-data:www-data /opt/hrms/dist。4.3 上线后的优化与扩展建议系统上线能用只是第一步后面优化空间还很大。我不主张一上来就上很多复杂组件按性价比排序来做Excel导入导出员工批量导入、工资条导出这是人事最常用的功能用阿里EasyExcel包代码量不大但实用性极强Redis缓存先拿来做登录token的校验和验证码存储等业务量真上去了再考虑缓存热点数据操作日志用一个AOP切面记录关键操作谁在什么时候改了谁的工资这既是管理需求也是合规要求数据备份MySQL定时mysqldump加 crontab 每天备份备份文件异地存放出事故时能救命文件上传员工头像、简历附件本地目录存储先顶着等文件多了再上OSS扩展开源也很容易因为表结构和模块边界当初拆分得清楚。新增一个培训管理模块就是建表、写实体、写Mapper、写页面四条线的事不会影响既有功能。最后讲一点个人体会。前后端分离系统做起来最怕的不是技术难点是前后端各写各的联调时互相甩锅。想把项目做得顺第一步就要把接口文档、统一返回体、错误码约定死白纸黑字写清楚。其次是权限设计这玩意儿越早定越省事等页面写完了再返工加权限控制那就是灾难。还有这类系统代码量不小但技术难度真不高核心就是耐心拆模块、细心做表设计、踏实走流程——做到了这三点系统大概率稳得很。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑