Browse Source

Merge branch 'dev' into basket-item-product-info-updater

# Conflicts:
#	EShop.sln
pull/190/head
gdlcf88 4 years ago
parent
commit
35f8616b40
  1. 4
      Directory.Build.props
  2. 229
      EShop.sln
  3. 2
      common.props
  4. 7
      docs/README.md
  5. 46
      docs/plugins/flash-sales/README.md
  6. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs
  7. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
  8. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj
  9. 4
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/EShopOrdersDomainSharedModule.cs
  10. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
  11. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
  12. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
  13. 41
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs
  14. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs
  15. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs
  16. 80
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  17. 20
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderCreatedEventHandler.cs
  18. 1
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
  19. 2
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  20. 34
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs
  21. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
  22. 1
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj
  23. 45
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
  24. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs
  25. 11
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs
  26. 21
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
  27. 12
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
  28. 11
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs
  29. 8
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  30. 10
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs
  31. 223
      plugins/FlashSales/.editorconfig
  32. 1
      plugins/FlashSales/README.md
  33. 1
      plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.abppkg.json
  34. 10
      plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.csproj
  35. 9
      plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp/EShop/Plugins/FlashSales/MultiTenancy/MultiTenancyConsts.cs
  36. 3
      plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xml
  37. 30
      plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xsd
  38. 15
      plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj
  39. 162
      plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandler.cs
  40. 24
      plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationModule.cs
  41. 3
      plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xml
  42. 30
      plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xsd
  43. 3
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.abppkg.json
  44. 16
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj
  45. 15
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationContractsModule.cs
  46. 8
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesRemoteServiceConsts.cs
  47. 15
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSaleOrderResultDto.cs
  48. 21
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanCreateDto.cs
  49. 23
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanDto.cs
  50. 20
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanGetListInput.cs
  51. 11
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanPreOrderDto.cs
  52. 21
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanUpdateDto.cs
  53. 8
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/OrderFlashSalePlanInput.cs
  54. 19
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanAppService.cs
  55. 22
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultDto.cs
  56. 17
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultGetListInput.cs
  57. 15
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultAppService.cs
  58. 30
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissionDefinitionProvider.cs
  59. 31
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissions.cs
  60. 3
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml
  61. 30
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd
  62. 3
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.abppkg.json
  63. 23
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.csproj
  64. 33
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationModule.cs
  65. 106
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandler.cs
  66. 452
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppService.cs
  67. 33
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheInvalidator.cs
  68. 11
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheItem.cs
  69. 20
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanTokenCacheItem.cs
  70. 13
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductIsNotInThisStoreException.cs
  71. 12
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductSkuIsNotFoundException.cs
  72. 13
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/RelatedFlashSaleResultsExistException.cs
  73. 12
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/UnexpectedInventoryStrategyException.cs
  74. 132
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEventHandler.cs
  75. 59
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCache.cs
  76. 13
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCacheItem.cs
  77. 88
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultAppService.cs
  78. 13
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleCurrentResultCache.cs
  79. 13
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesAppService.cs
  80. 33
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesApplicationAutoMapperProfile.cs
  81. 16
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/Options/FlashSalesOptions.cs
  82. 33
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryManager.cs
  83. 46
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheInvalidator.cs
  84. 10
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheItem.cs
  85. 3
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xml
  86. 30
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xsd
  87. 3
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.abppkg.json
  88. 28
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj
  89. 36
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainSharedModule.cs
  90. 21
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/CreateFlashSaleOrderEto.cs
  91. 32
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEto.cs
  92. 23
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEto.cs
  93. 31
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanHasher.cs
  94. 9
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanHasher.cs
  95. 26
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEto.cs
  96. 6
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultFailedReason.cs
  97. 8
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatus.cs
  98. 34
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalesErrorCodes.cs
  99. 23
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FullAuditedEntityEto.cs
  100. 9
      plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/FlashSalesResource.cs

4
Directory.Build.props

@ -3,9 +3,9 @@
<AbpVersion>5.3.1</AbpVersion>
<EasyAbpAbpTreesModuleVersion>2.7.1</EasyAbpAbpTreesModuleVersion>
<EasyAbpPaymentServiceModuleVersion>2.0.14</EasyAbpPaymentServiceModuleVersion>
<EasyAbpPaymentServiceModuleVersion>2.0.15</EasyAbpPaymentServiceModuleVersion>
<EasyAbpAbpTagHelperPlusModuleVersion>1.0.1</EasyAbpAbpTagHelperPlusModuleVersion>
<EasyAbpBookingServiceModuleVersion>0.1.0</EasyAbpBookingServiceModuleVersion>
<EasyAbpBookingServiceModuleVersion>0.1.1</EasyAbpBookingServiceModuleVersion>
<DaprSdkVersion>1.7.0</DaprSdkVersion>
<OrleansVersion>3.6.2</OrleansVersion>
<NodaMoneyVersion>1.0.5</NodaMoneyVersion>

