| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- /**
- * 键集交叉核对:**前端能发出的 39 个 wire 键** vs **后端 updateMerchant 真正读的 50 个键**。
- *
- * 为什么要有这一件:键名拼错在前端**不会**当场报错 —— 它只会变成后端
- * `FAIL_ON_UNKNOWN_PROPERTIES=true` 下的一个 HTTP 500,或者更糟:键名拼对了但白名单漏了,
- * 于是「用户填了却没提交」。这两种都得靠集合运算抓,不能靠人眼扫 39 行。
- *
- * ★ 全程只在 **node 一个进程**里做,不走 awk/sed/sort/comm 那串管道。
- * 上一版用管道量出来的读数是自相矛盾的(前端 39 个键「一个都不在」后端的 50 个里,
- * 同时「后端有前端没有」= 50)⇒ 那是量具坏了,不是世界坏了。管道里每一环都可能
- * 带进 CR 或改掉排序,而 `comm` 对 CR 是**逐字节**敏感的、对排序规则也敏感。
- *
- * 用法:node fe-update-keycheck.mjs
- */
- import fs from "node:fs";
- import { createRequire } from "node:module";
- import { execFileSync } from "node:child_process";
- import { fileURLToPath } from "node:url";
- const ROOT = "D:/project2/payment-platform/";
- const SVC = ROOT + "java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingService.java";
- // ---------- 后端:updateMerchant 方法体里的 putIfNotBlank(p, "键") ----------
- const svcSrc = fs.readFileSync(SVC, "utf8");
- // 先定位方法体:从 `void updateMerchant(` 起,到下一个「顶格两空格的 `}` 收尾」为止。
- // 不写死行号 —— 行号会漂,而这条断言要在每次改动后都能重跑。
- const start = svcSrc.indexOf("void updateMerchant(");
- if (start < 0) throw new Error("找不到 updateMerchant —— 量具失效,别信任何读数");
- const bodyStart = svcSrc.indexOf("{", start);
- let depth = 0;
- let end = -1;
- for (let i = bodyStart; i < svcSrc.length; i++) {
- if (svcSrc[i] === "{") depth++;
- else if (svcSrc[i] === "}") {
- depth--;
- if (depth === 0) {
- end = i;
- break;
- }
- }
- }
- if (end < 0) throw new Error("updateMerchant 方法体没有闭合 —— 量具失效");
- const body = svcSrc.slice(bodyStart, end);
- // ★★ 线上真实形状是**三参**:`putIfNotBlank(p, "merchant_name", dto.getMerchantName());`
- // —— 所以正则必须吃到第二个逗号为止,**不能**写成「字符串后紧跟 `)`」。
- // 上一版就是栽在这里:那支正则一个都匹配不上,读数 0,而**另一个坏量具**(`grep -oE` 的
- // 匹配含结尾引号 + `sed 's/.*"//'` 的 `.*` 贪婪吃掉到最后那个引号)吐出了 50 行**空串**,
- // `wc -l` 报 50 —— 恰好等于预期的键数,于是「50」看起来是对的。见文件头 ★。
- const beKeys = [...body.matchAll(/putIfNotBlank\(\s*p\s*,\s*"([A-Za-z_][A-Za-z0-9_]*)"\s*,/g)].map((m) => m[1]);
- // 正对照(硬):全文件 `putIfNotBlank(` 的出现次数应当 = **调用点数 + 1 个方法定义**。
- // 这条同时证明两件事:① 这支正则不是空转;② updateMerchant 里的调用点数 == 全文件调用点数,
- // 即**没有**任何一处 putIfNotBlank 落在 updateMerchant 之外(漏掉就等于漏键)。
- const allOccur = [...svcSrc.matchAll(/putIfNotBlank\(/g)].length;
- const DEFINITION_SITES = 1;
- console.log(`[后端] updateMerchant 方法体字节数=${end - bodyStart}`);
- console.log(`[后端] 全文件 putIfNotBlank( 出现次数=${allOccur}(= 定义 1 处 + 调用 ${allOccur - DEFINITION_SITES} 处)`);
- console.log(`[后端] updateMerchant 内提取到 ${beKeys.length} 个键,去重后 ${new Set(beKeys).size} 个`);
- // ---------- 前端:跑台子把 UPDATE_KEYS 倒出来 ----------
- // ★ 2026-09-23 搬家后改成**相对本文件**定位。原先写死的是批次工作区的绝对路径,
- // 而工作区是 git-ignored 的 scratch(`git clean -fdx` 会毁掉它),源文件已随搬家删除
- // ⇒ 再照旧写绝对路径就会指向一个**不存在的文件**(正是「文件在、跑不动」那类坏法)。
- // 这两支是一对,跟着彼此走,所以用自身位置定位。
- const HARN = fileURLToPath(new URL("./fe-update-harness.mjs", import.meta.url));
- const PAGE = ROOT + "frontend/src/views/module_payment/collection/onboarding/index.vue";
- const out = execFileSync(process.execPath, [HARN, PAGE, "keycheck", "--dump-keys"], { encoding: "utf8" });
- const line = out.split(/\r?\n/).find((l) => l.startsWith("KEYS_JSON "));
- if (!line) throw new Error("台子没吐出 KEYS_JSON —— 量具失效");
- const feKeys = JSON.parse(line.slice("KEYS_JSON ".length));
- console.log(`[前端] 台子实跑倒出 ${feKeys.length} 个键`);
- // ---------- 集合运算 ----------
- const be = new Set(beKeys);
- const fe = new Set(feKeys);
- const feNotBe = feKeys.filter((k) => !be.has(k)).sort();
- const beNotFe = [...be].filter((k) => !fe.has(k)).sort();
- console.log("");
- console.log("=== 前端有、后端没有(**必须为空**:非空 = 拼错的键名 ⇒ 线上 HTTP 500)===");
- console.log(feNotBe.length ? feNotBe.join("\n") : "(空)");
- console.log("");
- console.log("=== 后端有、前端没有 ===");
- console.log(beNotFe.join("\n"));
- console.log(`个数 = ${beNotFe.length}`);
- // ---------- 断言 ----------
- let bad = 0;
- const assert = (name, cond, extra = "") => {
- console.log(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
- if (!cond) bad++;
- };
- assert(
- "A0 正控:全文件 putIfNotBlank( 出现次数 == 调用点数 + 1 个定义(证正则不空转、且无调用落在 updateMerchant 之外)",
- allOccur - DEFINITION_SITES === beKeys.length,
- `全文件出现=${allOccur} → 调用点=${allOccur - DEFINITION_SITES},方法体内提取=${beKeys.length}`
- );
- assert("A1 前端没有拼错的键(前端键集 ⊆ 后端键集)", feNotBe.length === 0, `多出=${JSON.stringify(feNotBe)}`);
- assert("A2 后端键集无重复(50 次调用 = 50 个不同键)", new Set(beKeys).size === beKeys.length);
- assert("A3 前端键集无重复", fe.size === feKeys.length);
- assert("A4 后端收 50 个", beKeys.length === 50, `actual=${beKeys.length}`);
- assert("A5 前端发 39 个", feKeys.length === 39, `actual=${feKeys.length}`);
- assert("A6 差恰好 11 个", beNotFe.length === 11, `actual=${beNotFe.length}`);
- assert(
- "A7 差的 11 个 = 10 个无入口字段 + rate_code",
- JSON.stringify(beNotFe) ===
- JSON.stringify(
- [
- "account_idcard_stt",
- "account_type",
- "actual_addr",
- "business_scope",
- "check_code",
- "fSettle_account_type",
- "legal_cert_stt",
- "license_stt",
- "p_cashier",
- "rate_code",
- "reg_capital",
- ].sort()
- ),
- `actual=${JSON.stringify(beNotFe)}`
- );
- console.log(`SUMMARY keycheck: ${bad === 0 ? "全绿" : bad + " 条红"}`);
- process.exitCode = bad === 0 ? 0 : 1;
|