/** * 前端各页面只读接口耗时巡检(跳过初始化/清空/写操作) * 用法: node scripts/frontend-audit-apis.js */ const http = require("http"); const fs = require("fs"); const path = require("path"); const BASE = { hostname: "localhost", port: 8088, prefix: "/sjnmtybt" }; const PAY_MONTH = "202608"; const TOWN = "T_YEXIE"; const VILLAGE = "V_YX_001"; const TIMEOUT_WARN = 3000; const TIMEOUT_FAIL = 15000; function req(method, apiPath, body, timeoutMs) { const t0 = Date.now(); return new Promise((resolve) => { const data = body == null ? null : JSON.stringify(body); const headers = { Accept: "application/json" }; if (data) { headers["Content-Type"] = "application/json"; headers["Content-Length"] = Buffer.byteLength(data); } const timer = setTimeout(() => { resolve({ ms: Date.now() - t0, code: 0, error: "TIMEOUT", data: null }); r.destroy(); }, timeoutMs || TIMEOUT_FAIL); const r = http.request( { hostname: BASE.hostname, port: BASE.port, path: BASE.prefix + apiPath, method, headers, }, (res) => { let raw = ""; res.on("data", (c) => (raw += c)); res.on("end", () => { clearTimeout(timer); const ms = Date.now() - t0; let json = null; try { json = JSON.parse(raw); } catch (_) { json = { parseError: true }; } resolve({ ms, http: res.statusCode, code: json && json.code != null ? json.code : res.statusCode, data: json && json.data, message: json && (json.message || json.msg), error: null, }); }); } ); r.on("error", (e) => { clearTimeout(timer); resolve({ ms: Date.now() - t0, code: 0, error: e.message, data: null }); }); if (data) r.write(data); r.end(); }); } /** 页面 → 只读接口用例 */ const PAGE_CASES = [ { page: "工作台 /dashboard", role: "all", cases: [ ["GET", `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`, null, 90000], ["GET", `/api/dashboard/summary?payMonth=${PAY_MONTH}&townId=${TOWN}`, null], ["GET", `/api/dashboard/region-stats?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`, null], ["GET", "/api/workflow/roles", null], ["GET", "/api/workflow/stages", null], ], }, { page: "人员录入 /base", role: "village,admin", cases: [ ["GET", "/api/regions?level=TOWN&parentId=D_SJ", null], ["GET", `/api/regions?level=VILLAGE&parentId=${TOWN}`, null], [ "POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, townId: TOWN, villageId: VILLAGE }, ], ["GET", `/api/amount-standards/current?townId=${TOWN}&standardType=MONTHLY`, null], ], }, { page: "居保匹配 /insurance", role: "social,admin", cases: [ [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 10, batchLevel: "TOWN", townId: TOWN, statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN", lite: true, }, ], [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 10, batchLevel: "DISTRICT", statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN", lite: true, }, ], ], }, { page: "审批管理 /approvals", role: "town,leader,admin", cases: [ [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, townId: TOWN, batchLevel: "TOWN", lite: true }, ], [ "POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, townId: TOWN, bizStatus: "PENDING_TOWN_APPROVE" }, ], ], }, { page: "补助标准 /status", role: "social,admin", cases: [ ["GET", `/api/amount-standards?townId=${TOWN}¤t=true`, null], ["GET", `/api/amount-standards/current?townId=${TOWN}&standardType=FUNERAL`, null], ], }, { page: "特殊业务 /special", role: "social,admin", cases: [ ["GET", "/api/special-biz/page?bizType=FUNERAL&pageNum=1&pageSize=20", null], ["GET", "/api/special-biz/page?bizType=ADJUST&pageNum=1&pageSize=20", null], [`GET`, `/api/special-biz/refund-stats?townId=${TOWN}&payMonth=${PAY_MONTH}`, null], ], }, { page: "月补贴清单 /monthly", role: "social,admin", cases: [ [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, townId: TOWN, batchLevel: "TOWN", payMonth: PAY_MONTH, lite: true, withTownTotals: true, }, ], [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, batchLevel: "TOWN", statusIn: "PENDING_STATUS_MAINTAIN,PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED", lite: true, withTownTotals: false, }, ], [ "POST", "/api/batches/unresolved-insurance-counts", { batchIds: ["TB_T_YEXIE_202608"] }, ], [ "POST", "/api/payments/details/page", { pageNum: 1, pageSize: 20, townId: TOWN, payMonth: PAY_MONTH }, ], ], }, { page: "回盘确认 /disk", role: "social,admin", cases: [ [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, townId: TOWN, statusIn: "EXPORTED,RETURNED", lite: true, }, ], [ "POST", "/api/return-disk/details/page", { pageNum: 1, pageSize: 20, townId: TOWN }, ], ], }, { page: "监控告警 /alerts", role: "all", cases: [ ["POST", "/api/alerts/page", { pageNum: 1, pageSize: 20, townId: TOWN }], ["POST", "/api/alerts/page", { pageNum: 1, pageSize: 20, level: "HIGH" }], ], }, { page: "数据报表 /reports", role: "all", cases: [ ["GET", `/api/reports/yoy?payMonth=${PAY_MONTH}`, null], ["GET", `/api/reports/topics/personnel?payMonth=${PAY_MONTH}&townId=${TOWN}`, null], ["GET", `/api/reports/topics/payment?payMonth=${PAY_MONTH}&townId=${TOWN}`, null], [ "POST", "/api/batch-stats/page", { pageNum: 1, pageSize: 20, townId: TOWN, payMonth: PAY_MONTH }, ], ], }, { page: "区划管理 /regions", role: "admin", cases: [ ["GET", "/api/regions?level=TOWN&parentId=D_SJ", null], ["GET", "/api/regions?level=VILLAGE", null], ], }, { page: "人员底数 /personnel-admin", role: "admin", cases: [ ["POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, townId: TOWN }], ["POST", "/api/personnel/page", { pageNum: 1, pageSize: 20 }], ], }, { page: "批次管理 /admin-batches", role: "admin", cases: [ [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, payMonth: PAY_MONTH, lite: true, withTownTotals: false }, ], [ "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, payMonth: PAY_MONTH, lite: true, withTownTotals: true }, ], ["GET", `/api/workflow/batches/DB${PAY_MONTH}/overview`, null], ], }, { page: "系统管理 /system", role: "admin", cases: [ ["GET", "/api/workflow/roles", null], ["GET", "/api/workflow/lanes", null], ["GET", "/api/workflow/special-biz-nodes", null], ["GET", "/api/admin/validate-rules", null], ], }, ]; function hintOf(data) { if (data == null) return "null"; if (Array.isArray(data)) return "arr=" + data.length; if (data.list) return "total=" + data.total + ",list=" + (data.list.length || 0); if (typeof data === "object") { const keys = Object.keys(data).slice(0, 8); const extra = []; if (data.monthAmount != null) extra.push("monthAmount=" + data.monthAmount); if (data.personnelCount != null) extra.push("personnel=" + data.personnelCount); if (data.currentMonth) { extra.push( "curAmt=" + (data.currentMonth.totalAmount || 0) + ",curPeople=" + (data.currentMonth.peopleCount || 0) ); } return extra.length ? extra.join(";") : "keys=" + keys.join(","); } return String(data).slice(0, 60); } function statusOf(r) { if (r.error === "TIMEOUT") return "TIMEOUT"; if (r.error) return "ERR"; if (r.code !== 200) return "BIZ_FAIL"; if (r.ms > TIMEOUT_FAIL) return "TIMEOUT"; if (r.ms > TIMEOUT_WARN) return "SLOW"; return "OK"; } (async () => { console.log("=== Frontend API Audit (read-only) ===\n"); const results = []; const seen = new Set(); for (const group of PAGE_CASES) { for (const row of group.cases) { const method = row[0]; const apiPath = row[1]; const body = row[2]; const timeoutMs = row[3]; const key = method + " " + apiPath + JSON.stringify(body || {}); if (seen.has(key)) continue; seen.add(key); const r = await req(method, apiPath, body, timeoutMs); const st = statusOf(r); results.push({ page: group.page, role: group.role, method, path: apiPath.split("?")[0], ms: r.ms, code: r.code, status: st, hint: hintOf(r.data), message: r.message || r.error || "", }); console.log( (st === "OK" ? "OK " : st.padEnd(5)) + " | " + String(r.ms).padStart(5) + "ms | " + group.page.split(" ")[0] + " | " + method + " " + apiPath.split("?")[0] ); } } // 数据一致性抽检 const checks = []; checks.push({ name: "叶榭工作台本月金额=村图表合计", pass: false, detail: "pending", }); const wbYexie = await req( "GET", `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=${TOWN}&months=6`, null, 120000 ); const yx = wbYexie.data || {}; const yxAmt = Number(yx.monthAmount || 0); const yxVillageSum = (yx.villageCharts || []).reduce( (s, v) => s + Number(v.amount || 0), 0 ); const yxAmtOk = yxAmt > 0 && Math.abs(yxAmt - yxVillageSum) < 1; checks[checks.length - 1] = { name: "叶榭工作台本月金额=村图表合计", pass: yxAmtOk, detail: `monthAmount=${yxAmt}, villageSum=${yxVillageSum}, ms=${wbYexie.ms}`, }; checks.push({ name: "叶榭本月新增不应≈总人数", pass: Number(yx.monthIncreaseCount || 0) < Number(yx.monthPeopleCount || 0) * 0.5, detail: `monthIncrease=${yx.monthIncreaseCount}, monthPeople=${yx.monthPeopleCount}`, }); const wb = await req( "GET", `/api/dashboard/workbench?payMonth=${PAY_MONTH}&townId=T_MAOGANG&months=6`, null ); const ma = wb.data || {}; const monthAmt = Number(ma.monthAmount || 0); const villageSum = (ma.villageCharts || []).reduce( (s, v) => s + Number(v.amount || 0), 0 ); const amtOk = monthAmt > 0 && Math.abs(monthAmt - villageSum) < 1; checks.push({ name: "泖港工作台本月金额=村图表合计", pass: amtOk, detail: `monthAmount=${monthAmt}, villageSum=${villageSum}`, }); const ins = await req("POST", "/api/batches/page", { pageNum: 1, pageSize: 10, batchLevel: "TOWN", townId: TOWN, statusIn: "PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN", lite: true, }); checks.push({ name: "居保列表 lite 查询 < 3s", pass: ins.ms < TIMEOUT_WARN && ins.code === 200, detail: `ms=${ins.ms}, total=${ins.data && ins.data.total}`, }); const insHist = await req("POST", "/api/batches/page", { pageNum: 1, pageSize: 10, batchLevel: "TOWN", townId: TOWN, statusIn: "PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED", lite: true, withTownTotals: true, }); const histBatch = insHist.data && insHist.data.list && insHist.data.list.length ? insHist.data.list[0] : null; const histPeople = Number((histBatch && histBatch.peopleCount) || 0); const histAmt = Number((histBatch && histBatch.totalAmount) || 0); checks.push({ name: "叶榭居保历史批人数/金额与明细一致", pass: histPeople > 1000 && histAmt > 1_000_000, detail: `peopleCount=${histPeople}, totalAmount=${histAmt}, ms=${insHist.ms}`, }); const outDir = path.join(__dirname, "audit-output"); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); const outFile = path.join(outDir, "api-audit-result.json"); fs.writeFileSync( outFile, JSON.stringify({ at: new Date().toISOString(), results, checks }, null, 2), "utf8" ); const slow = results.filter((r) => r.status === "SLOW" || r.status === "TIMEOUT"); const fail = results.filter((r) => r.status === "BIZ_FAIL" || r.status === "ERR"); console.log("\n=== Summary ==="); console.log("total=", results.length, "ok=", results.filter((r) => r.status === "OK").length); console.log("slow=", slow.length, "fail=", fail.length); checks.forEach((c) => console.log((c.pass ? "PASS" : "FAIL") + " | " + c.name + " | " + c.detail) ); console.log("\nWrote", outFile); process.exit(fail.length + checks.filter((c) => !c.pass).length > 0 ? 1 : 0); })();