279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
/**
|
||
* 合并运行时 API 日志与静态 api_mapping 数据
|
||
*
|
||
* 匹配逻辑:通过 URL 匹配(运行时日志的 url 对应 api_mapping 中的 fullUrl)
|
||
* 输出:带入参结构的增强版 api_mapping
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 读取数据
|
||
const configDir = path.join(__dirname, '..', 'config');
|
||
const apiMapping = JSON.parse(fs.readFileSync(path.join(configDir, 'api_mapping.json'), 'utf-8'));
|
||
|
||
// 自动扫描所有 api_log_*.json 文件,合并数据
|
||
const logFiles = fs.readdirSync(configDir).filter(f => f.startsWith('api_log_') && f.endsWith('.json'));
|
||
let apiLog = [];
|
||
for (const file of logFiles) {
|
||
const data = JSON.parse(fs.readFileSync(path.join(configDir, file), 'utf-8'));
|
||
console.log(` 📄 ${file}: ${data.length} 条记录`);
|
||
apiLog = apiLog.concat(data);
|
||
}
|
||
|
||
console.log(`📊 api_mapping: ${apiMapping.length} 个页面`);
|
||
console.log(`📊 api_log 总计: ${apiLog.length} 条运行时记录(来自 ${logFiles.length} 个文件)`);
|
||
|
||
// 1. 构建运行时数据索引
|
||
// 按文件时间顺序处理,新文件有数据则覆盖旧的,没数据则保留旧的
|
||
// 双重索引:pageKey:url → 记录列表(按页面区分)
|
||
// 全局索引:url → 记录列表(兜底用)
|
||
const pageIndex = {}; // { "页面名::URL" → [records] }
|
||
const globalIndex = {}; // { "URL" → [records] }
|
||
|
||
// 按文件名排序(文件名含时间戳,自然排序即为时间顺序)
|
||
logFiles.sort();
|
||
|
||
for (const file of logFiles) {
|
||
const data = JSON.parse(fs.readFileSync(path.join(configDir, file), 'utf-8'));
|
||
|
||
for (const record of data) {
|
||
const baseUrl = record.url.split('?')[0];
|
||
const pageName = record.currentPageName || '';
|
||
const hasData = (record.dataStructure && typeof record.dataStructure === 'object' && Object.keys(record.dataStructure).length > 0) ||
|
||
(record.paramsStructure && typeof record.paramsStructure === 'object' && Object.keys(record.paramsStructure).length > 0);
|
||
|
||
// 全局索引:新文件有数据则覆盖,无数据则追加(不覆盖已有的)
|
||
if (!globalIndex[baseUrl]) {
|
||
globalIndex[baseUrl] = [record];
|
||
} else if (hasData) {
|
||
// 新记录有入参,覆盖旧的
|
||
globalIndex[baseUrl] = [record];
|
||
}
|
||
// 新记录无入参,保留旧的,不做操作
|
||
|
||
// 页面索引
|
||
if (pageName && pageName !== '空白页') {
|
||
const pageKey = `${pageName}::${baseUrl}`;
|
||
if (!pageIndex[pageKey]) {
|
||
pageIndex[pageKey] = [record];
|
||
} else if (hasData) {
|
||
// 新记录有入参,覆盖旧的
|
||
pageIndex[pageKey] = [record];
|
||
}
|
||
// 新记录无入参,保留旧的
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`📊 运行时唯一接口数(去参数): ${Object.keys(globalIndex).length}`);
|
||
console.log(`📊 页面级索引条目数: ${Object.keys(pageIndex).length}`);
|
||
|
||
/**
|
||
* 合并多条运行时记录的入参结构(取字段并集)
|
||
* 这样不同调用传了不同的可选参数都能被收集到
|
||
*/
|
||
function mergeStructures(records) {
|
||
let mergedData = null;
|
||
let mergedParams = null;
|
||
let exampleData = null;
|
||
let exampleParams = null;
|
||
let method = 'GET';
|
||
let requestType = '';
|
||
let requestSource = '';
|
||
|
||
for (const r of records) {
|
||
method = (r.method || method);
|
||
requestType = r.requestType || requestType;
|
||
requestSource = r.requestSource || requestSource;
|
||
|
||
// 合并 dataStructure
|
||
if (r.dataStructure && typeof r.dataStructure === 'object') {
|
||
if (!mergedData) mergedData = {};
|
||
deepMergeKeys(mergedData, r.dataStructure);
|
||
}
|
||
// 合并 paramsStructure
|
||
if (r.paramsStructure && typeof r.paramsStructure === 'object') {
|
||
if (!mergedParams) mergedParams = {};
|
||
deepMergeKeys(mergedParams, r.paramsStructure);
|
||
}
|
||
|
||
// 收集示例数据 (dataFull/paramsFull)
|
||
// 取最后一条有数据的记录作为示例
|
||
if (r.dataFull && Object.keys(r.dataFull).length > 0) {
|
||
exampleData = r.dataFull;
|
||
}
|
||
if (r.paramsFull && Object.keys(r.paramsFull).length > 0) {
|
||
exampleParams = r.paramsFull;
|
||
}
|
||
}
|
||
|
||
return { method, requestType, requestSource, mergedData, mergedParams, exampleData, exampleParams };
|
||
}
|
||
|
||
/**
|
||
* 深度合并 key(取并集),保留类型信息
|
||
*/
|
||
function deepMergeKeys(target, source) {
|
||
for (const key of Object.keys(source)) {
|
||
if (!(key in target)) {
|
||
target[key] = source[key];
|
||
} else if (typeof target[key] === 'object' && target[key] !== null &&
|
||
typeof source[key] === 'object' && source[key] !== null &&
|
||
!Array.isArray(target[key]) && !Array.isArray(source[key])) {
|
||
// 两边都是对象,递归合并
|
||
deepMergeKeys(target[key], source[key]);
|
||
}
|
||
// 如果类型不同或已有值,保留原值(先到先得)
|
||
}
|
||
}
|
||
|
||
// 2. 合并数据
|
||
let matchedApiCount = 0;
|
||
let unmatchedApiCount = 0;
|
||
let matchedPageCount = 0;
|
||
|
||
for (const page of apiMapping) {
|
||
if (!page.apis || page.apis.length === 0) continue;
|
||
|
||
let pageHasMatch = false;
|
||
const pageName = page.menuName || '';
|
||
|
||
for (const api of page.apis) {
|
||
const baseUrl = api.fullUrl.split('?')[0];
|
||
|
||
// 优先用页面级索引(精确匹配当前页面的记录)
|
||
const pageKey = `${pageName}::${baseUrl}`;
|
||
let records = pageIndex[pageKey];
|
||
|
||
// 兜底用全局索引
|
||
if (!records || records.length === 0) {
|
||
records = globalIndex[baseUrl];
|
||
}
|
||
|
||
if (records && records.length > 0) {
|
||
// 合并该页面下所有同接口调用的入参(取并集)
|
||
const merged = mergeStructures(records);
|
||
|
||
api.httpMethod = merged.method;
|
||
api.dataStructure = merged.mergedData;
|
||
api.paramsStructure = merged.mergedParams;
|
||
api.exampleData = merged.exampleData;
|
||
api.exampleParams = merged.exampleParams;
|
||
api.requestType = merged.requestType;
|
||
api.runtimeSource = merged.requestSource;
|
||
api.runtimePage = pageName;
|
||
api.hasRuntimeData = true;
|
||
// 标记是否为当前页面的精确匹配
|
||
api.exactPageMatch = !!pageIndex[pageKey];
|
||
|
||
matchedApiCount++;
|
||
pageHasMatch = true;
|
||
} else {
|
||
api.hasRuntimeData = false;
|
||
unmatchedApiCount++;
|
||
}
|
||
}
|
||
|
||
if (pageHasMatch) matchedPageCount++;
|
||
}
|
||
|
||
console.log(`\n✅ 合并结果:`);
|
||
console.log(` 已匹配接口: ${matchedApiCount}`);
|
||
console.log(` 未匹配接口: ${unmatchedApiCount}`);
|
||
console.log(` 有运行时数据的页面: ${matchedPageCount}`);
|
||
|
||
// 3. 输出合并后的 JSON
|
||
const jsonOutput = path.join(__dirname, '..', 'config', 'api_mapping_merged.json');
|
||
fs.writeFileSync(jsonOutput, JSON.stringify(apiMapping, null, 2), 'utf-8');
|
||
console.log(`\n📁 JSON 已输出: ${jsonOutput}`);
|
||
|
||
// 4. 输出 Markdown 报告
|
||
const mdOutput = path.join(__dirname, '..', 'config', 'api_mapping_merged.md');
|
||
let md = '';
|
||
|
||
md += `# API 接口映射报告(含入参)\n\n`;
|
||
md += `> 生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
|
||
md += `## 统计\n\n`;
|
||
md += `| 指标 | 数值 |\n`;
|
||
md += `|------|------|\n`;
|
||
md += `| 总页面数 | ${apiMapping.length} |\n`;
|
||
md += `| 有运行时数据的页面 | ${matchedPageCount} |\n`;
|
||
md += `| 已匹配接口 | ${matchedApiCount} |\n`;
|
||
md += `| 未匹配接口(待采集) | ${unmatchedApiCount} |\n\n`;
|
||
|
||
// 按父菜单分组
|
||
const groups = {};
|
||
for (const item of apiMapping) {
|
||
const parent = item.parentMenu || '未分类';
|
||
if (!groups[parent]) groups[parent] = [];
|
||
groups[parent].push(item);
|
||
}
|
||
|
||
for (const [groupName, items] of Object.entries(groups)) {
|
||
md += `## ${groupName}\n\n`;
|
||
|
||
for (const item of items) {
|
||
if (item.status === 'unmatched' || item.status === 'dir_not_found') continue;
|
||
|
||
md += `### ${item.menuName}\n`;
|
||
md += `- 路径: \`${item.menuUrl}\`\n`;
|
||
md += `- 组件: \`${item.componentDir || item.componentPath}\`\n`;
|
||
md += `- 调用了 **${item.apiCount}** 个接口\n\n`;
|
||
|
||
if (item.apis && item.apis.length > 0) {
|
||
item.apis.forEach((api, idx) => {
|
||
const method = api.httpMethod || '-';
|
||
const status = api.hasRuntimeData ? '✅ 已采集' : '⏳ 待采集';
|
||
|
||
md += `#### ${idx + 1}. \`${api.functionName || '-'}\`\n\n`;
|
||
md += `- **方法**: \`${method}\`\n`;
|
||
md += `- **接口**: \`${api.path}\`\n`;
|
||
md += `- **完整URL**: \`${api.fullUrl}\`\n`;
|
||
md += `- **来源**: \`${api.serviceFile || '-'}\`\n`;
|
||
md += `- **状态**: ${status}\n`;
|
||
|
||
// 详细入参结构
|
||
if (api.dataStructure && Object.keys(api.dataStructure).length > 0) {
|
||
md += `- **请求体 (data/body)**:\n`;
|
||
md += '```json\n';
|
||
md += JSON.stringify(api.dataStructure, null, 2) + '\n';
|
||
md += '```\n';
|
||
}
|
||
if (api.paramsStructure && Object.keys(api.paramsStructure).length > 0) {
|
||
md += `- **查询参数 (params/query)**:\n`;
|
||
md += '```json\n';
|
||
md += JSON.stringify(api.paramsStructure, null, 2) + '\n';
|
||
md += '```\n';
|
||
}
|
||
|
||
// 示例请求参数展示
|
||
if ((api.exampleData && Object.keys(api.exampleData).length > 0) ||
|
||
(api.exampleParams && Object.keys(api.exampleParams).length > 0)) {
|
||
md += `- **示例请求参数 (运行时真实数据)**:\n`;
|
||
md += '```json\n';
|
||
const fullExample = {};
|
||
if (api.exampleData) fullExample.body = api.exampleData;
|
||
if (api.exampleParams) fullExample.query = api.exampleParams;
|
||
md += JSON.stringify(fullExample, null, 2) + '\n';
|
||
md += '```\n';
|
||
}
|
||
if (!api.hasRuntimeData) {
|
||
md += `- **入参**: 暂无运行时数据\n`;
|
||
} else if (
|
||
(!api.dataStructure || Object.keys(api.dataStructure).length === 0) &&
|
||
(!api.paramsStructure || Object.keys(api.paramsStructure).length === 0)
|
||
) {
|
||
md += `- **入参**: 无参数\n`;
|
||
}
|
||
|
||
md += '\n';
|
||
});
|
||
}
|
||
}
|
||
|
||
md += '---\n\n';
|
||
}
|
||
|
||
fs.writeFileSync(mdOutput, md, 'utf-8');
|
||
console.log(`📝 Markdown 已输出: ${mdOutput}`);
|