_verify_old_api_equivalence_e2e.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. #!/usr/bin/env python3
  2. """E2E-01: 旧 Business API 等价迁移统一验收编排。
  3. 1. 扫描 Web/ 无旧 api/business 直连
  4. 2. 串联运行 S1 / S2-S4 / S1-S4 full 子脚本
  5. 3. 校验 aidop_action_run_log 关键 action_code 存在
  6. """
  7. from __future__ import annotations
  8. import json
  9. import os
  10. import subprocess
  11. import sys
  12. import urllib.error
  13. import urllib.request
  14. from pathlib import Path
  15. ROOT = Path(__file__).resolve().parents[1]
  16. DOC = ROOT / "doc"
  17. WEB = ROOT / "Web"
  18. BASE = os.environ.get("AIDOP_API_BASE", "http://127.0.0.1:5005")
  19. TENANT = 797403760988229
  20. REQUIRED_ACTION_CODES = [
  21. "S1_ORDER_REVIEW",
  22. "S1_DELIVERY_CONFIRM",
  23. "S1_ORDER_REFRESH_PLAN",
  24. "S2_SCHEDULE_GENERATE",
  25. "S2_PRIORITY_RECHECK",
  26. "S2_KITTING_CHECK_BATCH",
  27. "S2_KITTING_CHECK_SINGLE",
  28. "S2_WORK_ORDER_RELEASE",
  29. "S4_MRP_GENERATE",
  30. "S3_DELIVERY_GENERATE",
  31. "S4_PROCUREMENT_PIPELINE",
  32. ]
  33. CHILD_SCRIPTS = [
  34. ("order_review", DOC / "_verify_order_review_e2e.py"),
  35. ("s2_s4_chain", DOC / "_verify_s2_s4_chain_e2e.py"),
  36. ("s1_s4_full", DOC / "_verify_s1_s4_full_e2e.py"),
  37. ("s3_delivery_18step", DOC / "_verify_s3_delivery_18step_e2e.py"),
  38. ]
  39. PREP_SCRIPT = DOC / "_prep_close_evidence.py"
  40. OLD_URL_PATTERNS = [
  41. "123.60.180.165:8087",
  42. "/api/business/",
  43. ]
  44. def scan_old_frontend_urls() -> list[str]:
  45. hits: list[str] = []
  46. if not WEB.exists():
  47. return hits
  48. skip_dirs = {"node_modules", "dist", ".git", ".pnpm"}
  49. for path in WEB.rglob("*"):
  50. if not path.is_file():
  51. continue
  52. if any(part in skip_dirs for part in path.parts):
  53. continue
  54. if path.suffix not in {".ts", ".vue", ".js", ".tsx", ".jsx"}:
  55. continue
  56. try:
  57. text = path.read_text(encoding="utf-8", errors="ignore")
  58. except OSError:
  59. continue
  60. for pat in OLD_URL_PATTERNS:
  61. if pat in text:
  62. hits.append(f"{path.relative_to(ROOT)}: {pat}")
  63. return hits
  64. def http_json(method: str, path: str, body=None, token: str | None = None):
  65. headers = {"Content-Type": "application/json", "Accept": "application/json"}
  66. if token:
  67. headers["Authorization"] = f"Bearer {token}"
  68. data = None if body is None else json.dumps(body).encode("utf-8")
  69. req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers, method=method)
  70. try:
  71. with urllib.request.urlopen(req, timeout=60) as resp:
  72. raw = resp.read().decode("utf-8")
  73. try:
  74. return resp.status, json.loads(raw)
  75. except json.JSONDecodeError:
  76. return resp.status, {"raw": raw[:2000]}
  77. except urllib.error.HTTPError as e:
  78. raw = e.read().decode("utf-8", errors="replace")
  79. try:
  80. return e.code, json.loads(raw)
  81. except json.JSONDecodeError:
  82. return e.code, {"raw": raw[:2000]}
  83. def login() -> str:
  84. node_script = r"""
  85. const { sm2 } = require('sm-crypto-v2');
  86. const PK = '0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312';
  87. console.log(JSON.stringify({ account: 'AIDOPDemo', password: sm2.doEncrypt('1234567890dop', PK, 1), tenantId: '797403760988229' }));
  88. """
  89. proc = subprocess.run(
  90. ["node", "-e", node_script],
  91. cwd=str(WEB),
  92. capture_output=True,
  93. text=True,
  94. timeout=30,
  95. )
  96. if proc.returncode != 0:
  97. raise RuntimeError(proc.stderr or proc.stdout)
  98. _, body = http_json("POST", "/api/sysAuth/login", json.loads(proc.stdout.strip()))
  99. token = (body.get("result") or {}).get("accessToken") or body.get("accessToken")
  100. if not token:
  101. raise RuntimeError(f"login failed: {body}")
  102. return token
  103. def parse_child_report(stdout: str) -> dict | None:
  104. text = (stdout or "").strip()
  105. if not text:
  106. return None
  107. try:
  108. return json.loads(text)
  109. except json.JSONDecodeError:
  110. start = text.find("{")
  111. end = text.rfind("}")
  112. if start >= 0 and end > start:
  113. try:
  114. return json.loads(text[start : end + 1])
  115. except json.JSONDecodeError:
  116. return None
  117. return None
  118. def scan_frontend_wiring() -> list[dict]:
  119. """CLOSE-E2E:检查关键能力是否已挂到前端页面(彻底打通之一)。"""
  120. checks: list[dict] = []
  121. def file_contains(rel: str, *needles: str) -> bool:
  122. path = ROOT / rel
  123. if not path.exists():
  124. return False
  125. text = path.read_text(encoding="utf-8", errors="ignore")
  126. return all(n in text for n in needles)
  127. def files_contain(rels: list[str], *needles: str) -> bool:
  128. text = ""
  129. for rel in rels:
  130. path = ROOT / rel
  131. if path.exists():
  132. text += path.read_text(encoding="utf-8", errors="ignore")
  133. return all(n in text for n in needles)
  134. wiring = [
  135. ("S1_order_review_page", "Web/src/views/aidop/business/orderList.vue", ("reviewSeOrder", "confirmSeOrderDelivery"), False),
  136. ("S2_schedule_page", "Web/src/views/aidop/production/workOrderSchedulingList.vue", ("productionSchedule",), False),
  137. (
  138. "S2_dispatch_page",
  139. [
  140. "Web/src/views/aidop/business/workOrderDispatchList.vue",
  141. "Web/src/views/aidop/business/workOrderDispatchForm.vue",
  142. ],
  143. ("onKitCheck", "releaseWorkOrder"),
  144. True,
  145. ),
  146. ("S2_single_kitting_page", "Web/src/views/aidop/business/workOrderDispatchList.vue", ("kittingCheckSingle",), False),
  147. ("S3_delivery_generate_page", "Web/src/views/aidop/s3/supply/deliveryScheduleList.vue", ("generateDeliverySchedule",), False),
  148. ("S3_procurement_pipeline_page", "Web/src/views/aidop/s3/supply/deliveryScheduleList.vue", ("executeProcurementPipeline",), False),
  149. ("S4_read_path_page", "Web/src/views/aidop/data-platform/actionRunLogs.vue", ("fetchS4ReadPathConsistency",), False),
  150. ]
  151. for item in wiring:
  152. name, rel, needles, multi = item
  153. if multi:
  154. ok = files_contain(rel, *needles)
  155. file_label = " + ".join(rel)
  156. else:
  157. ok = file_contains(rel, *needles)
  158. file_label = rel
  159. checks.append({"name": name, "file": file_label, "ok": ok, "needles": list(needles)})
  160. return checks
  161. def build_module_status(child_reports: dict[str, dict | None], wiring: list[dict]) -> dict:
  162. """汇总 S1-S4 打通状态:passed / 待对账 / 待补齐 / 需决策。"""
  163. modules: dict[str, dict] = {
  164. "S1": {"status": "待对账", "evidence": [], "gaps": []},
  165. "S2": {"status": "待对账", "evidence": [], "gaps": []},
  166. "S3": {"status": "待对账", "evidence": [], "gaps": []},
  167. "S4": {"status": "待对账", "evidence": [], "gaps": []},
  168. }
  169. def child_passed(name: str) -> bool:
  170. r = child_reports.get(name)
  171. return isinstance(r, dict) and bool(r.get("passed"))
  172. if child_passed("order_review"):
  173. modules["S1"]["evidence"].append("order_review_e2e")
  174. if child_passed("s1_s4_full"):
  175. modules["S1"]["evidence"].append("s1_s4_full_e2e")
  176. modules["S4"]["evidence"].append("s1_s4_full_e2e")
  177. if child_passed("s2_s4_chain"):
  178. modules["S2"]["evidence"].append("s2_s4_chain_e2e")
  179. modules["S3"]["evidence"].append("s2_s4_chain_e2e")
  180. modules["S4"]["evidence"].append("s2_s4_chain_e2e")
  181. if child_passed("s3_delivery_18step"):
  182. modules["S3"]["evidence"].append("s3_delivery_18step_e2e")
  183. wiring_map = {w["name"]: w["ok"] for w in wiring}
  184. if not wiring_map.get("S1_order_review_page"):
  185. modules["S1"]["gaps"].append("订单评审页未接线")
  186. if not wiring_map.get("S2_schedule_page") or not wiring_map.get("S2_dispatch_page"):
  187. modules["S2"]["gaps"].append("排程/下达页未接线")
  188. if not wiring_map.get("S2_single_kitting_page"):
  189. modules["S2"]["gaps"].append("单工单齐套无页面入口")
  190. if not wiring_map.get("S3_delivery_generate_page"):
  191. modules["S3"]["gaps"].append("交货计划生成无页面入口")
  192. if not wiring_map.get("S3_procurement_pipeline_page"):
  193. modules["S3"]["gaps"].append("采购闭环无页面入口")
  194. if not wiring_map.get("S4_read_path_page"):
  195. modules["S4"]["gaps"].append("S4读路径一致性无页面入口")
  196. modules["S2"]["gaps"].append("库存占用:需决策")
  197. modules["S3"]["gaps"].append("真实SAP/QAD:需决策")
  198. modules["S1"]["gaps"].append("替代料口径:需决策")
  199. decision_only = lambda gaps: not gaps or all("需决策" in g for g in gaps)
  200. required_evidence = {
  201. "S1": {"order_review_e2e", "s1_s4_full_e2e"},
  202. "S2": {"s2_s4_chain_e2e"},
  203. "S3": {"s3_delivery_18step_e2e"},
  204. "S4": {"s1_s4_full_e2e", "s2_s4_chain_e2e"},
  205. }
  206. for key, mod in modules.items():
  207. if mod["gaps"] and any("未接线" in g or "无页面" in g for g in mod["gaps"]):
  208. mod["status"] = "待补齐"
  209. elif not mod["evidence"]:
  210. mod["status"] = "待补齐"
  211. elif set(mod["evidence"]) >= required_evidence[key] and decision_only(mod["gaps"]):
  212. mod["status"] = "已打通"
  213. elif mod["evidence"]:
  214. mod["status"] = "待对账"
  215. return modules
  216. def run_child(name: str, script: Path) -> dict:
  217. if not script.exists():
  218. return {"name": name, "ok": False, "error": f"missing script: {script}"}
  219. proc = subprocess.run(
  220. [sys.executable, str(script)],
  221. cwd=str(ROOT),
  222. capture_output=True,
  223. text=True,
  224. timeout=300,
  225. )
  226. parsed = parse_child_report(proc.stdout or "")
  227. result = {
  228. "name": name,
  229. "ok": proc.returncode == 0,
  230. "exit_code": proc.returncode,
  231. "parsed": parsed,
  232. "stdout_tail": (proc.stdout or "")[-4000:],
  233. "stderr_tail": (proc.stderr or "")[-2000:],
  234. }
  235. if isinstance(parsed, dict):
  236. result["child_passed"] = parsed.get("passed")
  237. result["child_checks"] = parsed.get("checks")
  238. return result
  239. def check_action_logs(token: str) -> dict:
  240. checks = []
  241. for code in REQUIRED_ACTION_CODES:
  242. st, body = http_json(
  243. "GET",
  244. f"/api/Aidop/action-run-log/list?actionCode={code}&page=1&pageSize=1",
  245. token=token,
  246. )
  247. total = 0
  248. if isinstance(body, dict):
  249. total = body.get("total") or 0
  250. checks.append({"action_code": code, "http": st, "total": total, "ok": st == 200 and total > 0})
  251. return {"checks": checks, "ok": all(c["ok"] for c in checks)}
  252. def main():
  253. report = {
  254. "matrix_doc": "doc/plan/旧Business API等价验收矩阵.md",
  255. "steps": [],
  256. "checks": [],
  257. }
  258. url_hits = scan_old_frontend_urls()
  259. report["steps"].append({"step": "old_url_scan", "hits": url_hits, "ok": len(url_hits) == 0})
  260. report["checks"].append({"name": "no_old_frontend_url", "ok": len(url_hits) == 0, "detail": url_hits})
  261. prep_result = run_child("prep_close_evidence", PREP_SCRIPT)
  262. report["steps"].append({"step": "prep_close_evidence", **prep_result})
  263. report["checks"].append({"name": "prep_close_evidence", "ok": prep_result.get("ok", False)})
  264. child_results = []
  265. child_reports: dict[str, dict | None] = {}
  266. for name, script in CHILD_SCRIPTS:
  267. result = run_child(name, script)
  268. child_results.append(result)
  269. child_reports[name] = result.get("parsed") if isinstance(result.get("parsed"), dict) else None
  270. report["steps"].append({"step": f"child_{name}", **result})
  271. report["checks"].append({"name": f"child_{name}", "ok": result.get("ok", False)})
  272. wiring = scan_frontend_wiring()
  273. report["steps"].append({"step": "frontend_wiring", "checks": wiring, "ok": all(c["ok"] for c in wiring)})
  274. report["checks"].append({
  275. "name": "frontend_wiring",
  276. "ok": all(c["ok"] for c in wiring),
  277. "detail": [c for c in wiring if not c["ok"]],
  278. })
  279. report["module_status"] = build_module_status(child_reports, wiring)
  280. report["gaps"] = {
  281. k: v.get("gaps", [])
  282. for k, v in report["module_status"].items()
  283. }
  284. try:
  285. token = login()
  286. log_check = check_action_logs(token)
  287. report["steps"].append({"step": "action_log_codes", **log_check})
  288. report["checks"].append({
  289. "name": "action_log_codes",
  290. "ok": log_check["ok"],
  291. "detail": [c for c in log_check["checks"] if not c["ok"]],
  292. })
  293. except Exception as ex:
  294. report["steps"].append({"step": "action_log_codes", "ok": False, "error": str(ex)})
  295. report["checks"].append({"name": "action_log_codes", "ok": False, "detail": str(ex)})
  296. report["passed"] = all(c["ok"] for c in report["checks"])
  297. out = DOC / "_verify_old_api_equivalence_e2e_result.json"
  298. out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  299. print(json.dumps(report, ensure_ascii=False, indent=2))
  300. sys.exit(0 if report["passed"] else 5)
  301. if __name__ == "__main__":
  302. main()