e2e-api-flow-test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * 按业务流程全量调用本地 Swagger 接口。
  3. * 用法: node scripts/e2e-api-flow-test.js
  4. */
  5. const http = require("http");
  6. const fs = require("fs");
  7. const path = require("path");
  8. const BASE = "http://localhost:8088/sjnmtybt";
  9. const results = [];
  10. function req(method, pathName, body, contentType) {
  11. return new Promise((resolve) => {
  12. const u = new URL(BASE + pathName);
  13. let data = null;
  14. const headers = { Accept: "application/json" };
  15. if (body != null) {
  16. if (Buffer.isBuffer(body)) {
  17. data = body;
  18. if (contentType) headers["Content-Type"] = contentType;
  19. } else if (typeof body === "string") {
  20. data = body;
  21. headers["Content-Type"] = contentType || "application/json";
  22. } else {
  23. data = JSON.stringify(body);
  24. headers["Content-Type"] = "application/json";
  25. }
  26. headers["Content-Length"] = Buffer.byteLength(data);
  27. }
  28. const r = http.request(
  29. { hostname: u.hostname, port: u.port, path: u.pathname + u.search, method, headers },
  30. (res) => {
  31. let raw = "";
  32. res.on("data", (c) => (raw += c));
  33. res.on("end", () => {
  34. let json = null;
  35. try {
  36. json = JSON.parse(raw);
  37. } catch (e) {
  38. json = { parseError: true, raw: raw.slice(0, 400) };
  39. }
  40. resolve({ status: res.statusCode, json, raw });
  41. });
  42. }
  43. );
  44. r.on("error", (e) => resolve({ status: 0, json: { error: e.message }, raw: "" }));
  45. if (data) r.write(data);
  46. r.end();
  47. });
  48. }
  49. function okBiz(resp) {
  50. if (resp.status !== 200) return false;
  51. if (!resp.json || resp.json.parseError || resp.json.error) return false;
  52. if (typeof resp.json.code === "number") return resp.json.code === 200;
  53. return true;
  54. }
  55. async function call(lane, name, method, pathName, body, opts) {
  56. opts = opts || {};
  57. const resp = await req(method, pathName, body, opts.contentType);
  58. const pass = opts.expectFail ? !okBiz(resp) : okBiz(resp);
  59. const bizCode = resp.json && resp.json.code;
  60. const msg = (resp.json && (resp.json.message || resp.json.error)) || "";
  61. const data = resp.json && resp.json.data;
  62. let hint = "";
  63. if (data == null) hint = "data=null";
  64. else if (Array.isArray(data)) hint = "arr=" + data.length;
  65. else if (data.list) hint = "total=" + data.total + ",list=" + data.list.length;
  66. else if (typeof data === "object") hint = "keys=" + Object.keys(data).slice(0, 10).join(",");
  67. else hint = String(data).slice(0, 60);
  68. const row = {
  69. lane,
  70. name,
  71. method,
  72. path: pathName,
  73. http: resp.status,
  74. biz: bizCode,
  75. pass: !!pass,
  76. msg: String(msg).slice(0, 120),
  77. hint,
  78. };
  79. results.push(row);
  80. console.log(`[${row.pass ? "OK " : "FAIL"}] ${lane} | ${method} ${pathName} | ${name} | ${hint} | ${row.msg}`);
  81. return resp;
  82. }
  83. async function uploadReturn(batchId) {
  84. // multipart without form-data dependency
  85. const boundary = "----SjBoundary" + Date.now();
  86. const fileContent = "N7R MOCK RETURN FILE\nP001 SUCCESS\nP002 FAIL 003\n";
  87. const body =
  88. `--${boundary}\r\n` +
  89. `Content-Disposition: form-data; name="batchId"\r\n\r\n${batchId}\r\n` +
  90. `--${boundary}\r\n` +
  91. `Content-Disposition: form-data; name="file"; filename="LSJ20260320.N7R"\r\n` +
  92. `Content-Type: application/octet-stream\r\n\r\n` +
  93. fileContent +
  94. `\r\n--${boundary}--\r\n`;
  95. return call(
  96. "RETURN",
  97. "上传回盘文件",
  98. "POST",
  99. "/api/return-disk/upload",
  100. Buffer.from(body, "utf8"),
  101. { contentType: "multipart/form-data; boundary=" + boundary }
  102. );
  103. }
  104. async function main() {
  105. console.log("=== E2E API FLOW TEST ===\n");
  106. // 0 META
  107. await call("META", "流程角色", "GET", "/api/workflow/roles");
  108. await call("META", "业务泳道", "GET", "/api/workflow/lanes");
  109. await call("META", "月度节点", "GET", "/api/workflow/stages");
  110. await call("META", "特殊业务节点", "GET", "/api/workflow/special-biz-nodes");
  111. await call("META", "看板汇总", "GET", "/api/dashboard/summary?payMonth=202603");
  112. await call("META", "看板汇总按镇", "GET", "/api/dashboard/summary?payMonth=202603&townId=T_YEXIE");
  113. await call(
  114. "META",
  115. "历史批次",
  116. "POST",
  117. "/api/dashboard/history-batches",
  118. { pageNum: 1, pageSize: 20, townId: "T_YEXIE", status: "RETURNED" }
  119. );
  120. // 1 MASTER
  121. await call("MASTER", "区-镇", "GET", "/api/regions?level=TOWN&parentId=D_SJ");
  122. await call("MASTER", "镇-村叶榭", "GET", "/api/regions?level=VILLAGE&parentId=T_YEXIE");
  123. await call("MASTER", "镇-村佘山", "GET", "/api/regions?level=VILLAGE&parentId=T_SHESHAN");
  124. await call("MASTER", "金额标准", "GET", "/api/amount-standards?townId=T_YEXIE&current=true");
  125. await call("MASTER", "当前月补", "GET", "/api/amount-standards/current?townId=T_YEXIE&standardType=MONTHLY");
  126. await call("MASTER", "当前丧葬", "GET", "/api/amount-standards/current?townId=T_YEXIE&standardType=FUNERAL");
  127. // 2 MONTHLY PASS
  128. console.log("\n--- MONTHLY PASS ---\n");
  129. await call("MONTHLY_PASS", "人员叶榭", "POST", "/api/personnel/page", {
  130. pageNum: 1,
  131. pageSize: 50,
  132. townId: "T_YEXIE",
  133. });
  134. await call("MONTHLY_PASS", "人员详情P001", "GET", "/api/personnel/P001");
  135. await call("MONTHLY_PASS", "人员新增", "POST", "/api/personnel", {
  136. name: "测试新增",
  137. idNumber: "310117199001011999",
  138. townId: "T_YEXIE",
  139. villageId: "V_YX_001",
  140. bankCardNumber: "6228480030009999999",
  141. monthlyStandard: 1280,
  142. source: "村级录入",
  143. enjoyStartMonth: "202605",
  144. });
  145. await call("MONTHLY_PASS", "批次叶榭", "POST", "/api/batches/page", {
  146. pageNum: 1,
  147. pageSize: 20,
  148. townId: "T_YEXIE",
  149. });
  150. await call("MONTHLY_PASS", "批次详情", "GET", "/api/batches/VB202604001");
  151. await call("MONTHLY_PASS", "流程总览待初审", "GET", "/api/workflow/batches/VB202604001/overview");
  152. const created = await call("MONTHLY_PASS", "创建村级批次", "POST", "/api/batches", {
  153. batchNo: "202605",
  154. batchLevel: "VILLAGE",
  155. payMonth: "202605",
  156. townId: "T_YEXIE",
  157. villageId: "V_YX_001",
  158. payDate: "2026-05-20",
  159. personnelIds: ["P001", "P002"],
  160. });
  161. const newBatchId = created.json && created.json.data && created.json.data.id;
  162. if (newBatchId) {
  163. await call("MONTHLY_PASS", "提交镇经发", "POST", `/api/batches/${newBatchId}/submit`);
  164. } else {
  165. results.push({
  166. lane: "MONTHLY_PASS",
  167. name: "提交镇经发",
  168. method: "SKIP",
  169. path: "/api/batches/{id}/submit",
  170. http: 0,
  171. biz: null,
  172. pass: false,
  173. msg: "无新批次ID",
  174. hint: "",
  175. });
  176. }
  177. await call("MONTHLY_PASS", "镇初审通过", "POST", "/api/batches/town-approve", {
  178. bizId: "VB202604001",
  179. action: "PASS",
  180. opinion: "材料齐全,同意",
  181. operator: "镇经发-王审",
  182. });
  183. await call("MONTHLY_PASS", "触发居保匹配", "POST", "/api/insurance-match/trigger", {
  184. batchId: "VB202604001",
  185. personnelIds: ["P001", "P003"],
  186. });
  187. await call("MONTHLY_PASS", "匹配结果列表", "GET", "/api/insurance-match/batches/VB202604001/results");
  188. await call("MONTHLY_PASS", "人工确认居保", "POST", "/api/insurance-match/manual-confirm", {
  189. personnelId: "P005",
  190. batchId: "VB202604001",
  191. pass: true,
  192. remark: "人工核实通过",
  193. });
  194. await call("MONTHLY_PASS", "生成月补贴清单", "POST", "/api/payments/batches/MB202604002/generate");
  195. await call("MONTHLY_PASS", "发放明细待审批", "POST", "/api/payments/details/page", {
  196. pageNum: 1,
  197. pageSize: 50,
  198. batchId: "MB202604002",
  199. });
  200. await call("MONTHLY_PASS", "领导审批通过", "POST", "/api/batches/leader-approve", {
  201. bizId: "MB202604002",
  202. action: "PASS",
  203. opinion: "同意发放",
  204. });
  205. await call("MONTHLY_PASS", "生成对照出盘", "POST", "/api/payments/export-disk", {
  206. batchId: "MB202604002",
  207. payBank: "农商",
  208. adjustOnly: false,
  209. });
  210. await call("MONTHLY_PASS", "流程分页", "POST", "/api/process-logs/page", {
  211. pageNum: 1,
  212. pageSize: 50,
  213. batchId: "VB202604001",
  214. });
  215. await call("MONTHLY_PASS", "流程时间线", "GET", "/api/process-logs/batches/VB202604001/timeline");
  216. await call("MONTHLY_PASS", "流程总览领导批", "GET", "/api/workflow/batches/MB202604002/overview");
  217. // 3 REJECT
  218. console.log("\n--- MONTHLY REJECT ---\n");
  219. await call("MONTHLY_REJECT", "镇初审驳回", "POST", "/api/batches/town-approve", {
  220. bizId: "VB202604001",
  221. action: "REJECT",
  222. opinion: "材料不齐",
  223. });
  224. await call("MONTHLY_REJECT", "领导审批驳回", "POST", "/api/batches/leader-approve", {
  225. bizId: "MB202604002",
  226. action: "REJECT",
  227. opinion: "金额有误",
  228. });
  229. // 4 SPECIAL
  230. console.log("\n--- SPECIAL ---\n");
  231. await call("SPECIAL", "人员状态暂停", "PUT", "/api/personnel/status", {
  232. personnelId: "P006",
  233. bizStatus: "PAUSED",
  234. payThisMonth: false,
  235. remarks: "暂停测试",
  236. });
  237. const sp = await call("SPECIAL", "创建丧葬特殊业务", "POST", "/api/special-biz", {
  238. personnelId: "P004",
  239. batchNo: "202603",
  240. bizType: "FUNERAL",
  241. amount: 6000,
  242. reason: "死亡丧葬补贴",
  243. heirName: "赵小明",
  244. heirIdNumber: "310117199001011111",
  245. heirBankCard: "6228480030005234567",
  246. });
  247. const spId = sp.json && sp.json.data && sp.json.data.id;
  248. await call("SPECIAL", "特殊业务分页", "GET", "/api/special-biz/page?bizType=FUNERAL&pageNum=1&pageSize=20");
  249. if (spId) {
  250. await call("SPECIAL", "特殊业务领导审批", "POST", "/api/special-biz/leader-approve", {
  251. bizId: spId,
  252. action: "PASS",
  253. opinion: "同意",
  254. });
  255. await call("SPECIAL", "特殊业务出盘", "POST", `/api/special-biz/${spId}/export-disk`);
  256. }
  257. await call("SPECIAL", "调标保存", "POST", "/api/amount-standards", {
  258. townId: "T_YEXIE",
  259. standardType: "MONTHLY",
  260. amount: 1380,
  261. effectiveTime: Date.now(),
  262. adjustReason: "2026调标测试",
  263. });
  264. await call("SPECIAL", "创建调标特殊业务", "POST", "/api/special-biz", {
  265. personnelId: "P001",
  266. bizType: "STANDARD_ADJUST",
  267. amount: 100,
  268. reason: "调标差额补发",
  269. });
  270. await call("SPECIAL", "告警分页", "POST", "/api/alerts/page", { pageNum: 1, pageSize: 50 });
  271. const alerts = await call("SPECIAL", "死亡告警", "POST", "/api/alerts/page", {
  272. pageNum: 1,
  273. pageSize: 20,
  274. type: "DEATH_STATUS",
  275. });
  276. const alertId =
  277. alerts.json && alerts.json.data && alerts.json.data.list && alerts.json.data.list[0] && alerts.json.data.list[0].id;
  278. if (alertId) {
  279. await call("SPECIAL", "告警详情", "GET", `/api/alerts/${alertId}`);
  280. await call("SPECIAL", "告警处置", "POST", "/api/alerts/handle", {
  281. alertId,
  282. handleStatus: "CLOSED",
  283. handleResult: "已停发并进入丧葬流程",
  284. handler: "业务员A",
  285. });
  286. }
  287. // 5 RETURN
  288. console.log("\n--- RETURN ---\n");
  289. await uploadReturn("MB202603001");
  290. await call("RETURN", "回盘确认", "POST", "/api/return-disk/confirm", {
  291. batchId: "MB202603001",
  292. fileName: "LSJ20260320.N7R",
  293. summaryNo: "170000061804",
  294. returnDate: "2026-03-22",
  295. });
  296. await call("RETURN", "回盘明细", "POST", "/api/return-disk/details/page", {
  297. pageNum: 1,
  298. pageSize: 50,
  299. batchId: "MB202603001",
  300. });
  301. await call("RETURN", "批次统计", "POST", "/api/batch-stats/page", {
  302. pageNum: 1,
  303. pageSize: 20,
  304. batchNo: "MB202603001",
  305. });
  306. await call("RETURN", "重算统计", "POST", "/api/batch-stats/recalculate/MB202603001");
  307. await call("RETURN", "发放明细已回盘", "POST", "/api/payments/details/page", {
  308. pageNum: 1,
  309. pageSize: 50,
  310. batchId: "MB202603001",
  311. });
  312. await call("RETURN", "流程总览已回盘", "GET", "/api/workflow/batches/MB202603001/overview");
  313. await call("RETURN", "归档批次", "POST", "/api/dashboard/archive/MB202603001");
  314. // 6 OTHER
  315. console.log("\n--- OTHER ---\n");
  316. await call("OTHER", "区划保存", "POST", "/api/regions", {
  317. code: "V_TEST",
  318. name: "测试村",
  319. level: "VILLAGE",
  320. parentId: "T_YEXIE",
  321. enabled: true,
  322. });
  323. await call("OTHER", "区划删除", "DELETE", "/api/regions/V_TEST");
  324. await call("OTHER", "人员删除", "DELETE", "/api/personnel/P_DRAFT_X");
  325. await call("OTHER", "人员按状态", "POST", "/api/personnel/page", {
  326. pageNum: 1,
  327. pageSize: 20,
  328. bizStatus: "NORMAL",
  329. });
  330. await call("OTHER", "人员按村", "POST", "/api/personnel/page", {
  331. pageNum: 1,
  332. pageSize: 20,
  333. villageId: "V_YX_001",
  334. });
  335. await call("OTHER", "批次按阶段", "POST", "/api/batches/page", {
  336. pageNum: 1,
  337. pageSize: 20,
  338. currentStage: "STAGE5_LEADER_ONLINE_APPROVE",
  339. });
  340. await call("OTHER", "不存在人员应失败", "GET", "/api/personnel/NOT_EXIST", null, { expectFail: true });
  341. // Excel import empty multipart-ish skip if hard; try minimal
  342. const b = "----B" + Date.now();
  343. const excelBody =
  344. `--${b}\r\nContent-Disposition: form-data; name="townId"\r\n\r\nT_YEXIE\r\n` +
  345. `--${b}\r\nContent-Disposition: form-data; name="villageId"\r\n\r\nV_YX_001\r\n` +
  346. `--${b}\r\nContent-Disposition: form-data; name="file"; filename="p.xlsx"\r\n` +
  347. `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n\r\n` +
  348. `fake\r\n--${b}--\r\n`;
  349. await call("OTHER", "人员Excel导入契约", "POST", "/api/personnel/import", Buffer.from(excelBody), {
  350. contentType: "multipart/form-data; boundary=" + b,
  351. });
  352. const fail = results.filter((r) => !r.pass);
  353. const pass = results.filter((r) => r.pass);
  354. console.log("\n=== SUMMARY ===");
  355. console.log("total=", results.length, "pass=", pass.length, "fail=", fail.length);
  356. if (fail.length) {
  357. console.log("\nFAILED:");
  358. fail.forEach((f) =>
  359. console.log("-", f.lane, f.method, f.path, "|", f.name, "| http=", f.http, "biz=", f.biz, "|", f.msg, "|", f.hint)
  360. );
  361. }
  362. const out = path.join(__dirname, "e2e-api-flow-result.json");
  363. fs.writeFileSync(out, JSON.stringify({ summary: { total: results.length, pass: pass.length, fail: fail.length }, results }, null, 2));
  364. console.log("\nWrote", out);
  365. process.exit(fail.length ? 2 : 0);
  366. }
  367. main().catch((e) => {
  368. console.error(e);
  369. process.exit(1);
  370. });