generate-api-docs.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env node
  2. /**
  3. * API文档生成脚本
  4. * 用于从JSDoc注释生成Apifox可导入的OpenAPI文档
  5. */
  6. const fs = require('fs');
  7. const path = require('path');
  8. // 读取文章路由文件
  9. const articlesRoutePath = path.join(__dirname, '../routes/admin/articles.js');
  10. const routeContent = fs.readFileSync(articlesRoutePath, 'utf8');
  11. // 提取JSDoc注释
  12. function extractJSDocComments(content) {
  13. // 匹配包含@api的JSDoc注释块
  14. const jsdocRegex = /\/\*\*[\s\S]*?\*\//g;
  15. const comments = content.match(jsdocRegex) || [];
  16. const apis = [];
  17. comments.forEach((comment, index) => {
  18. // 检查是否包含@api标记
  19. if (comment.includes('@api {') || comment.includes('@apiName')) {
  20. const api = parseJSDocComment(comment);
  21. if (api && api.method && api.path) {
  22. apis.push(api);
  23. }
  24. }
  25. });
  26. return apis;
  27. }
  28. // 解析JSDoc注释
  29. function parseJSDocComment(comment) {
  30. const lines = comment.split('\n');
  31. let api = {};
  32. lines.forEach(line => {
  33. line = line.trim().replace(/^\*\s*/, ''); // 移除JSDoc的*前缀
  34. if (line.startsWith('@api ')) {
  35. // 解析 @api {method} path name 格式
  36. const match = line.match(/@api\s+{(\w+)}\s+(.+?)\s+(.+)/);
  37. if (match) {
  38. api.method = match[1].toLowerCase();
  39. api.path = match[2];
  40. api.name = match[3];
  41. }
  42. } else if (line.startsWith('@apiName ')) {
  43. api.name = line.replace('@apiName ', '');
  44. } else if (line.startsWith('@apiGroup ')) {
  45. api.group = line.replace('@apiGroup ', '');
  46. } else if (line.startsWith('@apiVersion ')) {
  47. api.version = line.replace('@apiVersion ', '');
  48. } else if (line.startsWith('@apiDescription ')) {
  49. api.description = line.replace('@apiDescription ', '');
  50. } else if (line.startsWith('@apiParam ')) {
  51. if (!api.parameters) api.parameters = [];
  52. // 解析参数:@apiParam {type} [name] description
  53. const paramMatch = line.match(/@apiParam\s+{([^}]+)}\s+\[?([^\]]+)\]?\s+(.+)/);
  54. if (paramMatch) {
  55. const param = {
  56. type: paramMatch[1],
  57. name: paramMatch[2],
  58. description: paramMatch[3],
  59. required: !line.includes('[')
  60. };
  61. api.parameters.push(param);
  62. }
  63. } else if (line.startsWith('@apiSuccess ')) {
  64. if (!api.responses) api.responses = {};
  65. const successMatch = line.match(/@apiSuccess\s+{([^}]+)}\s+(.+)/);
  66. if (successMatch) {
  67. api.responses[successMatch[1]] = successMatch[2];
  68. }
  69. }
  70. });
  71. return Object.keys(api).length > 0 ? api : null;
  72. }
  73. // 生成OpenAPI文档
  74. function generateOpenAPIDoc(apis) {
  75. const openapi = {
  76. openapi: "3.0.0",
  77. info: {
  78. title: "飞鸟农业API文档",
  79. version: "1.0.0",
  80. description: "飞鸟农业平台后端API接口文档"
  81. },
  82. servers: [
  83. {
  84. url: "http://localhost:3000",
  85. description: "开发环境"
  86. }
  87. ],
  88. paths: {},
  89. components: {
  90. schemas: {
  91. Article: {
  92. type: "object",
  93. properties: {
  94. id: { type: "integer", description: "文章ID" },
  95. title: { type: "string", description: "文章标题" },
  96. subtitle: { type: "string", description: "文章副标题" },
  97. content: { type: "string", description: "文章内容" },
  98. type: { type: "integer", description: "文章类型" },
  99. img: { type: "string", description: "文章图片URL" },
  100. date: { type: "string", format: "date-time", description: "文章日期" },
  101. author: { type: "string", description: "作者" },
  102. category: { type: "integer", description: "用户分类ID" },
  103. crop: { type: "integer", description: "作物分类ID" },
  104. isRecommended: { type: "integer", enum: [0, 1], description: "是否推荐" },
  105. seoKeyword: { type: "string", description: "SEO关键词" },
  106. seoDescription: { type: "string", description: "SEO描述" },
  107. createdAt: { type: "string", format: "date-time", description: "创建时间" },
  108. updatedAt: { type: "string", format: "date-time", description: "更新时间" },
  109. cropInfo: {
  110. type: "object",
  111. properties: {
  112. id: { type: "integer", description: "作物ID" },
  113. name: { type: "string", description: "作物名称" },
  114. level: { type: "integer", description: "作物层级" },
  115. parentId: { type: "integer", description: "父级作物ID" }
  116. }
  117. }
  118. }
  119. },
  120. ApiResponse: {
  121. type: "object",
  122. properties: {
  123. status: { type: "boolean", description: "请求状态" },
  124. message: { type: "string", description: "响应消息" },
  125. data: { type: "object", description: "响应数据" },
  126. errors: { type: "array", items: { type: "string" }, description: "错误信息" }
  127. }
  128. }
  129. }
  130. }
  131. };
  132. // 转换API到OpenAPI格式
  133. apis.forEach(api => {
  134. const pathKey = api.path;
  135. if (!openapi.paths[pathKey]) {
  136. openapi.paths[pathKey] = {};
  137. }
  138. const operation = {
  139. tags: [api.group || "Default"],
  140. summary: api.name,
  141. description: api.description || "",
  142. parameters: [],
  143. responses: {
  144. "200": {
  145. description: "成功响应",
  146. content: {
  147. "application/json": {
  148. schema: { $ref: "#/components/schemas/ApiResponse" }
  149. }
  150. }
  151. },
  152. "400": {
  153. description: "请求参数错误",
  154. content: {
  155. "application/json": {
  156. schema: { $ref: "#/components/schemas/ApiResponse" }
  157. }
  158. }
  159. },
  160. "404": {
  161. description: "资源未找到",
  162. content: {
  163. "application/json": {
  164. schema: { $ref: "#/components/schemas/ApiResponse" }
  165. }
  166. }
  167. },
  168. "500": {
  169. description: "服务器内部错误",
  170. content: {
  171. "application/json": {
  172. schema: { $ref: "#/components/schemas/ApiResponse" }
  173. }
  174. }
  175. }
  176. }
  177. };
  178. // 添加参数
  179. if (api.parameters) {
  180. api.parameters.forEach(param => {
  181. if (param.name.startsWith(':')) {
  182. // 路径参数
  183. operation.parameters.push({
  184. name: param.name.substring(1),
  185. in: "path",
  186. required: true,
  187. schema: { type: param.type.toLowerCase() },
  188. description: param.description
  189. });
  190. } else {
  191. // 查询参数
  192. operation.parameters.push({
  193. name: param.name,
  194. in: "query",
  195. required: param.required,
  196. schema: { type: param.type.toLowerCase() },
  197. description: param.description
  198. });
  199. }
  200. });
  201. }
  202. // 添加请求体(POST/PUT请求)
  203. if (['post', 'put', 'patch'].includes(api.method)) {
  204. operation.requestBody = {
  205. required: true,
  206. content: {
  207. "application/json": {
  208. schema: { $ref: "#/components/schemas/Article" }
  209. }
  210. }
  211. };
  212. }
  213. openapi.paths[pathKey][api.method] = operation;
  214. });
  215. return openapi;
  216. }
  217. // 主函数
  218. function main() {
  219. console.log('正在生成API文档...');
  220. try {
  221. // 提取JSDoc注释
  222. const apis = extractJSDocComments(routeContent);
  223. console.log(`找到 ${apis.length} 个API接口`);
  224. // 生成OpenAPI文档
  225. const openapiDoc = generateOpenAPIDoc(apis);
  226. // 保存文档
  227. const outputPath = path.join(__dirname, '../docs/api-docs.json');
  228. const outputDir = path.dirname(outputPath);
  229. if (!fs.existsSync(outputDir)) {
  230. fs.mkdirSync(outputDir, { recursive: true });
  231. }
  232. fs.writeFileSync(outputPath, JSON.stringify(openapiDoc, null, 2));
  233. console.log(`API文档已生成: ${outputPath}`);
  234. // 同时生成YAML格式(Apifox支持)
  235. const yaml = require('js-yaml');
  236. const yamlPath = path.join(__dirname, '../docs/api-docs.yaml');
  237. fs.writeFileSync(yamlPath, yaml.dump(openapiDoc));
  238. console.log(`API文档已生成: ${yamlPath}`);
  239. console.log('\n=== 如何在Apifox中导入 ===');
  240. console.log('1. 打开Apifox');
  241. console.log('2. 选择项目 -> 导入');
  242. console.log('3. 选择"OpenAPI"格式');
  243. console.log('4. 导入生成的 api-docs.yaml 或 api-docs.json 文件');
  244. console.log('5. 完成导入后,所有接口注释都会显示在Apifox中');
  245. } catch (error) {
  246. console.error('生成API文档失败:', error.message);
  247. process.exit(1);
  248. }
  249. }
  250. // 运行脚本
  251. if (require.main === module) {
  252. main();
  253. }
  254. module.exports = { extractJSDocComments, generateOpenAPIDoc };