newCloud/scripts/merge_api_data.js
ylj20011123 f651d3c91b update
2026-02-28 18:56:16 +08:00

279 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 合并运行时 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}`);