/** * 全生命周期联调:月度补助 + 特殊业务 + 回盘/待补发(含异常断言) * 用法:node e2e-lifecycle-full.js * 每次使用独立发放月,避免与历史批次冲突。 */ const http = require("http"); const asserts = []; function check(name, cond, detail) { asserts.push({ name, pass: !!cond, detail: detail || "" }); console.log((cond ? "PASS" : "FAIL") + " | " + name + (detail ? " | " + detail : "")); } function req(method, path, body) { 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 r = http.request( { hostname: "localhost", port: 8088, path: "/sjnmtybt" + path, method, headers }, (res) => { let raw = ""; res.on("data", (c) => (raw += c)); res.on("end", () => { try { resolve(JSON.parse(raw)); } catch (e) { resolve({ code: res.statusCode, parseError: true, raw: String(raw).slice(0, 200) }); } }); } ); r.on("error", (e) => resolve({ code: 0, error: e.message })); if (data) r.write(data); r.end(); }); } /** 生成独立 YYYYMM,降低与存量批次冲突概率 */ function uniquePayMonth(offsetMonths) { const d = new Date(); d.setMonth(d.getMonth() + (offsetMonths || 0)); // 再叠毫秒尾数进「虚构年」段,避免同月重复跑 const base = 2100 + (Date.now() % 50); const m = String(((d.getMonth() + Math.abs(offsetMonths || 0)) % 12) + 1).padStart(2, "0"); return String(base) + m; } async function purgeVillageMonth(townId, villageId, payMonth) { const page = await req("POST", "/api/batches/page", { pageNum: 1, pageSize: 50, townId, villageId, payMonth, batchLevel: "VILLAGE", }); const list = page.data?.list || []; for (const b of list) { if (b?.id && b.status !== "ARCHIVED") { await req("POST", "/api/batches/" + encodeURIComponent(b.id) + "/purge"); } } } const townId = "T_YEXIE"; const villageId = "V_YX_001"; const villageId2 = "V_YX_002"; const payMonth = uniquePayMonth(0); const payMonthFail = uniquePayMonth(1); (async () => { console.log("联调发放月 payMonth=" + payMonth + " payMonthFail=" + payMonthFail); await purgeVillageMonth(townId, villageId, payMonth); await purgeVillageMonth(townId, villageId2, payMonthFail); console.log("\n=== 0 前置:人员 ==="); const idNo = "31011719900101" + String(Date.now()).slice(-4); const saved = await req("POST", "/api/personnel", { name: "生命周期测员", idNumber: idNo, townId, villageId, bankCardNumber: "6228480030009999888", monthlyStandard: 1280, enjoyStartMonth: "202601", source: "联调", }); check("新建人员", saved.code === 200 && !!saved.data?.id, saved.message || saved.data?.id); const pid = saved.data?.id; if (!pid) { console.log("无人员,中止"); process.exit(2); } console.log("\n=== 1 月度补助全生命周期 ==="); const batch = await req("POST", "/api/batches", { batchNo: payMonth, batchLevel: "VILLAGE", payMonth, townId, villageId, payDate: "2026-08-20", personnelIds: [pid], }); check("建村批", batch.code === 200 && !!batch.data?.id, batch.message || batch.data?.id); const bid = batch.data?.id; if (!bid) { console.log("无批次,中止月度段"); } const badDup = await req("POST", "/api/batches", { batchNo: payMonth, batchLevel: "VILLAGE", payMonth, townId, villageId, personnelIds: [pid], }); check( "同村同月重复建批应失败或复用", badDup.code !== 200 || badDup.data?.id === bid, badDup.code + "/" + (badDup.data?.id || badDup.message) ); if (bid) { // 草稿须先提交初审,才能镇审(门禁) const submit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit"); check( "提交初审", submit.code === 200 && submit.data?.status === "PENDING_TOWN_APPROVE", submit.message + " " + submit.data?.status ); const townPass = await req("POST", "/api/batches/town-approve", { bizId: bid, action: "PASS", opinion: "材料齐全", }); check("镇初审通过", townPass.code === 200, townPass.message); let b = await req("GET", "/api/batches/" + encodeURIComponent(bid)); check("初审后进入居保", b.data?.currentStage === "STAGE3_INSURANCE_MATCH", b.data?.currentStage); const match = await req("POST", "/api/insurance-match/trigger", { batchId: bid }); check("居保匹配", match.code === 200, match.message); await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader"); const leaderReject = await req("POST", "/api/batches/leader-approve", { bizId: bid, action: "REJECT", opinion: "先驳回测", }); check("领导可驳回", leaderReject.code === 200, leaderReject.message); b = await req("GET", "/api/batches/" + encodeURIComponent(bid)); check("驳回后状态 REJECTED", b.data?.status === "REJECTED", b.data?.status); // 领导驳回后回到清单阶段,再 submit-leader 后复审 await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader"); const leaderPass = await req("POST", "/api/batches/leader-approve", { bizId: bid, action: "PASS", opinion: "同意出盘", }); check("领导再通过", leaderPass.code === 200, leaderPass.message); b = await req("GET", "/api/batches/" + encodeURIComponent(bid)); check( "可出盘或待领导后通过", ["READY_EXPORT", "PENDING_LEADER_APPROVE", "EXPORTED"].includes(b.data?.status), b.data?.status ); if (b.data?.status === "PENDING_LEADER_APPROVE") { await req("POST", "/api/batches/leader-approve", { bizId: bid, action: "PASS", opinion: "同意" }); b = await req("GET", "/api/batches/" + encodeURIComponent(bid)); } const exp = await req("POST", "/api/payments/export-disk", { batchId: bid, bypassDeadline: true, operator: "业务", }); check("出盘", exp.code === 200, exp.message); let ret2 = await req("POST", "/api/return-disk/confirm", { batchId: bid, manualAllSuccess: false, returnDate: "2026-08-25", operator: "业务", lines: [{ personnelId: pid, success: true }], }); if (ret2.code !== 200) { ret2 = await req("POST", "/api/return-disk/confirm", { batchId: bid, manualAllSuccess: true, returnDate: "2026-08-25", operator: "业务", }); } check("回盘确认", ret2.code === 200, ret2.message); const reConfirm = await req("POST", "/api/return-disk/confirm", { batchId: bid, manualAllSuccess: true, returnDate: "2026-08-26", }); check("重复回盘应拒绝", reConfirm.code !== 200, reConfirm.code + " " + reConfirm.message); } console.log("\n=== 2 特殊业务:丧葬 ==="); const p2 = await req("POST", "/api/personnel", { name: "丧葬测员", idNumber: "31011719880101" + String(Date.now()).slice(-4), townId, villageId: villageId2, bankCardNumber: "6228480030007777666", monthlyStandard: 1280, enjoyStartMonth: "202601", }); const pid2 = p2.data?.id; const funNoPerson = await req("POST", "/api/special-biz", { bizType: "FUNERAL", amount: 6000 }); check("丧葬未选人失败", funNoPerson.code !== 200, funNoPerson.message); const fun = await req("POST", "/api/special-biz", { bizType: "FUNERAL", personnelId: pid2, amount: 6000, reason: "去世", heirName: "继承人甲", heirIdNumber: "310117199001011111", heirBankCard: "6228480030001111000", heirPhone: "13800001111", }); check("创建丧葬单", fun.code === 200 && !!fun.data?.id, fun.message); const funId = fun.data?.id; check("丧葬金额默认/传入", Number(fun.data?.amount) === 6000, fun.data?.amount); const funRe = await req("POST", "/api/special-biz/leader-approve", { bizId: funId, action: "PASS", opinion: "同意", }); check("丧葬审批通过", funRe.code === 200 && funRe.data?.approveStatus === "已通过", funRe.data?.approveStatus); const funRe2 = await req("POST", "/api/special-biz/leader-approve", { bizId: funId, action: "PASS", opinion: "再审", }); check("重复审批拒绝", funRe2.code !== 200, funRe2.message); const p2g = await req("GET", "/api/personnel/" + encodeURIComponent(pid2)); check("审批后人员丧葬态", p2g.data?.bizStatus === "FUNERAL", p2g.data?.bizStatus); const expSp = await req("POST", "/api/special-biz/" + funId + "/export-disk"); check("特殊出盘(已通过)", expSp.code === 200, expSp.message); const expSp2 = await req("POST", "/api/special-biz/" + funId + "/export-disk"); check("特殊出盘不可重复", expSp2.code !== 200, expSp2.message); console.log("\n=== 3 特殊业务:暂停→恢复 ==="); const p3 = await req("POST", "/api/personnel", { name: "暂停测员", idNumber: "31011719770101" + String(Date.now()).slice(-4), townId, villageId, bankCardNumber: "6228480030005555444", monthlyStandard: 1280, enjoyStartMonth: "202601", }); const pid3 = p3.data?.id; const resumeEarly = await req("POST", "/api/special-biz", { bizType: "RESUME", personnelId: pid3 }); check("非暂停人员恢复应失败", resumeEarly.code !== 200, resumeEarly.message); const pause = await req("POST", "/api/special-biz", { bizType: "PAUSE", personnelId: pid3, reason: "外出", }); check("创建暂停", pause.code === 200, pause.message); await req("POST", "/api/special-biz/leader-approve", { bizId: pause.data.id, action: "PASS", opinion: "同意暂停", }); const p3g = await req("GET", "/api/personnel/" + encodeURIComponent(pid3)); check("暂停后状态 PAUSED", p3g.data?.bizStatus === "PAUSED", p3g.data?.bizStatus); const preview = await req("GET", "/api/special-biz/resume-preview?personnelId=" + encodeURIComponent(pid3)); check("恢复预览", preview.code === 200 && preview.data?.amount != null, JSON.stringify(preview.data).slice(0, 120)); const resume = await req("POST", "/api/special-biz", { bizType: "RESUME", personnelId: pid3 }); check("创建恢复", resume.code === 200, resume.message); await req("POST", "/api/special-biz/leader-approve", { bizId: resume.data.id, action: "PASS", opinion: "同意恢复", }); const p3g2 = await req("GET", "/api/personnel/" + encodeURIComponent(pid3)); check("恢复后 NORMAL", p3g2.data?.bizStatus === "NORMAL", p3g2.data?.bizStatus); console.log("\n=== 4 特殊业务:调标(需历史两档标准)==="); // 清掉本镇本年未归档调标批,避免「一年仅一次」挡住联调 const year = String(new Date().getFullYear()); // 多扫几轮,清掉同号重复残留 for (let round = 0; round < 3; round++) { const page = await req("POST", "/api/batches/page", { pageNum: 1, pageSize: 200, townId, batchLevel: "VILLAGE", }); let n = 0; for (const b of page.data?.list || []) { const id = String(b?.id || ""); const pm = String(b?.payMonth || ""); const remarks = String(b?.remarks || ""); // 调标一年一次会扫历史(含已归档),联调前一并 purge if (id.includes(year + "99") || pm === year + "99" || remarks.includes("YEARLY_ADJUST|" + year + "99")) { const pr = await req("POST", "/api/batches/" + encodeURIComponent(id) + "/purge"); console.log(" purged adjust batch " + id + " st=" + b.status + " rows=" + (pr.data?.purgedRows ?? "?")); n++; } } if (!n) break; } // 读取当前有效土地标准,再上调一档,保证有正差额 const curStd = await req("GET", "/api/amount-standards/current?townId=" + encodeURIComponent(townId)); const curAmt = Number(curStd.data?.amount || 1280); const nextAmt = curAmt + 100 + (Date.now() % 40); const stdNew = await req("POST", "/api/amount-standards", { townId, standardType: "YEARLY_ADJUST", amount: nextAmt, funeralAmount: 6000, effectiveTime: Date.now(), adjustReason: "联调调标+" + nextAmt, }); check("写入更高年度标准" + nextAmt, stdNew.code === 200, stdNew.message + " cur=" + curAmt); const adjNoTown = await req("POST", "/api/special-biz", { bizType: "STANDARD_ADJUST" }); check("调标无镇失败", adjNoTown.code !== 200, adjNoTown.message); const adj = await req("POST", "/api/special-biz", { bizType: "STANDARD_ADJUST", townId, reason: "联调调标补发", }); check( "创建调标特殊单", adj.code === 200 && !!adj.data?.id, adj.message + " batches=" + (adj.data?.relatedBatchIds || "") ); const adjId = adj.data?.id; const adjPassEarly = await req("POST", "/api/special-biz/leader-approve", { bizId: adjId, action: "PASS", opinion: "未审村批就通过", }); check("未审村批时调标特殊单不可通过", adjPassEarly.code !== 200, adjPassEarly.message); const batchIds = String(adj.data?.relatedBatchIds || "") .split(",") .map((s) => s.trim()) .filter(Boolean); check("调标生成村批", batchIds.length >= 1, "n=" + batchIds.length); for (const id of batchIds) { const lp = await req("POST", "/api/batches/leader-approve", { bizId: id, action: "PASS", opinion: "同意调标村批", }); check("村批领导通过 " + id, lp.code === 200, lp.message); } const adjPass = await req("POST", "/api/special-biz/leader-approve", { bizId: adjId, action: "PASS", opinion: "村批已齐", }); check( "村批通过后特殊单可通过", adjPass.code === 200 && adjPass.data?.approveStatus === "已通过", adjPass.message ); const adjExp = await req("POST", "/api/special-biz/" + adjId + "/export-disk"); check("调标不可走特殊出盘", adjExp.code !== 200, adjExp.message); console.log("\n=== 5 回盘失败→待补发 ==="); const p4 = await req("POST", "/api/personnel", { name: "回盘失败员", idNumber: "31011719660101" + String(Date.now()).slice(-4), townId, villageId: villageId2, bankCardNumber: "6228480030003333222", monthlyStandard: 1280, enjoyStartMonth: "202601", }); const pid4 = p4.data?.id; const bFail = await req("POST", "/api/batches", { batchNo: payMonthFail, batchLevel: "VILLAGE", payMonth: payMonthFail, townId, villageId: villageId2, personnelIds: [pid4], }); const bidFail = bFail.data?.id; if (bidFail) { await req("POST", "/api/batches/" + encodeURIComponent(bidFail) + "/submit"); await req("POST", "/api/batches/town-approve", { bizId: bidFail, action: "PASS", opinion: "ok" }); await req("POST", "/api/insurance-match/trigger", { batchId: bidFail }); await req("POST", "/api/batches/" + encodeURIComponent(bidFail) + "/submit-leader"); await req("POST", "/api/batches/leader-approve", { bizId: bidFail, action: "PASS", opinion: "ok" }); const expFail = await req("POST", "/api/payments/export-disk", { batchId: bidFail, bypassDeadline: true, operator: "联调", }); check("历史月补办出盘", expFail.code === 200, expFail.message); const pays = await req("POST", "/api/payments/details/page", { batchId: bidFail, pageNum: 1, pageSize: 20, }); const payId = (pays.data?.list || [])[0]?.id; const failRet = await req("POST", "/api/return-disk/confirm", { batchId: bidFail, returnDate: "2026-07-28", lines: [ { paymentDetailId: payId, success: false, failReasonCode: "CODE_003", failReasonDesc: "冻结", }, ], }); check("失败回盘", failRet.code === 200, failRet.message); const arrears = await req("POST", "/api/return-arrears/page", { pageNum: 1, pageSize: 50, personnelId: pid4, }); check( "生成待补发台账", arrears.code === 200 && (arrears.data?.total || 0) >= 1, "total=" + arrears.data?.total ); } else { check("失败回盘建批", false, bFail.message); } console.log("\n=== 汇总 ==="); const pass = asserts.filter((a) => a.pass).length; const fail = asserts.filter((a) => !a.pass).length; console.log("PASS=" + pass + " FAIL=" + fail + " TOTAL=" + asserts.length); if (fail) { asserts.filter((a) => !a.pass).forEach((a) => console.log(" - " + a.name + " | " + a.detail)); process.exit(2); } console.log("ALL GREEN"); })().catch((e) => { console.error(e); process.exit(1); });