仓位规划与补货:ABC/XYZ + 热力路径的“智能补货”模块

仓位规划与补货:ABC/XYZ + 热力路径的“智能补货”模块 🚀



0) 目标与范围 🎯

  • 目标:用 ABC/XYZ + 热力路径评分库位规划(slotting)补货决策;在 ABP vNext 中以 后台作业 周期生成 ReplenishmentTask,支持 波次前置补货冲突/缺工位回退
  • 范围:仓内(Inbound→Storage→Replenishment→Picking)。
  • 方法:ABC×XYZ(9 宫格)分层 + ROP/Safety Stock;Z 类用 Croston/SBA;热力路径评分驱动重槽位与补货优先级;ABP 的 BackgroundWorkers + Distributed Lock + Event Bus(Outbox/Inbox) 实现编排与一致性。

总体架构 🗺️

WMS Core
租户循环 + 分布式锁
Publish useOutbox:true
ReplenishmentAppService
Planner/Resolver/Dispatcher
Slotting/Scorer
Background Worker ⏱️
PostgreSQL
Redis
Distributed Event Bus
Ops Dashboard/Alerts
Picking Exec Service

1) 领域建模 🏗️

主数据
Sku(Id, Code, Uom, Pack, Cube)
Location(Id, Zone, Aisle, Level, Type)
Slot(Id, LocationId, Capacity, CubeLimit, Score)
Tenant(Id)

业务数据
Inventory(SkuId, SlotId, OnHand, Reserved)
Demand(OrderLineId, SkuId, Qty, NeedByTime)
Wave(Id, Cutoff, Carrier)

计划与任务
ReplenishmentPlan(Id, TenantId, Window, StrategyKey, IdempotencyKey)
ReplenishmentTask(Id, FromSlot, ToSlot, Qty, Priority, Reason, Status, ConcurrencyStamp)

上下文边界
Wms.SlottingWms.ReplenishmentWms.Execution(跨上下文通过应用服务/分布式事件)。

多租户
所有聚合实现 IMultiTenant;后台作业用 ICurrentTenant.Change(tenantId) 切换上下文执行,保证隔离与幂等。

领域模型 🧩

1
many
1
many
1
many
1
many
1
many
1
many
«interface»
ITenantReader
+GetTenantIdsAsync(page,size,ct)
«interface»
IMultiTenant
Tenant
+Guid Id
Sku
+Guid Id
+string Code
+string Uom
+int Pack
+double Cube
Location
+Guid Id
+string Zone
+string Aisle
+string Level
+string Type
Slot
+Guid Id
+Guid LocationId
+int Capacity
+double CubeLimit
+double Score
Inventory
+Guid SkuId
+Guid SlotId
+int OnHand
+int Reserved
+Reserve(int qty)
Demand
+Guid OrderLineId
+Guid SkuId
+int Qty
+DateTime NeedByTime
Wave
+Guid Id
+DateTime Cutoff
+string Carrier
ReplenishmentPlan
+Guid Id
+Guid TenantId
+TimeSpan Window
+string StrategyKey
+string IdempotencyKey
ReplenishmentTask
+Guid Id
+string FromSlot
+string ToSlot
+int Qty
+int Priority
+string Reason
+string Status
+string ConcurrencyStamp

2) ABC/XYZ 与补货阈值 📊

ABC(价值/频次):常用模板——A≈前 70–80%,B≈15–20%,C≈5–10%(可调)。
XYZ(稳定性/可预测性):用 CV = σ/μ;两套可切换模板:

  • 标准:X≤0.25,Y∈(0.25,0.5],Z>0.5
  • 严格:X<0.10,Y∈[0.10,0.25],Z>0.25

⚠️ 阈值非行业常数,应结合 SLO(如 95%)与品类波动校准。

SafetyStock/ROP

  • 常规:SafetyStock = z * sqrt(σD^2 * L + μD^2 * σL^2)ROP = μD * L + SafetyStock
  • Z 类(间歇性):用 Croston/SBA 得到期均需求 f,到货期需求≈f * L;若到货期不确定,保留 σL 项进入 SS 计算(见 §9)。