229
EShop.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29020.237
# Visual Studio Version 17
VisualStudioVersion = 17.2.32526.322
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EShopSample.Domain", "samples\EShopSample\aspnet-core\src\EShopSample.Domain\EShopSample.Domain.csproj", "{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}"
EndProject
@ -303,77 +303,77 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{D261
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EShopSample", "EShopSample", "{FE0A5FD6-AEE7-4738-9A6D-261BF086704D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Stores.Application.Shared", "modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Shared\EasyAbp.EShop.Stores.Application.Shared.csproj", "{06700175-4E04-4993-A1FB-24E7A8835E19}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Stores.Application.Shared", "modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Shared\EasyAbp.EShop.Stores.Application.Shared.csproj", "{06700175-4E04-4993-A1FB-24E7A8835E19}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{72F34527-9295-4F29-923E-4B075A4F31A2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Application", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Application\EasyAbp.EShop.Plugins.Coupons.Application.csproj", "{1EB6504F-7FF0-4628-9D7C-81BDE33D9490}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Application", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Application\EasyAbp.EShop.Plugins.Coupons.Application.csproj", "{1EB6504F-7FF0-4628-9D7C-81BDE33D9490}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Application.Contracts", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Application.Contracts\EasyAbp.EShop.Plugins.Coupons.Application.Contracts.csproj", "{C7D05698-E509-4F5C-A792-0036190AE6FF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Application.Contracts", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Application.Contracts\EasyAbp.EShop.Plugins.Coupons.Application.Contracts.csproj", "{C7D05698-E509-4F5C-A792-0036190AE6FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Domain", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Domain\EasyAbp.EShop.Plugins.Coupons.Domain.csproj", "{4DBC9216-80FD-4E71-BCB3-0F1ADA63AEC2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Domain", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Domain\EasyAbp.EShop.Plugins.Coupons.Domain.csproj", "{4DBC9216-80FD-4E71-BCB3-0F1ADA63AEC2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Domain.Shared", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Domain.Shared\EasyAbp.EShop.Plugins.Coupons.Domain.Shared.csproj", "{31F1DC02-5790-4FAD-A8F2-0D6F9FE21F00}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Domain.Shared", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Domain.Shared\EasyAbp.EShop.Plugins.Coupons.Domain.Shared.csproj", "{31F1DC02-5790-4FAD-A8F2-0D6F9FE21F00}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.csproj", "{EE6B9D10-7038-4CDE-A6BF-3941AFD84D7A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.csproj", "{EE6B9D10-7038-4CDE-A6BF-3941AFD84D7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.HttpApi\EasyAbp.EShop.Plugins.Coupons.HttpApi.csproj", "{0E22DE0A-032B-442E-AEB2-E299B333BADC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.HttpApi\EasyAbp.EShop.Plugins.Coupons.HttpApi.csproj", "{0E22DE0A-032B-442E-AEB2-E299B333BADC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi.Client", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.csproj", "{85D39FAB-A9A8-4942-BF94-A6061FCFBF7A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi.Client", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.csproj", "{85D39FAB-A9A8-4942-BF94-A6061FCFBF7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.MongoDB", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.MongoDB\EasyAbp.EShop.Plugins.Coupons.MongoDB.csproj", "{C97330F5-4634-4865-9D7D-E33F87808938}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.MongoDB", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.MongoDB\EasyAbp.EShop.Plugins.Coupons.MongoDB.csproj", "{C97330F5-4634-4865-9D7D-E33F87808938}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Web", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Web\EasyAbp.EShop.Plugins.Coupons.Web.csproj", "{C11BD71D-FA6B-4833-9CAE-82ED9D857A84}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Web", "plugins\Coupons\src\EasyAbp.EShop.Plugins.Coupons.Web\EasyAbp.EShop.Plugins.Coupons.Web.csproj", "{C11BD71D-FA6B-4833-9CAE-82ED9D857A84}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4001814E-A67B-490D-9E13-2FB9A34B0A0B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Application.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.Application.Tests\EasyAbp.EShop.Plugins.Coupons.Application.Tests.csproj", "{10675CED-5689-4B2F-895A-E13820928A3B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Application.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.Application.Tests\EasyAbp.EShop.Plugins.Coupons.Application.Tests.csproj", "{10675CED-5689-4B2F-895A-E13820928A3B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.Domain.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.Domain.Tests\EasyAbp.EShop.Plugins.Coupons.Domain.Tests.csproj", "{B29ABA23-37B2-419A-9E3C-F1A8703D584D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.Domain.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.Domain.Tests\EasyAbp.EShop.Plugins.Coupons.Domain.Tests.csproj", "{B29ABA23-37B2-419A-9E3C-F1A8703D584D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests.csproj", "{D81EC4DC-3944-4CFE-875A-5DD07BE487AE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests\EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests.csproj", "{D81EC4DC-3944-4CFE-875A-5DD07BE487AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp.csproj", "{8B22E009-5ABA-4878-83E5-38154F2B8E2F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp\EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp.csproj", "{8B22E009-5ABA-4878-83E5-38154F2B8E2F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests\EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests.csproj", "{86CAD303-A0E5-42C9-89A5-61D9AAA4AD8F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests\EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests.csproj", "{86CAD303-A0E5-42C9-89A5-61D9AAA4AD8F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupons.TestBase", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.TestBase\EasyAbp.EShop.Plugins.Coupons.TestBase.csproj", "{B076C103-DF0B-464B-A9CB-4BE5CAFEE067}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Coupons.TestBase", "plugins\Coupons\test\EasyAbp.EShop.Plugins.Coupons.TestBase\EasyAbp.EShop.Plugins.Coupons.TestBase.csproj", "{B076C103-DF0B-464B-A9CB-4BE5CAFEE067}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Orders.Plugins.Coupons", "plugins\Coupons\src\EasyAbp.EShop.Orders.Plugins.Coupons\EasyAbp.EShop.Orders.Plugins.Coupons.csproj", "{3C385657-8365-470F-9F4F-30F31F9FCA42}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Orders.Plugins.Coupons", "plugins\Coupons\src\EasyAbp.EShop.Orders.Plugins.Coupons\EasyAbp.EShop.Orders.Plugins.Coupons.csproj", "{3C385657-8365-470F-9F4F-30F31F9FCA42}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Inventories", "Inventories", "{9AC27747-E175-487F-92C9-434DEE543273}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DaprActors", "DaprActors", "{6E6FE4B9-4117-4F57-B219-EE47E4046096}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Products.DaprActorsInventory.Domain\EasyAbp.EShop.Products.DaprActorsInventory.Domain.csproj", "{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Products.DaprActorsInventory.Domain\EasyAbp.EShop.Products.DaprActorsInventory.Domain.csproj", "{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj", "{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj", "{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors\EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj", "{B6F3ACD5-463E-4455-A094-057A82941A94}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors\EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj", "{B6F3ACD5-463E-4455-A094-057A82941A94}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F468A386-5660-4888-981A-6ECF15182D32}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{485204B1-7603-4EA0-B3A4-73CB89B0D5BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests", "plugins\Inventories\DaprActors\test\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests.csproj", "{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests", "plugins\Inventories\DaprActors\test\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests.csproj", "{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore.csproj", "{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore.csproj", "{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.csproj", "{83F6434F-74DC-4389-870D-46510E28C029}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.csproj", "{83F6434F-74DC-4389-870D-46510E28C029}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OrleansGrains", "OrleansGrains", "{88D17635-75D7-48A1-B622-E6FB3DCACEF8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8E978749-7972-4703-8A94-6A90080C78DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions.csproj", "{AB3477DB-3457-4167-A086-BAD104D69604}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions.csproj", "{AB3477DB-3457-4167-A086-BAD104D69604}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo.csproj", "{0D613460-A0AD-4EAF-B719-785FE65E97E8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo.csproj", "{0D613460-A0AD-4EAF-B719-785FE65E97E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.csproj", "{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.csproj", "{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F58B6EEF-5AFF-4B79-BC71-A2D8C71F5E77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests", "plugins\Inventories\OrleansGrains\test\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests.csproj", "{D652EBF0-27CA-44C2-BB78-F446B87377C7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests", "plugins\Inventories\OrleansGrains\test\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests.csproj", "{D652EBF0-27CA-44C2-BB78-F446B87377C7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Booking", "Booking", "{CE945F1D-6636-47D5-A619-C16C4E14CF8C}"
EndProject
@ -419,6 +419,56 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Payments.Book
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Baskets.Booking.Application", "plugins\Booking\src\EasyAbp.EShop.Plugins.Baskets.Booking.Application\EasyAbp.EShop.Plugins.Baskets.Booking.Application.csproj", "{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FlashSales", "FlashSales", "{867AD9F8-FD56-469D-A90D-C569EB9C3D2A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Application", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Application\EasyAbp.EShop.Plugins.FlashSales.Application.csproj", "{26611C4C-6910-498A-9FBA-BECC09392ADE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Application.Contracts", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj", "{FCD72398-F832-4914-8ACF-EA4C1DD24BBF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Domain.Shared", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj", "{55145D4A-5A98-4B53-9293-CCB7236E69F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Domain", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Domain\EasyAbp.EShop.Plugins.FlashSales.Domain.csproj", "{7E535D1A-3259-4098-8E47-A81B3D8B3920}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore\EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.csproj", "{2D1C0E98-D86A-4C60-98CE-86C4E5571B91}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.MongoDB", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.MongoDB\EasyAbp.EShop.Plugins.FlashSales.MongoDB.csproj", "{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.HttpApi", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.HttpApi\EasyAbp.EShop.Plugins.FlashSales.HttpApi.csproj", "{0D5558BD-A22F-4565-A92A-BB14D517B1A5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F29C5BCD-E6C0-4556-A631-CACA41B1050B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9C180C9E-50E9-4624-BE06-5C8C24A028E4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client\EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.csproj", "{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Web", "plugins\FlashSales\src\EasyAbp.EShop.Plugins.FlashSales.Web\EasyAbp.EShop.Plugins.FlashSales.Web.csproj", "{6F06F221-1371-433A-8C90-3123B5EF32AD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Application.Tests", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.Application.Tests\EasyAbp.EShop.Plugins.FlashSales.Application.Tests.csproj", "{6EBE36C7-53DB-4194-BDDB-199F789F44E6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.Domain.Tests", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.Domain.Tests\EasyAbp.EShop.Plugins.FlashSales.Domain.Tests.csproj", "{654C320E-78BD-4074-AC2D-154E95457BBC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.Tests", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.Tests\EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.Tests.csproj", "{D2AAFF29-3F7B-4C90-AA99-AC686393B670}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.ConsoleTestApp", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.ConsoleTestApp\EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.ConsoleTestApp.csproj", "{2291B087-FEC5-47BD-A7ED-04725D54E6A6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.MongoDB.Tests", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.MongoDB.Tests\EasyAbp.EShop.Plugins.FlashSales.MongoDB.Tests.csproj", "{68993D65-2C9C-438F-8B94-15E98C469A16}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.FlashSales.TestBase", "plugins\FlashSales\test\EasyAbp.EShop.Plugins.FlashSales.TestBase\EasyAbp.EShop.Plugins.FlashSales.TestBase.csproj", "{417AB8E2-1488-4814-9699-3B189D1ABA67}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Orders.Plugins.FlashSales.Application", "plugins\FlashSales\src\EasyAbp.EShop.Orders.Plugins.FlashSales.Application\EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj", "{5732E880-CB72-49A0-AC4F-A0620F4E4D16}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.Plugins.FlashSales.Application", "plugins\FlashSales\src\EasyAbp.EShop.Products.Plugins.FlashSales.Application\EasyAbp.EShop.Products.Plugins.FlashSales.Application.csproj", "{6AD2F468-D86C-4F9A-B280-3BCC15661C47}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions", "plugins\FlashSales\src\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj", "{C1EB9DC0-F572-41DF-9716-893B154BBB13}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts", "plugins\FlashSales\src\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj", "{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi", "plugins\FlashSales\src\EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi\EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.csproj", "{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client", "plugins\FlashSales\src\EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client\EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client.csproj", "{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests", "plugins\FlashSales\test\EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests\EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.csproj", "{17A3486C-1845-4B4E-B1A6-752106F0C309}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1089,6 +1139,94 @@ Global
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14}.Release|Any CPU.Build.0 = Release|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26611C4C-6910-498A-9FBA-BECC09392ADE}.Release|Any CPU.Build.0 = Release|Any CPU
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF}.Release|Any CPU.Build.0 = Release|Any CPU
{55145D4A-5A98-4B53-9293-CCB7236E69F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55145D4A-5A98-4B53-9293-CCB7236E69F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55145D4A-5A98-4B53-9293-CCB7236E69F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55145D4A-5A98-4B53-9293-CCB7236E69F5}.Release|Any CPU.Build.0 = Release|Any CPU
{7E535D1A-3259-4098-8E47-A81B3D8B3920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E535D1A-3259-4098-8E47-A81B3D8B3920}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E535D1A-3259-4098-8E47-A81B3D8B3920}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E535D1A-3259-4098-8E47-A81B3D8B3920}.Release|Any CPU.Build.0 = Release|Any CPU
{2D1C0E98-D86A-4C60-98CE-86C4E5571B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D1C0E98-D86A-4C60-98CE-86C4E5571B91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D1C0E98-D86A-4C60-98CE-86C4E5571B91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D1C0E98-D86A-4C60-98CE-86C4E5571B91}.Release|Any CPU.Build.0 = Release|Any CPU
{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE}.Release|Any CPU.Build.0 = Release|Any CPU
{0D5558BD-A22F-4565-A92A-BB14D517B1A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D5558BD-A22F-4565-A92A-BB14D517B1A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D5558BD-A22F-4565-A92A-BB14D517B1A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D5558BD-A22F-4565-A92A-BB14D517B1A5}.Release|Any CPU.Build.0 = Release|Any CPU
{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C}.Release|Any CPU.Build.0 = Release|Any CPU
{6F06F221-1371-433A-8C90-3123B5EF32AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F06F221-1371-433A-8C90-3123B5EF32AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F06F221-1371-433A-8C90-3123B5EF32AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F06F221-1371-433A-8C90-3123B5EF32AD}.Release|Any CPU.Build.0 = Release|Any CPU
{6EBE36C7-53DB-4194-BDDB-199F789F44E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EBE36C7-53DB-4194-BDDB-199F789F44E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EBE36C7-53DB-4194-BDDB-199F789F44E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6EBE36C7-53DB-4194-BDDB-199F789F44E6}.Release|Any CPU.Build.0 = Release|Any CPU
{654C320E-78BD-4074-AC2D-154E95457BBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{654C320E-78BD-4074-AC2D-154E95457BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{654C320E-78BD-4074-AC2D-154E95457BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{654C320E-78BD-4074-AC2D-154E95457BBC}.Release|Any CPU.Build.0 = Release|Any CPU
{D2AAFF29-3F7B-4C90-AA99-AC686393B670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2AAFF29-3F7B-4C90-AA99-AC686393B670}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2AAFF29-3F7B-4C90-AA99-AC686393B670}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2AAFF29-3F7B-4C90-AA99-AC686393B670}.Release|Any CPU.Build.0 = Release|Any CPU
{2291B087-FEC5-47BD-A7ED-04725D54E6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2291B087-FEC5-47BD-A7ED-04725D54E6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2291B087-FEC5-47BD-A7ED-04725D54E6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2291B087-FEC5-47BD-A7ED-04725D54E6A6}.Release|Any CPU.Build.0 = Release|Any CPU
{68993D65-2C9C-438F-8B94-15E98C469A16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68993D65-2C9C-438F-8B94-15E98C469A16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68993D65-2C9C-438F-8B94-15E98C469A16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68993D65-2C9C-438F-8B94-15E98C469A16}.Release|Any CPU.Build.0 = Release|Any CPU
{417AB8E2-1488-4814-9699-3B189D1ABA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{417AB8E2-1488-4814-9699-3B189D1ABA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{417AB8E2-1488-4814-9699-3B189D1ABA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{417AB8E2-1488-4814-9699-3B189D1ABA67}.Release|Any CPU.Build.0 = Release|Any CPU
{5732E880-CB72-49A0-AC4F-A0620F4E4D16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5732E880-CB72-49A0-AC4F-A0620F4E4D16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5732E880-CB72-49A0-AC4F-A0620F4E4D16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5732E880-CB72-49A0-AC4F-A0620F4E4D16}.Release|Any CPU.Build.0 = Release|Any CPU
{6AD2F468-D86C-4F9A-B280-3BCC15661C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AD2F468-D86C-4F9A-B280-3BCC15661C47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AD2F468-D86C-4F9A-B280-3BCC15661C47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AD2F468-D86C-4F9A-B280-3BCC15661C47}.Release|Any CPU.Build.0 = Release|Any CPU
{C1EB9DC0-F572-41DF-9716-893B154BBB13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1EB9DC0-F572-41DF-9716-893B154BBB13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1EB9DC0-F572-41DF-9716-893B154BBB13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1EB9DC0-F572-41DF-9716-893B154BBB13}.Release|Any CPU.Build.0 = Release|Any CPU
{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9}.Release|Any CPU.Build.0 = Release|Any CPU
{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7}.Release|Any CPU.Build.0 = Release|Any CPU
{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8}.Release|Any CPU.Build.0 = Release|Any CPU
{17A3486C-1845-4B4E-B1A6-752106F0C309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17A3486C-1845-4B4E-B1A6-752106F0C309}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17A3486C-1845-4B4E-B1A6-752106F0C309}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17A3486C-1845-4B4E-B1A6-752106F0C309}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1261,16 +1399,16 @@ Global
{3C385657-8365-470F-9F4F-30F31F9FCA42} = {72F34527-9295-4F29-923E-4B075A4F31A2}
{9AC27747-E175-487F-92C9-434DEE543273} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0}
{6E6FE4B9-4117-4F57-B219-EE47E4046096} = {9AC27747-E175-487F-92C9-434DEE543273}
{F468A386-5660-4888-981A-6ECF15182D32} = {6E6FE4B9-4117-4F57-B219-EE47E4046096}
{B6F3ACD5-463E-4455-A094-057A82941A94} = {F468A386-5660-4888-981A-6ECF15182D32}
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291} = {F468A386-5660-4888-981A-6ECF15182D32}
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1} = {F468A386-5660-4888-981A-6ECF15182D32}
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291} = {F468A386-5660-4888-981A-6ECF15182D32}
{B6F3ACD5-463E-4455-A094-057A82941A94} = {F468A386-5660-4888-981A-6ECF15182D32}
{F468A386-5660-4888-981A-6ECF15182D32} = {6E6FE4B9-4117-4F57-B219-EE47E4046096}
{485204B1-7603-4EA0-B3A4-73CB89B0D5BC} = {6E6FE4B9-4117-4F57-B219-EE47E4046096}
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB} = {485204B1-7603-4EA0-B3A4-73CB89B0D5BC}
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884} = {F468A386-5660-4888-981A-6ECF15182D32}
{83F6434F-74DC-4389-870D-46510E28C029} = {8E978749-7972-4703-8A94-6A90080C78DE}
{88D17635-75D7-48A1-B622-E6FB3DCACEF8} = {9AC27747-E175-487F-92C9-434DEE543273}
{8E978749-7972-4703-8A94-6A90080C78DE} = {88D17635-75D7-48A1-B622-E6FB3DCACEF8}
{83F6434F-74DC-4389-870D-46510E28C029} = {8E978749-7972-4703-8A94-6A90080C78DE}
{AB3477DB-3457-4167-A086-BAD104D69604} = {8E978749-7972-4703-8A94-6A90080C78DE}
{0D613460-A0AD-4EAF-B719-785FE65E97E8} = {8E978749-7972-4703-8A94-6A90080C78DE}
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD} = {8E978749-7972-4703-8A94-6A90080C78DE}
@ -1298,6 +1436,31 @@ Global
{C0399352-1278-4D91-8D4E-7491FD77C18B} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{4480BFAF-C981-4242-A509-EDA6F572E45C} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{F9FD36E6-0ED0-4649-8E1C-33B2CDE85B14} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}
{867AD9F8-FD56-469D-A90D-C569EB9C3D2A} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0}
{26611C4C-6910-498A-9FBA-BECC09392ADE} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{FCD72398-F832-4914-8ACF-EA4C1DD24BBF} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{55145D4A-5A98-4B53-9293-CCB7236E69F5} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{7E535D1A-3259-4098-8E47-A81B3D8B3920} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{2D1C0E98-D86A-4C60-98CE-86C4E5571B91} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{B5ED3D8B-E2DC-4B38-A9C7-2A18B47385EE} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{0D5558BD-A22F-4565-A92A-BB14D517B1A5} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{F29C5BCD-E6C0-4556-A631-CACA41B1050B} = {867AD9F8-FD56-469D-A90D-C569EB9C3D2A}
{9C180C9E-50E9-4624-BE06-5C8C24A028E4} = {867AD9F8-FD56-469D-A90D-C569EB9C3D2A}
{6658CE7D-CA7D-47A7-8B9E-6D8776CE607C} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{6F06F221-1371-433A-8C90-3123B5EF32AD} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{6EBE36C7-53DB-4194-BDDB-199F789F44E6} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{654C320E-78BD-4074-AC2D-154E95457BBC} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{D2AAFF29-3F7B-4C90-AA99-AC686393B670} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{2291B087-FEC5-47BD-A7ED-04725D54E6A6} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{68993D65-2C9C-438F-8B94-15E98C469A16} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{417AB8E2-1488-4814-9699-3B189D1ABA67} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
{5732E880-CB72-49A0-AC4F-A0620F4E4D16} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{6AD2F468-D86C-4F9A-B280-3BCC15661C47} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{C1EB9DC0-F572-41DF-9716-893B154BBB13} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{F08D9409-4D01-4639-A7B8-A70B7ED8E0F9} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{B137BF4B-8C0A-4CE2-AF22-BD9BD29C86B7} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{274769DC-5DD6-4CFD-8078-5E0E0CE8D6D8} = {F29C5BCD-E6C0-4556-A631-CACA41B1050B}
{17A3486C-1845-4B4E-B1A6-752106F0C309} = {9C180C9E-50E9-4624-BE06-5C8C24A028E4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F}

2
common.props

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>3.0.0-preview.2</Version>
<Version>3.0.0-preview.18</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>EasyAbp Team</Authors>

7
docs/README.md

@ -80,7 +80,7 @@ We have launched an online demo for this module: [https://eshop.samples.easyabp.
## Advanced Usages
We can customize some features to use EShop in complex application scenarios.
We can customize the EShop for complex application scenarios.
### Gift Card Shop
@ -89,8 +89,8 @@ We can customize some features to use EShop in complex application scenarios.
* Read the article to learn how to implement it. (todo)
### Paid Knowledge Market
* Also use the [EasyAbp.SharedResources](https://easyabp.io/modules/SharedResources/) module.
* The carrier of knowledge can be articles, pictures, audios, videos, files, streams, etc.
* Install the [EasyAbp.SharedResources](https://easyabp.io/modules/SharedResources/) module.
* The carrier of knowledge can be articles, pictures, audio, videos, files, streams, etc.
* When a knowledge order is paid, automatically authorize the customer to access the resources he purchased.
* Read the article to learn how to implement it. (todo)
@ -109,6 +109,7 @@ We can customize some features to use EShop in complex application scenarios.
* Inventories
* [DaprActors](https://github.com/EasyAbp/EShop/tree/dev/plugins/Inventories/DaprActors)
* [OrleansGrains](https://github.com/EasyAbp/EShop/tree/dev/plugins/Inventories/OrleansGrains)
* [FlashSales](https://github.com/EasyAbp/EShop/tree/dev/plugins/FlashSales)
* [Booking](https://github.com/EasyAbp/EShop/tree/dev/plugins/Booking)
## Roadmap

46
docs/plugins/flash-sales/README.md

@ -0,0 +1,46 @@
# EShop.Plugins.FlashSales
[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2FEasyAbp%2FEShop%2Fmaster%2FDirectory.Build.props)](https://abp.io)
[![NuGet](https://img.shields.io/nuget/v/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared)
[![NuGet Download](https://img.shields.io/nuget/dt/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared)
[![Discord online](https://badgen.net/discord/online-members/S6QaezrCRq?label=Discord)](https://discord.gg/S6QaezrCRq)
[![GitHub stars](https://img.shields.io/github/stars/EasyAbp/EShop?style=social)](https://www.github.com/EasyAbp/EShop)
A flash-sales plugin for EShop.
## Installation
1. Install the following NuGet packages. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-nuget-packages))
- EasyAbp.EShop.Orders.Plugins.FlashSales.Application (install at EasyAbp.EShop.Orders.Application location)
- EasyAbp.EShop.Products.Plugins.FlashSales.Application (install at EasyAbp.EShop.Products.Application location)
- EasyAbp.EShop.Plugins.FlashSales.Application
- EasyAbp.EShop.Plugins.FlashSales.Application.Contracts
- EasyAbp.EShop.Plugins.FlashSales.Domain
- EasyAbp.EShop.Plugins.FlashSales.Domain.Shared
- EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore
- EasyAbp.EShop.Plugins.FlashSales.HttpApi
- EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client
- (Optional) EasyAbp.EShop.Plugins.FlashSales.MongoDB
- (Optional) EasyAbp.EShop.Plugins.FlashSales.Web
- (Optional) EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts (install at EasyAbp.EShop.Products.Application.Contracts location)
- (Optional) EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi (install at EasyAbp.EShop.Products.HttpApi location)
- (Optional) EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client (install at EasyAbp.EShop.Products.HttpApi.Client location)
2. Add `DependsOn(typeof(EShopXxxModule))` attribute to configure the module dependencies. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-module-dependencies))
3. Add `builder.ConfigureEShopPluginsFlashSales();` to the `OnModelCreating()` method in **MyProjectDbContext.cs**.
4. Add EF Core migrations and update your database. See: [ABP document](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF#add-database-migration).
## Usage
### Admins
1. Create a published flash-sale plan.
### Customers
1. Use `/api/e-shop/plugins/flash-sales/flash-sale-plan/{planId}/pre-order` (POST) to pre-order. It will return an expiration time if your pre-order request succeeds. Don't forget to re-invoke this API to refresh your request before the expiration time.
2. When the flash sale starts, use `/api/e-shop/plugins/flash-sales/flash-sale-plan/{planId}/order` (POST) to create your order. If you are fast enough, it will occupy the inventory and create an order for you in the background.
3. If you are told that you have succeeded, continuous use `/api/e-shop/plugins/flash-sales/flash-sale-result/current/{planId}` (GET) to query the order creation result until it succeeds or fails.

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Data;
@ -26,7 +27,9 @@ namespace EasyAbp.EShop.Orders.Orders.Dtos
public string ProductUniqueName { get; set; }
public string ProductDisplayName { get; set; }
public InventoryStrategy? ProductInventoryStrategy { get; set; }
public string SkuName { get; set; }
public string SkuDescription { get; set; }

8
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs

@ -93,6 +93,13 @@ namespace EasyAbp.EShop.Orders.Orders
order.SetOrderNumber(await _orderNumberGenerator.CreateAsync(order));
// set ReducedInventoryAfterPlacingTime directly if an order contains no OrderLine with `InventoryStrategy.ReduceAfterPlacing`.
// see https://github.com/EasyAbp/EShop/issues/214
if (order.OrderLines.All(x => x.ProductInventoryStrategy != InventoryStrategy.ReduceAfterPlacing))
{
order.SetReducedInventoryAfterPlacingTime(_clock.Now);
}
return order;
}
@ -148,6 +155,7 @@ namespace EasyAbp.EShop.Orders.Orders
productGroupDisplayName: product.ProductGroupDisplayName,
productUniqueName: product.UniqueName,
productDisplayName: product.DisplayName,
productInventoryStrategy: product.InventoryStrategy,
skuName: productSku.Name,
skuDescription: await _productSkuDescriptionProvider.GenerateAsync(product, productSku),
mediaResources: product.MediaResources,

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj

@ -12,6 +12,7 @@
<PackageReference Include="Volo.Abp.Validation" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.Auditing.Contracts" Version="$(AbpVersion)" />
<ProjectReference Include="..\..\..\EasyAbp.EShop.Payments\src\EasyAbp.EShop.Payments.Domain.Shared\EasyAbp.EShop.Payments.Domain.Shared.csproj" />
<ProjectReference Include="..\..\..\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Domain.Shared\EasyAbp.EShop.Products.Domain.Shared.csproj" />
<ProjectReference Include="..\..\..\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Domain.Shared\EasyAbp.EShop.Stores.Domain.Shared.csproj" />
</ItemGroup>

4
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/EShopOrdersDomainSharedModule.cs

@ -2,6 +2,7 @@
using Volo.Abp.Localization;
using EasyAbp.EShop.Orders.Localization;
using EasyAbp.EShop.Payments;
using EasyAbp.EShop.Products;
using EasyAbp.EShop.Stores;
using Volo.Abp.Auditing;
using Volo.Abp.Localization.ExceptionHandling;
@ -15,7 +16,8 @@ namespace EasyAbp.EShop.Orders
typeof(AbpValidationModule),
typeof(AbpAuditingContractsModule),
typeof(EShopStoresDomainSharedModule),
typeof(EShopPaymentsDomainSharedModule)
typeof(EShopPaymentsDomainSharedModule),
typeof(EShopProductsDomainSharedModule)
)]
public class EShopOrdersDomainSharedModule : AbpModule
{

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json

@ -35,6 +35,7 @@
"OrderLineProductGroupDisplayName": "Product group",
"OrderLineProductUniqueName": "Product unique name",
"OrderLineProductDisplayName": "Product display name",
"OrderLineProductInventoryStrategy": "Product inventory strategy",
"OrderLineSkuName": "SKU name",
"OrderLineSkuDescription": "SKU description",
"OrderLineMediaResources": "Media resources",

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json

@ -35,6 +35,7 @@
"OrderLineProductGroupDisplayName": "商品组名称",
"OrderLineProductUniqueName": "商品编号",
"OrderLineProductDisplayName": "商品名称",
"OrderLineProductInventoryStrategy": "商品库存策略",
"OrderLineSkuName": "SKU 编号",
"OrderLineSkuDescription": "SKU 描述",
"OrderLineMediaResources": "多媒体资源",

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json

@ -35,6 +35,7 @@
"OrderLineProductGroupDisplayName": "商品組名稱",
"OrderLineProductUniqueName": "商品編號",
"OrderLineProductDisplayName": "商品名稱",
"OrderLineProductInventoryStrategy": "商品庫存策略",
"OrderLineSkuName": "SKU 編號",
"OrderLineSkuDescription": "SKU 描述",
"OrderLineMediaResources": "多媒體資源",

41
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs

@ -1,4 +1,5 @@
using System;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.Data;
namespace EasyAbp.EShop.Orders.Orders
@ -6,44 +7,50 @@ namespace EasyAbp.EShop.Orders.Orders
public interface IOrderLine : IHasExtraProperties
{
Guid ProductId { get; }
Guid ProductSkuId { get; }
Guid? ProductDetailId { get; }
DateTime ProductModificationTime { get; }
DateTime? ProductDetailModificationTime { get; }
string ProductGroupName { get; }
string ProductGroupDisplayName { get; }
string ProductUniqueName { get; }
string ProductDisplayName { get; }
/// <summary>
/// If it is <c>null</c>, should get the <see cref="InventoryStrategy"/> from the Product entity.
/// See https://github.com/EasyAbp/EShop/issues/215
/// </summary>
InventoryStrategy? ProductInventoryStrategy { get; }
string SkuName { get; }
string SkuDescription { get; }
string MediaResources { get; }
string Currency { get; }
decimal UnitPrice { get; }
decimal TotalPrice { get; }
decimal TotalDiscount { get; }
/// <summary>
/// ActualTotalPrice = TotalPrice - TotalDiscount
/// </summary>
decimal ActualTotalPrice { get; }
int Quantity { get; }
int RefundedQuantity { get; }
decimal RefundAmount { get; }

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs

@ -1,4 +1,5 @@
using System;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
@ -25,7 +26,9 @@ namespace EasyAbp.EShop.Orders.Orders
public string ProductUniqueName { get; set; }
public string ProductDisplayName { get; set; }
public InventoryStrategy? ProductInventoryStrategy { get; set; }
public string SkuName { get; set; }
public string SkuDescription { get; set; }

8
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities.Auditing;
@ -30,7 +30,9 @@ namespace EasyAbp.EShop.Orders.Orders
[NotNull]
public virtual string ProductDisplayName { get; protected set; }
public virtual InventoryStrategy? ProductInventoryStrategy { get; protected set; }
[CanBeNull]
public virtual string SkuName { get; protected set; }
@ -77,6 +79,7 @@ namespace EasyAbp.EShop.Orders.Orders
[NotNull] string productGroupDisplayName,
[CanBeNull] string productUniqueName,
[NotNull] string productDisplayName,
InventoryStrategy productInventoryStrategy,
[CanBeNull] string skuName,
[CanBeNull] string skuDescription,
[CanBeNull] string mediaResources,
@ -96,6 +99,7 @@ namespace EasyAbp.EShop.Orders.Orders
ProductGroupDisplayName = productGroupDisplayName;
ProductUniqueName = productUniqueName;
ProductDisplayName = productDisplayName;
ProductInventoryStrategy = productInventoryStrategy;
SkuName = skuName;
SkuDescription = skuDescription;
MediaResources = mediaResources;

80
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs

@ -674,5 +674,85 @@ namespace EasyAbp.EShop.Orders.Orders
throw;
}
}
[Fact]
public async Task Should_Set_ReducedInventoryAfterPlacingTime_If_No_ReduceAfterPlacing_OrderLine()
{
var createOrderDto = new CreateOrderDto
{
CustomerRemark = "customer remark",
StoreId = OrderTestData.Store1Id,
OrderLines = new List<CreateOrderLineDto>
{
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku1Id,
Quantity = 10
},
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku2Id,
Quantity = 1
}
}
};
OrderDto createResponse = null;
await WithUnitOfWorkAsync(async () =>
{
createResponse = await _orderAppService.CreateAsync(createOrderDto);
});
var order = await _orderAppService.GetAsync(createResponse.Id);
// The fake inventory reducer (OrderCreatedEventHandler) should skip handling.
OrderCreatedEventHandler.LastOrderId.ShouldBe(order.Id);
OrderCreatedEventHandler.SkippedHandling.ShouldBeTrue();
order.ReducedInventoryAfterPlacingTime.ShouldNotBeNull();
}
[Fact]
public async Task Should_Not_Set_ReducedInventoryAfterPlacingTime_If_Contains_ReduceAfterPlacing_OrderLine()
{
Product1.InventoryStrategy = InventoryStrategy.ReduceAfterPlacing;
var createOrderDto = new CreateOrderDto
{
CustomerRemark = "customer remark",
StoreId = OrderTestData.Store1Id,
OrderLines = new List<CreateOrderLineDto>
{
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku1Id,
Quantity = 10
},
new CreateOrderLineDto
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku2Id,
Quantity = 1
}
}
};
OrderDto createResponse = null;
await WithUnitOfWorkAsync(async () =>
{
createResponse = await _orderAppService.CreateAsync(createOrderDto);
});
var order = await _orderAppService.GetAsync(createResponse.Id);
// The fake inventory reducer (OrderCreatedEventHandler) should handle it.
OrderCreatedEventHandler.LastOrderId.ShouldBe(order.Id);
OrderCreatedEventHandler.SkippedHandling.ShouldBeFalse();
order.ReducedInventoryAfterPlacingTime.ShouldNotBeNull();
}
}
}

20
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderCreatedEventHandler.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
@ -9,6 +10,9 @@ namespace EasyAbp.EShop.Orders.Orders
{
public class OrderCreatedEventHandler : IDistributedEventHandler<EntityCreatedEto<OrderEto>>, ITransientDependency
{
public static Guid LastOrderId { get; set; }
public static bool SkippedHandling { get; set; }
private readonly ICurrentTenant _currentTenant;
private readonly IDistributedEventBus _distributedEventBus;
@ -19,9 +23,21 @@ namespace EasyAbp.EShop.Orders.Orders
_currentTenant = currentTenant;
_distributedEventBus = distributedEventBus;
}
public virtual async Task HandleEventAsync(EntityCreatedEto<OrderEto> eventData)
{
LastOrderId = eventData.Entity.Id;
// skip if an order contains no OrderLine with `InventoryStrategy.ReduceAfterPlacing`.
// see https://github.com/EasyAbp/EShop/issues/214
if (eventData.Entity.ReducedInventoryAfterPlacingTime is not null)
{
SkippedHandling = true;
return;
}
SkippedHandling = false;
using var changeTenant = _currentTenant.Change(eventData.Entity.TenantId);
await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPlacedResultEto

1
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs

@ -38,6 +38,7 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
"Default",
null,
"Product 1",
InventoryStrategy.NoNeed,
null,
null,
null,

2
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Payments.Refunds;
using EasyAbp.EShop.Products.Products;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
@ -46,6 +47,7 @@ namespace EasyAbp.EShop.Orders.Orders
"Default",
null,
"Product 1",
InventoryStrategy.NoNeed,
null,
null,
null,

34
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs

@ -13,23 +13,23 @@ namespace EasyAbp.EShop.Products.Products.Dtos
{
[DisplayName("ProductStoreId")]
public Guid StoreId { get; set; }
[DisplayName("ProductProductGroupName")]
public string ProductGroupName { get; set; }
[DisplayName("ProductDetailId")]
public Guid? ProductDetailId { get; set; }
[DisplayName("ProductCategory")]
public ICollection<Guid> CategoryIds { get; set; }
[DisplayName("ProductUniqueName")]
public string UniqueName { get; set; }
[Required]
[DisplayName("ProductDisplayName")]
public string DisplayName { get; set; }
public ICollection<CreateUpdateProductAttributeDto> ProductAttributes { get; set; }
[DisplayName("ProductInventoryStrategy")]
@ -46,10 +46,10 @@ namespace EasyAbp.EShop.Products.Products.Dtos
[DisplayName("ProductIsPublished")]
public bool IsPublished { get; set; }
[DisplayName("ProductIsHidden")]
public bool IsHidden { get; set; }
[DisplayName("ProductPaymentExpireIn")]
public TimeSpan? PaymentExpireIn { get; set; }
@ -64,7 +64,7 @@ namespace EasyAbp.EShop.Products.Products.Dtos
new[] { "PaymentExpireIn" }
);
}
if (ProductAttributes.Select(a => a.DisplayName.Trim()).Distinct().Count() != ProductAttributes.Count)
{
yield return new ValidationResult(
@ -73,15 +73,17 @@ namespace EasyAbp.EShop.Products.Products.Dtos
);
}
var optionNameList = ProductAttributes.SelectMany(a => a.ProductAttributeOptions)
.Select(o => o.DisplayName.Trim()).ToList();
if (optionNameList.Distinct().Count() != optionNameList.Count)
foreach (var productAttribute in ProductAttributes)
{
yield return new ValidationResult(
"DisplayNames of ProductAttributeOptions should be unique!",
new[] { "ProductAttributeOptions" }
);
var options = productAttribute.ProductAttributeOptions;
if (options.Select(o => o.DisplayName.Trim()).Distinct().Count() != options.Count)
{
yield return new ValidationResult(
"DisplayNames of ProductAttributeOptions should be unique!",
new[] { "ProductAttributeOptions" }
);
}
}
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs

@ -12,8 +12,10 @@ namespace EasyAbp.EShop.Products.ProductInventories
Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(IList<InventoryQueryModel> models);
Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold);
Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold,
bool isFlashSale = false);
Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold);
Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold,
bool isFlashSale = false);
}
}

1
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.*" />
<PackageReference Include="NodaMoney" Version="$(NodaMoneyVersion)" />
<PackageReference Include="EasyAbp.Abp.Trees.Domain" Version="$(EasyAbpAbpTreesModuleVersion)" />
<PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" />

45
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.ProductInventories;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
@ -13,6 +15,14 @@ namespace EasyAbp.EShop.Products.Products
{
public class DefaultProductInventoryProvider : IProductInventoryProvider, ITransientDependency
{
/// <summary>
/// The <see cref="GetLockKeyAsync(InventoryQueryModel)"/> lock key format.
/// <para>{0}: Tenant ID</para>
/// <para>{1}: Product ID</para>
/// <para>{2}: ProductSku ID</para>
/// </summary>
public const string DefaultProductInventoryLockKeyFormat = "eshop-product-inventory-{0}-{1}-{2}";
public static string DefaultProductInventoryProviderName { get; set; } = "Default";
public static string DefaultProductInventoryProviderDisplayName { get; set; } = "Default";
public static string DefaultProductInventoryProviderDescription { get; set; } = "Default";
@ -24,17 +34,23 @@ namespace EasyAbp.EShop.Products.Products
private readonly ICurrentTenant _currentTenant;
private readonly IDistributedEventBus _distributedEventBus;
private readonly IProductInventoryRepository _productInventoryRepository;
private readonly IAbpDistributedLock _distributedLock;
private readonly ILogger<DefaultProductInventoryProvider> _logger;
public DefaultProductInventoryProvider(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IDistributedEventBus distributedEventBus,
IProductInventoryRepository productInventoryRepository)
IProductInventoryRepository productInventoryRepository,
IAbpDistributedLock distributedLock,
ILogger<DefaultProductInventoryProvider> logger)
{
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
_distributedEventBus = distributedEventBus;
_productInventoryRepository = productInventoryRepository;
_distributedLock = distributedLock;
_logger = logger;
}
[UnitOfWork]
@ -60,8 +76,17 @@ namespace EasyAbp.EShop.Products.Products
[UnitOfWork(true)]
public virtual async Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity,
bool decreaseSold)
bool decreaseSold, bool isFlashSale = false)
{
await using var handle = await _distributedLock.TryAcquireAsync(await GetLockKeyAsync(model), TimeSpan.FromSeconds(30));
if (handle == null)
{
_logger.LogWarning("TryIncreaseInventory failed to acquire lock for product inventory: {TenantId},{ProductId},{ProductSkuId}",
model.TenantId, model.ProductId, model.ProductSkuId);
return false;
}
var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryIncreaseInventoryAsync(model, productInventory, quantity, decreaseSold);
@ -69,8 +94,17 @@ namespace EasyAbp.EShop.Products.Products
[UnitOfWork(true)]
public virtual async Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity,
bool increaseSold)
bool increaseSold, bool isFlashSale = false)
{
await using var handle = await _distributedLock.TryAcquireAsync(await GetLockKeyAsync(model), TimeSpan.FromSeconds(30));
if (handle == null)
{
_logger.LogWarning("TryReduceInventory failed to acquire lock for product inventory: {TenantId},{ProductId},{ProductSkuId}",
model.TenantId, model.ProductId, model.ProductSkuId);
return false;
}
var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryReduceInventoryAsync(model, productInventory, quantity, increaseSold);
@ -158,5 +192,10 @@ namespace EasyAbp.EShop.Products.Products
newInventory,
sold));
}
protected virtual Task<string> GetLockKeyAsync(InventoryQueryModel model)
{
return Task.FromResult(string.Format(DefaultProductInventoryLockKeyFormat, model.TenantId, model.ProductId, model.ProductSkuId));
}
}
}

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

@ -9,4 +9,6 @@ public interface IProductInventoryProviderResolver
Task<bool> ExistProviderAsync([NotNull] string providerName);
Task<IProductInventoryProvider> GetAsync(Product product);
Task<IProductInventoryProvider> GetAsync([NotNull] string providerName);
}

11
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs

@ -1,6 +1,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.Uow;
@ -10,13 +11,16 @@ namespace EasyAbp.EShop.Products.Products
// see: https://github.com/EasyAbp/EShop/issues/139
public class InventoryRollbackOrderCanceledEventHandler : IDistributedEventHandler<OrderCanceledEto>, ITransientDependency
{
private readonly ILogger<InventoryRollbackOrderCanceledEventHandler> _logger;
private readonly IProductManager _productManager;
private readonly IProductRepository _productRepository;
public InventoryRollbackOrderCanceledEventHandler(
ILogger<InventoryRollbackOrderCanceledEventHandler> logger,
IProductManager productManager,
IProductRepository productRepository)
{
_logger = logger;
_productManager = productManager;
_productRepository = productRepository;
}
@ -40,7 +44,12 @@ namespace EasyAbp.EShop.Products.Products
var productSku = product.ProductSkus.Single(x => x.Id == orderLine.ProductSkuId);
await _productManager.TryIncreaseInventoryAsync(product, productSku, orderLine.Quantity, true);
if (!await _productManager.TryIncreaseInventoryAsync(product, productSku, orderLine.Quantity, true))
{
_logger.LogWarning(
"InventoryRollbackOrderCanceledEventHandler: inventory rollback failed! productId = {productId}, productSkuId = {productSkuId}, quantity = {quantity}, reduceSold = {reduceSold}",
orderLine.ProductId, orderLine.ProductSkuId, orderLine.Quantity, true);
}
}
}
}

21
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs

@ -39,25 +39,36 @@ namespace EasyAbp.EShop.Products.Products
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(EntityCreatedEto<OrderEto> eventData)
{
// skip if an order contains no OrderLine with `InventoryStrategy.ReduceAfterPlacing`.
// see https://github.com/EasyAbp/EShop/issues/214
if (eventData.Entity.ReducedInventoryAfterPlacingTime is not null)
{
return;
}
using var changeTenant = _currentTenant.Change(eventData.Entity.TenantId);
var models = new List<ConsumeInventoryModel>();
foreach (var orderLine in eventData.Entity.OrderLines)
{
// Todo: Should use ProductHistory.
var product = await _productRepository.FindAsync(orderLine.ProductId);
var inventoryStrategy = orderLine.ProductInventoryStrategy;
if (inventoryStrategy is not null && inventoryStrategy != InventoryStrategy.ReduceAfterPlacing)
{
continue;
}
var product = await _productRepository.FindAsync(orderLine.ProductId);
var productSku = product?.ProductSkus.FirstOrDefault(sku => sku.Id == orderLine.ProductSkuId);
if (productSku == null)
{
await PublishInventoryReductionResultEventAsync(eventData, false);
return;
}
if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPlacing)
inventoryStrategy ??= product.InventoryStrategy;
if (inventoryStrategy != InventoryStrategy.ReduceAfterPlacing)
{
continue;
}

12
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs

@ -44,11 +44,14 @@ namespace EasyAbp.EShop.Products.Products
foreach (var orderLine in eventData.Order.OrderLines)
{
// Todo: Should use ProductHistory.
var product = await _productRepository.FindAsync(orderLine.ProductId);
var inventoryStrategy = orderLine.ProductInventoryStrategy;
if (inventoryStrategy is not null && inventoryStrategy != InventoryStrategy.ReduceAfterPayment)
{
continue;
}
var product = await _productRepository.FindAsync(orderLine.ProductId);
var productSku = product?.ProductSkus.FirstOrDefault(sku => sku.Id == orderLine.ProductSkuId);
if (productSku == null)
{
await PublishInventoryReductionResultEventAsync(eventData, false);
@ -56,7 +59,8 @@ namespace EasyAbp.EShop.Products.Products
return;
}
if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPayment)
inventoryStrategy ??= product.InventoryStrategy;
if (inventoryStrategy != InventoryStrategy.ReduceAfterPayment)
{
continue;
}

11
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Options;
using EasyAbp.EShop.Products.ProductInventories;
@ -47,11 +46,17 @@ public class ProductInventoryProviderResolver : IProductInventoryProviderResolve
return Task.FromResult(GetProviderByName(options.Value.DefaultInventoryProviderName));
}
public virtual Task<IProductInventoryProvider> GetAsync([CanBeNull] string providerName)
{
return Task.FromResult(GetProviderByName(providerName));
}
protected virtual IProductInventoryProvider GetProviderByName([CanBeNull] string providerName)
{
if (providerName.IsNullOrEmpty())
{
providerName = DefaultProductInventoryProvider.DefaultProductInventoryProviderName;
var options = ServiceProvider.GetRequiredService<IOptions<EShopProductsOptions>>();
providerName = options.Value.DefaultInventoryProviderName ?? DefaultProductInventoryProvider.DefaultProductInventoryProviderName;
}
TryBuildNameToProviderTypeMapping();
@ -67,7 +72,7 @@ public class ProductInventoryProviderResolver : IProductInventoryProviderResolve
{
return;
}
var options = ServiceProvider.GetRequiredService<IOptions<EShopProductsOptions>>().Value;
foreach (var pair in options.InventoryProviders.GetConfigurationsDictionary())

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

@ -259,8 +259,10 @@ namespace EasyAbp.EShop.Products.Products
{
var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
var isFlashSale = product.InventoryStrategy is InventoryStrategy.FlashSales;
return await (await _productInventoryProviderResolver.GetAsync(product))
.TryIncreaseInventoryAsync(model, quantity, reduceSold);
.TryIncreaseInventoryAsync(model, quantity, reduceSold, isFlashSale);
}
public virtual async Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity,
@ -268,8 +270,10 @@ namespace EasyAbp.EShop.Products.Products
{
var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
var isFlashSale = product.InventoryStrategy is InventoryStrategy.FlashSales;
return await (await _productInventoryProviderResolver.GetAsync(product))
.TryReduceInventoryAsync(model, quantity, increaseSold);
.TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale);
}
public virtual async Task<PriceDataModel> GetRealPriceAsync(Product product, ProductSku productSku)

10
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs

@ -34,10 +34,11 @@ public class FakeProductInventoryProvider : IProductInventoryProvider, ITransien
return Task.FromResult(result);
}
public Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold)
public Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold,
bool isFlashSale = false)
{
Model.Inventory++;
if (decreaseSold)
{
Model.Sold--;
@ -46,10 +47,11 @@ public class FakeProductInventoryProvider : IProductInventoryProvider, ITransien
return Task.FromResult(true);
}
public Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold)
public Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold,
bool isFlashSale = false)
{
Model.Inventory--;
if (increaseSold)
{
Model.Sold++;

223
plugins/FlashSales/.editorconfig

@ -0,0 +1,223 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = omit_if_default
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = 0
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = false
dotnet_style_allow_statement_immediately_after_block_experimental = false
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
csharp_style_prefer_parameter_null_checking = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = file_scoped
csharp_style_prefer_method_group_conversion = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:suggestion
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

1
plugins/FlashSales/README.md

@ -0,0 +1 @@
../../docs/plugins/flash-sales/README.md

1
plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.abppkg.json

@ -0,0 +1 @@
{}

10
plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.csproj

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
</Project>

9
plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp/EShop/Plugins/FlashSales/MultiTenancy/MultiTenancyConsts.cs

@ -0,0 +1,9 @@
namespace EasyAbp.EShop.Plugins.FlashSales.MultiTenancy;
public static class MultiTenancyConsts
{
/* Enable/disable multi-tenancy in a single point
* to test your module with multi-tenancy.
*/
public const bool IsEnabled = false;
}

3
plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

15
plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Orders\src\EasyAbp.EShop.Orders.Application\EasyAbp.EShop.Orders.Application.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj" />
</ItemGroup>
</Project>

162
plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandler.cs

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
using EasyAbp.EShop.Products.ProductDetails;
using EasyAbp.EShop.Products.ProductDetails.Dtos;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Orders.Orders;
public class CreateFlashSaleOrderEventHandler : IDistributedEventHandler<CreateFlashSaleOrderEto>, ITransientDependency
{
protected INewOrderGenerator NewOrderGenerator { get; }
protected IObjectMapper ObjectMapper { get; }
protected IEnumerable<IOrderDiscountProvider> OrderDiscountProviders { get; }
protected IOrderRepository OrderRepository { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected IProductAppService ProductAppService { get; }
protected IProductDetailAppService ProductDetailAppService { get; }
protected IFlashSalePlanHasher FlashSalePlanHasher { get; }
public CreateFlashSaleOrderEventHandler(
INewOrderGenerator newOrderGenerator,
IObjectMapper objectMapper,
IEnumerable<IOrderDiscountProvider> orderDiscountProviders,
IOrderRepository orderRepository,
IDistributedEventBus distributedEventBus,
IProductAppService productAppService,
IProductDetailAppService productDetailAppService,
IFlashSalePlanHasher flashSalePlanHasher)
{
NewOrderGenerator = newOrderGenerator;
ObjectMapper = objectMapper;
OrderDiscountProviders = orderDiscountProviders;
OrderRepository = orderRepository;
DistributedEventBus = distributedEventBus;
ProductAppService = productAppService;
ProductDetailAppService = productDetailAppService;
FlashSalePlanHasher = flashSalePlanHasher;
}
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(CreateFlashSaleOrderEto eventData)
{
var product = await ProductAppService.GetAsync(eventData.Plan.ProductId);
var productSku = product.GetSkuById(eventData.Plan.ProductSkuId);
if (!await ValidateHashTokenAsync(eventData.Plan, product, productSku, eventData.HashToken))
{
await DistributedEventBus.PublishAsync(new FlashSaleOrderCreationResultEto
{
TenantId = eventData.TenantId,
PlanId = eventData.Plan.Id,
OrderId = null,
UserId = eventData.UserId,
StoreId = eventData.Plan.StoreId,
ResultId = eventData.ResultId,
Success = false,
Reason = FlashSaleResultFailedReason.InvalidHashToken,
ProductInventoryProviderName = product.InventoryProviderName,
ProductId = eventData.Plan.ProductId,
ProductSkuId = eventData.Plan.ProductSkuId,
AllowToTryAgain = true
});
return;
}
var input = await ConvertToCreateOrderDtoAsync(eventData);
var productDict = await GetProductDictionaryAsync(product);
var productDetailDict = await GetProductDetailDictionaryAsync(product, productSku);
var order = await NewOrderGenerator.GenerateAsync(eventData.UserId, input, productDict, productDetailDict);
await OrderRepository.InsertAsync(order, autoSave: true);
await DistributedEventBus.PublishAsync(new FlashSaleOrderCreationResultEto
{
TenantId = eventData.TenantId,
PlanId = eventData.Plan.Id,
OrderId = order.Id,
UserId = eventData.UserId,
StoreId = eventData.Plan.StoreId,
ResultId = eventData.ResultId,
Success = true,
Reason = null,
ProductInventoryProviderName = product.InventoryProviderName,
ProductId = eventData.Plan.ProductId,
ProductSkuId = eventData.Plan.ProductSkuId,
AllowToTryAgain = false
});
}
protected virtual Task<Dictionary<Guid, ProductDto>> GetProductDictionaryAsync(ProductDto product)
{
var productDict = new Dictionary<Guid, ProductDto>()
{
{product.Id, product}
};
return Task.FromResult(productDict);
}
protected virtual async Task<Dictionary<Guid, ProductDetailDto>> GetProductDetailDictionaryAsync(ProductDto product, ProductSkuDto productSku)
{
var dict = new Dictionary<Guid, ProductDetailDto>();
var productDetailId = productSku.ProductDetailId ?? product.ProductDetailId;
if (productDetailId.HasValue)
{
dict.Add(productDetailId.Value, await ProductDetailAppService.GetAsync(productDetailId.Value));
}
return dict;
}
protected virtual Task<CreateOrderDto> ConvertToCreateOrderDtoAsync(CreateFlashSaleOrderEto eventData)
{
var input = new CreateOrderDto()
{
StoreId = eventData.Plan.StoreId,
CustomerRemark = eventData.CustomerRemark,
OrderLines = new List<CreateOrderLineDto>()
{
new CreateOrderLineDto()
{
ProductId = eventData.Plan.ProductId,
ProductSkuId = eventData.Plan.ProductSkuId,
Quantity = 1
}
}
};
eventData.MapExtraPropertiesTo(input);
return Task.FromResult(input);
}
protected virtual async Task<bool> ValidateHashTokenAsync(FlashSalePlanEto plan, ProductDto product, ProductSkuDto productSku, string originHashToken)
{
var hashToken = await FlashSalePlanHasher.HashAsync(plan.LastModificationTime, product.LastModificationTime, productSku.LastModificationTime);
return string.Equals(hashToken, originHashToken, StringComparison.InvariantCulture);
}
}

24
plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationModule.cs

@ -0,0 +1,24 @@
using EasyAbp.EShop.Plugins.FlashSales;
using EasyAbp.EShop.Products;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
namespace EasyAbp.EShop.Orders.Plugins.FlashSales;
[DependsOn(
typeof(EShopOrdersApplicationModule),
typeof(EShopProductsApplicationContractsModule),
typeof(EShopPluginsFlashSalesApplicationContractsModule)
)]
public class EShopOrdersPluginsFlashSalesApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<EShopOrdersPluginsFlashSalesApplicationModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<EShopOrdersPluginsFlashSalesApplicationModule>(validate: true);
});
}
}

3
plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

3
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.abppkg.json

@ -0,0 +1,3 @@
{
"role": "lib.application-contracts"
}

16
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.Authorization" Version="$(AbpVersion)" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj" />
</ItemGroup>
</Project>

15
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationContractsModule.cs

@ -0,0 +1,15 @@
using Volo.Abp.Application;
using Volo.Abp.Modularity;
using Volo.Abp.Authorization;
namespace EasyAbp.EShop.Plugins.FlashSales;
[DependsOn(
typeof(EShopPluginsFlashSalesDomainSharedModule),
typeof(AbpDddApplicationContractsModule),
typeof(AbpAuthorizationModule)
)]
public class EShopPluginsFlashSalesApplicationContractsModule : AbpModule
{
}

8
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesRemoteServiceConsts.cs

@ -0,0 +1,8 @@
namespace EasyAbp.EShop.Plugins.FlashSales;
public class EShopPluginsFlashSalesRemoteServiceConsts
{
public const string RemoteServiceName = "EasyAbpEShopPluginsFlashSales";
public const string ModuleName = "easyAbpEShopPluginsFlashSales";
}

15
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSaleOrderResultDto.cs

@ -0,0 +1,15 @@
using JetBrains.Annotations;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
public class FlashSaleOrderResultDto : ExtensibleEntityDto
{
public bool IsSuccess { get; set; }
[CanBeNull]
public string ErrorCode { get; set; }
[CanBeNull]
public string ErrorMessage { get; set; }
}

21
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanCreateDto.cs

@ -0,0 +1,21 @@
using System;
using EasyAbp.EShop.Stores.Stores;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
[Serializable]
public class FlashSalePlanCreateDto : ExtensibleEntityDto, IMultiStore
{
public Guid StoreId { get; set; }
public DateTime BeginTime { get; set; }
public DateTime EndTime { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public bool IsPublished { get; set; }
}

23
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanDto.cs

@ -0,0 +1,23 @@
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Entities;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
[Serializable]
public class FlashSalePlanDto : ExtensibleFullAuditedEntityDto<Guid>, IHasConcurrencyStamp
{
public Guid StoreId { get; set; }
public DateTime BeginTime { get; set; }
public DateTime EndTime { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public bool IsPublished { get; set; }
public string ConcurrencyStamp { get; set; }
}

20
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanGetListInput.cs

@ -0,0 +1,20 @@
using System;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
[Serializable]
public class FlashSalePlanGetListInput : ExtensiblePagedAndSortedResultRequestDto
{
public Guid? StoreId { get; set; }
public Guid? ProductId { get; set; }
public Guid? ProductSkuId { get; set; }
public bool IncludeUnpublished { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
}

11
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanPreOrderDto.cs

@ -0,0 +1,11 @@
using System;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
public class FlashSalePlanPreOrderDto : ExtensibleEntityDto
{
public DateTime ExpiresTime { get; set; }
public double ExpiresInSeconds { get; set; }
}

21
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanUpdateDto.cs

@ -0,0 +1,21 @@
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Entities;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
[Serializable]
public class FlashSalePlanUpdateDto : ExtensibleEntityDto, IHasConcurrencyStamp
{
public DateTime BeginTime { get; set; }
public DateTime EndTime { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public bool IsPublished { get; set; }
public string ConcurrencyStamp { get; set; }
}

8
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/OrderFlashSalePlanInput.cs

@ -0,0 +1,8 @@
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
public class OrderFlashSalePlanInput : ExtensibleEntityDto
{
public string CustomerRemark { get; set; }
}

19
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanAppService.cs

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
using Volo.Abp.Application.Services;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public interface IFlashSalePlanAppService :
ICrudAppService<
FlashSalePlanDto,
Guid,
FlashSalePlanGetListInput,
FlashSalePlanCreateDto,
FlashSalePlanUpdateDto>
{
Task<FlashSalePlanPreOrderDto> PreOrderAsync(Guid id);
Task<FlashSaleOrderResultDto> OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput);
}

22
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultDto.cs

@ -0,0 +1,22 @@
using System;
using EasyAbp.EShop.Stores.Stores;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
public class FlashSaleResultDto : ExtensibleFullAuditedEntityDto<Guid>, IMultiStore
{
public Guid StoreId { get; set; }
public Guid PlanId { get; set; }
public FlashSaleResultStatus Status { get; set; }
public string Reason { get; set; }
public Guid UserId { get; set; }
public Guid? OrderId { get; set; }
public DateTime ReducedInventoryTime { get; set; }
}

17
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultGetListInput.cs

@ -0,0 +1,17 @@
using System;
using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
public class FlashSaleResultGetListInput : ExtensiblePagedAndSortedResultRequestDto
{
public virtual Guid? StoreId { get; set; }
public virtual Guid? PlanId { get; set; }
public virtual FlashSaleResultStatus? Status { get; set; }
public virtual Guid? UserId { get; set; }
public virtual Guid? OrderId { get; set; }
}

15
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultAppService.cs

@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using Volo.Abp.Application.Services;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public interface IFlashSaleResultAppService :
IReadOnlyAppService<
FlashSaleResultDto,
Guid,
FlashSaleResultGetListInput>
{
Task<FlashSaleResultDto> GetCurrentAsync(Guid planId);
}

30
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissionDefinitionProvider.cs

@ -0,0 +1,30 @@
using EasyAbp.EShop.Plugins.FlashSales.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace EasyAbp.EShop.Plugins.FlashSales.Permissions;
public class FlashSalesPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup(FlashSalesPermissions.GroupName, L("Permission:FlashSales"));
var flashSalePlanPermission = myGroup.AddPermission(FlashSalesPermissions.FlashSalePlan.Default, L("Permission:FlashSalePlan"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Manage, L("Permission:Manage"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.CrossStore, L("Permission:CrossStore"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Create, L("Permission:Create"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Update, L("Permission:Update"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Delete, L("Permission:Delete"));
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.PreOrder, L("Permission:PreOrder"));
var flashSaleResultPermission = myGroup.AddPermission(FlashSalesPermissions.FlashSaleResult.Default, L("Permission:FlashSaleResult"));
flashSaleResultPermission.AddChild(FlashSalesPermissions.FlashSaleResult.Manage, L("Permission:Manage"));
flashSaleResultPermission.AddChild(FlashSalesPermissions.FlashSaleResult.CrossStore, L("Permission:CrossStore"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<FlashSalesResource>(name);
}
}

31
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissions.cs

@ -0,0 +1,31 @@
using Volo.Abp.Reflection;
namespace EasyAbp.EShop.Plugins.FlashSales.Permissions;
public class FlashSalesPermissions
{
public const string GroupName = "EasyAbp.EShop.Plugins.FlashSales";
public static string[] GetAll()
{
return ReflectionHelper.GetPublicConstantsRecursively(typeof(FlashSalesPermissions));
}
public class FlashSalePlan
{
public const string Default = GroupName + ".FlashSalePlan";
public const string Manage = Default + ".Manage";
public const string CrossStore = Default + ".CrossStore";
public const string Update = Default + ".Update";
public const string Create = Default + ".Create";
public const string Delete = Default + ".Delete";
public const string PreOrder = Default + ".PreOrder";
}
public class FlashSaleResult
{
public const string Default = GroupName + ".FlashSaleResult";
public const string Manage = Default + ".Manage";
public const string CrossStore = Default + ".CrossStore";
}
}

3
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

3
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.abppkg.json

@ -0,0 +1,3 @@
{
"role": "lib.application"
}

23
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.csproj

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.*" />
<PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.Ddd.Application" Version="$(AbpVersion)" />
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Application.Contracts\EasyAbp.EShop.Products.Application.Contracts.csproj" />
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Shared\EasyAbp.EShop.Stores.Application.Shared.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Domain\EasyAbp.EShop.Plugins.FlashSales.Domain.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj" />
</ItemGroup>
</Project>

33
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationModule.cs

@ -0,0 +1,33 @@
using EasyAbp.EShop.Products;
using EasyAbp.EShop.Products.Plugins.FlashSales;
using EasyAbp.EShop.Stores;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Application;
using Volo.Abp.AutoMapper;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
namespace EasyAbp.EShop.Plugins.FlashSales;
[DependsOn(
typeof(EShopProductsApplicationContractsModule),
typeof(EShopPluginsFlashSalesDomainModule),
typeof(EShopPluginsFlashSalesApplicationContractsModule),
typeof(EShopStoresApplicationSharedModule),
typeof(EShopProductsPluginsFlashSalesAbstractionsModule),
typeof(EShopProductsPluginsFlashSalesApplicationContractsModule),
typeof(AbpDddApplicationModule),
typeof(AbpAutoMapperModule),
typeof(AbpCachingModule)
)]
public class EShopPluginsFlashSalesApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<EShopPluginsFlashSalesApplicationModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<EShopPluginsFlashSalesApplicationModule>(validate: true);
});
}
}

106
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandler.cs

@ -0,0 +1,106 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using EasyAbp.Eshop.Products.Products;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class FlashSaleOrderCreationResultEventHandler : IDistributedEventHandler<FlashSaleOrderCreationResultEto>,
ITransientDependency
{
protected ILogger<FlashSaleOrderCreationResultEventHandler> Logger { get; }
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IObjectMapper ObjectMapper { get; }
protected IAbpApplication AbpApplication { get; }
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; }
protected IFlashSaleResultRepository FlashSaleResultRepository { get; }
public FlashSaleOrderCreationResultEventHandler(
ILogger<FlashSaleOrderCreationResultEventHandler> logger,
IFlashSaleInventoryManager flashSaleInventoryManager,
IUnitOfWorkManager unitOfWorkManager,
IObjectMapper objectMapper,
IAbpApplication abpApplication,
IFlashSaleCurrentResultCache flashSaleCurrentResultCache,
IFlashSaleResultRepository flashSaleResultRepository)
{
Logger = logger;
FlashSaleInventoryManager = flashSaleInventoryManager;
UnitOfWorkManager = unitOfWorkManager;
ObjectMapper = objectMapper;
AbpApplication = abpApplication;
FlashSaleCurrentResultCache = flashSaleCurrentResultCache;
FlashSaleResultRepository = flashSaleResultRepository;
}
[UnitOfWork]
public virtual async Task HandleEventAsync(FlashSaleOrderCreationResultEto eventData)
{
var flashSaleResult = await FlashSaleResultRepository.GetAsync(eventData.ResultId);
if (eventData.Success)
{
flashSaleResult.MarkAsSuccessful(eventData.OrderId!.Value);
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true);
}
else
{
flashSaleResult.MarkAsFailed(eventData.Reason);
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true);
}
UnitOfWorkManager.Current.OnCompleted(async () =>
{
if (eventData.Success)
{
await ResetFlashSaleCurrentResultCacheAsync(flashSaleResult);
}
else
{
// try to roll back the inventory.
if (!await FlashSaleInventoryManager.TryRollBackInventoryAsync(
eventData.TenantId, eventData.ProductInventoryProviderName, eventData.StoreId,
eventData.ProductId, eventData.ProductSkuId))
{
Logger.LogWarning("Failed to roll back the flash sale inventory.");
return; // avoid to remove cache if the rollback failed.
}
// remove the cache so the user can try to order again.
if (eventData.AllowToTryAgain)
{
await FlashSaleCurrentResultCache.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId);
}
else
{
await ResetFlashSaleCurrentResultCacheAsync(flashSaleResult);
}
}
});
}
protected virtual async Task ResetFlashSaleCurrentResultCacheAsync(FlashSaleResult flashSaleResult)
{
using var scope = AbpApplication.ServiceProvider.CreateScope();
var objectMapper = scope.ServiceProvider.GetRequiredService<IObjectMapper>();
var flashSaleCurrentResultCache = scope.ServiceProvider.GetRequiredService<IFlashSaleCurrentResultCache>();
await flashSaleCurrentResultCache.SetAsync(flashSaleResult.PlanId, flashSaleResult.UserId,
new FlashSaleCurrentResultCacheItem
{
TenantId = flashSaleResult.TenantId,
ResultDto = objectMapper.Map<FlashSaleResult, FlashSaleResultDto>(flashSaleResult)
});
}
}

452
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppService.cs

@ -0,0 +1,452 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.Eshop.Products.Products;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using EasyAbp.EShop.Plugins.FlashSales.Options;
using EasyAbp.EShop.Plugins.FlashSales.Permissions;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using EasyAbp.EShop.Stores.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.DistributedLocking;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class FlashSalePlanAppService :
MultiStoreCrudAppService<FlashSalePlan, FlashSalePlanDto, Guid, FlashSalePlanGetListInput, FlashSalePlanCreateDto, FlashSalePlanUpdateDto>,
IFlashSalePlanAppService
{
/// <summary>
/// The <see cref="GetPreOrderCacheKeyAsync(FlashSalePlanCacheItem)"/> cache key format.
/// <para>{0}: FlashSalePlan ID</para>
/// <para>{1}: User ID</para>
/// </summary>
public const string PreOrderCacheKeyFormat = "eshopflashsales_{0}_{1}";
protected override string CrossStorePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.CrossStore;
protected override string GetPolicyName { get; set; } = null;
protected override string GetListPolicyName { get; set; } = null;
protected override string CreatePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Create;
protected override string UpdatePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Update;
protected override string DeletePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Delete;
protected virtual string PreOrderPolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.PreOrder;
protected IFlashSalePlanRepository FlashSalePlanRepository { get; }
protected IProductAppService ProductAppService { get; }
protected IDistributedCache<FlashSalePlanPreOrderCacheItem> PreOrderDistributedCache { get; }
protected IDistributedCache<FlashSalePlanCacheItem, Guid> PlanDistributedCache { get; }
protected IDistributedCache<ProductCacheItem, Guid> ProductDistributedCache { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected IFlashSaleResultRepository FlashSaleResultRepository { get; }
protected IAbpDistributedLock DistributedLock { get; }
protected IFlashSalePlanHasher FlashSalePlanHasher { get; }
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; }
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; }
protected FlashSalesOptions Options { get; }
public FlashSalePlanAppService(
IFlashSalePlanRepository flashSalePlanRepository,
IProductAppService productAppService,
IDistributedCache<FlashSalePlanPreOrderCacheItem> tokenDistributedCache,
IDistributedCache<FlashSalePlanCacheItem, Guid> planDistributedCache,
IDistributedCache<ProductCacheItem, Guid> productDistributedCache,
IDistributedEventBus distributedEventBus,
IFlashSaleResultRepository flashSaleResultRepository,
IAbpDistributedLock distributedLock,
IFlashSalePlanHasher flashSalePlanHasher,
IFlashSaleInventoryManager flashSaleInventoryManager,
IFlashSaleCurrentResultCache flashSaleCurrentResultCache,
IOptions<FlashSalesOptions> options)
: base(flashSalePlanRepository)
{
FlashSalePlanRepository = flashSalePlanRepository;
ProductAppService = productAppService;
PreOrderDistributedCache = tokenDistributedCache;
PlanDistributedCache = planDistributedCache;
ProductDistributedCache = productDistributedCache;
DistributedEventBus = distributedEventBus;
FlashSaleResultRepository = flashSaleResultRepository;
DistributedLock = distributedLock;
FlashSalePlanHasher = flashSalePlanHasher;
FlashSaleInventoryManager = flashSaleInventoryManager;
FlashSaleCurrentResultCache = flashSaleCurrentResultCache;
Options = options.Value;
}
public override async Task<FlashSalePlanDto> GetAsync(Guid id)
{
var flashSalePlan = await GetEntityByIdAsync(id);
await CheckGetPolicyAsync();
if (!flashSalePlan.IsPublished)
{
await CheckMultiStorePolicyAsync(flashSalePlan.StoreId, FlashSalesPermissions.FlashSalePlan.Manage);
}
return await MapToGetOutputDtoAsync(flashSalePlan);
}
public override async Task<PagedResultDto<FlashSalePlanDto>> GetListAsync(FlashSalePlanGetListInput input)
{
await CheckGetListPolicyAsync();
if (input.IncludeUnpublished)
{
await CheckMultiStorePolicyAsync(input.StoreId, FlashSalesPermissions.FlashSalePlan.Manage);
}
return await base.GetListAsync(input);
}
public override async Task<FlashSalePlanDto> CreateAsync(FlashSalePlanCreateDto input)
{
await CheckMultiStorePolicyAsync(input.StoreId, CreatePolicyName);
var product = await ProductAppService.GetAsync(input.ProductId);
var productSku = product.GetSkuById(input.ProductSkuId);
await ValidateProductAsync(input.ProductId, product, input.StoreId);
var flashSalePlan = new FlashSalePlan(
GuidGenerator.Create(),
CurrentTenant.Id,
input.StoreId,
input.BeginTime,
input.EndTime,
product.Id,
productSku.Id,
input.IsPublished
);
await FlashSalePlanRepository.InsertAsync(flashSalePlan, autoSave: true);
return await MapToGetOutputDtoAsync(flashSalePlan);
}
public override async Task<FlashSalePlanDto> UpdateAsync(Guid id, FlashSalePlanUpdateDto input)
{
var flashSalePlan = await GetEntityByIdAsync(id);
var product = await ProductAppService.GetAsync(input.ProductId);
var productSku = product.GetSkuById(input.ProductSkuId);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
await ValidateProductAsync(input.ProductId, product, flashSalePlan.StoreId);
if (await ExistRelatedFlashSaleResultsAsync(id) && (input.ProductId != flashSalePlan.ProductId || input.ProductSkuId != flashSalePlan.ProductSkuId))
{
throw new RelatedFlashSaleResultsExistException(id);
}
flashSalePlan.SetTimeRange(input.BeginTime, input.EndTime);
flashSalePlan.SetProductSku(flashSalePlan.StoreId, product.Id, productSku.Id);
flashSalePlan.SetPublished(input.IsPublished);
flashSalePlan.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
await FlashSalePlanRepository.UpdateAsync(flashSalePlan, autoSave: true);
return await MapToGetOutputDtoAsync(flashSalePlan);
}
public override async Task DeleteAsync(Guid id)
{
var flashSalePlan = await GetEntityByIdAsync(id);
await CheckMultiStorePolicyAsync(flashSalePlan.StoreId, DeletePolicyName);
if (await ExistRelatedFlashSaleResultsAsync(id))
{
throw new RelatedFlashSaleResultsExistException(id);
}
await FlashSalePlanRepository.DeleteAsync(flashSalePlan);
}
protected override async Task<IQueryable<FlashSalePlan>> CreateFilteredQueryAsync(FlashSalePlanGetListInput input)
{
return (await base.CreateFilteredQueryAsync(input))
.WhereIf(input.StoreId.HasValue, x => x.StoreId == input.StoreId.Value)
.WhereIf(input.ProductId.HasValue, x => x.ProductId == input.ProductId.Value)
.WhereIf(input.ProductSkuId.HasValue, x => x.ProductSkuId == input.ProductSkuId.Value)
.WhereIf(!input.IncludeUnpublished, x => x.IsPublished)
.WhereIf(input.Start.HasValue, x => x.BeginTime >= input.Start.Value)
.WhereIf(input.End.HasValue, x => x.BeginTime <= input.End.Value);
}
[Authorize]
[UnitOfWork(IsDisabled = true)]
public virtual async Task<FlashSalePlanPreOrderDto> PreOrderAsync(Guid id)
{
await CheckPolicyAsync(PreOrderPolicyName);
var plan = await GetFlashSalePlanCacheAsync(id);
var product = await GetProductCacheAsync(plan.ProductId);
var productSku = product.GetSkuById(plan.ProductSkuId);
var expiresTime = DateTimeOffset.Now.Add(Options.PreOrderExpires);
await ValidatePreOrderAsync(plan, product, productSku);
await SetPreOrderCacheAsync(plan, product, productSku, expiresTime);
return new FlashSalePlanPreOrderDto { ExpiresTime = Clock.Normalize(expiresTime.LocalDateTime), ExpiresInSeconds = Options.PreOrderExpires.TotalSeconds };
}
[DisableAuditing]
[UnitOfWork(IsDisabled = true)]
[Authorize]
public virtual async Task<FlashSaleOrderResultDto> OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput)
{
var preOrderCache = await GetPreOrderCacheAsync(id);
if (preOrderCache == null)
{
return CreateFailureResultDto(FlashSalesErrorCodes.PreOrderExpired);
}
var plan = await GetFlashSalePlanCacheAsync(id);
var now = Clock.Now;
if (plan.BeginTime > now)
{
return CreateFailureResultDto(FlashSalesErrorCodes.FlashSaleNotStarted);
}
if (now >= plan.EndTime)
{
return CreateFailureResultDto(FlashSalesErrorCodes.FlashSaleIsOver);
}
await RemovePreOrderCacheAsync(id);
var userId = CurrentUser.GetId();
var lockKey = $"create-flash-sale-order-{plan.Id}-{userId}";
await using var handle = await DistributedLock.TryAcquireAsync(lockKey);
if (handle == null)
{
return CreateFailureResultDto(FlashSalesErrorCodes.BusyToCreateFlashSaleOrder);
}
if (await FlashSaleCurrentResultCache.GetAsync(plan.Id, CurrentUser.GetId()) is not null)
{
return CreateFailureResultDto(FlashSalesErrorCodes.DuplicateFlashSalesOrder);
}
if (!await FlashSaleInventoryManager.TryReduceInventoryAsync(plan.TenantId, preOrderCache.InventoryProviderName,
plan.StoreId, plan.ProductId, plan.ProductSkuId))
{
await FlashSaleCurrentResultCache.RemoveAsync(plan.Id, CurrentUser.GetId());
return CreateFailureResultDto(FlashSalesErrorCodes.ProductSkuInventoryExceeded);
}
try
{
var createFlashSaleResultEto =
await PrepareCreateFlashSaleResultEtoAsync(plan, flashSalePlanInput, userId, Clock.Now, preOrderCache);
await FlashSaleCurrentResultCache.SetAsync(plan.Id, CurrentUser.GetId(), new FlashSaleCurrentResultCacheItem
{
TenantId = CurrentTenant.Id,
ResultDto = new FlashSaleResultDto
{
Id = createFlashSaleResultEto.ResultId,
StoreId = createFlashSaleResultEto.Plan.StoreId,
PlanId = createFlashSaleResultEto.Plan.Id,
UserId = createFlashSaleResultEto.UserId
}
});
await DistributedEventBus.PublishAsync(createFlashSaleResultEto, false, false);
return new FlashSaleOrderResultDto
{
IsSuccess = true
};
}
catch (Exception e)
{
Logger.LogWarning("Failed to publish the CreateFlashSaleOrderEto event!");
Logger.LogException(e, LogLevel.Warning);
await FlashSaleInventoryManager.TryRollBackInventoryAsync(plan.TenantId,
preOrderCache.InventoryProviderName, plan.StoreId, plan.ProductId, plan.ProductSkuId);
await FlashSaleCurrentResultCache.RemoveAsync(plan.Id, CurrentUser.GetId());
return CreateFailureResultDto(FlashSalesErrorCodes.DistributedEventBusUnavailable);
}
}
protected virtual FlashSaleOrderResultDto CreateFailureResultDto(string errorCode)
{
return new FlashSaleOrderResultDto
{
IsSuccess = false,
ErrorCode = errorCode,
ErrorMessage = L[errorCode]
};
}
#region PreOrderCache
protected virtual async Task<FlashSalePlanCacheItem> GetFlashSalePlanCacheAsync(Guid id)
{
return await PlanDistributedCache.GetOrAddAsync(id, async () =>
{
var flashSalePlan = await FlashSalePlanRepository.GetAsync(id);
return ObjectMapper.Map<FlashSalePlan, FlashSalePlanCacheItem>(flashSalePlan);
});
}
protected virtual Task<string> GetPreOrderCacheKeyAsync(Guid planId)
{
return Task.FromResult(string.Format(PreOrderCacheKeyFormat, planId, CurrentUser.Id));
}
protected virtual async Task<FlashSalePlanPreOrderCacheItem> GetPreOrderCacheAsync(Guid planId)
{
return await PreOrderDistributedCache.GetAsync(await GetPreOrderCacheKeyAsync(planId));
}
protected virtual async Task RemovePreOrderCacheAsync(Guid planId)
{
await PreOrderDistributedCache.RemoveAsync(await GetPreOrderCacheKeyAsync(planId));
}
protected virtual async Task SetPreOrderCacheAsync(FlashSalePlanCacheItem plan, ProductDto product, ProductSkuDto productSku, DateTimeOffset expirationTime)
{
var hashToken = await FlashSalePlanHasher.HashAsync(plan.LastModificationTime, product.LastModificationTime, productSku.LastModificationTime);
await PreOrderDistributedCache.SetAsync(await GetPreOrderCacheKeyAsync(plan.Id), new FlashSalePlanPreOrderCacheItem()
{
HashToken = hashToken,
PlanId = plan.Id,
ProductId = product.Id,
ProductSkuId = productSku.Id,
InventoryProviderName = product.InventoryProviderName,
}, new DistributedCacheEntryOptions()
{
AbsoluteExpiration = expirationTime
});
}
#endregion
protected virtual Task ValidatePreOrderAsync(FlashSalePlanCacheItem plan, ProductDto product, ProductSkuDto productSku)
{
if (!product.IsPublished)
{
throw new BusinessException(FlashSalesErrorCodes.ProductIsNotPublished);
}
if (product.InventoryStrategy != InventoryStrategy.FlashSales)
{
throw new UnexpectedInventoryStrategyException(InventoryStrategy.FlashSales);
}
if (!plan.IsPublished)
{
throw new EntityNotFoundException(typeof(FlashSalePlan), plan.Id);
}
if (Clock.Now >= plan.EndTime)
{
throw new BusinessException(FlashSalesErrorCodes.FlashSaleIsOver);
}
return Task.CompletedTask;
}
protected virtual async Task<bool> ExistRelatedFlashSaleResultsAsync(Guid planId)
{
return await FlashSaleResultRepository.AnyAsync(x => x.PlanId == planId);
}
protected virtual Task ValidateProductAsync(Guid productId, ProductDto product, Guid storeId)
{
if (product.StoreId != storeId)
{
throw new ProductIsNotInThisStoreException(productId, storeId);
}
if (product.InventoryStrategy != InventoryStrategy.FlashSales)
{
throw new UnexpectedInventoryStrategyException(InventoryStrategy.FlashSales);
}
return Task.CompletedTask;
}
protected virtual Task<CreateFlashSaleResultEto> PrepareCreateFlashSaleResultEtoAsync(
FlashSalePlanCacheItem plan, OrderFlashSalePlanInput flashSalePlanInput, Guid userId,
DateTime reducedInventoryTime, FlashSalePlanPreOrderCacheItem preOrderCacheItem)
{
var planEto = ObjectMapper.Map<FlashSalePlanCacheItem, FlashSalePlanEto>(plan);
planEto.TenantId = CurrentTenant.Id;
var eto = new CreateFlashSaleResultEto
{
TenantId = CurrentTenant.Id,
UserId = userId,
ResultId = GuidGenerator.Create(),
ReducedInventoryTime = reducedInventoryTime,
CustomerRemark = flashSalePlanInput.CustomerRemark,
Plan = planEto,
ProductInventoryProviderName = preOrderCacheItem.InventoryProviderName,
HashToken = preOrderCacheItem.HashToken
};
if (flashSalePlanInput.ExtraProperties != null)
{
flashSalePlanInput.MapExtraPropertiesTo(eto, MappingPropertyDefinitionChecks.Source);
}
return Task.FromResult(eto);
}
protected virtual async Task<ProductCacheItem> GetProductCacheAsync(Guid productId)
{
return await ProductDistributedCache.GetOrAddAsync(productId, async () =>
{
var productDto = await ProductAppService.GetAsync(productId);
var cacheItem = ObjectMapper.Map<ProductDto, ProductCacheItem>(productDto);
if (cacheItem != null)
{
cacheItem.TenantId = CurrentTenant.Id;
}
return cacheItem;
});
}
}

33
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheInvalidator.cs

@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class FlashSalePlanCacheInvalidator : ILocalEventHandler<EntityChangedEventData<FlashSalePlan>>, ITransientDependency
{
protected IDistributedCache<FlashSalePlanCacheItem, Guid> DistributedCache { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public FlashSalePlanCacheInvalidator(
IDistributedCache<FlashSalePlanCacheItem, Guid> distributedCache,
IUnitOfWorkManager unitOfWorkManager)
{
DistributedCache = distributedCache;
UnitOfWorkManager = unitOfWorkManager;
}
public virtual async Task HandleEventAsync(EntityChangedEventData<FlashSalePlan> eventData)
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
UnitOfWorkManager.Current?.OnCompleted(async () =>
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
});
}
}

11
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheItem.cs

@ -0,0 +1,11 @@
using System;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
using Volo.Abp.MultiTenancy;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
[Serializable]
public class FlashSalePlanCacheItem : FlashSalePlanDto, IMultiTenant
{
public Guid? TenantId { get; set; }
}

20
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanTokenCacheItem.cs

@ -0,0 +1,20 @@
using System;
using Volo.Abp.MultiTenancy;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
[Serializable]
public class FlashSalePlanPreOrderCacheItem : IMultiTenant
{
public Guid? TenantId { get; set; }
public string HashToken { get; set; }
public Guid PlanId { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public string InventoryProviderName { get; set; }
}

13
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductIsNotInThisStoreException.cs

@ -0,0 +1,13 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class ProductIsNotInThisStoreException : BusinessException
{
public ProductIsNotInThisStoreException(Guid productId, Guid storeId) : base(FlashSalesErrorCodes.ProductIsNotInThisStore)
{
WithData(nameof(productId), productId);
WithData(nameof(storeId), storeId);
}
}

12
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductSkuIsNotFoundException.cs

@ -0,0 +1,12 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class ProductSkuIsNotFoundException : BusinessException
{
public ProductSkuIsNotFoundException(Guid productSkuId) : base(FlashSalesErrorCodes.ProductSkuIsNotFound)
{
WithData(nameof(productSkuId), productSkuId);
}
}

13
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/RelatedFlashSaleResultsExistException.cs

@ -0,0 +1,13 @@
using System;
using Volo.Abp;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
[Serializable]
public class RelatedFlashSaleResultsExistException : BusinessException
{
public RelatedFlashSaleResultsExistException(Guid planId) : base(FlashSalesErrorCodes.RelatedFlashSaleResultsExist)
{
WithData(nameof(planId), planId);
}
}

12
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/UnexpectedInventoryStrategyException.cs

@ -0,0 +1,12 @@
using EasyAbp.EShop.Products.Products;
using Volo.Abp;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class UnexpectedInventoryStrategyException : BusinessException
{
public UnexpectedInventoryStrategyException(InventoryStrategy expectedInventoryStrategy) : base(FlashSalesErrorCodes.UnexpectedInventoryStrategy)
{
WithData(nameof(expectedInventoryStrategy), expectedInventoryStrategy);
}
}

132
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEventHandler.cs

@ -0,0 +1,132 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using EasyAbp.Eshop.Products.Products;
using EasyAbp.EShop.Products.Products;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public class CreateFlashSaleResultEventHandler : IDistributedEventHandler<CreateFlashSaleResultEto>,
ITransientDependency
{
protected IObjectMapper ObjectMapper { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected ILogger<CreateFlashSaleResultEventHandler> Logger { get; }
protected IAbpDistributedLock AbpDistributedLock { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected IAbpApplication AbpApplication { get; }
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; }
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; }
protected IFlashSaleResultRepository FlashSaleResultRepository { get; }
public CreateFlashSaleResultEventHandler(
IObjectMapper objectMapper,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ILogger<CreateFlashSaleResultEventHandler> logger,
IAbpDistributedLock abpDistributedLock,
IDistributedEventBus distributedEventBus,
IAbpApplication abpApplication,
IFlashSaleInventoryManager flashSaleInventoryManager,
IFlashSaleCurrentResultCache flashSaleCurrentResultCache,
IFlashSaleResultRepository flashSaleResultRepository)
{
ObjectMapper = objectMapper;
CurrentTenant = currentTenant;
UnitOfWorkManager = unitOfWorkManager;
Logger = logger;
AbpDistributedLock = abpDistributedLock;
DistributedEventBus = distributedEventBus;
AbpApplication = abpApplication;
FlashSaleInventoryManager = flashSaleInventoryManager;
FlashSaleCurrentResultCache = flashSaleCurrentResultCache;
FlashSaleResultRepository = flashSaleResultRepository;
}
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(CreateFlashSaleResultEto eventData)
{
await using var handle = await AbpDistributedLock.TryAcquireAsync(await GetLockKeyAsync(eventData));
if (handle is null)
{
throw new AbpException("Concurrent flash sale result creation");
}
var ongoingResult = await FlashSaleResultRepository.FirstOrDefaultAsync(x =>
x.PlanId == eventData.Plan.Id &&
x.UserId == eventData.UserId &&
x.Status != FlashSaleResultStatus.Failed);
if (ongoingResult is not null)
{
Logger.LogWarning("Duplicate ongoing flash sale result creation");
await FlashSaleCurrentResultCache.SetAsync(eventData.Plan.Id, eventData.UserId,
new FlashSaleCurrentResultCacheItem
{
TenantId = ongoingResult.TenantId,
ResultDto = ObjectMapper.Map<FlashSaleResult, FlashSaleResultDto>(ongoingResult)
});
// try to roll back the inventory.
UnitOfWorkManager.Current.OnCompleted(async () =>
{
using var scope = AbpApplication.ServiceProvider.CreateScope();
var flashSaleInventoryManager = scope.ServiceProvider.GetRequiredService<IFlashSaleInventoryManager>();
if (!await flashSaleInventoryManager.TryRollBackInventoryAsync(eventData.TenantId,
eventData.ProductInventoryProviderName, eventData.Plan.StoreId,
eventData.Plan.ProductId, eventData.Plan.ProductSkuId))
{
Logger.LogWarning("Failed to roll back the flash sale inventory.");
}
});
return; // avoid to create a result entity and an order.
}
var result = new FlashSaleResult(
id: eventData.ResultId,
tenantId: CurrentTenant.Id,
storeId: eventData.Plan.StoreId,
planId: eventData.Plan.Id,
userId: eventData.UserId,
reducedInventoryTime: eventData.ReducedInventoryTime
);
var eto = new CreateFlashSaleOrderEto
{
TenantId = eventData.TenantId,
ResultId = eventData.ResultId,
UserId = eventData.UserId,
CustomerRemark = eventData.CustomerRemark,
Plan = eventData.Plan,
HashToken = eventData.HashToken
};
eventData.MapExtraPropertiesTo(eto, MappingPropertyDefinitionChecks.None);
await DistributedEventBus.PublishAsync(eto);
await FlashSaleResultRepository.InsertAsync(result, autoSave: true);
}
protected virtual Task<string> GetLockKeyAsync(CreateFlashSaleResultEto eventData)
{
return Task.FromResult($"eshopflashsales-creating-result_{eventData.Plan.Id}-{eventData.UserId}");
}
}

59
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCache.cs

@ -0,0 +1,59 @@
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.Options;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public class FlashSaleCurrentResultCache : IFlashSaleCurrentResultCache, ITransientDependency
{
/// <summary>
/// The <see cref="GetKeyAsync(Guid,Guid)"/> cache key format.
/// <para>{0}: FlashSalePlan ID</para>
/// <para>{1}: User ID</para>
/// </summary>
public const string FlashSaleCurrentResultCacheKeyFormat = "eshopflashsales-current-result_{0}_{1}";
protected IDistributedCache<FlashSaleCurrentResultCacheItem> Cache { get; }
protected FlashSalesOptions Options { get; }
public FlashSaleCurrentResultCache(
IDistributedCache<FlashSaleCurrentResultCacheItem> cache,
IOptions<FlashSalesOptions> options)
{
Cache = cache;
Options = options.Value;
}
public virtual async Task<FlashSaleCurrentResultCacheItem> GetAsync(Guid planId, Guid userId)
{
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId);
return await Cache.GetAsync(flashSaleCurrentResultCacheKey);
}
public virtual async Task SetAsync(Guid planId, Guid userId, FlashSaleCurrentResultCacheItem cacheItem)
{
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId);
await Cache.SetAsync(flashSaleCurrentResultCacheKey, cacheItem,
new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.Add(Options.FlashSaleCurrentResultCacheExpires)
});
}
public virtual async Task RemoveAsync(Guid planId, Guid userId)
{
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId);
await Cache.RemoveAsync(flashSaleCurrentResultCacheKey);
}
protected virtual Task<string> GetKeyAsync(Guid planId, Guid userId)
{
return Task.FromResult(string.Format(FlashSaleCurrentResultCacheKeyFormat, planId, userId));
}
}

