#!/usr/bin/env python3 from __future__ import annotations import argparse import re from pathlib import Path from typing import List, Tuple INDEX_BLOCK_RE = re.compile( r"#\[OA\\Get\((?P.*?)\)\]\s*" r"#\[Permission\((?P.*?)\)\]\s*" r"#\[.*?\]\s*" r"public function index\(Request \$request\): \\support\\Response\s*\{(?P.*?)\}", re.S, ) UPDATE_METHOD_RE = re.compile( r"(?P\s*)public function update\(Request \$request\): \\support\\Response\s*\{(?P.*?)\n(?P=indent)\}", re.S, ) EXPORT_METHOD_RE = re.compile(r"public function export\(Request \$request\): \\support\\Response") EXTENDS_CRUD_RE = re.compile(r"class\s+\w+\s+extends\s+Crud") LIST_PERMISSION_RE = re.compile(r"['\"]([^'\"]+:list)['\"]") PATH_RE = re.compile(r"path:\s*['\"]([^'\"]+)['\"]") TAGS_RE = re.compile(r"tags:\s*(\[[^\]]*\])") SKIP_PATH_KEYWORDS = ( '/monitor/online-user', '/monitor/server', '/monitor/redis', '/login', '/profile', ) def detect_newline(text: str) -> str: return '\r\n' if '\r\n' in text else '\n' def normalize_permission(raw: str) -> str | None: match = LIST_PERMISSION_RE.search(raw) return match.group(1) if match else None def build_export_block(path: str, tags_expr: str, permission_code: str, newline: str, indent: str) -> str: export_path = f"{path.rstrip('/')}/export" lines = [ '', f"{indent}#[OA\\Post(", f"{indent} path: '{export_path}',", f"{indent} summary: '导出',", f"{indent} tags: {tags_expr}", f"{indent})]", f"{indent}#[Permission(code: '{permission_code}', description: '导出')]", f"{indent}#[SimpleResponse(schema: [], example: [])]", f"{indent}public function export(Request $request): \\support\\Response", f"{indent}{{", f"{indent} return parent::export($request);", f"{indent}}}", ] return newline.join(lines) def should_skip_path(path: str) -> bool: return any(keyword in path for keyword in SKIP_PATH_KEYWORDS) def patch_file(file_path: Path, apply: bool) -> Tuple[str, str]: original = file_path.read_text(encoding='utf-8') if not EXTENDS_CRUD_RE.search(original): return 'skip', 'not-crud' if EXPORT_METHOD_RE.search(original): return 'skip', 'has-export' if '$this->service' not in original: return 'skip', 'no-service-assign' index_match = INDEX_BLOCK_RE.search(original) if not index_match: return 'skip', 'no-standard-index' index_body = index_match.group('index_body') if 'parent::index($request)' not in index_body: return 'skip', 'custom-index' permission_code = normalize_permission(index_match.group('permission')) if not permission_code: return 'skip', 'no-list-permission' path_match = PATH_RE.search(index_match.group('oa_get')) if not path_match: return 'skip', 'no-path' path = path_match.group(1) if should_skip_path(path): return 'skip', 'skip-special-path' tags_match = TAGS_RE.search(index_match.group('oa_get')) if not tags_match: return 'skip', 'no-tags' tags_expr = tags_match.group(1) update_match = UPDATE_METHOD_RE.search(original) if not update_match: return 'skip', 'no-update-method' update_body = update_match.group('body') if 'parent::update($request)' not in update_body: return 'skip', 'custom-update' indent = update_match.group('indent') newline = detect_newline(original) export_block = build_export_block(path, tags_expr, permission_code, newline, indent) insert_pos = update_match.end() patched = original[:insert_pos] + export_block + original[insert_pos:] if patched == original: return 'skip', 'no-change' if apply: file_path.write_text(patched, encoding='utf-8', newline='') return 'patched', path return 'preview', path def collect_targets(backend_root: Path) -> List[Path]: return sorted( path for path in backend_root.rglob('*.php') if 'vendor' not in path.parts ) def main() -> int: parser = argparse.ArgumentParser(description='批量为标准 CRUD 控制器补 export() 路由') parser.add_argument('--apply', action='store_true', help='真正写入文件;默认仅预览') parser.add_argument('--backend-root', default=None, help='backend 根目录路径,默认自动推断') args = parser.parse_args() script_path = Path(__file__).resolve() backend_root = Path(args.backend_root).resolve() if args.backend_root else script_path.parents[2] if not backend_root.exists(): print(f'[ERROR] backend 根目录不存在: {backend_root}') return 1 results: List[Tuple[str, str, str]] = [] for file_path in collect_targets(backend_root): status, detail = patch_file(file_path, args.apply) if status in {'patched', 'preview'}: results.append((status, detail, str(file_path))) mode = 'APPLY' if args.apply else 'PREVIEW' print(f'=== {mode} RESULT ===') if not results: print('没有发现可补 export() 的标准 CRUD 控制器。') return 0 for status, detail, file_name in results: print(f'[{status}] {detail} -> {file_name}') print(f'总计: {len(results)} 个文件') if not args.apply: print('提示: 追加 --apply 才会真正写入文件') return 0 if __name__ == '__main__': raise SystemExit(main())