引言:履约路由的价值 履约路由:决定订单从哪个仓库发货、用哪个物流承运商。
好的履约路由能带来:
降低物流成本(选择最优物流) 提升时效(就近发货) 提高客户满意度 均衡仓库负载 差的履约路由会导致:
物流成本高 时效差 某些仓库爆仓,某些仓库闲置 一、履约路由架构 1.1 路由流程 订单 ──> 库存检查 ──> 选仓 ──> 选物流 ──> 生成履约单 ──> 下发WMS │ │ │ │ │ │ ▼ ▼ ▼ 有货仓库 最优仓库 最优物流 1.2 路由因素 因素 说明 权重 库存 仓库是否有货 必要条件 距离 仓库到收货地的距离 影响时效 成本 物流费用 影响利润 时效 物流时效要求 影响体验 仓库负载 仓库当前处理能力 影响发货速度 承运商能力 承运商覆盖范围、服务质量 影响可选项 二、选仓策略 2.1 策略类型 策略 说明 适用场景 就近发货 选择离收货地最近的仓库 时效优先 成本最优 选择物流成本最低的仓库 成本优先 库存均衡 优先选择库存多的仓库 均衡库存 负载均衡 优先选择负载低的仓库 均衡产能 综合评分 多因素加权评分 综合考虑 2.2 就近发货策略 @Component public class NearestWarehouseStrategy implements WarehouseStrategy { @Override public String getStrategyType() { return "NEAREST"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); return candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .min(Comparator.comparingDouble(w -> calculateDistance(w.getWarehouseId(), destination))) .map(WarehouseInventory::getWarehouseId) .orElseThrow(() -> new NoAvailableWarehouseException(order.getOrderId())); } /** * 计算仓库到目的地的距离 * 简化版:使用经纬度计算直线距离 */ private double calculateDistance(String warehouseId, Address destination) { Warehouse warehouse = warehouseService.getWarehouse(warehouseId); double lat1 = warehouse.getLatitude(); double lon1 = warehouse.getLongitude(); double lat2 = getLatitude(destination); double lon2 = getLongitude(destination); // Haversine公式计算球面距离 double R = 6371; // 地球半径(公里) double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1); double a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } } 2.3 成本最优策略 @Component public class CostOptimalWarehouseStrategy implements WarehouseStrategy { @Override public String getStrategyType() { return "COST_OPTIMAL"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); double totalWeight = calculateTotalWeight(order); return candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .min(Comparator.comparingDouble(w -> calculateShippingCost(w.getWarehouseId(), destination, totalWeight))) .map(WarehouseInventory::getWarehouseId) .orElseThrow(() -> new NoAvailableWarehouseException(order.getOrderId())); } /** * 计算物流成本 */ private double calculateShippingCost(String warehouseId, Address destination, double weight) { // 获取该仓库可用的物流商 List<Carrier> carriers = carrierService.getCarriersForWarehouse(warehouseId); // 计算每个物流商的费用,取最低 return carriers.stream() .filter(c -> c.canDeliver(destination)) .mapToDouble(c -> c.calculateFee(warehouseId, destination, weight)) .min() .orElse(Double.MAX_VALUE); } } 2.4 综合评分策略(推荐) @Component public class ScoringWarehouseStrategy implements WarehouseStrategy { // 权重配置 private static final double DISTANCE_WEIGHT = 0.3; private static final double COST_WEIGHT = 0.4; private static final double INVENTORY_WEIGHT = 0.2; private static final double LOAD_WEIGHT = 0.1; @Override public String getStrategyType() { return "SCORING"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); // 过滤有库存的仓库 List<WarehouseInventory> available = candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .collect(Collectors.toList()); if (available.isEmpty()) { throw new NoAvailableWarehouseException(order.getOrderId()); } // 计算各维度的归一化值 double maxDistance = available.stream() .mapToDouble(w -> calculateDistance(w.getWarehouseId(), destination)) .max().orElse(1); double maxCost = available.stream() .mapToDouble(w -> calculateShippingCost(w.getWarehouseId(), destination, order)) .max().orElse(1); double maxInventory = available.stream() .mapToDouble(WarehouseInventory::getAvailableQty) .max().orElse(1); double maxLoad = available.stream() .mapToDouble(w -> getWarehouseLoad(w.getWarehouseId())) .max().orElse(1); // 计算综合得分 return available.stream() .max(Comparator.comparingDouble(w -> { double distanceScore = 1 - calculateDistance(w.getWarehouseId(), destination) / maxDistance; double costScore = 1 - calculateShippingCost(w.getWarehouseId(), destination, order) / maxCost; double inventoryScore = w.getAvailableQty() / maxInventory; double loadScore = 1 - getWarehouseLoad(w.getWarehouseId()) / maxLoad; return distanceScore * DISTANCE_WEIGHT + costScore * COST_WEIGHT + inventoryScore * INVENTORY_WEIGHT + loadScore * LOAD_WEIGHT; })) .map(WarehouseInventory::getWarehouseId) .orElseThrow(); } } 三、选物流策略 3.1 物流选择因素 因素 说明 覆盖范围 物流商是否能送达目的地 时效 预计送达时间 费用 物流费用 服务质量 丢包率、破损率、投诉率 追踪能力 是否支持物流追踪 3.2 物流选择实现 @Service public class CarrierSelectionService { /** * 选择最优物流 */ public Carrier selectCarrier(String warehouseId, Address destination, double weight, String serviceLevel) { // 1. 获取仓库可用的物流商 List<Carrier> carriers = carrierService.getCarriersForWarehouse(warehouseId); // 2. 过滤能送达的物流商 List<Carrier> available = carriers.stream() .filter(c -> c.canDeliver(destination)) .filter(c -> c.getMaxWeight() >= weight) .collect(Collectors.toList()); if (available.isEmpty()) { throw new NoAvailableCarrierException(warehouseId, destination); } // 3. 根据服务等级选择 switch (serviceLevel) { case "EXPRESS": // 时效优先 return selectByTimeEfficiency(available, destination); case "ECONOMY": // 成本优先 return selectByCost(available, warehouseId, destination, weight); default: // 综合评分 return selectByScore(available, warehouseId, destination, weight); } } /** * 时效优先选择 */ private Carrier selectByTimeEfficiency(List<Carrier> carriers, Address destination) { return carriers.stream() .min(Comparator.comparingInt(c -> c.getEstimatedDays(destination))) .orElseThrow(); } /** * 成本优先选择 */ private Carrier selectByCost(List<Carrier> carriers, String warehouseId, Address destination, double weight) { return carriers.stream() .min(Comparator.comparingDouble(c -> c.calculateFee(warehouseId, destination, weight))) .orElseThrow(); } /** * 综合评分选择 */ private Carrier selectByScore(List<Carrier> carriers, String warehouseId, Address destination, double weight) { return carriers.stream() .max(Comparator.comparingDouble(c -> { double costScore = 100 - c.calculateFee(warehouseId, destination, weight); double timeScore = 100 - c.getEstimatedDays(destination) * 10; double qualityScore = c.getQualityScore(); // 0-100 return costScore * 0.4 + timeScore * 0.3 + qualityScore * 0.3; })) .orElseThrow(); } } 四、规则引擎 4.1 规则配置 -- 路由规则表 CREATE TABLE t_routing_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', rule_type VARCHAR(32) NOT NULL COMMENT '类型:WAREHOUSE/CARRIER', priority INT DEFAULT 0 COMMENT '优先级(越大越优先)', -- 条件 condition_channel VARCHAR(32) COMMENT '渠道条件', condition_country VARCHAR(32) COMMENT '国家条件', condition_sku_category VARCHAR(32) COMMENT 'SKU分类条件', condition_weight_min DECIMAL(10,2) COMMENT '最小重量', condition_weight_max DECIMAL(10,2) COMMENT '最大重量', condition_amount_min DECIMAL(12,2) COMMENT '最小金额', -- 动作 action_warehouse_id VARCHAR(32) COMMENT '指定仓库', action_carrier_id VARCHAR(32) COMMENT '指定物流', action_strategy VARCHAR(32) COMMENT '选择策略', enabled TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_type_priority (rule_type, priority DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='路由规则表'; -- 示例规则 INSERT INTO t_routing_rule (rule_name, rule_type, priority, condition_country, action_warehouse_id) VALUES ('美国订单走美西仓', 'WAREHOUSE', 100, 'US', 'WH_US_WEST'), ('欧洲订单走德国仓', 'WAREHOUSE', 100, 'DE,FR,IT,ES', 'WH_DE'); INSERT INTO t_routing_rule (rule_name, rule_type, priority, condition_channel, action_carrier_id) VALUES ('Amazon订单用UPS', 'CARRIER', 100, 'AMAZON', 'UPS'), ('eBay订单用USPS', 'CARRIER', 90, 'EBAY', 'USPS'); 4.2 规则引擎实现 @Service public class RoutingRuleEngine { @Autowired private RoutingRuleRepository ruleRepository; /** * 匹配仓库规则 */ public String matchWarehouseRule(Order order) { List<RoutingRule> rules = ruleRepository.findByTypeAndEnabled("WAREHOUSE", true); // 按优先级排序 rules.sort(Comparator.comparingInt(RoutingRule::getPriority).reversed()); for (RoutingRule rule : rules) { if (matchCondition(rule, order)) { if (rule.getActionWarehouseId() != null) { return rule.getActionWarehouseId(); } if (rule.getActionStrategy() != null) { return rule.getActionStrategy(); // 返回策略名称 } } } return "SCORING"; // 默认使用评分策略 } /** * 匹配物流规则 */ public String matchCarrierRule(Order order, String warehouseId) { List<RoutingRule> rules = ruleRepository.findByTypeAndEnabled("CARRIER", true); rules.sort(Comparator.comparingInt(RoutingRule::getPriority).reversed()); for (RoutingRule rule : rules) { if (matchCondition(rule, order)) { if (rule.getActionCarrierId() != null) { return rule.getActionCarrierId(); } } } return null; // 无匹配规则,使用默认策略 } /** * 条件匹配 */ private boolean matchCondition(RoutingRule rule, Order order) { // 渠道匹配 if (rule.getConditionChannel() != null && !rule.getConditionChannel().contains(order.getChannel())) { return false; } // 国家匹配 if (rule.getConditionCountry() != null) { String country = order.getShippingAddress().getCountryCode(); if (!rule.getConditionCountry().contains(country)) { return false; } } // 重量匹配 double weight = calculateWeight(order); if (rule.getConditionWeightMin() != null && weight < rule.getConditionWeightMin()) { return false; } if (rule.getConditionWeightMax() != null && weight > rule.getConditionWeightMax()) { return false; } // 金额匹配 if (rule.getConditionAmountMin() != null && order.getTotalAmount().compareTo(rule.getConditionAmountMin()) < 0) { return false; } return true; } } 五、履约调度服务 5.1 完整调度流程 @Service @Slf4j public class FulfillmentRoutingService { @Autowired private RoutingRuleEngine ruleEngine; @Autowired private Map<String, WarehouseStrategy> warehouseStrategies; @Autowired private CarrierSelectionService carrierSelectionService; /** * 订单履约路由 */ @Transactional public FulfillmentOrder route(Order order) { log.info("开始履约路由: orderId={}", order.getOrderId()); // 1. 匹配仓库规则 String warehouseResult = ruleEngine.matchWarehouseRule(order); String warehouseId; if (isWarehouseId(warehouseResult)) { // 规则指定了具体仓库 warehouseId = warehouseResult; log.info("规则指定仓库: {}", warehouseId); } else { // 规则指定了策略,执行策略选仓 WarehouseStrategy strategy = warehouseStrategies.get(warehouseResult); List<WarehouseInventory> candidates = getWarehouseCandidates(order); warehouseId = strategy.selectWarehouse(order, candidates); log.info("策略选仓: strategy={}, warehouse={}", warehouseResult, warehouseId); } // 2. 匹配物流规则 String carrierId = ruleEngine.matchCarrierRule(order, warehouseId); if (carrierId == null) { // 无规则匹配,使用策略选物流 Carrier carrier = carrierSelectionService.selectCarrier( warehouseId, order.getShippingAddress(), calculateWeight(order), order.getServiceLevel() ); carrierId = carrier.getCarrierId(); } log.info("选择物流: {}", carrierId); // 3. 创建履约订单 FulfillmentOrder fulfillmentOrder = new FulfillmentOrder(); fulfillmentOrder.setFulfillmentOrderId(generateFulfillmentOrderId()); fulfillmentOrder.setSourceOrderId(order.getOrderId()); fulfillmentOrder.setWarehouseId(warehouseId); fulfillmentOrder.setCarrierId(carrierId); fulfillmentOrder.setShippingAddress(order.getShippingAddress()); fulfillmentOrder.setItems(convertItems(order.getItems())); fulfillmentOrder.setStatus(FulfillmentStatus.PENDING); fulfillmentOrderRepository.save(fulfillmentOrder); log.info("履约路由完成: orderId={}, fulfillmentOrderId={}, warehouse={}, carrier={}", order.getOrderId(), fulfillmentOrder.getFulfillmentOrderId(), warehouseId, carrierId); return fulfillmentOrder; } } 六、总结 6.1 核心要点 选仓策略:就近、成本最优、综合评分 选物流策略:时效优先、成本优先、综合评分 规则引擎:支持灵活配置路由规则 评分模型:多因素加权,可调整权重 6.2 实施建议 先实现基础的就近发货策略 再实现规则引擎,支持特殊场景 最后实现综合评分策略 持续优化权重参数 系列文章导航
...