Parcourir la source

feat:修改接口,添加apifox文档注释

wangsisi il y a 12 heures
Parent
commit
5fb8d036c3

+ 105 - 0
docs/README.md

@@ -0,0 +1,105 @@
+# API文档生成说明
+
+## 概述
+
+本项目使用JSDoc注释格式为API接口添加文档注释,并通过脚本自动生成OpenAPI格式的文档,可以直接导入到Apifox中进行API测试和文档管理。
+
+## 使用方法
+
+### 1. 生成API文档
+
+```bash
+# 方法1:使用npm脚本
+npm run docs:generate
+
+# 方法2:直接运行脚本
+node scripts/generate-api-docs.js
+```
+
+### 2. 在Apifox中导入
+
+1. 打开Apifox
+2. 选择项目 → 导入
+3. 选择"OpenAPI"格式
+4. 导入生成的 `api-docs.yaml` 或 `api-docs.json` 文件
+5. 完成导入后,所有接口注释都会显示在Apifox中
+
+## JSDoc注释格式
+
+### 基本结构
+
+```javascript
+/**
+ * @api {method} path 接口名称
+ * @apiName 接口名称
+ * @apiGroup 分组名称
+ * @apiVersion 版本号
+ * 
+ * @apiDescription 接口描述
+ * 
+ * @apiParam {Type} [paramName] 参数描述
+ * @apiSuccess {Type} fieldName 返回字段描述
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 200 OK
+ * {
+ *   "status": true,
+ *   "message": "成功",
+ *   "data": {}
+ * }
+ */
+```
+
+### 参数类型
+
+- `{String}` - 字符串类型
+- `{Number}` - 数字类型
+- `{Boolean}` - 布尔类型
+- `{Object}` - 对象类型
+- `{Array}` - 数组类型
+- `{Date}` - 日期类型
+
+### 参数说明
+
+- `[paramName]` - 可选参数
+- `paramName` - 必填参数
+- `:id` - 路径参数
+
+## 支持的API接口
+
+目前已添加文档注释的接口:
+
+1. **GET /admin/articles** - 查询文章列表
+   - 支持分页:`currentPage`, `pageSize`
+   - 支持筛选:`title`, `cropIds`, `categoryId`, `isRecommended`
+
+2. **GET /admin/articles/:id** - 查询文章详情
+
+3. **POST /admin/articles** - 创建文章
+   - 必填字段:`title`, `content`
+   - 可选字段:`subtitle`, `isRecommended`, `category`, `crop` 等
+
+4. **PUT /admin/articles/:id** - 更新文章
+
+5. **DELETE /admin/articles/:id** - 删除文章
+
+## 文件说明
+
+- `scripts/generate-api-docs.js` - API文档生成脚本
+- `docs/api-docs.json` - 生成的JSON格式API文档
+- `docs/api-docs.yaml` - 生成的YAML格式API文档
+
+## 注意事项
+
+1. 修改API接口后,需要重新运行生成脚本
+2. JSDoc注释必须严格按照格式编写,否则可能解析失败
+3. 生成的文档会覆盖之前的文件
+4. 建议在每次API修改后都重新生成文档
+
+## 扩展
+
+如需为其他路由文件添加API文档,请:
+
+1. 在相应的路由文件中添加JSDoc注释
+2. 修改 `scripts/generate-api-docs.js` 中的文件路径
+3. 重新运行生成脚本

+ 713 - 0
docs/api-docs.json

