| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- /**
- * 全流程业务门禁联调:人员状态必须随流程推进,未过节点不可“假通过”。
- * 用法:node scripts/e2e-biz-gates-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 : ""));
- return !!cond;
- }
- 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, raw: String(raw).slice(0, 240) });
- }
- });
- }
- );
- r.on("error", (e) => resolve({ code: 0, error: e.message }));
- if (data) r.write(data);
- r.end();
- });
- }
- async function getPerson(id) {
- const r = await req("GET", "/api/personnel/" + encodeURIComponent(id));
- return r.data || null;
- }
- async function createPerson(name, extras) {
- const body = Object.assign(
- {
- name,
- idNumber: "31011719900101" + String(Date.now()).slice(-4),
- townId: "T_YEXIE",
- villageId: "V_YX_001",
- bankCardNumber: "622848003000" + String(Date.now()).slice(-7),
- monthlyStandard: 1280,
- enjoyStartMonth: "202601",
- },
- extras || {}
- );
- // 避免身份证撞号
- await new Promise((r) => setTimeout(r, 5));
- body.idNumber = "3101171990" + String(Date.now()).slice(-8);
- const r = await req("POST", "/api/personnel", body);
- return r;
- }
- async function ensureBatch(payMonth) {
- return req(
- "POST",
- "/api/batches/ensure-current-month?townId=T_YEXIE&villageId=V_YX_001&payMonth=" +
- encodeURIComponent(payMonth)
- );
- }
- async function purgeMonth(payMonth) {
- const page = await req("POST", "/api/batches/page", {
- pageNum: 1,
- pageSize: 50,
- townId: "T_YEXIE",
- villageId: "V_YX_001",
- payMonth,
- batchLevel: "VILLAGE",
- });
- for (const b of page.data?.list || []) {
- if (b?.id && b.status !== "ARCHIVED") {
- await req("POST", "/api/batches/" + encodeURIComponent(b.id) + "/purge");
- }
- }
- }
- (async () => {
- const payA = "23" + String(Date.now()).slice(-4);
- const payB = "24" + String(Date.now()).slice(-4);
- console.log("\n========== 场景A:正常闭环 + 状态门禁 ==========");
- console.log("payMonth=" + payA);
- await purgeMonth(payA);
- // A0 空人选不可提交(创建草稿不带人员;ensure 会默认选人故不用 ensure)
- const emptyCreate = await req("POST", "/api/batches", {
- batchLevel: "VILLAGE",
- payMonth: payA,
- townId: "T_YEXIE",
- villageId: "V_YX_001",
- batchNo: "GATE-EMPTY-" + payA,
- });
- let bid = emptyCreate.data?.id;
- check(
- "A0 建草稿批",
- !!bid && emptyCreate.data?.status === "DRAFT",
- emptyCreate.data?.status || emptyCreate.message
- );
- const emptySubmit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit");
- check("A0 未选人禁止提交初审", emptySubmit.code !== 200, emptySubmit.code + " " + emptySubmit.message);
- // A1 建正常人员
- const pOk = await createPerson("门禁正常员");
- const pidOk = pOk.data?.id;
- check("A1 建人员", !!pidOk, pidOk);
- let person = await getPerson(pidOk);
- check("A1 初始草稿", person?.bizStatus === "DRAFT", person?.bizStatus);
- check(
- "A1 初始居保未匹配",
- !person?.insuranceMatchResult || person.insuranceMatchResult === "PENDING",
- person?.insuranceMatchResult
- );
- // A2 选人提交(复用空草稿批)
- const put = await req("PUT", "/api/batches/" + encodeURIComponent(bid) + "/members", {
- personnelIds: [pidOk],
- });
- check("A2 选人", put.code === 200, put.message);
- const submit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit");
- check(
- "A2 提交初审",
- submit.code === 200 && submit.data?.status === "PENDING_TOWN_APPROVE",
- submit.data?.status || submit.message
- );
- person = await getPerson(pidOk);
- check("A2 提交后人员仍草稿(待镇审)", person?.bizStatus === "DRAFT", person?.bizStatus);
- // A3 未镇审不可居保推进领导 / 不可出盘 / 不可提交领导
- const earlyLeader = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
- check("A3 未居保不可提交领导", earlyLeader.code !== 200, earlyLeader.code + " " + earlyLeader.message);
- const earlyExport = await req("POST", "/api/payments/export-disk", {
- batchId: bid,
- bypassDeadline: true,
- });
- check("A3 未领导审不可出盘", earlyExport.code !== 200, earlyExport.code + " " + earlyExport.message);
- // A4 镇审通过 → 待居保匹配
- const town = await req("POST", "/api/batches/town-approve", {
- bizId: bid,
- action: "PASS",
- opinion: "资格材料齐全",
- });
- check(
- "A4 镇审通过",
- town.code === 200 && town.data?.status === "PENDING_INSURANCE_MATCH",
- town.data?.status
- );
- person = await getPerson(pidOk);
- check(
- "A4 人员→待居保匹配",
- person?.bizStatus === "PENDING_INSURANCE_MATCH",
- person?.bizStatus
- );
- // A5 居保前仍不可提交领导(若状态仍是待居保且未匹配)
- const beforeMatchLeader = await req(
- "POST",
- "/api/batches/" + encodeURIComponent(bid) + "/submit-leader"
- );
- // 允许:若系统要求先 trigger;或仍是 PENDING_INSURANCE_MATCH 且未匹配应拒绝
- person = await getPerson(pidOk);
- if (!person?.insuranceMatchResult || person.insuranceMatchResult === "PENDING") {
- check(
- "A5 未匹配禁止提交领导",
- beforeMatchLeader.code !== 200,
- beforeMatchLeader.code + " " + beforeMatchLeader.message
- );
- }
- // A6 居保匹配成功
- const match = await req("POST", "/api/insurance-match/trigger", {
- batchId: bid,
- operator: "业务",
- });
- check("A6 居保触发", match.code === 200, JSON.stringify(match.data).slice(0, 120));
- check("A6 居保无 FAIL", Number(match.data?.failCount || 0) === 0, "fail=" + match.data?.failCount);
- person = await getPerson(pidOk);
- check("A6 人员→正常在库", person?.bizStatus === "NORMAL", person?.bizStatus);
- check(
- "A6 居保→SUCCESS",
- person?.insuranceMatchResult === "SUCCESS",
- person?.insuranceMatchResult
- );
- check(
- "A6 批次进入待领导或可提交",
- match.data?.nextStatus === "PENDING_LEADER_APPROVE" ||
- match.data?.nextStatus === "PENDING_STATUS_MAINTAIN",
- match.data?.nextStatus
- );
- // A7 提交领导(幂等或推进)
- const toLeader = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
- check(
- "A7 提交领导",
- toLeader.code === 200 && toLeader.data?.status === "PENDING_LEADER_APPROVE",
- toLeader.message + " " + toLeader.data?.status
- );
- // A8 领导驳回 → 不可出盘
- const leaderReject = await req("POST", "/api/batches/leader-approve", {
- bizId: bid,
- action: "REJECT",
- opinion: "清单需调整",
- });
- check("A8 领导驳回", leaderReject.code === 200, leaderReject.data?.status);
- const exportAfterReject = await req("POST", "/api/payments/export-disk", {
- batchId: bid,
- bypassDeadline: true,
- });
- check(
- "A8 驳回后禁止出盘",
- exportAfterReject.code !== 200,
- exportAfterReject.code + " " + exportAfterReject.message
- );
- // A9 再提交领导并通过
- // 驳回后状态可能是 REJECTED / PENDING_STATUS_MAINTAIN 等,再走 submit-leader 或直接 leader PASS(按实现)
- let st = (await req("GET", "/api/batches/" + encodeURIComponent(bid))).data?.status;
- if (st === "REJECTED" || st === "PENDING_STATUS_MAINTAIN" || st === "PENDING_INSURANCE_MATCH") {
- const again = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
- if (again.code !== 200) {
- // 部分实现驳回后需重新居保
- await req("POST", "/api/insurance-match/trigger", { batchId: bid });
- await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
- }
- }
- const leaderPass = await req("POST", "/api/batches/leader-approve", {
- bizId: bid,
- action: "PASS",
- opinion: "同意出盘",
- });
- check(
- "A9 领导通过",
- leaderPass.code === 200 &&
- (leaderPass.data?.status === "READY_EXPORT" || leaderPass.data?.status === "EXPORTED"),
- leaderPass.data?.status
- );
- person = await getPerson(pidOk);
- check("A9 通过后人员仍正常", person?.bizStatus === "NORMAL", person?.bizStatus);
- // A10 出盘回盘归档
- const exp = await req("POST", "/api/payments/export-disk", {
- batchId: bid,
- bypassDeadline: true,
- });
- check("A10 出盘", exp.code === 200, exp.data?.fileName || exp.message);
- const ret = await req("POST", "/api/return-disk/confirm", {
- batchId: bid,
- manualAllSuccess: true,
- operator: "业务",
- });
- check("A10 回盘", ret.code === 200, ret.message);
- const ret2 = await req("POST", "/api/return-disk/confirm", {
- batchId: bid,
- manualAllSuccess: true,
- });
- check("A10 重复回盘拒绝", ret2.code !== 200, ret2.message);
- const arch = await req("POST", "/api/dashboard/archive/" + encodeURIComponent(bid));
- check("A10 归档", arch.code === 200 && arch.data?.status === "ARCHIVED", arch.data?.status);
- console.log("\n========== 场景B:居保 FAIL 不可跳过 ==========");
- console.log("payMonth=" + payB);
- await purgeMonth(payB);
- const pFail = await createPerson("门禁失败员", {
- remarks: "INSURANCE_FORCE_FAIL|联调强制失败",
- });
- const pidFail = pFail.data?.id;
- check("B1 建 FAIL 人员", !!pidFail, pidFail);
- const ensB = await ensureBatch(payB);
- const bidB = ensB.data?.id;
- await req("PUT", "/api/batches/" + encodeURIComponent(bidB) + "/members", {
- personnelIds: [pidFail],
- });
- await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit");
- await req("POST", "/api/batches/town-approve", {
- bizId: bidB,
- action: "PASS",
- opinion: "先过初审",
- });
- const matchFail = await req("POST", "/api/insurance-match/trigger", { batchId: bidB });
- check(
- "B2 居保产生 FAIL",
- matchFail.code === 200 && Number(matchFail.data?.failCount || 0) > 0,
- "fail=" + matchFail.data?.failCount + " next=" + matchFail.data?.nextStatus
- );
- check(
- "B2 批次停在待状态维护",
- matchFail.data?.nextStatus === "PENDING_STATUS_MAINTAIN",
- matchFail.data?.nextStatus
- );
- let pf = await getPerson(pidFail);
- check("B2 人员异常待处理", pf?.bizStatus === "ABNORMAL", pf?.bizStatus);
- check("B2 居保结果 FAIL", pf?.insuranceMatchResult === "FAIL", pf?.insuranceMatchResult);
- const skipLeader = await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit-leader");
- check("B3 FAIL 禁止提交领导", skipLeader.code !== 200, skipLeader.message);
- const skipLeaderApprove = await req("POST", "/api/batches/leader-approve", {
- bizId: bidB,
- action: "PASS",
- opinion: "强行通过",
- });
- check(
- "B3 FAIL 禁止领导直接通过",
- skipLeaderApprove.code !== 200,
- skipLeaderApprove.code + " " + skipLeaderApprove.message
- );
- // 人工确认后方可提交领导
- const manual = await req("POST", "/api/insurance-match/manual-confirm", {
- personnelId: pidFail,
- batchId: bidB,
- pass: true,
- operator: "业务",
- });
- check("B4 人工确认", manual.code === 200, manual.message);
- pf = await getPerson(pidFail);
- check("B4 人工确认后 NORMAL", pf?.bizStatus === "NORMAL", pf?.bizStatus);
- check(
- "B4 人工确认后 MANUAL_PASS",
- pf?.insuranceMatchResult === "MANUAL_PASS",
- pf?.insuranceMatchResult
- );
- const afterManual = await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit-leader");
- check(
- "B4 确认后可提交领导",
- afterManual.code === 200 && afterManual.data?.status === "PENDING_LEADER_APPROVE",
- afterManual.data?.status
- );
- console.log("\n========== 场景C:镇审驳回 + 暂停人员默认不纳入 ==========");
- const payC = "25" + String(Date.now()).slice(-4);
- await purgeMonth(payC);
- const pDraft = await createPerson("驳回测员");
- const pidDraft = pDraft.data?.id;
- const ensC = await ensureBatch(payC);
- const bidC = ensC.data?.id;
- await req("PUT", "/api/batches/" + encodeURIComponent(bidC) + "/members", {
- personnelIds: [pidDraft],
- });
- await req("POST", "/api/batches/" + encodeURIComponent(bidC) + "/submit");
- const townReject = await req("POST", "/api/batches/town-approve", {
- bizId: bidC,
- action: "REJECT",
- opinion: "材料不全",
- });
- check(
- "C1 镇审驳回",
- townReject.code === 200 && townReject.data?.status === "REJECTED",
- townReject.data?.status
- );
- const rejectLeader = await req("POST", "/api/batches/" + encodeURIComponent(bidC) + "/submit-leader");
- check("C1 驳回后不可提交领导", rejectLeader.code !== 200, rejectLeader.message);
- // 暂停人员:创建暂停特殊业务并审批后,默认不应再被 ensure 全选
- const pPause = await createPerson("暂停测员C");
- const pidPause = pPause.data?.id;
- // 先置为 NORMAL 再暂停(特殊业务要求)
- await req("PUT", "/api/personnel/status", {
- personnelId: pidPause,
- bizStatus: "NORMAL",
- payThisMonth: true,
- });
- const pauseBiz = await req("POST", "/api/special-biz", {
- bizType: "PAUSE",
- personnelId: pidPause,
- reason: "外出",
- });
- check("C2 创建暂停单", pauseBiz.code === 200, pauseBiz.message);
- if (pauseBiz.data?.id) {
- await req("POST", "/api/special-biz/leader-approve", {
- bizId: pauseBiz.data.id,
- action: "PASS",
- opinion: "同意暂停",
- });
- }
- const paused = await getPerson(pidPause);
- check("C2 暂停后 PAUSED", paused?.bizStatus === "PAUSED", paused?.bizStatus);
- const payD = "26" + String(Date.now()).slice(-4);
- await purgeMonth(payD);
- // 同时放入正常人 + 暂停人,选人应允许,但 ensure 默认不应带暂停人
- const ensD = await ensureBatch(payD);
- const bidD = ensD.data?.id;
- // 查默认明细是否含暂停人
- const pays = await req("POST", "/api/payments/details/page", {
- pageNum: 1,
- pageSize: 200,
- batchId: bidD,
- });
- const ids = (pays.data?.list || []).map((x) => x.personnelId);
- check(
- "C3 ensure 默认不含暂停人",
- !ids.includes(pidPause),
- "ids=" + ids.slice(0, 8).join(",")
- );
- console.log("\n========== 汇总 ==========");
- const fail = asserts.filter((a) => !a.pass);
- console.log("PASS=" + (asserts.length - fail.length) + " FAIL=" + fail.length + " TOTAL=" + asserts.length);
- for (const f of fail) {
- console.log(" - " + f.name + (f.detail ? " | " + f.detail : ""));
- }
- process.exit(fail.length ? 1 : 0);
- })().catch((e) => {
- console.error(e);
- process.exit(1);
- });
|