|
|
|
@ -12,6 +12,39 @@ class ApiCollector {
@@ -12,6 +12,39 @@ class ApiCollector {
|
|
|
|
|
this.apiMappings = [] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 获取API基础URL |
|
|
|
|
* @returns {string} API基础URL |
|
|
|
|
*/ |
|
|
|
|
getApiBaseUrl() { |
|
|
|
|
try { |
|
|
|
|
// 读取配置文件
|
|
|
|
|
const configPath = resolve(__dirname, '../../src/config/app.config.js') |
|
|
|
|
if (existsSync(configPath)) { |
|
|
|
|
const configContent = readFileSync(configPath, 'utf-8') |
|
|
|
|
|
|
|
|
|
// 解析配置文件,提取apiBaseUrl
|
|
|
|
|
// 匹配 development 环境中的 apiBaseUrl 配置
|
|
|
|
|
const developmentMatch = configContent.match(/development:\s*\{[^}]*apiBaseUrl:\s*[^:]*:\s*['"`]([^'"`]+)['"`]/s) |
|
|
|
|
if (developmentMatch) { |
|
|
|
|
return developmentMatch[1] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果上面没匹配到,尝试匹配简单的字符串格式
|
|
|
|
|
const simpleMatch = configContent.match(/apiBaseUrl:\s*['"`]([^'"`]+)['"`]/) |
|
|
|
|
if (simpleMatch) { |
|
|
|
|
return simpleMatch[1] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果无法从配置文件获取,返回空字符串(使用相对路径)
|
|
|
|
|
return '' |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn('⚠️ 无法获取API基础URL,使用相对路径:', error.message) |
|
|
|
|
return '' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 收集所有模块的API映射 |
|
|
|
|
* @param {Array} moduleDirs - 模块目录列表 |
|
|
|
@ -25,8 +58,7 @@ class ApiCollector {
@@ -25,8 +58,7 @@ class ApiCollector {
|
|
|
|
|
if (moduleApiMappings.length > 0) { |
|
|
|
|
this.apiMappings.push({ |
|
|
|
|
module: moduleName, |
|
|
|
|
serviceName: moduleApiMappings[0].serviceName, |
|
|
|
|
servicePath: moduleApiMappings[0].servicePath, |
|
|
|
|
serviceName: moduleApiMappings.serviceName, |
|
|
|
|
apiMappings: moduleApiMappings |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
@ -50,18 +82,31 @@ class ApiCollector {
@@ -50,18 +82,31 @@ class ApiCollector {
|
|
|
|
|
const serviceFiles = readdirSync(servicesPath).filter(file => file.endsWith('.js')) |
|
|
|
|
const moduleApiMappings = [] |
|
|
|
|
|
|
|
|
|
let firstServiceName = null |
|
|
|
|
|
|
|
|
|
serviceFiles.forEach(serviceFile => { |
|
|
|
|
const servicePath = resolve(servicesPath, serviceFile) |
|
|
|
|
const serviceName = serviceFile.replace('.js', '') |
|
|
|
|
|
|
|
|
|
// 记录第一个服务名称(第一层需要)
|
|
|
|
|
if (!firstServiceName) { |
|
|
|
|
firstServiceName = serviceName |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const serviceApiMappings = this.analyzeServiceFile(servicePath, serviceName, moduleName) |
|
|
|
|
// 不再为每个API映射添加serviceName(第二层冗余信息)
|
|
|
|
|
moduleApiMappings.push(...serviceApiMappings) |
|
|
|
|
} catch (error) { |
|
|
|
|
// 静默处理错误
|
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 为模块API映射添加serviceName(第一层需要)
|
|
|
|
|
if (firstServiceName) { |
|
|
|
|
moduleApiMappings.serviceName = firstServiceName |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return moduleApiMappings |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -113,7 +158,7 @@ class ApiCollector {
@@ -113,7 +158,7 @@ class ApiCollector {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从AST节点中提取API映射信息 |
|
|
|
|
* 从AST节点中提取API映射信息 - 优化版本:只使用@serverApiMethod注解 |
|
|
|
|
* @param {Object} methodPath - 方法路径 |
|
|
|
|
* @param {string} methodName - 方法名称 |
|
|
|
|
* @param {string} serviceName - 服务名称 |
|
|
|
@ -121,49 +166,86 @@ class ApiCollector {
@@ -121,49 +166,86 @@ class ApiCollector {
|
|
|
|
|
* @returns {Object|null} API映射对象 |
|
|
|
|
*/ |
|
|
|
|
extractApiMapping(methodPath, methodName, serviceName, servicePath) { |
|
|
|
|
let httpMethod = null |
|
|
|
|
let apiPath = null |
|
|
|
|
// 只从@serverApiMethod注解中提取信息(强规则)
|
|
|
|
|
const annotationInfo = this.extractFromAnnotation(methodPath, servicePath) |
|
|
|
|
if (annotationInfo) { |
|
|
|
|
// 获取API基础URL
|
|
|
|
|
const apiBaseUrl = this.getApiBaseUrl() |
|
|
|
|
|
|
|
|
|
// 组合完整的API路径
|
|
|
|
|
const fullPath = `${apiBaseUrl}${annotationInfo.path}` |
|
|
|
|
|
|
|
|
|
// 提取路径部分,去掉协议、服务器地址和端口
|
|
|
|
|
const pathOnly = this.extractPathFromUrl(fullPath) |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
apiMethodName: methodName, |
|
|
|
|
method: annotationInfo.method, |
|
|
|
|
path: pathOnly |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 查找HTTP方法调用
|
|
|
|
|
traverse(methodPath.node, { |
|
|
|
|
CallExpression(path) { |
|
|
|
|
const node = path.node |
|
|
|
|
if (node.callee && node.callee.type === 'MemberExpression') { |
|
|
|
|
const object = node.callee.object |
|
|
|
|
const property = node.callee.property |
|
|
|
|
// 如果没有注解,返回null(不再回退到代码分析)
|
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从完整URL中提取路径部分 |
|
|
|
|
* @param {string} fullUrl - 完整的URL |
|
|
|
|
* @returns {string} 路径部分 |
|
|
|
|
*/ |
|
|
|
|
extractPathFromUrl(fullUrl) { |
|
|
|
|
try { |
|
|
|
|
// 如果URL包含协议,使用URL对象解析
|
|
|
|
|
if (fullUrl.includes('://')) { |
|
|
|
|
const url = new URL(fullUrl) |
|
|
|
|
// 解码路径,确保路径参数保持原始格式
|
|
|
|
|
return decodeURIComponent(url.pathname) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果没有协议,直接返回(已经是路径格式)
|
|
|
|
|
return fullUrl |
|
|
|
|
} catch (error) { |
|
|
|
|
// 如果解析失败,返回原始字符串
|
|
|
|
|
return fullUrl |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从@serverApiMethod注解中提取API信息 |
|
|
|
|
* @param {Object} methodPath - 方法路径 |
|
|
|
|
* @param {string} servicePath - 服务文件路径 |
|
|
|
|
* @returns {Object|null} 注解信息 |
|
|
|
|
*/ |
|
|
|
|
extractFromAnnotation(methodPath, servicePath) { |
|
|
|
|
try { |
|
|
|
|
const content = readFileSync(servicePath, 'utf-8') |
|
|
|
|
const lines = content.split('\n') |
|
|
|
|
const methodLine = methodPath.node.loc ? methodPath.node.loc.start.line : 0 |
|
|
|
|
|
|
|
|
|
// 查找方法定义前的注释行(从方法行开始向上搜索)
|
|
|
|
|
for (let i = methodLine - 2; i >= 0; i--) { |
|
|
|
|
const line = lines[i].trim() |
|
|
|
|
|
|
|
|
|
// 查找@serverApiMethod注解
|
|
|
|
|
const annotationMatch = line.match(/\/\/\s*@serverApiMethod\s+(\w+)\s+(.+)/) |
|
|
|
|
if (annotationMatch) { |
|
|
|
|
const method = annotationMatch[1].toUpperCase() |
|
|
|
|
const path = annotationMatch[2].trim() |
|
|
|
|
|
|
|
|
|
if (object && property &&
|
|
|
|
|
object.type === 'Identifier' &&
|
|
|
|
|
property.type === 'Identifier') { |
|
|
|
|
|
|
|
|
|
const method = property.name.toLowerCase() |
|
|
|
|
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) { |
|
|
|
|
httpMethod = method.toUpperCase() |
|
|
|
|
|
|
|
|
|
// 提取API路径
|
|
|
|
|
if (node.arguments && node.arguments.length > 0) { |
|
|
|
|
const firstArg = node.arguments[0] |
|
|
|
|
if (firstArg.type === 'StringLiteral') { |
|
|
|
|
apiPath = firstArg.value |
|
|
|
|
} else if (firstArg.type === 'TemplateLiteral') { |
|
|
|
|
apiPath = firstArg.quasis[0].value.raw |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
method: method, |
|
|
|
|
path: path |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果遇到非注释行且不是空行,停止搜索
|
|
|
|
|
if (line && !line.startsWith('//') && !line.startsWith('*') && !line.startsWith('/*')) { |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, methodPath.scope, methodPath) |
|
|
|
|
|
|
|
|
|
if (httpMethod && apiPath) { |
|
|
|
|
return { |
|
|
|
|
apiMethodName: methodName, |
|
|
|
|
method: httpMethod, |
|
|
|
|
path: apiPath, |
|
|
|
|
line: methodPath.node.loc ? methodPath.node.loc.start.line : 0, |
|
|
|
|
serviceName: serviceName, |
|
|
|
|
servicePath: servicePath |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error(`❌ 解析注解失败: ${servicePath}`, error.message) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return null |
|
|
|
|