# -*- coding: utf-8 -*- """生成 A4 响应描述缺口的静态普查报告。""" import importlib.util import json import re from collections import Counter from pathlib import Path ROOT = Path(r"d:\code\crm-backend-matt") EFFORT = ROOT / ".scratch" / "a4-doc-fields" E2E = ROOT / ".scratch" / "customer-e2e" SOURCES = [ROOT / "crm-customer" / "src" / "main" / "java", ROOT / "crm-rule" / "src" / "main" / "java"] def load_defs(): spec = importlib.util.spec_from_file_location("defs_a4", E2E / "defs_a4.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.DEFS def load_specimens(): specimens = {} for name in ("specimens-core.json", "specimens-heavy.json"): with (E2E / name).open(encoding="utf-8") as source: specimens.update(json.load(source)) return specimens def specimen_for(definition, specimens): path = definition["path"] candidates = [path] if "{workspace}" in path: candidates = [path.replace("{workspace}", workspace) for workspace in ("mine", "overview", "pool")] for candidate in candidates: for normalized in (candidate, re.sub(r"\{(id|customerId|memberUserId|taskId)\}", "{id}", candidate)): found = specimens.get(f"{definition['method']} {normalized}") if found is not None: return found return None def classify(response): page = re.search(r"PageResult<([A-Za-z][A-Za-z0-9_]*)>", response) if page: return "PageResult", page.group(1) listed = re.search(r"List<([A-Za-z][A-Za-z0-9_]*)>", response) if listed: return "List", listed.group(1) if response.startswith("null"): return "空响应", None if response.startswith("非信封") or response.startswith("文字"): return "非标准响应", None if response.startswith("新") and "id" in response: return "原始标量", None if response.startswith("任务 taskId"): return "原始标量", None if response.startswith("BatchResult"): return "内联结构", "BatchResult" if response.startswith("CustomerReminderRule{"): return "内联结构", "CustomerReminderRule" name = re.match(r"([A-Za-z][A-Za-z0-9_]*)", response) return ("单个 DTO", name.group(1)) if name else ("非标准响应", None) def source_index(): index = {} for source_root in SOURCES: for path in source_root.rglob("*.java"): index.setdefault(path.stem, []).append(path) return index def definition_lines(): lines = (E2E / "defs_a4.py").read_text(encoding="utf-8").splitlines() return { name: number for number, line in enumerate(lines, 1) for name in re.findall(r"dict\([^\n]*name='([^']+)'", line) } def relative(path): return path.relative_to(ROOT).as_posix() def main(): definitions = load_defs() specimens = load_specimens() missing = [item for item in definitions if ((specimen_for(item, specimens) or {}).get("response") or {}).get("data") is None] sources = source_index() lines = definition_lines() rows = [] for item in missing: kind, type_name = classify(item["resp"]) matches = sources.get(type_name, []) if type_name else [] rows.append({"item": item, "kind": kind, "type": type_name, "matches": matches, "line": lines[item["name"]]}) out = [ "# A4 响应字段缺口返回类型普查", "", "依据 `.scratch/customer-e2e/defs_a4.py` 与两份 specimens JSON 静态计算;缺口定义为未取得 `response.data` 的端点。源码命中范围为 `crm-customer` 与 `crm-rule` 的 `src/main/java`。", "", "## 汇总", "", f"- A4 定义端点总数:{len(definitions)}", f"- 已有 E2E `response.data`、生成器可展开字段表:{len(definitions) - len(rows)}", f"- 当前缺口:{len(rows)}", "", "| 返回形态 | 端点数 |", "| --- | ---: |", ] for kind, count in sorted(Counter(row["kind"] for row in rows).items()): out.append(f"| {kind} | {count} |") out.extend(["", "## 各类代表端点", ""]) for kind in sorted({row["kind"] for row in rows}): examples = [row for row in rows if row["kind"] == kind][:2] out.append(f"- {kind}:" + ";".join( f"`{row['item']['method']} {row['item']['path']}`(`defs_a4.py:{row['line']}`)" for row in examples )) source_candidates = [row for row in rows if row["type"] and row["kind"] not in ("内联结构",)] unique = [row for row in source_candidates if len(row["matches"]) == 1] missing_source = [row for row in source_candidates if len(row["matches"]) == 0] ambiguous = [row for row in source_candidates if len(row["matches"]) > 1] out.extend([ "", "## Java 类型名直查结果", "", f"- 可按 `resp` 中类型名唯一命中 `.java` 文件:{len(unique)} 条。", f"- 找不到同名 `.java` 文件:{len(missing_source)} 条。", f"- 同名文件歧义:{len(ambiguous)} 条。", "", ]) if missing_source: out.extend(["### 未命中类型", ""]) for row in missing_source: item = row["item"] out.append(f"- `{item['method']} {item['path']}` — `{row['type']}`;`resp`:{item['resp']}") out.append("") if ambiguous: out.extend(["### 歧义类型", ""]) for row in ambiguous: item = row["item"] locations = "、".join(f"`{relative(path)}`" for path in row["matches"]) out.append(f"- `{item['method']} {item['path']}` — `{row['type']}`:{locations}") out.append("") out.extend(["## 逐端点清单", ""]) for number, row in enumerate(rows, 1): item = row["item"] out.extend([ f"### {number:02d}. {item['name']}", "", f"- 对账键:`{item['method']} {item['path']}`(`defs_a4.py:{row['line']}`)", f"- 返回描述:`{item['resp']}`", f"- 分类:{row['kind']}" + (f";候选类型:`{row['type']}`" if row["type"] else ""), ]) if row["matches"]: out.append("- 源码命中:" + "、".join(f"`{relative(path)}`" for path in row["matches"])) elif row["type"]: out.append("- 源码命中:无") out.append("") (EFFORT / "endpoint-response-survey.md").write_text("\n".join(out), encoding="utf-8", newline="\n") print(f"defs={len(definitions)} expanded={len(definitions) - len(rows)} missing={len(rows)}") print(f"source unique={len(unique)} missing={len(missing_source)} ambiguous={len(ambiguous)}") if __name__ == "__main__": main()