news 2026/10/5 13:35:06

SpringBoot+Vue前后端分离人事系统实战:从零搭建到Nginx部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue前后端分离人事系统实战:从零搭建到Nginx部署

前后端分离这个词,做后端的人几乎每天都能听到,但真正从零把一个完整的人事系统搭起来并部署上线,和看教程、写Demo完全是两回事。人事系统看起来不就是增删改查嘛,但部门层级、员工档案、考勤统计、薪资计算、权限分配这些东西叠加在一起,复杂度马上就上来了。我这套系统用SpringBoot+Vue+MyBatis+MySQL把前后端完全拆开,源码、部署流程我都完整走了一遍,今天这篇就当是给没做过完整项目的人一份可直接抄作业的实战笔记。

先说清楚这套东西适合谁看:在校学生做毕业设计、刚工作一两年的后端想补全前端知识、或者公司内部需要一个能跑起来的人事管理后台。前端我只用了Vue 3 + Element Plus,后端就是SpringBoot 2.7 + MyBatis + MySQL,没有引入任何花哨的组件和中间件,门槛不高,但覆盖了一个真实系统该有的完整链路:登录鉴权、动态路由、部门树、员工资料管理、考勤打卡、薪资条、以及生产环境的Nginx部署。说实话,把这些串起来跑通了,比刷一百道面试题都管用。

1. 人事系统前后端分离架构:为什么我坚持这么拆

1.1 单体也能做,但改动成本越来越高

先别急着谈架构,很多人会问:一个几百人规模的人事系统,JSP+SpringMVC单体应用不也能做?能,我早期就用单体写过类似系统,但做人事系统最大的痛点不是并发,而是需求改得勤。今天加一个加班补贴字段,明天改一下考勤规则,后天又要在工资条里塞一个绩效列。单体应用下前端页面和后端接口全耦合在一起,改一个字段往往要同时动Controller、Service、Mapper、JSP,稍不注意就把页面改崩了。

前后端分离之后,后端只需要把接口稳定下来,前端页面随便改,后端一点不用动。我现在的开发节奏是:前端同学调接口联调,后端继续加新接口,互不阻塞。这个体验一旦习惯了,是真的回不去。

1.2 前后端分离的四个关键收益

做这个人事系统我复盘了一下,前后端分离带来的好处可以落到四个具体点上:

第一,职责边界清楚。后端只管业务逻辑和数据处理,输出JSON;前端只管交互和展示,调用接口。代码里不会混着HTML标签和Java代码,审查代码时一眼就能定位问题在哪一层。

第二,并行开发效率高。接口先定好契约,两边同时开工。我这边用Swagger把接口文档生成了,前端照着文档Mock数据就能开发界面,不用等后端写完。

第三,部署灵活。前端打包成纯静态文件扔Nginx里,后端是一个JAR包,可以打在多台服务器后面做负载均衡,静态资源也能扔CDN。一旦以后要扩容,比单体方便太多。

第四,排查问题快。线上出Bug了,先看后端接口返回什么,再定位前端哪里报错。前后端日志分开看,责任明确,不会再出现“页面白屏不知道是后端挂了还是前端崩了”的尴尬。

1.3 技术选型复盘:SpringBoot+Vue+MyBatis+MySQL的理由

这套选型可能看起来“没有惊喜”,但做人事系统,稳定性比炫技重要得多。我详细对比过几组方案,最终还是落了这套:

  • 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.servlet,MyBatis-Spring-Boot-Starter当时对SpringBoot 3的支持还不稳定,折腾了半小时直接放弃。最后锁定SpringBoot 2.7.x,为什么?因为2.7支持Java 8,绝大多数公司的生产环境还是Java 8,而且所有第三方组件的兼容方案网上都有,踩坑成本最低。

核心依赖我只需要这么几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

这里有个容易踩的坑:MySQL驱动坐标。老版本是mysql-connector-java,MySQL 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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这里面的每一项参数都有说法。useSSL=false是因为MySQL 8默认开了SSL,本地开发没配证书,不关掉会报SSL连接错误。serverTimezone=Asia/Shanghai也是必须的,不然插入时间字段时,数据库时区和JVM时区不一致,日期会差8个小时。allowPublicKeyRetrieval=true是配合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。

第四步,权限控制用角色字段判断。比如员工的roleId=2,部门经理roleId=1,管理员roleId=0。在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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

parent_id为NULL表示根节点。查询整棵部门树有两种方式,第一种是递归查,第二种是后端先查所有的部门记录,然后在Java内存里组装成树。数据量小,我用了第二种,一次SQL搞定,配合Map按ID索引组装,效率高、代码也不复杂。

员工档案表里有几个字段设计上要特别说明。hire_date用DATE类型而不是VARCHAR,方便后面做入职周年统计。phone用VARCHAR(20)而不是BIGINT,因为手机号可能存在前导零的情况(虽然现在少),而且BIGINT在Java里对应Long,JSON序列化时容易丢精度,这个坑我踩过一次。所有涉及金额的字段,我在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 vite@latest hr-frontend -- --template vue

创建完先装Element Plus:

npm install element-plus npm install vue-router@4 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 token,AccessToken过期后用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.active=prod --server.port=8080

生产环境的application-prod.yml里数据源配置换成服务器上的MySQL地址。打包前记得检查一件事:pom.xml里有没有漏配置maven-compiler-plugin的编译级别,我习惯指定Java 8,避免服务器上的JDK版本和本机不一致。

如果内存紧张,启动命令可以加JVM参数限制:

java -Xms256m -Xmx512m -jar hrm-1.0.0.jar --spring.profiles.active=prod --server.port=8080

服务器上我用systemd管理JAR进程,写一个hrm.service文件,这样服务崩了能自动重启,开机也能自启,比screens和nohup靠谱得多:

[Unit] Description=HRM Application After=network.target [Service] Type=simple User=root WorkingDirectory=/data/hrm ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /data/hrm/hrm-1.0.0.jar --spring.profiles.active=prod Restart=always RestartSec=3 [Install] WantedBy=multi-user.target

5.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_user'@'localhost' IDENTIFIED BY '你自己的强密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON hrm_db.* TO 'hrm_user'@'localhost'; FLUSH PRIVILEGES;

第三步,导入建表SQL。我项目有一个init.sql,每次发布前都会检查更新,用source命令导入或者mysql < init.sql一次性执行。

数据备份这块,我用crontab每天凌晨跑一次mysqldump:

mysqldump -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调用SalaryService,SalaryService又回头调用UserService。这个报错本质是设计问题,最佳解决方式是重构,把互相依赖的逻辑抽到第三层Service。但如果你赶工期,可以在application.yml里临时开回来:

spring: main: allow-circular-references: true

不过我不推荐这个做法,循环依赖是技术上的一种坏味道,它能跑但迟早成为重构的阻碍。后来我还是把业务分层重新梳理了,Controller只调Service,Service之间通过事件或者第三层Service协作,启动速度也变快了。

6.2 MyBatis XML中的特殊字符转义

这个坑每次写MyBatis都会遇到。比如查工资大于5000的员工:

<select id="getHighSalaryEmployees" resultType="Employee"> SELECT * FROM employee WHERE salary > 5000 </select>

看到没?XML里的>符号单独出现没问题,但如果你写salary > 5000,有些解析器会报错,更常见的是写age < 30这种小于号时直接XML解析失败,因为<在XML里是标签的开始标志。正确做法是用转义字符或者<![CDATA[]]>:

<select id="getHighSalaryEmployees" resultType="Employee"> SELECT * FROM employee WHERE salary <![CDATA[ > ]]> 5000 </select>

我后来写SQL统一用<![CDATA[]]>包裹所有带特殊符号的比较条件,虽然丑了点,但再没出过解析问题。

6.3 SQL连接SSL报错与时区问题

MySQL 8之后连接URL不加参数,控制台会刷两个警告:SSL连接警告和时区警告。SSL警告就是前面说的useSSL=false能解决,但如果你连接的是云数据库且有强制SSL要求,就得反过来配置useSSL=true并指定证书路径,这个看具体环境。时区问题如果你不设置,MySQL 8默认用的是服务器的系统时区,而Java的serverTimezone没有显式指定时,日期查询会差8个小时。

表现形式很隐蔽:员工录入的入职日期是2024-06-01,页面显示确实没问题,但如果你用BETWEEN按月份筛选,最后一天的记录会漏掉,因为数据库存储的其实是2024-06-01 16:00(东八区和UTC相差8小时)。这种Bug查起来真的是怀疑人生,我是在做工资按月统计时发现6月份的数据比查SQL少了几条,才定位到时区问题。结论就是URL上必须写serverTimezone=Asia/Shanghai,另外一个备选是数据库连接时把JDBC驱动参数useTimezone=true&serverTimezone=Asia/Shanghai也加上。

6.4 前端打包后刷新404与接口404的区分

上线后有两个“404”特别容易混淆,处理方式完全不同。

第一个是页面刷新404。用户访问http://域名/employee/list,刷新一下浏览器报404,但F12看接口其实是有响应的。这基本就是Nginx没配try_files,Vue Router的History模式需要服务器把所有非静态文件路径都指向index.html。配置方法前面写了,这个要确认一次配对了。

第二个是接口404。前端页面能打开,但列表数据加载不出来,F12看到请求返回404。这时候就要检查前端请求的路径和后端Controller的@RequestMapping是否完全一致。我有一次后端写的是/salary/list,前端调的是/api/salary/list,Nginx代理又把/api前缀去掉了,结果请求变成/salary/list,接口路径没对上,404得很冤枉。排查这类问题的标准动作是:先在浏览器直接访问后端地址http://IP:8080/实际路径验证后端接口,再通过Nginx的域名访问一次,对比两次结果就知道是哪层出了问题。

