|
|
@@ -0,0 +1,162 @@
|
|
|
+#!/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;
|
|
|
+}
|
|
|
+
|
|
|
+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.firstRow = firstRow(r);
|
|
|
+ out.code = r.code ?? null;
|
|
|
+ out.msg = r.msg || null;
|
|
|
+ out.risk = r.risk || false;
|
|
|
+ out.authFail = r.authFail || 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);
|
|
|
+ const chainId = (report.chains[0] || {}).chainId || '96059480';
|
|
|
+ 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);
|
|
|
+}
|