gin + es 实践 04

API 接口设计

Go-ES 项目提供了一套完整的 RESTful API,用于产品管理和搜索。本文档详细介绍了 API 的设计原则、接口定义和使用方法。

API 设计原则

本项目的 API 设计遵循以下原则:

  1. RESTful 风格:使用标准的 HTTP 方法表示操作类型
  2. 资源导向:API 路径以资源名词为中心
  3. JSON 格式:请求和响应均使用 JSON 格式
  4. 版本控制:API 路径包含版本号
  5. 统一错误处理:使用一致的错误响应格式
  6. 自文档化:使用 Swagger 注解自动生成 API 文档

API 基础信息

  • 基础路径/api/v1
  • 内容类型application/json
  • 认证方式:API Key(通过 Authorization 头)

API 端点概览

方法路径描述
POST/products创建产品
GET/products获取产品列表
GET/products/{id}获取产品详情
PUT/products/{id}更新产品
DELETE/products/{id}删除产品
GET/products/category/{category}获取指定类别产品
GET/products/search搜索产品
POST/products/reindex重建产品索引

API 详细说明

1. 创建产品

请求:

POST /api/v1/products
Content-Type: application/json

{
  "name": "示例产品",
  "description": "这是一个示例产品",
  "price": 99.99,
  "category": "电子产品",
  "tags": ["新品", "热销"]
}

响应:

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "name": "示例产品",
  "description": "这是一个示例产品",
  "price": 99.99,
  "category": "电子产品",
  "tags": ["新品", "热销"],
  "created_at": "2023-05-01T12:00:00Z",
  "updated_at": "2023-05-01T12:00:00Z"
}

处理逻辑:

func (h *ProductHandler) Create(c *gin.Context) {
    var req dto.CreateProductRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
        return
    }

    product, err := h.productAppService.CreateProduct(c.Request.Context(), &req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建产品失败: %v", err)})
        return
    }

    c.JSON(http.StatusCreated, product)
}

2. 获取产品列表

请求:

GET /api/v1/products?page=1&size=10

响应:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 100,
  "page": 1,
  "page_size": 10,
  "products": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "示例产品",
      "description": "这是一个示例产品",
      "price": 99.99,
      "category": "电子产品",
      "tags": ["新品", "热销"],
      "created_at": "2023-05-01T12:00:00Z",
      "updated_at": "2023-05-01T12:00:00Z"
    },
    // ... 更多产品
  ]
}

处理逻辑:

func (h *ProductHandler) List(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))

    products, err := h.productAppService.ListProducts(c.Request.Context(), page, size)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取产品列表失败: %v", err)})
        return
    }

    c.JSON(http.StatusOK, products)
}

3. 获取产品详情

请求:

GET /api/v1/products/f47ac10b-58cc-4372-a567-0e02b2c3d479

响应:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "name": "示例产品",
  "description": "这是一个示例产品",
  "price": 99.99,
  "category": "电子产品",
  "tags": ["新品", "热销"],
  "created_at": "2023-05-01T12:00:00Z",
  "updated_at": "2023-05-01T12:00:00Z"
}

处理逻辑:

func (h *ProductHandler) Get(c *gin.Context) {
    id := c.Param("id")
    if id == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "产品ID不能为空"})
        return
    }

    product, err := h.productAppService.GetProduct(c.Request.Context(), id)
    if err != nil {
        if err.Error() == "产品不存在" {
            c.JSON(http.StatusNotFound, gin.H{"error": "产品不存在"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取产品失败: %v", err)})
        return
    }

    c.JSON(http.StatusOK, product)
}

4. 搜索产品

请求:

GET /api/v1/products/search?q=手机&category=电子产品&page=1&size=10

响应:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 5,
  "page": 1,
  "page_size": 10,
  "products": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "高端智能手机",
      "description": "这是一款功能强大的智能手机",
      "price": 3999.99,
      "category": "电子产品",
      "tags": ["手机", "智能设备"],
      "created_at": "2023-05-01T12:00:00Z",
      "updated_at": "2023-05-01T12:00:00Z"
    },
    // ... 更多产品
  ]
}

处理逻辑:

