All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
581 lines
15 KiB
Markdown
581 lines
15 KiB
Markdown
# 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:"每页数量"`
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 文档生成流程
|
||
|
||
### 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: 如何调试文档生成?
|
||
|
||
```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)
|