fe-update-keycheck.mjs 6.7 KB

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