
前后端分离人事系统听起来像是教科书里的课程设计但真正把这套东西从零跑到线上的人都知道它背后涉及的不只是代码而是一整套完整的前后端协作模式。SpringBoot负责接口Vue渲染页面MyBatis操作MySQL这三样东西组合在一起就是当前大多数中小型企业项目的标准配置。我断断续续折腾过好几个类似的人事系统也帮别人部署上线过几次这次干脆把一套能跑通的全源码和部署步骤整理出来分享给正在做练习、搞毕业设计或者刚入门前后端分离开发的朋友。这篇文章不会只丢一个源码链接就完事我会把里面最关键的设计思路、代码片段、联调过程、部署细节都拆开讲清楚。你看完以后不仅能跑起来项目还能知道改哪里、为什么改、踩坑了怎么解决。哪怕你之前没碰过SpringBoot和Vue只要按着步骤来也能一步步搭出属于自己的管理系统。1. 项目整体设计与技术选型解析1.1 为什么做一套前后端分离的人事系统先说一个很现实的问题人事系统这种项目功能无非就是员工增删改查、部门管理、考勤、薪资、权限在传统的JSP时代早就做过无数遍了为什么现在非要用前后端分离再写一遍因为实际开发环境变了。现在的企业项目前端是一个团队后端是一个团队两边通过接口文档和数据结构协作。前端代码部署在Nginx或者CDN上后端代码打包成jar包独立运行互不占用部署资源也互不拖累发布节奏。前端改页面样式不需要重新发后端服务后端加接口也不影响前端静态页面。人事系统虽然业务规模不大但它的模块划分足够全面特别适合拿来模拟这种真实协作场景。如果你只是想做一个内部小工具前后端不分离确实更快但如果你是奔着找工作、进项目组甚至接私活去的那前后端分离就是必须掌握的技能。这套人事系统麻雀虽小五脏俱全能帮你把整个开发链条串起来。1.2 核心技术栈各自负责什么先看一张简单的角色分工表心里有个底技术栈在这个项目里扮演的角色为什么选它SpringBoot后端服务提供RESTful接口内置Tomcat配置少启动快企业使用率高Vue前端页面渲染用户交互组件化开发数据驱动视图开发效率高MyBatis数据库访问层执行SQLSQL由开发者控制灵活复杂查询好优化MySQL数据存储开源免费稳定配套生态成熟整个请求链路是这样的用户在浏览器打开Vue页面Vue发起axios请求请求到SpringBoot的ControllerController调ServiceService调Mapper接口Mapper执行由MyBatis管理的SQL语句去操作MySQL数据库拿到数据后一层层返回最终由Vue把JSON数据渲染到页面上。这套技术栈里SpringBoot解决了“不用复杂配置就能跑起来一个Web服务”的问题Vue解决了“操作DOM太麻烦”的问题MyBatis解决了“Java对象和数据库字段之间转换”的问题MySQL负责最终的数据落地。单独拆开每样都是经典组合在一起就是目前最主流的中小型系统方案。1.3 人事系统的功能模块规划一个拿得出手的人事系统至少需要下面这些模块用户登录与权限管理区分管理员、HR、普通员工登录成功后返回Token前端通过路由和菜单控制页面访问。部门管理维护公司组织结构增删改查支持层级。员工管理员工信息的添加、编辑、删除、分页查询、关键字搜索这是系统最核心的模块。考勤管理记录上下班打卡时间、请假申请、考勤统计。薪资管理设置基本工资、绩效生成工资记录。公告管理发布公司通知员工登录后能看到最新公告。从工作量上看员工管理和权限管理是最费时间的尤其是员工信息的字段非常多姓名、性别、手机号、邮箱、身份证、入职时间、职位、部门、学历、头像等等。前端要做复杂的表单校验后端要做数据校验和异常处理。这套源码里我把这些核心模块都实现了一遍考勤和薪资简化了一些但主流程是完整的方便你在此基础上继续扩展。2. 环境准备与源码导入2.1 开发环境版本千万不用乱选很多新手卡的第一步不是写代码而是环境版本匹配不上。这个项目我的建议是JDK1.8 或 11Maven3.6 及以上Node.js12.16 或 14Vue CLI 4 项目用MySQL5.7 或 8.0后端IDEIntelliJ IDEA前端IDEVSCode 或 WebStorm 都可以这里特别提醒一句不要盲目追求SpringBoot最新版。当前这套项目是基于SpringBoot 2.5.x或者2.7.x开发的你要是直接换成SpringBoot 3.x很多依赖的写法都不一样了JDK也得换到17容易在一开始就劝退自己。选一个经过大量项目验证的稳定版本远比尝鲜重要。MySQL我建议直接装8.0因为5.7虽然老当益壮但8.0的安装教程和可视化工具支持已经非常成熟。Navicat连接的时候注意选择MySQL协议不要选错。2.2 数据库初始化与配置文件修改拿到源码后先别急着跑后端数据库必须先准备好。打开MySQL新建一个数据库名字随意我用的是hr_db字符集选utf8mb4这个很关键以后存emoji或者特殊字符不会乱码。然后导入源码里提供的hr.sql脚本。命令行执行mysql -u root -p hr_db hr.sql或者在Navicat里直接打开SQL文件运行。脚本会创建表结构并且插入默认的管理员账号和演示数据。接着修改后端配置文件application.yml这是整个项目最重要的配置文件spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hr_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456这里每一段参数都有讲究。useSSLfalse是关掉SSL连接避免MySQL 8报ssl连接错误serverTimezoneAsia/Shanghai是为了解决时区相差8小时的问题不写经常会报The server time zone value异常characterEncodingutf8保证中文不会乱码。2.3 前端依赖安装与启动命令前端项目通常在frontend目录下。打开命令行进入目录cd frontend npm install npm run servenpm install会安装package.json里声明的所有依赖这个过程在国内可能会比较慢甚至直接卡死。如果遇到这种情况先设置一下淘宝镜像npm config set registry https://registry.npmmirror.com然后再执行npm install速度会明显提升。启动成功后命令行会提示App running at Local: http://localhost:8080。这时候先别急着打开因为后端服务还没启动。我们需要先在IDEA里启动SpringBoot应用再访问前端页面才能看到登录界面。3. 后端核心实现与关键代码解析3.1 SpringBoot项目结构与分层思路后端项目我习惯按下面的结构组织目录hr-server ├── pom.xml └── src/main/java/com/hr ├── HrApplication.java ├── controller # 接口层接收前端请求 ├── service # 业务层处理业务逻辑 │ └── impl ├── mapper # MyBatis数据访问接口 ├── entity # 实体类对应数据库表 ├── common # 统一返回结果、异常处理 └── config # 配置类拦截器、跨域等分层的逻辑很简单Controller不写业务代码只负责参数接收和结果包装Service专注业务逻辑比如校验、计算、事务Mapper只负责数据库操作。这样分层以后每个类的职责都很单一出了问题也容易定位。再加上MyBatis的接口和XML文件分离SQL和Java代码互不干扰后期优化SQL也不影响业务层。如果项目再大一点你还可以引入DTO、VO等对象把前端传进来的参数和返回给前端的数据单独定义减少实体类和前端字段的耦合。这套人事系统目前直接用了实体类返回对学习来说已经足够。3.2 MyBatis的XML映射与SQL写法MyBatis最核心的是Mapper接口和XML映射文件。比如员工查询接口Mapper接口定义方法public interface EmployeeMapper { ListEmployee selectEmployeeList(Param(keyword) String keyword, Param(offset) int offset, Param(limit) int limit); }对应的EmployeeMapper.xmlselect idselectEmployeeList resultTypecom.hr.entity.Employee SELECT id, name, phone, department_id, position, hire_date, status FROM employee where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY id DESC LIMIT #{offset}, #{limit} /select注意这里用的是#{}不是${}。#{}会被MyBatis预编译成?占位符自动加引号有效防止SQL注入。${}是字符串拼接千万别用在用户输入的地方。如果要动态拼接表名、排序列名这种不受用户控制的场景才考虑${}。很多人看MyBatis面试题的时候会看到XMLConfigBuilder、XMLMapperBuilder这些类它们负责把配置文件解析成Configuration对象。平时写项目用不到但理解这条初始化链路后遇到“明明XML文件写对了运行时却说找不到SQL”这种问题你就知道应该去检查XML路径是否被扫描到了。3.3 登录认证与权限拦截实现人事系统里面权限控制是必须的。我采用的是JWT方案用户登录成功后后端生成一个Token字符串返回给前端前端把Token存到localStorage里后续每个请求在Header里带上Authorization: token后端拦截器统一校验。核心是自定义一个拦截器Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null JwtUtil.verify(token)) { return true; } response.setStatus(401); return false; } }然后注册拦截器并放行登录接口Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/login); } }有了这个机制未登录的用户访问员工接口会直接返回401前端收到401后就跳转登录页。角色权限我用了简单的RBAC模型用户表、角色表、用户角色关联表登录后一次性查出用户对应的角色然后在前端渲染对应菜单。如果你的需求角色很少也可以在后端接口上用注解判断角色但那样扩展性差一些。3.4 员工管理接口的完整链路拿新增员工来说前端提交一个表单对象后端Controller接收RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; PostMapping public Result add(RequestBody Employee employee) { employeeService.add(employee); return Result.success(); } GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword) { PageResultEmployee data employeeService.getPage(page, size, keyword); return Result.success(data); } }Service层要做的是参数校验、设置默认状态、然后调用Mapper插入数据库。这里我统一用了Result对象返回包含code、message、data三个字段前端拿到后先判断code是否为200再处理数据。有一个细节很容易被忽略更新员工信息时要把创建时间和更新时间分开处理。插入时设置create_time和update_time更新时只修改update_time。很多新手会把创建时间丢了或者在查询列表时字段映射错误导致时间显示为null。这就是为什么我用MyBatis的resultType而不是resultMap做简单映射——字段名和数据库列名保持一致时能省不少事。4. 前端Vue实现解析4.1 Vue项目结构、路由与页面规划前端项目我使用的是Vue 2.6 Element UI稳定且资料多。目录结构大致如下frontend ├── public ├── src │ ├── main.js # 入口文件 │ ├── App.vue │ ├── router # 路由配置 │ ├── api # 接口封装 │ ├── views # 页面组件 │ ├── components # 公共组件 │ ├── utils # 工具类request.js等 │ └── assets # 静态资源路由设计上登录页单独一条路由登录成功后进入Layout组件里面套子路由比如/employee、/department、/attendance、/salary。所有业务页面都放在Layout的内容区里左侧是菜单顶部是用户信息。vue-router在使用的时候要特别注意两个点一是路由模式如果部署到Nginx我建议使用history模式但必须在Nginx里配置try_files否则刷新页面会404二是动态路由如果不同角色看到的菜单不一样前端要在登录拿到用户权限后用router.addRoutes动态添加路由这比一次性写死所有路由更安全。4.2 Axios统一封装与登录拦截前端最重要的一个公共文件是utils/request.js它把axios实例统一封装起来。我一般这么写import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) } else { Message.error(error.message || 请求失败) } return Promise.reject(error) } ) export default request把所有请求都走这个封装好的实例好处是不用每个页面都重复写token校验和错误提示。尤其是401拦截这是前后端分离项目里最常见的联动逻辑前端只要在这里写一次整个项目的登录状态控制就统一了。API管理上我会在src/api/employee.js里导出一个个函数import request from /utils/request export function getEmployeePage(params) { return request({ url: /employee/page, method: get, params }) }这样页面组件里只需要import { getEmployeePage } from /api/employee调用起来非常清爽以后后端接口地址变了只需要改API文件不用满项目去找。4.3 动态菜单与员工管理页面落地动态菜单的实现其实不复杂登录接口返回当前用户的菜单列表前端把它转成el-menu需要的结构然后用v-for渲染。el-menu el-menu-item v-foritem in menuList :indexitem.path :keyitem.path {{ item.title }} /el-menu-item /el-menu如果菜单有层级就需要递归组件。这套源码里我用的是两层级父菜单作为el-submenu子菜单作为el-menu-item已经能覆盖多数管理系统的场景。员工管理页面是典型的列表弹窗结构。页面上方是搜索表单中间是表格下方是分页。我常用的做法是el-table :datatableData border v-loadingloading el-table-column propname label姓名 / el-table-column propphone label手机号 / el-table-column propdepartmentName label部门 / el-table-column label操作 template slot-scopescope el-button typetext clickopenEdit(scope.row)编辑/el-button el-button typetext clickhandleDelete(scope.row.id)删除/el-button /template /el-table-column /el-table新增和编辑共用一个Dialog判断form.id是否存在来区分。保存成功后重新加载列表。列表数据在created()钩子里调用created() { this.loadData() }这里有个小技巧加载表格数据前设置loading true请求回来后置为false不然快速切换页签时表格会闪一下看起来很不专业。5. 前后端联调与生产环境部署教程5.1 本地联调跨域代理的正确姿势前端开发服务跑在8080端口后端接口跑在8081端口直接让浏览器去请求另一个端口会触发跨域问题。解决办法是让前端的开发服务器代理请求在vue.config.js里配置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端页面请求/api/employee/page时Vue开发服务器会把这个请求转发到http://localhost:8081/api/employee/page浏览器看到的请求是同源的所以不会跨域。后端不用额外配置CORS开发阶段用代理是既简单又安全的方式。如果你用的不是Vue CLI而是Vite配置位置在vite.config.js的server.proxy写法类似。千万别在浏览器里直接访问8081接口调试那样一定跨域调通了也是绕过的CORS不是真实场景。5.2 后端打包成jar并启动开发环境调试没问题后就该打包部署了。后端在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个hr-server-0.0.1-SNAPSHOT.jar。把这个jar上传到服务器然后运行java -jar hr-server.jar如果想让服务在后台运行用nohupnohup java -jar hr-server.jar server.log 21 日志输出到server.log排查问题非常方便。SpringBoot内置了Tomcat所以不需要额外安装Tomcat容器。这也是我之前遇到很多人在问“tomcat部署前后端分离项目”的答案之一后端jar本身就是Web服务直接启动即可。如果一定要用传统Tomcat部署SpringBoot项目也不是不行但需要把打包方式改成war并实现SpringBootServletInitializer配置过程相对麻烦现在已经不是主流做法。5.3 前端打包并部署到Nginx前端打包npm run build打包完成后生成dist目录这个目录里是静态文件可以上传到服务器的任意一个目录比如/usr/share/nginx/html。Nginx配置参考如下server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } }这里两个关键点第一location /api/的proxy_pass要把接口请求反向代理到后端的8081端口。注意后面的URL如果proxy_pass没有带路径则会把原始请求的完整路径转发过去如果带了路径可能会丢失一部分。通常建议不带路径直接proxy_pass http://127.0.0.1:8081;这样最不会出错。第二try_files $uri $uri/ /index.html;必须写。前端用的是history模式路由当浏览器直接访问/employee地址时服务器上并没有这个真实文件Nginx需要自动返回index.html然后由Vue Router接管路由。不写这句话刷新页面就是404。6. 常见问题与排查技巧实录6.1 版本兼容性相关的坑我见过最多的报错就是Jackson版本冲突、MyBatis绑定异常、JUnit版本问题。很大一部分原因是SpringBoot版本和MyBatis Starter版本不配套。建议直接使用源码pom.xml里的版本组合不要轻易升级。排查思路先看mvn dependency:tree确认MyBatis、SpringBoot的子依赖版本是否冲突如果是Invalid bound statement则优先检查XML文件的namespace是否和Mapper接口全限定名一致id是否和接口方法名一致。还有一个冷门坑resources目录没有把XML文件打包进去导致运行环境中找不到Mapper XML。解决方案是在pom.xml的build中添加resources配置把src/main/resources下的xml文件包含进来。6.2 MySQL连接相关的报错集合这里整理一份速查表报错信息原因解决办法The server time zone value时区未设置URL加serverTimezoneAsia/ShanghaiSSL connection error未关闭SSLURL加useSSLfalsePublic Key Retrieval is not allowedMySQL8安全机制URL加allowPublicKeyRetrievaltrueUnknown database数据库未创建先创建hr_dbAccess denied for user用户名或密码错误检查application.yml连接MySQL时还有一个常见问题就是你安装MySQL教程走了很多步结果服务没启动。Windows下可以执行net start mysql或者去服务管理器启动MySQL服务。我遇到过更蠢的情况端口被占用了localhost:3306连不上用netstat -ano | findstr 3306一查才看到被别的程序占了。6.3 前后端接口404与跨域排查前端能打开页面但列表数据出不来大概率是接口路径不匹配。我建议先打开浏览器开发者工具看Network里请求的URL是什么再看看后端控制台有没有收到请求。如果请求发到了/api/employee/page但后端报404检查Controller类上是不是有RequestMapping(/api/employee)方法路径是不是/page。注意SpringBoot后端的context-path如果你在application.yml里设置了server.servlet.context-path: /hr那么前端/api代理也要加上/hr不然就会404。如果请求直接失败显示ERR_CONNECTION_REFUSED那多半是后端服务没起来或者端口不是8081。日志是最好用的排查工具后端别用Println乱打至少用log.info问题定位快很多。6.4 几个容易忽略的小细节第一个细节前端跨域请求有时会有预检OPTIONS请求后端拦截器如果对OPTIONS请求拦截并判为未登录就会拦截失败。我在前面的拦截器代码里专门加了一个判断直接放行OPTIONS这一步容易漏。第二个细节员工头像上传或者导入导出功能如果用到文件上传SpringBoot的默认文件大小限制是1MB很容易上传失败。需要在application.yml里调整spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB第三个细节Element UI的表格列如果宽度不够会显示省略号。可以设置min-width或者用show-overflow-tooltip让鼠标悬停显示完整内容。第四个细节打包前端后如果部署到子目录下比如http://example.com/hr/你需要把vue.config.js里的publicPath改成/hr/并且在路由里配置base: /hr/。不然静态资源会加载不到。这些坑我前前后后都踩过。每一次排查完我都建议把原因和解决过程记下来以后遇到同类问题直接查自己的笔记比重新搜索答案快得多。我个人在实际操作中的体会是人事系统这类前后端分离项目难点从来不是某个单独的技术而是整条链路跑通前的耐心。数据库字符集、时区、代理配置、路径映射随便一个小环节出问题都能让人折腾半天。但只要你能按照先数据库、再后端、再前端、再联调的顺序一步步来并且学会看日志这套系统跑起来以后你会对整个前后端分离开发有一个非常扎实的感知。最后再分享一个小技巧手动在浏览器里访问后端接口时可以先把后端跑起来地址输入http://localhost:8081/api/employee/page看到返回JSON数据说明后端没问题再把前端跑起来开着开发者工具看请求如果前端请求到了但后端日志没收到就是代理配置问题如果后端有日志但数据渲染不出来多半是字段名对不上。用这种“由后往前”的排查方式很快就能定位问题所在。希望这篇内容能帮你顺利把这套人事系统跑起来也能在里面学到动手能力。