diff --git a/Directory.Build.props b/Directory.Build.props
index 8ebd2c05..089ab547 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,9 +3,9 @@
5.3.1
2.7.1
- 2.0.14
+ 2.0.15
1.0.1
- 0.1.0
+ 0.1.1
1.7.0
3.6.2
1.0.5
diff --git a/EShop.sln b/EShop.sln
index 859398c4..15b9023c 100644
--- a/EShop.sln
+++ b/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}
diff --git a/common.props b/common.props
index 9ea44847..b31780b5 100644
--- a/common.props
+++ b/common.props
@@ -1,7 +1,7 @@
latest
- 3.0.0-preview.2
+ 3.0.0-preview.18
$(NoWarn);CS1591
true
EasyAbp Team
diff --git a/docs/README.md b/docs/README.md
index 29938b6b..2bbc2bd3 100644
--- a/docs/README.md
+++ b/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
diff --git a/docs/plugins/flash-sales/README.md b/docs/plugins/flash-sales/README.md
new file mode 100644
index 00000000..9b2896a5
--- /dev/null
+++ b/docs/plugins/flash-sales/README.md
@@ -0,0 +1,46 @@
+# EShop.Plugins.FlashSales
+
+[](https://abp.io)
+[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared)
+[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared)
+[](https://discord.gg/S6QaezrCRq)
+[](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.
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs
index 143f4b10..1927d372 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application.Contracts/EasyAbp/EShop/Orders/Orders/Dtos/OrderLineDto.cs
+++ b/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; }
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
index 495930c8..fa253029 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
+++ b/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,
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj
index cee825f5..de525f48 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj
+++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp.EShop.Orders.Domain.Shared.csproj
@@ -12,6 +12,7 @@
+
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/EShopOrdersDomainSharedModule.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/EShopOrdersDomainSharedModule.cs
index 2bf5fdd1..f05614f7 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/EShopOrdersDomainSharedModule.cs
+++ b/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
{
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
index 67aa9901..6240dc00 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
+++ b/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",
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
index 00245f2f..a4f962f4 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
+++ b/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": "多媒体资源",
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
index 6529af18..7db41b9f 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
+++ b/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": "多媒體資源",
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs
index feadc7e2..f63f14a3 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/IOrderLine.cs
+++ b/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; }
-
+
+ ///
+ /// If it is null, should get the from the Product entity.
+ /// See https://github.com/EasyAbp/EShop/issues/215
+ ///
+ InventoryStrategy? ProductInventoryStrategy { get; }
+
string SkuName { get; }
-
+
string SkuDescription { get; }
-
+
string MediaResources { get; }
-
+
string Currency { get; }
-
+
decimal UnitPrice { get; }
-
+
decimal TotalPrice { get; }
-
+
decimal TotalDiscount { get; }
-
+
///
/// ActualTotalPrice = TotalPrice - TotalDiscount
///
decimal ActualTotalPrice { get; }
int Quantity { get; }
-
+
int RefundedQuantity { get; }
decimal RefundAmount { get; }
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs
index 7698bc53..6bce13d7 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderLineEto.cs
+++ b/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; }
diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs
index 9967f0d2..e8b84996 100644
--- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderLine.cs
+++ b/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;
diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
index a2f4896b..7ca763f0 100644
--- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
+++ b/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
+ {
+ 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
+ {
+ 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();
+ }
}
}
\ No newline at end of file
diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderCreatedEventHandler.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderCreatedEventHandler.cs
index ea8e7ae3..da491e8b 100644
--- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderCreatedEventHandler.cs
+++ b/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>, 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 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
diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
index c20476dc..13806a0e 100644
--- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
+++ b/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,
diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
index 4a4a61f1..e28f5e3c 100644
--- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
+++ b/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,
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs
index 6301fa24..0f88780f 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs
+++ b/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 CategoryIds { get; set; }
-
+
[DisplayName("ProductUniqueName")]
public string UniqueName { get; set; }
-
+
[Required]
[DisplayName("ProductDisplayName")]
public string DisplayName { get; set; }
-
+
public ICollection 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" }
+ );
+ }
}
}
}
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
index eaf89186..981abbed 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
@@ -12,8 +12,10 @@ namespace EasyAbp.EShop.Products.ProductInventories
Task> GetSkuIdInventoryDataMappingAsync(IList models);
- Task TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold);
+ Task TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold,
+ bool isFlashSale = false);
- Task TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold);
+ Task TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold,
+ bool isFlashSale = false);
}
}
\ No newline at end of file
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj
index 2dab26aa..663bb9c9 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj
@@ -8,6 +8,7 @@
+
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
index 613a434c..82b9c1a6 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
@@ -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
{
+ ///
+ /// The lock key format.
+ /// {0}: Tenant ID
+ /// {1}: Product ID
+ /// {2}: ProductSku ID
+ ///
+ 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 _logger;
public DefaultProductInventoryProvider(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IDistributedEventBus distributedEventBus,
- IProductInventoryRepository productInventoryRepository)
+ IProductInventoryRepository productInventoryRepository,
+ IAbpDistributedLock distributedLock,
+ ILogger 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 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 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 GetLockKeyAsync(InventoryQueryModel model)
+ {
+ return Task.FromResult(string.Format(DefaultProductInventoryLockKeyFormat, model.TenantId, model.ProductId, model.ProductSkuId));
+ }
}
}
\ No newline at end of file
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs
index 1fd1daca..09ea7ab9 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs
@@ -9,4 +9,6 @@ public interface IProductInventoryProviderResolver
Task ExistProviderAsync([NotNull] string providerName);
Task GetAsync(Product product);
+
+ Task GetAsync([NotNull] string providerName);
}
\ No newline at end of file
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs
index 6b8f3d70..386575b5 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs
+++ b/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, ITransientDependency
{
+ private readonly ILogger _logger;
private readonly IProductManager _productManager;
private readonly IProductRepository _productRepository;
public InventoryRollbackOrderCanceledEventHandler(
+ ILogger 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);
+ }
}
}
}
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
index 1e16d1ba..c0d80119 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
@@ -39,25 +39,36 @@ namespace EasyAbp.EShop.Products.Products
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(EntityCreatedEto 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();
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;
}
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
index 0d2a82fc..4f3b5140 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
@@ -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;
}
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs
index bd1e4472..a3b18957 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs
+++ b/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 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>();
+ providerName = options.Value.DefaultInventoryProviderName ?? DefaultProductInventoryProvider.DefaultProductInventoryProviderName;
}
TryBuildNameToProviderTypeMapping();
@@ -67,7 +72,7 @@ public class ProductInventoryProviderResolver : IProductInventoryProviderResolve
{
return;
}
-
+
var options = ServiceProvider.GetRequiredService>().Value;
foreach (var pair in options.InventoryProviders.GetConfigurationsDictionary())
diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
index a6f7e35c..ade8db3d 100644
--- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
+++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
@@ -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 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 GetRealPriceAsync(Product product, ProductSku productSku)
diff --git a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs b/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs
index e49d2ff2..b5c5ddbb 100644
--- a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs
+++ b/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 TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold)
+ public Task 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 TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold)
+ public Task TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold,
+ bool isFlashSale = false)
{
Model.Inventory--;
-
+
if (increaseSold)
{
Model.Sold++;
diff --git a/plugins/FlashSales/.editorconfig b/plugins/FlashSales/.editorconfig
new file mode 100644
index 00000000..6f8b7846
--- /dev/null
+++ b/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
diff --git a/plugins/FlashSales/README.md b/plugins/FlashSales/README.md
new file mode 120000
index 00000000..5cd476be
--- /dev/null
+++ b/plugins/FlashSales/README.md
@@ -0,0 +1 @@
+../../docs/plugins/flash-sales/README.md
\ No newline at end of file
diff --git a/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.abppkg.json b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.abppkg.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.abppkg.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.csproj b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.csproj
new file mode 100644
index 00000000..1669f598
--- /dev/null
+++ b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp.EShop.Plugins.FlashSales.Host.Shared.csproj
@@ -0,0 +1,10 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
diff --git a/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp/EShop/Plugins/FlashSales/MultiTenancy/MultiTenancyConsts.cs b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/EasyAbp/EShop/Plugins/FlashSales/MultiTenancy/MultiTenancyConsts.cs
new file mode 100644
index 00000000..6d205dd3
--- /dev/null
+++ b/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;
+}
diff --git a/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xml b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xsd b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/host/EasyAbp.EShop.Plugins.FlashSales.Host.Shared/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj
new file mode 100644
index 00000000..5455d3fd
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandler.cs b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandler.cs
new file mode 100644
index 00000000..9c0c7ddb
--- /dev/null
+++ b/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, ITransientDependency
+{
+ protected INewOrderGenerator NewOrderGenerator { get; }
+
+ protected IObjectMapper ObjectMapper { get; }
+
+ protected IEnumerable 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 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> GetProductDictionaryAsync(ProductDto product)
+ {
+ var productDict = new Dictionary()
+ {
+ {product.Id, product}
+ };
+
+ return Task.FromResult(productDict);
+ }
+
+ protected virtual async Task> GetProductDetailDictionaryAsync(ProductDto product, ProductSkuDto productSku)
+ {
+ var dict = new Dictionary();
+
+ var productDetailId = productSku.ProductDetailId ?? product.ProductDetailId;
+
+ if (productDetailId.HasValue)
+ {
+ dict.Add(productDetailId.Value, await ProductDetailAppService.GetAsync(productDetailId.Value));
+ }
+
+ return dict;
+ }
+
+ protected virtual Task ConvertToCreateOrderDtoAsync(CreateFlashSaleOrderEto eventData)
+ {
+ var input = new CreateOrderDto()
+ {
+ StoreId = eventData.Plan.StoreId,
+ CustomerRemark = eventData.CustomerRemark,
+ OrderLines = new List()
+ {
+ new CreateOrderLineDto()
+ {
+ ProductId = eventData.Plan.ProductId,
+ ProductSkuId = eventData.Plan.ProductSkuId,
+ Quantity = 1
+ }
+ }
+ };
+
+ eventData.MapExtraPropertiesTo(input);
+
+ return Task.FromResult(input);
+ }
+
+ protected virtual async Task 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);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationModule.cs
new file mode 100644
index 00000000..6086e4d7
--- /dev/null
+++ b/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();
+ Configure(options =>
+ {
+ options.AddMaps(validate: true);
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Orders.Plugins.FlashSales.Application/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.abppkg.json
new file mode 100644
index 00000000..49032794
--- /dev/null
+++ b/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"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj
new file mode 100644
index 00000000..b29cdfb9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationContractsModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationContractsModule.cs
new file mode 100644
index 00000000..bfdd3261
--- /dev/null
+++ b/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
+{
+
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesRemoteServiceConsts.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesRemoteServiceConsts.cs
new file mode 100644
index 00000000..0250ea15
--- /dev/null
+++ b/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";
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSaleOrderResultDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSaleOrderResultDto.cs
new file mode 100644
index 00000000..26fb18ba
--- /dev/null
+++ b/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; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanCreateDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanCreateDto.cs
new file mode 100644
index 00000000..40b5d80c
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanDto.cs
new file mode 100644
index 00000000..5e16106e
--- /dev/null
+++ b/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, 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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanGetListInput.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanGetListInput.cs
new file mode 100644
index 00000000..9192589a
--- /dev/null
+++ b/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; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanPreOrderDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanPreOrderDto.cs
new file mode 100644
index 00000000..ccfdea14
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanUpdateDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/FlashSalePlanUpdateDto.cs
new file mode 100644
index 00000000..0a4075f3
--- /dev/null
+++ b/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; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/OrderFlashSalePlanInput.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/Dtos/OrderFlashSalePlanInput.cs
new file mode 100644
index 00000000..99410899
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanAppService.cs
new file mode 100644
index 00000000..66b72049
--- /dev/null
+++ b/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 PreOrderAsync(Guid id);
+
+ Task OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput);
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultDto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultDto.cs
new file mode 100644
index 00000000..46c19205
--- /dev/null
+++ b/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, 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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultGetListInput.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/Dtos/FlashSaleResultGetListInput.cs
new file mode 100644
index 00000000..b5e0b823
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultAppService.cs
new file mode 100644
index 00000000..4b95c6a5
--- /dev/null
+++ b/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 GetCurrentAsync(Guid planId);
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissionDefinitionProvider.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissionDefinitionProvider.cs
new file mode 100644
index 00000000..2f985ef0
--- /dev/null
+++ b/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(name);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Plugins/FlashSales/Permissions/FlashSalesPermissions.cs
new file mode 100644
index 00000000..cd4d571a
--- /dev/null
+++ b/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";
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.abppkg.json
new file mode 100644
index 00000000..412567ac
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.application"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.csproj
new file mode 100644
index 00000000..12758834
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp.EShop.Plugins.FlashSales.Application.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationModule.cs
new file mode 100644
index 00000000..48e94a13
--- /dev/null
+++ b/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();
+ Configure(options =>
+ {
+ options.AddMaps(validate: true);
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandler.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandler.cs
new file mode 100644
index 00000000..f337268e
--- /dev/null
+++ b/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,
+ ITransientDependency
+{
+ protected ILogger 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 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();
+ var flashSaleCurrentResultCache = scope.ServiceProvider.GetRequiredService();
+
+ await flashSaleCurrentResultCache.SetAsync(flashSaleResult.PlanId, flashSaleResult.UserId,
+ new FlashSaleCurrentResultCacheItem
+ {
+ TenantId = flashSaleResult.TenantId,
+ ResultDto = objectMapper.Map(flashSaleResult)
+ });
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppService.cs
new file mode 100644
index 00000000..ab38c192
--- /dev/null
+++ b/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,
+ IFlashSalePlanAppService
+{
+ ///
+ /// The cache key format.
+ /// {0}: FlashSalePlan ID
+ /// {1}: User ID
+ ///
+ 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 PreOrderDistributedCache { get; }
+
+ protected IDistributedCache PlanDistributedCache { get; }
+
+ protected IDistributedCache 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 tokenDistributedCache,
+ IDistributedCache planDistributedCache,
+ IDistributedCache productDistributedCache,
+ IDistributedEventBus distributedEventBus,
+ IFlashSaleResultRepository flashSaleResultRepository,
+ IAbpDistributedLock distributedLock,
+ IFlashSalePlanHasher flashSalePlanHasher,
+ IFlashSaleInventoryManager flashSaleInventoryManager,
+ IFlashSaleCurrentResultCache flashSaleCurrentResultCache,
+ IOptions 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 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> GetListAsync(FlashSalePlanGetListInput input)
+ {
+ await CheckGetListPolicyAsync();
+
+ if (input.IncludeUnpublished)
+ {
+ await CheckMultiStorePolicyAsync(input.StoreId, FlashSalesPermissions.FlashSalePlan.Manage);
+ }
+
+ return await base.GetListAsync(input);
+ }
+
+ public override async Task 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 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> 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 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 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 GetFlashSalePlanCacheAsync(Guid id)
+ {
+ return await PlanDistributedCache.GetOrAddAsync(id, async () =>
+ {
+ var flashSalePlan = await FlashSalePlanRepository.GetAsync(id);
+ return ObjectMapper.Map(flashSalePlan);
+ });
+ }
+
+ protected virtual Task GetPreOrderCacheKeyAsync(Guid planId)
+ {
+ return Task.FromResult(string.Format(PreOrderCacheKeyFormat, planId, CurrentUser.Id));
+ }
+
+ protected virtual async Task 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 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 PrepareCreateFlashSaleResultEtoAsync(
+ FlashSalePlanCacheItem plan, OrderFlashSalePlanInput flashSalePlanInput, Guid userId,
+ DateTime reducedInventoryTime, FlashSalePlanPreOrderCacheItem preOrderCacheItem)
+ {
+ var planEto = ObjectMapper.Map(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 GetProductCacheAsync(Guid productId)
+ {
+ return await ProductDistributedCache.GetOrAddAsync(productId, async () =>
+ {
+ var productDto = await ProductAppService.GetAsync(productId);
+
+ var cacheItem = ObjectMapper.Map(productDto);
+
+ if (cacheItem != null)
+ {
+ cacheItem.TenantId = CurrentTenant.Id;
+ }
+
+ return cacheItem;
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheInvalidator.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheInvalidator.cs
new file mode 100644
index 00000000..d8320613
--- /dev/null
+++ b/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>, ITransientDependency
+{
+ protected IDistributedCache DistributedCache { get; }
+ protected IUnitOfWorkManager UnitOfWorkManager { get; }
+
+ public FlashSalePlanCacheInvalidator(
+ IDistributedCache distributedCache,
+ IUnitOfWorkManager unitOfWorkManager)
+ {
+ DistributedCache = distributedCache;
+ UnitOfWorkManager = unitOfWorkManager;
+ }
+
+ public virtual async Task HandleEventAsync(EntityChangedEventData eventData)
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+
+ UnitOfWorkManager.Current?.OnCompleted(async () =>
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheItem.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanCacheItem.cs
new file mode 100644
index 00000000..4aefcbad
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanTokenCacheItem.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanTokenCacheItem.cs
new file mode 100644
index 00000000..efaff0e9
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductIsNotInThisStoreException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductIsNotInThisStoreException.cs
new file mode 100644
index 00000000..3733fb84
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductSkuIsNotFoundException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/ProductSkuIsNotFoundException.cs
new file mode 100644
index 00000000..f1ab3263
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/RelatedFlashSaleResultsExistException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/RelatedFlashSaleResultsExistException.cs
new file mode 100644
index 00000000..b50dfeb6
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/UnexpectedInventoryStrategyException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/UnexpectedInventoryStrategyException.cs
new file mode 100644
index 00000000..cc9deae9
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEventHandler.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEventHandler.cs
new file mode 100644
index 00000000..2244db6e
--- /dev/null
+++ b/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,
+ ITransientDependency
+{
+ protected IObjectMapper ObjectMapper { get; }
+ protected ICurrentTenant CurrentTenant { get; }
+ protected IUnitOfWorkManager UnitOfWorkManager { get; }
+ protected ILogger 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 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(ongoingResult)
+ });
+
+ // try to roll back the inventory.
+ UnitOfWorkManager.Current.OnCompleted(async () =>
+ {
+ using var scope = AbpApplication.ServiceProvider.CreateScope();
+
+ var flashSaleInventoryManager = scope.ServiceProvider.GetRequiredService();
+
+ 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 GetLockKeyAsync(CreateFlashSaleResultEto eventData)
+ {
+ return Task.FromResult($"eshopflashsales-creating-result_{eventData.Plan.Id}-{eventData.UserId}");
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCache.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCache.cs
new file mode 100644
index 00000000..f88863a5
--- /dev/null
+++ b/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
+{
+ ///
+ /// The cache key format.
+ /// {0}: FlashSalePlan ID
+ /// {1}: User ID
+ ///
+ public const string FlashSaleCurrentResultCacheKeyFormat = "eshopflashsales-current-result_{0}_{1}";
+
+ protected IDistributedCache Cache { get; }
+ protected FlashSalesOptions Options { get; }
+
+ public FlashSaleCurrentResultCache(
+ IDistributedCache cache,
+ IOptions options)
+ {
+ Cache = cache;
+ Options = options.Value;
+ }
+
+ public virtual async Task 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 GetKeyAsync(Guid planId, Guid userId)
+ {
+ return Task.FromResult(string.Format(FlashSaleCurrentResultCacheKeyFormat, planId, userId));
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCacheItem.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleCurrentResultCacheItem.cs
new file mode 100644
index 00000000..032c129a
--- /dev/null
+++ b/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; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultAppService.cs
new file mode 100644
index 00000000..205b8eed
--- /dev/null
+++ b/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,
+ 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 GetAsync(Guid id)
+ {
+ throw new NotSupportedException();
+ }
+
+ public virtual async Task 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> 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> 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);
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleCurrentResultCache.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleCurrentResultCache.cs
new file mode 100644
index 00000000..e6b63e7d
--- /dev/null
+++ b/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 GetAsync(Guid planId, Guid userId);
+
+ Task SetAsync(Guid planId, Guid userId, FlashSaleCurrentResultCacheItem cacheItem);
+
+ Task RemoveAsync(Guid planId, Guid userId);
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesAppService.cs
new file mode 100644
index 00000000..4d6c6e6a
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesApplicationAutoMapperProfile.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/FlashSalesApplicationAutoMapperProfile.cs
new file mode 100644
index 00000000..b66c03f5
--- /dev/null
+++ b/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()
+ .MapExtraProperties();
+ CreateMap(MemberList.Source);
+ CreateMap(MemberList.Source);
+ CreateMap()
+ .MapExtraProperties();
+ CreateMap()
+ .MapExtraProperties();
+
+ CreateMap()
+ .MapExtraProperties();
+
+ CreateMap(MemberList.Source)
+ .MapExtraProperties();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/Options/FlashSalesOptions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Plugins/FlashSales/Options/FlashSalesOptions.cs
new file mode 100644
index 00000000..cb677006
--- /dev/null
+++ b/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
+{
+ ///
+ /// Default: 3 minutes
+ ///
+ public TimeSpan PreOrderExpires { get; set; } = TimeSpan.FromMinutes(3);
+
+ ///
+ /// Default: 5 minutes
+ ///
+ public TimeSpan FlashSaleCurrentResultCacheExpires { get; set; } = TimeSpan.FromMinutes(5);
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryManager.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryManager.cs
new file mode 100644
index 00000000..41ad7ee8
--- /dev/null
+++ b/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 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 TryRollBackInventoryAsync(Guid? tenantId, string providerName, Guid storeId,
+ Guid productId, Guid productSkuId)
+ {
+ return await FlashSaleInventoryReducerAppService.TryIncreaseAsync(new IncreaseInventoryInput(
+ tenantId, providerName, storeId, productId, productSkuId, 1, true));
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheInvalidator.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheInvalidator.cs
new file mode 100644
index 00000000..4e975b85
--- /dev/null
+++ b/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>,
+ IDistributedEventHandler>,
+ ITransientDependency
+{
+ protected IDistributedCache DistributedCache { get; }
+ protected IUnitOfWorkManager UnitOfWorkManager { get; }
+
+ public ProductCacheInvalidator(
+ IDistributedCache distributedCache,
+ IUnitOfWorkManager unitOfWorkManager)
+ {
+ DistributedCache = distributedCache;
+ UnitOfWorkManager = unitOfWorkManager;
+ }
+
+ public virtual async Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+
+ UnitOfWorkManager.Current?.OnCompleted(async () =>
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+ });
+ }
+
+ public virtual async Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+
+ UnitOfWorkManager.Current?.OnCompleted(async () =>
+ {
+ await DistributedCache.RemoveAsync(eventData.Entity.Id);
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheItem.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ProductCacheItem.cs
new file mode 100644
index 00000000..0c59d0f7
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Application/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.abppkg.json
new file mode 100644
index 00000000..8b3de05f
--- /dev/null
+++ b/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"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj
new file mode 100644
index 00000000..95717151
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj
@@ -0,0 +1,28 @@
+
+
+
+
+
+ netstandard2.0
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainSharedModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainSharedModule.cs
new file mode 100644
index 00000000..2063355f
--- /dev/null
+++ b/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(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ .AddBaseTypes(typeof(AbpValidationResource))
+ .AddVirtualJson("EasyAbp/EShop/Plugins/FlashSales/Localization");
+ });
+
+ Configure(options =>
+ {
+ options.MapCodeNamespace(FlashSalesErrorCodes.Namespace, typeof(FlashSalesResource));
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/CreateFlashSaleOrderEto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/CreateFlashSaleOrderEto.cs
new file mode 100644
index 00000000..4f50a1d3
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEto.cs
new file mode 100644
index 00000000..88ce602b
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEto.cs
new file mode 100644
index 00000000..275e352f
--- /dev/null
+++ b/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, 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; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanHasher.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanHasher.cs
new file mode 100644
index 00000000..783c44b3
--- /dev/null
+++ b/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 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();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanHasher.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanHasher.cs
new file mode 100644
index 00000000..6da39e91
--- /dev/null
+++ b/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 HashAsync(DateTime? planLastModificationTime, DateTime? productLastModificationTime, DateTime? productSkuLastModificationTime);
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/CreateFlashSaleResultEto.cs
new file mode 100644
index 00000000..b37298ba
--- /dev/null
+++ b/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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultFailedReason.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultFailedReason.cs
new file mode 100644
index 00000000..d02bcc8d
--- /dev/null
+++ b/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";
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatus.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatus.cs
new file mode 100644
index 00000000..4b2c972b
--- /dev/null
+++ b/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
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalesErrorCodes.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FlashSalesErrorCodes.cs
new file mode 100644
index 00000000..94479a94
--- /dev/null
+++ b/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)}";
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FullAuditedEntityEto.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/FullAuditedEntityEto.cs
new file mode 100644
index 00000000..7961d1f6
--- /dev/null
+++ b/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 : 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; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/FlashSalesResource.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/FlashSalesResource.cs
new file mode 100644
index 00000000..b2519b45
--- /dev/null
+++ b/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
+{
+
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/en.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/en.json
new file mode 100644
index 00000000..a090db59
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/en.json
@@ -0,0 +1,55 @@
+{
+ "culture": "en",
+ "texts": {
+ "EasyAbp.EShop.Plugins.FlashSales:InvalidEndTime": "End time must be later than start time",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotInThisStore": "Product '{productId}' is not in this store '{storeId}'",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuIsNotFound": "ProductSku '{productSkuId}' is not found",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotPublished": "Product is not published",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuInventoryExceeded": "ProductSku inventory exceeded",
+ "EasyAbp.EShop.Plugins.FlashSales:UnexpectedInventoryStrategy": "Only the specified inventory strategy {expectedInventoryStrategy} product is allowed.",
+ "EasyAbp.EShop.Plugins.FlashSales:PreOrderExpired": "Pre order expired",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleNotStarted": "Flash-sale not started",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleIsOver": "Flash-sale is over",
+ "EasyAbp.EShop.Plugins.FlashSales:BusyToCreateFlashSaleOrder": "Busy to create flash-sale order",
+ "EasyAbp.EShop.Plugins.FlashSales:DuplicateFlashSalesOrder": "Duplicate flash-sales order",
+ "EasyAbp.EShop.Plugins.FlashSales:RelatedFlashSaleResultsExist": "Related flash-sales results exist",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleResultStatusNotPending": "The status of flash-sale result has been changed",
+ "EasyAbp.EShop.Plugins.FlashSales:Menu:DistributedEventBusUnavailable": "The distributed event bus is unavailable",
+ "Menu:FlashSalesManagement": "Flash-Sales Management",
+ "Permission:FlashSalePlan": "FlashSale Plan",
+ "Permission:Manage": "Manage",
+ "Permission:Create": "Create",
+ "Permission:Update": "Update",
+ "Permission:Delete": "Delete",
+ "Menu:FlashSalePlan": "Flash-Sale Plan",
+ "FlashSalePlan": "Flash-Sale Plan",
+ "FlashSalePlanStoreId": "Store ID",
+ "FlashSalePlanBeginTime": "Begin Time",
+ "FlashSalePlanEndTime": "End Time",
+ "FlashSalePlanProductId": "Product ID",
+ "FlashSalePlanProductSkuId": "Product SKU ID",
+ "FlashSalePlanIsPublished": "Is Published",
+ "CreateFlashSalePlan": "Create",
+ "EditFlashSalePlan": "Edit",
+ "FlashSalePlanDeletionConfirmationMessage": "Are you sure to delete the flash-sale plan {0}?",
+ "SuccessfullyDeleted": "Successfully deleted",
+ "Permission:FlashSaleResult": "Flash-Sale Result",
+ "Menu:FlashSaleResult": "Flash-Sale Result",
+ "FlashSaleResult": "Flash-Sale Result",
+ "FlashSaleResultStoreId": "Store ID",
+ "FlashSaleResultPlanId": "Plan ID",
+ "FlashSaleResultStatus": "Status",
+ "FlashSaleResultReason": "Reason",
+ "FlashSaleResultUserId": "User ID",
+ "FlashSaleResultOrderId": "Order ID",
+ "FlashSaleResultReducedInventoryTime": "Inventory Reduction Time",
+ "FlashSaleResultCreationTime": "Creation Time",
+ "ViewFlashSaleResult": "View",
+ "Enum:FlashSaleResultStatus.Pending": "Pending",
+ "Enum:FlashSaleResultStatus.Successful": "Successful",
+ "Enum:FlashSaleResultStatus.Failed": "Failed",
+ "Enum:FlashSaleResultStatus.0": "Pending",
+ "Enum:FlashSaleResultStatus.1": "Successful",
+ "Enum:FlashSaleResultStatus.2": "Failed"
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hans.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hans.json
new file mode 100644
index 00000000..2a5a34e8
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hans.json
@@ -0,0 +1,55 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "EasyAbp.EShop.Plugins.FlashSales:InvalidEndTime": "结束时间必须晚于开始时间",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotInThisStore": "产品'{productId}'不在此店铺'{storeId}'中",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuIsNotFound": "产品SKU'{productSkuId}'不存在",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotPublished": "产品尚未发布",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuInventoryExceeded": "产品SKU库存不足",
+ "EasyAbp.EShop.Plugins.FlashSales:UnexpectedInventoryStrategy": "只允许使用指定{expectedInventoryStrategy}库存策略的产品",
+ "EasyAbp.EShop.Plugins.FlashSales:PreOrderExpired": "预下单已过期",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleNotStarted": "闪购尚未开始",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleIsOver": "闪购已经结束",
+ "EasyAbp.EShop.Plugins.FlashSales:BusyToCreateFlashSaleOrder": "闪购下单繁忙",
+ "EasyAbp.EShop.Plugins.FlashSales:DuplicateFlashSalesOrder": "重复闪购下单",
+ "EasyAbp.EShop.Plugins.FlashSales:RelatedFlashSaleResultsExist": "已存在关联的闪购结果",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleResultStatusNotPending": "闪购结果的状态已经变更",
+ "EasyAbp.EShop.Plugins.FlashSales:Menu:DistributedEventBusUnavailable": "分布式事件总线不可用",
+ "Menu:FlashSalesManagement": "闪购",
+ "Permission:FlashSalePlan": "闪购计划",
+ "Permission:Manage": "管理",
+ "Permission:Create": "新建",
+ "Permission:Update": "更新",
+ "Permission:Delete": "删除",
+ "Menu:FlashSalePlan": "闪购计划",
+ "FlashSalePlan": "闪购计划",
+ "FlashSalePlanStoreId": "店铺 ID",
+ "FlashSalePlanBeginTime": "开始时间",
+ "FlashSalePlanEndTime": "结束时间",
+ "FlashSalePlanProductId": "产品 ID",
+ "FlashSalePlanProductSkuId": "产品SKU ID",
+ "FlashSalePlanIsPublished": "发布",
+ "CreateFlashSalePlan": "新建",
+ "EditFlashSalePlan": "編輯",
+ "FlashSalePlanDeletionConfirmationMessage": "确认删除闪购计划 {0}?",
+ "SuccessfullyDeleted": "删除成功",
+ "Permission:FlashSaleResult": "闪购结果",
+ "Menu:FlashSaleResult": "闪购结果",
+ "FlashSaleResult": "闪购结果",
+ "FlashSaleResultStoreId": "店铺 ID",
+ "FlashSaleResultPlanId": "计划 ID",
+ "FlashSaleResultStatus": "状态",
+ "FlashSaleResultReason": "原因",
+ "FlashSaleResultUserId": "用户 ID",
+ "FlashSaleResultOrderId": "订单 ID",
+ "FlashSaleResultReducedInventoryTime": "扣库存时间",
+ "FlashSaleResultCreationTime": "创建时间",
+ "ViewFlashSaleResult": "查看",
+ "Enum:FlashSaleResultStatus.Pending": "等待中",
+ "Enum:FlashSaleResultStatus.Successful": "成功",
+ "Enum:FlashSaleResultStatus.Failed": "失败",
+ "Enum:FlashSaleResultStatus.0": "等待中",
+ "Enum:FlashSaleResultStatus.1": "成功",
+ "Enum:FlashSaleResultStatus.2": "失败"
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hant.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hant.json
new file mode 100644
index 00000000..91ad46ab
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/EasyAbp/EShop/Plugins/FlashSales/Localization/zh-Hant.json
@@ -0,0 +1,55 @@
+{
+ "culture": "zh-Hant",
+ "texts": {
+ "EasyAbp.EShop.Plugins.FlashSales:InvalidEndTime": "結束時間必須晚於開始時間",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotInThisStore": "產品'{productId}'不在此店鋪'{storeId}'中",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuIsNotFound": "產品SKU'{productSkuId}'不存在",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductIsNotPublished": "產品尚未發佈",
+ "EasyAbp.EShop.Plugins.FlashSales:ProductSkuInventoryExceeded": "產品SKU庫存不足",
+ "EasyAbp.EShop.Plugins.FlashSales:UnexpectedInventoryStrategy": "只允許使用指定的{expectedInventoryStrategy}庫存策略的產品",
+ "EasyAbp.EShop.Plugins.FlashSales:PreOrderExpired": "預下單已過期",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleNotStarted": "閃購尚未開始",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleIsOver": "閃購已經結束",
+ "EasyAbp.EShop.Plugins.FlashSales:BusyToCreateFlashSaleOrder": "閃購下單繁忙",
+ "EasyAbp.EShop.Plugins.FlashSales:DuplicateFlashSalesOrder": "重複閃購下單",
+ "EasyAbp.EShop.Plugins.FlashSales:RelatedFlashSaleResultsExist": "已存在關聯的閃購結果",
+ "EasyAbp.EShop.Plugins.FlashSales:FlashSaleResultStatusNotPending": "閃購結果的状态已經變更",
+ "EasyAbp.EShop.Plugins.FlashSales:Menu:DistributedEventBusUnavailable": "分佈式事件總線不可用",
+ "Menu:FlashSalesManagement": "閃購管理",
+ "Permission:FlashSalePlan": "閃購計畫",
+ "Permission:Manage": "管理",
+ "Permission:Create": "新建",
+ "Permission:Update": "更新",
+ "Permission:Delete": "删除",
+ "Menu:FlashSalePlan": "閃購計畫",
+ "FlashSalePlan": "閃購計畫",
+ "FlashSalePlanStoreId": "店鋪ID",
+ "FlashSalePlanBeginTime": "開始時間",
+ "FlashSalePlanEndTime": "結束時間",
+ "FlashSalePlanProductId": "產品ID",
+ "FlashSalePlanProductSkuId": "產品SKU ID",
+ "FlashSalePlanIsPublished": "發佈",
+ "CreateFlashSalePlan": "新建",
+ "EditFlashSalePlan": "更新",
+ "FlashSalePlanDeletionConfirmationMessage": "確認删除閃購計畫 {0}?",
+ "SuccessfullyDeleted": "删除成功",
+ "Permission:FlashSaleResult": "閃購結果",
+ "Menu:FlashSaleResult": "閃購結果",
+ "FlashSaleResult": "閃購結果",
+ "FlashSaleResultStoreId": "店铺 ID",
+ "FlashSaleResultPlanId": "計畫 ID",
+ "FlashSaleResultStatus": "狀態",
+ "FlashSaleResultReason": "原因",
+ "FlashSaleResultUserId": "用戶 ID",
+ "FlashSaleResultOrderId": "訂單 ID",
+ "FlashSaleResultReducedInventoryTime": "扣庫存時間",
+ "FlashSaleResultCreationTime": "創建時間",
+ "ViewFlashSaleResult": "查看",
+ "Enum:FlashSaleResultStatus.Pending": "等待中",
+ "Enum:FlashSaleResultStatus.Successful": "成功",
+ "Enum:FlashSaleResultStatus.Failed": "失敗",
+ "Enum:FlashSaleResultStatus.0": "等待中",
+ "Enum:FlashSaleResultStatus.1": "成功",
+ "Enum:FlashSaleResultStatus.2": "失敗"
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain.Shared/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.abppkg.json
new file mode 100644
index 00000000..1d574efe
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.domain"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.csproj
new file mode 100644
index 00000000..15aa2bfd
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp.EShop.Plugins.FlashSales.Domain.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainAutoMapperProfile.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainAutoMapperProfile.cs
new file mode 100644
index 00000000..fed8b5cd
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainAutoMapperProfile.cs
@@ -0,0 +1,16 @@
+using AutoMapper;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+public class EShopPluginsFlashSalesDomainAutoMapperProfile : Profile
+{
+ public EShopPluginsFlashSalesDomainAutoMapperProfile()
+ {
+ /* You can configure your AutoMapper mapping configuration here.
+ * Alternatively, you can split your mapping configurations
+ * into multiple profile classes for a better organization. */
+ CreateMap()
+ .MapExtraProperties();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainModule.cs
new file mode 100644
index 00000000..f05ee3d6
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesDomainModule.cs
@@ -0,0 +1,33 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.AutoMapper;
+using Volo.Abp.Domain;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(AbpDddDomainModule),
+ typeof(AbpAutoMapperModule),
+ typeof(EShopPluginsFlashSalesDomainSharedModule)
+)]
+public class EShopPluginsFlashSalesDomainModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAutoMapperObjectMapper();
+
+ Configure(options =>
+ {
+ options.AddMaps(validate: true);
+ });
+
+ Configure(options =>
+ {
+ options.EtoMappings.Add(typeof(EShopPluginsFlashSalesDomainModule));
+
+ options.AutoEventSelectors.Add();
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan.cs
new file mode 100644
index 00000000..45d9f05e
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan.cs
@@ -0,0 +1,59 @@
+using System;
+using EasyAbp.EShop.Stores.Stores;
+using Volo.Abp.Domain.Entities.Auditing;
+using Volo.Abp.MultiTenancy;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public class FlashSalePlan : FullAuditedAggregateRoot, IMultiTenant, IMultiStore
+{
+ public virtual Guid? TenantId { get; protected set; }
+
+ public virtual Guid StoreId { get; protected set; }
+
+ public virtual DateTime BeginTime { get; set; }
+
+ public virtual DateTime EndTime { get; set; }
+
+ public virtual Guid ProductId { get; protected set; }
+
+ public virtual Guid ProductSkuId { get; protected set; }
+
+ public virtual bool IsPublished { get; protected set; }
+
+ protected FlashSalePlan()
+ {
+ }
+
+ public FlashSalePlan(Guid id, Guid? tenantId, Guid storeId, DateTime beginTime, DateTime endTime, Guid productId, Guid productSkuId, bool isPublished)
+ : base(id)
+ {
+ TenantId = tenantId;
+ SetTimeRange(beginTime, endTime);
+ SetProductSku(storeId, productId, productSkuId);
+ SetPublished(isPublished);
+ }
+
+ public void SetTimeRange(DateTime beginTime, DateTime endTime)
+ {
+ if (beginTime > endTime)
+ {
+ throw new InvalidEndTimeException();
+ }
+
+ BeginTime = beginTime;
+ EndTime = endTime;
+ }
+
+ public void SetProductSku(Guid storeId, Guid productId, Guid productSkuId)
+ {
+ StoreId = storeId;
+ ProductId = productId;
+ ProductSkuId = productSkuId;
+ }
+
+ public void SetPublished(bool isPublished)
+ {
+ IsPublished = isPublished;
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanRepository.cs
new file mode 100644
index 00000000..6fba405c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/IFlashSalePlanRepository.cs
@@ -0,0 +1,8 @@
+using System;
+using Volo.Abp.Domain.Repositories;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public interface IFlashSalePlanRepository : IRepository
+{
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/InvalidEndTimeException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/InvalidEndTimeException.cs
new file mode 100644
index 00000000..72ff19ef
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/InvalidEndTimeException.cs
@@ -0,0 +1,12 @@
+using Volo.Abp;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public class InvalidEndTimeException : BusinessException
+{
+ public InvalidEndTimeException()
+ : base(FlashSalesErrorCodes.InvalidEndTime)
+ {
+
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult.cs
new file mode 100644
index 00000000..a1a25619
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult.cs
@@ -0,0 +1,59 @@
+using System;
+using JetBrains.Annotations;
+using Volo.Abp;
+using Volo.Abp.Domain.Entities.Auditing;
+using Volo.Abp.MultiTenancy;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public class FlashSaleResult : FullAuditedAggregateRoot, IFlashSaleResult, IMultiTenant
+{
+ public virtual Guid? TenantId { get; protected set; }
+
+ public virtual Guid StoreId { get; protected set; }
+
+ public virtual Guid PlanId { get; protected set; }
+
+ public virtual FlashSaleResultStatus Status { get; protected set; }
+
+ public virtual string Reason { get; protected set; }
+
+ public virtual Guid UserId { get; protected set; }
+
+ public virtual Guid? OrderId { get; protected set; }
+
+ public virtual DateTime ReducedInventoryTime { get; protected set; }
+
+ protected FlashSaleResult() { }
+
+ public FlashSaleResult(
+ Guid id, Guid? tenantId, Guid storeId, Guid planId, Guid userId, DateTime reducedInventoryTime) : base(id)
+ {
+ TenantId = tenantId;
+ StoreId = storeId;
+ PlanId = planId;
+ Status = FlashSaleResultStatus.Pending;
+ UserId = userId;
+ ReducedInventoryTime = reducedInventoryTime;
+ }
+
+ public void MarkAsSuccessful(Guid orderId)
+ {
+ if (Status != FlashSaleResultStatus.Pending)
+ {
+ throw new FlashSaleResultStatusNotPendingException(Id);
+ }
+ Status = FlashSaleResultStatus.Successful;
+ OrderId = orderId;
+ }
+
+ public void MarkAsFailed([NotNull] string reason)
+ {
+ if (Status != FlashSaleResultStatus.Pending)
+ {
+ throw new FlashSaleResultStatusNotPendingException(Id);
+ }
+ Status = FlashSaleResultStatus.Failed;
+ Reason = Check.NotNullOrEmpty(reason, nameof(reason));
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatusNotPendingException.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatusNotPendingException.cs
new file mode 100644
index 00000000..9402b683
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultStatusNotPendingException.cs
@@ -0,0 +1,12 @@
+using System;
+using Volo.Abp;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public class FlashSaleResultStatusNotPendingException : BusinessException
+{
+ public FlashSaleResultStatusNotPendingException(Guid resultId) : base(FlashSalesErrorCodes.FlashSaleResultStatusNotPending)
+ {
+ WithData(nameof(resultId), resultId);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResult.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResult.cs
new file mode 100644
index 00000000..967485f9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResult.cs
@@ -0,0 +1,17 @@
+using System;
+using EasyAbp.EShop.Stores.Stores;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public interface IFlashSaleResult : IMultiStore
+{
+ Guid PlanId { get; }
+
+ FlashSaleResultStatus Status { get; }
+
+ string Reason { get; }
+
+ Guid UserId { get; }
+
+ Guid? OrderId { get; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultRepository.cs
new file mode 100644
index 00000000..dc30ef1b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/IFlashSaleResultRepository.cs
@@ -0,0 +1,9 @@
+using System;
+using Volo.Abp.Domain.Repositories;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public interface IFlashSaleResultRepository : IRepository
+{
+
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalesDbProperties.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalesDbProperties.cs
new file mode 100644
index 00000000..909df743
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/FlashSalesDbProperties.cs
@@ -0,0 +1,10 @@
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+public static class FlashSalesDbProperties
+{
+ public static string DbTablePrefix { get; set; } = "EasyAbpEShopPluginsFlashSales";
+
+ public static string DbSchema { get; set; } = null;
+
+ public const string ConnectionStringName = "EasyAbpEShopPluginsFlashSales";
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettingDefinitionProvider.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettingDefinitionProvider.cs
new file mode 100644
index 00000000..817f807d
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettingDefinitionProvider.cs
@@ -0,0 +1,13 @@
+using Volo.Abp.Settings;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Settings;
+
+public class FlashSalesSettingDefinitionProvider : SettingDefinitionProvider
+{
+ public override void Define(ISettingDefinitionContext context)
+ {
+ /* Define module settings here.
+ * Use names from FlashSalesSettings class.
+ */
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettings.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettings.cs
new file mode 100644
index 00000000..1cae4330
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/EasyAbp/EShop/Plugins/FlashSales/Settings/FlashSalesSettings.cs
@@ -0,0 +1,10 @@
+namespace EasyAbp.EShop.Plugins.FlashSales.Settings;
+
+public static class FlashSalesSettings
+{
+ public const string GroupName = "FlashSales";
+
+ /* Add constants for setting names. Example:
+ * public const string MySettingName = GroupName + ".MySettingName";
+ */
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Domain/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.abppkg.json
new file mode 100644
index 00000000..e1c64f01
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.ef"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.csproj
new file mode 100644
index 00000000..84e4197c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/EShopPluginsFlashSalesEntityFrameworkCoreModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/EShopPluginsFlashSalesEntityFrameworkCoreModule.cs
new file mode 100644
index 00000000..336d42e3
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/EShopPluginsFlashSalesEntityFrameworkCoreModule.cs
@@ -0,0 +1,23 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.EntityFrameworkCore;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesDomainModule),
+ typeof(AbpEntityFrameworkCoreModule)
+)]
+public class EShopPluginsFlashSalesEntityFrameworkCoreModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAbpDbContext(options =>
+ {
+ options.AddRepository();
+ options.AddRepository();
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContext.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContext.cs
new file mode 100644
index 00000000..d9002f5c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContext.cs
@@ -0,0 +1,28 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Microsoft.EntityFrameworkCore;
+using Volo.Abp.Data;
+using Volo.Abp.EntityFrameworkCore;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+
+[ConnectionStringName(FlashSalesDbProperties.ConnectionStringName)]
+public class FlashSalesDbContext : AbpDbContext, IFlashSalesDbContext
+{
+ public DbSet FlashSalePlans { get; set; }
+
+ public DbSet FlashSaleResults { get; set; }
+
+ public FlashSalesDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ builder.ConfigureEShopPluginsFlashSales();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContextModelCreatingExtensions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContextModelCreatingExtensions.cs
new file mode 100644
index 00000000..f12853a7
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/FlashSalesDbContextModelCreatingExtensions.cs
@@ -0,0 +1,32 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Microsoft.EntityFrameworkCore;
+using Volo.Abp;
+using Volo.Abp.EntityFrameworkCore.Modeling;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+
+public static class FlashSalesDbContextModelCreatingExtensions
+{
+ public static void ConfigureEShopPluginsFlashSales(
+ this ModelBuilder builder)
+ {
+ Check.NotNull(builder, nameof(builder));
+
+ builder.Entity(b =>
+ {
+ b.ToTable(FlashSalesDbProperties.DbTablePrefix + "FlashSalePlans", FlashSalesDbProperties.DbSchema);
+ b.ConfigureByConvention();
+
+ /* Configure more properties here */
+ });
+
+ builder.Entity(b =>
+ {
+ b.ToTable(FlashSalesDbProperties.DbTablePrefix + "FlashSaleResults", FlashSalesDbProperties.DbSchema);
+ b.ConfigureByConvention();
+
+ /* Configure more properties here */
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/IFlashSalesDbContext.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/IFlashSalesDbContext.cs
new file mode 100644
index 00000000..a0bfd185
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/EntityFrameworkCore/IFlashSalesDbContext.cs
@@ -0,0 +1,15 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Microsoft.EntityFrameworkCore;
+using Volo.Abp.Data;
+using Volo.Abp.EntityFrameworkCore;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+
+[ConnectionStringName(FlashSalesDbProperties.ConnectionStringName)]
+public interface IFlashSalesDbContext : IEfCoreDbContext
+{
+ DbSet FlashSalePlans { get; set; }
+
+ DbSet FlashSaleResults { get; set; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/EfCoreFlashSalePlanRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/EfCoreFlashSalePlanRepository.cs
new file mode 100644
index 00000000..b78bffbd
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/EfCoreFlashSalePlanRepository.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
+using Volo.Abp.EntityFrameworkCore;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans
+{
+ public class EfCoreFlashSalePlanRepository : EfCoreRepository, IFlashSalePlanRepository
+ {
+ public EfCoreFlashSalePlanRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
+ {
+ }
+
+ public override async Task> WithDetailsAsync()
+ {
+ return (await GetQueryableAsync()).IncludeDetails();
+ }
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEfCoreQuerableExtensions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEfCoreQuerableExtensions.cs
new file mode 100644
index 00000000..cbbad1a8
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanEfCoreQuerableExtensions.cs
@@ -0,0 +1,18 @@
+using System.Linq;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public static class FlashSalePlanEfCoreQueryableExtensions
+{
+ public static IQueryable IncludeDetails(this IQueryable queryable, bool include = true)
+ {
+ if (!include)
+ {
+ return queryable;
+ }
+
+ return queryable
+ // .Include(x => x.xxx) // TODO: AbpHelper generated
+ ;
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/EfCoreFlashSaleResultRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/EfCoreFlashSaleResultRepository.cs
new file mode 100644
index 00000000..937ecdf5
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/EfCoreFlashSaleResultRepository.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore;
+using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
+using Volo.Abp.EntityFrameworkCore;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public class EfCoreFlashSaleResultRepository : EfCoreRepository, IFlashSaleResultRepository
+{
+ public EfCoreFlashSaleResultRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
+ {
+ }
+
+ public override async Task> WithDetailsAsync()
+ {
+ return (await GetQueryableAsync()).IncludeDetails();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultEfCoreQuerableExtensions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultEfCoreQuerableExtensions.cs
new file mode 100644
index 00000000..ef21cde9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultEfCoreQuerableExtensions.cs
@@ -0,0 +1,16 @@
+using System.Linq;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public static class FlashSaleResultEfCoreQuerableExtensions
+{
+ public static IQueryable IncludeDetails(this IQueryable queryable, bool include = true)
+ {
+ if (!include)
+ {
+ return queryable;
+ }
+
+ return queryable;
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.EntityFrameworkCore/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.abppkg.json
new file mode 100644
index 00000000..7deef5e3
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.http-api-client"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.csproj
new file mode 100644
index 00000000..918f6179
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+ netstandard2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiClientModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiClientModule.cs
new file mode 100644
index 00000000..07577fda
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiClientModule.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Http.Client;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesApplicationContractsModule),
+ typeof(AbpHttpClientModule))]
+public class EShopPluginsFlashSalesHttpApiClientModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddHttpClientProxies(
+ typeof(EShopPluginsFlashSalesApplicationContractsModule).Assembly,
+ EShopPluginsFlashSalesRemoteServiceConsts.RemoteServiceName
+ );
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.abppkg.json
new file mode 100644
index 00000000..515bfe64
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.http-api"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.csproj
new file mode 100644
index 00000000..416585c3
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Plugins.FlashSales.HttpApi.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiModule.cs
new file mode 100644
index 00000000..b3c4a90d
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesHttpApiModule.cs
@@ -0,0 +1,32 @@
+using Localization.Resources.AbpUi;
+using EasyAbp.EShop.Plugins.FlashSales.Localization;
+using Volo.Abp.AspNetCore.Mvc;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesApplicationContractsModule),
+ typeof(AbpAspNetCoreMvcModule))]
+public class EShopPluginsFlashSalesHttpApiModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(mvcBuilder =>
+ {
+ mvcBuilder.AddApplicationPartIfNotExists(typeof(EShopPluginsFlashSalesHttpApiModule).Assembly);
+ });
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Resources
+ .Get()
+ .AddBaseTypes(typeof(AbpUiResource));
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanController.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanController.cs
new file mode 100644
index 00000000..8dc38783
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanController.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Auditing;
+using Volo.Abp.Uow;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+[Area(EShopPluginsFlashSalesRemoteServiceConsts.ModuleName)]
+[RemoteService(Name = EShopPluginsFlashSalesRemoteServiceConsts.RemoteServiceName)]
+[Route("/api/e-shop/plugins/flash-sales/flash-sale-plan")]
+public class FlashSalePlanController :
+ FlashSalesController,
+ IFlashSalePlanAppService
+{
+ protected IFlashSalePlanAppService Service { get; }
+
+ public FlashSalePlanController(IFlashSalePlanAppService flashSalePlanAppService)
+ {
+ Service = flashSalePlanAppService;
+ }
+
+ [HttpGet("{id}")]
+ public virtual Task GetAsync(Guid id)
+ {
+ return Service.GetAsync(id);
+ }
+
+ [HttpGet]
+ public virtual Task> GetListAsync(FlashSalePlanGetListInput input)
+ {
+ return Service.GetListAsync(input);
+ }
+
+ [HttpPost("{id}")]
+ public virtual Task CreateAsync(FlashSalePlanCreateDto input)
+ {
+ return Service.CreateAsync(input);
+ }
+
+ [HttpPut("{id}")]
+ public virtual Task UpdateAsync(Guid id, FlashSalePlanUpdateDto input)
+ {
+ return Service.UpdateAsync(id, input);
+ }
+
+ [HttpDelete("{id}")]
+ public virtual Task DeleteAsync(Guid id)
+ {
+ return Service.DeleteAsync(id);
+ }
+
+ [UnitOfWork(IsDisabled = true)]
+ [HttpPost("{id}/pre-order")]
+ public virtual Task PreOrderAsync(Guid id)
+ {
+ return Service.PreOrderAsync(id);
+ }
+
+ [DisableAuditing]
+ [UnitOfWork(IsDisabled = true)]
+ [HttpPost("{id}/order")]
+ public virtual Task OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput)
+ {
+ return Service.OrderAsync(id, flashSalePlanInput);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultController.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultController.cs
new file mode 100644
index 00000000..0f6c1707
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResultController.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp;
+using Volo.Abp.Application.Dtos;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+[Area(EShopPluginsFlashSalesRemoteServiceConsts.ModuleName)]
+[RemoteService(Name = EShopPluginsFlashSalesRemoteServiceConsts.RemoteServiceName)]
+[Route("/api/e-shop/plugins/flash-sales/flash-sale-result")]
+public class FlashSaleResultController : FlashSalesController, IFlashSaleResultAppService
+{
+ protected IFlashSaleResultAppService Service { get; }
+
+ public FlashSaleResultController(IFlashSaleResultAppService service)
+ {
+ Service = service;
+ }
+
+ [HttpGet("{id}")]
+ public virtual Task GetAsync(Guid id)
+ {
+ return Service.GetAsync(id);
+ }
+
+ [HttpGet]
+ public virtual Task> GetListAsync(FlashSaleResultGetListInput input)
+ {
+ return Service.GetListAsync(input);
+ }
+
+ [HttpGet("current/{planId}")]
+ public virtual Task GetCurrentAsync(Guid planId)
+ {
+ return Service.GetCurrentAsync(planId);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalesController.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalesController.cs
new file mode 100644
index 00000000..f087a9c7
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Plugins/FlashSales/FlashSalesController.cs
@@ -0,0 +1,12 @@
+using EasyAbp.EShop.Plugins.FlashSales.Localization;
+using Volo.Abp.AspNetCore.Mvc;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+public abstract class FlashSalesController : AbpControllerBase
+{
+ protected FlashSalesController()
+ {
+ LocalizationResource = typeof(FlashSalesResource);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.HttpApi/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/EasyAbp.EShop.Plugins.FlashSales.Installer.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/EasyAbp.EShop.Plugins.FlashSales.Installer.csproj
new file mode 100644
index 00000000..60bb3626
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/EasyAbp.EShop.Plugins.FlashSales.Installer.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+ net6.0
+ true
+ EasyAbp.EShop.Plugins.FlashSales
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerModule.cs
new file mode 100644
index 00000000..619b2d4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerModule.cs
@@ -0,0 +1,20 @@
+using Volo.Abp.Studio;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(AbpStudioModuleInstallerModule),
+ typeof(AbpVirtualFileSystemModule)
+ )]
+public class FlashSalesInstallerModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerPipelineBuilder.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerPipelineBuilder.cs
new file mode 100644
index 00000000..56e7fd3f
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FlashSalesInstallerPipelineBuilder.cs
@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Studio.ModuleInstalling;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
+[ExposeServices(typeof(IModuleInstallingPipelineBuilder))]
+public class FlashSalesInstallerPipelineBuilder : ModuleInstallingPipelineBuilderBase, IModuleInstallingPipelineBuilder, ITransientDependency
+{
+ public async Task BuildAsync(ModuleInstallingContext context)
+ {
+ return GetBasePipeline(context);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Installer/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.abppkg.json
new file mode 100644
index 00000000..8b23fd1e
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.mongodb"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.csproj
new file mode 100644
index 00000000..3900b394
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp.EShop.Plugins.FlashSales.MongoDB.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/MongoFlashSalePlanRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/MongoFlashSalePlanRepository.cs
new file mode 100644
index 00000000..4e0c476b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/MongoFlashSalePlanRepository.cs
@@ -0,0 +1,13 @@
+using System;
+using EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+using Volo.Abp.Domain.Repositories.MongoDB;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public class MongoFlashSalePlanRepository : MongoDbRepository, IFlashSalePlanRepository
+{
+ public MongoFlashSalePlanRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider)
+ {
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/MongoFlashSaleResultRepository.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/MongoFlashSaleResultRepository.cs
new file mode 100644
index 00000000..fc8a74a2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/FlashSaleResults/MongoFlashSaleResultRepository.cs
@@ -0,0 +1,13 @@
+using System;
+using EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+using Volo.Abp.Domain.Repositories.MongoDB;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+public class MongoFlashSaleResultRepository : MongoDbRepository, IFlashSaleResultRepository
+{
+ public MongoFlashSaleResultRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider)
+ {
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/EShopPluginsFlashSalesMongoDbModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/EShopPluginsFlashSalesMongoDbModule.cs
new file mode 100644
index 00000000..fcfceade
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/EShopPluginsFlashSalesMongoDbModule.cs
@@ -0,0 +1,23 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesDomainModule),
+ typeof(AbpMongoDbModule)
+ )]
+public class EShopPluginsFlashSalesMongoDbModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddMongoDbContext(options =>
+ {
+ options.AddRepository();
+ options.AddRepository();
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContext.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContext.cs
new file mode 100644
index 00000000..b7dfc7a2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContext.cs
@@ -0,0 +1,22 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using MongoDB.Driver;
+using Volo.Abp.Data;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+
+[ConnectionStringName(FlashSalesDbProperties.ConnectionStringName)]
+public class FlashSalesMongoDbContext : AbpMongoDbContext, IFlashSalesMongoDbContext
+{
+ public IMongoCollection FlashSalePlans => Collection();
+
+ public IMongoCollection FlashSaleResults => Collection();
+
+ protected override void CreateModel(IMongoModelBuilder modelBuilder)
+ {
+ base.CreateModel(modelBuilder);
+
+ modelBuilder.ConfigureFlashSales();
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContextExtensions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContextExtensions.cs
new file mode 100644
index 00000000..e3d2201a
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/FlashSalesMongoDbContextExtensions.cs
@@ -0,0 +1,19 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using Volo.Abp;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+
+public static class FlashSalesMongoDbContextExtensions
+{
+ public static void ConfigureFlashSales(
+ this IMongoModelBuilder builder)
+ {
+ Check.NotNull(builder, nameof(builder));
+
+ builder.Entity(b => b.CollectionName = FlashSalesDbProperties.DbTablePrefix + "FlashSalePlans");
+
+ builder.Entity(b => b.CollectionName = FlashSalesDbProperties.DbTablePrefix + "FlashSaleResults");
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/IFlashSalesMongoDbContext.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/IFlashSalesMongoDbContext.cs
new file mode 100644
index 00000000..6a4c7951
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/EasyAbp/EShop/Plugins/FlashSales/MongoDB/IFlashSalesMongoDbContext.cs
@@ -0,0 +1,15 @@
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using MongoDB.Driver;
+using Volo.Abp.Data;
+using Volo.Abp.MongoDB;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.MongoDB;
+
+[ConnectionStringName(FlashSalesDbProperties.ConnectionStringName)]
+public interface IFlashSalesMongoDbContext : IAbpMongoDbContext
+{
+ IMongoCollection FlashSalePlans { get; }
+
+ IMongoCollection FlashSaleResults { get; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.MongoDB/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebAutoMapperProfile.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebAutoMapperProfile.cs
new file mode 100644
index 00000000..5a88342e
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebAutoMapperProfile.cs
@@ -0,0 +1,25 @@
+using AutoMapper;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan.ViewModels;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult.ViewModels;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web;
+
+public class EShopPluginsFlashSalesWebAutoMapperProfile : Profile
+{
+ public EShopPluginsFlashSalesWebAutoMapperProfile()
+ {
+ /* You can configure your AutoMapper mapping configuration here.
+ * Alternatively, you can split your mapping configurations
+ * into multiple profile classes for a better organization. */
+ CreateMap(MemberList.Destination)
+ .ForMember(dest => dest.ExtraProperties, opt => opt.Ignore());
+ CreateMap(MemberList.Destination)
+ .ForMember(dest => dest.ExtraProperties, opt => opt.Ignore());
+ CreateMap(MemberList.Destination)
+ .ForSourceMember(dest => dest.ExtraProperties, opt => opt.DoNotValidate());
+ CreateMap(MemberList.Destination)
+ .ForSourceMember(dest => dest.ExtraProperties, opt => opt.DoNotValidate());
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebModule.cs
new file mode 100644
index 00000000..443dc336
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EShopPluginsFlashSalesWebModule.cs
@@ -0,0 +1,58 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.DependencyInjection;
+using EasyAbp.EShop.Plugins.FlashSales.Localization;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Menus;
+using Volo.Abp.AspNetCore.Mvc.Localization;
+using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
+using Volo.Abp.AutoMapper;
+using Volo.Abp.Modularity;
+using Volo.Abp.UI.Navigation;
+using Volo.Abp.VirtualFileSystem;
+using EasyAbp.EShop.Plugins.FlashSales.Permissions;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesApplicationContractsModule),
+ typeof(AbpAspNetCoreMvcUiThemeSharedModule),
+ typeof(AbpAutoMapperModule)
+ )]
+public class EShopPluginsFlashSalesWebModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.PreConfigure(options =>
+ {
+ options.AddAssemblyResource(typeof(FlashSalesResource), typeof(EShopPluginsFlashSalesWebModule).Assembly);
+ });
+
+ PreConfigure(mvcBuilder =>
+ {
+ mvcBuilder.AddApplicationPartIfNotExists(typeof(EShopPluginsFlashSalesWebModule).Assembly);
+ });
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.MenuContributors.Add(new FlashSalesMenuContributor());
+ });
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ context.Services.AddAutoMapperObjectMapper();
+ Configure(options =>
+ {
+ options.AddMaps(validate: true);
+ });
+
+ Configure(options =>
+ {
+ //Configure authorization.
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.abppkg.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.abppkg.json
new file mode 100644
index 00000000..930c4018
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.mvc"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.csproj
new file mode 100644
index 00000000..cedf9892
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/EasyAbp.EShop.Plugins.FlashSales.Web.csproj
@@ -0,0 +1,40 @@
+
+
+
+
+
+ net6.0
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ true
+ Library
+ EasyAbp.EShop.Plugins.FlashSales.Web
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenuContributor.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenuContributor.cs
new file mode 100644
index 00000000..50d1a707
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenuContributor.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.Localization;
+using EasyAbp.EShop.Plugins.FlashSales.Permissions;
+using Volo.Abp.UI.Navigation;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Menus;
+
+public class FlashSalesMenuContributor : IMenuContributor
+{
+ public async Task ConfigureMenuAsync(MenuConfigurationContext context)
+ {
+ if (context.Menu.Name == StandardMenus.Main)
+ {
+ await ConfigureMainMenuAsync(context);
+ }
+ }
+
+ private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
+ {
+ var l = context.GetLocalizer();
+ //Add main menu items.
+ var flashSalesManagementMenuItem = new ApplicationMenuItem(FlashSalesMenus.Prefix, l["Menu:FlashSalesManagement"]);
+
+ if (await context.IsGrantedAsync(FlashSalesPermissions.FlashSalePlan.Default))
+ {
+ flashSalesManagementMenuItem.AddItem(
+ new ApplicationMenuItem(
+ FlashSalesMenus.FlashSalePlan,
+ l["Menu:FlashSalePlan"],
+ "/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan"
+ )
+ );
+ }
+
+ if (await context.IsGrantedAsync(FlashSalesPermissions.FlashSaleResult.Default))
+ {
+ flashSalesManagementMenuItem.AddItem(
+ new ApplicationMenuItem(
+ FlashSalesMenus.FlashSaleResult,
+ l["Menu:FlashSaleResult"],
+ "/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult"
+ )
+ );
+ }
+
+ if (!flashSalesManagementMenuItem.Items.IsNullOrEmpty())
+ {
+ var eShopMenuItem = context.Menu.Items.GetOrAdd(i => i.Name == FlashSalesMenus.ModuleGroupPrefix,
+ () => new ApplicationMenuItem(FlashSalesMenus.ModuleGroupPrefix, l["Menu:EasyAbpEShop"], icon: "fa fa-shopping-bag"));
+
+ eShopMenuItem.Items.Add(flashSalesManagementMenuItem);
+ }
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenus.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenus.cs
new file mode 100644
index 00000000..e792e1aa
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Menus/FlashSalesMenus.cs
@@ -0,0 +1,12 @@
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Menus;
+
+public static class FlashSalesMenus
+{
+ public const string ModuleGroupPrefix = "EasyAbp.EShop";
+
+ public const string Prefix = ModuleGroupPrefix + ".Plugins.FlashSales";
+
+ public const string FlashSalePlan = Prefix + ".FlashSalePlan";
+
+ public const string FlashSaleResult = Prefix + ".FlashSaleResult";
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml
new file mode 100644
index 00000000..8e86f4a7
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml
@@ -0,0 +1,21 @@
+@page
+@using Microsoft.AspNetCore.Mvc.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan
+
+@inject IHtmlLocalizer L
+@model CreateModalModel
+@{
+ Layout = null;
+}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml.cs
new file mode 100644
index 00000000..08eab7da
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal.cshtml.cs
@@ -0,0 +1,27 @@
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan.ViewModels;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan;
+
+public class CreateModalModel : FlashSalesPageModel
+{
+ [BindProperty]
+ public CreateFlashSalePlanViewModel ViewModel { get; set; }
+
+ protected IFlashSalePlanAppService Service { get; }
+
+ public CreateModalModel(IFlashSalePlanAppService service)
+ {
+ Service = service;
+ }
+
+ public virtual async Task OnPostAsync()
+ {
+ var dto = ObjectMapper.Map(ViewModel);
+ await Service.CreateAsync(dto);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml
new file mode 100644
index 00000000..356d19dd
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml
@@ -0,0 +1,22 @@
+@page
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using Microsoft.AspNetCore.Mvc.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan
+
+@inject IHtmlLocalizer L
+@model EditModalModel
+@{
+ Layout = null;
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml.cs
new file mode 100644
index 00000000..faa08b8e
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal.cshtml.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan.ViewModels;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan;
+
+public class EditModalModel : FlashSalesPageModel
+{
+ [HiddenInput]
+ [BindProperty(SupportsGet = true)]
+ public Guid Id { get; set; }
+
+ [BindProperty]
+ public EditFlashSalePlanViewModel ViewModel { get; set; }
+
+ protected IFlashSalePlanAppService Service { get; }
+
+ public EditModalModel(IFlashSalePlanAppService service)
+ {
+ Service = service;
+ }
+
+ public virtual async Task OnGetAsync()
+ {
+ var dto = await Service.GetAsync(Id);
+ ViewModel = ObjectMapper.Map(dto);
+ }
+
+ public virtual async Task OnPostAsync()
+ {
+ var dto = ObjectMapper.Map(ViewModel);
+ await Service.UpdateAsync(Id, dto);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml
new file mode 100644
index 00000000..ece592b6
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml
@@ -0,0 +1,50 @@
+@page
+@using EasyAbp.EShop.Plugins.FlashSales.Permissions
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Mvc.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Layout
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Menus
+
+@model IndexModel
+
+@inject IPageLayout PageLayout
+@inject IHtmlLocalizer L
+@inject IAuthorizationService Authorization
+@{
+ PageLayout.Content.Title = L["FlashSalePlan"].Value;
+ PageLayout.Content.BreadCrumb.Add(L["Menu:FlashSalePlan"].Value);
+ PageLayout.Content.MenuItemName = FlashSalesMenus.FlashSalePlan;
+}
+
+@section scripts
+{
+
+}
+@section styles
+{
+
+}
+
+
+
+
+
+ @L["FlashSalePlan"]
+
+
+ @if (await Authorization.IsGrantedAsync(FlashSalesPermissions.FlashSalePlan.Create))
+ {
+
+ }
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml.cs
new file mode 100644
index 00000000..c45df6d6
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/Index.cshtml.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan;
+
+public class IndexModel : FlashSalesPageModel
+{
+ public virtual async Task OnGetAsync()
+ {
+ await Task.CompletedTask;
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/CreateFlashSalePlanViewModel.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/CreateFlashSalePlanViewModel.cs
new file mode 100644
index 00000000..8f167e89
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/CreateFlashSalePlanViewModel.cs
@@ -0,0 +1,25 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan.ViewModels;
+
+public class CreateFlashSalePlanViewModel
+{
+ [Display(Name = "FlashSalePlanStoreId")]
+ public Guid StoreId { get; set; }
+
+ [Display(Name = "FlashSalePlanBeginTime")]
+ public DateTime BeginTime { get; set; }
+
+ [Display(Name = "FlashSalePlanEndTime")]
+ public DateTime EndTime { get; set; }
+
+ [Display(Name = "FlashSalePlanProductId")]
+ public Guid ProductId { get; set; }
+
+ [Display(Name = "FlashSalePlanProductSkuId")]
+ public Guid ProductSkuId { get; set; }
+
+ [Display(Name = "FlashSalePlanIsPublished")]
+ public bool IsPublished { get; set; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/EditFlashSalePlanViewModel.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/EditFlashSalePlanViewModel.cs
new file mode 100644
index 00000000..a7cab334
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/ViewModels/EditFlashSalePlanViewModel.cs
@@ -0,0 +1,33 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
+using Volo.Abp.Domain.Entities;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSalePlans.FlashSalePlan.ViewModels;
+
+public class EditFlashSalePlanViewModel : IHasConcurrencyStamp
+{
+ [DisabledInput]
+ [ReadOnlyInput]
+ [Display(Name = "FlashSalePlanStoreId")]
+ public Guid StoreId { get; set; }
+
+ [Display(Name = "FlashSalePlanBeginTime")]
+ public DateTime BeginTime { get; set; }
+
+ [Display(Name = "FlashSalePlanEndTime")]
+ public DateTime EndTime { get; set; }
+
+ [Display(Name = "FlashSalePlanProductId")]
+ public Guid ProductId { get; set; }
+
+ [Display(Name = "FlashSalePlanProductSkuId")]
+ public Guid ProductSkuId { get; set; }
+
+ [Display(Name = "FlashSalePlanIsPublished")]
+ public bool IsPublished { get; set; }
+
+ [HiddenInput]
+ public string ConcurrencyStamp { get; set; }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/index.css b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/index.css
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/index.js b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/index.js
new file mode 100644
index 00000000..ff9c2d89
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/index.js
@@ -0,0 +1,93 @@
+$(function () {
+
+ var l = abp.localization.getResource('EasyAbpEShopPluginsFlashSales');
+
+ var service = easyAbp.eShop.plugins.flashSales.flashSalePlans.flashSalePlan;
+ var createModal = new abp.ModalManager(abp.appPath + 'EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/CreateModal');
+ var editModal = new abp.ModalManager(abp.appPath + 'EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlan/EditModal');
+
+ var dataTable = $('#FlashSalePlanTable').DataTable(abp.libs.datatables.normalizeConfiguration({
+ processing: true,
+ serverSide: true,
+ paging: true,
+ searching: false,
+ autoWidth: false,
+ scrollCollapse: true,
+ order: [[2, "asc"]],
+ ajax: abp.libs.datatables.createAjax(service.getList, function () {
+ return {
+ includeUnpublished: abp.auth.isGranted('EasyAbp.EShop.Plugins.FlashSales.FlashSalePlan.Manage')
+ };
+ }),
+ columnDefs: [
+ {
+ rowAction: {
+ items:
+ [
+ {
+ text: l('Edit'),
+ visible: abp.auth.isGranted('EasyAbp.EShop.Plugins.FlashSales.FlashSalePlan.Update'),
+ action: function (data) {
+ editModal.open({ id: data.record.id });
+ }
+ },
+ {
+ text: l('Delete'),
+ visible: abp.auth.isGranted('EasyAbp.EShop.Plugins.FlashSales.FlashSalePlan.Delete'),
+ confirmMessage: function (data) {
+ return l('FlashSalePlanDeletionConfirmationMessage', data.record.id);
+ },
+ action: function (data) {
+ service.delete(data.record.id)
+ .then(function () {
+ abp.notify.info(l('SuccessfullyDeleted'));
+ dataTable.ajax.reload();
+ });
+ }
+ }
+ ]
+ }
+ },
+ {
+ title: l('FlashSalePlanStoreId'),
+ data: "storeId"
+ },
+ {
+ title: l('FlashSalePlanBeginTime'),
+ data: "beginTime",
+ dataFormat: 'datetime'
+ },
+ {
+ title: l('FlashSalePlanEndTime'),
+ data: "endTime",
+ dataFormat: 'datetime'
+ },
+ {
+ title: l('FlashSalePlanProductId'),
+ data: "productId"
+ },
+ {
+ title: l('FlashSalePlanProductSkuId'),
+ data: "productSkuId"
+ },
+ {
+ title: l('FlashSalePlanIsPublished'),
+ data: "isPublished",
+ dataFormat: 'boolean'
+ },
+ ]
+ }));
+
+ createModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ editModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ $('#NewFlashSalePlanButton').click(function (e) {
+ e.preventDefault();
+ createModal.open();
+ });
+});
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml
new file mode 100644
index 00000000..88d70cfa
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml
@@ -0,0 +1,41 @@
+@page
+@using EasyAbp.EShop.Plugins.FlashSales.Permissions
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Mvc.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Layout
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Menus
+
+@model IndexModel
+
+@inject IPageLayout PageLayout
+@inject IHtmlLocalizer L
+@inject IAuthorizationService Authorization
+@{
+ PageLayout.Content.Title = L["FlashSaleResult"].Value;
+ PageLayout.Content.BreadCrumb.Add(L["Menu:FlashSaleResult"].Value);
+ PageLayout.Content.MenuItemName = FlashSalesMenus.FlashSaleResult;
+}
+
+@section scripts
+{
+
+}
+@section styles
+{
+
+}
+
+
+
+
+
+ @L["FlashSaleResult"]
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml.cs
new file mode 100644
index 00000000..def3ff36
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/Index.cshtml.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult;
+
+public class IndexModel : PageModel
+{
+ public void OnGet()
+ {
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml
new file mode 100644
index 00000000..789f6864
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml
@@ -0,0 +1,22 @@
+@page
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using Microsoft.AspNetCore.Mvc.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult
+
+@inject IHtmlLocalizer L
+@model ViewModalModel
+@{
+ Layout = null;
+}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml.cs
new file mode 100644
index 00000000..40d67ff5
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal.cshtml.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
+using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult.ViewModels;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult;
+
+public class ViewModalModel : FlashSalesPageModel
+{
+ [HiddenInput]
+ [BindProperty(SupportsGet = true)]
+ public Guid Id { get; set; }
+
+ [BindProperty]
+ public ViewFlashSaleResultViewModel ViewModel { get; set; }
+
+ protected IFlashSaleResultAppService Service { get; }
+
+ public ViewModalModel(IFlashSaleResultAppService service)
+ {
+ Service = service;
+ }
+
+ public virtual async Task OnGetAsync()
+ {
+ var dto = await Service.GetAsync(Id);
+ ViewModel = ObjectMapper.Map(dto);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModels/ViewFlashSaleResultViewModel.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModels/ViewFlashSaleResultViewModel.cs
new file mode 100644
index 00000000..88ab34ec
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModels/ViewFlashSaleResultViewModel.cs
@@ -0,0 +1,29 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales.FlashSaleResults.FlashSaleResult.ViewModels;
+
+public class ViewFlashSaleResultViewModel
+{
+ [Display(Name = "FlashSaleResultStoreId")]
+ public Guid StoreId { get; set; }
+
+ [Display(Name = "FlashSaleResultPlanId")]
+ public Guid PlanId { get; set; }
+
+ [Display(Name = "FlashSaleResultStatus")]
+ public FlashSaleResultStatus Status { get; set; }
+
+ [Display(Name = "FlashSaleResultReason")]
+ public string Reason { get; set; }
+
+ [Display(Name = "FlashSaleResultUserId")]
+ public Guid UserId { get; set; }
+
+ [Display(Name = "FlashSaleResultOrderId")]
+ public Guid? OrderId { get; set; }
+
+ [Display(Name = "FlashSaleResultReducedInventoryTime")]
+ public DateTime ReducedInventoryTime { get; set; }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/index.css b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/index.css
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/index.js b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/index.js
new file mode 100644
index 00000000..f94b74e6
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/index.js
@@ -0,0 +1,74 @@
+$(function () {
+
+ var l = abp.localization.getResource('EasyAbpEShopPluginsFlashSales');
+
+ var service = easyAbp.eShop.plugins.flashSales.flashSaleResults.flashSaleResult;
+ var viewModal = new abp.ModalManager(abp.appPath + 'EShop/Plugins/FlashSales/FlashSaleResults/FlashSaleResult/ViewModal');
+
+ var dataTable = $('#FlashSaleResultTable').DataTable(abp.libs.datatables.normalizeConfiguration({
+ processing: true,
+ serverSide: true,
+ paging: true,
+ searching: false,
+ autoWidth: false,
+ scrollCollapse: true,
+ order: [[6, "desc"]],
+ ajax: abp.libs.datatables.createAjax(service.getList),
+ columnDefs: [
+ {
+ rowAction: {
+ items:
+ [
+ {
+ text: l('ViewFlashSaleResult'),
+ action: function (data) {
+ viewModal.open({ id: data.record.id });
+ }
+ }
+ ]
+ }
+ },
+ {
+ title: l('FlashSaleResultStoreId'),
+ data: "storeId"
+ },
+ {
+ title: l('FlashSaleResultPlanId'),
+ data: "planId"
+ },
+ {
+ title: l('FlashSaleResultStatus'),
+ data: "status",
+ render: function (data) {
+ return l('Enum:FlashSaleResultStatus.' + data);
+ }
+ },
+ {
+ title: l('FlashSaleResultReason'),
+ data: "reason"
+ },
+ {
+ title: l('FlashSaleResultUserId'),
+ data: "userId"
+ },
+ {
+ title: l('FlashSaleResultOrderId'),
+ data: "orderId"
+ },
+ {
+ title: l('FlashSaleResultCreationTime'),
+ data: "creationTime",
+ dataFormat: 'datetime'
+ },
+ {
+ title: l('FlashSaleResultReducedInventoryTime'),
+ data: "reducedInventoryTime",
+ dataFormat: 'datetime'
+ },
+ ]
+ }));
+
+ viewModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+});
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml
new file mode 100644
index 00000000..fed6c363
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml
@@ -0,0 +1,12 @@
+@page
+@using Microsoft.Extensions.Localization
+@using EasyAbp.EShop.Plugins.FlashSales.Localization
+@using EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales
+
+@model IndexModel
+@inject IStringLocalizer L
+@{
+}
+
+FlashSales
+@L["SamplePageMessage"]
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml.cs
new file mode 100644
index 00000000..a4551d28
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/Index.cshtml.cs
@@ -0,0 +1,8 @@
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages.EShop.Plugins.FlashSales;
+
+public class IndexModel : FlashSalesPageModel
+{
+ public void OnGet()
+ {
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/_ViewImports.cshtml b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/_ViewImports.cshtml
new file mode 100644
index 00000000..c1da1f5f
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/EShop/Plugins/FlashSales/_ViewImports.cshtml
@@ -0,0 +1,4 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
+@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
+@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/FlashSalesPageModel.cs b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/FlashSalesPageModel.cs
new file mode 100644
index 00000000..2844b9d8
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Pages/FlashSalesPageModel.cs
@@ -0,0 +1,15 @@
+using EasyAbp.EShop.Plugins.FlashSales.Localization;
+using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.Web.Pages;
+
+/* Inherit your PageModel classes from this class.
+ */
+public abstract class FlashSalesPageModel : AbpPageModel
+{
+ protected FlashSalesPageModel()
+ {
+ LocalizationResourceType = typeof(FlashSalesResource);
+ ObjectMapperContext = typeof(EShopPluginsFlashSalesWebModule);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Properties/launchSettings.json b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Properties/launchSettings.json
new file mode 100644
index 00000000..100df60b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Plugins.FlashSales.Web/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:64779/",
+ "sslPort": 44326
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "EasyAbp.EShop.Plugins.FlashSales.Web": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:5001;http://localhost:5000"
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj
new file mode 100644
index 00000000..ceb72923
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs
new file mode 100644
index 00000000..6926ce29
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs
@@ -0,0 +1,11 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Products.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopProductsApplicationContractsModule)
+)]
+public class EShopProductsPluginsFlashSalesAbstractionsModule : AbpModule
+{
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Products/IFlashSaleInventoryManager.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Products/IFlashSaleInventoryManager.cs
new file mode 100644
index 00000000..8041e743
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/EasyAbp/Eshop/Products/Products/IFlashSaleInventoryManager.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EasyAbp.Eshop.Products.Products;
+
+public interface IFlashSaleInventoryManager
+{
+ Task TryReduceInventoryAsync(Guid? tenantId, string providerName, Guid storeId, Guid productId,
+ Guid productSkuId);
+
+ Task TryRollBackInventoryAsync(Guid? tenantId, string providerName, Guid storeId, Guid productId,
+ Guid productSkuId);
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj
new file mode 100644
index 00000000..91c56364
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationContractsModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationContractsModule.cs
new file mode 100644
index 00000000..efd5d81b
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationContractsModule.cs
@@ -0,0 +1,10 @@
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Products.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopProductsApplicationContractsModule)
+)]
+public class EShopProductsPluginsFlashSalesApplicationContractsModule : AbpModule
+{
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissionDefinitionProvider.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissionDefinitionProvider.cs
new file mode 100644
index 00000000..aedf09da
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissionDefinitionProvider.cs
@@ -0,0 +1,26 @@
+using EasyAbp.EShop.Products.Localization;
+using Volo.Abp.Authorization.Permissions;
+using Volo.Abp.Localization;
+
+namespace EasyAbp.EShop.Products.Permissions
+{
+ public class ProductsPluginsFlashSalesPermissionDefinitionProvider : PermissionDefinitionProvider
+ {
+ public override void Define(IPermissionDefinitionContext context)
+ {
+ var moduleGroup = context.AddGroup(ProductsPluginsFlashSalesPermissions.GroupName, L("Permission:Products"));
+
+ var flashSaleInventoryPermission = moduleGroup.AddPermission(ProductsPluginsFlashSalesPermissions.FlashSaleInventory.Default, L("Permission:FlashSaleInventory"))
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ flashSaleInventoryPermission.AddChild(ProductsPluginsFlashSalesPermissions.FlashSaleInventory.Increase, L("Permission:InventoryIncrease"))
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ flashSaleInventoryPermission.AddChild(ProductsPluginsFlashSalesPermissions.FlashSaleInventory.Reduce, L("Permission:InventoryReduce"))
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ }
+
+ private static LocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissions.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissions.cs
new file mode 100644
index 00000000..66e90962
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Plugins/FlashSales/Permissions/ProductsPluginsFlashSalesPermissions.cs
@@ -0,0 +1,21 @@
+using Volo.Abp.Reflection;
+
+namespace EasyAbp.EShop.Products.Permissions
+{
+ public class ProductsPluginsFlashSalesPermissions
+ {
+ public const string GroupName = "EasyAbp.EShop.Products.Plugins.FlashSales";
+
+ public class FlashSaleInventory
+ {
+ public const string Default = GroupName + ".FlashSaleInventory";
+ public const string Reduce = Default + ".Reduce";
+ public const string Increase = Default + ".Increase";
+ }
+
+ public static string[] GetAll()
+ {
+ return ReflectionHelper.GetPublicConstantsRecursively(typeof(ProductsPluginsFlashSalesPermissions));
+ }
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/IncreaseInventoryInput.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/IncreaseInventoryInput.cs
new file mode 100644
index 00000000..e520d5bb
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/IncreaseInventoryInput.cs
@@ -0,0 +1,37 @@
+using System;
+using EasyAbp.EShop.Stores.Stores;
+using Volo.Abp.MultiTenancy;
+
+namespace EasyAbp.EShop.Products.Products.Dtos;
+
+public class IncreaseInventoryInput : IMultiTenant, IMultiStore
+{
+ public Guid? TenantId { get; set; }
+
+ public string ProviderName { get; set; }
+
+ public Guid StoreId { get; set; }
+
+ public Guid ProductId { get; set; }
+
+ public Guid ProductSkuId { get; set; }
+
+ public int Quantity { get; set; }
+
+ public bool ReduceSold { get; set; }
+
+ public IncreaseInventoryInput()
+ {
+ }
+
+ public IncreaseInventoryInput(Guid? tenantId, string providerName, Guid storeId, Guid productId, Guid productSkuId, int quantity, bool reduceSold)
+ {
+ TenantId = tenantId;
+ ProviderName = providerName;
+ StoreId = storeId;
+ ProductId = productId;
+ ProductSkuId = productSkuId;
+ Quantity = quantity;
+ ReduceSold = reduceSold;
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ReduceInventoryInput.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ReduceInventoryInput.cs
new file mode 100644
index 00000000..fa5ac94c
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ReduceInventoryInput.cs
@@ -0,0 +1,37 @@
+using System;
+using EasyAbp.EShop.Stores.Stores;
+using Volo.Abp.MultiTenancy;
+
+namespace EasyAbp.EShop.Products.Products.Dtos;
+
+public class ReduceInventoryInput : IMultiTenant, IMultiStore
+{
+ public Guid? TenantId { get; set; }
+
+ public string ProviderName { get; set; }
+
+ public Guid StoreId { get; set; }
+
+ public Guid ProductId { get; set; }
+
+ public Guid ProductSkuId { get; set; }
+
+ public int Quantity { get; set; }
+
+ public bool IncreaseSold { get; set; }
+
+ public ReduceInventoryInput()
+ {
+ }
+
+ public ReduceInventoryInput(Guid? tenantId, string providerName, Guid storeId, Guid productId, Guid productSkuId, int quantity, bool increaseSold)
+ {
+ TenantId = tenantId;
+ ProviderName = providerName;
+ StoreId = storeId;
+ ProductId = productId;
+ ProductSkuId = productSkuId;
+ Quantity = quantity;
+ IncreaseSold = increaseSold;
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/IFlashSaleInventoryAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/IFlashSaleInventoryAppService.cs
new file mode 100644
index 00000000..5cd43aed
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/EasyAbp/EShop/Products/Products/IFlashSaleInventoryAppService.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Products.Products.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace EasyAbp.EShop.Products.Products;
+
+public interface IFlashSaleInventoryAppService : IApplicationService
+{
+ Task TryReduceAsync(ReduceInventoryInput input);
+
+ Task TryIncreaseAsync(IncreaseInventoryInput input);
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp.EShop.Products.Plugins.FlashSales.Application.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp.EShop.Products.Plugins.FlashSales.Application.csproj
new file mode 100644
index 00000000..88afd375
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp.EShop.Products.Plugins.FlashSales.Application.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs
new file mode 100644
index 00000000..dcee02c4
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesApplicationModule.cs
@@ -0,0 +1,14 @@
+using EasyAbp.EShop.Plugins.FlashSales;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Products.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopProductsApplicationModule),
+ typeof(EShopPluginsFlashSalesApplicationContractsModule),
+ typeof(EShopProductsPluginsFlashSalesApplicationContractsModule),
+ typeof(EShopProductsPluginsFlashSalesAbstractionsModule)
+)]
+public class EShopProductsPluginsFlashSalesApplicationModule : AbpModule
+{
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryAppService.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryAppService.cs
new file mode 100644
index 00000000..3b03efb6
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/FlashSaleInventoryAppService.cs
@@ -0,0 +1,41 @@
+using System.Threading.Tasks;
+using EasyAbp.EShop.Products.Permissions;
+using EasyAbp.EShop.Products.Products.Dtos;
+
+namespace EasyAbp.EShop.Products.Products;
+
+public class FlashSaleInventoryAppService : ProductsAppService, IFlashSaleInventoryAppService
+{
+ protected ILocalFlashSaleInventoryManager LocalFlashSaleInventoryManager { get; }
+
+ public FlashSaleInventoryAppService(ILocalFlashSaleInventoryManager localFlashSaleInventoryManager)
+ {
+ LocalFlashSaleInventoryManager = localFlashSaleInventoryManager;
+ }
+
+ public virtual async Task TryReduceAsync(ReduceInventoryInput input)
+ {
+ await CheckPolicyAsync(ProductsPluginsFlashSalesPermissions.FlashSaleInventory.Reduce);
+
+ return await LocalFlashSaleInventoryManager.TryReduceInventoryAsync(
+ input.TenantId,
+ input.ProviderName,
+ input.StoreId,
+ input.ProductId,
+ input.ProductSkuId
+ );
+ }
+
+ public virtual async Task TryIncreaseAsync(IncreaseInventoryInput input)
+ {
+ await CheckPolicyAsync(ProductsPluginsFlashSalesPermissions.FlashSaleInventory.Increase);
+
+ return await LocalFlashSaleInventoryManager.TryRollBackInventoryAsync(
+ input.TenantId,
+ input.ProviderName,
+ input.StoreId,
+ input.ProductId,
+ input.ProductSkuId
+ );
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ILocalFlashSaleInventoryManager.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ILocalFlashSaleInventoryManager.cs
new file mode 100644
index 00000000..95982ed4
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/ILocalFlashSaleInventoryManager.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using EasyAbp.Eshop.Products.Products;
+
+namespace EasyAbp.EShop.Products.Products;
+
+public interface ILocalFlashSaleInventoryManager : IFlashSaleInventoryManager
+{
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/LocalFlashSaleInventoryManager.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/LocalFlashSaleInventoryManager.cs
new file mode 100644
index 00000000..aae7dad0
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/EasyAbp/EShop/Products/Products/LocalFlashSaleInventoryManager.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Products.ProductInventories;
+using Volo.Abp.DependencyInjection;
+
+namespace EasyAbp.EShop.Products.Products;
+
+public class LocalFlashSaleInventoryManager : ILocalFlashSaleInventoryManager, ITransientDependency
+{
+ protected IProductInventoryProviderResolver ProductInventoryProviderResolver { get; }
+
+ public LocalFlashSaleInventoryManager(IProductInventoryProviderResolver productInventoryProviderResolver)
+ {
+ ProductInventoryProviderResolver = productInventoryProviderResolver;
+ }
+
+ public virtual async Task TryReduceInventoryAsync(Guid? tenantId, string providerName, Guid storeId, Guid productId,
+ Guid productSkuId)
+ {
+ var model = new InventoryQueryModel(tenantId, storeId, productId, productSkuId);
+ return await (await ProductInventoryProviderResolver.GetAsync(providerName))
+ .TryReduceInventoryAsync(model, 1, true, true);
+ }
+
+ public virtual async Task TryRollBackInventoryAsync(Guid? tenantId, string providerName, Guid storeId, Guid productId,
+ Guid productSkuId)
+ {
+ var model = new InventoryQueryModel(tenantId, storeId, productId, productSkuId);
+ return await (await ProductInventoryProviderResolver.GetAsync(providerName))
+ .TryIncreaseInventoryAsync(model, 1, true, true);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.Application/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client.csproj
new file mode 100644
index 00000000..860bfdaa
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiClientModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiClientModule.cs
new file mode 100644
index 00000000..0268b99a
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiClientModule.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace EasyAbp.EShop.Products.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopProductsPluginsFlashSalesApplicationContractsModule)
+)]
+public class EShopProductsPluginsFlashSalesHttpApiClientModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddHttpClientProxies(
+ typeof(EShopProductsApplicationContractsModule).Assembly,
+ EShopProductsRemoteServiceConsts.RemoteServiceName
+ );
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.Client/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.csproj b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.csproj
new file mode 100644
index 00000000..b96c1c89
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiModule.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiModule.cs
new file mode 100644
index 00000000..3d2bf12d
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Plugins/FlashSales/EShopProductsPluginsFlashSalesHttpApiModule.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Products.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopProductsHttpApiModule),
+ typeof(EShopProductsPluginsFlashSalesApplicationContractsModule)
+)]
+public class EShopProductsPluginsFlashSalesHttpApiModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(mvcBuilder =>
+ {
+ mvcBuilder.AddApplicationPartIfNotExists(typeof(EShopProductsPluginsFlashSalesHttpApiModule).Assembly);
+ });
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Products/FlashSaleInventoryController.cs b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Products/FlashSaleInventoryController.cs
new file mode 100644
index 00000000..4806a4ba
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/EasyAbp/EShop/Products/Products/FlashSaleInventoryController.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using EasyAbp.EShop.Products.Products.Dtos;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp;
+
+namespace EasyAbp.EShop.Products.Products;
+
+[RemoteService(Name = EShopProductsRemoteServiceConsts.RemoteServiceName)]
+[Route("/api/e-shop/products/plugins/flash-sale-inventory")]
+public class FlashSaleInventoryController : ProductsController, IFlashSaleInventoryAppService
+{
+ protected IFlashSaleInventoryAppService AppService { get; }
+
+ public FlashSaleInventoryController(IFlashSaleInventoryAppService appService)
+ {
+ AppService = appService;
+ }
+
+ [HttpPost("try-increase")]
+ public virtual Task TryIncreaseAsync(IncreaseInventoryInput input)
+ {
+ return AppService.TryIncreaseAsync(input);
+ }
+
+ [HttpPost("try-reduce")]
+ public virtual Task TryReduceAsync(ReduceInventoryInput input)
+ {
+ return AppService.TryReduceAsync(input);
+ }
+}
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xml b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xml
new file mode 100644
index 00000000..be0de3a9
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xsd b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xsd
new file mode 100644
index 00000000..3f3946e2
--- /dev/null
+++ b/plugins/FlashSales/src/EasyAbp.EShop.Products.Plugins.FlashSales.HttpApi/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.abppkg.json b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.abppkg.json
new file mode 100644
index 00000000..a686451f
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.test"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.csproj b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.csproj
new file mode 100644
index 00000000..bd18bfdc
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandlerTests.cs b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandlerTests.cs
new file mode 100644
index 00000000..1305861f
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Orders/CreateFlashSaleOrderEventHandlerTests.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using EasyAbp.EShop.Orders.Plugins.FlashSales;
+using EasyAbp.EShop.Plugins.FlashSales;
+using EasyAbp.EShop.Products.ProductDetails.Dtos;
+using EasyAbp.EShop.Products.ProductDetails;
+using EasyAbp.EShop.Products.Products;
+using EasyAbp.EShop.Products.Products.Dtos;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NSubstitute;
+using Xunit;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+using Volo.Abp.Users;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.MultiTenancy;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+
+namespace EasyAbp.EShop.Orders.Orders;
+
+public class CreateFlashSaleOrderEventHandlerTests : OrdersPluginsFlashSalesApplicationTestBase
+{
+ protected CreateFlashSaleOrderEventHandler EventHandler { get; }
+
+ protected ICurrentUser CurrentUser { get; }
+
+ protected ICurrentTenant CurrentTenant { get; }
+
+ protected IDistributedEventBus DistributedEventBus { get; }
+
+ protected IFlashSalePlanHasher FlashSalePlanHasher { get; }
+
+ protected IOrderRepository OrderRepository { get; }
+
+ protected ProductDto Product1 { get; set; }
+
+ public CreateFlashSaleOrderEventHandlerTests()
+ {
+ EventHandler = GetRequiredService();
+ CurrentUser = GetRequiredService();
+ CurrentTenant = GetRequiredService();
+ DistributedEventBus = GetRequiredService();
+ FlashSalePlanHasher = GetRequiredService();
+ OrderRepository = GetRequiredService();
+ }
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ Product1 = CreateMockProductDto();
+
+ var productAppService = Substitute.For();
+ productAppService.GetAsync(FlashSalesTestData.Product1Id).Returns(Task.FromResult(Product1));
+ services.Replace(ServiceDescriptor.Singleton(productAppService));
+
+ var productDetailAppService = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(productDetailAppService));
+ productDetailAppService.GetAsync(FlashSalesTestData.ProductDetail1Id).Returns(Task.FromResult(
+ new ProductDetailDto
+ {
+ Id = FlashSalesTestData.ProductDetail1Id,
+ CreationTime = FlashSalesTestData.ProductDetailLastModificationTime,
+ LastModificationTime = FlashSalesTestData.ProductDetailLastModificationTime,
+ StoreId = FlashSalesTestData.Store1Id,
+ Description = "My Details 1"
+ }));
+ productDetailAppService.GetAsync(FlashSalesTestData.ProductDetail2Id).Returns(Task.FromResult(
+ new ProductDetailDto
+ {
+ Id = FlashSalesTestData.ProductDetail2Id,
+ StoreId = FlashSalesTestData.Store1Id,
+ Description = "My Details 2"
+ }));
+
+ var distributedEventBus = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(distributedEventBus));
+
+ var flashSalePlanHasher = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(flashSalePlanHasher));
+
+ var orderRepository = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(orderRepository));
+
+ base.AfterAddApplication(services);
+ }
+
+ [Fact]
+ public async Task HandleEventAsync()
+ {
+ FlashSalePlanHasher.HashAsync(default, default, default)
+ .ReturnsForAnyArgs("My Hash Token");
+ OrderRepository.InsertAsync(default, default, default)
+ .ReturnsForAnyArgs(callInfo => callInfo.Arg());
+
+ var createFlashSaleOrderEto = new CreateFlashSaleOrderEto
+ {
+ TenantId = CurrentTenant.Id,
+ UserId = CurrentUser.GetId(),
+ ResultId = FlashSalesTestData.Result1Id,
+ CustomerRemark = "My Remark",
+ HashToken = "My Hash Token",
+ Plan = new FlashSalePlanEto
+ {
+ Id = FlashSalesTestData.Plan1Id,
+ TenantId = CurrentTenant.Id,
+ StoreId = FlashSalesTestData.Store1Id,
+ BeginTime = DateTime.Now,
+ EndTime = DateTime.Now.AddMinutes(30),
+ ProductId = FlashSalesTestData.Product1Id,
+ ProductSkuId = FlashSalesTestData.ProductSku1Id,
+ IsPublished = true
+ }
+ };
+
+ await EventHandler.HandleEventAsync(createFlashSaleOrderEto);
+
+ await DistributedEventBus.Received()
+ .PublishAsync(Arg.Is(eto =>
+ eto.TenantId == CurrentTenant.Id &&
+ eto.ResultId == FlashSalesTestData.Result1Id &&
+ eto.Success &&
+ eto.StoreId == FlashSalesTestData.Store1Id &&
+ eto.PlanId == FlashSalesTestData.Plan1Id &&
+ eto.UserId == CurrentUser.GetId() &&
+ eto.OrderId != null &&
+ eto.Reason == null
+ ));
+ }
+
+ [Fact]
+ public async Task HandleEventAsync_Should_Publish_False_When_ValidateHashToken_Failed()
+ {
+ FlashSalePlanHasher.HashAsync(default, default, default)
+ .ReturnsForAnyArgs("My Hash Token");
+
+ var createFlashSaleOrderEto = new CreateFlashSaleOrderEto
+ {
+ TenantId = CurrentTenant.Id,
+ UserId = CurrentUser.GetId(),
+ ResultId = FlashSalesTestData.Result1Id,
+ CustomerRemark = "My Remark",
+ HashToken = "My Hash Token Failed",
+ Plan = new FlashSalePlanEto
+ {
+ Id = FlashSalesTestData.Plan1Id,
+ TenantId = CurrentTenant.Id,
+ StoreId = FlashSalesTestData.Store1Id,
+ BeginTime = DateTime.Now,
+ EndTime = DateTime.Now.AddMinutes(30),
+ ProductId = FlashSalesTestData.Product1Id,
+ ProductSkuId = FlashSalesTestData.ProductSku1Id,
+ IsPublished = true
+ }
+ };
+
+ await EventHandler.HandleEventAsync(createFlashSaleOrderEto);
+
+ await DistributedEventBus.Received()
+ .PublishAsync(Arg.Is(eto =>
+ eto.TenantId == CurrentTenant.Id &&
+ eto.ResultId == FlashSalesTestData.Result1Id &&
+ !eto.Success &&
+ eto.StoreId == FlashSalesTestData.Store1Id &&
+ eto.PlanId == FlashSalesTestData.Plan1Id &&
+ eto.UserId == CurrentUser.GetId() &&
+ eto.OrderId == null &&
+ eto.Reason == FlashSaleResultFailedReason.InvalidHashToken
+ ));
+ }
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationTestsModule.cs b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationTestsModule.cs
new file mode 100644
index 00000000..fa4d143e
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/EShopOrdersPluginsFlashSalesApplicationTestsModule.cs
@@ -0,0 +1,13 @@
+using EasyAbp.EShop.Plugins.FlashSales;
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Orders.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopOrdersPluginsFlashSalesApplicationModule),
+ typeof(EShopPluginsFlashSalesTestBaseModule)
+ )]
+public class EShopOrdersPluginsFlashSalesApplicationTestsModule : AbpModule
+{
+
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/OrdersPluginsFlashSalesApplicationTestBase.cs b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/OrdersPluginsFlashSalesApplicationTestBase.cs
new file mode 100644
index 00000000..a055ca98
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Orders/Plugins/FlashSales/OrdersPluginsFlashSalesApplicationTestBase.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using EasyAbp.EShop.Plugins.FlashSales;
+using EasyAbp.EShop.Products.Products;
+using EasyAbp.EShop.Products.Products.Dtos;
+
+namespace EasyAbp.EShop.Orders.Plugins.FlashSales;
+
+/* Inherit from this class for your application layer tests.
+ * See SampleAppService_Tests for example.
+ */
+public abstract class OrdersPluginsFlashSalesApplicationTestBase : FlashSalesTestBase
+{
+ protected virtual ProductDto CreateMockProductDto()
+ {
+ return new ProductDto
+ {
+ CreationTime = DateTime.Now,
+ IsPublished = true,
+ Id = FlashSalesTestData.Product1Id,
+ StoreId = FlashSalesTestData.Store1Id,
+ ProductGroupName = "Default",
+ ProductGroupDisplayName = "Default",
+ UniqueName = "Pencil",
+ DisplayName = "Hello pencil",
+ ProductDetailId = FlashSalesTestData.ProductDetail1Id,
+ ProductSkus = new List
+ {
+ new ProductSkuDto
+ {
+ Id = FlashSalesTestData.ProductSku1Id,
+ Name = "My SKU",
+ OrderMinQuantity = 0,
+ OrderMaxQuantity = 100,
+ AttributeOptionIds = new List(),
+ Price = 1m,
+ Currency = "USD",
+ ProductDetailId = null,
+ Inventory = 10,
+ },
+ new ProductSkuDto
+ {
+ Id = FlashSalesTestData.ProductSku2Id,
+ Name = "My SKU 2",
+ OrderMinQuantity = 0,
+ OrderMaxQuantity = 100,
+ AttributeOptionIds = new List(),
+ Price = 2m,
+ Currency = "USD",
+ ProductDetailId = FlashSalesTestData.ProductDetail2Id,
+ Inventory = 0
+ },
+ new ProductSkuDto
+ {
+ Id = FlashSalesTestData.ProductSku3Id,
+ Name = "My SKU 3",
+ OrderMinQuantity = 0,
+ OrderMaxQuantity = 100,
+ AttributeOptionIds = new List(),
+ Price = 3m,
+ Currency = "USD",
+ ProductDetailId = FlashSalesTestData.ProductDetail2Id,
+ Inventory = 1
+ }
+ },
+ InventoryStrategy = InventoryStrategy.FlashSales,
+ LastModificationTime = FlashSalesTestData.ProductLastModificationTime
+ };
+ }
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xml b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xml
new file mode 100644
index 00000000..1715698c
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xsd b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xsd
new file mode 100644
index 00000000..ffa6fc4b
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Orders.Plugins.FlashSales.Application.Tests/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.abppkg.json b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.abppkg.json
new file mode 100644
index 00000000..a686451f
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.abppkg.json
@@ -0,0 +1,3 @@
+{
+ "role": "lib.test"
+}
\ No newline at end of file
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.csproj b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.csproj
new file mode 100644
index 00000000..0719f417
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp.EShop.Plugins.FlashSales.Application.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/CreateTimeRange.cs b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/CreateTimeRange.cs
new file mode 100644
index 00000000..2dbc3aa4
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/CreateTimeRange.cs
@@ -0,0 +1,9 @@
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+public enum CreateTimeRange
+{
+ Starting,
+ NotStart,
+ Expired,
+ WillBeExpired,
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationTestModule.cs b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationTestModule.cs
new file mode 100644
index 00000000..cf845dff
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/EShopPluginsFlashSalesApplicationTestModule.cs
@@ -0,0 +1,12 @@
+using Volo.Abp.Modularity;
+
+namespace EasyAbp.EShop.Plugins.FlashSales;
+
+[DependsOn(
+ typeof(EShopPluginsFlashSalesApplicationModule),
+ typeof(EShopPluginsFlashSalesDomainTestModule)
+ )]
+public class EShopPluginsFlashSalesApplicationTestModule : AbpModule
+{
+
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandlerTests.cs b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandlerTests.cs
new file mode 100644
index 00000000..75a081b2
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSaleOrderCreationResultEventHandlerTests.cs
@@ -0,0 +1,257 @@
+using System.Threading.Tasks;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults;
+using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos;
+using EasyAbp.EShop.Products.ProductDetails;
+using EasyAbp.EShop.Products.ProductDetails.Dtos;
+using EasyAbp.Eshop.Products.Products;
+using EasyAbp.EShop.Products.Products;
+using EasyAbp.EShop.Products.Products.Dtos;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NSubstitute;
+using Shouldly;
+using Volo.Abp.ObjectMapping;
+using Volo.Abp.Users;
+using Xunit;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+
+public class FlashSaleOrderCreationResultEventHandlerTests : FlashSalesApplicationTestBase
+{
+ protected IObjectMapper ObjectMapper { get; }
+ protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; }
+ protected FlashSaleOrderCreationResultEventHandler FlashSaleOrderCreationResultEventHandler { get; }
+ protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; set; }
+
+ private ProductDto Product1 { get; set; }
+
+ public FlashSaleOrderCreationResultEventHandlerTests()
+ {
+ ObjectMapper = GetRequiredService();
+ FlashSaleCurrentResultCache = GetRequiredService();
+ FlashSaleOrderCreationResultEventHandler = GetRequiredService();
+ FlashSaleInventoryManager = GetRequiredService();
+ }
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ Product1 = CreateMockProductDto();
+
+ var productAppService = Substitute.For();
+ productAppService.GetAsync(FlashSalesTestData.Product1Id).Returns(Task.FromResult(Product1));
+ services.Replace(ServiceDescriptor.Singleton(productAppService));
+
+ var productDetailAppService = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(productDetailAppService));
+ productDetailAppService.GetAsync(FlashSalesTestData.ProductDetail1Id).Returns(Task.FromResult(
+ new ProductDetailDto
+ {
+ Id = FlashSalesTestData.ProductDetail1Id,
+ CreationTime = FlashSalesTestData.ProductDetailLastModificationTime,
+ LastModificationTime = FlashSalesTestData.ProductDetailLastModificationTime,
+ StoreId = FlashSalesTestData.Store1Id,
+ Description = "My Details 1"
+ }));
+ productDetailAppService.GetAsync(FlashSalesTestData.ProductDetail2Id).Returns(Task.FromResult(
+ new ProductDetailDto
+ {
+ Id = FlashSalesTestData.ProductDetail2Id,
+ StoreId = FlashSalesTestData.Store1Id,
+ Description = "My Details 2"
+ }));
+
+ var flashSaleInventoryManager = Substitute.For();
+ services.Replace(ServiceDescriptor.Singleton(flashSaleInventoryManager));
+
+ base.AfterAddApplication(services);
+ }
+
+ protected async Task SetFlashSaleCurrentResultCacheAsync(FlashSaleResult flashSaleResult)
+ {
+ await FlashSaleCurrentResultCache.SetAsync(flashSaleResult.PlanId, flashSaleResult.UserId,
+ new FlashSaleCurrentResultCacheItem
+ {
+ TenantId = CurrentTenant.Id,
+ ResultDto = ObjectMapper.Map(flashSaleResult)
+ });
+ }
+
+ [Fact]
+ public async Task HandleEventAsync_When_Create_Order_Success()
+ {
+ var plan = await CreateFlashSalePlanAsync();
+ var flashSaleResult = await CreatePendingResultAsync(plan.Id, plan.StoreId, CurrentUser.GetId());
+ await SetFlashSaleCurrentResultCacheAsync(flashSaleResult);
+
+ var flashSaleOrderCreationResultEto = new FlashSaleOrderCreationResultEto
+ {
+ TenantId = flashSaleResult.TenantId,
+ ResultId = flashSaleResult.Id,
+ Success = true,
+ StoreId = flashSaleResult.StoreId,
+ PlanId = flashSaleResult.PlanId,
+ OrderId = GuidGenerator.Create(),
+ Reason = null,
+ UserId = flashSaleResult.UserId,
+ ProductInventoryProviderName = Product1.InventoryProviderName,
+ ProductId = plan.ProductId,
+ ProductSkuId = plan.ProductSkuId,
+ AllowToTryAgain = false
+ };
+
+ FlashSaleInventoryManager
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId)
+ .Returns(Task.FromResult(true));
+
+ await FlashSaleOrderCreationResultEventHandler.HandleEventAsync(flashSaleOrderCreationResultEto);
+
+ var flashResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id);
+ flashResult.Status.ShouldBe(FlashSaleResultStatus.Successful);
+ flashResult.OrderId.ShouldBe(flashSaleOrderCreationResultEto.OrderId);
+ flashResult.Reason.ShouldBe(null);
+
+ var flashSaleCurrentResultCache = await FlashSaleCurrentResultCache.GetAsync(flashResult.PlanId, flashResult.UserId);
+ flashSaleCurrentResultCache.ShouldNotBeNull();
+ flashSaleCurrentResultCache.ResultDto.Id.ShouldBe(flashSaleResult.Id);
+ flashSaleCurrentResultCache.ResultDto.Status.ShouldBe(FlashSaleResultStatus.Successful);
+
+ await FlashSaleInventoryManager.DidNotReceive()
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId);
+ }
+
+ [Fact]
+ public async Task HandleEventAsync_When_Create_Order_Failed_And_Not_Allow_To_Try_Again()
+ {
+ var plan = await CreateFlashSalePlanAsync();
+ var flashSaleResult = await CreatePendingResultAsync(plan.Id, plan.StoreId, CurrentUser.GetId());
+ await SetFlashSaleCurrentResultCacheAsync(flashSaleResult);
+
+ var flashSaleOrderCreationResultEto = new FlashSaleOrderCreationResultEto
+ {
+ TenantId = flashSaleResult.TenantId,
+ ResultId = flashSaleResult.Id,
+ Success = false,
+ StoreId = FlashSalesTestData.Store1Id,
+ PlanId = flashSaleResult.PlanId,
+ OrderId = null,
+ Reason = "Failed reason",
+ UserId = flashSaleResult.UserId,
+ ProductInventoryProviderName = Product1.InventoryProviderName,
+ ProductId = plan.ProductId,
+ ProductSkuId = plan.ProductSkuId,
+ AllowToTryAgain = false
+ };
+
+ FlashSaleInventoryManager
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId)
+ .Returns(Task.FromResult(true));
+
+ await FlashSaleOrderCreationResultEventHandler.HandleEventAsync(flashSaleOrderCreationResultEto);
+
+ var flashResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id);
+ flashResult.Status.ShouldBe(FlashSaleResultStatus.Failed);
+ flashResult.OrderId.ShouldBe(null);
+ flashResult.Reason.ShouldBe("Failed reason");
+
+ var flashSaleCurrentResultCache = await FlashSaleCurrentResultCache.GetAsync(flashResult.PlanId, flashResult.UserId);
+ flashSaleCurrentResultCache.ShouldNotBeNull();
+ flashSaleCurrentResultCache.ResultDto.Id.ShouldBe(flashSaleResult.Id);
+ flashSaleCurrentResultCache.ResultDto.Status.ShouldBe(FlashSaleResultStatus.Failed);
+
+ await FlashSaleInventoryManager.Received()
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId);
+ }
+
+ [Fact]
+ public async Task HandleEventAsync_When_Create_Order_Failed_And_Allow_To_Try_Again()
+ {
+ var plan = await CreateFlashSalePlanAsync();
+ var flashSaleResult = await CreatePendingResultAsync(plan.Id, plan.StoreId, CurrentUser.GetId());
+ await SetFlashSaleCurrentResultCacheAsync(flashSaleResult);
+
+ var flashSaleOrderCreationResultEto = new FlashSaleOrderCreationResultEto
+ {
+ TenantId = flashSaleResult.TenantId,
+ ResultId = flashSaleResult.Id,
+ Success = false,
+ StoreId = FlashSalesTestData.Store1Id,
+ PlanId = flashSaleResult.PlanId,
+ OrderId = null,
+ Reason = "Failed reason",
+ UserId = flashSaleResult.UserId,
+ ProductInventoryProviderName = Product1.InventoryProviderName,
+ ProductId = plan.ProductId,
+ ProductSkuId = plan.ProductSkuId,
+ AllowToTryAgain = true
+ };
+
+ FlashSaleInventoryManager
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId)
+ .Returns(Task.FromResult(true));
+
+ await FlashSaleOrderCreationResultEventHandler.HandleEventAsync(flashSaleOrderCreationResultEto);
+
+ var flashResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id);
+ flashResult.Status.ShouldBe(FlashSaleResultStatus.Failed);
+ flashResult.OrderId.ShouldBe(null);
+ flashResult.Reason.ShouldBe("Failed reason");
+
+ var flashSaleCurrentResultCache = await FlashSaleCurrentResultCache.GetAsync(flashResult.PlanId, flashResult.UserId);
+ flashSaleCurrentResultCache.ShouldBeNull();
+
+ await FlashSaleInventoryManager.Received()
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId);
+ }
+
+ [Fact]
+ public async Task Should_Not_Remove_FlashSaleCurrentResultCache_When_TryRollBackInventory_Failed()
+ {
+ var plan = await CreateFlashSalePlanAsync();
+ var flashSaleResult = await CreatePendingResultAsync(plan.Id, plan.StoreId, CurrentUser.GetId());
+ await SetFlashSaleCurrentResultCacheAsync(flashSaleResult);
+
+ var flashSaleOrderCreationResultEto = new FlashSaleOrderCreationResultEto
+ {
+ TenantId = flashSaleResult.TenantId,
+ ResultId = flashSaleResult.Id,
+ Success = false,
+ StoreId = flashSaleResult.StoreId,
+ PlanId = flashSaleResult.PlanId,
+ OrderId = null,
+ Reason = FlashSaleResultFailedReason.InvalidHashToken,
+ UserId = flashSaleResult.UserId,
+ ProductInventoryProviderName = Product1.InventoryProviderName,
+ ProductId = plan.ProductId,
+ ProductSkuId = plan.ProductSkuId,
+ AllowToTryAgain = true
+ };
+
+ FlashSaleInventoryManager
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId)
+ .Returns(Task.FromResult(false));
+
+ await FlashSaleOrderCreationResultEventHandler.HandleEventAsync(flashSaleOrderCreationResultEto);
+
+ var flashSaleCurrentResultCache = await FlashSaleCurrentResultCache.GetAsync(plan.Id, CurrentUser.GetId());
+ flashSaleCurrentResultCache.ShouldNotBeNull();
+
+ await FlashSaleInventoryManager.Received()
+ .TryRollBackInventoryAsync(flashSaleOrderCreationResultEto.TenantId,
+ flashSaleOrderCreationResultEto.ProductInventoryProviderName, flashSaleOrderCreationResultEto.StoreId,
+ flashSaleOrderCreationResultEto.ProductId, flashSaleOrderCreationResultEto.ProductSkuId);
+ }
+}
diff --git a/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppServiceTests.cs b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppServiceTests.cs
new file mode 100644
index 00000000..0b748d20
--- /dev/null
+++ b/plugins/FlashSales/test/EasyAbp.EShop.Plugins.FlashSales.Application.Tests/EasyAbp/EShop/Plugins/FlashSales/FlashSalePlans/FlashSalePlanAppServiceTests.cs
@@ -0,0 +1,508 @@
+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.Products.Products;
+using EasyAbp.EShop.Products.Products.Dtos;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using NSubstitute;
+using Shouldly;
+using Volo.Abp;
+using Volo.Abp.Caching;
+using Volo.Abp.Data;
+using Volo.Abp.DistributedLocking;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.ObjectExtending;
+using Volo.Abp.Users;
+using Xunit;
+
+namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans;
+public class FlashSalePlanAppServiceTests : FlashSalesApplicationTestBase
+{
+ protected IFlashSalePlanAppService AppService { get; }
+
+ protected IDistributedEventBus DistributedEventBus { get; }
+
+ protected IDistributedCache ProductDistributedCache { get; }
+
+ private ProductDto Product1 { get; set; }
+
+ public FlashSalePlanAppServiceTests()
+ {
+ AppService = GetRequiredService();
+ DistributedEventBus = GetRequiredService