From 0e0fcbb3782a553f2d5655b6bc798ca264c39d6d Mon Sep 17 00:00:00 2001 From: yanhui <2353496031@qq.com> Date: Sat, 19 Sep 2026 17:53:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20MKH-BOM=20=E6=97=A5=E9=94=80=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E5=A2=9E=E5=8A=A0=E5=87=BA=E5=BA=93=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=8C=E6=A0=87=E4=BA=86=E3=80=8C=E5=B7=B2=E5=8F=91=E8=B4=A7?= =?UTF-8?q?=E3=80=8D=E4=BD=86=E6=97=A0=E5=AE=9E=E9=99=85=E5=87=BA=E5=BA=93?= =?UTF-8?q?=E7=9A=84=E5=8D=95=E4=B8=8D=E8=AE=A1=E9=94=80=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务口径:标了「已发货」但没有实际出库的 SKU,不需要出现缺货预警。 即在 ⑦-a 的金额/标签判据之上,再以出库表为准判定「有没有真出货」。 已发货分支计入销量需同时满足(待发货分支不变): 1. 非样品/测试店铺(⑦-a) 2. 非异常单:pay_amount <= 0.01 或 labels 含「特殊单」(⑦-a) 3. 出库表能查到该 o_id:rpt.jst_salesOutboundInfo / rpt.jst_qmcust_salesoutbound 两个必须豁免的情形(否则会大面积误杀真实单): · 拆分单(status='Split'):出库记录挂在子单上,父单必然查不到。 实测窗口内 40 张 Split 单 100% 无出库记录;不豁免会误杀 「京东-HX旗舰店104 09-19 实付 1194.14 元」这类真实单 · 今天发货的单:当天出库尚未同步完(09-19 全天出库表仅 1 行,09-18 有 14,690 行)。 豁免只到「昨天」为止——09-18 的 18 张无出库 Sent 单 100% 是特殊单, 且出库表 09-18 数据完整,豁免 09-18 会放过刷单 豁免只免「出库校验」,金额/标签判据照样生效。 真实数据验收(近 15 天窗口,1,086 张已发货 MKH 单): · 出库表命中 889 张 → 197 张无出库 = 40 张拆分单 + 157 张 Sent · 157 张里 131 张是 ≤0.01 元或「特殊单」(即 ⑦-a 已排除的刷单), 唯一 1 张正常金额的是 09-05 的 388 元单(标签「发货后取消/发货后售后/风控订单」) · 兜底生效 SKU = 12 个(与只按金额/标签判据一致),兜底日销最大值 = 0.2 · 10 个异常单 SKU 在预警列表中零出现;总预警 27 条(缺货 21 + 仅预警 6) 性能:1086 个订单号分 2 块并行 UNION 查询,冷缓存约 0.4~1.4s; 整窗口拉全量 DISTINCT o_id 实测要 8s,故不采用。 降级策略:出库数据取不到时整体跳过出库校验(退化成只按金额/标签判据)并标 degraded。 漏杀只是多报一条预警,错杀会让真实缺货彻底隐身。 注意:jst_salesOutboundInfo.qty 全表 1,119,606 行恒为 0,只能判「有没有出库行」, 不能用 SUM(qty)(永远得 0)。 验证:check-syntax 151 文件通过;node --test 338/339(唯一失败是既有的 resilientSecretStore Windows 权限断言,与本改动无关);mkhBomService.test.js 45/45。 --- backend/src/services/mkhBomService.js | 213 +++++++++++++++++++++++--- backend/test/mkhBomService.test.js | 97 +++++++++++- 2 files changed, 284 insertions(+), 26 deletions(-) diff --git a/backend/src/services/mkhBomService.js b/backend/src/services/mkhBomService.js index bb740f2..77b16e9 100644 --- a/backend/src/services/mkhBomService.js +++ b/backend/src/services/mkhBomService.js @@ -149,6 +149,33 @@ // ⚠️ 这是**补漏**不是新口径:日销来源仍是「近 15 天发货量 ÷ 15」, // 只是把上游漏掉的发货自己算一遍。上游修好后兜底会自动失效(快照 > 0)。 // +// ── ⑦-c 出库校验:**必须真出库才算销量**(2026-09-19 业务确认口径)──── +// 业务原话:「这些异常 SKU,系统标记了『已发货』,但没有实际出库的,不需要出现缺货预警」。 +// 即 ⑦-a 的金额/标签判据再往根上退一步——**以出库表为准**: +// 已发货分支要计入销量,必须同时满足 +// ① 不是异常单(`pay_amount ≤ 0.01` 或 `labels` 含「特殊单」,⑦-a) +// ② 在出库表里查得到该 `o_id`(`jst_salesOutboundInfo` / `jst_qmcust_salesoutbound`) +// 实测(2026-09-19,窗口 15 天、1,086 张已发货 MKH 单): +// · 出库表命中 889 张 → 197 张无出库,其中 **40 张是拆分单、157 张是 Sent** +// · 157 张里 **131 张是 ≤0.01 元或「特殊单」**(即 ⑦-a 已排除的那批刷单), +// 剩下唯一 1 张正常金额的是 09-05 的 388 元单(标签「发货后取消 / 发货后售后 / +// 风控订单」)—— ② 这条判据正是为它这类「标了发货其实没出库」的单兜底 +// · 与只按金额/标签判据相比,**当前窗口的兜底生效 SKU 集合完全一致(12 个)**, +// 属"同解但更根本",代价是每次冷缓存多约 0.5s 的出库查询 +// 🔥 **两个必须豁免的情形**(不豁免就会大面积误杀真实单,实测数据如下): +// ① **拆分单**(`status='Split'`):父单被拆成子单后出库挂在**子单**上, +// 父单必然查不到出库 —— 窗口内 40 张 Split 单 **100% 无出库记录**, +// 若不豁免会误杀「京东-HX旗舰店104 09-19 实付 1194.14 元」这类真实单 +// ② **今天发货的单**:当天出库数据尚未同步完(实测 09-19 全天出库表只有 1 行, +// 而 09-18 有 14,690 行)→ 09-19 的 7 张 Sent 无出库单里特殊单为 0, +// 说明无出库是**同步滞后**而非虚假发货。 +// 注意豁免只到「昨天」为止:09-18 的 18 张无出库 Sent 单 **100% 是特殊单**, +// 出库表 09-18 数据完整,所以 09-18 不能被豁免(否则会放过刷单) +// ⚠️ 只判**有没有出库行**:`jst_salesOutboundInfo.qty` 全表 1,119,606 行恒为 0 +// (`SUM(qty)` 永远得 0,近 15 天 180,433 行同样),别拿数量做判据。 +// ⚠️ 出库数据取不到时**整体跳过出库校验**(退化成只按金额/标签判据)并标 degraded: +// 漏杀只是多报一条预警,错杀会让真实缺货彻底隐身。 +// // ── ⑧ 在途加工单(2026-09-17 业务确认口径)──────────────────────── // 规则:**只要该 MKH 商品存在未完成的加工单,就不再列入补货预警** // (工厂已经在做了,不重复提示补货),改标为「已排产」。 @@ -372,6 +399,21 @@ const SALE_EXCLUDED_SHOP_KEYWORDS = Object.freeze(['样品', '测试']); const SALE_EXCLUDED_ORDER_PAY_AMOUNT_MAX = 0.01; const SALE_EXCLUDED_ORDER_LABEL_KEYWORDS = Object.freeze(['特殊单']); +// ── 出库校验参数(见文件头 ⑦-c)────────────────────────────────── +// 出库表:判定「标了已发货的单到底有没有真出货」。两张表按渠道分开,**必须都查**—— +// 2026-09-19 实测近 15 天窗口内 1,086 张 MKH 已发货单:京东表命中 889、天猫表命中 198, +// jst_qmcust_orderlist 来源的 208 张单里 7 张只有京东表有记录、1 张只有天猫表有记录。 +const OUTBOUND_TABLES = Object.freeze([ + 'rpt.jst_salesOutboundInfo', // 京东 / 拼多多 / 抖音 等 + 'rpt.jst_qmcust_salesoutbound', // 天猫 / 淘宝 / 1688 等 +]); +// 单个查询里塞多少个 o_id:1000 个参数远低于 MSSQL 单语句 2100 上限, +// 实测 1086 个订单号分 2 块并行查询合计 ≈ 515ms(整窗口拉全量 DISTINCT 要 8s,别那么干) +const OUTBOUND_LOOKUP_CHUNK_SIZE = 1000; +// 「拆分单」状态:父单被拆成子单后,出库记录挂在**子单**上,父单必然查不到出库。 +// 2026-09-19 实测窗口内 40 张 Split 单 **100% 无出库记录** → 不能拿「无出库」判它造假。 +const SPLIT_ORDER_STATUSES = Object.freeze(['split']); + // ── 配件不足 / 可组装数参数(见文件头 ⑪)──────────────────────── // 配件库存仓:MKH BOM 配件归属仓「简尚-崔桥工厂半成品包装项目仓」的 wms_co_id const COMPONENT_STOCK_WMS_CO_ID = 15718196; @@ -1317,15 +1359,92 @@ const isExcludedSaleOrder = (row = {}) => { return paid !== null && paid <= SALE_EXCLUDED_ORDER_PAY_AMOUNT_MAX; }; +/** 是否为「拆分单」——父单的出库记录挂在子单上,父单查不到出库属正常(见 ⑦-c) */ +const isSplitOrder = (status) => { + const text = toText(status); + return Boolean(text) && SPLIT_ORDER_STATUSES.includes(text.toLowerCase()); +}; + +/** 发货日是否为**今天** —— 当天出库尚未同步完成,不能用出库判据(见 ⑦-c) */ +const isShippedToday = (sendDate) => { + const date = sendDate instanceof Date ? sendDate : new Date(sendDate); + if (Number.isNaN(date.getTime())) return false; + const today = new Date(); + return date.getFullYear() === today.getFullYear() + && date.getMonth() === today.getMonth() + && date.getDate() === today.getDate(); +}; + +/** + * 该已发货单能否用「出库记录」来判定真伪(见 ⑦-c)。 + * 不能判定的两种情形,退回只按 ⑦-a 的金额/标签判据,**不做无出库排除**,避免误杀: + * · 拆分单:出库挂在子单上,父单必然查不到 + * · 今天发货:当天出库数据尚未同步完(实测 09-19 全天出库表只有 1 行) + * 取不到订单号时同样豁免——宁可漏杀,不可错杀。 + */ +const isOutboundCheckSkipped = (row = {}) => ( + isSplitOrder(row.status) + || isShippedToday(row.sendDate) + || !toText(row.oId) +); + +/** + * 批量查「这些订单号在出库表里有没有实际出库记录」(见 ⑦-c)。 + * 只取 o_id 一列、分块并行查,失败时返回空集合 + 错误信息让调用方降级。 + * ⚠️ 只判**有没有行**:`qty` 列全表 1,119,606 行恒为 0,`SUM(qty)` 永远得 0,别用它。 + */ +async function loadOutboundOrderIds(orderIds) { + const ids = [...new Set((orderIds || []).map((id) => toText(id)).filter(Boolean))]; + if (!ids.length) return { ids: new Set(), error: null }; + + const chunks = []; + for (let index = 0; index < ids.length; index += OUTBOUND_LOOKUP_CHUNK_SIZE) { + chunks.push(ids.slice(index, index + OUTBOUND_LOOKUP_CHUNK_SIZE)); + } + + const settled = await Promise.allSettled(chunks.map((chunk) => { + const params = {}; + const placeholders = chunk + .map((value, index) => { params[`outboundOid${index}`] = value; return `@outboundOid${index}`; }) + .join(', '); + const union = OUTBOUND_TABLES + .map((table) => `SELECT o_id AS oId FROM ${table} WITH (NOLOCK) WHERE o_id IN (${placeholders})`) + .join('\n UNION\n '); + return cloudDatabase.query( + `SELECT DISTINCT outbound_rows.oId AS oId + FROM ( + ${union} + ) outbound_rows`, + params, + { requestTimeout: 180000 } + ); + })); + + const hits = new Set(); + const failed = []; + settled.forEach((result, index) => { + if (result.status !== 'fulfilled') { + failed.push(`出库表分块#${index + 1}: ${result.reason?.message || result.reason}`); + return; + } + result.value.forEach((row) => { + const text = toText(row.oId); + if (text) hits.add(text); + }); + }); + return { ids: hits, error: failed.length ? failed.join(' | ') : null }; +} + /** * 订单索引 —— 进程内缓存,**一次扫表同时得到两份数据**(合并的理由见文件头 ⑦-a): * pendingBySku : Map<归一化编码, { pendingOrderQty, pendingOrderCount, pendingOrderEarliest }> (⑩) * shippedBySku : Map<归一化编码, 近 SALE_WINDOW_DAYS 天已发货件数>(⑦-a,已排除样品/测试店) * 口径: * · 待发货 = `send_date IS NULL` 且状态非取消/合并(⑩) - * · 已发货 = `send_date` 落在近 SALE_WINDOW_DAYS 天窗口内(⑦-a,已排除样品/测试店 + 0.01 元异常单) + * · 已发货 = `send_date` 落在近 SALE_WINDOW_DAYS 天窗口内(⑦-a),且 + * ①非样品/测试店铺、②非异常单(金额/标签)、③**有实际出库记录**(⑦-c) * · 两者商品都取自主表 `skus` 文本列,`MKH-` 打头的组合装不计 - * · 异常单过滤只作用于**已发货**分支:0.01 元特殊单「标了发货但没真出库」,不算销售 + * · 三条过滤只作用于**已发货**分支:待发货(⑩)本来就是没出货的订单,不做出库校验 * ⚠️ 不降级为「抛错」:取不到时按空索引处理(补货预警退化成旧口径),并缩短缓存以便重试。 */ async function loadOrderIndex() { @@ -1352,6 +1471,7 @@ async function loadOrderIndex() { o.shop_name AS shopName, o.pay_amount AS payAmount, o.labels AS labels, + o.status AS status, o.skus AS skus FROM ${table} o WITH (NOLOCK) WHERE o.skus LIKE @pendingOrderSkuLike @@ -1365,11 +1485,14 @@ async function loadOrderIndex() { )`, { pendingOrderSkuLike: PENDING_ORDER_SKU_LIKE, ...statusParams }, { requestTimeout: 180000 } - ))).then((results) => { + ))).then(async (results) => { const pendingBySku = new Map(); const shippedBySku = new Map(); const seenOrderIds = new Set(); const failed = []; + const shippedRows = []; + const pendingRows = []; + const outboundCheckIds = []; results.forEach((result, index) => { if (result.status !== 'fulfilled') { @@ -1390,27 +1513,61 @@ async function loadOrderIndex() { // 已发货(⑦-a):0.01 元特殊单「标了发货但没真出库」,同样不计入日销 if (shipped && isExcludedSaleOrder(row)) return; - const orderDate = row.orderDate instanceof Date ? row.orderDate : null; - parseOrderSkus(row.skus).forEach(({ code, qty }) => { - if (!isMkhProductCode(code)) return; - const key = normalizeCode(code); - if (!key) return; - if (shipped) { - shippedBySku.set(key, (shippedBySku.get(key) || 0) + qty); - return; - } - const entry = pendingBySku.get(key) || { - pendingOrderQty: 0, - pendingOrderCount: 0, - pendingOrderEarliest: null, - }; - entry.pendingOrderQty += qty; - entry.pendingOrderCount += 1; - if (orderDate && (!entry.pendingOrderEarliest || orderDate < entry.pendingOrderEarliest)) { - entry.pendingOrderEarliest = orderDate; - } - pendingBySku.set(key, entry); - }); + if (shipped) { + shippedRows.push(row); + // ⑦-c:可用于出库校验的单先记下订单号,第二遍统一过滤 + if (!isOutboundCheckSkipped(row)) outboundCheckIds.push(orderId); + return; + } + pendingRows.push(row); + }); + }); + + // ⑦-c:已发货单再过一道「有没有真出库」。 + // 出库数据取不到(或只取到一部分)时**整体不做出库排除**,退化成只按金额/标签判据—— + // 宁可漏杀不可错杀:漏杀只是多一条预警,错杀会让真实缺货彻底隐身。 + let outboundReady = false; + let outboundIds = new Set(); + if (outboundCheckIds.length) { + const outbound = await loadOutboundOrderIds(outboundCheckIds); + if (outbound.error) { + console.warn('[MKH-BOM] 出库数据源读取失败,本次跳过出库校验:', outbound.error); + failed.push(outbound.error); + } else { + outboundReady = true; + outboundIds = outbound.ids; + } + } + + shippedRows.forEach((row) => { + const orderId = row.oId === null || row.oId === undefined ? null : String(row.oId); + // ⑦-c:能判定、却没查到出库 → 系统标了「已发货」但货没真出,不计入销量 + if (outboundReady && !isOutboundCheckSkipped(row) && !outboundIds.has(orderId)) return; + parseOrderSkus(row.skus).forEach(({ code, qty }) => { + if (!isMkhProductCode(code)) return; + const key = normalizeCode(code); + if (!key) return; + shippedBySku.set(key, (shippedBySku.get(key) || 0) + qty); + }); + }); + + pendingRows.forEach((row) => { + const orderDate = row.orderDate instanceof Date ? row.orderDate : null; + parseOrderSkus(row.skus).forEach(({ code, qty }) => { + if (!isMkhProductCode(code)) return; + const key = normalizeCode(code); + if (!key) return; + const entry = pendingBySku.get(key) || { + pendingOrderQty: 0, + pendingOrderCount: 0, + pendingOrderEarliest: null, + }; + entry.pendingOrderQty += qty; + entry.pendingOrderCount += 1; + if (orderDate && (!entry.pendingOrderEarliest || orderDate < entry.pendingOrderEarliest)) { + entry.pendingOrderEarliest = orderDate; + } + pendingBySku.set(key, entry); }); }); @@ -2226,6 +2383,14 @@ module.exports = { SALE_EXCLUDED_SHOP_KEYWORDS, SALE_EXCLUDED_ORDER_PAY_AMOUNT_MAX, SALE_EXCLUDED_ORDER_LABEL_KEYWORDS, + // ⑦-c 出库校验 + isSplitOrder, + isShippedToday, + isOutboundCheckSkipped, + loadOutboundOrderIds, + OUTBOUND_TABLES, + OUTBOUND_LOOKUP_CHUNK_SIZE, + SPLIT_ORDER_STATUSES, PENDING_ORDER_EXCLUDED_STATUS, ORDER_TABLES, // ⑪ 配件不足 / 可组装数 diff --git a/backend/test/mkhBomService.test.js b/backend/test/mkhBomService.test.js index 52ccce1..051922d 100644 --- a/backend/test/mkhBomService.test.js +++ b/backend/test/mkhBomService.test.js @@ -425,7 +425,14 @@ test('order index: reads both channel tables, merges and dedupes by o_id', async const originalQuery = cloudDatabase.query; const queried = []; + // 出库表命中集合(⑦-c):9005 / 9007 / 9011 有真实出库记录; + // 9012(金额正常但无出库)、9009 / 9010 / 9015(异常单)刻意不在此列 + const outboundHits = ['9005', '9007', '9011']; cloudDatabase.query = async (sql) => { + if (sql.includes('jst_salesOutboundInfo') || sql.includes('jst_qmcust_salesoutbound')) { + queried.push('outbound'); + return outboundHits.map((oId) => ({ oId })); + } if (sql.includes('jst_qmcust_orderlist')) { queried.push('qmcust'); return [ @@ -503,6 +510,46 @@ test('order index: reads both channel tables, merges and dedupes by o_id', async labels: '店铺-拼多多,模块化成品', skus: '1.MKHDNZ0410260700023', }, + // ⑦-c:金额正常、但出库表查不到 → 系统标了「已发货」货没真出 → 排除 + // (业务原话:标了已发货但没有实际出库的不需要出现缺货预警) + { + oId: 9012, + sendDate: new Date('2026-09-10T00:00:00Z'), + shopName: '拼多多-HX旗舰店105', + payAmount: 388, + labels: '发货后取消,发货后售后,风控订单', + skus: '1.MKHDNZ0409260601888', + }, + // ⑦-c 豁免①:拆分单的出库挂在子单上,父单查不到属正常 → 必须保留 + // (实测窗口内 40 张 Split 单 100% 无出库记录) + { + oId: 9013, + status: 'Split', + sendDate: new Date('2026-09-15T00:00:00Z'), + shopName: '京东-HX旗舰店104', + payAmount: 1194.14, + labels: '以旧换新,模块化成品', + skus: '1.MKHDNZ0409260601777', + }, + // ⑦-c 豁免②:今天发货的单,当天出库尚未同步完 → 必须保留 + { + oId: 9014, + sendDate: new Date(), + shopName: '京东-HX旗舰店104', + payAmount: 500, + labels: '模块化成品', + skus: '1.MKHDNZ0409260601666', + }, + // ⑦-c:拆分单/今天单虽然豁免**出库**校验,但金额/标签判据照样生效 + { + oId: 9015, + status: 'Split', + sendDate: new Date('2026-09-15T00:00:00Z'), + shopName: '拼多多-亿家达书房家具官方旗舰店', + payAmount: 0.01, + labels: '特殊单,模块化成品', + skus: '1.MKHDNZ0410260700777', + }, ]; }; @@ -510,7 +557,7 @@ test('order index: reads both channel tables, merges and dedupes by o_id', async svc.clearMkhBomCache(); const index = await _private.loadOrderIndex(); - assert.deepEqual(queried.sort(), ['orders', 'qmcust'], '两张订单表都必须读到'); + assert.deepEqual(queried.sort(), ['orders', 'outbound', 'qmcust'], '两张订单表 + 出库表都必须读到'); assert.equal(index.degraded, false); const a = index.pendingBySku.get('MKHDNZ0409260700059'); @@ -532,7 +579,53 @@ test('order index: reads both channel tables, merges and dedupes by o_id', async assert.equal(index.shippedBySku.has('MKHDNZ0410260700099'), false, '标签含「特殊单」的不得计入日销'); // 有免额的正常单必须照常计入 assert.equal(index.shippedBySku.get('MKHDNZ0410260700023'), 1, '有免额的正常单不得被误排除'); - assert.equal(index.shippedBySku.size, 2, '被排除的店铺/异常单不进入索引'); + // ⑦-c 出库校验:标了已发货但出库表查不到 → 不计入日销 + assert.equal(index.shippedBySku.has('MKHDNZ0409260601888'), false, '无实际出库记录的不得计入日销'); + // ⑦-c 豁免:拆分单 / 今天发货的单不能用出库判定,必须保留 + assert.equal(index.shippedBySku.get('MKHDNZ0409260601777'), 1, '拆分单无出库记录属正常,不得误杀'); + assert.equal(index.shippedBySku.get('MKHDNZ0409260601666'), 1, '今天发货的出库尚未同步,不得误杀'); + // ⑦-c 豁免只免「出库校验」,金额/标签判据照样生效 + assert.equal(index.shippedBySku.has('MKHDNZ0410260700777'), false, '拆分单的 0.01 元异常单仍要排除'); + assert.equal(index.shippedBySku.size, 4, '被排除的店铺/异常单/无出库单不进入索引'); + } finally { + cloudDatabase.query = originalQuery; + svc.clearMkhBomCache(); + } +}); + +test('order index: outbound lookup failure skips the check instead of dropping sales', async () => { + const cloudDatabase = require('../src/config/cloudDatabase'); + const svc = require('../src/services/mkhBomService'); + const originalQuery = cloudDatabase.query; + + // 出库表整体不可用(例如视图未同步)——此时宁可漏杀不可错杀: + // 不做出库排除(否则真实销售会被整批砍掉),但要标 degraded 触发短 TTL 重试。 + cloudDatabase.query = async (sql) => { + if (sql.includes('jst_salesOutboundInfo') || sql.includes('jst_qmcust_salesoutbound')) { + throw new Error('出库视图不可用'); + } + if (sql.includes('jst_qmcust_orderlist')) return []; + return [ + { + oId: 9101, + sendDate: new Date('2026-09-10T00:00:00Z'), + shopName: '京东-HX旗舰店104', + payAmount: 388, + labels: '模块化成品', + skus: '1.MKHDNZ0409260601777', + }, + ]; + }; + + try { + svc.clearMkhBomCache(); + const index = await _private.loadOrderIndex(); + assert.equal(index.degraded, true, '出库源失败要标 degraded,让接口透出并尽快重试'); + assert.equal( + index.shippedBySku.get('MKHDNZ0409260601777'), + 1, + '出库数据取不到时不得据此砍掉销量(退化成只按金额/标签判据)' + ); } finally { cloudDatabase.query = originalQuery; svc.clearMkhBomCache();