feat: add backend_v1 migration
Some checks failed
build quyun / Build (push) Has been cancelled

This commit is contained in:
2025-12-19 14:46:58 +08:00
parent 218eb4689c
commit 24bd161df9
119 changed files with 12259 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
package errorx
import (
"fmt"
"runtime"
)
// AppError 应用错误结构
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
StatusCode int `json:"-"`
Data any `json:"data,omitempty"`
ID string `json:"id,omitempty"`
// 调试信息
originalErr error
file string
params []any
sql string
}
// Error 实现 error 接口
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Unwrap 允许通过 errors.Unwrap 遍历到原始错误
func (e *AppError) Unwrap() error { return e.originalErr }
// WithData 添加数据
func (e *AppError) WithData(data any) *AppError {
e.Data = data
return e
}
// WithMsg 设置消息
func (e *AppError) WithMsg(msg string) *AppError {
e.Message = msg
return e
}
// WithSQL 记录SQL信息
func (e *AppError) WithSQL(sql string) *AppError {
e.sql = sql
return e
}
// WithParams 记录参数信息,并自动获取调用位置
func (e *AppError) WithParams(params ...any) *AppError {
e.params = params
if _, file, line, ok := runtime.Caller(1); ok {
e.file = fmt.Sprintf("%s:%d", file, line)
}
return e
}
// NewError 创建应用错误
func NewError(code ErrorCode, statusCode int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
StatusCode: statusCode,
}
}

View File

@@ -0,0 +1,90 @@
package errorx
// ErrorCode 错误码类型
type ErrorCode int
const (
// 1000-1099: 数据相关错误
CodeRecordNotFound ErrorCode = 1001
CodeRecordDuplicated ErrorCode = 1002
CodeDataCorrupted ErrorCode = 1003
CodeDataTooLarge ErrorCode = 1004
CodeDataValidationFail ErrorCode = 1005
CodeConstraintViolated ErrorCode = 1006
CodeDataExpired ErrorCode = 1007
CodeDataLocked ErrorCode = 1008
// 1100-1199: 请求相关错误
CodeBadRequest ErrorCode = 1101
CodeMissingParameter ErrorCode = 1102
CodeInvalidParameter ErrorCode = 1103
CodeParameterTooLong ErrorCode = 1104
CodeParameterTooShort ErrorCode = 1105
CodeInvalidFormat ErrorCode = 1106
CodeUnsupportedMethod ErrorCode = 1107
CodeRequestTooLarge ErrorCode = 1108
CodeInvalidJSON ErrorCode = 1109
CodeInvalidXML ErrorCode = 1110
// 1200-1299: 认证授权错误
CodeUnauthorized ErrorCode = 1201
CodeForbidden ErrorCode = 1202
CodeTokenExpired ErrorCode = 1203
CodeTokenInvalid ErrorCode = 1204
CodeTokenMissing ErrorCode = 1205
CodePermissionDenied ErrorCode = 1206
CodeAccountDisabled ErrorCode = 1207
CodeAccountLocked ErrorCode = 1208
CodeInvalidCredentials ErrorCode = 1209
CodeSessionExpired ErrorCode = 1210
// 1300-1399: 业务逻辑错误
CodeBusinessLogic ErrorCode = 1301
CodeWorkflowError ErrorCode = 1302
CodeStatusConflict ErrorCode = 1303
CodeOperationFailed ErrorCode = 1304
CodeResourceConflict ErrorCode = 1305
CodePreconditionFailed ErrorCode = 1306
CodeQuotaExceeded ErrorCode = 1307
CodeResourceExhausted ErrorCode = 1308
// 1400-1499: 外部服务错误
CodeExternalService ErrorCode = 1401
CodeServiceUnavailable ErrorCode = 1402
CodeServiceTimeout ErrorCode = 1403
CodeThirdPartyError ErrorCode = 1404
CodeNetworkError ErrorCode = 1405
CodeDatabaseError ErrorCode = 1406
CodeCacheError ErrorCode = 1407
CodeMessageQueueError ErrorCode = 1408
// 1500-1599: 系统错误
CodeInternalError ErrorCode = 1501
CodeConfigurationError ErrorCode = 1502
CodeFileSystemError ErrorCode = 1503
CodeMemoryError ErrorCode = 1504
CodeConcurrencyError ErrorCode = 1505
CodeDeadlockError ErrorCode = 1506
// 1600-1699: 限流和频率控制
CodeRateLimitExceeded ErrorCode = 1601
CodeTooManyRequests ErrorCode = 1602
CodeConcurrentLimit ErrorCode = 1603
CodeAPIQuotaExceeded ErrorCode = 1604
// 1700-1799: 文件和上传错误
CodeFileNotFound ErrorCode = 1701
CodeFileTooBig ErrorCode = 1702
CodeInvalidFileType ErrorCode = 1703
CodeFileCorrupted ErrorCode = 1704
CodeUploadFailed ErrorCode = 1705
CodeDownloadFailed ErrorCode = 1706
CodeFilePermission ErrorCode = 1707
// 1800-1899: 加密和安全错误
CodeEncryptionError ErrorCode = 1801
CodeDecryptionError ErrorCode = 1802
CodeSignatureInvalid ErrorCode = 1803
CodeCertificateInvalid ErrorCode = 1804
CodeSecurityViolation ErrorCode = 1805
)