@@ -0,0 +1,713 @@
+{
+  "openapi": "3.0.0",
+  "info": {
+    "title": "飞鸟农业API文档",
+    "version": "1.0.0",
+    "description": "飞鸟农业平台后端API接口文档"
+  },
+  "servers": [
+    {
+      "url": "http://localhost:3000",
+      "description": "开发环境"
+    }
+  ],
+  "paths": {
+    "/admin/articles": {
+      "get": {
+        "tags": [
+          "Articles"
+        ],
+        "summary": "GetArticles",
+        "description": "获取文章列表,支持分页和多条件筛选",
+        "parameters": [
+          {
+            "name": "currentPage=1",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "当前页码,默认为1"
+          },
+          {
+            "name": "pageSize=10",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "每页显示数量,默认为10"
+          },
+          {
+            "name": "title",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "标题搜索关键词(模糊匹配)"
+          },
+          {
+            "name": "cropIds",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string|array"
+            },
+            "description": "作物筛选ID,支持逗号分隔的多个ID或数组形式"
+          },
+          {
+            "name": "categoryId",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "用户分类ID,根据用户传递的category参数值进行精确匹配"
+          },
+          {
+            "name": "isRecommended",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "推荐筛选,0-非推荐文章,1-推荐文章"
+          }
+        ],
+        "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"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "tags": [
+          "Articles"
+        ],
+        "summary": "CreateArticle",
+        "description": "创建新的文章,支持富文本内容和图片",
+        "parameters": [
+          {
+            "name": "title",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章标题(必填,1-500字符)"
+          },
+          {
+            "name": "content",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章内容(必填,富文本格式,最大5MB)"
+          },
+          {
+            "name": "type",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "文章类型"
+          },
+          {
+            "name": "img",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章图片URL"
+          },
+          {
+            "name": "date",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "date"
+            },
+            "description": "文章发布日期"
+          },
+          {
+            "name": "author",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "作者"
+          },
+          {
+            "name": "category",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "用户分类ID(用户传递的参数)"
+          },
+          {
+            "name": "crop",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "作物分类ID"
+          },
+          {
+            "name": "isRecommended=0",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "是否推荐,0-不推荐,1-推荐"
+          },
+          {
+            "name": "subtitle",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "副标题(最大200字符)"
+          },
+          {
+            "name": "seoKeyword",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "SEO关键词"
+          },
+          {
+            "name": "seoDescription",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "SEO描述"
+          }
+        ],
+        "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"
+                }
+              }
+            }
+          }
+        },
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Article"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/admin/articles/:id": {
+      "get": {
+        "tags": [
+          "Articles"
+        ],
+        "summary": "GetArticleById",
+        "description": "根据文章ID获取文章详细信息",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "number"
+            },
+            "description": "文章ID(路径参数)"
+          }
+        ],
+        "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"
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "Articles"
+        ],
+        "summary": "DeleteArticle",
+        "description": "根据文章ID删除文章",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "number"
+            },
+            "description": "文章ID(路径参数)"
+          }
+        ],
+        "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"
+                }
+              }
+            }
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "Articles"
+        ],
+        "summary": "UpdateArticle",
+        "description": "根据文章ID更新文章信息",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "number"
+            },
+            "description": "文章ID(路径参数)"
+          },
+          {
+            "name": "title",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章标题(1-500字符)"
+          },
+          {
+            "name": "content",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章内容(富文本格式,最大5MB)"
+          },
+          {
+            "name": "type",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "文章类型"
+          },
+          {
+            "name": "img",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "文章图片URL"
+          },
+          {
+            "name": "date",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "date"
+            },
+            "description": "文章发布日期"
+          },
+          {
+            "name": "author",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "作者"
+          },
+          {
+            "name": "category",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "用户分类ID"
+          },
+          {
+            "name": "crop",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "作物分类ID"
+          },
+          {
+            "name": "isRecommended",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "number"
+            },
+            "description": "是否推荐,0-不推荐,1-推荐"
+          },
+          {
+            "name": "subtitle",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "副标题(最大200字符)"
+          },
+          {
+            "name": "seoKeyword",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "SEO关键词"
+          },
+          {
+            "name": "seoDescription",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            },
+            "description": "SEO描述"
+          }
+        ],
+        "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"
+                }
+              }
+            }
+          }
+        },
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Article"
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "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": "错误信息"
+          }
+        }
+      }
+    }
+  }
+}

+ 464 - 0
docs/api-docs.yaml

