/** * 批量提取所有页面接口地址的自动化脚本 * * 处理流程: * 1. 解析 menu.json 提取所有 SYSTEMMODULE_URL * 2. 解析 routes.ts 构建嵌套路径映射(子父级拼接) * 3. 定位组件文件夹及所有子组件 * 4. 提取 import 的 service 文件 * 5. 在 service 文件中提取 request() 路径,并根据引用的 request 工具确定 prefix * 6. 输出结果到 config/api_mapping.json 和 config/api_mapping.md */ const fs = require('fs'); const path = require('path'); // 项目根目录 const ROOT = path.resolve(__dirname, '..'); const SRC = path.join(ROOT, 'src'); const PAGES = path.join(SRC, 'pages'); // ============ 第一部分:request prefix 映射 ============ // 根据 service 文件 import 的 request 来源确定 prefix const REQUEST_PREFIX_MAP = { 'request': 'https://eshangtech.com:18900/EShangApiMain', 'requestUpLoad': 'http://220.180.35.180:8000/EShangApiMain', 'requestTest': 'http://dev.eshangtech.com:8900/EShangApiMain', 'requestTestTest': 'http://dev.eshangtech.com:8001/EShangApiMain', 'requestNoPrefix': 'https://eshangtech.com:18900/', 'requestPythonTest': 'http://192.168.1.207:8002/', 'requestNewTest': 'https://eshangtech.com:18900/EShangApiMain', 'requestNewJava': 'https://java.es.eshangtech.com:443', 'requestnew': '/EShangApiDashboard', 'requestEncryption': 'https://eshangtech.com:18900/MemberApi', 'requestJava': 'https://java.es.eshangtech.com', 'requestDashboard': 'https://eshangtech.com:18900/EshangApiDashboard', 'requestCodeBuilder': 'http://dev.eshangtech.com:8001/CodeBuilderApi', 'requestCode': 'https://eshangtech.com:18900/CommercialApi', 'requestAHYD': 'https://ahyd.eshangtech.com/EShangApiMain', }; // ============ 第二部分:解析 routes.ts 构建路径映射 ============ /** * 解析 routes.ts 文件,构建 fullPath → component 映射 * 处理嵌套路由的路径拼接 */ function parseRoutes() { const routesFile = path.join(ROOT, 'config', 'routes.ts'); const content = fs.readFileSync(routesFile, 'utf-8'); // 用正则提取所有路由对象(简化处理:逐行匹配 path 和 component) const routeMap = {}; // 使用栈来跟踪嵌套路径 const pathStack = []; let braceDepth = 0; const depthToPath = {}; // 记录每个深度对应的 path 值 const lines = content.split('\n'); let currentPath = null; let currentComponent = null; let currentObjStartDepth = -1; for (const line of lines) { const trimmed = line.trim(); // 计算花括号深度 for (const ch of trimmed) { if (ch === '{') braceDepth++; if (ch === '}') { // 当离开一个层级时,清除该层级的 path if (depthToPath[braceDepth]) { delete depthToPath[braceDepth]; } braceDepth--; } } // 匹配 path 属性 const pathMatch = trimmed.match(/^path:\s*['"]([^'"]+)['"]/); if (pathMatch) { const pathVal = pathMatch[1]; depthToPath[braceDepth] = pathVal; } // 匹配 component 属性 const compMatch = trimmed.match(/^component:\s*['"]([^'"]+)['"]/); if (compMatch) { const comp = compMatch[1]; // 跳过 layout 组件 if (comp.includes('Layout') || comp.includes('layout')) continue; // 构建完整路径:从 depthToPath 中按层级拼接 const fullPath = buildFullPath(depthToPath, braceDepth); if (fullPath && fullPath !== '/') { routeMap[fullPath] = comp; } } } return routeMap; } /** * 根据深度映射构建完整路径 */ function buildFullPath(depthToPath, currentDepth) { // 收集所有当前有效的路径段 const segments = []; const depths = Object.keys(depthToPath).map(Number).sort((a, b) => a - b); for (const d of depths) { if (d <= currentDepth) { segments.push(depthToPath[d]); } } if (segments.length === 0) return null; // 拼接路径 let result = ''; for (const seg of segments) { if (seg.startsWith('/')) { // 绝对路径,重新开始 result = seg; } else { // 相对路径,拼接 if (result.endsWith('/')) { result += seg; } else { result += '/' + seg; } } } return result; } // ============ 第三部分:备用路径匹配 ============ /** * 当 routes.ts 中找不到完全匹配时,尝试通过 URL 路径推断组件位置 * 扫描 src/pages 目录查找匹配的文件夹 */ function findComponentByUrl(menuUrl) { // 从 URL 提取最后一段作为组件名 const segments = menuUrl.split('/').filter(Boolean); if (segments.length === 0) return null; const lastSegment = segments[segments.length - 1]; // 在 pages 目录下递归搜索包含该名称的文件夹 const found = findDirRecursive(PAGES, lastSegment); return found; } function findDirRecursive(dir, targetName) { if (!fs.existsSync(dir)) return null; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { // 不区分大小写匹配 if (entry.name.toLowerCase() === targetName.toLowerCase()) { const fullPath = path.join(dir, entry.name); // 确认有 index.tsx 或 index.ts if (fs.existsSync(path.join(fullPath, 'index.tsx')) || fs.existsSync(path.join(fullPath, 'index.ts'))) { return fullPath; } } // 递归搜索子目录 const result = findDirRecursive(path.join(dir, entry.name), targetName); if (result) return result; } } return null; } // ============ 第四部分:定位组件并提取所有被引用的文件 ============ /** * 根据 component 路径解析到实际文件夹 * 处理两种情况: * 1. component 指向目录(如 './contract' → src/pages/contract/) * 2. component 指向文件(如 './contract/list' → src/pages/contract/list.tsx) * 此时返回文件所在的父目录 */ function resolveComponentDir(componentPath) { let resolved = componentPath.replace(/^\.\//, ''); const fullPath = path.join(PAGES, resolved); // 情况1:路径本身是目录(如 src/pages/contract/) if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { return fullPath; } // 情况2:路径指向文件(如 src/pages/contract/list → 实际是 list.tsx) // 检查是否存在同名的 .tsx / .ts 文件 const extensions = ['.tsx', '.ts', '.jsx', '.js']; for (const ext of extensions) { if (fs.existsSync(fullPath + ext)) { // 文件存在,返回其父目录作为组件目录 return path.dirname(fullPath); } } // 情况3:尝试去掉末尾的 /index if (resolved.endsWith('/index')) { resolved = resolved.replace(/\/index$/, ''); const altPath = path.join(PAGES, resolved); if (fs.existsSync(altPath) && fs.statSync(altPath).isDirectory()) { return altPath; } } return null; } /** * 获取组件目录下所有 .tsx / .ts 文件(包括 components 子目录) */ function getAllComponentFiles(dir) { const files = []; if (!fs.existsSync(dir)) return files; function scan(currentDir) { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { scan(fullPath); } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) { // 排除 service.ts 本身和 .d.ts if (!entry.name.endsWith('.d.ts')) { files.push(fullPath); } } } } scan(dir); return files; } // ============ 第五部分:提取 service 导入和接口路径 ============ /** * 从组件文件中提取所有 import 的 service 路径 */ function extractServiceImports(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const imports = []; // 匹配各种 import 模式 // import { xxx } from '../xxx/service' // import { xxx } from '@/pages/xxx/service' // import { xxx } from '@/services/xxx' const importRegex = /import\s+\{([^}]+)\}\s+from\s+['"](.*?)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { const functions = match[1].split(',').map(f => f.trim()).filter(Boolean); let importPath = match[2]; // 只关注 service 文件、services 目录、options 目录的导入 if (importPath.includes('service') || importPath.includes('services') || importPath.includes('options')) { imports.push({ functions, importPath, sourceFile: filePath }); } } return imports; } /** * 解析 import 路径到实际文件路径 */ function resolveImportPath(importPath, sourceFile) { let resolved; if (importPath.startsWith('@/')) { // @/ 别名 → src/ resolved = path.join(SRC, importPath.replace('@/', '')); } else if (importPath.startsWith('.')) { // 相对路径 resolved = path.resolve(path.dirname(sourceFile), importPath); } else { return null; // node_modules 等,跳过 } // 尝试各种扩展名 const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx', '.js', '/index.js']; for (const ext of extensions) { const candidate = resolved + ext; if (fs.existsSync(candidate)) { return candidate; } } // 如果直接存在 if (fs.existsSync(resolved)) { if (fs.statSync(resolved).isDirectory()) { for (const ext of ['/index.ts', '/index.tsx', '/index.js']) { const idx = resolved + ext; if (fs.existsSync(idx)) return idx; } } return resolved; } return null; } // 缓存已解析的 service 文件 const serviceCache = {}; /** * 从 service 文件中提取所有 request() 调用的路径 * 同时确定引用的是哪个 request 工具(确定 prefix) * 并记录每个 request() 调用所属的函数名 */ function extractApisFromService(serviceFilePath) { if (serviceCache[serviceFilePath]) { return serviceCache[serviceFilePath]; } if (!fs.existsSync(serviceFilePath)) { serviceCache[serviceFilePath] = []; return []; } const content = fs.readFileSync(serviceFilePath, 'utf-8'); const apis = []; // 1. 确定该 service 文件使用的 request 来源 let prefix = REQUEST_PREFIX_MAP['request']; // 默认 prefix // 匹配 import request from '@/utils/requestXXX' const requestImportRegex = /import\s+\w+\s+from\s+['"]@\/utils\/(\w+)['"]/g; let reqMatch; while ((reqMatch = requestImportRegex.exec(content)) !== null) { const requestName = reqMatch[1]; if (REQUEST_PREFIX_MAP[requestName] !== undefined) { prefix = REQUEST_PREFIX_MAP[requestName]; } } // 也匹配相对路径的 request 导入 const relRequestRegex = /import\s+\w+\s+from\s+['"]\.\.?\/.*?(\w+)['"].*?/g; while ((reqMatch = relRequestRegex.exec(content)) !== null) { const fileName = reqMatch[1]; if (REQUEST_PREFIX_MAP[fileName] !== undefined) { prefix = REQUEST_PREFIX_MAP[fileName]; } } // 2. 逐行扫描,追踪当前函数名,并提取 request() 调用 const lines = content.split('\n'); let currentFuncName = '(unknown)'; // 函数定义的正则匹配模式: // export async function handleXxx(...) // export function handleXxx(...) // export const handleXxx = async (...) // export const handleXxx = (...) const funcDefRegex = /^export\s+(?:async\s+)?function\s+(\w+)|^export\s+const\s+(\w+)\s*=/; for (const line of lines) { const trimmed = line.trim(); // 检查是否是函数定义行 const funcMatch = trimmed.match(funcDefRegex); if (funcMatch) { currentFuncName = funcMatch[1] || funcMatch[2]; } // 检查是否有 request() 调用 const requestCallRegex = /request\(\s*['"`]([^'"`\$]+)['"`]/g; let apiMatch; while ((apiMatch = requestCallRegex.exec(trimmed)) !== null) { const apiPath = apiMatch[1]; // 构建完整 URL let fullUrl; if (prefix.endsWith('/')) { fullUrl = prefix + apiPath.replace(/^\//, ''); } else { fullUrl = prefix + apiPath; } apis.push({ path: apiPath, fullUrl: fullUrl, prefix: prefix, functionName: currentFuncName, serviceFile: path.relative(ROOT, serviceFilePath) }); } } // 3. 也检查该 service 文件是否还 import 了其他 service(二级引用) const serviceImports = extractServiceImports(serviceFilePath); for (const imp of serviceImports) { // 避免循环引用 const resolvedPath = resolveImportPath(imp.importPath, serviceFilePath); if (resolvedPath && resolvedPath !== serviceFilePath && !serviceCache[resolvedPath]) { const subApis = extractApisFromService(resolvedPath); apis.push(...subApis); } } serviceCache[serviceFilePath] = apis; return apis; } // ============ 第六部分:解析 menu.json 提取所有模块 ============ function extractModules(items, parentPath = '') { const modules = []; for (const item of items) { if (item.SYSTEMMODULE_URL && item.SYSTEMMODULE_URL !== '.' && item.SYSTEMMODULE_URL !== '/') { // 跳过外部链接 if (!item.SYSTEMMODULE_URL.startsWith('http')) { modules.push({ name: item.SYSTEMMODULE_NAME, url: item.SYSTEMMODULE_URL, parentMenu: parentPath }); } } if (item.children) { const menuName = item.SYSTEMMENU_NAME || ''; const newParent = parentPath ? `${parentPath} > ${menuName}` : menuName; modules.push(...extractModules(item.children, newParent)); } } return modules; } // ============ 第七部分:主流程 ============ function main() { console.log('🚀 开始批量提取页面接口...\n'); // 1. 读取 menu.json const menuData = require(path.join(ROOT, 'config', 'menu.json')); const modules = extractModules(menuData); console.log(`📋 共发现 ${modules.length} 个有效页面模块\n`); // 2. 解析 routes.ts 构建路径映射 const routeMap = parseRoutes(); console.log(`🗺️ 路由映射构建完成,共 ${Object.keys(routeMap).length} 条路由\n`); // 3. 逐个处理每个模块 const results = []; let matchedCount = 0; let unmatchedCount = 0; let totalApis = 0; for (const mod of modules) { const result = { menuName: mod.name, menuUrl: mod.url, parentMenu: mod.parentMenu, componentPath: null, componentDir: null, apiCount: 0, apis: [], status: 'unmatched' }; // 3.1 在路由映射中查找 component let component = routeMap[mod.url]; // 3.2 如果没有直接匹配,尝试带参数路径的匹配 if (!component) { // 处理带动态参数的路由,比如 /setting/department/:id 匹配 /setting/department/userstype for (const [routePath, comp] of Object.entries(routeMap)) { if (routePath.includes(':')) { const routeRegex = routePath.replace(/:[^/]+/g, '[^/]+'); if (new RegExp(`^${routeRegex}$`).test(mod.url)) { component = comp; break; } } } } if (component) { result.componentPath = component; // 3.3 解析 component 路径到实际文件夹 const componentDir = resolveComponentDir(component); if (componentDir) { result.componentDir = path.relative(ROOT, componentDir); result.status = 'matched'; matchedCount++; // 3.4 获取所有组件文件 const componentFiles = getAllComponentFiles(componentDir); // 3.5 提取所有 service 导入 const allApis = new Set(); // 用 Set 去重 const apiDetails = []; for (const file of componentFiles) { const serviceImports = extractServiceImports(file); for (const imp of serviceImports) { const resolvedService = resolveImportPath(imp.importPath, file); if (resolvedService) { const apis = extractApisFromService(resolvedService); for (const api of apis) { if (!allApis.has(api.fullUrl)) { allApis.add(api.fullUrl); apiDetails.push(api); } } } } } result.apiCount = apiDetails.length; result.apis = apiDetails.map(a => ({ fullUrl: a.fullUrl, path: a.path, prefix: a.prefix, functionName: a.functionName, serviceFile: a.serviceFile })); totalApis += apiDetails.length; } else { result.status = 'dir_not_found'; unmatchedCount++; } } else { // 3.6 备用:通过 URL 最后一段搜索 const fallbackDir = findComponentByUrl(mod.url); if (fallbackDir) { result.componentDir = path.relative(ROOT, fallbackDir); result.status = 'matched_fallback'; matchedCount++; const componentFiles = getAllComponentFiles(fallbackDir); const allApis = new Set(); const apiDetails = []; for (const file of componentFiles) { const serviceImports = extractServiceImports(file); for (const imp of serviceImports) { const resolvedService = resolveImportPath(imp.importPath, file); if (resolvedService) { const apis = extractApisFromService(resolvedService); for (const api of apis) { if (!allApis.has(api.fullUrl)) { allApis.add(api.fullUrl); apiDetails.push(api); } } } } } result.apiCount = apiDetails.length; result.apis = apiDetails.map(a => ({ fullUrl: a.fullUrl, path: a.path, prefix: a.prefix, functionName: a.functionName, serviceFile: a.serviceFile })); totalApis += apiDetails.length; } else { unmatchedCount++; } } results.push(result); } // 4. 输出统计 console.log('========== 统计结果 =========='); console.log(`✅ 匹配成功: ${matchedCount} 个页面`); console.log(`❌ 未匹配: ${unmatchedCount} 个页面`); console.log(`📡 总接口数: ${totalApis} 个`); console.log(''); // 5. 输出 JSON 文件 const jsonOutput = path.join(ROOT, 'config', 'api_mapping.json'); fs.writeFileSync(jsonOutput, JSON.stringify(results, null, 2), 'utf-8'); console.log(`📄 JSON 已输出: ${jsonOutput}`); // 6. 输出 Markdown 文件 const mdOutput = path.join(ROOT, 'config', 'api_mapping.md'); let md = '# 系统页面接口映射表\n\n'; md += `> 自动生成时间: ${new Date().toLocaleString()}\n\n`; md += `> 总页面数: ${modules.length} | 匹配成功: ${matchedCount} | 未匹配: ${unmatchedCount} | 总接口数: ${totalApis}\n\n`; // 按父级菜单分组 const groupedByParent = {}; for (const r of results) { const parent = r.parentMenu || '未分类'; if (!groupedByParent[parent]) { groupedByParent[parent] = []; } groupedByParent[parent].push(r); } for (const [parentMenu, items] of Object.entries(groupedByParent)) { md += `## ${parentMenu}\n\n`; for (const item of items) { if (item.status === 'unmatched' || item.status === 'dir_not_found') { md += `### ❌ ${item.menuName}\n`; md += `- 路径: \`${item.menuUrl}\`\n`; md += `- 状态: 未找到对应组件\n\n`; continue; } md += `### ${item.menuName}\n`; md += `- 路径: \`${item.menuUrl}\`\n`; md += `- 组件: \`${item.componentDir || item.componentPath}\`\n`; md += `- 调用了 **${item.apiCount}** 个接口\n\n`; if (item.apis.length > 0) { md += '| # | 函数名 | 接口路径 | 完整地址 | 来源 |\n'; md += '|---|--------|----------|----------|------|\n'; item.apis.forEach((api, idx) => { md += `| ${idx + 1} | \`${api.functionName || '-'}\` | \`${api.path}\` | \`${api.fullUrl}\` | \`${api.serviceFile}\` |\n`; }); md += '\n'; } } md += '---\n\n'; } // 7. 添加未匹配页面汇总 const unmatched = results.filter(r => r.status === 'unmatched' || r.status === 'dir_not_found'); if (unmatched.length > 0) { md += '## ⚠️ 未匹配页面汇总\n\n'; md += '| # | 页面名称 | 路径 | 所属菜单 |\n'; md += '|---|----------|------|----------|\n'; unmatched.forEach((item, idx) => { md += `| ${idx + 1} | ${item.menuName} | \`${item.menuUrl}\` | ${item.parentMenu} |\n`; }); md += '\n'; } fs.writeFileSync(mdOutput, md, 'utf-8'); console.log(`📝 Markdown 已输出: ${mdOutput}`); // 8. 打印部分结果预览 console.log('\n========== 结果预览(前 10 个有接口的页面)==========\n'); const withApis = results.filter(r => r.apiCount > 0).slice(0, 10); for (const r of withApis) { console.log(`📌 【${r.menuName}】调用了 ${r.apiCount} 个接口,分别为:`); r.apis.forEach((api, idx) => { console.log(` ${idx + 1}. ${api.fullUrl}`); }); console.log(''); } } main();