#!/usr/bin/env node 'use strict'; // 淘宝闪购经营数据 · 门店维度取数诊断 // ---------------------------------------------------------------------------- // 用途:在「装了商家客户端」的电脑上运行,对门店粒度取数做参数对照实验, // 生成 JSON 诊断报告,拷回开发机即可定位「门店数据全为 0」一类问题。 // 运行:node scripts/diagnose-napos.js [日期YYYYMMDD] [门店ID] // · 日期缺省取昨日;门店缺省取组织树前 2 家 // · 全程只调用只读查询接口,不写任何业务数据 // 输出:napos-诊断报告-<时间戳>.json(与本文件同目录)+ 控制台摘要 // 提示:Windows 建议在「启动.bat」打开的窗口里运行(已切 UTF-8,中文不乱码)。 const fs = require('fs'); const path = require('path'); const os = require('os'); const kit = require('../napos/kit.js'); const PORT = Number(process.env.NAPOS_CDP_PORT || 9669); function ymdShift(ymd, delta) { const d = new Date(Number(ymd.slice(0, 4)), Number(ymd.slice(4, 6)) - 1, Number(ymd.slice(6, 8))); d.setDate(d.getDate() + delta); const z = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`; } function todayYmd() { const d = new Date(); const z = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`; } function sumOf(rows, field) { return Math.round((rows || []).reduce((s, r) => s + (Number(r[field]) || 0), 0) * 100) / 100; } function firstRow(r) { const row = (r.daily && r.daily[0]) || null; if (!row) return null; const { raw, ...rest } = row; return rest; } // 13 个核心指标的门店级存在性探针:区分「字段不存在」与「存在但为 0/空」, // 回答「下单人数门店级全 0」「营业时长/异常关店时长取不到」是上游不给还是键名不同。 const CORE_METRICS = ['valid_order_cnt', 'gross_amt', 'income_amt', 'expense_amt', 'exp_uv', 'order_user_cnt', 'clk_exp_rate', 'ord_clk_rate', 'per_actual_pay', 'act_ord_cnt', 'act_shop_subsidy_amt', 'shop_roi', 'rebuy_rate_30d']; function metricProbe(row) { if (!row) return null; const keys = Object.keys(row); const coreMetrics = {}; for (const k of CORE_METRICS) { coreMetrics[k] = Object.prototype.hasOwnProperty.call(row, k) ? row[k] : '(字段不存在)'; } const relatedExtraKeys = keys .filter((k) => /user|buyer|hour|close|open_time|biz|pause|rest/i.test(k)) .map((k) => `${k}=${JSON.stringify(row[k])}`); return { fieldCount: keys.length, coreMetrics, relatedExtraKeys }; } async function runCase(tag, params) { const out = { tag, ...params, rows: 0, ordersSum: 0, grossSum: 0, expSum: 0, firstRow: null }; try { const r = await kit.statsHistory({ port: PORT, from: params.from, to: params.to, dateType: 'self_define', chainId: params.chainId, shopListIn: params.shopListIn }); out.rows = (r.daily || []).length; out.ordersSum = sumOf(r.daily, 'valid_order_cnt'); out.grossSum = sumOf(r.daily, 'gross_amt'); out.expSum = sumOf(r.daily, 'exp_uv'); out.orderUserSum = sumOf(r.daily, 'order_user_cnt'); out.firstRow = firstRow(r); out.metricProbe = metricProbe((r.daily || [])[0]); out.code = r.code ?? null; out.msg = r.msg || null; out.risk = r.risk || false; out.authFail = r.authFail || false; out.shapeWarning = r.shapeWarning || false; out.ok = r.ok; } catch (e) { out.error = e.message || String(e); } return out; } (async () => { const report = { generatedAt: new Date().toISOString(), platform: `${os.platform()} ${os.release()}`, node: process.version, port: PORT, HISTORY_URL: 'https://lsycm.alibaba.com/api/overview/chain/custom/historyOverview', requestBodyTemplate: '{ shopId:"", secretkey, timestamp, chainId, dateType:"self_define", key:"self_define", compareOption:"", beginDate, endDate, bizDate, shopListIn }', }; console.log(`[1/4] 检查客户端调试端口 ${PORT} ...`); const st = await kit.status(PORT); report.status = { portOk: !!st.portOk, browser: st.browser || null, targets: (st.targets || []).length, probeError: st.probeError || null }; if (!st.portOk) { report.conclusion = '客户端不在线:请完全退出客户端后,以 --remote-debugging-port=' + PORT + ' 参数重新启动并登录,再运行本脚本。'; finish(report); return; } console.log(' 已连接:', st.browser || '(未知浏览器)'); console.log('[2/4] 读取门店组织树 ...'); let tree; try { tree = await kit.shopTree({ port: PORT }); } catch (e) { report.treeError = e.message || String(e); report.conclusion = '读取组织树失败:' + report.treeError; finish(report); return; } report.merchant = tree.merchant || null; report.chains = (tree.chains || []).map((c) => ({ chainId: String(c.chainId), chainName: c.chainName })); const shops = (tree.shops || []).map((s) => ({ shopId: String(s.shopId), shopName: s.shopName, chainId: String(s.chainId || ''), busyLevel: s.busyLevel || null })); report.shopCount = shops.length; const testDate = /^\d{8}$/.test(process.argv[2] || '') ? process.argv[2] : ymdShift(todayYmd(), -1); // 不做硬编码 chainId 兜底:对着陌生主体跑对照实验只会产出误导性报告 if (!report.chains.length) { report.conclusion = '组织树没有连锁信息,无法构造连锁对照实验。请确认该主体确有连锁结构后重跑。'; finish(report); return; } const chainId = report.chains[0].chainId; const picked = []; if (/^\d+$/.test(process.argv[3] || '')) { const one = shops.find((s) => s.shopId === process.argv[3]); if (one) picked.push(one); } for (const s of shops) { if (picked.length >= 2) break; if (!picked.includes(s)) picked.push(s); } report.testDate = testDate; report.testShops = picked; console.log(` 主体: ${(tree.merchant && tree.merchant.name) || '?'} · ${shops.length} 家门店 · 测试日期: ${testDate}`); console.log('[3/4] 连锁维度对照查询(应有真实数据)...'); report.chainControl = await runCase('连锁对照·不带门店', { from: testDate, to: testDate, chainId, shopListIn: '' }); console.log(` 行:${report.chainControl.rows} 有效订单:${report.chainControl.ordersSum} 营业额:${report.chainControl.grossSum}`); console.log('[4/4] 门店维度参数对照实验 ...'); report.cases = []; for (const s of picked) { const cases = [ { tag: `A 现行方式 chainId=-1 + shopListIn`, chainId: '-1', shopListIn: s.shopId }, { tag: `B 变体 chainId=${chainId} + shopListIn`, chainId, shopListIn: s.shopId }, { tag: `D 变体 chainId=(空) + shopListIn`, chainId: '', shopListIn: s.shopId }, ]; for (const c of cases) { const r = await runCase(c.tag, { from: testDate, to: testDate, chainId: c.chainId, shopListIn: c.shopListIn }); r.shopId = s.shopId; r.shopName = s.shopName; report.cases.push(r); console.log(` ${s.shopName} | ${c.tag} => 行:${r.rows} 单:${r.ordersSum} 额:${r.grossSum}${r.error ? ' 异常:' + r.error : ''}`); await new Promise((res) => setTimeout(res, 600)); } } // 结论:哪个变体在门店维度返回了非零数据 const hit = (report.cases || []).filter((c) => c.rows > 0 && (c.ordersSum > 0 || c.grossSum > 0)); const tags = [...new Set(hit.map((c) => c.tag.split(' ')[0]))]; if (!report.chainControl.rows || report.chainControl.ordersSum === 0) { report.conclusion = '连锁对照查询也为零:该日期可能无经营数据,请换一个链级行有数的日期重跑(node scripts/diagnose-napos.js 日期YYYYMMDD)。'; } else if (hit.length) { report.conclusion = `门店维度有数据的参数形式:${tags.join('、')}。请把本报告交回开发侧修正取数参数。`; } else { report.conclusion = '所有参数形式下门店维度均为零:需进一步核对数据权限/门店经营状态(报告含各变体原始返回首行)。'; } finish(report); })().catch((e) => { console.error('诊断脚本异常:', e.message); process.exit(1); }); function finish(report) { report.conclusion = report.conclusion || '见各项数据'; const out = path.join(__dirname, `napos-诊断报告-${Date.now()}.json`); fs.writeFileSync(out, JSON.stringify(report, null, 2), 'utf8'); console.log('\n========== 诊断结论 =========='); console.log(report.conclusion); console.log('\n报告已生成,请把这个文件拷回开发机:'); console.log(' ' + out); }