前后端分离这个词做后端的人几乎每天都能听到但真正从零把一个完整的人事系统搭起来并部署上线和看教程、写Demo完全是两回事。人事系统看起来不就是增删改查嘛但部门层级、员工档案、考勤统计、薪资计算、权限分配这些东西叠加在一起复杂度马上就上来了。我这套系统用SpringBootVueMyBatisMySQL把前后端完全拆开源码、部署流程我都完整走了一遍今天这篇就当是给没做过完整项目的人一份可直接抄作业的实战笔记。先说清楚这套东西适合谁看在校学生做毕业设计、刚工作一两年的后端想补全前端知识、或者公司内部需要一个能跑起来的人事管理后台。前端我只用了Vue 3 Element Plus后端就是SpringBoot 2.7 MyBatis MySQL没有引入任何花哨的组件和中间件门槛不高但覆盖了一个真实系统该有的完整链路登录鉴权、动态路由、部门树、员工资料管理、考勤打卡、薪资条、以及生产环境的Nginx部署。说实话把这些串起来跑通了比刷一百道面试题都管用。1. 人事系统前后端分离架构为什么我坚持这么拆1.1 单体也能做但改动成本越来越高先别急着谈架构很多人会问一个几百人规模的人事系统JSPSpringMVC单体应用不也能做能我早期就用单体写过类似系统但做人事系统最大的痛点不是并发而是需求改得勤。今天加一个加班补贴字段明天改一下考勤规则后天又要在工资条里塞一个绩效列。单体应用下前端页面和后端接口全耦合在一起改一个字段往往要同时动Controller、Service、Mapper、JSP稍不注意就把页面改崩了。前后端分离之后后端只需要把接口稳定下来前端页面随便改后端一点不用动。我现在的开发节奏是前端同学调接口联调后端继续加新接口互不阻塞。这个体验一旦习惯了是真的回不去。1.2 前后端分离的四个关键收益做这个人事系统我复盘了一下前后端分离带来的好处可以落到四个具体点上第一职责边界清楚。后端只管业务逻辑和数据处理输出JSON前端只管交互和展示调用接口。代码里不会混着HTML标签和Java代码审查代码时一眼就能定位问题在哪一层。第二并行开发效率高。接口先定好契约两边同时开工。我这边用Swagger把接口文档生成了前端照着文档Mock数据就能开发界面不用等后端写完。第三部署灵活。前端打包成纯静态文件扔Nginx里后端是一个JAR包可以打在多台服务器后面做负载均衡静态资源也能扔CDN。一旦以后要扩容比单体方便太多。第四排查问题快。线上出Bug了先看后端接口返回什么再定位前端哪里报错。前后端日志分开看责任明确不会再出现“页面白屏不知道是后端挂了还是前端崩了”的尴尬。1.3 技术选型复盘SpringBootVueMyBatisMySQL的理由这套选型可能看起来“没有惊喜”但做人事系统稳定性比炫技重要得多。我详细对比过几组方案最终还是落了这套SpringBoot省掉了大量XML配置内嵌Tomcat一个JAR就能跑。相比SSH那一套老古董开发效率高一个量级。Vue这几年前端的主流框架生态成熟Element Plus里现成的表格、表单、树组件做后台管理类页面效率极高。MyBatis有人可能会问为什么不用JPA。人事系统的查询太灵活了员工列表要按部门、按入职时间、按姓名模糊查工资条要按月份统计考勤要按天聚合MyBatis写原生SQL最直观也最容易调优。JPA管简单CRUD方便但一涉及复杂查询要么看它自动生成的烂SQL干瞪眼要么花大把时间写JPQL。MySQL这个不用多解释免费、稳定、公司里最不缺会MySQL的人。人事系统每秒几十个请求顶天了MySQL绰绰有余。这套组合唯一的缺点就是没有技术新鲜感但项目落地讲究的是稳定和好维护。团队里新来了成员SpringBoot和Vue随手能上手比上一个没人会的冷门框架强百倍。2. 后端核心实现SpringBoot工程从骨架到登录鉴权2.1 工程结构与依赖的版本陷阱后端工程我用标准的Maven模块管理实际上单模块就够了因为系统不大。核心结构调整为标准的Controller-Service-Mapper三层com.company.hrm ├── controller // 接收请求返回ResultVO ├── service // 业务逻辑事务边界 ├── mapper // MyBatis接口 ├── entity // 数据库实体 ├── dto // 前端交互的数据载体避免直接暴露实体 ├── vo // 视图对象比如登录返回的Token信息 ├── config // 拦截器、Web配置、CORS配置 ├── common // 统一返回、异常、工具类 └── HrmApplication.java说下依赖版本这个坑我刚开始创建工程时顺手选了SpringBoot 3.0结果一堆老依赖不兼容javax.servlet变成了jakarta.servletMyBatis-Spring-Boot-Starter当时对SpringBoot 3的支持还不稳定折腾了半小时直接放弃。最后锁定SpringBoot 2.7.x为什么因为2.7支持Java 8绝大多数公司的生产环境还是Java 8而且所有第三方组件的兼容方案网上都有踩坑成本最低。核心依赖我只需要这么几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency这里有个容易踩的坑MySQL驱动坐标。老版本是mysql-connector-javaMySQL 8.0之后官方改成了mysql-connector-j依赖名称改了但很多人还在按老教程写导致依赖下载不到。另外如果用的MySQL 8.x驱动类要写com.mysql.cj.jdbc.Driver老驱动类com.mysql.jdbc.Driver只是兼容保留建议直接用新的。2.2 数据库连接与MyBatis整合细节application.yml里最关键的几项配置直接贴出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hrm_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.company.hrm.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl数据库URL这里面的每一项参数都有说法。useSSLfalse是因为MySQL 8默认开了SSL本地开发没配证书不关掉会报SSL连接错误。serverTimezoneAsia/Shanghai也是必须的不然插入时间字段时数据库时区和JVM时区不一致日期会差8个小时。allowPublicKeyRetrievaltrue是配合MySQL 8的加密认证用的不加的话连接时会报Public Key Retrieval is not allowed。map-underscore-to-camel-case这个配置一定加上它能把数据库里的dept_id自动映射成Java类里的deptId少写一堆Results注解。MyBatis的XML文件放在resources/mapper目录下和Mapper接口包名对应上就行。2.3 JWT登录鉴权的完整链路人事系统里不是谁都能看薪资数据的权限控制是刚需。我用的是最主流的JWT方案具体链路分成四步。第一步用户登录时调用login接口把用户名和经过MD5加盐处理的密码传给后端。MD5现在安全强度不够我在代码里又加了一层随机盐每个用户的盐不同就算数据库泄露了彩虹表也撞不出来。比对成功后生成Token。第二步生成Token时把用户ID、用户名、角色ID封装进去再设置过期时间我这边设置的是24小时。参考代码逻辑public String generateToken(User user) { Date now new Date(); Date expireDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(roleId, user.getRoleId()) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }第三步写一个拦截器HandlerInterceptor统一校验Token。拦截器里从请求头的Authorization字段取出Token解析校验通过就把用户信息放进ThreadLocal方便后续业务代码随时拿当前登录用户。校验失败直接抛401异常交给全局异常处理器返回统一的JSON。第四步权限控制用角色字段判断。比如员工的roleId2部门经理roleId1管理员roleId0。在Controller上用自定义注解标记需要的角色拦截器里校验。这种做法比Spring Security轻量很多人事系统内部的权限粒度到角色级就够了没必要把Security那全家桶搬过来。2.4 考勤模块与工资条模块的接口设计思路整个系统里考勤模块最容易写乱因为牵扯到打卡、统计、异常数据处理。我的设计思路是打卡记录一个表考勤汇总一个表汇总用定时任务生成。打卡表记录的就是原始数据员工ID、打卡时间。每天早上8点、下午6点各打一次卡。我写了两个接口checkIn和checkOut打卡时判断时间是否迟到早退把状态标记上。考勤汇总表按月存储员工ID、月份、应出勤天数、实际出勤、迟到次数、早退次数、旷工天数。月底用定时任务生成。一开始我想实时联表查后来发现月底所有人都在拉考勤报表MySQL瞬间就慢了改成提前汇总后查询就是单表扫描扫百万级都是百毫秒内的事。工资条模块的设计更考究薪资数据属于敏感数据我做了两个层面的设计。对外接口不返回全部字段而是拆成基础工资、绩效、扣款、实发几个DTO对内权限严格限制查询薪资接口只允许员工查看自己的记录管理员可以按部门筛选。3. 数据库设计实战人事系统的表结构与SQL要点3.1 六张核心表的关系设计人事系统的数据库有六张核心表关系不复杂但字段设计上有一些细节值得展开。表名作用核心字段关联关系sys_user登录用户id, username, password, salt, role_id, status与员工表一对一department部门id, parent_id, name, leader_id自关联成树employee员工档案id, user_id, dept_id, name, sex, phone, hire_date, salary_id多对一部门attendance考勤记录id, employee_id, work_date, check_in_time, check_out_time, status多对一员工salary薪资id, employee_id, base_salary, performance, bonus, deductions, pay_date多对一员工leave_record请假单id, employee_id, type, start_date, end_date, reason, apply_status多对一员工这里要注意一点sys_user和employee我做了分离而不是合成一张表。原因是登录账号和员工档案的字段关注点完全不同账号关注密码、角色、状态档案关注姓名、电话、入职日期。合在一起会让表变得臃肿而且“一个员工有多个角色”的扩展性也没了。3.2 部门树与员工档案表的关键字段部门表最核心的设计点是parent_id自关联。建表DDL关键字段如下CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT NULL, name VARCHAR(50) NOT NULL, leader_id BIGINT DEFAULT NULL, sort_order INT DEFAULT 0, KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;parent_id为NULL表示根节点。查询整棵部门树有两种方式第一种是递归查第二种是后端先查所有的部门记录然后在Java内存里组装成树。数据量小我用了第二种一次SQL搞定配合Map按ID索引组装效率高、代码也不复杂。员工档案表里有几个字段设计上要特别说明。hire_date用DATE类型而不是VARCHAR方便后面做入职周年统计。phone用VARCHAR(20)而不是BIGINT因为手机号可能存在前导零的情况虽然现在少而且BIGINT在Java里对应LongJSON序列化时容易丢精度这个坑我踩过一次。所有涉及金额的字段我在salary表里统一用DECIMAL(10,2)千万别用FLOAT或DOUBLE存钱浮点数会有精度误差算工资差一分钱都麻烦。3.3 考勤统计的SQL优化思路月底生成考勤汇总表SQL是最容易慢的环节。我最初的写法是SELECT employee_id, COUNT(*) FROM attendance WHERE work_date BETWEEN ? AND ? GROUP BY employee_id;这种写法对attendance表是几十万级的全表扫描如果再加几个条件性能会非常难看。做完索引之后效果立竿见影ALTER TABLE attendance ADD INDEX idx_emp_date (employee_id, work_date);联合索引的列顺序很关键employee_id在前work_date在后这是因为查询条件基本都是“某个员工某个月”的定位。再配合汇总表月底跑一次批量INSERT整个系统的查询压力会小很多。还有个细节统计“应出勤天数”时不能只数工作日要排除法定节假日。我在系统里维护一张holiday表把每年的法定假日和调休存进去统计时用NOT IN排除。虽然这张表要人工维护但比起接入第三方节假日API稳定性更高、不受网络影响。4. Vue前端从零跑通环境、路由、状态管理与接口封装4.1 环境安装与脚手架搭建要点前端部分我用了Vue 3 Vite Element Plus Pinia。为什么不用Vue 2说实话Vue 3的Composition API写起来逻辑复用性太好了同样的代码量比Options API清晰很多。Vite比Webpack快得不是一点半点开发环境秒级热更新。环境安装注意几个点。Node.js版本不能太低vite 4要求Node 16以上我用的Node 18。Vue脚手架创建命令npm create vitelatest hr-frontend -- --template vue创建完先装Element Plusnpm install element-plus npm install vue-router4 pinia axios这里有一个我踩过的坑Element Plus的图标需要单独安装element-plus/icons-vue不然组件里el-icon全部空白。还有就是Vite默认配置对Vue 3的支持没问题但加载Element Plus组件时建议用完整引入别看按需引入文档说得天花乱坠真正写项目你会发现按需引入的插件配置出问题时的排查成本远高于那点打包体积差。4.2 vue-router动态路由与权限控制的配合人事系统里不同角色的用户登录后看到的菜单是不一样的。管理员看到员工管理和薪资管理普通员工只看得到自己的考勤和工资条部门经理多一个部门考勤汇总。这个需求用动态路由实现最标准。思路是路由表分成两部分一部分是常驻路由登录页、404页另一部分是动态路由表比如需要权限的页面在路由配置的meta里标记roles数组。用户登录后根据角色ID过滤出他能访问的路由用router.addRoute动态添加。// 路由守卫中的核心处理逻辑 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) return next() return next(/login) } // 已登录但还没动态挂载路由 if (!store.permissionRoutesLoaded) { store.generateRoutes().then(accessRoutes { accessRoutes.forEach(route router.addRoute(route)) next({ ...to, replace: true }) // 重新进入目标路由 }) } else { next() } })动态路由做完之后菜单也要跟着路由走。我在侧边栏组件里遍历Vue Router的options.routes根据当前用户角色过滤meta里的权限信息渲染菜单。这样菜单和路由是一套数据源不会出现“路由有页面但菜单点不到”的问题。4.3 Axios封装与Token刷新Vue项目里我习惯把Axios封装成request.js工具统一处理三件事请求拦截器、响应拦截器、错误提示。代码不长但每一段都有用const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器携带Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers[Authorization] token return config }) // 响应拦截器统一处理业务码和HTTP异常 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response?.status 401) { localStorage.clear() window.location.href /login } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } )响应拦截器里的401处理特别重要后端我拦截器鉴权失败返回401前端收到这个状态码就清空本地存储、跳回登录页这是整个权限闭环的最后一环。Token刷新机制我在这个项目里没加因为只有24小时过期时间但如果上线后你们想防Token泄漏可以做refresh token双Token方案思路也很简单后端再发一个长期有效的refresh tokenAccessToken过期后用refresh token换新的。4.4 Element Plus表单校验实战人事系统表单多员工信息、请假申请、薪资录入全是表单。Element Plus自带表单校验但实际开发中有两个容易被忽略的点。第一表单校验规则要和后端校验对齐。比如手机号正则前端校验通过了后端还得再校验一次我在后端用了Hibernate Validator的Pattern注解规则保证两个端一致。第二日期范围校验。请假模块里结束日期必须大于开始日期这个用Element Plus的validator自定义函数实现const validateEndDate (rule, value, callback) { if (!value) return callback(new Error(请选择结束日期)) if (value form.startDate) return callback(new Error(结束日期不能早于开始日期)) callback() }这里的value是Date对象直接用比较运算符就能判断其实不用额外写函数用datePicker的pickerOptions.disabledDate就能禁用非法日期我两个方案都试过个人觉得disabledDate体验更好用户根本没法选错日期。5. 完整部署流程从本地联调到Linux服务器上线5.1 后端Maven打包与JAR启动部署前先说一个被反复问到的问题本地好好的打包生产就起不来。大部分原因是application.yml里的配置没有区分环境。我用SpringBoot的Profile机制开发环境用application-dev.yml生产用application-prod.yml启动时显式指定mvn clean package -DskipTests java -jar target/hrm-1.0.0.jar --spring.profiles.activeprod --server.port8080生产环境的application-prod.yml里数据源配置换成服务器上的MySQL地址。打包前记得检查一件事pom.xml里有没有漏配置maven-compiler-plugin的编译级别我习惯指定Java 8避免服务器上的JDK版本和本机不一致。如果内存紧张启动命令可以加JVM参数限制java -Xms256m -Xmx512m -jar hrm-1.0.0.jar --spring.profiles.activeprod --server.port8080服务器上我用systemd管理JAR进程写一个hrm.service文件这样服务崩了能自动重启开机也能自启比screens和nohup靠谱得多[Unit] DescriptionHRM Application Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/data/hrm ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /data/hrm/hrm-1.0.0.jar --spring.profiles.activeprod Restartalways RestartSec3 [Install] WantedBymulti-user.target5.2 前端打包与Nginx静态托管前端打包很简单npm run build打包产物在dist目录。把这个目录上传到服务器的/data/www/hrm-web下Nginx配置如下server { listen 80; server_name 你的域名或IP; # 前端静态资源 root /data/www/hrm-web; index index.html; # 解决Vue History模式刷新404 location / { try_files $uri $uri/ /index.html; } # 接口反向代理到后端JAR 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; } # 打包后的JS/CSS带hash可以设强缓存 location /assets/ { expires 30d; add_header Cache-Control public, immutable; } }这里有两个关键点。第一try_files $uri $uri/ /index.html;是必须的Vue Router用了History模式后刷新非根路径时会404这句话把请求全部转发回index.html从前端路由接管。第二/api代理必须加前端直接访问后端端口会有跨域问题通过Nginx同源代理转发浏览器看到的所有请求都是同域跨域问题在服务器层面直接解决了。5.3 接口代理与跨域配置开发阶段跨域是绕不开的。我比较推荐用Vite的代理配置在vite.config.js里而不是后端开CORS。原因很简单后端开CORS意味着所有来源都可以跨域访问生产上如果忘了关接口就暴露了。Vite代理只在开发环境生效生产走Nginx同源代理一条链路解决。// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }注意这个rewrite后端接口我写的路径是/api/user/login但Nginx代理到后端时后端不出意外都得按实际路径转发。我在开发环境把前端请求统一加/api前缀代理到后端时把前缀去掉生产环境Nginx的location /api代理到后端时保留/api路径。前后端约定好这个前缀规则就行我踩过一次“跨域报错但明明配置了代理”的坑最后发现是changeOrigin没设成true请求头里的Host还是localhost:5173后端判断来源直接拒了。5.4 生产环境MySQL初始化与数据备份生产环境MySQL我用的5.7版本小项目5.7完全够用而且内存占用比8.0低。安装过程核心几步第一步用MySQL官方仓库或者RPM包安装装完跑一遍mysql_secure_installation把匿名用户和test库删掉。第二步创建业务库和专用账号别用root账号跑业务CREATE DATABASE hrm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER hrm_userlocalhost IDENTIFIED BY 你自己的强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON hrm_db.* TO hrm_userlocalhost; FLUSH PRIVILEGES;第三步导入建表SQL。我项目有一个init.sql每次发布前都会检查更新用source命令导入或者mysql init.sql一次性执行。数据备份这块我用crontab每天凌晨跑一次mysqldumpmysqldump -u hrm_user -p密码 hrm_db /data/backup/hrm_$(date \%Y\%m\%d).sql find /data/backup -name *.sql -mtime 30 -delete第二行是自动清理30天前的备份防止磁盘被占满。6. 踩坑实录这个项目里最值得记住的几个坑6.1 SpringBoot版本过高导致的循环依赖问题前面提到我一开始用SpringBoot 3.0不仅依赖不兼容还遇到循环依赖的报错。SpringBoot 2.6开始官方默认禁止了Bean之间的循环依赖如果你代码里有A依赖B、B又依赖A的情况项目启动直接报The dependencies of some of the beans in the application context form a cycle我当时的代码里Service互相调用比较多比如UserService调用SalaryServiceSalaryService又回头调用UserService。这个报错本质是设计问题最佳解决方式是重构把互相依赖的逻辑抽到第三层Service。但如果你赶工期可以在application.yml里临时开回来spring: main: allow-circular-references: true不过我不推荐这个做法循环依赖是技术上的一种坏味道它能跑但迟早成为重构的阻碍。后来我还是把业务分层重新梳理了Controller只调ServiceService之间通过事件或者第三层Service协作启动速度也变快了。6.2 MyBatis XML中的特殊字符转义这个坑每次写MyBatis都会遇到。比如查工资大于5000的员工select idgetHighSalaryEmployees resultTypeEmployee SELECT * FROM employee WHERE salary 5000 /select看到没XML里的符号单独出现没问题但如果你写salary 5000有些解析器会报错更常见的是写age 30这种小于号时直接XML解析失败因为在XML里是标签的开始标志。正确做法是用转义字符或者![CDATA[]]select idgetHighSalaryEmployees resultTypeEmployee SELECT * FROM employee WHERE salary ![CDATA[ ]] 5000 /select我后来写SQL统一用![CDATA[]]包裹所有带特殊符号的比较条件虽然丑了点但再没出过解析问题。6.3 SQL连接SSL报错与时区问题MySQL 8之后连接URL不加参数控制台会刷两个警告SSL连接警告和时区警告。SSL警告就是前面说的useSSLfalse能解决但如果你连接的是云数据库且有强制SSL要求就得反过来配置useSSLtrue并指定证书路径这个看具体环境。时区问题如果你不设置MySQL 8默认用的是服务器的系统时区而Java的serverTimezone没有显式指定时日期查询会差8个小时。表现形式很隐蔽员工录入的入职日期是2024-06-01页面显示确实没问题但如果你用BETWEEN按月份筛选最后一天的记录会漏掉因为数据库存储的其实是2024-06-01 16:00东八区和UTC相差8小时。这种Bug查起来真的是怀疑人生我是在做工资按月统计时发现6月份的数据比查SQL少了几条才定位到时区问题。结论就是URL上必须写serverTimezoneAsia/Shanghai另外一个备选是数据库连接时把JDBC驱动参数useTimezonetrueserverTimezoneAsia/Shanghai也加上。6.4 前端打包后刷新404与接口404的区分上线后有两个“404”特别容易混淆处理方式完全不同。第一个是页面刷新404。用户访问http://域名/employee/list刷新一下浏览器报404但F12看接口其实是有响应的。这基本就是Nginx没配try_filesVue Router的History模式需要服务器把所有非静态文件路径都指向index.html。配置方法前面写了这个要确认一次配对了。第二个是接口404。前端页面能打开但列表数据加载不出来F12看到请求返回404。这时候就要检查前端请求的路径和后端Controller的RequestMapping是否完全一致。我有一次后端写的是/salary/list前端调的是/api/salary/listNginx代理又把/api前缀去掉了结果请求变成/salary/list接口路径没对上404得很冤枉。排查这类问题的标准动作是先在浏览器直接访问后端地址http://IP:8080/实际路径验证后端接口再通过Nginx的域名访问一次对比两次结果就知道是哪层出了问题。6.5 MyBatis一二级缓存注意别踩坑这个坑说起来跟人事系统特有场景相关。MyBatis默认开启一级缓存SqlSession级别默认不开启二级缓存。一开始我对员工查询接口开启了二级缓存结果出现权限数据串味的问题管理员A查询的员工列表被缓存了普通员工B查同一接口按道理只能看到自己部门的人结果直接命中缓存看到了别人的数据。这个问题的根因是MyBatis的二级缓存默认粒度是Mapper命名空间它不知道你的SQL里带了动态的条件比如按部门过滤和权限信息。我的解决方案简单粗暴所有涉及权限过滤的查询接口明确关闭二级缓存在Mapper XML里加cache-ref namespace.../或者不用cache/配置避免二次缓存生效。对于这个体量的系统一级缓存已经够用了二级缓存带来的性能提升有限却引入数据错乱的风险不值得。写在最后的个人体会前后端分离的人事系统做到最后最深的体会是真正耗时间的不是写代码而是联调、排查环境和版本问题。SpringBootVueMyBatisMySQL这套技术栈看着“老套”但它组合起来非常牢靠社区资料也多遇到问题搜一下基本都有答案。如果你也要做一个类似的管理系统我建议从数据库设计和权限模型入手这两块想清楚了剩下的增删改查其实很机械。最后分享一个小技巧项目里我单独写了一个init.sql包含所有建表语句和初始数据管理员账号、演示部门等新环境部署时一条命令就能把数据库准备到位。我每次重装服务器从安装依赖到系统跑起来不超过半小时。把部署流程脚本化这件事越早做越香。
阅读完成 · 觉得有帮助?