@@ -0,0 +1,464 @@
+openapi: 3.0.0
+info:
+  title: 飞鸟农业API文档
+  version: 1.0.0
+  description: 飞鸟农业平台后端API接口文档
+servers:
+  - url: http://localhost:3000
+    description: 开发环境
+paths:
+  /admin/articles:
+    get:
+      tags:
+        - Articles
+      summary: GetArticles
+      description: 获取文章列表,支持分页和多条件筛选
+      parameters:
+        - name: currentPage=1
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 当前页码,默认为1
+        - name: pageSize=10
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 每页显示数量,默认为10
+        - name: title
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 标题搜索关键词(模糊匹配)
+        - name: cropIds
+          in: query
+          required: false
+          schema:
+            type: string|array
+          description: 作物筛选ID,支持逗号分隔的多个ID或数组形式
+        - name: categoryId
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 用户分类ID,根据用户传递的category参数值进行精确匹配
+        - name: isRecommended
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 推荐筛选,0-非推荐文章,1-推荐文章
+      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'
+    post:
+      tags:
+        - Articles
+      summary: CreateArticle
+      description: 创建新的文章,支持富文本内容和图片
+      parameters:
+        - name: title
+          in: query
+          required: true
+          schema:
+            type: string
+          description: 文章标题(必填,1-500字符)
+        - name: content
+          in: query
+          required: true
+          schema:
+            type: string
+          description: 文章内容(必填,富文本格式,最大5MB)
+        - name: type
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 文章类型
+        - name: img
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 文章图片URL
+        - name: date
+          in: query
+          required: false
+          schema:
+            type: date
+          description: 文章发布日期
+        - name: author
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 作者
+        - name: category
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 用户分类ID(用户传递的参数)
+        - name: crop
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 作物分类ID
+        - name: isRecommended=0
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 是否推荐,0-不推荐,1-推荐
+        - name: subtitle
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 副标题(最大200字符)
+        - name: seoKeyword
+          in: query
+          required: false
+          schema:
+            type: string
+          description: SEO关键词
+        - name: seoDescription
+          in: query
+          required: false
+          schema:
+            type: string
+          description: SEO描述
+      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'
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Article'
+  /admin/articles/:id:
+    get:
+      tags:
+        - Articles
+      summary: GetArticleById
+      description: 根据文章ID获取文章详细信息
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: number
+          description: 文章ID(路径参数)
+      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'
+    delete:
+      tags:
+        - Articles
+      summary: DeleteArticle
+      description: 根据文章ID删除文章
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: number
+          description: 文章ID(路径参数)
+      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'
+    put:
+      tags:
+        - Articles
+      summary: UpdateArticle
+      description: 根据文章ID更新文章信息
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: number
+          description: 文章ID(路径参数)
+        - name: title
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 文章标题(1-500字符)
+        - name: content
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 文章内容(富文本格式,最大5MB)
+        - name: type
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 文章类型
+        - name: img
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 文章图片URL
+        - name: date
+          in: query
+          required: false
+          schema:
+            type: date
+          description: 文章发布日期
+        - name: author
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 作者
+        - name: category
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 用户分类ID
+        - name: crop
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 作物分类ID
+        - name: isRecommended
+          in: query
+          required: false
+          schema:
+            type: number
+          description: 是否推荐,0-不推荐,1-推荐
+        - name: subtitle
+          in: query
+          required: false
+          schema:
+            type: string
+          description: 副标题(最大200字符)
+        - name: seoKeyword
+          in: query
+          required: false
+          schema:
+            type: string
+          description: SEO关键词
+        - name: seoDescription
+          in: query
+          required: false
+          schema:
+            type: string
+          description: SEO描述
+      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'
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Article'
+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: 错误信息

+ 24 - 0
migrations/20250914092031-add-is-recommended-and-subtitle-to-articles.js

@@ -0,0 +1,24 @@
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+  async up (queryInterface, Sequelize) {
+    await queryInterface.addColumn('Articles', 'isRecommended', {
+      type: Sequelize.INTEGER,
+      allowNull: true,
+      defaultValue: 0,
+      comment: '是否推荐:0-不推荐,1-推荐'
+    });
+
+    await queryInterface.addColumn('Articles', 'subtitle', {
+      type: Sequelize.TEXT,
+      allowNull: true,
+      comment: '副标题'
+    });
+  },
+
+  async down (queryInterface, Sequelize) {
+    await queryInterface.removeColumn('Articles', 'isRecommended');
+    await queryInterface.removeColumn('Articles', 'subtitle');
+  }
+};

+ 5 - 3
models/article.js

@@ -10,10 +10,10 @@ module.exports = (sequelize, DataTypes) => {
      * The `models/index` file will call this method automatically.
      */
     static associate(models) {
-      // 文章与分类的关联关系
+      // 文章与作物的关联关系
       Article.belongsTo(models.Category, {
-        foreignKey: 'category',
-        as: 'categoryInfo'
+        foreignKey: 'crop',
+        as: 'cropInfo'
       });
     }
   }
