From 4545ef0087dd8cfe3be8a7492734e54cd4585e52 Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Fri, 10 Jun 2022 18:03:51 +0800 Subject: [PATCH] Introduce inventory rollback feature and refactor IProductInventoryProvider --- .../ProductInventoryAppService.cs | 13 +-- .../Products/Products/ProductAppService.cs | 102 ++++++++---------- .../IProductInventoryProvider.cs | 17 +++ .../ProductInventories}/InventoryDataModel.cs | 2 +- .../ProductInventories/InventoryQueryModel.cs | 28 +++++ .../IProductInventoryRepository.cs | 2 +- .../Products/ConsumeInventoryModel.cs | 23 +++- .../DefaultProductInventoryProvider.cs | 58 +++++----- .../Products/IProductInventoryProvider.cs | 17 --- .../Products/Products/IProductManager.cs | 1 + .../Products/OrderCreatedEventHandler.cs | 45 ++++++-- .../Products/OrderPaidEventHandler.cs | 55 +++++++--- .../EShop/Products/Products/ProductManager.cs | 59 +++++----- .../ProductInventoryRepository.cs | 5 +- 14 files changed, 261 insertions(+), 166 deletions(-) create mode 100644 modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs rename modules/EasyAbp.EShop.Products/src/{EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products => EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories}/InventoryDataModel.cs (71%) create mode 100644 modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs delete mode 100644 modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs index 3d595c87..57daea29 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs @@ -39,7 +39,7 @@ namespace EasyAbp.EShop.Products.ProductInventories { throw new EntityNotFoundException(typeof(ProductSku), productSkuId); } - + productInventory = new ProductInventory(GuidGenerator.Create(), CurrentTenant.Id, productId, productSkuId, 0, 0); @@ -60,7 +60,7 @@ namespace EasyAbp.EShop.Products.ProductInventories await AuthorizationService.CheckMultiStorePolicyAsync(product.StoreId, ProductsPermissions.ProductInventory.Update, ProductsPermissions.ProductInventory.CrossStore); - + var productInventory = await _repository.FindAsync(x => x.ProductSkuId == input.ProductSkuId); if (productInventory == null) @@ -80,10 +80,12 @@ namespace EasyAbp.EShop.Products.ProductInventories protected virtual async Task ChangeInventoryAsync(Product product, ProductInventory productInventory, int changedInventory) { + var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, + productInventory.ProductSkuId); + if (changedInventory >= 0) { - if (!await _productInventoryProvider.TryIncreaseInventoryAsync(product, productInventory, - changedInventory, false)) + if (!await _productInventoryProvider.TryIncreaseInventoryAsync(model, changedInventory, false)) { throw new InventoryChangeFailedException(productInventory.ProductId, productInventory.ProductSkuId, productInventory.Inventory, changedInventory); @@ -91,8 +93,7 @@ namespace EasyAbp.EShop.Products.ProductInventories } else { - if (!await _productInventoryProvider.TryReduceInventoryAsync(product, productInventory, - -changedInventory, false)) + if (!await _productInventoryProvider.TryReduceInventoryAsync(model, -changedInventory, false)) { throw new InventoryChangeFailedException(productInventory.ProductId, productInventory.ProductSkuId, productInventory.Inventory, changedInventory); 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 20a76dca..f1e6799d 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 @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EasyAbp.EShop.Products.Options; +using EasyAbp.EShop.Products.ProductInventories; using EasyAbp.EShop.Products.Products.CacheItems; using EasyAbp.EShop.Stores.Stores; using Microsoft.Extensions.Options; @@ -14,7 +15,9 @@ using Volo.Abp.Domain.Entities; namespace EasyAbp.EShop.Products.Products { - public class ProductAppService : MultiStoreCrudAppService, IProductAppService + public class ProductAppService : + MultiStoreCrudAppService, IProductAppService { protected override string CreatePolicyName { get; set; } = ProductsPermissions.Products.Create; protected override string DeletePolicyName { get; set; } = ProductsPermissions.Products.Delete; @@ -81,14 +84,11 @@ namespace EasyAbp.EShop.Products.Products await _productManager.CreateAsync(product, input.CategoryIds); var dto = await MapToGetOutputDtoAsync(product); - + await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); - - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); return dto; } @@ -101,14 +101,14 @@ namespace EasyAbp.EShop.Products.Products public override async Task UpdateAsync(Guid id, CreateUpdateProductDto input) { var product = await GetEntityByIdAsync(id); - + await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName); if (input.StoreId != product.StoreId) { await CheckMultiStorePolicyAsync(input.StoreId, UpdatePolicyName); } - + CheckProductIsNotStatic(product); MapToEntity(input, product); @@ -118,15 +118,12 @@ namespace EasyAbp.EShop.Products.Products await _productManager.UpdateAsync(product, input.CategoryIds); var dto = await MapToGetOutputDtoAsync(product); - + await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); - return dto; } @@ -137,10 +134,10 @@ namespace EasyAbp.EShop.Products.Products var usedAttributeOptionIds = new HashSet(); foreach (var serializedAttributeOptionIds in product.ProductSkus.Select(sku => - sku.SerializedAttributeOptionIds)) + sku.SerializedAttributeOptionIds)) { foreach (var attributeOptionId in await _attributeOptionIdsSerializer.DeserializeAsync( - serializedAttributeOptionIds)) + serializedAttributeOptionIds)) { usedAttributeOptionIds.Add(attributeOptionId); } @@ -182,9 +179,9 @@ namespace EasyAbp.EShop.Products.Products .Except(attributeDto.ProductAttributeOptions.Select(o => o.DisplayName)).ToList(); if (!isProductSkusEmpty && removedOptionNames.Any() && usedAttributeOptionIds - .Intersect(attribute.ProductAttributeOptions - .Where(option => removedOptionNames.Contains(option.DisplayName)) - .Select(option => option.Id)).Any()) + .Intersect(attribute.ProductAttributeOptions + .Where(option => removedOptionNames.Contains(option.DisplayName)) + .Select(option => option.Id)).Any()) { throw new ProductAttributeOptionsDeletionFailedException(); } @@ -217,7 +214,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); return dto; } @@ -248,7 +245,7 @@ namespace EasyAbp.EShop.Products.Products var dto = await MapToGetOutputDtoAsync(product); await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); return dto; } @@ -290,7 +287,10 @@ namespace EasyAbp.EShop.Products.Products protected virtual async Task LoadDtoInventoryDataAsync(Product product, ProductDto productDto) { - var inventoryDataDict = await _productInventoryProvider.GetInventoryDataDictionaryAsync(product); + var models = product.ProductSkus.Select(x => + new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, x.Id)).ToList(); + + var inventoryDataDict = await _productInventoryProvider.GetSkuIdInventoryDataMappingAsync(models); productDto.Sold = 0; @@ -313,7 +313,7 @@ namespace EasyAbp.EShop.Products.Products return productDto; } - + protected virtual async Task LoadDtoPriceDataAsync(Product product, ProductDto productDto) { foreach (var productSku in product.ProductSkus) @@ -321,7 +321,7 @@ namespace EasyAbp.EShop.Products.Products var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id); var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku); - + productSkuDto.Price = priceDataModel.Price; productSkuDto.DiscountedPrice = priceDataModel.DiscountedPrice; } @@ -338,17 +338,14 @@ namespace EasyAbp.EShop.Products.Products public override async Task DeleteAsync(Guid id) { var product = await GetEntityByIdAsync(id); - + await CheckMultiStorePolicyAsync(product.StoreId, DeletePolicyName); - + CheckProductIsNotStatic(product); await _productManager.DeleteAsync(product); - - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); } private static void CheckProductIsNotStatic(Product product) @@ -362,9 +359,9 @@ namespace EasyAbp.EShop.Products.Products public async Task CreateSkuAsync(Guid productId, CreateProductSkuDto input) { var product = await GetEntityByIdAsync(productId); - + await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName); - + CheckProductIsNotStatic(product); var sku = ObjectMapper.Map(input); @@ -374,15 +371,12 @@ namespace EasyAbp.EShop.Products.Products await _productManager.CreateSkuAsync(product, sku); var dto = await MapToGetOutputDtoAsync(product); - + await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); - return dto; } @@ -401,15 +395,12 @@ namespace EasyAbp.EShop.Products.Products await _productManager.UpdateSkuAsync(product, sku); var dto = await MapToGetOutputDtoAsync(product); - + await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); - return dto; } @@ -426,15 +417,12 @@ namespace EasyAbp.EShop.Products.Products await _productManager.DeleteSkuAsync(product, sku); var dto = await MapToGetOutputDtoAsync(product); - + await LoadDtoExtraDataAsync(product, dto); - await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); + await LoadDtosProductGroupDisplayNameAsync(new[] { dto }); + + UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); }); - UnitOfWorkManager.Current.OnCompleted(async () => - { - await ClearProductViewCacheAsync(product.StoreId); - }); - return dto; } diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs new file mode 100644 index 00000000..9a24847e --- /dev/null +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace EasyAbp.EShop.Products.ProductInventories +{ + public interface IProductInventoryProvider + { + Task GetInventoryDataAsync(InventoryQueryModel model); + + Task> GetSkuIdInventoryDataMappingAsync(IList models); + + Task TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold); + + Task TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryDataModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs similarity index 71% rename from modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryDataModel.cs rename to modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs index bdc1e838..11355343 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryDataModel.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs @@ -1,4 +1,4 @@ -namespace EasyAbp.EShop.Products.Products +namespace EasyAbp.EShop.Products.ProductInventories { public class InventoryDataModel { diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs new file mode 100644 index 00000000..99d2c3c1 --- /dev/null +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs @@ -0,0 +1,28 @@ +using System; +using EasyAbp.EShop.Stores.Stores; +using Volo.Abp.MultiTenancy; + +namespace EasyAbp.EShop.Products.ProductInventories; + +public class InventoryQueryModel : IMultiTenant, IMultiStore +{ + public Guid? TenantId { get; set; } + + public Guid StoreId { get; set; } + + public Guid ProductId { get; set; } + + public Guid ProductSkuId { get; set; } + + public InventoryQueryModel() + { + } + + public InventoryQueryModel(Guid? tenantId, Guid storeId, Guid productId, Guid productSkuId) + { + TenantId = tenantId; + StoreId = storeId; + ProductId = productId; + ProductSkuId = productSkuId; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs index 7eb77d15..86d23060 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs @@ -11,6 +11,6 @@ namespace EasyAbp.EShop.Products.ProductInventories { Task GetInventoryDataAsync(Guid productSkuId, CancellationToken cancellationToken = default); - Task> GetInventoryDataDictionaryAsync(List productSkuIds, CancellationToken cancellationToken = default); + Task> GetSkuIdInventoryDataMappingAsync(List productSkuIds, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs index 1d25894c..204e1bb1 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs @@ -4,12 +4,27 @@ namespace EasyAbp.EShop.Products.Products { public class ConsumeInventoryModel { - public Product Product { get; set; } + public Product Product { get; } - public ProductSku ProductSku { get; set; } + public ProductSku ProductSku { get; } - public Guid StoreId { get; set; } + public Guid StoreId { get; } - public int Quantity { get; set; } + public int Quantity { get; } + + public bool Consumed { get; private set;} + + public ConsumeInventoryModel(Product product, ProductSku productSku, Guid storeId, int quantity) + { + Product = product; + ProductSku = productSku; + StoreId = storeId; + Quantity = quantity; + } + + public void SetConsumed(bool consumed) + { + Consumed = consumed; + } } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs index e22a1881..43eab943 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs @@ -33,50 +33,54 @@ namespace EasyAbp.EShop.Products.Products _distributedEventBus = distributedEventBus; _productInventoryRepository = productInventoryRepository; } - + [UnitOfWork] - public virtual async Task GetInventoryDataAsync(Product product, ProductSku productSku) + public virtual async Task GetInventoryDataAsync(InventoryQueryModel model) { - return await _productInventoryRepository.GetInventoryDataAsync(productSku.Id); + return await _productInventoryRepository.GetInventoryDataAsync(model.ProductSkuId); } [UnitOfWork] - public virtual async Task> GetInventoryDataDictionaryAsync(Product product) + public virtual async Task> GetSkuIdInventoryDataMappingAsync( + IList models) { - var dict = await _productInventoryRepository.GetInventoryDataDictionaryAsync(product.ProductSkus - .Select(sku => sku.Id).ToList()); + var dict = await _productInventoryRepository.GetSkuIdInventoryDataMappingAsync( + models.Select(x => x.ProductSkuId).ToList()); - foreach (var sku in product.ProductSkus) + foreach (var model in models) { - dict.GetOrAdd(sku.Id, () => new InventoryDataModel()); + dict.GetOrAdd(model.ProductSkuId, () => new InventoryDataModel()); } return dict; } [UnitOfWork(true)] - public virtual async Task TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold) + public virtual async Task TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, + bool decreaseSold) { - var productInventory = await GetOrCreateProductInventoryAsync(product.Id, productSku.Id); - - return await TryIncreaseInventoryAsync(product, productInventory, quantity, decreaseSold); + var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId); + + return await TryIncreaseInventoryAsync(model, productInventory, quantity, decreaseSold); } [UnitOfWork(true)] - public virtual async Task TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold) + public virtual async Task TryReduceInventoryAsync(InventoryQueryModel model, int quantity, + bool increaseSold) { - var productInventory = await GetOrCreateProductInventoryAsync(product.Id, productSku.Id); - - return await TryReduceInventoryAsync(product, productInventory, quantity, increaseSold); + var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId); + + return await TryReduceInventoryAsync(model, productInventory, quantity, increaseSold); } - + [UnitOfWork] - protected virtual async Task GetOrCreateProductInventoryAsync(Guid productId, Guid productSkuId) + protected virtual async Task GetOrCreateProductInventoryAsync(Guid productId, + Guid productSkuId) { var productInventory = await _productInventoryRepository.FindAsync(x => x.ProductId == productId && x.ProductSkuId == productSkuId); - + if (productInventory is null) { productInventory = new ProductInventory(_guidGenerator.Create(), _currentTenant.Id, productId, @@ -87,15 +91,16 @@ namespace EasyAbp.EShop.Products.Products return productInventory; } - + [UnitOfWork(true)] - public virtual async Task TryIncreaseInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool decreaseSold) + protected virtual async Task TryIncreaseInventoryAsync(InventoryQueryModel model, + ProductInventory productInventory, int quantity, bool decreaseSold) { if (quantity < 0) { return false; } - + var originalInventory = productInventory.Inventory; if (!productInventory.TryIncreaseInventory(quantity, decreaseSold)) @@ -105,15 +110,16 @@ namespace EasyAbp.EShop.Products.Products await _productInventoryRepository.UpdateAsync(productInventory, true); - await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId, + await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId, productInventory.ProductId, productInventory.ProductSkuId, originalInventory, productInventory.Inventory, productInventory.Sold); - + return true; } [UnitOfWork(true)] - public virtual async Task TryReduceInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool increaseSold) + protected virtual async Task TryReduceInventoryAsync(InventoryQueryModel model, + ProductInventory productInventory, int quantity, bool increaseSold) { if (quantity < 0) { @@ -129,7 +135,7 @@ namespace EasyAbp.EShop.Products.Products await _productInventoryRepository.UpdateAsync(productInventory, true); - await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId, + await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId, productInventory.ProductId, productInventory.ProductSkuId, originalInventory, productInventory.Inventory, productInventory.Sold); diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs deleted file mode 100644 index 769a410a..00000000 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace EasyAbp.EShop.Products.Products -{ - public interface IProductInventoryProvider - { - Task GetInventoryDataAsync(Product product, ProductSku productSku); - - Task> GetInventoryDataDictionaryAsync(Product product); - - Task TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold); - - Task TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold); - } -} \ No newline at end of file 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 6ae5c05f..884fe3e5 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 @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using EasyAbp.EShop.Products.ProductInventories; using Volo.Abp.Domain.Services; namespace EasyAbp.EShop.Products.Products diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs index 4c5f24da..1e16d1ba 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using EasyAbp.EShop.Orders.Orders; +using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.EventBus.Distributed; @@ -15,6 +16,7 @@ namespace EasyAbp.EShop.Products.Products private readonly ICurrentTenant _currentTenant; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IDistributedEventBus _distributedEventBus; + private readonly ILogger _logger; private readonly IProductRepository _productRepository; private readonly IProductManager _productManager; @@ -22,12 +24,14 @@ namespace EasyAbp.EShop.Products.Products ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, IDistributedEventBus distributedEventBus, + ILogger logger, IProductRepository productRepository, IProductManager productManager) { _currentTenant = currentTenant; _unitOfWorkManager = unitOfWorkManager; _distributedEventBus = distributedEventBus; + _logger = logger; _productRepository = productRepository; _productManager = productManager; } @@ -65,33 +69,52 @@ namespace EasyAbp.EShop.Products.Products return; } - models.Add(new ConsumeInventoryModel - { - Product = product, - ProductSku = productSku, - StoreId = eventData.Entity.StoreId, - Quantity = orderLine.Quantity - }); + models.Add(new ConsumeInventoryModel( + product, productSku, eventData.Entity.StoreId, orderLine.Quantity)); + } foreach (var model in models) { - if (await _productManager.TryReduceInventoryAsync(model.Product, model.ProductSku, model.Quantity, true)) + if (await _productManager.TryReduceInventoryAsync( + model.Product, model.ProductSku, model.Quantity, true)) { continue; } - // Todo: should release unused inventory since (external) inventory providers may not be transactional. + await TryRollbackInventoriesAsync(models); + await _unitOfWorkManager.Current.RollbackAsync(); - + await PublishInventoryReductionResultEventAsync(eventData, false, true); - + return; } await PublishInventoryReductionResultEventAsync(eventData, true); } + protected virtual async Task TryRollbackInventoriesAsync(IEnumerable models) + { + var result = true; + + foreach (var model in models.Where(x => x.Consumed)) + { + if (await _productManager.TryIncreaseInventoryAsync( + model.Product, model.ProductSku, model.Quantity, true)) + { + continue; + } + + result = false; + _logger.LogWarning( + "OrderCreatedEventHandler: inventory rollback failed! productId = {productId}, productSkuId = {productSkuId}, quantity = {quantity}, reduceSold = {reduceSold}", + model.Product.Id, model.ProductSku.Id, model.Quantity, true); + } + + return result; + } + protected virtual async Task PublishInventoryReductionResultEventAsync(EntityCreatedEto orderCreatedEto, bool isSuccess, bool publishNow = false) { await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPlacedResultEto diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs index 3f561dd6..0d2a82fc 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using EasyAbp.EShop.Orders.Orders; +using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; using Volo.Abp.MultiTenancy; @@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Products.Products { private readonly ICurrentTenant _currentTenant; private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly ILogger _logger; private readonly IDistributedEventBus _distributedEventBus; private readonly IProductRepository _productRepository; private readonly IProductManager _productManager; @@ -20,24 +22,26 @@ namespace EasyAbp.EShop.Products.Products public OrderPaidEventHandler( ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, + ILogger logger, IDistributedEventBus distributedEventBus, IProductRepository productRepository, IProductManager productManager) { _currentTenant = currentTenant; _unitOfWorkManager = unitOfWorkManager; + _logger = logger; _distributedEventBus = distributedEventBus; _productRepository = productRepository; _productManager = productManager; } - + [UnitOfWork(true)] public virtual async Task HandleEventAsync(OrderPaidEto eventData) { using var changeTenant = _currentTenant.Change(eventData.Order.TenantId); var models = new List(); - + foreach (var orderLine in eventData.Order.OrderLines) { // Todo: Should use ProductHistory. @@ -48,10 +52,10 @@ namespace EasyAbp.EShop.Products.Products if (productSku == null) { await PublishInventoryReductionResultEventAsync(eventData, false); - + return; } - + if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPayment) { continue; @@ -60,37 +64,56 @@ namespace EasyAbp.EShop.Products.Products if (!await _productManager.IsInventorySufficientAsync(product, productSku, orderLine.Quantity)) { await PublishInventoryReductionResultEventAsync(eventData, false); - + return; } - models.Add(new ConsumeInventoryModel - { - Product = product, - ProductSku = productSku, - StoreId = eventData.Order.StoreId, - Quantity = orderLine.Quantity - }); + models.Add(new ConsumeInventoryModel(product, productSku, eventData.Order.StoreId, orderLine.Quantity)); } foreach (var model in models) { - if (await _productManager.TryReduceInventoryAsync(model.Product, model.ProductSku, model.Quantity, true)) + if (await _productManager.TryReduceInventoryAsync( + model.Product, model.ProductSku, model.Quantity, true)) { continue; } + await TryRollbackInventoriesAsync(models); + await _unitOfWorkManager.Current.RollbackAsync(); - + await PublishInventoryReductionResultEventAsync(eventData, false, true); - + return; } await PublishInventoryReductionResultEventAsync(eventData, true); } - protected virtual async Task PublishInventoryReductionResultEventAsync(OrderPaidEto orderPaidEto, bool isSuccess, bool publishNow = false) + protected virtual async Task TryRollbackInventoriesAsync(IEnumerable models) + { + var result = true; + + foreach (var model in models.Where(x => x.Consumed)) + { + if (await _productManager.TryIncreaseInventoryAsync( + model.Product, model.ProductSku, model.Quantity, true)) + { + continue; + } + + result = false; + _logger.LogWarning( + "OrderPaidEventHandler: inventory rollback failed! productId = {productId}, productSkuId = {productSkuId}, quantity = {quantity}, reduceSold = {reduceSold}", + model.Product.Id, model.ProductSku.Id, model.Quantity, true); + } + + return result; + } + + protected virtual async Task PublishInventoryReductionResultEventAsync(OrderPaidEto orderPaidEto, + bool isSuccess, bool publishNow = false) { await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPaidResultEto { 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 abfce434..35777235 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 @@ -5,6 +5,7 @@ using System.Threading.Tasks; using EasyAbp.EShop.Products.Options.ProductGroups; using EasyAbp.EShop.Products.ProductCategories; using EasyAbp.EShop.Products.ProductDetails; +using EasyAbp.EShop.Products.ProductInventories; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Services; @@ -44,9 +45,9 @@ namespace EasyAbp.EShop.Products.Products public virtual async Task CreateAsync(Product product, IEnumerable categoryIds = null) { product.TrimUniqueName(); - + await CheckProductGroupNameAsync(product); - + await CheckProductUniqueNameAsync(product); await _productRepository.InsertAsync(product, autoSave: true); @@ -64,7 +65,7 @@ namespace EasyAbp.EShop.Products.Products { throw new NonexistentProductGroupException(product.ProductGroupName); } - + return Task.CompletedTask; } @@ -91,7 +92,7 @@ namespace EasyAbp.EShop.Products.Products await _productRepository.DeleteAsync(product, true); } - + [UnitOfWork(true)] public virtual async Task DeleteAsync(Guid id) { @@ -108,13 +109,13 @@ namespace EasyAbp.EShop.Products.Products await CheckSkuAttributeOptionsAsync(product, productSku); await CheckProductSkuNameUniqueAsync(product, productSku); - + productSku.TrimName(); - + product.ProductSkus.AddIfNotContains(productSku); - + await CheckProductDetailAsync(product); - + return await _productRepository.UpdateAsync(product, true); } @@ -124,9 +125,9 @@ namespace EasyAbp.EShop.Products.Products { return Task.CompletedTask; } - + if (product.ProductSkus.Where(sku => sku.Id != productSku.Id) - .FirstOrDefault(sku => sku.Name == productSku.Name) != null) + .FirstOrDefault(sku => sku.Name == productSku.Name) != null) { throw new ProductSkuCodeDuplicatedException(product.Id, productSku.Name); } @@ -169,7 +170,7 @@ namespace EasyAbp.EShop.Products.Products public virtual async Task DeleteSkuAsync(Product product, ProductSku productSku) { product.ProductSkus.Remove(productSku); - + return await _productRepository.UpdateAsync(product, true); } @@ -178,20 +179,20 @@ namespace EasyAbp.EShop.Products.Products { await _productRepository.CheckUniqueNameAsync(product); } - + protected virtual async Task CheckProductDetailAsync(Product product) { if (product.ProductDetailId.HasValue) { await CheckProductDetailExistAsync(product.ProductDetailId.Value, product.StoreId); } - + foreach (var sku in product.ProductSkus.Where(x => x.ProductDetailId.HasValue)) { await CheckProductDetailExistAsync(sku.ProductDetailId!.Value, product.StoreId); } } - + [UnitOfWork] protected virtual async Task CheckProductDetailExistAsync(Guid productDetailId, Guid storeId) { @@ -202,7 +203,7 @@ namespace EasyAbp.EShop.Products.Products throw new EntityNotFoundException(typeof(ProductDetail), productDetailId); } } - + [UnitOfWork(true)] protected virtual async Task UpdateProductCategoriesAsync(Guid productId, IEnumerable categoryIds) { @@ -212,7 +213,7 @@ namespace EasyAbp.EShop.Products.Products { return; } - + foreach (var categoryId in categoryIds) { await _productCategoryRepository.InsertAsync( @@ -222,24 +223,34 @@ namespace EasyAbp.EShop.Products.Products public virtual async Task IsInventorySufficientAsync(Product product, ProductSku productSku, int quantity) { - var inventoryData = await _productInventoryProvider.GetInventoryDataAsync(product, productSku); - + var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id); + + var inventoryData = await _productInventoryProvider.GetInventoryDataAsync(model); + return product.InventoryStrategy == InventoryStrategy.NoNeed || inventoryData.Inventory - quantity >= 0; } public virtual async Task GetInventoryDataAsync(Product product, ProductSku productSku) { - return await _productInventoryProvider.GetInventoryDataAsync(product, productSku); + var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id); + + return await _productInventoryProvider.GetInventoryDataAsync(model); } - public virtual async Task TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool reduceSold) + public virtual async Task TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, + bool reduceSold) { - return await _productInventoryProvider.TryIncreaseInventoryAsync(product, productSku, quantity, reduceSold); + var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id); + + return await _productInventoryProvider.TryIncreaseInventoryAsync(model, quantity, reduceSold); } - public virtual async Task TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold) + public virtual async Task TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, + bool increaseSold) { - return await _productInventoryProvider.TryReduceInventoryAsync(product, productSku, quantity, increaseSold); + var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id); + + return await _productInventoryProvider.TryReduceInventoryAsync(model, quantity, increaseSold); } public virtual async Task GetRealPriceAsync(Product product, ProductSku productSku) @@ -247,7 +258,7 @@ namespace EasyAbp.EShop.Products.Products var price = await _productPriceProvider.GetPriceAsync(product, productSku); var discountedPrice = price; - + // Todo: provider execution ordering. foreach (var provider in LazyServiceProvider.LazyGetService>()) { diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs index 4001e37b..a9c380cc 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using EasyAbp.EShop.Products.EntityFrameworkCore; -using EasyAbp.EShop.Products.Products; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -29,9 +28,9 @@ namespace EasyAbp.EShop.Products.ProductInventories .FirstOrDefaultAsync(cancellationToken); } - public async Task> GetInventoryDataDictionaryAsync(List productSkuIds, CancellationToken cancellationToken = default) + public async Task> GetSkuIdInventoryDataMappingAsync(List productSkuIds, CancellationToken cancellationToken = default) { - return await GetQueryable() + return await (await GetQueryableAsync()) .Where(x => productSkuIds.Contains(x.ProductSkuId)) .ToDictionaryAsync(x => x.ProductSkuId, x => new InventoryDataModel {