234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
|
|
package errs
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"net/http"
|
|||
|
|
|
|||
|
|
"backend/pkg/library/errors/code"
|
|||
|
|
"google.golang.org/grpc/codes"
|
|||
|
|
"google.golang.org/grpc/status"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Scope is a global variable that should be set by the service or module.
|
|||
|
|
var Scope = code.Unset
|
|||
|
|
|
|||
|
|
// Error represents a structured error with an 8-digit code.
|
|||
|
|
// The code is composed of a 2-digit scope, a 3-digit category, and a 3-digit detail.
|
|||
|
|
// Format: SSCCCDDD
|
|||
|
|
type Error struct {
|
|||
|
|
scope uint32 // 2-digit service scope
|
|||
|
|
category uint32 // 3-digit category
|
|||
|
|
detail uint32 // 3-digit detail
|
|||
|
|
msg string // Display message for the client
|
|||
|
|
internalErr error // The actual underlying error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New creates a new Error.
|
|||
|
|
// It ensures that category is within 0-999 and detail is within 0-999.
|
|||
|
|
func New(scope, category, detail uint32, displayMsg string) *Error {
|
|||
|
|
if category > uint32(code.MaxCategory) {
|
|||
|
|
category = uint32(code.ReservedMaxCategory)
|
|||
|
|
}
|
|||
|
|
if detail > uint32(code.MaxDetail) {
|
|||
|
|
detail = uint32(code.ReservedMaxDetail)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &Error{
|
|||
|
|
scope: scope,
|
|||
|
|
category: category,
|
|||
|
|
detail: detail,
|
|||
|
|
msg: displayMsg,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Error returns the display message. This is intended for the client.
|
|||
|
|
// For internal logging and debugging, use Unwrap() to get the underlying error.
|
|||
|
|
func (e *Error) Error() string {
|
|||
|
|
if e == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.msg
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Scope returns the 2-digit scope of the error.
|
|||
|
|
func (e *Error) Scope() uint32 {
|
|||
|
|
if e == nil {
|
|||
|
|
return uint32(code.Unset)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.scope
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Category returns the 3-digit category of the error.
|
|||
|
|
func (e *Error) Category() uint32 {
|
|||
|
|
if e == nil {
|
|||
|
|
return uint32(code.DefaultCategory)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.category
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Detail returns the 2-digit detail code of the error.
|
|||
|
|
func (e *Error) Detail() uint32 {
|
|||
|
|
if e == nil {
|
|||
|
|
return uint32(code.DefaultDetail)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.detail
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SubCode returns the 6-digit code (category + detail).
|
|||
|
|
func (e *Error) SubCode() uint32 {
|
|||
|
|
if e == nil {
|
|||
|
|
return code.OK
|
|||
|
|
}
|
|||
|
|
c := e.category*code.CategoryMultiplier + e.detail
|
|||
|
|
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Code returns the full 8-digit error code (scope + category + detail).
|
|||
|
|
func (e *Error) Code() uint32 {
|
|||
|
|
if e == nil {
|
|||
|
|
return code.NonCode
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.Scope()*code.ScopeMultiplier + e.SubCode()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DisplayCode returns the 8-digit error code as a zero-padded string.
|
|||
|
|
func (e *Error) DisplayCode() string {
|
|||
|
|
if e == nil {
|
|||
|
|
return "00000000"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fmt.Sprintf("%08d", e.Code())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Is checks if the target error is of type *Error and has the same sub-code.
|
|||
|
|
// It is called by errors.Is(). Do not use it directly.
|
|||
|
|
func (e *Error) Is(target error) bool {
|
|||
|
|
var err *Error
|
|||
|
|
if !errors.As(target, &err) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.SubCode() == err.SubCode()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Unwrap returns the underlying wrapped error.
|
|||
|
|
func (e *Error) Unwrap() error {
|
|||
|
|
if e == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e.internalErr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wrap sets the internal error for the current error.
|
|||
|
|
func (e *Error) Wrap(internalErr error) *Error {
|
|||
|
|
if e != nil {
|
|||
|
|
e.internalErr = internalErr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return e
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GRPCStatus converts the error to a gRPC status.
|
|||
|
|
func (e *Error) GRPCStatus() *status.Status {
|
|||
|
|
if e == nil {
|
|||
|
|
return status.New(codes.OK, "")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return status.New(codes.Code(e.Code()), e.Error())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HTTPStatus returns the corresponding HTTP status code for the error.
|
|||
|
|
func (e *Error) HTTPStatus() int {
|
|||
|
|
if e == nil || e.SubCode() == code.OK {
|
|||
|
|
return http.StatusOK
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch e.Category() {
|
|||
|
|
// Input
|
|||
|
|
case uint32(code.InputInvalidFormat):
|
|||
|
|
return http.StatusBadRequest // 400:輸入格式錯
|
|||
|
|
case uint32(code.InputNotValidImplementation),
|
|||
|
|
uint32(code.InputInvalidRange):
|
|||
|
|
return http.StatusUnprocessableEntity // 422:語意正確但無法處理(範圍/實作)
|
|||
|
|
|
|||
|
|
// DB
|
|||
|
|
case uint32(code.DBError):
|
|||
|
|
return http.StatusInternalServerError // 500:後端暫時性故障(若你偏好 503 可自行調整)
|
|||
|
|
case uint32(code.DBDataConvert):
|
|||
|
|
return http.StatusUnprocessableEntity // 422:可修正的資料轉換失敗
|
|||
|
|
case uint32(code.DBDuplicate):
|
|||
|
|
return http.StatusConflict // 409:唯一鍵/重複
|
|||
|
|
|
|||
|
|
// Resource
|
|||
|
|
case uint32(code.ResNotFound):
|
|||
|
|
return http.StatusNotFound // 404:資源不存在
|
|||
|
|
case uint32(code.ResInvalidFormat):
|
|||
|
|
return http.StatusUnprocessableEntity // 422:資源表示/格式不符
|
|||
|
|
case uint32(code.ResAlreadyExist):
|
|||
|
|
return http.StatusConflict // 409:已存在
|
|||
|
|
case uint32(code.ResInsufficient):
|
|||
|
|
return http.StatusBadRequest // 400:數量/容量/條件不足(可由客戶端修正)
|
|||
|
|
case uint32(code.ResInsufficientPerm):
|
|||
|
|
return http.StatusForbidden // 403:資源層面的權限不足
|
|||
|
|
case uint32(code.ResInvalidMeasureID):
|
|||
|
|
return http.StatusBadRequest // 400:ID 無效
|
|||
|
|
case uint32(code.ResExpired):
|
|||
|
|
return http.StatusGone // 410:資源已過期/不可用
|
|||
|
|
case uint32(code.ResMigrated):
|
|||
|
|
return http.StatusGone // 410:已遷移(若需導引可由上層加 Location)
|
|||
|
|
case uint32(code.ResInvalidState):
|
|||
|
|
return http.StatusConflict // 409:目前狀態不允許此操作
|
|||
|
|
case uint32(code.ResInsufficientQuota):
|
|||
|
|
return http.StatusTooManyRequests // 429:配額不足/達上限
|
|||
|
|
case uint32(code.ResMultiOwner):
|
|||
|
|
return http.StatusConflict // 409:多所有者衝突
|
|||
|
|
|
|||
|
|
// Auth
|
|||
|
|
case uint32(code.AuthUnauthorized),
|
|||
|
|
uint32(code.AuthExpired),
|
|||
|
|
uint32(code.AuthInvalidPosixTime),
|
|||
|
|
uint32(code.AuthSigPayloadMismatch):
|
|||
|
|
return http.StatusUnauthorized // 401:未驗證/無效憑證
|
|||
|
|
case uint32(code.AuthForbidden):
|
|||
|
|
return http.StatusForbidden // 403:有身分但沒權限
|
|||
|
|
|
|||
|
|
// System
|
|||
|
|
case uint32(code.SysTooManyRequest):
|
|||
|
|
return http.StatusTooManyRequests // 429:節流
|
|||
|
|
case uint32(code.SysInternal):
|
|||
|
|
return http.StatusInternalServerError // 500:系統內部錯
|
|||
|
|
case uint32(code.SysMaintain):
|
|||
|
|
return http.StatusServiceUnavailable // 503:維護中
|
|||
|
|
case uint32(code.SysTimeout):
|
|||
|
|
return http.StatusGatewayTimeout // 504:處理/下游逾時
|
|||
|
|
|
|||
|
|
// PubSub
|
|||
|
|
case uint32(code.PSuPublish),
|
|||
|
|
uint32(code.PSuConsume):
|
|||
|
|
return http.StatusBadGateway // 502:訊息中介/外部匯流排失敗
|
|||
|
|
case uint32(code.PSuTooLarge):
|
|||
|
|
return http.StatusRequestEntityTooLarge // 413:訊息太大
|
|||
|
|
|
|||
|
|
// Service
|
|||
|
|
case uint32(code.SvcMaintenance):
|
|||
|
|
return http.StatusServiceUnavailable // 503:服務維護
|
|||
|
|
case uint32(code.SvcInternal):
|
|||
|
|
return http.StatusInternalServerError // 500:服務內部錯
|
|||
|
|
case uint32(code.SvcThirdParty):
|
|||
|
|
return http.StatusBadGateway // 502:第三方依賴失敗
|
|||
|
|
case uint32(code.SvcHTTP400):
|
|||
|
|
return http.StatusBadRequest // 400:明確指派 400
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fallback
|
|||
|
|
return http.StatusInternalServerError
|
|||
|
|
}
|