Merge pull request 'fix(wle): 转仓代发(转x00005)不再计入原仓发货量' (#85) from yanhui-wle-transfer-denominator-20261024 into main

This commit is contained in:
yanhui 2026-10-24 12:26:44 +08:00
commit a839d78d56
5 changed files with 36 additions and 47 deletions

View File

@ -251,20 +251,16 @@ function buildNotifyText(data = {}, options = {}) {
const excludedCount = (Number(exclusions.carrierPackages) || 0)
+ (Number(exclusions.voidedPackages) || 0)
+ (Number(exclusions.nonRealPackages) || 0);
+ (Number(exclusions.nonRealPackages) || 0)
+ (Number(exclusions.transferPackages) || 0);
if (excludedCount > 0) {
head.push(`已剔除 ${excludedCount} 个包裹(承运商 / 已终止 / 刷单·信誉单,不计考核)`);
head.push(`已剔除 ${excludedCount} 个包裹(承运商 / 已终止 / 刷单·信誉单 / 转仓代发,不计考核)`);
}
// 现场拦截(售后已签收):货被快递当场拉回、售后仓已签收,不算异常;仍算发货分母
const interceptCount = Number(exclusions.interceptPackages) || 0;
if (interceptCount > 0) {
head.push(`另有 ${interceptCount} 个包裹属现场拦截(售后已签收),不计异常`);
}
// 转仓代发(转x00005):记账仓下单、目标仓实际发出,本仓永远停在「未揽收」,不算异常
const transferCount = Number(exclusions.transferPackages) || 0;
if (transferCount > 0) {
head.push(`另有 ${transferCount} 个包裹属转仓代发(转x00005),不计异常`);
}
const centerLines = totalPackages > 0 ? buildCenterLines(data) : [];
// 明细按仓聚集 + 每仓保底 1 条(避免大仓吃满额度),组间按异常数降序、组内保持时长降序

View File

@ -158,7 +158,7 @@ const EXCLUDED_ORDER_TYPE_LABELS = Object.freeze([
{ keyword: '转空包', label: '信誉单' },
]);
// ── 转仓代发(业务 2026-10-23 指定:不用核对异常)──────────────────────────
// ── 转仓代发(业务 2026-10-23 指定不核对异常;2026-10-24 补充不计原仓发货量)──
// 聚水潭「转仓」操作会在 `order_labels` 留一条 `转x00005` 形式的标签
// (x + 目标仓编码)。这类单**记账仓是原仓**(如崔桥工厂仓),货却由目标仓
// (x00005 = 曹县)实际发出 —— 承运账号也跟着变成曹县韵达小件。
@ -169,8 +169,9 @@ const EXCLUDED_ORDER_TYPE_LABELS = Object.freeze([
// 快递分布:曹县韵达小件 133、邮政快递包裹POSTB 124、睢宁中通 41 ……
// 目前只出现过 `转x00005` 一种编码,但标签是机器生成的(后续可能出现 x00006),
// 所以用正则匹配「转x<3位以上数字>」,不写死单个编码。
// ⚠️ 与「转空包」区分:转空包是发空包(信誉单,假发货);转仓代发是**真发货**,
// 只是不从记账仓发 —— 所以保留发货分母,只从异常清单摘掉(同「现场拦截」)。
// ⚠️ 与「转空包」区分:转空包是发空包(信誉单,假发货);转仓代发是**真发货**。
// 🔥 口径:**整单不计** —— 货不是从原仓走的,既不算原仓异常,也不进原仓发货分母
// (记进分母会把分母做大、异常率被稀释)。这与「现场拦截」相反,后者货是本仓发的、留分母。
const TRANSFER_WAREHOUSE_LABEL_PATTERN = /转\s*[xX]\s*\d{3,}/;
// ── 现场拦截(货已被售后仓签收)不算转运异常(业务 2026-10-23 指定)────────

View File

@ -408,6 +408,18 @@ async function getLogisticsExceptions(options = {}) {
continue;
}
// ④ 转仓代发(`转x00005` 这类标签):记账仓是原仓,货由目标仓实际发出。
// 业务 2026-10-24 定版:**原仓的发货量也不该算** —— 货不是从这个仓走的,
// 记在原仓只会把发货分母做大、异常率稀释。所以按「整单不计」处理
// (既不算异常、也不进发货分母),与承运商/刷单那几类同级,不是「只摘异常」。
const transferReason = registry.matchTransferWarehouse(row.order_labels);
if (transferReason) {
skippedByTransfer.packages += 1;
skippedByTransfer.orders.add(oId);
skippedByTransfer.reasons[transferReason] = (skippedByTransfer.reasons[transferReason] || 0) + 1;
continue;
}
const stat = centerStat.get(center.key);
stat.shippedOrders.add(oId);
stat.packages += 1;
@ -419,21 +431,10 @@ async function getLogisticsExceptions(options = {}) {
const typeMeta = registry.TYPE_MAP.get(type) || { label: type, tone: 'default' };
// ④ 转仓代发(`转x00005` 这类标签):记账仓是原仓,货由目标仓发出,
// 原仓物流状态必然停在「未揽收 / 无物流单」→ 全是假异常(业务 2026-10-23 指定不核对)。
// ⚠️ 与 a/b/c 三类不同:货**确实发了**,所以保留发货分母,只从异常清单摘掉。
const transferReason = registry.matchTransferWarehouse(row.order_labels);
if (transferReason) {
skippedByTransfer.packages += 1;
skippedByTransfer.orders.add(oId);
skippedByTransfer.reasons[transferReason] = (skippedByTransfer.reasons[transferReason] || 0) + 1;
continue;
}
// ⑤ 现场拦截:售后单有签收单号 + 实退数量 > 0 —— 货已被售后仓签收,
// 物流环节其实结束了(派送时被拦下、原车拉回),不算「卡在网点发不出去」。
// ⚠️ 同④:**保留发货分母**(货确实发出去了),只从异常清单里摘掉,
// 异常率才不会被错误地放大。
// ⚠️ 与上面几类不同:这里**保留发货分母**(货确实从这个仓发出去了),
// 只从异常清单里摘掉,异常率才不会被错误地放大。
if (interceptedOrders.has(oId)) {
skippedByIntercept.packages += 1;
skippedByIntercept.orders.add(oId);
@ -546,7 +547,7 @@ async function getLogisticsExceptions(options = {}) {
interceptPackages: skippedByIntercept.packages,
interceptOrders: skippedByIntercept.orders.size,
interceptReasons: skippedByIntercept.reasons,
// 转仓代发(转x00005)—— 同上,只摘异常、保留发货分母
// 转仓代发(转x00005)—— 整单不计:既不算异常,也不进原仓发货分母
transferPackages: skippedByTransfer.packages,
transferOrders: skippedByTransfer.orders.size,
transferReasons: skippedByTransfer.reasons,

View File

@ -555,9 +555,9 @@ test('转仓标签识别:转x00005(含大小写/空格)都认,转空包
assert.equal(registry.matchTransferWarehouse(null), null);
});
test('转仓代发不算异常,但仍计入发货分母', async () => {
test('转仓代发整单不计:不算异常,也不进原仓发货分母', async () => {
const mixed = [
// 30:转x00005 · 未揽收 → 崔桥下单、曹县代发,本仓永远「未揽收」,必须不报异常
// 30:转x00005 · 未揽收 → 崔桥下单、曹县代发,货不是从原仓走的,整单不计
mk({ inner_order_id: 30, so_id: 'SO-30', logistic_status_name: '未揽收', logistic_time: null,
logistic_company_name: '曹县韵达小件', order_labels: '["京喜","转x00005"]' }),
// 31:同样「未揽收」但没转仓标签 → 必须留下
@ -571,7 +571,13 @@ test('转仓代发不算异常,但仍计入发货分母', async () => {
assert.equal(data.exclusions.transferPackages, 1);
assert.equal(data.exclusions.transferReasons['转x00005'], 1);
assert.equal(data.summary.exceptionPackages, 1, '转仓单不计异常');
assert.equal(data.summary.packageCount, 2, '转仓单仍计入发货分母(货确实发了)');
// 🔥 业务 2026-10-24:货不从原仓走,原仓发货量也不该算
assert.equal(data.summary.packageCount, 1, '转仓单不进原仓发货分母');
assert.equal(data.summary.shippedOrders, 1, '转仓单不算原仓发货单');
// 仓维度:崔桥仓发货单数也只剩 31 那单(30 是转仓、不计)
const center = data.centers.find((c) => c.key === 'CQ');
assert.equal(center.shippedOrders, 1, '崔桥仓发货单数不应包含转仓单');
assert.equal(center.exceptionOrders, 1, '崔桥仓异常只算 31');
}, mixed);
});

View File

@ -780,14 +780,15 @@ function WarehouseLogisticsException() {
: null}
;<b>{exclusions?.orderTypeLabels?.length ? exclusions.orderTypeLabels.join(' / ') : '刷单 / 信誉单'}</b>
(按订单标签「刷单」「转空包」识别)—— 都不是真实发货,不用跟物流;
<b>现场拦截</b>(售后单有签收单号且实退数量 &gt; 0,货已被售后仓签收)—— 物流环节已结束,不算异常。
<b>现场拦截</b>(售后单有签收单号且实退数量 &gt; 0,货已被售后仓签收)—— 物流环节已结束,不算异常;
<b>转仓代发</b>(标签如「转x00005」,货由目标仓发出)—— 不算本仓异常,也不算本仓发货量。
{exclusions && (exclusions.carrierOrders > 0 || exclusions.voidedOrders > 0
|| exclusions.nonRealOrders > 0) ? (
|| exclusions.nonRealOrders > 0 || exclusions.transferOrders > 0) ? (
<>
{' '}本批已剔除 <b>{(exclusions.carrierOrders || 0) + (exclusions.voidedOrders || 0)
+ (exclusions.nonRealOrders || 0)}</b> 单
+ (exclusions.nonRealOrders || 0) + (exclusions.transferOrders || 0)}</b> 单
(承运商 {exclusions.carrierOrders || 0} 单、已终止 {exclusions.voidedOrders || 0} 单、
刷单/信誉单 {exclusions.nonRealOrders || 0} 单),
刷单/信誉单 {exclusions.nonRealOrders || 0} 单、转仓代发 {exclusions.transferOrders || 0} 单),
{exclusions.carrierReasons && Object.keys(exclusions.carrierReasons).length ? (
<>
其中承运商剔除:<b>
@ -815,22 +816,6 @@ function WarehouseLogisticsException() {
—— 货是被快递当场拦下原车拉回、由售后仓签收的,<b>仍计入发货分母、只是不再报异常</b>。
</>
) : null}
{exclusions?.transferOrders > 0 ? (
<>
{' '}另有 <b>{exclusions.transferOrders}</b> 单属<b>转仓代发</b>
{exclusions.transferReasons && Object.keys(exclusions.transferReasons).length ? (
<>(
{Object.entries(exclusions.transferReasons)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `标签${k} ${v} 单`)
.join(' / ')}
)
</>
) : null}
—— 记账仓下单、实际由目标仓发出,本仓物流状态必然停在「未揽收」,
<b>仍计入发货分母、只是不再报异常</b>。
</>
) : null}
<br />
页面按<b>当前物流快照</b>计算:核验时刻之后才更新的轨迹(比如 9 点后才揽收、才签收)会让对应异常自动消失,
所以这里的数字是「到此刻为止还异常的」—— 想留档 09:00 那一刻的结论,需要每天 9 点落一份快照。