ABC×XYZ 策略流转 🧠

分位/累计
阈值模板
AX/AY/BX...
计算年度价值贡献 or 拣选频次
分层: A/B/C
计算CV=σ/μ
分层: X/Y/Z
9宫格策略矩阵
库位策略&补货阈值
计算ROP/SS
进入补货计划&任务生成

3) 热力路径与库位评分(可解释、可调参)🔥

指标定义

  • freq:SKU 拣选命中率/频次(近 7–30 天)
  • travelTime:拣选站↔货位最短代价(图模型:节点=货位/交叉口/拣选站;边=通道/垂直连接,权重=长度/设备速度/切层时间)
  • congestion1 - v_actual / v_freeflow(近 15 分钟班次化均值;来自 RTLS/历史轨迹/波次负载)
  • cluster:订单内共现 PMI(近 7 天)

归一化与权重

  • 默认 min-max(可切换 z-score);
  • score = w1*norm(freq) + w2*(-norm(travelTime)) + w3*(-norm(congestion)) + w4*norm(cluster)
  • 重槽位触发Δscore > θsaving_distance - move_cost > 0

评分数据管线 ⚙️

历史订单/RTLS/拓扑
清洗/聚合
归一化(min-max/z-score)
权重合成 w1..w4
库位得分 Score
Δscore>θ 且 预计收益>0?
生成重槽位建议
维持现状

ℹ️ “拣选旅行时间占比高(如 60–75%)”属行业经验,需以本仓实测校准。


4) 波次前置补货流程 ⏳

触发:① 切单前 T 分钟;② 每 N 分钟滚动;③ 热 SKU 事件。
步骤

  1. 汇总波次→计算拣选位缺口
  2. 从补货池择优(同区最近、最少拆托、避拥堵);
  3. 生成 ReplenishmentTask(reason=pre-wave)(与拣选资源抢占规则可配置);
  4. 冲突检测(容量/叉车/人力/封锁)→ 回退(拆单/替代/延后/降级波次)。

前置补货时序 📩

Worker ReplenishmentAppService Planner/Resolver DB/Redis EventBus Picking Exec Dashboard TryAcquire(lock per tenant) 1 BuildPlanAndDispatch() 2 Aggregate demand / gap 3 Read inv/slots/topology 4 Tasks(pre-wave) 5 Resolve conflicts/fallback 6 ResolvedTasks 7 Publish(useOutbox:true) 8 Dispatch 9 Update dashboard 10 Skip tenant (next cycle) 11 alt [Got Lock] [No Lock] Worker ReplenishmentAppService Planner/Resolver DB/Redis EventBus Picking Exec Dashboard

5) 任务编排与后台作业(ABP vNext)🧵

5.1 模块依赖与注册

[DependsOn(
    typeof(AbpBackgroundWorkersModule),
    typeof(AbpDistributedLockingModule) // 还需引入对应 Provider,如 Redis
)]
public class WmsApplicationModule : AbpModule
{
    public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext ctx)
    {
        await ctx.AddBackgroundWorkerAsync<PreWaveReplenishWorker>();
    }
}

5.2 可配置选项(分页/并发/窗口/锁)

public class ReplenishWorkerOptions
{
    public int PageSize { get; set; } = 100;          // 多租户分页
    public int MaxConcurrency { get; set; } = 8;      // 并发上限(控 DB/Redis QPS)
    public TimeSpan PlanWindow { get; set; } = TimeSpan.FromMinutes(30);
    public int PreWaveMinutes { get; set; } = 15;
    public TimeSpan LockTtl { get; set; } = TimeSpan.FromMinutes(60); // Provider 层TTL约定
}

🧰 通过 IOptionsSnapshot<ReplenishWorkerOptions> 做租户/仓级覆盖与热更新。

5.3 ITenantReader 实现指引(避免全量拉取)

// 示例:基于自有租户表的最简分页读取(伪代码)
public class EfTenantReader : ITenantReader
{
    private readonly MyTenantDbContext _db;
    public EfTenantReader(MyTenantDbContext db) => _db = db;

    public async Task<IReadOnlyList<Guid>> GetTenantIdsAsync(int page, int size, CancellationToken ct)
        => await _db.Tenants
            .OrderBy(t => t.Id)
            .Skip((page - 1) * size)
            .Take(size)
            .Select(t => t.Id)
            .ToListAsync(ct);
}

5.4 Worker(作用域解析 + 分布式锁 + 多租户分页/限流 + 日志)

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedLocking;
using Volo.Abp.MultiTenancy;

public interface ITenantReader { Task<IReadOnlyList<Guid>> GetTenantIdsAsync(int page, int size, CancellationToken ct); }

public class PreWaveReplenishWorker : AsyncPeriodicBackgroundWorkerBase
{
    private readonly ICurrentTenant _currentTenant;
    private readonly ITenantReader _tenantReader;
    private readonly IAbpDistributedLock _distLock;
    private readonly ReplenishWorkerOptions _opt;
    private readonly ILogger<PreWaveReplenishWorker> _logger;

    public PreWaveReplenishWorker(
        AbpAsyncTimer timer,
        IServiceScopeFactory scopeFactory,
        ICurrentTenant currentTenant,
        ITenantReader tenantReader,
        IAbpDistributedLock distLock,
        IOptionsSnapshot<ReplenishWorkerOptions> opt,
        ILogger<PreWaveReplenishWorker> logger)
        : base(timer, scopeFactory)
    {
        Timer.Period = 60_000; // 每分钟
        _currentTenant = currentTenant;
        _tenantReader = tenantReader;
        _distLock = distLock;
        _opt = opt.Value;
        _logger = logger;
    }

    protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext context)
    {
        var ctk = context.CancellationToken;
        for (var page = 1; ; page++)
        {
            var batch = await _tenantReader.GetTenantIdsAsync(page, _opt.PageSize, ctk);
            if (batch.Count == 0) break;

            using var sem = new SemaphoreSlim(_opt.MaxConcurrency); // 控 DB/Redis 峰值
            var tasks = batch.Select(async tid =>
            {
                await sem.WaitAsync(ctk);
                try
                {
                    using (_currentTenant.Change(tid))
                    using var scope = context.ServiceProvider.CreateScope();
                    var svc = scope.ServiceProvider.GetRequiredService<IReplenishmentAppService>();

                    // 每租户互斥:拿不到锁即“跳过本轮”(正常并发情形)
                    var lockName = $"replenish:plan:{tid}";
                    await using var handle = await _distLock.TryAcquireAsync(lockName);
                    if (handle is null)
                    {
                        _logger.LogDebug("Skip tenant {Tenant} (lock not acquired).", tid);
                        return;
                    }

                    // TTL/续约在 Provider (如 Redis) 层配置,建议 TTL ≈ 2×PlanWindow
                    await svc.BuildPlanAndDispatchAsync(
                        new PlanArgs(_opt.PlanWindow, _opt.PreWaveMinutes),
                        ctk);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "PreWave plan failed for tenant {Tenant}.", tid);
                }
                finally { sem.Release(); }
            });
            await Task.WhenAll(tasks);
        }
    }
}

5.5 事件与一致性(Outbox/Inbox)

  • 推荐:为分布式事件显式启用 Outbox,消费者启用 Inbox 去重
  • ⚠️ 不要依赖默认值:不同版本/配置默认项可能不同,本文采用 useOutbox: true 明确指定。
  • 多租户事件携带 TenantId 以自动传播上下文。
  • 🔔 锁 TTL 运行约定:锁 Provider(如 Redis)需配置租约 TTL 与自动续约建议 TTL ≈ 2×PlanWindow,防止故障致锁悬挂。

