Browse Source

Merge pull request #259 from EasyAbp/batch-discount-products

Batch discount products to improve performance
pull/261/head
Super 3 years ago
committed by GitHub
parent
commit
196712c413
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductSkuDto.cs
  2. 109
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  3. 116
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs
  4. 46
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/GetProductsRealTimePriceContext.cs
  5. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsForSku.cs
  6. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsForSku.cs
  7. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProductView.cs
  8. 34
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs
  9. 31
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductRealTimePriceInfoModel.cs
  10. 4
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductViewDiscountModels.cs
  11. 21
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs
  12. 10
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductPriceProvider.cs
  13. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountProvider.cs
  14. 4
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs
  15. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs
  16. 5
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductPriceProvider.cs
  17. 29
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductAndSkuDataModel.cs
  18. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs
  19. 34
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs
  20. 16
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  21. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductView.cs
  22. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs
  23. 87
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs
  24. 2
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductData.cs
  25. 6
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore/EasyAbp/EShop/Plugins/Baskets/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs
  26. 4
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/Dtos/DiscountProductInputDto.cs
  27. 4
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/Dtos/DiscountProductOutputDto.cs
  28. 2
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/IPromotionIntegrationService.cs
  29. 24
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application/EasyAbp/EShop/Plugins/Promotions/Promotions/PromotionIntegrationService.cs
  30. 3
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/IPromotionHandler.cs
  31. 13
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/MinQuantityOrderDiscount/MinQuantityOrderDiscountPromotionHandler.cs
  32. 3
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/PromotionHandlerBase.cs
  33. 9
      plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/SimpleProductDiscount/SimpleProductDiscountPromotionHandler.cs
  34. 23
      plugins/Promotions/src/EasyAbp.EShop.Products.Plugins.Promotions.Domain/EasyAbp/EShop/Products/Plugins/Promotions/PromotionProductDiscountProvider.cs
  35. 21
      plugins/Promotions/test/EasyAbp.EShop.Plugins.Promotions.Application.Tests/PromotionTypes/MinQuantityOrderDiscountTests.cs
  36. 23
      plugins/Promotions/test/EasyAbp.EShop.Plugins.Promotions.Application.Tests/PromotionTypes/SimpleProductDiscountTests.cs

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductSkuDto.cs

@ -5,7 +5,7 @@ using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Products.Products.Dtos
{
[Serializable]
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku, IHasFullDiscountsForProduct
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku, IHasFullDiscountsForSku
{
public List<Guid> AttributeOptionIds { get; set; }

109
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs

@ -82,8 +82,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
@ -116,8 +118,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
@ -201,19 +205,21 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
return dto;
}
protected virtual Task LoadDtosProductGroupDisplayNameAsync(IEnumerable<ProductDto> dtos)
protected virtual Task LoadDtosProductGroupDisplayNameAsync(List<(Product, ProductDto)> items)
{
var dict = _options.Groups.GetConfigurationsDictionary();
foreach (var dto in dtos)
foreach (var (_, productDto) in items)
{
dto.ProductGroupDisplayName = dict[dto.ProductGroupName].DisplayName;
productDto.ProductGroupDisplayName = dict[productDto.ProductGroupName].DisplayName;
}
return Task.CompletedTask;
@ -232,8 +238,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
return dto;
}
@ -258,23 +266,21 @@ namespace EasyAbp.EShop.Products.Products
var products = await AsyncExecuter.ToListAsync(query);
var now = Clock.Now;
var items = new List<ProductDto>();
var items = new List<(Product, ProductDto)>();
foreach (var product in products)
{
var productDto = await MapToGetListOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, productDto, now);
items.Add(productDto);
items.Add(new ValueTuple<Product, ProductDto>(
product, await MapToGetListOutputDtoAsync(product)));
}
await LoadDtosExtraDataAsync(items, now);
await LoadDtosProductGroupDisplayNameAsync(items);
return new PagedResultDto<ProductDto>(totalCount, items);
return new PagedResultDto<ProductDto>(totalCount, items.Select(x => x.Item2).ToList());
}
protected virtual async Task<ProductDto> LoadDtoInventoryDataAsync(Product product, ProductDto productDto)
protected virtual async Task LoadDtoInventoryDataAsync(Product product, ProductDto productDto)
{
var models = product.ProductSkus.Select(x =>
new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, x.Id)).ToList();
@ -293,41 +299,46 @@ namespace EasyAbp.EShop.Products.Products
productSkuDto.Sold = inventoryData.Sold;
productDto.Sold += productSkuDto.Sold;
}
return productDto;
}
protected virtual async Task<ProductDto> LoadDtoExtraDataAsync(Product product, ProductDto productDto,
DateTime now)
protected virtual async Task LoadDtosExtraDataAsync(List<(Product, ProductDto)> items, DateTime now)
{
await LoadDtoInventoryDataAsync(product, productDto);
await LoadDtoPriceDataAsync(product, productDto, now);
foreach (var (product, productDto) in items)
{
await LoadDtoInventoryDataAsync(product, productDto);
}
return productDto;
await LoadDtosPriceDataAsync(items, now);
}
protected virtual async Task<ProductDto> LoadDtoPriceDataAsync(Product product, ProductDto productDto,
DateTime now)
protected virtual async Task LoadDtosPriceDataAsync(List<(Product, ProductDto)> items, DateTime now)
{
foreach (var productSku in product.ProductSkus)
var context =
await _productManager.GetRealTimePricesAsync(
ProductAndSkuDataModel.CreateByProducts(items.Select(x => x.Item1)).ToList(), now);
foreach (var (product, productDto) in items)
{
var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id);
foreach (var productSku in product.ProductSkus)
{
var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id);
var realTimePriceInfoModel = await _productManager.GetRealTimePriceAsync(product, productSku, now);
var realTimePriceInfoModel = context.GetRealTimePrice(productSku);
productSkuDto.PriceWithoutDiscount = realTimePriceInfoModel.PriceWithoutDiscount;
productSkuDto.Price = realTimePriceInfoModel.TotalDiscountedPrice;
productSkuDto.ProductDiscounts = realTimePriceInfoModel.Discounts.ProductDiscounts;
productSkuDto.OrderDiscountPreviews = realTimePriceInfoModel.Discounts.OrderDiscountPreviews;
}
productSkuDto.PriceWithoutDiscount = realTimePriceInfoModel.PriceWithoutDiscount;
productSkuDto.Price = realTimePriceInfoModel.TotalDiscountedPrice;
productSkuDto.ProductDiscounts = realTimePriceInfoModel.ProductDiscounts;
productSkuDto.OrderDiscountPreviews = realTimePriceInfoModel.OrderDiscountPreviews;
}
if (productDto.ProductSkus.Count <= 0)
{
continue;
}
if (productDto.ProductSkus.Count > 0)
{
productDto.MinimumPrice = productDto.ProductSkus.Min(sku => sku.Price);
productDto.MaximumPrice = productDto.ProductSkus.Max(sku => sku.Price);
}
return productDto;
}
public override async Task DeleteAsync(Guid id)
@ -367,8 +378,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
@ -392,8 +405,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
@ -414,8 +429,10 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
var items = new List<(Product, ProductDto)> { new(product, dto) };
await LoadDtosExtraDataAsync(items, Clock.Now);
await LoadDtosProductGroupDisplayNameAsync(items);
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });

116
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs

@ -123,23 +123,23 @@ namespace EasyAbp.EShop.Products.Products
await _repository.DeleteAsync(x => x.StoreId == storeId, true);
var productViews = new List<ProductView>();
var productViews = new Dictionary<Product, ProductView>();
foreach (var product in products)
{
var productView = ObjectMapper.Map<Product, ProductView>(product);
productView.ProductDiscounts ??= new List<ProductDiscountInfoModel>();
productView.OrderDiscountPreviews ??= new List<OrderDiscountPreviewInfoModel>();
await FillPriceInfoWithRealPriceAsync(product, productView, now);
productViews.Add(await _repository.InsertAsync(productView));
productViews[product] = productView;
}
await FillPriceInfoWithRealPriceAsync(productViews, now);
await _repository.InsertManyAsync(productViews.Values, true);
await uow.SaveChangesAsync();
await uow.CompleteAsync();
var duration = await GetCacheDurationOrNullAsync(productViews, now);
var duration = await GetCacheDurationOrNullAsync(productViews.Values.ToList(), now);
if (duration.HasValue)
{
@ -151,7 +151,7 @@ namespace EasyAbp.EShop.Products.Products
}
}
protected async Task<TimeSpan?> GetCacheDurationOrNullAsync(List<ProductView> productViews,
protected virtual async Task<TimeSpan?> GetCacheDurationOrNullAsync(List<ProductView> productViews,
DateTime now)
{
// refresh the cache when a new discount takes effect or an old discount ends.
@ -191,75 +191,87 @@ namespace EasyAbp.EShop.Products.Products
return duration.HasValue && duration.Value < defaultDuration ? duration : defaultDuration;
}
protected virtual async Task FillPriceInfoWithRealPriceAsync(Product product, ProductView productView,
protected virtual async Task FillPriceInfoWithRealPriceAsync(Dictionary<Product, ProductView> productViews,
DateTime now)
{
if (product.ProductSkus.IsNullOrEmpty())
var models = new List<ProductAndSkuDataModel>();
foreach (var product in productViews.Keys)
{
return;
models.AddRange(ProductAndSkuDataModel.CreateByProduct(product));
}
decimal? min = null, max = null;
decimal? minWithoutDiscount = null, maxWithoutDiscount = null;
var context = await _productManager.GetRealTimePricesAsync(models, now);
var discounts = new DiscountForProductModels();
foreach (var productSku in product.ProductSkus)
foreach (var (product, productView) in productViews)
{
var overrideProductDiscounts = false;
var realTimePrice = await _productManager.GetRealTimePriceAsync(product, productSku, now);
var discountedPrice = realTimePrice.TotalDiscountedPrice;
if (min is null || discountedPrice < min.Value)
if (product.ProductSkus.IsNullOrEmpty())
{
min = discountedPrice;
overrideProductDiscounts = true;
return;
}
if (max is null || discountedPrice > max.Value)
{
max = discountedPrice;
}
decimal? min = null, max = null;
decimal? minWithoutDiscount = null, maxWithoutDiscount = null;
if (minWithoutDiscount is null || realTimePrice.PriceWithoutDiscount < minWithoutDiscount.Value)
{
minWithoutDiscount = realTimePrice.PriceWithoutDiscount;
}
var discounts = new ProductViewDiscountModels();
if (maxWithoutDiscount is null || realTimePrice.PriceWithoutDiscount > maxWithoutDiscount.Value)
foreach (var productSku in product.ProductSkus)
{
maxWithoutDiscount = realTimePrice.PriceWithoutDiscount;
}
var overrideProductDiscounts = false;
var realTimePrice = context.GetRealTimePrice(productSku);
var discountedPrice = realTimePrice.TotalDiscountedPrice;
foreach (var discount in realTimePrice.Discounts.ProductDiscounts)
{
var existingDiscount =
discounts.ProductDiscounts.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (min is null || discountedPrice < min.Value)
{
min = discountedPrice;
overrideProductDiscounts = true;
}
if (existingDiscount is null)
if (max is null || discountedPrice > max.Value)
{
discounts.ProductDiscounts.Add(discount);
max = discountedPrice;
}
else if (overrideProductDiscounts)
if (minWithoutDiscount is null || realTimePrice.PriceWithoutDiscount < minWithoutDiscount.Value)
{
discounts.ProductDiscounts.ReplaceOne(existingDiscount, discount);
minWithoutDiscount = realTimePrice.PriceWithoutDiscount;
}
}
foreach (var discount in realTimePrice.Discounts.OrderDiscountPreviews)
{
var existingDiscount =
discounts.OrderDiscountPreviews.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (maxWithoutDiscount is null || realTimePrice.PriceWithoutDiscount > maxWithoutDiscount.Value)
{
maxWithoutDiscount = realTimePrice.PriceWithoutDiscount;
}
if (existingDiscount is null)
foreach (var discount in realTimePrice.ProductDiscounts)
{
discounts.OrderDiscountPreviews.Add(discount);
var existingDiscount =
discounts.ProductDiscounts.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (existingDiscount is null)
{
discounts.ProductDiscounts.Add(discount);
}
else if (overrideProductDiscounts)
{
discounts.ProductDiscounts.ReplaceOne(existingDiscount, discount);
}
}
foreach (var discount in realTimePrice.OrderDiscountPreviews)
{
var existingDiscount =
discounts.OrderDiscountPreviews.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (existingDiscount is null)
{
discounts.OrderDiscountPreviews.Add(discount);
}
}
}
}
productView.SetPrices(min, max, minWithoutDiscount, maxWithoutDiscount);
productView.SetDiscounts(discounts);
productView.SetPrices(min, max, minWithoutDiscount, maxWithoutDiscount);
productView.SetDiscounts(discounts);
}
}
}
}

