From 39841cc168addbedf75708a0a71e9070f18d3cfc 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 15:39:49 +0800 Subject: [PATCH] feat: add category repository --- Makefile | 2 + pkg/domain/entity/category.go | 17 ++ pkg/domain/kyc/status.go | 4 + pkg/domain/redis.go | 5 + pkg/domain/repository/category.go | 25 +++ pkg/domain/usecase/category.go | 25 +++ pkg/mock/repository/category.go | 133 ++++++++++++++ pkg/mock/repository/kyc.go | 131 ++++++++++++++ pkg/repository/category.go | 191 ++++++++++++++++++++ pkg/repository/category_test.go | 282 ++++++++++++++++++++++++++++++ pkg/repository/kyc_test.go | 23 +-- 11 files changed, 827 insertions(+), 11 deletions(-) create mode 100644 pkg/domain/entity/category.go create mode 100755 pkg/domain/repository/category.go create mode 100755 pkg/domain/usecase/category.go create mode 100644 pkg/mock/repository/category.go create mode 100644 pkg/mock/repository/kyc.go create mode 100644 pkg/repository/category.go create mode 100644 pkg/repository/category_test.go diff --git a/Makefile b/Makefile index 402200e..b9fe018 100644 --- a/Makefile +++ b/Makefile @@ -39,4 +39,6 @@ mock-gen: # 建立 mock 資料 mockgen -source=./pkg/domain/repository/product_item.go -destination=./pkg/mock/repository/product_item.go -package=mock mockgen -source=./pkg/domain/repository/product_statistics.go -destination=./pkg/mock/repository/product_statistics.go -package=mock mockgen -source=./pkg/domain/repository/tags.go -destination=./pkg/mock/repository/tags.go -package=mock + mockgen -source=./pkg/domain/repository/kyc.go -destination=./pkg/mock/repository/kyc.go -package=mock + mockgen -source=./pkg/domain/repository/category.go -destination=./pkg/mock/repository/category.go -package=mock @echo "Generate mock files successfully" diff --git a/pkg/domain/entity/category.go b/pkg/domain/entity/category.go new file mode 100644 index 0000000..33fe613 --- /dev/null +++ b/pkg/domain/entity/category.go @@ -0,0 +1,17 @@ +package entity + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Category 類別以後獨立服務 +type Category struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Name string `bson:"name"` // 服務類別 + UpdatedAt int64 `bson:"updated_at"` // 更新時間 + CreatedAt int64 `bson:"created_at"` // 創建時間 +} + +func (c *Category) CollectionName() string { + return "category" +} diff --git a/pkg/domain/kyc/status.go b/pkg/domain/kyc/status.go index ba00c9f..8e002ce 100644 --- a/pkg/domain/kyc/status.go +++ b/pkg/domain/kyc/status.go @@ -2,6 +2,10 @@ package kyc type Status string +func (s *Status) ToString() string { + return string(*s) +} + const ( StatusPending Status = "PENDING" StatusAPPROVED Status = "APPROVED" diff --git a/pkg/domain/redis.go b/pkg/domain/redis.go index a8ff107..f467b8d 100644 --- a/pkg/domain/redis.go +++ b/pkg/domain/redis.go @@ -19,6 +19,7 @@ const ( GetProductItemRedisKey RedisKey = "get_item" GetProductStatisticsRedisKey RedisKey = "statistics" GetTagsRedisKey RedisKey = "tags" + CategoryRedisKey RedisKey = "category" ) func GetProductRK(id string) string { @@ -36,3 +37,7 @@ func GetProductStatisticsRK(id string) string { func GetTagsRK(id string) string { return GetTagsRedisKey.With(id).ToString() } + +func GetCategoryRedisKey(id string) string { + return CategoryRedisKey.With(id).ToString() +} diff --git a/pkg/domain/repository/category.go b/pkg/domain/repository/category.go new file mode 100755 index 0000000..e76c41e --- /dev/null +++ b/pkg/domain/repository/category.go @@ -0,0 +1,25 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +// CategoryRepository 操作更多信息 +type CategoryRepository interface { + Insert(ctx context.Context, data *entity.Category) error + FindOneByID(ctx context.Context, id string) (*entity.Category, error) + Update(ctx context.Context, id string, data *entity.Category) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) + ListCategory(ctx context.Context, params *CategoryQueryParams) ([]*entity.Category, int64, error) + // IsCategoryExists 丟進去存在的再拿出來 + IsCategoryExists(ctx context.Context, ids []string) []string +} + +type CategoryQueryParams struct { + PageSize int64 // 每頁顯示的專案數量 + PageIndex int64 // 要查詢的頁數 + ID []string +} diff --git a/pkg/domain/usecase/category.go b/pkg/domain/usecase/category.go new file mode 100755 index 0000000..d5cc366 --- /dev/null +++ b/pkg/domain/usecase/category.go @@ -0,0 +1,25 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "context" +) + +type CategoryUseCase interface { + // Insert 創建新的類別 + Insert(ctx context.Context, data *entity.Category) error + // FindOneByID 根據 ID 查詢單個類別 + FindOneByID(ctx context.Context, id string) (*entity.Category, error) + // Update 更新類別信息 + Update(ctx context.Context, id string, data *entity.Category) error + // Delete 刪除指定 ID 的類別 + Delete(ctx context.Context, id string) error + // ListCategory 根據查詢參數獲取類別列表 + ListCategory(ctx context.Context, params CategoryQueryParams) ([]*entity.Category, int64, error) +} + +type CategoryQueryParams struct { + PageSize int64 // 每頁顯示的專案數量 + PageIndex int64 // 要查詢的頁數 + ID []string +} diff --git a/pkg/mock/repository/category.go b/pkg/mock/repository/category.go new file mode 100644 index 0000000..00074c1 --- /dev/null +++ b/pkg/mock/repository/category.go @@ -0,0 +1,133 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/category.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/category.go -destination=./pkg/mock/repository/category.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + repository "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockCategoryRepository is a mock of CategoryRepository interface. +type MockCategoryRepository struct { + ctrl *gomock.Controller + recorder *MockCategoryRepositoryMockRecorder + isgomock struct{} +} + +// MockCategoryRepositoryMockRecorder is the mock recorder for MockCategoryRepository. +type MockCategoryRepositoryMockRecorder struct { + mock *MockCategoryRepository +} + +// NewMockCategoryRepository creates a new mock instance. +func NewMockCategoryRepository(ctrl *gomock.Controller) *MockCategoryRepository { + mock := &MockCategoryRepository{ctrl: ctrl} + mock.recorder = &MockCategoryRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCategoryRepository) EXPECT() *MockCategoryRepositoryMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockCategoryRepository) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockCategoryRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCategoryRepository)(nil).Delete), ctx, id) +} + +// FindOneByID mocks base method. +func (m *MockCategoryRepository) FindOneByID(ctx context.Context, id string) (*entity.Category, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneByID", ctx, id) + ret0, _ := ret[0].(*entity.Category) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneByID indicates an expected call of FindOneByID. +func (mr *MockCategoryRepositoryMockRecorder) FindOneByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByID", reflect.TypeOf((*MockCategoryRepository)(nil).FindOneByID), ctx, id) +} + +// Insert mocks base method. +func (m *MockCategoryRepository) Insert(ctx context.Context, data *entity.Category) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockCategoryRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockCategoryRepository)(nil).Insert), ctx, data) +} + +// IsCategoryExists mocks base method. +func (m *MockCategoryRepository) IsCategoryExists(ctx context.Context, ids []string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsCategoryExists", ctx, ids) + ret0, _ := ret[0].([]string) + return ret0 +} + +// IsCategoryExists indicates an expected call of IsCategoryExists. +func (mr *MockCategoryRepositoryMockRecorder) IsCategoryExists(ctx, ids any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCategoryExists", reflect.TypeOf((*MockCategoryRepository)(nil).IsCategoryExists), ctx, ids) +} + +// ListCategory mocks base method. +func (m *MockCategoryRepository) ListCategory(ctx context.Context, params *repository.CategoryQueryParams) ([]*entity.Category, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCategory", ctx, params) + ret0, _ := ret[0].([]*entity.Category) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListCategory indicates an expected call of ListCategory. +func (mr *MockCategoryRepositoryMockRecorder) ListCategory(ctx, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCategory", reflect.TypeOf((*MockCategoryRepository)(nil).ListCategory), ctx, params) +} + +// Update mocks base method. +func (m *MockCategoryRepository) Update(ctx context.Context, id string, data *entity.Category) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, id, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockCategoryRepositoryMockRecorder) Update(ctx, id, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCategoryRepository)(nil).Update), ctx, id, data) +} diff --git a/pkg/mock/repository/kyc.go b/pkg/mock/repository/kyc.go new file mode 100644 index 0000000..eb4f31d --- /dev/null +++ b/pkg/mock/repository/kyc.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/domain/repository/kyc.go +// +// Generated by this command: +// +// mockgen -source=./pkg/domain/repository/kyc.go -destination=./pkg/mock/repository/kyc.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + repository "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + gomock "go.uber.org/mock/gomock" +) + +// MockKYCRepository is a mock of KYCRepository interface. +type MockKYCRepository struct { + ctrl *gomock.Controller + recorder *MockKYCRepositoryMockRecorder + isgomock struct{} +} + +// MockKYCRepositoryMockRecorder is the mock recorder for MockKYCRepository. +type MockKYCRepositoryMockRecorder struct { + mock *MockKYCRepository +} + +// NewMockKYCRepository creates a new mock instance. +func NewMockKYCRepository(ctrl *gomock.Controller) *MockKYCRepository { + mock := &MockKYCRepository{ctrl: ctrl} + mock.recorder = &MockKYCRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKYCRepository) EXPECT() *MockKYCRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockKYCRepository) Create(ctx context.Context, kyc *entity.KYC) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, kyc) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockKYCRepositoryMockRecorder) Create(ctx, kyc any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockKYCRepository)(nil).Create), ctx, kyc) +} + +// FindByID mocks base method. +func (m *MockKYCRepository) FindByID(ctx context.Context, id string) (*entity.KYC, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByID", ctx, id) + ret0, _ := ret[0].(*entity.KYC) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByID indicates an expected call of FindByID. +func (mr *MockKYCRepositoryMockRecorder) FindByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockKYCRepository)(nil).FindByID), ctx, id) +} + +// FindLatestByUID mocks base method. +func (m *MockKYCRepository) FindLatestByUID(ctx context.Context, uid string) (*entity.KYC, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindLatestByUID", ctx, uid) + ret0, _ := ret[0].(*entity.KYC) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindLatestByUID indicates an expected call of FindLatestByUID. +func (mr *MockKYCRepositoryMockRecorder) FindLatestByUID(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindLatestByUID", reflect.TypeOf((*MockKYCRepository)(nil).FindLatestByUID), ctx, uid) +} + +// List mocks base method. +func (m *MockKYCRepository) List(ctx context.Context, params repository.KYCQueryParams) ([]*entity.KYC, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, params) + ret0, _ := ret[0].([]*entity.KYC) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// List indicates an expected call of List. +func (mr *MockKYCRepositoryMockRecorder) List(ctx, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockKYCRepository)(nil).List), ctx, params) +} + +// UpdateKYCInfo mocks base method. +func (m *MockKYCRepository) UpdateKYCInfo(ctx context.Context, id string, update *repository.KYCUpdateParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateKYCInfo", ctx, id, update) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateKYCInfo indicates an expected call of UpdateKYCInfo. +func (mr *MockKYCRepositoryMockRecorder) UpdateKYCInfo(ctx, id, update any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKYCInfo", reflect.TypeOf((*MockKYCRepository)(nil).UpdateKYCInfo), ctx, id, update) +} + +// UpdateStatus mocks base method. +func (m *MockKYCRepository) UpdateStatus(ctx context.Context, id, status, reason string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStatus", ctx, id, status, reason) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateStatus indicates an expected call of UpdateStatus. +func (mr *MockKYCRepositoryMockRecorder) UpdateStatus(ctx, id, status, reason any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockKYCRepository)(nil).UpdateStatus), ctx, id, status, reason) +} diff --git a/pkg/repository/category.go b/pkg/repository/category.go new file mode 100644 index 0000000..b133301 --- /dev/null +++ b/pkg/repository/category.go @@ -0,0 +1,191 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "errors" + + "time" + + "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" +) + +type CategoryRepositoryParam struct { + Conf *mgo.Conf + CacheConf cache.CacheConf + DBOpts []mon.Option + CacheOpts []cache.Option +} + +type CategoryRepository struct { + DB mgo.DocumentDBWithCacheUseCase +} + +func MustCategoryRepository(param CategoryRepositoryParam) repository.CategoryRepository { + e := entity.Category{} + + documentDB, err := mgo.MustDocumentDBWithCache( + param.Conf, + e.CollectionName(), + param.CacheConf, + param.DBOpts, + param.CacheOpts, + ) + if err != nil { + panic(err) + } + + return &CategoryRepository{ + DB: documentDB, + } +} + +func (repo *CategoryRepository) IsCategoryExists(ctx context.Context, ids []string) []string { + // 將傳入的 string id 轉換為 ObjectID + objectIDs := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + objID, err := primitive.ObjectIDFromHex(id) + if err == nil { // 如果轉換失敗,忽略該 id + objectIDs = append(objectIDs, objID) + } + } + + // 查詢存在的標籤 + filter := bson.M{"_id": bson.M{"$in": objectIDs}} + // find 並沒有快取要快取要去其他地方做 + opts := options.Find().SetProjection(bson.M{"_id": 1}) // 只返回 _id 欄位 + // 執行查詢並獲取結果 + var category []*entity.Category + err := repo.DB.GetClient().Find(ctx, &category, filter, opts) + if err != nil { + return []string{} + } + + // 將存在的標籤ID轉換回 string,並加入結果列表 + existingIDs := make([]string, 0, len(category)) + for _, item := range category { + existingIDs = append(existingIDs, item.ID.Hex()) + } + + return existingIDs +} + +func (repo *CategoryRepository) Insert(ctx context.Context, data *entity.Category) error { + now := time.Now().UTC().UnixNano() + if data.ID.IsZero() { + data.ID = primitive.NewObjectID() + data.CreatedAt = now + data.UpdatedAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, data) + + return err +} + +func (repo *CategoryRepository) FindOneByID(ctx context.Context, id string) (*entity.Category, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var data entity.Category + rk := domain.GetCategoryRedisKey(id) + err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid}) + switch { + case err == nil: + return &data, nil + case errors.Is(err, mon.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +func (repo *CategoryRepository) Update(ctx context.Context, id string, data *entity.Category) (*mongo.UpdateResult, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + updateFields := bson.M{} + if data.Name != "" { + updateFields["name"] = data.Name + } + + updateFields["updated_at"] = time.Now().UTC().UnixNano() + + // 構建查找條件 + filter := bson.M{"_id": oid} + update := bson.M{"$set": updateFields} + opt := options.Update().SetUpsert(false) + + rk := domain.GetCategoryRedisKey(id) + res, err := repo.DB.UpdateOne(ctx, rk, filter, update, opt) + if err != nil { + return nil, err + } + + // 檢查更新結果,若沒有匹配的文檔,則返回錯誤 + if res.MatchedCount == 0 { + return nil, ErrNotFound // 自定義的錯誤表示未找到記錄 + } + + return res, err +} + +func (repo *CategoryRepository) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectID + } + rk := domain.GetCategoryRedisKey(id) + res, err := repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid}) + + return res, err +} + +func (repo *CategoryRepository) ListCategory(ctx context.Context, params *repository.CategoryQueryParams) ([]*entity.Category, int64, error) { + // TODO 有需要列表快取實在取列表快取 + // 構建查詢過濾器 + filter := bson.M{} + if len(params.ID) > 0 { + objectIDs := make([]primitive.ObjectID, 0, len(params.ID)) + for _, id := range params.ID { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + continue + } + objectIDs = append(objectIDs, objID) + } + filter["_id"] = bson.M{"$in": objectIDs} + } + + // 設置排序選項 + opts := options.Find().SetSkip((params.PageIndex - 1) * params.PageSize).SetLimit(params.PageSize) + opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) + + // 查詢符合條件的總數 + count, err := repo.DB.GetClient().CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + // 執行查詢並獲取結果 + var category []*entity.Category + err = repo.DB.GetClient().Find(ctx, &category, filter, opts) + if err != nil { + return nil, 0, err + } + + return category, count, nil +} diff --git a/pkg/repository/category_test.go b/pkg/repository/category_test.go new file mode 100644 index 0000000..cdbaf14 --- /dev/null +++ b/pkg/repository/category_test.go @@ -0,0 +1,282 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "github.com/zeromicro/go-zero/core/stores/redis" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" + "time" +) + +func SetupTestCategoryRepository(db string) (repository.CategoryRepository, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + s, _ := miniredis.Run() + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + cacheConf := cache.CacheConf{ + cache.NodeConf{ + RedisConf: redis.RedisConf{ + Host: s.Addr(), + Type: redis.NodeType, + }, + Weight: 100, + }, + } + + cacheOpts := []cache.Option{ + cache.WithExpiry(1000 * time.Microsecond), + cache.WithNotFoundExpiry(1000 * time.Microsecond), + } + + param := CategoryRepositoryParam{ + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, + DBOpts: []mon.Option{ + mgo.SetCustomDecimalType(), + mgo.InitMongoOptions(*conf), + }, + } + + repo := MustCategoryRepository(param) + //_, _ = repo.Index20250317001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestCategoryRepository_Insert(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + testData := &entity.Category{ + Name: "Test Category", + } + + err = repo.Insert(ctx, testData) + assert.NoError(t, err, "插入操作應該成功") + assert.NotZero(t, testData.ID, "ID 應該被生成") + assert.NotNil(t, testData.CreatedAt, "CreateAt 應該被設置") + assert.NotNil(t, testData.UpdatedAt, "UpdateAt 應該被設置") +} + +func TestCategoryRepository_FindOneByID(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + testData := &entity.Category{ + Name: "Test Category Find", + } + _ = repo.Insert(ctx, testData) + + tests := []struct { + name string + id string + expectError bool + }{ + { + name: "Valid ID - Find Record", + id: testData.ID.Hex(), + expectError: false, + }, + { + name: "Invalid ID Format", + id: "invalid_id", + expectError: true, + }, + { + name: "Nonexistent ID", + id: primitive.NewObjectID().Hex(), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := repo.FindOneByID(ctx, tt.id) + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, testData.Name, result.Name) + } + }) + } +} + +func TestCategoryRepository_Update(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + testData := &entity.Category{ + Name: "Test Category Update", + } + _ = repo.Insert(ctx, testData) + updatedData := &entity.Category{ + ID: testData.ID, + Name: "Updated Category Name", + } + result, err := repo.Update(ctx, testData.ID.Hex(), updatedData) + assert.NoError(t, err, "更新操作應該成功") + assert.Equal(t, int64(1), result.ModifiedCount, "更新的文件數量應為 1") + + // 檢查更新後的數據 + updatedRecord, err := repo.FindOneByID(ctx, testData.ID.Hex()) + assert.NoError(t, err, "應該可以找到更新的記錄") + assert.Equal(t, "Updated Category Name", updatedRecord.Name, "Category 名稱應該被更新") +} + +func TestCategoryRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + testData := &entity.Category{ + Name: "Test Category Delete", + } + _ = repo.Insert(ctx, testData) + + tests := []struct { + name string + id string + expectError bool + expectCount int64 + }{ + { + name: "Valid ID - Successful Deletion", + id: testData.ID.Hex(), + expectError: false, + expectCount: 1, + }, + { + name: "Invalid ID Format", + id: "invalid_id", + expectError: true, + expectCount: 0, + }, + { + name: "Nonexistent ID", + id: primitive.NewObjectID().Hex(), + expectError: false, + expectCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count, err := repo.Delete(ctx, tt.id) + if tt.expectError { + assert.Error(t, err) + assert.Equal(t, int64(0), count) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectCount, count) + } + }) + } +} + +func TestCategoryRepository_ListProduct(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + // 插入測試數據 + for i := 0; i < 5; i++ { + _ = repo.Insert(ctx, &entity.Category{ + Name: "Test Category List", + }) + } + + params := &repository.CategoryQueryParams{ + PageSize: 2, + PageIndex: 1, + } + + categories, count, err := repo.ListCategory(ctx, params) + assert.NoError(t, err, "查詢應該成功") + assert.Equal(t, int64(5), count, "總數應該為 5") + assert.Len(t, categories, int(params.PageSize), "返回的數據應符合頁面大小") +} + +func TestCategoryRepository_IsCategoryExists(t *testing.T) { + repo, tearDown, err := SetupTestCategoryRepository("testDB") + defer tearDown() + assert.NoError(t, err) + + ctx := context.Background() + + // 插入一些測試分類數據 + category1 := &entity.Category{ID: primitive.NewObjectID()} + category2 := &entity.Category{ID: primitive.NewObjectID()} + category3 := &entity.Category{ID: primitive.NewObjectID()} + _ = repo.Insert(ctx, category1) + _ = repo.Insert(ctx, category2) + + // 定義測試案例 + tests := []struct { + name string + ids []string + expectedIDs []string + }{ + { + name: "部分分類存在", + ids: []string{category1.ID.Hex(), category2.ID.Hex(), category3.ID.Hex()}, // category3 不存在 + expectedIDs: []string{category1.ID.Hex(), category2.ID.Hex()}, + }, + { + name: "所有分類存在", + ids: []string{category1.ID.Hex(), category2.ID.Hex()}, + expectedIDs: []string{category1.ID.Hex(), category2.ID.Hex()}, + }, + { + name: "無分類存在", + ids: []string{category3.ID.Hex()}, // category3 不存在 + expectedIDs: []string{}, + }, + { + name: "包含無效 ID", + ids: []string{category1.ID.Hex(), "invalid_id"}, // "invalid_id" 是無效 ID 格式 + expectedIDs: []string{category1.ID.Hex()}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := repo.IsCategoryExists(ctx, tt.ids) + + assert.ElementsMatch(t, tt.expectedIDs, result, "返回的存在ID應該與期望值匹配") + }) + } +} diff --git a/pkg/repository/kyc_test.go b/pkg/repository/kyc_test.go index 4a07939..04554af 100644 --- a/pkg/repository/kyc_test.go +++ b/pkg/repository/kyc_test.go @@ -2,6 +2,7 @@ package repository 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" mgo "code.30cm.net/digimon/library-go/mongo" "context" @@ -317,43 +318,43 @@ func TestKYCRepository_UpdateStatus(t *testing.T) { ctx := context.Background() // 建立一筆 KYC 資料 - kyc := &entity.KYC{ + k := &entity.KYC{ UID: "user-status", Name: "測試用戶", - Status: "PENDING", + Status: kyc.StatusPending, CreatedAt: time.Now().UnixNano(), UpdatedAt: time.Now().UnixNano(), } - err = model.Create(ctx, kyc) + err = model.Create(ctx, k) assert.NoError(t, err) tests := []struct { name string id string - newStatus string + newStatus kyc.Status reason string expectErr bool - expectStatus string + expectStatus kyc.Status }{ { name: "成功更新為 REJECTED", - id: kyc.ID.Hex(), - newStatus: "REJECTED", + id: k.ID.Hex(), + newStatus: kyc.StatusREJECTED, reason: "文件不符", expectErr: false, - expectStatus: "REJECTED", + expectStatus: kyc.StatusREJECTED, }, { name: "失敗 - 無效 ID 格式", id: "not-a-valid-id", - newStatus: "APPROVED", + newStatus: kyc.StatusAPPROVED, reason: "", expectErr: true, }, { name: "失敗 - 找不到資料", id: primitive.NewObjectID().Hex(), // 合法但不存在 - newStatus: "APPROVED", + newStatus: kyc.StatusAPPROVED, reason: "", expectErr: false, // Mongo 不會報錯,但更新筆數是 0,後面可以補強這邊驗證 }, @@ -361,7 +362,7 @@ func TestKYCRepository_UpdateStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := model.UpdateStatus(ctx, tt.id, tt.newStatus, tt.reason) + err := model.UpdateStatus(ctx, tt.id, tt.newStatus.ToString(), tt.reason) if tt.expectErr { assert.Error(t, err) } else {