feat: move error into library

This commit is contained in:
王性驊 2025-10-02 22:56:14 +08:00
parent 639879c089
commit 78505faf75
25 changed files with 1345 additions and 28 deletions

View File

@ -1148,7 +1148,7 @@
"url": "https://localhost:8888"
}
],
"x-date": "2025-10-02 17:47:43",
"x-date": "2025-10-02 22:08:26",
"x-description": "This is a go-doc generated swagger file.",
"x-generator": "go-doc",
"x-github": "https://github.com/danielchan-25/go-doc",

5
go.mod
View File

@ -2,8 +2,9 @@ module backend
go 1.25.1
replace backend/pkg/library/errs => ./pkg/library/errs
require (
code.30cm.net/digimon/library-go/errs v1.2.14
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
github.com/alicebob/miniredis/v2 v2.35.0
github.com/shopspring/decimal v1.4.0
@ -14,6 +15,7 @@ require (
go.mongodb.org/mongo-driver/v2 v2.3.0
go.uber.org/mock v0.4.0
golang.org/x/crypto v0.37.0
google.golang.org/grpc v1.67.0
google.golang.org/protobuf v1.36.5
)
@ -105,7 +107,6 @@ require (
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

2
go.sum
View File

@ -1,5 +1,3 @@
code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU=
code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 h1:szWsI0K+1iEHmc/AtKx+5c7tDIc1AZdStvT0tVza1pg=
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=

View File

@ -5,8 +5,8 @@ import (
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"code.30cm.net/digimon/library-go/errs"
ers "code.30cm.net/digimon/library-go/errs"
"backend/pkg/library/errs"
ers "backend/pkg/library/errs"
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"

View File

@ -2,7 +2,7 @@ package auth
import (
"backend/internal/domain"
"code.30cm.net/digimon/library-go/errs"
"backend/pkg/library/errs"
"net/http"
"backend/internal/logic/auth"

View File

@ -5,8 +5,8 @@ import (
"backend/internal/types"
mb "backend/pkg/member/domain/member"
member "backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
"context"
"google.golang.org/protobuf/proto"
"time"

View File

@ -0,0 +1,14 @@
package code
// Category for general operations: 10 - 490
const (
_ = iota
CatInput uint32 = iota * 10
CatDB
CatResource
CatGRPC
CatAuth
CatSystem
CatPubSub
CatService
)

View File

@ -0,0 +1,75 @@
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 // 資源有多個所有者
)
/* 詳細代碼 - 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 錯誤
)

View File

@ -0,0 +1,13 @@
package code
// CatToStr collects general error messages for each Category
// It is used to send back to API caller
var CatToStr = map[uint32]string{
CatInput: "Invalid Input Data",
CatDB: "Database Error",
CatResource: "Resource Error",
CatGRPC: "Internal Service Communication Error",
CatAuth: "Authentication Error",
CatService: "Internal Service Communication Error",
CatSystem: "System Error",
}

View File

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

View File

@ -0,0 +1,535 @@
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, " ")))
}
// 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, " ")))
}
// 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
}
// ResourceAlreadyExist returns Err
func ResourceAlreadyExist(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
}

View File

@ -0,0 +1,184 @@
package errs
import (
"context"
"testing"
"backend/pkg/library/errs/code"
"github.com/zeromicro/go-zero/core/logx"
)
func TestFromError(t *testing.T) {
t.Run("nil error", func(t *testing.T) {
if err := FromError(nil); err != nil {
t.Errorf("expected nil, got %v", err)
}
})
t.Run("LibError type", func(t *testing.T) {
libErr := NewError(1, 200, 10, "test error")
if err := FromError(libErr); err != libErr {
t.Errorf("expected %v, got %v", libErr, err)
}
})
}
func TestFromCode(t *testing.T) {
tests := []struct {
name string
code uint32
expected *LibError
}{
{"valid code", 1200314, NewError(12, 3, 14, "")},
{"invalid code", 9999999, NewError(99, 999, 99, "")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := FromCode(tt.code)
if err.FullCode() != tt.expected.FullCode() {
t.Errorf("expected %v, got %v", tt.expected.FullCode(), err.FullCode())
}
})
}
}
func TestSystemTimeoutError(t *testing.T) {
tests := []struct {
name string
input []string
expected string
}{
{"single string", []string{"timeout"}, "timeout"},
{"multiple strings", []string{"timeout", "occurred"}, "timeout occurred"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := SystemTimeoutError(tt.input...)
if err.Error() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, err.Error())
}
})
}
}
func TestSystemInternalErrorL(t *testing.T) {
tests := []struct {
name string
input []string
}{
{"internal error", []string{"internal error"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO()
err := SystemInternalErrorL(logx.WithContext(ctx), nil, tt.input...)
if err.Error() != tt.input[0] {
t.Errorf("expected %s, got %s", tt.input[0], err.Error())
}
})
}
}
func TestInvalidFormatL(t *testing.T) {
mockLogger := logx.WithContext(context.Background())
tests := []struct {
name string
input []string
expected string
}{
{"invalid format", []string{"invalid format"}, "invalid format"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InvalidFormatL(mockLogger, nil, tt.input...)
if err.Error() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, err.Error())
}
})
}
}
func TestDBErrorL(t *testing.T) {
mockLogger := logx.WithContext(context.Background())
tests := []struct {
name string
input []string
expected string
}{
{"DB error", []string{"DB error"}, "DB error"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := DBErrorL(mockLogger, nil, tt.input...)
if err.Error() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, err.Error())
}
})
}
}
func TestResourceNotFoundL(t *testing.T) {
mockLogger := logx.WithContext(context.Background())
tests := []struct {
name string
input []string
expected string
}{
{"resource not found", []string{"resource not found"}, "resource not found"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ResourceNotFoundL(mockLogger, nil, tt.input...)
if err.Error() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, err.Error())
}
})
}
}
func TestInsufficientPermissionL(t *testing.T) {
mockLogger := logx.WithContext(context.Background())
tests := []struct {
name string
input []string
expected string
}{
{"insufficient permission", []string{"permission denied"}, "permission denied"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InsufficientPermissionL(mockLogger, nil, tt.input...)
if err.Error() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, err.Error())
}
})
}
}
func TestIsAuthUnauthorizedError(t *testing.T) {
tests := []struct {
name string
err *LibError
expected bool
}{
{"Unauthorized error", NewError(1, code.Unauthorized, 0, ""), true},
{"AuthExpired error", NewError(1, code.AuthExpired, 0, ""), true},
{"Other error", NewError(1, code.ArkInternal, 0, ""), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := IsAuthUnauthorizedError(tt.err)
if res != tt.expected {
t.Errorf("expected %t, got %t", tt.expected, res)
}
})
}
}

View File

@ -0,0 +1,88 @@
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
}

214
pkg/library/errs/errors.go Normal file
View File

@ -0,0 +1,214 @@
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:
// 如果資源不足,返回 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:
// 如果時間無效或禁止訪問,返回 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,
}
}

View File

@ -0,0 +1,177 @@
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)
}
})
}
}

View File

@ -1,6 +1,6 @@
package domain
import "code.30cm.net/digimon/library-go/errs"
import "backend/pkg/library/errs"
// Verify Error Code
const (

View File

@ -9,8 +9,8 @@ import (
"backend/pkg/member/domain/entity"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
)

View File

@ -7,8 +7,8 @@ import (
"backend/pkg/member/domain"
"backend/pkg/member/domain/entity"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
GIDLib "code.30cm.net/digimon/library-go/utils/invited_code"
"github.com/zeromicro/go-zero/core/logx"
)

View File

@ -13,8 +13,8 @@ import (
"backend/pkg/member/domain/repository"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
)

View File

@ -13,8 +13,8 @@ import (
mockRepo "backend/pkg/member/mock/repository"
"backend/pkg/member/repository"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.uber.org/mock/gomock"

View File

@ -8,8 +8,8 @@ import (
"backend/pkg/member/domain/member"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
)
func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase.GenerateRefreshCodeRequest) (usecase.GenerateRefreshCodeResponse, error) {

View File

@ -13,8 +13,8 @@ import (
"backend/pkg/member/domain"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
)
func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecase.VerifyAuthResultRequest) (usecase.GoogleTokenInfo, error) {

View File

@ -8,8 +8,8 @@ import (
"backend/pkg/member/domain"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
"github.com/stretchr/testify/assert"
)

View File

@ -11,8 +11,8 @@ import (
"backend/pkg/member/domain"
"backend/pkg/member/domain/usecase"
"code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"backend/pkg/library/errs"
"backend/pkg/library/errs/code"
)
// LineCodeToAccessToken 透過 Line 授權碼換取 Access Token

View File

@ -11,7 +11,7 @@ import (
"backend/pkg/member/domain/usecase"
mockRepo "backend/pkg/member/mock/repository"
"code.30cm.net/digimon/library-go/errs"
"backend/pkg/library/errs"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)