6) 冲突/回退与缺工位策略 🧯

  • 库存不足:二级库位/跨区借调;必要时拣选改路径并延后补货;
  • 工位/叉车不足:排队 + 合并同路径任务;拥堵阈值触发延时;
  • 库位封锁:临时黑名单/降级到邻近库位(取评分次优);
  • 失败重试:指数退避 + 人工干预队列;事件流可回放、可审计

7) API/DTO(最小集 + 幂等契约)🛠️

  • GET /slotting/hotmap?zone=Z1&period=7d
  • POST /replenishments/plan?chaos=false(波次 id/时间窗/策略阈值;chaos=true 开启故障注入)
  • POST /replenishments/tasks/dispatch
  • PATCH /tasks/{id}/start|finish|fail
public record PlanArgs(TimeSpan Window, int PreWaveMinutes,
    double CvX=0.25, double CvY=0.5, double ServiceLevel=0.95);

public record ReplenishmentTaskDto(Guid Id, string FromSlot, string ToSlot,
    string Sku, int Qty, int Priority, string Reason, string Status, string ConcurrencyStamp);

📌 幂等契约:同一租户 + 同一窗口只允许生成一次计划(IdempotencyKey = tenantId + windowRange)。
🧷 写入时机:在创建计划前先写入带过期的去重表(或依赖唯一键),写入成功再继续;写入失败则说明已生成过,直接返回(自然幂等)。


8) 可观测 & SLO(口径可复现)📈

  • 补货命中率 =(拣选开始前已就位的拣选位数)/(需补货的拣选位数)。窗口:每波次 + 每班次
  • 平均补货时长:任务创建→就位平均时长。窗口:15 分钟滑动 + 每班次
  • 二次走位率:有二次回位的订单行 / 总订单行。窗口:每日 + 每区域
  • 人均/单均拣选行走距离:RTLS/路径估算。窗口:每班次
  • 拥堵热区告警congestion > 0.4 持续 ≥5 分钟(1 分钟粒度)。

指标经事件总线汇聚(Outbox/Inbox 保证),推荐接入 OpenTelemetry + 时序库(Prometheus/ClickHouse)。


9) 校验与仿真(含 Croston/SBA 稳健实现)🧪

对照组:A=仅 ABC;B=ABC+热力;C=ABC+热力+前置补货
指标:拣选距离、补货命中率、订单周期时长(历史 4–8 周回放;按班次/区域分层,做统计显著性检验)。

Croston/SBA(单遍在线,含 α 校验/输入清洗,新增 return_next)

# croston_sba.py
import numpy as np

def croston_sba(y, alpha=0.1, return_next=False):
    """
    Intermittent demand forecasting (Croston with SBA bias correction).
    Returns per-period expected demand f_t; lead-time demand ≈ f_t * L.
    Args:
        y: 1D array-like of nonnegative demand (zeros allowed)
        alpha: smoothing factor in (0,1)
        return_next: if True, return next-period forecast (scalar) instead of full series
    """
    y = np.asarray(y, dtype=float)
    if not (0 < alpha < 1):
        raise ValueError("alpha must be in (0,1)")
    y = np.where(np.isfinite(y) & (y > 0), y, 0.0)  # sanitize negatives/NaN/inf

    f = np.zeros_like(y)
    nz = np.where(y > 0)[0]
    if len(nz) == 0:
        return 0.0 if return_next else f

    a = y[nz[0]]            # demand size level
    p = float(nz[0] + 1)    # inter-arrival level = first nonzero interval
    q, eps = 0, 1e-8

    for t in range(len(y)):
        q += 1
        if y[t] > 0:
            a = alpha * y[t] + (1 - alpha) * a
            p = alpha * q    + (1 - alpha) * p
            q = 0
        f[t] = (1 - alpha/2.0) * (a / max(p, eps))  # SBA correction + guard

    return f[-1] if return_next else f

📝 说明:f_t期均需求。若到货期为 L,则 L 期间需求≈f_t * L。若 L 存在不确定性,请在 SS 计算中保留 σL 项。


