From b1a892653219d396bc30079b3069eecb4569d195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Tue, 4 Nov 2025 17:47:36 +0800 Subject: [PATCH] feat: update error --- generate/api/common.api | 13 +- go.mod | 4 - go.sum | 2 - internal/domain/const.go | 5 - internal/handler/auth/login_handler.go | 44 +- .../handler/auth/refresh_token_handler.go | 42 +- internal/handler/auth/register_handler.go | 42 +- .../auth/request_password_reset_handler.go | 42 +- .../handler/auth/reset_password_handler.go | 44 +- .../verify_password_reset_code_handler.go | 42 +- internal/handler/ping/ping_handler.go | 15 +- .../handler/user/get_user_info_handler.go | 42 +- .../user/request_verification_code_handler.go | 22 +- .../user/submit_verification_code_handler.go | 22 +- .../handler/user/update_password_handler.go | 22 +- .../handler/user/update_user_info_handler.go | 22 +- internal/logic/auth/login_logic.go | 9 +- internal/logic/auth/refresh_token_logic.go | 3 +- internal/logic/auth/register_logic.go | 9 +- .../auth/request_password_reset_logic.go | 21 +- internal/logic/auth/reset_password_logic.go | 9 +- .../auth/verify_password_reset_code_logic.go | 4 +- internal/middleware/auth_middleware.go | 31 +- internal/svc/account_model.go | 2 + internal/svc/logs.go | 53 ++ internal/svc/service_context.go | 11 +- internal/svc/token.go | 3 + internal/types/types.go | 18 +- pkg/library/errors/.golangci.yaml | 55 ++ pkg/library/errors/Makefile | 13 + pkg/library/errors/README.md | 186 ++++++ pkg/library/errors/code/types.go | 102 ++++ pkg/library/errors/errors.go | 233 ++++++++ pkg/library/errors/errors_test.go | 215 +++++++ pkg/library/errors/ez_func.go | 467 +++++++++++++++ pkg/library/errors/ez_functions_test.go | 347 +++++++++++ pkg/library/errors/from_errors.go | 73 +++ pkg/library/errors/from_errors_test.go | 115 ++++ pkg/library/errs/code/category.go | 15 - pkg/library/errs/code/code.go | 90 --- pkg/library/errs/code/messsage.go | 13 - pkg/library/errs/code/scope.go | 18 - pkg/library/errs/easy_func.go | 563 ------------------ pkg/library/errs/easy_func_test.go | 185 ------ pkg/library/errs/error_code.go | 89 --- pkg/library/errs/errors.go | 223 ------- pkg/library/errs/errors_test.go | 178 ------ pkg/member/domain/errors.go | 33 - pkg/member/mock/repository/account_uid.go | 15 + pkg/member/repository/account_test.go | 4 +- pkg/member/repository/account_uid_test.go | 4 +- pkg/member/repository/user.go | 6 +- pkg/member/repository/user_test.go | 4 +- pkg/member/usecase/account.go | 20 +- pkg/member/usecase/binding.go | 64 +- pkg/member/usecase/generate.go | 30 +- pkg/member/usecase/member.go | 203 +++---- pkg/member/usecase/member_test.go | 19 +- pkg/member/usecase/verify.go | 51 +- pkg/member/usecase/verify_google.go | 71 +-- pkg/member/usecase/verify_google_test.go | 23 +- pkg/member/usecase/verify_line.go | 65 +- pkg/member/usecase/verify_test.go | 5 +- pkg/notification/domain/error.go | 14 - pkg/notification/repository/aws_ses_mailer.go | 24 +- .../repository/mitake_sms_sender.go | 19 +- pkg/notification/repository/smtp_mailer.go | 21 +- pkg/notification/usecase/delivery.go | 65 +- pkg/notification/usecase/template.go | 46 +- pkg/permission/usecase/token.go | 435 +++++--------- pkg/permission/usecase/token_claims_test.go | 13 +- 71 files changed, 2616 insertions(+), 2416 deletions(-) delete mode 100644 internal/domain/const.go create mode 100644 internal/svc/logs.go create mode 100644 pkg/library/errors/.golangci.yaml create mode 100644 pkg/library/errors/Makefile create mode 100644 pkg/library/errors/README.md create mode 100644 pkg/library/errors/code/types.go create mode 100644 pkg/library/errors/errors.go create mode 100644 pkg/library/errors/errors_test.go create mode 100644 pkg/library/errors/ez_func.go create mode 100644 pkg/library/errors/ez_functions_test.go create mode 100644 pkg/library/errors/from_errors.go create mode 100644 pkg/library/errors/from_errors_test.go delete mode 100644 pkg/library/errs/code/category.go delete mode 100644 pkg/library/errs/code/code.go delete mode 100644 pkg/library/errs/code/messsage.go delete mode 100644 pkg/library/errs/code/scope.go delete mode 100644 pkg/library/errs/easy_func.go delete mode 100644 pkg/library/errs/easy_func_test.go delete mode 100644 pkg/library/errs/error_code.go delete mode 100644 pkg/library/errs/errors.go delete mode 100644 pkg/library/errs/errors_test.go delete mode 100644 pkg/member/domain/errors.go delete mode 100644 pkg/notification/domain/error.go diff --git a/generate/api/common.api b/generate/api/common.api index 0194bc0..80d5cd9 100755 --- a/generate/api/common.api +++ b/generate/api/common.api @@ -13,10 +13,10 @@ type ( } // 錯誤響應 - ErrorResp { - Code int `json:"code"` - Msg string `json:"msg"` - Details string `json:"details,omitempty"` + Resp { + Code string `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 } @@ -25,9 +25,4 @@ type ( Authorization { Authorization string `header:"Authorization" validate:"required"` } - Status { - Code int64 `json:"code"` // 狀態碼 - Message string `json:"message"` // 訊息 - Data interface{} `json:"data,omitempty"` // 可選的資料,當有返回時才出現 - } ) diff --git a/go.mod b/go.mod index a9633b4..367ac84 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module backend go 1.25.1 -replace backend/pkg/library/errs => ./pkg/library/errs - require ( code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 github.com/alicebob/miniredis/v2 v2.35.0 @@ -20,7 +18,6 @@ require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.39.0 github.com/zeromicro/go-zero v1.9.1 - go.mongodb.org/mongo-driver v1.17.4 go.mongodb.org/mongo-driver/v2 v2.3.0 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.42.0 @@ -110,7 +107,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect diff --git a/go.sum b/go.sum index 67f351c..8d47ee5 100644 --- a/go.sum +++ b/go.sum @@ -262,8 +262,6 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeromicro/go-zero v1.9.1 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g= github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY= -go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= -go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU= go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/internal/domain/const.go b/internal/domain/const.go deleted file mode 100644 index 1cebed4..0000000 --- a/internal/domain/const.go +++ /dev/null @@ -1,5 +0,0 @@ -package domain - -const SuccessCode = 10200 -const SuccessMessage = "success" -const DefaultScope = "gateway" diff --git a/internal/handler/auth/login_handler.go b/internal/handler/auth/login_handler.go index bf9f198..ce699e1 100644 --- a/internal/handler/auth/login_handler.go +++ b/internal/handler/auth/login_handler.go @@ -1,11 +1,11 @@ package auth import ( - "backend/internal/domain" "backend/internal/logic/auth" "backend/internal/svc" "backend/internal/types" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "github.com/zeromicro/go-zero/rest/httpx" @@ -16,39 +16,39 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InputInvalidRangeError(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + // Code: e.DisplayCode(), + // Message: err.Error(), + // Error: err, + // }) + // + // return + //} l := auth.NewLoginLogic(r.Context(), svcCtx) resp, err := l.Login(&req) - if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/auth/refresh_token_handler.go b/internal/handler/auth/refresh_token_handler.go index 8758288..a4c2d3b 100644 --- a/internal/handler/auth/refresh_token_handler.go +++ b/internal/handler/auth/refresh_token_handler.go @@ -1,8 +1,8 @@ package auth import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -17,38 +17,38 @@ func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.RefreshTokenReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := auth.NewRefreshTokenLogic(r.Context(), svcCtx) resp, err := l.RefreshToken(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/auth/register_handler.go b/internal/handler/auth/register_handler.go index 7ec6a92..cbd4dc8 100644 --- a/internal/handler/auth/register_handler.go +++ b/internal/handler/auth/register_handler.go @@ -1,8 +1,8 @@ package auth import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -16,38 +16,38 @@ func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := auth.NewRegisterLogic(r.Context(), svcCtx) resp, err := l.Register(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/auth/request_password_reset_handler.go b/internal/handler/auth/request_password_reset_handler.go index 68ee9f0..b7c345d 100644 --- a/internal/handler/auth/request_password_reset_handler.go +++ b/internal/handler/auth/request_password_reset_handler.go @@ -1,8 +1,8 @@ package auth import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -17,38 +17,38 @@ func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.RequestPasswordResetReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx) resp, err := l.RequestPasswordReset(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/auth/reset_password_handler.go b/internal/handler/auth/reset_password_handler.go index 670a788..8049c49 100644 --- a/internal/handler/auth/reset_password_handler.go +++ b/internal/handler/auth/reset_password_handler.go @@ -1,8 +1,8 @@ package auth import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -17,38 +17,38 @@ func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.ResetPasswordReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } - - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } + // + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := auth.NewResetPasswordLogic(r.Context(), svcCtx) resp, err := l.ResetPassword(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/auth/verify_password_reset_code_handler.go b/internal/handler/auth/verify_password_reset_code_handler.go index 8946d36..52f5de9 100644 --- a/internal/handler/auth/verify_password_reset_code_handler.go +++ b/internal/handler/auth/verify_password_reset_code_handler.go @@ -1,8 +1,8 @@ package auth import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -17,38 +17,38 @@ func VerifyPasswordResetCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc return func(w http.ResponseWriter, r *http.Request) { var req types.VerifyCodeReq if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := auth.NewVerifyPasswordResetCodeLogic(r.Context(), svcCtx) resp, err := l.VerifyPasswordResetCode(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/ping/ping_handler.go b/internal/handler/ping/ping_handler.go index b7a26b4..dfd2cb1 100644 --- a/internal/handler/ping/ping_handler.go +++ b/internal/handler/ping/ping_handler.go @@ -1,6 +1,9 @@ package ping import ( + "backend/internal/types" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/ping" @@ -15,9 +18,17 @@ func PingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { l := ping.NewPingLogic(r.Context(), svcCtx) err := l.Ping() if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) } else { - httpx.Ok(w) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, + }) } } } diff --git a/internal/handler/user/get_user_info_handler.go b/internal/handler/user/get_user_info_handler.go index 6dde980..3d904e9 100644 --- a/internal/handler/user/get_user_info_handler.go +++ b/internal/handler/user/get_user_info_handler.go @@ -1,8 +1,8 @@ package user import ( - "backend/internal/domain" - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/user" @@ -16,38 +16,38 @@ func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.Authorization if err := httpx.Parse(r, &req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), Message: err.Error(), }) return } - if err := svcCtx.Validate.ValidateAll(req); err != nil { - e := errs.InvalidFormat(err.Error()) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ - Code: int64(e.FullCode()), - Message: err.Error(), - }) - - return - } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} l := user.NewGetUserInfoLogic(r.Context(), svcCtx) resp, err := l.GetUserInfo(&req) if err != nil { e := errs.FromError(err) - httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ - Code: int(e.FullCode()), - Msg: e.Error(), - Error: e, + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Status{ - Code: domain.SuccessCode, - Message: domain.SuccessMessage, + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, Data: resp, }) } diff --git a/internal/handler/user/request_verification_code_handler.go b/internal/handler/user/request_verification_code_handler.go index a4a4c02..01a2227 100644 --- a/internal/handler/user/request_verification_code_handler.go +++ b/internal/handler/user/request_verification_code_handler.go @@ -1,6 +1,8 @@ package user import ( + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/user" @@ -15,16 +17,30 @@ func RequestVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc return func(w http.ResponseWriter, r *http.Request) { var req types.RequestVerificationCodeReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + return } l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx) resp, err := l.RequestVerificationCode(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, + Data: resp, + }) } } } diff --git a/internal/handler/user/submit_verification_code_handler.go b/internal/handler/user/submit_verification_code_handler.go index 375467c..82af8a4 100644 --- a/internal/handler/user/submit_verification_code_handler.go +++ b/internal/handler/user/submit_verification_code_handler.go @@ -1,6 +1,8 @@ package user import ( + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/user" @@ -15,16 +17,30 @@ func SubmitVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc return func(w http.ResponseWriter, r *http.Request) { var req types.SubmitVerificationCodeReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + return } l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx) resp, err := l.SubmitVerificationCode(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, + Data: resp, + }) } } } diff --git a/internal/handler/user/update_password_handler.go b/internal/handler/user/update_password_handler.go index f827a00..3c5562e 100644 --- a/internal/handler/user/update_password_handler.go +++ b/internal/handler/user/update_password_handler.go @@ -1,6 +1,8 @@ package user import ( + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/user" @@ -15,16 +17,30 @@ func UpdatePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.UpdatePasswordReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + return } l := user.NewUpdatePasswordLogic(r.Context(), svcCtx) resp, err := l.UpdatePassword(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, + Data: resp, + }) } } } diff --git a/internal/handler/user/update_user_info_handler.go b/internal/handler/user/update_user_info_handler.go index 3478d9b..155d309 100644 --- a/internal/handler/user/update_user_info_handler.go +++ b/internal/handler/user/update_user_info_handler.go @@ -1,6 +1,8 @@ package user import ( + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/user" @@ -15,16 +17,30 @@ func UpdateUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.UpdateUserInfoReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + return } l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx) resp, err := l.UpdateUserInfo(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ + Code: code.SUCCESSCode, + Message: code.SUCCESSMessage, + Data: resp, + }) } } } diff --git a/internal/logic/auth/login_logic.go b/internal/logic/auth/login_logic.go index 469c950..c6ea36d 100644 --- a/internal/logic/auth/login_logic.go +++ b/internal/logic/auth/login_logic.go @@ -1,8 +1,7 @@ package auth import ( - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" memberD "backend/pkg/member/domain/member" member "backend/pkg/member/domain/usecase" "context" @@ -41,7 +40,7 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro } if !cr.Status { - return nil, errs.Unauthorized("failed to verify password") + return nil, errs.AuthUnauthorizedError("failed to verify password") } case "platform": switch req.Platform.Provider { @@ -66,10 +65,10 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro } req.LoginID = userInfo.UserID default: - return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "unsupported 3 party platform") + return nil, errs.InputInvalidFormatError("unsupported 3 party platform") } default: - return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "failed to get correct auth method") + return nil, errs.InputInvalidFormatError("failed to get correct auth method") } account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, member.GetUIDByAccountRequest{ diff --git a/internal/logic/auth/refresh_token_logic.go b/internal/logic/auth/refresh_token_logic.go index 84715fd..fed1d15 100644 --- a/internal/logic/auth/refresh_token_logic.go +++ b/internal/logic/auth/refresh_token_logic.go @@ -1,7 +1,6 @@ package auth import ( - "backend/internal/domain" "backend/pkg/permission/domain/entity" "context" "time" @@ -35,7 +34,7 @@ func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *type tk, err := l.svcCtx.TokenUC.RefreshToken(l.ctx, entity.RefreshTokenReq{ Token: req.RefreshToken, - Scope: domain.DefaultScope, + Scope: "gateway", Expires: time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshTokenExpiry).Unix(), DeviceID: data["uid"], }) diff --git a/internal/logic/auth/register_logic.go b/internal/logic/auth/register_logic.go index 94ebca3..61ff21c 100644 --- a/internal/logic/auth/register_logic.go +++ b/internal/logic/auth/register_logic.go @@ -3,8 +3,7 @@ package auth import ( "backend/internal/svc" "backend/internal/types" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" mb "backend/pkg/member/domain/member" member "backend/pkg/member/domain/usecase" "backend/pkg/permission/domain/usecase" @@ -42,7 +41,7 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er case "credentials": fn, ok := PrepareFunc[mb.Digimon.ToString()] if !ok { - return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method") + return nil, errs.InputInvalidRangeError("failed to get correct credentials method") } bd, err = fn(l.ctx, req, l.svcCtx) if err != nil { @@ -51,14 +50,14 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er case "platform": fn, ok := PrepareFunc[req.Platform.Provider] if !ok { - return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method") + return nil, errs.InputInvalidRangeError("failed to get correct credentials method") } bd, err = fn(l.ctx, req, l.svcCtx) if err != nil { return nil, err } default: - return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "failed to get correct auth method") + return nil, errs.InputInvalidFormatError("failed to get correct auth method") } // Step 2: 建立帳號 diff --git a/internal/logic/auth/request_password_reset_logic.go b/internal/logic/auth/request_password_reset_logic.go index 81a2a1e..3b5556f 100644 --- a/internal/logic/auth/request_password_reset_logic.go +++ b/internal/logic/auth/request_password_reset_logic.go @@ -3,8 +3,7 @@ package auth import ( "backend/internal/domain" "backend/internal/utils" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" "context" @@ -61,7 +60,7 @@ func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPassw // 獲取用戶資訊並確認綁定帳號 account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: acc}) if err != nil { - return nil, errs.ResourceNotFoundWithScope(code.CloudEPMember, 0, fmt.Sprintf("account not found:%s", acc)) + return nil, errs.ResNotFoundError(fmt.Sprintf("account not found:%s", acc)) } info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{UID: account.UID}) if err != nil { @@ -88,13 +87,13 @@ func (l *RequestPasswordResetLogic) validateAndNormalizeAccount(accountType, acc case member.AccountTypePhone: phone, isPhone := utils.NormalizeTaiwanMobile(account) if !isPhone { - return "", errs.InvalidFormatWithScope(code.CloudEPMember, "phone number is invalid") + return "", errs.InputInvalidFormatError("phone number is invalid") } return phone, nil case member.AccountTypeMail: if !utils.IsValidEmail(account) { - return "", errs.InvalidFormatWithScope(code.CloudEPMember, "email is invalid") + return "", errs.InputInvalidFormatError("email is invalid") } return account, nil @@ -102,13 +101,13 @@ func (l *RequestPasswordResetLogic) validateAndNormalizeAccount(accountType, acc default: } - return "", errs.InvalidFormatWithScope(code.CloudEPMember, "unsupported account type") + return "", errs.InputInvalidFormatError("unsupported account type") } // checkVerifyCodeCooldown 檢查是否已在限制時間內發送過驗證碼 func (l *RequestPasswordResetLogic) checkVerifyCodeCooldown(rk string) error { if cachedCode, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cachedCode != "" { - return errs.TooManyWithScope(code.CloudEPMember, "verification code already sent, please wait 3min for system to send again") + return errs.SysTooManyRequestError("verification code already sent, please wait 3min for system to send again") } return nil @@ -122,7 +121,7 @@ func (l *RequestPasswordResetLogic) checkAccountAndPlatform(acc string) error { } if accountInfo.Data.Platform != member.Digimon { - return errs.InvalidFormatWithScope(code.CloudEPMember, + return errs.InputInvalidFormatError( "failed to send verify code since platform not correct") } @@ -132,9 +131,9 @@ func (l *RequestPasswordResetLogic) checkAccountAndPlatform(acc string) error { // setRedisKeyWithExpiry 設置 Redis 鍵 func (l *RequestPasswordResetLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) { if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status { - _ = errs.DatabaseErrorWithScopeL(code.CloudEPMember, 0, logx.WithContext(l.ctx), []logx.LogField{ - {Key: "redisKey", Value: rk}, - {Key: "error", Value: err.Error()}, + _ = errs.DBErrorErrorL(l.svcCtx.Logger, []errs.LogField{ + {Key: "redisKey", Val: rk}, + {Key: "error", Val: err.Error()}, }, "failed to set redis expire").Wrap(err) } } diff --git a/internal/logic/auth/reset_password_logic.go b/internal/logic/auth/reset_password_logic.go index 86bb438..9f5adf8 100644 --- a/internal/logic/auth/reset_password_logic.go +++ b/internal/logic/auth/reset_password_logic.go @@ -2,8 +2,7 @@ package auth import ( "backend/internal/domain" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" "backend/pkg/permission/domain/entity" @@ -35,7 +34,7 @@ func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Res func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (*types.RespOK, error) { // 驗證密碼,兩次密碼要一致 if req.Password != req.PasswordConfirm { - return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "password confirmation does not match") + return nil, errs.InputInvalidFormatError("password confirmation does not match") } // 驗證碼 @@ -46,7 +45,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (*types. }) if err != nil { // 表使沒有這驗證碼 - return nil, errs.ForbiddenWithScope(code.CloudEPMember, 0, "failed to get verify code") + return nil, errs.AuthForbiddenError("failed to get verify code") } info, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: req.Identifier}) @@ -55,7 +54,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (*types. } if info.Data.Platform != member.Digimon { - return nil, errs.ForbiddenWithScope(code.CloudEPMember, 0, "invalid platform") + return nil, errs.AuthForbiddenError("invalid platform") } // 更新 diff --git a/internal/logic/auth/verify_password_reset_code_logic.go b/internal/logic/auth/verify_password_reset_code_logic.go index 9980057..fffa501 100644 --- a/internal/logic/auth/verify_password_reset_code_logic.go +++ b/internal/logic/auth/verify_password_reset_code_logic.go @@ -1,7 +1,7 @@ package auth import ( - "backend/pkg/library/errs" + errs "backend/pkg/library/errors" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" "context" @@ -34,7 +34,7 @@ func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.Verify LoginID: req.Identifier, CodeType: member.GenerateCodeTypeForgetPassword, }); err != nil { - e := errs.Forbidden("failed to get verify code").Wrap(err) + e := errs.AuthForbiddenError("failed to get verify code").Wrap(err) return nil, e } diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go index 64157f5..8e8c957 100644 --- a/internal/middleware/auth_middleware.go +++ b/internal/middleware/auth_middleware.go @@ -2,7 +2,6 @@ package middleware import ( "backend/internal/types" - "backend/pkg/library/errs" "backend/pkg/permission/domain/entity" "backend/pkg/permission/domain/token" "context" @@ -35,7 +34,7 @@ func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { // 解析 Header header := types.Authorization{} if err := httpx.ParseHeaders(r, &header); err != nil { - m.writeErrorResponse(w, r, http.StatusBadRequest, "Failed to parse headers", int64(errs.InvalidFormat("").FullCode())) + //m.writeErrorResponse(w, r, http.StatusBadRequest, "Failed to parse headers") return } @@ -43,19 +42,19 @@ func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { // 驗證 Token claim, err := uc.ParseClaims(header.Authorization, m.TokenSec, true) if err != nil { - // 是否需要紀錄錯誤,是不是只要紀錄除了驗證失敗或過期之外的真錯誤 - m.writeErrorResponse(w, r, - http.StatusUnauthorized, "failed to verify toke", - int64(100400)) + //// 是否需要紀錄錯誤,是不是只要紀錄除了驗證失敗或過期之外的真錯誤 + //m.writeErrorResponse(w, r, + // http.StatusUnauthorized, "failed to verify toke", + // int64(100400)) return } // 驗證 Token 是否在黑名單中 if _, err := m.TokenUseCase.ValidationToken(r.Context(), entity.ValidationTokenReq{Token: header.Authorization}); err != nil { - m.writeErrorResponse(w, r, http.StatusForbidden, - "failed to get toke", - int64(100400)) + //m.writeErrorResponse(w, r, http.StatusForbidden, + // "failed to get toke", + // int64(100400)) return } @@ -76,10 +75,10 @@ func SetContext(r *http.Request, claim uc.TokenClaims) context.Context { return ctx } -// writeErrorResponse 用於處理錯誤回應 -func (m *AuthMiddleware) writeErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int, message string, code int64) { - httpx.WriteJsonCtx(r.Context(), w, statusCode, types.ErrorResp{ - Code: int(code), - Msg: message, - }) -} +//// writeErrorResponse 用於處理錯誤回應 +//func (m *AuthMiddleware) writeErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int, message string, code int64) { +// httpx.WriteJsonCtx(r.Context(), w, statusCode, types.Resp{ +// Code: int(code), +// Msg: message, +// }) +//} diff --git a/internal/svc/account_model.go b/internal/svc/account_model.go index 6200bf1..464d3ad 100644 --- a/internal/svc/account_model.go +++ b/internal/svc/account_model.go @@ -8,6 +8,7 @@ import ( "backend/pkg/member/repository" uc "backend/pkg/member/usecase" "context" + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" @@ -79,6 +80,7 @@ func NewAccountUC(c *config.Config, rds *redis.Redis) usecase.AccountUseCase { VerifyCodeModel: repository.NewVerifyCodeRepository(rds), GenerateUID: guid, Config: prepareCfg(c), + Logger: MustLogger(logx.WithContext(context.Background())), }) } diff --git a/internal/svc/logs.go b/internal/svc/logs.go new file mode 100644 index 0000000..0b05ce1 --- /dev/null +++ b/internal/svc/logs.go @@ -0,0 +1,53 @@ +package svc + +import ( + errs "backend/pkg/library/errors" + "context" + "github.com/zeromicro/go-zero/core/logx" +) + +type logger struct { + l logx.Logger +} + +func (lgr *logger) WithCallerSkip(skip int) errs.Logger { + return &logger{ + l: logx.WithContext(context.Background()).WithCallerSkip(skip), + } +} + +func (lgr *logger) WithFields(fields ...errs.LogField) errs.Logger { + return &logger{ + l: logx.WithContext(context.Background()).WithFields(fTof(fields)...), + } +} + +func (lgr *logger) Error(msg string) { + lgr.l.Error(msg) +} + +func (lgr *logger) Info(msg string) { + lgr.l.Info(msg) +} + +func (lgr *logger) Warn(msg string) { + lgr.l.Error(msg) +} + +func MustLogger(log logx.Logger) errs.Logger { + return &logger{ + l: log, + } +} + +func fTof(field []errs.LogField) []logx.LogField { + f := make([]logx.LogField, 0, len(field)+1) + for _, v := range field { + f = append(f, logx.LogField{ + Key: v.Key, + Value: v.Val, + }) + } + + return f +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 3c11f77..a17dc79 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -3,8 +3,11 @@ package svc import ( "backend/internal/config" "backend/internal/middleware" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" + "backend/pkg/library/errors/code" + "context" + "github.com/zeromicro/go-zero/core/logx" + vi "backend/pkg/library/validator" memberUC "backend/pkg/member/domain/usecase" tokenUC "backend/pkg/permission/domain/usecase" @@ -24,6 +27,7 @@ type ServiceContext struct { RolePermission tokenUC.RolePermissionUseCase UserRoleUC tokenUC.UserRoleUseCase Redis *redis.Redis + Logger errs.Logger } func NewServiceContext(c config.Config) *ServiceContext { @@ -31,7 +35,7 @@ func NewServiceContext(c config.Config) *ServiceContext { if err != nil { panic(err) } - errs.Scope = code.CloudEPPortalGW + errs.Scope = code.Gateway rp := NewPermissionUC(&c) tkUC := NewTokenUC(&c, rds) @@ -50,5 +54,6 @@ func NewServiceContext(c config.Config) *ServiceContext { RolePermission: rp.RolePermission, UserRoleUC: rp.UserRole, Redis: rds, + Logger: MustLogger(logx.WithContext(context.Background())), } } diff --git a/internal/svc/token.go b/internal/svc/token.go index b000170..53a8eb2 100644 --- a/internal/svc/token.go +++ b/internal/svc/token.go @@ -6,6 +6,8 @@ import ( "backend/pkg/permission/domain/usecase" "backend/pkg/permission/repository" uc "backend/pkg/permission/usecase" + "context" + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "github.com/zeromicro/go-zero/core/stores/redis" @@ -17,6 +19,7 @@ func NewTokenUC(c *config.Config, rds *redis.Redis) usecase.TokenUseCase { Redis: rds, }), Config: c, + Logger: MustLogger(logx.WithContext(context.Background())), }) } diff --git a/internal/types/types.go b/internal/types/types.go index 5f25016..074da8a 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -16,13 +16,6 @@ type CredentialsPayload struct { AccountType string `json:"account_type" validate:"required,oneof=email phone any"` // 帳號型別 email phone any } -type ErrorResp struct { - Code int `json:"code"` - Msg string `json:"msg"` - Details string `json:"details,omitempty"` - Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 -} - type LoginReq struct { AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` // 驗證類型 credentials platform LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼 @@ -100,13 +93,14 @@ type ResetPasswordReq struct { PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼 } -type RespOK struct { +type Resp struct { + Code string `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 } -type Status struct { - Code int64 `json:"code"` // 狀態碼 - Message string `json:"message"` // 訊息 - Data interface{} `json:"data,omitempty"` // 可選的資料,當有返回時才出現 +type RespOK struct { } type SubmitVerificationCodeReq struct { diff --git a/pkg/library/errors/.golangci.yaml b/pkg/library/errors/.golangci.yaml new file mode 100644 index 0000000..b57947c --- /dev/null +++ b/pkg/library/errors/.golangci.yaml @@ -0,0 +1,55 @@ +run: + timeout: 3m + issues-exit-code: 2 + tests: false # 不檢查測試檔案 + +linters: + enable: + - govet # 官方靜態分析,抓潛在 bug + - staticcheck # 最強 bug/反模式偵測 + - revive # golint 進化版,風格與註解規範 + - gofmt # 風格格式化檢查 + - goimports # import 排序 + - errcheck # error 忽略警告 + - ineffassign # 無效賦值 + - unused # 未使用變數 + - bodyclose # HTTP body close + - gosimple # 靜態分析簡化警告(staticcheck 也包含,可選) + - typecheck # 型別檢查 + - misspell # 拼字檢查 + - gocritic # bug-prone code + - gosec # 資安檢查 + - prealloc # slice/array 預分配 + - unparam # 未使用參數 + +issues: + exclude-rules: + - path: _test\.go + linters: + - funlen + - goconst + - cyclop + - gocognit + - lll + - wrapcheck + - contextcheck + +linters-settings: + revive: + severity: warning + rules: + - name: blank-imports + severity: error + gofmt: + simplify: true + lll: + line-length: 140 + +# 可自訂目錄忽略(視專案需求加上) +# skip-dirs: +# - vendor +# - third_party + +# 可以設定本機與 CI 上都一致 +# env: +# GOLANGCI_LINT_CACHE: ".golangci-lint-cache" diff --git a/pkg/library/errors/Makefile b/pkg/library/errors/Makefile new file mode 100644 index 0000000..36844e3 --- /dev/null +++ b/pkg/library/errors/Makefile @@ -0,0 +1,13 @@ +GOFMT ?= gofmt "-s" +GOFILES := $(shell find . -name "*.go") + + +.PHONY: test +test: # 進行測試 + go test -v --cover ./... + +.PHONY: fmt +fmt: # 格式優化 + $(GOFMT) -w $(GOFILES) + goimports -w ./ + golangci-lint run \ No newline at end of file diff --git a/pkg/library/errors/README.md b/pkg/library/errors/README.md new file mode 100644 index 0000000..cbc459a --- /dev/null +++ b/pkg/library/errors/README.md @@ -0,0 +1,186 @@ +# 錯誤碼 × HTTP 對照表 + +這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照,並提供**實務範例**。 +錯誤系統採用 8 碼格式 `SSCCCDDD`: + +- `SS` = Scope(服務/模組,兩位數) +- `CCC` = Category(類別,三位數,影響 HTTP 狀態) +- `DDD` = Detail(細節,三位數,自定義業務碼) + +> 例如:`10101000` → Scope=10、Category=101(InputInvalidFormat)、Detail=000。 + +## 目錄 +- [1) 快速查表](#1-快速查表依類別整理) +- [2) 使用範例](#2-使用範例) +- [3) 小撇步與慣例](#3-小撇步與慣例) +- [4) 安裝與測試](#4-安裝與測試) +- [5) 變更日誌](#5-變更日誌) + +--- + +## 1) 快速查表(依類別整理) + +### A. Input(Category 1xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|---------------|:----:|---| +| `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 | +| `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 | +| `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 | + +### B. DB(Category 2xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|-------------|:----:|---| +| `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 | +| `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 | +| `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 | + +### C. Resource(Category 3xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|-------------------|:----:|---| +| `ResNotFound` (301) | 資源未找到 | **404 Not Found** | 目標不存在/無此 ID。 | +| `ResInvalidFormat` (302) | 無效資源格式 | **422 Unprocessable Entity** | 表示層/Schema 不符。 | +| `ResAlreadyExist` (303) | 資源已存在 | **409 Conflict** | 重複建立/命名衝突。 | +| `ResInsufficient` (304) | 資源不足 | **400 Bad Request** | 數量/容量不足(用戶可改參數再試)。 | +| `ResInsufficientPerm` (305) | 權限不足 | **403 Forbidden** | 已驗證但無權限。 | +| `ResInvalidMeasureID` (306) | 無效測量ID | **400 Bad Request** | ID 本身不合法。 | +| `ResExpired` (307) | 資源過期 | **410 Gone** | 已不可用(可於上層補 Location)。 | +| `ResMigrated` (308) | 資源已遷移 | **410 Gone** | 同上,如需導引請於上層處理。 | +| `ResInvalidState` (309) | 無效狀態 | **409 Conflict** | 當前狀態不允許此操作。 | +| `ResInsufficientQuota` (310) | 配額不足 | **429 Too Many Requests** | 達配額/速率限制。 | +| `ResMultiOwner` (311) | 多所有者 | **409 Conflict** | 所有權歧異造成衝突。 | + +### D. Auth(Category 5xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|-------------------------|:----:|---| +| `AuthUnauthorized` (501) | 未授權/未驗證 | **401 Unauthorized** | 缺 Token、無效 Token。 | +| `AuthExpired` (502) | 授權過期 | **401 Unauthorized** | Token 過期或時效失效。 | +| `AuthInvalidPosixTime` (503) | 無效 POSIX 時間 | **401 Unauthorized** | 時戳異常導致驗簽失敗。 | +| `AuthSigPayloadMismatch` (504) | 簽名與載荷不符 | **401 Unauthorized** | 驗簽失敗。 | +| `AuthForbidden` (505) | 禁止存取 | **403 Forbidden** | 已驗證但沒有操作權限。 | + +### E. System(Category 6xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|---------------|:----:|---| +| `SysInternal` (601) | 系統內部錯誤 | **500 Internal Server Error** | 未預期的系統錯。 | +| `SysMaintain` (602) | 系統維護中 | **503 Service Unavailable** | 維護/停機。 | +| `SysTimeout` (603) | 系統超時 | **504 Gateway Timeout** | 下游/處理逾時。 | +| `SysTooManyRequest` (604) | 請求過多 | **429 Too Many Requests** | 節流/限流。 | + +### F. PubSub(Category 7xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|---------|:----:|---| +| `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 | +| `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 | +| `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 | + +### G. Service(Category 8xx) + +| Category 常數 | 說明 | HTTP | 原因/說明 | +|---|---------------|:----:|---| +| `SvcInternal` (801) | 服務內部錯誤 | **500 Internal Server Error** | 非基礎設施層的內錯。 | +| `SvcThirdParty` (802) | 第三方失敗 | **502 Bad Gateway** | 呼叫外部服務失敗。 | +| `SvcHTTP400` (803) | 明確指派 400 | **400 Bad Request** | 自行指定。 | +| `SvcMaintenance` (804) | 服務維護中 | **503 Service Unavailable** | 模組級維運中。 | + +--- + +## 2) 使用範例 + +### 2.1 在 Handler 中回傳錯誤 + +```go +import ( + "net/http" + errs "gitlab.supermicro.com/infra/infra-core/errors" + "gitlab.supermicro.com/infra/infra-core/errors/code" +) + +func init() { + errs.Scope = code.Gateway // 設定當前服務的 Scope +} + +func GetUser(w http.ResponseWriter, r *http.Request) error { + id := r.URL.Query().Get("id") + if id == "" { + return errs.InputInvalidFormatError("缺少參數: id") // 現在是 8 位碼 + } + + u, err := repo.Find(r.Context(), id) + switch { + case errors.Is(err, repo.ErrNotFound): + return errs.ResNotFoundError("user", id) + case err != nil: + return errs.DBErrorError("查詢使用者失敗").Wrap(err) // Wrap 內部錯誤 + } + + // … 寫入回應 + return nil +} + +// 統一寫出 HTTP 錯誤 +func writeHTTP(w http.ResponseWriter, e *errs.Error) { + http.Error(w, e.Error(), e.HTTPStatus()) +} +``` + +### 2.2 取出 Wrap 的內部錯誤 + +```go +if internal := e.Unwrap(); internal != nil { + log.Error("Internal error: ", internal) +} +``` + +### 2.3 搭配日誌裝飾器(`WithLog` / `WithLogWrap`) + +```go +log := logger.WithFields(errs.LogField{Key: "req_id", Val: rid}) + +if badInput { + return errs.WithLog(log, nil, errs.InputInvalidFormatError, "email 無效") +} + +if err := repo.Save(ctx, u); err != nil { + return errs.WithLogWrap( + log, + []errs.LogField{{Key: "entity", Val: "user"}, {Key: "op", Val: "save"}}, + errs.DBErrorError, + err, + "儲存失敗", + ) +} +``` + +### 2.4 只知道 Category+Detail 的動態場景(`EL` / `ELWrap`) + +```go +// 依流程動態產生 +return errs.EL(log, nil, code.SysTimeout, 123, "下游逾時") // 自定義 detail=123 + +// 或需保留 cause: +return errs.ELWrap(log, nil, code.SvcThirdParty, 456, err, "金流商失敗") +``` + +### 2.5 gRPC 互通 + +```go +// 由 *errs.Error 轉為 gRPC status +st := e.GRPCStatus() // *status.Status + +// 客戶端收到 gRPC error → 轉回 *errs.Error +e := errs.FromGRPCError(grpcErr) +fmt.Println(e.DisplayCode(), e.Error()) // e.g., "10101000" "error msg" +``` + +### 2.6 從 8 碼反解(`FromCode`) + +```go +e := errs.FromCode(10101000) // 10101000 +fmt.Println(e.Scope(), e.Category(), e.Detail()) // 10, 101, 000 +``` \ No newline at end of file diff --git a/pkg/library/errors/code/types.go b/pkg/library/errors/code/types.go new file mode 100644 index 0000000..0a03b41 --- /dev/null +++ b/pkg/library/errors/code/types.go @@ -0,0 +1,102 @@ +package code + +type Scope uint32 // SS (00..99) +type Category uint32 // CCC (000..999) +type Detail uint32 // DDD (000..999) // Updated to 3 digits + +const ( + Unset Scope = 0 + CategoryMultiplier uint32 = 1000 + ScopeMultiplier uint32 = 1000000 + NonCode uint32 = 0 + OK uint32 = 0 // Already exists, but merged for completeness; avoid duplication if needed + SUCCESSCode = "00000000" + SUCCESSMessage = "success" +) + +// Boundary constants for validation +const ( + MaxCategory Category = 999 // Maximum allowed category value + MaxDetail Detail = 999 // Maximum allowed detail value (updated) + + DefaultCategory Category = 0 + DefaultDetail Detail = 0 + + // Reserved values - DO NOT USE in normal operations + // These are used internally for overflow protection + + ReservedMaxCategory Category = 999 // Used when category > 999 + ReservedMaxDetail Detail = 999 // Used when detail > 999 (updated) +) + +// New 3-digit categories (merged from original category + detail) +// Input errors (100-109) +const ( + InputInvalidFormat Category = 101 + InputNotValidImplementation Category = 102 + InputInvalidRange Category = 103 +) + +// DB errors (200-209) +const ( + DBError Category = 201 + DBDataConvert Category = 202 + DBDuplicate Category = 203 +) + +// Resource errors (300-399) +const ( + ResNotFound Category = 301 + ResInvalidFormat Category = 302 + ResAlreadyExist Category = 303 + ResInsufficient Category = 304 + ResInsufficientPerm Category = 305 + ResInvalidMeasureID Category = 306 + ResExpired Category = 307 + ResMigrated Category = 308 + ResInvalidState Category = 309 + ResInsufficientQuota Category = 310 + ResMultiOwner Category = 311 +) + +// GRPC category + +const ( + CatGRPC Category = 400 +) + +// Auth errors (500-509) +const ( + AuthUnauthorized Category = 501 + AuthExpired Category = 502 + AuthInvalidPosixTime Category = 503 + AuthSigPayloadMismatch Category = 504 + AuthForbidden Category = 505 +) + +// System errors (600-609) +const ( + SysInternal Category = 601 + SysMaintain Category = 602 + SysTimeout Category = 603 + SysTooManyRequest Category = 604 +) + +// PubSub errors (700-709) +const ( + PSuPublish Category = 701 + PSuConsume Category = 702 + PSuTooLarge Category = 703 +) + +// Service errors (800-809) +const ( + SvcInternal Category = 801 + SvcThirdParty Category = 802 + SvcHTTP400 Category = 803 + SvcMaintenance Category = 804 +) + +const ( + Gateway Scope = 10 +) diff --git a/pkg/library/errors/errors.go b/pkg/library/errors/errors.go new file mode 100644 index 0000000..b0babab --- /dev/null +++ b/pkg/library/errors/errors.go @@ -0,0 +1,233 @@ +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 +} diff --git a/pkg/library/errors/errors_test.go b/pkg/library/errors/errors_test.go new file mode 100644 index 0000000..dc07090 --- /dev/null +++ b/pkg/library/errors/errors_test.go @@ -0,0 +1,215 @@ +package errs + +import ( + "errors" + "net/http" + "testing" + + "backend/pkg/library/errors/code" + "google.golang.org/grpc/codes" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + scope uint32 + category uint32 + detail uint32 + displayMsg string + wantScope uint32 + wantCategory uint32 + wantDetail uint32 + wantMsg string + }{ + {"basic", 10, 201, 123, "test", 10, 201, 123, "test"}, + {"clamp category", 10, 1000, 0, "clamp cat", 10, 999, 0, "clamp cat"}, + {"clamp detail", 10, 101, 1000, "clamp det", 10, 101, 999, "clamp det"}, + {"zero values", 0, 0, 0, "", 0, 0, 0, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := New(tt.scope, tt.category, tt.detail, tt.displayMsg) + if e.Scope() != tt.wantScope || e.Category() != tt.wantCategory || e.Detail() != tt.wantDetail || e.msg != tt.wantMsg { + t.Errorf("New() = %+v, want scope=%d cat=%d det=%d msg=%q", e, tt.wantScope, tt.wantCategory, tt.wantDetail, tt.wantMsg) + } + }) + } +} + +func TestErrorMethods(t *testing.T) { + e := New(10, 201, 123, "test error") + tests := []struct { + name string + err *Error + wantErr string + wantScope uint32 + wantCat uint32 + wantDet uint32 + }{ + {"non-nil", e, "test error", 10, 201, 123}, + {"nil", nil, "", uint32(code.Unset), uint32(code.DefaultCategory), uint32(code.DefaultDetail)}, // Adjust if Default* not defined; use 0 + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Error(); got != tt.wantErr { + t.Errorf("Error() = %q, want %q", got, tt.wantErr) + } + if got := tt.err.Scope(); got != tt.wantScope { + t.Errorf("Scope() = %d, want %d", got, tt.wantScope) + } + if got := tt.err.Category(); got != tt.wantCat { + t.Errorf("Category() = %d, want %d", got, tt.wantCat) + } + if got := tt.err.Detail(); got != tt.wantDet { + t.Errorf("Detail() = %d, want %d", got, tt.wantDet) + } + }) + } +} + +func TestCodes(t *testing.T) { + tests := []struct { + name string + err *Error + wantSubCode uint32 + wantCode uint32 + wantDisplay string + }{ + {"basic", New(10, 201, 123, ""), 201123, 10201123, "10201123"}, + {"nil", nil, code.OK, code.NonCode, "00000000"}, + {"max clamp", New(99, 999, 999, ""), 999999, 99999999, "99999999"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.SubCode(); got != tt.wantSubCode { + t.Errorf("SubCode() = %d, want %d", got, tt.wantSubCode) + } + if got := tt.err.Code(); got != tt.wantCode { + t.Errorf("Code() = %d, want %d", got, tt.wantCode) + } + if got := tt.err.DisplayCode(); got != tt.wantDisplay { + t.Errorf("DisplayCode() = %q, want %q", got, tt.wantDisplay) + } + }) + } +} + +func TestIs(t *testing.T) { + e1 := New(10, 201, 123, "") + e2 := New(10, 201, 123, "") // same subcode + e3 := New(10, 202, 123, "") // different category + stdErr := errors.New("std") + tests := []struct { + name string + err error + target error + want bool + }{ + {"match", e1, e2, true}, + {"mismatch", e1, e3, false}, + {"not Error type", e1, stdErr, false}, + {"nil err", nil, e2, false}, + {"nil target", e1, nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := errors.Is(tt.err, tt.target); got != tt.want { + t.Errorf("Is() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWrapUnwrap(t *testing.T) { + internal := errors.New("internal") + tests := []struct { + name string + err *Error + wrapErr error + wantUnwrap error + }{ + {"wrap non-nil", New(10, 201, 0, ""), internal, internal}, + {"wrap nil", nil, internal, nil}, // Wrap on nil does nothing + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.err.Wrap(tt.wrapErr) + if unwrapped := got.Unwrap(); unwrapped != tt.wantUnwrap { + t.Errorf("Unwrap() = %v, want %v", unwrapped, tt.wantUnwrap) + } + }) + } +} + +func TestGRPCStatus(t *testing.T) { + tests := []struct { + name string + err *Error + wantCode codes.Code + wantMsg string + }{ + {"non-nil", New(10, 201, 123, "grpc err"), codes.Code(10201123), "grpc err"}, + {"nil", nil, codes.OK, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := tt.err.GRPCStatus() + if s.Code() != tt.wantCode || s.Message() != tt.wantMsg { + t.Errorf("GRPCStatus() = code=%v msg=%q, want code=%v msg=%q", s.Code(), s.Message(), tt.wantCode, tt.wantMsg) + } + }) + } +} + +func TestHTTPStatus(t *testing.T) { + tests := []struct { + name string + err *Error + want int + }{ + {"nil", nil, http.StatusOK}, + {"OK subcode", New(10, 0, 0, ""), http.StatusOK}, + {"InputInvalidFormat", New(10, uint32(code.InputInvalidFormat), 0, ""), http.StatusBadRequest}, + {"InputNotValidImplementation", New(10, uint32(code.InputNotValidImplementation), 0, ""), http.StatusUnprocessableEntity}, + {"DBError", New(10, uint32(code.DBError), 0, ""), http.StatusInternalServerError}, + {"ResNotFound", New(10, uint32(code.ResNotFound), 0, ""), http.StatusNotFound}, + // Add all other categories to cover switch branches + {"InputInvalidRange", New(10, uint32(code.InputInvalidRange), 0, ""), http.StatusUnprocessableEntity}, + {"DBDataConvert", New(10, uint32(code.DBDataConvert), 0, ""), http.StatusUnprocessableEntity}, + {"DBDuplicate", New(10, uint32(code.DBDuplicate), 0, ""), http.StatusConflict}, + {"ResInvalidFormat", New(10, uint32(code.ResInvalidFormat), 0, ""), http.StatusUnprocessableEntity}, + {"ResAlreadyExist", New(10, uint32(code.ResAlreadyExist), 0, ""), http.StatusConflict}, + {"ResInsufficient", New(10, uint32(code.ResInsufficient), 0, ""), http.StatusBadRequest}, + {"ResInsufficientPerm", New(10, uint32(code.ResInsufficientPerm), 0, ""), http.StatusForbidden}, + {"ResInvalidMeasureID", New(10, uint32(code.ResInvalidMeasureID), 0, ""), http.StatusBadRequest}, + {"ResExpired", New(10, uint32(code.ResExpired), 0, ""), http.StatusGone}, + {"ResMigrated", New(10, uint32(code.ResMigrated), 0, ""), http.StatusGone}, + {"ResInvalidState", New(10, uint32(code.ResInvalidState), 0, ""), http.StatusConflict}, + {"ResInsufficientQuota", New(10, uint32(code.ResInsufficientQuota), 0, ""), http.StatusTooManyRequests}, + {"ResMultiOwner", New(10, uint32(code.ResMultiOwner), 0, ""), http.StatusConflict}, + {"AuthUnauthorized", New(10, uint32(code.AuthUnauthorized), 0, ""), http.StatusUnauthorized}, + {"AuthExpired", New(10, uint32(code.AuthExpired), 0, ""), http.StatusUnauthorized}, + {"AuthInvalidPosixTime", New(10, uint32(code.AuthInvalidPosixTime), 0, ""), http.StatusUnauthorized}, + {"AuthSigPayloadMismatch", New(10, uint32(code.AuthSigPayloadMismatch), 0, ""), http.StatusUnauthorized}, + {"AuthForbidden", New(10, uint32(code.AuthForbidden), 0, ""), http.StatusForbidden}, + {"SysTooManyRequest", New(10, uint32(code.SysTooManyRequest), 0, ""), http.StatusTooManyRequests}, + {"SysInternal", New(10, uint32(code.SysInternal), 0, ""), http.StatusInternalServerError}, + {"SysMaintain", New(10, uint32(code.SysMaintain), 0, ""), http.StatusServiceUnavailable}, + {"SysTimeout", New(10, uint32(code.SysTimeout), 0, ""), http.StatusGatewayTimeout}, + {"PSuPublish", New(10, uint32(code.PSuPublish), 0, ""), http.StatusBadGateway}, + {"PSuConsume", New(10, uint32(code.PSuConsume), 0, ""), http.StatusBadGateway}, + {"PSuTooLarge", New(10, uint32(code.PSuTooLarge), 0, ""), http.StatusRequestEntityTooLarge}, + {"SvcMaintenance", New(10, uint32(code.SvcMaintenance), 0, ""), http.StatusServiceUnavailable}, + {"SvcInternal", New(10, uint32(code.SvcInternal), 0, ""), http.StatusInternalServerError}, + {"SvcThirdParty", New(10, uint32(code.SvcThirdParty), 0, ""), http.StatusBadGateway}, + {"SvcHTTP400", New(10, uint32(code.SvcHTTP400), 0, ""), http.StatusBadRequest}, + {"fallback unknown", New(10, 999, 0, ""), http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.HTTPStatus(); got != tt.want { + t.Errorf("HTTPStatus() = %d, want %d", got, tt.want) + } + }) + } +} diff --git a/pkg/library/errors/ez_func.go b/pkg/library/errors/ez_func.go new file mode 100644 index 0000000..4824fb8 --- /dev/null +++ b/pkg/library/errors/ez_func.go @@ -0,0 +1,467 @@ +package errs + +import ( + "strings" + + "backend/pkg/library/errors/code" +) + +/* ========================= + 日誌介面(與你現有 Logger 對齊) + ========================= */ + +// Logger 你現有的 logger 介面(與外部一致即可) +type Logger interface { + WithCallerSkip(n int) Logger + WithFields(fields ...LogField) Logger + Error(msg string) + Warn(msg string) + Info(msg string) +} + +// LogField 結構化欄位 +type LogField struct { + Key string + Val any +} + +/* ========================= + 共用小工具 + ========================= */ + +// joinMsg:把可變參數字串用空白串接(避免到處判斷 nil / 空 slice) +func joinMsg(s []string) string { + if len(s) == 0 { + return "" + } + + return strings.Join(s, " ") +} + +// logErr:統一打一筆 error log(避免重複記錄) +func logErr(l Logger, fields []LogField, e *Error) { + if l == nil || e == nil { + return + } + ll := l.WithCallerSkip(1) + if len(fields) > 0 { + ll = ll.WithFields(fields...) + } + // 需要更多欄位可在此擴充,例如:e.DisplayCode()、e.Category()、e.Detail() + ll.Error(e.Error()) +} + +/* ========================= + 共用裝飾器(把任意 ez 建構器包成帶日誌版本) + ========================= */ + +// WithLog 將任一 *Error 建構器(如 SysTimeoutError)轉成帶日誌的版本 +func WithLog(l Logger, fields []LogField, ctor func(s ...string) *Error, s ...string) *Error { + e := ctor(s...) + logErr(l, fields, e) + + return e +} + +// WithLogWrap 同上,但會同時 Wrap 內部 cause +func WithLogWrap(l Logger, fields []LogField, ctor func(s ...string) *Error, cause error, s ...string) *Error { + e := ctor(s...).Wrap(cause) + logErr(l, fields, e) + + return e +} + +/* ========================= + 泛用建構器(當你懶得記函式名時) + ========================= */ + +// EL 依 Category/Detail 直接建構並記錄日誌 +func EL(l Logger, fields []LogField, cat code.Category, det code.Detail, s ...string) *Error { + e := New(uint32(Scope), uint32(cat), uint32(det), joinMsg(s)) + logErr(l, fields, e) + + return e +} + +// ELWrap 同上,並 Wrap cause +func ELWrap(l Logger, fields []LogField, cat code.Category, det code.Detail, cause error, s ...string) *Error { + e := New(uint32(Scope), uint32(cat), uint32(det), joinMsg(s)).Wrap(cause) + logErr(l, fields, e) + + return e +} + +/* ======================================================================= + 一、基礎 ez 建構器(純建構 *Error,不帶日誌) + 分類順序:Input → DB → Resource → Auth → System → PubSub → Service + ======================================================================= */ + +/* ----- Input (CatInput) ----- */ + +func InputInvalidFormatError(s ...string) *Error { + return New(uint32(Scope), uint32(code.InputInvalidFormat), 0, joinMsg(s)) +} +func InputNotValidImplementationError(s ...string) *Error { + return New(uint32(Scope), uint32(code.InputNotValidImplementation), 0, joinMsg(s)) +} +func InputInvalidRangeError(s ...string) *Error { + return New(uint32(Scope), uint32(code.InputInvalidRange), 0, joinMsg(s)) +} + +/* ----- DB (CatDB) ----- */ + +func DBErrorError(s ...string) *Error { + return New(uint32(Scope), uint32(code.DBError), 0, joinMsg(s)) +} +func DBDataConvertError(s ...string) *Error { + return New(uint32(Scope), uint32(code.DBDataConvert), 0, joinMsg(s)) +} +func DBDuplicateError(s ...string) *Error { + return New(uint32(Scope), uint32(code.DBDuplicate), 0, joinMsg(s)) +} + +/* ----- Resource (CatResource) ----- */ + +func ResNotFoundError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResNotFound), 0, joinMsg(s)) +} +func ResInvalidFormatError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInvalidFormat), 0, joinMsg(s)) +} +func ResAlreadyExistError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResAlreadyExist), 0, joinMsg(s)) +} +func ResInsufficientError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInsufficient), 0, joinMsg(s)) +} +func ResInsufficientPermError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInsufficientPerm), 0, joinMsg(s)) +} +func ResInvalidMeasureIDError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInvalidMeasureID), 0, joinMsg(s)) +} +func ResExpiredError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResExpired), 0, joinMsg(s)) +} +func ResMigratedError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResMigrated), 0, joinMsg(s)) +} +func ResInvalidStateError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInvalidState), 0, joinMsg(s)) +} +func ResInsufficientQuotaError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResInsufficientQuota), 0, joinMsg(s)) +} +func ResMultiOwnerError(s ...string) *Error { + return New(uint32(Scope), uint32(code.ResMultiOwner), 0, joinMsg(s)) +} + +/* ----- Auth (CatAuth) ----- */ + +func AuthUnauthorizedError(s ...string) *Error { + return New(uint32(Scope), uint32(code.AuthUnauthorized), 0, joinMsg(s)) +} +func AuthExpiredError(s ...string) *Error { + return New(uint32(Scope), uint32(code.AuthExpired), 0, joinMsg(s)) +} +func AuthInvalidPosixTimeError(s ...string) *Error { + return New(uint32(Scope), uint32(code.AuthInvalidPosixTime), 0, joinMsg(s)) +} +func AuthSigPayloadMismatchError(s ...string) *Error { + return New(uint32(Scope), uint32(code.AuthSigPayloadMismatch), 0, joinMsg(s)) +} +func AuthForbiddenError(s ...string) *Error { + return New(uint32(Scope), uint32(code.AuthForbidden), 0, joinMsg(s)) +} + +/* ----- System (CatSystem) ----- */ + +func SysInternalError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SysInternal), 0, joinMsg(s)) +} +func SysMaintainError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SysMaintain), 0, joinMsg(s)) +} +func SysTimeoutError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SysTimeout), 0, joinMsg(s)) +} +func SysTooManyRequestError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SysTooManyRequest), 0, joinMsg(s)) +} + +/* ----- PubSub (CatPubSub) ----- */ + +func PSuPublishError(s ...string) *Error { + return New(uint32(Scope), uint32(code.PSuPublish), 0, joinMsg(s)) +} +func PSuConsumeError(s ...string) *Error { + return New(uint32(Scope), uint32(code.PSuConsume), 0, joinMsg(s)) +} +func PSuTooLargeError(s ...string) *Error { + return New(uint32(Scope), uint32(code.PSuTooLarge), 0, joinMsg(s)) +} + +/* ----- Service (CatService) ----- */ + +func SvcInternalError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SvcInternal), 0, joinMsg(s)) +} +func SvcThirdPartyError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SvcThirdParty), 0, joinMsg(s)) +} +func SvcHTTP400Error(s ...string) *Error { + return New(uint32(Scope), uint32(code.SvcHTTP400), 0, joinMsg(s)) +} +func SvcMaintenanceError(s ...string) *Error { + return New(uint32(Scope), uint32(code.SvcMaintenance), 0, joinMsg(s)) +} + +/* ============================================================================= + 二、帶日誌版本:L / WrapL(在「基礎 ez 建構器」之上包裝 WithLog / WithLogWrap) + 分類順序同上:Input → DB → Resource → Auth → System → PubSub → Service + ============================================================================= */ + +/* ----- Input (CatInput) ----- */ + +func InputInvalidFormatErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, InputInvalidFormatError, s...) +} +func InputInvalidFormatErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, InputInvalidFormatError, cause, s...) +} + +func InputNotValidImplementationErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, InputNotValidImplementationError, s...) +} +func InputNotValidImplementationErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, InputNotValidImplementationError, cause, s...) +} + +func InputInvalidRangeErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, InputInvalidRangeError, s...) +} +func InputInvalidRangeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, InputInvalidRangeError, cause, s...) +} + +/* ----- DB (CatDB) ----- */ + +func DBErrorErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, DBErrorError, s...) +} +func DBErrorErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, DBErrorError, cause, s...) +} + +func DBDataConvertErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, DBDataConvertError, s...) +} +func DBDataConvertErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, DBDataConvertError, cause, s...) +} + +func DBDuplicateErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, DBDuplicateError, s...) +} +func DBDuplicateErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, DBDuplicateError, cause, s...) +} + +/* ----- Resource (CatResource) ----- */ + +func ResNotFoundErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResNotFoundError, s...) +} +func ResNotFoundErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResNotFoundError, cause, s...) +} + +func ResInvalidFormatErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInvalidFormatError, s...) +} +func ResInvalidFormatErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInvalidFormatError, cause, s...) +} + +func ResAlreadyExistErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResAlreadyExistError, s...) +} +func ResAlreadyExistErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResAlreadyExistError, cause, s...) +} + +func ResInsufficientErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInsufficientError, s...) +} +func ResInsufficientErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInsufficientError, cause, s...) +} + +func ResInsufficientPermErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInsufficientPermError, s...) +} +func ResInsufficientPermErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInsufficientPermError, cause, s...) +} + +func ResInvalidMeasureIDErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInvalidMeasureIDError, s...) +} +func ResInvalidMeasureIDErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInvalidMeasureIDError, cause, s...) +} + +func ResExpiredErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResExpiredError, s...) +} +func ResExpiredErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResExpiredError, cause, s...) +} + +func ResMigratedErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResMigratedError, s...) +} +func ResMigratedErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResMigratedError, cause, s...) +} + +func ResInvalidStateErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInvalidStateError, s...) +} +func ResInvalidStateErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInvalidStateError, cause, s...) +} + +func ResInsufficientQuotaErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResInsufficientQuotaError, s...) +} +func ResInsufficientQuotaErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResInsufficientQuotaError, cause, s...) +} + +func ResMultiOwnerErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, ResMultiOwnerError, s...) +} +func ResMultiOwnerErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, ResMultiOwnerError, cause, s...) +} + +/* ----- Auth (CatAuth) ----- */ + +func AuthUnauthorizedErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, AuthUnauthorizedError, s...) +} +func AuthUnauthorizedErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, AuthUnauthorizedError, cause, s...) +} + +func AuthExpiredErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, AuthExpiredError, s...) +} +func AuthExpiredErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, AuthExpiredError, cause, s...) +} + +func AuthInvalidPosixTimeErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, AuthInvalidPosixTimeError, s...) +} +func AuthInvalidPosixTimeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, AuthInvalidPosixTimeError, cause, s...) +} + +func AuthSigPayloadMismatchErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, AuthSigPayloadMismatchError, s...) +} +func AuthSigPayloadMismatchErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, AuthSigPayloadMismatchError, cause, s...) +} + +func AuthForbiddenErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, AuthForbiddenError, s...) +} +func AuthForbiddenErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, AuthForbiddenError, cause, s...) +} + +/* ----- System (CatSystem) ----- */ + +func SysInternalErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SysInternalError, s...) +} +func SysInternalErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SysInternalError, cause, s...) +} + +func SysMaintainErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SysMaintainError, s...) +} +func SysMaintainErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SysMaintainError, cause, s...) +} + +func SysTimeoutErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SysTimeoutError, s...) +} +func SysTimeoutErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SysTimeoutError, cause, s...) +} + +func SysTooManyRequestErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SysTooManyRequestError, s...) +} +func SysTooManyRequestErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SysTooManyRequestError, cause, s...) +} + +/* ----- PubSub (CatPubSub) ----- */ + +func PSuPublishErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, PSuPublishError, s...) +} +func PSuPublishErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, PSuPublishError, cause, s...) +} + +func PSuConsumeErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, PSuConsumeError, s...) +} +func PSuConsumeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, PSuConsumeError, cause, s...) +} + +func PSuTooLargeErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, PSuTooLargeError, s...) +} +func PSuTooLargeErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, PSuTooLargeError, cause, s...) +} + +/* ----- Service (CatService) ----- */ + +func SvcInternalErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SvcInternalError, s...) +} +func SvcInternalErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SvcInternalError, cause, s...) +} + +func SvcThirdPartyErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SvcThirdPartyError, s...) +} +func SvcThirdPartyErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SvcThirdPartyError, cause, s...) +} + +func SvcHTTP400ErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SvcHTTP400Error, s...) +} +func SvcHTTP400ErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SvcHTTP400Error, cause, s...) +} + +func SvcMaintenanceErrorL(l Logger, fields []LogField, s ...string) *Error { + return WithLog(l, fields, SvcMaintenanceError, s...) +} +func SvcMaintenanceErrorWrapL(l Logger, fields []LogField, cause error, s ...string) *Error { + return WithLogWrap(l, fields, SvcMaintenanceError, cause, s...) +} diff --git a/pkg/library/errors/ez_functions_test.go b/pkg/library/errors/ez_functions_test.go new file mode 100644 index 0000000..a819d60 --- /dev/null +++ b/pkg/library/errors/ez_functions_test.go @@ -0,0 +1,347 @@ +package errs + +import ( + "errors" + "reflect" + "testing" + + "backend/pkg/library/errors/code" +) + +// fakeLogger as before +type fakeLogger struct { + calls []string + lastMsg string + fieldsStack [][]LogField + callerSkips []int +} + +func (l *fakeLogger) WithCallerSkip(n int) Logger { l.callerSkips = append(l.callerSkips, n); return l } +func (l *fakeLogger) WithFields(fields ...LogField) Logger { + cp := make([]LogField, len(fields)) + copy(cp, fields) + l.fieldsStack = append(l.fieldsStack, cp) + return l +} +func (l *fakeLogger) Error(msg string) { l.calls = append(l.calls, "ERROR"); l.lastMsg = msg } +func (l *fakeLogger) Warn(msg string) { l.calls = append(l.calls, "WARN"); l.lastMsg = msg } +func (l *fakeLogger) Info(msg string) { l.calls = append(l.calls, "INFO"); l.lastMsg = msg } +func (l *fakeLogger) reset() { + l.calls, l.lastMsg, l.fieldsStack, l.callerSkips = nil, "", nil, nil +} + +func init() { Scope = code.Gateway } + +func TestJoinMsg(t *testing.T) { + tests := []struct { + name string + in []string + want string + }{ + {"nil", nil, ""}, + {"empty", []string{}, ""}, + {"single", []string{"a"}, "a"}, + {"multi", []string{"a", "b", "c"}, "a b c"}, + {"with spaces", []string{"hello", "world"}, "hello world"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := joinMsg(tt.in); got != tt.want { + t.Errorf("joinMsg() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestLogErr(t *testing.T) { + tests := []struct { + name string + l Logger + fields []LogField + e *Error + wantCall string + wantFields bool + wantCallerSkip int + }{ + {"nil logger", nil, nil, New(10, 101, 0, "err"), "", false, 0}, + {"nil error", &fakeLogger{}, nil, nil, "", false, 0}, + {"basic log", &fakeLogger{}, nil, New(10, 101, 0, "err"), "ERROR", false, 1}, + {"with fields", &fakeLogger{}, []LogField{{Key: "k", Val: "v"}}, New(10, 101, 0, "err"), "ERROR", true, 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var fl *fakeLogger + if tt.l != nil { + var ok bool + fl, ok = tt.l.(*fakeLogger) + if !ok { + t.Fatalf("logger is not *fakeLogger") + } + fl.reset() + } + logErr(tt.l, tt.fields, tt.e) + if fl == nil { + if tt.wantCall != "" { + t.Errorf("expected log but logger is nil") + } + return + } + if tt.wantCall == "" && len(fl.calls) > 0 { + t.Errorf("unexpected log call") + } + if tt.wantCall != "" && (len(fl.calls) == 0 || fl.calls[0] != tt.wantCall) { + t.Errorf("expected call %q, got %v", tt.wantCall, fl.calls) + } + if tt.wantFields && (len(fl.fieldsStack) == 0 || !reflect.DeepEqual(fl.fieldsStack[0], tt.fields)) { + t.Errorf("fields mismatch: got %v, want %v", fl.fieldsStack, tt.fields) + } + if tt.wantCallerSkip != 0 && (len(fl.callerSkips) == 0 || fl.callerSkips[0] != tt.wantCallerSkip) { + t.Errorf("callerSkip = %v, want %d", fl.callerSkips, tt.wantCallerSkip) + } + }) + } +} + +func TestEL(t *testing.T) { + tests := []struct { + name string + cat code.Category + det code.Detail + s []string + wantCat uint32 + wantDet uint32 + wantMsg string + wantLog bool + }{ + {"basic", code.ResNotFound, 123, []string{"not found"}, uint32(code.ResNotFound), 123, "not found", true}, + {"nil logger", code.ResNotFound, 0, []string{}, uint32(code.ResNotFound), 0, "", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &fakeLogger{} + e := EL(l, nil, tt.cat, tt.det, tt.s...) + if e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg { + t.Errorf("EL = cat=%d det=%d msg=%q, want %d %d %q", e.Category(), e.Detail(), e.Error(), tt.wantCat, tt.wantDet, tt.wantMsg) + } + if tt.wantLog && len(l.calls) == 0 { + t.Errorf("expected log") + } + }) + } +} + +func TestELWrap(t *testing.T) { + tests := []struct { + name string + cat code.Category + det code.Detail + cause error + s []string + wantCat uint32 + wantDet uint32 + wantMsg string + wantUnwrap string + wantLog bool + }{ + {"basic", code.SysInternal, 456, errors.New("internal"), []string{"sys err"}, uint32(code.SysInternal), 456, "sys err", "internal", true}, + {"no log", code.SysInternal, 0, nil, []string{}, uint32(code.SysInternal), 0, "", "", false}, // nil cause ok + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &fakeLogger{} + e := ELWrap(l, nil, tt.cat, tt.det, tt.cause, tt.s...) + if e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg { + t.Errorf("ELWrap = cat=%d det=%d msg=%q, want %d %d %q", e.Category(), e.Detail(), e.Error(), tt.wantCat, tt.wantDet, tt.wantMsg) + } + unw := e.Unwrap() + gotUnwrap := "" + if unw != nil { + gotUnwrap = unw.Error() + } + if gotUnwrap != tt.wantUnwrap { + t.Errorf("Unwrap = %q, want %q", gotUnwrap, tt.wantUnwrap) + } + if tt.wantLog && len(l.calls) == 0 { + t.Errorf("expected log") + } + }) + } +} + +// Expand TestBaseConstructors with all base funcs +func TestBaseConstructors(t *testing.T) { + tests := []struct { + name string + fn func(...string) *Error + wantCat uint32 + wantDet uint32 + wantMsg string + }{ + {"InputInvalidFormatError", InputInvalidFormatError, uint32(code.InputInvalidFormat), 0, "test msg"}, + {"InputNotValidImplementationError", InputNotValidImplementationError, uint32(code.InputNotValidImplementation), 0, "test msg"}, + {"InputInvalidRangeError", InputInvalidRangeError, uint32(code.InputInvalidRange), 0, "test msg"}, + {"DBErrorError", DBErrorError, uint32(code.DBError), 0, "test msg"}, + {"DBDataConvertError", DBDataConvertError, uint32(code.DBDataConvert), 0, "test msg"}, + {"DBDuplicateError", DBDuplicateError, uint32(code.DBDuplicate), 0, "test msg"}, + {"ResNotFoundError", ResNotFoundError, uint32(code.ResNotFound), 0, "test msg"}, + {"ResInvalidFormatError", ResInvalidFormatError, uint32(code.ResInvalidFormat), 0, "test msg"}, + {"ResAlreadyExistError", ResAlreadyExistError, uint32(code.ResAlreadyExist), 0, "test msg"}, + {"ResInsufficientError", ResInsufficientError, uint32(code.ResInsufficient), 0, "test msg"}, + {"ResInsufficientPermError", ResInsufficientPermError, uint32(code.ResInsufficientPerm), 0, "test msg"}, + {"ResInvalidMeasureIDError", ResInvalidMeasureIDError, uint32(code.ResInvalidMeasureID), 0, "test msg"}, + {"ResExpiredError", ResExpiredError, uint32(code.ResExpired), 0, "test msg"}, + {"ResMigratedError", ResMigratedError, uint32(code.ResMigrated), 0, "test msg"}, + {"ResInvalidStateError", ResInvalidStateError, uint32(code.ResInvalidState), 0, "test msg"}, + {"ResInsufficientQuotaError", ResInsufficientQuotaError, uint32(code.ResInsufficientQuota), 0, "test msg"}, + {"ResMultiOwnerError", ResMultiOwnerError, uint32(code.ResMultiOwner), 0, "test msg"}, + {"AuthUnauthorizedError", AuthUnauthorizedError, uint32(code.AuthUnauthorized), 0, "test msg"}, + {"AuthExpiredError", AuthExpiredError, uint32(code.AuthExpired), 0, "test msg"}, + {"AuthInvalidPosixTimeError", AuthInvalidPosixTimeError, uint32(code.AuthInvalidPosixTime), 0, "test msg"}, + {"AuthSigPayloadMismatchError", AuthSigPayloadMismatchError, uint32(code.AuthSigPayloadMismatch), 0, "test msg"}, + {"AuthForbiddenError", AuthForbiddenError, uint32(code.AuthForbidden), 0, "test msg"}, + {"SysInternalError", SysInternalError, uint32(code.SysInternal), 0, "test msg"}, + {"SysMaintainError", SysMaintainError, uint32(code.SysMaintain), 0, "test msg"}, + {"SysTimeoutError", SysTimeoutError, uint32(code.SysTimeout), 0, "test msg"}, + {"SysTooManyRequestError", SysTooManyRequestError, uint32(code.SysTooManyRequest), 0, "test msg"}, + {"PSuPublishError", PSuPublishError, uint32(code.PSuPublish), 0, "test msg"}, + {"PSuConsumeError", PSuConsumeError, uint32(code.PSuConsume), 0, "test msg"}, + {"PSuTooLargeError", PSuTooLargeError, uint32(code.PSuTooLarge), 0, "test msg"}, + {"SvcInternalError", SvcInternalError, uint32(code.SvcInternal), 0, "test msg"}, + {"SvcThirdPartyError", SvcThirdPartyError, uint32(code.SvcThirdParty), 0, "test msg"}, + {"SvcHTTP400Error", SvcHTTP400Error, uint32(code.SvcHTTP400), 0, "test msg"}, + {"SvcMaintenanceError", SvcMaintenanceError, uint32(code.SvcMaintenance), 0, "test msg"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.fn("test", "msg") + if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" { + t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg") + } + }) + } +} + +// Expand TestLConstructors with all L funcs +func TestLConstructors(t *testing.T) { + tests := []struct { + name string + fn func(Logger, []LogField, ...string) *Error + wantCat uint32 + wantDet uint32 + wantMsg string + wantLog bool + }{ + {"InputInvalidFormatErrorL", InputInvalidFormatErrorL, uint32(code.InputInvalidFormat), 0, "test msg", true}, + {"InputNotValidImplementationErrorL", InputNotValidImplementationErrorL, uint32(code.InputNotValidImplementation), 0, "test msg", true}, + {"InputInvalidRangeErrorL", InputInvalidRangeErrorL, uint32(code.InputInvalidRange), 0, "test msg", true}, + {"DBErrorErrorL", DBErrorErrorL, uint32(code.DBError), 0, "test msg", true}, + {"DBDataConvertErrorL", DBDataConvertErrorL, uint32(code.DBDataConvert), 0, "test msg", true}, + {"DBDuplicateErrorL", DBDuplicateErrorL, uint32(code.DBDuplicate), 0, "test msg", true}, + {"ResNotFoundErrorL", ResNotFoundErrorL, uint32(code.ResNotFound), 0, "test msg", true}, + {"ResInvalidFormatErrorL", ResInvalidFormatErrorL, uint32(code.ResInvalidFormat), 0, "test msg", true}, + {"ResAlreadyExistErrorL", ResAlreadyExistErrorL, uint32(code.ResAlreadyExist), 0, "test msg", true}, + {"ResInsufficientErrorL", ResInsufficientErrorL, uint32(code.ResInsufficient), 0, "test msg", true}, + {"ResInsufficientPermErrorL", ResInsufficientPermErrorL, uint32(code.ResInsufficientPerm), 0, "test msg", true}, + {"ResInvalidMeasureIDErrorL", ResInvalidMeasureIDErrorL, uint32(code.ResInvalidMeasureID), 0, "test msg", true}, + {"ResExpiredErrorL", ResExpiredErrorL, uint32(code.ResExpired), 0, "test msg", true}, + {"ResMigratedErrorL", ResMigratedErrorL, uint32(code.ResMigrated), 0, "test msg", true}, + {"ResInvalidStateErrorL", ResInvalidStateErrorL, uint32(code.ResInvalidState), 0, "test msg", true}, + {"ResInsufficientQuotaErrorL", ResInsufficientQuotaErrorL, uint32(code.ResInsufficientQuota), 0, "test msg", true}, + {"ResMultiOwnerErrorL", ResMultiOwnerErrorL, uint32(code.ResMultiOwner), 0, "test msg", true}, + {"AuthUnauthorizedErrorL", AuthUnauthorizedErrorL, uint32(code.AuthUnauthorized), 0, "test msg", true}, + {"AuthExpiredErrorL", AuthExpiredErrorL, uint32(code.AuthExpired), 0, "test msg", true}, + {"AuthInvalidPosixTimeErrorL", AuthInvalidPosixTimeErrorL, uint32(code.AuthInvalidPosixTime), 0, "test msg", true}, + {"AuthSigPayloadMismatchErrorL", AuthSigPayloadMismatchErrorL, uint32(code.AuthSigPayloadMismatch), 0, "test msg", true}, + {"AuthForbiddenErrorL", AuthForbiddenErrorL, uint32(code.AuthForbidden), 0, "test msg", true}, + {"SysInternalErrorL", SysInternalErrorL, uint32(code.SysInternal), 0, "test msg", true}, + {"SysMaintainErrorL", SysMaintainErrorL, uint32(code.SysMaintain), 0, "test msg", true}, + {"SysTimeoutErrorL", SysTimeoutErrorL, uint32(code.SysTimeout), 0, "test msg", true}, + {"SysTooManyRequestErrorL", SysTooManyRequestErrorL, uint32(code.SysTooManyRequest), 0, "test msg", true}, + {"PSuPublishErrorL", PSuPublishErrorL, uint32(code.PSuPublish), 0, "test msg", true}, + {"PSuConsumeErrorL", PSuConsumeErrorL, uint32(code.PSuConsume), 0, "test msg", true}, + {"PSuTooLargeErrorL", PSuTooLargeErrorL, uint32(code.PSuTooLarge), 0, "test msg", true}, + {"SvcInternalErrorL", SvcInternalErrorL, uint32(code.SvcInternal), 0, "test msg", true}, + {"SvcThirdPartyErrorL", SvcThirdPartyErrorL, uint32(code.SvcThirdParty), 0, "test msg", true}, + {"SvcHTTP400ErrorL", SvcHTTP400ErrorL, uint32(code.SvcHTTP400), 0, "test msg", true}, + {"SvcMaintenanceErrorL", SvcMaintenanceErrorL, uint32(code.SvcMaintenance), 0, "test msg", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &fakeLogger{} + fields := []LogField{} + e := tt.fn(l, fields, "test", "msg") + if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" { + t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg") + } + if tt.wantLog && len(l.calls) == 0 { + t.Errorf("expected log call") + } + }) + } +} + +// Add TestWrapLConstructors similarly +func TestWrapLConstructors(t *testing.T) { + tests := []struct { + name string + fn func(Logger, []LogField, error, ...string) *Error + wantCat uint32 + wantDet uint32 + wantMsg string + wantUnwrap string + wantLog bool + }{ + {"InputInvalidFormatErrorWrapL", InputInvalidFormatErrorWrapL, uint32(code.InputInvalidFormat), 0, "test msg", "cause err", true}, + {"InputNotValidImplementationErrorWrapL", InputNotValidImplementationErrorWrapL, uint32(code.InputNotValidImplementation), 0, "test msg", "cause err", true}, + {"InputInvalidRangeErrorWrapL", InputInvalidRangeErrorWrapL, uint32(code.InputInvalidRange), 0, "test msg", "cause err", true}, + {"DBErrorErrorWrapL", DBErrorErrorWrapL, uint32(code.DBError), 0, "test msg", "cause err", true}, + {"DBDataConvertErrorWrapL", DBDataConvertErrorWrapL, uint32(code.DBDataConvert), 0, "test msg", "cause err", true}, + {"DBDuplicateErrorWrapL", DBDuplicateErrorWrapL, uint32(code.DBDuplicate), 0, "test msg", "cause err", true}, + {"ResNotFoundErrorWrapL", ResNotFoundErrorWrapL, uint32(code.ResNotFound), 0, "test msg", "cause err", true}, + {"ResInvalidFormatErrorWrapL", ResInvalidFormatErrorWrapL, uint32(code.ResInvalidFormat), 0, "test msg", "cause err", true}, + {"ResAlreadyExistErrorWrapL", ResAlreadyExistErrorWrapL, uint32(code.ResAlreadyExist), 0, "test msg", "cause err", true}, + {"ResInsufficientErrorWrapL", ResInsufficientErrorWrapL, uint32(code.ResInsufficient), 0, "test msg", "cause err", true}, + {"ResInsufficientPermErrorWrapL", ResInsufficientPermErrorWrapL, uint32(code.ResInsufficientPerm), 0, "test msg", "cause err", true}, + {"ResInvalidMeasureIDErrorWrapL", ResInvalidMeasureIDErrorWrapL, uint32(code.ResInvalidMeasureID), 0, "test msg", "cause err", true}, + {"ResExpiredErrorWrapL", ResExpiredErrorWrapL, uint32(code.ResExpired), 0, "test msg", "cause err", true}, + {"ResMigratedErrorWrapL", ResMigratedErrorWrapL, uint32(code.ResMigrated), 0, "test msg", "cause err", true}, + {"ResInvalidStateErrorWrapL", ResInvalidStateErrorWrapL, uint32(code.ResInvalidState), 0, "test msg", "cause err", true}, + {"ResInsufficientQuotaErrorWrapL", ResInsufficientQuotaErrorWrapL, uint32(code.ResInsufficientQuota), 0, "test msg", "cause err", true}, + {"ResMultiOwnerErrorWrapL", ResMultiOwnerErrorWrapL, uint32(code.ResMultiOwner), 0, "test msg", "cause err", true}, + {"AuthUnauthorizedErrorWrapL", AuthUnauthorizedErrorWrapL, uint32(code.AuthUnauthorized), 0, "test msg", "cause err", true}, + {"AuthExpiredErrorWrapL", AuthExpiredErrorWrapL, uint32(code.AuthExpired), 0, "test msg", "cause err", true}, + {"AuthInvalidPosixTimeErrorWrapL", AuthInvalidPosixTimeErrorWrapL, uint32(code.AuthInvalidPosixTime), 0, "test msg", "cause err", true}, + {"AuthSigPayloadMismatchErrorWrapL", AuthSigPayloadMismatchErrorWrapL, uint32(code.AuthSigPayloadMismatch), 0, "test msg", "cause err", true}, + {"AuthForbiddenErrorWrapL", AuthForbiddenErrorWrapL, uint32(code.AuthForbidden), 0, "test msg", "cause err", true}, + {"SysInternalErrorWrapL", SysInternalErrorWrapL, uint32(code.SysInternal), 0, "test msg", "cause err", true}, + {"SysMaintainErrorWrapL", SysMaintainErrorWrapL, uint32(code.SysMaintain), 0, "test msg", "cause err", true}, + {"SysTimeoutErrorWrapL", SysTimeoutErrorWrapL, uint32(code.SysTimeout), 0, "test msg", "cause err", true}, + {"SysTooManyRequestErrorWrapL", SysTooManyRequestErrorWrapL, uint32(code.SysTooManyRequest), 0, "test msg", "cause err", true}, + {"PSuPublishErrorWrapL", PSuPublishErrorWrapL, uint32(code.PSuPublish), 0, "test msg", "cause err", true}, + {"PSuConsumeErrorWrapL", PSuConsumeErrorWrapL, uint32(code.PSuConsume), 0, "test msg", "cause err", true}, + {"PSuTooLargeErrorWrapL", PSuTooLargeErrorWrapL, uint32(code.PSuTooLarge), 0, "test msg", "cause err", true}, + {"SvcInternalErrorWrapL", SvcInternalErrorWrapL, uint32(code.SvcInternal), 0, "test msg", "cause err", true}, + {"SvcThirdPartyErrorWrapL", SvcThirdPartyErrorWrapL, uint32(code.SvcThirdParty), 0, "test msg", "cause err", true}, + {"SvcHTTP400ErrorWrapL", SvcHTTP400ErrorWrapL, uint32(code.SvcHTTP400), 0, "test msg", "cause err", true}, + {"SvcMaintenanceErrorWrapL", SvcMaintenanceErrorWrapL, uint32(code.SvcMaintenance), 0, "test msg", "cause err", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &fakeLogger{} + fields := []LogField{} + cause := errors.New("cause err") + e := tt.fn(l, fields, cause, "test", "msg") + if e == nil || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != "test msg" { + t.Errorf("%s = cat=%d det=%d msg=%q, want %d 0 %q", tt.name, e.Category(), e.Detail(), e.Error(), tt.wantCat, "test msg") + } + if tt.wantUnwrap != "" && e.Unwrap().Error() != tt.wantUnwrap { + t.Errorf("Unwrap() = %q, want %q", e.Unwrap().Error(), tt.wantUnwrap) + } + if tt.wantLog && len(l.calls) == 0 { + t.Errorf("expected log call") + } + }) + } +} + +// ... Add more tests for edge cases, like empty strings, multiple args in joinMsg, etc. diff --git a/pkg/library/errors/from_errors.go b/pkg/library/errors/from_errors.go new file mode 100644 index 0000000..ce8664a --- /dev/null +++ b/pkg/library/errors/from_errors.go @@ -0,0 +1,73 @@ +package errs + +import ( + "errors" + + "backend/pkg/library/errors/code" + "google.golang.org/grpc/status" +) + +func newBuiltinGRPCErr(scope, detail uint32, msg string) *Error { + return &Error{ + category: uint32(code.CatGRPC), + detail: detail, + scope: scope, + msg: msg, + } +} + +// FromError tries to let error as Err +// it supports to unwrap error that has Error +// return nil if failed to transfer +func FromError(err error) *Error { + if err == nil { + return nil + } + + var e *Error + if errors.As(err, &e) { + return e + } + + return nil +} + +// FromCode parses code as following 8 碼 +// Decimal: 10201000 +// 10 represents Scope +// 201 represents Category +// 000 represents Detail error code +func FromCode(code uint32) *Error { + const CodeMultiplier = 1000000 + const SubMultiplier = 1000 + // 獲取 scope,前兩位數 + scope := code / CodeMultiplier + + // 獲取 detail,最後三位數 + detail := code % SubMultiplier + + // 獲取 category,中間三位數 + category := (code / SubMultiplier) % SubMultiplier + + return &Error{ + category: category, + detail: detail, + scope: scope, + msg: "", + } +} + +// FromGRPCError transfer error to Err +// useful for gRPC client +func FromGRPCError(err error) *Error { + s, _ := status.FromError(err) + e := FromCode(uint32(s.Code())) + e.msg = s.Message() + + // For GRPC built-in code + if e.Scope() == uint32(code.Unset) && e.Category() == 0 && e.Code() != code.OK { + e = newBuiltinGRPCErr(uint32(Scope), e.detail, s.Message()) // Note: detail is now 3-digit, but built-in codes are small + } + + return e +} diff --git a/pkg/library/errors/from_errors_test.go b/pkg/library/errors/from_errors_test.go new file mode 100644 index 0000000..aaee2ca --- /dev/null +++ b/pkg/library/errors/from_errors_test.go @@ -0,0 +1,115 @@ +package errs + +import ( + "errors" + "fmt" + "testing" + + "backend/pkg/library/errors/code" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestNewBuiltinGRPCErr(t *testing.T) { + tests := []struct { + name string + scope uint32 + detail uint32 + msg string + wantCat uint32 + wantScope uint32 + wantDet uint32 + wantMsg string + }{ + {"basic", 10, 3, "test", uint32(code.CatGRPC), 10, 3, "test"}, + {"zero", 0, 0, "", uint32(code.CatGRPC), 0, 0, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := newBuiltinGRPCErr(tt.scope, tt.detail, tt.msg) + if e.Category() != tt.wantCat || e.Scope() != tt.wantScope || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg { + t.Errorf("newBuiltinGRPCErr = cat=%d scope=%d det=%d msg=%q, want %d %d %d %q", + e.Category(), e.Scope(), e.Detail(), e.Error(), tt.wantCat, tt.wantScope, tt.wantDet, tt.wantMsg) + } + }) + } +} + +func TestFromError(t *testing.T) { + base := New(10, uint32(code.DBError), 0, "base") + // but actually use fmt.Errorf("%w", base) for proper wrapping + tests := []struct { + name string + in error + want *Error + }{ + {"nil", nil, nil}, + {"not Error", errors.New("std"), nil}, + {"direct Error", base, base}, + {"wrapped Error", fmt.Errorf("wrap: %w", base), base}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FromError(tt.in) + if (got == nil) != (tt.want == nil) { + t.Errorf("FromError = %v, want nil=%v", got, tt.want == nil) + } + if got != nil && (got.Category() != tt.want.Category() || got.Detail() != tt.want.Detail()) { + t.Errorf("FromError = cat=%d det=%d, want %d %d", got.Category(), got.Detail(), tt.want.Category(), tt.want.Detail()) + } + }) + } +} + +func TestFromCode(t *testing.T) { + tests := []struct { + name string + code uint32 + wantScope uint32 + wantCat uint32 + wantDet uint32 + }{ + {"basic", 10201123, 10, 201, 123}, + {"zero", 0, 0, 0, 0}, + {"max", 99999999, 99, 999, 999}, + {"overflow code", 100000000, 100, 0, 0}, // Parses as scope=100, but uint32 limits + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := FromCode(tt.code) + if e.Scope() != tt.wantScope || e.Category() != tt.wantCat || e.Detail() != tt.wantDet { + t.Errorf("FromCode = scope=%d cat=%d det=%d, want %d %d %d", e.Scope(), e.Category(), e.Detail(), tt.wantScope, tt.wantCat, tt.wantDet) + } + }) + } +} + +func TestFromGRPCError(t *testing.T) { + Scope = code.Gateway + tests := []struct { + name string + in error + wantScope uint32 + wantCat uint32 + wantDet uint32 + wantMsg string + }{ + {"nil", nil, 0, 0, 0, ""}, + {"builtin OK", status.New(codes.OK, "").Err(), 0, 0, 0, ""}, + {"builtin InvalidArgument", status.New(codes.InvalidArgument, "bad").Err(), uint32(code.Gateway), uint32(code.CatGRPC), uint32(codes.InvalidArgument), "bad"}, + {"custom code", status.New(codes.Code(10201123), "custom").Err(), 10, 201, 123, "custom"}, + {"unset scope with builtin", status.New(codes.NotFound, "not found").Err(), uint32(code.Gateway), uint32(code.CatGRPC), uint32(codes.NotFound), "not found"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := FromGRPCError(tt.in) + if e == nil && (tt.wantScope != 0 || tt.wantCat != 0 || tt.wantDet != 0) { + t.Errorf("got nil, want non-nil") + } + if e != nil && (e.Scope() != tt.wantScope || e.Category() != tt.wantCat || e.Detail() != tt.wantDet || e.Error() != tt.wantMsg) { + t.Errorf("FromGRPCError = scope=%d cat=%d det=%d msg=%q, want %d %d %d %q", + e.Scope(), e.Category(), e.Detail(), e.Error(), tt.wantScope, tt.wantCat, tt.wantDet, tt.wantMsg) + } + }) + } +} diff --git a/pkg/library/errs/code/category.go b/pkg/library/errs/code/category.go deleted file mode 100644 index ecbe7d4..0000000 --- a/pkg/library/errs/code/category.go +++ /dev/null @@ -1,15 +0,0 @@ -package code - -// Category for general operations: 10 - 490 -const ( - _ = iota - CatInput uint32 = iota * 10 - CatDB - CatResource - CatGRPC - CatAuth - CatSystem - CatPubSub - CatService - CatToken -) diff --git a/pkg/library/errs/code/code.go b/pkg/library/errs/code/code.go deleted file mode 100644 index eae1400..0000000 --- a/pkg/library/errs/code/code.go +++ /dev/null @@ -1,90 +0,0 @@ -package code - -const ( - OK uint32 = 0 -) - -// 詳細代碼 - 輸入類 01x -const ( - _ = iota + CatInput - InvalidFormat // 無效格式 - NotValidImplementation // 非有效實現 - InvalidRange // 無效範圍 -) - -// 詳細代碼 - 資料庫類 02x -const ( - _ = iota + CatDB - DBError // 資料庫一般錯誤 - DBDataConvert // 資料轉換錯誤 - DBDuplicate // 資料重複 -) - -// 詳細代碼 - 資源類 03x -const ( - _ = iota + CatResource - ResourceNotFound // 資源未找到 - InvalidResourceFormat // 無效的資源格式 - ResourceAlreadyExist // 資源已存在 - ResourceInsufficient // 資源不足 - InsufficientPermission // 權限不足 - InvalidMeasurementID // 無效的測量ID - ResourceExpired // 資源過期 - ResourceMigrated // 資源已遷移 - InvalidResourceState // 無效的資源狀態 - InsufficientQuota // 配額不足 - ResourceHasMultiOwner // 資源有多個所有者 - UserSuspended // 沒有權限使用該資源 - TooManyRequest // 單位時間內請求太多次 -) - -/* 詳細代碼 - GRPC */ -// GRPC 的詳細代碼使用 Go GRPC 的內建代碼。 -// 參考 "google.golang.org/grpc/codes" 獲取更多詳細資訊。 - -// 詳細代碼 - 驗證類 05x -const ( - _ = iota + CatAuth - Unauthorized // 未授權 - AuthExpired // 授權過期 - InvalidPosixTime // 無效的 POSIX 時間 - SigAndPayloadNotMatched // 簽名和載荷不匹配 - Forbidden // 禁止訪問 -) - -// 詳細代碼 - 系統類 06x -const ( - _ = iota + CatSystem - SystemInternalError // 系統內部錯誤 - SystemMaintainError // 系統維護錯誤 - SystemTimeoutError // 系統超時錯誤 -) - -// 詳細代碼 - PubSub 07x -const ( - _ = iota + CatPubSub - Publish // 發佈錯誤 - Consume // 消費錯誤 - MsgSizeTooLarge // 訊息過大 -) - -// 詳細代碼 - 特定服務類 08x -const ( - _ = iota + CatService - ArkInternal // Ark 內部錯誤 - ThirdParty - ArkHTTP400 // Ark HTTP 400 錯誤 -) - -// 詳細代碼 - Token 類 09x -const ( - _ = iota + CatToken - TokenCreateError // Token 創建錯誤 - TokenValidateError // Token 驗證錯誤 - TokenExpired // Token 過期 - TokenNotFound // Token 未找到 - TokenBlacklisted // Token 已被列入黑名單 - InvalidJWT // 無效的 JWT - RefreshTokenError // Refresh Token 錯誤 - OneTimeTokenError // 一次性 Token 錯誤 -) diff --git a/pkg/library/errs/code/messsage.go b/pkg/library/errs/code/messsage.go deleted file mode 100644 index 84c9016..0000000 --- a/pkg/library/errs/code/messsage.go +++ /dev/null @@ -1,13 +0,0 @@ -package code - -// CatToStr collects general error messages for each Category -// It is used to send back to API caller -var CatToStr = map[uint32]string{ - CatInput: "Invalid Input Data", - CatDB: "Database Error", - CatResource: "Resource Error", - CatGRPC: "Internal Service Communication Error", - CatAuth: "Authentication Error", - CatService: "Internal Service Communication Error", - CatSystem: "System Error", -} diff --git a/pkg/library/errs/code/scope.go b/pkg/library/errs/code/scope.go deleted file mode 100644 index 5f35685..0000000 --- a/pkg/library/errs/code/scope.go +++ /dev/null @@ -1,18 +0,0 @@ -package code - -// Scope -const ( - Unset uint32 = iota - CloudEPPortalGW - CloudEPMember - CloudEPPermission - CloudEPNotification - CloudEPTweeting - CloudEPOrder - CloudEPFileStorage - CloudEPProduct - CloudEPSecKill - CloudEPCart - CloudEPComment - CloudEPReaction -) diff --git a/pkg/library/errs/easy_func.go b/pkg/library/errs/easy_func.go deleted file mode 100644 index e299e11..0000000 --- a/pkg/library/errs/easy_func.go +++ /dev/null @@ -1,563 +0,0 @@ -package errs - -import ( - "errors" - "fmt" - "strings" - - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" - "google.golang.org/grpc/status" -) - -const ( - defaultDetailCode = 00 -) - -func newBuiltinGRPCErr(scope, detail uint32, msg string) *LibError { - return &LibError{ - category: code.CatGRPC, - code: detail, - scope: scope, - msg: msg, - } -} - -// FromError tries to let error as Err -// it supports to unwrap error that has Error -// return nil if failed to transfer -func FromError(err error) *LibError { - if err == nil { - return nil - } - - var e *LibError - if errors.As(err, &e) { - return e - } - - return nil -} - -// FromCode parses code as following 7 碼 -// Decimal: 1200314 -// 12 represents Scope -// 003 represents Category -// 14 represents Detail error code -func FromCode(code uint32) *LibError { - // 獲取 scope,前兩位數 - scope := code / 100000 - - // 獲取 detail,最後兩位數 - detail := code % 100 - - // 獲取 category,中間三位數 - category := (code / 100) % 1000 - - return &LibError{ - category: category * 100, // category 放大為三位數的整百數 - code: category*100 + detail, // 重構完整的 code - scope: scope, - msg: "", - } -} - -// FromGRPCError transfer error to Err -// useful for gRPC client -func FromGRPCError(err error) *LibError { - s, _ := status.FromError(err) - e := FromCode(uint32(s.Code())) - e.msg = s.Message() - - // For GRPC built-in code - if e.Scope() == code.Unset && e.Category() == 0 && e.Code() != code.OK { - e = newBuiltinGRPCErr(Scope, e.Code(), s.Message()) - } - - return e -} - -/*** System ***/ - -// SystemTimeoutError xxx6300 returns Error 系統超時 -func SystemTimeoutError(s ...string) *LibError { - return NewError(Scope, code.SystemTimeoutError, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SystemTimeoutErrorL logs error message and returns Err -func SystemTimeoutErrorL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := SystemTimeoutError(s...) - if filed != nil { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// SystemInternalError xxx6100 returns Err struct -func SystemInternalError(s ...string) *LibError { - return NewError(Scope, code.SystemInternalError, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SystemInternalErrorScope xxx6100 returns Err struct -func SystemInternalErrorScope(scope uint32, s ...string) *LibError { - return NewError(scope, code.SystemInternalError, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SystemInternalErrorL logs error message and returns Err -func SystemInternalErrorL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := SystemInternalError(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -/*** CatInput ***/ - -// InvalidFormat returns Err struct -func InvalidFormat(s ...string) *LibError { - return NewError(Scope, code.InvalidFormat, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InvalidFormatL logs error message and returns Err -func InvalidFormatL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidFormat(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InvalidRange returns Err struct -func InvalidRange(s ...string) *LibError { - return NewError(Scope, code.InvalidRange, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InvalidRangeL logs error message and returns Err -func InvalidRangeL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidRange(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// NotValidImplementation returns Err struct -func NotValidImplementation(s ...string) *LibError { - return NewError(Scope, code.NotValidImplementation, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// NotValidImplementationL logs error message and returns Err -func NotValidImplementationL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := NotValidImplementation(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -/*** CatDB ***/ - -// DBError returns Err -func DBError(s ...string) *LibError { - return NewError(Scope, code.DBError, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -func DBErrorWithScope(scope uint32, s ...string) *LibError { - return NewError(scope, code.DBError, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// DBErrorL logs error message and returns Err -func DBErrorL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := DBError(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - return e -} - -// DBDataConvert returns Err -func DBDataConvert(s ...string) *LibError { - return NewError(Scope, code.DBDataConvert, defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// DBDataConvertL logs error message and returns Err -func DBDataConvertL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := DBDataConvert(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// DBDuplicate returns Err -func DBDuplicate(s ...string) *LibError { - return NewError(Scope, code.DBDuplicate, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// DBDuplicateL logs error message and returns Err -func DBDuplicateL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := DBDuplicate(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -/*** CatResource ***/ - -// ResourceNotFound returns Err and logging -func ResourceNotFound(s ...string) *LibError { - return NewError(Scope, code.ResourceNotFound, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceNotFoundL logs error message and returns Err -func ResourceNotFoundL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceNotFound(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InvalidResourceFormat returns Err -func InvalidResourceFormat(s ...string) *LibError { - return NewError(Scope, code.InvalidResourceFormat, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InvalidResourceFormatL logs error message and returns Err -func InvalidResourceFormatL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidResourceFormat(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InvalidResourceState returns status not correct. -// for example: company should be destroy, agent should be no-sensor/fail-install ... -func InvalidResourceState(s ...string) *LibError { - return NewError(Scope, code.InvalidResourceState, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InvalidResourceStateL logs error message and returns status not correct. -func InvalidResourceStateL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidResourceState(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -func ResourceInsufficient(s ...string) *LibError { - return NewError(Scope, code.ResourceInsufficient, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -func ResourceInsufficientL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceInsufficient(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InsufficientPermission returns Err -func InsufficientPermission(s ...string) *LibError { - return NewError(Scope, code.InsufficientPermission, - defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InsufficientPermissionL returns Err and log -func InsufficientPermissionL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InsufficientPermission(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// UserSuspended returns Err -func UserSuspended(scope uint32, s ...string) *LibError { - return NewError(scope, code.UserSuspended, - defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceAlreadyExist returns Err -func ResourceAlreadyExist(s ...string) *LibError { - return NewError(Scope, code.ResourceAlreadyExist, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceAlreadyExistWithScope returns Err -func ResourceAlreadyExistWithScope(scope uint32, s ...string) *LibError { - return NewError(scope, code.ResourceAlreadyExist, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceAlreadyExistL logs error message and returns Err -func ResourceAlreadyExistL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceAlreadyExist(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InvalidMeasurementID returns Err -func InvalidMeasurementID(s ...string) *LibError { - return NewError(Scope, code.InvalidMeasurementID, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InvalidMeasurementIDL logs error message and returns Err -func InvalidMeasurementIDL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidMeasurementID(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// ResourceExpired returns Err -func ResourceExpired(s ...string) *LibError { - return NewError(Scope, code.ResourceExpired, - defaultDetailCode, fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceExpiredL logs error message and returns Err -func ResourceExpiredL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceExpired(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// ResourceMigrated returns Err -func ResourceMigrated(s ...string) *LibError { - return NewError(Scope, code.ResourceMigrated, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ResourceMigratedL logs error message and returns Err -func ResourceMigratedL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceMigrated(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InsufficientQuota returns Err -func InsufficientQuota(s ...string) *LibError { - return NewError(Scope, code.InsufficientQuota, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// InsufficientQuotaL logs error message and returns Err -func InsufficientQuotaL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InsufficientQuota(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -/*** CatAuth ***/ - -// Unauthorized returns Err -func Unauthorized(s ...string) *LibError { - return NewError(Scope, code.Unauthorized, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// UnauthorizedL logs error message and returns Err -func UnauthorizedL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := Unauthorized(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// AuthExpired returns Err -func AuthExpired(s ...string) *LibError { - return NewError(Scope, code.AuthExpired, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// AuthExpiredL logs error message and returns Err -func AuthExpiredL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := AuthExpired(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// InvalidPosixTime returns Err -func InvalidPosixTime(s ...string) *LibError { - return NewError(Scope, code.InvalidPosixTime, defaultDetailCode, - fmt.Sprintf("i%s", strings.Join(s, " "))) -} - -// InvalidPosixTimeL logs error message and returns Err -func InvalidPosixTimeL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidPosixTime(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// SigAndPayloadNotMatched returns Err -func SigAndPayloadNotMatched(s ...string) *LibError { - return NewError(Scope, code.SigAndPayloadNotMatched, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SigAndPayloadNotMatchedL logs error message and returns Err -func SigAndPayloadNotMatchedL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := SigAndPayloadNotMatched(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// Forbidden returns Err -func Forbidden(s ...string) *LibError { - return NewError(Scope, code.Forbidden, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// ForbiddenL logs error message and returns Err -func ForbiddenL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := Forbidden(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// IsAuthUnauthorizedError check the err is unauthorized error -func IsAuthUnauthorizedError(err *LibError) bool { - switch err.Code() / 100 { - case code.Unauthorized, code.AuthExpired, code.InvalidPosixTime, - code.SigAndPayloadNotMatched, code.Forbidden, - code.InvalidFormat, code.ResourceNotFound: - return true - default: - return false - } -} - -/*** CatPubSub ***/ - -// Publish returns Err -func Publish(s ...string) *LibError { - return NewError(Scope, code.Publish, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// PublishL logs error message and returns Err -func PublishL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := Publish(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - return e -} - -// Consume returns Err -func Consume(s ...string) *LibError { - return NewError(Scope, code.Consume, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -func ConsumeL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := Consume(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// MsgSizeTooLarge returns Err -func MsgSizeTooLarge(s ...string) *LibError { - return NewError(Scope, code.MsgSizeTooLarge, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// MsgSizeTooLargeL logs error message and returns Err -func MsgSizeTooLargeL(l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := MsgSizeTooLarge(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -func TooManyWithScope(scope uint32, s ...string) *LibError { - return NewError(scope, code.TooManyRequest, defaultDetailCode, - fmt.Sprintf("%s", strings.Join(s, " "))) -} diff --git a/pkg/library/errs/easy_func_test.go b/pkg/library/errs/easy_func_test.go deleted file mode 100644 index d18359a..0000000 --- a/pkg/library/errs/easy_func_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package errs - -import ( - "context" - "testing" - - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" -) - -func TestFromError(t *testing.T) { - t.Run("nil error", func(t *testing.T) { - if err := FromError(nil); err != nil { - t.Errorf("expected nil, got %v", err) - } - }) - - t.Run("LibError type", func(t *testing.T) { - libErr := NewError(1, 200, 10, "test error") - if err := FromError(libErr); err != libErr { - t.Errorf("expected %v, got %v", libErr, err) - } - }) -} - -func TestFromCode(t *testing.T) { - tests := []struct { - name string - code uint32 - expected *LibError - }{ - {"valid code", 1200314, NewError(12, 3, 14, "")}, - {"invalid code", 9999999, NewError(99, 999, 99, "")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := FromCode(tt.code) - if err.FullCode() != tt.expected.FullCode() { - t.Errorf("expected %v, got %v", tt.expected.FullCode(), err.FullCode()) - } - }) - } -} - -func TestSystemTimeoutError(t *testing.T) { - tests := []struct { - name string - input []string - expected string - }{ - {"single string", []string{"timeout"}, "timeout"}, - {"multiple strings", []string{"timeout", "occurred"}, "timeout occurred"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := SystemTimeoutError(tt.input...) - if err.Error() != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, err.Error()) - } - }) - } -} - -func TestSystemInternalErrorL(t *testing.T) { - tests := []struct { - name string - input []string - }{ - {"internal error", []string{"internal error"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.TODO() - err := SystemInternalErrorL(logx.WithContext(ctx), nil, tt.input...) - if err.Error() != tt.input[0] { - t.Errorf("expected %s, got %s", tt.input[0], err.Error()) - } - }) - } -} - -func TestInvalidFormatL(t *testing.T) { - mockLogger := logx.WithContext(context.Background()) - tests := []struct { - name string - input []string - expected string - }{ - {"invalid format", []string{"invalid format"}, "invalid format"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := InvalidFormatL(mockLogger, nil, tt.input...) - if err.Error() != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, err.Error()) - } - }) - } -} - -func TestDBErrorL(t *testing.T) { - mockLogger := logx.WithContext(context.Background()) - tests := []struct { - name string - input []string - expected string - }{ - {"DB error", []string{"DB error"}, "DB error"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := DBErrorL(mockLogger, nil, tt.input...) - if err.Error() != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, err.Error()) - } - }) - } -} - -func TestResourceNotFoundL(t *testing.T) { - mockLogger := logx.WithContext(context.Background()) - tests := []struct { - name string - input []string - expected string - }{ - {"resource not found", []string{"resource not found"}, "resource not found"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ResourceNotFoundL(mockLogger, nil, tt.input...) - if err.Error() != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, err.Error()) - } - }) - } -} - -func TestInsufficientPermissionL(t *testing.T) { - mockLogger := logx.WithContext(context.Background()) - tests := []struct { - name string - input []string - expected string - }{ - {"insufficient permission", []string{"permission denied"}, "permission denied"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := InsufficientPermissionL(mockLogger, nil, tt.input...) - if err.Error() != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, err.Error()) - } - }) - } -} - -func TestIsAuthUnauthorizedError(t *testing.T) { - tests := []struct { - name string - err *LibError - expected bool - }{ - {"Unauthorized error", NewError(1, code.Unauthorized, 0, ""), true}, - {"AuthExpired error", NewError(1, code.AuthExpired, 0, ""), true}, - {"Other error", NewError(1, code.ArkInternal, 0, ""), false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res := IsAuthUnauthorizedError(tt.err) - if res != tt.expected { - t.Errorf("expected %t, got %t", tt.expected, res) - } - }) - } -} diff --git a/pkg/library/errs/error_code.go b/pkg/library/errs/error_code.go deleted file mode 100644 index 6a0a1df..0000000 --- a/pkg/library/errs/error_code.go +++ /dev/null @@ -1,89 +0,0 @@ -package errs - -import ( - "fmt" - "strings" - - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ErrorCode uint32 - -func (e ErrorCode) ToUint32() uint32 { - return uint32(e) -} - -func ThirdPartyError(scope uint32, ec ErrorCode, s ...string) *LibError { - return NewError(scope, code.ThirdParty, ec.ToUint32(), fmt.Sprintf("thirty error: %s", strings.Join(s, " "))) -} - -func ThirdPartyErrorL(scope uint32, ec ErrorCode, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ThirdPartyError(scope, ec, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} - -func DatabaseErrorWithScope(scope uint32, ec ErrorCode, s ...string) *LibError { - return NewError(scope, code.DBError, ec.ToUint32(), strings.Join(s, " ")) -} - -func DatabaseErrorWithScopeL(scope uint32, - ec ErrorCode, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := DatabaseErrorWithScope(scope, ec, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} - -func ResourceNotFoundWithScope(scope uint32, ec ErrorCode, s ...string) *LibError { - return NewError(scope, code.ResourceNotFound, ec.ToUint32(), fmt.Sprintf("resource not found: %s", strings.Join(s, " "))) -} - -func ResourceNotFoundWithScopeL(scope uint32, ec ErrorCode, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ResourceNotFoundWithScope(scope, ec, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} - -func InvalidRangeWithScope(scope uint32, ec ErrorCode, s ...string) *LibError { - return NewError(scope, code.CatInput, ec.ToUint32(), fmt.Sprintf("invalid range: %s", strings.Join(s, " "))) -} - -func InvalidRangeWithScopeL(scope uint32, ec ErrorCode, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidRangeWithScope(scope, ec, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} - -func InvalidFormatWithScope(scope uint32, s ...string) *LibError { - return NewError(scope, code.CatInput, code.InvalidFormat, fmt.Sprintf("invalid range: %s", strings.Join(s, " "))) -} - -func InvalidFormatWithScopeL(scope uint32, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := InvalidFormatWithScope(scope, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} - -func ForbiddenWithScope(scope uint32, ec ErrorCode, s ...string) *LibError { - return NewError(scope, code.Forbidden, ec.ToUint32(), fmt.Sprintf("forbidden: %s", strings.Join(s, " "))) -} - -func ForbiddenWithScopeL(scope uint32, ec ErrorCode, - l logx.Logger, filed []logx.LogField, s ...string) *LibError { - e := ForbiddenWithScope(scope, ec, s...) - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - - return e -} diff --git a/pkg/library/errs/errors.go b/pkg/library/errs/errors.go deleted file mode 100644 index 95cf8c2..0000000 --- a/pkg/library/errs/errors.go +++ /dev/null @@ -1,223 +0,0 @@ -package errs - -import ( - "errors" - "fmt" - "net/http" - - "backend/pkg/library/errs/code" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// Scope 全域變數應由服務或模組設置 -var Scope = code.Unset - -// LibError 7 碼,服務 2 碼,詳細錯誤 2 碼 ,Cat 3 碼 不參與,獨立的 code 組成為( 000 category + 00 detail) -type LibError struct { - scope uint32 // 系統代號,*100000 來操作,顯示時不夠會補足 7 位數, Library 定義 -> 輸入時都只要輸入兩位數 - category uint32 // 類別代碼 Library 定義 -> 不客製化業務訊息時,用這個大類別來給錯誤 3 碼 - code uint32 // 細項 ,每個 repo 裡自行定義 -> 一萬以下都可以 2 碼 - msg string // 顯示用的,給前端看的 Msg - internalErr error // 紀錄且包含真正的錯誤,通常用這個 -} - -// Error 是錯誤的介面 -// 私有屬性 "displayMsg" 的 getter 函數,這邊只顯示業務邏輯錯誤,因為可能會帶出去給客戶端 -// 要如何定位系統真的發生什麼錯?請使用 internalErr 來做定位,通常是會印 Log 的所以用這個 -func (e *LibError) Error() string { - if e == nil { - return "" - } - - return e.msg -} - -// Category 私有屬性 "category" 的 getter 函數 -func (e *LibError) Category() uint32 { - if e == nil { - return 0 - } - - return e.category -} - -// Scope 私有屬性 "scope" 的 getter 函數 -func (e *LibError) Scope() uint32 { - if e == nil { - return code.Unset - } - - return e.scope -} - -// Code 私有屬性 "code" 的 getter 函數 -func (e *LibError) Code() uint32 { - if e == nil { - return code.OK - } - - return e.code -} - -func (e *LibError) FullCode() uint32 { - if e == nil { - return 0 - } - - return e.Scope()*100000 + e.Code() -} - -// DisplayErrorCode 要顯示的 Error Code -func (e *LibError) DisplayErrorCode() string { - if e == nil { - return "000000" - } - - return fmt.Sprintf("%06d", e.FullCode()) -} - -// InternalError 帶入真正的 error -func (e *LibError) InternalError() error { - var err error = fmt.Errorf("failed to get internal error") - if e == nil { - return err - } - - if e.internalErr != nil { - err = e.internalErr - } - - return err -} - -// GeneralError 轉換 category 級別錯誤訊息,模糊化 -func (e *LibError) GeneralError() string { - if e == nil { - return "" - } - - errStr, ok := code.CatToStr[e.Category()] - if !ok { - return "" - } - - return errStr -} - -// Is 在執行 errors.Is() 時調用。 -// 除非你非常確定你在做什麼,否則不要直接使用這個函數。 -// 請使用 errors.Is 代替。 -// 此函數比較兩個錯誤變量是否都是 *Err,並且具有相同的 code(不檢查包裹的內部錯誤) -func (e *LibError) Is(f error) bool { - var err *LibError - ok := errors.As(f, &err) - if !ok { - return false - } - - return e.Code() == err.Code() -} - -// Unwrap 返回底層錯誤 -// 解除包裹錯誤的結果本身可能具有 Unwrap 方法; -// 我們稱通過反覆解除包裹產生的錯誤序列為錯誤鏈。 -func (e *LibError) Unwrap() error { - if e == nil { - return nil - } - - return e.internalErr -} - -// Wrap 將內部錯誤設置到 Err 結構 -func (e *LibError) Wrap(internalErr error) *LibError { - if e != nil { - e.internalErr = internalErr - } - - return e -} - -func (e *LibError) GRPCStatus() *status.Status { - if e == nil { - return status.New(codes.OK, "") - } - - return status.New(codes.Code(e.FullCode()), e.Error()) -} - -// HTTPStatus 返回對應的 HTTP 狀態碼 -func (e *LibError) HTTPStatus() int { - // 如果錯誤為空或錯誤碼為 OK,則返回 200 狀態碼 - if e == nil || e.Code() == code.OK { - return http.StatusOK - } - - // 將 code 轉換為與常量定義相同的格式 (category + detail) - // 例如:code=3004 -> (3004%100) + 30 = 4 + 30 = 34 - codeValue := (e.Code() % 100) + e.Category() - - // 根據錯誤碼判斷對應的 HTTP 狀態碼 - switch codeValue { - case code.ResourceInsufficient, code.InvalidFormat: - // 如果資源不足,返回 400 狀態碼 - return http.StatusBadRequest - case code.Unauthorized, code.InsufficientPermission: - // 如果未授權或權限不足,返回 401 狀態碼 - return http.StatusUnauthorized - case code.InsufficientQuota: - // 如果配額不足,返回 402 狀態碼 - return http.StatusPaymentRequired - case code.InvalidPosixTime, code.Forbidden, code.UserSuspended: - // 如果時間無效或禁止訪問,返回 403 狀態碼 - return http.StatusForbidden - case code.ResourceNotFound: - // 如果資源未找到,返回 404 狀態碼 - return http.StatusNotFound - case code.ResourceAlreadyExist, code.InvalidResourceState: - // 如果資源已存在或狀態無效,返回 409 狀態碼 - return http.StatusConflict - case code.NotValidImplementation: - // 如果實現無效,返回 501 狀態碼 - return http.StatusNotImplemented - case code.TooManyRequest: - // 如果實現無效,返回 501 狀態碼 - return http.StatusTooManyRequests - default: - // 如果沒有匹配的錯誤碼,則繼續下一步 - } - - // 根據錯誤的類別判斷對應的 HTTP 狀態碼 - switch e.Category() { - case code.CatInput: - // 如果錯誤屬於輸入錯誤類別,返回 400 狀態碼 - return http.StatusBadRequest - default: - // 如果沒有符合的條件,返回 500 狀態碼 - return http.StatusInternalServerError - } -} - -// NewError 創建新的 Error -// 確保 category 在 0 到 999 之間,將超出的 category 設為最大值 999 -// 確保 detail 在 0 到 99 之間,將超出的 detail 設為最大值 99 -func NewError(scope, category, detail uint32, displayMsg string) *LibError { - // 確保 category 在 0 到 999 之間 - if category > 999 { - category = 999 // 將超出的 category 設為最大值 999 - } - - // 確保 detail 在 0 到 99 之間 - if detail > 99 { - detail = 99 // 將超出的 detail 設為最大值 99 - } - - return &LibError{ - category: category, - code: category*100 + detail, - scope: scope, - msg: displayMsg, - } -} diff --git a/pkg/library/errs/errors_test.go b/pkg/library/errs/errors_test.go deleted file mode 100644 index dbbff07..0000000 --- a/pkg/library/errs/errors_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package errs - -import ( - "errors" - "net/http" - "testing" - - "backend/pkg/library/errs/code" - - "google.golang.org/grpc/codes" -) - -func TestNewError(t *testing.T) { - tests := []struct { - name string - scope uint32 - category uint32 - detail uint32 - displayMsg string - expectedMsg string - expectedCat uint32 - expectedDet uint32 - }{ - {"valid error", 1, 200, 10, "test error", "test error", 200, 20010}, - {"category overflow", 1, 1000, 10, "test error", "test error", 999, 99910}, - {"detail overflow", 1, 200, 150, "test error", "test error", 200, 20099}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := NewError(tt.scope, tt.category, tt.detail, tt.displayMsg) - if err.Error() != tt.expectedMsg { - t.Errorf("expected %s, got %s", tt.expectedMsg, err.Error()) - } - if err.Category() != tt.expectedCat { - t.Errorf("expected category %d, got %d", tt.expectedCat, err.Category()) - } - if err.Code() != tt.expectedDet { - t.Errorf("expected code %d, got %d", tt.expectedDet, err.Code()) - } - }) - } -} - -func TestLibError_FullCode(t *testing.T) { - tests := []struct { - name string - scope uint32 - category uint32 - detail uint32 - expectedCode uint32 - }{ - {"valid code", 1, 200, 10, 120010}, - {"category overflow", 1, 1000, 10, 199910}, - {"detail overflow", 1, 200, 150, 120099}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := NewError(tt.scope, tt.category, tt.detail, "test") - if err.FullCode() != tt.expectedCode { - t.Errorf("expected %d, got %d", tt.expectedCode, err.FullCode()) - } - }) - } -} - -func TestLibError_HTTPStatus(t *testing.T) { - tests := []struct { - name string - err *LibError - expected int - }{ - {"bad request - ResourceInsufficient", NewError(1, code.CatResource, 4, "bad request"), http.StatusBadRequest}, - {"unauthorized", NewError(1, code.CatAuth, 1, "unauthorized"), http.StatusUnauthorized}, - {"forbidden", NewError(1, code.CatAuth, 5, "forbidden"), http.StatusForbidden}, - {"not found", NewError(1, code.CatResource, 1, "not found"), http.StatusNotFound}, - {"internal server error", NewError(1, code.CatDB, 95, "db error"), http.StatusInternalServerError}, - {"input err", NewError(1, code.CatInput, 1, "input error"), http.StatusBadRequest}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if status := tt.err.HTTPStatus(); status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} - -func TestLibError_Error(t *testing.T) { - err := NewError(0, 100, 5, "test error") - expected := "test error" - if err.Error() != expected { - t.Errorf("expected '%s', got '%s'", expected, err.Error()) - } -} - -func TestLibError_Is(t *testing.T) { - err1 := NewError(0, 1, 1, "error 1") - err2 := NewError(0, 1, 1, "error 2") - err3 := errors.New("other error") - - if !err1.Is(err2) { - t.Error("expected errors to be equal") - } - - if err1.Is(err3) { - t.Error("expected errors to not be equal") - } -} - -func TestLibError_DisplayErrorCode(t *testing.T) { - tests := []struct { - name string - err *LibError - expected string - }{ - {"valid code", NewError(1, 200, 10, "test error"), "120010"}, - {"nil error", nil, "000000"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if code := tt.err.DisplayErrorCode(); code != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, code) - } - }) - } -} - -func TestLibError_Unwrap(t *testing.T) { - originalErr := errors.New("original error") - libErr := NewError(0, 1, 1, "wrapped error").Wrap(originalErr) - - if unwrappedErr := libErr.Unwrap(); unwrappedErr != originalErr { - t.Errorf("expected original error, got %v", unwrappedErr) - } -} - -func TestLibError_InternalError(t *testing.T) { - tests := []struct { - name string - internalErr error - expected error - }{ - {"valid internal error", errors.New("internal"), errors.New("internal")}, - {"nil internal error", nil, errors.New("failed to get internal error")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := NewError(1, 200, 10, "test").Wrap(tt.internalErr) - if internalErr := err.InternalError(); internalErr.Error() != tt.expected.Error() { - t.Errorf("expected %v, got %v", tt.expected, internalErr) - } - }) - } -} - -func TestLibError_GRPCStatus(t *testing.T) { - tests := []struct { - name string - err *LibError - expected codes.Code - }{ - {"valid GRPC status", NewError(1, 200, 10, "test error"), codes.Code(120010)}, - {"nil error", nil, codes.OK}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if status := tt.err.GRPCStatus().Code(); status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} diff --git a/pkg/member/domain/errors.go b/pkg/member/domain/errors.go deleted file mode 100644 index 45641c2..0000000 --- a/pkg/member/domain/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -package domain - -import "backend/pkg/library/errs" - -// Verify Error Code -const ( - FailedToVerifyGoogle errs.ErrorCode = iota + 1 - FailedToVerifyGoogleTimeout - FailedToVerifyGoogleHTTPCode - FailedToVerifyGoogleTokenExpired - FailedToVerifyGoogleInvalidAudience - FailedToVerifyLine -) - -// PWS Error Code -const ( - HashPasswordErrorCode = 10 + iota - InsertAccountErrorCode - BindingUserTabletErrorCode - FailedToFindAccountErrorCode - FailedToBindAccountErrorCode - FailedToIncAccountErrorCode - FailedFindUIDByLoginIDErrorCode - FailedFindOneByAccountErrorCode - FailedToUpdatePasswordErrorCode - FailedToFindUserErrorCode - FailedToUpdateUserErrorCode - FailedToUpdateUserStatusErrorCode - FailedToGetUserInfoErrorCode - FailedToGetVerifyCodeErrorCode - FailedToGetCodeOnRedisErrorCode - FailedToGetCodeCorrectErrorCode -) diff --git a/pkg/member/mock/repository/account_uid.go b/pkg/member/mock/repository/account_uid.go index 1d5793c..1158742 100644 --- a/pkg/member/mock/repository/account_uid.go +++ b/pkg/member/mock/repository/account_uid.go @@ -72,6 +72,21 @@ func (mr *MockAccountUIDRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindOne), ctx, id) } +// FindOneByUID mocks base method. +func (m *MockAccountUIDRepository) FindOneByUID(ctx context.Context, uid string) (*entity.AccountUID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneByUID", ctx, uid) + ret0, _ := ret[0].(*entity.AccountUID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneByUID indicates an expected call of FindOneByUID. +func (mr *MockAccountUIDRepositoryMockRecorder) FindOneByUID(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByUID", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindOneByUID), ctx, uid) +} + // FindUIDByLoginID mocks base method. func (m *MockAccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) { m.ctrl.T.Helper() diff --git a/pkg/member/repository/account_test.go b/pkg/member/repository/account_test.go index 1e112ff..5a102fc 100644 --- a/pkg/member/repository/account_test.go +++ b/pkg/member/repository/account_test.go @@ -16,7 +16,7 @@ import ( "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" "github.com/zeromicro/go-zero/core/stores/redis" ) @@ -131,7 +131,7 @@ func TestAccountModel_FindOne(t *testing.T) { } err = repo.Insert(context.TODO(), account) assert.NoError(t, err, "插入應成功") - nid := primitive.NewObjectID() + nid := bson.NewObjectID() t.Logf("Inserted account ID: %s, nid:%s", account.ID.Hex(), nid.Hex()) tests := []struct { diff --git a/pkg/member/repository/account_uid_test.go b/pkg/member/repository/account_uid_test.go index 264f595..b247e77 100644 --- a/pkg/member/repository/account_uid_test.go +++ b/pkg/member/repository/account_uid_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/redis" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) func SetupTestAccountUIDRepository(db string) (repository.AccountUIDRepository, func(), error) { @@ -156,7 +156,7 @@ func TestDefaultAccountUidModel_FindOne(t *testing.T) { }, { name: "Non-existent ObjectID", - id: primitive.NewObjectID().Hex(), + id: bson.NewObjectID().Hex(), expectError: true, }, } diff --git a/pkg/member/repository/user.go b/pkg/member/repository/user.go index 95ffd6a..3272528 100644 --- a/pkg/member/repository/user.go +++ b/pkg/member/repository/user.go @@ -293,7 +293,7 @@ func (repo *UserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, em // 不常寫,再找一次可接受 id := repo.UIDToID(ctx, uid) if id == "" { - return errors.New("invalid uid") + return fmt.Errorf("invalid uid") } rk := domain.GetUserRedisKey(id) @@ -326,14 +326,14 @@ func (repo *UserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, ph // 不常寫,再找一次可接受 id := repo.UIDToID(ctx, uid) if id == "" { - return errors.New("invalid uid") + return fmt.Errorf("invalid uid") } rk := domain.GetUserRedisKey(id) // 執行更新操作 result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]}) if err != nil { - return fmt.Errorf("failed to update status for uid %s: %w", uid, err) + return err } // 檢查更新結果,若沒有匹配的文檔,則返回錯誤 diff --git a/pkg/member/repository/user_test.go b/pkg/member/repository/user_test.go index 2bbbc7f..985f223 100644 --- a/pkg/member/repository/user_test.go +++ b/pkg/member/repository/user_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/redis" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -243,7 +243,7 @@ func TestCustomUserModel_FindOne(t *testing.T) { }, { name: "Non-existent ObjectID", - id: primitive.NewObjectID().Hex(), + id: bson.NewObjectID().Hex(), expectError: true, }, } diff --git a/pkg/member/usecase/account.go b/pkg/member/usecase/account.go index 07f1e14..47649d8 100644 --- a/pkg/member/usecase/account.go +++ b/pkg/member/usecase/account.go @@ -1,10 +1,13 @@ package usecase import ( + errs "backend/pkg/library/errors" "backend/pkg/member/domain/config" "backend/pkg/member/domain/repository" "backend/pkg/member/domain/usecase" + repo "backend/pkg/member/repository" "context" + "errors" ) type MemberUseCaseParam struct { @@ -14,6 +17,7 @@ type MemberUseCaseParam struct { VerifyCodeModel repository.VerifyCodeRepository GenerateUID repository.AutoIDRepository Config config.Config + Logger errs.Logger } type MemberUseCase struct { @@ -29,7 +33,21 @@ func MustMemberUseCase(param MemberUseCaseParam) usecase.AccountUseCase { func (use *MemberUseCase) FindLoginIDByUID(ctx context.Context, uid string) (usecase.BindingUser, error) { data, err := use.AccountUID.FindOneByUID(ctx, uid) if err != nil { - return usecase.BindingUser{}, err + if errors.Is(err, repo.ErrNotFound) { + e := errs.ResNotFoundError("failed to find user by uid: " + uid) + + return usecase.BindingUser{}, e + } + + e := errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "uid", Val: uid}, + {Key: "func", Val: "AccountUID.FindOneByUID"}, + {Key: "err", Val: err.Error()}, + }, + "failed to use database") + + return usecase.BindingUser{}, e } return usecase.BindingUser{ diff --git a/pkg/member/usecase/binding.go b/pkg/member/usecase/binding.go index de75822..5f6cab1 100644 --- a/pkg/member/usecase/binding.go +++ b/pkg/member/usecase/binding.go @@ -1,18 +1,14 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "errors" "fmt" - "backend/pkg/member/domain" "backend/pkg/member/domain/entity" "backend/pkg/member/domain/usecase" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/mon" ) @@ -35,14 +31,11 @@ func (use *MemberUseCase) BindUserInfo(ctx context.Context, req usecase.CreateUs // Insert 新增 if err := use.User.Insert(ctx, insert); err != nil { - e := errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.BindingUserTabletErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.Insert"}, - {Key: "err", Value: err.Error()}, + e := errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.Insert"}, + {Key: "err", Val: err.Error()}, }, "failed to binding user info").Wrap(err) @@ -56,23 +49,18 @@ func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUs // 先確定有這個Account _, err := use.Account.FindOneByAccount(ctx, req.LoginID) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToFindAccountErrorCode, + e = errs.ResNotFoundError( fmt.Sprintf("failed to insert account: %s", req.UID), ) default: - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToFindAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.FindOneByAccount"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.FindOneByAccount"}, + {Key: "err", Val: err.Error()}, }, "failed to find account").Wrap(err) } @@ -95,14 +83,11 @@ func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUs UID: uid, Type: req.Type, }); err != nil { - e := errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToBindAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.Insert"}, - {Key: "err", Value: err.Error()}, + e := errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.Insert"}, + {Key: "err", Val: err.Error()}, }, "failed to bind account").Wrap(err) @@ -119,9 +104,7 @@ func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUs func (use *MemberUseCase) BindVerifyEmail(ctx context.Context, uid, email string) error { err := use.User.UpdateEmailVerifyStatus(ctx, uid, email) if err != nil { - e := errs.DatabaseErrorWithScope( - code.CloudEPMember, - domain.FailedToFindAccountErrorCode, + e := errs.DBErrorError( fmt.Sprintf("failed to Binding uid: %s, email: %s", uid, email), ) @@ -134,9 +117,12 @@ func (use *MemberUseCase) BindVerifyEmail(ctx context.Context, uid, email string func (use *MemberUseCase) BindVerifyPhone(ctx context.Context, uid, phone string) error { err := use.User.UpdatePhoneVerifyStatus(ctx, uid, phone) if err != nil { - e := errs.DatabaseErrorWithScope( - code.CloudEPMember, - domain.FailedToFindAccountErrorCode, + e := errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "req", Val: uid}, + {Key: "func", Val: "User.UpdatePhoneVerifyStatus"}, + {Key: "err", Val: err.Error()}, + }, fmt.Sprintf("failed to Binding uid: %s, phone: %s", uid, phone), ) diff --git a/pkg/member/usecase/generate.go b/pkg/member/usecase/generate.go index e239457..f5ff16a 100644 --- a/pkg/member/usecase/generate.go +++ b/pkg/member/usecase/generate.go @@ -1,32 +1,26 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "math" - "backend/pkg/member/domain" "backend/pkg/member/domain/entity" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" - "github.com/zeromicro/go-zero/core/logx" ) func (use *MemberUseCase) Generate(ctx context.Context) (string, error) { var data entity.AutoID err := use.GenerateUID.Inc(ctx, &data) if err != nil { - e := errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToIncAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "func", Value: "AutoIDModel.Inc"}, - {Key: "err", Value: err.Error()}, + e := errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "func", Val: "AutoIDModel.Inc"}, + {Key: "err", Val: err.Error()}, }, - "failed to inc account num").Wrap(err) + "failed to inc account num") return "", e } @@ -35,12 +29,10 @@ func (use *MemberUseCase) Generate(ctx context.Context) (string, error) { sum := GIDLib.InitAutoID + data.Counter if sum > math.MaxInt64 { return "", - errs.InvalidRangeWithScopeL( - code.CloudEPMember, - domain.FailedToIncAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "func", Value: "MemberUseCase.Generate"}, + errs.InputInvalidRangeErrorL( + use.Logger, + []errs.LogField{ + {Key: "func", Val: "MemberUseCase.Generate"}, }, "sum exceeds the maximum int64 value") } diff --git a/pkg/member/usecase/member.go b/pkg/member/usecase/member.go index f1628f1..0083e1c 100644 --- a/pkg/member/usecase/member.go +++ b/pkg/member/usecase/member.go @@ -1,6 +1,7 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "errors" "fmt" @@ -8,16 +9,11 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo" - "backend/pkg/member/domain" "backend/pkg/member/domain/entity" "backend/pkg/member/domain/member" "backend/pkg/member/domain/repository" "backend/pkg/member/domain/usecase" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/mon" ) @@ -52,11 +48,11 @@ func (use *MemberUseCase) CreateUserAccount(ctx context.Context, req usecase.Cre // validateCreateUserAccountRequest validates the create user account request func (use *MemberUseCase) validateCreateUserAccountRequest(req usecase.CreateLoginUserRequest) error { if req.LoginID == "" { - return errs.InvalidFormatWithScope(code.CloudEPMember, "login ID is required") + return errs.InputInvalidRangeError("login ID is required") } if req.Platform == member.Digimon && req.Token == "" { - return errs.InvalidFormatWithScope(code.CloudEPMember, "password is required for Digimon platform") + return errs.InputInvalidRangeError("password is required for Digimon platform") } return nil @@ -71,12 +67,18 @@ func (use *MemberUseCase) processPasswordForPlatform(platform member.Platform, p // Hash password for Digimon platform token, err := HasPasswordFunc(password, use.Config.Bcrypt.Cost) if err != nil { - return "", errs.NewError( - code.CloudEPMember, - code.CatSystem, - domain.HashPasswordErrorCode, - fmt.Sprintf("failed to encrypt password: %s", err.Error()), - ) + e := errs.SvcInternalErrorL( + use.Logger, + []errs.LogField{ + {Key: "platform", Val: platform}, + {Key: "password", Val: password}, + {Key: "func", Val: "HasPasswordFunc"}, + {Key: "error", Val: err.Error()}, + }, + fmt.Sprintf("failed to encrypt password"), + ).Wrap(err) + + return "", e } return token, nil @@ -88,16 +90,14 @@ func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Acc if err != nil { if mongo.IsDuplicateKeyError(err) { - return errs.ResourceAlreadyExistWithScope(code.CloudEPMember, "account duplicate").Wrap(err) + return errs.ResAlreadyExistError("account duplicate").Wrap(err) } - return errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.InsertAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "Account.Insert"}, - {Key: "err", Value: err.Error()}, + + return errs.DBErrorErrorL(use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "Account.Insert"}, + {Key: "err", Val: err.Error()}, }, "failed to insert to database").Wrap(err) } @@ -108,24 +108,19 @@ func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Acc func (use *MemberUseCase) GetUIDByAccount(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetUIDByAccountResponse, error) { account, err := use.AccountUID.FindUIDByLoginID(ctx, req.Account) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedFindUIDByLoginIDErrorCode, + e = errs.ResNotFoundError( fmt.Sprintf("failed to find uid by account: %s", req.Account), ) default: - // 錯誤代碼 20-201-07 - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedFindUIDByLoginIDErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "AccountUID.FindUIDByLoginID"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "AccountUID.FindUIDByLoginID"}, + {Key: "err", Val: err.Error()}, }, "failed to find uid by account").Wrap(err) } @@ -142,25 +137,21 @@ func (use *MemberUseCase) GetUIDByAccount(ctx context.Context, req usecase.GetUI func (use *MemberUseCase) GetUserAccountInfo(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetAccountInfoResponse, error) { account, err := use.Account.FindOneByAccount(ctx, req.Account) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): // 錯誤代碼 20-301-08 - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedFindOneByAccountErrorCode, + e = errs.ResNotFoundError( fmt.Sprintf("failed to find account: %s", req.Account), ) default: // 錯誤代碼 20-201-08 - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedFindOneByAccountErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "Account.FindOneByAccount"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "Account.FindOneByAccount"}, + {Key: "err", Val: err.Error()}, }, "failed to find account").Wrap(err) } @@ -190,14 +181,11 @@ func (use *MemberUseCase) GetUserInfo(ctx context.Context, req usecase.GetUserIn user, err = use.User.FindOneByNickName(ctx, req.NickName) default: // 驗證至少提供一個查詢參數 - return usecase.UserInfo{}, errs.InvalidFormatWithScope( - code.CloudEPMember, - "UID or NickName must be provided", - ) + return usecase.UserInfo{}, errs.InputInvalidRangeError("UID or NickName must be provided") } // 查詢失敗時處理錯誤 if err != nil { - return usecase.UserInfo{}, handleUserQueryError(ctx, err, req) + return usecase.UserInfo{}, use.handleUserQueryError(ctx, err, req) } // 返回查詢結果 @@ -205,23 +193,17 @@ func (use *MemberUseCase) GetUserInfo(ctx context.Context, req usecase.GetUserIn } // 將查詢錯誤處理邏輯封裝為單獨的函數 -func handleUserQueryError(ctx context.Context, err error, req usecase.GetUserInfoRequest) error { +func (use *MemberUseCase) handleUserQueryError(ctx context.Context, err error, req usecase.GetUserInfoRequest) error { if errors.Is(err, mon.ErrNotFound) { - return errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToGetUserInfoErrorCode, - fmt.Sprintf("user not found: %s", req.UID), - ) + return errs.ResNotFoundError(fmt.Sprintf("user not found: %s", req.UID)) } - return errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToGetUserInfoErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "MemberUseCase.GetUserInfo"}, - {Key: "err", Value: err.Error()}, + return errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "MemberUseCase.GetUserInfo"}, + {Key: "err", Val: err.Error()}, }, "failed to query user info").Wrap(err) } @@ -255,12 +237,7 @@ func (use *MemberUseCase) UpdateUserToken(ctx context.Context, req usecase.Updat // 密碼加密 token, e := HasPasswordFunc(req.Token, use.Config.Bcrypt.Cost) if e != nil { - return errs.NewError( - code.CloudEPMember, - code.CatSystem, - domain.HashPasswordErrorCode, - fmt.Sprintf("failed to encrypt err: %s", e.Error()), - ) + return errs.SysInternalError(fmt.Sprintf("failed to encrypt err: %s", e.Error())) } toInt8, err := safeInt64ToInt8(req.Platform) if err != nil { @@ -268,25 +245,17 @@ func (use *MemberUseCase) UpdateUserToken(ctx context.Context, req usecase.Updat } err = use.Account.UpdateTokenByLoginID(ctx, req.Account, token, member.Platform(toInt8)) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): - // 錯誤代碼 20-301-08 - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToUpdatePasswordErrorCode, - fmt.Sprintf("failed to upadte password since account not found: %s", req.Account), - ) + e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.Account)) default: - // 錯誤代碼 20-201-02 - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToUpdatePasswordErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "Account.UpdateTokenByLoginID"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "Account.UpdateTokenByLoginID"}, + {Key: "err", Val: err.Error()}, }, "failed to update password").Wrap(err) } @@ -312,23 +281,17 @@ func (use *MemberUseCase) UpdateUserInfo(ctx context.Context, req *usecase.Updat Currency: req.Currency, }) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToUpdateUserErrorCode, - fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID), - ) + e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.UID)) default: - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToUpdateUserErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.UpdateUserDetailsByUid"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.UpdateUserDetailsByUid"}, + {Key: "err", Val: err.Error()}, }, "failed to update user info").Wrap(err) } @@ -342,24 +305,18 @@ func (use *MemberUseCase) UpdateUserInfo(ctx context.Context, req *usecase.Updat func (use *MemberUseCase) UpdateStatus(ctx context.Context, req usecase.UpdateStatusRequest) error { err := use.User.UpdateStatus(ctx, req.UID, req.Status.ToInt32()) if err != nil { - var e *errs.LibError + var e *errs.Error switch { case errors.Is(err, mon.ErrNotFound): - e = errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToFindUserErrorCode, - fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID), - ) + e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.UID)) default: - e = errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToUpdateUserStatusErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.UpdateStatus"}, - {Key: "err", Value: err.Error()}, + e = errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.UpdateStatus"}, + {Key: "err", Val: err.Error()}, }, "failed to update user info").Wrap(err) } @@ -380,14 +337,12 @@ func (use *MemberUseCase) ListMember(ctx context.Context, req usecase.ListUserIn PageIndex: req.PageIndex, }) if err != nil { - e := errs.DatabaseErrorWithScopeL( - code.CloudEPMember, - domain.FailedToGetUserInfoErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "User.ListMembers"}, - {Key: "err", Value: err.Error()}, + e := errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "func", Val: "User.ListMembers"}, + {Key: "err", Val: err.Error()}, }, "failed to list members").Wrap(err) diff --git a/pkg/member/usecase/member_test.go b/pkg/member/usecase/member_test.go index f74a874..e392c4c 100644 --- a/pkg/member/usecase/member_test.go +++ b/pkg/member/usecase/member_test.go @@ -1,20 +1,16 @@ package usecase import ( - "context" - "errors" - "testing" - - "backend/pkg/member/domain" + errs "backend/pkg/library/errors" "backend/pkg/member/domain/config" "backend/pkg/member/domain/entity" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" mockRepo "backend/pkg/member/mock/repository" "backend/pkg/member/repository" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + "context" + "errors" + "testing" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/mon" @@ -154,12 +150,7 @@ func TestGetUIDByAccount(t *testing.T) { mockSetup: func() { mockAccountUIDRepo.EXPECT(). FindUIDByLoginID(gomock.Any(), "notfounduser"). - Return(nil, errs.NewError( - code.CloudEPMember, - code.CatResource, - domain.FailedFindUIDByLoginIDErrorCode, - "account not found", - )) + Return(nil, errs.ResNotFoundError("account not found")) }, wantResp: usecase.GetUIDByAccountResponse{}, wantErr: true, diff --git a/pkg/member/usecase/verify.go b/pkg/member/usecase/verify.go index 6fc8198..36f6385 100644 --- a/pkg/member/usecase/verify.go +++ b/pkg/member/usecase/verify.go @@ -1,25 +1,18 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "fmt" - "backend/pkg/member/domain" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" ) func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase.GenerateRefreshCodeRequest) (usecase.GenerateRefreshCodeResponse, error) { checkType, status := member.GetCodeNameByCode(param.CodeType) if !status { - e := errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToGetVerifyCodeErrorCode, - fmt.Sprintf("failed to get verify code type: %d", param.CodeType), - ) + e := errs.ResNotFoundError(fmt.Sprintf("failed to get verify code type: %d", param.CodeType)) return usecase.GenerateRefreshCodeResponse{}, e } @@ -32,17 +25,13 @@ func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase if vc == "" { vc, err = generateVerifyCode(6) if err != nil { - return usecase.GenerateRefreshCodeResponse{}, errs.SystemInternalError(err.Error()) + return usecase.GenerateRefreshCodeResponse{}, errs.SysInternalError(err.Error()) } err = use.VerifyCodeModel.SetVerifyCode(ctx, param.LoginID, checkType, vc) if err != nil { return usecase.GenerateRefreshCodeResponse{}, - errs.DatabaseErrorWithScope( - code.CloudEPMember, - domain.FailedToGetCodeOnRedisErrorCode, - "failed to set verify code", - ) + errs.DBErrorError("failed to set verify code") } } @@ -70,35 +59,25 @@ func (use *MemberUseCase) VerifyRefreshCode(ctx context.Context, param usecase.V func (use *MemberUseCase) CheckRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error { checkType, status := member.GetCodeNameByCode(param.CodeType) if !status { - return errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToGetVerifyCodeErrorCode, - fmt.Sprintf("failed to get verify code type: %d", param.CodeType), - ) + return errs.ResNotFoundError(fmt.Sprintf("failed to get verify code type: %d", param.CodeType)) } get, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType) if err != nil { - return errs.DatabaseErrorWithScope( - code.CloudEPMember, - domain.FailedToGetCodeOnRedisErrorCode, + return errs.DBErrorErrorL( + use.Logger, + []errs.LogField{ + {Key: "err", Val: err.Error()}, + }, "failed to set verify code", ) } if get == "" { - return errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToGetCodeOnRedisErrorCode, - "failed to get data", - ) + return errs.ResNotFoundError("failed to get data", param.LoginID, checkType) } if get != param.VerifyCode { - return errs.ForbiddenWithScope( - code.CloudEPMember, - domain.FailedToGetCodeCorrectErrorCode, - "failed to verify code", - ) + return errs.AuthForbiddenError("failed to verify code") } return nil @@ -107,11 +86,7 @@ func (use *MemberUseCase) CheckRefreshCode(ctx context.Context, param usecase.Ve func (use *MemberUseCase) VerifyPlatformAuthResult(ctx context.Context, param usecase.VerifyAuthResultRequest) (usecase.VerifyAuthResultResponse, error) { account, err := use.Account.FindOneByAccount(ctx, param.Account) if err != nil { - return usecase.VerifyAuthResultResponse{}, errs.ResourceNotFoundWithScope( - code.CloudEPMember, - domain.FailedToFindAccountErrorCode, - fmt.Sprintf("failed to find account: %s", param.Account), - ) + return usecase.VerifyAuthResultResponse{}, errs.ResNotFoundError(fmt.Sprintf("failed to find account: %s", param.Account)) } return usecase.VerifyAuthResultResponse{ diff --git a/pkg/member/usecase/verify_google.go b/pkg/member/usecase/verify_google.go index 7acaedc..eb586c2 100644 --- a/pkg/member/usecase/verify_google.go +++ b/pkg/member/usecase/verify_google.go @@ -1,6 +1,7 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "encoding/json" "errors" @@ -10,28 +11,20 @@ import ( "strconv" "time" - "backend/pkg/member/domain" "backend/pkg/member/domain/usecase" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" ) func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecase.VerifyAuthResultRequest) (usecase.GoogleTokenInfo, error) { var tokenInfo usecase.GoogleTokenInfo // 發送 Google Token Info API 請求 - body, err := fetchGoogleTokenInfo(ctx, req.Token) + body, err := use.fetchGoogleTokenInfo(ctx, req.Token) if err != nil { return tokenInfo, err } // 解析返回的 JSON 數據 if err := json.Unmarshal(body, &tokenInfo); err != nil { - return tokenInfo, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogle, - "failed to parse token info", - ) + return tokenInfo, errs.SysInternalError("failed to parse token info") } // 驗證 Token 資訊 @@ -43,31 +36,33 @@ func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecas } // fetchGoogleTokenInfo 發送 Google TokenInfo API 請求並返回響應內容 -func fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) { +func (use *MemberUseCase) fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) { uri := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", token) // 發送請求 r, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { - return nil, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogle, - "failed to create request", err.Error()) + return nil, errs.SvcThirdPartyErrorL( + use.Logger, + []errs.LogField{ + {Key: "request URL", Val: uri}, + {Key: "func", Val: "fetchGoogleTokenInfo"}, + }, "failed to create request", err.Error()) } resp, err := http.DefaultClient.Do(r) if err != nil { if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleTimeout, - "request timeout", - ) + return nil, errs.SysTimeoutError("fetch google timeout") } - return nil, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleTimeout, + return nil, errs.SvcThirdPartyErrorL( + use.Logger, + []errs.LogField{ + {Key: "request URL", Val: uri}, + {Key: "method", Val: http.MethodGet}, + {Key: "func", Val: "fetchGoogleTokenInfo"}, + }, "failed to request Google TokenInfo API", ) } @@ -75,21 +70,13 @@ func fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) { // 檢查返回的 HTTP 狀態碼 if resp.StatusCode != http.StatusOK { - return nil, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleHTTPCode, - fmt.Sprintf("unexpected status code: %d", resp.StatusCode), - ) + return nil, errs.SvcThirdPartyError(fmt.Sprintf("unexpected status code: %d", resp.StatusCode)) } // 讀取響應內容 body, err := io.ReadAll(resp.Body) if err != nil { - return nil, errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogle, - "failed to read response body", - ) + return nil, errs.SvcThirdPartyError("failed to read response body") } return body, nil @@ -100,29 +87,17 @@ func validateGoogleTokenInfo(tokenInfo usecase.GoogleTokenInfo, expectedClientID // **驗證 1: Token 是否過期** expiration, err := strconv.ParseInt(tokenInfo.Exp, 10, 64) if err != nil || expiration <= time.Now().UTC().Unix() { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleTokenExpired, - "token is expired", - ) + return errs.AuthExpiredError("token is expired") } // **驗證 2: Audience (aud) 是否與 Google Client ID 匹配** if tokenInfo.Aud != expectedClientID { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleInvalidAudience, - "invalid audience", - ) + return errs.SvcThirdPartyError("invalid audience") } // **驗證 3: 是否 email 已驗證** if tokenInfo.EmailVerified == "false" { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogle, - "email is not verified", - ) + return errs.SvcThirdPartyError("email is not verified") } return nil diff --git a/pkg/member/usecase/verify_google_test.go b/pkg/member/usecase/verify_google_test.go index 71af03a..fa68e0c 100644 --- a/pkg/member/usecase/verify_google_test.go +++ b/pkg/member/usecase/verify_google_test.go @@ -1,16 +1,13 @@ package usecase import ( + errs "backend/pkg/library/errors" "strconv" "testing" "time" - "backend/pkg/member/domain" "backend/pkg/member/domain/usecase" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - "github.com/stretchr/testify/assert" ) @@ -38,11 +35,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) { Aud: expectedClientID, EmailVerified: "true", }, - expectedErr: errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleTokenExpired, - "token is expired", - ), + expectedErr: errs.SvcThirdPartyError("token is expired"), }, { name: "Invalid audience", @@ -51,11 +44,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) { Aud: "invalid-client-id", EmailVerified: "true", }, - expectedErr: errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogleInvalidAudience, - "invalid audience", - ), + expectedErr: errs.SvcThirdPartyError("invalid audience"), }, { name: "Email not verified", @@ -64,11 +53,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) { Aud: expectedClientID, EmailVerified: "false", }, - expectedErr: errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyGoogle, - "email is not verified", - ), + expectedErr: errs.SvcThirdPartyError("email is not verified"), }, } diff --git a/pkg/member/usecase/verify_line.go b/pkg/member/usecase/verify_line.go index 5849fdf..781913c 100644 --- a/pkg/member/usecase/verify_line.go +++ b/pkg/member/usecase/verify_line.go @@ -1,6 +1,7 @@ package usecase import ( + errs "backend/pkg/library/errors" "bytes" "context" "encoding/json" @@ -8,11 +9,7 @@ import ( "net/http" "net/url" - "backend/pkg/member/domain" "backend/pkg/member/domain/usecase" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" ) // LineCodeToAccessToken 透過 Line 授權碼換取 Access Token @@ -57,11 +54,16 @@ func (use *MemberUseCase) LineGetProfileByAccessToken(ctx context.Context, acces func (use *MemberUseCase) doPost(ctx context.Context, uri string, headers map[string]string, body string, result interface{}) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBufferString(body)) if err != nil { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "failed to create request", - ) + return errs.SvcThirdPartyErrorL( + use.Logger, + []errs.LogField{ + {Key: "uri", Val: uri}, + {Key: "headers", Val: headers}, + {Key: "body", Val: body}, + {Key: "result", Val: result}, + {Key: "method", Val: http.MethodPost}, + }, + "failed to create request") } for key, value := range headers { req.Header.Set(key, value) @@ -74,11 +76,15 @@ func (use *MemberUseCase) doPost(ctx context.Context, uri string, headers map[st func (use *MemberUseCase) doGet(ctx context.Context, uri string, headers map[string]string, result interface{}) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "failed to create request", - ) + return errs.SvcThirdPartyErrorL( + use.Logger, + []errs.LogField{ + {Key: "uri", Val: uri}, + {Key: "headers", Val: headers}, + {Key: "result", Val: result}, + {Key: "method", Val: http.MethodGet}, + }, + "failed to create request") } for key, value := range headers { req.Header.Set(key, value) @@ -92,37 +98,28 @@ func (use *MemberUseCase) doRequest(req *http.Request, result interface{}) error client := &http.Client{} resp, err := client.Do(req) if err != nil { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "failed to send request", - ) + return errs.SvcThirdPartyErrorL( + use.Logger, + []errs.LogField{ + {Key: "req", Val: req}, + {Key: "result", Val: result}, + {Key: "method", Val: http.MethodGet}, + }, + "failed to create request") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "unexpected status code: "+http.StatusText(resp.StatusCode), - ) + return errs.SvcThirdPartyError("unexpected status code: " + http.StatusText(resp.StatusCode)) } body, err := io.ReadAll(resp.Body) if err != nil { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "failed to read response body", - ) + return errs.SvcThirdPartyError("failed to read response body") } if err := json.Unmarshal(body, result); err != nil { - return errs.ThirdPartyError( - code.CloudEPMember, - domain.FailedToVerifyLine, - "failed to parse response body", - ) + return errs.SvcThirdPartyError("failed to parse response body") } return nil diff --git a/pkg/member/usecase/verify_test.go b/pkg/member/usecase/verify_test.go index 6ac8c56..6ac4fcb 100644 --- a/pkg/member/usecase/verify_test.go +++ b/pkg/member/usecase/verify_test.go @@ -1,6 +1,7 @@ package usecase import ( + errs "backend/pkg/library/errors" "context" "errors" "testing" @@ -10,8 +11,6 @@ import ( "backend/pkg/member/domain/usecase" mockRepo "backend/pkg/member/mock/repository" - "backend/pkg/library/errs" - "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) @@ -241,7 +240,7 @@ func TestVerifyPlatformAuthResult(t *testing.T) { Token: "someToken", }, mockSetup: func() { - mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "nonExistentAccount").Return(nil, errs.ResourceNotFound("account not found")) + mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "nonExistentAccount").Return(nil, errs.ResNotFoundError("account not found")) }, wantResp: usecase.VerifyAuthResultResponse{}, wantErr: true, diff --git a/pkg/notification/domain/error.go b/pkg/notification/domain/error.go deleted file mode 100644 index d5bbad4..0000000 --- a/pkg/notification/domain/error.go +++ /dev/null @@ -1,14 +0,0 @@ -package domain - -import "backend/pkg/library/errs" - -// Notification Error Codes -const ( - NotificationErrorCode errs.ErrorCode = 1 + iota - FailedToSendEmailErrorCode - FailedToSendSMSErrorCode - FailedToGetTemplateErrorCode - FailedToRenderTemplateErrorCode - FailedToSaveHistoryErrorCode - FailedToRetryDeliveryErrorCode -) diff --git a/pkg/notification/repository/aws_ses_mailer.go b/pkg/notification/repository/aws_ses_mailer.go index 1ccfc53..f693836 100644 --- a/pkg/notification/repository/aws_ses_mailer.go +++ b/pkg/notification/repository/aws_ses_mailer.go @@ -2,20 +2,12 @@ package repository import ( "backend/pkg/notification/config" - "backend/pkg/notification/domain" "backend/pkg/notification/domain/repository" "context" - "fmt" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/ses/types" - "github.com/zeromicro/go-zero/core/logx" - "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/ses" + "github.com/aws/aws-sdk-go-v2/service/ses/types" ) // AwsEmailDeliveryParam 傳送參數配置 @@ -85,18 +77,8 @@ func (repo *AwsEmailDeliveryRepository) SendMail(ctx context.Context, req reposi // 發送郵件(直接使用傳入的 context,不創建新的 context) _, err := repo.Client.SendEmail(ctx, input) if err != nil { - return errs.ThirdPartyErrorL( - code.CloudEPNotification, - domain.FailedToSendEmailErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "AwsEmailDeliveryRepository.SendEmail"}, - {Key: "err", Value: err.Error()}, - }, - fmt.Sprintf("failed to send mail by aws ses: %v", err)).Wrap(err) + return err } - logx.WithContext(ctx).Infof("Email sent successfully via AWS SES to %v", req.To) return nil } diff --git a/pkg/notification/repository/mitake_sms_sender.go b/pkg/notification/repository/mitake_sms_sender.go index ca93173..e47b85e 100644 --- a/pkg/notification/repository/mitake_sms_sender.go +++ b/pkg/notification/repository/mitake_sms_sender.go @@ -2,16 +2,9 @@ package repository import ( "backend/pkg/notification/config" - "backend/pkg/notification/domain" "backend/pkg/notification/domain/repository" "context" - "fmt" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - "github.com/minchao/go-mitake" - "github.com/zeromicro/go-zero/core/logx" ) // MitakeSMSDeliveryParam 三竹傳送參數配置 @@ -40,19 +33,9 @@ func (repo *MitakeSMSDeliveryRepository) SendSMS(ctx context.Context, req reposi // 讓 delivery usecase 統一管理重試和超時 _, err := repo.Client.Send(message) if err != nil { - return errs.ThirdPartyErrorL( - code.CloudEPNotification, - domain.FailedToSendSMSErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "req", Value: req}, - {Key: "func", Value: "MitakeSMSDeliveryRepository.Send"}, - {Key: "err", Value: err.Error()}, - }, - fmt.Sprintf("failed to send sms by mitake: %v", err)).Wrap(err) + return err } - logx.WithContext(ctx).Infof("SMS sent successfully via Mitake to %s", req.PhoneNumber) return nil } diff --git a/pkg/notification/repository/smtp_mailer.go b/pkg/notification/repository/smtp_mailer.go index edb24bc..a589eea 100644 --- a/pkg/notification/repository/smtp_mailer.go +++ b/pkg/notification/repository/smtp_mailer.go @@ -2,15 +2,8 @@ package repository import ( "backend/pkg/notification/config" - "backend/pkg/notification/domain" "backend/pkg/notification/domain/repository" "context" - "fmt" - - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" "gopkg.in/gomail.v2" ) @@ -46,21 +39,9 @@ func (repo *SMTPMailRepository) SendMail(ctx context.Context, req repository.Mai m.SetHeader("Subject", req.Subject) m.SetBody("text/html", req.Body) - // 直接發送,不使用 goroutine pool - // 讓 delivery usecase 統一管理重試和超時 if err := repo.Client.DialAndSend(m); err != nil { - return errs.ThirdPartyErrorL( - code.CloudEPNotification, - domain.FailedToSendEmailErrorCode, - logx.WithContext(ctx), - []logx.LogField{ - {Key: "func", Value: "SMTPMailRepository.SendMail"}, - {Key: "req", Value: req}, - {Key: "err", Value: err.Error()}, - }, - fmt.Sprintf("failed to send mail by smtp: %v", err)).Wrap(err) + return err } - logx.WithContext(ctx).Infof("Email sent successfully via SMTP to %v", req.To) return nil } diff --git a/pkg/notification/usecase/delivery.go b/pkg/notification/usecase/delivery.go index 997d5c4..cfbf01b 100644 --- a/pkg/notification/usecase/delivery.go +++ b/pkg/notification/usecase/delivery.go @@ -2,7 +2,6 @@ package usecase import ( "backend/pkg/notification/config" - "backend/pkg/notification/domain" "backend/pkg/notification/domain/entity" "backend/pkg/notification/domain/repository" "backend/pkg/notification/domain/usecase" @@ -12,10 +11,7 @@ import ( "sort" "time" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" + errs "backend/pkg/library/errors" ) // DeliveryUseCaseParam 傳送參數配置 @@ -24,6 +20,7 @@ type DeliveryUseCaseParam struct { EmailProviders []usecase.EmailProvider DeliveryConfig config.DeliveryConfig HistoryRepo repository.HistoryRepository // 可選的歷史記錄 repository + Logger errs.Logger // 日誌記錄器 } // DeliveryUseCase 通知發送服務 @@ -69,7 +66,13 @@ func (use *DeliveryUseCase) SendMessage(ctx context.Context, req usecase.SMSMess if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != nil { - logx.WithContext(ctx).Errorf("Failed to create SMS history: %v", err) + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "type", Val: "sms"}, + {Key: "func", Val: "HistoryRepo.CreateHistory"}, + {Key: "err", Val: err.Error()}, + }, + "Failed to create SMS history") } } @@ -95,7 +98,13 @@ func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq) if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != nil { - logx.WithContext(ctx).Errorf("Failed to create email history: %v", err) + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "type", Val: "email"}, + {Key: "func", Val: "HistoryRepo.CreateHistory"}, + {Key: "err", Val: err.Error()}, + }, + "Failed to create email history") } } @@ -112,7 +121,6 @@ type providerAdapter interface { getProviderName(index int) string getProviderSort(index int) int64 send(ctx context.Context, providerIndex int) error - getErrorCode() errs.ErrorCode getType() string } @@ -142,10 +150,6 @@ func (a *smsProviderAdapter) send(ctx context.Context, providerIndex int) error }) } -func (a *smsProviderAdapter) getErrorCode() errs.ErrorCode { - return domain.FailedToSendSMSErrorCode -} - func (a *smsProviderAdapter) getType() string { return "SMS" } @@ -177,10 +181,6 @@ func (a *emailProviderAdapter) send(ctx context.Context, providerIndex int) erro }) } -func (a *emailProviderAdapter) getErrorCode() errs.ErrorCode { - return domain.FailedToSendEmailErrorCode -} - func (a *emailProviderAdapter) getType() string { return "Email" } @@ -252,8 +252,14 @@ func (use *DeliveryUseCase) sendWithRetry( attemptRecord.ErrorMessage = err.Error() lastErr = err - logx.WithContext(ctx).Errorf("%s send attempt %d failed for provider %d: %v", - adapter.getType(), attempt+1, providerIndex, err) + _ = errs.SvcThirdPartyErrorL(use.param.Logger, + []errs.LogField{ + {Key: "type", Val: adapter.getType()}, + {Key: "attempt", Val: attempt + 1}, + {Key: "provider", Val: providerIndex}, + {Key: "err", Val: err.Error()}, + }, + fmt.Sprintf("%s send attempt %d failed for provider %d", adapter.getType(), attempt+1, providerIndex)) // 如果不是最後一次嘗試,等待後重試 if attempt < use.param.DeliveryConfig.MaxRetries-1 { @@ -278,8 +284,7 @@ func (use *DeliveryUseCase) sendWithRetry( use.updateHistory(ctx, history) use.addAttemptRecord(ctx, history.ID, attemptRecord) - logx.WithContext(ctx).Infof("%s sent successfully after %d attempts", - adapter.getType(), totalAttempts) + // 成功發送不需要記錄錯誤,這裡可以選擇記錄信息日誌或直接返回 return nil } @@ -295,9 +300,7 @@ func (use *DeliveryUseCase) sendWithRetry( history.CompletedAt = &now use.updateHistory(ctx, history) - return errs.ThirdPartyError( - code.CloudEPNotification, - adapter.getErrorCode(), + return errs.SvcThirdPartyError( fmt.Sprintf("Failed to send %s after %d attempts across %d providers", adapter.getType(), totalAttempts, providerCount)) } @@ -317,7 +320,13 @@ func (use *DeliveryUseCase) calculateDelay(attempt int) time.Duration { func (use *DeliveryUseCase) updateHistory(ctx context.Context, history *entity.DeliveryHistory) { if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.UpdateHistory(ctx, history); err != nil { - logx.WithContext(ctx).Errorf("Failed to update delivery history: %v", err) + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "func", Val: "HistoryRepo.UpdateHistory"}, + {Key: "history_id", Val: history.ID}, + {Key: "err", Val: err.Error()}, + }, + "Failed to update delivery history") } } } @@ -326,7 +335,13 @@ func (use *DeliveryUseCase) updateHistory(ctx context.Context, history *entity.D func (use *DeliveryUseCase) addAttemptRecord(ctx context.Context, historyID string, attempt entity.DeliveryAttempt) { if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.AddAttempt(ctx, historyID, attempt); err != nil { - logx.WithContext(ctx).Errorf("Failed to add attempt record: %v", err) + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "func", Val: "HistoryRepo.AddAttempt"}, + {Key: "history_id", Val: historyID}, + {Key: "err", Val: err.Error()}, + }, + "Failed to add attempt record") } } } diff --git a/pkg/notification/usecase/template.go b/pkg/notification/usecase/template.go index 6352164..561ddae 100644 --- a/pkg/notification/usecase/template.go +++ b/pkg/notification/usecase/template.go @@ -1,7 +1,6 @@ package usecase import ( - "backend/pkg/notification/domain" "backend/pkg/notification/domain/entity" "backend/pkg/notification/domain/repository" "backend/pkg/notification/domain/template" @@ -10,14 +9,12 @@ import ( "fmt" "strings" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" - - "github.com/zeromicro/go-zero/core/logx" + errs "backend/pkg/library/errors" ) type TemplateUseCaseParam struct { TemplateRepo repository.TemplateRepository // 可選的資料庫模板 repository + Logger errs.Logger // 日誌記錄器 } type TemplateUseCase struct { @@ -35,28 +32,19 @@ func (use *TemplateUseCase) GetEmailTemplateByStatic(_ context.Context, language // 查找指定語言的模板映射 templateByLang, exists := template.EmailTemplateMap[language] if !exists { - return template.EmailTemplate{}, errs.ResourceNotFoundWithScope( - code.CloudEPNotification, - domain.FailedToGetTemplateErrorCode, - fmt.Sprintf("email template not found for language: %s", language)) + return template.EmailTemplate{}, errs.ResNotFoundError(fmt.Sprintf("email template not found for language: %s", language)) } // 查找指定類型的模板生成函數 templateFunc, exists := templateByLang[templateID] if !exists { - return template.EmailTemplate{}, errs.ResourceNotFoundWithScope( - code.CloudEPNotification, - domain.FailedToGetTemplateErrorCode, - fmt.Sprintf("email template not found for type ID: %s", templateID)) + return template.EmailTemplate{}, errs.ResNotFoundError(fmt.Sprintf("email template not found for type ID: %s", templateID)) } // 執行模板生成函數 tmp, err := templateFunc() if err != nil { - return template.EmailTemplate{}, errs.DatabaseErrorWithScope( - code.CloudEPNotification, - domain.FailedToGetTemplateErrorCode, - fmt.Sprintf("error generating email template: %v", err)) + return template.EmailTemplate{}, errs.DBErrorError(fmt.Sprintf("error generating email template: %v", err)) } return tmp, nil @@ -68,7 +56,6 @@ func (use *TemplateUseCase) GetEmailTemplate(ctx context.Context, language templ if use.param.TemplateRepo != nil { dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "email", string(language), string(templateID)) if err == nil && dbTemplate != nil && dbTemplate.IsActive { - logx.WithContext(ctx).Infof("Using database template for %s/%s", language, templateID) return template.EmailTemplate{ Title: dbTemplate.Subject, Body: dbTemplate.Body, @@ -77,12 +64,19 @@ func (use *TemplateUseCase) GetEmailTemplate(ctx context.Context, language templ // 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板 if err != nil { - logx.WithContext(ctx).WithFields(logx.LogField{Key: "error", Value: err.Error()}).Error("Failed to get template from database, falling back to static") + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "type", Val: "email"}, + {Key: "language", Val: string(language)}, + {Key: "template_id", Val: string(templateID)}, + {Key: "func", Val: "TemplateRepo.GetTemplate"}, + {Key: "err", Val: err.Error()}, + }, + "Failed to get template from database, falling back to static") } } // 2. 回退到靜態模板 - logx.WithContext(ctx).Infof("Using static template for %s/%s", language, templateID) return use.GetEmailTemplateByStatic(ctx, language, templateID) } @@ -92,7 +86,6 @@ func (use *TemplateUseCase) GetSMSTemplate(ctx context.Context, language templat if use.param.TemplateRepo != nil { dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "sms", string(language), string(templateID)) if err == nil && dbTemplate != nil && dbTemplate.IsActive { - logx.WithContext(ctx).Infof("Using database SMS template for %s/%s", language, templateID) return usecase.SMSTemplateResp{ Body: dbTemplate.Body, }, nil @@ -100,12 +93,19 @@ func (use *TemplateUseCase) GetSMSTemplate(ctx context.Context, language templat // 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板 if err != nil { - logx.WithContext(ctx).WithFields(logx.LogField{Key: "error", Value: err.Error()}).Error("Failed to get SMS template from database, falling back to static") + _ = errs.DBErrorErrorL(use.param.Logger, + []errs.LogField{ + {Key: "type", Val: "sms"}, + {Key: "language", Val: string(language)}, + {Key: "template_id", Val: string(templateID)}, + {Key: "func", Val: "TemplateRepo.GetTemplate"}, + {Key: "err", Val: err.Error()}, + }, + "Failed to get SMS template from database, falling back to static") } } // 2. 回退到靜態模板(SMS 暫時沒有靜態模板,返回默認) - logx.WithContext(ctx).Infof("Using default SMS template for %s/%s", language, templateID) return use.getDefaultSMSTemplate(templateID), nil } diff --git a/pkg/permission/usecase/token.go b/pkg/permission/usecase/token.go index ab4dcc8..5ccc7e3 100755 --- a/pkg/permission/usecase/token.go +++ b/pkg/permission/usecase/token.go @@ -7,21 +7,19 @@ import ( "time" "backend/internal/config" - "backend/pkg/library/errs" - "backend/pkg/library/errs/code" + errs "backend/pkg/library/errors" "backend/pkg/permission/domain/entity" "backend/pkg/permission/domain/repository" "backend/pkg/permission/domain/token" "backend/pkg/permission/domain/usecase" "github.com/segmentio/ksuid" - "github.com/zeromicro/go-zero/core/logx" ) type TokenUseCaseParam struct { TokenRepo repository.TokenRepository - - Config *config.Config + Logger errs.Logger + Config *config.Config } type TokenUseCase struct { @@ -31,14 +29,7 @@ type TokenUseCase struct { func (use *TokenUseCase) ReadTokenBasicData(ctx context.Context, token string) (map[string]string, error) { claims, err := ParseClaims(token, use.Config.Token.AccessSecret, false) if err != nil { - return nil, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "ParseClaims", - req: token, - err: err, - message: "validate token claims error", - errorCode: code.TokenValidateError, - }) + return nil, errs.AuthSigPayloadMismatchError("validate token claims error") } return claims, nil @@ -60,13 +51,7 @@ func (use *TokenUseCase) NewToken(ctx context.Context, req entity.AuthorizationR err = use.TokenRepo.Create(ctx, *tokenObj) if err != nil { - return entity.TokenResp{}, use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.Create", - req: req, - err: err, - message: "failed to create token", - errorCode: code.TokenCreateError, - }) + return entity.TokenResp{}, errs.DBErrorError("failed to create token") } return entity.TokenResp{ @@ -127,13 +112,7 @@ func (use *TokenUseCase) newToken(ctx context.Context, req *entity.Authorization var err error token.AccessToken, err = accessTokenGenerator(token, tc, use.Config.Token.AccessSecret) if err != nil { - return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "accessTokenGenerator", - req: req, - err: err, - message: "failed to generator access token", - errorCode: code.TokenCreateError, - }) + return nil, errs.SysInternalError("failed to generator access token").Wrap(err) } if req.IsRefreshToken { @@ -147,27 +126,13 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok // Step 1: 檢查 refresh token tokenObj, err := use.TokenRepo.GetAccessTokenByOneTimeToken(ctx, req.Token) if err != nil { - return entity.RefreshTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.GetAccessTokenByOneTimeToken", - req: req, - err: err, - message: "failed to get access token", - errorCode: code.TokenValidateError, - }) + return entity.RefreshTokenResp{}, errs.DBErrorError("failed to get access token").Wrap(err) } // Step 2: 提取 Claims Data claimsData, err := ParseClaims(tokenObj.AccessToken, use.Config.Token.AccessSecret, false) if err != nil { - return entity.RefreshTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "extractClaims", - req: req, - err: err, - message: "failed to extract claims", - errorCode: code.TokenValidateError, - }) + return entity.RefreshTokenResp{}, errs.AuthSigPayloadMismatchError("failed to extract claims") } // Step 3: 創建新 token @@ -183,37 +148,16 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok Role: claimsData.Role(), }) if err != nil { - return entity.RefreshTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "use.newToken", - req: req, - err: err, - message: "failed to create new token", - errorCode: code.TokenValidateError, - }) + return entity.RefreshTokenResp{}, errs.DBErrorError("failed to create new token").Wrap(err) } if err := use.TokenRepo.Create(ctx, *newToken); err != nil { - return entity.RefreshTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.Create", - req: req, - err: err, - message: "failed to create new token", - errorCode: code.TokenValidateError, - }) + return entity.RefreshTokenResp{}, errs.DBErrorError("failed to create new token").Wrap(err) } // Step 4: 刪除舊 token 並創建新 token if err := use.TokenRepo.Delete(ctx, tokenObj); err != nil { - return entity.RefreshTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.Delete", - req: req, - err: err, - message: "failed to delete old token", - errorCode: code.TokenValidateError, - }) + return entity.RefreshTokenResp{}, errs.DBErrorError("failed to delete old token").Wrap(err) } // 返回新的 Token 響應 @@ -228,35 +172,17 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok func (use *TokenUseCase) CancelToken(ctx context.Context, req entity.CancelTokenReq) error { claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, false) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "CancelToken extractClaims", - req: req, - err: err, - message: "failed to get token claims", - errorCode: code.TokenValidateError, - }) + return errs.AuthSigPayloadMismatchError("failed to get token claims") } token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID()) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo GetAccessTokenByID", - req: req, - err: err, - message: fmt.Sprintf("failed to get token claims :%s", claims.ID()), - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError(fmt.Sprintf("failed to get token claims :%s", claims.ID())) } err = use.TokenRepo.Delete(ctx, token) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo Delete", - req: req, - err: err, - message: fmt.Sprintf("failed to delete token :%s", token.ID), - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError(fmt.Sprintf("failed to delete token :%s", token.ID)).Wrap(err) } return nil @@ -265,26 +191,12 @@ func (use *TokenUseCase) CancelToken(ctx context.Context, req entity.CancelToken func (use *TokenUseCase) ValidationToken(ctx context.Context, req entity.ValidationTokenReq) (entity.ValidationTokenResp, error) { claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, true) if err != nil { - return entity.ValidationTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "ParseClaims", - req: req, - err: err, - message: "validate token claims error", - errorCode: code.TokenValidateError, - }) + return entity.ValidationTokenResp{}, errs.AuthSigPayloadMismatchError("validate token claims error") } token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID()) if err != nil { - return entity.ValidationTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.GetAccessTokenByID", - req: req, - err: err, - message: fmt.Sprintf("failed to get token :%s", claims.ID()), - errorCode: code.TokenValidateError, - }) + return entity.ValidationTokenResp{}, errs.DBErrorError(fmt.Sprintf("failed to get token :%s", claims.ID())).Wrap(err) } return entity.ValidationTokenResp{ @@ -307,26 +219,14 @@ func (use *TokenUseCase) CancelTokens(ctx context.Context, req entity.DoTokenByU if req.UID != "" { err := use.TokenRepo.DeleteAccessTokensByUID(ctx, req.UID) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.DeleteAccessTokensByUID", - req: req, - err: err, - message: "failed to cancel tokens by uid", - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError("failed to cancel tokens by uid").Wrap(err) } } if len(req.IDs) > 0 { err := use.TokenRepo.DeleteAccessTokenByID(ctx, req.IDs) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.DeleteAccessTokenByID", - req: req, - err: err, - message: "failed to cancel tokens by token ids", - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError("failed to cancel tokens by token ids").Wrap(err) } } @@ -336,13 +236,7 @@ func (use *TokenUseCase) CancelTokens(ctx context.Context, req entity.DoTokenByU func (use *TokenUseCase) CancelTokenByDeviceID(ctx context.Context, req entity.DoTokenByDeviceIDReq) error { err := use.TokenRepo.DeleteAccessTokensByDeviceID(ctx, req.DeviceID) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.DeleteAccessTokensByDeviceID", - req: req, - err: err, - message: "failed to cancel token by device id", - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError("failed to cancel tokens by device id").Wrap(err) } return nil @@ -351,13 +245,7 @@ func (use *TokenUseCase) CancelTokenByDeviceID(ctx context.Context, req entity.D func (use *TokenUseCase) GetUserTokensByDeviceID(ctx context.Context, req entity.DoTokenByDeviceIDReq) ([]*entity.TokenResp, error) { uidTokens, err := use.TokenRepo.GetAccessTokensByDeviceID(ctx, req.DeviceID) if err != nil { - return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.GetAccessTokensByDeviceID", - req: req, - err: err, - message: "failed to get token by device id", - errorCode: code.TokenNotFound, - }) + return nil, errs.DBErrorError("failed to get tokens by device id").Wrap(err) } tokens := make([]*entity.TokenResp, 0, len(uidTokens)) @@ -376,13 +264,7 @@ func (use *TokenUseCase) GetUserTokensByDeviceID(ctx context.Context, req entity func (use *TokenUseCase) GetUserTokensByUID(ctx context.Context, req entity.QueryTokenByUIDReq) ([]*entity.TokenResp, error) { uidTokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, req.UID) if err != nil { - return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.GetAccessTokensByUID", - req: req, - err: err, - message: "failed to get token by uid", - errorCode: code.TokenNotFound, - }) + return nil, errs.DBErrorError("failed to get tokens by uid").Wrap(err) } tokens := make([]*entity.TokenResp, 0, len(uidTokens)) @@ -402,26 +284,12 @@ func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateO // 驗證Token claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, false) if err != nil { - return entity.CreateOneTimeTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "ParseClaims", - req: req, - err: err, - message: "failed to get token claims", - errorCode: code.OneTimeTokenError, - }) + return entity.CreateOneTimeTokenResp{}, errs.AuthSigPayloadMismatchError("failed to get token claims").Wrap(err) } tokenObj, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID()) if err != nil { - return entity.CreateOneTimeTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.GetAccessTokenByID", - req: req, - err: err, - message: "failed to get token by id", - errorCode: code.OneTimeTokenError, - }) + return entity.CreateOneTimeTokenResp{}, errs.DBErrorError("failed to get token by id").Wrap(err) } oneTimeToken := refreshTokenGenerator(ksuid.New().String()) @@ -430,14 +298,7 @@ func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateO Data: claims, Token: tokenObj, }, time.Minute); err != nil { - return entity.CreateOneTimeTokenResp{}, - use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.CreateOneTimeToken", - req: req, - err: err, - message: "create one time token error", - errorCode: code.OneTimeTokenError, - }) + return entity.CreateOneTimeTokenResp{}, errs.DBErrorError("failed to create new one-time token").Wrap(err) } return entity.CreateOneTimeTokenResp{ @@ -448,81 +309,29 @@ func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateO func (use *TokenUseCase) CancelOneTimeToken(ctx context.Context, req entity.CancelOneTimeTokenReq) error { err := use.TokenRepo.DeleteOneTimeToken(ctx, req.Token, nil) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "TokenRepo.DeleteOneTimeToken", - req: req, - err: err, - message: "failed to del one time token by token", - errorCode: code.OneTimeTokenError, - }) + return errs.DBErrorError("failed to del one time token by token") } return nil } -type wrapTokenErrorReq struct { - funcName string - req any - err error - message string - errorCode uint32 -} - -// wrapTokenError 將錯誤信息封裝到 errs.LibError 中 -func (use *TokenUseCase) wrapTokenError(ctx context.Context, param wrapTokenErrorReq) error { - logFields := []logx.LogField{ - {Key: "req", Value: param.req}, - {Key: "func", Value: param.funcName}, - {Key: "err", Value: param.err.Error()}, - } - - logx.WithContext(ctx).Errorw(param.message, logFields...) - - wrappedErr := errs.NewError( - code.CatToken, - code.CatToken, - param.errorCode, - param.message, - ).Wrap(param.err) - - return wrappedErr -} - // BlacklistToken 將 JWT token 加入黑名單 (立即撤銷) func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reason string) error { // 解析 JWT 獲取完整的 claims claimMap, err := parseToken(token, use.Config.Token.AccessSecret, false) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.parseToken", - req: token, - err: err, - message: "failed to parse token claims", - errorCode: code.InvalidJWT, - }) + return errs.AuthSigPayloadMismatchError("failed to parse token claims").Wrap(err) } // 獲取 JTI (JWT ID) jti, exists := claimMap["jti"] if !exists { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.getJTI", - req: token, - err: entity.ErrInvalidJTI, - message: "token missing JTI claim", - errorCode: code.InvalidJWT, - }) + return errs.ResNotFoundError("token missing JTI claim").Wrap(err) } jtiStr, ok := jti.(string) if !ok { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.convertJTI", - req: token, - err: entity.ErrInvalidJTI, - message: "JTI claim is not a string", - errorCode: code.InvalidJWT, - }) + return errs.ResNotFoundError("token missing JTI claim").Wrap(err) } // 獲取 UID (可能在 data 中) @@ -538,13 +347,7 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso // 獲取過期時間 exp, exists := claimMap["exp"] if !exists { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.getExp", - req: token, - err: entity.ErrTokenExpired, - message: "token missing exp claim", - errorCode: code.TokenExpired, - }) + return errs.AuthExpiredError("token missing exp claim").Wrap(err) } // 將 exp 轉換為 int64 (JWT 中通常是 float64) @@ -557,23 +360,11 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso case string: parsedExp, err := strconv.ParseInt(v, 10, 64) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.parseExp", - req: token, - err: err, - message: "failed to parse exp claim", - errorCode: code.TokenExpired, - }) + return errs.SysInternalError("failed to parse exp claim").Wrap(err) } expInt = parsedExp default: - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.convertExp", - req: token, - err: fmt.Errorf("exp claim is not a valid type: %T", exp), - message: "exp claim type conversion failed", - errorCode: code.TokenExpired, - }) + return errs.SysInternalError("exp claim type conversion failed").Wrap(err) } // 創建黑名單條目 @@ -587,19 +378,25 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso // 添加到黑名單 err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算 if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistToken.AddToBlacklist", - req: jtiStr, - err: err, - message: "failed to add token to blacklist", - errorCode: code.TokenCreateError, - }) + return errs.DBErrorError("failed to add token to blacklist").Wrap(err) } - logx.WithContext(ctx).Infow("token blacklisted", - logx.Field("jti", jtiStr), - logx.Field("uid", uid), - logx.Field("reason", reason)) + // 記錄成功日誌(如果 Logger 存在) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "jti", + Val: jtiStr, + }, + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "reason", + Val: reason, + }).Info("token blacklisted") + } return nil } @@ -608,13 +405,7 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso func (use *TokenUseCase) IsTokenBlacklisted(ctx context.Context, jti string) (bool, error) { isBlacklisted, err := use.TokenRepo.IsBlacklisted(ctx, jti) if err != nil { - return false, use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "IsTokenBlacklisted", - req: jti, - err: err, - message: "failed to check blacklist status", - errorCode: code.TokenValidateError, - }) + return false, errs.DBErrorError("failed to check blacklist status").Wrap(err) } return isBlacklisted, nil @@ -625,13 +416,7 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string, // 獲取用戶的所有 token tokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, uid) if err != nil { - return use.wrapTokenError(ctx, wrapTokenErrorReq{ - funcName: "BlacklistAllUserTokens.GetAccessTokensByUID", - req: uid, - err: err, - message: "failed to get user tokens", - errorCode: code.TokenValidateError, - }) + return errs.DBErrorError("failed to get user tokens").Wrap(err) } // 為每個 token 創建黑名單條目 @@ -639,36 +424,75 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string, // 解析 token 獲取 JTI 和過期時間 claims, err := ParseClaims(token.AccessToken, use.Config.Token.AccessSecret, false) if err != nil { - logx.WithContext(ctx).Errorw("failed to parse token for blacklisting", - logx.Field("uid", uid), - logx.Field("tokenID", token.ID), - logx.Field("error", err)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "tokenID", + Val: token.ID, + }, + errs.LogField{ + Key: "error", + Val: err, + }).Error("failed to parse token for blacklisting") + } continue // 跳過無效的 token,繼續處理其他 token } jti, exists := claims["jti"] if !exists || jti == "" { - logx.WithContext(ctx).Errorw("token missing JTI claim", - logx.Field("uid", uid), - logx.Field("tokenID", token.ID)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "tokenID", + Val: token.ID, + }).Error("failed to parse token for blacklisting") + } continue } exp, exists := claims["exp"] if !exists { - logx.WithContext(ctx).Errorw("token missing exp claim", - logx.Field("uid", uid), - logx.Field("tokenID", token.ID)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "tokenID", + Val: token.ID, + }).Error("token missing exp claim") + } continue } // 將 exp 字符串轉換為 int64 expInt, err := strconv.ParseInt(exp, 10, 64) if err != nil { - logx.WithContext(ctx).Errorw("failed to parse exp claim", - logx.Field("uid", uid), - logx.Field("tokenID", token.ID), - logx.Field("error", err)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "tokenID", + Val: token.ID, + }, + errs.LogField{ + Key: "error", + Val: err, + }).Error("failed to parse exp claim") + } + continue } @@ -683,10 +507,21 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string, // 添加到黑名單 err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算 if err != nil { - logx.WithContext(ctx).Errorw("failed to add token to blacklist", - logx.Field("uid", uid), - logx.Field("jti", jti), - logx.Field("error", err)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "jti", + Val: jti, + }, + errs.LogField{ + Key: "error", + Val: err, + }).Error("failed to add token to blacklist") + } // 繼續處理其他 token,不要因為一個失敗就停止 } } @@ -694,16 +529,34 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string, // 刪除用戶的所有 token 記錄 err = use.TokenRepo.DeleteAccessTokensByUID(ctx, uid) if err != nil { - logx.WithContext(ctx).Errorw("failed to delete user tokens", - logx.Field("uid", uid), - logx.Field("error", err)) - // 這不是致命錯誤,因為 token 已經被加入黑名單 + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "error", + Val: err, + }).Error("failed to delete user tokens") + } } - logx.WithContext(ctx).Infow("all user tokens blacklisted", - logx.Field("uid", uid), - logx.Field("tokenCount", len(tokens)), - logx.Field("reason", reason)) + if use.Logger != nil { + use.Logger.WithFields( + errs.LogField{ + Key: "uid", + Val: uid, + }, + errs.LogField{ + Key: "tokenCount", + Val: len(tokens), + }, + errs.LogField{ + Key: "reason", + Val: reason, + }).Error("all user tokens blacklisted") + } return nil } diff --git a/pkg/permission/usecase/token_claims_test.go b/pkg/permission/usecase/token_claims_test.go index 645e7df..ade9cc1 100644 --- a/pkg/permission/usecase/token_claims_test.go +++ b/pkg/permission/usecase/token_claims_test.go @@ -160,10 +160,12 @@ func TestTokenClaims_SetAndGetAccount(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tc := make(TokenClaims) - tc.SetAccount(tt.account) + tc.SetLoginID(tt.account) - // Note: there's no GetAccount method, so we just verify it's set - assert.Equal(t, tt.account, tc["account"]) + // 使用 LoginID() 方法獲取 + assert.Equal(t, tt.account, tc.LoginID()) + // 也驗證直接訪問 login_id 欄位 + assert.Equal(t, tt.account, tc["login_id"]) }) } } @@ -229,7 +231,7 @@ func TestTokenClaims_MultipleFields(t *testing.T) { tc.SetRole("admin") tc.SetDeviceID("device456") tc.SetScope("read write") - tc.SetAccount("user@example.com") + tc.SetLoginID("user@example.com") tc["uid"] = "user789" t.Run("verify all fields", func(t *testing.T) { @@ -237,7 +239,8 @@ func TestTokenClaims_MultipleFields(t *testing.T) { assert.Equal(t, "admin", tc.Role()) assert.Equal(t, "device456", tc.DeviceID()) assert.Equal(t, "read write", tc["scope"]) - assert.Equal(t, "user@example.com", tc["account"]) + assert.Equal(t, "user@example.com", tc.LoginID()) + assert.Equal(t, "user@example.com", tc["login_id"]) assert.Equal(t, "user789", tc.UID()) }) }