13
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCacheItem.cs

@ -0,0 +1,13 @@
using System;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using Volo.Abp.MultiTenancy;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
[Serializable]
public class FlashSaleCurrentResultCacheItem : IMultiTenant
{
public Guid? TenantId { get; set; }
public FlashSaleResultDto ResultDto { get; set; }
}

88
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultAppService.cs

@ -0,0 +1,88 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using EasyAbp.EShop.Plugins.FlashSales.Permissions;
using EasyAbp.EShop.Stores.Stores;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Users;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public class FlashSaleResultAppService :
MultiStoreCrudAppService<FlashSaleResult, FlashSaleResultDto, Guid, FlashSaleResultGetListInput>,
IFlashSaleResultAppService
{
protected override string CrossStorePolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.CrossStore;
protected override string GetPolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.Default;
protected override string GetListPolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.Default;
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; }
public FlashSaleResultAppService(
IFlashSaleCurrentResultCache flashSaleCurrentResultCache,
IFlashSaleResultRepository flashSaleResultRepository) : base(flashSaleResultRepository)
{
FlashSaleCurrentResultCache = flashSaleCurrentResultCache;
}
[Authorize]
public override Task<FlashSaleResultDto> GetAsync(Guid id)
{
throw new NotSupportedException();
}
public virtual async Task<FlashSaleResultDto> GetCurrentAsync(Guid planId)
{
await CheckGetPolicyAsync();
var cache = await FlashSaleCurrentResultCache.GetAsync(planId, CurrentUser.GetId());
if (cache is not null)
{
return cache.ResultDto;
}
var list = await GetListAsync(new FlashSaleResultGetListInput
{
MaxResultCount = 1,
Sorting = "Status ASC, CreationTime DESC",
PlanId = planId,
UserId = CurrentUser.GetId()
});
if (list.TotalCount == 0)
{
throw new EntityNotFoundException(typeof(FlashSaleResult));
}
return list.Items[0];
}
public override async Task<PagedResultDto<FlashSaleResultDto>> GetListAsync(FlashSaleResultGetListInput input)
{
if (input.UserId.HasValue && input.UserId == CurrentUser.Id)
{
await CheckGetListPolicyAsync();
}
else
{
await CheckMultiStorePolicyAsync(input.StoreId, GetListPolicyName);
}
return await base.GetListAsync(input);
}
protected override async Task<IQueryable<FlashSaleResult>> CreateFilteredQueryAsync(
FlashSaleResultGetListInput input)
{
return (await base.CreateFilteredQueryAsync(input))
.WhereIf(input.StoreId.HasValue, x => x.StoreId == input.StoreId.Value)
.WhereIf(input.PlanId.HasValue, x => x.PlanId == input.PlanId.Value)
.WhereIf(input.Status.HasValue, x => x.Status == input.Status.Value)
.WhereIf(input.UserId.HasValue, x => x.UserId == input.UserId.Value)
.WhereIf(input.OrderId.HasValue, x => x.OrderId == input.OrderId.Value);
}
}