10) 应用服务与事件(落地骨架)🧩

应用服务(评分 + 阈值 + 波次前置 + Outbox 显式开启 + 幂等去重写入)

public async Task BuildPlanAndDispatchAsync(PlanArgs args, CancellationToken ct)
{
    // 1) ABC/XYZ(累积价值 + CV)
    var segments = await _stats.GetAbcXyzAsync(new XyzThresholds(args.CvX, args.CvY), ct);

    // 2) 热力评分(freq/travel/congestion/cluster 标准化 + 加权)
    var slots = await _slotRepo.GetAllAsync(ct);
    foreach (var s in slots) s.Score = _scorer.Score(s);

    // 3) 生成幂等计划键(租户+窗口),先写去重表/唯一键,成功才继续
    var idemKey = _planner.MakeIdempotencyKey(args.Window);
    if (!await _planner.EnsureIdempotencyAsync(idemKey, ct)) return;

    var plan = await _planner.BuildAsync(args.Window, args.PreWaveMinutes, segments, ct);
    plan.IdempotencyKey = idemKey;

    var tasks = _dispatcher.CreateTasks(plan, reason: "pre-wave");

    // 4) 冲突检测与回退
    var resolved = await _resolver.ResolveAsync(tasks, ct);

    // 5) 发布事件(显式启用 Outbox;消费者侧启用 Inbox 去重)
    foreach (var t in resolved)
        await _eventBus.PublishAsync(new ReplenishmentTaskCreatedEto(t), useOutbox: true);
}

事件处理(简化)

public class ReplenishmentTaskCreatedHandler :
    IDistributedEventHandler<ReplenishmentTaskCreatedEto>, ITransientDependency
{
    public async Task HandleEventAsync(ReplenishmentTaskCreatedEto eto)
    {
        // 推送到看板/告警;或触发执行微服务
        await Task.CompletedTask;
    }
}

常用 using 汇总(减少踩坑)

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedLocking;
using Volo.Abp.MultiTenancy;

11) 一键复现 🐳

docker-compose.yml(片段)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: wms
    ports: ["5432:5432"]
  redis:
    image: redis:7
    ports: ["6379:6379"]
  wms:
    build: .
    depends_on: [db, redis]
    environment:
      ConnectionStrings__Default: "Host=db;Port=5432;Database=wms;Username=postgres;Password=wms"

初始化

  1. 导入 SKU/库位/历史订单(含零需求段);
  2. dotnet run 启动;
  3. GET /slotting/hotmap 验证热力/拥堵指标;
  4. POST /replenishments/plan?chaos=false 触发 Plan→Task→Event 全链路。

参数模板 🧾

{
  "Thresholds": {
    "ABC": { "A": 0.8, "B": 0.95 },
    "XYZ": { "X": 0.25, "Y": 0.5 },   // 或严格模板 { "X": 0.10, "Y": 0.25 }
    "ServiceLevel": 0.95
  },
  "Scoring": {
    "Normalization": "minmax",         // 可 minmax 或 zscore
    "Weights": { "freq": 0.4, "travelTime": 0.35, "congestion": 0.15, "cluster": 0.10 },
    "ReSlottingTrigger": { "scoreGain": 0.12, "minSavingMeters": 250 }
  },
  "PreWave": { "WindowMinutes": 30, "FireBeforeCutoffMinutes": 15 },
  "Worker": { "PageSize": 100, "MaxConcurrency": 8, "LockTtlMinutes": 60 },
  "Congestion": { "Threshold": 0.4, "WindowSec": 300 }
}

附:运行时交互全景 🌊

Obs
Execution
Pre-Wave
Slotting & Replenishment
SLO计算
事件汇聚
看板/告警
拣选/补货执行
任务下发
计算缺口
汇总波次需求
择优来源
生成任务
冲突检测/回退
热力评分
统计ABC/XYZ
重槽位建议
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kookoos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值