2025-03-19 11:28:07 +00:00
|
|
|
package repository
|
|
|
|
|
|
|
|
import (
|
2025-03-20 10:44:01 +00:00
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"time"
|
|
|
|
|
2025-03-19 11:28:07 +00:00
|
|
|
"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}
|
2025-03-31 13:24:40 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
}
|
2025-03-19 11:28:07 +00:00
|
|
|
|
|
|
|
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}}
|
2025-03-31 13:24:40 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
}
|
2025-03-19 11:28:07 +00:00
|
|
|
|
|
|
|
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}
|
2025-03-31 13:24:40 +00:00
|
|
|
now := time.Now().UTC().UnixNano()
|
2025-03-19 11:28:07 +00:00
|
|
|
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}
|
2025-03-31 13:24:40 +00:00
|
|
|
now := time.Now().UTC().UnixNano()
|
2025-03-19 11:28:07 +00:00
|
|
|
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}}
|
2025-03-31 13:24:40 +00:00
|
|
|
now := time.Now().UTC().UnixNano()
|
2025-03-20 10:44:01 +00:00
|
|
|
|
|
|
|
// 在更新之前先檢查 fansCount 是否過大
|
|
|
|
if fansCount > uint64(math.MaxInt64) {
|
|
|
|
return fmt.Errorf("fansCount value too large: %d", fansCount)
|
|
|
|
}
|
|
|
|
delta := -int64(fansCount)
|
2025-03-19 11:28:07 +00:00
|
|
|
update := bson.M{
|
2025-03-20 10:44:01 +00:00
|
|
|
"$inc": bson.M{"fans_count": delta},
|
2025-03-19 11:28:07 +00:00
|
|
|
"$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
|
|
|
|
}
|