e2e-biz-gates-full.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. /**
  2. * 全流程业务门禁联调:人员状态必须随流程推进,未过节点不可“假通过”。
  3. * 用法:node scripts/e2e-biz-gates-full.js
  4. */
  5. const http = require("http");
  6. const asserts = [];
  7. function check(name, cond, detail) {
  8. asserts.push({ name, pass: !!cond, detail: detail || "" });
  9. console.log((cond ? "PASS" : "FAIL") + " | " + name + (detail ? " | " + detail : ""));
  10. return !!cond;
  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, raw: String(raw).slice(0, 240) });
  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. async function getPerson(id) {
  40. const r = await req("GET", "/api/personnel/" + encodeURIComponent(id));
  41. return r.data || null;
  42. }
  43. async function createPerson(name, extras) {
  44. const body = Object.assign(
  45. {
  46. name,
  47. idNumber: "31011719900101" + String(Date.now()).slice(-4),
  48. townId: "T_YEXIE",
  49. villageId: "V_YX_001",
  50. bankCardNumber: "622848003000" + String(Date.now()).slice(-7),
  51. monthlyStandard: 1280,
  52. enjoyStartMonth: "202601",
  53. },
  54. extras || {}
  55. );
  56. // 避免身份证撞号
  57. await new Promise((r) => setTimeout(r, 5));
  58. body.idNumber = "3101171990" + String(Date.now()).slice(-8);
  59. const r = await req("POST", "/api/personnel", body);
  60. return r;
  61. }
  62. async function ensureBatch(payMonth) {
  63. return req(
  64. "POST",
  65. "/api/batches/ensure-current-month?townId=T_YEXIE&villageId=V_YX_001&payMonth=" +
  66. encodeURIComponent(payMonth)
  67. );
  68. }
  69. async function purgeMonth(payMonth) {
  70. const page = await req("POST", "/api/batches/page", {
  71. pageNum: 1,
  72. pageSize: 50,
  73. townId: "T_YEXIE",
  74. villageId: "V_YX_001",
  75. payMonth,
  76. batchLevel: "VILLAGE",
  77. });
  78. for (const b of page.data?.list || []) {
  79. if (b?.id && b.status !== "ARCHIVED") {
  80. await req("POST", "/api/batches/" + encodeURIComponent(b.id) + "/purge");
  81. }
  82. }
  83. }
  84. (async () => {
  85. const payA = "23" + String(Date.now()).slice(-4);
  86. const payB = "24" + String(Date.now()).slice(-4);
  87. console.log("\n========== 场景A:正常闭环 + 状态门禁 ==========");
  88. console.log("payMonth=" + payA);
  89. await purgeMonth(payA);
  90. // A0 空人选不可提交(创建草稿不带人员;ensure 会默认选人故不用 ensure)
  91. const emptyCreate = await req("POST", "/api/batches", {
  92. batchLevel: "VILLAGE",
  93. payMonth: payA,
  94. townId: "T_YEXIE",
  95. villageId: "V_YX_001",
  96. batchNo: "GATE-EMPTY-" + payA,
  97. });
  98. let bid = emptyCreate.data?.id;
  99. check(
  100. "A0 建草稿批",
  101. !!bid && emptyCreate.data?.status === "DRAFT",
  102. emptyCreate.data?.status || emptyCreate.message
  103. );
  104. const emptySubmit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit");
  105. check("A0 未选人禁止提交初审", emptySubmit.code !== 200, emptySubmit.code + " " + emptySubmit.message);
  106. // A1 建正常人员
  107. const pOk = await createPerson("门禁正常员");
  108. const pidOk = pOk.data?.id;
  109. check("A1 建人员", !!pidOk, pidOk);
  110. let person = await getPerson(pidOk);
  111. check("A1 初始草稿", person?.bizStatus === "DRAFT", person?.bizStatus);
  112. check(
  113. "A1 初始居保未匹配",
  114. !person?.insuranceMatchResult || person.insuranceMatchResult === "PENDING",
  115. person?.insuranceMatchResult
  116. );
  117. // A2 选人提交(复用空草稿批)
  118. const put = await req("PUT", "/api/batches/" + encodeURIComponent(bid) + "/members", {
  119. personnelIds: [pidOk],
  120. });
  121. check("A2 选人", put.code === 200, put.message);
  122. const submit = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit");
  123. check(
  124. "A2 提交初审",
  125. submit.code === 200 && submit.data?.status === "PENDING_TOWN_APPROVE",
  126. submit.data?.status || submit.message
  127. );
  128. person = await getPerson(pidOk);
  129. check("A2 提交后人员仍草稿(待镇审)", person?.bizStatus === "DRAFT", person?.bizStatus);
  130. // A3 未镇审不可居保推进领导 / 不可出盘 / 不可提交领导
  131. const earlyLeader = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  132. check("A3 未居保不可提交领导", earlyLeader.code !== 200, earlyLeader.code + " " + earlyLeader.message);
  133. const earlyExport = await req("POST", "/api/payments/export-disk", {
  134. batchId: bid,
  135. bypassDeadline: true,
  136. });
  137. check("A3 未领导审不可出盘", earlyExport.code !== 200, earlyExport.code + " " + earlyExport.message);
  138. // A4 镇审通过 → 待居保匹配
  139. const town = await req("POST", "/api/batches/town-approve", {
  140. bizId: bid,
  141. action: "PASS",
  142. opinion: "资格材料齐全",
  143. });
  144. check(
  145. "A4 镇审通过",
  146. town.code === 200 && town.data?.status === "PENDING_INSURANCE_MATCH",
  147. town.data?.status
  148. );
  149. person = await getPerson(pidOk);
  150. check(
  151. "A4 人员→待居保匹配",
  152. person?.bizStatus === "PENDING_INSURANCE_MATCH",
  153. person?.bizStatus
  154. );
  155. // A5 居保前仍不可提交领导(若状态仍是待居保且未匹配)
  156. const beforeMatchLeader = await req(
  157. "POST",
  158. "/api/batches/" + encodeURIComponent(bid) + "/submit-leader"
  159. );
  160. // 允许:若系统要求先 trigger;或仍是 PENDING_INSURANCE_MATCH 且未匹配应拒绝
  161. person = await getPerson(pidOk);
  162. if (!person?.insuranceMatchResult || person.insuranceMatchResult === "PENDING") {
  163. check(
  164. "A5 未匹配禁止提交领导",
  165. beforeMatchLeader.code !== 200,
  166. beforeMatchLeader.code + " " + beforeMatchLeader.message
  167. );
  168. }
  169. // A6 居保匹配成功
  170. const match = await req("POST", "/api/insurance-match/trigger", {
  171. batchId: bid,
  172. operator: "业务",
  173. });
  174. check("A6 居保触发", match.code === 200, JSON.stringify(match.data).slice(0, 120));
  175. check("A6 居保无 FAIL", Number(match.data?.failCount || 0) === 0, "fail=" + match.data?.failCount);
  176. person = await getPerson(pidOk);
  177. check("A6 人员→正常在库", person?.bizStatus === "NORMAL", person?.bizStatus);
  178. check(
  179. "A6 居保→SUCCESS",
  180. person?.insuranceMatchResult === "SUCCESS",
  181. person?.insuranceMatchResult
  182. );
  183. check(
  184. "A6 批次进入待领导或可提交",
  185. match.data?.nextStatus === "PENDING_LEADER_APPROVE" ||
  186. match.data?.nextStatus === "PENDING_STATUS_MAINTAIN",
  187. match.data?.nextStatus
  188. );
  189. // A7 提交领导(幂等或推进)
  190. const toLeader = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  191. check(
  192. "A7 提交领导",
  193. toLeader.code === 200 && toLeader.data?.status === "PENDING_LEADER_APPROVE",
  194. toLeader.message + " " + toLeader.data?.status
  195. );
  196. // A8 领导驳回 → 不可出盘
  197. const leaderReject = await req("POST", "/api/batches/leader-approve", {
  198. bizId: bid,
  199. action: "REJECT",
  200. opinion: "清单需调整",
  201. });
  202. check("A8 领导驳回", leaderReject.code === 200, leaderReject.data?.status);
  203. const exportAfterReject = await req("POST", "/api/payments/export-disk", {
  204. batchId: bid,
  205. bypassDeadline: true,
  206. });
  207. check(
  208. "A8 驳回后禁止出盘",
  209. exportAfterReject.code !== 200,
  210. exportAfterReject.code + " " + exportAfterReject.message
  211. );
  212. // A9 再提交领导并通过
  213. // 驳回后状态可能是 REJECTED / PENDING_STATUS_MAINTAIN 等,再走 submit-leader 或直接 leader PASS(按实现)
  214. let st = (await req("GET", "/api/batches/" + encodeURIComponent(bid))).data?.status;
  215. if (st === "REJECTED" || st === "PENDING_STATUS_MAINTAIN" || st === "PENDING_INSURANCE_MATCH") {
  216. const again = await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  217. if (again.code !== 200) {
  218. // 部分实现驳回后需重新居保
  219. await req("POST", "/api/insurance-match/trigger", { batchId: bid });
  220. await req("POST", "/api/batches/" + encodeURIComponent(bid) + "/submit-leader");
  221. }
  222. }
  223. const leaderPass = await req("POST", "/api/batches/leader-approve", {
  224. bizId: bid,
  225. action: "PASS",
  226. opinion: "同意出盘",
  227. });
  228. check(
  229. "A9 领导通过",
  230. leaderPass.code === 200 &&
  231. (leaderPass.data?.status === "READY_EXPORT" || leaderPass.data?.status === "EXPORTED"),
  232. leaderPass.data?.status
  233. );
  234. person = await getPerson(pidOk);
  235. check("A9 通过后人员仍正常", person?.bizStatus === "NORMAL", person?.bizStatus);
  236. // A10 出盘回盘归档
  237. const exp = await req("POST", "/api/payments/export-disk", {
  238. batchId: bid,
  239. bypassDeadline: true,
  240. });
  241. check("A10 出盘", exp.code === 200, exp.data?.fileName || exp.message);
  242. const ret = await req("POST", "/api/return-disk/confirm", {
  243. batchId: bid,
  244. manualAllSuccess: true,
  245. operator: "业务",
  246. });
  247. check("A10 回盘", ret.code === 200, ret.message);
  248. const ret2 = await req("POST", "/api/return-disk/confirm", {
  249. batchId: bid,
  250. manualAllSuccess: true,
  251. });
  252. check("A10 重复回盘拒绝", ret2.code !== 200, ret2.message);
  253. const arch = await req("POST", "/api/dashboard/archive/" + encodeURIComponent(bid));
  254. check("A10 归档", arch.code === 200 && arch.data?.status === "ARCHIVED", arch.data?.status);
  255. console.log("\n========== 场景B:居保 FAIL 不可跳过 ==========");
  256. console.log("payMonth=" + payB);
  257. await purgeMonth(payB);
  258. const pFail = await createPerson("门禁失败员", {
  259. remarks: "INSURANCE_FORCE_FAIL|联调强制失败",
  260. });
  261. const pidFail = pFail.data?.id;
  262. check("B1 建 FAIL 人员", !!pidFail, pidFail);
  263. const ensB = await ensureBatch(payB);
  264. const bidB = ensB.data?.id;
  265. await req("PUT", "/api/batches/" + encodeURIComponent(bidB) + "/members", {
  266. personnelIds: [pidFail],
  267. });
  268. await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit");
  269. await req("POST", "/api/batches/town-approve", {
  270. bizId: bidB,
  271. action: "PASS",
  272. opinion: "先过初审",
  273. });
  274. const matchFail = await req("POST", "/api/insurance-match/trigger", { batchId: bidB });
  275. check(
  276. "B2 居保产生 FAIL",
  277. matchFail.code === 200 && Number(matchFail.data?.failCount || 0) > 0,
  278. "fail=" + matchFail.data?.failCount + " next=" + matchFail.data?.nextStatus
  279. );
  280. check(
  281. "B2 批次停在待状态维护",
  282. matchFail.data?.nextStatus === "PENDING_STATUS_MAINTAIN",
  283. matchFail.data?.nextStatus
  284. );
  285. let pf = await getPerson(pidFail);
  286. check("B2 人员异常待处理", pf?.bizStatus === "ABNORMAL", pf?.bizStatus);
  287. check("B2 居保结果 FAIL", pf?.insuranceMatchResult === "FAIL", pf?.insuranceMatchResult);
  288. const skipLeader = await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit-leader");
  289. check("B3 FAIL 禁止提交领导", skipLeader.code !== 200, skipLeader.message);
  290. const skipLeaderApprove = await req("POST", "/api/batches/leader-approve", {
  291. bizId: bidB,
  292. action: "PASS",
  293. opinion: "强行通过",
  294. });
  295. check(
  296. "B3 FAIL 禁止领导直接通过",
  297. skipLeaderApprove.code !== 200,
  298. skipLeaderApprove.code + " " + skipLeaderApprove.message
  299. );
  300. // 人工确认后方可提交领导
  301. const manual = await req("POST", "/api/insurance-match/manual-confirm", {
  302. personnelId: pidFail,
  303. batchId: bidB,
  304. pass: true,
  305. operator: "业务",
  306. });
  307. check("B4 人工确认", manual.code === 200, manual.message);
  308. pf = await getPerson(pidFail);
  309. check("B4 人工确认后 NORMAL", pf?.bizStatus === "NORMAL", pf?.bizStatus);
  310. check(
  311. "B4 人工确认后 MANUAL_PASS",
  312. pf?.insuranceMatchResult === "MANUAL_PASS",
  313. pf?.insuranceMatchResult
  314. );
  315. const afterManual = await req("POST", "/api/batches/" + encodeURIComponent(bidB) + "/submit-leader");
  316. check(
  317. "B4 确认后可提交领导",
  318. afterManual.code === 200 && afterManual.data?.status === "PENDING_LEADER_APPROVE",
  319. afterManual.data?.status
  320. );
  321. console.log("\n========== 场景C:镇审驳回 + 暂停人员默认不纳入 ==========");
  322. const payC = "25" + String(Date.now()).slice(-4);
  323. await purgeMonth(payC);
  324. const pDraft = await createPerson("驳回测员");
  325. const pidDraft = pDraft.data?.id;
  326. const ensC = await ensureBatch(payC);
  327. const bidC = ensC.data?.id;
  328. await req("PUT", "/api/batches/" + encodeURIComponent(bidC) + "/members", {
  329. personnelIds: [pidDraft],
  330. });
  331. await req("POST", "/api/batches/" + encodeURIComponent(bidC) + "/submit");
  332. const townReject = await req("POST", "/api/batches/town-approve", {
  333. bizId: bidC,
  334. action: "REJECT",
  335. opinion: "材料不全",
  336. });
  337. check(
  338. "C1 镇审驳回",
  339. townReject.code === 200 && townReject.data?.status === "REJECTED",
  340. townReject.data?.status
  341. );
  342. const rejectLeader = await req("POST", "/api/batches/" + encodeURIComponent(bidC) + "/submit-leader");
  343. check("C1 驳回后不可提交领导", rejectLeader.code !== 200, rejectLeader.message);
  344. // 暂停人员:创建暂停特殊业务并审批后,默认不应再被 ensure 全选
  345. const pPause = await createPerson("暂停测员C");
  346. const pidPause = pPause.data?.id;
  347. // 先置为 NORMAL 再暂停(特殊业务要求)
  348. await req("PUT", "/api/personnel/status", {
  349. personnelId: pidPause,
  350. bizStatus: "NORMAL",
  351. payThisMonth: true,
  352. });
  353. const pauseBiz = await req("POST", "/api/special-biz", {
  354. bizType: "PAUSE",
  355. personnelId: pidPause,
  356. reason: "外出",
  357. });
  358. check("C2 创建暂停单", pauseBiz.code === 200, pauseBiz.message);
  359. if (pauseBiz.data?.id) {
  360. await req("POST", "/api/special-biz/leader-approve", {
  361. bizId: pauseBiz.data.id,
  362. action: "PASS",
  363. opinion: "同意暂停",
  364. });
  365. }
  366. const paused = await getPerson(pidPause);
  367. check("C2 暂停后 PAUSED", paused?.bizStatus === "PAUSED", paused?.bizStatus);
  368. const payD = "26" + String(Date.now()).slice(-4);
  369. await purgeMonth(payD);
  370. // 同时放入正常人 + 暂停人,选人应允许,但 ensure 默认不应带暂停人
  371. const ensD = await ensureBatch(payD);
  372. const bidD = ensD.data?.id;
  373. // 查默认明细是否含暂停人
  374. const pays = await req("POST", "/api/payments/details/page", {
  375. pageNum: 1,
  376. pageSize: 200,
  377. batchId: bidD,
  378. });
  379. const ids = (pays.data?.list || []).map((x) => x.personnelId);
  380. check(
  381. "C3 ensure 默认不含暂停人",
  382. !ids.includes(pidPause),
  383. "ids=" + ids.slice(0, 8).join(",")
  384. );
  385. console.log("\n========== 汇总 ==========");
  386. const fail = asserts.filter((a) => !a.pass);
  387. console.log("PASS=" + (asserts.length - fail.length) + " FAIL=" + fail.length + " TOTAL=" + asserts.length);
  388. for (const f of fail) {
  389. console.log(" - " + f.name + (f.detail ? " | " + f.detail : ""));
  390. }
  391. process.exit(fail.length ? 1 : 0);
  392. })().catch((e) => {
  393. console.error(e);
  394. process.exit(1);
  395. });