案例 7:零售库存补货建议
本案例对应 JQuick-Java 仓库 USE-CASES.md 的场景 7:零售库存补货建议,属于供应链/连锁零售领域,解决多仓 SKU 的「需求测算 → 安全库存 → 目标库存 → 缺口 → MOQ/整箱/上限约束 → 紧急度与加急建议」整条补货决策链路。
连锁零售企业的补货系统需要按 SKU、按仓库给出日常下单建议。每个商品维护一组补货参数:当前库存、基础安全库存、日均销量、补货提前期(天)、最小起订量 MOQ、整箱包装规格、供应商履约率(0-1)、是否处于促销期、旺淡季标记(PEAK/NORMAL/OFF)以及单次最大下单量。
规则链路先计算提前期需求(日均销量 × 提前期天数);动态安全库存在基础值之上,促销期额外加 5 天日均销量,供应商履约率低于 0.9 时再加 3 天日均销量;目标库存为安全库存与提前期需求之和,旺季再上浮 7 天销量、淡季下调 3 天销量,并保证不为负。
随后计算库存缺口(目标库存减当前库存,不为负),建议下单量依次经过三道供应约束:不足 MOQ 时按 MOQ 起订、按整箱包装规格向上取整、超过供应商单次上限时封顶(超出部分提示拆单)。与此同时,系统按当前库存可售天数相对提前期的位置,输出 CRITICAL/HIGH/MEDIUM/LOW 四级紧急度,并在可售天数不足提前期(或旺季临近)时建议加急物流。
决策结果包含提前期需求、安全库存、目标库存、缺口、最终下单量、可售天数、紧急度、是否加急以及一组备注(MOQ 上调、整箱取整、封顶拆单、促销与履约率说明等),直接驱动采购下单与仓间调度。场景 7 覆盖 6 组形态:紧急缺货、库存充足短路、促销旺季按 MOQ 上调到 500、淡季整箱取整为 60、中等紧急 HIGH、爆款大单封顶 1000 建议拆单。
- 促销加 5 天、低履约率加 3 天、旺季加 7 天、淡季减 3 天等修正系数散落在定时任务、SQL 与各处硬编码中,换季或换供应商时要协调多处发版。
- MOQ 最小起订、整箱取整、单次上限三道约束顺序敏感,硬编码实现经常漏掉其中一道,导致采购下单被供应商退回。
- 紧急度阈值随提前期浮动(不超过提前期、+3 天、+7 天),不同仓库、不同提前期的商品难以统一口径。
- 大促前参数需要频繁调整,但发布窗口有限,改个安全库存天数都要排期上线,容易错过备货窗口。
- 补货结果往往只给一个下单数量,采购不理解「缺口 390 为什么下单 500」「2085 的缺口为什么只下 1000」,缺乏可解释的约束备注。
- 不少 WMS/ERP 仍运行在 JDK 8 或信创环境,重型供应链计划平台落不下去。
本案例采用标准「六件套」:入参模型承载 SKU 与供应参数,决策结果模型承载整条链路的中间值与建议,规则接口声明 10 个规则方法,XML 脚本外置全部测算口径与阈值,Service 串联补货流水线并组装备注,JUnit 以 6 组补货形态回归。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | SKU 补货参数、补货决策结果 | ReplenishmentItem.java、ReplenishDecision.java |
| 规则集与 XML | 需求/安全库存/目标库存、MOQ/整箱/上限、可售天数/紧急度共 10 个方法 | InventoryRules.java、inventory.xml |
| 编排服务 | 补货链路串联、封顶拆单提示、库存充足短路、备注组装 | ReplenishmentService.java |
| 测试 | 6 组补货形态(紧急/充足/促销旺季/淡季取整/中等紧急/封顶) | ReplenishmentServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
demandDuringLeadTime |
日均销量 × 提前期天数 | 提前期需求量 |
safetyStockLevel |
基础安全库存;促销 +日均×5;履约率低于 0.9 再 +日均×3 | 动态安全库存 |
targetStock |
安全库存 + 提前期需求;旺季 PEAK +日均×7;淡季 OFF -日均×3;不为负 | 目标库存 |
shortage |
目标库存 - 当前库存,结果不为负 | 库存缺口 |
adjustToMoq |
下单量高于 0 且低于 MOQ 时按 MOQ 下单 | MOQ 后数量 |
roundToPack |
按整箱包装规格向上取整(脚本中以 while 循环累加实现) | 整箱数量 |
stockDays |
当前库存 / 日均销量;日均销量不大于 0 时返回 999 防除零 | 可售天数 |
urgency |
≤提前期 CRITICAL / ≤提前期+3 HIGH / ≤提前期+7 MEDIUM / 否则 LOW | CRITICAL/HIGH/MEDIUM/LOW |
suggestExpress |
可售天数 ≤提前期,或旺季且 ≤提前期+3 → 建议加急 | true / false |
cappedQty |
下单量超过单次上限时封顶为上限 | 最终下单量 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将六个文件按仓库原有包结构放入 Maven 工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试放入 com.github.paohaijiao.demo 包,编排服务放入 com.github.paohaijiao.demo.service 包,XML 脚本放入 src/test/resources/demo/inventory.xml,包名全部保持源码原样。工程根目录执行 mvn test -Dtest=ReplenishmentServiceTest 即可运行。
ReplenishmentItem.java
Section titled “ReplenishmentItem.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 补货决策要素(零售补货所需的商品、库存与供应参数) */@Data@NoArgsConstructor@AllArgsConstructorpublic class ReplenishmentItem {
/** * 商品编码 */ private String sku;
/** * 商品名称 */ private String skuName;
/** * 仓库 */ private String warehouse;
/** * 当前库存 */ private int currentStock;
/** * 基础安全库存 */ private int safetyStock;
/** * 日均销量 */ private int avgDailySales;
/** * 补货提前期(天) */ private int leadTimeDays;
/** * 最小起订量 */ private int moq;
/** * 包装规格(整箱数量) */ private int packSize;
/** * 供应商履约率(0-1) */ private double supplierReliability;
/** * 是否促销期 */ private boolean promotion;
/** * 季节系数:PEAK-旺季 / NORMAL-平季 / OFF-淡季 */ private String seasonFactor;
/** * 单次最大下单量 */ private int maxOrder;}ReplenishDecision.java
Section titled “ReplenishDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 补货决策结果 */@Datapublic class ReplenishDecision {
/** * 商品编码 */ private String sku;
/** * 补货提前期需求 */ private int demandDuringLeadTime;
/** * 动态安全库存 */ private int safetyStockLevel;
/** * 目标库存 */ private int targetStock;
/** * 库存缺口 */ private int shortage;
/** * 最终建议下单量(已考虑 MOQ、包装与上限) */ private int orderQty;
/** * 可售天数 */ private int stockDays;
/** * 紧急度:CRITICAL / HIGH / MEDIUM / LOW */ private String urgency;
/** * 是否建议加急补货 */ private boolean suggestExpress;
/** * 说明与建议 */ private List<String> notes = new ArrayList<String>();
public void addNote(String note) { this.notes.add(note); }}InventoryRules.java
Section titled “InventoryRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 7(生产级样板):零售库存补货规则集 * <p> * 规则按「需求测算 → 缺口计算 → 下单约束 → 紧急度」拆分: * <ul> * <li>测算类:提前期需求、动态安全库存、目标库存</li> * <li>约束类:MOQ、包装规格取整(规则内演示 while 循环)、单次上限</li> * <li>决策类:可售天数、紧急度、是否加急</li> * </ul> * 规则脚本见 {@code src/test/resources/demo/inventory.xml}。 */public interface InventoryRules {
/** * 补货提前期需求 = 日均销量 × 提前期 */ int demandDuringLeadTime(@Param("avgDailySales") int avgDailySales, @Param("leadTimeDays") int leadTimeDays);
/** * 动态安全库存:促销期与低履约率供应商需要更高安全库存 */ int safetyStockLevel(@Param("safetyStock") int safetyStock, @Param("avgDailySales") int avgDailySales, @Param("supplierReliability") double supplierReliability, @Param("promotion") boolean promotion);
/** * 目标库存:安全库存 + 提前期需求,并按季节系数修正 */ int targetStock(@Param("safetyStock") int safetyStock, @Param("demandDuringLeadTime") int demandDuringLeadTime, @Param("seasonFactor") String seasonFactor, @Param("avgDailySales") int avgDailySales);
/** * 库存缺口 */ int shortage(@Param("targetStock") int targetStock, @Param("currentStock") int currentStock);
/** * 最小起订量约束 */ int adjustToMoq(@Param("qty") int qty, @Param("moq") int moq);
/** * 包装规格向上取整(while 循环示例) */ int roundToPack(@Param("qty") int qty, @Param("packSize") int packSize);
/** * 可售天数 */ int stockDays(@Param("currentStock") int currentStock, @Param("avgDailySales") int avgDailySales);
/** * 紧急度:CRITICAL / HIGH / MEDIUM / LOW */ String urgency(@Param("stockDays") int stockDays, @Param("leadTimeDays") int leadTimeDays);
/** * 是否建议加急补货 */ boolean suggestExpress(@Param("stockDays") int stockDays, @Param("leadTimeDays") int leadTimeDays, @Param("seasonFactor") String seasonFactor);
/** * 单次下单量上限 */ int cappedQty(@Param("qty") int qty, @Param("maxOrder") int maxOrder);}inventory.xml
Section titled “inventory.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.InventoryRules">
<!-- 提前期需求 --> <java name="demandDuringLeadTime" returnClass="int"> <![CDATA[ int def demandDuringLeadTime(int:avgDailySales, int:leadTimeDays) { return avgDailySales * leadTimeDays; } ]]> </java>
<!-- 动态安全库存 --> <java name="safetyStockLevel" returnClass="int"> <![CDATA[ int def safetyStockLevel(int:safetyStock, int:avgDailySales, double:supplierReliability, boolean:promotion) { int level = safetyStock; if (promotion) { level = level + avgDailySales * 5; } if (supplierReliability < 0.9) { level = level + avgDailySales * 3; } return level; } ]]> </java>
<!-- 目标库存 --> <java name="targetStock" returnClass="int"> <![CDATA[ int def targetStock(int:safetyStock, int:demandDuringLeadTime, str:seasonFactor, int:avgDailySales) { int target = safetyStock + demandDuringLeadTime; if (seasonFactor == "PEAK") { target = target + avgDailySales * 7; } else if (seasonFactor == "OFF") { target = target - avgDailySales * 3; } if (target < 0) { target = 0; } return target; } ]]> </java>
<!-- 库存缺口 --> <java name="shortage" returnClass="int"> <![CDATA[ int def shortage(int:targetStock, int:currentStock) { int gap = targetStock - currentStock; if (gap < 0) { gap = 0; } return gap; } ]]> </java>
<!-- 最小起订量约束 --> <java name="adjustToMoq" returnClass="int"> <![CDATA[ int def adjustToMoq(int:qty, int:moq) { int result = qty; if (qty > 0 && qty < moq) { result = moq; } return result; } ]]> </java>
<!-- 包装规格向上取整:while 循环示例 --> <java name="roundToPack" returnClass="int"> <![CDATA[ int def roundToPack(int:qty, int:packSize) { if (qty <= 0) { return 0; } if (packSize <= 1) { return qty; } int rounded = 0; while (rounded < qty) { rounded = rounded + packSize; } return rounded; } ]]> </java>
<!-- 可售天数 --> <java name="stockDays" returnClass="int"> <![CDATA[ int def stockDays(int:currentStock, int:avgDailySales) { if (avgDailySales <= 0) { return 999; } return currentStock / avgDailySales; } ]]> </java>
<!-- 紧急度 --> <java name="urgency" returnClass="java.lang.String"> <![CDATA[ str def urgency(int:stockDays, int:leadTimeDays) { if (stockDays <= leadTimeDays) { return "CRITICAL"; } else if (stockDays <= leadTimeDays + 3) { return "HIGH"; } else if (stockDays <= leadTimeDays + 7) { return "MEDIUM"; } else { return "LOW"; } } ]]> </java>
<!-- 是否加急补货 --> <java name="suggestExpress" returnClass="boolean"> <![CDATA[ boolean def suggestExpress(int:stockDays, int:leadTimeDays, str:seasonFactor) { if (stockDays <= leadTimeDays) { return true; } else if (seasonFactor == "PEAK" && stockDays <= leadTimeDays + 3) { return true; } else { return false; } } ]]> </java>
<!-- 单次下单量上限 --> <java name="cappedQty" returnClass="int"> <![CDATA[ int def cappedQty(int:qty, int:maxOrder) { int result = qty; if (result > maxOrder) { result = maxOrder; } return result; } ]]> </java>
</javas>ReplenishmentService.java
Section titled “ReplenishmentService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.InventoryRules;import com.github.paohaijiao.demo.model.ReplenishDecision;import com.github.paohaijiao.demo.model.ReplenishmentItem;
/** * 场景 7:零售库存补货建议服务 * <p> * 职责划分: * <ul> * <li><b>Java 侧</b>:链路串联(需求 → 安全库存 → 目标 → 缺口 → 约束 → 上限)、建议组装</li> * <li><b>规则侧</b>:销量测算口径、安全库存策略、季节系数、取整规则、紧急度阈值,全部外置在 XML 中</li> * </ul> */public class ReplenishmentService {
private final InventoryRules rules;
public ReplenishmentService(InventoryRules rules) { this.rules = rules; }
/** * 计算补货建议 * * @param item 商品与库存快照 * @return 补货决策 */ public ReplenishDecision replenish(ReplenishmentItem item) { ReplenishDecision decision = new ReplenishDecision(); decision.setSku(item.getSku());
int ddlt = rules.demandDuringLeadTime(item.getAvgDailySales(), item.getLeadTimeDays()); int safety = rules.safetyStockLevel(item.getSafetyStock(), item.getAvgDailySales(), item.getSupplierReliability(), item.isPromotion()); int target = rules.targetStock(safety, ddlt, item.getSeasonFactor(), item.getAvgDailySales()); int shortage = rules.shortage(target, item.getCurrentStock()); int roundedQty = rules.roundToPack(rules.adjustToMoq(shortage, item.getMoq()), item.getPackSize()); int orderQty = rules.cappedQty(roundedQty, item.getMaxOrder()); int stockDays = rules.stockDays(item.getCurrentStock(), item.getAvgDailySales()); String urgency = rules.urgency(stockDays, item.getLeadTimeDays());
decision.setDemandDuringLeadTime(ddlt); decision.setSafetyStockLevel(safety); decision.setTargetStock(target); decision.setShortage(shortage); decision.setOrderQty(orderQty); decision.setStockDays(stockDays); decision.setUrgency(urgency); decision.setSuggestExpress(rules.suggestExpress(stockDays, item.getLeadTimeDays(), item.getSeasonFactor()));
buildNotes(item, decision, roundedQty, orderQty); return decision; }
private void buildNotes(ReplenishmentItem item, ReplenishDecision decision, int roundedQty, int orderQty) { if (decision.getShortage() == 0) { decision.addNote("库存充足,本次无需补货"); return; } if (roundedQty > decision.getShortage()) { decision.addNote("按 MOQ/整箱包装取整后下单量高于库存缺口,属正常约束"); } if (orderQty < roundedQty) { decision.addNote("单次下单量受上限约束封顶为 " + orderQty + ",超出部分建议拆单"); } if (decision.getShortage() < item.getMoq()) { decision.addNote("缺口低于最小起订量,按 MOQ 下单"); } if (item.isPromotion()) { decision.addNote("促销期需求上浮,安全库存已自动抬升"); } if (item.getSupplierReliability() < 0.9) { decision.addNote("供应商履约率 " + item.getSupplierReliability() + " 偏低,安全库存已上调"); } if ("PEAK".equals(item.getSeasonFactor())) { decision.addNote("旺季备货,目标库存上调 7 天销量"); } if (decision.isSuggestExpress()) { decision.addNote("可售天数不足提前期,建议加急补货"); } }}ReplenishmentServiceTest.java
Section titled “ReplenishmentServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.ReplenishDecision;import com.github.paohaijiao.demo.model.ReplenishmentItem;import com.github.paohaijiao.demo.service.ReplenishmentService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 7 生产级样板:零售库存补货建议 * <p> * 覆盖 6 组补货形态:紧急缺货、库存充足、促销旺季、淡季整箱取整、 * 中等紧急、单次上限封顶。 */public class ReplenishmentServiceTest {
private ReplenishmentService service;
@Before public void setUp() { InventoryRules rules = JQuickJava.create() .createApi(InventoryRules.class, "demo/inventory.xml"); this.service = new ReplenishmentService(rules); }
/** * 紧急缺货:库存 20、日均 30、供应商履约率 0.8 → 目标 290,紧急 CRITICAL */ @Test public void shouldUrgentlyReplenishShortage() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7001"); item.setSkuName("矿泉水 550ml"); item.setWarehouse("华东仓"); item.setCurrentStock(20); item.setSafetyStock(50); item.setAvgDailySales(30); item.setLeadTimeDays(5); item.setMoq(50); item.setPackSize(10); item.setSupplierReliability(0.8); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(150, decision.getDemandDuringLeadTime()); Assert.assertEquals(140, decision.getSafetyStockLevel()); Assert.assertEquals(290, decision.getTargetStock()); Assert.assertEquals(270, decision.getShortage()); Assert.assertEquals(270, decision.getOrderQty()); Assert.assertEquals(0, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.isSuggestExpress()); }
/** * 库存充足:库存 500、目标 200 → 无需补货,紧急度 LOW */ @Test public void shouldSkipWhenStockSufficient() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7002"); item.setSkuName("卷纸"); item.setWarehouse("华北仓"); item.setCurrentStock(500); item.setSafetyStock(50); item.setAvgDailySales(30); item.setLeadTimeDays(5); item.setMoq(50); item.setPackSize(10); item.setSupplierReliability(0.95); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(150, decision.getDemandDuringLeadTime()); Assert.assertEquals(50, decision.getSafetyStockLevel()); Assert.assertEquals(200, decision.getTargetStock()); Assert.assertEquals(0, decision.getShortage()); Assert.assertEquals(0, decision.getOrderQty()); Assert.assertEquals(16, decision.getStockDays()); Assert.assertEquals("LOW", decision.getUrgency()); Assert.assertFalse(decision.isSuggestExpress()); Assert.assertTrue(decision.getNotes().contains("库存充足,本次无需补货")); }
/** * 促销旺季:低履约 + 促销 + 旺季,缺口 390 但 MOQ 500 → 按 MOQ 下单 */ @Test public void shouldBoostSafetyStockInPromotionPeak() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7003"); item.setSkuName("空调扇"); item.setWarehouse("华南仓"); item.setCurrentStock(100); item.setSafetyStock(50); item.setAvgDailySales(20); item.setLeadTimeDays(7); item.setMoq(500); item.setPackSize(100); item.setSupplierReliability(0.85); item.setPromotion(true); item.setSeasonFactor("PEAK"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(140, decision.getDemandDuringLeadTime()); Assert.assertEquals(210, decision.getSafetyStockLevel()); Assert.assertEquals(490, decision.getTargetStock()); Assert.assertEquals(390, decision.getShortage()); Assert.assertEquals(500, decision.getOrderQty()); Assert.assertEquals(5, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.getNotes().contains("促销期需求上浮,安全库存已自动抬升")); }
/** * 淡季整箱取整:目标 110、缺口 50、包装 20 → 取整为 60 */ @Test public void shouldRoundUpToPackSize() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7004"); item.setSkuName("暖宝宝"); item.setWarehouse("华北仓"); item.setCurrentStock(60); item.setSafetyStock(40); item.setAvgDailySales(10); item.setLeadTimeDays(10); item.setMoq(20); item.setPackSize(20); item.setSupplierReliability(0.98); item.setPromotion(false); item.setSeasonFactor("OFF"); item.setMaxOrder(5000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(100, decision.getDemandDuringLeadTime()); Assert.assertEquals(110, decision.getTargetStock()); Assert.assertEquals(50, decision.getShortage()); Assert.assertEquals(60, decision.getOrderQty()); Assert.assertEquals(6, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); }
/** * 中等紧急:可售 6 天、提前期 3 天 → HIGH(提前期 +3 天内) */ @Test public void shouldMarkHighUrgency() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7005"); item.setSkuName("咖啡豆"); item.setWarehouse("华东仓"); item.setCurrentStock(30); item.setSafetyStock(40); item.setAvgDailySales(5); item.setLeadTimeDays(3); item.setMoq(10); item.setPackSize(5); item.setSupplierReliability(0.95); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(1000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(15, decision.getDemandDuringLeadTime()); Assert.assertEquals(55, decision.getTargetStock()); Assert.assertEquals(25, decision.getShortage()); Assert.assertEquals(25, decision.getOrderQty()); Assert.assertEquals(6, decision.getStockDays()); Assert.assertEquals("HIGH", decision.getUrgency()); Assert.assertFalse(decision.isSuggestExpress()); }
/** * 单次上限封顶:缺口 2085、整箱取整 2100,但单次上限 1000 → 下单 1000 */ @Test public void shouldCapOrderQuantity() { ReplenishmentItem item = new ReplenishmentItem(); item.setSku("SKU-7006"); item.setSkuName("爆款零食"); item.setWarehouse("华中仓"); item.setCurrentStock(15); item.setSafetyStock(100); item.setAvgDailySales(400); item.setLeadTimeDays(5); item.setMoq(100); item.setPackSize(100); item.setSupplierReliability(0.9); item.setPromotion(false); item.setSeasonFactor("NORMAL"); item.setMaxOrder(1000);
ReplenishDecision decision = service.replenish(item);
Assert.assertEquals(2000, decision.getDemandDuringLeadTime()); Assert.assertEquals(100, decision.getSafetyStockLevel()); Assert.assertEquals(2100, decision.getTargetStock()); Assert.assertEquals(2085, decision.getShortage()); Assert.assertEquals(1000, decision.getOrderQty()); Assert.assertEquals(0, decision.getStockDays()); Assert.assertEquals("CRITICAL", decision.getUrgency()); Assert.assertTrue(decision.getNotes().contains("按 MOQ/整箱包装取整后下单量高于库存缺口,属正常约束")); }}在工程根目录执行:
mvn test -Dtest=ReplenishmentServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldUrgentlyReplenishShortage |
矿泉水:库存 20、日均 30、提前期 5 天、履约率 0.8 | 提前期需求 150、安全库存 140、目标 290、缺口 270、下单 270、可售 0 天、CRITICAL、加急 true |
shouldSkipWhenStockSufficient |
卷纸:库存 500、履约率 0.95 | 目标 200、缺口 0、下单 0、可售 16 天、LOW、不加急、含「库存充足,本次无需补货」备注 |
shouldBoostSafetyStockInPromotionPeak |
空调扇:促销 + 旺季 PEAK + 履约率 0.85、MOQ 500 | 需求 140、安全库存 210、目标 490、缺口 390、下单 500(按 MOQ 上调)、可售 5 天、CRITICAL |
shouldRoundUpToPackSize |
暖宝宝:淡季 OFF、缺口 50、箱规 20 | 需求 100、目标 110、缺口 50、整箱取整后下单 60、可售 6 天、CRITICAL |
shouldMarkHighUrgency |
咖啡豆:库存 30、日均 5、提前期 3 天 | 需求 15、目标 55、缺口 25、下单 25、可售 6 天、HIGH、不加急 |
shouldCapOrderQuantity |
爆款零食:日均 400、缺口 2085、整箱 2100、单次上限 1000 | 需求 2000、目标 2100、下单 1000(封顶)、可售 0 天、CRITICAL、含取整约束备注 |
USE-CASES.md 场景 7 收录的两组实测输出:
紧急缺货(当前库存 20、目标库存 290、缺口 270):
目标库存: 290 缺口: 270 紧急度: CRITICAL 建议加急: true促销旺季大单(目标 490、缺口 390,受 MOQ 约束):
目标库存: 490 缺口: 390 下单量: 500(按 MOQ 上调)- 安全库存修正天数(5/3)、旺淡季修正(+7/-3)、MOQ、箱规、单次上限、紧急度阈值全部外置在 XML 中,换季与供应商变更改脚本即可,无需重新发版。
roundToPack在规则脚本内用 while 循环累加实现整箱向上取整,并对数量不大于 0、箱规不大于 1 做了边界保护,展示了脚本的循环表达能力。- Service 以流水线方式串联「需求 → 安全库存 → 目标 → 缺口 → MOQ → 整箱 → 上限」,库存充足时备注短路,并在封顶时输出拆单提示,业务约束顺序一目了然。
stockDays用整数除法计算可售天数,对日均销量不大于 0 的冷僻商品返回 999 天,避免除零;缺口与目标库存在脚本侧统一兜底不为负。- 规则接口通过
@Param按名绑定多参数(如targetStock的 4 个入参),代理实例创建一次即可在批量补货任务中复用,底层为 ASM 调用链。 - Java 8 即可运行、无重型规则平台依赖,可直接嵌入 JDK 8 的 WMS/ERP 与信创环境,高频 SKU 决策无额外进程开销。