API 接口设计
Go-ES 项目提供了一套完整的 RESTful API,用于产品管理和搜索。本文档详细介绍了 API 的设计原则、接口定义和使用方法。
API 设计原则
本项目的 API 设计遵循以下原则:
- RESTful 风格:使用标准的 HTTP 方法表示操作类型
- 资源导向:API 路径以资源名词为中心
- JSON 格式:请求和响应均使用 JSON 格式
- 版本控制:API 路径包含版本号
- 统一错误处理:使用一致的错误响应格式
- 自文档化:使用 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 文档
- 启动应用后,访问 Swagger UI:
http://localhost:8080/swagger/index.html
- 在 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()
}
}