feat: 添加应用错误处理结构及预定义错误码
This commit is contained in:
65
templates/project/app/errorx/app_error.go.tpl
Normal file
65
templates/project/app/errorx/app_error.go.tpl
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
91
templates/project/app/errorx/codes.go.tpl
Normal file
91
templates/project/app/errorx/codes.go.tpl
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package errorx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
"github.com/gofiber/fiber/v3/binder"
|
|
||||||
"github.com/gofiber/utils/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Middleware(c fiber.Ctx) error {
|
|
||||||
err := c.Next()
|
|
||||||
if err != nil {
|
|
||||||
return Wrap(err).Response(c)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
isFormat bool
|
|
||||||
err error
|
|
||||||
params []any
|
|
||||||
sql string
|
|
||||||
file string
|
|
||||||
|
|
||||||
StatusCode int `json:"-" xml:"-"`
|
|
||||||
Code int `json:"code" xml:"code"`
|
|
||||||
Message string `json:"message" xml:"message"`
|
|
||||||
Data any `json:"data,omitempty" xml:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(code, statusCode int, message string) *Response {
|
|
||||||
return &Response{
|
|
||||||
isFormat: true,
|
|
||||||
StatusCode: statusCode,
|
|
||||||
Code: code,
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) WithMsg(msg string) *Response {
|
|
||||||
r.Message = msg
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) Sql(sql string) *Response {
|
|
||||||
r.sql = sql
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) from(err *Response) *Response {
|
|
||||||
r.Code = err.Code
|
|
||||||
r.Message = err.Message
|
|
||||||
r.StatusCode = err.StatusCode
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) Params(params ...any) *Response {
|
|
||||||
r.params = params
|
|
||||||
if _, file, line, ok := runtime.Caller(1); ok {
|
|
||||||
r.file = fmt.Sprintf("%s:%d", file, line)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func Wrap(err error) *Response {
|
|
||||||
if e, ok := err.(*Response); ok {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
return &Response{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) Wrap(err error) *Response {
|
|
||||||
r.err = err
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) format() {
|
|
||||||
r.isFormat = true
|
|
||||||
if e, ok := r.err.(*fiber.Error); ok {
|
|
||||||
r.Code = e.Code
|
|
||||||
r.Message = e.Message
|
|
||||||
r.StatusCode = e.Code
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.err != nil {
|
|
||||||
msg := r.err.Error()
|
|
||||||
if errors.Is(r.err, gorm.ErrRecordNotFound) {
|
|
||||||
r.from(RecordNotExists)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(r.err, gorm.ErrDuplicatedKey) {
|
|
||||||
r.from(RecordDuplicated)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Code = http.StatusInternalServerError
|
|
||||||
r.StatusCode = http.StatusInternalServerError
|
|
||||||
r.Message = msg
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) Error() string {
|
|
||||||
if !r.isFormat {
|
|
||||||
r.format()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[%d] %s", r.Code, r.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) Response(ctx fiber.Ctx) error {
|
|
||||||
if !r.isFormat {
|
|
||||||
r.format()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := utils.ToLower(utils.UnsafeString(ctx.Request().Header.ContentType()))
|
|
||||||
contentType = binder.FilterFlags(utils.ParseVendorSpecificContentType(contentType))
|
|
||||||
|
|
||||||
log.
|
|
||||||
WithError(r.err).
|
|
||||||
WithField("file", r.file).
|
|
||||||
WithField("sql", r.sql).
|
|
||||||
WithField("params", r.params).
|
|
||||||
Errorf("response error: %+v", r)
|
|
||||||
|
|
||||||
// Parse body accordingly
|
|
||||||
switch contentType {
|
|
||||||
case fiber.MIMETextXML, fiber.MIMEApplicationXML:
|
|
||||||
return ctx.Status(r.StatusCode).XML(r)
|
|
||||||
case fiber.MIMETextHTML, fiber.MIMETextPlain:
|
|
||||||
return ctx.Status(r.StatusCode).SendString(r.Message)
|
|
||||||
default:
|
|
||||||
return ctx.Status(r.StatusCode).JSON(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
RecordDuplicated = New(1001, http.StatusBadRequest, "记录重复")
|
|
||||||
RecordNotExists = New(http.StatusNotFound, http.StatusNotFound, "记录不存在")
|
|
||||||
BadRequest = New(http.StatusBadRequest, http.StatusBadRequest, "请求错误")
|
|
||||||
Unauthorized = New(http.StatusUnauthorized, http.StatusUnauthorized, "未授权")
|
|
||||||
InternalErr = New(http.StatusInternalServerError, http.StatusInternalServerError, "内部错误")
|
|
||||||
)
|
|
||||||
106
templates/project/app/errorx/handler.go.tpl
Normal file
106
templates/project/app/errorx/handler.go.tpl
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
38
templates/project/app/errorx/middleware.go.tpl
Normal file
38
templates/project/app/errorx/middleware.go.tpl
Normal 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)
|
||||||
|
}
|
||||||
106
templates/project/app/errorx/predefined.go.tpl
Normal file
106
templates/project/app/errorx/predefined.go.tpl
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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, "安全违规")
|
||||||
|
)
|
||||||
|
|
||||||
127
templates/project/app/errorx/response.go.tpl
Normal file
127
templates/project/app/errorx/response.go.tpl
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,6 @@ func Serve(cmd *cobra.Command, args []string) error {
|
|||||||
route.Register(group)
|
route.Register(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc.Http.Serve()
|
return svc.Http.Serve(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -12,8 +13,14 @@ import (
|
|||||||
"go.ipao.vip/atom/opt"
|
"go.ipao.vip/atom/opt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/helmet"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/limiter"
|
||||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||||
"github.com/gofiber/fiber/v3/middleware/recover"
|
"github.com/gofiber/fiber/v3/middleware/recover"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/requestid"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultProvider() container.ProviderContainer {
|
func DefaultProvider() container.ProviderContainer {
|
||||||
@@ -44,7 +51,7 @@ func (svc *Service) listenerConfig() fiber.ListenConfig {
|
|||||||
listenConfig.CertKeyFile = svc.conf.Tls.Key
|
listenConfig.CertKeyFile = svc.conf.Tls.Key
|
||||||
}
|
}
|
||||||
container.AddCloseAble(func() {
|
container.AddCloseAble(func() {
|
||||||
svc.Engine.Shutdown()
|
svc.Engine.ShutdownWithTimeout(time.Second * 10)
|
||||||
})
|
})
|
||||||
return listenConfig
|
return listenConfig
|
||||||
}
|
}
|
||||||
@@ -53,8 +60,29 @@ func (svc *Service) Listener(ln net.Listener) error {
|
|||||||
return svc.Engine.Listener(ln, svc.listenerConfig())
|
return svc.Engine.Listener(ln, svc.listenerConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) Serve() error {
|
func (svc *Service) Serve(ctx context.Context) error {
|
||||||
return svc.Engine.Listen(svc.conf.Address(), svc.listenerConfig())
|
ln, err := net.Listen("tcp", svc.conf.Address())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the server in a goroutine so we can listen for context cancellation
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
serverErr <- svc.Engine.Listener(ln, svc.listenerConfig())
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Shutdown the server gracefully
|
||||||
|
if shutdownErr := svc.Engine.Shutdown(); shutdownErr != nil {
|
||||||
|
return shutdownErr
|
||||||
|
}
|
||||||
|
// treat context cancellation as graceful shutdown
|
||||||
|
return nil
|
||||||
|
case err := <-serverErr:
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Provide(opts ...opt.Option) error {
|
func Provide(opts ...opt.Option) error {
|
||||||
@@ -66,25 +94,58 @@ func Provide(opts ...opt.Option) error {
|
|||||||
|
|
||||||
return container.Container.Provide(func() (*Service, error) {
|
return container.Container.Provide(func() (*Service, error) {
|
||||||
engine := fiber.New(fiber.Config{
|
engine := fiber.New(fiber.Config{
|
||||||
StrictRouting: true,
|
StrictRouting: true,
|
||||||
|
CaseSensitive: true,
|
||||||
|
BodyLimit: 10 * 1024 * 1024, // 10 MiB
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
ProxyHeader: fiber.HeaderXForwardedFor,
|
||||||
|
EnableIPValidation: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// request id first for correlation
|
||||||
|
engine.Use(requestid.New())
|
||||||
|
|
||||||
|
// recover with stack + request id
|
||||||
engine.Use(recover.New(recover.Config{
|
engine.Use(recover.New(recover.Config{
|
||||||
EnableStackTrace: true,
|
EnableStackTrace: true,
|
||||||
StackTraceHandler: func(c fiber.Ctx, e any) {
|
StackTraceHandler: func(c fiber.Ctx, e any) {
|
||||||
log.Error(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack()))
|
rid := c.Get(fiber.HeaderXRequestID)
|
||||||
|
log.WithField("request_id", rid).Error(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack()))
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if config.StaticRoute != nil && config.StaticPath != nil {
|
// basic security + compression
|
||||||
engine.Use(config.StaticRoute, config.StaticPath)
|
engine.Use(helmet.New())
|
||||||
|
engine.Use(compress.New(compress.Config{Level: compress.LevelDefault}))
|
||||||
|
|
||||||
|
// optional CORS based on config
|
||||||
|
if config.Cors != nil {
|
||||||
|
corsCfg := buildCORSConfig(config.Cors)
|
||||||
|
if corsCfg != nil {
|
||||||
|
engine.Use(cors.New(*corsCfg))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logging with request id and latency
|
||||||
engine.Use(logger.New(logger.Config{
|
engine.Use(logger.New(logger.Config{
|
||||||
Format: `[${ip}:${port}] - [${time}] - ${method} - ${status} - ${path} ${latency} "${ua}"` + "\n",
|
// requestid middleware stores ctx.Locals("requestid")
|
||||||
TimeFormat: time.RFC1123,
|
Format: `${time} [${ip}] ${method} ${status} ${path} ${latency} rid=${locals:requestid} "${ua}"\n`,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
TimeZone: "Asia/Shanghai",
|
TimeZone: "Asia/Shanghai",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// rate limit (enable standard headers; adjust Max via config if needed)
|
||||||
|
engine.Use(limiter.New(limiter.Config{Max: 0}))
|
||||||
|
|
||||||
|
// static files (Fiber v3 Static helper moved; enable via filesystem middleware later)
|
||||||
|
// if config.StaticRoute != nil && config.StaticPath != nil { ... }
|
||||||
|
|
||||||
|
// health endpoints
|
||||||
|
engine.Get("/healthz", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) })
|
||||||
|
engine.Get("/readyz", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) })
|
||||||
|
|
||||||
engine.Hooks().OnPostShutdown(func(err error) error {
|
engine.Hooks().OnPostShutdown(func(err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("http server shutdown error: ", err)
|
log.Error("http server shutdown error: ", err)
|
||||||
@@ -99,3 +160,44 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}, nil
|
}, nil
|
||||||
}, o.DiOptions()...)
|
}, o.DiOptions()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildCORSConfig converts provider Cors config into fiber cors.Config
|
||||||
|
func buildCORSConfig(c *Cors) *cors.Config {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Mode == "disabled" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
origins []string
|
||||||
|
headers []string
|
||||||
|
methods []string
|
||||||
|
exposes []string
|
||||||
|
allowCreds bool
|
||||||
|
)
|
||||||
|
for _, w := range c.Whitelist {
|
||||||
|
if w.AllowOrigin != "" {
|
||||||
|
origins = append(origins, w.AllowOrigin)
|
||||||
|
}
|
||||||
|
if w.AllowHeaders != "" {
|
||||||
|
headers = append(headers, w.AllowHeaders)
|
||||||
|
}
|
||||||
|
if w.AllowMethods != "" {
|
||||||
|
methods = append(methods, w.AllowMethods)
|
||||||
|
}
|
||||||
|
if w.ExposeHeaders != "" {
|
||||||
|
exposes = append(exposes, w.ExposeHeaders)
|
||||||
|
}
|
||||||
|
allowCreds = allowCreds || w.AllowCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := cors.Config{
|
||||||
|
AllowOrigins: lo.Uniq(origins),
|
||||||
|
AllowHeaders: lo.Uniq(headers),
|
||||||
|
AllowMethods: lo.Uniq(methods),
|
||||||
|
ExposeHeaders: lo.Uniq(exposes),
|
||||||
|
AllowCredentials: allowCreds,
|
||||||
|
}
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user