# -*- coding: utf-8 -*- """把 store / admin 模块控制器的 @RequestMapping(method={GET,POST}) 改成 @GetMapping / @PostMapping。 HTTP 方法依据两个后台前端仓库的实际 axios 调用: store: F:/shop/yoshop2.0-store/src/api/**/*.js admin: F:/shop/yoshop2.0-admin/src/api/api.config.js + 各调用文件(接口少,显式列表) 用法:python migrate_store_admin_mapping.py [--apply] """ import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from _root import ROOT # noqa: E402 JAVA = ROOT / "yoshop-java/src/main/java/cn/yoshop" STORE_API = Path(r"F:/shop/yoshop2.0-store/src/api") APPLY = "--apply" in sys.argv # admin 前端接口 -> 方法(api.config.js 里的 key 与调用处 method 对应) ADMIN_METHODS = { "/admin/passport/login": "Post", "/admin/passport/logout": "Post", "/admin/admin.user/detail": "Get", "/admin/admin.user/renew": "Post", "/admin/store/index": "Get", "/admin/store/superLogin": "Get", "/admin/store/recycle": "Get", "/admin/store/add": "Post", "/admin/store/edit": "Post", "/admin/store/recovery": "Post", "/admin/store/delete": "Post", "/admin/store/move": "Post", "/admin/store.api/index": "Get", "/admin/store.api/add": "Post", "/admin/store.api/edit": "Post", "/admin/store.api/delete": "Post", "/admin/store.menu/index": "Get", "/admin/store.menu/info": "Get", "/admin/store.menu/add": "Post", "/admin/store.menu/edit": "Post", "/admin/store.menu/setApis": "Post", "/admin/store.menu/delete": "Post", "/admin/setting.cache/clear": "Post", "/admin/setting.science/info": "Get", "/admin/setting.queue/test": "Post", "/admin/setting.timer/test": "Post", } MAP_RE = re.compile(r"(\w+)\s*:\s*'([^']+)'") CALL_RE = re.compile(r"axios\(\{(.*?)\}\)", re.S) URL_RE = re.compile(r"url:\s*([A-Za-z0-9_.]+|'[^']*')") METHOD_RE = re.compile(r"method:\s*'(\w+)'") def parse_store_front(): table = {} for f in sorted(STORE_API.rglob("*.js")): text = f.read_text(encoding="utf-8", errors="ignore") const = re.search(r"const\s+api\s*=\s*\{(.*?)\n\}", text, re.S) local = dict(MAP_RE.findall(const.group(1))) if const else {} for block in CALL_RE.findall(text): um, mm = URL_RE.search(block), METHOD_RE.search(block) if not um or not mm: continue url = um.group(1).strip("'") if not url.startswith("/"): parts = [p for p in url.split(".") if p != "api"] if parts[-1] not in local: print(f"!! 跳过无法解析的 url: {url} ({f.name})") continue url = local[parts[-1]] table["/store" + url] = mm.group(1).capitalize() return table ANN_RE = re.compile(r"@RequestMapping\((.*?)\)", re.S) VALUE_RE = re.compile(r'value\s*=\s*(\{[^}]*\}|"[^"]*")') METHOD_ATTR_RE = re.compile(r'method\s*=\s*(RequestMethod\.\w+|\{[^}]*\})') PRODUCES_RE = re.compile(r'produces\s*=\s*("[^"]*"|[A-Za-z0-9_.]+)') def norm_path(p): if "_" in p: head, *rest = p.split("_") return head + "".join(w[:1].upper() + w[1:] for w in rest) return p def migrate(module: str, methods: dict): ctrl = JAVA / module / "controller" total = 0 for f in sorted(ctrl.glob("*.java")): src = f.read_text(encoding="utf-8") cls = re.search(r'^@RequestMapping\("([^"]+)"\)', src, re.M) prefix = cls.group(1) if cls else "" missed = [] def repl(m): nonlocal total body = m.group(1) vm = VALUE_RE.search(body) if not vm: return m.group(0) value_expr = vm.group(1) paths = re.findall(r'"([^"]*)"', value_expr) full = prefix + paths[0] key = norm_path(full) if key not in methods: missed.append(full) return m.group(0) ann = methods[key] parts = [f"value = {value_expr}"] pm = PRODUCES_RE.search(body) if pm: parts.append(f"produces = {pm.group(1)}") total += 1 return f"@{ann}Mapping({', '.join(parts)})" new_src = ANN_RE.sub(repl, src) if missed: print(f" !! {module}/{f.name} 前端未找到: {missed}") if new_src == src: continue if "RequestMethod." not in new_src: new_src = new_src.replace( "import org.springframework.web.bind.annotation.RequestMethod;\n", "") for name in ("GetMapping", "PostMapping"): imp = f"import org.springframework.web.bind.annotation.{name};" if f"@{name}(" in new_src and imp not in new_src: new_src = new_src.replace( "import org.springframework.web.bind.annotation.RequestMapping;", imp + "\nimport org.springframework.web.bind.annotation.RequestMapping;", 1) # 组内排序,tools.jackson / com.baomidou 保持原有靠前位置 lines = new_src.split("\n") out, i = [], 0 while i < len(lines): if lines[i].startswith("import "): j = i while j < len(lines) and lines[j].startswith("import "): j += 1 grp = lines[i:j] head = [x for x in grp if x.startswith(("import com.baomidou.", "import tools.jackson."))] rest = sorted(x for x in grp if x not in head) out.extend(sorted(head) + rest) i = j else: out.append(lines[i]) i += 1 new_src = "\n".join(out) if APPLY: f.write_text(new_src, encoding="utf-8") return total store_methods = parse_store_front() # 前端不调用的 PHP 生命周期钩子(无副作用、无请求体):按浏览器/探活方式用 GET store_methods["/store/controller/initialize"] = "Get" store_methods["/store/statistics.data/initialize"] = "Get" print("store 前端接口数:", len(store_methods)) n1 = migrate("store", store_methods) n2 = migrate("admin", ADMIN_METHODS) print(f"{'已改写' if APPLY else '待改写'} store={n1} admin={n2}")