Files
junhong_cmp_fiber/docs/api-documentation-guide.md
huang a924e63e68
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m42s
feat: 实现物联网卡独立管理和批量导入功能
新增物联网卡独立管理模块,支持单卡查询、批量导入和状态管理。主要变更包括:

功能特性:
- 新增物联网卡 CRUD 接口(查询、分页列表、删除)
- 支持 CSV/Excel 批量导入物联网卡
- 实现异步导入任务处理和进度跟踪
- 新增 ICCID 号码格式校验器(支持 Luhn 算法)
- 新增 CSV 文件解析工具(支持编码检测和错误处理)

数据库变更:
- 移除 iot_card 和 device 表的 owner_id/owner_type 字段
- 新增 iot_card_import_task 导入任务表
- 为导入任务添加运营商类型字段

测试覆盖:
- 新增 IoT 卡 Store 层单元测试
- 新增 IoT 卡导入任务单元测试
- 新增 IoT 卡集成测试(包含导入流程测试)
- 新增 CSV 工具和 ICCID 校验器测试

文档更新:
- 更新 OpenAPI 文档(新增 7 个 IoT 卡接口)
- 归档 OpenSpec 变更提案
- 更新 API 文档规范和生成器指南

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 11:03:43 +08:00

14 KiB
Raw Blame History

API 文档生成规范

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

目录


核心原则

强制要求

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

// ✅ 正确:使用 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.gocmd/gendocs/main.go 添加 Handler 到文档生成器

详细说明

步骤 1: 添加 Handler 字段

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

步骤 2: 实例化 Handler

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

步骤 3: 调用路由注册

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

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

必须同时更新两个文件:

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

验证检查

完成上述步骤后,运行以下命令验证:

# 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. 注册函数签名

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

3. RouteSpec 结构

type RouteSpec struct {
    Summary string        // 操作摘要(中文,简短)
    Input   interface{}   // 请求参数 DTO
    Output  interface{}   // 响应结果 DTO
    Tags    []string      // 分类标签(用于文档分组)
    Auth    bool          // 是否需要认证
}

4. 完整示例

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 标签,禁止使用行内注释。

// ❌ 错误
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 中列出所有可能值(中文)。

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. 请求参数类型标签

// 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

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

6. 分页响应规范

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

文档生成流程

1. 自动生成

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

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

生成的文档位置:

  • docs/admin-openapi.yaml - 独立生成
  • logs/openapi.yaml - 运行时生成

2. 验证文档

# 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 中添加:

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

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

常见问题

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

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

检查清单(按优先级排序)

  1. 【最常遗漏】 是否在文档生成器中添加了 Handler

    必须同时检查两个文件:

    // cmd/api/docs.go
    handlers := &bootstrap.Handlers{
        Xxx: admin.NewXxxHandler(nil),  // 是否添加?
    }
    
    // cmd/gendocs/main.go  
    handlers := &bootstrap.Handlers{
        Xxx: admin.NewXxxHandler(nil),  // 是否添加?
    }
    
  2. 是否使用了 Register() 函数?

    // ❌ 错误
    router.Post("/path", handler.Method)
    
    // ✅ 正确
    Register(router, doc, basePath, "POST", "/path", handler.Method, RouteSpec{...})
    
  3. 路由注册函数是否接收了 doc *openapi.Generator 参数?

    func registerXxxRoutes(router fiber.Router, handler *admin.XxxHandler, doc *openapi.Generator, basePath string)
    
  4. 是否调用了路由注册函数?

    • 检查 internal/routes/admin.go 中是否调用了 registerXxxRoutes()
    • 检查 internal/routes/routes.go 是否调用了 RegisterAdminRoutes()

快速定位问题

# 检查 Handler 是否在文档生成器中注册
grep "NewXxxHandler" cmd/api/docs.go cmd/gendocs/main.go

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

原因:路径参数(如 /:id)的 DTO 缺少对应字段。

解决方案:创建组合参数 DTO

// ❌ 错误:直接使用 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 标签?

    ShopName string `json:"shop_name" description:"店铺名称"`
    
  2. 是否使用了行内注释(不会被识别)?

    // ❌ 错误
    ShopName string `json:"shop_name"` // 店铺名称
    
    // ✅ 正确
    ShopName string `json:"shop_name" description:"店铺名称"`
    

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

完整步骤(共 6 步):

  1. 创建 Handlerinternal/handler/admin/xxx.go

  2. 添加到 Handlers 结构体internal/bootstrap/types.go

    type Handlers struct {
        Xxx *admin.XxxHandler
    }
    
  3. 实例化 Handlerinternal/bootstrap/handlers.go

    Xxx: admin.NewXxxHandler(services.Xxx),
    
  4. 创建路由文件internal/routes/xxx.go

    func registerXxxRoutes(api fiber.Router, h *admin.XxxHandler, doc *openapi.Generator, basePath string) {
        // 使用 Register() 注册路由
    }
    
  5. 调用路由注册internal/routes/admin.go

    if handlers.Xxx != nil {
        registerXxxRoutes(authGroup, handlers.Xxx, doc, basePath)
    }
    
  6. 更新文档生成器⚠️ 两个文件都要改):

    • cmd/api/docs.go
    • cmd/gendocs/main.go
    handlers := &bootstrap.Handlers{
        Xxx: admin.NewXxxHandler(nil),
    }
    
  7. 验证

    go build ./...
    go run cmd/gendocs/main.go
    grep "/api/admin/xxx" docs/admin-openapi.yaml
    

Q5: 如何调试文档生成?

# 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))
"

参考资料