@@ -41,6 +41,8 @@ module.exports = (sequelize, DataTypes) => {
     author:DataTypes.TEXT,
     category:DataTypes.INTEGER,
     crop:DataTypes.INTEGER,
+    isRecommended:DataTypes.INTEGER,
+    subtitle:DataTypes.TEXT,
     seoKeyword:DataTypes.TEXT,
     seoDescription:DataTypes.TEXT,
   }, {

+ 21 - 0
package-lock.json

@@ -16,6 +16,9 @@
         "mysql2": "^3.12.0",
         "nodemon": "^3.1.9",
         "sequelize": "^6.37.5"
+      },
+      "devDependencies": {
+        "js-yaml": "^4.1.0"
       }
     },
     "node_modules/@types/debug": {
@@ -68,6 +71,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
     "node_modules/array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -523,6 +532,18 @@
       "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
       "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
     },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",

+ 5 - 1
package.json

@@ -3,7 +3,8 @@
   "version": "0.0.0",
   "private": true,
   "scripts": {
-    "start": "nodemon ./bin/www"
+    "start": "nodemon ./bin/www",
+    "docs:generate": "node scripts/generate-api-docs.js"
   },
   "dependencies": {
     "cookie-parser": "~1.4.4",
@@ -14,5 +15,8 @@
     "mysql2": "^3.12.0",
     "nodemon": "^3.1.9",
     "sequelize": "^6.37.5"
+  },
+  "devDependencies": {
+    "js-yaml": "^4.1.0"
   }
 }

+ 383 - 32
routes/admin/articles.js

@@ -3,9 +3,96 @@ const router = express.Router();
 const {Article, Category} = require('../../models')
 const {Op} = require('sequelize')
 
