feat: OpenAPI 契约对齐与框架优化
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
主要变更: 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 个主规范文件 破坏性变更:无 向后兼容:是
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
# Change: OpenAPI 文档契约对齐
|
||||
|
||||
## Why
|
||||
|
||||
确保 OpenAPI 文档描述的响应结构与真实运行时一致,避免 SDK 生成和接口对接问题。
|
||||
|
||||
**当前问题**:
|
||||
|
||||
1. **响应字段名不一致**:
|
||||
- OpenAPI 错误响应定义为 `message` 字段
|
||||
- 真实运行时返回为 `msg` 字段
|
||||
|
||||
2. **成功响应缺少 envelope**:
|
||||
- OpenAPI 文档直接返回 DTO schema
|
||||
- 真实运行时包裹在 `{code, data, msg, timestamp}` 中
|
||||
|
||||
3. **handlers 清单不完整**:
|
||||
- `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 清单不一致
|
||||
- 缺少部分 handler(PersonalCustomer、ShopPackageBatchAllocation、ShopPackageBatchPricing)
|
||||
|
||||
4. **个人客户路由未纳入文档**:
|
||||
- `/api/c/v1` 路由未使用 `Register(...)` 机制
|
||||
- 不在 OpenAPI 文档体系中
|
||||
|
||||
## What Changes
|
||||
|
||||
### 4.1 响应字段名对齐
|
||||
|
||||
修改 OpenAPI 错误响应 schema:
|
||||
|
||||
```yaml
|
||||
# ❌ 当前
|
||||
components:
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
properties:
|
||||
code: { type: integer }
|
||||
message: { type: string } # 错误:应为 msg
|
||||
data: { type: object }
|
||||
timestamp: { type: string }
|
||||
|
||||
# ✅ 修复后
|
||||
components:
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
properties:
|
||||
code: { type: integer, example: 0 }
|
||||
msg: { type: string, example: "success" } # 对齐真实字段名
|
||||
data: { type: object }
|
||||
timestamp: { type: string, format: date-time }
|
||||
```
|
||||
|
||||
### 4.2 成功响应体现 envelope
|
||||
|
||||
修改成功响应格式,包裹 DTO:
|
||||
|
||||
```yaml
|
||||
# ❌ 当前(直接返回 DTO)
|
||||
/api/admin/users:
|
||||
get:
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserDTO'
|
||||
|
||||
# ✅ 修复后(包裹 envelope)
|
||||
/api/admin/users:
|
||||
get:
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer, example: 0 }
|
||||
msg: { type: string, example: "success" }
|
||||
data:
|
||||
$ref: '#/components/schemas/UserDTO'
|
||||
timestamp: { type: string, format: date-time }
|
||||
```
|
||||
|
||||
### 4.3 补齐 handlers 清单
|
||||
|
||||
在文档生成器中补充缺失的 handler:
|
||||
|
||||
```go
|
||||
// cmd/api/docs.go 和 cmd/gendocs/main.go
|
||||
|
||||
handlers := &bootstrap.Handlers{
|
||||
// ... 现有 handlers
|
||||
|
||||
// 补充缺失的 handlers
|
||||
PersonalCustomer: personal.NewPersonalCustomerHandler(nil),
|
||||
ShopPackageBatchAllocation: admin.NewShopPackageBatchAllocationHandler(nil),
|
||||
ShopPackageBatchPricing: admin.NewShopPackageBatchPricingHandler(nil),
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 个人客户路由纳入文档
|
||||
|
||||
改造 `internal/routes/personal.go` 使用 `Register(...)` 机制:
|
||||
|
||||
```go
|
||||
// ❌ 当前
|
||||
func RegisterPersonalRoutes(app *fiber.App, handlers *bootstrap.Handlers) {
|
||||
api := app.Group("/api/c/v1")
|
||||
api.Get("/cards/:iccid", handlers.PersonalCustomer.GetCard)
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 修复后
|
||||
func RegisterPersonalRoutes(doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers) {
|
||||
doc.Register(openapi.RouteSpec{
|
||||
Method: "GET",
|
||||
Path: "/api/c/v1/cards/:iccid",
|
||||
Handler: handlers.PersonalCustomer.GetCard,
|
||||
Summary: "获取个人客户卡详情",
|
||||
Tags: []string{"个人客户"},
|
||||
Auth: true,
|
||||
Input: nil, // 路径参数
|
||||
Output: &dto.CardDetailResponse{},
|
||||
})
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Decisions
|
||||
|
||||
### OpenAPI 生成策略
|
||||
|
||||
1. **统一 envelope 包裹**:所有成功响应使用 `{code, data, msg, timestamp}`
|
||||
2. **字段名一致**:错误响应使用 `msg` 而非 `message`
|
||||
3. **DTO 保持具体类型**:`data` 字段保留具体的 DTO schema
|
||||
4. **自动化 handlers 构造**:文档生成时 handlers 可以传入 `nil` 依赖
|
||||
|
||||
### 文档生成复用
|
||||
|
||||
抽取公共函数避免重复:
|
||||
|
||||
```go
|
||||
// pkg/openapi/handlers.go (新建)
|
||||
func BuildDocHandlers() *bootstrap.Handlers {
|
||||
// 文档生成用,所有依赖传 nil
|
||||
return &bootstrap.Handlers{
|
||||
Account: admin.NewAccountHandler(nil, nil),
|
||||
Shop: admin.NewShopHandler(nil, nil),
|
||||
// ... 所有 handlers
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 中复用:
|
||||
|
||||
```go
|
||||
handlers := openapi.BuildDocHandlers()
|
||||
```
|
||||
|
||||
## Impact
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- OpenAPI 文档结构变化(响应格式)
|
||||
- 需要通知 SDK 使用方重新生成 SDK
|
||||
- 前端可能需要调整响应解析逻辑(如果直接使用 OpenAPI 生成的类型)
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
- 更新 `docs/api-documentation-guide.md` 补充 envelope 说明
|
||||
- 补充个人客户 API 路由注册示例
|
||||
- 在 API 文档中说明 envelope 格式
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
生成文档后对比验证:
|
||||
|
||||
```bash
|
||||
# 1. 重新生成文档
|
||||
go run cmd/gendocs/main.go
|
||||
|
||||
# 2. 对比差异
|
||||
diff logs/openapi.yaml logs/openapi.yaml.old
|
||||
|
||||
# 3. 验证关键点
|
||||
# - 检查响应字段名是否为 msg(非 message)
|
||||
# - 检查成功响应是否包含 envelope
|
||||
# - 检查 /api/c/v1 路由是否出现
|
||||
# - 检查接口数量是否完整
|
||||
```
|
||||
|
||||
## Affected Specs
|
||||
|
||||
- **UPDATE**: `openspec/specs/openapi-generation/spec.md`
|
||||
- 补充 envelope 包裹要求
|
||||
- 更新字段名规范
|
||||
|
||||
- **UPDATE**: `openspec/specs/personal-customer/spec.md`
|
||||
- 个人客户 API 进入文档体系
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### 编译检查
|
||||
```bash
|
||||
go build -o /tmp/test_gendocs ./cmd/gendocs
|
||||
```
|
||||
|
||||
### 文档生成
|
||||
```bash
|
||||
go run cmd/gendocs/main.go
|
||||
```
|
||||
|
||||
### 文档验证
|
||||
|
||||
检查生成的 `logs/openapi.yaml`:
|
||||
|
||||
- [ ] 错误响应字段名为 `msg`(非 `message`)
|
||||
- [ ] 成功响应包含 envelope:
|
||||
```yaml
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer }
|
||||
msg: { type: string }
|
||||
data: { ... }
|
||||
timestamp: { type: string }
|
||||
```
|
||||
- [ ] `/api/c/v1` 路由出现在文档中
|
||||
- [ ] 接口数量完整(与已注册路由一致)
|
||||
|
||||
### 示例响应验证
|
||||
|
||||
对比文档示例与真实响应:
|
||||
|
||||
**文档示例**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"username": "admin"
|
||||
},
|
||||
"timestamp": "2026-01-29T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**真实响应**(curl 测试):
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/admin/users/1 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
确认字段名和结构一致。
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
| 任务 | 预估时间 |
|
||||
|-----|---------|
|
||||
| 4.1 响应字段名对齐 | 0.5h |
|
||||
| 4.2 成功响应 envelope | 1h |
|
||||
| 4.3 补齐 handlers 清单 | 0.5h |
|
||||
| 4.4 个人客户路由纳入 | 1h |
|
||||
| 文档验证 | 0.5h |
|
||||
| 文档更新 | 0.5h |
|
||||
|
||||
**总计**:约 4 小时
|
||||
Reference in New Issue
Block a user