extract_api.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. # -*- coding: utf-8 -*-
  2. """扫描萤火商城 PHP 源码,导出接口清单(控制器+方法)为 CSV 和 Markdown"""
  3. import os, re, csv
  4. ROOT = r"F:\shop\shop\app"
  5. OUT = str(ROOT)
  6. MODULES = ["admin", "api", "store", "timer"]
  7. MAGIC = re.compile(r"^__")
  8. def snake(name: str) -> str:
  9. return re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name).lower()
  10. rows = []
  11. for module in MODULES:
  12. base = os.path.join(ROOT, module, "controller")
  13. for dirpath, _, files in os.walk(base):
  14. for fn in sorted(files):
  15. if not fn.endswith(".php"):
  16. continue
  17. path = os.path.join(dirpath, fn)
  18. rel = os.path.relpath(path, base).replace("\\", "/")
  19. text = open(path, encoding="utf-8", errors="replace").read()
  20. # 类名
  21. m = re.search(r"class\s+(\w+)", text)
  22. if not m:
  23. continue
  24. cls = m.group(1)
  25. # 控制器 URL(TP6 多级控制器用点号连接,全部转下划线)
  26. # 例:goods/Goods.php → "goods";goods/Comment.php → "goods.comment"
  27. parts = rel.split("/")
  28. parts[-1] = parts[-1][:-4]
  29. ctrl_url = ".".join(snake(p) for p in parts)
  30. # 逐方法解析
  31. for m in re.finditer(r"(/\*\*(.*?)\*/)?\s*(?:protected|public|private)?\s*function\s+(\w+)\s*\(([^)]*)\)", text):
  32. comment, body_args, name = m.group(2), m.group(4), m.group(3)
  33. if MAGIC.match(name):
  34. continue
  35. # 只统计 public(ThinkPHP 路由可达)
  36. before = text[: m.start()]
  37. if "protected" in m.group(0) or "private" in m.group(0):
  38. continue
  39. brief = ""
  40. if comment:
  41. for line in comment.splitlines():
  42. line = line.strip().lstrip("*").strip()
  43. if line and not line.startswith("@"):
  44. brief = line
  45. break
  46. action = snake(name)
  47. url = f"/{module}/" + (ctrl_url + "/" if ctrl_url else "") + action
  48. line_no = text[: m.start()].count("\n") + 1
  49. rows.append({
  50. "module": module,
  51. "controller": cls,
  52. "controller_file": rel,
  53. "controller_url": ctrl_url,
  54. "action": name,
  55. "url": url,
  56. "args": " ".join(body_args.split()),
  57. "comment": brief,
  58. "line": line_no,
  59. })
  60. rows.sort(key=lambda r: (MODULES.index(r["module"]), r["controller_file"], r["line"]))
  61. os.makedirs(OUT, exist_ok=True)
  62. csv_path = os.path.join(OUT, "API清单.csv")
  63. with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
  64. w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
  65. w.writeheader()
  66. w.writerows(rows)
  67. md_path = os.path.join(OUT, "API清单.md")
  68. with open(md_path, "w", encoding="utf-8") as f:
  69. f.write("# 萤火商城接口清单(Java 重写对齐基线)\n\n")
  70. f.write("> 自动扫描自 `F:\\shop\\shop`(ThinkPHP 6 多应用路由:`/模块/控制器/方法`,"
  71. "多级控制器用点号连接)。响应统一为 `{\"status\":200,\"message\":\"success\",\"data\":{...}}`。\n\n")
  72. total = len(rows)
  73. f.write(f"**接口总数:{total}**\n\n")
  74. for module in MODULES:
  75. sub = [r for r in rows if r["module"] == module]
  76. f.write(f"\n## 模块 {module}({len(sub)} 个接口)\n\n")
  77. cur = None
  78. for r in sub:
  79. key = r["controller_file"]
  80. if key != cur:
  81. cur = key
  82. f.write(f"\n### {r['controller']}(`app/{module}/controller/{key}`)\n\n")
  83. f.write("| 方法 | URL | 参数 | 说明 |\n|---|---|---|---|\n")
  84. args = r["args"].replace("|", "\\|") or "—"
  85. comment = (r["comment"] or "").replace("|", "\\|") or "—"
  86. f.write(f"| {r['action']} | `{r['url']}` | {args} | {comment} |\n")
  87. print(f"接口总数: {len(rows)}")
  88. from collections import Counter
  89. from _root import ROOT
  90. print(Counter(r["module"] for r in rows))
  91. print("CSV:", csv_path)
  92. print("MD :", md_path)