-/* 
-    查询文章列表
-    GET /admin/articles
+/**
+ * @api {get} /admin/articles 查询文章列表
+ * @apiName GetArticles
+ * @apiGroup Articles
+ * @apiVersion 1.0.0
+ * 
+ * @apiDescription 获取文章列表,支持分页和多条件筛选
+ * 
+ * @apiParam {Number} [currentPage=1] 当前页码,默认为1
+ * @apiParam {Number} [pageSize=10] 每页显示数量,默认为10
+ * @apiParam {String} [title] 标题搜索关键词(模糊匹配)
+ * @apiParam {String|Array} [cropIds] 作物筛选ID,支持逗号分隔的多个ID或数组形式
+ * @apiParam {Number} [categoryId] 用户分类ID,根据用户传递的category参数值进行精确匹配
+ * @apiParam {Number} [isRecommended] 推荐筛选,0-非推荐文章,1-推荐文章
+ * 
+ * @apiSuccess {Boolean} status 请求状态
+ * @apiSuccess {String} message 响应消息
+ * @apiSuccess {Object} data 响应数据
+ * @apiSuccess {Array} data.articles 文章列表
+ * @apiSuccess {Number} data.articles.id 文章ID
+ * @apiSuccess {String} data.articles.title 文章标题
+ * @apiSuccess {String} [data.articles.subtitle] 文章副标题
+ * @apiSuccess {String} data.articles.content 文章内容
+ * @apiSuccess {Number} data.articles.type 文章类型
+ * @apiSuccess {String} [data.articles.img] 文章图片
+ * @apiSuccess {Date} [data.articles.date] 文章日期
+ * @apiSuccess {String} [data.articles.author] 作者
+ * @apiSuccess {Number} data.articles.category 用户分类ID
+ * @apiSuccess {Number} data.articles.crop 作物分类ID
+ * @apiSuccess {Number} data.articles.isRecommended 是否推荐(0-不推荐,1-推荐)
+ * @apiSuccess {String} [data.articles.seoKeyword] SEO关键词
+ * @apiSuccess {String} [data.articles.seoDescription] SEO描述
+ * @apiSuccess {Date} data.articles.createdAt 创建时间
+ * @apiSuccess {Date} data.articles.updatedAt 更新时间
+ * @apiSuccess {Object} [data.articles.cropInfo] 作物信息
+ * @apiSuccess {Number} data.articles.cropInfo.id 作物ID
+ * @apiSuccess {String} data.articles.cropInfo.name 作物名称
+ * @apiSuccess {Number} data.articles.cropInfo.level 作物层级
+ * @apiSuccess {Number} data.articles.cropInfo.parentId 父级作物ID
+ * @apiSuccess {Object} data.pagination 分页信息
+ * @apiSuccess {Number} data.pagination.total 总数量
+ * @apiSuccess {Number} data.pagination.currentPage 当前页码
+ * @apiSuccess {Number} data.pagination.pageSize 每页数量
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 200 OK
+ * {
+ *   "status": true,
+ *   "message": "成功",
+ *   "data": {
+ *     "articles": [
+ *       {
+ *         "id": 96,
+ *         "title": "测试文章",
+ *         "subtitle": "副标题",
+ *         "content": "<p>文章内容</p>",
+ *         "type": 1,
+ *         "img": null,
+ *         "date": null,
+ *         "author": "作者",
+ *         "category": 1,
+ *         "crop": 43,
+ *         "isRecommended": 1,
+ *         "seoKeyword": null,
+ *         "seoDescription": null,
+ *         "createdAt": "2025-09-14T09:21:24.000Z",
+ *         "updatedAt": "2025-09-14T09:21:24.000Z",
+ *         "cropInfo": {
+ *           "id": 43,
+ *           "name": "荔枝",
+ *           "level": 2,
+ *           "parentId": 40
+ *         }
+ *       }
+ *     ],
+ *     "pagination": {
+ *       "total": 19,
+ *       "currentPage": 1,
+ *       "pageSize": 10
+ *     }
+ *   }
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 500 Internal Server Error
+ * {
+ *   "status": false,
+ *   "message": "失败",
+ *   "errors": ["错误信息"]
+ * }
  */
 router.get('/', async function(req, res, next) {
     try {
@@ -26,9 +113,9 @@ router.get('/', async function(req, res, next) {
             offset,
             include: [{
                 model: Category,
-                as: 'categoryInfo',
+                as: 'cropInfo',
                 attributes: ['id', 'name', 'level', 'parentId'],
-                required: false // LEFT JOIN,即使没有分类也能返回文章
+                required: false // LEFT JOIN,即使没有作物也能返回文章
             }]
         }
 
@@ -42,29 +129,45 @@ router.get('/', async function(req, res, next) {
             }
         }
 
-        // 分类筛选 - 支持多选和包含子分类
-        if(query.categoryIds){
-            let categoryIds = [];
+        // 作物筛选 - 支持多选和包含子分类
+        if(query.cropIds){
+            let cropIds = [];
             
-            // 处理categoryIds参数(支持逗号分隔的多个ID)
-            if(typeof query.categoryIds === 'string'){
-                categoryIds = query.categoryIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
-            } else if(Array.isArray(query.categoryIds)){
-                categoryIds = query.categoryIds.map(id => parseInt(id)).filter(id => !isNaN(id));
+            // 处理cropIds参数(支持逗号分隔的多个ID)
+            if(typeof query.cropIds === 'string'){
+                cropIds = query.cropIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
+            } else if(Array.isArray(query.cropIds)){
+                cropIds = query.cropIds.map(id => parseInt(id)).filter(id => !isNaN(id));
             } else {
-                categoryIds = [parseInt(query.categoryIds)].filter(id => !isNaN(id));
+                cropIds = [parseInt(query.cropIds)].filter(id => !isNaN(id));
             }
 
-            if(categoryIds.length > 0){
+            if(cropIds.length > 0){
                 // 获取所有选中的分类及其子分类的ID
-                const allCategoryIds = await getAllCategoryIdsWithChildren(categoryIds);
+                const allCropIds = await getAllCategoryIdsWithChildren(cropIds);
                 
-                whereConditions.category = {
-                    [Op.in]: allCategoryIds
+                whereConditions.crop = {
+                    [Op.in]: allCropIds
                 };
             }
         }
 
+        // 用户分类筛选 - 根据用户传递的category参数值查询
+        if(query.categoryId){
+            const categoryId = parseInt(query.categoryId);
+            if(!isNaN(categoryId)){
+                whereConditions.category = categoryId;
+            }
+        }
+
+        // 推荐筛选 - 根据是否推荐进行筛选
+        if(query.isRecommended !== undefined){
+            const isRecommended = parseInt(query.isRecommended);
+            if(!isNaN(isRecommended) && (isRecommended === 0 || isRecommended === 1)){
+                whereConditions.isRecommended = isRecommended;
+            }
+        }
+
         // 如果有查询条件,添加到condition中
         if(Object.keys(whereConditions).length > 0){
             condition.where = whereConditions;
@@ -93,9 +196,76 @@ router.get('/', async function(req, res, next) {
     }
 });
 
-/* 
-    查询文章详情
-    GET /admin/articles/:id
+/**
+ * @api {get} /admin/articles/:id 查询文章详情
+ * @apiName GetArticleById
+ * @apiGroup Articles
+ * @apiVersion 1.0.0
+ * 
+ * @apiDescription 根据文章ID获取文章详细信息
+ * 
+ * @apiParam {Number} id 文章ID(路径参数)
+ * 
+ * @apiSuccess {Boolean} status 请求状态
+ * @apiSuccess {String} message 响应消息
+ * @apiSuccess {Object} data 文章详细信息
+ * @apiSuccess {Number} data.id 文章ID
+ * @apiSuccess {String} data.title 文章标题
+ * @apiSuccess {String} [data.subtitle] 文章副标题
+ * @apiSuccess {String} data.content 文章内容
+ * @apiSuccess {Number} data.type 文章类型
+ * @apiSuccess {String} [data.img] 文章图片
+ * @apiSuccess {Date} [data.date] 文章日期
+ * @apiSuccess {String} [data.author] 作者
+ * @apiSuccess {Number} data.category 用户分类ID
+ * @apiSuccess {Number} data.crop 作物分类ID
+ * @apiSuccess {Number} data.isRecommended 是否推荐(0-不推荐,1-推荐)
+ * @apiSuccess {String} [data.seoKeyword] SEO关键词
+ * @apiSuccess {String} [data.seoDescription] SEO描述
+ * @apiSuccess {Date} data.createdAt 创建时间
+ * @apiSuccess {Date} data.updatedAt 更新时间
+ * @apiSuccess {Object} [data.cropInfo] 作物信息
+ * @apiSuccess {Number} data.cropInfo.id 作物ID
+ * @apiSuccess {String} data.cropInfo.name 作物名称
+ * @apiSuccess {Number} data.cropInfo.level 作物层级
+ * @apiSuccess {Number} data.cropInfo.parentId 父级作物ID
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 200 OK
+ * {
+ *   "status": true,
+ *   "message": "成功",
+ *   "data": {
+ *     "id": 96,
+ *     "title": "测试文章",
+ *     "subtitle": "副标题",
+ *     "content": "<p>文章内容</p>",
+ *     "type": 1,
+ *     "img": null,
+ *     "date": null,
+ *     "author": "作者",
+ *     "category": 1,
+ *     "crop": 43,
+ *     "isRecommended": 1,
+ *     "seoKeyword": null,
+ *     "seoDescription": null,
+ *     "createdAt": "2025-09-14T09:21:24.000Z",
+ *     "updatedAt": "2025-09-14T09:21:24.000Z",
+ *     "cropInfo": {
+ *       "id": 43,
+ *       "name": "荔枝",
+ *       "level": 2,
+ *       "parentId": 40
+ *     }
+ *   }
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 404 Not Found
+ * {
+ *   "status": false,
+ *   "message": "文章未找到"
+ * }
  */
 router.get('/:id', async function(req, res, next) {
     try {
@@ -106,7 +276,7 @@ router.get('/:id', async function(req, res, next) {
         const article = await Article.findByPk(id, {
             include: [{
                 model: Category,
-                as: 'categoryInfo',
+                as: 'cropInfo',
                 attributes: ['id', 'name', 'level', 'parentId'],
                 required: false
             }]
@@ -133,9 +303,93 @@ router.get('/:id', async function(req, res, next) {
     }
 });
 
-/* 
-    创建文章
-    POST /admin/articles/
+/**
+ * @api {post} /admin/articles 创建文章
+ * @apiName CreateArticle
+ * @apiGroup Articles
+ * @apiVersion 1.0.0
+ * 
+ * @apiDescription 创建新的文章,支持富文本内容和图片
+ * 
+ * @apiParam {String} title 文章标题(必填,1-500字符)
+ * @apiParam {String} content 文章内容(必填,富文本格式,最大5MB)
+ * @apiParam {Number} [type] 文章类型
+ * @apiParam {String} [img] 文章图片URL
+ * @apiParam {Date} [date] 文章发布日期
+ * @apiParam {String} [author] 作者
+ * @apiParam {Number} [category] 用户分类ID(用户传递的参数)
+ * @apiParam {Number} [crop] 作物分类ID
+ * @apiParam {Number} [isRecommended=0] 是否推荐,0-不推荐,1-推荐
+ * @apiParam {String} [subtitle] 副标题(最大200字符)
+ * @apiParam {String} [seoKeyword] SEO关键词
+ * @apiParam {String} [seoDescription] SEO描述
+ * 
+ * @apiSuccess {Boolean} status 请求状态
+ * @apiSuccess {String} message 响应消息
+ * @apiSuccess {Object} data 创建的文章信息
+ * @apiSuccess {Number} data.id 文章ID
+ * @apiSuccess {String} data.title 文章标题
+ * @apiSuccess {String} [data.subtitle] 文章副标题
+ * @apiSuccess {String} data.content 文章内容
+ * @apiSuccess {Number} data.type 文章类型
+ * @apiSuccess {String} [data.img] 文章图片
+ * @apiSuccess {Date} [data.date] 文章日期
+ * @apiSuccess {String} [data.author] 作者
+ * @apiSuccess {Number} data.category 用户分类ID
+ * @apiSuccess {Number} data.crop 作物分类ID
+ * @apiSuccess {Number} data.isRecommended 是否推荐
+ * @apiSuccess {String} [data.seoKeyword] SEO关键词
+ * @apiSuccess {String} [data.seoDescription] SEO描述
+ * @apiSuccess {Date} data.createdAt 创建时间
+ * @apiSuccess {Date} data.updatedAt 更新时间
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 201 Created
+ * {
+ *   "status": true,
+ *   "message": "成功",
+ *   "data": {
+ *     "id": 97,
+ *     "title": "新文章标题",
+ *     "subtitle": "副标题",
+ *     "content": "<p>文章内容</p>",
+ *     "type": 1,
+ *     "img": null,
+ *     "date": null,
+ *     "author": "作者",
+ *     "category": 1,
+ *     "crop": 43,
+ *     "isRecommended": 1,
+ *     "seoKeyword": null,
+ *     "seoDescription": null,
+ *     "createdAt": "2025-09-14T09:21:49.333Z",
+ *     "updatedAt": "2025-09-14T09:21:49.333Z"
+ *   }
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "status": false,
+ *   "message": "请求参数错误",
+ *   "errors": ["标题不能为空"]
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "status": false,
+ *   "message": "请求参数错误",
+ *   "errors": ["推荐字段只能是0或1"]
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "status": false,
+ *   "message": "请求参数错误",
+ *   "errors": ["副标题长度不能超过200个字符"]
+ * }
  */
 router.post('/', async function(req, res, next) {
     try {
@@ -175,7 +429,8 @@ router.post('/', async function(req, res, next) {
         console.error('请求体:', JSON.stringify(req.body, null, 2));
 
         if(error.message === '标题不能为空' || error.message === '内容不能为空' || 
-           error.message.includes('长度不能超过') || error.message.includes('不允许的脚本标签')){
+           error.message.includes('长度不能超过') || error.message.includes('不允许的脚本标签') ||
+           error.message.includes('推荐字段只能是0或1') || error.message.includes('副标题长度不能超过')){
             res.status(400).json({ 
                 status:false,
                 message:'请求参数错误',
@@ -212,9 +467,32 @@ router.post('/', async function(req, res, next) {
     }
 });
 
-/* 
-    删除文章
-    GET /admin/articles/:id
+/**
+ * @api {delete} /admin/articles/:id 删除文章
+ * @apiName DeleteArticle
+ * @apiGroup Articles
+ * @apiVersion 1.0.0
+ * 
+ * @apiDescription 根据文章ID删除文章
+ * 
+ * @apiParam {Number} id 文章ID(路径参数)
+ * 
+ * @apiSuccess {Boolean} status 请求状态
+ * @apiSuccess {String} message 响应消息
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 200 OK
+ * {
+ *   "status": true,
+ *   "message": "成功"
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 404 Not Found
+ * {
+ *   "status": false,
+ *   "message": "文章未找到"
+ * }
  */
 router.delete('/:id', async function(req, res, next) {
     try {
@@ -246,9 +524,70 @@ router.delete('/:id', async function(req, res, next) {
     }
 });
 
-/* 
-    更新文章
-    GET /admin/articles/:id
+/**
+ * @api {put} /admin/articles/:id 更新文章
+ * @apiName UpdateArticle
+ * @apiGroup Articles
+ * @apiVersion 1.0.0
+ * 
+ * @apiDescription 根据文章ID更新文章信息
+ * 
+ * @apiParam {Number} id 文章ID(路径参数)
+ * @apiParam {String} [title] 文章标题(1-500字符)
+ * @apiParam {String} [content] 文章内容(富文本格式,最大5MB)
+ * @apiParam {Number} [type] 文章类型
+ * @apiParam {String} [img] 文章图片URL
+ * @apiParam {Date} [date] 文章发布日期
+ * @apiParam {String} [author] 作者
+ * @apiParam {Number} [category] 用户分类ID
+ * @apiParam {Number} [crop] 作物分类ID
+ * @apiParam {Number} [isRecommended] 是否推荐,0-不推荐,1-推荐
+ * @apiParam {String} [subtitle] 副标题(最大200字符)
+ * @apiParam {String} [seoKeyword] SEO关键词
+ * @apiParam {String} [seoDescription] SEO描述
+ * 
+ * @apiSuccess {Boolean} status 请求状态
+ * @apiSuccess {String} message 响应消息
+ * @apiSuccess {Object} data 更新后的文章信息
+ * 
+ * @apiSuccessExample {json} Success-Response:
+ * HTTP/1.1 200 OK
+ * {
+ *   "status": true,
+ *   "message": "成功",
+ *   "data": {
+ *     "id": 97,
+ *     "title": "更新后的标题",
+ *     "subtitle": "更新后的副标题",
+ *     "content": "<p>更新后的内容</p>",
+ *     "type": 1,
+ *     "img": null,
+ *     "date": null,
+ *     "author": "作者",
+ *     "category": 1,
+ *     "crop": 43,
+ *     "isRecommended": 1,
+ *     "seoKeyword": null,
+ *     "seoDescription": null,
+ *     "createdAt": "2025-09-14T09:21:49.333Z",
+ *     "updatedAt": "2025-09-14T09:22:10.000Z"
+ *   }
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 404 Not Found
+ * {
+ *   "status": false,
+ *   "message": "文章未找到"
+ * }
+ * 
+ * @apiErrorExample {json} Error-Response:
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "status": false,
+ *   "message": "请求参数错误",
+ *   "errors": ["推荐字段只能是0或1"]
+ * }
  */
 router.put('/:id', async function(req, res, next) {
     try {
@@ -340,6 +679,8 @@ function filterBody(req){
             author: req.body.author ? String(req.body.author).trim() : null,
             category: req.body.category ? parseInt(req.body.category) : null,
             crop: req.body.crop ? parseInt(req.body.crop) : null,
+            isRecommended: req.body.isRecommended !== undefined ? parseInt(req.body.isRecommended) : 0,
+            subtitle: req.body.subtitle ? String(req.body.subtitle).trim() : null,
             seoKeyword: req.body.seoKeyword ? String(req.body.seoKeyword).trim() : null,
             seoDescription: req.body.seoDescription ? String(req.body.seoDescription).trim() : null
         };
@@ -363,6 +704,16 @@ function filterBody(req){
             throw new Error('内容过长,请减少内容长度');
         }
 
+        // 验证推荐字段 - 只能是0或1
+        if (body.isRecommended !== 0 && body.isRecommended !== 1) {
+            throw new Error('推荐字段只能是0或1');
+        }
+
+        // 验证副标题长度
+        if (body.subtitle && body.subtitle.length > 200) {
+            throw new Error('副标题长度不能超过200个字符');
+        }
+
         // 检查富文本内容是否包含危险标签或脚本
         const dangerousTags = /<script[^>]*>.*?<\/script>/gi;
         if (dangerousTags.test(body.content)) {

+ 286 - 0
scripts/generate-api-docs.js

@@ -0,0 +1,286 @@
+#!/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 };