SpringBoot+Vue3+MyBatis实现车辆管理系统实战指南
1. 选型这件事为什么是SpringBootVue3MyBatis而不是别的车辆管理系统在大多数企业里都是看起来简单、做起来恶心的业务系统。说它简单是因为核心就围绕车辆信息、出车记录、油耗、维修保养、保险年检这几条线说它恶心是因为每个企业的车辆管理规则都不一样有的要管司机排班有的要管GPS轨迹有的要管费用分摊你永远没法用一个死板的模板去套。我这次做这套系统时技术栈选型其实是纠结过一轮的。一开始考虑过用若依这种成熟脚手架直接改热词里也很多人搜若依vue3 ts报错可见用的人不少。但最终我还是选择从零搭一套SpringBootVue3MyBatisMySQL的前后端分离项目原因很简单这套组合足够轻、足够稳、也足够通用源码丢给任何一个人都能看得懂、改得动。先说说每个组件在这个项目里的具体角色这套组合的选型逻辑我就不藏着掖着了。SpringBoot负责提供后端服务它最大的价值不是快而是约定优于配置。传统SSH项目要写一堆XML配置光一个数据源就能折腾半天。SpringBoot把自动配置做到极致application.yml一写内嵌Tomcat一启动整个服务就跑起来了这在车辆管理系统这种需要快速迭代、频繁改业务规则的项目里效率优势非常明显。Vue3负责前端页面选它而不是Vue2核心原因有两个一是Composition API对复杂业务逻辑的组织能力更强车辆管理页面里表单联动、状态切换特别多用setup语法组织代码比Options API清爽太多了二是TypeScript的支持更友好团队里如果有人接手代码可读性和可维护性都上了一个台阶。如果你还在犹豫学Vue2还是Vue3直接选Vue3就对了现在新项目用Vue2基本等于给自己埋技术债。MyBatis负责数据持久层选它而不选JPA或者MyBatis-Plus是因为车辆管理系统的SQL普遍带有较强的业务特征比如多表关联查询车辆出车记录、统计月度油耗、筛选保险到期车辆等。MyBatis的XML里可以精确控制每一条SQL遇到需要优化索引命中的场景直接改SQL就行不会被ORM层的自动生成的查询带偏。虽然MyBatis-Plus用起来代码量更少但当你需要处理复杂动态SQL时原生MyBatis的灵活度还是最让我放心的而且mybatis缓存、mybatis拦截器这些机制用好了性能提升非常明显。MySQL作为数据库这个不用多解释车辆管理系统的数据量级一般在百万级以下MySQL的单库性能完全够用运维成本又低。配合InnoDB引擎事务支持可靠并发情况下数据一致性也扛得住。一句话总结这套组合的优势后端稳定能扛业务前端灵活能快改需求数据库简单好维护前后端用JSON交互接口清晰拆开部署、合体运行都没问题。这套方案也是目前中小型管理系统最主流的技术路线网上资料多招人也容易踩坑了Stack Overflow一搜一堆答案。2. 数据库表结构设计车辆管理系统的地基比代码更考验功底很多人拿到车辆管理系统的需求第一反应是建一张车辆表然后围绕它堆字段。这种做法不是不行但系统活不过三个月你就会发现各种别扭保险费和年检费要混在一起加油记录和维修记录全塞在备注里出车审批流程完全没法追溯我在建表之前做了整整两天的业务梳理。车辆管理系统看起来简单实际上牵涉的角色特别多管理员、调度员、司机、财务、维修工每个角色关心的数据维度都不一样。管理员看整体使用率调度员看车辆是否有空、司机是否在岗财务看油耗和费用支出维修工看保养周期和故障记录。一张表根本兜不住这些信息必须拆成多个业务模块的表再通过外键和业务逻辑把它们串联起来。下面是我最终落地的核心表结构你可以直接参考这比我构建代码更花心思。2.1 车辆信息表vehicle这张表是系统的主表几乎所有业务都围绕车辆ID展开。字段名类型说明idbigint主键自增plate_numbervarchar(20)车牌号必填业务上要求唯一vehicle_typevarchar(20)车辆类型轿车/SUV/货车/客车等brandvarchar(50)品牌型号比如大众帕萨特engine_numbervarchar(50)发动机号vinvarchar(50)车架号purchase_datedate购入日期purchase_pricedecimal(10,2)购入价格财务统计用seat_countint核载人数statustinyint车辆状态0空闲/1出车中/2维修中/3已报废create_timedatetime创建时间update_timedatetime更新时间有个细节要提醒status 字段千万别直接存中文一定要用数字状态码。前端显示的时候做一次映射这样后期扩展状态就非常灵活。比如你后来想加一个已预约的状态直接加一个数字枚举就行不用改表结构。2.2 司机信息表driver司机表和车辆表是独立的还是合并这个要看你系统的派车逻辑。如果是一车一司机的固定模式直接把司机信息放到车辆表也行。但大多数企业是多个司机共用一辆车这时就一定要拆表。司机字段里除了姓名、驾驶证号、电话、入职时间这些基础信息我额外加了一个driver_status字段记录司机当前是否可出车。这个字段在后面做自动排班的时候会非常有用。另外我建议驾驶证有效期和年检提醒都做成日期字段配合定时任务就可以实现到期自动提醒这个功能在实际使用中好评率特别高。2.3 出车记录表dispatch_record这是整个系统里数据量最大的一张表每次出车、还车都会产生一条记录后续的油耗统计、里程统计都依赖这张表。核心字段包括车辆ID、司机ID、出车时间、预计还车时间、实际还车时间、出车里程、还车里程、出车事由、目的地、审批人、审批状态。出车里程和还车里程两个字段是分开存的用于计算单次行程里程注意不是直接存一个本次里程数字段——因为实际使用中经常出现司机忘记记里程的情况分开存两个值至少可以根据上一次的还车里程做交叉校验。审批状态我用了四个值0待审批、1已通过、2已拒绝、3已取消。这里要注意审批流程在车辆管理这种轻量级系统里不太建议引入Flowable这种重量级工作流引擎除非你们的审批链路特别复杂比如涉及多级审批、条件分支。如果只是发起申请-管理员审批这种简单模式用状态字段加一张审批记录表就完全够了。很多团队一上来就说要用FlowableSpringBoot集成Flowable和UI就能折腾一周典型的过度设计。2.4 加油记录表fuel_record车辆管理里油费通常是最让财务头疼的。设计这张表时我除了保留加油量、加油金额、加油日期、加油站点这些常规字段外还额外记录了当时的车辆里程数。为什么要记里程因为要算百公里油耗。百公里油耗 加油量 / (本次加油里程 - 上次加油里程) * 100这个指标能直接反映车辆的运行状态。同一辆车百公里油耗突然从8升飙到11升基本可以判断是发动机出了问题或者胎压异常该安排检修了。这个逻辑不是拍脑袋想的是实际跑了三个月数据之后总结出来的非常准。2.5 维修保养表maintenance_record维修和保养我放到了同一张表里用一个type字段区分。字段包括车辆ID、类型保养/维修、项目描述、费用、维修厂、开始时间、结束时间、状态。比较关键的是保养提醒逻辑根据车辆的保养周期按里程或按时间结合最近一次保养记录自动生成保养计划。这个提醒逻辑实现起来不复杂MySQL里一条SQL就能查出来哪些车辆需要保养了SELECT * FROM maintenance_record WHERE type 1 AND vehicle_id ? ORDER BY end_time DESC LIMIT 1然后对比当前里程和上次保养里程差值超过设定阈值就提醒。2.6 保险年检表insurance_inspection保险年检单独建表记录保险类型交强险/商业险/车船税、保险公司、保单号、生效日期、到期日期。到期前30天自动生成提醒任务推送给管理人员。我特别建议把保险和年检分成两张表或者至少在表里加一个item_type字段区分。原因是保险和年检的提醒周期不同对应的处理部门也可能不同。合并到一张表后查询会稍微麻烦但表数量少管理成本更低。用item_type字段区分就够了。2.7 违章记录表violation_record违章记录包含违章时间、地点、行为描述、扣分、罚款金额、处理状态。这张表单独建的原因是企业车辆违章处理通常需要走内部审批而且年底财务统计罚款支出时也要用。建表的关键点违章记录要关联司机ID因为大多数企业是谁开车谁负责罚款也是从司机绩效里扣的。关联好司机之后人事部门年底打印司机的违章汇总表就非常方便了。这里还有一个硬要求所有涉及金额的字段油费、维修费、罚款全部用decimal类型Java对应BigDecimal绝对不要用double。别问为什么问就是被坑过用double算钱到最后一定会出现精度问题。3. 后端SpringBootMyBatis的实现登录鉴权、核心接口与缓存策略后端技术栈确定后就是具体落地了。SpringBoot版本我用的2.7系列不推荐上来就用3.x倒不是3.x不好而是3.x要求JDK17起步很多公司还在用JDK8如果你们团队没有强需求上虚拟线程、GraalVM这些新特性SpringBoot 2.7JDK8的组合是最稳妥的。3.1 项目结构按业务模块分包而不是按技术层次分包很多人建项目喜欢建一个controller包、一个service包、一个mapper包把所有Controller塞在一起所有Service堆在一起。小项目还好业务一多就完了找文件全靠CtrlShiftN相关代码散落得到处都是。我这次采用了按业务模块分包的结构com.vehicle ├── common // 通用配置、工具类、异常处理 ├── config // SpringBoot配置类 ├── controller // 接口入口 ├── dto // 数据传输对象 ├── entity // 数据库实体 ├── mapper // MyBatis Mapper接口 ├── service // 业务逻辑层 └── vo // 视图对象等一下我上面说了按业务模块分包但这个项目实际还是按技术层次分包了不是的经过权衡车辆管理系统的功能模块本身不算特别多按技术层次分包在代码量级可控时反而是最清晰的每个开发都知道Controller放哪、Mapper放哪上手成本最低。如果你的系统业务模块非常多比如几十个那就改成按功能模块分包会更合理。这个没有绝对标准团队习惯和项目规模说了算。3.2 登录鉴权JWT是无状态设计的关键车辆管理系统是一个多角色系统管理员、调度员、司机、财务看到的页面和能调用的接口完全不同鉴权是必须的。我用了JWTSpringBoot拦截器的方式实现登录鉴权。JWT的好处是无状态服务端不用存Session前后端分离部署时天然友好。用户登录成功后服务端用密钥签发一个token前端把token存到localStorage里每次请求在header里带上Authorization: Bearer token后端拦截器解析token拿到用户ID和角色放行或拒绝。核心代码其实不复杂重点在于拦截器的注册。拦截器里做三件事放行登录接口、校验token合法性、把用户信息塞到ThreadLocal里方便后续业务方法取用。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是预检请求直接放行 if (OPTIONS.equals(request.getMethod())) { return true; } // 从请求头获取token String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } // 校验token try { Claims claims JwtUtil.parseToken(token); // 将用户信息放入ThreadLocal UserContext.setUserId(claims.get(userId, Long.class)); UserContext.setRole(claims.get(role, String.class)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录已过期请重新登录\}); return false; } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后一定要清理ThreadLocal防止线程池复用导致数据串号 UserContext.clear(); } }这里有个特别容易踩的坑ThreadLocal必须在线程结束前清理。SpringBoot的Controller是基于Tomcat线程池的线程处理完一个请求后会归还到线程池如果不清理ThreadLocal下一个请求复用这个线程时就会读到上一个用户的ID导致数据越权访问。这个问题非常隐蔽线上环境偶发出现排查起来让人崩溃。别问我怎么知道的说多了都是泪。密码加密我用的是BCrypt不是MD5。MD5虽然看起来方便但彩虹表破解太容易了安全性完全不够看。SpringSecurity框架里有现成的BCryptPasswordEncoder单独用也没问题加盐加密两个相同密码加密后的结果都不一样这才是合格的密码存储方案。3.3 核心业务接口设计每一类操作都想清楚owner车辆管理系统的接口设计我总结下来核心就是围绕车辆-人-时间三个维度做CRUD和统计。以出车申请为例这个功能是整个系统的业务核心接口链路比较长司机提交出车申请 - 管理员查看待审批列表 - 管理员通过/拒绝 - 司机出车 - 司机还车 - 系统计算里程和时长后端接口对应的就是createDispatchRecord、approveDispatch、startTrip、endTrip。每个接口都做了状态校验比如approveDispatch里必须判断当前记录状态是待审批防止重复审批。状态流转的校验一定要写在事务方法里并且在改状态的同时更新update_time字段。车辆状态的联动更新也很重要审批通过后车辆状态要切成出车中还车后再切回空闲。这个联动操作要放在同一个事务里要么都成功要么都失败。我用的方式是在Service方法上加Transactional注解同时把车辆状态的修改也放在同一个方法里保证原子性。3.4 MyBatis缓存与拦截器性能优化和通用逻辑的好帮手热词里很多人搜mybatis缓存这块确实值得说道说道。MyBatis的缓存分为一级缓存和二级缓存。一级缓存是SqlSession级别的同一个SqlSession执行相同查询时第二次会直接从缓存里拿结果。在SpringBoot集成环境里每次请求默认会创建一个新的SqlSession单独一条请求内的重复查询能命中但跨请求是没用的作用比较有限。二级缓存是Mapper级别的在多个SqlSession之间共享默认是关闭的。车辆管理系统这种并发量不高、数据变更频率中等的系统可以适当开启二级缓存。但要注意缓存失效策略如果设置不好脏数据问题会让你痛不欲生。我的建议是车辆、司机这些低频变更的主数据可以开出车记录、加油记录这些高频写入的表千万别开。我在这个项目里把mybatis拦截器的能力用了起来实现了一个自定义的自动填充拦截器。MyBatis拦截器Interceptor可以拦截四大对象的方法Executor、StatementHandler、ParameterHandler、ResultSetHandler。我利用它实现了create_time和update_time的自动填充Intercepts({ Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class TimeFillInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; // 获取SQL类型 SqlCommandType sqlCommandType ms.getSqlCommandType(); if (parameter ! null SqlCommandType.INSERT.equals(sqlCommandType)) { // 反射调用setCreateTime和setUpdateTime setFieldValue(parameter, createTime, new Date()); setFieldValue(parameter, updateTime, new Date()); } if (parameter ! null SqlCommandType.UPDATE.equals(sqlCommandType)) { setFieldValue(parameter, updateTime, new Date()); } return invocation.proceed(); } }这样所有表的insert和update操作都不用手动写时间字段了统一由拦截器填充省了巨多的重复代码。网上有很多这样的实现代码这个思路你可以放心借鉴。3.5 数据权限与动态SQL不同角色看不同数据车辆管理系统有一个天然的需求不同角色看到的数据范围不同。管理员能看到全部车辆的记录普通司机只能看到自己提交的出车申请。这个用if标签动态拼接SQL就能实现效果比硬编码在Java代码里好多了。select idlistDispatchRecords resultTypecom.vehicle.vo.DispatchRecordVO SELECT d.*, v.plate_number, dr.name AS driver_name FROM dispatch_record d LEFT JOIN vehicle v ON d.vehicle_id v.id LEFT JOIN driver dr ON d.driver_id dr.id where if teststatus ! null AND d.status #{status} /if if testvehicleId ! null AND d.vehicle_id #{vehicleId} /if if testdriverId ! null AND d.driver_id #{driverId} /if if testuserId ! null and role driver AND d.driver_id #{userId} /if /where ORDER BY d.create_time DESC /select注意这里的角色判断我传入了两个参数userId和role如果当前用户是司机角色就自动加上driver_id userId的条件司机的数据范围天然被限制住了。数据权限在SQL层面做比在Java代码里做过滤要安全得多也高效得多。3.6 内存泄漏与性能陷阱排查写后端时遇到过几次内存问题这里分享一个排查思路。SpringBoot应用跑了一段时间后如果内存占用越来越高不要慌先看三个方向一是看是否有静态集合类在不断add数据比如一个static Map用来做缓存却从不清除二是看ThreadLocal是否在该清理的地方没有清理三是看连接池是否泄漏数据库连接用完没有归还。我这次排查过一次诡异的问题每次调用导出Excel接口后内存就多几百MBGC也回收不掉。最后定位到是POI的Workbook没有被关闭新创建的Workbook占用的堆外内存得不到释放。这类问题定位起来一定要带上jvisualvm或者Arthas直接看内存分配情况比纯靠代码review高效太多了。4. 前端Vue3Element Plus的实现工程化搭建、核心页面与交互细节前端部分我用的Vue3ViteElement PlusPinia这套组合是当前Vue3后台管理系统的主流标配。Vite的开发体验比Webpack强太多冷启动基本秒开HMR也是毫秒级。4.1 工程化搭建别在环境配置上浪费时间很多人在Vue3开发环境搭建上卡壳尤其是Windows环境。热词里搜windows vue3开发环境、vue3安装及环境配置的人非常多这里我把步骤捋一遍。Node.js版本建议18以上Vite5要求Node 18直接在官网下载安装包就可以了实在懒就用nvm管理多版本切换起来灵活不同项目需要的Node版本不一样不用nvm的话你会浪费大量时间在删了重装上。创建项目用Vite官方脚手架npm create vitelatest vehicle-web -- --template vue cd vehicle-web npm install npm install vue-router4 pinia element-plus axiosElement Plus按需导入需要配置unplugin-auto-import和unplugin-vue-components两个插件配置好了之后不用在代码里显式import组件体积也小很多。这里特别要注意如果你用的是TypeScriptVite创建项目时会自动生成env.d.ts文件Element Plus组件自动导入的TS类型声明要额外配置一下不然编辑器会满屏飘红报找不到组件。搜若依vue3 ts报错的朋友大概率就是卡在这个环节。4.2 API层封装统一的响应拦截让前后端联调省心前后端分离项目里axios封装是第一步就要做的事。我封装了一套统一的API请求工具类核心逻辑包括请求时自动附加JWT token、统一处理响应数据、HTTP 401时自动跳转到登录页、统一拦截后端返回的业务错误码并弹出提示。// request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器附加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 // 根据后端约定code为200表示成功 if (res.code 200) { return res } else { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { if (error.response?.status 401) { // token失效跳转登录页 localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络请求异常) return Promise.reject(error) } ) export default service前后端联调时最容易出现的问题就是跨域。我这里用了Vite的代理配置在vite.config.js中把前端请求代理到后端地址// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样前端代码里所有请求都写/api/xxx开发环境代理到8080生产环境用Nginx转发到后端服务。跨域问题在开发阶段就被根治了不用在后端到处配CrossOrigin。4.3 车辆管理页面状态、表单、筛选每一个都是交互细节车辆列表页面是整个系统最基本的页面但要做好也不容易。上表格、全列展示这是最低级的做法。我的设计是顶部放筛选条件车牌号模糊搜索、车辆类型下拉、车辆状态下拉中间是表格区域展示车辆的核心字段状态字段用Tag标签展示不同颜色空闲绿色、出车中蓝色、维修中橙色、报废灰色右侧放操作按钮详情、编辑、申请出车、安排维修点击车辆编号可以展开详情抽屉展示车辆信息、最近出车记录、最近维修记录车辆表单有一个特别注意的点车牌号、发动机号、VIN这些字段要有格式校验。车牌号的正则表达式写起来不复杂但注意要兼容新能源车牌8位最后一位可以是字母很多系统在这里翻车了。// 车牌号校验兼容蓝牌和新能源绿牌 const platePattern /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/4.4 出车申请流程Vue3表单联动与状态控制出车申请是司机角色的核心操作这个页面用了Vue3的reactive和computed做表单联动。司机选择车辆后页面自动展示车辆的当前状态是否为空闲状态如果是维修中则置灰不可选选择司机后自动带出驾驶证号和电话。出发里程这个字段默认值是上一次还车记录的里程数但允许司机修改。表单验证用Element Plus的rules车辆必选、司机必选、出车事由必填字数限制20-200字、预计归队时间不能早于出车时间。如果预计出车时间早于当前时间也会提示出车时间不能早于当前时间。这里有个交互细节当车辆状态不是空闲时选择车辆后要立即给出红色错误提示不要等到提交时才提示。这样司机在第一步就知道选错了车体验好了很多。提交成功后不是弹个消息就完事而是跳转到我的出车记录页面司机能立刻看到这条申请的审批状态。4.5 仪表盘统计报表是管理员的决策入口仪表盘是管理员登录后看到的默认首页我做了四个核心指标卡片和两个图表总车辆数、在途车辆数、维修中车辆数、本月出车次数近7天出车趋势折线图车辆类型分布饼图待办事项列表待审批出车申请、即将到期保险、即将年检车辆、即将到期驾驶证这些统计接口对应后端的一组聚合查询SQL比如-- 查询各状态车辆数量 SELECT status, COUNT(*) AS cnt FROM vehicle GROUP BY status-- 查询近7天出车趋势 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM dispatch_record WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time)图表我用的是ECharts配合Vue3的封装整体效果很专业。ECharts的按需引入一定要做不然整个包几百KB全打进bundle里首屏加载会慢得让人抓狂。可以用echarts/core按需注册也可以用unplugin-auto-import按需全量引入虽然配置省事但Vite打包后的chunk体积能相差一倍以上。5. 部署、配置与踩坑记录MySQL连接细节、跨域和首次运行的坑写完了代码只是第一步真正让系统跑起来、稳定运行才是考验经验的地方。很多人项目代码写完了部署到服务器上就各种报错根本原因是在本地环境太顺利了一些隐藏的环境差异直接暴露无遗。5.1 MySQL安装与连接配置字符集和时区两个大坑热词里很多人搜mysql安装配置教程、mysql安装教程8.0说明这个环节卡住的人不在少数。我这次用的是MySQL 8.0版本下载地址就不放了直接去官网下载就行。安装时注意选择Server only不要把Workbench、Sample Databases这些东西都装上占用资源且没用。MySQL 8.0安装过程中最大的坑是认证方式默认是caching_sha2_password而SpringBoot 2.7自带的mysql-connector-java对旧版本兼容得更好连8.0的默认认证方式可能会报错。解决办法有两个一是在连接字符串里指定useSSLfalse和allowPublicKeyRetrievaltruespring: datasource: url: jdbc:mysql://localhost:3306/vehicle_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue二是创建用户时指定mysql_native_password认证方式CREATE USER vehicle% IDENTIFIED WITH mysql_native_password BY your_password;两个方案选一个就行。我建议直接用方案一不折腾用户权限只改连接串配置就能解决。时区问题也特别常见。MySQL默认时区和Java服务器时区如果不一致会出现日期时间差8小时的问题。在连接串里加上serverTimezoneAsia/Shanghai是标准解法同时服务器和MySQL的time_zone也要设置为东八区。5.2 跨域问题前端后端分离部署时的通关密钥开发环境下我通过Vite代理解决了跨域。生产环境下前后端分离部署两个服务在不同域名或端口上跨域是必然存在的。后端的跨域配置我建议用WebMvcConfigurer而不是CrossOrigin注解。CrossOrigin要加在Controller上加多了代码很乱而且它没法统一处理各种预检请求的细节。全局配置一劳永逸Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意上面的JwtInterceptor配置里需要单独处理OPTIONS请求因为跨域预检请求不会携带token如果拦截器直接校验预检请求就会被拦截导致浏览器报跨域错误。我已经在拦截器的代码里做了if (OPTIONS.equals(request.getMethod())) return true的处理。5.3 使用Nginx部署前后端资源统一纳管生产环境我用了Nginx托管前端静态资源并反向代理后端API。这样用户只需要访问一个域名/端口不用关心前后端各自的服务地址。server { listen 80; server_name vehicle.example.com; # 前端静态资源 root /usr/share/nginx/html; index index.html; # Vue Router history模式配置 location / { try_files $uri $uri/ /index.html; } # API反向代理 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; } }关于Vue Router的history模式生产环境如果不用Nginx的try_files配置刷新页面就会出现404。很多人只配置了前端路由没有配置Nginx对应转发规则一刷新就出错这个问题在部署时几乎必踩。如果不想折腾Nginx也可以用hash模式地址栏多个#号而已但刷新就没问题了。5.4 前端打包体积优化首屏加载从8秒降到2秒首屏加载慢是前后端分离项目的通病。我在这个项目里做了三件事效果立竿见影。第一件是路由懒加载。Vue3的defineAsyncComponent或动态import实现按需加载组件首屏只加载当前路由需要的代码数据统计页面、Excel导出页面这些非核心页面全部懒加载。const routes [ { path: /vehicle/list, component: () import(/views/vehicle/VehicleList.vue) }, { path: /statistics, component: () import(/views/statistics/Statistics.vue) } ]第二件是压缩打包体积。Vite打包时配置代码分割把第三方依赖element-plus、echarts、axios单独拆分出来利用浏览器缓存机制这样用户二次访问时这些不会变的大包直接命中缓存加载速度会快很多。第三件是开启Gzip压缩。Nginx配置gzip on后文本资源传输体积能减少60%-70%效果非常直观。5.5 数据初始化与系统验证别让演示数据坑了第一次上线的你系统第一次部署后数据库表会自动创建吗不会。MyBatis默认不会帮你建表你需要手动执行SQL脚本。我建议把建表语句和初始数据写在一个init.sql文件里部署时统一执行。初始数据至少包含一个管理员账号、几辆车不同状态、几个司机、几条出车记录方便登录系统后直接看到页面效果。上线前有一个验证流程我每次必做登录、退出、token过期后再操作是否正常录入车辆时必填项校验是否生效出车流程完整走一遍申请-审批-出车-还车删除一辆正在出车中的车辆系统是否报错或给出提示提交乱码数据如特殊字符、超长文本会不会导致页面崩溃这些看起来基础但做的和不做的区别是前者上线后敢拍胸脯后者天天接客服投诉电话。6. 系统跑通之后的进阶方向从能用走向好用一套车辆管理系统做完并在公司内部跑起来只能算完成了一半。后面根据实际使用的反馈逐步做版本迭代才能真正满足业务需求。这里分享几个我实际做过或者观察到的进阶方向你可以按需取用。6.1 加入费用统计与财务报表车辆管理系统的最终报表使用者是财务人员。增加月度费用统计表按车辆维度统计油费、维修费、过路费、停车费、罚款并支持按月份、按部门、按车辆类型多维筛选。这个功能的SQL要做得比较细通常需要几个子查询再UNION写的时候注意索引的使用不然数据量一大仍然会慢。热词里搜的mysql排序在报表排序这块就能用上排序字段建立联合索引可以大幅提升查询效率。6.2 对接微信通知审批、提醒全自动触达审批流和到期提醒做得再好如果管理人员频繁刷新系统才能看到使用意愿会大幅下降。我的建议是接入企业微信或钉钉的webhook机器人把待审批通知、保险到期提醒、年检提醒直接推送到手机上。后端实现的核心逻辑是在SpringBoot里加一个定时任务Component public class RemindTask { Scheduled(cron 0 30 8 * * ?) public void sendInsuranceRemind() { // 查询30天内到期的保险记录 ListInsuranceRecord records insuranceRecordMapper.selectExpiringSoon(30); // 组装消息并推送到钉钉/webhook for (InsuranceRecord record : records) { String msg String.format(【车辆管理系统】车辆%s的%s将于%s到期请及时续保, record.getPlateNumber(), record.getInsuranceType(), record.getExpireDate()); webhookClient.send(msg); } } }这个定时任务在SpringBoot里开启非常简单在启动类上加EnableScheduling注解然后在任务方法上配置cron表达式即可。整体效果非常实用。6.3 引入流程图或工作流引擎什么时候才值得如果你的公司车辆审批链路复杂起来了比如需要多级审批、条件分支超过3万的维修费需要总经理审批这时候引入Flowable这类工作流引擎是合理的。Flowable支持BPMN规范的流程定义能在SpringBoot里很好集成。还有个热词flowable springboot ui就是Flowable官方提供的模型设计器可以直接在页面上拖拽画审批流程图不用再用XML写死流程。但我的建议仍然是业务没有明确需求之前不要急着上工作流引擎。工作流引擎的学习成本、运维成本、以及与业务代码的耦合度都相当高为了一个看起来需要的审批功能投入产出比很可能并不理想。6.4 报表与打印锐浪报表这类专业方案能做什么热词里有一条springboot与锐浪报表服务器深度整合实战指南说明不少人在做系统时会遇到报表打印的需求。车辆管理系统里确实有很多单据需要打印派车单、维修结算单、加油明细表等等。前端直接用浏览器的window.print()能实现基本打印但遇到复杂的套打、分页、自定义纸张、打印精度要求高时体验就差很多了。这时候就需要锐浪报表GridReport这类专业报表工具。它可以由后端动态生成报表数据再配合报表模板实现精确打印。这个方案的坑在于版式设计阶段比较复杂模板需要花时间细细调节但一旦模板做好后续的维护非常稳定。如果你的项目报表需求突出值得考虑这类工具。6.5 对接GPS定位与电子围栏管理车辆出车轨迹时接入GPS设备数据是一个常见方向。有GPS定位模块的车载终端通常会提供开放API供后端拉取车辆位置数据。SpringBoot定时任务定时拉取各车的坐标存入车辆实时位置表前端地图高德/百度/腾讯根据坐标绘制轨迹。电子围栏功能则是在后端设置一个多边形区域判断车辆当前坐标是否在围栏内超出时自动告警。这块的核心难点其实不在代码而在于GPS设备选型和数据对接协议的稳定性。不过电子围栏的算法本身倒是不难用射线法判断坐标是否在多边形内部几十行代码就搞定了。6.6 考虑移动端适配管理人员经常需要在外出时审批出车申请司机也需要在路上查看自己的用车记录。可以考虑用移动端H5的方式复用现有的后端接口前端单独写一套适配移动端的页面即可不需要单独做App。H5的页面体积控制在很小的范围内微信浏览器或企业微信里嵌入使用体验完全可以接受。技术选型上移动端可以继续用Vue3Vant组件库Vant对移动端的适配做得比较完善开发速度也很快。后端接口基本不用改只需要为移动端增加几个特定的聚合接口即可。7. 这套源码沉淀下来的经验教训项目做完后回头看踩过的坑和最终沉淀的心得都值得记录下来这部分内容算是这套源码之外最值钱的部分。第一主数据表字段设计要留足冗余。车辆信息表里的brand和vehicle_type字段前者存的是品牌型号字符串后者存的是类型编号。实际使用中品牌型号是相对固定的基础数据但类型很可能会调整。留冗余字段、加索引、加创建时间这样后期做统计报表时扩展性会好很多。第二所有列表查询必须分页。很多目录查询接口出车记录、维修记录、加油记录数据量慢慢涨起来后一次查全表会导致前端渲染卡顿。SpringBoot用PageHelper或者MyBatis的分页插件就能实现前端传pageNum和pageSize后端统一返回分页对象。第三操作日志一定要做。谁在什么时间审批了哪个司机的出车申请、改了什么字段都得能追溯。出现纠纷时日志是唯一的裁决依据。实现方式可以用AOP切面统一处理也可以用拦截器在业务层手动记录。前者侵入性小后者更可控我建议核心业务的手动记录通用的自动记录用AOP。第四配置文件里的敏感信息要用环境变量。数据库密码、JWT密钥这些不能直接写在application.yml里提交到代码仓库。用${MYSQL_PASSWORD}这种占位符部署时通过环境变量注入。这点在团队协作时尤其重要不然离职一个开发公司的数据库密码就得全改一遍。第五代码注释和接口文档同步维护。车辆管理系统这种业务型项目代码逻辑本身不复杂但业务规则复杂。每个接口都要写明入参、出参、业务逻辑、状态流转否则半年后自己都看不懂自己写的代码。推荐用Swagger/OpenAPI自动生成接口文档但swagger注解要写完整才能保证文档与实际接口保持一致。第六数据库备份策略要提前定好。车辆管理系统的数据量虽然不大但都是重要的业务数据。至少要每天凌晨做一次全量备份保留最近30天数据。Linux下用crontab一行命令就能搞定。0 2 * * * mysqldump -u vehicle -ppassword vehicle_db /backup/vehicle_$(date \%Y\%m\%d).sql生产环境的数据备份只有两条原则自动执行、定期验证恢复。备份文件不验证是否可恢复等于没备份。最后说回这套系统本身。如果你需要拿它作为学习资料或者二次开发的基础建议按这样的顺序去读源码先看数据库表结构理解业务模型再看后端的Controller理解接口定义接着看Service方法理解业务逻辑最后看前端页面理解交互和数据流转。沿着表-接口-逻辑-页面这条链路走一遍整个系统就完全吃透了。然后你就可以根据自己的业务场景在这个骨架上添加新的功能模块增删改查、统计报表、流程审批在这个系统之上做定制比从零起步省太多时间了。