123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- #!/usr/bin/env node
- /**
- * API文档生成脚本
- * 用于从JSDoc注释生成Apifox可导入的OpenAPI文档
- */
- const fs = require('fs');
- const path = require('path');
- // 读取文章路由文件
- const articlesRoutePath = path.join(__dirname, '../routes/admin/articles.js');
- const routeContent = fs.readFileSync(articlesRoutePath, 'utf8');
- // 提取JSDoc注释
- function extractJSDocComments(content) {
- // 匹配包含@api的JSDoc注释块
- const jsdocRegex = /\/\*\*[\s\S]*?\*\//g;
- const comments = content.match(jsdocRegex) || [];
-
- const apis = [];
-
- comments.forEach((comment, index) => {
- // 检查是否包含@api标记
- if (comment.includes('@api {') || comment.includes('@apiName')) {
- const api = parseJSDocComment(comment);
- if (api && api.method && api.path) {
- apis.push(api);
- }
- }
- });
-
- return apis;
- }
- // 解析JSDoc注释
- function parseJSDocComment(comment) {
- const lines = comment.split('\n');
- let api = {};
-
- lines.forEach(line => {
- line = line.trim().replace(/^\*\s*/, ''); // 移除JSDoc的*前缀
-
- if (line.startsWith('@api ')) {
- // 解析 @api {method} path name 格式
- const match = line.match(/@api\s+{(\w+)}\s+(.+?)\s+(.+)/);
- if (match) {
- api.method = match[1].toLowerCase();
- api.path = match[2];
- api.name = match[3];
- }
- } else if (line.startsWith('@apiName ')) {
- api.name = line.replace('@apiName ', '');
- } else if (line.startsWith('@apiGroup ')) {
- api.group = line.replace('@apiGroup ', '');
- } else if (line.startsWith('@apiVersion ')) {
- api.version = line.replace('@apiVersion ', '');
- } else if (line.startsWith('@apiDescription ')) {
- api.description = line.replace('@apiDescription ', '');
- } else if (line.startsWith('@apiParam ')) {
- if (!api.parameters) api.parameters = [];
-
- // 解析参数:@apiParam {type} [name] description
- const paramMatch = line.match(/@apiParam\s+{([^}]+)}\s+\[?([^\]]+)\]?\s+(.+)/);
- if (paramMatch) {
- const param = {
- type: paramMatch[1],
- name: paramMatch[2],
- description: paramMatch[3],
- required: !line.includes('[')
- };
- api.parameters.push(param);
- }
- } else if (line.startsWith('@apiSuccess ')) {
- if (!api.responses) api.responses = {};
-
- const successMatch = line.match(/@apiSuccess\s+{([^}]+)}\s+(.+)/);
- if (successMatch) {
- api.responses[successMatch[1]] = successMatch[2];
- }
- }
- });
-
-
- return Object.keys(api).length > 0 ? api : null;
- }
- // 生成OpenAPI文档
- function generateOpenAPIDoc(apis) {
- const openapi = {
- openapi: "3.0.0",
- info: {
- title: "飞鸟农业API文档",
- version: "1.0.0",
- description: "飞鸟农业平台后端API接口文档"
- },
- servers: [
- {
- url: "http://localhost:3000",
- description: "开发环境"
- }
- ],
- paths: {},
- components: {
- schemas: {
- Article: {
- type: "object",
- properties: {
- id: { type: "integer", description: "文章ID" },
- title: { type: "string", description: "文章标题" },
- subtitle: { type: "string", description: "文章副标题" },
- content: { type: "string", description: "文章内容" },
- type: { type: "integer", description: "文章类型" },
- img: { type: "string", description: "文章图片URL" },
- date: { type: "string", format: "date-time", description: "文章日期" },
- author: { type: "string", description: "作者" },
- category: { type: "integer", description: "用户分类ID" },
- crop: { type: "integer", description: "作物分类ID" },
- isRecommended: { type: "integer", enum: [0, 1], description: "是否推荐" },
- seoKeyword: { type: "string", description: "SEO关键词" },
- seoDescription: { type: "string", description: "SEO描述" },
- createdAt: { type: "string", format: "date-time", description: "创建时间" },
- updatedAt: { type: "string", format: "date-time", description: "更新时间" },
- cropInfo: {
- type: "object",
- properties: {
- id: { type: "integer", description: "作物ID" },
- name: { type: "string", description: "作物名称" },
- level: { type: "integer", description: "作物层级" },
- parentId: { type: "integer", description: "父级作物ID" }
- }
- }
- }
- },
- ApiResponse: {
- type: "object",
- properties: {
- status: { type: "boolean", description: "请求状态" },
- message: { type: "string", description: "响应消息" },
- data: { type: "object", description: "响应数据" },
- errors: { type: "array", items: { type: "string" }, description: "错误信息" }
- }
- }
- }
- }
- };
-
- // 转换API到OpenAPI格式
- apis.forEach(api => {
- const pathKey = api.path;
- if (!openapi.paths[pathKey]) {
- openapi.paths[pathKey] = {};
- }
-
- const operation = {
- tags: [api.group || "Default"],
- summary: api.name,
- description: api.description || "",
- parameters: [],
- responses: {
- "200": {
- description: "成功响应",
- content: {
- "application/json": {
- schema: { $ref: "#/components/schemas/ApiResponse" }
- }
- }
- },
- "400": {
- description: "请求参数错误",
- content: {
- "application/json": {
- schema: { $ref: "#/components/schemas/ApiResponse" }
- }
- }
- },
- "404": {
- description: "资源未找到",
- content: {
- "application/json": {
- schema: { $ref: "#/components/schemas/ApiResponse" }
- }
- }
- },
- "500": {
- description: "服务器内部错误",
- content: {
- "application/json": {
- schema: { $ref: "#/components/schemas/ApiResponse" }
- }
- }
- }
- }
- };
-
- // 添加参数
- if (api.parameters) {
- api.parameters.forEach(param => {
- if (param.name.startsWith(':')) {
- // 路径参数
- operation.parameters.push({
- name: param.name.substring(1),
- in: "path",
- required: true,
- schema: { type: param.type.toLowerCase() },
- description: param.description
- });
- } else {
- // 查询参数
- operation.parameters.push({
- name: param.name,
- in: "query",
- required: param.required,
- schema: { type: param.type.toLowerCase() },
- description: param.description
- });
- }
- });
- }
-
- // 添加请求体(POST/PUT请求)
- if (['post', 'put', 'patch'].includes(api.method)) {
- operation.requestBody = {
- required: true,
- content: {
- "application/json": {
- schema: { $ref: "#/components/schemas/Article" }
- }
- }
- };
- }
-
- openapi.paths[pathKey][api.method] = operation;
- });
-
- return openapi;
- }
- // 主函数
- function main() {
- console.log('正在生成API文档...');
-
- try {
- // 提取JSDoc注释
- const apis = extractJSDocComments(routeContent);
- console.log(`找到 ${apis.length} 个API接口`);
-
- // 生成OpenAPI文档
- const openapiDoc = generateOpenAPIDoc(apis);
-
- // 保存文档
- const outputPath = path.join(__dirname, '../docs/api-docs.json');
- const outputDir = path.dirname(outputPath);
-
- if (!fs.existsSync(outputDir)) {
- fs.mkdirSync(outputDir, { recursive: true });
- }
-
- fs.writeFileSync(outputPath, JSON.stringify(openapiDoc, null, 2));
- console.log(`API文档已生成: ${outputPath}`);
-
- // 同时生成YAML格式(Apifox支持)
- const yaml = require('js-yaml');
- const yamlPath = path.join(__dirname, '../docs/api-docs.yaml');
- fs.writeFileSync(yamlPath, yaml.dump(openapiDoc));
- console.log(`API文档已生成: ${yamlPath}`);
-
- console.log('\n=== 如何在Apifox中导入 ===');
- console.log('1. 打开Apifox');
- console.log('2. 选择项目 -> 导入');
- console.log('3. 选择"OpenAPI"格式');
- console.log('4. 导入生成的 api-docs.yaml 或 api-docs.json 文件');
- console.log('5. 完成导入后,所有接口注释都会显示在Apifox中');
-
- } catch (error) {
- console.error('生成API文档失败:', error.message);
- process.exit(1);
- }
- }
- // 运行脚本
- if (require.main === module) {
- main();
- }
- module.exports = { extractJSDocComments, generateOpenAPIDoc };
|