SpringBoot+Flowable实现航空货运调度订单配送系统 1. 航空货运调度为什么不能照搬快递系统先讲一个真实场景。前几年我接手了一个航空货运调度系统的项目客户是一家做航空货运代理的公司日均订单量在三千到五千单左右每天要协调十几架次航班的舱位还要安排几十辆货车做机场到市区的短驳配送。项目启动时业务方负责人跟我说过一句话我到现在印象都很深我们试过买市面上的快递管理系统也试过让外包团队按快递物流的逻辑做结果都用不起来。为什么用不起来因为航空货运的调度逻辑和快递有本质区别。快递系统的核心是路由和分拣货物从A地到B地中间经过若干中转场每轮都有固定的时间段和线路系统只需要按地址和时效套路由模板。但航空货运的核心是运力匹配运力又分为两个维度空中的航班舱位和地面的车辆运力。航班有固定时刻但舱位会根据当天的货物实际重量、体积、危险品限制动态收紧和释放地面车辆要在航班落地之后的规定时间内完成提货和派送。货物能不能上某一个航班不是看地址而是看货物属性是否含电池、是否活体、是否温控、重量体积和当前舱位的实时余量。所以这套系统的调度难点不是把货物运到哪而是在什么时间窗口内把什么货物放到哪个航班、哪趟车上。本文要分享的就是基于SpringBoot落地这套航空货运调度订单配送系统的完整过程包含数据模型设计、舱位扣减并发处理、Flowable工作流集成、以及我实际踩过的几个坑。如果你正在做物流调度、运力匹配、订单履约类的系统这篇内容应该能帮你少走一些弯路。1.1 业务角色和调度流程概览动手写代码之前一定要先把业务链路摸清楚。航空货运订单配送这套系统里我梳理下来主要有这几个角色货主下单填写货物名称、件数、重量、体积、目的地、期望到达时间。销售/客服录入订单、跟客户确认报价、回答货物状态。调度员核心角色负责把订单分配到具体航班舱位并安排机场提货、车辆配送。仓库操作员负责货物入库、称重复核、安检、打板把货物固定到航空托盘上。司机接收配送任务完成机场到收货人之间的运输。管理员配置航线、航班时刻、运价规则、账号权限。完整流程大致是货主下单 → 销售审核 → 仓库收货并称重复核 → 调度员根据航班时刻和舱位余量订舱 → 货物打板过安检 → 装机起飞 → 到达站提货 → 调度车辆配送 → 签收回单。这个流程里的每一个节点都需要系统有明确的状态记录和操作痕迹这也是我为什么在系统里引入Flowable工作流引擎的原因后面会详细展开。一个容易被忽略的点是航空货运的代理由很多是并单操作。一个客户下的多件货物可能会拆到不同航班也有可能多个订单的货拼在一起走同一个航班同一票主单。所以订单和运单不能是简单的1:1关系订单是业务维度运单是操作维度。这两个概念没理清楚后面写表结构一定会返工。2. 技术选型与整体架构SpringBoot是底座Flowable管流程技术栈方面我最终定的方案是Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis RabbitMQ Flowable 6.6 XXL-Job MinIO。JDK用的11部署在四台8C16G的云服务器上一台Nginx做负载均衡两台应用节点一台单独跑MySQL和Redis。每个组件都不是白选的我简单说一下理由组件用途选择理由Spring Boot应用框架生态成熟团队招人容易起步快MyBatis-PlusORM复杂SQL好控制分页、条件构造器省事MySQL 8.0主数据库数据量在百万级别单库完全够用Redis缓存和分布式锁航班时刻缓存、舱位预扣、热点数据RabbitMQ异步消息订单状态变更通知、配送任务推送Flowable工作流引擎订单全生命周期状态流转可控、可追溯XXL-Job定时任务航班取消轮询、超时未支付自动取消MinIO文件存储运单拍照、签收单、货物照片2.1 项目模块划分我没有用微服务这个体量用微服务属于给自己找事。我按Maven多模块的方式拆了六个业务模块代码分开但部署还是一个应用本质上就是减少模块间的耦合。cloud-order订单管理下单、审核、改单、取消。cloud-dispatch调度核心航班舱位管理、订舱分配、配载计算。cloud-delivery地面配送提货任务、派送任务、司机APP接口。cloud-workflowFlowable相关流程部署、任务处理、状态查询。cloud-common公共包统一返回体、异常处理、工具类。cloud-admin后台管理航线配置、用户权限、运价规则、数据看板。模块间通过Spring Boot的FeignClient互相调用不用直接引入依赖调用Service接口就够了。单体应用阶段Feign是多余的还容易出序列化和网络超时的幺蛾子。2.2 为什么用Flowable来管订单状态很多同学做订单系统习惯用一个status字段从0到N手动if/else推进。订单流程简单时这么干没问题但航空货运的订单流转有大量分支和异常场景比如订舱后航班取消需要重新订舱。货物安检不通过需要退回改走其他航班。快递员提货后客户临时修改收货地址。一单多件部分签收。用状态字段的硬编码方式处理这些情况代码里会充斥大量状态判断每加一种异常就要动主流程代码时间长了根本维护不动。Flowable的好处是把状态流转和业务代码分离。流程节点、条件分支、会签、驳回都定义在BPMN文件里改流程不一定要改Java代码。这个特性在航空货运这种异常流程特别多的业务里价值巨大。我用的方式是Flowable管理订单状态机业务数据存在自己的业务表里Flowable的流程实例ID存到订单表的process_instance_id字段通过流程实例ID做关联。当流程流转到某个节点时监听器或ServiceTask去调用对应模块的业务方法。这样既利用了工作流引擎的流程控制能力又不至于把全部业务逻辑都塞进Flowable导致调试困难。3. 订单、运单、舱位的数据模型设计这块是整篇文章里最实在的部分数据模型设计直接决定了后续调度逻辑能不能写顺。我建议你照着这个思路建表比网上很多《XX管理系统数据库设计》要贴合航空货运的真实业务。3.1 订单和运单的主表设计先说核心的订单表order_info我保留了这些关键字段字段类型说明idbigint主键order_novarchar(32)订单号规则YYYYMMDD随机数customer_idbigint货主IDgoods_namevarchar(255)货物名称goods_typetinyint货物类型1普通 2含电池 3活体 4温控 5危险品total_weightdecimal(10,2)总重量单位kgtotal_volumedecimal(10,2)总体积单位m³total_packagesint总件数origin_airportvarchar(10)始发机场三字码dest_airportvarchar(10)目的机场三字码expect_arrival_timedatetime期望到达时间statustinyint业务状态冗余字段方便联表查询flow_process_idvarchar(64)Flowable流程实例IDdeletedtinyint逻辑删除create_time, update_timedatetime记录时间这里我特别说明一下status字段和Flowable的流程节点是有对应关系的但业务表里必须冗余一个status字段。因为在列表查询时如果用Flowable的流程表做条件过滤性能很差而且多表关联复杂。实际做法是Flowable流程节点负责流转业务表status字段负责查询。每次流程节点变更时通过监听器同步更新业务表的status。运单表waybill_info是操作层面的主表和订单是多对一关系字段类型说明idbigint主键waybill_novarchar(32)运单号order_idbigint关联订单IDmaster_waybillvarchar(32)主运单号航空主单flight_novarchar(16)航班号flight_datedate航班日期cargo_statustinyint货物状态1待入库 2已入库 3已安检 4已打板 5已装机 6已起飞 7已落地 8已提货 9已派送 10已签收space_idbigint占用舱位IDoperation_remarkvarchar(500)操作备注这张表是整个配送链条的核心货物状态每前进一步司机、仓库操作员、调度员都在这里更新状态。3.2 航班舱位表和预分配逻辑舱位的核心表是flight_schedule航班时刻表和flight_space航班舱位表。flight_schedule字段字段类型说明idbigint主键flight_novarchar(16)航班号origin_airportvarchar(10)始发机场dest_airportvarchar(10)目的机场depart_timedatetime计划起飞时间arrive_timedatetime计划到达时间week_daysvarchar(20)执飞周期如1,3,5表示周一三五aircraft_typevarchar(20)机型max_payloaddecimal(10,2)最大业载kgmax_volumedecimal(10,2)最大舱位体积m³statustinyint1有效 0停飞flight_space表记录每个航班的舱位余量字段类型说明idbigint主键schedule_idbigint航班时刻IDflight_datedate航班日期具体哪一天space_typetinyint舱位类型1主货舱 2腹舱available_weightdecimal(10,2)可用重量余量kgavailable_volumedecimal(10,2)可用体积余量m³versionint乐观锁版本号注意flight_space是以具体某一天的一个航班为粒度生成的。航班时刻表是计划flight_space是当天实际可用的运力。每天晚上由XXL-Job根据flight_schedule生成未来一周的舱位舱容初始数据。这样做的好处是如果有临时加班机或取消航班直接对flight_space做增删改不影响基础航班计划表。优化点在flight_space上我建了一个唯一索引uk_schedule_id_flight_date_space_type保证同一个航班同一天同一个舱型只有一条记录这是后续做舱位预扣时防止超卖的第一道防线。4. 调度核心逻辑舱位分配与并发扣减舱位分配是整个系统里最容易出bug的地方。我前后重构了三版才把超卖、错配这些问题压下去。第四部分我会详细展开这块的完整实现思路。4.1 预分配策略先到先得还是优先级优先业务方对舱位分配提了一个很具体要求不能简单按下单时间先到先得。因为航空货运的利润来源主要是高价值、高时效的货物比如生鲜、医疗试剂、电子产品。如果舱位被低价值的普货占满高价值货物来了没舱位等于白白损失利润。我最终实现的分配策略分两步第一步按航班和航线预筛候选航班。系统会根据订单的始发地、目的地、期望到达时间去flight_schedule里筛选出所有满足时效要求的航班按起飞时间升序排列。如果订单期望当天到就优先选当天最早起飞的航班匹配。第二步舱位分配按优先级加权。每个订单有一个优先级分数priority_score计算规则是优先级分数 货物类型权重 客户等级权重 时效紧迫度权重其中货物类型权重危险品、温控、活体这种特殊货物加30分普通普货加0分。客户等级权重VIP客户加20分普通客户加0分。时效紧迫度权重期望到达时间在24小时内的加50分48小时内的加20分。综合分数高的订单在舱位紧张时优先分配。代码实现时我用一个分配队列PriorityQueueOrderCandidate做排序然后逐个尝试分配。每个订单需要满足两个硬性约束剩余可用重量 订单重量剩余可用体积 订单体积如果当前候选航班舱位不足就顺延到下一个航班。这个逻辑听起来简单但加上并发之后就会出现严重问题下面细说。4.2 并发扣减舱位的超卖问题舱位分配是一个典型的读-改-写并发场景。假设两个订单同时看到了同一个航班剩余500kg舱位订单A需要400kg订单B需要300kg。如果没有并发控制两个请求都读到500kg各自的业务逻辑都觉得能分配最后把可用余量改成了100kgA扣完和200kgB扣完实际上超卖了200kg。这在航空货运里是要出大事故的——超卖舱位意味着到了机场打板时货物装不上飞机客户的货被拉下赔偿是小事信任损失是大事。我的第一版实现是用synchronized锁住分配方法在单实例部署下没出问题但两台应用节点一上synchronized就失效了。后来改成了MySQL乐观锁在flight_space表加了version字段更新SQL这样写UPDATE flight_space SET available_weight available_weight - #{weight}, available_volume available_volume - #{volume}, version version 1 WHERE id #{spaceId} AND version #{oldVersion} AND available_weight #{weight} AND available_volume #{volume}如果UPDATE影响行数为0说明舱位余量不够或版本号冲突此时要么重新读余量再匹配要么直接换下一个航班。这个方案能解决分布式并发扣减问题但有个性能隐患当多个订单同时抢同一航班的舱位时乐观锁冲突率很高一个请求可能要重试好多次。所以第二版我在乐观锁前面加了一层Redis预扣方案。具体流程调度请求进来先尝试在Redis里用DECRBY扣减航班的可用重量。如果Redis剩余量足够扣减成功才继续走MySQL的乐观锁更新。MySQL更新成功Redis扣减结果作为最终结果不补偿。MySQL更新失败比如版本冲突则对Redis做INCRBY补偿回滚然后换航班重试。为什么不再完全依赖乐观锁因为DECRBY是原子操作能快速挡掉大部分明显无法分配的请求减少无谓的数据库写冲突。同时Redis扣减加上一个5分钟的过期保护key防止极端情况下Redis和MySQL数据不一致。这套方案上线后舱位分配的并发冲突率从之前的30%降到了8%以内调度接口的P99耗时从850ms降到了220ms。4.3 航班取消和临时换机的处理调度系统里最麻烦的事情之一就是航班变动。航空公司的航班计划经常会调整今天飞的航班明天不飞了或者机型换小了、业载能力降了。系统必须对这种变化做出反应。我的做法是通过XXL-Job定时任务每30分钟拉取一次航空公司的航班动态接口比对本地flight_schedule和flight_space的数据如果航班取消把对应flight_space的可用余量置为0同时批量查询该航班上所有已分配但未起飞的运单将运单状态改回待订舱生成重新订舱任务推送给调度员处理。如果机型换小、业载降低就把flight_space的最大业载调小同时检查已分配的货物是否超出新的业载上限。超出部分自动释放并重新进入待分配队列。这里有个重要细节换机处理不能全自动必须保留人工确认环节。所以我在调度员工作台做了航班变动待处理列表系统给出建议方案建议哪些货物改签调度员确认后才执行。自动化是提效的但最终的决策权要留给业务人员这个原则在很多企业级系统里都通用。5. 订单全链路状态跟踪Flowable落地细节第五部分我说说Flowable具体是怎么用的。市面上讲Flowable的文章不少但大多停留在请假审批这种简单场景。航空货运的订单状态流转比请假复杂得多我实际做下来总结出了一套可复用的拆解思路。5.1 订单流转的流程定义我定义了一个名为order_transport_flow的BPMN流程包含以下几个节点按照实际业务顺序串联并配有条件分支订单创建流程启动录入订单信息。销售审核审核订单信息是否完整单价是否确认。审核不通过走驳回修改分支回到创建节点。仓库入库货物到仓仓库操作员登记实际重量、体积这个很关键经常会和下单预估不一致实际数据影响舱位分配。调度订舱实际重量体积确认后调度员执行订舱操作分配航班和舱位。打板过检货物按航班打板过安检。安检不通过则走异常分支安检退回通知客服联系客户处理。装机起飞飞机起飞货物状态更新为已起飞。到达提货航班落地目的站提取货物。配送签收司机配送客户签收流程终结。每个节点对应一个Flowable的UserTask或者ServiceTask。UserTask给具体的角色去处理比如调度订舱节点assignee是调度员角色ServiceTask调用业务Service完成系统内部的数据更新比如起飞后自动更新航班动态。部署流程定义的核心代码Autowired private RepositoryService repositoryService; public void deployFlow() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/order_transport_flow.bpmn20.xml) .name(订单运输流程) .deploy(); log.info(流程部署成功ID: {}, deployment.getId()); }启动流程并关联业务订单的代码Autowired private RuntimeService runtimeService; public String startOrderFlow(String orderNo, String userId) { MapString, Object variables new HashMap(); variables.put(orderNo, orderNo); variables.put(initiator, userId); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(order_transport_flow, orderNo, variables); return processInstance.getId(); }这里有个非常重要的经验流程变量variables里面不要放业务对象的大字段只放必要的信息比如订单号、操作人、金额这种基础类型。因为Flowable的流程变量存储在ACT_RU_VARIABLE表里如果放了一个大的JSON对象这个表会迅速膨胀而且流程引擎的表本身不适合存大数据。正确的做法是流程变量只存业务主键订单号要查业务详情时通过订单号回查业务表。流程节点完成和跳转的代码Autowired private TaskService taskService; public void completeTask(String taskId, String operatorId, MapString, Object vars) { taskService.setAssignee(taskId, operatorId); taskService.complete(taskId, vars); }5.2 分支判断和网关配置在Flowable的BPMN文件中分支判断用的是表达式。比如仓库入库节点之后要根据实重和预估重的偏差率决定是否走调度订舱正常分支还是走重量确认异常分支。BPMN里配置bpmn:exclusiveGateway idgateway_weight_check name重量校验网关 defaultflow_normal / bpmn:sequenceFlow idflow_normal sourceRefgateway_weight_check targetRefdispatch_space / bpmn:sequenceFlow idflow_abnormal sourceRefgateway_weight_check targetRefweight_confirm_exception bpmn:conditionExpression xsi:typebpmn:tFormalExpression ${weightDeviationRate 15} /bpmn:conditionExpression /bpmn:sequenceFlowJava代码里just设置流程变量MapString, Object vars new HashMap(); double deviationRate Math.abs(actualWeight - estimatedWeight) / estimatedWeight * 100; vars.put(weightDeviationRate, deviationRate); taskService.complete(taskId, vars);Flowable会自己根据表达式判断走哪个分支。这套设计让业务流程调整变得非常灵活。比如业务方某天说重量偏差超过10%就要人工确认以前是改Java代码重新发布现在是改一下BPMN文件里的表达式阈值重新部署流程定义即可线上的存量流程不受影响。5.3 超时未处理任务的自动催办订单流程流转中最怕节点卡住没人处理。比如调度订舱节点如果调度员半天没操作客户的货就滞留仓库。我加了一个超时提醒机制在Flowable的UserTask上配置了flowable:dueDate扩展属性任务创建时计算一个期望完成时间比如订舱节点给2小时并存入任务的dueDate。然后用XXL-Job每分钟扫描一次ACT_RU_TASK表找出超过dueDate还未完成的任务给对应的处理人推送一条钉钉/企业微信通知并支持升级通知给上级调度主管。一个小时后再未处理进入升级告警。这套机制上线后调度节点的平均处理时长从4小时降到了1.5小时客户投诉没人管我的货的情况基本消失。6. 实际踩过的坑N1查询、缓存穿透、状态不一致第六部分没有任何理论全是我在这个项目上花了大量时间排查的问题每一个都有真实的线上事故背景。6.1 调度列表的N1查询问题调度员的工作台是一个列表页默认展示未来24小时内所有待订舱的订单每行要显示订单信息、客户名称、货物信息、匹配航班数。第一版我直接在Service层做了循环查询for (OrderInfo order : orderList) { // 查询客户信息 Customer customer customerMapper.selectById(order.getCustomerId()); // 查询匹配航班列表 ListFlightSchedule flights flightScheduleMapper.selectMatchFlights(order); // 拼装返回 }这个逻辑在订单量少的时候没问题但订单量到两千条以上时这个接口响应时间直接飙升到6秒以上。原因就是2000个订单×1次客户查询1次航班查询4000次SQL什么连接池都扛不住。优化方案非常简单先把客户ID列表收集起来批量查询ListLong customerIds orderList.stream() .map(OrderInfo::getCustomerId) .distinct() .collect(Collectors.toList()); MapLong, Customer customerMap customerMapper.selectBatchIds(customerIds) .stream() .collect(Collectors.toMap(Customer::getId, c - c));航班匹配查询我换成了批量一次查一个时间窗口内的所有航班再在内存里按订单目的地分组过滤。经过这两步优化接口响应时间降到了300ms以内。这个N1问题在MyBatis的collection标签嵌套查询中特别容易出现建议你始终优先考虑批量查询后手动组装数据可控性远高于依赖ORM层的自动嵌套映射。6.2 航班时刻的缓存穿透问题有段时间线上出现了一个奇怪的现象每晚8点到9点之间MySQL的CPU使用率会突然飙到90%以上。查了慢查询日志发现大量SELECT * FROM flight_schedule WHERE origin_airport ? AND dest_airport ? AND status 1的SQL单条执行并不慢但量太大了。原因分析每晚8点是业务方集中导入次日航班计划的时间同时大量货主在这个时段集中下单。航班数据被业务方全量删除后重新导入Redis缓存里的航班数据全部失效。而订单模块接口里有一段逻辑下单时要查一次航班计划来校验航线是否存在这个接口被货主端高频调用。缓存失效的一瞬间几千个请求同时打到了数据库形成了缓存穿透。解决方法是双管齐下第一给航班计划查询加上空值缓存。如果数据库里没查到数据也往Redis里放一个空值过期时间设为30秒防止恶意高频查询穿透到数据库。第二加分布式锁做缓存重建。当缓存失效时不是所有请求都去查数据库而是第一个请求获取锁去查库回填缓存其他请求短暂等待后直接走缓存。用Redisson的RLock实现代码简洁可靠RLock lock redissonClient.getLock(flight_schedule_lock: origin _ dest); try { boolean acquired lock.tryLock(3, TimeUnit.SECONDS); if (!acquired) { // 拿不到锁就查一次数据库兜底 return flightScheduleMapper.selectValidFlights(origin, dest); } // 拿到锁查库回填缓存 ListFlightSchedule list flightScheduleMapper.selectValidFlights(origin, dest); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(list), 30, TimeUnit.MINUTES); return list; } finally { lock.unlock(); }上线后晚8点的数据库CPU峰值从90%降到了40%左右。6.3 订单状态和Flowable流程实例不一致最后一个坑比较隐蔽也是在交付之后才被客户发现的有些订单在业务表里的status是已签收但Flowable的流程实例卡在配送签收节点没有结束。后来排查发现是因为completeTask过程和业务表更新不是原子操作。我当时写的是// 先更新业务表状态为已签收 orderMapper.updateStatus(orderId, 10); // 再完成Flowable任务 taskService.complete(taskId, vars);如果第二步抛异常第一步的已签收已经提交如果不在同一个事务里两边数据就产生了不一致。解决方案是把两步放在同一个Spring事务中Transactional(rollbackFor Exception.class) public void completeTaskWithBizUpdate(String taskId, Long orderId, Integer targetStatus, MapString, Object vars) { orderMapper.updateStatus(orderId, targetStatus); taskService.complete(taskId, vars); }但这里又涉及一个新的坑Flowable的内部操作也有自己的事务如果Spring事务先提交了Flowable的任务还没完成业务表状态先变了或者反过来。我用的是把Flowable的complete操作放在事务的最后一步并且将Flowable的JDBC连接加入到Spring的同一个事务管理中。具体做法是配置Flowable的DataSource和业务数据源共用同一条MySQL连接并开启Spring托管事务。如果你们项目的Flowable是独立数据库比如内网单独一台MySQL那跨库事务就比较难保证建议采用异步对账的方式定时任务扫描Flowable的任务表和业务表的状态发现不一致时触发补偿更新。对账脚本虽然多写几百行代码但能保证最终一致性我当时的做法是两条路都走同库事务 每日对账双保险。7. 部署架构与生产环境配置代码写得再好部署和调优跟不上照样崩。第七部分简单说说生产环境的部署方案和一些关键配置给准备上线的同学做个参考。7.1 服务部署和JVM参数生产环境我用的方案是Nginx两台Keepalived做高可用→ Spring Boot应用两台8C16G→ MySQL一台16C32G主从没上做了每日全量备份binlog→ Redis一台8C16G。应用节点通过systemd守护进程管理启动参数JAVA_OPTS-Xms8g -Xmx8g -Xmn4g -XX:UseG1GC -XX:MaxGCPauseMillis100 \ -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/jvm/heapdump.hprof \ -Dspring.profiles.activeprodG1垃圾回收器在8G堆上的停顿时间控制得比较好适合这种业务接口对响应时间有要求的系统。HeapDump参数一定要开线上OOM时没有dump文件排查问题等于盲人摸象。7.2 数据库连接池和Redis配置连接池用HikariCPSpring Boot默认生产配置spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 max-lifetime: 1800000 connection-test-query: SELECT 1maximum-pool-size: 20是压测后定出来的值。之前按网上教程配了50反而出现了数据库连接等待时间增加的问题因为单库2C的写入能力有限太多连接反而导致锁竞争。Redis配置上要注意两点一是给Key加上统一的业务前缀比如cargo:flight:{id}、cargo:space:{id}方便排查和清理二是Redis的maxmemory-policy要设置成allkeys-lru防止缓存数据膨胀把Redis内存打爆。7.3 监控和告警生产环境必须做监控不然系统出问题你都不知道。我用的方案是Prometheus Grafana Alertmanager。Spring Boot应用引入micrometer-registry-prometheus暴露/actuator/prometheus端点。重点监控指标接口QPS和P99响应时间调度接口、订单列表接口。JVM堆内存使用率、Full GC次数和耗时。数据库连接池活跃连接数和等待时间。Flowable的待办任务数量超过一定阈值触发告警。XXL-Job任务执行成功率。告警渠道用的是钉钉群机器人配合Alertmanager的Webhook。这套监控上线后的实际效果有一次航班时刻导入任务执行失败监控在1分钟内就发出了告警提前发现了一次潜在的运力数据异常避免了次日出货混乱。8. 写在最后的一些心得这个项目从立项到稳定运行前后花了四个多月。回头看最有价值的不是写了多少行代码而是对整个航空货运业务的理解。技术框架SpringBoot也好、Flowable也好都是可替换的但业务理解、数据模型设计、并发控制这些沉淀下来的思路才是真正能复用和迁移的东西。有几个经验想单独提一下。第一个是做业务系统先把业务流程图画清楚再动手写代码。这个项目第一版失败就是因为没和业务方把异常流程聊透结果数据库表改了三轮。后来我把所有状态流转、分支条件、超时处理全部画成流程图和客户确认过开发和联调效率至少提升了50%。第二个是不要过度设计。这个系统一开始业务方提了很多需求包括要做司机端APP、要对接航司系统自动获取运价、要做BI数据分析我全都没在第一个版本做。先把核心的订单、调度、配送链路跑通才是最重要的。第三个是团队协作时要保持模块接口稳定。我的项目组里两个人负责调度模块两个人负责订单和配送模块。每天下班前用接口定义文档做一次对齐每周做一次代码Review大大减少了联调时的返工。如果后续要扩展我最想做的方向是把舱位分配算法升级成基于运筹优化的自动配载综合货物优先级、航班时刻、装卸成本做全局最优调度而不是现在的优先级队列贪心策略。数据积累足够之后也可以训练一个到港时效预测模型让客户的期望到达时间更贴近实际运力情况。但这些都是后话了先把系统稳定跑起来业务模型跑顺再一步步往智能化方向走。希望这篇内容能给正在做物流调度、订单履约系统的朋友一些参考。如果你也在搞类似系统欢迎多交流踩过的坑互相分享一下能帮大家都省点时间。