164 lines
4.0 KiB
Go
164 lines
4.0 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"
|
||
|
||
"quyun/v2/database/models"
|
||
"quyun/v2/pkg/consts"
|
||
)
|
||
|
||
// 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, ctx)
|
||
|
||
// 根据 Content-Type 返回不同格式
|
||
return s.sendResponse(ctx, appErr)
|
||
}
|
||
|
||
// logError 记录错误日志
|
||
func (s *ResponseSender) logError(appErr *AppError, ctx fiber.Ctx) {
|
||
// 确保每个错误实例都有唯一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"]
|
||
|
||
requestID := ctx.Get(fiber.HeaderXRequestID)
|
||
path := ctx.Path()
|
||
method := ctx.Method()
|
||
ua := ctx.Get(fiber.HeaderUserAgent)
|
||
remoteIP := ctx.IP()
|
||
query := string(ctx.Request().URI().QueryString())
|
||
fullPath := path
|
||
if query != "" {
|
||
fullPath = fmt.Sprintf("%s?%s", path, query)
|
||
}
|
||
|
||
fields := 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,
|
||
"request_id": requestID,
|
||
"method": method,
|
||
"path": path,
|
||
"full_path": fullPath,
|
||
"user_agent": ua,
|
||
"ip": remoteIP,
|
||
}
|
||
|
||
if user := ctx.Locals(consts.CtxKeyUser); user != nil {
|
||
if model, ok := user.(*models.User); ok {
|
||
fields["user_id"] = model.ID
|
||
fields["user_roles"] = model.Roles
|
||
}
|
||
}
|
||
|
||
if tenant := ctx.Locals(consts.CtxKeyTenant); tenant != nil {
|
||
if model, ok := tenant.(*models.Tenant); ok {
|
||
fields["tenant_id"] = model.ID
|
||
fields["tenant_code"] = model.Code
|
||
}
|
||
}
|
||
|
||
logEntry := log.WithFields(fields)
|
||
|
||
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)
|
||
}
|
||
}
|