From cb7e9fb5bb5b4d8883da1de25d514b6e72cebbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 4 Apr 2025 14:39:01 +0800 Subject: [PATCH] feat: add tags --- pkg/domain/entity/kyc.go | 29 ++++--- pkg/domain/kyc/status.go | 9 ++ pkg/domain/usecase/kyc.go | 46 +++++++++- pkg/domain/usecase/tags.go | 39 +++++++++ pkg/repository/kyc.go | 5 ++ pkg/usecase/kyc.go | 167 +++++++++++++++++++++++++++++++++++++ pkg/usecase/tags.go | 141 +++++++++++++++++++++++++++++++ 7 files changed, 422 insertions(+), 14 deletions(-) create mode 100644 pkg/domain/kyc/status.go create mode 100644 pkg/usecase/tags.go diff --git a/pkg/domain/entity/kyc.go b/pkg/domain/entity/kyc.go index 8d6131f..c7a41d2 100644 --- a/pkg/domain/entity/kyc.go +++ b/pkg/domain/entity/kyc.go @@ -1,6 +1,9 @@ package entity -import "go.mongodb.org/mongo-driver/bson/primitive" +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/kyc" + "go.mongodb.org/mongo-driver/bson/primitive" +) type KYC struct { ID primitive.ObjectID `bson:"_id,omitempty"` @@ -12,18 +15,18 @@ type KYC struct { Address string `bson:"address"` // 戶籍地址(或居住地址) PostalCode string `bson:"postal_code"` // 郵遞區號(海外使用) // 上傳文件網址(可為 object storage 的 URL) - IDFrontImage string `bson:"id_front_image"` // 身分證/護照 正面 - IDBackImage string `bson:"id_back_image"` // 身分證/居留證 反面 - BankStatementImg string `bson:"bank_statement_img"` // 銀行存摺封面照 - BankCode string `bson:"bank_code"` // 銀行代碼(可為 SWIFT) - BankName string `bson:"bank_name"` // 銀行名稱(顯示用) - BranchCode string `bson:"branch_code"` // 分行代碼 - BranchName string `bson:"branch_name"` // 分行名稱(顯示用) - BankAccount string `bson:"bank_account"` // 銀行帳號 - Status string `bson:"status"` // 審核狀態:PENDING, APPROVED, REJECTED - RejectReason string `bson:"reject_reason"` // 若被駁回,原因描述 - UpdatedAt int64 `bson:"updated_at,omitempty"` - CreatedAt int64 `bson:"created_at,omitempty"` + IDFrontImage string `bson:"id_front_image"` // 身分證/護照 正面 + IDBackImage string `bson:"id_back_image"` // 身分證/居留證 反面 + BankStatementImg string `bson:"bank_statement_img"` // 銀行存摺封面照 + BankCode string `bson:"bank_code"` // 銀行代碼(可為 SWIFT) + BankName string `bson:"bank_name"` // 銀行名稱(顯示用) + BranchCode string `bson:"branch_code"` // 分行代碼 + BranchName string `bson:"branch_name"` // 分行名稱(顯示用) + BankAccount string `bson:"bank_account"` // 銀行帳號 + Status kyc.Status `bson:"status"` // 審核狀態:PENDING, APPROVED, REJECTED + RejectReason string `bson:"reject_reason"` // 若被駁回,原因描述 + UpdatedAt int64 `bson:"updated_at,omitempty"` + CreatedAt int64 `bson:"created_at,omitempty"` } func (p *KYC) CollectionName() string { diff --git a/pkg/domain/kyc/status.go b/pkg/domain/kyc/status.go new file mode 100644 index 0000000..ba00c9f --- /dev/null +++ b/pkg/domain/kyc/status.go @@ -0,0 +1,9 @@ +package kyc + +type Status string + +const ( + StatusPending Status = "PENDING" + StatusAPPROVED Status = "APPROVED" + StatusREJECTED Status = "REJECTED" +) diff --git a/pkg/domain/usecase/kyc.go b/pkg/domain/usecase/kyc.go index 1bba0ff..8cac391 100644 --- a/pkg/domain/usecase/kyc.go +++ b/pkg/domain/usecase/kyc.go @@ -1,4 +1,48 @@ package usecase -type KYCUseCase struct { +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "context" +) + +type KYCUseCase interface { + // Create 建立 KYC 資料 + Create(ctx context.Context, kyc *entity.KYC) error + // FindLatestByUID 根據使用者 UID 取得最新 KYC 紀錄 + FindLatestByUID(ctx context.Context, uid string) (*entity.KYC, error) + // FindByID 根據 KYC ID 查詢 + FindByID(ctx context.Context, id string) (*entity.KYC, error) + // List 分頁查詢(後台審核列表用) + List(ctx context.Context, params KYCQueryParams) ([]*entity.KYC, int64, error) + // UpdateStatus 更新 KYC 狀態與審核原因(審核用) + UpdateStatus(ctx context.Context, id string, status string, reason string) error + // UpdateKYCInfo 更新使用者的 KYC(限於尚未審核的) + UpdateKYCInfo(ctx context.Context, id string, update *KYCUpdateParams) error +} + +type KYCQueryParams struct { + UID *string + Country *string + Status *string // PENDING, APPROVED, REJECTED + PageSize int64 + PageIndex int64 + SortByDate bool // 是否依申請時間倒序 +} + +type KYCUpdateParams struct { + Name *string + Identification *string + IdentificationType *string + Address *string + PostalCode *string + DateOfBirth *string + Gender *string + IDFrontImage *string + IDBackImage *string + BankStatementImg *string + BankCode *string + BankName *string + BranchCode *string + BranchName *string + BankAccount *string } diff --git a/pkg/domain/usecase/tags.go b/pkg/domain/usecase/tags.go index aed2454..993a7d1 100644 --- a/pkg/domain/usecase/tags.go +++ b/pkg/domain/usecase/tags.go @@ -1 +1,40 @@ package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product" + "context" +) + +type ProductBaseTags interface { + // Create 新增一筆 Tag 資料 + Create(ctx context.Context, tag *entity.Tags) error + // GetByID 根據 tag 的內部 ID 取得資料 + GetByID(ctx context.Context, id string) (*entity.Tags, error) + // Update 更新現有的 Tag 資料 + Update(ctx context.Context, id string, tag TagModifyParams) error + // Delete 刪除指定 ID 的 Tag 資料 + Delete(ctx context.Context, id string) error + // List 根據查詢條件取得 Tag 資料列表 + // 回傳值分別為資料列表與符合條件的總筆數 + List(ctx context.Context, params TagQueryParams) ([]*entity.Tags, int64, error) + // GetByIDs 根據查詢條件取得 Tag 資料列表 + GetByIDs(ctx context.Context, ids []string) ([]*entity.Tags, error) +} + +// TagQueryParams 為查詢 Tags 時的參數結構 +type TagQueryParams struct { + Types *product.ItemType // 過濾 Tag 類型 + Name *string // 過濾名稱(部分比對) + ShowType *product.ShowType // 過濾顯示筐 + // 可根據需求增加其他查詢條件,如分頁、排序等 + PageSize int64 + PageIndex int64 +} + +type TagModifyParams struct { + Types *product.ItemType // 過濾 Tag 類型 + Name *string // 過濾名稱(部分比對) + ShowType *product.ShowType // 過濾顯示筐 + Cover *string `bson:"cover,omitempty"` // 封面圖片 +} diff --git a/pkg/repository/kyc.go b/pkg/repository/kyc.go index 063b42d..a7ca35f 100644 --- a/pkg/repository/kyc.go +++ b/pkg/repository/kyc.go @@ -5,10 +5,12 @@ import ( "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" mgo "code.30cm.net/digimon/library-go/mongo" "context" + "errors" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "time" ) @@ -62,6 +64,9 @@ func (repo *KYCRepository) FindLatestByUID(ctx context.Context, uid string) (*en err := repo.DB.GetClient().FindOne(ctx, &result, filter, opts) if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrNotFound + } return nil, err } return &result, nil diff --git a/pkg/usecase/kyc.go b/pkg/usecase/kyc.go index aed2454..a8e8ab8 100644 --- a/pkg/usecase/kyc.go +++ b/pkg/usecase/kyc.go @@ -1 +1,168 @@ package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/kyc" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/usecase" + repo "code.30cm.net/digimon/app-cloudep-product-service/pkg/repository" + "code.30cm.net/digimon/library-go/errs" + "context" + "errors" + "fmt" + "github.com/zeromicro/go-zero/core/logx" +) + +type KYCUseCaseParam struct { + KYCRepo repository.KYCRepository +} + +type KYCUseCase struct { + KYCUseCaseParam +} + +func MustKYCUseCase(param KYCUseCaseParam) usecase.KYCUseCase { + return &KYCUseCase{ + param, + } +} + +func (use *KYCUseCase) Create(ctx context.Context, data *entity.KYC) error { + latest, err := use.KYCRepo.FindLatestByUID(ctx, data.UID) + + // 發生真正的錯誤(非找不到) + if err != nil && !errors.Is(err, repo.ErrNotFound) { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: data}, + {Key: "func", Value: "KYCRepo.FindLatestByUID"}, + {Key: "err", Value: err.Error()}, + }, "failed to get latest kyc") + } + + // 若查到資料,且不是被駁回的,表示已存在審核中/已通過資料 → 禁止再次建立 + if err == nil && latest.Status != kyc.StatusREJECTED { + return errs.ForbiddenL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: data}, + {Key: "func", Value: "KYCRepo.FindLatestByUID"}, + {Key: "reason", Value: "KYC already in progress or approved"}, + }, "不能重複送出 KYC 資料") + } + + // ✅ 若查不到資料(ErrNotFound),或前一筆是 REJECTED,允許建立 + data.Status = kyc.StatusPending + err = use.KYCRepo.Create(ctx, data) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: data}, + {Key: "func", Value: "KYCRepo.Create"}, + {Key: "err", Value: err.Error()}, + }, "failed to create kyc review") + } + + return nil +} + +func (use *KYCUseCase) FindLatestByUID(ctx context.Context, uid string) (*entity.KYC, error) { + latest, err := use.KYCRepo.FindLatestByUID(ctx, uid) + if err != nil { + return nil, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "uid", Value: uid}, + {Key: "func", Value: "KYCRepo.FindLatestByUID"}, + {Key: "err", Value: err.Error()}, + }, "failed to get latest kyc") + } + + return latest, nil +} + +func (use *KYCUseCase) FindByID(ctx context.Context, id string) (*entity.KYC, error) { + byID, err := use.KYCRepo.FindByID(ctx, id) + if err != nil { + return nil, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "KYCRepo.FindByID"}, + {Key: "err", Value: err.Error()}, + }, "failed to get kyc") + } + + return byID, nil +} + +func (use *KYCUseCase) List(ctx context.Context, params usecase.KYCQueryParams) ([]*entity.KYC, int64, error) { + q := repository.KYCQueryParams{ + PageIndex: params.PageIndex, + PageSize: params.PageSize, + SortByDate: true, + } + if params.UID != nil { + q.UID = params.UID + } + if params.Country != nil { + q.Country = params.Country + } + if params.Status != nil { + q.Status = params.Status + } + + list, i, err := use.KYCRepo.List(ctx, q) + if err != nil { + return nil, 0, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "params", Value: params}, + {Key: "func", Value: "KYCRepo.List"}, + {Key: "err", Value: err.Error()}, + }, "failed to list kyc") + } + + return list, i, nil +} + +func (use *KYCUseCase) UpdateStatus(ctx context.Context, id string, status string, reason string) error { + err := use.KYCRepo.UpdateStatus(ctx, id, status, reason) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "params", Value: fmt.Sprintf("id:%s, status:%s, reason: %s", id, status, reason)}, + {Key: "func", Value: "KYCRepo.UpdateStatus"}, + {Key: "err", Value: err.Error()}, + }, "failed to update kyc status") + } + + return nil +} + +func (use *KYCUseCase) UpdateKYCInfo(ctx context.Context, id string, update *usecase.KYCUpdateParams) error { + err := use.KYCRepo.UpdateKYCInfo(ctx, id, &repository.KYCUpdateParams{ + Name: update.Name, + Identification: update.Identification, + IdentificationType: update.IdentificationType, + Address: update.Address, + PostalCode: update.PostalCode, + DateOfBirth: update.DateOfBirth, + Gender: update.Gender, + IDFrontImage: update.IDFrontImage, + IDBackImage: update.IDBackImage, + BankStatementImg: update.BankStatementImg, + BankCode: update.BankCode, + BankName: update.BankName, + BranchCode: update.BranchCode, + BranchName: update.BranchName, + BankAccount: update.BankAccount, + }) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "params", Value: update}, + {Key: "func", Value: "KYCRepo.UpdateKYCInfo"}, + {Key: "err", Value: err.Error()}, + }, "failed to update kyc") + } + + return nil +} diff --git a/pkg/usecase/tags.go b/pkg/usecase/tags.go new file mode 100644 index 0000000..bdb0c38 --- /dev/null +++ b/pkg/usecase/tags.go @@ -0,0 +1,141 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + repo "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/usecase" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/repository" + "code.30cm.net/digimon/library-go/errs" + "context" + "github.com/zeromicro/go-zero/core/logx" +) + +type TagsUseCaseParam struct { + TagsRepo repository.TagsRepository +} + +type TagsUseCase struct { + TagsUseCaseParam +} + +func MustTagsUseCase(param TagsUseCaseParam) usecase.ProductBaseTags { + return &TagsUseCase{ + param, + } +} + +func (use *TagsUseCase) Create(ctx context.Context, data *entity.Tags) error { + err := use.TagsRepo.Create(ctx, data) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "param", Value: data}, + {Key: "func", Value: "TagsRepo.Create"}, + {Key: "err", Value: err.Error()}, + }, "failed to create tags") + } + + return nil +} + +func (use *TagsUseCase) GetByID(ctx context.Context, id string) (*entity.Tags, error) { + tag, err := use.TagsRepo.GetByID(ctx, id) + if err != nil { + return nil, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "TagsRepo.GetByID"}, + {Key: "err", Value: err.Error()}, + }, "failed to get tags") + } + + return tag, nil +} + +func (use *TagsUseCase) GetByIDs(ctx context.Context, ids []string) ([]*entity.Tags, error) { + tags, err := use.TagsRepo.GetByIDs(ctx, ids) + if err != nil { + return nil, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "ids", Value: ids}, + {Key: "func", Value: "TagsRepo.GetByIDs"}, + {Key: "err", Value: err.Error()}, + }, "failed to get tags") + } + + return tags, nil +} + +func (use *TagsUseCase) Delete(ctx context.Context, id string) error { + // TODO 是否要刪除已綁定的Tag 還是在查詢的時候做 + + err := use.TagsRepo.Delete(ctx, id) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "TagsRepo.Delete"}, + {Key: "err", Value: err.Error()}, + }, "failed to delete tags") + } + + return nil +} + +func (use *TagsUseCase) List(ctx context.Context, params usecase.TagQueryParams) ([]*entity.Tags, int64, error) { + q := repo.TagQueryParams{ + PageSize: params.PageSize, + PageIndex: params.PageIndex, + } + if params.Types != nil { + q.Types = params.Types + } + + if params.Name != nil { + q.Name = params.Name + } + if params.ShowType != nil { + q.ShowType = params.ShowType + } + + list, total, err := use.TagsRepo.List(ctx, q) + if err != nil { + return nil, 0, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "params", Value: params}, + {Key: "func", Value: "TagsRepo.List"}, + {Key: "err", Value: err.Error()}, + }, "failed to list tags") + } + + return list, total, nil +} + +func (use *TagsUseCase) Update(ctx context.Context, id string, tag usecase.TagModifyParams) error { + update := repo.TagModifyParams{} + if tag.Name != nil { + update.Name = tag.Name + } + if tag.Types != nil { + update.Types = tag.Types + } + if tag.ShowType != nil { + update.ShowType = tag.ShowType + } + if tag.Cover != nil { + update.Cover = tag.Cover + } + + err := use.TagsRepo.Update(ctx, id, update) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "params", Value: tag}, + {Key: "func", Value: "TagsRepo.Update"}, + {Key: "err", Value: err.Error()}, + }, "failed to update tags") + } + + return nil +}