From 9856c49769b1958d4ad650e7fbbe75a149a8a55c Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Sun, 9 Apr 2023 19:19:50 +0800 Subject: [PATCH] Clear the product view cache when product discounts begin or end --- .../Products/Products/ProductAppService.cs | 27 ++++---- .../Products/ProductViewAppService.cs | 67 ++++++++++++++++--- .../Products/Products/IProductManager.cs | 14 ++-- .../EShop/Products/Products/PriceDataModel.cs | 5 +- .../Products/ProductDiscountContext.cs | 6 +- .../EShop/Products/Products/ProductManager.cs | 5 +- 6 files changed, 88 insertions(+), 36 deletions(-) diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs index b2496bc9..95e1ceac 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs @@ -82,7 +82,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); @@ -116,7 +116,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); @@ -201,7 +201,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); return dto; @@ -232,7 +232,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); return dto; @@ -257,13 +257,14 @@ namespace EasyAbp.EShop.Products.Products var products = await AsyncExecuter.ToListAsync(query); + var now = Clock.Now; var items = new List(); foreach (var product in products) { var productDto = await MapToGetListOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, productDto); + await LoadDtoExtraDataAsync(product, productDto, now); items.Add(productDto); } @@ -296,21 +297,23 @@ namespace EasyAbp.EShop.Products.Products return productDto; } - protected virtual async Task LoadDtoExtraDataAsync(Product product, ProductDto productDto) + protected virtual async Task LoadDtoExtraDataAsync(Product product, ProductDto productDto, + DateTime now) { await LoadDtoInventoryDataAsync(product, productDto); - await LoadDtoPriceDataAsync(product, productDto); + await LoadDtoPriceDataAsync(product, productDto, now); return productDto; } - protected virtual async Task LoadDtoPriceDataAsync(Product product, ProductDto productDto) + protected virtual async Task LoadDtoPriceDataAsync(Product product, ProductDto productDto, + DateTime now) { foreach (var productSku in product.ProductSkus) { var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id); - var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku); + var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); productSkuDto.PriceWithoutDiscount = priceDataModel.PriceWithoutDiscount; productSkuDto.Price = priceDataModel.DiscountedPrice; @@ -364,7 +367,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); @@ -389,7 +392,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); @@ -411,7 +414,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); - await LoadDtoExtraDataAsync(product, dto); + await LoadDtoExtraDataAsync(product, dto, Clock.Now); await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs index e9235266..7458aa4e 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs @@ -117,31 +117,80 @@ namespace EasyAbp.EShop.Products.Products { var products = await _productRepository.GetListAsync(x => x.StoreId == storeId, true); + var now = Clock.Now; + using var uow = UnitOfWorkManager.Begin(true, true); await _repository.DeleteAsync(x => x.StoreId == storeId, true); + var productViews = new List(); + foreach (var product in products) { var productView = ObjectMapper.Map(product); - await FillPriceInfoWithRealPriceAsync(product, productView); + await FillPriceInfoWithRealPriceAsync(product, productView, now); - await _repository.InsertAsync(productView); + productViews.Add(await _repository.InsertAsync(productView)); } await uow.SaveChangesAsync(); await uow.CompleteAsync(); - await _cache.SetAsync(await GetCacheKeyAsync(storeId), new ProductViewCacheItem(), - new DistributedCacheEntryOptions + var duration = await GetCacheDurationOrNullAsync(productViews, now); + + if (duration.HasValue) + { + await _cache.SetAsync(await GetCacheKeyAsync(storeId), new ProductViewCacheItem(), + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = duration + }); + } + } + + protected async Task GetCacheDurationOrNullAsync(List productViews, + DateTime now) + { + // refresh the cache when a new discount takes effect or an old discount ends. + var minFromTime = productViews.SelectMany(x => x.ProductDiscounts) + .Where(x => x.FromTime.HasValue && x.FromTime > now).MinBy(x => x.FromTime)?.FromTime; + var minToTime = productViews.SelectMany(x => x.ProductDiscounts) + .Where(x => x.ToTime.HasValue && x.ToTime > now).MinBy(x => x.ToTime)?.ToTime; + + DateTime? nextTime; + if (minFromTime.HasValue) + { + if (minToTime.HasValue) + { + nextTime = minFromTime < minToTime ? minFromTime : minToTime; + } + else { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Convert.ToInt32( - await SettingProvider.GetOrNullAsync(ProductsSettings.ProductView.CacheDurationSeconds))) - }); + nextTime = minFromTime; + } + } + else + { + nextTime = minToTime; + } + + // use `Clock.Now` to achieve higher timeliness. + var duration = nextTime.HasValue ? nextTime - Clock.Now : null; + + if (duration <= TimeSpan.Zero) + { + return null; + } + + var defaultDuration = TimeSpan.FromSeconds(Convert.ToInt32( + await SettingProvider.GetOrNullAsync(ProductsSettings.ProductView.CacheDurationSeconds))); + + return duration.HasValue && duration.Value < defaultDuration ? duration : defaultDuration; } - protected virtual async Task FillPriceInfoWithRealPriceAsync(Product product, ProductView productView) + protected virtual async Task FillPriceInfoWithRealPriceAsync(Product product, ProductView productView, + DateTime now) { if (product.ProductSkus.IsNullOrEmpty()) { @@ -156,7 +205,7 @@ namespace EasyAbp.EShop.Products.Products foreach (var productSku in product.ProductSkus) { var overrideProductDiscounts = false; - var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku); + var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); if (min is null || priceDataModel.DiscountedPrice < min.Value) { diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs index 884fe3e5..90ee9f2c 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs @@ -13,23 +13,23 @@ namespace EasyAbp.EShop.Products.Products Task UpdateAsync(Product product, IEnumerable categoryIds = null); Task DeleteAsync(Product product); - + Task DeleteAsync(Guid id); Task CreateSkuAsync(Product product, ProductSku productSku); - + Task UpdateSkuAsync(Product product, ProductSku productSku); - + Task DeleteSkuAsync(Product product, ProductSku productSku); - + Task IsInventorySufficientAsync(Product product, ProductSku productSku, int quantity); - + Task GetInventoryDataAsync(Product product, ProductSku productSku); Task TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool reduceSold); - + Task TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold); - Task GetRealPriceAsync(Product product, ProductSku productSku); + Task GetRealPriceAsync(Product product, ProductSku productSku, DateTime now); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs index 7bd90669..eeae5683 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Volo.Abp.Timing; namespace EasyAbp.EShop.Products.Products; @@ -24,14 +23,14 @@ public class PriceDataModel : IHasFullDiscountsInfo public List OrderDiscountPreviews { get; } = new(); - public PriceDataModel(decimal priceWithoutDiscount, IClock clock) + public PriceDataModel(decimal priceWithoutDiscount, DateTime now) { if (PriceWithoutDiscount < decimal.Zero) { throw new OverflowException(); } - Now = clock.Now; + Now = now; PriceWithoutDiscount = priceWithoutDiscount; } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs index 7bec21bb..66537efb 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs @@ -1,4 +1,4 @@ -using Volo.Abp.Timing; +using System; namespace EasyAbp.EShop.Products.Products; @@ -10,10 +10,10 @@ public class ProductDiscountContext public PriceDataModel PriceDataModel { get; } - public ProductDiscountContext(Product product, ProductSku productSku, decimal priceFromPriceProvider, IClock clock) + public ProductDiscountContext(Product product, ProductSku productSku, decimal priceFromPriceProvider, DateTime now) { Product = product; ProductSku = productSku; - PriceDataModel = new PriceDataModel(priceFromPriceProvider, clock); + PriceDataModel = new PriceDataModel(priceFromPriceProvider, now); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs index f15eeb4c..d20e06d3 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs @@ -268,11 +268,12 @@ namespace EasyAbp.EShop.Products.Products .TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale); } - public virtual async Task GetRealPriceAsync(Product product, ProductSku productSku) + public virtual async Task GetRealPriceAsync(Product product, ProductSku productSku, + DateTime now) { var price = await _productPriceProvider.GetPriceAsync(product, productSku); - var context = new ProductDiscountContext(product, productSku, price, Clock); + var context = new ProductDiscountContext(product, productSku, price, now); // Todo: provider execution ordering. foreach (var provider in LazyServiceProvider.LazyGetService>())