generate-api-docs.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. #!/usr/bin/env node
  2. import fs from 'fs';
  3. import path from 'path';
  4. import { fileURLToPath } from 'url';
  5. const __filename = fileURLToPath(import.meta.url);
  6. const __dirname = path.dirname(__filename);
  7. const projectRoot = path.resolve(__dirname, '..');
  8. // CRUD操作映射
  9. const CRUD_OPERATIONS = {
  10. list: { method: 'POST', path: '/list', description: '获取列表' },
  11. detail: { method: 'GET', path: '/detail', description: '获取详情' },
  12. add: { method: 'POST', path: '/add', description: '新增' },
  13. edit: { method: 'PUT', path: '/update', description: '更新' },
  14. delete: { method: 'DELETE', path: '/delete', description: '删除' },
  15. export: { method: 'GET', path: '/export', description: '导出' },
  16. report: { method: 'GET', path: '/report', description: '报表' },
  17. };
  18. // 扫描所有service文件
  19. function scanServiceFiles(dir) {
  20. const serviceFiles = [];
  21. function scanDirectory(currentDir) {
  22. const items = fs.readdirSync(currentDir, { withFileTypes: true });
  23. for (const item of items) {
  24. const fullPath = path.join(currentDir, item.name);
  25. if (item.isDirectory()) {
  26. scanDirectory(fullPath);
  27. } else if (
  28. item.name.endsWith('.service.js') ||
  29. (item.name === 'index.js' && (fullPath.includes('/api/') || fullPath.includes('\\api\\')))
  30. ) {
  31. serviceFiles.push({
  32. path: fullPath,
  33. relativePath: path.relative(projectRoot, fullPath),
  34. moduleName: extractModuleName(fullPath),
  35. });
  36. }
  37. }
  38. }
  39. scanDirectory(dir);
  40. return serviceFiles;
  41. }
  42. // 提取模块名称
  43. function extractModuleName(filePath) {
  44. const parts = filePath.split(path.sep);
  45. // 对于 .service.js 文件
  46. const serviceIndex = parts.findIndex((part) => part.endsWith('.service.js'));
  47. if (serviceIndex > 0) {
  48. return parts[serviceIndex - 1];
  49. }
  50. // 对于 api/index.js 文件
  51. const apiIndex = parts.findIndex((part) => part === 'api');
  52. if (apiIndex > 0 && parts[apiIndex + 1] === 'index.js') {
  53. // 取api目录的上一级目录作为模块名
  54. return parts[apiIndex - 1];
  55. }
  56. return path.basename(filePath, '.js');
  57. }
  58. // 解析service文件内容
  59. async function parseServiceFile(filePath) {
  60. try {
  61. const content = fs.readFileSync(filePath, 'utf-8');
  62. // 提取API对象 - 支持多种格式
  63. let apiContent = null;
  64. // 格式1: const api = {...};
  65. let apiMatch = content.match(/const api = \{([\s\S]*?)\};/);
  66. if (apiMatch) {
  67. apiContent = apiMatch[1];
  68. } else {
  69. // 格式2: export default {...}
  70. apiMatch = content.match(/export default \{([\s\S]*?)\};?$/m);
  71. if (apiMatch) {
  72. apiContent = apiMatch[1];
  73. }
  74. }
  75. if (!apiContent) {
  76. return null;
  77. }
  78. const apis = parseApiContent(apiContent, filePath);
  79. return {
  80. filePath,
  81. apis,
  82. };
  83. } catch (error) {
  84. console.error(`解析文件失败 ${filePath}:`, error.message);
  85. return null;
  86. }
  87. }
  88. // 解析API内容
  89. function parseApiContent(content, filePath) {
  90. const apis = [];
  91. // 首先解析嵌套的API结构
  92. const nestedApiRegex = /(\w+):\s*\{([\s\S]*?)(?=\w+:\s*\{|$)/g;
  93. let match;
  94. while ((match = nestedApiRegex.exec(content)) !== null) {
  95. const [, apiName, apiBody] = match;
  96. // 检查是否包含CRUD或方法定义
  97. if (apiBody.includes('CRUD') || apiBody.includes('=>') || apiBody.includes('request(')) {
  98. const cleanApiBody = apiBody.replace(/,\s*$/, '').trim();
  99. const apiInfo = parseApiBody(apiName, cleanApiBody, filePath);
  100. if (apiInfo) {
  101. apis.push(apiInfo);
  102. }
  103. }
  104. }
  105. // 同时解析顶层方法
  106. const topLevelEndpoints = parseCustomEndpoints(content);
  107. if (topLevelEndpoints.length > 0) {
  108. const topLevelApi = {
  109. name: 'api',
  110. endpoints: topLevelEndpoints,
  111. };
  112. apis.push(topLevelApi);
  113. }
  114. return apis;
  115. }
  116. // 解析API主体
  117. function parseApiBody(apiName, body, filePath) {
  118. const api = {
  119. name: apiName,
  120. endpoints: [],
  121. };
  122. // 检查是否使用CRUD
  123. const crudMatch = body.match(/\.\.\.CRUD\(['"`]([^'"`]+)['"`](?:,\s*\[([^\]]*)\])?\)/);
  124. if (crudMatch) {
  125. const [, basePath, methodsStr] = crudMatch;
  126. const methods = methodsStr
  127. ? methodsStr.split(',').map((m) => m.trim().replace(/['"`]/g, ''))
  128. : ['list', 'detail', 'add', 'edit', 'delete'];
  129. // 添加CRUD端点
  130. methods.forEach((method) => {
  131. if (CRUD_OPERATIONS[method]) {
  132. const operation = CRUD_OPERATIONS[method];
  133. const fullUrl = basePath + operation.path;
  134. // 过滤掉以 shop/admin/ 开头的接口
  135. if (!fullUrl.startsWith('shop/admin/')) {
  136. api.endpoints.push({
  137. name: method,
  138. method: operation.method,
  139. url: fullUrl,
  140. description: operation.description,
  141. type: 'CRUD',
  142. });
  143. }
  144. }
  145. });
  146. }
  147. // 解析自定义端点
  148. const customEndpoints = parseCustomEndpoints(body);
  149. api.endpoints.push(...customEndpoints);
  150. return api.endpoints.length > 0 ? api : null;
  151. }
  152. // 解析自定义端点
  153. function parseCustomEndpoints(body) {
  154. const endpoints = [];
  155. // 匹配自定义方法 - 支持多行格式
  156. const methodPattern = /(\w+):\s*\([^)]*\)\s*=>\s*([\s\S]*?)(?=,\s*\w+:|$)/g;
  157. let match;
  158. while ((match = methodPattern.exec(body)) !== null) {
  159. const [, methodName, methodBody] = match;
  160. // 跳过已知的CRUD方法和展开操作
  161. if (
  162. ['list', 'detail', 'add', 'edit', 'delete', 'export', 'report'].includes(methodName) ||
  163. methodBody.includes('...CRUD')
  164. ) {
  165. continue;
  166. }
  167. // 解析request调用
  168. const requestMatch = methodBody.match(/request\(\{([\s\S]*?)\}\)/);
  169. if (requestMatch) {
  170. const requestConfig = requestMatch[1];
  171. // 提取URL和方法
  172. const urlMatch = requestConfig.match(/url:\s*['"`]([^'"`]+)['"`]/);
  173. const methodMatch = requestConfig.match(/method:\s*['"`]([^'"`]+)['"`]/);
  174. if (urlMatch) {
  175. const url = urlMatch[1];
  176. // 过滤掉以 shop/admin/ 开头的接口
  177. if (!url.startsWith('shop/admin/')) {
  178. const description = generateMethodDescription(methodName);
  179. endpoints.push({
  180. name: methodName,
  181. method: methodMatch ? methodMatch[1] : 'GET',
  182. url: url,
  183. description,
  184. type: 'Custom',
  185. });
  186. }
  187. }
  188. }
  189. // 解析SELECT调用
  190. const selectMatch = methodBody.match(/SELECT\(['"`]([^'"`]+)['"`]/);
  191. if (selectMatch) {
  192. const url = selectMatch[1] + '/select';
  193. // 过滤掉以 shop/admin/ 开头的接口
  194. if (!url.startsWith('shop/admin/')) {
  195. endpoints.push({
  196. name: methodName,
  197. method: 'GET',
  198. url: url,
  199. description: generateMethodDescription(methodName),
  200. type: 'Custom',
  201. });
  202. }
  203. }
  204. }
  205. return endpoints;
  206. }
  207. // 生成方法描述
  208. function generateMethodDescription(methodName) {
  209. const descriptions = {
  210. batchShowStatus: '批量上下架商品',
  211. getStatusNum: '获取商品状态数量',
  212. select: '选择列表',
  213. getType: '获取类型',
  214. dispatch: '发货',
  215. dispatchByUpload: '上传发货单发货',
  216. total: '获取总览数据',
  217. ranking: '获取排行数据',
  218. chart: '获取图表数据',
  219. getStats: '获取统计数据',
  220. userDetail: '获取用户详情',
  221. myUsers: '获取我的用户',
  222. getUserAddresses: '获取用户地址',
  223. share: '分享相关',
  224. coupon: '优惠券相关',
  225. couponList: '优惠券列表',
  226. };
  227. return descriptions[methodName] || `自定义接口 - ${methodName}`;
  228. }
  229. // 生成Markdown文档
  230. function generateMarkdown(serviceData) {
  231. let markdown = `# 系统API接口文档\n\n`;
  232. markdown += `> 自动生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
  233. markdown += `## 📋 目录\n\n`;
  234. // 生成目录
  235. serviceData.forEach((service, index) => {
  236. markdown += `${index + 1}. [${service.moduleName}模块](#${index + 1}-${service.moduleName}模块)\n`;
  237. });
  238. markdown += `\n---\n\n`;
  239. // 生成详细内容
  240. serviceData.forEach((service, index) => {
  241. markdown += `## ${index + 1}. ${service.moduleName}模块\n\n`;
  242. markdown += `**文件路径**: \`${service.relativePath}\`\n\n`;
  243. if (service.data && service.data.apis) {
  244. service.data.apis.forEach((api) => {
  245. markdown += `### ${api.name}\n\n`;
  246. if (api.endpoints.length > 0) {
  247. markdown += `| 接口名称 | 请求方法 | 接口地址 | 说明 | 类型 |\n`;
  248. markdown += `|---------|---------|---------|------|------|\n`;
  249. api.endpoints.forEach((endpoint) => {
  250. markdown += `| ${endpoint.name} | ${endpoint.method} | ${endpoint.url} | ${endpoint.description} | ${endpoint.type} |\n`;
  251. });
  252. markdown += `\n`;
  253. }
  254. });
  255. }
  256. markdown += `---\n\n`;
  257. });
  258. // 添加统计信息
  259. const totalApis = serviceData.reduce((total, service) => {
  260. if (service.data && service.data.apis) {
  261. return (
  262. total + service.data.apis.reduce((apiTotal, api) => apiTotal + api.endpoints.length, 0)
  263. );
  264. }
  265. return total;
  266. }, 0);
  267. markdown += `## 📊 统计信息\n\n`;
  268. markdown += `- **模块总数**: ${serviceData.length}\n`;
  269. markdown += `- **接口总数**: ${totalApis}\n`;
  270. markdown += `- **生成时间**: ${new Date().toLocaleString('zh-CN')}\n\n`;
  271. return markdown;
  272. }
  273. // 主函数
  274. async function main() {
  275. console.log('🔍 开始扫描API接口文件...\n');
  276. // 扫描service文件
  277. const srcDir = path.join(projectRoot, 'src');
  278. const serviceFiles = scanServiceFiles(srcDir);
  279. console.log(`📁 找到 ${serviceFiles.length} 个service文件:\n`);
  280. serviceFiles.forEach((file) => {
  281. console.log(` - ${file.relativePath} (${file.moduleName})`);
  282. });
  283. console.log('\n🔄 开始解析API定义...\n');
  284. // 解析每个service文件
  285. const serviceData = [];
  286. for (const file of serviceFiles) {
  287. console.log(`正在解析: ${file.relativePath}`);
  288. const data = await parseServiceFile(file.path);
  289. serviceData.push({
  290. ...file,
  291. data,
  292. });
  293. }
  294. console.log('\n📝 生成API文档...\n');
  295. // 生成文档
  296. const markdown = generateMarkdown(serviceData);
  297. // 保存文档
  298. const outputPath = path.join(projectRoot, 'docs', 'api-interfaces.md');
  299. // 确保docs目录存在
  300. const docsDir = path.dirname(outputPath);
  301. if (!fs.existsSync(docsDir)) {
  302. fs.mkdirSync(docsDir, { recursive: true });
  303. }
  304. fs.writeFileSync(outputPath, markdown, 'utf-8');
  305. console.log(`✅ API文档生成完成!`);
  306. console.log(`📄 文档路径: ${outputPath}`);
  307. console.log(`📊 统计信息:`);
  308. console.log(` - 模块数量: ${serviceData.length}`);
  309. const totalApis = serviceData.reduce((total, service) => {
  310. if (service.data && service.data.apis) {
  311. return (
  312. total + service.data.apis.reduce((apiTotal, api) => apiTotal + api.endpoints.length, 0)
  313. );
  314. }
  315. return total;
  316. }, 0);
  317. console.log(` - 接口数量: ${totalApis}`);
  318. }
  319. main().catch(console.error);