'use strict'; /** * 智能问数智能体 · 层间一致性核对工具 * ---------------------------------------------------------------------------- * cross_check:按日核对 门店合计 vs 连锁合计 vs 公司行(营业额 + 有效订单)。 * 从 data_quality 中独立成工具,供模型对指定区间单独抽查(如环比异常时先排除 * 数据质量问题)。容差与判定同 data_quality:相对差 >0.5% 记不一致、>20% 判严重。 */ const { eachDay, ymdDash } = require('./lake.js'); const CROSS_TOL = 0.005; const CROSS_FAIL = 0.2; function crossSanitize(args, ctx) { const a = args || {}; const { dateRange } = ctx; const from = /^\d{8}$/.test(String(a.from || '')) ? String(a.from) : dateRange.from; const to = /^\d{8}$/.test(String(a.to || '')) ? String(a.to) : dateRange.to; return { from: from < dateRange.from ? dateRange.from : from, to: to > dateRange.to ? dateRange.to : to, }; } function sumBy(rows, key) { const m = new Map(); for (const r of rows) m.set(r.date, (m.get(r.date) || 0) + (Number(r[key]) || 0)); return m; } function relDiff(a, b) { return Math.abs(a - b) / Math.max(Math.abs(b), 1); } function crossHandler(ctx, args) { const { svc, entityKey } = ctx; const store = svc.store; const { from, to } = args; const inRange = (r) => r.date >= from && r.date <= to; const shopRows = store.queryRows(entityKey, { from, to, scope: 'shop' }).filter(inRange); const chainRows = store.queryRows(entityKey, { from, to, scope: 'chain' }).filter(inRange); const companyRows = store.queryRows(entityKey, { from, to, scope: 'company' }).filter(inRange); if (!shopRows.length && !chainRows.length) { return { summary: `区间 ${ymdDash(from)} ~ ${ymdDash(to)} 无可核对的行(门店/连锁层均无数据)。`, data: { from, to, checked: 0 } }; } const metrics = [ { key: 'grossAmt', label: '营业额' }, { key: 'validOrderCnt', label: '有效订单' }, ]; // 层×指标 各建一张按日求和表(日期字符串不做数值运算) const sums = {}; for (const m of metrics) { sums[m.key] = { shop: sumBy(shopRows, m.key), chain: sumBy(chainRows, m.key), company: sumBy(companyRows, m.key) }; } const days = eachDay(from, to).filter((d) => sums.grossAmt.shop.has(d) || sums.grossAmt.chain.has(d)); const mismatches = []; let checked = 0; for (const d of days) { for (const m of metrics) { const shop = sums[m.key].shop.get(d); const chain = sums[m.key].chain.get(d); const company = sums[m.key].company.get(d); // 同层求和口径:门店合计对比连锁合计;连锁合计对比公司行(任一侧缺失跳过该对比) const pairs = []; if (shop !== undefined && chain !== undefined) pairs.push(['门店合计 vs 连锁', shop, chain]); if (chain !== undefined && company !== undefined) pairs.push(['连锁合计 vs 公司', chain, company]); for (const [kind, a, b] of pairs) { checked += 1; const diff = relDiff(a, b); if (diff > CROSS_TOL) mismatches.push({ date: d, metric: m.label, kind, a: Math.round(a * 100) / 100, b: Math.round(b * 100) / 100, diffPct: +(diff * 100).toFixed(2) }); } } } mismatches.sort((x, y) => y.diffPct - x.diffPct); const severe = mismatches.some((m) => m.diffPct / 100 > CROSS_FAIL); const lines = []; if (!mismatches.length) { lines.push(`层间一致性通过:抽查 ${days.length} 天 × 2 指标(共 ${checked} 组对比),门店合计/连锁/公司层间一致(容差 ${(CROSS_TOL * 100).toFixed(1)}%)。`); } else { lines.push(`层间一致性${severe ? '严重不一致' : '存在不一致'}:${mismatches.length} 组对比超容差(共核对 ${checked} 组),最差 ${ymdDash(mismatches[0].date)} ${mismatches[0].kind} ${mismatches[0].metric}:${mismatches[0].a} vs ${mismatches[0].b}(差 ${mismatches[0].diffPct}%)。`); for (const m of mismatches.slice(0, 8)) { lines.push(` ${ymdDash(m.date)} ${m.metric} ${m.kind}:${m.a} vs ${m.b}(差 ${m.diffPct}%)`); } if (mismatches.length > 8) lines.push(` …其余 ${mismatches.length - 8} 组略`); lines.push('层间不一致通常意味着某一粒度取数异常(如门店行全零、部分门店缺数),相关结论应标注「数据质量存疑」。'); } return { summary: lines.join('\n'), data: { from, to, checked, mismatchCount: mismatches.length, severe, mismatches: mismatches.slice(0, 20), verdicts: [{ kind: 'cross-layer', check: `层间一致性 · 门店/连锁/公司(${ymdDash(from)} ~ ${ymdDash(to)})`, status: mismatches.length === 0 ? 'ok' : (severe ? 'fail' : 'warn'), detail: mismatches.length === 0 ? `抽查 ${days.length} 天 × 2 指标共 ${checked} 组对比全部一致` : `${mismatches.length} 组超容差,最差 ${ymdDash(mismatches[0].date)} ${mismatches[0].metric} 差 ${mismatches[0].diffPct}%`, }], }, }; } module.exports = { crossSanitize, crossHandler, CROSS_TOL, CROSS_FAIL };