app-cloudep-product-service/pkg/usecase/product.go

630 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"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"
"context"
"errors"
"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())
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 nil, 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) {
_, 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")
}
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")
}
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 nil, 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) {
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
}
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 nil, 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),
}
}