You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
6.7 KiB
171 lines
6.7 KiB
# -*- 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<T>", page.group(1)
|
|
listed = re.search(r"List<([A-Za-z][A-Za-z0-9_]*)>", response)
|
|
if listed:
|
|
return "List<T>", 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()
|
|
|