13
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleCurrentResultCache.cs

@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public interface IFlashSaleCurrentResultCache
{
Task<FlashSaleCurrentResultCacheItem> GetAsync(Guid planId, Guid userId);
Task SetAsync(Guid planId, Guid userId, FlashSaleCurrentResultCacheItem cacheItem);
Task RemoveAsync(Guid planId, Guid userId);
}

13
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesAppService.cs

@ -0,0 +1,13 @@
using EasyAbp.EShop.Plugins.FlashSales.Localization;
using Volo.Abp.Application.Services;
namespace EasyAbp.EShop.Plugins.FlashSales;
public abstract class FlashSalesAppService : ApplicationService
{
protected FlashSalesAppService()
{
LocalizationResource = typeof(FlashSalesResource);
ObjectMapperContext = typeof(EShopPluginsFlashSalesApplicationModule);
}
}

33
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesApplicationAutoMapperProfile.cs

@ -0,0 +1,33 @@
using AutoMapper;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
namespace EasyAbp.EShop.Plugins.FlashSales;
public class FlashSalesApplicationAutoMapperProfile : Profile
{
public FlashSalesApplicationAutoMapperProfile()
{
/* You can configure your AutoMapper mapping configuration here.
* Alternatively, you can split your mapping configurations
* into multiple profile classes for a better organization. */
CreateMap<FlashSalePlan, FlashSalePlanDto>()
.MapExtraProperties();
CreateMap<FlashSalePlanCreateDto, FlashSalePlan>(MemberList.Source);
CreateMap<FlashSalePlanUpdateDto, FlashSalePlan>(MemberList.Source);
CreateMap<FlashSalePlan, FlashSalePlanCacheItem>()
.MapExtraProperties();
CreateMap<FlashSalePlanCacheItem, FlashSalePlanEto>()
.MapExtraProperties();
CreateMap<FlashSaleResult, FlashSaleResultDto>()
.MapExtraProperties();
CreateMap<ProductDto, ProductCacheItem>(MemberList.Source)
.MapExtraProperties();
}
}

