128 lines
3.1 KiB
Go
128 lines
3.1 KiB
Go
package errorx
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/gofiber/fiber/v3"
|
||
"github.com/gofiber/fiber/v3/binder"
|
||
"github.com/gofiber/utils/v2"
|
||
"github.com/google/uuid"
|
||
log "github.com/sirupsen/logrus"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ResponseSender 响应发送器
|
||
type ResponseSender struct {
|
||
handler *ErrorHandler
|
||
}
|
||
|
||
// NewResponseSender 创建响应发送器
|
||
func NewResponseSender() *ResponseSender {
|
||
return &ResponseSender{
|
||
handler: NewErrorHandler(),
|
||
}
|
||
}
|
||
|
||
// SendError 发送错误响应
|
||
func (s *ResponseSender) SendError(ctx fiber.Ctx, err error) error {
|
||
appErr := s.handler.Handle(err)
|
||
|
||
// 记录错误日志
|
||
s.logError(appErr)
|
||
|
||
// 根据 Content-Type 返回不同格式
|
||
return s.sendResponse(ctx, appErr)
|
||
}
|
||
|
||
// logError 记录错误日志
|
||
func (s *ResponseSender) logError(appErr *AppError) {
|
||
// 确保每个错误实例都有唯一ID,便于日志关联
|
||
if appErr.ID == "" {
|
||
appErr.ID = uuid.NewString()
|
||
}
|
||
|
||
// 构造详细的错误级联链路(包含类型、状态、定位等)
|
||
chain := make([]map[string]any, 0, 4)
|
||
var e error = appErr
|
||
for e != nil {
|
||
entry := map[string]any{
|
||
"type": fmt.Sprintf("%T", e),
|
||
"error": e.Error(),
|
||
}
|
||
switch v := e.(type) {
|
||
case *AppError:
|
||
entry["code"] = v.Code
|
||
entry["statusCode"] = v.StatusCode
|
||
if v.file != "" {
|
||
entry["file"] = v.file
|
||
}
|
||
if len(v.params) > 0 {
|
||
entry["params"] = v.params
|
||
}
|
||
if v.sql != "" {
|
||
entry["sql"] = v.sql
|
||
}
|
||
if v.ID != "" {
|
||
entry["id"] = v.ID
|
||
}
|
||
case *fiber.Error:
|
||
entry["statusCode"] = v.Code
|
||
entry["message"] = v.Message
|
||
}
|
||
|
||
// GORM 常见错误归类标记
|
||
if errors.Is(e, gorm.ErrRecordNotFound) {
|
||
entry["gorm"] = "record_not_found"
|
||
} else if errors.Is(e, gorm.ErrDuplicatedKey) {
|
||
entry["gorm"] = "duplicated_key"
|
||
} else if errors.Is(e, gorm.ErrInvalidTransaction) {
|
||
entry["gorm"] = "invalid_transaction"
|
||
}
|
||
|
||
chain = append(chain, entry)
|
||
e = errors.Unwrap(e)
|
||
}
|
||
|
||
root := chain[len(chain)-1]["error"]
|
||
|
||
logEntry := log.WithFields(log.Fields{
|
||
"id": appErr.ID,
|
||
"code": appErr.Code,
|
||
"statusCode": appErr.StatusCode,
|
||
"file": appErr.file,
|
||
"sql": appErr.sql,
|
||
"params": appErr.params,
|
||
"error_chain": chain,
|
||
"root_error": root,
|
||
})
|
||
|
||
if appErr.originalErr != nil {
|
||
logEntry = logEntry.WithError(appErr.originalErr)
|
||
}
|
||
|
||
// 根据错误级别记录不同级别的日志
|
||
if appErr.StatusCode >= 500 {
|
||
logEntry.Error("系统错误: ", appErr.Message)
|
||
} else if appErr.StatusCode >= 400 {
|
||
logEntry.Warn("客户端错误: ", appErr.Message)
|
||
} else {
|
||
logEntry.Info("应用错误: ", appErr.Message)
|
||
}
|
||
}
|
||
|
||
// sendResponse 发送响应
|
||
func (s *ResponseSender) sendResponse(ctx fiber.Ctx, appErr *AppError) error {
|
||
contentType := utils.ToLower(utils.UnsafeString(ctx.Request().Header.ContentType()))
|
||
contentType = binder.FilterFlags(utils.ParseVendorSpecificContentType(contentType))
|
||
|
||
switch contentType {
|
||
case fiber.MIMETextXML, fiber.MIMEApplicationXML:
|
||
return ctx.Status(appErr.StatusCode).XML(appErr)
|
||
case fiber.MIMETextHTML, fiber.MIMETextPlain:
|
||
return ctx.Status(appErr.StatusCode).SendString(appErr.Message)
|
||
default:
|
||
return ctx.Status(appErr.StatusCode).JSON(appErr)
|
||
}
|
||
}
|