/** * 按业务流程全量调用本地 Swagger 接口。 * 用法: node scripts/e2e-api-flow-test.js */ const http = require("http"); const fs = require("fs"); const path = require("path"); const BASE = "http://localhost:8088/sjnmtybt"; const results = []; function req(method, pathName, body, contentType) { return new Promise((resolve) => { const u = new URL(BASE + pathName); let data = null; const headers = { Accept: "application/json" }; if (body != null) { if (Buffer.isBuffer(body)) { data = body; if (contentType) headers["Content-Type"] = contentType; } else if (typeof body === "string") { data = body; headers["Content-Type"] = contentType || "application/json"; } else { data = JSON.stringify(body); headers["Content-Type"] = "application/json"; } headers["Content-Length"] = Buffer.byteLength(data); } const r = http.request( { hostname: u.hostname, port: u.port, path: u.pathname + u.search, method, headers }, (res) => { let raw = ""; res.on("data", (c) => (raw += c)); res.on("end", () => { let json = null; try { json = JSON.parse(raw); } catch (e) { json = { parseError: true, raw: raw.slice(0, 400) }; } resolve({ status: res.statusCode, json, raw }); }); } ); r.on("error", (e) => resolve({ status: 0, json: { error: e.message }, raw: "" })); if (data) r.write(data); r.end(); }); } function okBiz(resp) { if (resp.status !== 200) return false; if (!resp.json || resp.json.parseError || resp.json.error) return false; if (typeof resp.json.code === "number") return resp.json.code === 200; return true; } async function call(lane, name, method, pathName, body, opts) { opts = opts || {}; const resp = await req(method, pathName, body, opts.contentType); const pass = opts.expectFail ? !okBiz(resp) : okBiz(resp); const bizCode = resp.json && resp.json.code; const msg = (resp.json && (resp.json.message || resp.json.error)) || ""; const data = resp.json && resp.json.data; let hint = ""; if (data == null) hint = "data=null"; else if (Array.isArray(data)) hint = "arr=" + data.length; else if (data.list) hint = "total=" + data.total + ",list=" + data.list.length; else if (typeof data === "object") hint = "keys=" + Object.keys(data).slice(0, 10).join(","); else hint = String(data).slice(0, 60); const row = { lane, name, method, path: pathName, http: resp.status, biz: bizCode, pass: !!pass, msg: String(msg).slice(0, 120), hint, }; results.push(row); console.log(`[${row.pass ? "OK " : "FAIL"}] ${lane} | ${method} ${pathName} | ${name} | ${hint} | ${row.msg}`); return resp; } async function uploadReturn(batchId) { // multipart without form-data dependency const boundary = "----SjBoundary" + Date.now(); const fileContent = "N7R MOCK RETURN FILE\nP001 SUCCESS\nP002 FAIL 003\n"; const body = `--${boundary}\r\n` + `Content-Disposition: form-data; name="batchId"\r\n\r\n${batchId}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="LSJ20260320.N7R"\r\n` + `Content-Type: application/octet-stream\r\n\r\n` + fileContent + `\r\n--${boundary}--\r\n`; return call( "RETURN", "上传回盘文件", "POST", "/api/return-disk/upload", Buffer.from(body, "utf8"), { contentType: "multipart/form-data; boundary=" + boundary } ); } async function main() { console.log("=== E2E API FLOW TEST ===\n"); // 0 META await call("META", "流程角色", "GET", "/api/workflow/roles"); await call("META", "业务泳道", "GET", "/api/workflow/lanes"); await call("META", "月度节点", "GET", "/api/workflow/stages"); await call("META", "特殊业务节点", "GET", "/api/workflow/special-biz-nodes"); await call("META", "看板汇总", "GET", "/api/dashboard/summary?payMonth=202603"); await call("META", "看板汇总按镇", "GET", "/api/dashboard/summary?payMonth=202603&townId=T_YEXIE"); await call( "META", "历史批次", "POST", "/api/dashboard/history-batches", { pageNum: 1, pageSize: 20, townId: "T_YEXIE", status: "RETURNED" } ); // 1 MASTER await call("MASTER", "区-镇", "GET", "/api/regions?level=TOWN&parentId=D_SJ"); await call("MASTER", "镇-村叶榭", "GET", "/api/regions?level=VILLAGE&parentId=T_YEXIE"); await call("MASTER", "镇-村佘山", "GET", "/api/regions?level=VILLAGE&parentId=T_SHESHAN"); await call("MASTER", "金额标准", "GET", "/api/amount-standards?townId=T_YEXIE¤t=true"); await call("MASTER", "当前月补", "GET", "/api/amount-standards/current?townId=T_YEXIE&standardType=MONTHLY"); await call("MASTER", "当前丧葬", "GET", "/api/amount-standards/current?townId=T_YEXIE&standardType=FUNERAL"); // 2 MONTHLY PASS console.log("\n--- MONTHLY PASS ---\n"); await call("MONTHLY_PASS", "人员叶榭", "POST", "/api/personnel/page", { pageNum: 1, pageSize: 50, townId: "T_YEXIE", }); await call("MONTHLY_PASS", "人员详情P001", "GET", "/api/personnel/P001"); await call("MONTHLY_PASS", "人员新增", "POST", "/api/personnel", { name: "测试新增", idNumber: "310117199001011999", townId: "T_YEXIE", villageId: "V_YX_001", bankCardNumber: "6228480030009999999", monthlyStandard: 1280, source: "村级录入", enjoyStartMonth: "202605", }); await call("MONTHLY_PASS", "批次叶榭", "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, townId: "T_YEXIE", }); await call("MONTHLY_PASS", "批次详情", "GET", "/api/batches/VB202604001"); await call("MONTHLY_PASS", "流程总览待初审", "GET", "/api/workflow/batches/VB202604001/overview"); const created = await call("MONTHLY_PASS", "创建村级批次", "POST", "/api/batches", { batchNo: "202605", batchLevel: "VILLAGE", payMonth: "202605", townId: "T_YEXIE", villageId: "V_YX_001", payDate: "2026-05-20", personnelIds: ["P001", "P002"], }); const newBatchId = created.json && created.json.data && created.json.data.id; if (newBatchId) { await call("MONTHLY_PASS", "提交镇经发", "POST", `/api/batches/${newBatchId}/submit`); } else { results.push({ lane: "MONTHLY_PASS", name: "提交镇经发", method: "SKIP", path: "/api/batches/{id}/submit", http: 0, biz: null, pass: false, msg: "无新批次ID", hint: "", }); } await call("MONTHLY_PASS", "镇初审通过", "POST", "/api/batches/town-approve", { bizId: "VB202604001", action: "PASS", opinion: "材料齐全,同意", operator: "镇经发-王审", }); await call("MONTHLY_PASS", "触发居保匹配", "POST", "/api/insurance-match/trigger", { batchId: "VB202604001", personnelIds: ["P001", "P003"], }); await call("MONTHLY_PASS", "匹配结果列表", "GET", "/api/insurance-match/batches/VB202604001/results"); await call("MONTHLY_PASS", "人工确认居保", "POST", "/api/insurance-match/manual-confirm", { personnelId: "P005", batchId: "VB202604001", pass: true, remark: "人工核实通过", }); await call("MONTHLY_PASS", "生成月补贴清单", "POST", "/api/payments/batches/MB202604002/generate"); await call("MONTHLY_PASS", "发放明细待审批", "POST", "/api/payments/details/page", { pageNum: 1, pageSize: 50, batchId: "MB202604002", }); await call("MONTHLY_PASS", "领导审批通过", "POST", "/api/batches/leader-approve", { bizId: "MB202604002", action: "PASS", opinion: "同意发放", }); await call("MONTHLY_PASS", "生成对照出盘", "POST", "/api/payments/export-disk", { batchId: "MB202604002", payBank: "农商", adjustOnly: false, }); await call("MONTHLY_PASS", "流程分页", "POST", "/api/process-logs/page", { pageNum: 1, pageSize: 50, batchId: "VB202604001", }); await call("MONTHLY_PASS", "流程时间线", "GET", "/api/process-logs/batches/VB202604001/timeline"); await call("MONTHLY_PASS", "流程总览领导批", "GET", "/api/workflow/batches/MB202604002/overview"); // 3 REJECT console.log("\n--- MONTHLY REJECT ---\n"); await call("MONTHLY_REJECT", "镇初审驳回", "POST", "/api/batches/town-approve", { bizId: "VB202604001", action: "REJECT", opinion: "材料不齐", }); await call("MONTHLY_REJECT", "领导审批驳回", "POST", "/api/batches/leader-approve", { bizId: "MB202604002", action: "REJECT", opinion: "金额有误", }); // 4 SPECIAL console.log("\n--- SPECIAL ---\n"); await call("SPECIAL", "人员状态暂停", "PUT", "/api/personnel/status", { personnelId: "P006", bizStatus: "PAUSED", payThisMonth: false, remarks: "暂停测试", }); const sp = await call("SPECIAL", "创建丧葬特殊业务", "POST", "/api/special-biz", { personnelId: "P004", batchNo: "202603", bizType: "FUNERAL", amount: 6000, reason: "死亡丧葬补贴", heirName: "赵小明", heirIdNumber: "310117199001011111", heirBankCard: "6228480030005234567", }); const spId = sp.json && sp.json.data && sp.json.data.id; await call("SPECIAL", "特殊业务分页", "GET", "/api/special-biz/page?bizType=FUNERAL&pageNum=1&pageSize=20"); if (spId) { await call("SPECIAL", "特殊业务领导审批", "POST", "/api/special-biz/leader-approve", { bizId: spId, action: "PASS", opinion: "同意", }); await call("SPECIAL", "特殊业务出盘", "POST", `/api/special-biz/${spId}/export-disk`); } await call("SPECIAL", "调标保存", "POST", "/api/amount-standards", { townId: "T_YEXIE", standardType: "MONTHLY", amount: 1380, effectiveTime: Date.now(), adjustReason: "2026调标测试", }); await call("SPECIAL", "创建调标特殊业务", "POST", "/api/special-biz", { personnelId: "P001", bizType: "STANDARD_ADJUST", amount: 100, reason: "调标差额补发", }); await call("SPECIAL", "告警分页", "POST", "/api/alerts/page", { pageNum: 1, pageSize: 50 }); const alerts = await call("SPECIAL", "死亡告警", "POST", "/api/alerts/page", { pageNum: 1, pageSize: 20, type: "DEATH_STATUS", }); const alertId = alerts.json && alerts.json.data && alerts.json.data.list && alerts.json.data.list[0] && alerts.json.data.list[0].id; if (alertId) { await call("SPECIAL", "告警详情", "GET", `/api/alerts/${alertId}`); await call("SPECIAL", "告警处置", "POST", "/api/alerts/handle", { alertId, handleStatus: "CLOSED", handleResult: "已停发并进入丧葬流程", handler: "业务员A", }); } // 5 RETURN console.log("\n--- RETURN ---\n"); await uploadReturn("MB202603001"); await call("RETURN", "回盘确认", "POST", "/api/return-disk/confirm", { batchId: "MB202603001", fileName: "LSJ20260320.N7R", summaryNo: "170000061804", returnDate: "2026-03-22", }); await call("RETURN", "回盘明细", "POST", "/api/return-disk/details/page", { pageNum: 1, pageSize: 50, batchId: "MB202603001", }); await call("RETURN", "批次统计", "POST", "/api/batch-stats/page", { pageNum: 1, pageSize: 20, batchNo: "MB202603001", }); await call("RETURN", "重算统计", "POST", "/api/batch-stats/recalculate/MB202603001"); await call("RETURN", "发放明细已回盘", "POST", "/api/payments/details/page", { pageNum: 1, pageSize: 50, batchId: "MB202603001", }); await call("RETURN", "流程总览已回盘", "GET", "/api/workflow/batches/MB202603001/overview"); await call("RETURN", "归档批次", "POST", "/api/dashboard/archive/MB202603001"); // 6 OTHER console.log("\n--- OTHER ---\n"); await call("OTHER", "区划保存", "POST", "/api/regions", { code: "V_TEST", name: "测试村", level: "VILLAGE", parentId: "T_YEXIE", enabled: true, }); await call("OTHER", "区划删除", "DELETE", "/api/regions/V_TEST"); await call("OTHER", "人员删除", "DELETE", "/api/personnel/P_DRAFT_X"); await call("OTHER", "人员按状态", "POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, bizStatus: "NORMAL", }); await call("OTHER", "人员按村", "POST", "/api/personnel/page", { pageNum: 1, pageSize: 20, villageId: "V_YX_001", }); await call("OTHER", "批次按阶段", "POST", "/api/batches/page", { pageNum: 1, pageSize: 20, currentStage: "STAGE5_LEADER_ONLINE_APPROVE", }); await call("OTHER", "不存在人员应失败", "GET", "/api/personnel/NOT_EXIST", null, { expectFail: true }); // Excel import empty multipart-ish skip if hard; try minimal const b = "----B" + Date.now(); const excelBody = `--${b}\r\nContent-Disposition: form-data; name="townId"\r\n\r\nT_YEXIE\r\n` + `--${b}\r\nContent-Disposition: form-data; name="villageId"\r\n\r\nV_YX_001\r\n` + `--${b}\r\nContent-Disposition: form-data; name="file"; filename="p.xlsx"\r\n` + `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n\r\n` + `fake\r\n--${b}--\r\n`; await call("OTHER", "人员Excel导入契约", "POST", "/api/personnel/import", Buffer.from(excelBody), { contentType: "multipart/form-data; boundary=" + b, }); const fail = results.filter((r) => !r.pass); const pass = results.filter((r) => r.pass); console.log("\n=== SUMMARY ==="); console.log("total=", results.length, "pass=", pass.length, "fail=", fail.length); if (fail.length) { console.log("\nFAILED:"); fail.forEach((f) => console.log("-", f.lane, f.method, f.path, "|", f.name, "| http=", f.http, "biz=", f.biz, "|", f.msg, "|", f.hint) ); } const out = path.join(__dirname, "e2e-api-flow-result.json"); fs.writeFileSync(out, JSON.stringify({ summary: { total: results.length, pass: pass.length, fail: fail.length }, results }, null, 2)); console.log("\nWrote", out); process.exit(fail.length ? 2 : 0); } main().catch((e) => { console.error(e); process.exit(1); });