123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- #!/usr/bin/env node
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- const projectRoot = path.resolve(__dirname, '..');
- // CRUD操作映射
- const CRUD_OPERATIONS = {
- list: { method: 'POST', path: '/list', description: '获取列表' },
- detail: { method: 'GET', path: '/detail', description: '获取详情' },
- add: { method: 'POST', path: '/add', description: '新增' },
- edit: { method: 'PUT', path: '/update', description: '更新' },
- delete: { method: 'DELETE', path: '/delete', description: '删除' },
- export: { method: 'GET', path: '/export', description: '导出' },
- report: { method: 'GET', path: '/report', description: '报表' },
- };
- // 扫描所有service文件
- function scanServiceFiles(dir) {
- const serviceFiles = [];
- function scanDirectory(currentDir) {
- const items = fs.readdirSync(currentDir, { withFileTypes: true });
- for (const item of items) {
- const fullPath = path.join(currentDir, item.name);
- if (item.isDirectory()) {
- scanDirectory(fullPath);
- } else if (
- item.name.endsWith('.service.js') ||
- (item.name === 'index.js' && (fullPath.includes('/api/') || fullPath.includes('\\api\\')))
- ) {
- serviceFiles.push({
- path: fullPath,
- relativePath: path.relative(projectRoot, fullPath),
- moduleName: extractModuleName(fullPath),
- });
- }
- }
- }
- scanDirectory(dir);
- return serviceFiles;
- }
- // 提取模块名称
- function extractModuleName(filePath) {
- const parts = filePath.split(path.sep);
- // 对于 .service.js 文件
- const serviceIndex = parts.findIndex((part) => part.endsWith('.service.js'));
- if (serviceIndex > 0) {
- return parts[serviceIndex - 1];
- }
- // 对于 api/index.js 文件
- const apiIndex = parts.findIndex((part) => part === 'api');
- if (apiIndex > 0 && parts[apiIndex + 1] === 'index.js') {
- // 取api目录的上一级目录作为模块名
- return parts[apiIndex - 1];
- }
- return path.basename(filePath, '.js');
- }
- // 解析service文件内容
- async function parseServiceFile(filePath) {
- try {
- const content = fs.readFileSync(filePath, 'utf-8');
- // 提取API对象 - 支持多种格式
- let apiContent = null;
- // 格式1: const api = {...};
- let apiMatch = content.match(/const api = \{([\s\S]*?)\};/);
- if (apiMatch) {
- apiContent = apiMatch[1];
- } else {
- // 格式2: export default {...}
- apiMatch = content.match(/export default \{([\s\S]*?)\};?$/m);
- if (apiMatch) {
- apiContent = apiMatch[1];
- }
- }
- if (!apiContent) {
- return null;
- }
- const apis = parseApiContent(apiContent, filePath);
- return {
- filePath,
- apis,
- };
- } catch (error) {
- console.error(`解析文件失败 ${filePath}:`, error.message);
- return null;
- }
- }
- // 解析API内容
- function parseApiContent(content, filePath) {
- const apis = [];
- // 首先解析嵌套的API结构
- const nestedApiRegex = /(\w+):\s*\{([\s\S]*?)(?=\w+:\s*\{|$)/g;
- let match;
- while ((match = nestedApiRegex.exec(content)) !== null) {
- const [, apiName, apiBody] = match;
- // 检查是否包含CRUD或方法定义
- if (apiBody.includes('CRUD') || apiBody.includes('=>') || apiBody.includes('request(')) {
- const cleanApiBody = apiBody.replace(/,\s*$/, '').trim();
- const apiInfo = parseApiBody(apiName, cleanApiBody, filePath);
- if (apiInfo) {
- apis.push(apiInfo);
- }
- }
- }
- // 同时解析顶层方法
- const topLevelEndpoints = parseCustomEndpoints(content);
- if (topLevelEndpoints.length > 0) {
- const topLevelApi = {
- name: 'api',
- endpoints: topLevelEndpoints,
- };
- apis.push(topLevelApi);
- }
- return apis;
- }
- // 解析API主体
- function parseApiBody(apiName, body, filePath) {
- const api = {
- name: apiName,
- endpoints: [],
- };
- // 检查是否使用CRUD
- const crudMatch = body.match(/\.\.\.CRUD\(['"`]([^'"`]+)['"`](?:,\s*\[([^\]]*)\])?\)/);
- if (crudMatch) {
- const [, basePath, methodsStr] = crudMatch;
- const methods = methodsStr
- ? methodsStr.split(',').map((m) => m.trim().replace(/['"`]/g, ''))
- : ['list', 'detail', 'add', 'edit', 'delete'];
- // 添加CRUD端点
- methods.forEach((method) => {
- if (CRUD_OPERATIONS[method]) {
- const operation = CRUD_OPERATIONS[method];
- const fullUrl = basePath + operation.path;
- // 过滤掉以 shop/admin/ 开头的接口
- if (!fullUrl.startsWith('shop/admin/')) {
- api.endpoints.push({
- name: method,
- method: operation.method,
- url: fullUrl,
- description: operation.description,
- type: 'CRUD',
- });
- }
- }
- });
- }
- // 解析自定义端点
- const customEndpoints = parseCustomEndpoints(body);
- api.endpoints.push(...customEndpoints);
- return api.endpoints.length > 0 ? api : null;
- }
- // 解析自定义端点
- function parseCustomEndpoints(body) {
- const endpoints = [];
- // 匹配自定义方法 - 支持多行格式
- const methodPattern = /(\w+):\s*\([^)]*\)\s*=>\s*([\s\S]*?)(?=,\s*\w+:|$)/g;
- let match;
- while ((match = methodPattern.exec(body)) !== null) {
- const [, methodName, methodBody] = match;
- // 跳过已知的CRUD方法和展开操作
- if (
- ['list', 'detail', 'add', 'edit', 'delete', 'export', 'report'].includes(methodName) ||
- methodBody.includes('...CRUD')
- ) {
- continue;
- }
- // 解析request调用
- const requestMatch = methodBody.match(/request\(\{([\s\S]*?)\}\)/);
- if (requestMatch) {
- const requestConfig = requestMatch[1];
- // 提取URL和方法
- const urlMatch = requestConfig.match(/url:\s*['"`]([^'"`]+)['"`]/);
- const methodMatch = requestConfig.match(/method:\s*['"`]([^'"`]+)['"`]/);
- if (urlMatch) {
- const url = urlMatch[1];
- // 过滤掉以 shop/admin/ 开头的接口
- if (!url.startsWith('shop/admin/')) {
- const description = generateMethodDescription(methodName);
- endpoints.push({
- name: methodName,
- method: methodMatch ? methodMatch[1] : 'GET',
- url: url,
- description,
- type: 'Custom',
- });
- }
- }
- }
- // 解析SELECT调用
- const selectMatch = methodBody.match(/SELECT\(['"`]([^'"`]+)['"`]/);
- if (selectMatch) {
- const url = selectMatch[1] + '/select';
- // 过滤掉以 shop/admin/ 开头的接口
- if (!url.startsWith('shop/admin/')) {
- endpoints.push({
- name: methodName,
- method: 'GET',
- url: url,
- description: generateMethodDescription(methodName),
- type: 'Custom',
- });
- }
- }
- }
- return endpoints;
- }
- // 生成方法描述
- function generateMethodDescription(methodName) {
- const descriptions = {
- batchShowStatus: '批量上下架商品',
- getStatusNum: '获取商品状态数量',
- select: '选择列表',
- getType: '获取类型',
- dispatch: '发货',
- dispatchByUpload: '上传发货单发货',
- total: '获取总览数据',
- ranking: '获取排行数据',
- chart: '获取图表数据',
- getStats: '获取统计数据',
- userDetail: '获取用户详情',
- myUsers: '获取我的用户',
- getUserAddresses: '获取用户地址',
- share: '分享相关',
- coupon: '优惠券相关',
- couponList: '优惠券列表',
- };
- return descriptions[methodName] || `自定义接口 - ${methodName}`;
- }
- // 生成Markdown文档
- function generateMarkdown(serviceData) {
- let markdown = `# 系统API接口文档\n\n`;
- markdown += `> 自动生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
- markdown += `## 📋 目录\n\n`;
- // 生成目录
- serviceData.forEach((service, index) => {
- markdown += `${index + 1}. [${service.moduleName}模块](#${index + 1}-${service.moduleName}模块)\n`;
- });
- markdown += `\n---\n\n`;
- // 生成详细内容
- serviceData.forEach((service, index) => {
- markdown += `## ${index + 1}. ${service.moduleName}模块\n\n`;
- markdown += `**文件路径**: \`${service.relativePath}\`\n\n`;
- if (service.data && service.data.apis) {
- service.data.apis.forEach((api) => {
- markdown += `### ${api.name}\n\n`;
- if (api.endpoints.length > 0) {
- markdown += `| 接口名称 | 请求方法 | 接口地址 | 说明 | 类型 |\n`;
- markdown += `|---------|---------|---------|------|------|\n`;
- api.endpoints.forEach((endpoint) => {
- markdown += `| ${endpoint.name} | ${endpoint.method} | ${endpoint.url} | ${endpoint.description} | ${endpoint.type} |\n`;
- });
- markdown += `\n`;
- }
- });
- }
- markdown += `---\n\n`;
- });
- // 添加统计信息
- const totalApis = serviceData.reduce((total, service) => {
- if (service.data && service.data.apis) {
- return (
- total + service.data.apis.reduce((apiTotal, api) => apiTotal + api.endpoints.length, 0)
- );
- }
- return total;
- }, 0);
- markdown += `## 📊 统计信息\n\n`;
- markdown += `- **模块总数**: ${serviceData.length}\n`;
- markdown += `- **接口总数**: ${totalApis}\n`;
- markdown += `- **生成时间**: ${new Date().toLocaleString('zh-CN')}\n\n`;
- return markdown;
- }
- // 主函数
- async function main() {
- console.log('🔍 开始扫描API接口文件...\n');
- // 扫描service文件
- const srcDir = path.join(projectRoot, 'src');
- const serviceFiles = scanServiceFiles(srcDir);
- console.log(`📁 找到 ${serviceFiles.length} 个service文件:\n`);
- serviceFiles.forEach((file) => {
- console.log(` - ${file.relativePath} (${file.moduleName})`);
- });
- console.log('\n🔄 开始解析API定义...\n');
- // 解析每个service文件
- const serviceData = [];
- for (const file of serviceFiles) {
- console.log(`正在解析: ${file.relativePath}`);
- const data = await parseServiceFile(file.path);
- serviceData.push({
- ...file,
- data,
- });
- }
- console.log('\n📝 生成API文档...\n');
- // 生成文档
- const markdown = generateMarkdown(serviceData);
- // 保存文档
- const outputPath = path.join(projectRoot, 'docs', 'api-interfaces.md');
- // 确保docs目录存在
- const docsDir = path.dirname(outputPath);
- if (!fs.existsSync(docsDir)) {
- fs.mkdirSync(docsDir, { recursive: true });
- }
- fs.writeFileSync(outputPath, markdown, 'utf-8');
- console.log(`✅ API文档生成完成!`);
- console.log(`📄 文档路径: ${outputPath}`);
- console.log(`📊 统计信息:`);
- console.log(` - 模块数量: ${serviceData.length}`);
- const totalApis = serviceData.reduce((total, service) => {
- if (service.data && service.data.apis) {
- return (
- total + service.data.apis.reduce((apiTotal, api) => apiTotal + api.endpoints.length, 0)
- );
- }
- return total;
- }, 0);
- console.log(` - 接口数量: ${totalApis}`);
- }
- main().catch(console.error);
|