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

90
pkg/errors/context.go Normal file
View File

@@ -0,0 +1,90 @@
package errors
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// ErrorContext 错误发生时的请求上下文(用于日志记录)
type ErrorContext struct {
RequestID string // 请求 ID唯一标识
Method string // HTTP 方法
Path string // 请求路径
Query string // Query 参数
Body string // 请求 Body限制 50KB
IP string // 客户端 IP
UserAgent string // User-Agent
UserID string // 用户 ID如果已认证
}
const (
// MaxBodyLogSize 请求 Body 日志记录最大字节数50KB
MaxBodyLogSize = 50 * 1024
)
// FromFiberContext 从 Fiber Context 提取错误上下文
func FromFiberContext(c *fiber.Ctx) *ErrorContext {
ctx := &ErrorContext{
Method: c.Method(),
Path: c.Path(),
Query: c.Request().URI().QueryArgs().String(),
IP: c.IP(),
UserAgent: c.Get("User-Agent"),
}
// 提取 Request ID
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
ctx.RequestID = rid.(string)
}
if ctx.RequestID == "" {
ctx.RequestID = c.Get("X-Request-ID")
}
// 提取 User ID如果已认证
if uid := c.Locals("user_id"); uid != nil {
if userID, ok := uid.(string); ok {
ctx.UserID = userID
}
}
// 提取请求 Body限制 50KB
bodyBytes := c.Body()
if len(bodyBytes) > 0 {
if len(bodyBytes) > MaxBodyLogSize {
// 超过限制时截断并添加提示
ctx.Body = string(bodyBytes[:MaxBodyLogSize]) + " ... (truncated)"
} else {
ctx.Body = string(bodyBytes)
}
}
return ctx
}
// ToLogFields 转换为 Zap 日志字段
func (ec *ErrorContext) ToLogFields() []zap.Field {
fields := []zap.Field{
zap.String("request_id", ec.RequestID),
zap.String("method", ec.Method),
zap.String("path", ec.Path),
zap.String("ip", ec.IP),
}
// 可选字段(非空时添加)
if ec.Query != "" {
fields = append(fields, zap.String("query", ec.Query))
}
if ec.Body != "" {
fields = append(fields, zap.String("body", ec.Body))
}
if ec.UserAgent != "" {
fields = append(fields, zap.String("user_agent", ec.UserAgent))
}
if ec.UserID != "" {
fields = append(fields, zap.String("user_id", ec.UserID))
}
return fields
}