# API 文档生成规范

**版本**: 1.1  
**最后更新**: 2026-01-24

## 目录

- [核心原则](#核心原则)
- [新增 Handler 检查清单](#新增-handler-检查清单)
- [路由注册规范](#路由注册规范)
- [DTO 规范](#dto-规范)
- [文档生成流程](#文档生成流程)
- [常见问题](#常见问题)

---

## 核心原则

### ✅ 强制要求

**所有 HTTP 接口必须使用统一的 `Register()` 函数注册，以确保自动加入 OpenAPI 文档生成。**

```go
// ✅ 正确：使用 Register() 函数
Register(router, doc, basePath, "POST", "/path", handler.Method, RouteSpec{
    Summary: "操作说明",
    Tags:    []string{"分类"},
    Input:   new(model.RequestDTO),
    Output:  new(model.ResponseDTO),
    Auth:    true,
})

// ❌ 错误：直接注册（不会生成文档）
router.Post("/path", handler.Method)
```

### 为什么这样做？

1. **文档自动同步**：代码即文档，避免文档与实现脱节
2. **前后端协作**：生成标准 OpenAPI 规范，前端可直接导入
3. **API 测试**：Swagger UI / Postman 可直接使用
4. **类型安全**：通过 DTO 结构体自动生成准确的字段定义

---

## 新增 Handler 检查清单

> ⚠️ **重要**: 新增 Handler 时，必须完成以下所有步骤，否则接口不会出现在 OpenAPI 文档中！

### 必须完成的 4 个步骤

| 步骤 | 文件位置 | 操作 |
|------|---------|------|
| 1️⃣ | `internal/bootstrap/types.go` | 在 `Handlers` 结构体中添加新 Handler 字段 |
| 2️⃣ | `internal/bootstrap/handlers.go` | 实例化新 Handler |
| 3️⃣ | `internal/routes/admin.go` | 调用路由注册函数 |
| 4️⃣ | `cmd/api/docs.go` 和 `cmd/gendocs/main.go` | **添加 Handler 到文档生成器** |

### 详细说明

#### 步骤 1: 添加 Handler 字段

```go
// internal/bootstrap/types.go
type Handlers struct {
    // ... 现有 Handler
    IotCard       *admin.IotCardHandler       // 新增
    IotCardImport *admin.IotCardImportHandler // 新增
}
```

#### 步骤 2: 实例化 Handler

```go
// internal/bootstrap/handlers.go
func initHandlers(services *Services) *Handlers {
    return &Handlers{
        // ... 现有 Handler
        IotCard:       admin.NewIotCardHandler(services.IotCard),
        IotCardImport: admin.NewIotCardImportHandler(services.IotCardImport),
    }
}
```

#### 步骤 3: 调用路由注册

```go
// internal/routes/admin.go
func RegisterAdminRoutes(...) {
    // ... 现有路由
    if handlers.IotCard != nil {
        registerIotCardRoutes(authGroup, handlers.IotCard, handlers.IotCardImport, doc, basePath)
    }
}
```

#### 步骤 4: 更新文档生成器 ⚠️ 最容易遗漏！

**必须同时更新两个文件：**

```go
// cmd/api/docs.go
func generateOpenAPIDocs(outputPath string, logger *zap.Logger) {
    handlers := &bootstrap.Handlers{
        // ... 现有 Handler
        IotCard:       admin.NewIotCardHandler(nil),       // 添加
        IotCardImport: admin.NewIotCardImportHandler(nil), // 添加
    }
    // ...
}
```

```go
// cmd/gendocs/main.go
func generateAdminDocs(outputPath string) error {
    handlers := &bootstrap.Handlers{
        // ... 现有 Handler
        IotCard:       admin.NewIotCardHandler(nil),       // 添加
        IotCardImport: admin.NewIotCardImportHandler(nil), // 添加
    }
    // ...
}
```

### 验证检查

完成上述步骤后，运行以下命令验证：

```bash
# 1. 编译检查
go build ./...

# 2. 重新生成文档
go run cmd/gendocs/main.go

# 3. 验证接口是否出现在文档中
grep "你的接口路径" docs/admin-openapi.yaml
```

---

## 路由注册规范

### 1. 基本结构

所有路由注册必须在 `internal/routes/` 目录中完成：

```
internal/routes/
├── registry.go         # Register() 函数定义
├── routes.go          # 总入口
├── admin.go           # Admin 域路由
├── h5.go              # H5 域路由
├── account.go         # 账号管理路由
├── role.go            # 角色管理路由
└── ...
```

### 2. 注册函数签名

```go
func registerXxxRoutes(
    api fiber.Router,           // Fiber 路由组
    h *admin.XxxHandler,        // Handler 实例
    doc *openapi.Generator,     // 文档生成器（可能为 nil）
    basePath string,            // 基础路径（如 "/api/admin"）
) {
    // 路由注册逻辑
}
```

### 3. RouteSpec 结构

```go
type RouteSpec struct {
    Summary     string        // 操作摘要（中文，简短，一行）
    Description string        // 详细说明，支持 Markdown 语法（可选）
    Input       interface{}   // 请求参数 DTO
    Output      interface{}   // 响应结果 DTO
    Tags        []string      // 分类标签（用于文档分组）
    Auth        bool          // 是否需要认证
}
```

### 4. Description 字段（Markdown 说明）

`Description` 字段用于添加接口的详细说明，支持 **CommonMark Markdown** 语法。Apifox 等 OpenAPI 工具会正确渲染这些 Markdown 内容。

**使用场景**：
- 业务规则说明
- 请求频率限制
- 注意事项
- 错误码说明
- 数据格式说明

**示例**：
```go
Register(router, doc, basePath, "POST", "/login", handler.Login, RouteSpec{
    Summary: "后台登录",
    Description: `## 登录说明

**请求频率限制**：每分钟最多 10 次

### 注意事项
1. 密码错误 5 次后账号将被锁定 30 分钟
2. Token 有效期为 24 小时

### 返回码说明
| 错误码 | 说明 |
|--------|------|
| 1001 | 用户名或密码错误 |
| 1002 | 账号已被锁定 |
`,
    Tags:   []string{"认证"},
    Input:  new(dto.LoginRequest),
    Output: new(dto.LoginResponse),
    Auth:   false,
})
```

**支持的 Markdown 语法**：
- 标题：`#`、`##`、`###`
- 列表：`-`、`1.`
- 表格：`| 列1 | 列2 |`
- 代码：`` `code` `` 和 ` ```code block``` `
- 强调：`**粗体**`、`*斜体*`
- 链接：`[文本](url)`

**最佳实践**：
- 保持简洁，控制在 500 字以内
- 使用结构化的 Markdown（标题、列表、表格）提高可读性
- 避免使用 HTML 标签（兼容性较差）

### 5. 完整示例

```go
func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *openapi.Generator, basePath string) {
    shops := router.Group("/shops")
    groupPath := basePath + "/shops"

    Register(shops, doc, groupPath, "GET", "", handler.List, RouteSpec{
        Summary: "店铺列表",
        Tags:    []string{"店铺管理"},
        Input:   new(model.ShopListRequest),
        Output:  new(model.ShopPageResult),
        Auth:    true,
    })

    Register(shops, doc, groupPath, "POST", "", handler.Create, RouteSpec{
        Summary: "创建店铺",
        Tags:    []string{"店铺管理"},
        Input:   new(model.CreateShopRequest),
        Output:  new(model.ShopResponse),
        Auth:    true,
    })

    Register(shops, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
        Summary: "更新店铺",
        Tags:    []string{"店铺管理"},
        Input:   new(model.UpdateShopParams),  // 组合参数（路径 + Body）
        Output:  new(model.ShopResponse),
        Auth:    true,
    })

    Register(shops, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
        Summary: "删除店铺",
        Tags:    []string{"店铺管理"},
        Input:   new(model.IDReq),  // 仅路径参数
        Output:  nil,
        Auth:    true,
    })
}
```

---

## DTO 规范

### 1. Description 标签（必须）

**所有字段必须使用 `description` 标签，禁止使用行内注释。**

```go
// ❌ 错误
type CreateShopRequest struct {
    ShopName string `json:"shop_name" validate:"required,min=1,max=100"` // 店铺名称
}

// ✅ 正确
type CreateShopRequest struct {
    ShopName string `json:"shop_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"店铺名称"`
}
```

### 2. 枚举字段规范

**必须在 `description` 中列出所有可能值（中文）。**

```go
type CreateShopRequest struct {
    Status int `json:"status" validate:"required,oneof=0 1" required:"true" description:"状态 (0:禁用, 1:启用)"`
    Level  int `json:"level" validate:"required,min=1,max=7" required:"true" minimum:"1" maximum:"7" description:"店铺层级 (1-7级)"`
}
```

### 3. 验证标签与 OpenAPI 标签一致

| validate 标签 | OpenAPI 标签 | 说明 |
|--------------|--------------|------|
| `required` | `required:"true"` | 必填字段 |
| `min=N,max=M` | `minimum:"N" maximum:"M"` | 数值范围 |
| `min=N,max=M` (字符串) | `minLength:"N" maxLength:"M"` | 字符串长度 |
| `len=N` | `minLength:"N" maxLength:"N"` | 固定长度 |
| `oneof=A B C` | `description` 中说明 | 枚举值 |

### 4. 请求参数类型标签

```go
// Query 参数
type ListRequest struct {
    Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
}

// Path 参数
type IDReq struct {
    ID uint `path:"id" description:"ID" required:"true"`
}

// Body 参数（默认）
type CreateRequest struct {
    Name string `json:"name" validate:"required" required:"true" description:"名称"`
}
```

### 5. 组合参数（路径 + Body）

对于 `PUT /:id` 类型的端点，需要创建组合参数 DTO：

```go
// 定义在 internal/model/common.go
type UpdateShopParams struct {
    IDReq                // 路径参数
    UpdateShopRequest    // Body 参数
}
```

### 6. 分页响应规范

```go
type ShopPageResult struct {
    Items []ShopResponse `json:"items" description:"店铺列表"`
    Total int64          `json:"total" description:"总记录数"`
    Page  int            `json:"page" description:"当前页码"`
    Size  int            `json:"size" description:"每页数量"`
}
```

### 7. 响应 Envelope 格式

**所有 API 响应都会被自动包裹在统一的 envelope 结构中。**

OpenAPI 文档会自动为成功响应生成以下结构:

```yaml
responses:
  "200":
    content:
      application/json:
        schema:
          type: object
          properties:
            code:
              type: integer
              example: 0
              description: 响应码
            msg:
              type: string
              example: success
              description: 响应消息
            data:
              $ref: '#/components/schemas/YourDTO'  # 你定义的 DTO
            timestamp:
              type: string
              format: date-time
              description: 时间戳
```

**注意事项**:
- DTO 中只需定义 `data` 字段的内容，无需定义 envelope 字段
- 错误响应使用 `msg` 字段（不是 `message`）
- 删除操作等无返回数据的接口，`data` 字段为 `null`

**示例**:

```go
// DTO 定义（只定义 data 部分）
type LoginResponse struct {
    Token    string `json:"token" description:"访问令牌"`
    Customer *PersonalCustomerDTO `json:"customer" description:"客户信息"`
}

// 实际 API 响应（自动包裹 envelope）
{
    "code": 0,
    "msg": "success",
    "data": {
        "token": "eyJhbGciOiJI...",
        "customer": {
            "id": 1,
            "phone": "13800000000"
        }
    },
    "timestamp": "2026-01-30T10:00:00Z"
}
```

---

## 文档生成流程

### 1. 自动生成

```bash
# 方式1：独立生成工具
go run cmd/gendocs/main.go

# 方式2：启动 API 服务时自动生成
go run cmd/api/main.go
```

生成的文档位置：
- `docs/admin-openapi.yaml` - 独立生成
- `logs/openapi.yaml` - 运行时生成

### 2. 验证文档

```bash
# 1. 检查生成的路径数量
python3 -c "
import yaml
with open('docs/admin-openapi.yaml', 'r', encoding='utf-8') as f:
    doc = yaml.safe_load(f)
    paths = list(doc.get('paths', {}).keys())
    print(f'总路径数: {len(paths)}')
    for p in sorted(paths):
        print(f'  {p}')
"

# 2. 在 Swagger UI 中测试
# 访问 https://editor.swagger.io/
# 粘贴 docs/admin-openapi.yaml 内容
```

### 3. 更新文档生成器

如果新增了 Handler，需要在 `cmd/gendocs/main.go` 中添加：

```go
// 3. 创建 Handler（使用 nil 依赖,因为只需要路由结构）
newHandler := admin.NewXxxHandler(nil)

handlers := &bootstrap.Handlers{
    // ... 其他 Handler
    Xxx: newHandler,  // 添加新 Handler
}
```

---

## 常见问题

### Q1: 为什么我的接口没有出现在文档中？

> ⚠️ **最常见原因**: 忘记在 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 中添加新 Handler！

**检查清单（按优先级排序）**：

1. ✅ **【最常遗漏】** 是否在文档生成器中添加了 Handler？
   
   必须同时检查两个文件：
   ```go
   // cmd/api/docs.go
   handlers := &bootstrap.Handlers{
       Xxx: admin.NewXxxHandler(nil),  // 是否添加？
   }
   
   // cmd/gendocs/main.go  
   handlers := &bootstrap.Handlers{
       Xxx: admin.NewXxxHandler(nil),  // 是否添加？
   }
   ```

2. ✅ 是否使用了 `Register()` 函数？
   ```go
   // ❌ 错误
   router.Post("/path", handler.Method)
   
   // ✅ 正确
   Register(router, doc, basePath, "POST", "/path", handler.Method, RouteSpec{...})
   ```

3. ✅ 路由注册函数是否接收了 `doc *openapi.Generator` 参数？
   ```go
   func registerXxxRoutes(router fiber.Router, handler *admin.XxxHandler, doc *openapi.Generator, basePath string)
   ```

4. ✅ 是否调用了路由注册函数？
   - 检查 `internal/routes/admin.go` 中是否调用了 `registerXxxRoutes()`
   - 检查 `internal/routes/routes.go` 是否调用了 `RegisterAdminRoutes()`

**快速定位问题**：
```bash
# 检查 Handler 是否在文档生成器中注册
grep "NewXxxHandler" cmd/api/docs.go cmd/gendocs/main.go
```

### Q2: 文档生成时报错 "undefined path parameter"？

**原因**：路径参数（如 `/:id`）的 DTO 缺少对应字段。

**解决方案**：创建组合参数 DTO

```go
// ❌ 错误：直接使用 Body DTO
Register(router, doc, basePath, "PUT", "/:id", handler.Update, RouteSpec{
    Input: new(model.UpdateShopRequest),  // 缺少 id 参数
})

// ✅ 正确：使用组合参数
type UpdateShopParams struct {
    IDReq                // 包含 id 参数
    UpdateShopRequest    // 包含 Body 参数
}

Register(router, doc, basePath, "PUT", "/:id", handler.Update, RouteSpec{
    Input: new(model.UpdateShopParams),
})
```

### Q3: DTO 字段在文档中没有描述？

**检查**：

1. ✅ 是否添加了 `description` 标签？
   ```go
   ShopName string `json:"shop_name" description:"店铺名称"`
   ```

2. ✅ 是否使用了行内注释（不会被识别）？
   ```go
   // ❌ 错误
   ShopName string `json:"shop_name"` // 店铺名称
   
   // ✅ 正确
   ShopName string `json:"shop_name" description:"店铺名称"`
   ```

### Q4: 如何为新模块添加路由？

**完整步骤**（共 6 步）：

1. **创建 Handler**：`internal/handler/admin/xxx.go`

2. **添加到 Handlers 结构体**：`internal/bootstrap/types.go`
   ```go
   type Handlers struct {
       Xxx *admin.XxxHandler
   }
   ```

3. **实例化 Handler**：`internal/bootstrap/handlers.go`
   ```go
   Xxx: admin.NewXxxHandler(services.Xxx),
   ```

4. **创建路由文件**：`internal/routes/xxx.go`
   ```go
   func registerXxxRoutes(api fiber.Router, h *admin.XxxHandler, doc *openapi.Generator, basePath string) {
       // 使用 Register() 注册路由
   }
   ```

5. **调用路由注册**：`internal/routes/admin.go`
   ```go
   if handlers.Xxx != nil {
       registerXxxRoutes(authGroup, handlers.Xxx, doc, basePath)
   }
   ```

6. **更新文档生成器**（⚠️ 两个文件都要改）：
   - `cmd/api/docs.go`
   - `cmd/gendocs/main.go`
   ```go
   handlers := &bootstrap.Handlers{
       Xxx: admin.NewXxxHandler(nil),
   }
   ```

7. **验证**：
   ```bash
   go build ./...
   go run cmd/gendocs/main.go
   grep "/api/admin/xxx" docs/admin-openapi.yaml
   ```

### Q5: 如何为个人客户路由（/api/c/v1）添加文档？

个人客户路由需要在独立的路由文件中注册，并使用 `Register()` 函数以纳入 OpenAPI 文档。

**示例**：`internal/routes/personal.go`

```go
func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers, personalAuthMiddleware *middleware.PersonalAuthMiddleware) {
    // 公开路由（不需要认证）
    publicGroup := router.Group("")
    
    Register(publicGroup, doc, basePath, "POST", "/login/send-code", handlers.PersonalCustomer.SendCode, RouteSpec{
        Summary:     "发送验证码",
        Description: "向指定手机号发送登录验证码",
        Tags:        []string{"个人客户 - 认证"},
        Auth:        false,
        Input:       &apphandler.SendCodeRequest{},
        Output:      nil,
    })

    Register(publicGroup, doc, basePath, "POST", "/login", handlers.PersonalCustomer.Login, RouteSpec{
        Summary:     "手机号登录",
        Description: "使用手机号和验证码登录",
        Tags:        []string{"个人客户 - 认证"},
        Auth:        false,
        Input:       &apphandler.LoginRequest{},
        Output:      &apphandler.LoginResponse{},
    })

    // 需要认证的路由
    authGroup := router.Group("")
    authGroup.Use(personalAuthMiddleware.Authenticate())

    Register(authGroup, doc, basePath, "GET", "/profile", handlers.PersonalCustomer.GetProfile, RouteSpec{
        Summary:     "获取个人资料",
        Description: "获取当前登录客户的个人资料",
        Tags:        []string{"个人客户 - 账户"},
        Auth:        true,
        Input:       nil,
        Output:      &apphandler.PersonalCustomerDTO{},
    })
}
```

**在 `routes.go` 中调用**：

```go
func RegisterRoutesWithDoc(app *fiber.App, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator) {
    // ... 其他路由
    
    // 个人客户路由 (挂载在 /api/c/v1)
    personalGroup := app.Group("/api/c/v1")
    RegisterPersonalCustomerRoutes(personalGroup, doc, "/api/c/v1", handlers, middlewares.PersonalAuth)
}
```

**关键点**：
- basePath 必须是完整路径（如 `/api/c/v1`）
- 需要传入 `personalAuthMiddleware` 以支持认证路由组
- Tags 使用中文并包含模块前缀（如 "个人客户 - 认证"）

### Q6: 如何调试文档生成？

```bash
# 1. 查看生成的 YAML 文件
cat docs/admin-openapi.yaml

# 2. 验证 YAML 格式
python3 -c "
import yaml
with open('docs/admin-openapi.yaml', 'r', encoding='utf-8') as f:
    doc = yaml.safe_load(f)
    print('YAML 格式正确')
"

# 3. 检查特定路径
python3 -c "
import yaml
with open('docs/admin-openapi.yaml', 'r', encoding='utf-8') as f:
    doc = yaml.safe_load(f)
    path = '/api/admin/shops'
    if path in doc['paths']:
        import json
        print(json.dumps(doc['paths'][path], indent=2, ensure_ascii=False))
"
```

---

## 参考资料

- [OpenAPI 3.0 规范](https://swagger.io/specification/)
- [Swagger UI](https://swagger.io/tools/swagger-ui/)
- [项目 DTO 规范](../AGENTS.md#dto-规范重要)
- [已有实现示例](../internal/routes/account.go)
