feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (pkg/errors/codes.go) - 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go) - 新增错误上下文管理 (pkg/errors/context.go) - 增强 Panic 恢复中间件 (internal/middleware/recover.go) - 新增完整的单元测试和集成测试 - 新增功能文档 (docs/003-error-handling/) - 新增功能规范 (specs/003-error-handling/) - 更新 CLAUDE.md 和 README.md
This commit is contained in:
489
specs/003-error-handling/contracts/error-responses.yaml
Normal file
489
specs/003-error-handling/contracts/error-responses.yaml
Normal file
@@ -0,0 +1,489 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 君鸿卡管系统 - 统一错误响应规范
|
||||
description: |
|
||||
本文档定义了系统所有 API 端点的统一错误响应格式和错误码。
|
||||
|
||||
**关键原则**:
|
||||
- 所有错误响应使用统一的 JSON 格式
|
||||
- 错误码范围: 1000-1999 (客户端错误), 2000-2999 (服务端错误)
|
||||
- HTTP 状态码与错误码映射一致
|
||||
- Request ID 仅在响应 Header 中传递 (X-Request-ID)
|
||||
- 敏感信息仅记录到日志,不返回给客户端
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: 君鸿卡管系统开发团队
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: 本地开发环境
|
||||
- url: https://api.example.com
|
||||
description: 生产环境
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- data
|
||||
- msg
|
||||
- timestamp
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: |
|
||||
应用错误码
|
||||
- 0: 成功
|
||||
- 1000-1999: 客户端错误
|
||||
- 2000-2999: 服务端错误
|
||||
example: 1001
|
||||
data:
|
||||
type: 'null'
|
||||
description: 错误响应时始终为 null
|
||||
example: null
|
||||
msg:
|
||||
type: string
|
||||
description: 用户友好的错误消息 (中文, 已脱敏)
|
||||
example: "参数验证失败"
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 格式的时间戳
|
||||
example: "2025-11-14T16:00:00+08:00"
|
||||
example:
|
||||
code: 1001
|
||||
data: null
|
||||
msg: "参数验证失败"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: 请求参数验证失败
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: 请求唯一标识符
|
||||
example: "f1d8b767-dfb3-4588-9fa0-8a97e5337184"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
InvalidParam:
|
||||
value:
|
||||
code: 1001
|
||||
data: null
|
||||
msg: "参数验证失败"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
RequestTooLarge:
|
||||
value:
|
||||
code: 1009
|
||||
data: null
|
||||
msg: "请求体过大"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
Unauthorized:
|
||||
description: 未授权访问 (缺失或无效的认证令牌)
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: 请求唯一标识符
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
MissingToken:
|
||||
value:
|
||||
code: 1002
|
||||
data: null
|
||||
msg: "缺失认证令牌"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
InvalidToken:
|
||||
value:
|
||||
code: 1003
|
||||
data: null
|
||||
msg: "无效或过期的令牌"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
Forbidden:
|
||||
description: 禁止访问 (权限不足)
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 1005
|
||||
data: null
|
||||
msg: "禁止访问"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
NotFound:
|
||||
description: 资源未找到
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 1006
|
||||
data: null
|
||||
msg: "资源未找到"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
Conflict:
|
||||
description: 资源冲突 (如重复创建)
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 1007
|
||||
data: null
|
||||
msg: "资源冲突"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
TooManyRequests:
|
||||
description: 请求过多 (触发限流)
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
Retry-After:
|
||||
schema:
|
||||
type: integer
|
||||
description: 建议重试的秒数
|
||||
example: 60
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 1008
|
||||
data: null
|
||||
msg: "请求过多,请稍后重试"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
InternalServerError:
|
||||
description: 内部服务器错误 (通用服务端错误)
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: 请求唯一标识符 (用于追踪和调试)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
InternalError:
|
||||
value:
|
||||
code: 2001
|
||||
data: null
|
||||
msg: "内部服务器错误"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
DatabaseError:
|
||||
value:
|
||||
code: 2002
|
||||
data: null
|
||||
msg: "数据库错误"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
RedisError:
|
||||
value:
|
||||
code: 2003
|
||||
data: null
|
||||
msg: "缓存服务错误"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
ServiceUnavailable:
|
||||
description: 服务暂时不可用
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
Retry-After:
|
||||
schema:
|
||||
type: integer
|
||||
description: 建议重试的秒数
|
||||
example: 300
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 2004
|
||||
data: null
|
||||
msg: "服务暂时不可用"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
GatewayTimeout:
|
||||
description: 请求超时
|
||||
headers:
|
||||
X-Request-ID:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 2005
|
||||
data: null
|
||||
msg: "请求超时"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
# 错误码完整清单
|
||||
paths: {}
|
||||
|
||||
x-error-codes:
|
||||
success:
|
||||
- code: 0
|
||||
message: "成功"
|
||||
http_status: 200
|
||||
|
||||
client_errors:
|
||||
- code: 1001
|
||||
message: "参数验证失败"
|
||||
http_status: 400
|
||||
description: "请求参数不符合验证规则"
|
||||
|
||||
- code: 1002
|
||||
message: "缺失认证令牌"
|
||||
http_status: 401
|
||||
description: "请求头中缺少 Authorization 令牌"
|
||||
|
||||
- code: 1003
|
||||
message: "无效或过期的令牌"
|
||||
http_status: 401
|
||||
description: "认证令牌无效或已过期"
|
||||
|
||||
- code: 1004
|
||||
message: "未授权访问"
|
||||
http_status: 401
|
||||
description: "用户未通过认证"
|
||||
|
||||
- code: 1005
|
||||
message: "禁止访问"
|
||||
http_status: 403
|
||||
description: "用户权限不足"
|
||||
|
||||
- code: 1006
|
||||
message: "资源未找到"
|
||||
http_status: 404
|
||||
description: "请求的资源不存在"
|
||||
|
||||
- code: 1007
|
||||
message: "资源冲突"
|
||||
http_status: 409
|
||||
description: "资源已存在或状态冲突"
|
||||
|
||||
- code: 1008
|
||||
message: "请求过多,请稍后重试"
|
||||
http_status: 429
|
||||
description: "触发限流规则"
|
||||
|
||||
- code: 1009
|
||||
message: "请求体过大"
|
||||
http_status: 400
|
||||
description: "请求体大小超过限制"
|
||||
|
||||
server_errors:
|
||||
- code: 2001
|
||||
message: "内部服务器错误"
|
||||
http_status: 500
|
||||
description: "服务器内部发生未预期的错误"
|
||||
|
||||
- code: 2002
|
||||
message: "数据库错误"
|
||||
http_status: 500
|
||||
description: "数据库操作失败 (具体错误仅记录到日志)"
|
||||
|
||||
- code: 2003
|
||||
message: "缓存服务错误"
|
||||
http_status: 500
|
||||
description: "Redis 操作失败 (具体错误仅记录到日志)"
|
||||
|
||||
- code: 2004
|
||||
message: "服务暂时不可用"
|
||||
http_status: 503
|
||||
description: "服务正在维护或过载"
|
||||
|
||||
- code: 2005
|
||||
message: "请求超时"
|
||||
http_status: 504
|
||||
description: "请求处理超时"
|
||||
|
||||
- code: 2006
|
||||
message: "任务队列错误"
|
||||
http_status: 500
|
||||
description: "Asynq 任务队列操作失败"
|
||||
|
||||
x-security-notes: |
|
||||
## 敏感信息保护
|
||||
|
||||
所有错误响应遵循以下安全原则:
|
||||
|
||||
1. **服务端错误 (2xxx)**: 始终返回通用错误消息,不暴露:
|
||||
- 数据库错误详情 (SQL 语句、表结构)
|
||||
- 文件路径或系统路径
|
||||
- 堆栈跟踪信息
|
||||
- 配置信息或密钥
|
||||
|
||||
2. **客户端错误 (1xxx)**: 可返回具体的业务错误消息,但不包括:
|
||||
- 其他用户的数据
|
||||
- 系统内部状态
|
||||
|
||||
3. **Request ID**:
|
||||
- 仅在响应 Header X-Request-ID 中传递
|
||||
- 不在响应体中包含
|
||||
- 用于日志追踪和调试
|
||||
|
||||
4. **日志记录**:
|
||||
- 完整的错误详情 (包括堆栈、原始错误) 仅记录到日志
|
||||
- 日志访问需要运维团队权限
|
||||
- 敏感字段 (密码、密钥) 不记录到日志
|
||||
|
||||
x-error-handling-flow: |
|
||||
## 错误处理流程
|
||||
|
||||
1. **请求处理**:
|
||||
- 中间件或 Handler 返回 error
|
||||
- 错误被 Fiber ErrorHandler 捕获
|
||||
|
||||
2. **错误分类**:
|
||||
- *AppError: 提取错误码和消息
|
||||
- *fiber.Error: 映射 HTTP 状态码
|
||||
- 其他 error: 默认 500 Internal Server Error
|
||||
|
||||
3. **响应检查**:
|
||||
- 如果响应已发送: 仅记录日志,不修改响应
|
||||
- 如果响应未发送: 生成错误响应
|
||||
|
||||
4. **日志记录**:
|
||||
- 记录完整的错误上下文 (Request ID, 路径, 参数, 原始错误)
|
||||
- 客户端错误 (1xxx): Warn 级别
|
||||
- 服务端错误 (2xxx): Error 级别
|
||||
|
||||
5. **响应返回**:
|
||||
- 设置响应 Header: X-Request-ID
|
||||
- 返回统一格式的 JSON 响应体
|
||||
- HTTP 状态码与错误码映射一致
|
||||
|
||||
x-examples:
|
||||
successful_request:
|
||||
summary: 成功请求示例
|
||||
request:
|
||||
method: GET
|
||||
url: /api/v1/users/123
|
||||
headers:
|
||||
Authorization: "Bearer valid-token"
|
||||
response:
|
||||
status: 200
|
||||
headers:
|
||||
X-Request-ID: "f1d8b767-dfb3-4588-9fa0-8a97e5337184"
|
||||
body:
|
||||
code: 0
|
||||
data:
|
||||
id: "123"
|
||||
username: "testuser"
|
||||
email: "test@example.com"
|
||||
msg: "success"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
client_error_missing_token:
|
||||
summary: 缺失认证令牌
|
||||
request:
|
||||
method: GET
|
||||
url: /api/v1/users/123
|
||||
headers: {}
|
||||
response:
|
||||
status: 401
|
||||
headers:
|
||||
X-Request-ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
body:
|
||||
code: 1002
|
||||
data: null
|
||||
msg: "缺失认证令牌"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
client_error_validation:
|
||||
summary: 参数验证失败
|
||||
request:
|
||||
method: POST
|
||||
url: /api/v1/users
|
||||
headers:
|
||||
Authorization: "Bearer valid-token"
|
||||
body:
|
||||
username: ""
|
||||
email: "invalid-email"
|
||||
response:
|
||||
status: 400
|
||||
headers:
|
||||
X-Request-ID: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
||||
body:
|
||||
code: 1001
|
||||
data: null
|
||||
msg: "参数验证失败"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
|
||||
server_error_database:
|
||||
summary: 数据库错误 (敏感信息已隐藏)
|
||||
request:
|
||||
method: GET
|
||||
url: /api/v1/users/123
|
||||
headers:
|
||||
Authorization: "Bearer valid-token"
|
||||
response:
|
||||
status: 500
|
||||
headers:
|
||||
X-Request-ID: "c3d4e5f6-a7b8-9012-cdef-123456789012"
|
||||
body:
|
||||
code: 2002
|
||||
data: null
|
||||
msg: "数据库错误"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
note: |
|
||||
客户端仅收到通用错误消息 "数据库错误"。
|
||||
完整的错误详情 (如 "pq: relation 'users' does not exist") 仅记录到服务器日志。
|
||||
客户端可使用 X-Request-ID 联系技术支持进行排查。
|
||||
|
||||
rate_limit_exceeded:
|
||||
summary: 触发限流
|
||||
request:
|
||||
method: GET
|
||||
url: /api/v1/users
|
||||
headers:
|
||||
Authorization: "Bearer valid-token"
|
||||
response:
|
||||
status: 429
|
||||
headers:
|
||||
X-Request-ID: "d4e5f6a7-b8c9-0123-def1-234567890123"
|
||||
Retry-After: "60"
|
||||
body:
|
||||
code: 1008
|
||||
data: null
|
||||
msg: "请求过多,请稍后重试"
|
||||
timestamp: "2025-11-14T16:00:00+08:00"
|
||||
Reference in New Issue
Block a user