| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- #!/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);
- }
|