4、核心算法实现:基于库存的最优匹配算法、成本最优拆分策略、时效优先拆分策略

好,咱们直接进入正题。前面几章我们把订单拆分的业务场景、数据模型都聊透了。这一章,是真正动手写代码的时候了。

我个人习惯,在讲具体算法之前,先画一张图,把三种核心策略的脉络理清楚。你想想看,没有地图就进森林,很容易迷路。

订单拆分核心算法策略 订单输入 拆分策略选择 库存最优匹配 优先消耗库存 成本最优拆分 最小化履约成本 时效优先拆分 最快送达 拆分子订单A 拆分子订单B 拆分子订单C 最终履约方案

这张图就是我们的作战地图。三种策略,对应三种不同的业务诉求。下面我一个一个拆开来讲。

4.1 基于库存的最优匹配算法

这个算法,说白了就是「哪个仓库有货,就优先从哪个仓库发」。听起来简单?嗯,这里要注意,实际场景远比你想象的复杂。

我在项目中遇到过这样一个坑:用户下单买了5件商品,系统发现A仓有3件,B仓有2件。如果简单按库存量匹配,A仓发3件,B仓发2件,看起来没问题。但问题是,A仓和B仓不在同一个城市,用户要付两笔运费,体验很差。

所以,真正的库存最优匹配,不是「有货就发」,而是「在满足发货条件的前提下,尽量用最少的仓库覆盖最多的商品」。

核心逻辑是这样的:

// 伪代码:库存最优匹配算法
function matchByInventory(orderItems, warehouseInventory) {
    // 1. 按仓库库存量降序排序
    let sortedWarehouses = sortByStockDesc(warehouseInventory);
    
    // 2. 贪心匹配:优先从库存最多的仓库开始
    let result = [];
    let remainingItems = [...orderItems];
    
    for (let warehouse of sortedWarehouses) {
        if (remainingItems.length === 0) break;
        
        let matchedItems = [];
        for (let item of remainingItems) {
            if (warehouse.stock[item.sku] >= item.quantity) {
                matchedItems.push(item);
                warehouse.stock[item.sku] -= item.quantity;
            }
        }
        
        if (matchedItems.length > 0) {
            result.push({
                warehouseId: warehouse.id,
                items: matchedItems
            });
            // 从待匹配列表中移除已匹配的商品
            remainingItems = remainingItems.filter(
                item => !matchedItems.includes(item)
            );
        }
    }
    
    return result;
}
我的经验:这个算法有个隐含假设——库存是充足的。如果库存不足,需要配合「缺货标记」一起使用。我曾经因为没做这个判断,导致系统生成了无法履约的订单,被运营同事追着骂了一下午。

4.2 成本最优拆分策略

成本最优,目标很明确:让履约成本最低。这里的成本包括:仓储费、配送费、包装费,甚至还有退货率折算的成本。

你想想看,如果用户买了一件99元的T恤和一台5000元的电脑,从同一个仓库发,配送费可能只要10元。但如果分开发,T恤从本地仓发(运费5元),电脑从总仓发(运费15元),总运费20元。哪个更划算?显然是合在一起发。

但问题来了:合在一起发,电脑的包装要求高,需要额外的泡沫箱和防震材料,包装成本可能增加8元。这时候,分开发反而更便宜。

所以,成本最优拆分,本质上是一个「组合优化问题」。我常用的方法是动态规划:

// 伪代码:成本最优拆分(动态规划)
function costOptimizedSplit(orderItems, warehouses) {
    // dp[i][j] 表示前i个商品,使用j个仓库的最小成本
    let n = orderItems.length;
    let m = warehouses.length;
    let dp = Array(n+1).fill(null).map(() => Array(m+1).fill(Infinity));
    dp[0][0] = 0;
    
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            // 尝试将第i个商品分配给第j个仓库
            let cost = calculateCost(orderItems[i-1], warehouses[j-1]);
            dp[i][j] = Math.min(dp[i][j], dp[i-1][j-1] + cost);
            
            // 尝试将第i个商品与前面的商品合并到同一个仓库
            for (let k = i-1; k >= 1; k--) {
                let combinedCost = calculateCombinedCost(
                    orderItems.slice(k-1, i), 
                    warehouses[j-1]
                );
                dp[i][j] = Math.min(dp[i][j], dp[k-1][j-1] + combinedCost);
            }
        }
    }
    
    return reconstructSolution(dp, orderItems, warehouses);
}
注意:动态规划的时间复杂度是O(n²·m),当订单商品数超过20件时,性能会急剧下降。我建议在实际系统中,先对商品按「体积/重量」做聚类,再跑DP,这样能大幅降低计算量。

