package usecase import ( "context" "errors" "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" "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/app-cloudep-product-service/pkg/utils" "code.30cm.net/digimon/library-go/errs" "github.com/zeromicro/go-zero/core/logx" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readconcern" ) type ProductUseCaseParam struct { ProductRepo repository.ProductRepository TagRepo repository.TagRepo TagBinding repository.TagBindingRepo ProductStatisticsRepo repository.ProductStatisticsRepo } type ProductUseCase struct { ProductUseCaseParam } func MustProductUseCase(param ProductUseCaseParam) usecase.ProductUseCase { return &ProductUseCase{ param, } } func (use *ProductUseCase) Create(ctx context.Context, product *usecase.Product) error { insert := convertUseCaseToEntity(product) // Transaction 設定:只做必要寫入 opts := options.Transaction().SetReadConcern(readconcern.Local()) //nolint:contextcheck err := use.ProductRepo.Transaction(ctx, func(sessCtx mongo.SessionContext) (any, error) { if err := use.ProductRepo.Insert(sessCtx, insert); err != nil { return nil, logDBError(ctx, "ProductRepo.Insert", []logx.LogField{{Key: "req", Value: product}}, err, "failed to create product") } if err := use.ProductStatisticsRepo.Create(sessCtx, &entity.ProductStatistics{ ProductID: insert.ID.Hex(), Orders: 0, OrdersUpdateTime: 0, AverageRating: 0, AverageRatingUpdateTime: 0, FansCount: 0, FansCountUpdateTime: 0, }); err != nil { return nil, logDBError(ctx, "ProductStatisticsRepo.Create", []logx.LogField{{Key: "ProductID", Value: insert.ID.Hex()}}, err, "failed to create product statistics") } tagsBinding := buildTagsBinding(insert.ID.Hex(), product.Tags) if err := use.TagBinding.BindTags(sessCtx, tagsBinding); err != nil { return nil, logDBError(ctx, "TagBinding.BindTags", []logx.LogField{{Key: "ReferenceID", Value: insert.ID.Hex()}, {Key: "tags", Value: product.Tags}}, err, "failed to bind product tags") } return struct{}{}, nil }, opts) if err != nil { return err } return nil } func (use *ProductUseCase) Update(ctx context.Context, id string, product *usecase.Product) error { update := convertUseCaseToUpdateParams(product) tagsBinding := buildTagsBinding(id, product.Tags) opts := options.Transaction().SetReadConcern(readconcern.Local()) err := use.ProductRepo.Transaction(ctx, func(sessCtx mongo.SessionContext) (any, error) { //nolint:contextcheck _, err := use.ProductRepo.Update(sessCtx, id, update) if err != nil { return nil, logDBError(ctx, "ProductRepo.Update", []logx.LogField{{Key: "req", Value: product}}, err, "failed to update product") } //nolint:contextcheck if err := use.TagBinding.UnbindTagByReferenceID(sessCtx, id); err != nil { return nil, logDBError(ctx, "TagBinding.UnbindTagByReferenceID", []logx.LogField{{Key: "ReferenceID", Value: id}}, err, "failed to unbind tags") } //nolint:contextcheck if err := use.TagBinding.BindTags(sessCtx, tagsBinding); err != nil { return nil, logDBError(ctx, "TagBinding.BindTags", []logx.LogField{{Key: "ReferenceID", Value: id}, {Key: "tags", Value: product.Tags}}, err, "failed to bind tags") } return struct{}{}, nil }, opts) return err } func (use *ProductUseCase) Delete(ctx context.Context, id string) error { // Transaction 設定:只做必要寫入 opts := options.Transaction().SetReadConcern(readconcern.Local()) err := use.ProductRepo.Transaction(ctx, func(sessCtx mongo.SessionContext) (any, error) { //nolint:contextcheck err := use.ProductRepo.Delete(sessCtx, id) if err != nil { e := errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "ReferenceID", Value: id}, {Key: "func", Value: "ProductRepo.Delete"}, {Key: "err", Value: err.Error()}, }, "failed to delete product") return nil, e } //nolint:contextcheck err = use.TagRepo.UnbindTagByReferenceID(sessCtx, id) if err != nil { e := errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "ReferenceID", Value: id}, {Key: "func", Value: "TagBinding.UnbindTagByReferenceID"}, {Key: "err", Value: err.Error()}, }, "failed to unbind tags") return nil, e } return struct{}{}, nil }, opts) if err != nil { return err } return nil } func (use *ProductUseCase) Get(ctx context.Context, id string) (*usecase.ProductResp, error) { product, err := use.ProductRepo.FindOneByID(ctx, id) if err != nil { return nil, logDBError(ctx, "ProductRepo.FindOneByID", []logx.LogField{{Key: "product_id", Value: id}}, err, "failed to find product") } stats, err := use.ProductStatisticsRepo.GetByID(ctx, id) if err != nil { return nil, logDBError(ctx, "ProductStatisticsRepo.GetByID", []logx.LogField{{Key: "product_id", Value: id}}, err, "failed to get product statistics") } bindings, err := use.TagRepo.GetBindingsByReference(ctx, id) if err != nil { return nil, logDBError(ctx, "TagRepo.GetBindingsByReference", []logx.LogField{{Key: "product_id", Value: id}}, err, "failed to get tag bindings") } tagIDs := make([]string, 0, len(bindings)) for _, item := range bindings { tagIDs = append(tagIDs, item.TagID) } tags, err := use.TagRepo.GetByIDs(ctx, tagIDs) if err != nil { return nil, logDBError(ctx, "TagRepo.GetByIDs", []logx.LogField{{Key: "product_id", Value: id}}, err, "failed to get tags") } return convertEntityToResp(product, stats, tags), nil } // TODO 效能有問題這邊優先改,List 會有N + 1 問題 func (use *ProductUseCase) List(ctx context.Context, data usecase.ProductQueryParams) ([]*usecase.ProductResp, int64, error) { query := &repository.ProductQueryParams{ PageSize: data.PageSize, PageIndex: data.PageIndex, } if data.Slug != nil { query.Slug = data.Slug } if data.UID != nil { query.UID = data.UID } if data.IsPublished != nil { query.IsPublished = data.IsPublished } if data.Category != nil { query.Category = data.Category } if data.StartTime != nil { query.StartTime = data.StartTime } if data.EndTime != nil { query.EndTime = data.EndTime } products, total, err := use.ProductRepo.ListProduct(ctx, query) if err != nil { return nil, 0, err } result := make([]*usecase.ProductResp, 0, len(products)) for _, p := range products { // 查詢統計資料 stats, _ := use.ProductStatisticsRepo.GetByID(ctx, p.ID.Hex()) // 查詢 Tag 資訊 tags, _ := use.TagRepo.GetBindingsByReference(ctx, p.ID.Hex()) tagIDs := make([]string, 0, len(tags)) for _, t := range tags { tagIDs = append(tagIDs, t.TagID) } tagsInfo, _ := use.TagRepo.GetByIDs(ctx, tagIDs) // 組合 Tags 回應 respTags := make([]usecase.Tags, 0, len(tagsInfo)) for _, tag := range tagsInfo { item := usecase.Tags{ ID: tag.ID.Hex(), Types: tag.Types, Name: tag.Name, ShowType: tag.ShowType, UpdatedAt: utils.UnixToRfc3339(tag.UpdatedAt), CreatedAt: utils.UnixToRfc3339(tag.CreatedAt), } if tag.Cover != nil { item.Cover = *tag.Cover } respTags = append(respTags, item) } // Media & CustomFields media := make([]usecase.Media, 0, len(p.Media)) for _, m := range p.Media { media = append(media, usecase.Media{ Sort: m.Sort, URL: m.URL, Type: m.Type, }) } customFields := make([]usecase.CustomFields, 0, len(p.CustomFields)) for _, f := range p.CustomFields { customFields = append(customFields, usecase.CustomFields{ Key: f.Key, Value: f.Value, }) } resp := &usecase.ProductResp{ ID: p.ID.Hex(), UID: p.UID, Title: p.Title, ShortTitle: utils.ToValue(p.ShortTitle), Details: utils.ToValue(p.Details), ShortDescription: p.ShortDescription, Media: media, Slug: utils.ToValue(p.Slug), IsPublished: p.IsPublished, Amount: p.Amount, StartTime: utils.UnixToRfc3339(utils.ToValue(p.StartTime)), EndTime: utils.UnixToRfc3339(utils.ToValue(p.EndTime)), Category: p.Category, CustomFields: customFields, Tags: respTags, Orders: stats.Orders, OrdersUpdateTime: utils.UnixToRfc3339(stats.OrdersUpdateTime), AverageRating: stats.AverageRating, AverageRatingUpdateTime: utils.UnixToRfc3339(stats.AverageRatingUpdateTime), FansCount: stats.FansCount, FansCountUpdateTime: utils.UnixToRfc3339(stats.FansCountUpdateTime), UpdatedAt: utils.UnixToRfc3339(p.UpdatedAt), CreatedAt: utils.UnixToRfc3339(p.CreatedAt), } result = append(result, resp) } return result, total, nil } func (use *ProductUseCase) IncOrders(ctx context.Context, productID string, count int64) error { err := use.ProductStatisticsRepo.IncOrders(ctx, productID, count) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "product_id", Value: productID}, {Key: "func", Value: "ProductStatisticsRepo.IncOrders"}, {Key: "err", Value: err.Error()}, }, "failed to inc order") } return nil } func (use *ProductUseCase) DecOrders(ctx context.Context, productID string, count int64) error { err := use.ProductStatisticsRepo.DecOrders(ctx, productID, count) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "product_id", Value: productID}, {Key: "func", Value: "ProductStatisticsRepo.DecOrders"}, {Key: "err", Value: err.Error()}, }, "failed to dec order") } return nil } func (use *ProductUseCase) UpdateAverageRating(ctx context.Context, productID string, averageRating float64) error { err := use.ProductStatisticsRepo.UpdateAverageRating(ctx, productID, averageRating) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "product_id", Value: productID}, {Key: "func", Value: "ProductStatisticsRepo.UpdateAverageRating"}, {Key: "err", Value: err.Error()}, }, "failed to update average rating") } return nil } func (use *ProductUseCase) IncFansCount(ctx context.Context, productID string, fansCount uint64) error { err := use.ProductStatisticsRepo.IncFansCount(ctx, productID, fansCount) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "product_id", Value: productID}, {Key: "func", Value: "ProductStatisticsRepo.IncFansCount"}, {Key: "err", Value: err.Error()}, }, "failed to inc fans count") } return nil } func (use *ProductUseCase) DecFansCount(ctx context.Context, productID string, fansCount uint64) error { err := use.ProductStatisticsRepo.DecFansCount(ctx, productID, fansCount) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "product_id", Value: productID}, {Key: "func", Value: "ProductStatisticsRepo.DecFansCount"}, {Key: "err", Value: err.Error()}, }, "failed to dec fans count") } return nil } func (use *ProductUseCase) BindTag(ctx context.Context, binding usecase.TagsBindingTable) error { _, err := use.TagRepo.GetByID(ctx, binding.TagID) if err != nil { if errors.Is(err, repo.ErrNotFound) { return errs.ResourceNotFound("failed to get tags") } return err } _, err = use.ProductRepo.FindOneByID(ctx, binding.ReferenceID) if err != nil { if errors.Is(err, repo.ErrNotFound) { return errs.ResourceNotFound("failed to get tags") } return err } err = use.TagBinding.BindTags(ctx, []*entity.TagsBindingTable{{ReferenceID: binding.ReferenceID, TagID: binding.TagID}}) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "binding", Value: binding}, {Key: "func", Value: "TagBinding.BindTags"}, {Key: "err", Value: err.Error()}, }, "failed to bind tags") } return nil } func (use *ProductUseCase) UnbindTag(ctx context.Context, binding usecase.TagsBindingTable) error { _, err := use.TagRepo.GetByID(ctx, binding.TagID) if err != nil { if errors.Is(err, repo.ErrNotFound) { return errs.ResourceNotFound("failed to get tags") } return err } _, err = use.ProductRepo.FindOneByID(ctx, binding.ReferenceID) if err != nil { if errors.Is(err, repo.ErrNotFound) { return errs.ResourceNotFound("failed to get tags") } return err } err = use.TagBinding.UnbindTag(ctx, binding.TagID, binding.ReferenceID) if err != nil { return errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "binding", Value: binding}, {Key: "func", Value: "TagBinding.UnbindTag"}, {Key: "err", Value: err.Error()}, }, "failed to unbind tags") } return nil } func (use *ProductUseCase) GetBindingsByReference(ctx context.Context, referenceID string) ([]usecase.TagsBindingTableResp, error) { ref, err := use.TagBinding.GetBindingsByReference(ctx, referenceID) if err != nil { return nil, errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "referenceID", Value: referenceID}, {Key: "func", Value: "TagBinding.GetBindingsByReference"}, {Key: "err", Value: err.Error()}, }, "failed to get bindings by reference") } result := make([]usecase.TagsBindingTableResp, 0, len(ref)) for _, bind := range ref { result = append(result, usecase.TagsBindingTableResp{ ID: bind.ID.Hex(), ReferenceID: bind.ReferenceID, TagID: bind.TagID, CreatedAt: utils.UnixToRfc3339(bind.CreatedAt), UpdatedAt: utils.UnixToRfc3339(bind.UpdatedAt), }) } return result, nil } func (use *ProductUseCase) ListTagBinding(ctx context.Context, params usecase.TagBindingQueryParams) ([]usecase.TagsBindingTableResp, int64, error) { ref, total, err := use.TagBinding.ListTagBinding(ctx, repository.TagBindingQueryParams{ ReferenceID: params.ReferenceID, TagID: params.TagID, PageIndex: params.PageIndex, PageSize: params.PageSize, }) if err != nil { return nil, 0, errs.DBErrorL(logx.WithContext(ctx), []logx.LogField{ {Key: "params", Value: params}, {Key: "func", Value: "TagBinding.ListTagBinding"}, {Key: "err", Value: err.Error()}, }, "failed to list tags") } result := make([]usecase.TagsBindingTableResp, 0, len(ref)) for _, bind := range ref { result = append(result, usecase.TagsBindingTableResp{ ID: bind.ID.Hex(), ReferenceID: bind.ReferenceID, TagID: bind.TagID, CreatedAt: utils.UnixToRfc3339(bind.CreatedAt), UpdatedAt: utils.UnixToRfc3339(bind.UpdatedAt), }) } return result, total, nil } func logDBError(ctx context.Context, funcName string, fields []logx.LogField, err error, msg string) error { return errs.DBErrorL(logx.WithContext(ctx), append(fields, logx.Field("func", funcName), logx.Field("err", err.Error())), msg) } func buildTagsBinding(referenceID string, tags []string) []*entity.TagsBindingTable { binding := make([]*entity.TagsBindingTable, 0, len(tags)) for _, tag := range tags { binding = append(binding, &entity.TagsBindingTable{ ReferenceID: referenceID, TagID: tag, }) } return binding } func convertUseCaseToEntity(product *usecase.Product) *entity.Product { insert := &entity.Product{} if product.UID != nil { insert.UID = *product.UID } if product.Title != nil { insert.Title = *product.Title } if product.IsPublished != nil { insert.IsPublished = *product.IsPublished } if product.Category != nil { insert.Category = *product.Category } if product.ShortTitle != nil { insert.ShortTitle = product.ShortTitle } if product.Details != nil { insert.Details = product.Details } if product.ShortDescription != nil { insert.ShortDescription = *product.ShortDescription } if product.Slug != nil { insert.Slug = product.Slug } if product.Amount != 0 { insert.Amount = product.Amount } if product.StartTime != nil { st := utils.Rfc3339ToUnix(utils.ToValue(product.StartTime)) insert.StartTime = &st } if product.EndTime != nil { et := utils.Rfc3339ToUnix(utils.ToValue(product.EndTime)) insert.EndTime = &et } if len(product.Media) > 0 { medias := make([]entity.Media, 0, len(product.Media)) for _, m := range product.Media { medias = append(medias, entity.Media{Sort: m.Sort, URL: m.URL, Type: m.Type}) } insert.Media = medias } if len(product.CustomFields) > 0 { cf := make([]entity.CustomFields, 0, len(product.CustomFields)) for _, field := range product.CustomFields { cf = append(cf, entity.CustomFields{Key: field.Key, Value: field.Value}) } insert.CustomFields = cf } return insert } func convertUseCaseToUpdateParams(product *usecase.Product) *repository.ProductUpdateParams { update := &repository.ProductUpdateParams{} if product.Title != nil { update.Title = product.Title } if product.IsPublished != nil { update.IsPublished = product.IsPublished } if product.Category != nil { update.Category = product.Category } if product.ShortTitle != nil { update.ShortTitle = product.ShortTitle } if product.Details != nil { update.Details = product.Details } if product.ShortDescription != nil { update.ShortDescription = *product.ShortDescription } if product.Slug != nil { update.Slug = product.Slug } if product.Amount != 0 { update.Amount = &product.Amount } if product.StartTime != nil { st := utils.Rfc3339ToUnix(utils.ToValue(product.StartTime)) update.StartTime = &st } if product.EndTime != nil { et := utils.Rfc3339ToUnix(utils.ToValue(product.EndTime)) update.EndTime = &et } if len(product.Media) > 0 { medias := make([]entity.Media, 0, len(product.Media)) for _, m := range product.Media { medias = append(medias, entity.Media{Sort: m.Sort, URL: m.URL, Type: m.Type}) } update.Media = medias } if len(product.CustomFields) > 0 { cf := make([]entity.CustomFields, 0, len(product.CustomFields)) for _, field := range product.CustomFields { cf = append(cf, entity.CustomFields{Key: field.Key, Value: field.Value}) } update.CustomFields = cf } return update } func convertEntityToResp(p *entity.Product, stats *entity.ProductStatistics, tags []*entity.Tags) *usecase.ProductResp { tagsResp := make([]usecase.Tags, 0, len(tags)) for _, tag := range tags { item := usecase.Tags{ ID: tag.ID.Hex(), Types: tag.Types, Name: tag.Name, ShowType: tag.ShowType, UpdatedAt: utils.UnixToRfc3339(tag.UpdatedAt), CreatedAt: utils.UnixToRfc3339(tag.CreatedAt), } if tag.Cover != nil { item.Cover = *tag.Cover } tagsResp = append(tagsResp, item) } media := make([]usecase.Media, 0, len(p.Media)) for _, m := range p.Media { media = append(media, usecase.Media{Sort: m.Sort, URL: m.URL, Type: m.Type}) } cf := make([]usecase.CustomFields, 0, len(p.CustomFields)) for _, field := range p.CustomFields { cf = append(cf, usecase.CustomFields{Key: field.Key, Value: field.Value}) } return &usecase.ProductResp{ ID: p.ID.Hex(), UID: p.UID, Title: p.Title, ShortTitle: utils.ToValue(p.ShortTitle), Details: utils.ToValue(p.Details), ShortDescription: p.ShortDescription, Slug: utils.ToValue(p.Slug), IsPublished: p.IsPublished, Amount: p.Amount, StartTime: utils.UnixToRfc3339(utils.ToValue(p.StartTime)), EndTime: utils.UnixToRfc3339(utils.ToValue(p.EndTime)), Category: p.Category, Media: media, CustomFields: cf, Tags: tagsResp, Orders: stats.Orders, OrdersUpdateTime: utils.UnixToRfc3339(stats.OrdersUpdateTime), AverageRating: stats.AverageRating, AverageRatingUpdateTime: utils.UnixToRfc3339(stats.AverageRatingUpdateTime), FansCount: stats.FansCount, FansCountUpdateTime: utils.UnixToRfc3339(stats.FansCountUpdateTime), UpdatedAt: utils.UnixToRfc3339(p.UpdatedAt), CreatedAt: utils.UnixToRfc3339(p.CreatedAt), } }