16
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/Options/FlashSalesOptions.cs

@ -0,0 +1,16 @@
using System;
namespace EasyAbp.EShop.Plugins.FlashSales.Options;
public class FlashSalesOptions
{
/// <summary>
/// Default: 3 minutes
/// </summary>
public TimeSpan PreOrderExpires { get; set; } = TimeSpan.FromMinutes(3);
/// <summary>
/// Default: 5 minutes
/// </summary>
public TimeSpan FlashSaleCurrentResultCacheExpires { get; set; } = TimeSpan.FromMinutes(5);
}

33
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryManager.cs

@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using EasyAbp.Eshop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Products.Products;
[ExposeServices(typeof(IFlashSaleInventoryManager), IncludeSelf = true)]
[Dependency(TryRegister = true)]
public class FlashSaleInventoryManager : IFlashSaleInventoryManager, ITransientDependency
{
protected IFlashSaleInventoryAppService FlashSaleInventoryReducerAppService { get; }
public FlashSaleInventoryManager(IFlashSaleInventoryAppService flashSaleInventoryReducerAppService)
{
FlashSaleInventoryReducerAppService = flashSaleInventoryReducerAppService;
}
public virtual async Task<bool> TryReduceInventoryAsync(Guid? tenantId, string providerName, Guid storeId,
Guid productId, Guid productSkuId)
{
return await FlashSaleInventoryReducerAppService.TryReduceAsync(new ReduceInventoryInput(
tenantId, providerName, storeId, productId, productSkuId, 1, true));
}
public virtual async Task<bool> TryRollBackInventoryAsync(Guid? tenantId, string providerName, Guid storeId,
Guid productId, Guid productSkuId)
{
return await FlashSaleInventoryReducerAppService.TryIncreaseAsync(new IncreaseInventoryInput(
tenantId, providerName, storeId, productId, productSkuId, 1, true));
}
}

