#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ P2 审计脚本:**按接口**去查「这个 Mapper 到底绑没绑」。 ## 为什么需要它(第二十七切片的血债) 第二十七切片迁 `/park/xc-car-charge-audit/query-last-xc-car-charge-audit` 时撞上 Invalid bound statement (not found): com.zteits.park.mapper.ParkingLotWhitelistMapper.selectByExample 新栈整包**没有**这个 namespace —— 但**老门户上这条链路是通的**,所以不是存量缺陷(红线⑥ 不适用),是搬丢了。根因是**一份 XML 用了别人的文件名**: zteits-park/src/main/resources/mybatis/mapper/HubParkingEquipmentMapper.xml └── ← 表也是 parking_lot_whitelist 老仓那一份文件同时是「`ParkingLotWhitelistMapper` 的**唯一**宿主」和「`HubParkingEquipmentMapper` 的一个空壳名」(后者在**老仓压根没绑定** —— 存量缺陷)。新栈按 `hub_parking_equipment` 表 **重新生成**了同名 XML,namespace 与表名都归正、顺手修掉了老仓那个缺陷,但那份文件里唯一装着的 11 条 whitelist 语句也随之被覆盖消失。⇒ 「查某个 mapper 的 XML 在不在」**不能按文件名找**。 `tools/p2_audit_mapper_ns.py` 查的是「XML 的 namespace 在不在 @MapperScan 的包下」, 方向相反 —— 它看不见「某个接口该有的 XML 没搬过来」。本脚本就是补这个方向的缺口。 ## 判定口径(必须写死,否则没人能复现) **扫描面**:两栈各 8 个模块的 `src/`(老仓对应 `zteits-{pay,order,bill,park,charge,sys,business}`), 文件名以 `Mapper.java` 结尾且含 `interface` 的算「Mapper 接口」;XML 取 ``。 **口径**:只统计 namespace 以 `com.zteits.{bill,business,charge,order,park,pay,sys}.mapper` 打头的 —— 这正是 `ParkingServerApplication` 第 2 条 `@MapperScan` 的 7 个包(与 `p2_audit_mapper_ns.py` 的 `SCAN_PACKAGES` 同源)。这一刀同时把 MyBatis-Plus 的 `com.zteits.parking.module.system.dal.mysql.*`(框架自带、无 XML)和 tk 的 `com.zteits.{sys,business}.util.MyMapper`(标记接口)挡在外面。 **四类结果**: | 类 | 含义 | 期望 | |---|---|---| | OK | 新栈有 `namespace == 接口 FQCN` 的 XML | 610 个左右 | | **A 丢搬** | 老栈有该 namespace、新栈没有 ⇒ **移植回归** | **必须 0** | | B 存量就断 | 两栈都没有绑得上的 XML(老仓当年就断) | 留痕,逐个看是否死代码 | | C 新栈独有 | 接口只在新栈存在(老仓没有)⇒ WIP 或死代码 | 留痕,看调用方 | **退出码**:A > 0 才 `exit 1`。B/C 是存量事实/WIP,不是本脚本要拦的东西。 ## B 类的两个子型(都是存量事实,别当回归修) * **namespace 写错**:老 XML 在、但 `` 与接口所在包不一致。 例:`zteits-pay/.../RealtimeInfoSMapper.xml` 的 namespace 是 `com.zteits.park.mapper.smapper.RealtimeInfoSMapper`,而接口在 `com.zteits.pay.mapper.smapper` —— 老门户调它同样是 `Invalid bound statement`,新栈**照搬**了这个事实。 * **压根没有 SQL**:接口有、两栈都没有任何 XML。例:`TdBOrderIncomeCalculationSMapper`。 ## C 类要当心:不是所有 C 都是死代码 `ParkingLotExtendAdapterMapper` 的**唯一**引用者 `ParkingLotExtendAdapterCache` 本身零引用 ⇒ 真死。 但 `TdCCouponRuleCustidCarnumRelMapper` 被 `TdCCustPersonServiceImpl` 的 `queryPersonAndCarNumForPageByUserPhone` / `saveCardNoAndCustIdAndCarNum` 调用, 而这两个方法**老仓没有**、在新栈是**新写的**(WIP)—— 一旦有人给它配个 Controller, 第一次调用就是 `Invalid bound statement` 500。本脚本把「调用方」列出来,就是为了让这条别被漏掉。 ## 用法 python3 -I tools/p2_audit_mapper_bind.py python3 -I tools/p2_audit_mapper_bind.py --legacy-dir /path/to/zteits-clouds """ import argparse import os import re import sys CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_LEGACY = "/Users/wangbiao/IdeaProjects/parking/zteits-clouds" # 与 ParkingServerApplication 的 @MapperScan / p2_audit_mapper_ns.py 的 SCAN_PACKAGES 同源 PKGS = ["bill", "business", "charge", "order", "park", "pay", "sys"] PREFIXES = tuple("com.zteits.%s.mapper" % k for k in PKGS) NEW_MODULES = ["parking-module-park", "parking-module-order", "parking-module-pay", "parking-module-bill", "parking-module-charge", "parking-module-business", "parking-module-system", "parking-module-infra"] LEGACY_MODULES = ["zteits-pay", "zteits-order", "zteits-bill", "zteits-park", "zteits-charge", "zteits-sys", "zteits-business"] SKIP_DIR_NAMES = {"target", "out", "node_modules", ".git"} NS_RE = re.compile(r'namespace\s*=\s*"([^"]+)"') PKG_RE = re.compile(r'package\s+([\w.]+);') def _walk(roots, ext): for root0 in roots: if not os.path.isdir(root0): continue for root, dirs, files in os.walk(root0): dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES] for name in files: if name.endswith(ext): yield os.path.join(root, name) def collect(roots): """→ (接口 FQCN 集合, {namespace: [xml 路径]})""" ifaces, namespaces = set(), {} for p in _walk(roots, ".java"): if not p.endswith("Mapper.java"): continue src = open(p, encoding="utf-8", errors="surrogateescape").read() m = PKG_RE.search(src) if m and re.search(r"\binterface\b", src): ifaces.add(m.group(1) + "." + os.path.basename(p)[:-5]) for p in _walk(roots, ".xml"): head = open(p, encoding="utf-8", errors="surrogateescape").read(8192) m = NS_RE.search(head) if m: namespaces.setdefault(m.group(1), []).append(p) return ifaces, namespaces def callers_of(simple_name, roots): """新栈里引用了这个简单名的文件(排除同名文件自己)。 ⚠️ 纯文本匹配、不做语义分析:命中可能是注释、可能是同名的别的类型。所以下面只拿它当 「有没有人提过这个名字」的粗筛,结论要顺着引用链再走一跳(`dead_chain`)。 """ hits = [] for p in _walk(roots, ".java"): if os.path.basename(p) == simple_name + ".java": continue if simple_name in open(p, encoding="utf-8", errors="surrogateescape").read(): hits.append(p) return hits def dead_chain(caller_path, roots): """引用方的类名**自己**有没有人引用?没有 ⇒ 整条链是死的(第二十七切片实测: `ParkingLotExtendAdapterMapper` ← `ParkingLotExtendAdapterCache` ← 无人引用)。""" return not callers_of(os.path.basename(caller_path)[:-5], roots) def misnamed_legacy_xml(legacy, namespaces): """老仓「文件名 != namespace 末段(忽略大小写)」的 XML —— 按文件名搬运必丢的那批。""" out = [] for ns, paths in namespaces.items(): simple = ns.rsplit(".", 1)[-1] for p in paths: stem = os.path.basename(p)[:-4] if stem.lower() != simple.lower(): out.append((ns, stem, os.path.relpath(p, legacy))) return sorted(out) def main(): ap = argparse.ArgumentParser() ap.add_argument("--legacy-dir", default=DEFAULT_LEGACY) ap.add_argument("--show-ok", action="store_true", help="连绑好的接口一起列(610 行,一般不用)") args = ap.parse_args() new_roots = [os.path.join(CLOUD, m, "src") for m in NEW_MODULES] legacy_roots = [os.path.join(args.legacy_dir, m, "src") for m in LEGACY_MODULES] if not os.path.isdir(args.legacy_dir): print("!! 老仓目录不存在:%s(用 --legacy-dir 指定)" % args.legacy_dir) return 2 new_if, new_ns = collect(new_roots) leg_if, leg_ns = collect(legacy_roots) new_if = {x for x in new_if if x.startswith(PREFIXES)} leg_if = {x for x in leg_if if x.startswith(PREFIXES)} bound = sorted(x for x in new_if if x in new_ns) unbound = sorted(x for x in new_if if x not in new_ns) cls_a = [x for x in unbound if x in leg_if and x in leg_ns] # 老有、新丢 cls_b = [x for x in unbound if x in leg_if and x not in leg_ns] # 两栈都断 cls_c = [x for x in unbound if x not in leg_if] # 新栈独有 print("口径:namespace 以 %s 打头" % (" / ".join(PREFIXES))) print(" 新栈 mapper 接口 %d(已绑定 %d,未绑定 %d)" % (len(new_if), len(bound), len(unbound))) print(" 老栈 mapper 接口 %d,老栈 namespace %d" % (len(leg_if), len(leg_ns))) if args.show_ok: print("\n== OK(新栈有同名 namespace 的 XML)==") for x in bound: print(" " + x) print("\n== A 丢搬(老有 namespace、新栈没有)== 必须 0") if cls_a: for x in cls_a: print(" !! %s" % x) for p in leg_ns[x]: print(" 老仓 XML:%s" % os.path.relpath(p, args.legacy_dir)) print(" ⇒ 这些是**移植回归**:老门户上通、新栈 500。按文件名找 XML 会漏,") print(" 要去老仓 grep `namespace=\"\"` 定位真正装它的那份文件。") else: print(" ✔ 0 个") print("\n== B 存量就断(两栈都没有绑得上的 XML)== 存量事实,留痕不修(红线⑥)") if cls_b: for x in cls_b: simple = x.rsplit(".", 1)[-1] same_name = [p for p in _walk(legacy_roots, ".xml") if os.path.basename(p)[:-4].lower() == simple.lower()] if same_name: wrong = [] for p in same_name: head = open(p, encoding="utf-8", errors="surrogateescape").read(8192) m = NS_RE.search(head) wrong.append(m.group(1) if m else "?") print(" · %s" % x) print(" 老仓有同名 XML,但 namespace 写成了:%s" % " / ".join(wrong)) else: print(" · %s(两栈都没有任何同名 XML:接口有、SQL 压根没写过)" % x) else: print(" ✔ 0 个") print("\n== C 新栈独有(老仓没有这个接口)== 老仓没得抄,看调用方:WIP 还是死代码") if cls_c: for x in cls_c: simple = x.rsplit(".", 1)[-1] callers = callers_of(simple, new_roots) print(" · %s" % x) if not callers: print(" ✔ 零引用 ⇒ 死代码") continue for p in callers: rel = os.path.relpath(p, CLOUD) if dead_chain(p, new_roots): print(" 被引用:%s" % rel) print(" └ 但**它自己**零引用 ⇒ 死链,不可达(真死代码)") else: print(" 被引用:%s" % rel) print(" └ ★ 它自己有引用方 ⇒ **可达**:谁来调这条链路,") print(" 第一次就是 `Invalid bound statement` 500(WIP 没补 XML)") else: print(" ✔ 0 个") mis = misnamed_legacy_xml(args.legacy_dir, leg_ns) print("\n== 附录:老仓「文件名 != namespace」的 XML %d 份(忽略大小写比较)==" % len(mis)) print(" 这份清单 = 「按文件名找 XML 会踩空」的候选。列在这里**不等于** namespace 丢了 ——") print(" 是否真丢,看上面的 A 类;这里只说明文件名不是绑定关系的可靠索引。") for ns, stem, rel in mis: print(" %-50s <- %s" % (ns, rel)) print(" 第二十七切片补搬的正是 `ParkingLotWhitelistMapper` 那条:老文件名是") print(" HubParkingEquipment(新栈按该表重生成了同名文件 ⇒ 覆盖掉这条 namespace)。") print(" 另一条 `TbAreaMangeSMapper.xml` 是**拼写**错(Mange/Manage),不是大小写,") print(" 两栈都原样带着这个文件名 ⇒ 它绑得好好的(A 类里没有它)。") return 1 if cls_a else 0 if __name__ == "__main__": sys.exit(main())