View File

@@ -0,0 +1,105 @@
package errorx
import (
"errors"
"net/http"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
// ErrorHandler 错误处理器
type ErrorHandler struct{}
// NewErrorHandler 创建错误处理器
func NewErrorHandler() *ErrorHandler {
return &ErrorHandler{}
}
// Handle 处理错误并返回统一格式
func (h *ErrorHandler) Handle(err error) *AppError {
if appErr, ok := err.(*AppError); ok {
return appErr
}
// 处理 Fiber 错误
if fiberErr, ok := err.(*fiber.Error); ok {
return h.handleFiberError(fiberErr)
}
// 处理 GORM 错误
if appErr := h.handleGormError(err); appErr != nil {
return appErr
}
// 默认内部错误
return &AppError{
Code: ErrInternalError.Code,
Message: err.Error(),
StatusCode: http.StatusInternalServerError,
originalErr: err,
}
}
// handleFiberError 处理 Fiber 错误
func (h *ErrorHandler) handleFiberError(fiberErr *fiber.Error) *AppError {
var appErr *AppError
switch fiberErr.Code {
case http.StatusBadRequest:
appErr = ErrBadRequest
case http.StatusUnauthorized:
appErr = ErrUnauthorized
case http.StatusForbidden:
appErr = ErrForbidden
case http.StatusNotFound:
appErr = ErrRecordNotFound
case http.StatusMethodNotAllowed:
appErr = ErrUnsupportedMethod
case http.StatusRequestEntityTooLarge:
appErr = ErrRequestTooLarge
case http.StatusTooManyRequests:
appErr = ErrTooManyRequests
default:
appErr = ErrInternalError
}
return &AppError{
Code: appErr.Code,
Message: fiberErr.Message,
StatusCode: fiberErr.Code,
originalErr: fiberErr,
}
}
// handleGormError 处理 GORM 错误
func (h *ErrorHandler) handleGormError(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &AppError{
Code: ErrRecordNotFound.Code,
Message: ErrRecordNotFound.Message,
StatusCode: ErrRecordNotFound.StatusCode,
originalErr: err,
}
}
if errors.Is(err, gorm.ErrDuplicatedKey) {
return &AppError{
Code: ErrRecordDuplicated.Code,
Message: ErrRecordDuplicated.Message,
StatusCode: ErrRecordDuplicated.StatusCode,
originalErr: err,
}
}
if errors.Is(err, gorm.ErrInvalidTransaction) {
return &AppError{
Code: ErrConcurrencyError.Code,
Message: "事务无效",
StatusCode: ErrConcurrencyError.StatusCode,
originalErr: err,
}
}
return nil
}

View File

@@ -0,0 +1,38 @@
package errorx
import "github.com/gofiber/fiber/v3"
// 全局实例
var DefaultSender = NewResponseSender()
// Middleware 错误处理中间件
func Middleware(c fiber.Ctx) error {
err := c.Next()
if err != nil {
return DefaultSender.SendError(c, err)
}
return nil
}
// 便捷函数
func Wrap(err error) *AppError {
if err == nil {
return nil
}
if appErr, ok := err.(*AppError); ok {
return &AppError{
Code: appErr.Code,
Message: appErr.Message,
StatusCode: appErr.StatusCode,
Data: appErr.Data,
originalErr: appErr,
}
}
return DefaultSender.handler.Handle(err)
}
func SendError(ctx fiber.Ctx, err error) error {
return DefaultSender.SendError(ctx, err)
}

View File

@@ -0,0 +1,105 @@
package errorx
import "net/http"
// 预定义错误 - 数据相关
var (
ErrRecordNotFound = NewError(CodeRecordNotFound, http.StatusNotFound, "记录不存在")
ErrRecordDuplicated = NewError(CodeRecordDuplicated, http.StatusConflict, "记录重复")
ErrDataCorrupted = NewError(CodeDataCorrupted, http.StatusBadRequest, "数据损坏")
ErrDataTooLarge = NewError(CodeDataTooLarge, http.StatusRequestEntityTooLarge, "数据过大")
ErrDataValidationFail = NewError(CodeDataValidationFail, http.StatusBadRequest, "数据验证失败")
ErrConstraintViolated = NewError(CodeConstraintViolated, http.StatusConflict, "约束违规")
ErrDataExpired = NewError(CodeDataExpired, http.StatusGone, "数据已过期")
ErrDataLocked = NewError(CodeDataLocked, http.StatusLocked, "数据已锁定")
)
// 预定义错误 - 请求相关
var (
ErrBadRequest = NewError(CodeBadRequest, http.StatusBadRequest, "请求错误")
ErrMissingParameter = NewError(CodeMissingParameter, http.StatusBadRequest, "缺少必需参数")
ErrInvalidParameter = NewError(CodeInvalidParameter, http.StatusBadRequest, "参数无效")
ErrParameterTooLong = NewError(CodeParameterTooLong, http.StatusBadRequest, "参数过长")
ErrParameterTooShort = NewError(CodeParameterTooShort, http.StatusBadRequest, "参数过短")
ErrInvalidFormat = NewError(CodeInvalidFormat, http.StatusBadRequest, "格式无效")
ErrUnsupportedMethod = NewError(CodeUnsupportedMethod, http.StatusMethodNotAllowed, "不支持的请求方法")
ErrRequestTooLarge = NewError(CodeRequestTooLarge, http.StatusRequestEntityTooLarge, "请求体过大")
ErrInvalidJSON = NewError(CodeInvalidJSON, http.StatusBadRequest, "JSON格式错误")
ErrInvalidXML = NewError(CodeInvalidXML, http.StatusBadRequest, "XML格式错误")
)
// 预定义错误 - 认证授权
var (
ErrUnauthorized = NewError(CodeUnauthorized, http.StatusUnauthorized, "未授权")
ErrForbidden = NewError(CodeForbidden, http.StatusForbidden, "禁止访问")
ErrTokenExpired = NewError(CodeTokenExpired, http.StatusUnauthorized, "Token已过期")
ErrTokenInvalid = NewError(CodeTokenInvalid, http.StatusUnauthorized, "Token无效")
ErrTokenMissing = NewError(CodeTokenMissing, http.StatusUnauthorized, "Token缺失")
ErrPermissionDenied = NewError(CodePermissionDenied, http.StatusForbidden, "权限不足")
ErrAccountDisabled = NewError(CodeAccountDisabled, http.StatusForbidden, "账户已禁用")
ErrAccountLocked = NewError(CodeAccountLocked, http.StatusLocked, "账户已锁定")
ErrInvalidCredentials = NewError(CodeInvalidCredentials, http.StatusUnauthorized, "凭据无效")
ErrSessionExpired = NewError(CodeSessionExpired, http.StatusUnauthorized, "会话已过期")
)
// 预定义错误 - 业务逻辑
var (
ErrBusinessLogic = NewError(CodeBusinessLogic, http.StatusBadRequest, "业务逻辑错误")
ErrWorkflowError = NewError(CodeWorkflowError, http.StatusBadRequest, "工作流错误")
ErrStatusConflict = NewError(CodeStatusConflict, http.StatusConflict, "状态冲突")
ErrOperationFailed = NewError(CodeOperationFailed, http.StatusInternalServerError, "操作失败")
ErrResourceConflict = NewError(CodeResourceConflict, http.StatusConflict, "资源冲突")
ErrPreconditionFailed = NewError(CodePreconditionFailed, http.StatusPreconditionFailed, "前置条件失败")
ErrQuotaExceeded = NewError(CodeQuotaExceeded, http.StatusForbidden, "配额超限")
ErrResourceExhausted = NewError(CodeResourceExhausted, http.StatusTooManyRequests, "资源耗尽")
)
// 预定义错误 - 外部服务
var (
ErrExternalService = NewError(CodeExternalService, http.StatusBadGateway, "外部服务错误")
ErrServiceUnavailable = NewError(CodeServiceUnavailable, http.StatusServiceUnavailable, "服务不可用")
ErrServiceTimeout = NewError(CodeServiceTimeout, http.StatusRequestTimeout, "服务超时")
ErrThirdPartyError = NewError(CodeThirdPartyError, http.StatusBadGateway, "第三方服务错误")
ErrNetworkError = NewError(CodeNetworkError, http.StatusBadGateway, "网络错误")
ErrDatabaseError = NewError(CodeDatabaseError, http.StatusInternalServerError, "数据库错误")
ErrCacheError = NewError(CodeCacheError, http.StatusInternalServerError, "缓存错误")
ErrMessageQueueError = NewError(CodeMessageQueueError, http.StatusInternalServerError, "消息队列错误")
)
// 预定义错误 - 系统错误
var (
ErrInternalError = NewError(CodeInternalError, http.StatusInternalServerError, "内部错误")
ErrConfigurationError = NewError(CodeConfigurationError, http.StatusInternalServerError, "配置错误")
ErrFileSystemError = NewError(CodeFileSystemError, http.StatusInternalServerError, "文件系统错误")
ErrMemoryError = NewError(CodeMemoryError, http.StatusInternalServerError, "内存错误")
ErrConcurrencyError = NewError(CodeConcurrencyError, http.StatusInternalServerError, "并发错误")
ErrDeadlockError = NewError(CodeDeadlockError, http.StatusInternalServerError, "死锁错误")
)
// 预定义错误 - 限流
var (
ErrRateLimitExceeded = NewError(CodeRateLimitExceeded, http.StatusTooManyRequests, "请求频率超限")
ErrTooManyRequests = NewError(CodeTooManyRequests, http.StatusTooManyRequests, "请求过多")
ErrConcurrentLimit = NewError(CodeConcurrentLimit, http.StatusTooManyRequests, "并发数超限")
ErrAPIQuotaExceeded = NewError(CodeAPIQuotaExceeded, http.StatusTooManyRequests, "API配额超限")
)
// 预定义错误 - 文件处理
var (
ErrFileNotFound = NewError(CodeFileNotFound, http.StatusNotFound, "文件不存在")
ErrFileTooBig = NewError(CodeFileTooBig, http.StatusRequestEntityTooLarge, "文件过大")
ErrInvalidFileType = NewError(CodeInvalidFileType, http.StatusBadRequest, "文件类型无效")
ErrFileCorrupted = NewError(CodeFileCorrupted, http.StatusBadRequest, "文件损坏")
ErrUploadFailed = NewError(CodeUploadFailed, http.StatusInternalServerError, "上传失败")
ErrDownloadFailed = NewError(CodeDownloadFailed, http.StatusInternalServerError, "下载失败")
ErrFilePermission = NewError(CodeFilePermission, http.StatusForbidden, "文件权限不足")
)
// 预定义错误 - 安全相关
var (
ErrEncryptionError = NewError(CodeEncryptionError, http.StatusInternalServerError, "加密错误")
ErrDecryptionError = NewError(CodeDecryptionError, http.StatusInternalServerError, "解密错误")
ErrSignatureInvalid = NewError(CodeSignatureInvalid, http.StatusUnauthorized, "签名无效")
ErrCertificateInvalid = NewError(CodeCertificateInvalid, http.StatusUnauthorized, "证书无效")
ErrSecurityViolation = NewError(CodeSecurityViolation, http.StatusForbidden, "安全违规")
)

View File

@@ -0,0 +1,127 @@
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)
}
}