ARTICLE DETAIL

资讯详情

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

前后端分离车辆管理系统实战:SpringBoot+Vue+MyBatis+MySQL完整设计

前后端分离车辆管理系统实战:SpringBoot+Vue+MyBatis+MySQL完整设计 前后端分离的企业车辆管理系统用 SpringBoot Vue MyBatis MySQL 这套组合做出来是我最近一个比较完整的实战项目。整个系统从零搭建包含了车辆档案、司机管理、用车申请、审批派车、行程记录、报表统计这些核心功能源码和部署过程我都整理完整了。这篇文章会把我的设计思路、表结构、后端关键代码、前端页面逻辑以及部署踩坑过程全部拆开讲清楚适合正在做毕业设计、或者刚接触前后端分离项目想找一个完整案例练手的朋友。这个项目最让我满意的地方是它的业务闭环是完整的申请人提交用车申请管理员审批调度员分配车辆和司机车辆跑完记录里程和费用最后还能统计油耗和出车次数。它不是那种只有增删改查的演示项目而是真正能落地到企业行政场景里用的系统。1. 项目需求与整体设计思路1.1 企业车辆管理到底在管什么先别急着写代码我把企业车辆管理的业务需求彻底理了一遍。很多新手拿到这种题目就急着建表结果做出来的系统只有车辆信息的增删改查根本算不上管理系统。真实的企业场景里车辆管理至少包含四个维度第一是车辆档案管理。车辆的品牌、型号、车牌号、发动机号、购置日期、保险到期日、年检到期日这些基础信息必须维护清楚。保险和年检快到期的车辆要有提醒不然车辆脱保上路出了事故企业要承担很大责任。第二是司机管理。司机的基本信息、驾照类型、驾照有效期、联系电话、当前状态都要记录。调度车辆的时候要能快速知道哪个司机是空闲的谁在出车谁在休息。第三是用车流程管理。员工用车不是直接拿钥匙开走要提交申请、领导审批、调度员派车、司机出车、归还车辆。整个流程要有痕迹事后能追溯。第四是费用和统计。油费、过路费、维修费这些成本要记录月底要能算出来每辆车的百公里油耗、每公里成本、出车次数这直接关系到企业的车辆调配决策。把这四个维度拆清楚系统的功能模块自然就出来了。我当时给这个项目划分了六个模块系统管理、车辆管理、司机管理、用车申请、行程管理、统计报表。系统管理里再细分为用户、角色、菜单、日志。1.2 为什么一定要前后端分离选前后端分离架构不是因为它流行而是有实实在在的好处。开发效率上前后端可以完全并行。后端同事定义好接口文档就开始写接口前端同事同时写页面谁也不用等谁。我用 Swagger 生成接口文档前端照着调就行。项目周期至少能缩短三分之一。部署灵活性上前端是纯静态文件扔到 Nginx 或者对象存储都能跑后端独立部署在应用服务器上。以后要加一个手机端 H5前端直接复用现有的接口就行后端不需要动。排障方面也有优势。接口返回的数据不对打开浏览器开发者工具看网络请求就能定位是前端传参问题还是后端逻辑问题不用像单体应用那样在大量模板代码里翻找。当然前后端分离也有成本——跨域、鉴权、联调这些事都要额外处理。这些坑我在后面章节会详细讲这里先记住一个结论对于这种需要多人协作、多端适配的企业管理系统前后端分离是当前最合理的方案。1.3 系统模块全景划分我画了一张模块图在脑子里这里用文字描述一下整体结构系统管理模块管的是用户登录、角色权限、菜单分配。我用的是基于角色的访问控制模型管理员、审批人、调度员、普通员工各是一类角色每类角色看到的菜单和能操作的接口都不一样。车辆管理模块维护车辆档案支持车牌号模糊查询、品牌筛选、状态筛选。列表要分页这是企业系统的标配。司机管理模块和车辆管理类似重点是多一个当前是否可调度的状态判断。司机状态会被行程模块联动修改。用车申请模块是核心业务流。普通员工填用车时间段、目的地、事由提交申请后进入审批流。审批通过后调度员选择车辆和司机然后形成行程单。行程管理模块记录每次实际出车的起始里程、结束里程、油费、过路费。车辆归还时还自动计算本次行驶里程。统计报表模块提供出车次数、里程汇总、油耗费用的统计用表格加柱状图展示。2. 技术选型为什么是 SpringBoot Vue MyBatis MySQL2.1 SpringBoot 的定位与优势后端框架我选 SpringBoot核心原因是它把 Spring 生态里的配置复杂度降到最低。以前用 Spring MVC 搭项目要写 web.xml、spring-mvc.xml、数据源配置、事务配置一大堆 XML。SpringBoot 用约定大于配置的思路一个 spring-boot-starter-web 起步依赖就把内嵌 Tomcat、JSON 序列化、请求映射全部带进来了我只需要在 application.yml 里写几行数据源配置就能跑起来。更重要的是 SpringBoot 的生态太成熟了。要做鉴权有 Spring Security 和 JWT 整合方案要写接口文档有 Knife4j要做参数校验有 Hibernate Validator。项目遇到任何问题搜一下基本都有成熟的解决方案。这个项目规模不需要微服务那一套SpringBoot 单体应用足够支撑几百个用户的并发访问。真到了需要横向扩展的时候SpringBoot 应用也方便做集群部署加一层负载均衡就行。2.2 Vue 生态怎么选Vue2 Element UI 还是 Vue3 Element Plus早期做的时候我纠结过 Vue 版本的问题。最后我选了 Vue 2 Element UI说实话不是因为技术上多领先而是考虑到维护性。很多中小型公司现有的企业管理系统还是 Vue 2 写的团队对 Vue 2 的语法、响应式原理、组件通信那一套已经很熟悉了。Element UI 的组件库也很稳定表格、表单、弹窗、分页这些企业系统高频组件都是现成的风格统一不需要自己写样式。如果读者现在是全新项目直接用 Vue 3 Element Plus Vite 也完全可以构建速度快很多组合式 API 写起来也更清爽。我强烈建议不要在这上面纠结太久。Vue 2 和 Vue 3 的核心概念是相通的组件、路由、状态管理、生命周期这些思想完全一致。选一个顺手的技术栈先把业务跑通比什么都重要。2.3 MyBatis 在这个项目里解决了什么问题ORM 框架里 JPA 和 MyBatis 之争一直存在。这个项目我选 MyBatis是因为车辆管理系统里有大量动态查询的场景。车辆列表要支持车牌号模糊搜索、品牌筛选、状态筛选三个条件还要随时组合。MyBatis 的动态 SQL 写这种查询非常自然if、where、foreach标签可以直接在 XML 里完成 SQL 拼接。JPA 虽然也能做但遇到复杂条件时要么写 Specification要么派生查询方法名长得吓人调试起来不够直观。另外 MyBatis 对 SQL 的控制力是 JPA 比不了的。比如分页查询我可以精确控制 limit 语句联表查询时可以手动优化 JOIN 顺序。对于需要精细控制数据库性能的场景这种半自动化反而更合适。2.4 MySQL 的选型与设计考量数据库选 MySQL最大的理由是它够用且成熟。企业车辆管理系统数据量不大即使是一家两三百辆车的中型企业加上历史行程记录也就几十万行数据。MySQL 的 InnoDB 引擎处理这种量级非常轻松加好索引后查询都在毫秒级。MySQL 的运维方案也多。备份有 mysqldump主从有 binlog集群有 MGR基本上不会遇到找不到解决方案的情况。部署也简单Windows 上装个安装包就能跑Linux 上用包管理器安装完成初始化就行。设计表的时候我坚持用 InnoDB 引擎和 utf8mb4 字符集。字符集特别注意如果建库时用了 utf8后面存生僻字或者 emoji 会乱码。utf8mb4 是 utf8 的超集能存所有字符所以建库我都用这个。3. 数据库设计与核心表结构3.1 数据库设计三原则动手建表之前我先定了三条设计原则后面所有表都按这个标准来。第一条核心表只存业务字段文件、图片这种大数据不直接进库。车辆的照片放服务器目录或者对象存储数据库里只存文件路径。这样表结构轻量查询不拖泥带水。第二条状态字段统一用 tinyint业务判断全部基于数值。车辆状态 0 代表空闲、1 代表出车中、2 代表维修中、3 代表已报废。审核状态 0 待审核、1 已通过、2 已驳回。这样后端判断逻辑简单也方便扩展新状态。第三条所有表都带 create_time 和 update_time 字段。系统上线后要排查问题、做审计时间字段必不可少。update_time 用 ON UPDATE CURRENT_TIMESTAMP 自动更新不用手动维护。3.2 核心表 DDL 与字段说明车辆表是整个系统的核心我给出创建语句CREATE TABLE tb_vehicle ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, plate_no varchar(20) NOT NULL COMMENT 车牌号, brand varchar(50) DEFAULT NULL COMMENT 品牌, model varchar(50) DEFAULT NULL COMMENT 型号, color varchar(20) DEFAULT NULL COMMENT 颜色, engine_no varchar(50) DEFAULT NULL COMMENT 发动机号, buy_date date DEFAULT NULL COMMENT 购置日期, insurance_date date DEFAULT NULL COMMENT 保险到期日, annual_check_date date DEFAULT NULL COMMENT 年检到期日, status tinyint(2) NOT NULL DEFAULT 0 COMMENT 状态0空闲1出车中2维修中3已报废, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_plate_no (plate_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆信息表;车牌号加唯一索引这是业务上必须的同一辆车不能重复录入。status 字段加普通索引因为列表页最常用的筛选条件就是状态。用车申请表是业务流的核心CREATE TABLE tb_vehicle_apply ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, apply_user_id bigint(20) NOT NULL COMMENT 申请人ID, vehicle_id bigint(20) DEFAULT NULL COMMENT 车辆ID, driver_id bigint(20) DEFAULT NULL COMMENT 司机ID, start_time datetime NOT NULL COMMENT 预计用车开始时间, end_time datetime NOT NULL COMMENT 预计用车结束时间, destination varchar(100) DEFAULT NULL COMMENT 目的地, reason varchar(255) DEFAULT NULL COMMENT 用车事由, status tinyint(2) NOT NULL DEFAULT 0 COMMENT 审核状态0待审核1已通过2已驳回, audit_user_id bigint(20) DEFAULT NULL COMMENT 审批人ID, audit_time datetime DEFAULT NULL COMMENT 审批时间, audit_remark varchar(255) DEFAULT NULL COMMENT 审批意见, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_apply_user (apply_user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用车申请表;预留的 vehicle_id 和 driver_id 在审批通过后由调度员填写。这里是先申请后派车的流程所以两个字段允许为空。开始时间和结束时间都加了索引因为查询车辆冲突时要根据时间范围检索。行程记录表记录了每次出车的实际数据CREATE TABLE tb_trip_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, vehicle_id bigint(20) NOT NULL COMMENT 车辆ID, driver_id bigint(20) NOT NULL COMMENT 司机ID, apply_id bigint(20) DEFAULT NULL COMMENT 关联申请ID, start_mileage decimal(10,2) NOT NULL COMMENT 出发里程, end_mileage decimal(10,2) DEFAULT NULL COMMENT 归还里程, fuel_cost decimal(10,2) DEFAULT NULL COMMENT 油费, toll_cost decimal(10,2) DEFAULT NULL COMMENT 过路费, start_time datetime DEFAULT NULL COMMENT 实际出发时间, end_time datetime DEFAULT NULL COMMENT 实际归还时间, description varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_vehicle (vehicle_id), KEY idx_driver (driver_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT行程记录表;里程字段用 decimal 而不是 double因为 float 和 double 会有精度误差涉及费用的数据必须用十进制精确类型。用户表和角色表结构相对简单users 表关联角色表做基于角色的权限控制。用户密码存的是 BCrypt 加密后的密文这个后面会说总之明文存储密码在企业系统里绝对不允许。4. 后端核心实现与关键代码4.1 项目分层与目录结构后端代码我严格按照 Controller-Service-Mapper 三层结构来组织。有些同学喜欢把业务逻辑全写在 Controller 里接口一多就变成一锅粥。我的包结构是这样的com.company.vms ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── common │ ├── Result.java │ ├── PageResult.java │ └── BusinessException.java └── config ├── CorsConfig.java └── WebMvcConfig.java实体类 Entity 对应数据库表结构DTO 接收前端请求参数VO 返回给前端展示的数据。DTO 和 VO 分开的好处是接口的入参和出参可以各自演进不互相影响。4.2 统一返回结构与全局异常处理所有接口统一返回 Result 结构这是前后端分离项目的基础设施。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 0; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }前端拿到响应后判断 code 等于 0 就取 data否则弹出 message。这里我特意把成功码定成 0 而不是 200避免和 HTTP 状态码混淆。全局异常处理用 RestControllerAdvice 实现把参数校验异常、业务异常、数据库异常统一拦截并转换成 Result 格式返回。这样后端即使忘记 try-catch前端也不会收到一堆难懂的异常堆栈。4.3 JWT 登录鉴权的实现细节登录模块我用了 JWT 方案。流程是这样的用户提交用户名密码后端校验通过后生成一个带过期时间的 token 返回给前端前端存在 localStorage 里每次请求通过 Authorization 请求头携带。后端写一个拦截器校验 token 的合法性和有效期Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } String userId JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(userId); return true; } }解析 token 后把用户 id 放到 ThreadLocal 里的 UserContext这样在 Service 层任何地方都能拿到当前登录用户不用每个接口都把用户 id 传来传去。这里有几个必须强调的细节。第一token 的签名密钥要足够长至少 32 个字符不能写在代码里明文建议通过环境变量注入。第二token 要设置过期时间企业系统一般设 2 到 8 小时太长了不安全太短了用户频繁登录体验差。第三服务端要增加 token 失效的黑名单机制用户修改密码后强制重新登录这是很多初学项目漏掉的。4.4 车辆管理模块动态 SQL 与分页查询车辆列表查询是学习 MyBatis 动态 SQL 最典型的场景。select idselectVehiclePage resultTypecom.company.vms.vo.VehicleVO SELECT v.*, CASE v.status WHEN 0 THEN 空闲 WHEN 1 THEN 出车中 WHEN 2 THEN 维修中 ELSE 已报废 END AS statusName FROM tb_vehicle v where if testplateNo ! null and plateNo ! AND v.plate_no LIKE CONCAT(%, #{plateNo}, %) /if if testbrand ! null and brand ! AND v.brand #{brand} /if if teststatus ! null AND v.status #{status} /if /where ORDER BY v.create_time DESC /select这个 SQL 的精妙之处在于where标签会自动处理 AND 前缀问题。如果 plateNo 为空、brand 为空、只有 status 有值生成的 SQL 就是WHERE v.status 0不会出现多余的 AND。分页我用 PageHelper 插件配置起来非常简单只需要在 pom 里加依赖然后在 Service 层调用 PageHelper.startPage() 就行public PageResultVehicleVO pageQuery(int pageNum, int pageSize, VehicleQuery query) { PageHelper.startPage(pageNum, pageSize); ListVehicleVO list vehicleMapper.selectVehiclePage(query); PageInfoVehicleVO pageInfo new PageInfo(list); return new PageResult(pageInfo.getTotal(), pageInfo.getList()); }PageHelper 底层通过拦截器自动拼接 limit 语句返回的 PageInfo 里自带 total 和页码信息省去手写 count 查询的麻烦。这里有个注意事项PageHelper.startPage 必须紧跟第一条查询语句中间不能有其它 SQL 操作否则分页线程变量就会被错误消费。4.5 用车审批流程与事务控制用车审批是这个系统业务逻辑最复杂的部分我把它做成一个状态机。普通员工提交申请时状态是 0待审核车辆和司机字段为空。审批人看到待审核列表可以同意或驳回。同意后状态变成 1已通过这时调度员来分配车辆和司机。分配完成后车辆和司机的状态自动改成出车中。行程完成后填写里程和费用信息车辆和司机状态恢复为空闲。这里最核心的控制点是状态流转必须用事务包起来。比如归还车辆这个操作要同时更新三张表行程记录表插入数据、车辆表状态改成空闲、司机表状态改成空闲。如果其中一个步骤失败而其它步骤成功数据就全乱了。Transactional(rollbackFor Exception.class) public void completeTrip(TripCompleteRequest request) { TripRecord record tripRecordMapper.selectById(request.getTripId()); if (record null) { throw new BusinessException(行程记录不存在); } record.setEndMileage(request.getEndMileage()); record.setFuelCost(request.getFuelCost()); record.setTollCost(request.getTollCost()); record.setEndTime(new Date()); tripRecordMapper.updateById(record); Vehicle vehicle new Vehicle(); vehicle.setId(record.getVehicleId()); vehicle.setStatus(0); vehicleMapper.updateById(vehicle); Driver driver new Driver(); driver.setId(record.getDriverId()); driver.setStatus(0); driverMapper.updateById(driver); }rollbackFor Exception.class 这个参数很容易被忽略它的意思是任何异常都触发回滚。不加这个参数的话默认只回滚 RuntimeException遇到 checked exception 事务就不生效了这是个经典坑。5. 前端核心实现与联调5.1 前端初始化与路由设计前端我用 Vue CLI 创建项目然后安装了 vue-router、axios、element-ui 三个核心依赖。项目结构上把页面放在 views 目录下组件放在 components 目录下api 请求统一放在 api 目录下。路由配置用懒加载这个在企业系统里非常重要。系统菜单多如果一次性加载全部页面组件首屏加载时间会非常长。用动态 import 实现按需加载只有访问到对应路由时才加载对应的 JS 文件const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /vehicle, component: () import(/views/vehicle/VehicleList.vue) }, { path: /apply, component: () import(/views/apply/ApplyForm.vue) } ]路由守卫负责登录校验和权限控制。我在全局前置守卫里判断没有 token 就跳转到登录页登录了但访问了没有权限的路由就拦截并提示router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next({ path: /login }) return } if (token to.path /login) { next({ path: / }) return } next() })不同角色动态生成路由那套方案我也试过功能上确实更高级但实现复杂度高调试成本大。对于中小型车辆管理系统直接在前端配置好菜单、根据角色权限控制显隐就够了后端接口再做一层权限拦截双保险。5.2 axios 封装要点axios 必须封装不然后期改 token 存储方案或统一处理错误时要改几十个文件。我在 api 目录下建了一个 request.js 文件import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use(response { const res response.data if (res.code 0) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) }, error { return Promise.reject(error) }) export default request这里有个细节很重要baseURL 设置成/api而不是写死端口地址。开发环境下通过 vue.config.js 的 devServer 代理把 /api 前缀转发到后端端口生产环境由 Nginx 处理转发。这样前端代码里没有任何后端地址部署时只需要改 Nginx 配置前端代码完全不用动。5.3 核心页面实现思路车辆管理列表页是最典型的增删改查页面布局是上方搜索栏中间表格下方分页。搜索栏放车牌号输入框、品牌下拉框、状态下拉框点击搜索重新加载第一页数据。表格用 el-table列显示车牌号、品牌、颜色、购置日期、保险到期日、状态。状态列用 el-tag 展示不同状态对应不同颜色空闲是绿色出车中是蓝色维修中是橙色已报废是红色。保险到期时间使用一个特殊样式如果距今不到 30 天字体变红提示续保。用车申请页交互稍微复杂一点。表单字段有开始时间、结束时间、目的地、用车事由。提交前要校验时间段合理性结束时间不能早于开始时间。提交后调用接口等待审批结果。派车页面是管理员的专属页面。审批通过的申请单在列表里展示管理员点击派车按钮弹出对话框选择车辆和司机。车辆下拉框只显示空闲车辆司机下拉框只显示空闲司机这样从源头上就避免了撞车撞人冲突。5.4 前后端联调与跨域问题本地开发时前端跑在 8080 端口后端跑在 8888 端口默认会触发浏览器的跨域拦截。我在 vue.config.js 里配置了代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8888, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这里使用代理而不是后端开启 CORS原因是生产环境也必须走 Nginx 代理。开发环境和生产环境保持同一套请求路径前端代码不用区分环境这是最好的实践。当然后端我也加了跨域配置类作为兜底但生产环境下会把它关闭只允许同源访问避免安全风险。6. 源码部署与生产环境配置6.1 环境准备与初始化部署前先列一下环境清单JDK 8 或 11建议 8稳定便宜兼容性好Maven 3.6 以上MySQL 5.7 或 8.0Node.js 14 以上前端构建时用Nginx 1.18 以上数据库初始化就一条命令的事mysql -uroot -p vms.sqlvms.sql 文件里建好库、建好表、插入了初始管理员账号。这里我特意把初始密码设置成了 BCrypt 加密后的字符串不用 MD5。原因很简单MD5 太容易被彩虹表破解BCrypt 每次加密结果都不同安全性高得多。6.2 后端打包与部署后端打包前检查 application.yml 里的数据库连接配置把数据库地址、用户名、密码改成实际环境的。spring: datasource: url: jdbc:mysql://localhost:3306/vms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password server: port: 8888JDBC URL 里三个关键参数一个都不能少useUnicodetrue 保证中文正常characterEncodingutf8 防止乱码serverTimezoneAsia/Shanghai 解决 MySQL 8 的时区报错。打包执行 Maven 命令mvn clean package -DskipTests然后启动java -jar target/vms-server-1.0.0.jar生产环境我建议用 systemd 守护进程管理崩溃了能自动重启。简单的做法是用 nohup 后台运行然后把启动进程写成一个 shell 脚本。6.3 前端构建与 Nginx 部署前端构建前检查 vue.config.js 里的 publicPath 配置如果部署在服务器根路径就设成 /如果部署在子路径比如 /vms/ 就要设成 /vms/。执行构建npm install npm run build构建完成后dist 目录里就是静态文件。我把它们拷贝到服务器的 /usr/share/nginx/html/vms 目录下然后写 Nginx 配置。6.4 Nginx 反向代理配置完整配置如下server { listen 80; server_name yourdomain.com; location /api/ { proxy_pass http://127.0.0.1:8888/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root /usr/share/nginx/html/vms; index index.html; try_files $uri $uri/ /index.html; } }两处核心配置说明一下。第一location /api/ 的 proxy_pass 地址末尾带了一个 /这样会把请求路径里的 /api 前缀去掉再转发给后端。比如前端请求 /api/vehicle/list后端收到的是 /vehicle/list不用改后端接口路径映射。第二try_files $uri $uri/ /index.html;这行解决前端 history 路由刷新 404 的问题。Vue 的 history 模式下访问 /vehicle 页面时浏览器请求的就是服务器上的 /vehicle 路径服务器上并没有这个文件不配置 try_files 就会 404。加上这行后找不到对应文件时自动回退到 index.html由前端路由接管渲染。6.5 HTTPS 与容器化增强建议生产环境上线必须上 HTTPS。我一般用 Nginx 配置 SSL 证书在配置里加监听 443 的 server 块证书文件用云服务商免费申请就行。别嫌麻烦现在浏览器对非 HTTPS 网站的提示已经非常醒目了客户和领导看到都会不舒服。Docker 打包也是一个好选项写一个 Dockerfile基础镜像用 openjdk:8-jre把 jar 包拷进去CMD 写启动命令。前端单独一个 Nginx 镜像把 dist 目录拷进去。用 docker-compose 一键启动 MySQL、后端、前端三个容器团队协作开发时环境一致性非常高。7. 常见问题排查与避坑记录7.1 速查表10 个高频问题与解决方案我把平时被问得最多的 10 个问题整理成了一张速查表问题现象原因解决方案前端请求接口返回 404Nginx 缺少 try_files 配置加上try_files $uri $uri/ /index.html;接口请求报跨域错误前端端口与后端端口不同开发环境配 vue 代理生产环境配 Nginx 代理MySQL 连接报时区错误缺少 serverTimezone 参数JDBC URL 加serverTimezoneAsia/Shanghai数据库中文乱码建库表时用了 utf8统一用 utf8mb4 字符集JWT 签名无效报错密钥长度不够或前后端密钥不一致使用 32 位以上密钥并统一配置PageHelper 分页失效startPage 与查询语句之间有其它操作确保 startPage 紧跟查询语句前端刷新页面白屏路由模式与服务器配置不匹配使用 history 模式时必须配置 try_files事务没有回滚未指定 rollbackFor使用Transactional(rollbackFor Exception.class)后端接口返回 500 堆栈未配置全局异常处理添加 RestControllerAdvice 全局异常处理器车辆状态与实际不符状态更新没有事务控制审批、派车、归还操作统一加事务7.2 MyBatis 动态 SQL 的条件不生效我在网上看到很多同学遇到过我明明传了条件但 WHERE 里就是没有的问题。这种情况八成是if里的 test 判断写错了。比如有人写teststatus ! null and status ! 但 status 是 Integer 类型Integer 永远不等于空字符串但这个判断也能通过反过来如果字段是 String 类型只判断! null就会漏掉空串场景。还有一个小坑是 XML 里的小于号。动态 SQL 里如果有create_time #{endTime}这样的条件直接在 XML 里写会报错要用lt;转义或者用![CDATA[]]包裹整个条件片段。7.3 前后端联调时接口参数对不上联调最常见的报错是 400 Bad Request往往因为前端传的参数名和后端 DTO 字段名不一致。前端传的是 startTime后端接收的参数是 start_date这类问题排查半天都查不出来。我的建议是前后端在定义接口文档时就统一命名规范后端 DTO 字段用驼峰命名前端请求参数也保持一致。后端在接收 JSON 时把 DTO 的字段名固定好前端照抄不要各写各的。另外日期格式也经常踩坑。前端默认传给后端的时间格式是 ISO 字符串比如 2025-06-01T12:00:00.000Z后端用 Date 字段接收时如果不做格式处理就会报错。解决方案是在 application.yml 里配置全局日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT87.4 部署后页面样式丢失或接口地址错误前端构建后部署发现页面 CSS 样式全没了。这个 90% 是 publicPath 配置不对。如果部署在域名根路径下publicPath 必须设成 /如果部署在子路径下就要设成对应的子路径不然后端返回的静态资源地址就会多一层或者少一层。接口地址错误的表现是页面能打开但所有请求都是 404。这时先按 F12 打开开发者工具看网络请求里的 URL 是什么对比实际后端接口地址查配置。记住一个原则所有接口地址都带 /api 前缀Nginx 负责剥离或者转发前端代码里永远不出现具体 IP 地址。结尾做这个项目最大的收获从零写完这套前后端分离的车辆管理系统我最深的体会是企业级项目最难的不是单个功能怎么写而是把一串状态流转、多表数据联动、权限控制这些问题串起来不出错。拿用车审批来说表面看就是一个表单加一个列表但背后涉及申请单状态、车辆状态、司机状态三张表的联动修改哪一步没做事务控制上线后迟早要出数据事故。拿前端来说难点也不是页面漂不漂亮而是路由权限怎么控、请求代理怎么配、刷新不 404 这些细节里的魔鬼。最后再分享两个小技巧。第一个是后端接口调试时多利用 Knife4j它生成的接口文档可以直接在线调用前端还没写的时候就能自测接口正确性。第二个是前端页面写完先别急着做特效把表格的 loading 状态、请求失败的 message 提示这些基础交互做扎实用户用起来会觉得系统很可靠。这套系统我已经跑通了完整的部署链路从数据库初始化到后端启动再到前端构建发布每一步都验证过。如果你正在找前后端分离项目的实战源码或者毕业设计选了这个题目按照这篇文章的步骤一步步来一定能跑起来。跑的过程中有卡住的地方对照第七部分的问题速查表基本就能解决。
返回列表