46
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/GetProductsRealTimePriceContext.cs

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp;
namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class GetProductsRealTimePriceContext
{
public DateTime Now { get; }
/// <summary>
/// ProductId to IProduct mapping.
/// </summary>
public Dictionary<Guid, IProduct> Products { get; }
/// <summary>
/// ProductSkuId to ProductRealTimePriceInfoModel mapping.
/// </summary>
public Dictionary<Guid, ProductRealTimePriceInfoModel> Models { get; }
public GetProductsRealTimePriceContext(DateTime now, IEnumerable<IProduct> products,
IEnumerable<ProductRealTimePriceInfoModel> models)
{
Now = now;
Products = Check.NotNull(products, nameof(products)).ToDictionary(x => x.Id);
Models = Check.NotNull(models, nameof(models)).ToDictionary(x => x.ProductSkuId);
}
/// <summary>
/// Ctor for serializers.
/// </summary>
public GetProductsRealTimePriceContext(DateTime now, Dictionary<Guid, IProduct> products,
Dictionary<Guid, ProductRealTimePriceInfoModel> models)
{
Now = now;
Products = products;
Models = models;
}
public ProductRealTimePriceInfoModel GetRealTimePrice(IProductSku productSku)
{
return Models[productSku.Id];
}
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsForProduct.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsForSku.cs

@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public interface IHasDiscountsForProduct
public interface IHasDiscountsForSku
{
/// <summary>
/// The Price of the ProductSku has been subtracted from these product discounts.

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsForProduct.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsForSku.cs

@ -1,6 +1,6 @@
namespace EasyAbp.EShop.Products.Products;
public interface IHasFullDiscountsForProduct : IHasDiscountsForProduct
public interface IHasFullDiscountsForSku : IHasDiscountsForSku
{
/// <summary>
/// The realtime price without subtracting the discount amount.

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProductView.cs

@ -1,6 +1,6 @@
namespace EasyAbp.EShop.Products.Products;
public interface IProductView : IProductBase, IHasDiscountsForProduct, IHasProductGroupDisplayName
public interface IProductView : IProductBase, IHasDiscountsForSku, IHasProductGroupDisplayName
{
decimal? MinimumPrice { get; }

34
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using Volo.Abp;
namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class ProductDiscountContext
{
public DateTime Now { get; }
public decimal PriceFromPriceProvider { get; }
public IProduct Product { get; }
public IProductSku ProductSku { get; }
public List<CandidateProductDiscountInfoModel> CandidateProductDiscounts { get; }
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; }
public ProductDiscountContext(DateTime now, IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, List<CandidateProductDiscountInfoModel> candidateProductDiscounts = null,
List<OrderDiscountPreviewInfoModel> orderDiscountPreviews = null)
{
Now = now;
Product = Check.NotNull(product, nameof(product));
ProductSku = Check.NotNull(productSku, nameof(productSku));
PriceFromPriceProvider = priceFromPriceProvider;
CandidateProductDiscounts = candidateProductDiscounts ?? new List<CandidateProductDiscountInfoModel>();
OrderDiscountPreviews = orderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
}
}

31
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductRealTimePriceInfoModel.cs

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class ProductRealTimePriceInfoModel : IHasDiscountsForSku
{
public Guid ProductId { get; }
public Guid ProductSkuId { get; }
public decimal PriceWithoutDiscount { get; }
public List<CandidateProductDiscountInfoModel> CandidateProductDiscounts { get; } = new();
public List<ProductDiscountInfoModel> ProductDiscounts { get; } = new();
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; } = new();
public decimal TotalDiscountAmount => ProductDiscounts.Where(x => x.InEffect).Sum(x => x.DiscountedAmount);
public decimal TotalDiscountedPrice => PriceWithoutDiscount - TotalDiscountAmount;
public ProductRealTimePriceInfoModel(Guid productId, Guid productSkuId, decimal priceWithoutDiscount)
{
ProductId = productId;
ProductSkuId = productSkuId;
PriceWithoutDiscount = priceWithoutDiscount;
}
}

4
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductViewDiscountModels.cs

@ -2,13 +2,13 @@ using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public class DiscountForProductModels : IHasDiscountsForProduct
public class ProductViewDiscountModels : IHasDiscountsForSku
{
public List<ProductDiscountInfoModel> ProductDiscounts { get; set; }
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; }
public DiscountForProductModels(List<ProductDiscountInfoModel> productDiscounts = null,
public ProductViewDiscountModels(List<ProductDiscountInfoModel> productDiscounts = null,
List<OrderDiscountPreviewInfoModel> orderDiscountPreviews = null)
{
ProductDiscounts = productDiscounts ?? new List<ProductDiscountInfoModel>();

21
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs

@ -1,21 +0,0 @@
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class RealTimePriceInfoModel
{
public decimal PriceWithoutDiscount { get; }
public DiscountForProductModels Discounts { get; }
public decimal TotalDiscountAmount =>
Discounts.ProductDiscounts.Where(x => x.InEffect).Sum(x => x.DiscountedAmount);
public decimal TotalDiscountedPrice => PriceWithoutDiscount - TotalDiscountAmount;
public RealTimePriceInfoModel(decimal priceWithoutDiscount, DiscountForProductModels discounts)
{
PriceWithoutDiscount = priceWithoutDiscount;
Discounts = discounts;
}
}

10
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductPriceProvider.cs

@ -1,13 +1,17 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Products.Products
{
public class DefaultProductPriceProvider : IProductPriceProvider, ITransientDependency
{
public virtual Task<decimal> GetPriceAsync(IProduct product, IProductSku productSku)
public virtual Task<List<ProductRealTimePriceInfoModel>> GetPricesAsync(
IEnumerable<ProductAndSkuDataModel> models)
{
return Task.FromResult(productSku.Price);
return Task.FromResult(models.Select(x =>
new ProductRealTimePriceInfoModel(x.Product.Id, x.ProductSku.Id, x.ProductSku.Price)).ToList());
}
}
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountProvider.cs

@ -6,5 +6,5 @@ public interface IProductDiscountProvider
{
int EffectOrder { get; }
Task DiscountAsync(ProductDiscountContext context);
Task DiscountAsync(GetProductsRealTimePriceContext context);
}

4
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs

@ -1,10 +1,8 @@
using System;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Products.Products;
public interface IProductDiscountResolver
{
Task<DiscountForProductModels> ResolveAsync(IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, DateTime now);
Task ResolveAsync(GetProductsRealTimePriceContext context);
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs

@ -30,6 +30,6 @@ namespace EasyAbp.EShop.Products.Products
Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold);
Task<RealTimePriceInfoModel> GetRealTimePriceAsync(Product product, ProductSku productSku, DateTime now);
Task<GetProductsRealTimePriceContext> GetRealTimePricesAsync(List<ProductAndSkuDataModel> models, DateTime now);
}
}

5
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductPriceProvider.cs

@ -1,9 +1,10 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Products.Products
{
public interface IProductPriceProvider
{
Task<decimal> GetPriceAsync(IProduct product, IProductSku productSku);
Task<List<ProductRealTimePriceInfoModel>> GetPricesAsync(IEnumerable<ProductAndSkuDataModel> models);
}
}

29
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductAndSkuDataModel.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class ProductAndSkuDataModel
{
public Product Product { get; }
public ProductSku ProductSku { get; }
public ProductAndSkuDataModel(Product product, ProductSku productSku)
{
Product = product;
ProductSku = productSku;
}
public static IEnumerable<ProductAndSkuDataModel> CreateByProduct(Product product)
{
return product.ProductSkus.Select(sku => new ProductAndSkuDataModel(product, sku));
}
public static IEnumerable<ProductAndSkuDataModel> CreateByProducts(IEnumerable<Product> products)
{
return from product in products
from productSku in product.ProductSkus
select new ProductAndSkuDataModel(product, productSku);
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs

@ -7,6 +7,8 @@ namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountElectionModel
{
public DateTime Now { get; }
/// <summary>
/// The search will stop and throw an exception if the number of executions is greater than <see cref="MaxDepth"/>.
/// <see cref="MaxDepth"/> = Math.Pow(2, <see cref="MaxCandidates"/>)
@ -38,8 +40,10 @@ public class ProductDiscountElectionModel
private HashSet<string> UsedCombinations { get; } = new();
public ProductDiscountElectionModel(IProduct product, IProductSku productSku, decimal priceFromPriceProvider)
public ProductDiscountElectionModel(DateTime now, IProduct product, IProductSku productSku,
decimal priceFromPriceProvider)
{
Now = now;
Product = product;
ProductSku = productSku;
PriceFromPriceProvider = priceFromPriceProvider;

34
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs

@ -10,36 +10,34 @@ public class ProductDiscountResolver : IProductDiscountResolver, ITransientDepen
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
public virtual async Task<DiscountForProductModels> ResolveAsync(IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, DateTime now)
public virtual async Task ResolveAsync(GetProductsRealTimePriceContext context)
{
var context = new ProductDiscountContext(now, product, productSku, priceFromPriceProvider);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>()
.OrderBy(x => x.EffectOrder))
{
await provider.DiscountAsync(context);
}
if (context.CandidateProductDiscounts.IsNullOrEmpty())
foreach (var model in context.Models.Values)
{
return new DiscountForProductModels(null, context.OrderDiscountPreviews);
}
var product = context.Products[model.ProductId];
var productSku = product.GetSkuById(model.ProductSkuId);
var electionModel =
new ProductDiscountElectionModel(context.Product, context.ProductSku, context.PriceFromPriceProvider);
var electionModel =
new ProductDiscountElectionModel(context.Now, product, productSku, model.PriceWithoutDiscount);
electionModel.TryEnqueue(new CandidateProductDiscounts(context.CandidateProductDiscounts));
electionModel.TryEnqueue(new CandidateProductDiscounts(model.CandidateProductDiscounts));
while (!electionModel.Done)
{
await EvolveAsync(electionModel, now);
}
while (!electionModel.Done)
{
await EvolveAsync(electionModel);
}
return new DiscountForProductModels(electionModel.GetBestScheme().Discounts, context.OrderDiscountPreviews);
model.ProductDiscounts.AddRange(electionModel.GetBestScheme().Discounts);
}
}
protected virtual Task EvolveAsync(ProductDiscountElectionModel electionModel, DateTime now)
protected virtual Task EvolveAsync(ProductDiscountElectionModel electionModel)
{
if (electionModel.Done)
{
@ -60,8 +58,8 @@ public class ProductDiscountResolver : IProductDiscountResolver, ITransientDepen
var discount = new ProductDiscountInfoModel(candidate, 0m, false);
productDiscountInfoModels.Add(discount);
if (candidate.FromTime.HasValue && now < candidate.FromTime ||
candidate.ToTime.HasValue && now > candidate.ToTime)
if (candidate.FromTime.HasValue && electionModel.Now < candidate.FromTime ||
candidate.ToTime.HasValue && electionModel.Now > candidate.ToTime)
{
continue;
}

16
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs

@ -271,15 +271,19 @@ namespace EasyAbp.EShop.Products.Products
.TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale);
}
public virtual async Task<RealTimePriceInfoModel> GetRealTimePriceAsync(Product product, ProductSku productSku,
DateTime now)
public virtual async Task<GetProductsRealTimePriceContext> GetRealTimePricesAsync(
List<ProductAndSkuDataModel> models, DateTime now)
{
var priceFromPriceProvider = await _productPriceProvider.GetPriceAsync(product, productSku);
var realTimePriceInfoModels = await _productPriceProvider.GetPricesAsync(models);
var discounts =
await _productDiscountResolver.ResolveAsync(product, productSku, priceFromPriceProvider, now);
var context = new GetProductsRealTimePriceContext(
now,
models.Select(x => x.Product).Distinct(),
realTimePriceInfoModels);
return new RealTimePriceInfoModel(priceFromPriceProvider, discounts);
await _productDiscountResolver.ResolveAsync(context);
return context;
}
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductView.cs

@ -127,10 +127,10 @@ namespace EasyAbp.EShop.Products.Products
MaximumPriceWithoutDiscount = maxWithoutDiscount;
}
public void SetDiscounts(IHasDiscountsForProduct discountsForProduct)
public void SetDiscounts(IHasDiscountsForSku discountsForSku)
{
ProductDiscounts = discountsForProduct.ProductDiscounts ?? new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = discountsForProduct.OrderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
ProductDiscounts = discountsForSku.ProductDiscounts ?? new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = discountsForSku.OrderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
}
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs

@ -18,12 +18,12 @@ public static class EShopProductsEntityTypeBuilderExtensions
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b)
{
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForProduct>())
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForSku>())
{
b.Property(nameof(IHasDiscountsForProduct.ProductDiscounts))
b.Property(nameof(IHasDiscountsForSku.ProductDiscounts))
.HasConversion<ProductDiscountsInfoValueConverter>()
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer());
b.Property(nameof(IHasDiscountsForProduct.OrderDiscountPreviews))
b.Property(nameof(IHasDiscountsForSku.OrderDiscountPreviews))
.HasConversion<OrderDiscountPreviewsInfoValueConverter>()
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer());
}

