ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue的车辆管理系统实战:从数据库设计到前后端部署

基于SpringBoot+Vue的车辆管理系统实战:从数据库设计到前后端部署 做企业车辆管理系统前前后后我也折腾过好几版。最开始用的是纯JSPServlet那套老古董后来转SSH再后来终于落到SpringBootVueMyBatisMySQL这个组合上这次把整套源码重新整理了一遍也把开发过程中遇到的各种坑都填平了。这套系统覆盖了车辆档案管理、用车申请审批、派车调度、加油维修保险记录、驾驶员管理、年检保险到期提醒、数据统计看板等核心功能前后端完全分离拿来就能跑。适合正在做毕业设计、公司内部想快速搭一套车辆管理后台、或者刚学完SpringBoot和Vue想找个完整项目练手的朋友直接照着源码梳理一遍基本就能把SpringBootMyBatisMySQL这条技术栈串明白了。很多人问我为什么车辆管理系统这种项目要用前后端分离直接把页面塞进Thymeleaf模板里不是更省事吗确实对一个小型后台管理系统来说服务端渲染的方案开发起来更快部署也简单。但实际做下来你会发现车辆管理牵扯到的角色跨度特别大——行政专员要用电脑批量导入车辆信息司机在手机端要看自己的派车任务老板要看驾驶舱数据大屏财务要导出加油维修明细。这种多端展示、多角色交互的场景前端分离之后接口复用率非常高一套后端接口同时喂给PC管理端和移动端后端代码写一遍就够了不用给每个端单独写模板页面。另外Vue生态的组件库和图表库确实好用一套成熟的后台模板改改就能用比手写jQuery操作DOM痛快太多。这套系统的价值不只是代码本身更重要的是它把“车辆”这个资源的完整生命周期给串起来了。买车、上牌、建档到日常用车、加油、保养、维修、年检、保险再到最后的报废、处置每一步都有数据支撑。下面我把整个项目的设计思路、核心实现和踩坑经验从头到尾拆开来聊。1. 项目整体设计与技术选型思路1.1 这套系统解决的核心问题企业车辆管理看着简单不就是登记一下车辆信息、记录一下出车情况吗真做起来完全不是那么回事。我调研过好几家公司的实际需求发现痛点集中在三块。第一块是车辆底数不清。公司几十台车分散在各个部门使用谁在用、车在哪、什么时候该保养了、保险什么时候到期全靠行政人员拿Excel记。一换人就全乱套资料交接出问题车险脱保了都没人知道。第二块是用车流程不规范。员工用车靠口头申请、纸质审批事后报销对不上私车公用和公车私用混在一起。第三块是成本核算没依据。每台车每个月加油花了多少、维修花了多少、百公里油耗是多少没有台账就分析不了年底做预算只能拍脑袋。所以这套源码在功能设计上就是冲着这三块痛点去的用车辆档案模块管好“底数”用申请-审批-派车-归还的闭环管好“流程”用加油/维修/保险/年检台账管好“成本”。一条线串下来数据自然就齐了。1.2 为什么锁定SpringBootMyBatisMySQL这套组合选SpringBoot不用多解释它把Spring配置那一大堆XML全简化掉了内置Tomcat打成Jar包就能跑开发体验比传统SSM好了一个量级。我早期做SSM项目光配Spring的Bean扫描、事务管理、MyBatis的SqlSessionFactory就得折腾半天SpringBoot自动配置一上来写个application.yml就全部搞定。选MyBatis而不是JPA核心原因有两个。第一车辆管理系统的查询条件非常灵活。比如车辆列表要支持车牌号模糊搜索、车辆类型筛选、状态筛选、所属部门筛选用户搜“京A”可能还想同时匹配车牌和司机姓名。这种查询用MyBatis的动态SQLif标签一拼条件随心组合SQL完全可控。用JPA写规格查询或者衍生查询条件一多那个方法名就长到没法看复杂的连表统计更是别扭到不行。第二我对SQL有掌控欲。车辆管理涉及多张表的关联统计比如“某台车本月加油总金额”直接在SQL里写SUM和GROUP BY性能如何、怎么走索引我都心里有数。MyBatis就是把SQL放回开发者手里这种做法对业务系统来说是最稳妥的。MySQL就没什么好纠结的中小型企业内部系统数据量撑死几十万条MySQL完全扛得住部署简单、运维成熟、学习资料也多。真要到了分布式微服务那种量级再考虑换更重的数据库也不迟。1.3 项目目录结构怎么组织这套源码包结构遵循了比较常规的规约式分层后端按controller - service - mapper - entity来切com.company.vehicle ├── common // 通用返回结果、异常处理、工具类 ├── config // SpringBoot配置类、拦截器注册、CORS跨域处理 ├── controller // 接口入口只做参数校验和结果包装 ├── dto // 前端传入的对象用于接收查询条件和表单数据 ├── entity // 数据库表映射实体 ├── mapper // MyBatis的Mapper接口对应XML文件 ├── service // 业务逻辑层事务边界在这里控制 └── utils // JWT工具、日期工具等前端Vue项目单独一个目录分成api、views、components、router、store几个模块。找我拿源码的同学我建议你第一遍不要急着跑起来先按这个结构把代码读一遍读完再动手会顺得多。2. 数据库设计车辆管理系统的核心表结构数据库设计是这套系统的地基地基打不好后面写代码全是补丁。车辆管理系统核心表我这边一共规划了八张用户表、车辆信息表、司机表、用车申请表、派车记录表、加油记录表、维修保养表、保险年检表。这里详细拆几张关键表的设计思路。2.1 车辆信息表的设计要点车辆信息表是整张数据模型的根所有跟车相关的业务都挂在它下面。表结构核心字段大概是这样CREATE TABLE vehicle_info ( id int(11) NOT NULL AUTO_INCREMENT, plate_number varchar(20) NOT NULL COMMENT 车牌号, vehicle_type tinyint(4) DEFAULT NULL COMMENT 车辆类型1轿车 2SUV 3商务车 4货车, brand varchar(50) DEFAULT NULL COMMENT 品牌, model varchar(100) DEFAULT NULL COMMENT 车型, vin varchar(50) DEFAULT NULL COMMENT 车架号, engine_number varchar(50) DEFAULT NULL COMMENT 发动机号, buy_date date DEFAULT NULL COMMENT 购置日期, buy_price decimal(10,2) DEFAULT NULL COMMENT 购置价格, seat_count int(11) DEFAULT NULL COMMENT 座位数, current_mileage decimal(10,1) DEFAULT NULL COMMENT 当前里程数, status tinyint(4) DEFAULT 1 COMMENT 车辆状态1可用 2已申请 3维修中 4已报废, department_id int(11) DEFAULT NULL COMMENT 归属部门ID, remark varchar(500) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_plate_number (plate_number), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆信息表;车牌号加唯一索引是必须的这个字段就是车辆的身份证业务上绝不允许重复。状态字段单独建索引是因为列表页最常用状态做筛选几十万条数据以后这个索引能省不少时间。create_time和update_time建议都配上后面排查数据问题的时候能救命。2.2 用车申请、审批与派车流程的库表设计车辆管理的业务核心在流程上。我这边设计了两张表一张是员工提的用车申请单另一张是管理员操作后的派车/归还记录。两表分离是因为一次申请可能因为实际情况拆成多次派车或者一次申请没有通过但不代表业务没发生过。用车申请表核心字段包括申请单号、申请人ID、申请部门、用车事由、出发地、目的地、计划用车开始时间、计划结束时间、申请状态。值得注意的是我加了vehicle_id这个字段但允许为空因为业务上存在“由管理员统一派车”的场景员工申请的时候可以不指定车由调度人员来分配这种灵活性在真实业务里需求非常大。派车记录表用于记录实际出车情况字段包括关联的申请单号、车辆ID、司机ID、实际出发时间、实际归还时间、出发时里程数、归还时里程数、油耗备注。这张表也是后续统计车辆使用率、计算行驶里程的核心数据源。这里要说一个经验之谈不要把申请和派车塞进一张表。我第一版设计的时候偷懒申请单里改了状态就直接当派车记录用结果出现“申请不通过还要留底”“同一申请分段派车”这类需求时代码改得非常痛苦。流程单据分开设计每张表管好自己的生命周期这是从坑里爬出来的教训。2.3 加油、维修、保险耗费记录的建模思路这三块属于车辆运营成本数据表结构上有一个共同点都通过vehicle_id关联到车辆信息表另外都记录金额、发生时间、经办人这样月底财务对账时直接按车辆分组SUM就能拉出单车成本表。以加油记录表为例核心字段有车辆ID、加油日期、加油量升、加油金额、当前油价、加油时里程数、加油站、加油类型92号/95号/柴油/电车充电费用。算百公里油耗的SQL用加油量和区间里程一除就出来SELECT v.plate_number, SUM(f.fuel_volume) AS total_fuel, MAX(f.mileage) - MIN(f.mileage) AS driving_range, ROUND(SUM(f.fuel_volume) / (MAX(f.mileage) - MIN(f.mileage)) * 100, 2) AS fuel_per_100km FROM fuel_record f LEFT JOIN vehicle_info v ON f.vehicle_id v.id WHERE f.vehicle_id #{vehicleId} AND f.fuel_date #{startDate} AND f.fuel_date #{endDate} GROUP BY f.vehicle_id, v.plate_number;维修保养表则要把保养类型保养/小修/大修/事故维修、维修项目明细、配件费、工时费、维修厂、维修日期等拆清楚。保险表重点记录保险类型交强险/三者/车损、保险公司、保单号、起保日期、到期日期并且要设置到期提醒逻辑并提前三十天在前端列表打上“即将到期”的标记。3. 后端核心模块实现认证、权限与车辆业务闭环3.1 登录鉴权与接口权限控制方案市面上现成的Sa-Token、Shiro这些权限框架我都用过最终这套源码里我选择了JWT SpringBoot拦截器自己实现轻量级权限控制。车辆管理系统角色就三种管理员、调度员、普通员工RBAC一句话就能说完引入重框架反而增加学习成本。具体做法是在用户登录成功后用JWT生成一段Token里面带上用户ID、用户名、角色编码设置过期时间返回给前端存起来。后端写一个拦截器拦截所有/api/**请求从Header里拿出Token解析校验通过就放行然后把用户信息塞进ThreadLocal后面业务代码随时可以拿当前登录人信息。角色权限控制我用一个自定义注解RequireRole(admin)标记在需要管理员权限的接口上拦截器里判断用户角色是否匹配。比如车辆报废删除这种敏感操作只允许管理员普通员工调用就直接返回“无权限”。这套方案简洁、够用而且面试的时候被问到原理也能讲得清楚。你要是想深入学习建议把JWT的三段式结构Header、Payload、Signature搞清楚再理解一下为什么不建议把敏感数据明文放进Payload里。3.2 车辆状态流转的核心逻辑车辆状态是这套系统的灵魂字段。我定义了四种状态可用、已申请、维修中、已报废。状态流转的规则是可用车辆被申请审批通过后变成“已申请”状态派车实际出发后因为车已经出去了状态维持“已申请”或者叫“使用中”看你业务定义归还车辆后恢复为“可用”车辆报送修时切到“维修中”只有在维修中或者可用的状态下才能提交报废。这个状态机逻辑一定要写在Service层统一管理不能散落在各个Controller里。我在Service层封装了applyVehicle、approveApplication、returnVehicle、sendToRepair、scrapVehicle几个方法每个方法里都做了状态合法性校验防止脏数据。比如一台已经在维修中的车就不允许再被别人申请如果编码实现不严谨前端页面可以防一手但接口防不住很容易出现并发脏状态。3.3 MyBatis动态SQL在复杂查询中的实战姿势车辆列表的筛选条件是典型的动态SQL场景我直接贴一段核心代码select idlistVehicles resultTypecom.company.vehicle.dto.VehicleQueryDTO SELECT v.id, v.plate_number, v.vehicle_type, v.brand, v.model, v.buy_date, v.buy_price, v.current_mileage, v.status, d.name AS department_name, (SELECT COUNT(*) FROM vehicle_apply va WHERE va.vehicle_id v.id AND va.apply_status 2) AS used_times FROM vehicle_info v LEFT JOIN department d ON v.department_id d.id where if testplateNumber ! null and plateNumber ! AND v.plate_number LIKE CONCAT(%, #{plateNumber}, %) /if if testvehicleType ! null and vehicleType ! 0 AND v.vehicle_type #{vehicleType} /if if teststatus ! null and status ! 0 AND v.status #{status} /if /where ORDER BY v.create_time DESC /select这段SQL里面有几个细节值得注意。where标签会自动剔除第一个条件前面的AND这个可以省很多事。LIKE CONCAT(%, #{plateNumber}, %)这么写而不是直接在Java层拼好%京A%再传进来主要是为了防止SQL注入确保参数化查询完全生效。子查询统计用车次数这种写法数据量不大的时候性能没问题但如果表很大就要考虑用LEFT JOIN GROUP BY替代避免每行都跑一次子查询。MyBatis干这活的精髓就是“动态SQL完全可控”你再也不用担心JPA那个自动生成的SQL乱加JOIN导致性能雪崩了。3.4 定时任务与到期提醒实现车辆年检、保险到期这种提醒不能光靠用户登录时才去查。我用SpringBoot自带的Scheduled注解做了两个定时任务一个每天凌晨扫描保险表和年检记录把三十天内到期或者已经过期的数据写入消息通知表另一个每天汇总昨天的加油和维修数据写入车辆月度统计表减少前端查询时的计算压力。写Scheduled有一个特别容易翻车的细节——默认是单线程串行执行的。如果你在代码里定义了多个定时任务跑得慢的那个会把后面的任务堵住。解决方法就是配置一个线程池让任务并行跑或者把耗时的统计任务放到异步线程里执行Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }这种坑通常不在书里写但不配置的话等到任务多了你会发现该发的消息延迟了闹钟变成哑巴。4. 前端Vue项目从页面搭建到接口联调4.1 Vue项目结构与路由设计前端我用的Vue 2 Element UI这套组合虽然Vue 3已经出来很久了但Element UI的组件生态对后台系统来说依然能打网上的资料也多遇到问题搜一下基本都有答案。如果你是2025年刚开始学前端直接从Vue 3 Element Plus起步也没问题接口设计完全不受影响。项目结构上我保持了常规的拆法src ├── api // 每个模块的接口请求方法 ├── components // 公共组件文件上传、车辆选择器、日期范围 ├── router // 路由配置带权限守卫 ├── store // Vuex状态管理存储用户信息和Token ├── utils // axios封装、日期格式化等工具 └── views // 页面组件 ├── dashboard // 数据看板 ├── vehicle // 车辆管理 ├── apply // 用车申请 ├── dispatch // 派车管理 ├── cost // 油费/维修/保险台账 └── system // 用户和角色管理路由守卫这块是前端权限控制的核心。在router.beforeEach里判断用户没登录一律重定向到登录页登录了但没有访问某页面的权限就跳到403页面。菜单是根据用户角色动态生成的管理员看到全部菜单普通员工只能看到“用车申请”和“我的申请记录”。4.2 车辆列表页的组件化拆解以车辆管理列表页为例前端的核心是把页面拆成四个组件搜索栏SearchBar、表格Table、分页Pagination、详情对话框DetailDialog。这样做的好处是每个组件只干一件事后面加需求或者改样式不会牵扯一大片代码。这里有一个开发效率上的心得先写好一个通用的列表页面模板后面油费记录、维修记录、申请记录这些列表页直接复用这套模板只是把表格列配置和API接口换掉开发效率能翻两三倍。源码里我封装了一个PageWrapper组件搜索表单、表格数据加载、分页跳转的逻辑全部封装进去子页面只需要传配置这也是这套源码让我自己最满意的一点。4.3 axios封装与登录状态管理前端请求后端接口最大的两个痛点是Token往哪放、接口报错怎么统一处理。我的做法是把axios实例单独封装在utils/request.js里通过请求拦截器从Vuex取出Token塞到Authorization请求头里service.interceptors.request.use(config { const token store.state.user.token if (token) { config.headers[Authorization] token } return config })响应拦截器统一处理后端返回的结构业务状态码200表示正常其他状态码弹Message提示HTTP 401表示Token失效自动跳到登录页网络异常则提示“网络连接失败”。这么一套下来每个页面的请求代码只剩下“调API接口、把返回值赋值给数据、搞定”不必每个请求都写重复的错误处理逻辑。4.4 车辆看板页面与ECharts图表实战数据看板是给老板看的东西视觉上要直观。首页我放了四个统计卡片车辆总数、今日出车次数、本月维修费用、待办审批数量下面挂两个图表——一个按车辆类型分布的饼图一个近六个月的加油费用趋势折线图。ECharts的使用有一个容易踩的坑就是图表容器初始化的时候如果父元素还没渲染完成宽度会计算成0图表就缩成一团。我的解决办法是在nextTick后再初始化图表并且监听窗口的resize事件调用chart.resize()。要是页面里用了v-if控制切换离开页面时要记得销毁图表实例否则会有内存泄漏。这些细节不处理的话看起来就是“图表偶尔正常偶尔抽风”排查了半天最后发现是生命周期的问题。5. 环境配置与本地跑通MySQL安装、IDEA导入与启动顺序5.1 MySQL安装与数据库初始化源码附带的sql目录里有一个完整的初始化脚本里面包含建库、建表、插入初始管理员账号和示例车辆数据。你本地只需要装好MySQL用Navicat或者命令行执行脚本就行。执行完记得检查一下vehicle_user表里是不是多了一个admin用户初始密码我统一设置为123456登录后建议立刻在用户管理里改掉。MySQL版本建议5.7或者8.0都可以两个版本在这套源码上跑起来没有差异。网上关于MySQL安装的教程多如牛毛但实际遇到最多的坑集中在服务启动了但连不上、密码策略太复杂导致123456这种弱密码不被接受、my.ini里编码没改成utf8mb4导致中文乱码。密码策略这块安装时选择“Use Legacy Authentication”就能避免后面一堆连接协议兼容性问题。5.2 SpringBoot配置文件的坑与YAML参数说明后端核心配置就是application.yml你需要改的地方只有三处数据源连接地址、数据库用户名密码、以及JWT的密钥。spring: datasource: url: jdbc:mysql://localhost:3306/vehicle_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.company.vehicle.entity configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-please-change-me expire-hours: 24map-underscore-to-camel-case: true这个配置极其关键。数据库字段是plate_number这种下划线命名Java实体里是plateNumber驼峰命名这一行配置开启后MyBatis回填结果时自动映射不用再手动写一堆resultMap。serverTimezoneAsia/Shanghai这个串也很重要MySQL 8.0以上版本不指定时区经常报错加上它省心。5.3 Vue依赖安装与打包后如何放进SpringBoot前端跑起来很简单进入vue-vehicle目录执行npm install npm run serve不过npm install如果要装一两个小时甚至报错大概率是镜像源的问题。设置一下国内镜像源npm config set registry https://registry.npmmirror.com然后再装速度会快非常多。开发模式下前后端分离前端跑在8080端口后端跑在8081端口跨域问题用两种方式解决。后端层面我在Config里配置了CORS允许跨域。前端层面vue.config.js里配置了devServer代理把所有/api开头的请求转发到后端地址开发环境我推荐用代理方式因为可以连带解决Cookie和Header的传递问题。打包部署时先把前端打成静态文件npm run build生成到dist目录。然后把dist里的文件全部复制到后端src/main/resources/static下重新打包后端Jar。这样整个系统就变成一个Jar包SpringBoot自动把static目录作为静态资源根目录前后端就合并成一个服务了部署只需要一个Jar包加一份MySQL配置省事太多。6. 常见问题与排查技巧实录6.1 SpringBoot版本太高导致启动失败很多同学拿着源码去跑第一关就卡在启动上。如果你用的是SpringBoot 2.7甚至3.0以上的版本和这套源码里的2.3.x依赖一冲突经常报各种Bean创建异常、包找不到的错误。这里给一个最稳的操作不要动代码里的依赖版本直接用源码附带的pom.xml让Maven下载对应版本。换句话说用IDEA导入项目的时候先确认你本机Maven的settings.xml配置的是国内镜像源然后项目右键Maven - Reload Project等依赖全部解析完再启动。如果是你自己从零搭建的SpringBoot 3.x版本这又另说——SpringBoot 3把javax包换成了jakarta很多老代码要跟着改所以一套成熟的源码不会莫名其妙把版本升得很高。6.2 MyBatis查询结果为null或映射不上的排查思路MyBatis查出来结果全是null大概率不是SQL没查出来而是映射没对上。优先检查三件事配置文件是否开启了map-underscore-to-camel-case实体类字段类型跟数据库字段类型是否匹配比如数据库是datetimeJava用LocalDateTime接收需要确认MyBatis版本是否支持Mapper接口的方法名和XML里的id是否一致参数是否有Param注解。还有一个隐形坑如果查询结果里包含create_time这类字段Java实体里也要有对应的createTime属性少了就静默丢失不报错一定要仔细对一遍实体类。6.3 前端页面显示正常但接口返回404或跨域报错出现这种问题先按顺序排查这四点后端是否启动了、启动在哪一个端口、前端代理是否指向了同一个端口、请求路径是否正确。我调试的时候最常用的一招是直接打开浏览器F12看Network面板把请求URL复制出来在浏览器新标签页直接访问一遍——如果直接访问通说明问题出在前端代理如果直接访问都不通说明是后端路由有问题。跨域报错这个事开发期用代理能解决部署期因为前端资源已经塞到后端静态目录了根本不存在跨域。所以如果部署后还报跨域思路就该往反向代理或者网关那头检查了。6.4 车辆状态并发问题流量不大也架不住两个人同时点同一台车的“申请用车”就会出现可用车辆被重复申请的问题。解决办法是在申请审批通过后更新车辆状态的SQL里加一个状态条件作为乐观锁UPDATE vehicle_info SET status 2 WHERE id #{vehicleId} AND status 1如果影响行数为0说明车辆已经被别人先手改状态了这时候直接给前端返回友好提示“车辆已被申请请刷新列表”。这个“更新一行如果影响行数为0就说明并发冲突”的思路是小系统里最实用、也最轻量的并发控制手段。这套系统熬夜赶工了大半个月推倒重来的次数也不少最后沉淀出来的经验是车辆管理系统听起来小但它把流程管理、资源调度、成本核算全串在一起了麻雀虽小五脏俱全。你拿到源码后建议先花两个小时把数据库表结构和表之间的关系过一遍再看后端业务流程的闭环。对照着自己公司或者学校里的实际需求在这个源码基础上再加定制功能。如果你想练手优先改这几个方向把车辆状态机完善成更复杂的流程引擎、加一个司机端小程序、或者把月度统计报表做得更精细。做系统最有意思的地方永远是下一个版本还能更好。
返回列表