frontend-audit-apis.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /**
  2. * 前端各页面只读接口耗时巡检(跳过初始化/清空/写操作)
  3. * 用法: node scripts/frontend-audit-apis.js
  4. */
  5. const http = require("http");
  6. const fs = require("fs");
  7. const path = require("path");
  8. const BASE = { hostname: "localhost", port: 8088, prefix: "/sjnmtybt" };
  9. const PAY_MONTH = "202608";
  10. const TOWN = "T_YEXIE";
  11. const VILLAGE = "V_YX_001";
  12. const TIMEOUT_WARN = 3000;
  13. const TIMEOUT_FAIL = 15000;
  14. function req(method, apiPath, body, timeoutMs) {
  15. const t0 = Date.now();
  16. return new Promise((resolve) => {
  17. const data = body == null ? null : JSON.stringify(body);
  18. const headers = { Accept: "application/json" };
  19. if (data) {
  20. headers["Content-Type"] = "application/json";
  21. headers["Content-Length"] = Buffer.byteLength(data);
  22. }
  23. const timer = setTimeout(() => {
  24. resolve({ ms: Date.now() - t0, code: 0, error: "TIMEOUT", data: null });
  25. r.destroy();
  26. }, timeoutMs || TIMEOUT_FAIL);
  27. const r = http.request(
  28. {
  29. hostname: BASE.hostname,
  30. port: BASE.port,
  31. path: BASE.prefix + apiPath,
  32. method,
  33. headers,
  34. },
  35. (res) => {
  36. let raw = "";
  37. res.on("data", (c) => (raw += c));
  38. res.on("end", () => {
  39. clearTimeout(timer);
  40. const ms = Date.now() - t0;
  41. let json = null;
  42. try {
  43. json = JSON.parse(raw);
  44. } catch (_) {
  45. json = { parseError: true };
  46. }
  47. resolve({
  48. ms,
  49. http: res.statusCode,
  50. code: json && json.code != null ? json.code : res.statusCode,
  51. data: json && json.data,
  52. message: json && (json.message || json.msg),
  53. error: null,
  54. });
  55. });
  56. }
  57. );
  58. r.on("error", (e) => {
  59. clearTimeout(timer);
  60. resolve({ ms: Date.now() - t0, code: 0, error: e.message, data: null });
  61. });
  62. if (data) r.write(data);
  63. r.end();
  64. });
  65. }
  66. /** 页面 → 只读接口用例 */
  67. const PAGE_CASES = [
  68. {
  69. page: "工作台 /dashboard",
  70. role: "all",
  71. cases: [
  72. ["GET", `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`, null, 90000],
  73. ["GET", `/api/dashboard/summary?payMonth=${PAY_MONTH}&townId=${TOWN}`, null],
  74. ["GET", `/api/dashboard/region-stats?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`, null],
  75. ["GET", "/api/workflow/roles", null],
  76. ["GET", "/api/workflow/stages", null],
  77. ],
  78. },
  79. {
  80. page: "人员录入 /base",
  81. role: "village,admin",
  82. cases: [
  83. ["GET", "/api/regions?level=TOWN&parentId=D_SJ", null],
  84. ["GET", `/api/regions?level=VILLAGE&parentId=${TOWN}`, null],
  85. [
  86. "POST",
  87. "/api/personnel/page",
  88. { pageNum: 1, pageSize: 20, townId: TOWN, villageId: VILLAGE },
  89. ],
  90. ["GET", `/api/amount-standards/current?townId=${TOWN}&standardType=MONTHLY`, null],
  91. ],
  92. },
  93. {
  94. page: "居保匹配 /insurance",
  95. role: "social,admin",
  96. cases: [
  97. [
  98. "POST",
  99. "/api/batches/page",
  100. {
  101. pageNum: 1,
  102. pageSize: 10,
  103. batchLevel: "TOWN",
  104. townId: TOWN,
  105. statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN",
  106. lite: true,
  107. },
  108. ],
  109. [
  110. "POST",
  111. "/api/batches/page",
  112. {
  113. pageNum: 1,
  114. pageSize: 10,
  115. batchLevel: "DISTRICT",
  116. statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN",
  117. lite: true,
  118. },
  119. ],
  120. ],
  121. },
  122. {
  123. page: "审批管理 /approvals",
  124. role: "town,leader,admin",
  125. cases: [
  126. [
  127. "POST",
  128. "/api/batches/page",
  129. { pageNum: 1, pageSize: 20, townId: TOWN, batchLevel: "TOWN", lite: true },
  130. ],
  131. [
  132. "POST",
  133. "/api/personnel/page",
  134. { pageNum: 1, pageSize: 20, townId: TOWN, bizStatus: "PENDING_TOWN_APPROVE" },
  135. ],
  136. ],
  137. },
  138. {
  139. page: "补助标准 /status",
  140. role: "social,admin",
  141. cases: [
  142. ["GET", `/api/amount-standards?townId=${TOWN}&current=true`, null],
  143. ["GET", `/api/amount-standards/current?townId=${TOWN}&standardType=FUNERAL`, null],
  144. ],
  145. },
  146. {
  147. page: "特殊业务 /special",
  148. role: "social,admin",
  149. cases: [
  150. ["GET", "/api/special-biz/page?bizType=FUNERAL&pageNum=1&pageSize=20", null],
  151. ["GET", "/api/special-biz/page?bizType=ADJUST&pageNum=1&pageSize=20", null],
  152. [`GET`, `/api/special-biz/refund-stats?townId=${TOWN}&payMonth=${PAY_MONTH}`, null],
  153. ],
  154. },
  155. {
  156. page: "月补贴清单 /monthly",
  157. role: "social,admin",
  158. cases: [
  159. [
  160. "POST",
  161. "/api/batches/page",
  162. {
  163. pageNum: 1,
  164. pageSize: 20,
  165. townId: TOWN,
  166. batchLevel: "TOWN",
  167. payMonth: PAY_MONTH,
  168. lite: true,
  169. withTownTotals: true,
  170. },
  171. ],
  172. [
  173. "POST",
  174. "/api/batches/page",
  175. {
  176. pageNum: 1,
  177. pageSize: 20,
  178. batchLevel: "TOWN",
  179. statusIn:
  180. "PENDING_STATUS_MAINTAIN,PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED",
  181. lite: true,
  182. withTownTotals: false,
  183. },
  184. ],
  185. [
  186. "POST",
  187. "/api/batches/unresolved-insurance-counts",
  188. { batchIds: ["TB_T_YEXIE_202608"] },
  189. ],
  190. [
  191. "POST",
  192. "/api/payments/details/page",
  193. { pageNum: 1, pageSize: 20, townId: TOWN, payMonth: PAY_MONTH },
  194. ],
  195. ],
  196. },
  197. {
  198. page: "回盘确认 /disk",
  199. role: "social,admin",
  200. cases: [
  201. [
  202. "POST",
  203. "/api/batches/page",
  204. {
  205. pageNum: 1,
  206. pageSize: 20,
  207. townId: TOWN,
  208. statusIn: "EXPORTED,RETURNED",
  209. lite: true,
  210. },
  211. ],
  212. [
  213. "POST",
  214. "/api/return-disk/details/page",
  215. { pageNum: 1, pageSize: 20, townId: TOWN },
  216. ],
  217. ],
  218. },
  219. {
  220. page: "监控告警 /alerts",
  221. role: "all",
  222. cases: [
  223. ["POST", "/api/alerts/page", { pageNum: 1, pageSize: 20, townId: TOWN }],
  224. ["POST", "/api/alerts/page", { pageNum: 1, pageSize: 20, level: "HIGH" }],
  225. ],
  226. },
  227. {
  228. page: "数据报表 /reports",
  229. role: "all",
  230. cases: [
  231. ["GET", `/api/reports/yoy?payMonth=${PAY_MONTH}`, null],
  232. ["GET", `/api/reports/topics/personnel?payMonth=${PAY_MONTH}&townId=${TOWN}`, null],
  233. ["GET", `/api/reports/topics/payment?payMonth=${PAY_MONTH}&townId=${TOWN}`, null],
  234. [
  235. "POST",
  236. "/api/batch-stats/page",
  237. { pageNum: 1, pageSize: 20, townId: TOWN, payMonth: PAY_MONTH },
  238. ],
  239. ],
  240. },
  241. {
  242. page: "区划管理 /regions",
  243. role: "admin",
  244. cases: [
  245. ["GET", "/api/regions?level=TOWN&parentId=D_SJ", null],
  246. ["GET", "/api/regions?level=VILLAGE", null],
  247. ],
  248. },
  249. {
  250. page: "人员底数 /personnel-admin",
  251. role: "admin",
  252. cases: [
  253. ["POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, townId: TOWN }],
  254. ["POST", "/api/personnel/page", { pageNum: 1, pageSize: 20 }],
  255. ],
  256. },
  257. {
  258. page: "批次管理 /admin-batches",
  259. role: "admin",
  260. cases: [
  261. [
  262. "POST",
  263. "/api/batches/page",
  264. { pageNum: 1, pageSize: 20, payMonth: PAY_MONTH, lite: true, withTownTotals: false },
  265. ],
  266. [
  267. "POST",
  268. "/api/batches/page",
  269. { pageNum: 1, pageSize: 20, payMonth: PAY_MONTH, lite: true, withTownTotals: true },
  270. ],
  271. ["GET", `/api/workflow/batches/DB${PAY_MONTH}/overview`, null],
  272. ],
  273. },
  274. {
  275. page: "系统管理 /system",
  276. role: "admin",
  277. cases: [
  278. ["GET", "/api/workflow/roles", null],
  279. ["GET", "/api/workflow/lanes", null],
  280. ["GET", "/api/workflow/special-biz-nodes", null],
  281. ["GET", "/api/admin/validate-rules", null],
  282. ],
  283. },
  284. ];
  285. function hintOf(data) {
  286. if (data == null) return "null";
  287. if (Array.isArray(data)) return "arr=" + data.length;
  288. if (data.list) return "total=" + data.total + ",list=" + (data.list.length || 0);
  289. if (typeof data === "object") {
  290. const keys = Object.keys(data).slice(0, 8);
  291. const extra = [];
  292. if (data.monthAmount != null) extra.push("monthAmount=" + data.monthAmount);
  293. if (data.personnelCount != null) extra.push("personnel=" + data.personnelCount);
  294. if (data.currentMonth) {
  295. extra.push(
  296. "curAmt=" + (data.currentMonth.totalAmount || 0) +
  297. ",curPeople=" + (data.currentMonth.peopleCount || 0)
  298. );
  299. }
  300. return extra.length ? extra.join(";") : "keys=" + keys.join(",");
  301. }
  302. return String(data).slice(0, 60);
  303. }
  304. function statusOf(r) {
  305. if (r.error === "TIMEOUT") return "TIMEOUT";
  306. if (r.error) return "ERR";
  307. if (r.code !== 200) return "BIZ_FAIL";
  308. if (r.ms > TIMEOUT_FAIL) return "TIMEOUT";
  309. if (r.ms > TIMEOUT_WARN) return "SLOW";
  310. return "OK";
  311. }
  312. (async () => {
  313. console.log("=== Frontend API Audit (read-only) ===\n");
  314. const results = [];
  315. const seen = new Set();
  316. for (const group of PAGE_CASES) {
  317. for (const row of group.cases) {
  318. const method = row[0];
  319. const apiPath = row[1];
  320. const body = row[2];
  321. const timeoutMs = row[3];
  322. const key = method + " " + apiPath + JSON.stringify(body || {});
  323. if (seen.has(key)) continue;
  324. seen.add(key);
  325. const r = await req(method, apiPath, body, timeoutMs);
  326. const st = statusOf(r);
  327. results.push({
  328. page: group.page,
  329. role: group.role,
  330. method,
  331. path: apiPath.split("?")[0],
  332. ms: r.ms,
  333. code: r.code,
  334. status: st,
  335. hint: hintOf(r.data),
  336. message: r.message || r.error || "",
  337. });
  338. console.log(
  339. (st === "OK" ? "OK " : st.padEnd(5)) +
  340. " | " +
  341. String(r.ms).padStart(5) +
  342. "ms | " +
  343. group.page.split(" ")[0] +
  344. " | " +
  345. method +
  346. " " +
  347. apiPath.split("?")[0]
  348. );
  349. }
  350. }
  351. // 数据一致性抽检
  352. const checks = [];
  353. checks.push({
  354. name: "叶榭工作台本月金额=村图表合计",
  355. pass: false,
  356. detail: "pending",
  357. });
  358. const wbYexie = await req(
  359. "GET",
  360. `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`,
  361. null,
  362. 120000
  363. );
  364. const yx = wbYexie.data || {};
  365. const yxAmt = Number(yx.monthAmount || 0);
  366. const yxVillageSum = (yx.villageCharts || []).reduce(
  367. (s, v) => s + Number(v.amount || 0),
  368. 0
  369. );
  370. const yxAmtOk = yxAmt > 0 && Math.abs(yxAmt - yxVillageSum) < 1;
  371. checks[checks.length - 1] = {
  372. name: "叶榭工作台本月金额=村图表合计",
  373. pass: yxAmtOk,
  374. detail: `monthAmount=${yxAmt}, villageSum=${yxVillageSum}, ms=${wbYexie.ms}`,
  375. };
  376. checks.push({
  377. name: "叶榭本月新增不应≈总人数",
  378. pass: Number(yx.monthIncreaseCount || 0) < Number(yx.monthPeopleCount || 0) * 0.5,
  379. detail: `monthIncrease=${yx.monthIncreaseCount}, monthPeople=${yx.monthPeopleCount}`,
  380. });
  381. const wb = await req(
  382. "GET",
  383. `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=T_MAOGANG&months=6`,
  384. null
  385. );
  386. const ma = wb.data || {};
  387. const monthAmt = Number(ma.monthAmount || 0);
  388. const villageSum = (ma.villageCharts || []).reduce(
  389. (s, v) => s + Number(v.amount || 0),
  390. 0
  391. );
  392. const amtOk = monthAmt > 0 && Math.abs(monthAmt - villageSum) < 1;
  393. checks.push({
  394. name: "泖港工作台本月金额=村图表合计",
  395. pass: amtOk,
  396. detail: `monthAmount=${monthAmt}, villageSum=${villageSum}`,
  397. });
  398. const ins = await req("POST", "/api/batches/page", {
  399. pageNum: 1,
  400. pageSize: 10,
  401. batchLevel: "TOWN",
  402. townId: TOWN,
  403. statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN",
  404. lite: true,
  405. });
  406. checks.push({
  407. name: "居保列表 lite 查询 < 3s",
  408. pass: ins.ms < TIMEOUT_WARN && ins.code === 200,
  409. detail: `ms=${ins.ms}, total=${ins.data && ins.data.total}`,
  410. });
  411. const insHist = await req("POST", "/api/batches/page", {
  412. pageNum: 1,
  413. pageSize: 10,
  414. batchLevel: "TOWN",
  415. townId: TOWN,
  416. statusIn: "PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED",
  417. lite: true,
  418. withTownTotals: true,
  419. });
  420. const histBatch =
  421. insHist.data && insHist.data.list && insHist.data.list.length
  422. ? insHist.data.list[0]
  423. : null;
  424. const histPeople = Number((histBatch && histBatch.peopleCount) || 0);
  425. const histAmt = Number((histBatch && histBatch.totalAmount) || 0);
  426. checks.push({
  427. name: "叶榭居保历史批人数/金额与明细一致",
  428. pass: histPeople > 1000 && histAmt > 1_000_000,
  429. detail: `peopleCount=${histPeople}, totalAmount=${histAmt}, ms=${insHist.ms}`,
  430. });
  431. const outDir = path.join(__dirname, "audit-output");
  432. if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
  433. const outFile = path.join(outDir, "api-audit-result.json");
  434. fs.writeFileSync(
  435. outFile,
  436. JSON.stringify({ at: new Date().toISOString(), results, checks }, null, 2),
  437. "utf8"
  438. );
  439. const slow = results.filter((r) => r.status === "SLOW" || r.status === "TIMEOUT");
  440. const fail = results.filter((r) => r.status === "BIZ_FAIL" || r.status === "ERR");
  441. console.log("\n=== Summary ===");
  442. console.log("total=", results.length, "ok=", results.filter((r) => r.status === "OK").length);
  443. console.log("slow=", slow.length, "fail=", fail.length);
  444. checks.forEach((c) =>
  445. console.log((c.pass ? "PASS" : "FAIL") + " | " + c.name + " | " + c.detail)
  446. );
  447. console.log("\nWrote", outFile);
  448. process.exit(fail.length + checks.filter((c) => !c.pass).length > 0 ? 1 : 0);
  449. })();