三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

SpringBoot+Vue车辆管理系统开发实践与优化

SpringBoot+Vue车辆管理系统开发实践与优化

1. 项目背景与核心价值

这个车辆管理系统是我去年为一个中型物流公司做的内部管理平台。当时他们还在用Excel表格记录200多辆货车的调度、维修和年检信息,经常出现数据丢失和版本混乱的问题。最夸张的一次,因为司机看不到最新的年检到期提醒,导致3辆货车被交管部门扣留,直接损失了15万的运输合同。

系统采用SpringBoot+Vue的前后端分离架构,实现了从车辆档案、司机管理到维修保养、违章记录的全生命周期管理。特别在数据可视化方面,通过ECharts实现了车辆状态实时监控看板,让调度主管能一眼看到哪些车可用、哪些在维修、哪些临近年检。

提示:这类管理系统最核心的价值不在于技术复杂度,而在于如何通过合理的字段设计和状态流转,把线下混乱的业务流程数字化。比如我们把年检提醒拆分成"到期前30天""到期前7天""已超期"三级预警,直接降低了90%的逾期情况。

2. 技术栈选型解析

2.1 为什么选择SpringBoot+MyBatis组合

后端选用SpringBoot 2.7 + MyBatis-Plus主要基于三个考量:

  1. 快速响应需求变化:物流行业政策调整频繁(比如去年新增了冷链车辆消杀记录要求),MyBatis的XML动态SQL比JPA的注解方式更灵活
  2. 历史数据兼容:客户原有部分数据在SQL Server中,MyBatis的多数据源支持比JPA更友好
  3. 性能优化空间:针对车辆轨迹这种大数据量表,可以直接手写优化SQL

实测中发现一个有意思的现象:使用MyBatis-Plus的LambdaQueryWrapper比原生MyBatis平均节省30%的代码量,但在多表复杂查询时,反而直接写XML更清晰。比如这个统计各车队维修成本的SQL:

<select id="getRepairCostByFleet" resultType="map"> SELECT f.fleet_name, SUM(r.cost) as total_cost FROM repair_records r JOIN vehicles v ON r.vehicle_id = v.id JOIN fleets f ON v.fleet_id = f.id WHERE r.repair_date BETWEEN #{startDate} AND #{endDate} GROUP BY f.id HAVING total_cost > #{minCost} </select>

2.2 Vue前端的技术取舍

前端用Vue3 + Element Plus主要解决两个痛点:

  1. 表单复杂度高:单个车辆新增表单包含78个字段,通过动态表单组件实现步骤拆分
  2. 权限粒度细:不同角色看到不同的操作按钮(如只有财务能看到成本分析)

特别推荐一个实用技巧:用Vue的provide/inject实现跨层级组件通信。比如当在车辆列表页点击"维修记录"时,维修模块需要知道当前选中车辆ID,但两者隔着多层路由组件:

// 父组件提供数据 provide() { return { currentVehicleId: computed(() => this.selectedVehicleId) } } // 孙子组件注入使用 inject: ['currentVehicleId']

3. 数据库设计关键点

3.1 核心表结构设计

车辆管理系统的数据库有12张核心表,其中最复杂的是车辆状态变更记录表。最初设计时犯了个错误——把所有状态变更都放在一个json字段里,导致无法做状态流转分析。后来拆分成这样:

CREATE TABLE vehicle_status_log ( id BIGINT PRIMARY KEY, vehicle_id BIGINT NOT NULL, old_status ENUM('IDLE','ON_ROUTE','MAINTENANCE','SCRAPPED'), new_status ENUM('IDLE','ON_ROUTE','MAINTENANCE','SCRAPPED'), change_reason VARCHAR(255), operator_id BIGINT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (vehicle_id) REFERENCES vehicles(id), INDEX idx_vehicle (vehicle_id), INDEX idx_status_change (old_status, new_status) );

这个设计带来了三个好处:

  1. 可以统计各状态的平均停留时间(比如发现车辆平均维修周期是4.7天)
  2. 能追溯异常状态变更(比如谁把报废车辆重新设为运营状态)
  3. 基于复合索引的查询性能提升5倍

3.2 MySQL性能优化实践

当车辆数据超过10万条时,我们遇到了三个典型性能问题及解决方案:

  1. 模糊查询慢:车牌号搜索LIKE '%A123%'用时3.2秒
    → 解决方案:增加倒排索引ALTER TABLE vehicles ADD FULLTEXT INDEX idx_plate_reverse (reverse_plate)

  2. 统计报表超时:月度运营报表计算超时
    → 解决方案:用物化视图提前计算CREATE MATERIALIZED VIEW mv_monthly_stats...

  3. 并发更新冲突:多个调度员同时抢单导致乐观锁重试
    → 解决方案:引入SELECT FOR UPDATE SKIP LOCKED

