feat: update error

This commit is contained in:
王性驊 2025-11-04 17:47:36 +08:00
parent d71ffea750
commit b1a8926532
71 changed files with 2616 additions and 2416 deletions

View File

@ -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"` // 可選的資料,當有返回時才出現
}
)

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -1,5 +0,0 @@
package domain
const SuccessCode = 10200
const SuccessMessage = "success"
const DefaultScope = "gateway"

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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{

View File

@ -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"],
})

View File

@ -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: 建立帳號

View File

@ -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)
}
}

View File

@ -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")
}
// 更新

View File

@ -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
}

View File

@ -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,
// })
//}

View File

@ -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())),
})
}

53
internal/svc/logs.go Normal file
View File

@ -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
}

View File

@ -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())),
}
}

View File

@ -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())),
})
}

View File

@ -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 {

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,186 @@
# 錯誤碼 × HTTP 對照表
這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照並提供**實務範例**。
錯誤系統採用 8 碼格式 `SSCCCDDD`
- `SS` = Scope服務/模組,兩位數)
- `CCC` = Category類別三位數影響 HTTP 狀態)
- `DDD` = Detail細節三位數自定義業務碼
> 例如:`10101000` → Scope=10、Category=101InputInvalidFormat、Detail=000。
## 目錄
- [1) 快速查表](#1-快速查表依類別整理)
- [2) 使用範例](#2-使用範例)
- [3) 小撇步與慣例](#3-小撇步與慣例)
- [4) 安裝與測試](#4-安裝與測試)
- [5) 變更日誌](#5-變更日誌)
---
## 1) 快速查表(依類別整理)
### A. InputCategory 1xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|---------------|:----:|---|
| `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 |
| `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 |
| `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 |
### B. DBCategory 2xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|-------------|:----:|---|
| `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 |
| `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 |
| `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 |
### C. ResourceCategory 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. AuthCategory 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. SystemCategory 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. PubSubCategory 7xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|---------|:----:|---|
| `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 |
| `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 |
| `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 |
### G. ServiceCategory 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
```

View File

@ -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
)

View File

@ -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 // 400ID 無效
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
}

View File

@ -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)
}
})
}
}

View File

@ -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...)
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
)

View File

@ -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 錯誤
)

View File

@ -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",
}

View File

@ -1,18 +0,0 @@
package code
// Scope
const (
Unset uint32 = iota
CloudEPPortalGW
CloudEPMember
CloudEPPermission
CloudEPNotification
CloudEPTweeting
CloudEPOrder
CloudEPFileStorage
CloudEPProduct
CloudEPSecKill
CloudEPCart
CloudEPComment
CloudEPReaction
)

View File

@ -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, " ")))
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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)
}
})
}
}

View File

@ -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
)

View File

@ -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()

View File

@ -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 {

View File

@ -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,
},
}

View File

@ -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
}
// 檢查更新結果,若沒有匹配的文檔,則返回錯誤

View File

@ -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,
},
}

View File

@ -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{

View File

@ -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),
)

View File

@ -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")
}

View File

@ -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)

View File

@ -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,

View File

@ -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{

View File

@ -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

View File

@ -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"),
},
}

View File

@ -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

View File

@ -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,

View File

@ -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
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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())
})
}