87
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs

@ -17,53 +17,58 @@ public class DemoProductDiscountProvider : IProductDiscountProvider
public int EffectOrder => DemoProductDiscountEffectOrder;
public Task DiscountAsync(ProductDiscountContext context)
public Task DiscountAsync(GetProductsRealTimePriceContext context)
{
if (context.Product.Id != ProductsTestData.Product1Id ||
context.ProductSku.Id != ProductsTestData.Product1Sku1Id)
foreach (var model in context.Models.Values)
{
return Task.CompletedTask;
}
if (model.ProductId != ProductsTestData.Product1Id ||
model.ProductSkuId != ProductsTestData.Product1Sku1Id)
{
return Task.CompletedTask;
}
var candidates = new List<CandidateProductDiscountInfoModel>
{
// These should take effect:
new(null, "DemoDiscount", "1", "Demo Discount 1",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new(null, "DemoDiscount", "2", "Demo Discount 2",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1), null),
new(null, "DemoDiscount", "3", "Demo Discount 3",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(1)),
new(null, "DemoDiscount", "4", "Demo Discount 4",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1), _clock.Now.AddDays(1)),
// These should not take effect:
new(null, "DemoDiscount", "5", "Demo Discount 5",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(-1)),
new(null, "DemoDiscount", "6", "Demo Discount 6",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1), null),
new(null, "DemoDiscount", "7", "Demo Discount 7",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1), _clock.Now.AddDays(2)),
// Only the one with the highest discount amount should take effect:
new("A", "DemoDiscount", "8", "Demo Discount 8",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new("A", "DemoDiscount", "9", "Demo Discount 9",
new DynamicDiscountAmountModel("USD", 0.01m, 0m, null), null, null),
};
var candidates = new List<CandidateProductDiscountInfoModel>
{
// These should take effect:
new(null, "DemoDiscount", "1", "Demo Discount 1",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new(null, "DemoDiscount", "2", "Demo Discount 2",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1), null),
new(null, "DemoDiscount", "3", "Demo Discount 3",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(1)),
new(null, "DemoDiscount", "4", "Demo Discount 4",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1),
_clock.Now.AddDays(1)),
// These should not take effect:
new(null, "DemoDiscount", "5", "Demo Discount 5",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(-1)),
new(null, "DemoDiscount", "6", "Demo Discount 6",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1), null),
new(null, "DemoDiscount", "7", "Demo Discount 7",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1),
_clock.Now.AddDays(2)),
// Only the one with the highest discount amount should take effect:
new("A", "DemoDiscount", "8", "Demo Discount 8",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new("A", "DemoDiscount", "9", "Demo Discount 9",
new DynamicDiscountAmountModel("USD", 0.01m, 0m, null), null, null),
};
foreach (var model in candidates)
{
context.CandidateProductDiscounts.Add(model);
}
foreach (var candidate in candidates)
{
model.CandidateProductDiscounts.Add(candidate);
}
var orderDiscountPreviewInfoModels = new List<OrderDiscountPreviewInfoModel>
{
new(null, "DemoDiscount", "1", "Demo Discount 1", null, null, null),
new(null, "DemoDiscount", "2", "Demo Discount 2", _clock.Now.AddDays(-1), _clock.Now.AddDays(1), null),
};
var orderDiscountPreviewInfoModels = new List<OrderDiscountPreviewInfoModel>
{
new(null, "DemoDiscount", "1", "Demo Discount 1", null, null, null),
new(null, "DemoDiscount", "2", "Demo Discount 2", _clock.Now.AddDays(-1), _clock.Now.AddDays(1), null),
};
foreach (var model in orderDiscountPreviewInfoModels)
{
context.OrderDiscountPreviews.Add(model);
foreach (var preview in orderDiscountPreviewInfoModels)
{
model.OrderDiscountPreviews.Add(preview);
}
}
return Task.CompletedTask;

