Files
junhong_cmp_fiber/specs/003-error-handling/data-model.md
huang fb83c9a706 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
2025-11-15 12:17:44 +08:00

11 KiB

Data Model: Fiber 错误处理集成

Feature: 003-error-handling
Date: 2025-11-14
Status: Draft

概述

本文档定义了 Fiber 错误处理集成所需的数据模型和结构。由于这是一个基础设施功能,主要涉及错误处理流程,没有持久化的数据实体,但有运行时的数据结构。

核心数据结构

1. AppError (应用错误类型)

位置: pkg/errors/errors.go (已存在,需扩展)

用途: 表示应用层的业务错误,包含错误码、消息和原始错误链

字段:

type AppError struct {
    Code       int    // 应用错误码 (1000-1999: 客户端错误, 2000-2999: 服务端错误)
    Message    string // 错误消息 (用户可见,已脱敏)
    HTTPStatus int    // HTTP 状态码 (根据 Code 自动映射)
    Err        error  // 底层原始错误 (可选,用于错误链)
}

方法:

// 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 错误响应格式,返回给客户端

字段:

type Response struct {
    Code      int    `json:"code"`      // 应用错误码 (0 = 成功, >0 = 错误)
    Data      any    `json:"data"`      // 响应数据 (错误时为 null)
    Message   string `json:"msg"`       // 可读消息 (用户友好,已脱敏)
    Timestamp string `json:"timestamp"` // ISO 8601 时间戳
}

示例:

{
  "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

用途: 记录错误发生时的请求上下文,用于日志记录和调试

字段:

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 时有值)
}

方法:

// 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

用途: 定义所有应用错误码和对应的默认消息

结构:

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 状态码映射规则:

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 (已存在,可能需要添加)

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

现状:

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 结构,无需修改

使用方式:

// 成功响应 (不变)
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 惯用法: 简单的结构体,少量的方法,清晰的职责划分。