diagnose-napos.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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. async function runCase(tag, params) {
  38. const out = { tag, ...params, rows: 0, ordersSum: 0, grossSum: 0, expSum: 0, firstRow: null };
  39. try {
  40. const r = await kit.statsHistory({ port: PORT, from: params.from, to: params.to, dateType: 'self_define',
  41. chainId: params.chainId, shopListIn: params.shopListIn });
  42. out.rows = (r.daily || []).length;
  43. out.ordersSum = sumOf(r.daily, 'valid_order_cnt');
  44. out.grossSum = sumOf(r.daily, 'gross_amt');
  45. out.expSum = sumOf(r.daily, 'exp_uv');
  46. out.firstRow = firstRow(r);
  47. out.code = r.code ?? null;
  48. out.msg = r.msg || null;
  49. out.risk = r.risk || false;
  50. out.authFail = r.authFail || false;
  51. out.ok = r.ok;
  52. } catch (e) {
  53. out.error = e.message || String(e);
  54. }
  55. return out;
  56. }
  57. (async () => {
  58. const report = {
  59. generatedAt: new Date().toISOString(),
  60. platform: `${os.platform()} ${os.release()}`,
  61. node: process.version,
  62. port: PORT,
  63. HISTORY_URL: 'https://lsycm.alibaba.com/api/overview/chain/custom/historyOverview',
  64. requestBodyTemplate: '{ shopId:"", secretkey, timestamp, chainId, dateType:"self_define", key:"self_define", compareOption:"", beginDate, endDate, bizDate, shopListIn }',
  65. };
  66. console.log(`[1/4] 检查客户端调试端口 ${PORT} ...`);
  67. const st = await kit.status(PORT);
  68. report.status = { portOk: !!st.portOk, browser: st.browser || null, targets: (st.targets || []).length, probeError: st.probeError || null };
  69. if (!st.portOk) {
  70. report.conclusion = '客户端不在线:请完全退出客户端后,以 --remote-debugging-port=' + PORT + ' 参数重新启动并登录,再运行本脚本。';
  71. finish(report);
  72. return;
  73. }
  74. console.log(' 已连接:', st.browser || '(未知浏览器)');
  75. console.log('[2/4] 读取门店组织树 ...');
  76. let tree;
  77. try {
  78. tree = await kit.shopTree({ port: PORT });
  79. } catch (e) {
  80. report.treeError = e.message || String(e);
  81. report.conclusion = '读取组织树失败:' + report.treeError;
  82. finish(report);
  83. return;
  84. }
  85. report.merchant = tree.merchant || null;
  86. report.chains = (tree.chains || []).map((c) => ({ chainId: String(c.chainId), chainName: c.chainName }));
  87. const shops = (tree.shops || []).map((s) => ({ shopId: String(s.shopId), shopName: s.shopName, chainId: String(s.chainId || ''), busyLevel: s.busyLevel || null }));
  88. report.shopCount = shops.length;
  89. const testDate = /^\d{8}$/.test(process.argv[2] || '') ? process.argv[2] : ymdShift(todayYmd(), -1);
  90. const chainId = (report.chains[0] || {}).chainId || '96059480';
  91. const picked = [];
  92. if (/^\d+$/.test(process.argv[3] || '')) {
  93. const one = shops.find((s) => s.shopId === process.argv[3]);
  94. if (one) picked.push(one);
  95. }
  96. for (const s of shops) {
  97. if (picked.length >= 2) break;
  98. if (!picked.includes(s)) picked.push(s);
  99. }
  100. report.testDate = testDate;
  101. report.testShops = picked;
  102. console.log(` 主体: ${(tree.merchant && tree.merchant.name) || '?'} · ${shops.length} 家门店 · 测试日期: ${testDate}`);
  103. console.log('[3/4] 连锁维度对照查询(应有真实数据)...');
  104. report.chainControl = await runCase('连锁对照·不带门店', { from: testDate, to: testDate, chainId, shopListIn: '' });
  105. console.log(` 行:${report.chainControl.rows} 有效订单:${report.chainControl.ordersSum} 营业额:${report.chainControl.grossSum}`);
  106. console.log('[4/4] 门店维度参数对照实验 ...');
  107. report.cases = [];
  108. for (const s of picked) {
  109. const cases = [
  110. { tag: `A 现行方式 chainId=-1 + shopListIn`, chainId: '-1', shopListIn: s.shopId },
  111. { tag: `B 变体 chainId=${chainId} + shopListIn`, chainId, shopListIn: s.shopId },
  112. { tag: `D 变体 chainId=(空) + shopListIn`, chainId: '', shopListIn: s.shopId },
  113. ];
  114. for (const c of cases) {
  115. const r = await runCase(c.tag, { from: testDate, to: testDate, chainId: c.chainId, shopListIn: c.shopListIn });
  116. r.shopId = s.shopId;
  117. r.shopName = s.shopName;
  118. report.cases.push(r);
  119. console.log(` ${s.shopName} | ${c.tag} => 行:${r.rows} 单:${r.ordersSum} 额:${r.grossSum}${r.error ? ' 异常:' + r.error : ''}`);
  120. await new Promise((res) => setTimeout(res, 600));
  121. }
  122. }
  123. // 结论:哪个变体在门店维度返回了非零数据
  124. const hit = (report.cases || []).filter((c) => c.rows > 0 && (c.ordersSum > 0 || c.grossSum > 0));
  125. const tags = [...new Set(hit.map((c) => c.tag.split(' ')[0]))];
  126. if (!report.chainControl.rows || report.chainControl.ordersSum === 0) {
  127. report.conclusion = '连锁对照查询也为零:该日期可能无经营数据,请换一个链级行有数的日期重跑(node scripts/diagnose-napos.js 日期YYYYMMDD)。';
  128. } else if (hit.length) {
  129. report.conclusion = `门店维度有数据的参数形式:${tags.join('、')}。请把本报告交回开发侧修正取数参数。`;
  130. } else {
  131. report.conclusion = '所有参数形式下门店维度均为零:需进一步核对数据权限/门店经营状态(报告含各变体原始返回首行)。';
  132. }
  133. finish(report);
  134. })().catch((e) => {
  135. console.error('诊断脚本异常:', e.message);
  136. process.exit(1);
  137. });
  138. function finish(report) {
  139. report.conclusion = report.conclusion || '见各项数据';
  140. const out = path.join(__dirname, `napos-诊断报告-${Date.now()}.json`);
  141. fs.writeFileSync(out, JSON.stringify(report, null, 2), 'utf8');
  142. console.log('\n========== 诊断结论 ==========');
  143. console.log(report.conclusion);
  144. console.log('\n报告已生成,请把这个文件拷回开发机:');
  145. console.log(' ' + out);
  146. }