☰
汽车保养管理系统实战:从保养周期算法到工单流转的完整实现
2026/9/25 12:31:28 网站建设 项目流程

汽车保养管理系统实战:从保养周期算法到工单流转的完整实现

汽车保养类系统的技术难点,并不在于页面有多少,而在于两件事:一是把「什么时候该保养」这件事算准,二是把「保养过程」这件事管住。前者是一套基于里程与时间的双维度定时算法,后者是一套工单状态机。本文以 Spring Boot + MyBatis Plus + MySQL 作为后端技术栈,以 UniApp(Vue 语法)作为用户端、Vue + Element UI 作为管理后台,拆解汽车保养系统从数据建模到接口落地的完整实现路径。

一、业务建模:把汽车保养拆成可计算的三张核心表

汽车保养业务看似是「车主约、门店做」的流程,落到代码层面其实只有三层数据:车辆档案、保养项目字典、保养工单。

  • 车辆档案:记录车牌、车型、当前里程、上次保养里程与时间。它是所有提醒计算的输入。
  • 保养项目字典:机油、机滤、空滤、刹车片等,每个项目维护两个周期字段——里程周期与时间周期。
  • 保养工单:把「预约—接单—服务—完工」的全过程串起来,并在完工时回写车辆的里程与时间基线。

这样设计的价值在于:提醒逻辑不依赖任何硬编码规则,全部由字典表驱动。新增一个保养项目时,只需要插一行数据,不需要改动任何一行业务代码。

