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

发布时间:2026/9/25 12:31:36
汽车保养管理系统实战:从保养周期算法到工单流转的完整实现 汽车保养管理系统实战从保养周期算法到工单流转的完整实现汽车保养类系统的技术难点并不在于页面有多少而在于两件事一是把「什么时候该保养」这件事算准二是把「保养过程」这件事管住。前者是一套基于里程与时间的双维度定时算法后者是一套工单状态机。本文以 Spring Boot MyBatis Plus MySQL 作为后端技术栈以 UniAppVue 语法作为用户端、Vue Element UI 作为管理后台拆解汽车保养系统从数据建模到接口落地的完整实现路径。一、业务建模把汽车保养拆成可计算的三张核心表汽车保养业务看似是「车主约、门店做」的流程落到代码层面其实只有三层数据车辆档案、保养项目字典、保养工单。车辆档案记录车牌、车型、当前里程、上次保养里程与时间。它是所有提醒计算的输入。保养项目字典机油、机滤、空滤、刹车片等每个项目维护两个周期字段——里程周期与时间周期。保养工单把「预约—接单—服务—完工」的全过程串起来并在完工时回写车辆的里程与时间基线。这样设计的价值在于提醒逻辑不依赖任何硬编码规则全部由字典表驱动。新增一个保养项目时只需要插一行数据不需要改动任何一行业务代码。CREATETABLEvehicle(idBIGINTNOTNULLAUTO_INCREMENT,user_idBIGINTNOTNULLCOMMENT车主用户ID,plate_noVARCHAR(16)NOTNULLCOMMENT车牌号,modelVARCHAR(64)DEFAULTNULLCOMMENT车型,current_mileageINTNOTNULLDEFAULT0COMMENT当前里程(km),last_service_mileageINTDEFAULTNULLCOMMENT上次保养里程(km),last_service_timeDATETIMEDEFAULTNULLCOMMENT上次保养时间,PRIMARYKEY(id),KEYidx_user(user_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT车辆档案;CREATETABLEmaintain_item(idBIGINTNOTNULLAUTO_INCREMENT,item_codeVARCHAR(32)NOTNULLCOMMENT项目编码,item_nameVARCHAR(64)NOTNULLCOMMENT项目名称,cycle_mileageINTDEFAULTNULLCOMMENT里程周期(km)为空表示不按里程判定,cycle_monthINTDEFAULTNULLCOMMENT时间周期(月)为空表示不按时间判定,sortINTNOTNULLDEFAULT0COMMENT排序,PRIMARYKEY(id),UNIQUEKEYuk_code(item_code))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT汽车保养项目字典;CREATETABLEmaintain_order(idBIGINTNOTNULLAUTO_INCREMENT,order_noVARCHAR(32)NOTNULLCOMMENT工单号,vehicle_idBIGINTNOTNULLCOMMENT车辆ID,statusTINYINTNOTNULLDEFAULT0COMMENT0待接单 1服务中 2待确认 3已完成 4已取消,appoint_timeDATETIMEDEFAULTNULLCOMMENT预约时间,finish_mileageINTDEFAULTNULLCOMMENT完工里程,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(id),UNIQUEKEYuk_order_no(order_no),KEYidx_vehicle(vehicle_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT汽车保养工单;需要特别说明的是周期字段允许为NULL。这个细节很重要——有些项目只按里程判定有些只按时间判定。如果用0表示「不判定」会和「周期为 0」的语义混淆后续排查问题时会非常痛苦。二、汽车保养提醒算法里程与时间的「先到为准」判定提醒的核心逻辑是对每个保养项目分别计算里程是否到期、时间是否到期任意一个满足即判定为到期并把触发原因回传给前端展示。这里有几个容易被忽略的边界情况当前里程小于上次保养里程车主换过仪表、录入错误或调表都会造成这种情况此时差值应为负数必须直接判定为「数据异常」而不是「到期」。上次保养记录为空新车或首次录入应按「不判定」处理否则会给全量用户推提醒。刚好等于阈值应采用而不是否则到期当天不会触发。publicclassMaintainReminder{publicstaticDueResultjudge(MaintainItemitem,Vehiclevehicle,LocalDatetoday){IntegerremainKmnull;IntegerremainDaysnull;booleanmileageDuefalse;booleantimeDuefalse;// 里程维度if(item.getCycleMileage()!nullvehicle.getLastServiceMileage()!nullvehicle.getCurrentMileage()!null){intdeltavehicle.getCurrentMileage()-vehicle.getLastServiceMileage();if(delta0){returnDueResult.error(item,里程数据异常请先校准);}remainKmitem.getCycleMileage()-delta;mileageDueremainKm0;}// 时间维度if(item.getCycleMonth()!nullvehicle.getLastServiceTime()!null){LocalDatenextvehicle.getLastServiceTime().toLocalDate().plusMonths(item.getCycleMonth());remainDays(int)ChronoUnit.DAYS.between(today,next);timeDueremainDays0;}booleanduemileageDue||timeDue;StringreasonbuildReason(item,due,mileageDue,timeDue,remainKm,remainDays);returnnewDueResult(item.getItemCode(),item.getItemName(),due,reason,remainKm,remainDays);}}buildReason负责把结构化的判定结果翻译成人话例如「距下次更换机油还有 800 公里」「空滤已超期 23 天」。前端拿到的就是可以直接渲染的文案不需要再写一套判断逻辑避免了两端规则不一致的问题。三、接口层基于 Spring Boot MyBatis Plus 的提醒查询汽车保养提醒接口应当保持「无状态、可缓存、易分页」的特点。由于大多数项目的判定结果只依赖于车辆档案和字典接口可以直接在内存中完成映射与过滤避免写复杂的 SQL 关联。RestControllerRequestMapping(/api/maintain)publicclassMaintainController{ResourceprivateVehicleMappervehicleMapper;ResourceprivateMaintainItemMapperitemMapper;GetMapping(/remind/{vehicleId})publicRListDueResultremind(PathVariableLongvehicleId){VehiclevehiclevehicleMapper.selectById(vehicleId);if(vehiclenull){returnR.fail(车辆档案不存在);}LocalDatetodayLocalDate.now();ListDueResultlistitemMapper.selectList(Wrappers.MaintainItemlambdaQuery().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)publicRPageMaintainOrderVOpageOrder(OrderQueryquery){LambdaQueryWrapperMaintainOrderwrapperWrappers.MaintainOrderlambdaQuery().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而是维护一张状态转移表MapInteger,SetIntegerTRANSITIONMap.of(0,Set.of(1,4),// 待接单 → 服务中 / 已取消1,Set.of(2,4),// 服务中 → 待确认 / 已取消2,Set.of(3)// 待确认 → 已完成);每次更新状态前先查表校验是否合法非法直接拒绝。状态变更时同时记录变更时间与操作人形成审计日志。五、完工回写与提醒重置工单进入「已完成」时需回写车辆的 last_service_mileage 和 last_service_time作为下一轮提醒计算的基线。同时将该工单关联的保养项目从提醒列表中移除直到下一次周期触发。常见问题解答FAQQ1提醒算法为什么不直接写 SQLA里程和时间是两个维度SQL 里用 OR 关联会导致索引失效且边界逻辑如里程倒挂、首次录入在 SQL 中难以优雅处理。全量字典在内存中计算几十条数据毫秒级完成可读性和可维护性远高于复杂 SQL。Q2如何防止技师跳过「待确认」直接完工A状态机表里不配置 1 → 3 的转移路径后端校验层统一拦截。前端按钮根据当前状态动态渲染从 UI 和 API 两层堵住越权操作。Q3多辆车、多项目怎么批量查提醒A先按 user_id 查出用户名下所有车辆再对每辆车遍历保养项目字典做判定。数据量不大时串行即可若车辆数较多可用 CompletableFuture 并行计算最后汇总返回。Q4保养周期支持自定义吗A支持。maintain_item 表的 cycle_mileage 和 cycle_month 均可单独配置任一字段为 NULL 即表示该维度不参与判定。门店可在后台自行维护项目字典无需改代码。