仓位规划与补货: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) 实现编排与一致性。
总体架构 🗺️
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.Slotting、Wms.Replenishment、Wms.Execution(跨上下文通过应用服务/分布式事件)。
多租户
所有聚合实现 IMultiTenant;后台作业用 ICurrentTenant.Change(tenantId) 切换上下文执行,保证隔离与幂等。
领域模型 🧩
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 策略流转 🧠
3) 热力路径与库位评分(可解释、可调参)🔥
指标定义
freq:SKU 拣选命中率/频次(近 7–30 天)travelTime:拣选站↔货位最短代价(图模型:节点=货位/交叉口/拣选站;边=通道/垂直连接,权重=长度/设备速度/切层时间)congestion:1 - 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。
评分数据管线 ⚙️
ℹ️ “拣选旅行时间占比高(如 60–75%)”属行业经验,需以本仓实测校准。
4) 波次前置补货流程 ⏳
触发:① 切单前 T 分钟;② 每 N 分钟滚动;③ 热 SKU 事件。
步骤:
- 汇总波次→计算拣选位缺口;
- 从补货池择优(同区最近、最少拆托、避拥堵);
- 生成
ReplenishmentTask(reason=pre-wave)(与拣选资源抢占规则可配置); - 冲突检测(容量/叉车/人力/封锁)→ 回退(拆单/替代/延后/降级波次)。
前置补货时序 📩
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=7dPOST /replenishments/plan?chaos=false(波次 id/时间窗/策略阈值;chaos=true开启故障注入)POST /replenishments/tasks/dispatchPATCH /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"
初始化
- 导入 SKU/库位/历史订单(含零需求段);
dotnet run启动;GET /slotting/hotmap验证热力/拥堵指标;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 }
}


被折叠的 条评论
为什么被折叠?



