- 新增统一错误码定义和管理 (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
490 lines
13 KiB
YAML
490 lines
13 KiB
YAML
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"
|