Files
junhong_cmp_fiber/docs/api-documentation-guide.md
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

701 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)