Compare commits
2 Commits
feat/permi
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
b1a8926532 | |
|
|
d71ffea750 |
|
|
@ -51,3 +51,11 @@ Token:
|
||||||
OneTimeTokenExpiry : 600s
|
OneTimeTokenExpiry : 600s
|
||||||
MaxTokensPerUser : 2
|
MaxTokensPerUser : 2
|
||||||
MaxTokensPerDevice : 2
|
MaxTokensPerDevice : 2
|
||||||
|
|
||||||
|
|
||||||
|
RoleConfig:
|
||||||
|
UIDPrefix: "AM"
|
||||||
|
UIDLength: 6
|
||||||
|
AdminRoleUID: "AM000000"
|
||||||
|
AdminUserUID: "B000000"
|
||||||
|
DefaultRoleName: "USER"
|
||||||
|
|
@ -3,11 +3,7 @@ syntax = "v1"
|
||||||
// ================ 通用響應 ================
|
// ================ 通用響應 ================
|
||||||
type (
|
type (
|
||||||
// 成功響應
|
// 成功響應
|
||||||
RespOK {
|
RespOK {}
|
||||||
Code int `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分頁響應
|
// 分頁響應
|
||||||
PagerResp {
|
PagerResp {
|
||||||
|
|
@ -17,10 +13,10 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
// 錯誤響應
|
// 錯誤響應
|
||||||
ErrorResp {
|
Resp {
|
||||||
Code int `json:"code"`
|
Code string `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Message string `json:"message"`
|
||||||
Details string `json:"details,omitempty"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ type (
|
||||||
|
|
||||||
// RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
|
// RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
|
||||||
RequestPasswordResetReq {
|
RequestPasswordResetReq {
|
||||||
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
|
Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機)
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +141,31 @@ type (
|
||||||
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
||||||
Authorization
|
Authorization
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MyInfo 用於獲取會員資訊的標準響應結構
|
||||||
|
MyInfo {
|
||||||
|
Platform string `json:"platform"` // 註冊平台
|
||||||
|
UID string `json:"uid"` // 用戶 UID
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL
|
||||||
|
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||||
|
Nickname *string `json:"nickname,omitempty"` // 暱稱
|
||||||
|
GenderCode *string `json:"gender_code,omitempty"` // 性別代碼
|
||||||
|
Birthdate *string `json:"birthdate,omitempty"` // 生日 (格式: 1993-04-17)
|
||||||
|
PhoneNumber *string `json:"phone_number,omitempty"` // 電話
|
||||||
|
IsPhoneVerified *bool `json:"is_phone_verified,omitempty"` // 手機是否已驗證
|
||||||
|
Email *string `json:"email,omitempty"` // 信箱
|
||||||
|
IsEmailVerified *bool `json:"is_email_verified,omitempty"` // 信箱是否已驗證
|
||||||
|
Address *string `json:"address,omitempty"` // 地址
|
||||||
|
UserStatus string `json:"user_status,omitempty"` // 用戶狀態
|
||||||
|
PreferredLanguage string `json:"preferred_language,omitempty"` // 偏好語言
|
||||||
|
Currency string `json:"currency,omitempty"` // 偏好幣種
|
||||||
|
AlarmCategory string `json:"alarm_category,omitempty"` // 告警狀態
|
||||||
|
PostCode *string `json:"post_code,omitempty"` // 郵遞區號
|
||||||
|
Carrier *string `json:"carrier,omitempty"` // 載具
|
||||||
|
Role string `json:"role"` // 角色
|
||||||
|
UpdateAt string `json:"update_at"`
|
||||||
|
CreateAt string `json:"create_at"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
@ -251,7 +276,7 @@ service gateway {
|
||||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||||
*/
|
*/
|
||||||
@handler getUserInfo
|
@handler getUserInfo
|
||||||
get /me (Authorization) returns (UserInfoResp)
|
get /me (Authorization) returns (MyInfo)
|
||||||
|
|
||||||
@doc(
|
@doc(
|
||||||
summary: "更新當前登入的會員資訊"
|
summary: "更新當前登入的會員資訊"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
db.role.deleteMany({
|
||||||
|
"uid": { "$in": ["ADMIN", "OPERATOR", "USER"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
db.role.insertMany([
|
||||||
|
{
|
||||||
|
"client_id": 1,
|
||||||
|
"uid": "ADMIN",
|
||||||
|
"name": "管理員",
|
||||||
|
"status": 1,
|
||||||
|
"create_time": NumberLong(1728745200),
|
||||||
|
"update_time": NumberLong(1728745200)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": 1,
|
||||||
|
"uid": "OPERATOR",
|
||||||
|
"name": "操作員",
|
||||||
|
"status": 1,
|
||||||
|
"create_time": NumberLong(1728745200),
|
||||||
|
"update_time": NumberLong(1728745200)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": 1,
|
||||||
|
"uid": "USER",
|
||||||
|
"name": "一般使用者",
|
||||||
|
"status": 1,
|
||||||
|
"create_time": NumberLong(1728745200),
|
||||||
|
"update_time": NumberLong(1728745200)
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 建立索引
|
||||||
|
db.role.createIndex({ "uid": 1 }, { unique: true });
|
||||||
|
db.role.createIndex({ "client_id": 1 });
|
||||||
|
db.role.createIndex({ "status": 1 });
|
||||||
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -2,8 +2,6 @@ module backend
|
||||||
|
|
||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
replace backend/pkg/library/errs => ./pkg/library/errs
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
|
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0
|
github.com/alicebob/miniredis/v2 v2.35.0
|
||||||
|
|
@ -20,7 +18,6 @@ require (
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/testcontainers/testcontainers-go v0.39.0
|
github.com/testcontainers/testcontainers-go v0.39.0
|
||||||
github.com/zeromicro/go-zero v1.9.1
|
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.mongodb.org/mongo-driver/v2 v2.3.0
|
||||||
go.uber.org/mock v0.6.0
|
go.uber.org/mock v0.6.0
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.42.0
|
||||||
|
|
@ -110,7 +107,6 @@ require (
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // 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/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // 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/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 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g=
|
||||||
github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY=
|
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 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,22 @@ type Config struct {
|
||||||
MaxTokensPerUser int
|
MaxTokensPerUser int
|
||||||
MaxTokensPerDevice int
|
MaxTokensPerDevice int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoleConfig 角色配置
|
||||||
|
RoleConfig struct {
|
||||||
|
// UID 前綴 (例如: AM, RL)
|
||||||
|
UIDPrefix string
|
||||||
|
|
||||||
|
// UID 數字長度
|
||||||
|
UIDLength int
|
||||||
|
|
||||||
|
// 管理員角色 UID
|
||||||
|
AdminRoleUID string
|
||||||
|
|
||||||
|
// 管理員用戶 UID
|
||||||
|
AdminUserUID string
|
||||||
|
|
||||||
|
// 預設角色名稱
|
||||||
|
DefaultRoleName string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
const SuccessCode = 10200
|
|
||||||
const SuccessMessage = "success"
|
|
||||||
const DefaultScope = "gateway"
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type RedisKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GenerateVerifyCodeRedisKey RedisKey = "rf_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (key RedisKey) ToString() string {
|
||||||
|
return string(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key RedisKey) With(s ...string) RedisKey {
|
||||||
|
parts := append([]string{string(key)}, s...)
|
||||||
|
|
||||||
|
return RedisKey(strings.Join(parts, ":"))
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/internal/domain"
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
ers "backend/pkg/library/errs"
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
|
@ -17,20 +16,21 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.LoginReq
|
var req types.LoginReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
e := errs.InvalidFormat(err.Error())
|
e := errs.InputInvalidFormatError(err.Error())
|
||||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||||
Code: int(e.FullCode()),
|
Code: e.DisplayCode(),
|
||||||
Msg: err.Error(),
|
Message: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||||
// e := errs.InvalidFormat(err.Error())
|
// e := errs.InputInvalidRangeError(err.Error())
|
||||||
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||||
// Code: int(e.FullCode()),
|
// Code: e.DisplayCode(),
|
||||||
// Msg: err.Error(),
|
// Message: err.Error(),
|
||||||
|
// Error: err,
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// return
|
// return
|
||||||
|
|
@ -39,16 +39,16 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
l := auth.NewLoginLogic(r.Context(), svcCtx)
|
l := auth.NewLoginLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.Login(&req)
|
resp, err := l.Login(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := ers.FromError(err)
|
e := errs.FromError(err)
|
||||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{
|
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||||
Code: int(e.FullCode()),
|
Code: e.DisplayCode(),
|
||||||
Msg: e.Error(),
|
Message: e.Error(),
|
||||||
Error: e,
|
Error: e.Unwrap(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{
|
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{
|
||||||
Code: domain.SuccessCode,
|
Code: code.SUCCESSCode,
|
||||||
Msg: domain.SuccessMessage,
|
Message: code.SUCCESSMessage,
|
||||||
Data: resp,
|
Data: resp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
|
|
@ -15,16 +17,40 @@ func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.RefreshTokenReq
|
var req types.RefreshTokenReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
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)
|
l := auth.NewRefreshTokenLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.RefreshToken(&req)
|
resp, err := l.RefreshToken(&req)
|
||||||
if err != nil {
|
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 {
|
} 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,8 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/internal/domain"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs"
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
|
|
@ -12,43 +12,42 @@ import (
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 註冊新帳號
|
|
||||||
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.LoginReq
|
var req types.LoginReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
e := errs.InvalidFormat(err.Error())
|
e := errs.InputInvalidFormatError(err.Error())
|
||||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||||
Code: int(e.FullCode()),
|
Code: e.DisplayCode(),
|
||||||
Msg: err.Error(),
|
Message: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
//if err := svcCtx.Validate.ValidateAll(req); err != nil {
|
||||||
e := errs.InvalidFormat(err.Error())
|
// e := errs.InvalidFormat(err.Error())
|
||||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{
|
// httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{
|
||||||
Code: int(e.FullCode()),
|
// Code: int64(e.FullCode()),
|
||||||
Msg: err.Error(),
|
// Message: err.Error(),
|
||||||
})
|
// })
|
||||||
|
//
|
||||||
return
|
// return
|
||||||
}
|
//}
|
||||||
|
|
||||||
l := auth.NewRegisterLogic(r.Context(), svcCtx)
|
l := auth.NewRegisterLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.Register(&req)
|
resp, err := l.Register(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.FromError(err)
|
e := errs.FromError(err)
|
||||||
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{
|
httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{
|
||||||
Code: int(e.FullCode()),
|
Code: e.DisplayCode(),
|
||||||
Msg: e.Error(),
|
Message: e.Error(),
|
||||||
Error: e,
|
Error: e.Unwrap(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{
|
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{
|
||||||
Code: domain.SuccessCode,
|
Code: code.SUCCESSCode,
|
||||||
Msg: domain.SuccessMessage,
|
Message: code.SUCCESSMessage,
|
||||||
Data: resp,
|
Data: resp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
|
|
@ -15,16 +17,40 @@ func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.RequestPasswordResetReq
|
var req types.RequestPasswordResetReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
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)
|
l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.RequestPasswordReset(&req)
|
resp, err := l.RequestPasswordReset(&req)
|
||||||
if err != nil {
|
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 {
|
} 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 auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
|
|
@ -15,16 +17,40 @@ func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.ResetPasswordReq
|
var req types.ResetPasswordReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
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)
|
l := auth.NewResetPasswordLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.ResetPassword(&req)
|
resp, err := l.ResetPassword(&req)
|
||||||
if err != nil {
|
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 {
|
} 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 auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/auth"
|
"backend/internal/logic/auth"
|
||||||
|
|
@ -15,16 +17,40 @@ func VerifyPasswordResetCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.VerifyCodeReq
|
var req types.VerifyCodeReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
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)
|
l := auth.NewVerifyPasswordResetCodeLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.VerifyPasswordResetCode(&req)
|
resp, err := l.VerifyPasswordResetCode(&req)
|
||||||
if err != nil {
|
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 {
|
} 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,9 @@
|
||||||
package ping
|
package ping
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/internal/types"
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/ping"
|
"backend/internal/logic/ping"
|
||||||
|
|
@ -15,9 +18,17 @@ func PingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
l := ping.NewPingLogic(r.Context(), svcCtx)
|
l := ping.NewPingLogic(r.Context(), svcCtx)
|
||||||
err := l.Ping()
|
err := l.Ping()
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
httpx.Ok(w)
|
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{
|
||||||
|
Code: code.SUCCESSCode,
|
||||||
|
Message: code.SUCCESSMessage,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/user"
|
"backend/internal/logic/user"
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,16 +16,40 @@ func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.Authorization
|
var req types.Authorization
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
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)
|
l := user.NewGetUserInfoLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.GetUserInfo(&req)
|
resp, err := l.GetUserInfo(&req)
|
||||||
if err != nil {
|
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 {
|
} 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
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/user"
|
"backend/internal/logic/user"
|
||||||
|
|
@ -15,16 +17,30 @@ func RequestVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.RequestVerificationCodeReq
|
var req types.RequestVerificationCodeReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx)
|
l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.RequestVerificationCode(&req)
|
resp, err := l.RequestVerificationCode(&req)
|
||||||
if err != nil {
|
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 {
|
} 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
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/user"
|
"backend/internal/logic/user"
|
||||||
|
|
@ -15,16 +17,30 @@ func SubmitVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.SubmitVerificationCodeReq
|
var req types.SubmitVerificationCodeReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx)
|
l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.SubmitVerificationCode(&req)
|
resp, err := l.SubmitVerificationCode(&req)
|
||||||
if err != nil {
|
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 {
|
} 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
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/user"
|
"backend/internal/logic/user"
|
||||||
|
|
@ -15,16 +17,30 @@ func UpdatePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.UpdatePasswordReq
|
var req types.UpdatePasswordReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := user.NewUpdatePasswordLogic(r.Context(), svcCtx)
|
l := user.NewUpdatePasswordLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.UpdatePassword(&req)
|
resp, err := l.UpdatePassword(&req)
|
||||||
if err != nil {
|
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 {
|
} 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
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/library/errors/code"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"backend/internal/logic/user"
|
"backend/internal/logic/user"
|
||||||
|
|
@ -15,16 +17,30 @@ func UpdateUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.UpdateUserInfoReq
|
var req types.UpdateUserInfoReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx)
|
l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx)
|
||||||
resp, err := l.UpdateUserInfo(&req)
|
resp, err := l.UpdateUserInfo(&req)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{
|
||||||
|
Code: code.SUCCESSCode,
|
||||||
|
Message: code.SUCCESSMessage,
|
||||||
|
Data: resp,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// 生成 Token
|
// 生成 Token
|
||||||
func generateToken(svc *svc.ServiceContext, ctx context.Context, req *types.LoginReq, uid string) (entity.TokenResp, error) {
|
func generateToken(svc *svc.ServiceContext, ctx context.Context, req *types.LoginReq, uid string, role string) (entity.TokenResp, error) {
|
||||||
// scope role 要修改,refresh tl
|
|
||||||
role := "user"
|
|
||||||
|
|
||||||
tk, err := svc.TokenUC.NewToken(ctx, entity.AuthorizationReq{
|
tk, err := svc.TokenUC.NewToken(ctx, entity.AuthorizationReq{
|
||||||
GrantType: token.ClientCredentials.ToString(),
|
GrantType: token.ClientCredentials.ToString(),
|
||||||
DeviceID: uid, // TODO 沒傳暫時先用UID 替代
|
DeviceID: uid, // TODO 沒傳暫時先用UID 替代
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
memberD "backend/pkg/member/domain/member"
|
memberD "backend/pkg/member/domain/member"
|
||||||
member "backend/pkg/member/domain/usecase"
|
member "backend/pkg/member/domain/usecase"
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -41,7 +40,7 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cr.Status {
|
if !cr.Status {
|
||||||
return nil, errs.Unauthorized("failed to verify password")
|
return nil, errs.AuthUnauthorizedError("failed to verify password")
|
||||||
}
|
}
|
||||||
case "platform":
|
case "platform":
|
||||||
switch req.Platform.Provider {
|
switch req.Platform.Provider {
|
||||||
|
|
@ -66,10 +65,10 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
||||||
}
|
}
|
||||||
req.LoginID = userInfo.UserID
|
req.LoginID = userInfo.UserID
|
||||||
default:
|
default:
|
||||||
return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "unsupported 3 party platform")
|
return nil, errs.InputInvalidFormatError("unsupported 3 party platform")
|
||||||
}
|
}
|
||||||
default:
|
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{
|
account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, member.GetUIDByAccountRequest{
|
||||||
|
|
@ -79,7 +78,12 @@ func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err erro
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID)
|
userRole, err := l.svcCtx.UserRoleUC.Get(l.ctx, account.UID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID, userRole.RoleUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/internal/domain"
|
|
||||||
"backend/pkg/permission/domain/entity"
|
"backend/pkg/permission/domain/entity"
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -35,7 +34,7 @@ func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *type
|
||||||
|
|
||||||
tk, err := l.svcCtx.TokenUC.RefreshToken(l.ctx, entity.RefreshTokenReq{
|
tk, err := l.svcCtx.TokenUC.RefreshToken(l.ctx, entity.RefreshTokenReq{
|
||||||
Token: req.RefreshToken,
|
Token: req.RefreshToken,
|
||||||
Scope: domain.DefaultScope,
|
Scope: "gateway",
|
||||||
Expires: time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshTokenExpiry).Unix(),
|
Expires: time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshTokenExpiry).Unix(),
|
||||||
DeviceID: data["uid"],
|
DeviceID: data["uid"],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ package auth
|
||||||
import (
|
import (
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
mb "backend/pkg/member/domain/member"
|
mb "backend/pkg/member/domain/member"
|
||||||
member "backend/pkg/member/domain/usecase"
|
member "backend/pkg/member/domain/usecase"
|
||||||
|
"backend/pkg/permission/domain/usecase"
|
||||||
"context"
|
"context"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
||||||
case "credentials":
|
case "credentials":
|
||||||
fn, ok := PrepareFunc[mb.Digimon.ToString()]
|
fn, ok := PrepareFunc[mb.Digimon.ToString()]
|
||||||
if !ok {
|
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)
|
bd, err = fn(l.ctx, req, l.svcCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -50,14 +50,14 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
||||||
case "platform":
|
case "platform":
|
||||||
fn, ok := PrepareFunc[req.Platform.Provider]
|
fn, ok := PrepareFunc[req.Platform.Provider]
|
||||||
if !ok {
|
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)
|
bd, err = fn(l.ctx, req, l.svcCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
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: 建立帳號
|
// Step 2: 建立帳號
|
||||||
|
|
@ -76,9 +76,18 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = l.svcCtx.UserRoleUC.Assign(l.ctx, usecase.AssignRoleRequest{
|
||||||
|
RoleUID: l.svcCtx.Config.RoleConfig.DefaultRoleName,
|
||||||
|
UserUID: account.UID,
|
||||||
|
Brand: "digimon",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: 生成 Token
|
// Step 5: 生成 Token
|
||||||
req.LoginID = bd.CreateAccountReq.LoginID
|
req.LoginID = bd.CreateAccountReq.LoginID
|
||||||
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID)
|
tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID, l.svcCtx.Config.RoleConfig.DefaultRoleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/internal/utils"
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/member/domain/member"
|
||||||
|
"backend/pkg/member/domain/usecase"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
|
|
@ -25,7 +31,109 @@ func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
||||||
|
|
||||||
// RequestPasswordReset 請求發送密碼重設驗證碼 aka 忘記密碼
|
// RequestPasswordReset 請求發送密碼重設驗證碼 aka 忘記密碼
|
||||||
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) {
|
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) {
|
||||||
// todo: add your logic here and delete this line
|
// 驗證並標準化帳號
|
||||||
|
acc, err := l.validateAndNormalizeAccount(req.AccountType, req.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return
|
// 檢查發送冷卻時間
|
||||||
|
rk := domain.GenerateVerifyCodeRedisKey.With(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword)).ToString()
|
||||||
|
if err := l.checkVerifyCodeCooldown(rk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 確認帳號是否註冊並檢查平台限制
|
||||||
|
if err := l.checkAccountAndPlatform(acc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成驗證碼
|
||||||
|
vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, usecase.GenerateRefreshCodeRequest{
|
||||||
|
LoginID: acc,
|
||||||
|
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取用戶資訊並確認綁定帳號
|
||||||
|
account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: acc})
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 發送驗證碼
|
||||||
|
fmt.Println("======= send", vcode.Data.VerifyCode, &info)
|
||||||
|
|
||||||
|
//nickname := getEmailShowName(&info)
|
||||||
|
//if err := l.sendVerificationCode(req.AccountType, acc, &info, vcode.Data.VerifyCode, nickname); err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 設置 Redis 鍵
|
||||||
|
l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, 60)
|
||||||
|
|
||||||
|
return &types.RespOK{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAndNormalizeAccount 驗證並標準化帳號
|
||||||
|
func (l *RequestPasswordResetLogic) validateAndNormalizeAccount(accountType, account string) (string, error) {
|
||||||
|
switch member.GetAccountTypeByCode(accountType) {
|
||||||
|
case member.AccountTypePhone:
|
||||||
|
phone, isPhone := utils.NormalizeTaiwanMobile(account)
|
||||||
|
if !isPhone {
|
||||||
|
return "", errs.InputInvalidFormatError("phone number is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return phone, nil
|
||||||
|
case member.AccountTypeMail:
|
||||||
|
if !utils.IsValidEmail(account) {
|
||||||
|
return "", errs.InputInvalidFormatError("email is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
case member.AccountTypeNone, member.AccountTypeDefine:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
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.SysTooManyRequestError("verification code already sent, please wait 3min for system to send again")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAccountAndPlatform 檢查帳號是否註冊及平台限制
|
||||||
|
func (l *RequestPasswordResetLogic) checkAccountAndPlatform(acc string) error {
|
||||||
|
accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: acc})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if accountInfo.Data.Platform != member.Digimon {
|
||||||
|
return errs.InputInvalidFormatError(
|
||||||
|
"failed to send verify code since platform not correct")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.DBErrorErrorL(l.svcCtx.Logger, []errs.LogField{
|
||||||
|
{Key: "redisKey", Val: rk},
|
||||||
|
{Key: "error", Val: err.Error()},
|
||||||
|
}, "failed to set redis expire").Wrap(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/member/domain/member"
|
||||||
|
"backend/pkg/member/domain/usecase"
|
||||||
|
"backend/pkg/permission/domain/entity"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
|
|
@ -15,7 +22,7 @@ type ResetPasswordLogic struct {
|
||||||
svcCtx *svc.ServiceContext
|
svcCtx *svc.ServiceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// 執行密碼重設
|
// NewResetPasswordLogic 執行密碼重設
|
||||||
func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic {
|
func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic {
|
||||||
return &ResetPasswordLogic{
|
return &ResetPasswordLogic{
|
||||||
Logger: logx.WithContext(ctx),
|
Logger: logx.WithContext(ctx),
|
||||||
|
|
@ -24,8 +31,58 @@ func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (resp *types.RespOK, err error) {
|
func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (*types.RespOK, error) {
|
||||||
// todo: add your logic here and delete this line
|
// 驗證密碼,兩次密碼要一致
|
||||||
|
if req.Password != req.PasswordConfirm {
|
||||||
|
return nil, errs.InputInvalidFormatError("password confirmation does not match")
|
||||||
|
}
|
||||||
|
|
||||||
return
|
// 驗證碼
|
||||||
|
err := l.svcCtx.AccountUC.VerifyRefreshCode(l.ctx, usecase.VerifyRefreshCodeRequest{
|
||||||
|
LoginID: req.Identifier,
|
||||||
|
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||||
|
VerifyCode: req.VerifyCode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// 表使沒有這驗證碼
|
||||||
|
return nil, errs.AuthForbiddenError("failed to get verify code")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: req.Identifier})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Data.Platform != member.Digimon {
|
||||||
|
return nil, errs.AuthForbiddenError("invalid platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
err = l.svcCtx.AccountUC.UpdateUserToken(l.ctx, usecase.UpdateTokenRequest{
|
||||||
|
Account: req.Identifier,
|
||||||
|
Token: req.Password,
|
||||||
|
Platform: member.Digimon.ToInt64(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rk := domain.GenerateVerifyCodeRedisKey.With(
|
||||||
|
fmt.Sprintf("%s-%d", req.Identifier, member.GenerateCodeTypeForgetPassword),
|
||||||
|
).ToString()
|
||||||
|
|
||||||
|
_, _ = l.svcCtx.Redis.Del(rk)
|
||||||
|
|
||||||
|
ac, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: req.Identifier})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.svcCtx.TokenUC.CancelTokens(l.ctx, entity.DoTokenByUIDReq{UID: ac.UID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功響應
|
||||||
|
return &types.RespOK{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
|
"backend/pkg/member/domain/member"
|
||||||
|
"backend/pkg/member/domain/usecase"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
|
|
@ -25,7 +28,16 @@ func NewVerifyPasswordResetCodeLogic(ctx context.Context, svcCtx *svc.ServiceCon
|
||||||
|
|
||||||
// VerifyPasswordResetCode 校驗密碼重設驗證碼(頁面需求,預先檢查看看, 顯示表演用)
|
// VerifyPasswordResetCode 校驗密碼重設驗證碼(頁面需求,預先檢查看看, 顯示表演用)
|
||||||
func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.VerifyCodeReq) (resp *types.RespOK, err error) {
|
func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.VerifyCodeReq) (resp *types.RespOK, err error) {
|
||||||
// todo: add your logic here and delete this line
|
// 先驗證,不刪除
|
||||||
|
if err := l.svcCtx.AccountUC.CheckRefreshCode(l.ctx, usecase.VerifyRefreshCodeRequest{
|
||||||
|
VerifyCode: req.VerifyCode,
|
||||||
|
LoginID: req.Identifier,
|
||||||
|
CodeType: member.GenerateCodeTypeForgetPassword,
|
||||||
|
}); err != nil {
|
||||||
|
e := errs.AuthForbiddenError("failed to get verify code").Wrap(err)
|
||||||
|
|
||||||
return
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.RespOK{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/pkg/member/domain/member"
|
||||||
|
"backend/pkg/member/domain/usecase"
|
||||||
|
"backend/pkg/permission/domain/token"
|
||||||
"context"
|
"context"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"time"
|
||||||
|
|
||||||
"backend/internal/svc"
|
"backend/internal/svc"
|
||||||
"backend/internal/types"
|
"backend/internal/types"
|
||||||
|
|
@ -15,7 +20,7 @@ type GetUserInfoLogic struct {
|
||||||
svcCtx *svc.ServiceContext
|
svcCtx *svc.ServiceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取得當前登入的會員資訊(自己)
|
// NewGetUserInfoLogic 取得當前登入的會員資訊(自己)
|
||||||
func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic {
|
func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic {
|
||||||
return &GetUserInfoLogic{
|
return &GetUserInfoLogic{
|
||||||
Logger: logx.WithContext(ctx),
|
Logger: logx.WithContext(ctx),
|
||||||
|
|
@ -24,8 +29,88 @@ func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (resp *types.UserInfoResp, err error) {
|
func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (*types.MyInfo, error) {
|
||||||
// todo: add your logic here and delete this line
|
uid := token.UID(l.ctx)
|
||||||
|
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{
|
||||||
|
UID: uid,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return
|
byUID, err := l.svcCtx.AccountUC.FindLoginIDByUID(l.ctx, uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{
|
||||||
|
Account: byUID.LoginID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userRole, err := l.svcCtx.UserRoleUC.Get(l.ctx, uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
role := userRole.RoleUID
|
||||||
|
res := &types.MyInfo{
|
||||||
|
Platform: accountInfo.Data.Platform.ToString(),
|
||||||
|
UID: info.UID,
|
||||||
|
UpdateAt: time.Unix(0, info.CreateTime).UTC().Format(time.RFC3339),
|
||||||
|
CreateAt: time.Unix(0, info.UpdateTime).UTC().Format(time.RFC3339),
|
||||||
|
Role: role,
|
||||||
|
UserStatus: info.UserStatus.CodeToString(),
|
||||||
|
PreferredLanguage: info.PreferredLanguage,
|
||||||
|
Currency: info.Currency,
|
||||||
|
AlarmCategory: info.AlarmCategory.CodeToString(),
|
||||||
|
}
|
||||||
|
if info.Address != nil {
|
||||||
|
res.Address = info.Address
|
||||||
|
}
|
||||||
|
if info.AvatarURL != nil {
|
||||||
|
res.AvatarURL = info.AvatarURL
|
||||||
|
}
|
||||||
|
if info.FullName != nil {
|
||||||
|
res.FullName = info.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Birthdate != nil {
|
||||||
|
b := ToDate(info.Birthdate)
|
||||||
|
res.Birthdate = b
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Address != nil {
|
||||||
|
res.Address = info.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Nickname != nil {
|
||||||
|
res.Nickname = info.Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Email != nil {
|
||||||
|
res.Email = info.Email
|
||||||
|
res.IsEmailVerified = proto.Bool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.PhoneNumber != nil {
|
||||||
|
res.PhoneNumber = info.PhoneNumber
|
||||||
|
res.IsPhoneVerified = proto.Bool(true)
|
||||||
|
}
|
||||||
|
if info.GenderCode != nil {
|
||||||
|
gc := member.GetGenderByCode(*info.GenderCode)
|
||||||
|
res.GenderCode = &gc
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToDate(n *int64) *string {
|
||||||
|
result := ""
|
||||||
|
if n != nil {
|
||||||
|
result = time.Unix(*n, 0).UTC().Format(time.DateOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,84 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"backend/internal/types"
|
||||||
|
"backend/pkg/permission/domain/entity"
|
||||||
|
"backend/pkg/permission/domain/token"
|
||||||
|
"context"
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
|
||||||
type AuthMiddleware struct {
|
"backend/pkg/permission/domain/usecase"
|
||||||
|
uc "backend/pkg/permission/usecase"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthMiddlewareParam struct {
|
||||||
|
TokenSec string
|
||||||
|
TokenUseCase usecase.TokenUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthMiddleware() *AuthMiddleware {
|
type AuthMiddleware struct {
|
||||||
return &AuthMiddleware{}
|
TokenSec string
|
||||||
|
TokenUseCase usecase.TokenUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthMiddleware(param AuthMiddlewareParam) *AuthMiddleware {
|
||||||
|
return &AuthMiddleware{
|
||||||
|
TokenSec: param.TokenSec,
|
||||||
|
TokenUseCase: param.TokenUseCase,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO generate middleware implement function, delete after code implementation
|
// 解析 Header
|
||||||
|
header := types.Authorization{}
|
||||||
|
if err := httpx.ParseHeaders(r, &header); err != nil {
|
||||||
|
//m.writeErrorResponse(w, r, http.StatusBadRequest, "Failed to parse headers")
|
||||||
|
|
||||||
// Passthrough to next handler if need
|
return
|
||||||
next(w, r)
|
}
|
||||||
|
|
||||||
|
// 驗證 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))
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置 context 並傳遞給下一個處理器
|
||||||
|
ctx := SetContext(r, claim)
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetContext(r *http.Request, claim uc.TokenClaims) context.Context {
|
||||||
|
ctx := context.WithValue(r.Context(), token.KeyRole, claim.Role())
|
||||||
|
ctx = context.WithValue(ctx, token.KeyUID, claim.UID())
|
||||||
|
ctx = context.WithValue(ctx, token.KeyDeviceID, claim.DeviceID())
|
||||||
|
ctx = context.WithValue(ctx, token.KeyScope, claim.Scope())
|
||||||
|
ctx = context.WithValue(ctx, token.KeyLoginID, claim.LoginID())
|
||||||
|
|
||||||
|
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.Resp{
|
||||||
|
// Code: int(code),
|
||||||
|
// Msg: message,
|
||||||
|
// })
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"backend/pkg/member/repository"
|
"backend/pkg/member/repository"
|
||||||
uc "backend/pkg/member/usecase"
|
uc "backend/pkg/member/usecase"
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
"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),
|
VerifyCodeModel: repository.NewVerifyCodeRepository(rds),
|
||||||
GenerateUID: guid,
|
GenerateUID: guid,
|
||||||
Config: prepareCfg(c),
|
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 (
|
import (
|
||||||
"backend/internal/config"
|
"backend/internal/config"
|
||||||
"backend/internal/middleware"
|
"backend/internal/middleware"
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
"backend/pkg/library/errors/code"
|
||||||
|
"context"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
|
||||||
vi "backend/pkg/library/validator"
|
vi "backend/pkg/library/validator"
|
||||||
memberUC "backend/pkg/member/domain/usecase"
|
memberUC "backend/pkg/member/domain/usecase"
|
||||||
tokenUC "backend/pkg/permission/domain/usecase"
|
tokenUC "backend/pkg/permission/domain/usecase"
|
||||||
|
|
@ -19,6 +22,12 @@ type ServiceContext struct {
|
||||||
AccountUC memberUC.AccountUseCase
|
AccountUC memberUC.AccountUseCase
|
||||||
Validate vi.Validate
|
Validate vi.Validate
|
||||||
TokenUC tokenUC.TokenUseCase
|
TokenUC tokenUC.TokenUseCase
|
||||||
|
PermissionUC tokenUC.PermissionUseCase
|
||||||
|
RoleUC tokenUC.RoleUseCase
|
||||||
|
RolePermission tokenUC.RolePermissionUseCase
|
||||||
|
UserRoleUC tokenUC.UserRoleUseCase
|
||||||
|
Redis *redis.Redis
|
||||||
|
Logger errs.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceContext(c config.Config) *ServiceContext {
|
func NewServiceContext(c config.Config) *ServiceContext {
|
||||||
|
|
@ -26,13 +35,25 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
errs.Scope = code.CloudEPPortalGW
|
errs.Scope = code.Gateway
|
||||||
|
|
||||||
|
rp := NewPermissionUC(&c)
|
||||||
|
tkUC := NewTokenUC(&c, rds)
|
||||||
|
|
||||||
return &ServiceContext{
|
return &ServiceContext{
|
||||||
Config: c,
|
Config: c,
|
||||||
AuthMiddleware: middleware.NewAuthMiddleware().Handle,
|
AuthMiddleware: middleware.NewAuthMiddleware(middleware.AuthMiddlewareParam{
|
||||||
|
TokenSec: c.Token.AccessSecret,
|
||||||
|
TokenUseCase: tkUC,
|
||||||
|
}).Handle,
|
||||||
AccountUC: NewAccountUC(&c, rds),
|
AccountUC: NewAccountUC(&c, rds),
|
||||||
Validate: vi.MustValidator(),
|
Validate: vi.MustValidator(),
|
||||||
TokenUC: NewTokenUC(&c, rds),
|
TokenUC: tkUC,
|
||||||
|
PermissionUC: rp.PermissionUC,
|
||||||
|
RoleUC: rp.RoleUC,
|
||||||
|
RolePermission: rp.RolePermission,
|
||||||
|
UserRoleUC: rp.UserRole,
|
||||||
|
Redis: rds,
|
||||||
|
Logger: MustLogger(logx.WithContext(context.Background())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,14 @@ package svc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/internal/config"
|
"backend/internal/config"
|
||||||
|
mgo "backend/pkg/library/mongo"
|
||||||
"backend/pkg/permission/domain/usecase"
|
"backend/pkg/permission/domain/usecase"
|
||||||
"backend/pkg/permission/repository"
|
"backend/pkg/permission/repository"
|
||||||
uc "backend/pkg/permission/usecase"
|
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"
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,5 +19,105 @@ func NewTokenUC(c *config.Config, rds *redis.Redis) usecase.TokenUseCase {
|
||||||
Redis: rds,
|
Redis: rds,
|
||||||
}),
|
}),
|
||||||
Config: c,
|
Config: c,
|
||||||
|
Logger: MustLogger(logx.WithContext(context.Background())),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PermissionUC struct {
|
||||||
|
PermissionUC usecase.PermissionUseCase
|
||||||
|
RoleUC usecase.RoleUseCase
|
||||||
|
RolePermission usecase.RolePermissionUseCase
|
||||||
|
UserRole usecase.UserRoleUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPermissionUC(c *config.Config) PermissionUC {
|
||||||
|
// 準備Mongo Config
|
||||||
|
conf := &mgo.Conf{
|
||||||
|
Schema: c.Mongo.Schema,
|
||||||
|
Host: c.Mongo.Host,
|
||||||
|
Database: c.Mongo.Database,
|
||||||
|
MaxStaleness: c.Mongo.MaxStaleness,
|
||||||
|
MaxPoolSize: c.Mongo.MaxPoolSize,
|
||||||
|
MinPoolSize: c.Mongo.MinPoolSize,
|
||||||
|
MaxConnIdleTime: c.Mongo.MaxConnIdleTime,
|
||||||
|
Compressors: c.Mongo.Compressors,
|
||||||
|
EnableStandardReadWriteSplitMode: c.Mongo.EnableStandardReadWriteSplitMode,
|
||||||
|
ConnectTimeoutMs: c.Mongo.ConnectTimeoutMs,
|
||||||
|
}
|
||||||
|
if c.Mongo.User != "" {
|
||||||
|
conf.User = c.Mongo.User
|
||||||
|
conf.Password = c.Mongo.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快取選項
|
||||||
|
cacheOpts := []cache.Option{
|
||||||
|
cache.WithExpiry(c.CacheExpireTime),
|
||||||
|
cache.WithNotFoundExpiry(c.CacheWithNotFoundExpiry),
|
||||||
|
}
|
||||||
|
dbOpts := []mon.Option{
|
||||||
|
mgo.SetCustomDecimalType(),
|
||||||
|
mgo.InitMongoOptions(*conf),
|
||||||
|
}
|
||||||
|
permRepo := repository.NewPermissionRepository(repository.PermissionRepositoryParam{
|
||||||
|
Conf: conf,
|
||||||
|
CacheConf: c.Cache,
|
||||||
|
CacheOpts: cacheOpts,
|
||||||
|
DBOpts: dbOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
rolePermRepo := repository.NewRolePermissionRepository(repository.RolePermissionRepositoryParam{
|
||||||
|
Conf: conf,
|
||||||
|
CacheConf: c.Cache,
|
||||||
|
CacheOpts: cacheOpts,
|
||||||
|
DBOpts: dbOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
roleRepo := repository.NewRoleRepository(repository.RoleRepositoryParam{
|
||||||
|
Conf: conf,
|
||||||
|
CacheConf: c.Cache,
|
||||||
|
CacheOpts: cacheOpts,
|
||||||
|
DBOpts: dbOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
userRoleRepo := repository.NewUserRoleRepository(repository.UserRoleRepositoryParam{
|
||||||
|
Conf: conf,
|
||||||
|
CacheConf: c.Cache,
|
||||||
|
CacheOpts: cacheOpts,
|
||||||
|
DBOpts: dbOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
puc := uc.NewPermissionUseCase(uc.PermissionUseCaseParam{
|
||||||
|
RoleRepo: roleRepo,
|
||||||
|
RolePermRepo: rolePermRepo,
|
||||||
|
UserRoleRepo: userRoleRepo,
|
||||||
|
PermRepo: permRepo,
|
||||||
|
})
|
||||||
|
rpuc := uc.NewRolePermissionUseCase(uc.RolePermissionUseCaseParam{
|
||||||
|
RoleRepo: roleRepo,
|
||||||
|
RolePermRepo: rolePermRepo,
|
||||||
|
UserRoleRepo: userRoleRepo,
|
||||||
|
PermRepo: permRepo,
|
||||||
|
PermUseCase: puc,
|
||||||
|
AdminRoleUID: c.RoleConfig.AdminRoleUID,
|
||||||
|
})
|
||||||
|
ruc := uc.NewRoleUseCase(uc.RoleUseCaseParam{
|
||||||
|
RoleRepo: roleRepo,
|
||||||
|
UserRoleRepo: userRoleRepo,
|
||||||
|
Config: uc.RoleUseCaseConfig{
|
||||||
|
AdminRoleUID: c.RoleConfig.AdminRoleUID,
|
||||||
|
UIDPrefix: c.RoleConfig.UIDPrefix,
|
||||||
|
UIDLength: c.RoleConfig.UIDLength,
|
||||||
|
},
|
||||||
|
RolePermUseCase: rpuc,
|
||||||
|
})
|
||||||
|
|
||||||
|
return PermissionUC{
|
||||||
|
PermissionUC: puc,
|
||||||
|
RolePermission: rpuc,
|
||||||
|
RoleUC: ruc,
|
||||||
|
UserRole: uc.NewUserRoleUseCase(uc.UserRoleUseCaseParam{
|
||||||
|
UserRoleRepo: userRoleRepo,
|
||||||
|
RoleRepo: roleRepo,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,6 @@ type CredentialsPayload struct {
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=email phone any"` // 帳號型別 email phone any
|
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 {
|
type LoginReq struct {
|
||||||
AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` // 驗證類型 credentials platform
|
AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` // 驗證類型 credentials platform
|
||||||
LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
|
LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
|
||||||
|
|
@ -37,6 +30,30 @@ type LoginResp struct {
|
||||||
TokenType string `json:"token_type"` // 通常固定為 "Bearer"
|
TokenType string `json:"token_type"` // 通常固定為 "Bearer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MyInfo struct {
|
||||||
|
Platform string `json:"platform"` // 註冊平台
|
||||||
|
UID string `json:"uid"` // 用戶 UID
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL
|
||||||
|
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||||
|
Nickname *string `json:"nickname,omitempty"` // 暱稱
|
||||||
|
GenderCode *string `json:"gender_code,omitempty"` // 性別代碼
|
||||||
|
Birthdate *string `json:"birthdate,omitempty"` // 生日 (格式: 1993-04-17)
|
||||||
|
PhoneNumber *string `json:"phone_number,omitempty"` // 電話
|
||||||
|
IsPhoneVerified *bool `json:"is_phone_verified,omitempty"` // 手機是否已驗證
|
||||||
|
Email *string `json:"email,omitempty"` // 信箱
|
||||||
|
IsEmailVerified *bool `json:"is_email_verified,omitempty"` // 信箱是否已驗證
|
||||||
|
Address *string `json:"address,omitempty"` // 地址
|
||||||
|
UserStatus string `json:"user_status,omitempty"` // 用戶狀態
|
||||||
|
PreferredLanguage string `json:"preferred_language,omitempty"` // 偏好語言
|
||||||
|
Currency string `json:"currency,omitempty"` // 偏好幣種
|
||||||
|
AlarmCategory string `json:"alarm_category,omitempty"` // 告警狀態
|
||||||
|
PostCode *string `json:"post_code,omitempty"` // 郵遞區號
|
||||||
|
Carrier *string `json:"carrier,omitempty"` // 載具
|
||||||
|
Role string `json:"role"` // 角色
|
||||||
|
UpdateAt string `json:"update_at"`
|
||||||
|
CreateAt string `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type PagerResp struct {
|
type PagerResp struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
|
@ -60,7 +77,7 @@ type RefreshTokenResp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestPasswordResetReq struct {
|
type RequestPasswordResetReq struct {
|
||||||
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
|
Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機)
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,10 +93,14 @@ type ResetPasswordReq struct {
|
||||||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼
|
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼
|
||||||
}
|
}
|
||||||
|
|
||||||
type RespOK struct {
|
type Resp struct {
|
||||||
Code int `json:"code"`
|
Code string `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Message string `json:"message"`
|
||||||
Data interface{} `json:"data,omitempty"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespOK struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitVerificationCodeReq struct {
|
type SubmitVerificationCodeReq struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalizeTaiwanMobile 標準化號碼並驗證是否為合法台灣手機號碼
|
||||||
|
func NormalizeTaiwanMobile(phone string) (string, bool) {
|
||||||
|
// 移除空格
|
||||||
|
phone = strings.ReplaceAll(phone, " ", "")
|
||||||
|
|
||||||
|
// 移除 "+886" 並將剩餘部分標準化
|
||||||
|
if strings.HasPrefix(phone, "+886") {
|
||||||
|
phone = strings.TrimPrefix(phone, "+886")
|
||||||
|
if !strings.HasPrefix(phone, "0") {
|
||||||
|
phone = "0" + phone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正則表達式驗證標準化後的號碼
|
||||||
|
regex := regexp.MustCompile(`^(09\d{8})$`)
|
||||||
|
if regex.MatchString(phone) {
|
||||||
|
return phone, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidEmail 驗證 Email 格式的函數
|
||||||
|
func IsValidEmail(email string) bool {
|
||||||
|
// 定義正則表達式
|
||||||
|
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
|
||||||
|
return regex.MatchString(email)
|
||||||
|
}
|
||||||
|
|
@ -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,89 +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 // 沒有權限使用該資源
|
|
||||||
)
|
|
||||||
|
|
||||||
/* 詳細代碼 - 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,558 +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
|
|
||||||
}
|
|
||||||
|
|
@ -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,215 +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
|
|
||||||
}
|
|
||||||
// 根據錯誤碼判斷對應的 HTTP 狀態碼
|
|
||||||
switch e.Code() / 100 {
|
|
||||||
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
|
|
||||||
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", NewError(1, code.CatService, code.ResourceInsufficient, "bad request"), http.StatusBadRequest},
|
|
||||||
{"unauthorized", NewError(1, code.CatAuth, code.Unauthorized, "unauthorized"), http.StatusUnauthorized},
|
|
||||||
{"forbidden", NewError(1, code.CatAuth, code.Forbidden, "forbidden"), http.StatusForbidden},
|
|
||||||
{"not found", NewError(1, code.CatResource, code.ResourceNotFound, "not found"), http.StatusNotFound},
|
|
||||||
{"internal server error", NewError(1, code.CatDB, 1095, "not found"), http.StatusInternalServerError},
|
|
||||||
{"input err", NewError(1, code.CatInput, 1095, "not found"), 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
|
|
||||||
)
|
|
||||||
|
|
@ -19,3 +19,35 @@ const (
|
||||||
const (
|
const (
|
||||||
CurrencyTWD Currency = "TWD"
|
CurrencyTWD Currency = "TWD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var genderMap = map[int64]string{
|
||||||
|
0: "",
|
||||||
|
1: "male",
|
||||||
|
2: "female",
|
||||||
|
3: "secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGenderByCode(g int64) string {
|
||||||
|
r, ok := genderMap[g]
|
||||||
|
if !ok {
|
||||||
|
return genderMap[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
var genderCodeMap = map[string]int64{
|
||||||
|
"": 0,
|
||||||
|
"male": 1,
|
||||||
|
"female": 2,
|
||||||
|
"secret": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGenderCodeByStr(g string) int64 {
|
||||||
|
r, ok := genderCodeMap[g]
|
||||||
|
if !ok {
|
||||||
|
return genderCodeMap[""]
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type AccountUIDRepository interface {
|
||||||
Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error)
|
Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error)
|
||||||
Delete(ctx context.Context, id string) (int64, error)
|
Delete(ctx context.Context, id string) (int64, error)
|
||||||
FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error)
|
FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error)
|
||||||
|
FindOneByUID(ctx context.Context, uid string) (*entity.AccountUID, error)
|
||||||
AccountUIDIndexUP
|
AccountUIDIndexUP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ type MemberUseCase interface {
|
||||||
GetUserInfo(ctx context.Context, req GetUserInfoRequest) (UserInfo, error)
|
GetUserInfo(ctx context.Context, req GetUserInfoRequest) (UserInfo, error)
|
||||||
// ListMember 取得會員列表
|
// ListMember 取得會員列表
|
||||||
ListMember(ctx context.Context, req ListUserInfoRequest) (ListUserInfoResponse, error)
|
ListMember(ctx context.Context, req ListUserInfoRequest) (ListUserInfoResponse, error)
|
||||||
|
// FindLoginIDByUID 取得login id
|
||||||
|
FindLoginIDByUID(ctx context.Context, uid string) (BindingUser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BindingMemberUseCase interface {
|
type BindingMemberUseCase interface {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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.
|
// FindUIDByLoginID mocks base method.
|
||||||
func (m *MockAccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) {
|
func (m *MockAccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/alicebob/miniredis/v2"
|
"github.com/alicebob/miniredis/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
"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"
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
)
|
)
|
||||||
|
|
@ -131,7 +131,7 @@ func TestAccountModel_FindOne(t *testing.T) {
|
||||||
}
|
}
|
||||||
err = repo.Insert(context.TODO(), account)
|
err = repo.Insert(context.TODO(), account)
|
||||||
assert.NoError(t, err, "插入應成功")
|
assert.NoError(t, err, "插入應成功")
|
||||||
nid := primitive.NewObjectID()
|
nid := bson.NewObjectID()
|
||||||
t.Logf("Inserted account ID: %s, nid:%s", account.ID.Hex(), nid.Hex())
|
t.Logf("Inserted account ID: %s, nid:%s", account.ID.Hex(), nid.Hex())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,20 @@ func (repo *AccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *AccountUIDRepository) FindOneByUID(ctx context.Context, uid string) (*entity.AccountUID, error) {
|
||||||
|
var data entity.AccountUID
|
||||||
|
|
||||||
|
err := repo.DB.GetClient().FindOne(ctx, &data, bson.M{"uid": uid})
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return &data, nil
|
||||||
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
|
return nil, ErrNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (repo *AccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
func (repo *AccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
||||||
// 等價於 db.account_uid_binding.createIndex({"login_id": 1}, {unique: true})
|
// 等價於 db.account_uid_binding.createIndex({"login_id": 1}, {unique: true})
|
||||||
repo.DB.PopulateIndex(ctx, "login_id", 1, true)
|
repo.DB.PopulateIndex(ctx, "login_id", 1, true)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
"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) {
|
func SetupTestAccountUIDRepository(db string) (repository.AccountUIDRepository, func(), error) {
|
||||||
|
|
@ -156,7 +156,7 @@ func TestDefaultAccountUidModel_FindOne(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Non-existent ObjectID",
|
name: "Non-existent ObjectID",
|
||||||
id: primitive.NewObjectID().Hex(),
|
id: bson.NewObjectID().Hex(),
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ func (repo *UserRepository) FindOneByUID(ctx context.Context, uid string) (*enti
|
||||||
// 不常寫,再找一次可接受
|
// 不常寫,再找一次可接受
|
||||||
id := repo.UIDToID(ctx, uid)
|
id := repo.UIDToID(ctx, uid)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, errors.New("invalid uid")
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
rk := domain.GetUserRedisKey(id)
|
rk := domain.GetUserRedisKey(id)
|
||||||
|
|
||||||
|
|
@ -293,7 +293,7 @@ func (repo *UserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, em
|
||||||
// 不常寫,再找一次可接受
|
// 不常寫,再找一次可接受
|
||||||
id := repo.UIDToID(ctx, uid)
|
id := repo.UIDToID(ctx, uid)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return errors.New("invalid uid")
|
return fmt.Errorf("invalid uid")
|
||||||
}
|
}
|
||||||
rk := domain.GetUserRedisKey(id)
|
rk := domain.GetUserRedisKey(id)
|
||||||
|
|
||||||
|
|
@ -326,14 +326,14 @@ func (repo *UserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, ph
|
||||||
// 不常寫,再找一次可接受
|
// 不常寫,再找一次可接受
|
||||||
id := repo.UIDToID(ctx, uid)
|
id := repo.UIDToID(ctx, uid)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return errors.New("invalid uid")
|
return fmt.Errorf("invalid uid")
|
||||||
}
|
}
|
||||||
rk := domain.GetUserRedisKey(id)
|
rk := domain.GetUserRedisKey(id)
|
||||||
|
|
||||||
// 執行更新操作
|
// 執行更新操作
|
||||||
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
||||||
if err != nil {
|
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/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
"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"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -243,7 +243,7 @@ func TestCustomUserModel_FindOne(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Non-existent ObjectID",
|
name: "Non-existent ObjectID",
|
||||||
id: primitive.NewObjectID().Hex(),
|
id: bson.NewObjectID().Hex(),
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/member/domain/config"
|
"backend/pkg/member/domain/config"
|
||||||
"backend/pkg/member/domain/repository"
|
"backend/pkg/member/domain/repository"
|
||||||
"backend/pkg/member/domain/usecase"
|
"backend/pkg/member/domain/usecase"
|
||||||
|
repo "backend/pkg/member/repository"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MemberUseCaseParam struct {
|
type MemberUseCaseParam struct {
|
||||||
|
|
@ -13,6 +17,7 @@ type MemberUseCaseParam struct {
|
||||||
VerifyCodeModel repository.VerifyCodeRepository
|
VerifyCodeModel repository.VerifyCodeRepository
|
||||||
GenerateUID repository.AutoIDRepository
|
GenerateUID repository.AutoIDRepository
|
||||||
Config config.Config
|
Config config.Config
|
||||||
|
Logger errs.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemberUseCase struct {
|
type MemberUseCase struct {
|
||||||
|
|
@ -24,3 +29,29 @@ func MustMemberUseCase(param MemberUseCaseParam) usecase.AccountUseCase {
|
||||||
param,
|
param,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (use *MemberUseCase) FindLoginIDByUID(ctx context.Context, uid string) (usecase.BindingUser, error) {
|
||||||
|
data, err := use.AccountUID.FindOneByUID(ctx, uid)
|
||||||
|
if err != nil {
|
||||||
|
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{
|
||||||
|
UID: data.UID,
|
||||||
|
LoginID: data.LoginID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/entity"
|
"backend/pkg/member/domain/entity"
|
||||||
"backend/pkg/member/domain/usecase"
|
"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"
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,14 +31,11 @@ func (use *MemberUseCase) BindUserInfo(ctx context.Context, req usecase.CreateUs
|
||||||
|
|
||||||
// Insert 新增
|
// Insert 新增
|
||||||
if err := use.User.Insert(ctx, insert); err != nil {
|
if err := use.User.Insert(ctx, insert); err != nil {
|
||||||
e := errs.DatabaseErrorWithScopeL(
|
e := errs.DBErrorErrorL(use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.BindingUserTabletErrorCode,
|
{Key: "req", Val: req},
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "User.Insert"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "User.Insert"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to binding user info").Wrap(err)
|
"failed to binding user info").Wrap(err)
|
||||||
|
|
||||||
|
|
@ -56,23 +49,18 @@ func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUs
|
||||||
// 先確定有這個Account
|
// 先確定有這個Account
|
||||||
_, err := use.Account.FindOneByAccount(ctx, req.LoginID)
|
_, err := use.Account.FindOneByAccount(ctx, req.LoginID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
e = errs.ResourceNotFoundWithScope(
|
e = errs.ResNotFoundError(
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToFindAccountErrorCode,
|
|
||||||
fmt.Sprintf("failed to insert account: %s", req.UID),
|
fmt.Sprintf("failed to insert account: %s", req.UID),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
e = errs.DBErrorErrorL(use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.FailedToFindAccountErrorCode,
|
{Key: "req", Val: req},
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "User.FindOneByAccount"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "User.FindOneByAccount"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to find account").Wrap(err)
|
"failed to find account").Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
@ -95,14 +83,11 @@ func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUs
|
||||||
UID: uid,
|
UID: uid,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
e := errs.DatabaseErrorWithScopeL(
|
e := errs.DBErrorErrorL(use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.FailedToBindAccountErrorCode,
|
{Key: "req", Val: req},
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "User.Insert"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "User.Insert"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to bind account").Wrap(err)
|
"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 {
|
func (use *MemberUseCase) BindVerifyEmail(ctx context.Context, uid, email string) error {
|
||||||
err := use.User.UpdateEmailVerifyStatus(ctx, uid, email)
|
err := use.User.UpdateEmailVerifyStatus(ctx, uid, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.DatabaseErrorWithScope(
|
e := errs.DBErrorError(
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToFindAccountErrorCode,
|
|
||||||
fmt.Sprintf("failed to Binding uid: %s, email: %s", uid, email),
|
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 {
|
func (use *MemberUseCase) BindVerifyPhone(ctx context.Context, uid, phone string) error {
|
||||||
err := use.User.UpdatePhoneVerifyStatus(ctx, uid, phone)
|
err := use.User.UpdatePhoneVerifyStatus(ctx, uid, phone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.DatabaseErrorWithScope(
|
e := errs.DBErrorErrorL(use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.FailedToFindAccountErrorCode,
|
{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),
|
fmt.Sprintf("failed to Binding uid: %s, phone: %s", uid, phone),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,26 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/entity"
|
"backend/pkg/member/domain/entity"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
|
|
||||||
GIDLib "code.30cm.net/digimon/library-go/utils/invited_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) {
|
func (use *MemberUseCase) Generate(ctx context.Context) (string, error) {
|
||||||
var data entity.AutoID
|
var data entity.AutoID
|
||||||
err := use.GenerateUID.Inc(ctx, &data)
|
err := use.GenerateUID.Inc(ctx, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.DatabaseErrorWithScopeL(
|
e := errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToIncAccountErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "AutoIDModel.Inc"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "AutoIDModel.Inc"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to inc account num").Wrap(err)
|
"failed to inc account num")
|
||||||
|
|
||||||
return "", e
|
return "", e
|
||||||
}
|
}
|
||||||
|
|
@ -35,12 +29,10 @@ func (use *MemberUseCase) Generate(ctx context.Context) (string, error) {
|
||||||
sum := GIDLib.InitAutoID + data.Counter
|
sum := GIDLib.InitAutoID + data.Counter
|
||||||
if sum > math.MaxInt64 {
|
if sum > math.MaxInt64 {
|
||||||
return "",
|
return "",
|
||||||
errs.InvalidRangeWithScopeL(
|
errs.InputInvalidRangeErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToIncAccountErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "MemberUseCase.Generate"},
|
||||||
[]logx.LogField{
|
|
||||||
{Key: "func", Value: "MemberUseCase.Generate"},
|
|
||||||
},
|
},
|
||||||
"sum exceeds the maximum int64 value")
|
"sum exceeds the maximum int64 value")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -8,16 +9,11 @@ import (
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/entity"
|
"backend/pkg/member/domain/entity"
|
||||||
"backend/pkg/member/domain/member"
|
"backend/pkg/member/domain/member"
|
||||||
"backend/pkg/member/domain/repository"
|
"backend/pkg/member/domain/repository"
|
||||||
"backend/pkg/member/domain/usecase"
|
"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"
|
"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
|
// validateCreateUserAccountRequest validates the create user account request
|
||||||
func (use *MemberUseCase) validateCreateUserAccountRequest(req usecase.CreateLoginUserRequest) error {
|
func (use *MemberUseCase) validateCreateUserAccountRequest(req usecase.CreateLoginUserRequest) error {
|
||||||
if req.LoginID == "" {
|
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 == "" {
|
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
|
return nil
|
||||||
|
|
@ -71,12 +67,18 @@ func (use *MemberUseCase) processPasswordForPlatform(platform member.Platform, p
|
||||||
// Hash password for Digimon platform
|
// Hash password for Digimon platform
|
||||||
token, err := HasPasswordFunc(password, use.Config.Bcrypt.Cost)
|
token, err := HasPasswordFunc(password, use.Config.Bcrypt.Cost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errs.NewError(
|
e := errs.SvcInternalErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
code.CatSystem,
|
[]errs.LogField{
|
||||||
domain.HashPasswordErrorCode,
|
{Key: "platform", Val: platform},
|
||||||
fmt.Sprintf("failed to encrypt password: %s", err.Error()),
|
{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
|
return token, nil
|
||||||
|
|
@ -88,16 +90,14 @@ func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Acc
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if mongo.IsDuplicateKeyError(err) {
|
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,
|
return errs.DBErrorErrorL(use.Logger,
|
||||||
domain.InsertAccountErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "Account.Insert"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "Account.Insert"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to insert to database").Wrap(err)
|
"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) {
|
func (use *MemberUseCase) GetUIDByAccount(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetUIDByAccountResponse, error) {
|
||||||
account, err := use.AccountUID.FindUIDByLoginID(ctx, req.Account)
|
account, err := use.AccountUID.FindUIDByLoginID(ctx, req.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
e = errs.ResourceNotFoundWithScope(
|
e = errs.ResNotFoundError(
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedFindUIDByLoginIDErrorCode,
|
|
||||||
fmt.Sprintf("failed to find uid by account: %s", req.Account),
|
fmt.Sprintf("failed to find uid by account: %s", req.Account),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
// 錯誤代碼 20-201-07
|
e = errs.DBErrorErrorL(
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.FailedFindUIDByLoginIDErrorCode,
|
{Key: "req", Val: req},
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "AccountUID.FindUIDByLoginID"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "AccountUID.FindUIDByLoginID"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to find uid by account").Wrap(err)
|
"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) {
|
func (use *MemberUseCase) GetUserAccountInfo(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetAccountInfoResponse, error) {
|
||||||
account, err := use.Account.FindOneByAccount(ctx, req.Account)
|
account, err := use.Account.FindOneByAccount(ctx, req.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
// 錯誤代碼 20-301-08
|
// 錯誤代碼 20-301-08
|
||||||
e = errs.ResourceNotFoundWithScope(
|
e = errs.ResNotFoundError(
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedFindOneByAccountErrorCode,
|
|
||||||
fmt.Sprintf("failed to find account: %s", req.Account),
|
fmt.Sprintf("failed to find account: %s", req.Account),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
// 錯誤代碼 20-201-08
|
// 錯誤代碼 20-201-08
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
e = errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedFindOneByAccountErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "Account.FindOneByAccount"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "Account.FindOneByAccount"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to find account").Wrap(err)
|
"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)
|
user, err = use.User.FindOneByNickName(ctx, req.NickName)
|
||||||
default:
|
default:
|
||||||
// 驗證至少提供一個查詢參數
|
// 驗證至少提供一個查詢參數
|
||||||
return usecase.UserInfo{}, errs.InvalidFormatWithScope(
|
return usecase.UserInfo{}, errs.InputInvalidRangeError("UID or NickName must be provided")
|
||||||
code.CloudEPMember,
|
|
||||||
"UID or NickName must be provided",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// 查詢失敗時處理錯誤
|
// 查詢失敗時處理錯誤
|
||||||
if err != nil {
|
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) {
|
if errors.Is(err, mon.ErrNotFound) {
|
||||||
return errs.ResourceNotFoundWithScope(
|
return errs.ResNotFoundError(fmt.Sprintf("user not found: %s", req.UID))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetUserInfoErrorCode,
|
|
||||||
fmt.Sprintf("user not found: %s", req.UID),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs.DatabaseErrorWithScopeL(
|
return errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToGetUserInfoErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "MemberUseCase.GetUserInfo"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "MemberUseCase.GetUserInfo"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to query user info").Wrap(err)
|
"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)
|
token, e := HasPasswordFunc(req.Token, use.Config.Bcrypt.Cost)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return errs.NewError(
|
return errs.SysInternalError(fmt.Sprintf("failed to encrypt err: %s", e.Error()))
|
||||||
code.CloudEPMember,
|
|
||||||
code.CatSystem,
|
|
||||||
domain.HashPasswordErrorCode,
|
|
||||||
fmt.Sprintf("failed to encrypt err: %s", e.Error()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
toInt8, err := safeInt64ToInt8(req.Platform)
|
toInt8, err := safeInt64ToInt8(req.Platform)
|
||||||
if err != nil {
|
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))
|
err = use.Account.UpdateTokenByLoginID(ctx, req.Account, token, member.Platform(toInt8))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
// 錯誤代碼 20-301-08
|
e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.Account))
|
||||||
e = errs.ResourceNotFoundWithScope(
|
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToUpdatePasswordErrorCode,
|
|
||||||
fmt.Sprintf("failed to upadte password since account not found: %s", req.Account),
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
// 錯誤代碼 20-201-02
|
e = errs.DBErrorErrorL(
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
use.Logger,
|
||||||
code.CloudEPMember,
|
[]errs.LogField{
|
||||||
domain.FailedToUpdatePasswordErrorCode,
|
{Key: "req", Val: req},
|
||||||
logx.WithContext(ctx),
|
{Key: "func", Val: "Account.UpdateTokenByLoginID"},
|
||||||
[]logx.LogField{
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "Account.UpdateTokenByLoginID"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to update password").Wrap(err)
|
"failed to update password").Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
@ -312,23 +281,17 @@ func (use *MemberUseCase) UpdateUserInfo(ctx context.Context, req *usecase.Updat
|
||||||
Currency: req.Currency,
|
Currency: req.Currency,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
e = errs.ResourceNotFoundWithScope(
|
e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.UID))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToUpdateUserErrorCode,
|
|
||||||
fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID),
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
e = errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToUpdateUserErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "User.UpdateUserDetailsByUid"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "User.UpdateUserDetailsByUid"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to update user info").Wrap(err)
|
"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 {
|
func (use *MemberUseCase) UpdateStatus(ctx context.Context, req usecase.UpdateStatusRequest) error {
|
||||||
err := use.User.UpdateStatus(ctx, req.UID, req.Status.ToInt32())
|
err := use.User.UpdateStatus(ctx, req.UID, req.Status.ToInt32())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var e *errs.LibError
|
var e *errs.Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, mon.ErrNotFound):
|
case errors.Is(err, mon.ErrNotFound):
|
||||||
e = errs.ResourceNotFoundWithScope(
|
e = errs.ResNotFoundError(fmt.Sprintf("failed to upadte password since account not found: %s", req.UID))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToFindUserErrorCode,
|
|
||||||
fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID),
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
e = errs.DatabaseErrorWithScopeL(
|
e = errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToUpdateUserStatusErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "User.UpdateStatus"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "User.UpdateStatus"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to update user info").Wrap(err)
|
"failed to update user info").Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
@ -380,14 +337,12 @@ func (use *MemberUseCase) ListMember(ctx context.Context, req usecase.ListUserIn
|
||||||
PageIndex: req.PageIndex,
|
PageIndex: req.PageIndex,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.DatabaseErrorWithScopeL(
|
e := errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToGetUserInfoErrorCode,
|
[]errs.LogField{
|
||||||
logx.WithContext(ctx),
|
{Key: "req", Val: req},
|
||||||
[]logx.LogField{
|
{Key: "func", Val: "User.ListMembers"},
|
||||||
{Key: "req", Value: req},
|
{Key: "err", Val: err.Error()},
|
||||||
{Key: "func", Value: "User.ListMembers"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
},
|
||||||
"failed to list members").Wrap(err)
|
"failed to list members").Wrap(err)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
errs "backend/pkg/library/errors"
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/config"
|
"backend/pkg/member/domain/config"
|
||||||
"backend/pkg/member/domain/entity"
|
"backend/pkg/member/domain/entity"
|
||||||
"backend/pkg/member/domain/member"
|
"backend/pkg/member/domain/member"
|
||||||
"backend/pkg/member/domain/usecase"
|
"backend/pkg/member/domain/usecase"
|
||||||
mockRepo "backend/pkg/member/mock/repository"
|
mockRepo "backend/pkg/member/mock/repository"
|
||||||
"backend/pkg/member/repository"
|
"backend/pkg/member/repository"
|
||||||
|
"context"
|
||||||
"backend/pkg/library/errs"
|
"errors"
|
||||||
"backend/pkg/library/errs/code"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
|
@ -154,12 +150,7 @@ func TestGetUIDByAccount(t *testing.T) {
|
||||||
mockSetup: func() {
|
mockSetup: func() {
|
||||||
mockAccountUIDRepo.EXPECT().
|
mockAccountUIDRepo.EXPECT().
|
||||||
FindUIDByLoginID(gomock.Any(), "notfounduser").
|
FindUIDByLoginID(gomock.Any(), "notfounduser").
|
||||||
Return(nil, errs.NewError(
|
Return(nil, errs.ResNotFoundError("account not found"))
|
||||||
code.CloudEPMember,
|
|
||||||
code.CatResource,
|
|
||||||
domain.FailedFindUIDByLoginIDErrorCode,
|
|
||||||
"account not found",
|
|
||||||
))
|
|
||||||
},
|
},
|
||||||
wantResp: usecase.GetUIDByAccountResponse{},
|
wantResp: usecase.GetUIDByAccountResponse{},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/member"
|
"backend/pkg/member/domain/member"
|
||||||
"backend/pkg/member/domain/usecase"
|
"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) {
|
func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase.GenerateRefreshCodeRequest) (usecase.GenerateRefreshCodeResponse, error) {
|
||||||
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
||||||
if !status {
|
if !status {
|
||||||
e := errs.ResourceNotFoundWithScope(
|
e := errs.ResNotFoundError(fmt.Sprintf("failed to get verify code type: %d", param.CodeType))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetVerifyCodeErrorCode,
|
|
||||||
fmt.Sprintf("failed to get verify code type: %d", param.CodeType),
|
|
||||||
)
|
|
||||||
|
|
||||||
return usecase.GenerateRefreshCodeResponse{}, e
|
return usecase.GenerateRefreshCodeResponse{}, e
|
||||||
}
|
}
|
||||||
|
|
@ -32,17 +25,13 @@ func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase
|
||||||
if vc == "" {
|
if vc == "" {
|
||||||
vc, err = generateVerifyCode(6)
|
vc, err = generateVerifyCode(6)
|
||||||
if err != nil {
|
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)
|
err = use.VerifyCodeModel.SetVerifyCode(ctx, param.LoginID, checkType, vc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return usecase.GenerateRefreshCodeResponse{},
|
return usecase.GenerateRefreshCodeResponse{},
|
||||||
errs.DatabaseErrorWithScope(
|
errs.DBErrorError("failed to set verify code")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetCodeOnRedisErrorCode,
|
|
||||||
"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 {
|
func (use *MemberUseCase) CheckRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error {
|
||||||
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
||||||
if !status {
|
if !status {
|
||||||
return errs.ResourceNotFoundWithScope(
|
return errs.ResNotFoundError(fmt.Sprintf("failed to get verify code type: %d", param.CodeType))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetVerifyCodeErrorCode,
|
|
||||||
fmt.Sprintf("failed to get verify code type: %d", param.CodeType),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType)
|
get, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.DatabaseErrorWithScope(
|
return errs.DBErrorErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToGetCodeOnRedisErrorCode,
|
[]errs.LogField{
|
||||||
|
{Key: "err", Val: err.Error()},
|
||||||
|
},
|
||||||
"failed to set verify code",
|
"failed to set verify code",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if get == "" {
|
if get == "" {
|
||||||
return errs.ResourceNotFoundWithScope(
|
return errs.ResNotFoundError("failed to get data", param.LoginID, checkType)
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetCodeOnRedisErrorCode,
|
|
||||||
"failed to get data",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if get != param.VerifyCode {
|
if get != param.VerifyCode {
|
||||||
return errs.ForbiddenWithScope(
|
return errs.AuthForbiddenError("failed to verify code")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToGetCodeCorrectErrorCode,
|
|
||||||
"failed to verify code",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func (use *MemberUseCase) VerifyPlatformAuthResult(ctx context.Context, param usecase.VerifyAuthResultRequest) (usecase.VerifyAuthResultResponse, error) {
|
||||||
account, err := use.Account.FindOneByAccount(ctx, param.Account)
|
account, err := use.Account.FindOneByAccount(ctx, param.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return usecase.VerifyAuthResultResponse{}, errs.ResourceNotFoundWithScope(
|
return usecase.VerifyAuthResultResponse{}, errs.ResNotFoundError(fmt.Sprintf("failed to find account: %s", param.Account))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToFindAccountErrorCode,
|
|
||||||
fmt.Sprintf("failed to find account: %s", param.Account),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return usecase.VerifyAuthResultResponse{
|
return usecase.VerifyAuthResultResponse{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -10,28 +11,20 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/usecase"
|
"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) {
|
func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecase.VerifyAuthResultRequest) (usecase.GoogleTokenInfo, error) {
|
||||||
var tokenInfo usecase.GoogleTokenInfo
|
var tokenInfo usecase.GoogleTokenInfo
|
||||||
// 發送 Google Token Info API 請求
|
// 發送 Google Token Info API 請求
|
||||||
body, err := fetchGoogleTokenInfo(ctx, req.Token)
|
body, err := use.fetchGoogleTokenInfo(ctx, req.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tokenInfo, err
|
return tokenInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析返回的 JSON 數據
|
// 解析返回的 JSON 數據
|
||||||
if err := json.Unmarshal(body, &tokenInfo); err != nil {
|
if err := json.Unmarshal(body, &tokenInfo); err != nil {
|
||||||
return tokenInfo, errs.ThirdPartyError(
|
return tokenInfo, errs.SysInternalError("failed to parse token info")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogle,
|
|
||||||
"failed to parse token info",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 驗證 Token 資訊
|
// 驗證 Token 資訊
|
||||||
|
|
@ -43,31 +36,33 @@ func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecas
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchGoogleTokenInfo 發送 Google TokenInfo API 請求並返回響應內容
|
// 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)
|
uri := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", token)
|
||||||
|
|
||||||
// 發送請求
|
// 發送請求
|
||||||
r, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.ThirdPartyError(
|
return nil, errs.SvcThirdPartyErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToVerifyGoogle,
|
[]errs.LogField{
|
||||||
"failed to create request", err.Error())
|
{Key: "request URL", Val: uri},
|
||||||
|
{Key: "func", Val: "fetchGoogleTokenInfo"},
|
||||||
|
}, "failed to create request", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
return nil, errs.ThirdPartyError(
|
return nil, errs.SysTimeoutError("fetch google timeout")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleTimeout,
|
|
||||||
"request timeout",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ThirdPartyError(
|
return nil, errs.SvcThirdPartyErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToVerifyGoogleTimeout,
|
[]errs.LogField{
|
||||||
|
{Key: "request URL", Val: uri},
|
||||||
|
{Key: "method", Val: http.MethodGet},
|
||||||
|
{Key: "func", Val: "fetchGoogleTokenInfo"},
|
||||||
|
},
|
||||||
"failed to request Google TokenInfo API",
|
"failed to request Google TokenInfo API",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -75,21 +70,13 @@ func fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) {
|
||||||
|
|
||||||
// 檢查返回的 HTTP 狀態碼
|
// 檢查返回的 HTTP 狀態碼
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errs.ThirdPartyError(
|
return nil, errs.SvcThirdPartyError(fmt.Sprintf("unexpected status code: %d", resp.StatusCode))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleHTTPCode,
|
|
||||||
fmt.Sprintf("unexpected status code: %d", resp.StatusCode),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 讀取響應內容
|
// 讀取響應內容
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.ThirdPartyError(
|
return nil, errs.SvcThirdPartyError("failed to read response body")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogle,
|
|
||||||
"failed to read response body",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return body, nil
|
return body, nil
|
||||||
|
|
@ -100,29 +87,17 @@ func validateGoogleTokenInfo(tokenInfo usecase.GoogleTokenInfo, expectedClientID
|
||||||
// **驗證 1: Token 是否過期**
|
// **驗證 1: Token 是否過期**
|
||||||
expiration, err := strconv.ParseInt(tokenInfo.Exp, 10, 64)
|
expiration, err := strconv.ParseInt(tokenInfo.Exp, 10, 64)
|
||||||
if err != nil || expiration <= time.Now().UTC().Unix() {
|
if err != nil || expiration <= time.Now().UTC().Unix() {
|
||||||
return errs.ThirdPartyError(
|
return errs.AuthExpiredError("token is expired")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleTokenExpired,
|
|
||||||
"token is expired",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **驗證 2: Audience (aud) 是否與 Google Client ID 匹配**
|
// **驗證 2: Audience (aud) 是否與 Google Client ID 匹配**
|
||||||
if tokenInfo.Aud != expectedClientID {
|
if tokenInfo.Aud != expectedClientID {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError("invalid audience")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleInvalidAudience,
|
|
||||||
"invalid audience",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **驗證 3: 是否 email 已驗證**
|
// **驗證 3: 是否 email 已驗證**
|
||||||
if tokenInfo.EmailVerified == "false" {
|
if tokenInfo.EmailVerified == "false" {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError("email is not verified")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogle,
|
|
||||||
"email is not verified",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/usecase"
|
"backend/pkg/member/domain/usecase"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -38,11 +35,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) {
|
||||||
Aud: expectedClientID,
|
Aud: expectedClientID,
|
||||||
EmailVerified: "true",
|
EmailVerified: "true",
|
||||||
},
|
},
|
||||||
expectedErr: errs.ThirdPartyError(
|
expectedErr: errs.SvcThirdPartyError("token is expired"),
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleTokenExpired,
|
|
||||||
"token is expired",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid audience",
|
name: "Invalid audience",
|
||||||
|
|
@ -51,11 +44,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) {
|
||||||
Aud: "invalid-client-id",
|
Aud: "invalid-client-id",
|
||||||
EmailVerified: "true",
|
EmailVerified: "true",
|
||||||
},
|
},
|
||||||
expectedErr: errs.ThirdPartyError(
|
expectedErr: errs.SvcThirdPartyError("invalid audience"),
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogleInvalidAudience,
|
|
||||||
"invalid audience",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Email not verified",
|
name: "Email not verified",
|
||||||
|
|
@ -64,11 +53,7 @@ func TestValidateGoogleTokenInfo(t *testing.T) {
|
||||||
Aud: expectedClientID,
|
Aud: expectedClientID,
|
||||||
EmailVerified: "false",
|
EmailVerified: "false",
|
||||||
},
|
},
|
||||||
expectedErr: errs.ThirdPartyError(
|
expectedErr: errs.SvcThirdPartyError("email is not verified"),
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyGoogle,
|
|
||||||
"email is not verified",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -8,11 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"backend/pkg/member/domain"
|
|
||||||
"backend/pkg/member/domain/usecase"
|
"backend/pkg/member/domain/usecase"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LineCodeToAccessToken 透過 Line 授權碼換取 Access Token
|
// 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 {
|
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))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBufferString(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToVerifyLine,
|
[]errs.LogField{
|
||||||
"failed to create request",
|
{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 {
|
for key, value := range headers {
|
||||||
req.Header.Set(key, value)
|
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 {
|
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)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToVerifyLine,
|
[]errs.LogField{
|
||||||
"failed to create request",
|
{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 {
|
for key, value := range headers {
|
||||||
req.Header.Set(key, value)
|
req.Header.Set(key, value)
|
||||||
|
|
@ -92,37 +98,28 @@ func (use *MemberUseCase) doRequest(req *http.Request, result interface{}) error
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyErrorL(
|
||||||
code.CloudEPMember,
|
use.Logger,
|
||||||
domain.FailedToVerifyLine,
|
[]errs.LogField{
|
||||||
"failed to send request",
|
{Key: "req", Val: req},
|
||||||
)
|
{Key: "result", Val: result},
|
||||||
|
{Key: "method", Val: http.MethodGet},
|
||||||
|
},
|
||||||
|
"failed to create request")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError("unexpected status code: " + http.StatusText(resp.StatusCode))
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyLine,
|
|
||||||
"unexpected status code: "+http.StatusText(resp.StatusCode),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError("failed to read response body")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyLine,
|
|
||||||
"failed to read response body",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(body, result); err != nil {
|
if err := json.Unmarshal(body, result); err != nil {
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError("failed to parse response body")
|
||||||
code.CloudEPMember,
|
|
||||||
domain.FailedToVerifyLine,
|
|
||||||
"failed to parse response body",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "backend/pkg/library/errors"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -10,8 +11,6 @@ import (
|
||||||
"backend/pkg/member/domain/usecase"
|
"backend/pkg/member/domain/usecase"
|
||||||
mockRepo "backend/pkg/member/mock/repository"
|
mockRepo "backend/pkg/member/mock/repository"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
@ -241,7 +240,7 @@ func TestVerifyPlatformAuthResult(t *testing.T) {
|
||||||
Token: "someToken",
|
Token: "someToken",
|
||||||
},
|
},
|
||||||
mockSetup: func() {
|
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{},
|
wantResp: usecase.VerifyAuthResultResponse{},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type SMTPConfig struct {
|
type SMTPConfig struct {
|
||||||
Enable bool
|
Enable bool
|
||||||
|
|
@ -13,6 +17,35 @@ type SMTPConfig struct {
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate 驗證 SMTP 配置
|
||||||
|
func (c *SMTPConfig) Validate() error {
|
||||||
|
if !c.Enable {
|
||||||
|
return nil // 未啟用則不驗證
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Host == "" {
|
||||||
|
return errors.New("smtp host is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Port <= 0 || c.Port > 65535 {
|
||||||
|
return fmt.Errorf("smtp port must be between 1 and 65535, got %d", c.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Username == "" {
|
||||||
|
return errors.New("smtp username is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Password == "" {
|
||||||
|
return errors.New("smtp password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Sort < 0 {
|
||||||
|
return fmt.Errorf("smtp sort must be >= 0, got %d", c.Sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type AmazonSesSettings struct {
|
type AmazonSesSettings struct {
|
||||||
Enable bool
|
Enable bool
|
||||||
Sort int
|
Sort int
|
||||||
|
|
@ -26,6 +59,39 @@ type AmazonSesSettings struct {
|
||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate 驗證 AWS SES 配置
|
||||||
|
func (c *AmazonSesSettings) Validate() error {
|
||||||
|
if !c.Enable {
|
||||||
|
return nil // 未啟用則不驗證
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Region == "" {
|
||||||
|
return errors.New("aws ses region is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Sender == "" {
|
||||||
|
return errors.New("aws ses sender is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AccessKey == "" {
|
||||||
|
return errors.New("aws ses access key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.SecretKey == "" {
|
||||||
|
return errors.New("aws ses secret key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Sort < 0 {
|
||||||
|
return fmt.Errorf("aws ses sort must be >= 0, got %d", c.Sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PoolSize < 0 {
|
||||||
|
return fmt.Errorf("aws ses pool size must be >= 0, got %d", c.PoolSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type MitakeSMSSender struct {
|
type MitakeSMSSender struct {
|
||||||
Enable bool
|
Enable bool
|
||||||
Sort int
|
Sort int
|
||||||
|
|
@ -35,6 +101,31 @@ type MitakeSMSSender struct {
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate 驗證 Mitake SMS 配置
|
||||||
|
func (c *MitakeSMSSender) Validate() error {
|
||||||
|
if !c.Enable {
|
||||||
|
return nil // 未啟用則不驗證
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.User == "" {
|
||||||
|
return errors.New("mitake user is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Password == "" {
|
||||||
|
return errors.New("mitake password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Sort < 0 {
|
||||||
|
return fmt.Errorf("mitake sort must be >= 0, got %d", c.Sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PoolSize < 0 {
|
||||||
|
return fmt.Errorf("mitake pool size must be >= 0, got %d", c.PoolSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeliveryConfig 傳送重試配置
|
// DeliveryConfig 傳送重試配置
|
||||||
type DeliveryConfig struct {
|
type DeliveryConfig struct {
|
||||||
MaxRetries int `json:"max_retries"` // 最大重試次數
|
MaxRetries int `json:"max_retries"` // 最大重試次數
|
||||||
|
|
@ -44,3 +135,72 @@ type DeliveryConfig struct {
|
||||||
Timeout time.Duration `json:"timeout"` // 單次發送超時時間
|
Timeout time.Duration `json:"timeout"` // 單次發送超時時間
|
||||||
EnableHistory bool `json:"enable_history"` // 是否啟用歷史記錄
|
EnableHistory bool `json:"enable_history"` // 是否啟用歷史記錄
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate 驗證 DeliveryConfig 配置
|
||||||
|
func (c *DeliveryConfig) Validate() error {
|
||||||
|
if c.MaxRetries < 0 {
|
||||||
|
return fmt.Errorf("max_retries must be >= 0, got %d", c.MaxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxRetries > 10 {
|
||||||
|
return fmt.Errorf("max_retries should not exceed 10, got %d", c.MaxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InitialDelay < 0 {
|
||||||
|
return fmt.Errorf("initial_delay must be >= 0, got %v", c.InitialDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InitialDelay > 10*time.Second {
|
||||||
|
return fmt.Errorf("initial_delay is too large (> 10s), got %v", c.InitialDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BackoffFactor < 1.0 {
|
||||||
|
return fmt.Errorf("backoff_factor must be >= 1.0, got %v", c.BackoffFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BackoffFactor > 10.0 {
|
||||||
|
return fmt.Errorf("backoff_factor is too large (> 10.0), got %v", c.BackoffFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxDelay < 0 {
|
||||||
|
return fmt.Errorf("max_delay must be >= 0, got %v", c.MaxDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxDelay > 5*time.Minute {
|
||||||
|
return fmt.Errorf("max_delay is too large (> 5m), got %v", c.MaxDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Timeout <= 0 {
|
||||||
|
return fmt.Errorf("timeout must be > 0, got %v", c.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Timeout > 5*time.Minute {
|
||||||
|
return fmt.Errorf("timeout is too large (> 5m), got %v", c.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查 InitialDelay 和 MaxDelay 的關係
|
||||||
|
if c.InitialDelay > c.MaxDelay && c.MaxDelay > 0 {
|
||||||
|
return fmt.Errorf("initial_delay (%v) should not exceed max_delay (%v)", c.InitialDelay, c.MaxDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults 設置默認值
|
||||||
|
func (c *DeliveryConfig) SetDefaults() {
|
||||||
|
if c.MaxRetries == 0 {
|
||||||
|
c.MaxRetries = 3
|
||||||
|
}
|
||||||
|
if c.InitialDelay == 0 {
|
||||||
|
c.InitialDelay = 100 * time.Millisecond
|
||||||
|
}
|
||||||
|
if c.BackoffFactor == 0 {
|
||||||
|
c.BackoffFactor = 2.0
|
||||||
|
}
|
||||||
|
if c.MaxDelay == 0 {
|
||||||
|
c.MaxDelay = 30 * time.Second
|
||||||
|
}
|
||||||
|
if c.Timeout == 0 {
|
||||||
|
c.Timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSMTPConfig_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config SMTPConfig
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "有效的 SMTP 配置",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: true,
|
||||||
|
Sort: 1,
|
||||||
|
Host: "smtp.gmail.com",
|
||||||
|
Port: 587,
|
||||||
|
Username: "test@gmail.com",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "未啟用的配置(不驗證)",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: false,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缺少 Host",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: true,
|
||||||
|
Port: 587,
|
||||||
|
Username: "test@gmail.com",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "smtp host is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "無效的 Port",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: true,
|
||||||
|
Host: "smtp.gmail.com",
|
||||||
|
Port: 99999,
|
||||||
|
Username: "test@gmail.com",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "smtp port must be between 1 and 65535",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缺少 Username",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: true,
|
||||||
|
Host: "smtp.gmail.com",
|
||||||
|
Port: 587,
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "smtp username is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "負數的 Sort",
|
||||||
|
config: SMTPConfig{
|
||||||
|
Enable: true,
|
||||||
|
Sort: -1,
|
||||||
|
Host: "smtp.gmail.com",
|
||||||
|
Port: 587,
|
||||||
|
Username: "test@gmail.com",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "smtp sort must be >= 0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAmazonSesSettings_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config AmazonSesSettings
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "有效的 AWS SES 配置",
|
||||||
|
config: AmazonSesSettings{
|
||||||
|
Enable: true,
|
||||||
|
Sort: 1,
|
||||||
|
PoolSize: 10,
|
||||||
|
Region: "us-west-2",
|
||||||
|
Sender: "noreply@example.com",
|
||||||
|
AccessKey: "AKIAIOSFODNN7EXAMPLE",
|
||||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "未啟用的配置",
|
||||||
|
config: AmazonSesSettings{
|
||||||
|
Enable: false,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缺少 Region",
|
||||||
|
config: AmazonSesSettings{
|
||||||
|
Enable: true,
|
||||||
|
Sender: "noreply@example.com",
|
||||||
|
AccessKey: "AKIAIOSFODNN7EXAMPLE",
|
||||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "aws ses region is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缺少 AccessKey",
|
||||||
|
config: AmazonSesSettings{
|
||||||
|
Enable: true,
|
||||||
|
Region: "us-west-2",
|
||||||
|
Sender: "noreply@example.com",
|
||||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "aws ses access key is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMitakeSMSSender_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config MitakeSMSSender
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "有效的 Mitake 配置",
|
||||||
|
config: MitakeSMSSender{
|
||||||
|
Enable: true,
|
||||||
|
Sort: 1,
|
||||||
|
PoolSize: 5,
|
||||||
|
User: "testuser",
|
||||||
|
Password: "testpass",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缺少 User",
|
||||||
|
config: MitakeSMSSender{
|
||||||
|
Enable: true,
|
||||||
|
Password: "testpass",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "mitake user is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryConfig_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config DeliveryConfig
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "有效的配置",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 100 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 30 * time.Second,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MaxRetries 為負數",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: -1,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "max_retries must be >= 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MaxRetries 過大",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: 20,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "max_retries should not exceed 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BackoffFactor 小於 1.0",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
BackoffFactor: 0.5,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "backoff_factor must be >= 1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timeout 為 0",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "timeout must be > 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InitialDelay 大於 MaxDelay",
|
||||||
|
config: DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 1 * time.Minute,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "initial_delay",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errMsg != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryConfig_SetDefaults(t *testing.T) {
|
||||||
|
config := DeliveryConfig{}
|
||||||
|
config.SetDefaults()
|
||||||
|
|
||||||
|
assert.Equal(t, 3, config.MaxRetries)
|
||||||
|
assert.Equal(t, 100*time.Millisecond, config.InitialDelay)
|
||||||
|
assert.Equal(t, 2.0, config.BackoffFactor)
|
||||||
|
assert.Equal(t, 30*time.Second, config.MaxDelay)
|
||||||
|
assert.Equal(t, 30*time.Second, config.Timeout)
|
||||||
|
|
||||||
|
// 測試不覆蓋已設置的值
|
||||||
|
config2 := DeliveryConfig{
|
||||||
|
MaxRetries: 5,
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
config2.SetDefaults()
|
||||||
|
|
||||||
|
assert.Equal(t, 5, config2.MaxRetries) // 保持原值
|
||||||
|
assert.Equal(t, 60*time.Second, config2.Timeout) // 保持原值
|
||||||
|
assert.Equal(t, 100*time.Millisecond, config2.InitialDelay) // 設置默認值
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "backend/pkg/library/errs"
|
|
||||||
|
|
||||||
// Notification Error Codes
|
|
||||||
const (
|
|
||||||
NotificationErrorCode errs.ErrorCode = 1 + iota
|
|
||||||
FailedToSendEmailErrorCode
|
|
||||||
FailedToSendSMSErrorCode
|
|
||||||
FailedToGetTemplateErrorCode
|
|
||||||
FailedToSaveHistoryErrorCode
|
|
||||||
FailedToRetryDeliveryErrorCode
|
|
||||||
)
|
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/pkg/notification/domain/entity"
|
||||||
"backend/pkg/notification/domain/template"
|
"backend/pkg/notification/domain/template"
|
||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateUseCase interface {
|
type TemplateUseCase interface {
|
||||||
|
// GetEmailTemplateByStatic 從靜態模板獲取郵件模板
|
||||||
GetEmailTemplateByStatic(ctx context.Context, language template.Language, templateID template.Type) (template.EmailTemplate, error)
|
GetEmailTemplateByStatic(ctx context.Context, language template.Language, templateID template.Type) (template.EmailTemplate, error)
|
||||||
|
|
||||||
|
// GetEmailTemplate 獲取郵件模板(優先從資料庫,回退到靜態模板)
|
||||||
|
GetEmailTemplate(ctx context.Context, language template.Language, templateID template.Type) (template.EmailTemplate, error)
|
||||||
|
|
||||||
|
// GetSMSTemplate 獲取 SMS 模板(優先從資料庫,回退到靜態模板)
|
||||||
|
GetSMSTemplate(ctx context.Context, language template.Language, templateID template.Type) (SMSTemplateResp, error)
|
||||||
|
|
||||||
|
// RenderEmailTemplate 渲染郵件模板(替換變數)
|
||||||
|
RenderEmailTemplate(ctx context.Context, tmpl template.EmailTemplate, params entity.TemplateParams) (EmailTemplateResp, error)
|
||||||
|
|
||||||
|
// RenderSMSTemplate 渲染 SMS 模板(替換變數)
|
||||||
|
RenderSMSTemplate(ctx context.Context, tmpl SMSTemplateResp, params entity.TemplateParams) (SMSTemplateResp, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,12 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/pkg/notification/config"
|
"backend/pkg/notification/config"
|
||||||
"backend/pkg/notification/domain"
|
|
||||||
"backend/pkg/notification/domain/repository"
|
"backend/pkg/notification/domain/repository"
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
pool "backend/pkg/library/worker_pool"
|
|
||||||
|
|
||||||
"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/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"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ses/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AwsEmailDeliveryParam 傳送參數配置
|
// AwsEmailDeliveryParam 傳送參數配置
|
||||||
|
|
@ -26,7 +17,7 @@ type AwsEmailDeliveryParam struct {
|
||||||
|
|
||||||
type AwsEmailDeliveryRepository struct {
|
type AwsEmailDeliveryRepository struct {
|
||||||
Client *ses.Client
|
Client *ses.Client
|
||||||
Pool pool.WorkerPool
|
Timeout int // 超時時間(秒),預設 30
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustAwsSesMailRepository(param AwsEmailDeliveryParam) repository.MailRepository {
|
func MustAwsSesMailRepository(param AwsEmailDeliveryParam) repository.MailRepository {
|
||||||
|
|
@ -42,14 +33,24 @@ func MustAwsSesMailRepository(param AwsEmailDeliveryParam) repository.MailReposi
|
||||||
// 創建 SES 客戶端
|
// 創建 SES 客戶端
|
||||||
sesClient := ses.NewFromConfig(cfg)
|
sesClient := ses.NewFromConfig(cfg)
|
||||||
|
|
||||||
|
// 設置默認超時時間
|
||||||
|
timeout := 30
|
||||||
|
if param.Conf.PoolSize > 0 {
|
||||||
|
timeout = param.Conf.PoolSize // 可以復用這個配置項,或新增專門的 Timeout 配置
|
||||||
|
}
|
||||||
|
|
||||||
return &AwsEmailDeliveryRepository{
|
return &AwsEmailDeliveryRepository{
|
||||||
Client: sesClient,
|
Client: sesClient,
|
||||||
Pool: pool.NewWorkerPool(param.Conf.PoolSize),
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *AwsEmailDeliveryRepository) SendMail(ctx context.Context, req repository.MailReq) error {
|
func (repo *AwsEmailDeliveryRepository) SendMail(ctx context.Context, req repository.MailReq) error {
|
||||||
err := use.Pool.Submit(func() {
|
// 檢查 context 是否已取消
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// 設置郵件參數
|
// 設置郵件參數
|
||||||
to := make([]string, 0, len(req.To))
|
to := make([]string, 0, len(req.To))
|
||||||
to = append(to, req.To...)
|
to = append(to, req.To...)
|
||||||
|
|
@ -73,38 +74,10 @@ func (use *AwsEmailDeliveryRepository) SendMail(ctx context.Context, req reposit
|
||||||
Source: aws.String(req.From),
|
Source: aws.String(req.From),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 發送郵件
|
// 發送郵件(直接使用傳入的 context,不創建新的 context)
|
||||||
// TODO 不明原因送不出去,會被 context cancel 這裡先把它手動加到100sec
|
_, err := repo.Client.SendEmail(ctx, input)
|
||||||
newCtx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
//nolint:contextcheck
|
|
||||||
if _, err := use.Client.SendEmail(newCtx, input); err != nil {
|
|
||||||
_ = errs.ThirdPartyErrorL(
|
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToSendEmailErrorCode,
|
|
||||||
logx.WithContext(ctx),
|
|
||||||
[]logx.LogField{
|
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "AwsEmailDeliveryU.SendEmail"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
|
||||||
"failed to send mail by aws ses")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := errs.ThirdPartyErrorL(
|
return err
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToSendEmailErrorCode,
|
|
||||||
logx.WithContext(ctx),
|
|
||||||
[]logx.LogField{
|
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "AwsEmailDeliveryU.SendEmail"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
|
||||||
"failed to send mail by aws ses")
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,9 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/pkg/notification/config"
|
"backend/pkg/notification/config"
|
||||||
"backend/pkg/notification/domain"
|
|
||||||
"backend/pkg/notification/domain/repository"
|
"backend/pkg/notification/domain/repository"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
pool "backend/pkg/library/worker_pool"
|
|
||||||
|
|
||||||
"github.com/minchao/go-mitake"
|
"github.com/minchao/go-mitake"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MitakeSMSDeliveryParam 三竹傳送參數配置
|
// MitakeSMSDeliveryParam 三竹傳送參數配置
|
||||||
|
|
@ -21,37 +14,26 @@ type MitakeSMSDeliveryParam struct {
|
||||||
|
|
||||||
type MitakeSMSDeliveryRepository struct {
|
type MitakeSMSDeliveryRepository struct {
|
||||||
Client *mitake.Client
|
Client *mitake.Client
|
||||||
Pool pool.WorkerPool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *MitakeSMSDeliveryRepository) SendSMS(ctx context.Context, req repository.SMSMessageRequest) error {
|
func (repo *MitakeSMSDeliveryRepository) SendSMS(ctx context.Context, req repository.SMSMessageRequest) error {
|
||||||
// 用 goroutine pool 送,否則會超時
|
// 檢查 context 是否已取消
|
||||||
err := use.Pool.Submit(func() {
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 構建簡訊訊息
|
||||||
message := mitake.Message{
|
message := mitake.Message{
|
||||||
Dstaddr: req.PhoneNumber,
|
Dstaddr: req.PhoneNumber,
|
||||||
Destname: req.RecipientName,
|
Destname: req.RecipientName,
|
||||||
Smbody: req.MessageContent,
|
Smbody: req.MessageContent,
|
||||||
}
|
}
|
||||||
_, err := use.Client.Send(message)
|
|
||||||
if err != nil {
|
|
||||||
logx.Error("failed to send sms via mitake")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 直接發送,不使用 goroutine pool
|
||||||
|
// 讓 delivery usecase 統一管理重試和超時
|
||||||
|
_, err := repo.Client.Send(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 錯誤代碼 20-201-04
|
return err
|
||||||
e := errs.ThirdPartyErrorL(
|
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToSendSMSErrorCode,
|
|
||||||
logx.WithContext(ctx),
|
|
||||||
[]logx.LogField{
|
|
||||||
{Key: "req", Value: req},
|
|
||||||
{Key: "func", Value: "MitakeSMSDeliveryRepository.Client.Send"},
|
|
||||||
{Key: "err", Value: err.Error()},
|
|
||||||
},
|
|
||||||
"failed to send sns by mitake").Wrap(err)
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -60,6 +42,5 @@ func (use *MitakeSMSDeliveryRepository) SendSMS(ctx context.Context, req reposit
|
||||||
func MustMitakeRepository(param MitakeSMSDeliveryParam) repository.SMSClientRepository {
|
func MustMitakeRepository(param MitakeSMSDeliveryParam) repository.SMSClientRepository {
|
||||||
return &MitakeSMSDeliveryRepository{
|
return &MitakeSMSDeliveryRepository{
|
||||||
Client: mitake.NewClient(param.Conf.User, param.Conf.Password, nil),
|
Client: mitake.NewClient(param.Conf.User, param.Conf.Password, nil),
|
||||||
Pool: pool.NewWorkerPool(param.Conf.PoolSize),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ import (
|
||||||
"backend/pkg/notification/config"
|
"backend/pkg/notification/config"
|
||||||
"backend/pkg/notification/domain/repository"
|
"backend/pkg/notification/domain/repository"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
pool "backend/pkg/library/worker_pool"
|
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
"gopkg.in/gomail.v2"
|
"gopkg.in/gomail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,7 +13,6 @@ type SMTPMailUseCaseParam struct {
|
||||||
|
|
||||||
type SMTPMailRepository struct {
|
type SMTPMailRepository struct {
|
||||||
Client *gomail.Dialer
|
Client *gomail.Dialer
|
||||||
Pool pool.WorkerPool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustSMTPUseCase(param SMTPMailUseCaseParam) repository.MailRepository {
|
func MustSMTPUseCase(param SMTPMailUseCaseParam) repository.MailRepository {
|
||||||
|
|
@ -28,26 +23,25 @@ func MustSMTPUseCase(param SMTPMailUseCaseParam) repository.MailRepository {
|
||||||
param.Conf.Username,
|
param.Conf.Username,
|
||||||
param.Conf.Password,
|
param.Conf.Password,
|
||||||
),
|
),
|
||||||
Pool: pool.NewWorkerPool(param.Conf.GoroutinePoolNum),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *SMTPMailRepository) SendMail(_ context.Context, req repository.MailReq) error {
|
func (repo *SMTPMailRepository) SendMail(ctx context.Context, req repository.MailReq) error {
|
||||||
// 用 goroutine pool 送,否則會超時
|
// 檢查 context 是否已取消
|
||||||
err := repo.Pool.Submit(func() {
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 構建郵件
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", req.From)
|
m.SetHeader("From", req.From)
|
||||||
m.SetHeader("To", req.To...)
|
m.SetHeader("To", req.To...)
|
||||||
m.SetHeader("Subject", req.Subject)
|
m.SetHeader("Subject", req.Subject)
|
||||||
m.SetBody("text/html", req.Body)
|
m.SetBody("text/html", req.Body)
|
||||||
if err := repo.Client.DialAndSend(m); err != nil {
|
|
||||||
logx.WithCallerSkip(1).WithFields(
|
|
||||||
logx.Field("func", "MailUseCase.SendMail"),
|
|
||||||
logx.Field("req", req),
|
|
||||||
logx.Field("err", err),
|
|
||||||
).Error("failed to send mail by mailgun")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if err := repo.Client.DialAndSend(m); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/pkg/notification/config"
|
"backend/pkg/notification/config"
|
||||||
"backend/pkg/notification/domain"
|
|
||||||
"backend/pkg/notification/domain/entity"
|
"backend/pkg/notification/domain/entity"
|
||||||
"backend/pkg/notification/domain/repository"
|
"backend/pkg/notification/domain/repository"
|
||||||
"backend/pkg/notification/domain/usecase"
|
"backend/pkg/notification/domain/usecase"
|
||||||
|
|
@ -12,10 +11,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeliveryUseCaseParam 傳送參數配置
|
// DeliveryUseCaseParam 傳送參數配置
|
||||||
|
|
@ -24,6 +20,7 @@ type DeliveryUseCaseParam struct {
|
||||||
EmailProviders []usecase.EmailProvider
|
EmailProviders []usecase.EmailProvider
|
||||||
DeliveryConfig config.DeliveryConfig
|
DeliveryConfig config.DeliveryConfig
|
||||||
HistoryRepo repository.HistoryRepository // 可選的歷史記錄 repository
|
HistoryRepo repository.HistoryRepository // 可選的歷史記錄 repository
|
||||||
|
Logger errs.Logger // 日誌記錄器
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeliveryUseCase 通知發送服務
|
// DeliveryUseCase 通知發送服務
|
||||||
|
|
@ -69,12 +66,21 @@ func (use *DeliveryUseCase) SendMessage(ctx context.Context, req usecase.SMSMess
|
||||||
|
|
||||||
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
||||||
if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 執行發送邏輯
|
// 執行發送邏輯
|
||||||
return use.sendSMSWithRetry(ctx, req, history)
|
return use.sendWithRetry(ctx, history, &smsProviderAdapter{
|
||||||
|
providers: use.param.SMSProviders,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq) error {
|
func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq) error {
|
||||||
|
|
@ -92,35 +98,132 @@ func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq)
|
||||||
|
|
||||||
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
||||||
if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 執行發送邏輯
|
// 執行發送邏輯
|
||||||
return use.sendEmailWithRetry(ctx, req, history)
|
return use.sendWithRetry(ctx, history, &emailProviderAdapter{
|
||||||
|
providers: use.param.EmailProviders,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendSMSWithRetry 發送 SMS 並實現重試機制
|
// providerAdapter 統一的供應商適配器接口
|
||||||
func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SMSMessageRequest, history *entity.DeliveryHistory) error {
|
type providerAdapter interface {
|
||||||
// 根據 Sort 欄位對 SMSProviders 進行排序
|
getProviderCount() int
|
||||||
providers := make([]usecase.SMSProvider, len(use.param.SMSProviders))
|
getProviderName(index int) string
|
||||||
copy(providers, use.param.SMSProviders)
|
getProviderSort(index int) int64
|
||||||
sort.Slice(providers, func(i, j int) bool {
|
send(ctx context.Context, providerIndex int) error
|
||||||
return providers[i].Sort < providers[j].Sort
|
getType() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// smsProviderAdapter SMS 供應商適配器
|
||||||
|
type smsProviderAdapter struct {
|
||||||
|
providers []usecase.SMSProvider
|
||||||
|
request usecase.SMSMessageRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *smsProviderAdapter) getProviderCount() int {
|
||||||
|
return len(a.providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *smsProviderAdapter) getProviderName(index int) string {
|
||||||
|
return fmt.Sprintf("sms_provider_%d", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *smsProviderAdapter) getProviderSort(index int) int64 {
|
||||||
|
return a.providers[index].Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *smsProviderAdapter) send(ctx context.Context, providerIndex int) error {
|
||||||
|
return a.providers[providerIndex].Repo.SendSMS(ctx, repository.SMSMessageRequest{
|
||||||
|
PhoneNumber: a.request.PhoneNumber,
|
||||||
|
RecipientName: a.request.RecipientName,
|
||||||
|
MessageContent: a.request.MessageContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *smsProviderAdapter) getType() string {
|
||||||
|
return "SMS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// emailProviderAdapter Email 供應商適配器
|
||||||
|
type emailProviderAdapter struct {
|
||||||
|
providers []usecase.EmailProvider
|
||||||
|
request usecase.MailReq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *emailProviderAdapter) getProviderCount() int {
|
||||||
|
return len(a.providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *emailProviderAdapter) getProviderName(index int) string {
|
||||||
|
return fmt.Sprintf("email_provider_%d", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *emailProviderAdapter) getProviderSort(index int) int64 {
|
||||||
|
return a.providers[index].Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *emailProviderAdapter) send(ctx context.Context, providerIndex int) error {
|
||||||
|
return a.providers[providerIndex].Repo.SendMail(ctx, repository.MailReq{
|
||||||
|
From: a.request.From,
|
||||||
|
To: a.request.To,
|
||||||
|
Subject: a.request.Subject,
|
||||||
|
Body: a.request.Body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *emailProviderAdapter) getType() string {
|
||||||
|
return "Email"
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerWithIndex 用於排序的結構
|
||||||
|
type providerWithIndex struct {
|
||||||
|
index int
|
||||||
|
sort int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendWithRetry 統一的發送重試邏輯
|
||||||
|
func (use *DeliveryUseCase) sendWithRetry(
|
||||||
|
ctx context.Context,
|
||||||
|
history *entity.DeliveryHistory,
|
||||||
|
adapter providerAdapter,
|
||||||
|
) error {
|
||||||
|
// 按 Sort 欄位對供應商進行排序
|
||||||
|
providerCount := adapter.getProviderCount()
|
||||||
|
sortedProviders := make([]providerWithIndex, providerCount)
|
||||||
|
for i := 0; i < providerCount; i++ {
|
||||||
|
sortedProviders[i] = providerWithIndex{
|
||||||
|
index: i,
|
||||||
|
sort: adapter.getProviderSort(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(sortedProviders, func(i, j int) bool {
|
||||||
|
return sortedProviders[i].sort < sortedProviders[j].sort
|
||||||
})
|
})
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
totalAttempts := 0
|
totalAttempts := 0
|
||||||
|
|
||||||
// 嘗試所有 providers
|
// 嘗試所有 providers
|
||||||
for providerIndex, provider := range providers {
|
for _, provider := range sortedProviders {
|
||||||
|
providerIndex := provider.index
|
||||||
|
|
||||||
// 為每個 provider 嘗試發送
|
// 為每個 provider 嘗試發送
|
||||||
for attempt := 0; attempt < use.param.DeliveryConfig.MaxRetries; attempt++ {
|
for attempt := 0; attempt < use.param.DeliveryConfig.MaxRetries; attempt++ {
|
||||||
totalAttempts++
|
totalAttempts++
|
||||||
|
|
||||||
// 更新歷史記錄狀態
|
// 更新歷史記錄狀態
|
||||||
history.Status = entity.DeliveryStatusSending
|
history.Status = entity.DeliveryStatusSending
|
||||||
history.Provider = fmt.Sprintf("sms_provider_%d", providerIndex)
|
history.Provider = adapter.getProviderName(providerIndex)
|
||||||
history.AttemptCount = totalAttempts
|
history.AttemptCount = totalAttempts
|
||||||
history.UpdatedAt = time.Now()
|
history.UpdatedAt = time.Now()
|
||||||
use.updateHistory(ctx, history)
|
use.updateHistory(ctx, history)
|
||||||
|
|
@ -131,11 +234,7 @@ func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SM
|
||||||
// 創建帶超時的 context
|
// 創建帶超時的 context
|
||||||
sendCtx, cancel := context.WithTimeout(ctx, use.param.DeliveryConfig.Timeout)
|
sendCtx, cancel := context.WithTimeout(ctx, use.param.DeliveryConfig.Timeout)
|
||||||
|
|
||||||
err := provider.Repo.SendSMS(sendCtx, repository.SMSMessageRequest{
|
err := adapter.send(sendCtx, providerIndex)
|
||||||
PhoneNumber: req.PhoneNumber,
|
|
||||||
RecipientName: req.RecipientName,
|
|
||||||
MessageContent: req.MessageContent,
|
|
||||||
})
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
@ -153,8 +252,14 @@ func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SM
|
||||||
attemptRecord.ErrorMessage = err.Error()
|
attemptRecord.ErrorMessage = err.Error()
|
||||||
lastErr = err
|
lastErr = err
|
||||||
|
|
||||||
logx.WithContext(ctx).Errorf("SMS send attempt %d failed for provider %d: %v",
|
_ = errs.SvcThirdPartyErrorL(use.param.Logger,
|
||||||
attempt+1, providerIndex, err)
|
[]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 {
|
if attempt < use.param.DeliveryConfig.MaxRetries-1 {
|
||||||
|
|
@ -179,7 +284,7 @@ func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SM
|
||||||
use.updateHistory(ctx, history)
|
use.updateHistory(ctx, history)
|
||||||
use.addAttemptRecord(ctx, history.ID, attemptRecord)
|
use.addAttemptRecord(ctx, history.ID, attemptRecord)
|
||||||
|
|
||||||
logx.WithContext(ctx).Infof("SMS sent successfully after %d attempts", totalAttempts)
|
// 成功發送不需要記錄錯誤,這裡可以選擇記錄信息日誌或直接返回
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,114 +300,9 @@ func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SM
|
||||||
history.CompletedAt = &now
|
history.CompletedAt = &now
|
||||||
use.updateHistory(ctx, history)
|
use.updateHistory(ctx, history)
|
||||||
|
|
||||||
return errs.ThirdPartyError(
|
return errs.SvcThirdPartyError(
|
||||||
code.CloudEPNotification,
|
fmt.Sprintf("Failed to send %s after %d attempts across %d providers",
|
||||||
domain.FailedToSendSMSErrorCode,
|
adapter.getType(), totalAttempts, providerCount))
|
||||||
fmt.Sprintf("Failed to send SMS after %d attempts across %d providers",
|
|
||||||
totalAttempts, len(providers)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendEmailWithRetry 發送 Email 並實現重試機制
|
|
||||||
func (use *DeliveryUseCase) sendEmailWithRetry(ctx context.Context, req usecase.MailReq, history *entity.DeliveryHistory) error {
|
|
||||||
// 根據 Sort 欄位對 EmailProviders 進行排序
|
|
||||||
providers := make([]usecase.EmailProvider, len(use.param.EmailProviders))
|
|
||||||
copy(providers, use.param.EmailProviders)
|
|
||||||
sort.Slice(providers, func(i, j int) bool {
|
|
||||||
return providers[i].Sort < providers[j].Sort
|
|
||||||
})
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
totalAttempts := 0
|
|
||||||
|
|
||||||
// 嘗試所有 providers
|
|
||||||
for providerIndex, provider := range providers {
|
|
||||||
// 為每個 provider 嘗試發送
|
|
||||||
for attempt := 0; attempt < use.param.DeliveryConfig.MaxRetries; attempt++ {
|
|
||||||
totalAttempts++
|
|
||||||
|
|
||||||
// 更新歷史記錄狀態
|
|
||||||
history.Status = entity.DeliveryStatusSending
|
|
||||||
history.Provider = fmt.Sprintf("email_provider_%d", providerIndex)
|
|
||||||
history.AttemptCount = totalAttempts
|
|
||||||
history.UpdatedAt = time.Now()
|
|
||||||
use.updateHistory(ctx, history)
|
|
||||||
|
|
||||||
// 記錄發送嘗試
|
|
||||||
attemptStart := time.Now()
|
|
||||||
|
|
||||||
// 創建帶超時的 context
|
|
||||||
sendCtx, cancel := context.WithTimeout(ctx, use.param.DeliveryConfig.Timeout)
|
|
||||||
|
|
||||||
err := provider.Repo.SendMail(sendCtx, repository.MailReq{
|
|
||||||
From: req.From,
|
|
||||||
To: req.To,
|
|
||||||
Subject: req.Subject,
|
|
||||||
Body: req.Body,
|
|
||||||
})
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
// 記錄嘗試結果
|
|
||||||
attemptDuration := time.Since(attemptStart)
|
|
||||||
attemptRecord := entity.DeliveryAttempt{
|
|
||||||
Provider: history.Provider,
|
|
||||||
AttemptAt: attemptStart,
|
|
||||||
Success: err == nil,
|
|
||||||
ErrorMessage: "",
|
|
||||||
Duration: attemptDuration.Milliseconds(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
attemptRecord.ErrorMessage = err.Error()
|
|
||||||
lastErr = err
|
|
||||||
|
|
||||||
logx.WithContext(ctx).Errorf("Email send attempt %d failed for provider %d: %v",
|
|
||||||
attempt+1, providerIndex, err)
|
|
||||||
|
|
||||||
// 如果不是最後一次嘗試,等待後重試
|
|
||||||
if attempt < use.param.DeliveryConfig.MaxRetries-1 {
|
|
||||||
delay := use.calculateDelay(attempt)
|
|
||||||
history.Status = entity.DeliveryStatusRetrying
|
|
||||||
use.updateHistory(ctx, history)
|
|
||||||
use.addAttemptRecord(ctx, history.ID, attemptRecord)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(delay):
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 發送成功
|
|
||||||
history.Status = entity.DeliveryStatusSuccess
|
|
||||||
history.UpdatedAt = time.Now()
|
|
||||||
now := time.Now()
|
|
||||||
history.CompletedAt = &now
|
|
||||||
use.updateHistory(ctx, history)
|
|
||||||
use.addAttemptRecord(ctx, history.ID, attemptRecord)
|
|
||||||
|
|
||||||
logx.WithContext(ctx).Infof("Email sent successfully after %d attempts", totalAttempts)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
use.addAttemptRecord(ctx, history.ID, attemptRecord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有 providers 都失敗了
|
|
||||||
history.Status = entity.DeliveryStatusFailed
|
|
||||||
history.ErrorMessage = fmt.Sprintf("All providers failed. Last error: %v", lastErr)
|
|
||||||
history.UpdatedAt = time.Now()
|
|
||||||
now := time.Now()
|
|
||||||
history.CompletedAt = &now
|
|
||||||
use.updateHistory(ctx, history)
|
|
||||||
|
|
||||||
return errs.ThirdPartyError(
|
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToSendEmailErrorCode,
|
|
||||||
fmt.Sprintf("Failed to send email after %d attempts across %d providers",
|
|
||||||
totalAttempts, len(providers)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateDelay 計算指數退避延遲
|
// calculateDelay 計算指數退避延遲
|
||||||
|
|
@ -320,7 +320,13 @@ func (use *DeliveryUseCase) calculateDelay(attempt int) time.Duration {
|
||||||
func (use *DeliveryUseCase) updateHistory(ctx context.Context, history *entity.DeliveryHistory) {
|
func (use *DeliveryUseCase) updateHistory(ctx context.Context, history *entity.DeliveryHistory) {
|
||||||
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
||||||
if err := use.param.HistoryRepo.UpdateHistory(ctx, history); err != 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -329,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) {
|
func (use *DeliveryUseCase) addAttemptRecord(ctx context.Context, historyID string, attempt entity.DeliveryAttempt) {
|
||||||
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil {
|
||||||
if err := use.param.HistoryRepo.AddAttempt(ctx, historyID, attempt); err != 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,377 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/pkg/notification/config"
|
||||||
|
"backend/pkg/notification/domain/entity"
|
||||||
|
"backend/pkg/notification/domain/repository"
|
||||||
|
"backend/pkg/notification/domain/usecase"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockSMSRepository 模擬 SMS Repository
|
||||||
|
type mockSMSRepository struct {
|
||||||
|
sendFunc func(ctx context.Context, req repository.SMSMessageRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSMSRepository) SendSMS(ctx context.Context, req repository.SMSMessageRequest) error {
|
||||||
|
if m.sendFunc != nil {
|
||||||
|
return m.sendFunc(ctx, req)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockMailRepository 模擬 Mail Repository
|
||||||
|
type mockMailRepository struct {
|
||||||
|
sendFunc func(ctx context.Context, req repository.MailReq) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMailRepository) SendMail(ctx context.Context, req repository.MailReq) error {
|
||||||
|
if m.sendFunc != nil {
|
||||||
|
return m.sendFunc(ctx, req)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockHistoryRepository 模擬 History Repository
|
||||||
|
type mockHistoryRepository struct {
|
||||||
|
histories []entity.DeliveryHistory
|
||||||
|
attempts map[string][]entity.DeliveryAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHistoryRepository) CreateHistory(ctx context.Context, history *entity.DeliveryHistory) error {
|
||||||
|
m.histories = append(m.histories, *history)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHistoryRepository) UpdateHistory(ctx context.Context, history *entity.DeliveryHistory) error {
|
||||||
|
for i := range m.histories {
|
||||||
|
if m.histories[i].ID == history.ID {
|
||||||
|
m.histories[i] = *history
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHistoryRepository) GetHistory(ctx context.Context, id string) (*entity.DeliveryHistory, error) {
|
||||||
|
for i := range m.histories {
|
||||||
|
if m.histories[i].ID == id {
|
||||||
|
return &m.histories[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHistoryRepository) AddAttempt(ctx context.Context, historyID string, attempt entity.DeliveryAttempt) error {
|
||||||
|
if m.attempts == nil {
|
||||||
|
m.attempts = make(map[string][]entity.DeliveryAttempt)
|
||||||
|
}
|
||||||
|
m.attempts[historyID] = append(m.attempts[historyID], attempt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHistoryRepository) ListHistory(ctx context.Context, filter repository.HistoryFilter) ([]*entity.DeliveryHistory, error) {
|
||||||
|
var result []*entity.DeliveryHistory
|
||||||
|
for i := range m.histories {
|
||||||
|
result = append(result, &m.histories[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_SendEmail_Success(t *testing.T) {
|
||||||
|
mockMail := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
return nil // 成功
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockHistory := &mockHistoryRepository{}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
EmailProviders: []usecase.EmailProvider{
|
||||||
|
{Sort: 1, Repo: mockMail},
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 10 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
EnableHistory: true,
|
||||||
|
},
|
||||||
|
HistoryRepo: mockHistory,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := uc.SendEmail(ctx, usecase.MailReq{
|
||||||
|
From: "test@example.com",
|
||||||
|
To: []string{"user@example.com"},
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "<p>Test email</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// 驗證歷史記錄
|
||||||
|
assert.Equal(t, 1, len(mockHistory.histories))
|
||||||
|
assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_SendEmail_RetryAndSuccess(t *testing.T) {
|
||||||
|
attemptCount := 0
|
||||||
|
mockMail := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
attemptCount++
|
||||||
|
if attemptCount < 3 {
|
||||||
|
return errors.New("temporary error")
|
||||||
|
}
|
||||||
|
return nil // 第三次成功
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockHistory := &mockHistoryRepository{}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
EmailProviders: []usecase.EmailProvider{
|
||||||
|
{Sort: 1, Repo: mockMail},
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 10 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
EnableHistory: true,
|
||||||
|
},
|
||||||
|
HistoryRepo: mockHistory,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := uc.SendEmail(ctx, usecase.MailReq{
|
||||||
|
From: "test@example.com",
|
||||||
|
To: []string{"user@example.com"},
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "<p>Test</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 3, attemptCount) // 重試了 3 次
|
||||||
|
assert.Equal(t, 1, len(mockHistory.histories))
|
||||||
|
assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status)
|
||||||
|
assert.Equal(t, 3, mockHistory.histories[0].AttemptCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_SendEmail_AllRetries_Failed(t *testing.T) {
|
||||||
|
mockMail := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
return errors.New("persistent error")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockHistory := &mockHistoryRepository{}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
EmailProviders: []usecase.EmailProvider{
|
||||||
|
{Sort: 1, Repo: mockMail},
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 10 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
EnableHistory: true,
|
||||||
|
},
|
||||||
|
HistoryRepo: mockHistory,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := uc.SendEmail(ctx, usecase.MailReq{
|
||||||
|
From: "test@example.com",
|
||||||
|
To: []string{"user@example.com"},
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "<p>Test</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Failed to send Email")
|
||||||
|
assert.Equal(t, 1, len(mockHistory.histories))
|
||||||
|
assert.Equal(t, entity.DeliveryStatusFailed, mockHistory.histories[0].Status)
|
||||||
|
assert.Equal(t, 3, mockHistory.histories[0].AttemptCount) // 嘗試了 3 次
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_SendEmail_Failover(t *testing.T) {
|
||||||
|
mockMail1 := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
return errors.New("provider 1 failed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockMail2 := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
return nil // 備援成功
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockHistory := &mockHistoryRepository{}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
EmailProviders: []usecase.EmailProvider{
|
||||||
|
{Sort: 1, Repo: mockMail1}, // 主要供應商
|
||||||
|
{Sort: 2, Repo: mockMail2}, // 備援供應商
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 2,
|
||||||
|
InitialDelay: 10 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
EnableHistory: true,
|
||||||
|
},
|
||||||
|
HistoryRepo: mockHistory,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := uc.SendEmail(ctx, usecase.MailReq{
|
||||||
|
From: "test@example.com",
|
||||||
|
To: []string{"user@example.com"},
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "<p>Test</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// 驗證使用了備援供應商
|
||||||
|
assert.Equal(t, 1, len(mockHistory.histories))
|
||||||
|
assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status)
|
||||||
|
// 總共嘗試次數:provider1 重試 2 次 + provider2 成功 1 次 = 3 次
|
||||||
|
assert.Equal(t, 3, mockHistory.histories[0].AttemptCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_SendSMS_Success(t *testing.T) {
|
||||||
|
mockSMS := &mockSMSRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.SMSMessageRequest) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockHistory := &mockHistoryRepository{}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
SMSProviders: []usecase.SMSProvider{
|
||||||
|
{Sort: 1, Repo: mockSMS},
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 10 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
EnableHistory: true,
|
||||||
|
},
|
||||||
|
HistoryRepo: mockHistory,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := uc.SendMessage(ctx, usecase.SMSMessageRequest{
|
||||||
|
PhoneNumber: "+886912345678",
|
||||||
|
RecipientName: "Test User",
|
||||||
|
MessageContent: "Your code: 123456",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(mockHistory.histories))
|
||||||
|
assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_CalculateDelay(t *testing.T) {
|
||||||
|
uc := &DeliveryUseCase{
|
||||||
|
param: DeliveryUseCaseParam{
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
InitialDelay: 100 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 1 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
attempt int
|
||||||
|
expected time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "第 0 次重試",
|
||||||
|
attempt: 0,
|
||||||
|
expected: 100 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "第 1 次重試",
|
||||||
|
attempt: 1,
|
||||||
|
expected: 200 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "第 2 次重試",
|
||||||
|
attempt: 2,
|
||||||
|
expected: 400 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "第 3 次重試",
|
||||||
|
attempt: 3,
|
||||||
|
expected: 800 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "第 10 次重試(達到 MaxDelay)",
|
||||||
|
attempt: 10,
|
||||||
|
expected: 1 * time.Second, // 受限於 MaxDelay
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
delay := uc.calculateDelay(tt.attempt)
|
||||||
|
assert.Equal(t, tt.expected, delay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliveryUseCase_ContextCancellation(t *testing.T) {
|
||||||
|
mockMail := &mockMailRepository{
|
||||||
|
sendFunc: func(ctx context.Context, req repository.MailReq) error {
|
||||||
|
// 模擬慢速操作
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
return errors.New("should not reach here")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := MustDeliveryUseCase(DeliveryUseCaseParam{
|
||||||
|
EmailProviders: []usecase.EmailProvider{
|
||||||
|
{Sort: 1, Repo: mockMail},
|
||||||
|
},
|
||||||
|
DeliveryConfig: config.DeliveryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 50 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxDelay: 500 * time.Millisecond,
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
EnableHistory: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 創建會被取消的 context
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := uc.SendEmail(ctx, usecase.MailReq{
|
||||||
|
From: "test@example.com",
|
||||||
|
To: []string{"user@example.com"},
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "<p>Test</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, context.DeadlineExceeded, err)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/pkg/notification/domain"
|
|
||||||
"backend/pkg/notification/domain/entity"
|
"backend/pkg/notification/domain/entity"
|
||||||
"backend/pkg/notification/domain/repository"
|
"backend/pkg/notification/domain/repository"
|
||||||
"backend/pkg/notification/domain/template"
|
"backend/pkg/notification/domain/template"
|
||||||
|
|
@ -10,14 +9,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateUseCaseParam struct {
|
type TemplateUseCaseParam struct {
|
||||||
TemplateRepo repository.TemplateRepository // 可選的資料庫模板 repository
|
TemplateRepo repository.TemplateRepository // 可選的資料庫模板 repository
|
||||||
|
Logger errs.Logger // 日誌記錄器
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateUseCase struct {
|
type TemplateUseCase struct {
|
||||||
|
|
@ -35,28 +32,19 @@ func (use *TemplateUseCase) GetEmailTemplateByStatic(_ context.Context, language
|
||||||
// 查找指定語言的模板映射
|
// 查找指定語言的模板映射
|
||||||
templateByLang, exists := template.EmailTemplateMap[language]
|
templateByLang, exists := template.EmailTemplateMap[language]
|
||||||
if !exists {
|
if !exists {
|
||||||
return template.EmailTemplate{}, errs.ResourceNotFoundWithScope(
|
return template.EmailTemplate{}, errs.ResNotFoundError(fmt.Sprintf("email template not found for language: %s", language))
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToGetTemplateErrorCode,
|
|
||||||
fmt.Sprintf("email template not found for language: %s", language))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找指定類型的模板生成函數
|
// 查找指定類型的模板生成函數
|
||||||
templateFunc, exists := templateByLang[templateID]
|
templateFunc, exists := templateByLang[templateID]
|
||||||
if !exists {
|
if !exists {
|
||||||
return template.EmailTemplate{}, errs.ResourceNotFoundWithScope(
|
return template.EmailTemplate{}, errs.ResNotFoundError(fmt.Sprintf("email template not found for type ID: %s", templateID))
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToGetTemplateErrorCode,
|
|
||||||
fmt.Sprintf("email template not found for type ID: %s", templateID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 執行模板生成函數
|
// 執行模板生成函數
|
||||||
tmp, err := templateFunc()
|
tmp, err := templateFunc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return template.EmailTemplate{}, errs.DatabaseErrorWithScope(
|
return template.EmailTemplate{}, errs.DBErrorError(fmt.Sprintf("error generating email template: %v", err))
|
||||||
code.CloudEPNotification,
|
|
||||||
domain.FailedToGetTemplateErrorCode,
|
|
||||||
fmt.Sprintf("error generating email template: %v", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmp, nil
|
return tmp, nil
|
||||||
|
|
@ -68,7 +56,6 @@ func (use *TemplateUseCase) GetEmailTemplate(ctx context.Context, language templ
|
||||||
if use.param.TemplateRepo != nil {
|
if use.param.TemplateRepo != nil {
|
||||||
dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "email", string(language), string(templateID))
|
dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "email", string(language), string(templateID))
|
||||||
if err == nil && dbTemplate != nil && dbTemplate.IsActive {
|
if err == nil && dbTemplate != nil && dbTemplate.IsActive {
|
||||||
logx.WithContext(ctx).Infof("Using database template for %s/%s", language, templateID)
|
|
||||||
return template.EmailTemplate{
|
return template.EmailTemplate{
|
||||||
Title: dbTemplate.Subject,
|
Title: dbTemplate.Subject,
|
||||||
Body: dbTemplate.Body,
|
Body: dbTemplate.Body,
|
||||||
|
|
@ -77,12 +64,19 @@ func (use *TemplateUseCase) GetEmailTemplate(ctx context.Context, language templ
|
||||||
|
|
||||||
// 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板
|
// 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板
|
||||||
if err != nil {
|
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. 回退到靜態模板
|
// 2. 回退到靜態模板
|
||||||
logx.WithContext(ctx).Infof("Using static template for %s/%s", language, templateID)
|
|
||||||
return use.GetEmailTemplateByStatic(ctx, 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 {
|
if use.param.TemplateRepo != nil {
|
||||||
dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "sms", string(language), string(templateID))
|
dbTemplate, err := use.param.TemplateRepo.GetTemplate(ctx, "sms", string(language), string(templateID))
|
||||||
if err == nil && dbTemplate != nil && dbTemplate.IsActive {
|
if err == nil && dbTemplate != nil && dbTemplate.IsActive {
|
||||||
logx.WithContext(ctx).Infof("Using database SMS template for %s/%s", language, templateID)
|
|
||||||
return usecase.SMSTemplateResp{
|
return usecase.SMSTemplateResp{
|
||||||
Body: dbTemplate.Body,
|
Body: dbTemplate.Body,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -100,12 +93,19 @@ func (use *TemplateUseCase) GetSMSTemplate(ctx context.Context, language templat
|
||||||
|
|
||||||
// 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板
|
// 記錄資料庫查詢失敗,但不返回錯誤,繼續使用靜態模板
|
||||||
if err != nil {
|
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 暫時沒有靜態模板,返回默認)
|
// 2. 回退到靜態模板(SMS 暫時沒有靜態模板,返回默認)
|
||||||
logx.WithContext(ctx).Infof("Using default SMS template for %s/%s", language, templateID)
|
|
||||||
return use.getDefaultSMSTemplate(templateID), nil
|
return use.getDefaultSMSTemplate(templateID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/pkg/notification/domain/entity"
|
||||||
|
"backend/pkg/notification/domain/template"
|
||||||
|
"backend/pkg/notification/domain/usecase"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTemplateUseCase_RenderEmailTemplate(t *testing.T) {
|
||||||
|
uc := MustTemplateUseCase(TemplateUseCaseParam{
|
||||||
|
TemplateRepo: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tmpl template.EmailTemplate
|
||||||
|
params entity.TemplateParams
|
||||||
|
expectedSubj string
|
||||||
|
expectedBody string
|
||||||
|
shouldContain []string
|
||||||
|
shouldNotError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "渲染基本參數",
|
||||||
|
tmpl: template.EmailTemplate{
|
||||||
|
Title: "Hello {{.Username}}",
|
||||||
|
Body: "<p>Your code is: {{.VerifyCode}}</p>",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
Username: "張三",
|
||||||
|
VerifyCode: "123456",
|
||||||
|
},
|
||||||
|
expectedSubj: "Hello 張三",
|
||||||
|
shouldContain: []string{"123456"},
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "渲染額外參數",
|
||||||
|
tmpl: template.EmailTemplate{
|
||||||
|
Title: "Welcome",
|
||||||
|
Body: "<p>Hello {{.Username}}, your link: {{.Link}}</p>",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
Username: "John",
|
||||||
|
Extra: map[string]string{
|
||||||
|
"Link": "https://example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldContain: []string{"John", "https://example.com"},
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "特殊字符不轉義(簡單字符串替換)",
|
||||||
|
tmpl: template.EmailTemplate{
|
||||||
|
Title: "Test",
|
||||||
|
Body: "<p>Name: {{.Username}}</p>",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
Username: "<script>alert('xss')</script>",
|
||||||
|
},
|
||||||
|
shouldContain: []string{"<script>alert('xss')</script>"}, // 使用簡單字符串替換,不轉義
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "空模板",
|
||||||
|
tmpl: template.EmailTemplate{
|
||||||
|
Title: "",
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
Username: "Test",
|
||||||
|
},
|
||||||
|
expectedSubj: "",
|
||||||
|
expectedBody: "",
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := uc.RenderEmailTemplate(ctx, tt.tmpl, tt.params)
|
||||||
|
|
||||||
|
if tt.shouldNotError {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tt.expectedSubj != "" {
|
||||||
|
assert.Equal(t, tt.expectedSubj, result.Subject)
|
||||||
|
}
|
||||||
|
if tt.expectedBody != "" {
|
||||||
|
assert.Equal(t, tt.expectedBody, result.Body)
|
||||||
|
}
|
||||||
|
for _, contain := range tt.shouldContain {
|
||||||
|
assert.Contains(t, result.Body, contain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateUseCase_RenderSMSTemplate(t *testing.T) {
|
||||||
|
uc := MustTemplateUseCase(TemplateUseCaseParam{
|
||||||
|
TemplateRepo: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tmpl usecase.SMSTemplateResp
|
||||||
|
params entity.TemplateParams
|
||||||
|
expectedBody string
|
||||||
|
shouldContain []string
|
||||||
|
shouldNotError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "渲染 SMS 驗證碼",
|
||||||
|
tmpl: usecase.SMSTemplateResp{
|
||||||
|
Body: "您的驗證碼是:{{.VerifyCode}},請在5分鐘內使用。",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
VerifyCode: "654321",
|
||||||
|
},
|
||||||
|
shouldContain: []string{"654321", "5分鐘"},
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SMS 純文本替換",
|
||||||
|
tmpl: usecase.SMSTemplateResp{
|
||||||
|
Body: "Hi {{.Username}}, your code: {{.VerifyCode}}",
|
||||||
|
},
|
||||||
|
params: entity.TemplateParams{
|
||||||
|
Username: "<test>",
|
||||||
|
VerifyCode: "111111",
|
||||||
|
},
|
||||||
|
shouldContain: []string{"<test>", "111111"}, // 使用簡單字符串替換
|
||||||
|
shouldNotError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := uc.RenderSMSTemplate(ctx, tt.tmpl, tt.params)
|
||||||
|
|
||||||
|
if tt.shouldNotError {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tt.expectedBody != "" {
|
||||||
|
assert.Equal(t, tt.expectedBody, result.Body)
|
||||||
|
}
|
||||||
|
for _, contain := range tt.shouldContain {
|
||||||
|
assert.Contains(t, result.Body, contain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateUseCase_GetEmailTemplateByStatic(t *testing.T) {
|
||||||
|
uc := MustTemplateUseCase(TemplateUseCaseParam{
|
||||||
|
TemplateRepo: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
language template.Language
|
||||||
|
templateID template.Type
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "獲取忘記密碼模板 (zh-tw)",
|
||||||
|
language: template.LanguageZhTW,
|
||||||
|
templateID: template.ForgetPasswordVerify,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "獲取綁定郵箱模板 (zh-tw)",
|
||||||
|
language: template.LanguageZhTW,
|
||||||
|
templateID: template.BindingEmail,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "不存在的語言",
|
||||||
|
language: template.Language("xx-xx"),
|
||||||
|
templateID: template.ForgetPasswordVerify,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "不存在的模板類型",
|
||||||
|
language: template.LanguageZhTW,
|
||||||
|
templateID: template.Type("non_existent"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := uc.GetEmailTemplateByStatic(ctx, tt.language, tt.templateID)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, result.Title)
|
||||||
|
assert.NotEmpty(t, result.Body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateUseCase_GetDefaultSMSTemplate(t *testing.T) {
|
||||||
|
uc := &TemplateUseCase{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
templateID template.Type
|
||||||
|
shouldContain []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "忘記密碼模板",
|
||||||
|
templateID: template.ForgetPasswordVerify,
|
||||||
|
shouldContain: []string{"密碼重設", "驗證碼"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "綁定郵箱模板",
|
||||||
|
templateID: template.BindingEmail,
|
||||||
|
shouldContain: []string{"綁定", "驗證碼"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "默認模板",
|
||||||
|
templateID: template.Type("unknown"),
|
||||||
|
shouldContain: []string{"驗證碼"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := uc.getDefaultSMSTemplate(tt.templateID)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, result.Body)
|
||||||
|
for _, contain := range tt.shouldContain {
|
||||||
|
assert.Contains(t, result.Body, contain)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ func DefaultConfig() Config {
|
||||||
UIDLength: 6,
|
UIDLength: 6,
|
||||||
AdminRoleUID: "AM000000",
|
AdminRoleUID: "AM000000",
|
||||||
AdminUserUID: "B000000",
|
AdminUserUID: "B000000",
|
||||||
DefaultRoleName: "user",
|
DefaultRoleName: "USER",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package permission
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultRole = "user"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
func (c ContextKey) String() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyRole ContextKey = "role"
|
||||||
|
KeyDeviceID ContextKey = "device_id"
|
||||||
|
KeyScope ContextKey = "scope"
|
||||||
|
KeyUID ContextKey = "uid"
|
||||||
|
KeyLoginID ContextKey = "login_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UID(ctx context.Context) string { return getString(ctx, KeyUID) }
|
||||||
|
func Scope(ctx context.Context) string { return getString(ctx, KeyScope) }
|
||||||
|
func Role(ctx context.Context) string { return getString(ctx, KeyRole) }
|
||||||
|
func DeviceID(ctx context.Context) string { return getString(ctx, KeyDeviceID) }
|
||||||
|
func LoginID(ctx context.Context) string { return getString(ctx, KeyLoginID) }
|
||||||
|
|
||||||
|
func WithUID(ctx context.Context, uid string) context.Context {
|
||||||
|
return context.WithValue(ctx, KeyUID, uid)
|
||||||
|
}
|
||||||
|
func WithScope(ctx context.Context, scope string) context.Context {
|
||||||
|
return context.WithValue(ctx, KeyScope, scope)
|
||||||
|
}
|
||||||
|
func WithRole(ctx context.Context, role string) context.Context {
|
||||||
|
return context.WithValue(ctx, KeyRole, role)
|
||||||
|
}
|
||||||
|
func WithDeviceID(ctx context.Context, id string) context.Context {
|
||||||
|
return context.WithValue(ctx, KeyDeviceID, id)
|
||||||
|
}
|
||||||
|
func WithLoginID(ctx context.Context, login string) context.Context {
|
||||||
|
return context.WithValue(ctx, KeyLoginID, login)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helper ---
|
||||||
|
func getString(ctx context.Context, key ContextKey) string {
|
||||||
|
if v, ok := ctx.Value(key).(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ type PermissionRepository struct {
|
||||||
DB mongo.DocumentDBWithCacheUseCase
|
DB mongo.DocumentDBWithCacheUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccountRepository(param PermissionRepositoryParam) repository.PermissionRepository {
|
func NewPermissionRepository(param PermissionRepositoryParam) repository.PermissionRepository {
|
||||||
e := entity.Permission{}
|
e := entity.Permission{}
|
||||||
documentDB, err := mongo.MustDocumentDBWithCache(
|
documentDB, err := mongo.MustDocumentDBWithCache(
|
||||||
param.Conf,
|
param.Conf,
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func setupPermissionRepo(db string) (domainRepo.PermissionRepository, func(), er
|
||||||
CacheConf: cacheConf,
|
CacheConf: cacheConf,
|
||||||
CacheOpts: cacheOpts,
|
CacheOpts: cacheOpts,
|
||||||
}
|
}
|
||||||
repo := NewAccountRepository(param)
|
repo := NewPermissionRepository(param)
|
||||||
_, _ = repo.Index20251009001UP(context.Background())
|
_, _ = repo.Index20251009001UP(context.Background())
|
||||||
|
|
||||||
return repo, tearDown, nil
|
return repo, tearDown, nil
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,18 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"backend/internal/config"
|
"backend/internal/config"
|
||||||
"backend/pkg/library/errs"
|
errs "backend/pkg/library/errors"
|
||||||
"backend/pkg/library/errs/code"
|
|
||||||
"backend/pkg/permission/domain/entity"
|
"backend/pkg/permission/domain/entity"
|
||||||
"backend/pkg/permission/domain/repository"
|
"backend/pkg/permission/domain/repository"
|
||||||
"backend/pkg/permission/domain/token"
|
"backend/pkg/permission/domain/token"
|
||||||
"backend/pkg/permission/domain/usecase"
|
"backend/pkg/permission/domain/usecase"
|
||||||
|
|
||||||
"github.com/segmentio/ksuid"
|
"github.com/segmentio/ksuid"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenUseCaseParam struct {
|
type TokenUseCaseParam struct {
|
||||||
TokenRepo repository.TokenRepository
|
TokenRepo repository.TokenRepository
|
||||||
|
Logger errs.Logger
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,16 +27,9 @@ type TokenUseCase struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *TokenUseCase) ReadTokenBasicData(ctx context.Context, token string) (map[string]string, error) {
|
func (use *TokenUseCase) ReadTokenBasicData(ctx context.Context, token string) (map[string]string, error) {
|
||||||
claims, err := parseClaims(token, use.Config.Token.AccessSecret, false)
|
claims, err := ParseClaims(token, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil,
|
return nil, errs.AuthSigPayloadMismatchError("validate token claims error")
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "parseClaims",
|
|
||||||
req: token,
|
|
||||||
err: err,
|
|
||||||
message: "validate token claims error",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
|
|
@ -60,13 +51,7 @@ func (use *TokenUseCase) NewToken(ctx context.Context, req entity.AuthorizationR
|
||||||
|
|
||||||
err = use.TokenRepo.Create(ctx, *tokenObj)
|
err = use.TokenRepo.Create(ctx, *tokenObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.TokenResp{}, use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return entity.TokenResp{}, errs.DBErrorError("failed to create token")
|
||||||
funcName: "TokenRepo.Create",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to create token",
|
|
||||||
errorCode: code.TokenCreateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity.TokenResp{
|
return entity.TokenResp{
|
||||||
|
|
@ -107,7 +92,7 @@ func (use *TokenUseCase) newToken(ctx context.Context, req *entity.Authorization
|
||||||
RefreshCreateAt: now,
|
RefreshCreateAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
if req.Data != nil {
|
if req.Data != nil {
|
||||||
for k, v := range req.Data {
|
for k, v := range req.Data {
|
||||||
tc[k] = v
|
tc[k] = v
|
||||||
|
|
@ -116,7 +101,7 @@ func (use *TokenUseCase) newToken(ctx context.Context, req *entity.Authorization
|
||||||
tc.SetRole(req.Role)
|
tc.SetRole(req.Role)
|
||||||
tc.SetID(token.ID)
|
tc.SetID(token.ID)
|
||||||
tc.SetScope(req.Scope)
|
tc.SetScope(req.Scope)
|
||||||
tc.SetAccount(req.Account)
|
tc.SetLoginID(req.Account)
|
||||||
|
|
||||||
token.UID = tc.UID()
|
token.UID = tc.UID()
|
||||||
|
|
||||||
|
|
@ -127,13 +112,7 @@ func (use *TokenUseCase) newToken(ctx context.Context, req *entity.Authorization
|
||||||
var err error
|
var err error
|
||||||
token.AccessToken, err = accessTokenGenerator(token, tc, use.Config.Token.AccessSecret)
|
token.AccessToken, err = accessTokenGenerator(token, tc, use.Config.Token.AccessSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return nil, errs.SysInternalError("failed to generator access token").Wrap(err)
|
||||||
funcName: "accessTokenGenerator",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to generator access token",
|
|
||||||
errorCode: code.TokenCreateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.IsRefreshToken {
|
if req.IsRefreshToken {
|
||||||
|
|
@ -147,27 +126,13 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok
|
||||||
// Step 1: 檢查 refresh token
|
// Step 1: 檢查 refresh token
|
||||||
tokenObj, err := use.TokenRepo.GetAccessTokenByOneTimeToken(ctx, req.Token)
|
tokenObj, err := use.TokenRepo.GetAccessTokenByOneTimeToken(ctx, req.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.RefreshTokenResp{},
|
return entity.RefreshTokenResp{}, errs.DBErrorError("failed to get access token").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "TokenRepo.GetAccessTokenByOneTimeToken",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get access token",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: 提取 Claims Data
|
// Step 2: 提取 Claims Data
|
||||||
claimsData, err := parseClaims(tokenObj.AccessToken, use.Config.Token.AccessSecret, false)
|
claimsData, err := ParseClaims(tokenObj.AccessToken, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.RefreshTokenResp{},
|
return entity.RefreshTokenResp{}, errs.AuthSigPayloadMismatchError("failed to extract claims")
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "extractClaims",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to extract claims",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: 創建新 token
|
// Step 3: 創建新 token
|
||||||
|
|
@ -179,41 +144,20 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok
|
||||||
Data: claimsData,
|
Data: claimsData,
|
||||||
Expires: req.Expires,
|
Expires: req.Expires,
|
||||||
IsRefreshToken: true,
|
IsRefreshToken: true,
|
||||||
Account: claimsData.Account(),
|
Account: claimsData.LoginID(),
|
||||||
Role: claimsData.Role(),
|
Role: claimsData.Role(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.RefreshTokenResp{},
|
return entity.RefreshTokenResp{}, errs.DBErrorError("failed to create new token").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "use.newToken",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to create new token",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := use.TokenRepo.Create(ctx, *newToken); err != nil {
|
if err := use.TokenRepo.Create(ctx, *newToken); err != nil {
|
||||||
return entity.RefreshTokenResp{},
|
return entity.RefreshTokenResp{}, errs.DBErrorError("failed to create new token").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "TokenRepo.Create",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to create new token",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: 刪除舊 token 並創建新 token
|
// Step 4: 刪除舊 token 並創建新 token
|
||||||
if err := use.TokenRepo.Delete(ctx, tokenObj); err != nil {
|
if err := use.TokenRepo.Delete(ctx, tokenObj); err != nil {
|
||||||
return entity.RefreshTokenResp{},
|
return entity.RefreshTokenResp{}, errs.DBErrorError("failed to delete old token").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "TokenRepo.Delete",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to delete old token",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回新的 Token 響應
|
// 返回新的 Token 響應
|
||||||
|
|
@ -226,65 +170,33 @@ func (use *TokenUseCase) RefreshToken(ctx context.Context, req entity.RefreshTok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *TokenUseCase) CancelToken(ctx context.Context, req entity.CancelTokenReq) error {
|
func (use *TokenUseCase) CancelToken(ctx context.Context, req entity.CancelTokenReq) error {
|
||||||
claims, err := parseClaims(req.Token, use.Config.Token.AccessSecret, false)
|
claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.AuthSigPayloadMismatchError("failed to get token claims")
|
||||||
funcName: "CancelToken extractClaims",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get token claims",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError(fmt.Sprintf("failed to get token claims :%s", claims.ID()))
|
||||||
funcName: "TokenRepo GetAccessTokenByID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: fmt.Sprintf("failed to get token claims :%s", claims.ID()),
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = use.TokenRepo.Delete(ctx, token)
|
err = use.TokenRepo.Delete(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError(fmt.Sprintf("failed to delete token :%s", token.ID)).Wrap(err)
|
||||||
funcName: "TokenRepo Delete",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: fmt.Sprintf("failed to delete token :%s", token.ID),
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (use *TokenUseCase) ValidationToken(ctx context.Context, req entity.ValidationTokenReq) (entity.ValidationTokenResp, error) {
|
func (use *TokenUseCase) ValidationToken(ctx context.Context, req entity.ValidationTokenReq) (entity.ValidationTokenResp, error) {
|
||||||
claims, err := parseClaims(req.Token, use.Config.Token.AccessSecret, true)
|
claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.ValidationTokenResp{},
|
return entity.ValidationTokenResp{}, errs.AuthSigPayloadMismatchError("validate token claims error")
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "parseClaims",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "validate token claims error",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
token, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.ValidationTokenResp{},
|
return entity.ValidationTokenResp{}, errs.DBErrorError(fmt.Sprintf("failed to get token :%s", claims.ID())).Wrap(err)
|
||||||
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{
|
return entity.ValidationTokenResp{
|
||||||
|
|
@ -307,26 +219,14 @@ func (use *TokenUseCase) CancelTokens(ctx context.Context, req entity.DoTokenByU
|
||||||
if req.UID != "" {
|
if req.UID != "" {
|
||||||
err := use.TokenRepo.DeleteAccessTokensByUID(ctx, req.UID)
|
err := use.TokenRepo.DeleteAccessTokensByUID(ctx, req.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to cancel tokens by uid").Wrap(err)
|
||||||
funcName: "TokenRepo.DeleteAccessTokensByUID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to cancel tokens by uid",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.IDs) > 0 {
|
if len(req.IDs) > 0 {
|
||||||
err := use.TokenRepo.DeleteAccessTokenByID(ctx, req.IDs)
|
err := use.TokenRepo.DeleteAccessTokenByID(ctx, req.IDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to cancel tokens by token ids").Wrap(err)
|
||||||
funcName: "TokenRepo.DeleteAccessTokenByID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to cancel tokens by token ids",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
func (use *TokenUseCase) CancelTokenByDeviceID(ctx context.Context, req entity.DoTokenByDeviceIDReq) error {
|
||||||
err := use.TokenRepo.DeleteAccessTokensByDeviceID(ctx, req.DeviceID)
|
err := use.TokenRepo.DeleteAccessTokensByDeviceID(ctx, req.DeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to cancel tokens by device id").Wrap(err)
|
||||||
funcName: "TokenRepo.DeleteAccessTokensByDeviceID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to cancel token by device id",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func (use *TokenUseCase) GetUserTokensByDeviceID(ctx context.Context, req entity.DoTokenByDeviceIDReq) ([]*entity.TokenResp, error) {
|
||||||
uidTokens, err := use.TokenRepo.GetAccessTokensByDeviceID(ctx, req.DeviceID)
|
uidTokens, err := use.TokenRepo.GetAccessTokensByDeviceID(ctx, req.DeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return nil, errs.DBErrorError("failed to get tokens by device id").Wrap(err)
|
||||||
funcName: "TokenRepo.GetAccessTokensByDeviceID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get token by device id",
|
|
||||||
errorCode: code.TokenNotFound,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := make([]*entity.TokenResp, 0, len(uidTokens))
|
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) {
|
func (use *TokenUseCase) GetUserTokensByUID(ctx context.Context, req entity.QueryTokenByUIDReq) ([]*entity.TokenResp, error) {
|
||||||
uidTokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, req.UID)
|
uidTokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, req.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return nil, errs.DBErrorError("failed to get tokens by uid").Wrap(err)
|
||||||
funcName: "TokenRepo.GetAccessTokensByUID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get token by uid",
|
|
||||||
errorCode: code.TokenNotFound,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := make([]*entity.TokenResp, 0, len(uidTokens))
|
tokens := make([]*entity.TokenResp, 0, len(uidTokens))
|
||||||
|
|
@ -400,28 +282,14 @@ func (use *TokenUseCase) GetUserTokensByUID(ctx context.Context, req entity.Quer
|
||||||
|
|
||||||
func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateOneTimeTokenReq) (entity.CreateOneTimeTokenResp, error) {
|
func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateOneTimeTokenReq) (entity.CreateOneTimeTokenResp, error) {
|
||||||
// 驗證Token
|
// 驗證Token
|
||||||
claims, err := parseClaims(req.Token, use.Config.Token.AccessSecret, false)
|
claims, err := ParseClaims(req.Token, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.CreateOneTimeTokenResp{},
|
return entity.CreateOneTimeTokenResp{}, errs.AuthSigPayloadMismatchError("failed to get token claims").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "parseClaims",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get token claims",
|
|
||||||
errorCode: code.OneTimeTokenError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenObj, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
tokenObj, err := use.TokenRepo.GetAccessTokenByID(ctx, claims.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.CreateOneTimeTokenResp{},
|
return entity.CreateOneTimeTokenResp{}, errs.DBErrorError("failed to get token by id").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "TokenRepo.GetAccessTokenByID",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get token by id",
|
|
||||||
errorCode: code.OneTimeTokenError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oneTimeToken := refreshTokenGenerator(ksuid.New().String())
|
oneTimeToken := refreshTokenGenerator(ksuid.New().String())
|
||||||
|
|
@ -430,14 +298,7 @@ func (use *TokenUseCase) NewOneTimeToken(ctx context.Context, req entity.CreateO
|
||||||
Data: claims,
|
Data: claims,
|
||||||
Token: tokenObj,
|
Token: tokenObj,
|
||||||
}, time.Minute); err != nil {
|
}, time.Minute); err != nil {
|
||||||
return entity.CreateOneTimeTokenResp{},
|
return entity.CreateOneTimeTokenResp{}, errs.DBErrorError("failed to create new one-time token").Wrap(err)
|
||||||
use.wrapTokenError(ctx, wrapTokenErrorReq{
|
|
||||||
funcName: "TokenRepo.CreateOneTimeToken",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "create one time token error",
|
|
||||||
errorCode: code.OneTimeTokenError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity.CreateOneTimeTokenResp{
|
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 {
|
func (use *TokenUseCase) CancelOneTimeToken(ctx context.Context, req entity.CancelOneTimeTokenReq) error {
|
||||||
err := use.TokenRepo.DeleteOneTimeToken(ctx, req.Token, nil)
|
err := use.TokenRepo.DeleteOneTimeToken(ctx, req.Token, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to del one time token by token")
|
||||||
funcName: "TokenRepo.DeleteOneTimeToken",
|
|
||||||
req: req,
|
|
||||||
err: err,
|
|
||||||
message: "failed to del one time token by token",
|
|
||||||
errorCode: code.OneTimeTokenError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 加入黑名單 (立即撤銷)
|
// BlacklistToken 將 JWT token 加入黑名單 (立即撤銷)
|
||||||
func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reason string) error {
|
func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reason string) error {
|
||||||
// 解析 JWT 獲取完整的 claims
|
// 解析 JWT 獲取完整的 claims
|
||||||
claimMap, err := parseToken(token, use.Config.Token.AccessSecret, false)
|
claimMap, err := parseToken(token, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.AuthSigPayloadMismatchError("failed to parse token claims").Wrap(err)
|
||||||
funcName: "BlacklistToken.parseToken",
|
|
||||||
req: token,
|
|
||||||
err: err,
|
|
||||||
message: "failed to parse token claims",
|
|
||||||
errorCode: code.InvalidJWT,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取 JTI (JWT ID)
|
// 獲取 JTI (JWT ID)
|
||||||
jti, exists := claimMap["jti"]
|
jti, exists := claimMap["jti"]
|
||||||
if !exists {
|
if !exists {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.ResNotFoundError("token missing JTI claim").Wrap(err)
|
||||||
funcName: "BlacklistToken.getJTI",
|
|
||||||
req: token,
|
|
||||||
err: entity.ErrInvalidJTI,
|
|
||||||
message: "token missing JTI claim",
|
|
||||||
errorCode: code.InvalidJWT,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jtiStr, ok := jti.(string)
|
jtiStr, ok := jti.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.ResNotFoundError("token missing JTI claim").Wrap(err)
|
||||||
funcName: "BlacklistToken.convertJTI",
|
|
||||||
req: token,
|
|
||||||
err: entity.ErrInvalidJTI,
|
|
||||||
message: "JTI claim is not a string",
|
|
||||||
errorCode: code.InvalidJWT,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取 UID (可能在 data 中)
|
// 獲取 UID (可能在 data 中)
|
||||||
|
|
@ -538,13 +347,7 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso
|
||||||
// 獲取過期時間
|
// 獲取過期時間
|
||||||
exp, exists := claimMap["exp"]
|
exp, exists := claimMap["exp"]
|
||||||
if !exists {
|
if !exists {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.AuthExpiredError("token missing exp claim").Wrap(err)
|
||||||
funcName: "BlacklistToken.getExp",
|
|
||||||
req: token,
|
|
||||||
err: entity.ErrTokenExpired,
|
|
||||||
message: "token missing exp claim",
|
|
||||||
errorCode: code.TokenExpired,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 將 exp 轉換為 int64 (JWT 中通常是 float64)
|
// 將 exp 轉換為 int64 (JWT 中通常是 float64)
|
||||||
|
|
@ -557,23 +360,11 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso
|
||||||
case string:
|
case string:
|
||||||
parsedExp, err := strconv.ParseInt(v, 10, 64)
|
parsedExp, err := strconv.ParseInt(v, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.SysInternalError("failed to parse exp claim").Wrap(err)
|
||||||
funcName: "BlacklistToken.parseExp",
|
|
||||||
req: token,
|
|
||||||
err: err,
|
|
||||||
message: "failed to parse exp claim",
|
|
||||||
errorCode: code.TokenExpired,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
expInt = parsedExp
|
expInt = parsedExp
|
||||||
default:
|
default:
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.SysInternalError("exp claim type conversion failed").Wrap(err)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 創建黑名單條目
|
// 創建黑名單條目
|
||||||
|
|
@ -587,19 +378,25 @@ func (use *TokenUseCase) BlacklistToken(ctx context.Context, token string, reaso
|
||||||
// 添加到黑名單
|
// 添加到黑名單
|
||||||
err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算
|
err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to add token to blacklist").Wrap(err)
|
||||||
funcName: "BlacklistToken.AddToBlacklist",
|
|
||||||
req: jtiStr,
|
|
||||||
err: err,
|
|
||||||
message: "failed to add token to blacklist",
|
|
||||||
errorCode: code.TokenCreateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.WithContext(ctx).Infow("token blacklisted",
|
// 記錄成功日誌(如果 Logger 存在)
|
||||||
logx.Field("jti", jtiStr),
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("reason", reason))
|
errs.LogField{
|
||||||
|
Key: "jti",
|
||||||
|
Val: jtiStr,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "reason",
|
||||||
|
Val: reason,
|
||||||
|
}).Info("token blacklisted")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func (use *TokenUseCase) IsTokenBlacklisted(ctx context.Context, jti string) (bool, error) {
|
||||||
isBlacklisted, err := use.TokenRepo.IsBlacklisted(ctx, jti)
|
isBlacklisted, err := use.TokenRepo.IsBlacklisted(ctx, jti)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return false, errs.DBErrorError("failed to check blacklist status").Wrap(err)
|
||||||
funcName: "IsTokenBlacklisted",
|
|
||||||
req: jti,
|
|
||||||
err: err,
|
|
||||||
message: "failed to check blacklist status",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isBlacklisted, nil
|
return isBlacklisted, nil
|
||||||
|
|
@ -625,50 +416,83 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string,
|
||||||
// 獲取用戶的所有 token
|
// 獲取用戶的所有 token
|
||||||
tokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, uid)
|
tokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return use.wrapTokenError(ctx, wrapTokenErrorReq{
|
return errs.DBErrorError("failed to get user tokens").Wrap(err)
|
||||||
funcName: "BlacklistAllUserTokens.GetAccessTokensByUID",
|
|
||||||
req: uid,
|
|
||||||
err: err,
|
|
||||||
message: "failed to get user tokens",
|
|
||||||
errorCode: code.TokenValidateError,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 為每個 token 創建黑名單條目
|
// 為每個 token 創建黑名單條目
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
// 解析 token 獲取 JTI 和過期時間
|
// 解析 token 獲取 JTI 和過期時間
|
||||||
claims, err := parseClaims(token.AccessToken, use.Config.Token.AccessSecret, false)
|
claims, err := ParseClaims(token.AccessToken, use.Config.Token.AccessSecret, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.WithContext(ctx).Errorw("failed to parse token for blacklisting",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("tokenID", token.ID),
|
errs.LogField{
|
||||||
logx.Field("error", err))
|
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
|
continue // 跳過無效的 token,繼續處理其他 token
|
||||||
}
|
}
|
||||||
|
|
||||||
jti, exists := claims["jti"]
|
jti, exists := claims["jti"]
|
||||||
if !exists || jti == "" {
|
if !exists || jti == "" {
|
||||||
logx.WithContext(ctx).Errorw("token missing JTI claim",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("tokenID", token.ID))
|
errs.LogField{
|
||||||
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "tokenID",
|
||||||
|
Val: token.ID,
|
||||||
|
}).Error("failed to parse token for blacklisting")
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
exp, exists := claims["exp"]
|
exp, exists := claims["exp"]
|
||||||
if !exists {
|
if !exists {
|
||||||
logx.WithContext(ctx).Errorw("token missing exp claim",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("tokenID", token.ID))
|
errs.LogField{
|
||||||
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "tokenID",
|
||||||
|
Val: token.ID,
|
||||||
|
}).Error("token missing exp claim")
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 將 exp 字符串轉換為 int64
|
// 將 exp 字符串轉換為 int64
|
||||||
expInt, err := strconv.ParseInt(exp, 10, 64)
|
expInt, err := strconv.ParseInt(exp, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.WithContext(ctx).Errorw("failed to parse exp claim",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("tokenID", token.ID),
|
errs.LogField{
|
||||||
logx.Field("error", err))
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "tokenID",
|
||||||
|
Val: token.ID,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "error",
|
||||||
|
Val: err,
|
||||||
|
}).Error("failed to parse exp claim")
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -683,10 +507,21 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string,
|
||||||
// 添加到黑名單
|
// 添加到黑名單
|
||||||
err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算
|
err = use.TokenRepo.AddToBlacklist(ctx, blacklistEntry, 0) // TTL=0 表示使用默認計算
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.WithContext(ctx).Errorw("failed to add token to blacklist",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("jti", jti),
|
errs.LogField{
|
||||||
logx.Field("error", err))
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "jti",
|
||||||
|
Val: jti,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "error",
|
||||||
|
Val: err,
|
||||||
|
}).Error("failed to add token to blacklist")
|
||||||
|
}
|
||||||
// 繼續處理其他 token,不要因為一個失敗就停止
|
// 繼續處理其他 token,不要因為一個失敗就停止
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -694,16 +529,34 @@ func (use *TokenUseCase) BlacklistAllUserTokens(ctx context.Context, uid string,
|
||||||
// 刪除用戶的所有 token 記錄
|
// 刪除用戶的所有 token 記錄
|
||||||
err = use.TokenRepo.DeleteAccessTokensByUID(ctx, uid)
|
err = use.TokenRepo.DeleteAccessTokensByUID(ctx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.WithContext(ctx).Errorw("failed to delete user tokens",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("error", err))
|
errs.LogField{
|
||||||
// 這不是致命錯誤,因為 token 已經被加入黑名單
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "error",
|
||||||
|
Val: err,
|
||||||
|
}).Error("failed to delete user tokens")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.WithContext(ctx).Infow("all user tokens blacklisted",
|
if use.Logger != nil {
|
||||||
logx.Field("uid", uid),
|
use.Logger.WithFields(
|
||||||
logx.Field("tokenCount", len(tokens)),
|
errs.LogField{
|
||||||
logx.Field("reason", reason))
|
Key: "uid",
|
||||||
|
Val: uid,
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "tokenCount",
|
||||||
|
Val: len(tokens),
|
||||||
|
},
|
||||||
|
errs.LogField{
|
||||||
|
Key: "reason",
|
||||||
|
Val: reason,
|
||||||
|
}).Error("all user tokens blacklisted")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
type tokenClaims map[string]string
|
type TokenClaims map[string]string
|
||||||
|
|
||||||
func (tc tokenClaims) SetID(id string) {
|
func (tc TokenClaims) SetID(id string) {
|
||||||
tc["id"] = id
|
tc["id"] = id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) SetRole(role string) {
|
func (tc TokenClaims) SetRole(role string) {
|
||||||
tc["role"] = role
|
tc["role"] = role
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) SetDeviceID(deviceID string) {
|
func (tc TokenClaims) SetDeviceID(deviceID string) {
|
||||||
tc["device_id"] = deviceID
|
tc["device_id"] = deviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) SetScope(scope string) {
|
func (tc TokenClaims) SetScope(scope string) {
|
||||||
tc["scope"] = scope
|
tc["scope"] = scope
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) SetAccount(account string) {
|
func (tc TokenClaims) SetLoginID(loginID string) {
|
||||||
tc["account"] = account
|
tc["login_id"] = loginID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) Role() string {
|
func (tc TokenClaims) Role() string {
|
||||||
role, ok := tc["role"]
|
role, ok := tc["role"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -31,7 +31,7 @@ func (tc tokenClaims) Role() string {
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) ID() string {
|
func (tc TokenClaims) ID() string {
|
||||||
id, ok := tc["id"]
|
id, ok := tc["id"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -40,7 +40,7 @@ func (tc tokenClaims) ID() string {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) DeviceID() string {
|
func (tc TokenClaims) DeviceID() string {
|
||||||
deviceID, ok := tc["device_id"]
|
deviceID, ok := tc["device_id"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -49,7 +49,7 @@ func (tc tokenClaims) DeviceID() string {
|
||||||
return deviceID
|
return deviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) UID() string {
|
func (tc TokenClaims) UID() string {
|
||||||
uid, ok := tc["uid"]
|
uid, ok := tc["uid"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -58,7 +58,7 @@ func (tc tokenClaims) UID() string {
|
||||||
return uid
|
return uid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) Scope() string {
|
func (tc TokenClaims) Scope() string {
|
||||||
scope, ok := tc["scope"]
|
scope, ok := tc["scope"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -67,8 +67,8 @@ func (tc tokenClaims) Scope() string {
|
||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc tokenClaims) Account() string {
|
func (tc TokenClaims) LoginID() string {
|
||||||
scope, ok := tc["account"]
|
scope, ok := tc["login_id"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func TestTokenClaims_SetAndGetID(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc.SetID(tt.id)
|
tc.SetID(tt.id)
|
||||||
|
|
||||||
result := tc.ID()
|
result := tc.ID()
|
||||||
|
|
@ -61,7 +61,7 @@ func TestTokenClaims_SetAndGetRole(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc.SetRole(tt.role)
|
tc.SetRole(tt.role)
|
||||||
|
|
||||||
result := tc.Role()
|
result := tc.Role()
|
||||||
|
|
@ -91,7 +91,7 @@ func TestTokenClaims_SetAndGetDeviceID(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc.SetDeviceID(tt.deviceID)
|
tc.SetDeviceID(tt.deviceID)
|
||||||
|
|
||||||
result := tc.DeviceID()
|
result := tc.DeviceID()
|
||||||
|
|
@ -125,7 +125,7 @@ func TestTokenClaims_SetAndGetScope(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc.SetScope(tt.scope)
|
tc.SetScope(tt.scope)
|
||||||
|
|
||||||
// Note: there's no GetScope method, so we just verify it's set
|
// Note: there's no GetScope method, so we just verify it's set
|
||||||
|
|
@ -159,11 +159,13 @@ func TestTokenClaims_SetAndGetAccount(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc.SetAccount(tt.account)
|
tc.SetLoginID(tt.account)
|
||||||
|
|
||||||
// Note: there's no GetAccount method, so we just verify it's set
|
// 使用 LoginID() 方法獲取
|
||||||
assert.Equal(t, tt.account, tc["account"])
|
assert.Equal(t, tt.account, tc.LoginID())
|
||||||
|
// 也驗證直接訪問 login_id 欄位
|
||||||
|
assert.Equal(t, tt.account, tc["login_id"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +191,7 @@ func TestTokenClaims_SetAndGetUID(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
tc["uid"] = tt.uid
|
tc["uid"] = tt.uid
|
||||||
|
|
||||||
result := tc.UID()
|
result := tc.UID()
|
||||||
|
|
@ -199,7 +201,7 @@ func TestTokenClaims_SetAndGetUID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_GetNonExistentField(t *testing.T) {
|
func TestTokenClaims_GetNonExistentField(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
|
|
||||||
t.Run("get non-existent ID", func(t *testing.T) {
|
t.Run("get non-existent ID", func(t *testing.T) {
|
||||||
result := tc.ID()
|
result := tc.ID()
|
||||||
|
|
@ -223,13 +225,13 @@ func TestTokenClaims_GetNonExistentField(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_MultipleFields(t *testing.T) {
|
func TestTokenClaims_MultipleFields(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
|
|
||||||
tc.SetID("token123")
|
tc.SetID("token123")
|
||||||
tc.SetRole("admin")
|
tc.SetRole("admin")
|
||||||
tc.SetDeviceID("device456")
|
tc.SetDeviceID("device456")
|
||||||
tc.SetScope("read write")
|
tc.SetScope("read write")
|
||||||
tc.SetAccount("user@example.com")
|
tc.SetLoginID("user@example.com")
|
||||||
tc["uid"] = "user789"
|
tc["uid"] = "user789"
|
||||||
|
|
||||||
t.Run("verify all fields", func(t *testing.T) {
|
t.Run("verify all fields", func(t *testing.T) {
|
||||||
|
|
@ -237,13 +239,14 @@ func TestTokenClaims_MultipleFields(t *testing.T) {
|
||||||
assert.Equal(t, "admin", tc.Role())
|
assert.Equal(t, "admin", tc.Role())
|
||||||
assert.Equal(t, "device456", tc.DeviceID())
|
assert.Equal(t, "device456", tc.DeviceID())
|
||||||
assert.Equal(t, "read write", tc["scope"])
|
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())
|
assert.Equal(t, "user789", tc.UID())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_Overwrite(t *testing.T) {
|
func TestTokenClaims_Overwrite(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
|
|
||||||
t.Run("overwrite ID", func(t *testing.T) {
|
t.Run("overwrite ID", func(t *testing.T) {
|
||||||
tc.SetID("token123")
|
tc.SetID("token123")
|
||||||
|
|
@ -263,7 +266,7 @@ func TestTokenClaims_Overwrite(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_MapBehavior(t *testing.T) {
|
func TestTokenClaims_MapBehavior(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
|
|
||||||
t.Run("can set custom fields", func(t *testing.T) {
|
t.Run("can set custom fields", func(t *testing.T) {
|
||||||
tc["custom_field"] = "custom_value"
|
tc["custom_field"] = "custom_value"
|
||||||
|
|
@ -271,7 +274,7 @@ func TestTokenClaims_MapBehavior(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can iterate over fields", func(t *testing.T) {
|
t.Run("can iterate over fields", func(t *testing.T) {
|
||||||
tc2 := make(tokenClaims)
|
tc2 := make(TokenClaims)
|
||||||
tc2.SetID("token123")
|
tc2.SetID("token123")
|
||||||
tc2.SetRole("admin")
|
tc2.SetRole("admin")
|
||||||
tc2["uid"] = "user123"
|
tc2["uid"] = "user123"
|
||||||
|
|
@ -303,7 +306,7 @@ func TestTokenClaims_MapBehavior(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_EmptyMap(t *testing.T) {
|
func TestTokenClaims_EmptyMap(t *testing.T) {
|
||||||
tc := make(tokenClaims)
|
tc := make(TokenClaims)
|
||||||
|
|
||||||
assert.Empty(t, tc.ID())
|
assert.Empty(t, tc.ID())
|
||||||
assert.Empty(t, tc.Role())
|
assert.Empty(t, tc.Role())
|
||||||
|
|
@ -313,7 +316,7 @@ func TestTokenClaims_EmptyMap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenClaims_NilMap(t *testing.T) {
|
func TestTokenClaims_NilMap(t *testing.T) {
|
||||||
var tc tokenClaims
|
var tc TokenClaims
|
||||||
|
|
||||||
t.Run("get from nil map", func(t *testing.T) {
|
t.Run("get from nil map", func(t *testing.T) {
|
||||||
assert.Empty(t, tc.ID())
|
assert.Empty(t, tc.ID())
|
||||||
|
|
@ -322,4 +325,3 @@ func TestTokenClaims_NilMap(t *testing.T) {
|
||||||
assert.Empty(t, tc.UID())
|
assert.Empty(t, tc.UID())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ func createAccessToken(token entity.Token, data any, secretKey string) (string,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ID: token.ID,
|
ID: token.ID,
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Unix(int64(token.ExpiresIn), 0)),
|
ExpiresAt: jwt.NewNumericDate(time.Unix(int64(token.ExpiresIn), 0)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Issuer: "permission",
|
Issuer: "permission",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -76,10 +77,10 @@ func parseToken(accessToken string, secret string, validate bool) (jwt.MapClaims
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseClaims(accessToken string, secret string, validate bool) (tokenClaims, error) {
|
func ParseClaims(accessToken string, secret string, validate bool) (TokenClaims, error) {
|
||||||
claimMap, err := parseToken(accessToken, secret, validate)
|
claimMap, err := parseToken(accessToken, secret, validate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tokenClaims{}, err
|
return TokenClaims{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
claimsData, ok := claimMap["data"].(map[string]any)
|
claimsData, ok := claimMap["data"].(map[string]any)
|
||||||
|
|
@ -87,7 +88,7 @@ func parseClaims(accessToken string, secret string, validate bool) (tokenClaims,
|
||||||
return convertMap(claimsData), nil
|
return convertMap(claimsData), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenClaims{}, fmt.Errorf("get data from claim map error")
|
return TokenClaims{}, fmt.Errorf("get data from claim map error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertMap(input map[string]interface{}) map[string]string {
|
func convertMap(input map[string]interface{}) map[string]string {
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ func TestParseClaims(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
claims, err := parseClaims(tt.accessToken, tt.secret, tt.validate)
|
claims, err := ParseClaims(tt.accessToken, tt.secret, tt.validate)
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue