ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue人事管理系统:前后端分离项目从搭建到部署全攻略

SpringBoot+Vue人事管理系统:前后端分离项目从搭建到部署全攻略 简介基于SpringBoot与Vue技术栈开发的企业人事管理系统源码包面向计算机相关专业毕业设计、课程设计及期末大作业场景是学习前后端分离开发模式的完整范例。项目覆盖员工信息、薪资、考勤、职位、招聘等核心业务模块同时具备权限控制、数据备份恢复、系统日志等辅助能力贴合企业实际人事管理需求。资源为zip压缩包共244个文件、总大小32.65MB其中78个java文件构成后端主体14个xml与2个yml文件负责框架配置1个sql文件提供数据库初始化脚本30个log文件便于运行日志分析与排错大量jpg、jpeg、png图片用于界面展示或文档配图并附README.md与.gitignore目录结构清晰、便于检索。已有28人学习浏览。下载后既能研读SpringBoot RESTful接口设计与Vue前端交互实现也可直接二次开发、功能扩展或作为毕业设计与期末大作业的项目基础。1. SpringBoot Vue 人事管理系统从下载到跑通需要哪四步SpringBoot 写接口、Vue 写页面前后端分离这套组合在真实项目里到处都是看上去没什么稀奇。但我拆过不少打着“企业人事管理系统”名义的压缩包能真正解压、按文档装好环境、一次跑通的其实不到一半——大部分都死在版本不匹配、数据库脚本缺字段、前端依赖装不上这些很基础的问题上。这个压缩包就是一套标准的 SpringBoot Vue 前后端分离项目后端负责员工、部门、考勤、薪资这些模块的接口前端负责页面展示和交互MySQL 脚本建库建表整套代码可以直接作为毕设或课程设计的底子。它适合三类人。第一类是拿它当毕业设计底子的学生需要快速理解业务流程并把项目跑起来第二类是刚入门想练前后端联调的全栈新人想看看 Axios、路由守卫、拦截器在实际项目里怎么落地第三类是把源码当脚手架改业务的人。要把它跑通普遍就四步检查环境版本、导入并初始化数据库、启动 SpringBoot 后端、安装依赖并启动 Vue 前端。后面我按这个顺序拆每一步都会讲到哪个环节容易翻车。2. 工程骨架与数据库脚本SpringBoot 目录、Vue 目录和建表 SQL 怎么对应拿到 zip 包的第一个动作不是急着 npm install而是先看工程结构和版本文件。这一步能帮你避免一半以上的环境问题。2.1 前后端分离的工程目录先分清哪些文件能删、哪些文件不能丢解压后通常能看到两个独立工程一个放后端 Java 代码一个放前端 Vue 代码目录大致是这样personnel-system/ ├── backend/ # SpringBoot 后端工程 │ ├── src/main/java/com/personnel/ │ │ ├── controller/ # 接口层只做参数接收和结果返回 │ │ ├── service/ # 业务层事务、校验、业务规则都在这 │ │ ├── mapper/ # MyBatis-Plus 的 Mapper 接口 │ │ ├── entity/ # 对应数据库表的实体类 │ │ ├── config/ # 跨域、拦截器、MyBatis-Plus 配置 │ │ └── common/ # 统一返回体、全局异常、JWT 工具 │ ├── src/main/resources/ │ │ ├── application.yml # 数据源、端口、JWT 密钥都在这 │ │ └── mapper/*.xml # 复杂 SQL 放在 XML 里 │ └── pom.xml # 依赖版本先看这个文件 └── frontend/ # Vue 前端工程 ├── src/ │ ├── api/ # 每个模块一个接口文件统一封装请求 │ ├── router/ # 路由表静态路由和动态路由入口 │ ├── views/ # 页面组件员工列表、考勤、薪资 │ ├── store/ # Vuex存用户信息和 token │ └── utils/request.js # Axios 实例拦截器都在这 ├── package.json # 前端依赖清单node-sass 还是 sass 看这里 └── vite.config.js # 本地开发代理转发到后端端口这个结构就是最常见的 SpringBoot 三层架构加 Vue 标准目录controller 只做调度service 管业务mapper 管数据库。拿到项目先确认两件事第一pom.xml 里 spring-boot-starter-parent 的版本和你本地 JDK 是否匹配第二package.json 里依赖是否完整、是否包含 node-sass 这种高版本 Node 的坑。源码的 src 目录是核心缺失了没法补node_modules 丢了无所谓npm install 能装回来这也是“vue 项目源码怎么发给别人”时要特别提醒的——只发 src 和配置文件就够了压缩包要排除 node_modules。2.2 建表 SQL 与字段边界员工、部门、考勤、薪资四张核心表的设计这套系统的核心数据模型围绕部门、员工、考勤、薪资四张表展开建表脚本的写法直接决定后端代码好不好写。以员工和考勤为例核心 SQL 大致是这样的-- 部门表 CREATE TABLE department ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(50) NOT NULL COMMENT 部门名称, parent_id bigint DEFAULT 0 COMMENT 上级部门ID0表示顶级, deleted tinyint DEFAULT 0 COMMENT 逻辑删除0正常 1删除, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门表; -- 员工表 CREATE TABLE employee ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, emp_no varchar(20) NOT NULL COMMENT 工号唯一, name varchar(30) NOT NULL COMMENT 姓名, department_id bigint DEFAULT NULL COMMENT 所属部门ID, position varchar(50) DEFAULT NULL COMMENT 岗位, phone varchar(20) DEFAULT NULL COMMENT 手机号, entry_date date DEFAULT NULL COMMENT 入职日期, status tinyint DEFAULT 1 COMMENT 在职状态1在职 0离职, deleted tinyint DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工表; -- 考勤表 CREATE TABLE attendance ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, emp_id bigint NOT NULL COMMENT 员工ID, work_date date NOT NULL COMMENT 上班日期, check_in datetime DEFAULT NULL COMMENT 上班打卡时间, check_out datetime DEFAULT NULL COMMENT 下班打卡时间, status varchar(10) DEFAULT normal COMMENT normal正常 late迟到 leave早退 absent缺勤, PRIMARY KEY (id), KEY idx_emp_date (emp_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤表; -- 薪资表 CREATE TABLE salary ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, emp_id bigint NOT NULL COMMENT 员工ID, salary_month varchar(7) NOT NULL COMMENT 薪资月份如2024-06, base_salary decimal(10,2) NOT NULL COMMENT 基本工资, bonus decimal(10,2) DEFAULT 0.00 COMMENT 奖金, deduction decimal(10,2) DEFAULT 0.00 COMMENT 扣款, final_salary decimal(10,2) DEFAULT 0.00 COMMENT 实发工资, PRIMARY KEY (id), UNIQUE KEY uk_emp_month (emp_id, salary_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT薪资表;这套设计的几个细节值得在答辩时讲清楚。主键统一用 bigint 自增没有用 UUID因为单库单表场景自增主键在索引性能和写入顺序上更优。每张表都有 deleted 字段做逻辑删除而不是物理 DELETE保留历史数据、可追溯代价是每次查询都要记得过滤 deleted0。薪资表的 salary_month 用 varchar(7) 而不是 date存“2024-06”这种格式按月统计直接 GROUP BY 这个字段就行不用再做日期格式化。金额字段用 decimal(10,2) 而不是 float因为二进制浮点数表示 0.1 不精确涉及钱的计算必须用定点数。考勤表加了 (emp_id, work_date) 联合索引因为最频繁的查询是按员工查某段时间的打卡记录。2.3 鉴权方案选型为什么用 JWTToken 存哪、过期怎么办前后端分离架构下不建议用 Session跨域、多端、集群部署都有兼容问题所以这套系统通常用 JWT 做鉴权。核心逻辑是登录成功后后端签发一个 token 返回给前端前端后续请求都带上它后端通过拦截器校验不需要在服务端存会话。JWT 工具类的写法大致是这样Component public class JwtUtil { Value(${jwt.secret}) private String secret; // 密钥上线必须换掉别用默认值 Value(${jwt.expire}) private Long expire; // 过期时间单位毫秒一般 2 小时 public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) // 主题存用户名 .claim(userId, userId) // 自定义载荷放用户ID .setIssuedAt(new Date()) // 签发时间 .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) // 对称加密签名 .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }逻辑说明setSubject 存用户名claim 里塞 userId这样后续接口不用再查一次用户表就能拿到主键。签名用的 HS256 是对称加密密钥一旦泄露 token 就能被伪造所以生产环境应该从配置文件读取而不是写死在代码里更稳妥的做法是换 RSA 非对称签名。token 过期后的处理流程也很重要。前端 Axios 拦截器收到 401 就清掉本地 token强制跳回登录页后端拦截器对 /api/login 这类白名单路径放行其他路径全部校验。这个机制第一版容易忽略的是白名单配置——漏掉验证码接口或登录接口会导致死循环跳转我在下一章讲拦截器时展开。3. SpringBoot 后端核心接口员工分页、考勤判定、薪资统计的写法后端这块是这套项目工作量最大的部分但真正核心的接口其实就三类员工管理、考勤判定、薪资统计。把这三种接口的写法吃透其他模块都是套同样的模板。3.1 统一返回体与全局异常先统一接口格式再写业务如果每个 controller 返回格式都不一样前端拦截器就得写一堆分支联调成本翻倍。所以第一步是定义一个统一返回体Data public class ResultT { private Integer code; // 200成功 401未登录 500业务错误 private String message; // 提示信息前端直接弹给用户 private T data; // 业务数据 public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(操作成功); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }逻辑说明所有接口都返回 Result 包装后的对象前端只看 code 判断成败message 直接展示给用户。配合 RestControllerAdvice 全局异常处理业务代码里抛出 RuntimeException 时会被捕获并转成 Result.error避免异常堆栈直接抛到前端。参数说明code 只用了 200/401/500 三段语义不要在业务里乱加自定义 code 段否则前后端都要跟着维护一套对照表。3.2 员工分页模糊查询Controller-Service-Mapper 三层各写多少员工管理最核心的接口是分页模糊查询带姓名和部门两个筛选条件。三层代码的分工是Controller 接收参数、Service 拼条件、Mapper 执行查询。用 MyBatis-Plus 的实现方式RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public ResultIPageEmployee page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String name, RequestParam(required false) Long departmentId) { return Result.ok(employeeService.pageQuery(pageNum, pageSize, name, departmentId)); } }Service 层Service public class EmployeeServiceImpl extends ServiceImplEmployeeMapper, Employee implements EmployeeService { Override public IPageEmployee pageQuery(Integer pageNum, Integer pageSize, String name, Long departmentId) { LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Employee::getName, name) .eq(departmentId ! null, Employee::getDepartmentId, departmentId) .eq(Employee::getDeleted, 0) .orderByDesc(Employee::getEntryDate); return this.page(new Page(pageNum, pageSize), wrapper); } }逻辑说明Controller 不做任何业务判断只负责参数接收和调用 Service。Service 里用 LambdaQueryWrapper 拼条件每个条件前面的布尔表达式表示“参数为空就不拼这个条件”比如 name 为空时 like 条件自动跳过。like 方法会自动在参数两侧拼上百分号实现模糊匹配。参数说明pageNum 从 1 开始pageSize 默认 10这两个参数前端会从 el-pagination 组件传过来。要注意的是 MyBatis-Plus 的 page 方法必须配合分页插件才生效config 配置类里要加 PaginationInnerInterceptor否则查出的是全量数据。很多新手在这是翻车的——写好了分页代码但漏了插件配置接口返回全表数据前端分页看起来就完全不对。3.3 考勤迟到判定LocalDateTime 和数据库时间字段别用错考勤模块的核心逻辑是判定打卡状态常见规则是上班时间 9:00 之后打卡算迟到没打卡算缺勤。用 LocalDateTime 写会比较清爽public String judgeStatus(LocalDateTime checkIn, String workDate) { if (checkIn null) { return absent; // 没打卡直接判缺勤 } // 上班时间 9:00精确到分钟 LocalDateTime onDutyTime LocalDateTime.parse(workDate 09:00, DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)); if (checkIn.isAfter(onDutyTime)) { return late; } return normal; }逻辑说明核心是用 LocalDateTime 的 isAfter 做比较不要拿 Date 或字符串去比字符串比较在“2024-06-17 09:00”和“2024-06-17 08:59”这种场景虽然碰巧正确但一旦时间格式变化就出错。缺勤要单独判断因为打卡时间为空时直接判迟到会漏掉“根本没来上班”的人。参数说明上班时间 9:00 是写死在方法里的实际项目建议放到配置项或班次表里这样不同部门、不同班次的规则可以复用同一个方法。DateTimeFormatter 要定义成 static final 常量不要在方法里每次都 new高频调用场景能省掉一部分对象创建开销。3.4 薪资聚合统计一条 SQL 还是内存计算薪资模块最常被问到的是“各部门月度薪资汇总”这类聚合统计用 SQL 比在 Java 里写循环高效得多SELECT d.name AS department_name, s.salary_month AS month, SUM(s.final_salary) AS total_salary, COUNT(DISTINCT s.emp_id) AS emp_count, ROUND(AVG(s.final_salary), 2) AS avg_salary FROM salary s LEFT JOIN employee e ON s.emp_id e.id LEFT JOIN department d ON e.department_id d.id WHERE s.salary_month #{month} GROUP BY d.id, d.name ORDER BY total_salary DESC;逻辑说明用 LEFT JOIN 而不是 INNER JOIN是因为离职员工可能没有部门内连接会把这部分薪资漏掉。GROUP BY 按部门分组后SUM 算部门总额AVG 算人均ROUND 保留两位小数。COUNT(DISTINCT) 要加 DISTINCT因为同一员工一个月只有一条薪资记录正常情况不需要但防止数据重复也是一种保险。参数说明salary_month 传“2024-06”这种格式与表字段设计保持一致。如果需求变成“算某个员工连续三个月的薪资波动”SQL 写起来会比较绕这种场景我一般会查出来放到内存里用 Java 循环算而不是硬写一条复杂 SQL——统计类需求要在 SQL 和代码之间找平衡不是所有聚合都适合塞给数据库。4. Vue 前端联调实战Axios 封装、路由守卫与员工列表页的可抄写法前端的坑往往不在页面样式而在请求层和路由层。这章讲的是接口联调最核心的三块拦截器、路由守卫、列表页闭环。写法是 Options API 风格Vue3 项目用 setup 写逻辑完全一样只是语法换成组合式。4.1 Axios 实例与拦截器Token 注入、401 跳转一次配齐所有请求统一走一个 Axios 实例不要在页面里单独 import axios// frontend/src/utils/request.js import axios from axios import { ElMessage } from element-ui import router from /router const service axios.create({ baseURL: /api, // 本地走 vite 代理生产走同源或 nginx 转发 timeout: 10000 // 10 秒超时 }) // 请求拦截器把 token 塞进请求头 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理后端返回 service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未登录)) } if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default service逻辑说明请求拦截器统一从 localStorage 取 token 并放到 Authorization 头后端拦截器从请求头里解析并校验。响应拦截器是前端鉴权的关键环节——token 过期后后端返回 401这里直接清掉本地 token 并跳登录页每个页面不需要自己处理登录失效。baseURL 用 /api 而不是写死后端地址是为了本地开发时配合 vite 代理生产环境打包后由 nginx 转发这样前端代码不用区分环境。参数说明timeout 设 10 秒接口超时会走 error 分支弹出网络异常。如果你的接口里有的请求比较慢可以单独在调用时覆盖 timeout比如上传文件接口改成 30 秒。还有一点拦截器里 return res 返回的是整个 Result 对象页面里要用 res.data 取业务数据这也是前后端约定要统一下来的。4.2 路由与菜单权限后端返回菜单前端用 addRoute 动态挂载权限控制常见两种做法前端把路由写死、菜单用 v-if 控制显示后端登录后返回菜单列表、前端动态注册路由。人事管理系统这种角色分明的项目第二种更接近企业真实做法// router/index.js const router new VueRouter({ mode: history, // history 模式部署到静态目录有坑第 5 章会讲 routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: Layout, redirect: /dashboard } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } // 已登录但动态路由还没挂载先拉菜单再继续 if (token !store.state.menuLoaded) { getMenu().then(menus { menus.forEach(m router.addRoute(Layout, m)) store.commit(setMenuLoaded, true) next({ path: to.path, replace: true }) // 重新进入当前路由 }) return } next() })逻辑说明beforeEach 守卫里先判断有没有 token没有就踢到登录页。登录后第一次跳转时动态路由还没挂载目标页面组件找不到所以先调用 getMenu 拉取当前角色的菜单列表逐条 addRoute 挂到 Layout 路由下再重定向到目标路径。参数说明后端返回的菜单结构通常包含 path、component、name 三个字段前端拿到后需要做一个 component 字符串到组件文件的映射。Vite 项目用 import.meta.glob 批量加载 views 目录Webpack 项目用 require.context。这个映射表很容易漏——后端返回的 component 名和前端文件路径不一致时addRoute 能成功但访问会白屏。刷新页面后菜单要重新拉取所以 store 里用 menuLoaded 标记是否已加载刷新后重置再走一遍拉取流程。4.3 员工列表页分页、搜索、弹窗表单的完整闭环列表页是 Vue 前端最典型的场景员工管理页通常由搜索区、表格、分页器、弹窗表单四部分组成页面骨架大概是这样template div el-input v-modelquery.name placeholder姓名搜索 clearable / el-select v-modelquery.departmentId placeholder按部门筛选 el-option v-ford in deptList :keyd.id :labeld.name :valued.id / /el-select el-button typeprimary clickloadPage查询/el-button el-button typesuccess clickopenDialog()新增/el-button el-table :datarows v-loadingloading el-table-column propempNo label工号 width120 / el-table-column propname label姓名 / el-table-column propdepartmentName label部门 / el-table-column label操作 template slot-scopescope el-button sizemini clickopenDialog(scope.row)编辑/el-button el-button sizemini typedanger clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table el-pagination :current-pagequery.pageNum :page-sizequery.pageSize :totaltotal layouttotal, prev, pager, next current-changehandlePageChange / /div /template script export default { data() { return { query: { pageNum: 1, pageSize: 10, name: , departmentId: null }, rows: [], total: 0, loading: false } }, methods: { async loadPage() { this.loading true try { const res await listEmployee(this.query) this.rows res.data.records // MyBatis-Plus IPage 的 records 字段 this.total res.data.total } finally { this.loading false } }, handlePageChange(pageNum) { this.query.pageNum pageNum // 分页器只传页码查询条件保留 this.loadPage() }, openDialog(row) { // row 有值就是编辑没值就是新增 } } } /script逻辑说明分页组件的 current-change 只返回当前页码所以要用 query 对象保存全部查询条件点击页码时带着条件重新查询。搜索框的 clearable 属性会显示清空按钮但清空后不会自动搜索要监听 clear 事件再拉一次数据否则会出现“输入框空了但表格还是旧结果”的现象。这里顺带说下 vue 路由参数在列表页的用法。从列表点编辑进入详情页时常见写法是 this.$router.push({ path: /employee/detail, query: { id: row.id } })query 方式的优势是刷新后参数还在 URL 上页面刷新不会丢。这条经验在跳转场景里几乎每次都会用到比直接用 params 更稳。5. 避坑篇从 javax 到 jakarta 到打包 404五条高频踩坑记录这一章每条都是复现这类源码时真实翻车过的场景现象是你在屏幕上看到的报错原因是对应到技术根因解决是能直接照做的操作。5.1 编译报错 javax 包不存在SpringBoot 版本太高引发的连锁反应现象mvn compile 直接报错提示 javax.persistence 或 javax.annotation 包不存在代码里大面积红色波浪线。 原因SpringBoot 3.x 要求 JDK 17同时把 Java EE 规范里的包名从 javax 迁到了 jakarta旧项目里所有 import javax.* 的代码都要换成 jakarta.*。很多毕业设计源码是基于 2.x 写的但你本地装的是 JDK 17就会撞上“springboot 版本太高”导致的兼容问题。 解决先看 pom.xml 里 spring-boot-starter-parent 的版本号。如果源码是 2.x 的写法本地装 JDK 8 或 11并把 pom 版本锁定在 2.7.x这是最省事的路径如果你坚持要用 3.x需要全局替换 import 里的 javax 为 jakarta还有部分配置类写法也会变。我的建议是能降级就不要升级毕设场景下项目能跑通比技术栈新更重要。5.2 Vue 打包放进 SpringBoot static 目录后刷新 404现象npm run build 生成 dist 目录拷贝到 backend 的 src/main/resources/static 下启动后端后访问首页正常但一刷新 /employee 这类子路径就报 404。 原因Vue 路由用了 history 模式浏览器直接请求 /employee 时SpringBoot 没有对应的 controller 映射就按找不到资源处理了。history 模式必须由服务器把所有非接口路径都回退到 index.html前端单独部署时 Nginx 天然支持这个行为但塞进 jar 里 SpringBoot 不会自动做。 解决三个办法。最省事的在 vue.config.js 里把 router 改成 hash 模式URL 会多一个 # 号不影响功能适合演示后端加一个转发 controller把所有不带 /api 前缀的路径 forward 到 /index.html最规范的是前端用 Nginx 部署配置 try_files $uri $uri/ /index.html。对应“vue 打包放进 springboot”这个操作我的习惯是毕设演示用 hash 最稳但要在答辩时说明这两者的区别。提示hash 模式路由的 URL 形如 /#/employee刷新不会发请求到服务器所以不存在 404 问题。5.3 跨域配置本地代理通了部署后接口全部 401现象本地执行 npm run dev 一切正常登录、列表都没问题打包部署到后端后所有请求都 401 或请求地址根本不对。 原因vite.config.js 里的 proxy 只在本地开发服务器生效构建后的代码没有 dev server 做代理请求直接发到当前域名下。如果后端配置了 server.servlet.context-path 或接口前缀前端 baseURL 是 /api 而后端实际路径不带这个前缀请求就打到错误地址上。 解决打开浏览器 F12 的 Network 面板看失败请求的完整 URL比对后端接口实际路径。两边的 /api 前缀要保持一致一般做法是后端 context-path 配成空接口统一用 /api 开头前端 baseURL 保持 /api两边天然对齐。跨域配置里注意 allowedOrigins 不要和 allowCredentials 同时用通配符SpringBoot 会直接拒绝这种矛盾配置。5.4 考勤时间差 8 小时serverTimezone 与 LocalDateTime 序列化现象页面显示的打卡时间比实际时间早 8 小时或晚 8 小时考勤迟到判断错乱明明 8:50 打卡却被判迟到。 原因MySQL 连接串没指定 serverTimezone数据库和 JDBC 驱动之间默认按服务器时区解析国内服务器通常是北京时间但容器或云数据库默认可能是 UTC两边一差就是 8 小时。SpringBoot 的 Jackson 序列化 LocalDateTime 时又套了一层时区转换两个因素叠加时间就错乱了。 解决数据源 URL 加上参数 serverTimezoneAsia/ShanghaicharacterEncodingutf8同时 application.yml 里对 Jackson 统一设置时区为 GMT8。数据库插入数据前先用 SELECT NOW() 看数据库当前时间如果和系统时间不一致优先排查连接串而不是在代码里做加减法补时差——那是最容易埋雷的临时方案。5.5 npm install 失败node-sass 与高版本 Node 的兼容迁移现象npm install 报错提示 node-sass 需要 python2或 binding.node 下载失败或直接 gyp ERR。 原因node-sass 是 C 原生模块编译需要 Python 和 C 工具链Node 16 以上版本和它的二进制预编译包已经不兼容国内网络下载 binding 文件又容易失败。 解决打开 package.json 看 devDependencies如果里面有 node-sass直接换成 dart-sassnpm uninstall node-sass npm install sass --save-dev。换完以后样式代码里若用了 /deep/ 深度选择器要改成 :deep()否则 Vue 单文件组件的样式穿透失效这是“vue 安装依赖”最容易忽视的连带修改。拿到项目先删掉 node_modules 再重装不要用压缩包里残留的旧依赖。6. 部署验证与进阶三种交付方式怎么选核心链路怎么一条命令自测6.1 部署选型jar 内置前端、Nginx 分离、Docker Compose代码跑通之后下一个问题是怎么交付。三种方式各有适用场景交付方式适合场景注意点前端打进 jar 的 static 目录毕设答辩、单机演示路由用 hash 模式或后端加转发Nginx 静态文件 jar 后端正式的前后端分离部署配置 Nginx 反向代理 /api 和 try_filesDocker Compose 一套拉起想展示容器化能力、需要快速迁移服务器内存不低于 2G数据库容器要挂载数据卷如果想用宝塔的 Docker 面板部署 SpringBoot 项目思路是 docker-compose.yml 里定义三个服务mysql 一个容器、后端 jar 一个容器、前端 nginx 一个容器nginx 里反代后端接口地址。这种容器化部署上线后日志查看、重启、回滚都方便运维成本比直接在服务器上装 JDK 和 node 低。6.2 核心链路自测一个脚本验证登录、员工查询、考勤统计部署验证最关键的是登录链路登录通了大概率其他模块都通。我会在服务器上放一个 bash 脚本每次部署完跑一遍#!/bin/bash BASE_URLhttp://localhost:8080 # 1. 登录拿 token TOKEN$(curl -s -X POST $BASE_URL/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} | jq -r .data.token) echo 登录获取到的 token: $TOKEN # 2. 带 token 查员工分页 curl -s $BASE_URL/api/employee/page?pageNum1pageSize10 \ -H Authorization: Bearer $TOKEN | jq .data.total # 3. 带 token 查考勤统计 curl -s $BASE_URL/api/attendance/statistics?month2024-06 \ -H Authorization: Bearer $TOKEN | jq .data这个脚本的核心思路是先用登录接口换 token再带着同一个 token 访问受保护的接口jq 处理后返回数据一眼能看清。端口、用户名密码按实际配置改如果项目有图形验证码脚本就得多一步先取验证码的接口逻辑。从那以后我每次拿到一个 SpringBoot Vue 的压缩包第一件事永远是先看 pom.xml 和 package.json 里的版本再决定装哪个环境而不是直接解压、启动、报错、上网搜。这套人事管理系统本身不难但版本兼容、时区、路由模式这三个环节足够让一个晚上报废希望这次的拆解和避坑记录能帮到你。本文还有配套的精品资源点击获取
返回列表