e2e-lifecycle-full.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /**
  2. * 全生命周期联调:月度补助 + 特殊业务 + 回盘/待补发(含异常断言)
  3. * 用法:node e2e-lifecycle-full.js
  4. * 每次使用独立发放月,避免与历史批次冲突。
  5. */
  6. const http = require("http");
  7. const asserts = [];
  8. function check(name, cond, detail) {
  9. asserts.push({ name, pass: !!cond, detail: detail || "" });
  10. console.log((cond ? "PASS" : "FAIL") + " | " + name + (detail ? " | " + detail : ""));
  11. }
  12. function req(method, path, body) {
  13. return new Promise((resolve) => {
  14. const data = body == null ? null : JSON.stringify(body);
  15. const headers = { Accept: "application/json" };
  16. if (data) {
  17. headers["Content-Type"] = "application/json";
  18. headers["Content-Length"] = Buffer.byteLength(data);
  19. }
  20. const r = http.request(
  21. { hostname: "localhost", port: 8088, path: "/sjnmtybt" + path, method, headers },
  22. (res) => {
  23. let raw = "";
  24. res.on("data", (c) => (raw += c));
  25. res.on("end", () => {
  26. try {
  27. resolve(JSON.parse(raw));
  28. } catch (e) {
  29. resolve({ code: res.statusCode, parseError: true, raw: String(raw).slice(0, 200) });
  30. }
  31. });
  32. }
  33. );
  34. r.on("error", (e) => resolve({ code: 0, error: e.message }));
  35. if (data) r.write(data);
  36. r.end();
  37. });
  38. }
  39. /** 生成独立 YYYYMM,降低与存量批次冲突概率 */
  40. function uniquePayMonth(offsetMonths) {
  41. const d = new Date();
  42. d.setMonth(d.getMonth() + (offsetMonths || 0));
  43. // 再叠毫秒尾数进「虚构年」段,避免同月重复跑
  44. const base = 2100 + (Date.now() % 50);
  45. const m = String(((d.getMonth() + Math.abs(offsetMonths || 0)) % 12) + 1).padStart(2, "0");
  46. return String(base) + m;
  47. }
  48. async function purgeVillageMonth(townId, villageId, payMonth) {
  49. const page = await req("POST", "/api/batches/page", {
  50. pageNum: 1,
  51. pageSize: 50,
  52. townId,
  53. villageId,
  54. payMonth,
  55. batchLevel: "VILLAGE",
  56. });
  57. const list = page.data?.list || [];
  58. for (const b of list) {
  59. if (b?.id && b.status !== "ARCHIVED") {
  60. await req("POST", "/api/batches/" + encodeURIComponent(b.id) + "/purge");
  61. }
  62. }
  63. }
  64. const townId = "T_YEXIE";
  65. const villageId = "V_YX_001";
  66. const villageId2 = "V_YX_002";
  67. const payMonth = uniquePayMonth(0);
  68. const payMonthFail = uniquePayMonth(1);
  69. (async () => {
  70. console.log("联调发放月 payMonth=" + payMonth + " payMonthFail=" + payMonthFail);
  71. await purgeVillageMonth(townId, villageId, payMonth);
  72. await purgeVillageMonth(townId, villageId2, payMonthFail);
  73. console.log("\n=== 0 前置:人员 ===");
  74. const idNo = "31011719900101" + String(Date.now()).slice(-4);
  75. const saved = await req("POST", "/api/personnel", {
  76. name: "生命周期测员",
  77. idNumber: idNo,
  78. townId,
  79. villageId,
  80. bankCardNumber: "6228480030009999888",
  81. monthlyStandard: 1280,
  82. enjoyStartMonth: "202601",
  83. source: "联调",
  84. });
  85. check("新建人员", saved.code === 200 && !!saved.data?.id, saved.message || saved.data?.id);
  86. const pid = saved.data?.id;
  87. if (!pid) {
  88. console.log("无人员,中止");
  89. process.exit(2);
  90. }
  91. console.log("\n=== 1 月度补助全生命周期 ===");
  92. const batch = await req("POST", "/api/batches", {
  93. batchNo: payMonth,
  94. batchLevel: "VILLAGE",
  95. payMonth,
  96. townId,
  97. villageId,
  98. payDate: "2026-08-20",
  99. personnelIds: [pid],
  100. });
  101. check("建村批", batch.code === 200 && !!batch.data?.id, batch.message || batch.data?.id);
  102. const bid = batch.data?.id;
  103. if (!bid) {
  104. console.log("无批次,中止月度段");
  105. }
  106. const badDup = await req("POST", "/api/batches", {
  107. batchNo: payMonth,
  108. batchLevel: "VILLAGE",
  109. payMonth,
  110. townId,
  111. villageId,
  112. personnelIds: [pid],
  113. });
  114. check(
  115. "同村同月重复建批应失败或复用",
  116. badDup.code !== 200 || badDup.data?.id === bid,
  117. badDup.code + "/" + (badDup.data?.id || badDup.message)
  118. );
  119. if (bid) {
  120. // 草稿须先提交初审,才能镇审(门禁)
  121. const submit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit");
  122. check(
  123. "提交初审",
  124. submit.code === 200 && submit.data?.status === "PENDING_TOWN_APPROVE",
  125. submit.message + " " + submit.data?.status
  126. );
  127. const townPass = await req("POST", "/api/batches/town-approve", {
  128. bizId: bid,
  129. action: "PASS",
  130. opinion: "材料齐全",
  131. });
  132. check("镇初审通过", townPass.code === 200, townPass.message);
  133. let b = await req("GET", "/api/batches/" + encodeURIComponent(bid));
  134. check("初审后进入居保", b.data?.currentStage === "STAGE3_INSURANCE_MATCH", b.data?.currentStage);
  135. const match = await req("POST", "/api/insurance-match/trigger", { batchId: bid });
  136. check("居保匹配", match.code === 200, match.message);
  137. await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  138. const leaderReject = await req("POST", "/api/batches/leader-approve", {
  139. bizId: bid,
  140. action: "REJECT",
  141. opinion: "先驳回测",
  142. });
  143. check("领导可驳回", leaderReject.code === 200, leaderReject.message);
  144. b = await req("GET", "/api/batches/" + encodeURIComponent(bid));
  145. check("驳回后状态 REJECTED", b.data?.status === "REJECTED", b.data?.status);
  146. // 领导驳回后回到清单阶段,再 submit-leader 后复审
  147. await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  148. const leaderPass = await req("POST", "/api/batches/leader-approve", {
  149. bizId: bid,
  150. action: "PASS",
  151. opinion: "同意出盘",
  152. });
  153. check("领导再通过", leaderPass.code === 200, leaderPass.message);
  154. b = await req("GET", "/api/batches/" + encodeURIComponent(bid));
  155. check(
  156. "可出盘或待领导后通过",
  157. ["READY_EXPORT", "PENDING_LEADER_APPROVE", "EXPORTED"].includes(b.data?.status),
  158. b.data?.status
  159. );
  160. if (b.data?.status === "PENDING_LEADER_APPROVE") {
  161. await req("POST", "/api/batches/leader-approve", { bizId: bid, action: "PASS", opinion: "同意" });
  162. b = await req("GET", "/api/batches/" + encodeURIComponent(bid));
  163. }
  164. const exp = await req("POST", "/api/payments/export-disk", {
  165. batchId: bid,
  166. bypassDeadline: true,
  167. operator: "业务",
  168. });
  169. check("出盘", exp.code === 200, exp.message);
  170. let ret2 = await req("POST", "/api/return-disk/confirm", {
  171. batchId: bid,
  172. manualAllSuccess: false,
  173. returnDate: "2026-08-25",
  174. operator: "业务",
  175. lines: [{ personnelId: pid, success: true }],
  176. });
  177. if (ret2.code !== 200) {
  178. ret2 = await req("POST", "/api/return-disk/confirm", {
  179. batchId: bid,
  180. manualAllSuccess: true,
  181. returnDate: "2026-08-25",
  182. operator: "业务",
  183. });
  184. }
  185. check("回盘确认", ret2.code === 200, ret2.message);
  186. const reConfirm = await req("POST", "/api/return-disk/confirm", {
  187. batchId: bid,
  188. manualAllSuccess: true,
  189. returnDate: "2026-08-26",
  190. });
  191. check("重复回盘应拒绝", reConfirm.code !== 200, reConfirm.code + " " + reConfirm.message);
  192. }
  193. console.log("\n=== 2 特殊业务:丧葬 ===");
  194. const p2 = await req("POST", "/api/personnel", {
  195. name: "丧葬测员",
  196. idNumber: "31011719880101" + String(Date.now()).slice(-4),
  197. townId,
  198. villageId: villageId2,
  199. bankCardNumber: "6228480030007777666",
  200. monthlyStandard: 1280,
  201. enjoyStartMonth: "202601",
  202. });
  203. const pid2 = p2.data?.id;
  204. const funNoPerson = await req("POST", "/api/special-biz", { bizType: "FUNERAL", amount: 6000 });
  205. check("丧葬未选人失败", funNoPerson.code !== 200, funNoPerson.message);
  206. const fun = await req("POST", "/api/special-biz", {
  207. bizType: "FUNERAL",
  208. personnelId: pid2,
  209. amount: 6000,
  210. reason: "去世",
  211. heirName: "继承人甲",
  212. heirIdNumber: "310117199001011111",
  213. heirBankCard: "6228480030001111000",
  214. heirPhone: "13800001111",
  215. });
  216. check("创建丧葬单", fun.code === 200 && !!fun.data?.id, fun.message);
  217. const funId = fun.data?.id;
  218. check("丧葬金额默认/传入", Number(fun.data?.amount) === 6000, fun.data?.amount);
  219. const funRe = await req("POST", "/api/special-biz/leader-approve", {
  220. bizId: funId,
  221. action: "PASS",
  222. opinion: "同意",
  223. });
  224. check("丧葬审批通过", funRe.code === 200 && funRe.data?.approveStatus === "已通过", funRe.data?.approveStatus);
  225. const funRe2 = await req("POST", "/api/special-biz/leader-approve", {
  226. bizId: funId,
  227. action: "PASS",
  228. opinion: "再审",
  229. });
  230. check("重复审批拒绝", funRe2.code !== 200, funRe2.message);
  231. const p2g = await req("GET", "/api/personnel/" + encodeURIComponent(pid2));
  232. check("审批后人员丧葬态", p2g.data?.bizStatus === "FUNERAL", p2g.data?.bizStatus);
  233. const expSp = await req("POST", "/api/special-biz/" + funId + "/export-disk");
  234. check("特殊出盘(已通过)", expSp.code === 200, expSp.message);
  235. const expSp2 = await req("POST", "/api/special-biz/" + funId + "/export-disk");
  236. check("特殊出盘不可重复", expSp2.code !== 200, expSp2.message);
  237. console.log("\n=== 3 特殊业务:暂停→恢复 ===");
  238. const p3 = await req("POST", "/api/personnel", {
  239. name: "暂停测员",
  240. idNumber: "31011719770101" + String(Date.now()).slice(-4),
  241. townId,
  242. villageId,
  243. bankCardNumber: "6228480030005555444",
  244. monthlyStandard: 1280,
  245. enjoyStartMonth: "202601",
  246. });
  247. const pid3 = p3.data?.id;
  248. const resumeEarly = await req("POST", "/api/special-biz", { bizType: "RESUME", personnelId: pid3 });
  249. check("非暂停人员恢复应失败", resumeEarly.code !== 200, resumeEarly.message);
  250. const pause = await req("POST", "/api/special-biz", {
  251. bizType: "PAUSE",
  252. personnelId: pid3,
  253. reason: "外出",
  254. });
  255. check("创建暂停", pause.code === 200, pause.message);
  256. await req("POST", "/api/special-biz/leader-approve", {
  257. bizId: pause.data.id,
  258. action: "PASS",
  259. opinion: "同意暂停",
  260. });
  261. const p3g = await req("GET", "/api/personnel/" + encodeURIComponent(pid3));
  262. check("暂停后状态 PAUSED", p3g.data?.bizStatus === "PAUSED", p3g.data?.bizStatus);
  263. const preview = await req("GET", "/api/special-biz/resume-preview?personnelId=" + encodeURIComponent(pid3));
  264. check("恢复预览", preview.code === 200 && preview.data?.amount != null, JSON.stringify(preview.data).slice(0, 120));
  265. const resume = await req("POST", "/api/special-biz", { bizType: "RESUME", personnelId: pid3 });
  266. check("创建恢复", resume.code === 200, resume.message);
  267. await req("POST", "/api/special-biz/leader-approve", {
  268. bizId: resume.data.id,
  269. action: "PASS",
  270. opinion: "同意恢复",
  271. });
  272. const p3g2 = await req("GET", "/api/personnel/" + encodeURIComponent(pid3));
  273. check("恢复后 NORMAL", p3g2.data?.bizStatus === "NORMAL", p3g2.data?.bizStatus);
  274. console.log("\n=== 4 特殊业务:调标(需历史两档标准)===");
  275. // 清掉本镇本年未归档调标批,避免「一年仅一次」挡住联调
  276. const year = String(new Date().getFullYear());
  277. // 多扫几轮,清掉同号重复残留
  278. for (let round = 0; round < 3; round++) {
  279. const page = await req("POST", "/api/batches/page", {
  280. pageNum: 1,
  281. pageSize: 200,
  282. townId,
  283. batchLevel: "VILLAGE",
  284. });
  285. let n = 0;
  286. for (const b of page.data?.list || []) {
  287. const id = String(b?.id || "");
  288. const pm = String(b?.payMonth || "");
  289. const remarks = String(b?.remarks || "");
  290. // 调标一年一次会扫历史(含已归档),联调前一并 purge
  291. if (id.includes(year + "99") || pm === year + "99" || remarks.includes("YEARLY_ADJUST|" + year + "99")) {
  292. const pr = await req("POST", "/api/batches/" + encodeURIComponent(id) + "/purge");
  293. console.log(" purged adjust batch " + id + " st=" + b.status + " rows=" + (pr.data?.purgedRows ?? "?"));
  294. n++;
  295. }
  296. }
  297. if (!n) break;
  298. }
  299. // 读取当前有效土地标准,再上调一档,保证有正差额
  300. const curStd = await req("GET", "/api/amount-standards/current?townId=" + encodeURIComponent(townId));
  301. const curAmt = Number(curStd.data?.amount || 1280);
  302. const nextAmt = curAmt + 100 + (Date.now() % 40);
  303. const stdNew = await req("POST", "/api/amount-standards", {
  304. townId,
  305. standardType: "YEARLY_ADJUST",
  306. amount: nextAmt,
  307. funeralAmount: 6000,
  308. effectiveTime: Date.now(),
  309. adjustReason: "联调调标+" + nextAmt,
  310. });
  311. check("写入更高年度标准" + nextAmt, stdNew.code === 200, stdNew.message + " cur=" + curAmt);
  312. const adjNoTown = await req("POST", "/api/special-biz", { bizType: "STANDARD_ADJUST" });
  313. check("调标无镇失败", adjNoTown.code !== 200, adjNoTown.message);
  314. const adj = await req("POST", "/api/special-biz", {
  315. bizType: "STANDARD_ADJUST",
  316. townId,
  317. reason: "联调调标补发",
  318. });
  319. check(
  320. "创建调标特殊单",
  321. adj.code === 200 && !!adj.data?.id,
  322. adj.message + " batches=" + (adj.data?.relatedBatchIds || "")
  323. );
  324. const adjId = adj.data?.id;
  325. const adjPassEarly = await req("POST", "/api/special-biz/leader-approve", {
  326. bizId: adjId,
  327. action: "PASS",
  328. opinion: "未审村批就通过",
  329. });
  330. check("未审村批时调标特殊单不可通过", adjPassEarly.code !== 200, adjPassEarly.message);
  331. const batchIds = String(adj.data?.relatedBatchIds || "")
  332. .split(",")
  333. .map((s) => s.trim())
  334. .filter(Boolean);
  335. check("调标生成村批", batchIds.length >= 1, "n=" + batchIds.length);
  336. for (const id of batchIds) {
  337. const lp = await req("POST", "/api/batches/leader-approve", {
  338. bizId: id,
  339. action: "PASS",
  340. opinion: "同意调标村批",
  341. });
  342. check("村批领导通过 " + id, lp.code === 200, lp.message);
  343. }
  344. const adjPass = await req("POST", "/api/special-biz/leader-approve", {
  345. bizId: adjId,
  346. action: "PASS",
  347. opinion: "村批已齐",
  348. });
  349. check(
  350. "村批通过后特殊单可通过",
  351. adjPass.code === 200 && adjPass.data?.approveStatus === "已通过",
  352. adjPass.message
  353. );
  354. const adjExp = await req("POST", "/api/special-biz/" + adjId + "/export-disk");
  355. check("调标不可走特殊出盘", adjExp.code !== 200, adjExp.message);
  356. console.log("\n=== 5 回盘失败→待补发 ===");
  357. const p4 = await req("POST", "/api/personnel", {
  358. name: "回盘失败员",
  359. idNumber: "31011719660101" + String(Date.now()).slice(-4),
  360. townId,
  361. villageId: villageId2,
  362. bankCardNumber: "6228480030003333222",
  363. monthlyStandard: 1280,
  364. enjoyStartMonth: "202601",
  365. });
  366. const pid4 = p4.data?.id;
  367. const bFail = await req("POST", "/api/batches", {
  368. batchNo: payMonthFail,
  369. batchLevel: "VILLAGE",
  370. payMonth: payMonthFail,
  371. townId,
  372. villageId: villageId2,
  373. personnelIds: [pid4],
  374. });
  375. const bidFail = bFail.data?.id;
  376. if (bidFail) {
  377. await req("POST", "/api/batches/" + encodeURIComponent(bidFail) + "/submit");
  378. await req("POST", "/api/batches/town-approve", { bizId: bidFail, action: "PASS", opinion: "ok" });
  379. await req("POST", "/api/insurance-match/trigger", { batchId: bidFail });
  380. await req("POST", "/api/batches/" + encodeURIComponent(bidFail) + "/submit-leader");
  381. await req("POST", "/api/batches/leader-approve", { bizId: bidFail, action: "PASS", opinion: "ok" });
  382. const expFail = await req("POST", "/api/payments/export-disk", {
  383. batchId: bidFail,
  384. bypassDeadline: true,
  385. operator: "联调",
  386. });
  387. check("历史月补办出盘", expFail.code === 200, expFail.message);
  388. const pays = await req("POST", "/api/payments/details/page", {
  389. batchId: bidFail,
  390. pageNum: 1,
  391. pageSize: 20,
  392. });
  393. const payId = (pays.data?.list || [])[0]?.id;
  394. const failRet = await req("POST", "/api/return-disk/confirm", {
  395. batchId: bidFail,
  396. returnDate: "2026-07-28",
  397. lines: [
  398. {
  399. paymentDetailId: payId,
  400. success: false,
  401. failReasonCode: "CODE_003",
  402. failReasonDesc: "冻结",
  403. },
  404. ],
  405. });
  406. check("失败回盘", failRet.code === 200, failRet.message);
  407. const arrears = await req("POST", "/api/return-arrears/page", {
  408. pageNum: 1,
  409. pageSize: 50,
  410. personnelId: pid4,
  411. });
  412. check(
  413. "生成待补发台账",
  414. arrears.code === 200 && (arrears.data?.total || 0) >= 1,
  415. "total=" + arrears.data?.total
  416. );
  417. } else {
  418. check("失败回盘建批", false, bFail.message);
  419. }
  420. console.log("\n=== 汇总 ===");
  421. const pass = asserts.filter((a) => a.pass).length;
  422. const fail = asserts.filter((a) => !a.pass).length;
  423. console.log("PASS=" + pass + " FAIL=" + fail + " TOTAL=" + asserts.length);
  424. if (fail) {
  425. asserts.filter((a) => !a.pass).forEach((a) => console.log(" - " + a.name + " | " + a.detail));
  426. process.exit(2);
  427. }
  428. console.log("ALL GREEN");
  429. })().catch((e) => {
  430. console.error(e);
  431. process.exit(1);
  432. });