6.5 MyBatis一二级缓存注意别踩坑

这个坑说起来跟人事系统特有场景相关。MyBatis默认开启一级缓存(SqlSession级别),默认不开启二级缓存。一开始我对员工查询接口开启了二级缓存,结果出现权限数据串味的问题:管理员A查询的员工列表被缓存了,普通员工B查同一接口,按道理只能看到自己部门的人,结果直接命中缓存,看到了别人的数据。

这个问题的根因是MyBatis的二级缓存默认粒度是Mapper命名空间,它不知道你的SQL里带了动态的条件(比如按部门过滤)和权限信息。我的解决方案简单粗暴:所有涉及权限过滤的查询接口明确关闭二级缓存,在Mapper XML里加:

<cache-ref namespace="..."/>

或者不用<cache/>配置,避免二次缓存生效。对于这个体量的系统,一级缓存已经够用了,二级缓存带来的性能提升有限,却引入数据错乱的风险,不值得。

写在最后的个人体会

前后端分离的人事系统做到最后,最深的体会是:真正耗时间的不是写代码,而是联调、排查环境和版本问题。SpringBoot+Vue+MyBatis+MySQL这套技术栈看着“老套”,但它组合起来非常牢靠,社区资料也多,遇到问题搜一下基本都有答案。如果你也要做一个类似的管理系统,我建议从数据库设计和权限模型入手,这两块想清楚了,剩下的增删改查其实很机械。

最后分享一个小技巧:项目里我单独写了一个init.sql,包含所有建表语句和初始数据(管理员账号、演示部门等),新环境部署时一条命令就能把数据库准备到位。我每次重装服务器,从安装依赖到系统跑起来不超过半小时。把部署流程脚本化这件事,越早做越香。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 13:34:13

Java解析Micaps Diamond4站点气象数据并批量入库MySQL实践

最近在处理气象预报数据的接入&#xff0c;系列文章写到第四篇&#xff0c;这次的主角是 Micaps 第4类数据&#xff0c;也就是常说的 Diamond4。做气象数据开发的同学对 Micaps 应该不陌生&#xff0c;它是一套在气象业务里用得非常多的人机交互系统&#xff0c;定义了多种文本…

作者头像 李华
网站建设 2026/10/5 13:32:42

Git查看未提交修改全攻略:从status到diff再到误删恢复

git status 这个命令&#xff0c;大概是每个用 Git 的人最早学会的三个命令之一&#xff1a;clone、commit、status。但用归用&#xff0c;真到“我到底改了什么、还有多少没提交”这种问题时&#xff0c;很多人还是会在终端里卡住——不是命令不会敲&#xff0c;而是不知道 Gi…

作者头像 李华
网站建设 2026/10/5 13:31:35

Redis核心技术与实战:缓存原理、数据类型与高并发治理

1. 缓存到底是什么&#xff1a;先搞清楚Redis为什么值得学1.1 一个让数据库"喘口气"的中间层很多刚开始接触后端开发的朋友都会遇到同一个困惑&#xff1a;明明数据库已经能存数据了&#xff0c;为什么还要在它前面再塞一个Redis&#xff1f;直接查MySQL不行吗&#…

作者头像 李华
网站建设 2026/10/5 13:27:52

OpenClaw多Agent编排实战:从单Agent到“龙虾大军”的全配置指南

OpenClaw 这个名字&#xff0c;最近在技术社区里出现的频率相当高——它是一个把大模型能力搬进终端的 Agent 工具&#xff0c;图标是一只举着钳子的龙虾。很多人装好之后&#xff0c;跑一个会话发现好像也就那样&#xff1a;让一个 Agent 从头到尾干完一件事&#xff0c;经常干…

作者头像 李华
网站建设 2026/10/5 13:27:51

SpringBoot+Vue3前后端分离图书管理系统实战解析

图书管理系统大概是Java开发圈子里最常见的实战项目了——校园毕设、培训机构作业、新人练手&#xff0c;到处都能看到它的身影。但很多项目还停留在JSPServlet或者SpringBootThymeleaf的旧模式&#xff0c;前后端耦合在一起&#xff0c;改个页面都要重启服务。今天要聊的这套源…

作者头像 李华
网站建设 2026/10/5 13:27:12

优启通3.7制作PE启动U盘与系统维护实战指南

做系统维护这些年&#xff0c;手里没几个趁手的PE工具真不行。最近一直在用优启通3.7&#xff08;2025修改版&#xff09;&#xff0c;趁着12月这版更新&#xff0c;把这段时间的实测体验和踩坑记录整理一下。这篇文章不聊虚的&#xff0c;主要讲清楚优启通3.7到底是什么、它比…

作者头像 李华