4. 典型业务场景实现

4.1 车辆维修流程状态机

维修流程包含6个状态和11种转换规则,我们用状态模式实现:

public interface RepairState { void startRepair(RepairContext context); void completeInspection(RepairContext context); // 其他动作... } @Component @Scope("prototype") public class WaitingInspectionState implements RepairState { @Override public void completeInspection(RepairContext context) { if (context.getRepair().getInspectionResult() == PASS) { context.setState(applicationContext.getBean(WaitingPartsState.class)); } else { context.setState(applicationContext.getBean(RejectedState.class)); } } }

配合Spring的StateMachine框架,最终实现了这样的转换配置:

states: - WAITING_INSPECTION - WAITING_PARTS - IN_REPAIR transitions: - source: WAITING_INSPECTION target: WAITING_PARTS event: INSPECTION_PASS

4.2 基于规则引擎的年检提醒

年检规则存在三个易变点:

  1. 不同车型年检周期不同(货车1年,客车6个月)
  2. 不同地区政策差异(如京津冀要求额外环保检测)
  3. 临时政策调整(如疫情期间延期)

我们采用Drools规则引擎实现动态配置:

rule "TruckAnnualInspection" when $v : Vehicle(type == "TRUCK", lastInspectionDate before[1y] today) then insert(new InspectionAlert($v, "ANNUAL")); end

管理员可以在后台直接修改规则文件,实时生效无需重启服务。

5. 踩坑与优化实录

5.1 MyBatis分页缓存陷阱

使用PageHelper分页时遇到一个隐蔽bug:当连续执行两个不同条件的查询时,第二个查询会错误地使用第一个查询的分页参数。原因是PageHelper的静态方法使用了ThreadLocal。

解决方案有两种:

  1. 每次查询后手动清理PageHelper.clearPage()
  2. 改用更安全的Lambda方式:
PageInfo<Vehicle> page = PageHelper.startPage(1, 10) .doSelectPageInfo(() -> mapper.selectByExample(example));

5.2 Vue表格内存泄漏

在车辆列表页发现内存持续增长,原因是:

  • 每行使用了自定义组件
  • 组件内监听了window.resize事件
  • 切换页面时未正确销毁

最终解决方案:

// 错误写法 - 会导致内存泄漏 mounted() { window.addEventListener('resize', this.handleResize) } // 正确写法 beforeUnmount() { window.removeEventListener('resize', this.handleResize) }

6. 部署与监控方案

6.1 基于Docker的部署架构

生产环境采用多容器部署:

version: '3' services: app: image: openjdk:17-jdk volumes: - ./logs:/app/logs deploy: resources: limits: memory: 2g mysql: image: mysql:8.0 environment: MYSQL_MAX_CONNECTIONS: 200

关键配置项:

  • MySQL连接池上限设为200(根据压测结果)
  • JVM内存限制2GB(避免容器OOM被杀)
  • 日志卷映射到宿主机

6.2 监控指标埋点

通过Spring Boot Actuator暴露的指标中,我们特别关注:

  1. http.server.requests:统计各API耗时
  2. jdbc.connections.active:数据库连接池使用率
  3. cache.size:本地缓存命中率

配置Grafana看板时发现一个有用的小技巧:对分页查询的耗时监控,需要过滤掉page和size参数,否则会产生大量时间序列:

http_server_requests_seconds_count{uri=~"/api/vehicles.*", uri!~".*[?&](page|size)=.*"}

7. 源码结构与关键实现

项目采用标准Maven多模块结构:

vehicle-system ├── vehicle-admin -- 后台管理模块 ├── vehicle-api -- REST接口模块 ├── vehicle-core -- 领域模型 └── vehicle-job -- 定时任务

一个值得分享的设计:在core模块中,我们抽象出VehicleCommand和VehicleEvent两类对象,实现CQRS模式:

public class VehicleCommand { private String plateNumber; private LocalDate purchaseDate; // 其他写操作字段 } public class VehicleEvent { private Long vehicleId; private EventType type; private String operator; // 其他读模型字段 }

这样设计带来两个好处:

  1. 写模型可以保持精简,只包含必要字段
  2. 读模型可以自由扩展,满足各种报表需求

8. 扩展与演进方向

目前系统已在三个物流公司稳定运行,接下来计划:

  1. 接入ELK实现日志分析,特别是对异常状态变更的审计
  2. 用WebSocket实现调度指令的实时推送
  3. 探索用TensorFlow预测车辆故障(基于历史维修数据)

有个有趣的发现:80%的车辆故障集中在20%的车型上,这为我们做预防性维护提供了数据支持。比如某型号冷藏车的压缩机平均每8个月需要更换,我们就在第7个月自动生成预防性维修工单。

← 返回列表