diff --git a/pkg/domain/product/item_status.go b/pkg/domain/product/item_status.go index e620932..9f13a4b 100644 --- a/pkg/domain/product/item_status.go +++ b/pkg/domain/product/item_status.go @@ -9,15 +9,17 @@ func (p *ItemStatus) ToString() string { } const ( - StatusActive ItemStatus = 1 + iota // 上架 - StatusInactive // 下架 - StatusOutOfStock // 缺貨 + StatusActive ItemStatus = 1 + iota // 上架 + StatusInactive // 下架 + StatusOutOfStock // 缺貨 + StatusUnderReview // 商品審核中 ) const ( - StatusActiveStr = "active" - StatusInactiveStr = "inactive" - StatusOutOfStockStr = "out_of_stock" + StatusActiveStr = "active" + StatusInactiveStr = "inactive" + StatusOutOfStockStr = "out_of_stock" + StatusUnderReviewStr = "under_review" ) var statusToStringMap = map[ItemStatus]string{ @@ -27,9 +29,10 @@ var statusToStringMap = map[ItemStatus]string{ } var stringToStatusMap = map[string]ItemStatus{ - StatusActiveStr: StatusActive, - StatusInactiveStr: StatusInactive, - StatusOutOfStockStr: StatusOutOfStock, + StatusActiveStr: StatusActive, + StatusInactiveStr: StatusInactive, + StatusOutOfStockStr: StatusOutOfStock, + StatusUnderReviewStr: StatusUnderReview, } // StatusToString 將 ProductItemStatus 轉換為字串 diff --git a/pkg/domain/usecase/kyc.go b/pkg/domain/usecase/kyc.go index aed2454..1bba0ff 100644 --- a/pkg/domain/usecase/kyc.go +++ b/pkg/domain/usecase/kyc.go @@ -1 +1,4 @@ package usecase + +type KYCUseCase struct { +} diff --git a/pkg/domain/usecase/product_item.go b/pkg/domain/usecase/product_item.go index aed2454..6dc3c09 100644 --- a/pkg/domain/usecase/product_item.go +++ b/pkg/domain/usecase/product_item.go @@ -1 +1,70 @@ package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product" + "context" +) + +type ProductItemUseCase interface { + // Create 新增單筆商品 + Create(ctx context.Context, productItem *ProductItems) error + // Get 根據 ID 查詢單筆商品 + Get(ctx context.Context, id string) (*ProductItems, error) + // Update 更新商品資訊 + Update(ctx context.Context, id string, productItem *UpdateProductItems) error + // UpdateStatus 更新商品資訊 + UpdateStatus(ctx context.Context, id string, status product.ItemStatus) error + // IncSalesCount 更新商品資訊 + IncSalesCount(ctx context.Context, id string, saleCount uint64) error + // DecSalesCount 更新商品資訊 + DecSalesCount(ctx context.Context, id string, saleCount uint64) error + // Delete ID 刪除商品 + Delete(ctx context.Context, id string) error + // List 列出符合條件的商品,可根據需求加入分頁或其他條件參數 + List(ctx context.Context, filter QueryProductItemParam) ([]*ProductItems, int64, error) +} + +type ProductItems struct { + ID string `json:"_id,omitempty"` // 專案 ID + ReferenceID string `json:"reference_id"` // 對應的專案 ID + Name string `json:"name"` // 名稱 + Description string `json:"description"` // 描述 + ShortDescription string `json:"short_description"` // 封面簡短描述 + IsUnLimit bool `json:"is_un_limit"` // 是否沒有數量上限 + IsFree bool `json:"is_free"` // 是否為免費品項(贈品) -> 開啟就是自訂金額 + Stock uint64 `json:"stock"` // 庫存總數 + Price string `json:"price"` // 價格 + SKU string `json:"sku"` // 型號:對應顯示 Item 的 FK + TimeSeries product.TimeSeries `json:"time_series"` // 時段種類 + Media []Media `json:"media,omitempty"` // 專案動態內容(圖片或者影片) + Status product.ItemStatus `json:"status"` // 商品狀態 + Freight []CustomFields `json:"freight,omitempty"` // 運費 + CustomFields []CustomFields `json:"custom_fields,omitempty"` // 自定義屬性 + SalesCount uint64 `json:"sales_count" ` // 已賣出數量(相反,減到零就不能在賣) + UpdatedAt string `json:"updated_at"` // 更新時間 + CreatedAt string `json:"created_at"` // 創建時間 +} + +type UpdateProductItems struct { + Name *string // 名稱 + Description *string // 描述 + ShortDescription *string // 封面簡短描述 + IsUnLimit *bool // 是否沒有數量上限 + IsFree *bool // 是否為免費品項(贈品) -> 開啟就是自訂金額 + Stock *uint64 // 庫存總數 + Price *string // 價格 + SKU *string // 型號:對應顯示 Item 的 FK + TimeSeries *product.TimeSeries // 時段種類 + Media []Media // 專案動態內容(圖片或者影片) + Freight []CustomFields // 運費 + CustomFields []CustomFields // 自定義屬性 +} + +type QueryProductItemParam struct { + PageSize int64 + PageIndex int64 + ReferenceID *string // 對應的專案 ID + IsUnLimit *bool // 是否沒有數量上限 + IsFree *bool // 是否為免費品項(贈品) -> 開啟就是自訂金額 + Status *product.ItemStatus // 商品狀態 +} diff --git a/pkg/repository/kyc.go b/pkg/repository/kyc.go index 178a8cf..943ef8f 100644 --- a/pkg/repository/kyc.go +++ b/pkg/repository/kyc.go @@ -98,10 +98,22 @@ func (repo *KYCRepository) List(ctx context.Context, params repository.KYCQueryP } var results []*entity.KYC - total, err := repo.DB.GetClient().FindManyWithTotal(ctx, &results, filter, params.PageIndex, params.PageSize, WithSort(sort)) + // 設置排序選項 + opts := options.Find().SetSkip((params.PageIndex - 1) * params.PageSize).SetLimit(params.PageSize) + opts.SetSort(bson.D{{Key: "updated_at", Value: -1}}) + + // 查詢符合條件的總數 + total, err := repo.DB.GetClient().CountDocuments(ctx, filter) if err != nil { return nil, 0, err } + + // 執行查詢並獲取結果 + err = repo.DB.GetClient().Find(ctx, &results, filter, opts) + if err != nil { + return nil, 0, err + } + return results, total, nil } diff --git a/pkg/usecase/kyc.go b/pkg/usecase/kyc.go new file mode 100644 index 0000000..aed2454 --- /dev/null +++ b/pkg/usecase/kyc.go @@ -0,0 +1 @@ +package usecase diff --git a/pkg/usecase/product_item.go b/pkg/usecase/product_item.go new file mode 100644 index 0000000..09f070f --- /dev/null +++ b/pkg/usecase/product_item.go @@ -0,0 +1,315 @@ +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" + "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/utils" + "code.30cm.net/digimon/library-go/errs" + "context" + "github.com/shopspring/decimal" + "github.com/zeromicro/go-zero/core/logx" +) + +type ProductItemUseCaseParam struct { + ProductItems repository.ProductItemRepository + TagRepo repository.TagRepo +} + +type ProductItemUseCase struct { + ProductItemUseCaseParam +} + +func MustProductItemUseCase(param ProductItemUseCaseParam) usecase.ProductItemUseCase { + return &ProductItemUseCase{ + param, + } +} + +// --- 輔助函式 --- + +// 將 usecase.ProductItems 轉換為 entity.ProductItems(用於 Create) +func toEntity(u *usecase.ProductItems) (entity.ProductItems, error) { + // 將價格字串轉換為 decimal.Decimal + priceDec, err := decimal.NewFromString(u.Price) + if err != nil { + return entity.ProductItems{}, err + } + return entity.ProductItems{ + // ID 在新增時通常由資料庫自動產生 + ReferenceID: u.ReferenceID, + Name: u.Name, + Description: u.Description, + ShortDescription: u.ShortDescription, + IsUnLimit: u.IsUnLimit, + IsFree: u.IsFree, + Stock: u.Stock, + Price: priceDec, + SKU: u.SKU, + TimeSeries: u.TimeSeries, + Media: convertMediaToEntity(u.Media), + Status: u.Status, + Freight: convertCustomFieldsToEntity(u.Freight), + CustomFields: convertCustomFieldsToEntity(u.CustomFields), + }, nil +} + +// 將 entity.ProductItems 轉換為 usecase.ProductItems(用於 Get 與 List) +func fromEntity(e *entity.ProductItems) *usecase.ProductItems { + return &usecase.ProductItems{ + ID: e.ID.Hex(), + ReferenceID: e.ReferenceID, + Name: e.Name, + Description: e.Description, + ShortDescription: e.ShortDescription, + IsUnLimit: e.IsUnLimit, + IsFree: e.IsFree, + Stock: e.Stock, + Price: e.Price.String(), + SKU: e.SKU, + TimeSeries: e.TimeSeries, + Media: convertEntityMedia(e.Media), + Status: e.Status, + Freight: convertEntityCustomFields(e.Freight), + CustomFields: convertEntityCustomFields(e.CustomFields), + SalesCount: e.SalesCount, + UpdatedAt: utils.UnixToRfc3339(e.UpdatedAt), + CreatedAt: utils.UnixToRfc3339(e.CreatedAt), + } +} + +// Media 轉換:usecase.Media -> entity.Media +func convertMediaToEntity(meds []usecase.Media) []entity.Media { + res := make([]entity.Media, len(meds)) + for i, m := range meds { + res[i] = entity.Media{ + Sort: m.Sort, + Type: m.Type, + URL: m.URL, + } + } + return res +} + +// Media 轉換:entity.Media -> usecase.Media +func convertEntityMedia(meds []entity.Media) []usecase.Media { + res := make([]usecase.Media, len(meds)) + for i, m := range meds { + res[i] = usecase.Media{ + Sort: m.Sort, + Type: m.Type, + URL: m.URL, + } + } + return res +} + +// CustomFields 轉換:usecase.CustomFields -> entity.CustomFields +func convertCustomFieldsToEntity(cfs []usecase.CustomFields) []entity.CustomFields { + res := make([]entity.CustomFields, len(cfs)) + for i, cf := range cfs { + res[i] = entity.CustomFields{ + Key: cf.Key, + Value: cf.Value, + } + } + return res +} + +// CustomFields 轉換:entity.CustomFields -> usecase.CustomFields +func convertEntityCustomFields(cfs []entity.CustomFields) []usecase.CustomFields { + res := make([]usecase.CustomFields, len(cfs)) + for i, cf := range cfs { + res[i] = usecase.CustomFields{ + Key: cf.Key, + Value: cf.Value, + } + } + return res +} + +// --- 實作 ProductItemUseCase interface --- + +func (use *ProductItemUseCase) Create(ctx context.Context, productItem *usecase.ProductItems) error { + // 轉換成 entity 層型別 + ent, err := toEntity(productItem) + if err != nil { + return errs.InvalidFormatL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "product_id", Value: productItem}, + {Key: "func", Value: "ProductItemUseCase.Create"}, + {Key: "err", Value: err.Error()}, + }, "failed to change use case into entity") + } + + err = use.ProductItemUseCaseParam.ProductItems.Insert(ctx, []entity.ProductItems{ent}) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: productItem}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.Insert"}, + {Key: "err", Value: err.Error()}, + }, "failed to insert product item") + } + + return nil +} + +func (use *ProductItemUseCase) Get(ctx context.Context, id string) (*usecase.ProductItems, error) { + ent, err := use.ProductItemUseCaseParam.ProductItems.FindByID(ctx, id) + if err != nil { + return nil, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.FindByID"}, + {Key: "err", Value: err.Error()}, + }, "failed to create product items") + } + return fromEntity(ent), nil +} + +func (use *ProductItemUseCase) Update(ctx context.Context, id string, data *usecase.UpdateProductItems) error { + // 構建更新參數(僅更新部分欄位) + update := &repository.ProductUpdateItem{} + if data.Name != nil { + update.Name = data.Name + } + if data.Description != nil { + update.Description = data.Description + } + if data.ShortDescription != nil { + update.ShortDescription = data.ShortDescription + } + if data.IsUnLimit != nil { + update.IsUnLimit = data.IsUnLimit + } + if data.IsFree != nil { + update.IsFree = data.IsFree + } + if data.Stock != nil { + // data.Stock 為 *uint64,需先解引用再轉換為 int64 + s := int64(*data.Stock) + update.Stock = &s + } + if data.Price != nil { + // data.Price 為 *string,需先解引用並轉換為 decimal.Decimal + dec, err := decimal.NewFromString(*data.Price) + if err != nil { + return errs.InvalidFormat("failed to convert price to decimal") + } + update.Price = &dec + } + if data.SKU != nil { + update.SKU = data.SKU + } + // 假設有 TimeSeries 欄位也需要更新(若有定義的話) + if data.TimeSeries != nil { + update.TimeSeries = data.TimeSeries + } + if len(data.Media) > 0 { + update.Media = convertMediaToEntity(data.Media) + } + if len(data.CustomFields) > 0 { + update.CustomFields = convertCustomFieldsToEntity(data.CustomFields) + } + if len(data.Freight) > 0 { + update.Freight = convertCustomFieldsToEntity(data.Freight) + } + if err := use.ProductItemUseCaseParam.ProductItems.Update(ctx, id, update); err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "req", Value: data}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.Update"}, + {Key: "err", Value: err.Error()}, + }, "failed to update product item") + } + + return nil +} + +func (use *ProductItemUseCase) UpdateStatus(ctx context.Context, id string, status product.ItemStatus) error { + err := use.ProductItemUseCaseParam.ProductItems.UpdateStatus(ctx, id, status) + if err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "status", Value: status}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.UpdateStatus"}, + {Key: "err", Value: err.Error()}, + }, "failed to update product item status") + } + + return nil +} + +func (use *ProductItemUseCase) IncSalesCount(ctx context.Context, id string, saleCount uint64) error { + // repository 方法使用 int64,故需轉型 + if err := use.ProductItemUseCaseParam.ProductItems.IncSalesCount(ctx, id, int64(saleCount)); err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "saleCount", Value: saleCount}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.IncSalesCount"}, + {Key: "err", Value: err.Error()}, + }, "failed to insert product item") + } + + return nil +} + +func (use *ProductItemUseCase) DecSalesCount(ctx context.Context, id string, saleCount uint64) error { + // repository 方法使用 int64,故需轉型 + if err := use.ProductItemUseCaseParam.ProductItems.DecSalesCount(ctx, id, int64(saleCount)); err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "saleCount", Value: saleCount}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.DecSalesCount"}, + {Key: "err", Value: err.Error()}, + }, "failed to Dec product item") + } + + return nil +} + +func (use *ProductItemUseCase) Delete(ctx context.Context, id string) error { + // repository.Delete 接收 slice,因此將 id 放入 slice 中 + if err := use.ProductItemUseCaseParam.ProductItems.Delete(ctx, []string{id}); err != nil { + return errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "id", Value: id}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.Delete"}, + {Key: "err", Value: err.Error()}, + }, "failed to delete product item") + } + + return nil +} + +func (use *ProductItemUseCase) List(ctx context.Context, filter usecase.QueryProductItemParam) ([]*usecase.ProductItems, int64, error) { + repoQuery := repository.ProductItemQueryParams{ + PageSize: filter.PageSize, + PageIndex: filter.PageIndex, + ReferenceID: filter.ReferenceID, + IsFree: filter.IsFree, + Status: filter.Status, + // 注意:若需要依 IsUnLimit 過濾,需擴充 repository.ProductItemQueryParams 結構 + } + entities, total, err := use.ProductItemUseCaseParam.ProductItems.ListProductItem(ctx, repoQuery) + if err != nil { + return nil, 0, errs.DBErrorL(logx.WithContext(ctx), + []logx.LogField{ + {Key: "filter", Value: filter}, + {Key: "func", Value: "ProductItemUseCaseParam.ProductItems.ListProductItem"}, + {Key: "err", Value: err.Error()}, + }, "failed to list product item") + } + var result []*usecase.ProductItems + for i := range entities { + // 逐筆轉換 entity -> usecase 型別 + result = append(result, fromEntity(&entities[i])) + } + return result, total, nil +}