Browse Source

Introduce inventory rollback feature and refactor IProductInventoryProvider

pull/158/head
gdlcf88 4 years ago
parent
commit
4545ef0087
  1. 13
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs
  2. 102
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  3. 17
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
  4. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs
  5. 28
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs
  6. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs
  7. 23
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs
  8. 58
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
  9. 17
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs
  10. 1
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs
  11. 45
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
  12. 55
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
  13. 59
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  14. 5
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs

13
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);

102
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<Product, ProductDto, Guid, GetProductListInput, CreateUpdateProductDto, CreateUpdateProductDto>, IProductAppService
public class ProductAppService :
MultiStoreCrudAppService<Product, ProductDto, Guid, GetProductListInput, CreateUpdateProductDto,
CreateUpdateProductDto>, 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<ProductDto> 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<Guid>();
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<ProductDto> 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<ProductDto> 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<ProductDto> CreateSkuAsync(Guid productId, CreateProductSkuDto input)
{
var product = await GetEntityByIdAsync(productId);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
CheckProductIsNotStatic(product);
var sku = ObjectMapper.Map<CreateProductSkuDto, ProductSku>(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;
}

17
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<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model);
Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(IList<InventoryQueryModel> models);
Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold);
Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold);
}
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryDataModel.cs → 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
{

28
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;
}
}

2
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<InventoryDataModel> GetInventoryDataAsync(Guid productSkuId, CancellationToken cancellationToken = default);
Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default);
Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default);
}
}

23
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;
}
}
}

58
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<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku)
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model)
{
return await _productInventoryRepository.GetInventoryDataAsync(productSku.Id);
return await _productInventoryRepository.GetInventoryDataAsync(model.ProductSkuId);
}
[UnitOfWork]
public virtual async Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(Product product)
public virtual async Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(
IList<InventoryQueryModel> 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<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold)
public virtual async Task<bool> 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<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold)
public virtual async Task<bool> 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<ProductInventory> GetOrCreateProductInventoryAsync(Guid productId, Guid productSkuId)
protected virtual async Task<ProductInventory> 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<bool> TryIncreaseInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool decreaseSold)
protected virtual async Task<bool> 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<bool> TryReduceInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool increaseSold)
protected virtual async Task<bool> 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);

17
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Products.Products
{
public interface IProductInventoryProvider
{
Task<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku);
Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(Product product);
Task<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold);
Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold);
}
}

1
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

45
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<OrderCreatedEventHandler> _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<OrderCreatedEventHandler> 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<bool> TryRollbackInventoriesAsync(IEnumerable<ConsumeInventoryModel> 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<OrderEto> orderCreatedEto, bool isSuccess, bool publishNow = false)
{
await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPlacedResultEto

55
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<OrderPaidEventHandler> _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<OrderPaidEventHandler> 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<ConsumeInventoryModel>();
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<bool> TryRollbackInventoriesAsync(IEnumerable<ConsumeInventoryModel> 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
{

59
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<Product> CreateAsync(Product product, IEnumerable<Guid> 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<Product> 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<Guid> 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<bool> 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<InventoryDataModel> 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<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool reduceSold)
public virtual async Task<bool> 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<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold)
public virtual async Task<bool> 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<PriceDataModel> 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<IEnumerable<IProductDiscountProvider>>())
{

5
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<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default)
public async Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default)
{
return await GetQueryable()
return await (await GetQueryableAsync())
.Where(x => productSkuIds.Contains(x.ProductSkuId))
.ToDictionaryAsync(x => x.ProductSkuId, x => new InventoryDataModel
{

Loading…
Cancel
Save