CREATETABLE`vehicle`(`id`BIGINTNOTNULLAUTO_INCREMENT,`user_id`BIGINTNOTNULLCOMMENT'车主用户ID',`plate_no`VARCHAR(16)NOTNULLCOMMENT'车牌号',`model`VARCHAR(64)DEFAULTNULLCOMMENT'车型',`current_mileage`INTNOTNULLDEFAULT0COMMENT'当前里程(km)',`last_service_mileage`INTDEFAULTNULLCOMMENT'上次保养里程(km)',`last_service_time`DATETIMEDEFAULTNULLCOMMENT'上次保养时间',PRIMARYKEY(`id`),KEY`idx_user`(`user_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='车辆档案';CREATETABLE`maintain_item`(`id`BIGINTNOTNULLAUTO_INCREMENT,`item_code`VARCHAR(32)NOTNULLCOMMENT'项目编码',`item_name`VARCHAR(64)NOTNULLCOMMENT'项目名称',`cycle_mileage`INTDEFAULTNULLCOMMENT'里程周期(km),为空表示不按里程判定',`cycle_month`INTDEFAULTNULLCOMMENT'时间周期(月),为空表示不按时间判定',`sort`INTNOTNULLDEFAULT0COMMENT'排序',PRIMARYKEY(`id`),UNIQUEKEY`uk_code`(`item_code`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='汽车保养项目字典';CREATETABLE`maintain_order`(`id`BIGINTNOTNULLAUTO_INCREMENT,`order_no`VARCHAR(32)NOTNULLCOMMENT'工单号',`vehicle_id`BIGINTNOTNULLCOMMENT'车辆ID',`status`TINYINTNOTNULLDEFAULT0COMMENT'0待接单 1服务中 2待确认 3已完成 4已取消',`appoint_time`DATETIMEDEFAULTNULLCOMMENT'预约时间',`finish_mileage`INTDEFAULTNULLCOMMENT'完工里程',`create_time`DATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(`id`),UNIQUEKEY`uk_order_no`(`order_no`),KEY`idx_vehicle`(`vehicle_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='汽车保养工单';

需要特别说明的是:周期字段允许为NULL。这个细节很重要——有些项目只按里程判定,有些只按时间判定。如果用0表示「不判定」,会和「周期为 0」的语义混淆,后续排查问题时会非常痛苦。

二、汽车保养提醒算法:里程与时间的「先到为准」判定

提醒的核心逻辑是:对每个保养项目,分别计算里程是否到期、时间是否到期,任意一个满足即判定为到期,并把触发原因回传给前端展示。

这里有几个容易被忽略的边界情况:

  1. 当前里程小于上次保养里程:车主换过仪表、录入错误或调表都会造成这种情况,此时差值应为负数,必须直接判定为「数据异常」而不是「到期」。
  2. 上次保养记录为空:新车或首次录入,应按「不判定」处理,否则会给全量用户推提醒。
  3. 刚好等于阈值:应采用>=而不是>,否则到期当天不会触发。
publicclassMaintainReminder{publicstaticDueResultjudge(MaintainItemitem,Vehiclevehicle,LocalDatetoday){IntegerremainKm=null;IntegerremainDays=null;booleanmileageDue=false;booleantimeDue=false;// 里程维度if(item.getCycleMileage()!=null&&vehicle.getLastServiceMileage()!=null&&vehicle.getCurrentMileage()!=null){intdelta=vehicle.getCurrentMileage()-vehicle.getLastServiceMileage();if(delta<0){returnDueResult.error(item,"里程数据异常,请先校准");}remainKm=item.getCycleMileage()-delta;mileageDue=remainKm<=0;}// 时间维度if(item.getCycleMonth()!=null&&vehicle.getLastServiceTime()!=null){LocalDatenext=vehicle.getLastServiceTime().toLocalDate().plusMonths(item.getCycleMonth());remainDays=(int)ChronoUnit.DAYS.between(today,next);timeDue=remainDays<=0;}booleandue=mileageDue||timeDue;Stringreason=buildReason(item,due,mileageDue,timeDue,remainKm,remainDays);returnnewDueResult(item.getItemCode(),item.getItemName(),due,reason,remainKm,remainDays);}}

buildReason负责把结构化的判定结果翻译成人话,例如「距下次更换机油还有 800 公里」「空滤已超期 23 天」。前端拿到的就是可以直接渲染的文案,不需要再写一套判断逻辑,避免了两端规则不一致的问题。

三、接口层:基于 Spring Boot + MyBatis Plus 的提醒查询

汽车保养提醒接口应当保持「无状态、可缓存、易分页」的特点。由于大多数项目的判定结果只依赖于车辆档案和字典,接口可以直接在内存中完成映射与过滤,避免写复杂的 SQL 关联。

@RestController@RequestMapping("/api/maintain")publicclassMaintainController{@ResourceprivateVehicleMappervehicleMapper;@ResourceprivateMaintainItemMapperitemMapper;@GetMapping("/remind/{vehicleId}")publicR<List<DueResult>>remind(@PathVariableLongvehicleId){Vehiclevehicle=vehicleMapper.selectById(vehicleId);if(vehicle==null){returnR.fail("车辆档案不存在");}LocalDatetoday=LocalDate.now();List<DueResult>list=itemMapper.selectList(Wrappers.<MaintainItem>lambdaQuery().orderByAsc(MaintainItem::getSort)).stream().map(item->MaintainReminder.judge(item,vehicle,today)).filter(DueResult::isDue).collect(Collectors.toList());returnR.ok(list);}}

字典表的数量级通常在几十条以内,全量取出后内存计算完全没有性能压力。真正需要分页的是工单列表接口,直接使用 MyBatis Plus 的Page对象即可:

@GetMapping("/order/page")publicR<Page<MaintainOrderVO>>pageOrder(OrderQueryquery){LambdaQueryWrapper<MaintainOrder>wrapper=Wrappers.<MaintainOrder>lambdaQuery().eq(query.getStatus()!=null,MaintainOrder::getStatus,query.getStatus()).eq(query.getVehicleId()!=null,MaintainOrder::getVehicleId,query.getVehicleId()).orderByDesc(MaintainOrder::getCreateTime);returnR.ok(orderMapper.selectPage(newPage<>(query.getPageNo(),query.getPageSize()),wrapper));}

四、工单状态机:用受限流转替代自由改状态

汽车保养工单怕的就是状态被随意修改。管理后台如果直接给一个下拉框让人把「待接单」改成「已完成」,数据很快就会失去可信度。

建议把状态流转收窄为固定路径:

0 待接单 → 1 服务中 → 2 待确认 → 3 已完成 0 待接单 / 1 服务中 → 4 已取消

代码层面,不要写 if-else,而是维护一张状态转移表:

Map<Integer,Set<Integer>>TRANSITION=Map.of(0,Set.of(1,4),// 待接单 → 服务中 / 已取消1,Set.of(2,4),// 服务中 → 待确认 / 已取消2,Set.of(3)// 待确认 → 已完成);

每次更新状态前,先查表校验是否合法,非法直接拒绝。状态变更时,同时记录变更时间与操作人,形成审计日志。

五、完工回写与提醒重置

工单进入「已完成」时,需回写车辆的 last_service_mileage 和 last_service_time,作为下一轮提醒计算的基线。同时,将该工单关联的保养项目从提醒列表中移除,直到下一次周期触发。
常见问题解答(FAQ)

Q1:提醒算法为什么不直接写 SQL?

A:里程和时间是两个维度,SQL 里用 OR 关联会导致索引失效,且边界逻辑(如里程倒挂、首次录入)在 SQL 中难以优雅处理。全量字典在内存中计算,几十条数据毫秒级完成,可读性和可维护性远高于复杂 SQL。

Q2:如何防止技师跳过「待确认」直接完工?

A:状态机表里不配置 1 → 3 的转移路径,后端校验层统一拦截。前端按钮根据当前状态动态渲染,从 UI 和 API 两层堵住越权操作。

Q3:多辆车、多项目怎么批量查提醒?

A:先按 user_id 查出用户名下所有车辆,再对每辆车遍历保养项目字典做判定。数据量不大时串行即可;若车辆数较多,可用 CompletableFuture 并行计算,最后汇总返回。

Q4:保养周期支持自定义吗?

A:支持。maintain_item 表的 cycle_mileage 和 cycle_month 均可单独配置,任一字段为 NULL 即表示该维度不参与判定。门店可在后台自行维护项目字典,无需改代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询