app-cloudep-product-service/pkg/repository/product_statistics.go

309 lines
8.4 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"math"
"time"
"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"
"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 ProductStatisticsRepositoryParam struct {
Conf *mgo.Conf
CacheConf cache.CacheConf
DBOpts []mon.Option
CacheOpts []cache.Option
}
type ProductStatisticsRepository struct {
DB mgo.DocumentDBWithCacheUseCase
}
func NewProductStatisticsRepository(param ProductStatisticsRepositoryParam) repository.ProductStatisticsRepo {
e := entity.ProductStatistics{}
documentDB, err := mgo.MustDocumentDBWithCache(
param.Conf,
e.CollectionName(),
param.CacheConf,
param.DBOpts,
param.CacheOpts,
)
if err != nil {
panic(err)
}
return &ProductStatisticsRepository{
DB: documentDB,
}
}
func (repo *ProductStatisticsRepository) Create(ctx context.Context, data *entity.ProductStatistics) error {
if data.ID.IsZero() {
now := time.Now().UTC().UnixNano()
data.ID = primitive.NewObjectID()
data.CreatedAt = now
data.UpdatedAt = now
}
rk := domain.GetProductStatisticsRK(data.ID.Hex())
_, err := repo.DB.InsertOne(ctx, rk, data)
productKey := domain.GetProductStatisticsRK(data.ProductID)
_ = repo.DB.SetCache(productKey, data)
return err
}
func (repo *ProductStatisticsRepository) GetByID(ctx context.Context, id string) (*entity.ProductStatistics, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
var result *entity.ProductStatistics
err = repo.DB.FindOne(ctx, domain.GetProductStatisticsRK(id), &result, bson.M{"_id": oid})
switch {
case err == nil:
return result, nil
case errors.Is(err, mon.ErrNotFound):
return nil, ErrNotFound
default:
return nil, err
}
}
func (repo *ProductStatisticsRepository) GetByProductID(ctx context.Context, productID string) (*entity.ProductStatistics, error) {
var result *entity.ProductStatistics
err := repo.DB.FindOne(ctx, domain.GetProductStatisticsRK(productID), &result, bson.M{"product_id": productID})
switch {
case err == nil:
return result, nil
case errors.Is(err, mon.ErrNotFound):
return nil, ErrNotFound
default:
return nil, err
}
}
func (repo *ProductStatisticsRepository) IncOrders(ctx context.Context, productID string, count int64) error {
filter := bson.M{"product_id": productID}
now := time.Now().UTC().UnixNano()
update := bson.M{
"$inc": bson.M{"total_orders": count},
"$set": bson.M{
"total_orders_update_time": now,
"updated_at": now,
},
}
rk := domain.GetProductStatisticsRK(productID)
_, err := repo.DB.UpdateOne(ctx, rk, filter, update)
if err != nil {
return fmt.Errorf("failed to decrease stock: %w", err)
}
id, err := repo.getIDByProductID(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) DecOrders(ctx context.Context, productID string, count int64) error {
filter := bson.M{"product_id": productID, "total_orders": bson.M{"$gte": count}}
now := time.Now().UTC().UnixNano()
update := bson.M{
"$inc": bson.M{"total_orders": -count},
"$set": bson.M{
"total_orders_update_time": now,
"updated_at": now,
},
}
rk := domain.GetProductStatisticsRK(productID)
_, err := repo.DB.UpdateOne(ctx, rk, filter, update)
if err != nil {
return fmt.Errorf("failed to decrease stock: %w", err)
}
id, err := repo.getIDByProductID(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) UpdateAverageRating(ctx context.Context, productID string, averageRating float64) error {
filter := bson.M{"product_id": productID}
now := time.Now().UTC().UnixNano()
update := bson.M{
"$set": bson.M{
"average_rating": averageRating,
"average_rating_time": now,
"updated_at": now,
},
}
_, err := repo.DB.UpdateOne(ctx, domain.GetProductStatisticsRK(productID), filter, update)
if err != nil {
return err
}
id, err := repo.getIDByProductID(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) IncFansCount(ctx context.Context, productID string, fansCount uint64) error {
filter := bson.M{"product_id": productID}
now := time.Now().UTC().UnixNano()
update := bson.M{
"$inc": bson.M{"fans_count": fansCount},
"$set": bson.M{
"fans_count_update_time": now,
"updated_at": now,
},
}
rk := domain.GetProductStatisticsRK(productID)
_, err := repo.DB.UpdateOne(ctx, rk, filter, update)
if err != nil {
return fmt.Errorf("failed to increment fans count: %w", err)
}
id, err := repo.getIDByProductID(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) DecFansCount(ctx context.Context, productID string, fansCount uint64) error {
// 只允許在 fans_count 大於或等於欲扣減值時進行扣減
filter := bson.M{"product_id": productID, "fans_count": bson.M{"$gte": fansCount}}
now := time.Now().UTC().UnixNano()
// 在更新之前先檢查 fansCount 是否過大
if fansCount > uint64(math.MaxInt64) {
return fmt.Errorf("fansCount value too large: %d", fansCount)
}
delta := -int64(fansCount)
update := bson.M{
"$inc": bson.M{"fans_count": delta},
"$set": bson.M{
"fans_count_update_time": now,
"updated_at": now,
},
}
rk := domain.GetProductStatisticsRK(productID)
_, err := repo.DB.UpdateOne(ctx, rk, filter, update)
if err != nil {
return fmt.Errorf("failed to decrement fans count: %w", err)
}
id, err := repo.getIDByProductID(ctx, productID)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) Delete(ctx context.Context, id string) error {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return ErrInvalidObjectID
}
productID, err := repo.getProductIDByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to get product_id by id: %w", err)
}
filter := bson.M{"_id": oid}
_, err = repo.DB.DeleteOne(ctx, domain.GetProductStatisticsRK(id), filter)
if err != nil {
return err
}
repo.clearCache(ctx, id, productID)
return nil
}
func (repo *ProductStatisticsRepository) Index20250317001UP(ctx context.Context) (*mongo.Cursor, error) {
// 等價於 db.account.createIndex({"product_id": 1})
repo.DB.PopulateIndex(ctx, "product_id", 1, true)
return repo.DB.GetClient().Indexes().List(ctx)
}
// 快取輔助函數
// clearCache 同時刪除 product_id 與 _id 兩個 cache key
func (repo *ProductStatisticsRepository) clearCache(ctx context.Context, id, productID string) {
keys := []string{
domain.GetProductStatisticsRK(productID),
domain.GetProductStatisticsRK(id),
}
for _, key := range keys {
_ = repo.DB.DelCache(ctx, key)
}
}
func (repo *ProductStatisticsRepository) getIDByProductID(ctx context.Context, productID string) (string, error) {
filter := bson.M{"product_id": productID}
var e entity.ProductStatistics
projection := bson.M{"_id": 1}
opts := options.FindOne().SetProjection(projection)
err := repo.DB.GetClient().FindOne(ctx, &e, filter, opts)
if err != nil {
return "", fmt.Errorf("failed to set projection: %w", err)
}
return e.ID.Hex(), nil
}
func (repo *ProductStatisticsRepository) getProductIDByID(ctx context.Context, id string) (string, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return "", err
}
filter := bson.M{"_id": oid}
var e entity.ProductStatistics
projection := bson.M{"product_id": 1}
opts := options.FindOne().SetProjection(projection)
err = repo.DB.GetClient().FindOne(ctx, &e, filter, opts)
if err != nil {
return "", fmt.Errorf("failed to set projection: %w", err)
}
return e.ProductID, nil
}