2
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductData.cs

@ -2,7 +2,7 @@
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
{
public interface IProductData : IHasFullDiscountsForProduct
public interface IProductData : IHasFullDiscountsForSku
{
string MediaResources { get; }

6
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore/EasyAbp/EShop/Plugins/Baskets/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs

@ -8,12 +8,12 @@ public static class EShopProductsEntityTypeBuilderExtensions
{
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b)
{
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForProduct>())
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForSku>())
{
b.Property(nameof(IHasDiscountsForProduct.ProductDiscounts))
b.Property(nameof(IHasDiscountsForSku.ProductDiscounts))
.HasConversion<ProductDiscountsInfoValueConverter>()
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer());
b.Property(nameof(IHasDiscountsForProduct.OrderDiscountPreviews))
b.Property(nameof(IHasDiscountsForSku.OrderDiscountPreviews))
.HasConversion<OrderDiscountPreviewsInfoValueConverter>()
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer());
}

4
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/Dtos/DiscountProductInputDto.cs

@ -6,13 +6,13 @@ namespace EasyAbp.EShop.Plugins.Promotions.Promotions.Dtos;
[Serializable]
public class DiscountProductInputDto
{
public ProductDiscountContext Context { get; set; }
public GetProductsRealTimePriceContext Context { get; set; }
public DiscountProductInputDto()
{
}
public DiscountProductInputDto(ProductDiscountContext context)
public DiscountProductInputDto(GetProductsRealTimePriceContext context)
{
Context = context;
}

4
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/Dtos/DiscountProductOutputDto.cs

@ -6,13 +6,13 @@ namespace EasyAbp.EShop.Plugins.Promotions.Promotions.Dtos;
[Serializable]
public class DiscountProductOutputDto
{
public ProductDiscountContext Context { get; set; }
public GetProductsRealTimePriceContext Context { get; set; }
public DiscountProductOutputDto()
{
}
public DiscountProductOutputDto(ProductDiscountContext context)
public DiscountProductOutputDto(GetProductsRealTimePriceContext context)
{
Context = context;
}

2
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application.Contracts/EasyAbp/EShop/Plugins/Promotions/Promotions/IPromotionIntegrationService.cs

@ -7,7 +7,7 @@ namespace EasyAbp.EShop.Plugins.Promotions.Promotions;
[IntegrationService]
public interface IPromotionIntegrationService
{
Task<DiscountProductOutputDto> DiscountProductAsync(DiscountProductInputDto input);
Task<DiscountProductOutputDto> DiscountProductsAsync(DiscountProductInputDto input);
Task<DiscountOrderOutputDto> DiscountOrderAsync(DiscountOrderInputDto input);
}

24
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Application/EasyAbp/EShop/Plugins/Promotions/Promotions/PromotionIntegrationService.cs

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.Promotions.Options;
using EasyAbp.EShop.Plugins.Promotions.Promotions.Dtos;
using EasyAbp.EShop.Plugins.Promotions.PromotionTypes;
using EasyAbp.EShop.Products.Products;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Application.Services;
@ -24,17 +25,30 @@ public class PromotionIntegrationService : ApplicationService, IPromotionIntegra
Options = options.Value;
}
public virtual async Task<DiscountProductOutputDto> DiscountProductAsync(DiscountProductInputDto input)
public virtual async Task<DiscountProductOutputDto> DiscountProductsAsync(DiscountProductInputDto input)
{
var context = input.Context;
var promotions = await GetUnexpiredPromotionsAsync(context.Product.StoreId, context.Now, true);
var storeGroupings = input.Context.Models.GroupBy(x => input.Context.Products[x.Value.ProductId].StoreId);
foreach (var promotion in promotions.OrderByDescending(x => x.Priority))
foreach (var storeGrouping in storeGroupings)
{
var promotionHandler = GetPromotionHandler(promotion.PromotionType);
var storeId = storeGrouping.Key;
var promotions = await GetUnexpiredPromotionsAsync(storeId, context.Now, true);
foreach (var promotion in promotions.OrderByDescending(x => x.Priority))
{
var promotionHandler = GetPromotionHandler(promotion.PromotionType);
foreach (var (_, model) in storeGrouping)
{
var product = context.Products[model.ProductId];
var productSku = product.GetSkuById(model.ProductSkuId);
await promotionHandler.HandleProductAsync(context, promotion);
await promotionHandler.HandleProductAsync(model, promotion, product, productSku);
}
}
}
return new DiscountProductOutputDto(context);

3
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/IPromotionHandler.cs

@ -7,7 +7,8 @@ namespace EasyAbp.EShop.Plugins.Promotions.PromotionTypes;
public interface IPromotionHandler
{
Task HandleProductAsync(ProductDiscountContext context, Promotion promotion);
Task HandleProductAsync(ProductRealTimePriceInfoModel model, Promotion promotion, IProduct product,
IProductSku productSku);
Task HandleOrderAsync(OrderDiscountContext context, Promotion promotion);

13
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/MinQuantityOrderDiscount/MinQuantityOrderDiscountPromotionHandler.cs

@ -17,30 +17,31 @@ public class MinQuantityOrderDiscountPromotionHandler : PromotionHandlerBase, IS
{
}
public override async Task HandleProductAsync(ProductDiscountContext context, Promotion promotion)
public override async Task HandleProductAsync(ProductRealTimePriceInfoModel model, Promotion promotion,
IProduct product, IProductSku productSku)
{
foreach (var discountModel in GetConfigurations<MinQuantityOrderDiscountConfigurations>(promotion).Discounts)
{
if (context.ProductSku.Currency != discountModel.DynamicDiscountAmount.Currency)
if (productSku.Currency != discountModel.DynamicDiscountAmount.Currency)
{
continue;
}
if (!discountModel.IsInScope(context.Product.ProductGroupName, context.Product.Id, context.ProductSku.Id))
if (!discountModel.IsInScope(product.ProductGroupName, product.Id, productSku.Id))
{
continue;
}
var newDiscount = new OrderDiscountPreviewInfoModel(PromotionConsts.PromotionEffectGroup,
PromotionConsts.PromotionDiscountName, promotion.UniqueName, promotion.DisplayName, promotion.FromTime,
promotion.ToTime, await CreateOrderDiscountPreviewRuleDataAsync(discountModel, context, promotion));
promotion.ToTime, await CreateOrderDiscountPreviewRuleDataAsync(discountModel, model, promotion));
context.OrderDiscountPreviews.Add(newDiscount);
model.OrderDiscountPreviews.Add(newDiscount);
}
}
protected virtual Task<string?> CreateOrderDiscountPreviewRuleDataAsync(MinQuantityOrderDiscountModel discountModel,
ProductDiscountContext context, Promotion promotion)
ProductRealTimePriceInfoModel model, Promotion promotion)
{
return Task.FromResult<string?>(null);
}

3
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/PromotionHandlerBase.cs

@ -20,7 +20,8 @@ public abstract class PromotionHandlerBase : IPromotionHandler
JsonSerializer = jsonSerializer;
}
public abstract Task HandleProductAsync(ProductDiscountContext context, Promotion promotion);
public abstract Task HandleProductAsync(ProductRealTimePriceInfoModel model, Promotion promotion, IProduct product,
IProductSku productSku);
public abstract Task HandleOrderAsync(OrderDiscountContext context, Promotion promotion);

9
plugins/Promotions/src/EasyAbp.EShop.Plugins.Promotions.Domain/EasyAbp/EShop/Plugins/Promotions/PromotionTypes/SimpleProductDiscount/SimpleProductDiscountPromotionHandler.cs

@ -16,16 +16,17 @@ public class SimpleProductDiscountPromotionHandler : PromotionHandlerBase, IScop
{
}
public override Task HandleProductAsync(ProductDiscountContext context, Promotion promotion)
public override Task HandleProductAsync(ProductRealTimePriceInfoModel model, Promotion promotion, IProduct product,
IProductSku productSku)
{
foreach (var discountModel in GetConfigurations<SimpleProductDiscountConfigurations>(promotion).Discounts)
{
if (context.ProductSku.Currency != discountModel.DynamicDiscountAmount.Currency)
if (productSku.Currency != discountModel.DynamicDiscountAmount.Currency)
{
continue;
}
if (!discountModel.IsInScope(context.Product.ProductGroupName, context.Product.Id, context.ProductSku.Id))
if (!discountModel.IsInScope(product.ProductGroupName, product.Id, productSku.Id))
{
continue;
}
@ -34,7 +35,7 @@ public class SimpleProductDiscountPromotionHandler : PromotionHandlerBase, IScop
PromotionConsts.PromotionDiscountName, promotion.UniqueName, promotion.DisplayName,
discountModel.DynamicDiscountAmount, promotion.FromTime, promotion.ToTime);
context.CandidateProductDiscounts.Add(discount);
model.CandidateProductDiscounts.Add(discount);
}
return Task.CompletedTask;