46
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheInvalidator.cs

@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Uow;
namespace EasyAbp.EShop.Products.Products;
public class ProductCacheInvalidator :
IDistributedEventHandler<EntityUpdatedEto<ProductEto>>,
IDistributedEventHandler<EntityDeletedEto<ProductEto>>,
ITransientDependency
{
protected IDistributedCache<ProductCacheItem, Guid> DistributedCache { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public ProductCacheInvalidator(
IDistributedCache<ProductCacheItem, Guid> distributedCache,
IUnitOfWorkManager unitOfWorkManager)
{
DistributedCache = distributedCache;
UnitOfWorkManager = unitOfWorkManager;
}
public virtual async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData)
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
UnitOfWorkManager.Current?.OnCompleted(async () =>
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
});
}
public virtual async Task HandleEventAsync(EntityDeletedEto<ProductEto> eventData)
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
UnitOfWorkManager.Current?.OnCompleted(async () =>
{
await DistributedCache.RemoveAsync(eventData.Entity.Id);
});
}
}

10
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheItem.cs

@ -0,0 +1,10 @@
using System;
using EasyAbp.EShop.Products.Products.Dtos;
using Volo.Abp.MultiTenancy;
namespace EasyAbp.EShop.Products.Products;
public class ProductCacheItem : ProductDto, IMultiTenant
{
public Guid? TenantId { get; set; }
}

