diagnose-napos.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env node
  2. 'use strict';
  3. // 淘宝闪购经营数据 · 门店维度取数诊断
  4. // ----------------------------------------------------------------------------
  5. // 用途:在「装了商家客户端」的电脑上运行,对门店粒度取数做参数对照实验,
  6. // 生成 JSON 诊断报告,拷回开发机即可定位「门店数据全为 0」一类问题。
  7. // 运行:node scripts/diagnose-napos.js [日期YYYYMMDD] [门店ID]
  8. // · 日期缺省取昨日;门店缺省取组织树前 2 家
  9. // · 全程只调用只读查询接口,不写任何业务数据
  10. // 输出:napos-诊断报告-<时间戳>.json(与本文件同目录)+ 控制台摘要
  11. // 提示:Windows 建议在「启动.bat」打开的窗口里运行(已切 UTF-8,中文不乱码)。
  12. const fs = require('fs');
  13. const path = require('path');
  14. const os = require('os');
  15. const kit = require('../napos/kit.js');
  16. const PORT = Number(process.env.NAPOS_CDP_PORT || 9669);
  17. function ymdShift(ymd, delta) {
  18. const d = new Date(Number(ymd.slice(0, 4)), Number(ymd.slice(4, 6)) - 1, Number(ymd.slice(6, 8)));
  19. d.setDate(d.getDate() + delta);
  20. const z = (n) => String(n).padStart(2, '0');
  21. return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`;
  22. }
  23. function todayYmd() {
  24. const d = new Date();
  25. const z = (n) => String(n).padStart(2, '0');
  26. return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`;
  27. }
  28. function sumOf(rows, field) {
  29. return Math.round((rows || []).reduce((s, r) => s + (Number(r[field]) || 0), 0) * 100) / 100;
  30. }
  31. function firstRow(r) {
  32. const row = (r.daily && r.daily[0]) || null;
  33. if (!row) return null;
  34. const { raw, ...rest } = row;
  35. return rest;
  36. }
  37. // 13 个核心指标的门店级存在性探针:区分「字段不存在」与「存在但为 0/空」,
  38. // 回答「下单人数门店级全 0」「营业时长/异常关店时长取不到」是上游不给还是键名不同。
  39. const CORE_METRICS = ['valid_order_cnt', 'gross_amt', 'income_amt', 'expense_amt', 'exp_uv',
  40. 'order_user_cnt', 'clk_exp_rate', 'ord_clk_rate', 'per_actual_pay', 'act_ord_cnt',
  41. 'act_shop_subsidy_amt', 'shop_roi', 'rebuy_rate_30d'];
  42. function metricProbe(row) {
  43. if (!row) return null;
  44. const keys = Object.keys(row);
  45. const coreMetrics = {};
  46. for (const k of CORE_METRICS) {
  47. coreMetrics[k] = Object.prototype.hasOwnProperty.call(row, k) ? row[k] : '(字段不存在)';
  48. }
  49. const relatedExtraKeys = keys
  50. .filter((k) => /user|buyer|hour|close|open_time|biz|pause|rest/i.test(k))
  51. .map((k) => `${k}=${JSON.stringify(row[k])}`);
  52. return { fieldCount: keys.length, coreMetrics, relatedExtraKeys };
  53. }
  54. async function runCase(tag, params) {
  55. const out = { tag, ...params, rows: 0, ordersSum: 0, grossSum: 0, expSum: 0, firstRow: null };
  56. try {
  57. const r = await kit.statsHistory({ port: PORT, from: params.from, to: params.to, dateType: 'self_define',
  58. chainId: params.chainId, shopListIn: params.shopListIn });
  59. out.rows = (r.daily || []).length;
  60. out.ordersSum = sumOf(r.daily, 'valid_order_cnt');
  61. out.grossSum = sumOf(r.daily, 'gross_amt');
  62. out.expSum = sumOf(r.daily, 'exp_uv');
  63. out.orderUserSum = sumOf(r.daily, 'order_user_cnt');
  64. out.firstRow = firstRow(r);
  65. out.metricProbe = metricProbe((r.daily || [])[0]);
  66. out.code = r.code ?? null;
  67. out.msg = r.msg || null;
  68. out.risk = r.risk || false;
  69. out.authFail = r.authFail || false;
  70. out.shapeWarning = r.shapeWarning || false;
  71. out.ok = r.ok;
  72. } catch (e) {
  73. out.error = e.message || String(e);
  74. }
  75. return out;
  76. }
  77. (async () => {
  78. const report = {
  79. generatedAt: new Date().toISOString(),
  80. platform: `${os.platform()} ${os.release()}`,
  81. node: process.version,
  82. port: PORT,
  83. HISTORY_URL: 'https://lsycm.alibaba.com/api/overview/chain/custom/historyOverview',
  84. requestBodyTemplate: '{ shopId:"", secretkey, timestamp, chainId, dateType:"self_define", key:"self_define", compareOption:"", beginDate, endDate, bizDate, shopListIn }',
  85. };
  86. console.log(`[1/4] 检查客户端调试端口 ${PORT} ...`);
  87. const st = await kit.status(PORT);
  88. report.status = { portOk: !!st.portOk, browser: st.browser || null, targets: (st.targets || []).length, probeError: st.probeError || null };
  89. if (!st.portOk) {
  90. report.conclusion = '客户端不在线:请完全退出客户端后,以 --remote-debugging-port=' + PORT + ' 参数重新启动并登录,再运行本脚本。';
  91. finish(report);
  92. return;
  93. }
  94. console.log(' 已连接:', st.browser || '(未知浏览器)');
  95. console.log('[2/4] 读取门店组织树 ...');
  96. let tree;
  97. try {
  98. tree = await kit.shopTree({ port: PORT });
  99. } catch (e) {
  100. report.treeError = e.message || String(e);
  101. report.conclusion = '读取组织树失败:' + report.treeError;
  102. finish(report);
  103. return;
  104. }
  105. report.merchant = tree.merchant || null;
  106. report.chains = (tree.chains || []).map((c) => ({ chainId: String(c.chainId), chainName: c.chainName }));
  107. const shops = (tree.shops || []).map((s) => ({ shopId: String(s.shopId), shopName: s.shopName, chainId: String(s.chainId || ''), busyLevel: s.busyLevel || null }));
  108. report.shopCount = shops.length;
  109. const testDate = /^\d{8}$/.test(process.argv[2] || '') ? process.argv[2] : ymdShift(todayYmd(), -1);
  110. // 不做硬编码 chainId 兜底:对着陌生主体跑对照实验只会产出误导性报告
  111. if (!report.chains.length) {
  112. report.conclusion = '组织树没有连锁信息,无法构造连锁对照实验。请确认该主体确有连锁结构后重跑。';
  113. finish(report);
  114. return;
  115. }
  116. const chainId = report.chains[0].chainId;
  117. const picked = [];
  118. if (/^\d+$/.test(process.argv[3] || '')) {
  119. const one = shops.find((s) => s.shopId === process.argv[3]);
  120. if (one) picked.push(one);
  121. }
  122. for (const s of shops) {
  123. if (picked.length >= 2) break;
  124. if (!picked.includes(s)) picked.push(s);
  125. }
  126. report.testDate = testDate;
  127. report.testShops = picked;
  128. console.log(` 主体: ${(tree.merchant && tree.merchant.name) || '?'} · ${shops.length} 家门店 · 测试日期: ${testDate}`);
  129. console.log('[3/4] 连锁维度对照查询(应有真实数据)...');
  130. report.chainControl = await runCase('连锁对照·不带门店', { from: testDate, to: testDate, chainId, shopListIn: '' });
  131. console.log(` 行:${report.chainControl.rows} 有效订单:${report.chainControl.ordersSum} 营业额:${report.chainControl.grossSum}`);
  132. console.log('[4/4] 门店维度参数对照实验 ...');
  133. report.cases = [];
  134. for (const s of picked) {
  135. const cases = [
  136. { tag: `A 现行方式 chainId=-1 + shopListIn`, chainId: '-1', shopListIn: s.shopId },
  137. { tag: `B 变体 chainId=${chainId} + shopListIn`, chainId, shopListIn: s.shopId },
  138. { tag: `D 变体 chainId=(空) + shopListIn`, chainId: '', shopListIn: s.shopId },
  139. ];
  140. for (const c of cases) {
  141. const r = await runCase(c.tag, { from: testDate, to: testDate, chainId: c.chainId, shopListIn: c.shopListIn });
  142. r.shopId = s.shopId;
  143. r.shopName = s.shopName;
  144. report.cases.push(r);
  145. console.log(` ${s.shopName} | ${c.tag} => 行:${r.rows} 单:${r.ordersSum} 额:${r.grossSum}${r.error ? ' 异常:' + r.error : ''}`);
  146. await new Promise((res) => setTimeout(res, 600));
  147. }
  148. }
  149. // 结论:哪个变体在门店维度返回了非零数据
  150. const hit = (report.cases || []).filter((c) => c.rows > 0 && (c.ordersSum > 0 || c.grossSum > 0));
  151. const tags = [...new Set(hit.map((c) => c.tag.split(' ')[0]))];
  152. if (!report.chainControl.rows || report.chainControl.ordersSum === 0) {
  153. report.conclusion = '连锁对照查询也为零:该日期可能无经营数据,请换一个链级行有数的日期重跑(node scripts/diagnose-napos.js 日期YYYYMMDD)。';
  154. } else if (hit.length) {
  155. report.conclusion = `门店维度有数据的参数形式:${tags.join('、')}。请把本报告交回开发侧修正取数参数。`;
  156. } else {
  157. report.conclusion = '所有参数形式下门店维度均为零:需进一步核对数据权限/门店经营状态(报告含各变体原始返回首行)。';
  158. }
  159. finish(report);
  160. })().catch((e) => {
  161. console.error('诊断脚本异常:', e.message);
  162. process.exit(1);
  163. });
  164. function finish(report) {
  165. report.conclusion = report.conclusion || '见各项数据';
  166. const out = path.join(__dirname, `napos-诊断报告-${Date.now()}.json`);
  167. fs.writeFileSync(out, JSON.stringify(report, null, 2), 'utf8');
  168. console.log('\n========== 诊断结论 ==========');
  169. console.log(report.conclusion);
  170. console.log('\n报告已生成,请把这个文件拷回开发机:');
  171. console.log(' ' + out);
  172. }