feat: update error
This commit is contained in:
parent
d71ffea750
commit
b1a8926532
|
|
@ -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
4
go.mod
|
|
@ -2,8 +2,6 @@ module backend
|
|||
|
||||
go 1.25.1
|
||||
|
||||
replace backend/pkg/library/errs => ./pkg/library/errs
|
||||
|
||||
require (
|
||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
|
||||
github.com/alicebob/miniredis/v2 v2.35.0
|
||||
|
|
@ -20,7 +18,6 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.39.0
|
||||
github.com/zeromicro/go-zero v1.9.1
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
|
|
@ -110,7 +107,6 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -262,8 +262,6 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
|||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeromicro/go-zero v1.9.1 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g=
|
||||
github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
package domain
|
||||
|
||||
const SuccessCode = 10200
|
||||
const SuccessMessage = "success"
|
||||
const DefaultScope = "gateway"
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: 建立帳號
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
// 更新
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
// })
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
# 錯誤碼 × HTTP 對照表
|
||||
|
||||
這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照,並提供**實務範例**。
|
||||
錯誤系統採用 8 碼格式 `SSCCCDDD`:
|
||||
|
||||
- `SS` = Scope(服務/模組,兩位數)
|
||||
- `CCC` = Category(類別,三位數,影響 HTTP 狀態)
|
||||
- `DDD` = Detail(細節,三位數,自定義業務碼)
|
||||
|
||||
> 例如:`10101000` → Scope=10、Category=101(InputInvalidFormat)、Detail=000。
|
||||
|
||||
## 目錄
|
||||
- [1) 快速查表](#1-快速查表依類別整理)
|
||||
- [2) 使用範例](#2-使用範例)
|
||||
- [3) 小撇步與慣例](#3-小撇步與慣例)
|
||||
- [4) 安裝與測試](#4-安裝與測試)
|
||||
- [5) 變更日誌](#5-變更日誌)
|
||||
|
||||
---
|
||||
|
||||
## 1) 快速查表(依類別整理)
|
||||
|
||||
### A. Input(Category 1xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 |
|
||||
| `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 |
|
||||
| `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 |
|
||||
|
||||
### B. DB(Category 2xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------|:----:|---|
|
||||
| `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 |
|
||||
| `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 |
|
||||
| `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 |
|
||||
|
||||
### C. Resource(Category 3xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------------|:----:|---|
|
||||
| `ResNotFound` (301) | 資源未找到 | **404 Not Found** | 目標不存在/無此 ID。 |
|
||||
| `ResInvalidFormat` (302) | 無效資源格式 | **422 Unprocessable Entity** | 表示層/Schema 不符。 |
|
||||
| `ResAlreadyExist` (303) | 資源已存在 | **409 Conflict** | 重複建立/命名衝突。 |
|
||||
| `ResInsufficient` (304) | 資源不足 | **400 Bad Request** | 數量/容量不足(用戶可改參數再試)。 |
|
||||
| `ResInsufficientPerm` (305) | 權限不足 | **403 Forbidden** | 已驗證但無權限。 |
|
||||
| `ResInvalidMeasureID` (306) | 無效測量ID | **400 Bad Request** | ID 本身不合法。 |
|
||||
| `ResExpired` (307) | 資源過期 | **410 Gone** | 已不可用(可於上層補 Location)。 |
|
||||
| `ResMigrated` (308) | 資源已遷移 | **410 Gone** | 同上,如需導引請於上層處理。 |
|
||||
| `ResInvalidState` (309) | 無效狀態 | **409 Conflict** | 當前狀態不允許此操作。 |
|
||||
| `ResInsufficientQuota` (310) | 配額不足 | **429 Too Many Requests** | 達配額/速率限制。 |
|
||||
| `ResMultiOwner` (311) | 多所有者 | **409 Conflict** | 所有權歧異造成衝突。 |
|
||||
|
||||
### D. Auth(Category 5xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|-------------------------|:----:|---|
|
||||
| `AuthUnauthorized` (501) | 未授權/未驗證 | **401 Unauthorized** | 缺 Token、無效 Token。 |
|
||||
| `AuthExpired` (502) | 授權過期 | **401 Unauthorized** | Token 過期或時效失效。 |
|
||||
| `AuthInvalidPosixTime` (503) | 無效 POSIX 時間 | **401 Unauthorized** | 時戳異常導致驗簽失敗。 |
|
||||
| `AuthSigPayloadMismatch` (504) | 簽名與載荷不符 | **401 Unauthorized** | 驗簽失敗。 |
|
||||
| `AuthForbidden` (505) | 禁止存取 | **403 Forbidden** | 已驗證但沒有操作權限。 |
|
||||
|
||||
### E. System(Category 6xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `SysInternal` (601) | 系統內部錯誤 | **500 Internal Server Error** | 未預期的系統錯。 |
|
||||
| `SysMaintain` (602) | 系統維護中 | **503 Service Unavailable** | 維護/停機。 |
|
||||
| `SysTimeout` (603) | 系統超時 | **504 Gateway Timeout** | 下游/處理逾時。 |
|
||||
| `SysTooManyRequest` (604) | 請求過多 | **429 Too Many Requests** | 節流/限流。 |
|
||||
|
||||
### F. PubSub(Category 7xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------|:----:|---|
|
||||
| `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 |
|
||||
| `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 |
|
||||
| `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 |
|
||||
|
||||
### G. Service(Category 8xx)
|
||||
|
||||
| Category 常數 | 說明 | HTTP | 原因/說明 |
|
||||
|---|---------------|:----:|---|
|
||||
| `SvcInternal` (801) | 服務內部錯誤 | **500 Internal Server Error** | 非基礎設施層的內錯。 |
|
||||
| `SvcThirdParty` (802) | 第三方失敗 | **502 Bad Gateway** | 呼叫外部服務失敗。 |
|
||||
| `SvcHTTP400` (803) | 明確指派 400 | **400 Bad Request** | 自行指定。 |
|
||||
| `SvcMaintenance` (804) | 服務維護中 | **503 Service Unavailable** | 模組級維運中。 |
|
||||
|
||||
---
|
||||
|
||||
## 2) 使用範例
|
||||
|
||||
### 2.1 在 Handler 中回傳錯誤
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
errs "gitlab.supermicro.com/infra/infra-core/errors"
|
||||
"gitlab.supermicro.com/infra/infra-core/errors/code"
|
||||
)
|
||||
|
||||
func init() {
|
||||
errs.Scope = code.Gateway // 設定當前服務的 Scope
|
||||
}
|
||||
|
||||
func GetUser(w http.ResponseWriter, r *http.Request) error {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
return errs.InputInvalidFormatError("缺少參數: id") // 現在是 8 位碼
|
||||
}
|
||||
|
||||
u, err := repo.Find(r.Context(), id)
|
||||
switch {
|
||||
case errors.Is(err, repo.ErrNotFound):
|
||||
return errs.ResNotFoundError("user", id)
|
||||
case err != nil:
|
||||
return errs.DBErrorError("查詢使用者失敗").Wrap(err) // Wrap 內部錯誤
|
||||
}
|
||||
|
||||
// … 寫入回應
|
||||
return nil
|
||||
}
|
||||
|
||||
// 統一寫出 HTTP 錯誤
|
||||
func writeHTTP(w http.ResponseWriter, e *errs.Error) {
|
||||
http.Error(w, e.Error(), e.HTTPStatus())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 取出 Wrap 的內部錯誤
|
||||
|
||||
```go
|
||||
if internal := e.Unwrap(); internal != nil {
|
||||
log.Error("Internal error: ", internal)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 搭配日誌裝飾器(`WithLog` / `WithLogWrap`)
|
||||
|
||||
```go
|
||||
log := logger.WithFields(errs.LogField{Key: "req_id", Val: rid})
|
||||
|
||||
if badInput {
|
||||
return errs.WithLog(log, nil, errs.InputInvalidFormatError, "email 無效")
|
||||
}
|
||||
|
||||
if err := repo.Save(ctx, u); err != nil {
|
||||
return errs.WithLogWrap(
|
||||
log,
|
||||
[]errs.LogField{{Key: "entity", Val: "user"}, {Key: "op", Val: "save"}},
|
||||
errs.DBErrorError,
|
||||
err,
|
||||
"儲存失敗",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 只知道 Category+Detail 的動態場景(`EL` / `ELWrap`)
|
||||
|
||||
```go
|
||||
// 依流程動態產生
|
||||
return errs.EL(log, nil, code.SysTimeout, 123, "下游逾時") // 自定義 detail=123
|
||||
|
||||
// 或需保留 cause:
|
||||
return errs.ELWrap(log, nil, code.SvcThirdParty, 456, err, "金流商失敗")
|
||||
```
|
||||
|
||||
### 2.5 gRPC 互通
|
||||
|
||||
```go
|
||||
// 由 *errs.Error 轉為 gRPC status
|
||||
st := e.GRPCStatus() // *status.Status
|
||||
|
||||
// 客戶端收到 gRPC error → 轉回 *errs.Error
|
||||
e := errs.FromGRPCError(grpcErr)
|
||||
fmt.Println(e.DisplayCode(), e.Error()) // e.g., "10101000" "error msg"
|
||||
```
|
||||
|
||||
### 2.6 從 8 碼反解(`FromCode`)
|
||||
|
||||
```go
|
||||
e := errs.FromCode(10101000) // 10101000
|
||||
fmt.Println(e.Scope(), e.Category(), e.Detail()) // 10, 101, 000
|
||||
```
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"backend/pkg/library/errors/code"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Scope is a global variable that should be set by the service or module.
|
||||
var Scope = code.Unset
|
||||
|
||||
// Error represents a structured error with an 8-digit code.
|
||||
// The code is composed of a 2-digit scope, a 3-digit category, and a 3-digit detail.
|
||||
// Format: SSCCCDDD
|
||||
type Error struct {
|
||||
scope uint32 // 2-digit service scope
|
||||
category uint32 // 3-digit category
|
||||
detail uint32 // 3-digit detail
|
||||
msg string // Display message for the client
|
||||
internalErr error // The actual underlying error
|
||||
}
|
||||
|
||||
// New creates a new Error.
|
||||
// It ensures that category is within 0-999 and detail is within 0-999.
|
||||
func New(scope, category, detail uint32, displayMsg string) *Error {
|
||||
if category > uint32(code.MaxCategory) {
|
||||
category = uint32(code.ReservedMaxCategory)
|
||||
}
|
||||
if detail > uint32(code.MaxDetail) {
|
||||
detail = uint32(code.ReservedMaxDetail)
|
||||
}
|
||||
|
||||
return &Error{
|
||||
scope: scope,
|
||||
category: category,
|
||||
detail: detail,
|
||||
msg: displayMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the display message. This is intended for the client.
|
||||
// For internal logging and debugging, use Unwrap() to get the underlying error.
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// Scope returns the 2-digit scope of the error.
|
||||
func (e *Error) Scope() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.Unset)
|
||||
}
|
||||
|
||||
return e.scope
|
||||
}
|
||||
|
||||
// Category returns the 3-digit category of the error.
|
||||
func (e *Error) Category() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.DefaultCategory)
|
||||
}
|
||||
|
||||
return e.category
|
||||
}
|
||||
|
||||
// Detail returns the 2-digit detail code of the error.
|
||||
func (e *Error) Detail() uint32 {
|
||||
if e == nil {
|
||||
return uint32(code.DefaultDetail)
|
||||
}
|
||||
|
||||
return e.detail
|
||||
}
|
||||
|
||||
// SubCode returns the 6-digit code (category + detail).
|
||||
func (e *Error) SubCode() uint32 {
|
||||
if e == nil {
|
||||
return code.OK
|
||||
}
|
||||
c := e.category*code.CategoryMultiplier + e.detail
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Code returns the full 8-digit error code (scope + category + detail).
|
||||
func (e *Error) Code() uint32 {
|
||||
if e == nil {
|
||||
return code.NonCode
|
||||
}
|
||||
|
||||
return e.Scope()*code.ScopeMultiplier + e.SubCode()
|
||||
}
|
||||
|
||||
// DisplayCode returns the 8-digit error code as a zero-padded string.
|
||||
func (e *Error) DisplayCode() string {
|
||||
if e == nil {
|
||||
return "00000000"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%08d", e.Code())
|
||||
}
|
||||
|
||||
// Is checks if the target error is of type *Error and has the same sub-code.
|
||||
// It is called by errors.Is(). Do not use it directly.
|
||||
func (e *Error) Is(target error) bool {
|
||||
var err *Error
|
||||
if !errors.As(target, &err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return e.SubCode() == err.SubCode()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying wrapped error.
|
||||
func (e *Error) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return e.internalErr
|
||||
}
|
||||
|
||||
// Wrap sets the internal error for the current error.
|
||||
func (e *Error) Wrap(internalErr error) *Error {
|
||||
if e != nil {
|
||||
e.internalErr = internalErr
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// GRPCStatus converts the error to a gRPC status.
|
||||
func (e *Error) GRPCStatus() *status.Status {
|
||||
if e == nil {
|
||||
return status.New(codes.OK, "")
|
||||
}
|
||||
|
||||
return status.New(codes.Code(e.Code()), e.Error())
|
||||
}
|
||||
|
||||
// HTTPStatus returns the corresponding HTTP status code for the error.
|
||||
func (e *Error) HTTPStatus() int {
|
||||
if e == nil || e.SubCode() == code.OK {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
switch e.Category() {
|
||||
// Input
|
||||
case uint32(code.InputInvalidFormat):
|
||||
return http.StatusBadRequest // 400:輸入格式錯
|
||||
case uint32(code.InputNotValidImplementation),
|
||||
uint32(code.InputInvalidRange):
|
||||
return http.StatusUnprocessableEntity // 422:語意正確但無法處理(範圍/實作)
|
||||
|
||||
// DB
|
||||
case uint32(code.DBError):
|
||||
return http.StatusInternalServerError // 500:後端暫時性故障(若你偏好 503 可自行調整)
|
||||
case uint32(code.DBDataConvert):
|
||||
return http.StatusUnprocessableEntity // 422:可修正的資料轉換失敗
|
||||
case uint32(code.DBDuplicate):
|
||||
return http.StatusConflict // 409:唯一鍵/重複
|
||||
|
||||
// Resource
|
||||
case uint32(code.ResNotFound):
|
||||
return http.StatusNotFound // 404:資源不存在
|
||||
case uint32(code.ResInvalidFormat):
|
||||
return http.StatusUnprocessableEntity // 422:資源表示/格式不符
|
||||
case uint32(code.ResAlreadyExist):
|
||||
return http.StatusConflict // 409:已存在
|
||||
case uint32(code.ResInsufficient):
|
||||
return http.StatusBadRequest // 400:數量/容量/條件不足(可由客戶端修正)
|
||||
case uint32(code.ResInsufficientPerm):
|
||||
return http.StatusForbidden // 403:資源層面的權限不足
|
||||
case uint32(code.ResInvalidMeasureID):
|
||||
return http.StatusBadRequest // 400:ID 無效
|
||||
case uint32(code.ResExpired):
|
||||
return http.StatusGone // 410:資源已過期/不可用
|
||||
case uint32(code.ResMigrated):
|
||||
return http.StatusGone // 410:已遷移(若需導引可由上層加 Location)
|
||||
case uint32(code.ResInvalidState):
|
||||
return http.StatusConflict // 409:目前狀態不允許此操作
|
||||
case uint32(code.ResInsufficientQuota):
|
||||
return http.StatusTooManyRequests // 429:配額不足/達上限
|
||||
case uint32(code.ResMultiOwner):
|
||||
return http.StatusConflict // 409:多所有者衝突
|
||||
|
||||
// Auth
|
||||
case uint32(code.AuthUnauthorized),
|
||||
uint32(code.AuthExpired),
|
||||
uint32(code.AuthInvalidPosixTime),
|
||||
uint32(code.AuthSigPayloadMismatch):
|
||||
return http.StatusUnauthorized // 401:未驗證/無效憑證
|
||||
case uint32(code.AuthForbidden):
|
||||
return http.StatusForbidden // 403:有身分但沒權限
|
||||
|
||||
// System
|
||||
case uint32(code.SysTooManyRequest):
|
||||
return http.StatusTooManyRequests // 429:節流
|
||||
case uint32(code.SysInternal):
|
||||
return http.StatusInternalServerError // 500:系統內部錯
|
||||
case uint32(code.SysMaintain):
|
||||
return http.StatusServiceUnavailable // 503:維護中
|
||||
case uint32(code.SysTimeout):
|
||||
return http.StatusGatewayTimeout // 504:處理/下游逾時
|
||||
|
||||
// PubSub
|
||||
case uint32(code.PSuPublish),
|
||||
uint32(code.PSuConsume):
|
||||
return http.StatusBadGateway // 502:訊息中介/外部匯流排失敗
|
||||
case uint32(code.PSuTooLarge):
|
||||
return http.StatusRequestEntityTooLarge // 413:訊息太大
|
||||
|
||||
// Service
|
||||
case uint32(code.SvcMaintenance):
|
||||
return http.StatusServiceUnavailable // 503:服務維護
|
||||
case uint32(code.SvcInternal):
|
||||
return http.StatusInternalServerError // 500:服務內部錯
|
||||
case uint32(code.SvcThirdParty):
|
||||
return http.StatusBadGateway // 502:第三方依賴失敗
|
||||
case uint32(code.SvcHTTP400):
|
||||
return http.StatusBadRequest // 400:明確指派 400
|
||||
}
|
||||
|
||||
// fallback
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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...)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 錯誤
|
||||
)
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package code
|
||||
|
||||
// Scope
|
||||
const (
|
||||
Unset uint32 = iota
|
||||
CloudEPPortalGW
|
||||
CloudEPMember
|
||||
CloudEPPermission
|
||||
CloudEPNotification
|
||||
CloudEPTweeting
|
||||
CloudEPOrder
|
||||
CloudEPFileStorage
|
||||
CloudEPProduct
|
||||
CloudEPSecKill
|
||||
CloudEPCart
|
||||
CloudEPComment
|
||||
CloudEPReaction
|
||||
)
|
||||
|
|
@ -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, " ")))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
// 檢查更新結果,若沒有匹配的文檔,則返回錯誤
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue