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:
2025-11-15 12:17:44 +08:00
parent a371f1cd21
commit fb83c9a706
33 changed files with 7373 additions and 52 deletions

173
pkg/errors/handler.go Normal file
View File

@@ -0,0 +1,173 @@
package errors
import (
"runtime/debug"
"time"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// SafeErrorHandler 返回受保护的 Fiber ErrorHandler
// 使用 defer/recover 防止 ErrorHandler 自身 panic 导致服务崩溃
func SafeErrorHandler(logger *zap.Logger) fiber.ErrorHandler {
return func(c *fiber.Ctx, err error) error {
// 使用 defer/recover 保护 ErrorHandler 自身
defer func() {
if r := recover(); r != nil {
// ErrorHandler 自身发生 panic记录日志并返回空响应
logger.Error("ErrorHandler panic",
zap.Any("panic", r),
zap.String("stack", string(debug.Stack())),
)
// 返回 500 空响应体,避免泄露错误信息
_ = c.Status(500).SendString("")
}
}()
// 调用核心错误处理逻辑
return handleError(c, err, logger)
}
}
// handleError 核心错误处理逻辑
func handleError(c *fiber.Ctx, err error, logger *zap.Logger) error {
// 1. 检查响应是否已发送
if c.Response().StatusCode() != fiber.StatusOK || len(c.Response().Body()) > 0 {
// 响应已发送,仅记录日志,不修改响应
safeLog(logger, "响应已发送后发生错误",
zap.Error(err),
zap.Int("status", c.Response().StatusCode()),
zap.Int("body_size", len(c.Response().Body())),
)
return nil
}
// 2. 提取错误上下文
errCtx := FromFiberContext(c)
// 3. 错误类型分类和处理
var code int
var message string
var httpStatus int
switch e := err.(type) {
case *AppError:
// 应用自定义错误
code = e.Code
message = e.Message
httpStatus = e.HTTPStatus
// 记录错误日志(包含完整上下文)
logFields := append(errCtx.ToLogFields(),
zap.Int("error_code", code),
zap.Error(err),
)
// 根据错误类型决定日志级别
logLevel := GetLogLevel(code)
if logLevel == "error" {
// 服务端错误 -> Error 级别
safeLogWithLevel(logger, "error", "服务端错误", logFields...)
} else {
// 客户端错误 -> Warn 级别
safeLogWithLevel(logger, "warn", "客户端错误", logFields...)
}
case *fiber.Error:
// Fiber 框架错误
httpStatus = e.Code
code = mapHTTPStatusToCode(httpStatus)
message = GetMessage(code, "zh")
safeLog(logger, "Fiber 框架错误",
append(errCtx.ToLogFields(),
zap.Int("http_status", httpStatus),
zap.String("fiber_message", e.Message),
)...,
)
default:
// 其他未知错误,默认为内部服务器错误
code = CodeInternalError
httpStatus = 500
message = GetMessage(CodeInternalError, "zh")
safeLog(logger, "未知错误",
append(errCtx.ToLogFields(),
zap.Error(err),
)...,
)
}
// 4. 敏感信息脱敏:所有 5xx 错误返回通用消息
if httpStatus >= 500 {
message = GetMessage(code, "zh")
}
// 5. 设置响应 Header X-Request-ID
if errCtx.RequestID != "" {
c.Set("X-Request-ID", errCtx.RequestID)
}
// 6. 返回统一 JSON 响应
return c.Status(httpStatus).JSON(fiber.Map{
"code": code,
"data": nil,
"msg": message,
"timestamp": time.Now().Format(time.RFC3339),
})
}
// safeLog 安全地记录日志,日志失败时静默处理(默认 Error 级别)
func safeLog(logger *zap.Logger, msg string, fields ...zap.Field) {
safeLogWithLevel(logger, "error", msg, fields...)
}
// safeLogWithLevel 安全地记录指定级别的日志,日志失败时静默处理
func safeLogWithLevel(logger *zap.Logger, level string, msg string, fields ...zap.Field) {
defer func() {
if r := recover(); r != nil {
// 日志系统 panic静默丢弃不阻塞响应
// 不记录到任何地方,避免无限循环
}
}()
switch level {
case "warn":
logger.Warn(msg, fields...)
case "error":
logger.Error(msg, fields...)
case "info":
logger.Info(msg, fields...)
default:
logger.Error(msg, fields...)
}
}
// mapHTTPStatusToCode 将 HTTP 状态码映射为应用错误码
func mapHTTPStatusToCode(status int) int {
switch status {
case 400:
return CodeInvalidParam
case 401:
return CodeUnauthorized
case 403:
return CodeForbidden
case 404:
return CodeNotFound
case 409:
return CodeConflict
case 429:
return CodeTooManyRequests
case 503:
return CodeServiceUnavailable
case 504:
return CodeTimeout
default:
if status >= 500 {
return CodeInternalError
}
return CodeInvalidParam
}
}