3
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

3
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.abppkg.json

@ -0,0 +1,3 @@
{
"role": "lib.domain-shared"
}

28
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Validation" Version="$(AbpVersion)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.*" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="EasyAbp\EShop\Plugins\FlashSales\Localization\*.json" />
<Content Remove="EasyAbp\EShop\Plugins\FlashSales\Localization\*.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Domain.Shared\EasyAbp.EShop.Products.Domain.Shared.csproj" />
</ItemGroup>
</Project>

36
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainSharedModule.cs

@ -0,0 +1,36 @@
using Volo.Abp.Modularity;
using Volo.Abp.Localization;
using EasyAbp.EShop.Plugins.FlashSales.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Validation;
using Volo.Abp.Validation.Localization;
using Volo.Abp.VirtualFileSystem;
namespace EasyAbp.EShop.Plugins.FlashSales;
[DependsOn(
typeof(AbpValidationModule)
)]
public class EShopPluginsFlashSalesDomainSharedModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<EShopPluginsFlashSalesDomainSharedModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<FlashSalesResource>("en")
.AddBaseTypes(typeof(AbpValidationResource))
.AddVirtualJson("EasyAbp/EShop/Plugins/FlashSales/Localization");
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace(FlashSalesErrorCodes.Namespace, typeof(FlashSalesResource));
});
}
}

