/** * 键集交叉核对:**前端能发出的 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;