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:
389
specs/003-error-handling/data-model.md
Normal file
389
specs/003-error-handling/data-model.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Data Model: Fiber 错误处理集成
|
||||
|
||||
**Feature**: 003-error-handling
|
||||
**Date**: 2025-11-14
|
||||
**Status**: Draft
|
||||
|
||||
## 概述
|
||||
|
||||
本文档定义了 Fiber 错误处理集成所需的数据模型和结构。由于这是一个基础设施功能,主要涉及错误处理流程,没有持久化的数据实体,但有运行时的数据结构。
|
||||
|
||||
## 核心数据结构
|
||||
|
||||
### 1. AppError (应用错误类型)
|
||||
|
||||
**位置**: `pkg/errors/errors.go` (已存在,需扩展)
|
||||
|
||||
**用途**: 表示应用层的业务错误,包含错误码、消息和原始错误链
|
||||
|
||||
**字段**:
|
||||
```go
|
||||
type AppError struct {
|
||||
Code int // 应用错误码 (1000-1999: 客户端错误, 2000-2999: 服务端错误)
|
||||
Message string // 错误消息 (用户可见,已脱敏)
|
||||
HTTPStatus int // HTTP 状态码 (根据 Code 自动映射)
|
||||
Err error // 底层原始错误 (可选,用于错误链)
|
||||
}
|
||||
```
|
||||
|
||||
**方法**:
|
||||
```go
|
||||
// Error 实现 error 接口
|
||||
func (e *AppError) Error() string
|
||||
|
||||
// Unwrap 支持错误链
|
||||
func (e *AppError) Unwrap() error
|
||||
|
||||
// WithHTTPStatus 设置自定义 HTTP 状态码
|
||||
func (e *AppError) WithHTTPStatus(status int) *AppError
|
||||
```
|
||||
|
||||
**验证规则**:
|
||||
- Code 必须在定义的范围内 (1000-2999)
|
||||
- Message 不能为空
|
||||
- HTTPStatus 如果未设置,根据 Code 自动映射
|
||||
|
||||
**关系**:
|
||||
- 无数据库关系 (运行时对象)
|
||||
- 可以包装其他 error 形成错误链
|
||||
|
||||
---
|
||||
|
||||
### 2. ErrorResponse (错误响应结构)
|
||||
|
||||
**位置**: `pkg/response/response.go` 中的 Response 结构 (已存在)
|
||||
|
||||
**用途**: 统一的 JSON 错误响应格式,返回给客户端
|
||||
|
||||
**字段**:
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 应用错误码 (0 = 成功, >0 = 错误)
|
||||
Data any `json:"data"` // 响应数据 (错误时为 null)
|
||||
Message string `json:"msg"` // 可读消息 (用户友好,已脱敏)
|
||||
Timestamp string `json:"timestamp"` // ISO 8601 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```json
|
||||
{
|
||||
"code": 1001,
|
||||
"data": null,
|
||||
"msg": "参数验证失败",
|
||||
"timestamp": "2025-11-14T16:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
**验证规则**:
|
||||
- Code 必须为非负整数
|
||||
- Timestamp 必须为 RFC3339 格式
|
||||
- Message 不能为空
|
||||
- 错误响应时 Data 为 null
|
||||
|
||||
**关系**:
|
||||
- 从 AppError 生成
|
||||
- Request ID 通过响应 Header X-Request-ID 传递,不在响应体中
|
||||
|
||||
---
|
||||
|
||||
### 3. ErrorContext (错误上下文)
|
||||
|
||||
**位置**: 新增 `pkg/errors/context.go`
|
||||
|
||||
**用途**: 记录错误发生时的请求上下文,用于日志记录和调试
|
||||
|
||||
**字段**:
|
||||
```go
|
||||
type ErrorContext struct {
|
||||
RequestID string // 请求 ID (唯一标识)
|
||||
Method string // HTTP 方法
|
||||
Path string // 请求路径
|
||||
Query string // Query 参数
|
||||
IP string // 客户端 IP
|
||||
UserAgent string // User-Agent
|
||||
UserID string // 用户 ID (如果已认证)
|
||||
Headers map[string]string // 重要的请求头 (可选)
|
||||
StackTrace string // 堆栈跟踪 (panic 时有值)
|
||||
}
|
||||
```
|
||||
|
||||
**方法**:
|
||||
```go
|
||||
// FromFiberContext 从 Fiber Context 提取错误上下文
|
||||
func FromFiberContext(c *fiber.Ctx) *ErrorContext
|
||||
|
||||
// ToLogFields 转换为 Zap 日志字段
|
||||
func (ec *ErrorContext) ToLogFields() []zap.Field
|
||||
```
|
||||
|
||||
**验证规则**:
|
||||
- RequestID 不能为空
|
||||
- Method 和 Path 不能为空
|
||||
- 其他字段可选
|
||||
|
||||
**用途场景**:
|
||||
- 记录错误日志时附加完整上下文
|
||||
- 调试时快速定位问题
|
||||
- 不返回给客户端 (仅内部使用)
|
||||
|
||||
---
|
||||
|
||||
### 4. ErrorCode (错误码枚举)
|
||||
|
||||
**位置**: 新增 `pkg/errors/codes.go`
|
||||
|
||||
**用途**: 定义所有应用错误码和对应的默认消息
|
||||
|
||||
**结构**:
|
||||
```go
|
||||
const (
|
||||
// 成功
|
||||
CodeSuccess = 0
|
||||
|
||||
// 客户端错误 (1000-1999) -> 4xx HTTP 状态码
|
||||
CodeInvalidParam = 1001 // 参数验证失败
|
||||
CodeMissingToken = 1002 // 缺失认证令牌
|
||||
CodeInvalidToken = 1003 // 无效或过期的令牌
|
||||
CodeUnauthorized = 1004 // 未授权
|
||||
CodeForbidden = 1005 // 禁止访问
|
||||
CodeNotFound = 1006 // 资源未找到
|
||||
CodeConflict = 1007 // 资源冲突
|
||||
CodeTooManyRequests = 1008 // 请求过多
|
||||
CodeRequestTooLarge = 1009 // 请求体过大
|
||||
|
||||
// 服务端错误 (2000-2999) -> 5xx HTTP 状态码
|
||||
CodeInternalError = 2001 // 内部服务器错误
|
||||
CodeDatabaseError = 2002 // 数据库错误
|
||||
CodeRedisError = 2003 // Redis 错误
|
||||
CodeServiceUnavailable = 2004 // 服务不可用
|
||||
CodeTimeout = 2005 // 请求超时
|
||||
CodeTaskQueueError = 2006 // 任务队列错误
|
||||
)
|
||||
|
||||
// 错误消息映射 (中文)
|
||||
var errorMessages = map[int]string{
|
||||
CodeSuccess: "成功",
|
||||
CodeInvalidParam: "参数验证失败",
|
||||
CodeMissingToken: "缺失认证令牌",
|
||||
CodeInvalidToken: "无效或过期的令牌",
|
||||
CodeUnauthorized: "未授权访问",
|
||||
CodeForbidden: "禁止访问",
|
||||
CodeNotFound: "资源未找到",
|
||||
CodeConflict: "资源冲突",
|
||||
CodeTooManyRequests: "请求过多,请稍后重试",
|
||||
CodeRequestTooLarge: "请求体过大",
|
||||
CodeInternalError: "内部服务器错误",
|
||||
CodeDatabaseError: "数据库错误",
|
||||
CodeRedisError: "缓存服务错误",
|
||||
CodeServiceUnavailable: "服务暂时不可用",
|
||||
CodeTimeout: "请求超时",
|
||||
CodeTaskQueueError: "任务队列错误",
|
||||
}
|
||||
|
||||
// GetMessage 获取错误消息
|
||||
func GetMessage(code int, lang string) string
|
||||
```
|
||||
|
||||
**HTTP 状态码映射规则**:
|
||||
```go
|
||||
func GetHTTPStatus(code int) int {
|
||||
switch code {
|
||||
case CodeInvalidParam, CodeRequestTooLarge:
|
||||
return 400 // Bad Request
|
||||
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized:
|
||||
return 401 // Unauthorized
|
||||
case CodeForbidden:
|
||||
return 403 // Forbidden
|
||||
case CodeNotFound:
|
||||
return 404 // Not Found
|
||||
case CodeConflict:
|
||||
return 409 // Conflict
|
||||
case CodeTooManyRequests:
|
||||
return 429 // Too Many Requests
|
||||
case CodeServiceUnavailable:
|
||||
return 503 // Service Unavailable
|
||||
case CodeTimeout:
|
||||
return 504 // Gateway Timeout
|
||||
default:
|
||||
if code >= 2000 && code < 3000 {
|
||||
return 500 // Internal Server Error
|
||||
}
|
||||
return 400 // 默认客户端错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理流程数据流
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 请求到达 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 中间件/Handler │
|
||||
│ 返回 error │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Fiber ErrorHandler │
|
||||
│ 1. 检查响应是否已发送 │
|
||||
│ 2. 提取错误类型和上下文 │
|
||||
└──────┬──────────────────────┘
|
||||
│
|
||||
├─────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────┐ ┌──────────┐
|
||||
│AppError│ │其他Error │
|
||||
└───┬────┘ └────┬─────┘
|
||||
│ │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ 生成上下文 │
|
||||
│ ErrorContext │
|
||||
└────┬───────────┘
|
||||
│
|
||||
├──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────────┐
|
||||
│记录日志 │ │生成响应 │
|
||||
│(完整上下文)│ │ErrorResponse│
|
||||
└─────────┘ └──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│返回给客户端 │
|
||||
│(脱敏后) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常量定义
|
||||
|
||||
### Request ID 上下文键
|
||||
|
||||
**位置**: `pkg/constants/constants.go` (已存在,可能需要添加)
|
||||
|
||||
```go
|
||||
const (
|
||||
ContextKeyRequestID = "request_id" // Fiber Locals 中存储 Request ID 的键
|
||||
HeaderRequestID = "X-Request-ID" // HTTP Header 中的 Request ID 键
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 非功能性约束
|
||||
|
||||
### 性能
|
||||
|
||||
- ErrorContext 创建: < 0.1ms
|
||||
- 错误日志记录: 异步,不阻塞响应 (< 0.5ms)
|
||||
- 错误响应生成: < 0.5ms
|
||||
- 总错误处理延迟: < 1ms (P95)
|
||||
|
||||
### 并发
|
||||
|
||||
- AppError 是不可变的 (immutable),线程安全
|
||||
- ErrorContext 仅在错误处理流程中创建和使用,不共享
|
||||
- 错误码常量映射只读,无并发问题
|
||||
|
||||
### 内存
|
||||
|
||||
- ErrorContext 在请求结束后释放
|
||||
- 预定义的错误对象可以复用 (如 ErrMissingToken)
|
||||
- 避免在错误处理中分配大量内存
|
||||
|
||||
---
|
||||
|
||||
## 与现有代码的集成
|
||||
|
||||
### 现有错误类型
|
||||
|
||||
**位置**: `pkg/errors/errors.go`
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
var (
|
||||
ErrMissingToken = errors.New("missing authentication token")
|
||||
ErrInvalidToken = errors.New("invalid or expired token")
|
||||
ErrRedisUnavailable = errors.New("redis unavailable")
|
||||
ErrTooManyRequests = errors.New("too many requests")
|
||||
)
|
||||
|
||||
type AppError struct {
|
||||
Code int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
```
|
||||
|
||||
**需要的修改**:
|
||||
1. 为 AppError 添加 HTTPStatus 字段
|
||||
2. 添加错误码常量 (CodeMissingToken 等)
|
||||
3. 添加 GetMessage() 函数支持多语言
|
||||
4. 添加 GetHTTPStatus() 函数映射 HTTP 状态码
|
||||
|
||||
### 现有响应结构
|
||||
|
||||
**位置**: `pkg/response/response.go`
|
||||
|
||||
**现状**: 已有 Response 结构,无需修改
|
||||
|
||||
**使用方式**:
|
||||
```go
|
||||
// 成功响应 (不变)
|
||||
response.Success(c, data)
|
||||
|
||||
// 错误响应 (现有)
|
||||
response.Error(c, httpStatus, code, message)
|
||||
|
||||
// 新增: 从 AppError 生成错误响应
|
||||
response.ErrorFromAppError(c, appErr)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据验证
|
||||
|
||||
### 错误码验证
|
||||
|
||||
- 必须在定义的范围内 (0, 1000-1999, 2000-2999)
|
||||
- 未定义的错误码记录警告日志
|
||||
- 默认映射到 500 Internal Server Error
|
||||
|
||||
### 错误消息验证
|
||||
|
||||
- 不能为空字符串
|
||||
- 长度限制: 最大 500 字符
|
||||
- 不包含换行符或特殊字符 (避免日志注入)
|
||||
|
||||
### Request ID 验证
|
||||
|
||||
- 必须是有效的 UUID v4 格式
|
||||
- 如果缺失,ErrorHandler 仍然继续处理
|
||||
- 记录警告日志
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本数据模型设计:
|
||||
|
||||
1. **简洁**: 仅定义必要的运行时结构,无持久化实体
|
||||
2. **扩展性**: 错误码枚举易于添加新错误类型
|
||||
3. **安全性**: 错误响应和日志上下文分离,避免敏感信息泄露
|
||||
4. **性能**: 结构轻量,错误处理开销小
|
||||
5. **兼容性**: 与现有 pkg/errors 和 pkg/response 自然集成
|
||||
|
||||
所有数据结构都遵循 Go 惯用法: 简单的结构体,少量的方法,清晰的职责划分。
|
||||
Reference in New Issue
Block a user