4.3 时效优先拆分策略

这个策略,是用户体验的底线。用户下单时选了「次日达」,你就必须保证每个子订单都能在24小时内送达。

时效优先的核心逻辑,不是「哪个仓库快就用哪个」,而是「所有仓库都必须满足时效要求」。如果一个仓库无法在承诺时间内送达,哪怕它库存再多、成本再低,也不能用。

我举个例子:用户在北京,买了一件羽绒服和一本笔记本。羽绒服在天津仓有货(次日达),笔记本在武汉仓有货(需要3天)。如果按时效优先,笔记本就不能从武汉仓发,必须从北京本地仓调货,哪怕成本高一些。

具体实现时,我习惯用「时效约束过滤 + 就近匹配」两步走:

// 伪代码:时效优先拆分
function timePrioritySplit(orderItems, warehouses, deliveryPromise) {
    // 第一步:时效约束过滤
    let eligibleWarehouses = warehouses.filter(warehouse => {
        return warehouse.deliveryTime[deliveryPromise] !== undefined;
    });
    
    // 第二步:按距离/时效排序,就近匹配
    let sortedWarehouses = eligibleWarehouses.sort((a, b) => {
        return a.deliveryTime[deliveryPromise] - b.deliveryTime[deliveryPromise];
    });
    
    let result = [];
    let remainingItems = [...orderItems];
    
    for (let warehouse of sortedWarehouses) {
        if (remainingItems.length === 0) break;
        
        let matchedItems = [];
        for (let item of remainingItems) {
            // 检查该仓库是否能满足时效
            if (warehouse.canDeliver(item, deliveryPromise)) {
                matchedItems.push(item);
            }
        }
        
        if (matchedItems.length > 0) {
            result.push({
                warehouseId: warehouse.id,
                items: matchedItems,
                estimatedDelivery: warehouse.deliveryTime[deliveryPromise]
            });
            remainingItems = remainingItems.filter(
                item => !matchedItems.includes(item)
            );
        }
    }
    
    // 如果还有剩余商品无法满足时效,需要走「降级策略」
    if (remainingItems.length > 0) {
        result = fallbackStrategy(remainingItems, result);
    }
    
    return result;
}
关键点:时效优先策略中,「降级策略」是必须的。当某个商品没有任何仓库能满足时效时,是延长承诺时间,还是取消订单?这需要业务方给出明确的规则。我见过最坑的情况是,系统默默降级了时效,用户投诉说「说好的次日达呢?」——所以,降级一定要有用户确认环节。

三种策略的对比与选择

讲到这里,你可能要问了:这三种策略,到底用哪个?

我的建议是:不要单选,要组合。实际系统中,往往是「库存匹配打底,成本优化调优,时效约束兜底」。

策略 适用场景 优点 缺点
库存最优匹配 大促期间、库存紧张 避免缺货,履约率高 可能增加配送成本
成本最优拆分 日常订单、利润敏感 降低履约成本 计算复杂,响应慢
时效优先拆分 会员订单、高价值用户 用户体验好 成本可能偏高

我曾经在一个项目中,把三种策略做成了「可配置的权重模型」。运营同学可以在后台调整权重:比如大促期间,库存权重设为0.6,成本权重0.2,时效权重0.2。这样系统就能根据业务目标,自动选择最优的拆分方案。

嗯,这个权重模型的具体实现,涉及一些数学计算,篇幅有限就不展开了。但核心思路就是:把多目标优化问题,转化为单目标优化问题,然后用贪心或DP求解。

最后说一句:算法再漂亮,也要经过线上验证。我建议先用小流量灰度测试,对比三种策略的履约率、成本和用户满意度,再决定最终方案。毕竟,理论归理论,实战才是检验真理的唯一标准。


专注资料整理