feat: add tags

This commit is contained in:
王性驊 2025-04-04 14:39:01 +08:00
parent 6f790d65b3
commit cb7e9fb5bb
7 changed files with 422 additions and 14 deletions

View File

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

9
pkg/domain/kyc/status.go Normal file
View File

@ -0,0 +1,9 @@
package kyc
type Status string
const (
StatusPending Status = "PENDING"
StatusAPPROVED Status = "APPROVED"
StatusREJECTED Status = "REJECTED"
)

View File

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

View File

@ -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"` // 封面圖片
}

View File

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

View File

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

141
pkg/usecase/tags.go Normal file
View File

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