21
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/CreateFlashSaleOrderEto.cs

@ -0,0 +1,21 @@
using System;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
[Serializable]
public class CreateFlashSaleOrderEto : ExtensibleObject, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid ResultId { get; set; }
public Guid UserId { get; set; }
public string CustomerRemark { get; set; }
public FlashSalePlanEto Plan { get; set; }
public string HashToken { get; set; }
}

32
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEto.cs

@ -0,0 +1,32 @@
using System;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class FlashSaleOrderCreationResultEto : ExtensibleObject, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid ResultId { get; set; }
public bool Success { get; set; }
public Guid StoreId { get; set; }
public Guid PlanId { get; set; }
public string Reason { get; set; }
public Guid UserId { get; set; }
public Guid? OrderId { get; set; }
public string ProductInventoryProviderName { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public bool AllowToTryAgain { get; set; }
}

23
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEto.cs

@ -0,0 +1,23 @@
using System;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
[Serializable]
public class FlashSalePlanEto : FullAuditedEntityEto<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid StoreId { get; set; }
public DateTime BeginTime { get; set; }
public DateTime EndTime { get; set; }
public Guid ProductId { get; set; }
public Guid ProductSkuId { get; set; }
public bool IsPublished { get; set; }
}

31
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanHasher.cs

@ -0,0 +1,31 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public class FlashSalePlanHasher : IFlashSalePlanHasher, ITransientDependency
{
public virtual Task<string> HashAsync(DateTime? planLastModificationTime, DateTime? productLastModificationTime, DateTime? productSkuLastModificationTime)
{
var input = $"{planLastModificationTime?.Ticks}|{productLastModificationTime?.Ticks}|{productSkuLastModificationTime?.Ticks}";
return Task.FromResult(CreateMd5(input));
}
private static string CreateMd5(string input)
{
using var md5 = MD5.Create();
var inputBytes = Encoding.UTF8.GetBytes(input);
var sb = new StringBuilder();
foreach (var t in md5.ComputeHash(inputBytes))
{
sb.Append(t.ToString("X2"));
}
return sb.ToString();
}
}

9
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanHasher.cs

@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
public interface IFlashSalePlanHasher
{
Task<string> HashAsync(DateTime? planLastModificationTime, DateTime? productLastModificationTime, DateTime? productSkuLastModificationTime);
}

26
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEto.cs

@ -0,0 +1,26 @@
using System;
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
[Serializable]
public class CreateFlashSaleResultEto : ExtensibleObject, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid ResultId { get; set; }
public Guid UserId { get; set; }
public DateTime ReducedInventoryTime { get; set; }
public string CustomerRemark { get; set; }
public FlashSalePlanEto Plan { get; set; }
public string ProductInventoryProviderName { get; set; }
public string HashToken { get; set; }
}

6
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultFailedReason.cs

@ -0,0 +1,6 @@
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public static class FlashSaleResultFailedReason
{
public const string InvalidHashToken = "InvalidHashToken";
}

8
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatus.cs

@ -0,0 +1,8 @@
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
public enum FlashSaleResultStatus
{
Pending = 0,
Successful = 1,
Failed = 2
}

34
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalesErrorCodes.cs

@ -0,0 +1,34 @@
namespace EasyAbp.EShop.Plugins.FlashSales;
public static class FlashSalesErrorCodes
{
public const string Namespace = "EasyAbp.EShop.Plugins.FlashSales";
public const string InvalidEndTime = $"{Namespace}:{nameof(InvalidEndTime)}";
public const string ProductIsNotInThisStore = $"{Namespace}:{nameof(ProductIsNotInThisStore)}";
public const string ProductSkuIsNotFound = $"{Namespace}:{nameof(ProductSkuIsNotFound)}";
public const string ProductIsNotPublished = $"{Namespace}:{nameof(ProductIsNotPublished)}";
public const string ProductSkuInventoryExceeded = $"{Namespace}:{nameof(ProductSkuInventoryExceeded)}";
public const string UnexpectedInventoryStrategy = $"{Namespace}:{nameof(UnexpectedInventoryStrategy)}";
public const string PreOrderExpired = $"{Namespace}:{nameof(PreOrderExpired)}";
public const string FlashSaleNotStarted = $"{Namespace}:{nameof(FlashSaleNotStarted)}";
public const string FlashSaleIsOver = $"{Namespace}:{nameof(FlashSaleIsOver)}";
public const string BusyToCreateFlashSaleOrder = $"{Namespace}:{nameof(BusyToCreateFlashSaleOrder)}";
public const string DuplicateFlashSalesOrder = $"{Namespace}:{nameof(DuplicateFlashSalesOrder)}";
public const string RelatedFlashSaleResultsExist = $"{Namespace}:{nameof(RelatedFlashSaleResultsExist)}";
public const string FlashSaleResultStatusNotPending = $"{Namespace}:{nameof(FlashSaleResultStatusNotPending)}";
public const string DistributedEventBusUnavailable = $"{Namespace}:{nameof(DistributedEventBusUnavailable)}";
}

23
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FullAuditedEntityEto.cs

@ -0,0 +1,23 @@
using System;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Plugins.FlashSales;
public class FullAuditedEntityEto<TPrimaryKey> : ExtensibleObject
{
public TPrimaryKey Id { get; set; }
public DateTime CreationTime { get; set; }
public Guid? CreatorId { get; set; }
public DateTime? LastModificationTime { get; set; }
public Guid? LastModifierId { get; set; }
public bool IsDeleted { get; set; }
public Guid? DeleterId { get; set; }
public DateTime? DeletionTime { get; set; }
}

9
plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/FlashSalesResource.cs

@ -0,0 +1,9 @@
using Volo.Abp.Localization;
namespace EasyAbp.EShop.Plugins.FlashSales.Localization;
[LocalizationResourceName("EasyAbpEShopPluginsFlashSales")]
public class FlashSalesResource
{
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save