func (h *ProductHandler) Search(c *gin.Context) {
    var searchReq dto.SearchProductRequest
    if err := c.ShouldBindQuery(&searchReq); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的搜索参数"})
        return
    }

    products, err := h.productAppService.SearchProducts(c.Request.Context(), &searchReq)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("搜索产品失败: %v", err)})
        return
    }

    c.JSON(http.StatusOK, products)
}

请求与响应模型

请求模型

创建产品请求
// CreateProductRequest 创建产品请求
type CreateProductRequest struct {
    Name        string   `json:"name" binding:"required"`
    Description string   `json:"description"`
    Price       float64  `json:"price" binding:"required,gt=0"`
    Category    string   `json:"category" binding:"required"`
    Tags        []string `json:"tags"`
}
更新产品请求
// UpdateProductRequest 更新产品请求
type UpdateProductRequest struct {
    Name        string   `json:"name" binding:"required"`
    Description string   `json:"description"`
    Price       float64  `json:"price" binding:"required,gt=0"`
    Category    string   `json:"category" binding:"required"`
    Tags        []string `json:"tags"`
}
搜索产品请求
// SearchProductRequest 搜索产品请求
type SearchProductRequest struct {
    Keyword  string `form:"q"`
    Category string `form:"category"`
    Page     int    `form:"page,default=1"`
    PageSize int    `form:"size,default=10"`
}

响应模型

产品响应
// ProductResponse 产品响应
type ProductResponse struct {
    ID          string    `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Price       float64   `json:"price"`
    Category    string    `json:"category"`
    Tags        []string  `json:"tags"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}
产品列表响应
// ProductListResponse 产品列表响应
type ProductListResponse struct {
    Total    int64            `json:"total"`
    Page     int              `json:"page"`
    PageSize int              `json:"page_size"`
    Products []ProductResponse `json:"products"`
}

错误处理

API 使用统一的错误响应格式:

{
  "error": "错误描述信息"
}

常见的错误响应包括:

  • 400 Bad Request: 请求参数无效
  • 404 Not Found: 请求的资源不存在
  • 500 Internal Server Error: 服务器内部错误

API 文档生成

本项目使用 Swagger 自动生成 API 文档。API 处理器中的注解如下所示:

// Create 创建产品
// @Summary 创建新产品
// @Description 创建一个新的产品并索引到Elasticsearch
// @Tags 产品
// @Accept  json
// @Produce  json
// @Param product body dto.CreateProductRequest true "产品信息"
// @Success 201 {object} dto.ProductResponse
// @Failure 400 {object} map[string]string "错误信息"
// @Failure 500 {object} map[string]string "错误信息"
// @Router /products [post]
func (h *ProductHandler) Create(c *gin.Context) {
    // 实现逻辑...
}

使用 Swagger 文档

  1. 启动应用后,访问 Swagger UI:
http://localhost:8080/swagger/index.html
  1. 在 Swagger UI 界面中可以:
    • 浏览所有 API 端点
    • 查看请求参数和响应格式
    • 直接在浏览器中测试 API

API 路由注册

所有 API 路由在 interfaces/api/router/router.go 中注册:

// SetupRouter 设置路由
func SetupRouter(engine *gin.Engine, productHandler *handler.ProductHandler) {
    // 设置中间件
    engine.Use(gin.Logger())
    engine.Use(gin.Recovery())
    engine.Use(middleware.Cors())

    // 添加Swagger
    engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))

    // API 路由组
    api := engine.Group("/api/v1")
    {
        // 产品相关路由
        products := api.Group("/products")
        {
            products.POST("", productHandler.Create)
            products.GET("", productHandler.List)
            products.GET("/:id", productHandler.Get)
            products.PUT("/:id", productHandler.Update)
            products.DELETE("/:id", productHandler.Delete)
            products.GET("/category/:category", productHandler.ListByCategory)
            products.GET("/search", productHandler.Search)
            products.POST("/reindex", productHandler.ReindexAll)
        }
    }
}

跨域处理

API 支持跨域请求,通过自定义中间件实现:

// Cors 添加CORS头
func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
        c.Writer.Header().Set("Access-Control-Max-Age", "86400")

        // 处理OPTIONS请求
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(200)
            return
        }

        // 处理请求
        c.Next()
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值