23
plugins/Promotions/src/EasyAbp.EShop.Products.Plugins.Promotions.Domain/EasyAbp/EShop/Products/Plugins/Promotions/PromotionProductDiscountProvider.cs

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.Promotions.Promotions;
using EasyAbp.EShop.Plugins.Promotions.Promotions.Dtos;
@ -19,19 +20,29 @@ public class PromotionProductDiscountProvider : IProductDiscountProvider, ITrans
PromotionIntegrationService = promotionIntegrationService;
}
public virtual async Task DiscountAsync(ProductDiscountContext context)
public virtual async Task DiscountAsync(GetProductsRealTimePriceContext context)
{
var dto = await PromotionIntegrationService.DiscountProductAsync(new DiscountProductInputDto(context));
if (context.Models.IsNullOrEmpty())
{
return;
}
var dto = await PromotionIntegrationService.DiscountProductsAsync(new DiscountProductInputDto(context));
if (dto.Context.Equals(context))
{
return;
}
context.CandidateProductDiscounts.Clear();
context.CandidateProductDiscounts.AddRange(dto.Context.CandidateProductDiscounts);
foreach (var model in context.Models.Values)
{
var targetModel = dto.Context.Models[model.ProductSkuId];
model.CandidateProductDiscounts.Clear();
model.CandidateProductDiscounts.AddRange(targetModel.CandidateProductDiscounts);
context.OrderDiscountPreviews.Clear();
context.OrderDiscountPreviews.AddRange(dto.Context.OrderDiscountPreviews);
model.OrderDiscountPreviews.Clear();
model.OrderDiscountPreviews.AddRange(targetModel.OrderDiscountPreviews);
}
}
}

21
plugins/Promotions/test/EasyAbp.EShop.Plugins.Promotions.Application.Tests/PromotionTypes/MinQuantityOrderDiscountTests.cs

@ -34,25 +34,24 @@ public class MinQuantityOrderDiscountTests : PromotionsApplicationTestBase
{
var promotion = await CreatePromotionAsync();
var productSku = new ProductSkuEto
{
Currency = "USD"
};
var product = new ProductEto
{
ProductGroupName = "MyProductGroup",
ProductSkus = new List<ProductSkuEto>
{
new()
{
Currency = "USD"
}
}
ProductSkus = new List<ProductSkuEto> { productSku }
};
var context = new ProductDiscountContext(DateTime.Now, product, product.ProductSkus.First(), 1.00m);
var model = new ProductRealTimePriceInfoModel(product.Id, productSku.Id, 1.00m);
await Handler.HandleProductAsync(context, promotion);
await Handler.HandleProductAsync(model, promotion, product, productSku);
context.OrderDiscountPreviews.Count.ShouldBe(1);
model.OrderDiscountPreviews.Count.ShouldBe(1);
var orderDiscount = context.OrderDiscountPreviews.First();
var orderDiscount = model.OrderDiscountPreviews.First();
orderDiscount.ShouldNotBeNull();
orderDiscount.EffectGroup.ShouldBe(PromotionConsts.PromotionEffectGroup);
orderDiscount.DisplayName.ShouldBe("test");

23
plugins/Promotions/test/EasyAbp.EShop.Plugins.Promotions.Application.Tests/PromotionTypes/SimpleProductDiscountTests.cs

@ -31,26 +31,25 @@ public class SimpleProductDiscountTests : PromotionsApplicationTestBase
{
var promotion = await CreatePromotionAsync();
var productSku = new ProductSkuEto
{
Currency = "USD",
Price = 1.00m,
};
var product = new ProductEto
{
ProductGroupName = "MyProductGroup",
ProductSkus = new List<ProductSkuEto>
{
new()
{
Currency = "USD",
Price = 1.00m,
}
}
ProductSkus = new List<ProductSkuEto> { productSku }
};
var context = new ProductDiscountContext(DateTime.Now, product, product.ProductSkus.First(), 1.00m);
var model = new ProductRealTimePriceInfoModel(product.Id, productSku.Id, 1.00m);
await Handler.HandleProductAsync(context, promotion);
await Handler.HandleProductAsync(model, promotion, product, productSku);
context.CandidateProductDiscounts.Count.ShouldBe(1);
model.CandidateProductDiscounts.Count.ShouldBe(1);
var productDiscount = context.CandidateProductDiscounts.First();
var productDiscount = model.CandidateProductDiscounts.First();
productDiscount.ShouldNotBeNull();
productDiscount.EffectGroup.ShouldBe(PromotionConsts.PromotionEffectGroup);
productDiscount.DisplayName.ShouldBe("test");

Loading…
Cancel
Save