mirror of https://github.com/EasyAbp/EShop.git
314 changed files with 27125 additions and 193 deletions
@ -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. |
|||
@ -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 |
|||
@ -0,0 +1 @@ |
|||
../../docs/plugins/flash-sales/README.md |
|||
@ -0,0 +1 @@ |
|||
{} |
|||
@ -0,0 +1,10 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
</Project> |
|||
@ -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; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,15 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Orders\src\EasyAbp.EShop.Orders.Application\EasyAbp.EShop.Orders.Application.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,162 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Orders.Orders.Dtos; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Products.ProductDetails; |
|||
using EasyAbp.EShop.Products.ProductDetails.Dtos; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.ObjectExtending; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Orders.Orders; |
|||
|
|||
public class CreateFlashSaleOrderEventHandler : IDistributedEventHandler<CreateFlashSaleOrderEto>, ITransientDependency |
|||
{ |
|||
protected INewOrderGenerator NewOrderGenerator { get; } |
|||
|
|||
protected IObjectMapper ObjectMapper { get; } |
|||
|
|||
protected IEnumerable<IOrderDiscountProvider> OrderDiscountProviders { get; } |
|||
|
|||
protected IOrderRepository OrderRepository { get; } |
|||
|
|||
protected IDistributedEventBus DistributedEventBus { get; } |
|||
|
|||
protected IProductAppService ProductAppService { get; } |
|||
|
|||
protected IProductDetailAppService ProductDetailAppService { get; } |
|||
|
|||
protected IFlashSalePlanHasher FlashSalePlanHasher { get; } |
|||
|
|||
public CreateFlashSaleOrderEventHandler( |
|||
INewOrderGenerator newOrderGenerator, |
|||
IObjectMapper objectMapper, |
|||
IEnumerable<IOrderDiscountProvider> orderDiscountProviders, |
|||
IOrderRepository orderRepository, |
|||
IDistributedEventBus distributedEventBus, |
|||
IProductAppService productAppService, |
|||
IProductDetailAppService productDetailAppService, |
|||
IFlashSalePlanHasher flashSalePlanHasher) |
|||
{ |
|||
NewOrderGenerator = newOrderGenerator; |
|||
ObjectMapper = objectMapper; |
|||
OrderDiscountProviders = orderDiscountProviders; |
|||
OrderRepository = orderRepository; |
|||
DistributedEventBus = distributedEventBus; |
|||
ProductAppService = productAppService; |
|||
ProductDetailAppService = productDetailAppService; |
|||
FlashSalePlanHasher = flashSalePlanHasher; |
|||
} |
|||
|
|||
[UnitOfWork(true)] |
|||
public virtual async Task HandleEventAsync(CreateFlashSaleOrderEto eventData) |
|||
{ |
|||
var product = await ProductAppService.GetAsync(eventData.Plan.ProductId); |
|||
var productSku = product.GetSkuById(eventData.Plan.ProductSkuId); |
|||
|
|||
if (!await ValidateHashTokenAsync(eventData.Plan, product, productSku, eventData.HashToken)) |
|||
{ |
|||
await DistributedEventBus.PublishAsync(new FlashSaleOrderCreationResultEto |
|||
{ |
|||
TenantId = eventData.TenantId, |
|||
PlanId = eventData.Plan.Id, |
|||
OrderId = null, |
|||
UserId = eventData.UserId, |
|||
StoreId = eventData.Plan.StoreId, |
|||
ResultId = eventData.ResultId, |
|||
Success = false, |
|||
Reason = FlashSaleResultFailedReason.InvalidHashToken, |
|||
ProductInventoryProviderName = product.InventoryProviderName, |
|||
ProductId = eventData.Plan.ProductId, |
|||
ProductSkuId = eventData.Plan.ProductSkuId, |
|||
AllowToTryAgain = true |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
var input = await ConvertToCreateOrderDtoAsync(eventData); |
|||
|
|||
var productDict = await GetProductDictionaryAsync(product); |
|||
|
|||
var productDetailDict = await GetProductDetailDictionaryAsync(product, productSku); |
|||
|
|||
var order = await NewOrderGenerator.GenerateAsync(eventData.UserId, input, productDict, productDetailDict); |
|||
|
|||
await OrderRepository.InsertAsync(order, autoSave: true); |
|||
|
|||
await DistributedEventBus.PublishAsync(new FlashSaleOrderCreationResultEto |
|||
{ |
|||
TenantId = eventData.TenantId, |
|||
PlanId = eventData.Plan.Id, |
|||
OrderId = order.Id, |
|||
UserId = eventData.UserId, |
|||
StoreId = eventData.Plan.StoreId, |
|||
ResultId = eventData.ResultId, |
|||
Success = true, |
|||
Reason = null, |
|||
ProductInventoryProviderName = product.InventoryProviderName, |
|||
ProductId = eventData.Plan.ProductId, |
|||
ProductSkuId = eventData.Plan.ProductSkuId, |
|||
AllowToTryAgain = false |
|||
}); |
|||
} |
|||
|
|||
protected virtual Task<Dictionary<Guid, ProductDto>> GetProductDictionaryAsync(ProductDto product) |
|||
{ |
|||
var productDict = new Dictionary<Guid, ProductDto>() |
|||
{ |
|||
{product.Id, product} |
|||
}; |
|||
|
|||
return Task.FromResult(productDict); |
|||
} |
|||
|
|||
protected virtual async Task<Dictionary<Guid, ProductDetailDto>> GetProductDetailDictionaryAsync(ProductDto product, ProductSkuDto productSku) |
|||
{ |
|||
var dict = new Dictionary<Guid, ProductDetailDto>(); |
|||
|
|||
var productDetailId = productSku.ProductDetailId ?? product.ProductDetailId; |
|||
|
|||
if (productDetailId.HasValue) |
|||
{ |
|||
dict.Add(productDetailId.Value, await ProductDetailAppService.GetAsync(productDetailId.Value)); |
|||
} |
|||
|
|||
return dict; |
|||
} |
|||
|
|||
protected virtual Task<CreateOrderDto> ConvertToCreateOrderDtoAsync(CreateFlashSaleOrderEto eventData) |
|||
{ |
|||
var input = new CreateOrderDto() |
|||
{ |
|||
StoreId = eventData.Plan.StoreId, |
|||
CustomerRemark = eventData.CustomerRemark, |
|||
OrderLines = new List<CreateOrderLineDto>() |
|||
{ |
|||
new CreateOrderLineDto() |
|||
{ |
|||
ProductId = eventData.Plan.ProductId, |
|||
ProductSkuId = eventData.Plan.ProductSkuId, |
|||
Quantity = 1 |
|||
} |
|||
} |
|||
}; |
|||
|
|||
eventData.MapExtraPropertiesTo(input); |
|||
|
|||
return Task.FromResult(input); |
|||
} |
|||
|
|||
protected virtual async Task<bool> ValidateHashTokenAsync(FlashSalePlanEto plan, ProductDto product, ProductSkuDto productSku, string originHashToken) |
|||
{ |
|||
var hashToken = await FlashSalePlanHasher.HashAsync(plan.LastModificationTime, product.LastModificationTime, productSku.LastModificationTime); |
|||
|
|||
return string.Equals(hashToken, originHashToken, StringComparison.InvariantCulture); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using EasyAbp.EShop.Plugins.FlashSales; |
|||
using EasyAbp.EShop.Products; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AutoMapper; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EasyAbp.EShop.Orders.Plugins.FlashSales; |
|||
|
|||
[DependsOn( |
|||
typeof(EShopOrdersApplicationModule), |
|||
typeof(EShopProductsApplicationContractsModule), |
|||
typeof(EShopPluginsFlashSalesApplicationContractsModule) |
|||
)] |
|||
public class EShopOrdersPluginsFlashSalesApplicationModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAutoMapperObjectMapper<EShopOrdersPluginsFlashSalesApplicationModule>(); |
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
options.AddMaps<EShopOrdersPluginsFlashSalesApplicationModule>(validate: true); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.application-contracts" |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(AbpVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Authorization" Version="$(AbpVersion)" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared\EasyAbp.EShop.Plugins.FlashSales.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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 |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace EasyAbp.EShop.Plugins.FlashSales; |
|||
|
|||
public class EShopPluginsFlashSalesRemoteServiceConsts |
|||
{ |
|||
public const string RemoteServiceName = "EasyAbpEShopPluginsFlashSales"; |
|||
|
|||
public const string ModuleName = "easyAbpEShopPluginsFlashSales"; |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos; |
|||
|
|||
[Serializable] |
|||
public class FlashSalePlanDto : ExtensibleFullAuditedEntityDto<Guid>, IHasConcurrencyStamp |
|||
{ |
|||
public Guid StoreId { get; set; } |
|||
|
|||
public DateTime BeginTime { get; set; } |
|||
|
|||
public DateTime EndTime { get; set; } |
|||
|
|||
public Guid ProductId { get; set; } |
|||
|
|||
public Guid ProductSkuId { get; set; } |
|||
|
|||
public bool IsPublished { get; set; } |
|||
|
|||
public string ConcurrencyStamp { get; set; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public interface IFlashSalePlanAppService : |
|||
ICrudAppService< |
|||
FlashSalePlanDto, |
|||
Guid, |
|||
FlashSalePlanGetListInput, |
|||
FlashSalePlanCreateDto, |
|||
FlashSalePlanUpdateDto> |
|||
{ |
|||
Task<FlashSalePlanPreOrderDto> PreOrderAsync(Guid id); |
|||
|
|||
Task<FlashSaleOrderResultDto> OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput); |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Stores.Stores; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
|
|||
public class FlashSaleResultDto : ExtensibleFullAuditedEntityDto<Guid>, IMultiStore |
|||
{ |
|||
public Guid StoreId { get; set; } |
|||
|
|||
public Guid PlanId { get; set; } |
|||
|
|||
public FlashSaleResultStatus Status { get; set; } |
|||
|
|||
public string Reason { get; set; } |
|||
|
|||
public Guid UserId { get; set; } |
|||
|
|||
public Guid? OrderId { get; set; } |
|||
|
|||
public DateTime ReducedInventoryTime { get; set; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public interface IFlashSaleResultAppService : |
|||
IReadOnlyAppService< |
|||
FlashSaleResultDto, |
|||
Guid, |
|||
FlashSaleResultGetListInput> |
|||
{ |
|||
Task<FlashSaleResultDto> GetCurrentAsync(Guid planId); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using EasyAbp.EShop.Plugins.FlashSales.Localization; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.Permissions; |
|||
|
|||
public class FlashSalesPermissionDefinitionProvider : PermissionDefinitionProvider |
|||
{ |
|||
public override void Define(IPermissionDefinitionContext context) |
|||
{ |
|||
var myGroup = context.AddGroup(FlashSalesPermissions.GroupName, L("Permission:FlashSales")); |
|||
|
|||
var flashSalePlanPermission = myGroup.AddPermission(FlashSalesPermissions.FlashSalePlan.Default, L("Permission:FlashSalePlan")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Manage, L("Permission:Manage")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.CrossStore, L("Permission:CrossStore")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Create, L("Permission:Create")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Update, L("Permission:Update")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.Delete, L("Permission:Delete")); |
|||
flashSalePlanPermission.AddChild(FlashSalesPermissions.FlashSalePlan.PreOrder, L("Permission:PreOrder")); |
|||
|
|||
var flashSaleResultPermission = myGroup.AddPermission(FlashSalesPermissions.FlashSaleResult.Default, L("Permission:FlashSaleResult")); |
|||
flashSaleResultPermission.AddChild(FlashSalesPermissions.FlashSaleResult.Manage, L("Permission:Manage")); |
|||
flashSaleResultPermission.AddChild(FlashSalesPermissions.FlashSaleResult.CrossStore, L("Permission:CrossStore")); |
|||
} |
|||
|
|||
private static LocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<FlashSalesResource>(name); |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.application" |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.*" /> |
|||
<PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application" Version="$(AbpVersion)" /> |
|||
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Application.Contracts\EasyAbp.EShop.Products.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Shared\EasyAbp.EShop.Stores.Application.Shared.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Plugins.FlashSales.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.FlashSales.Domain\EasyAbp.EShop.Plugins.FlashSales.Domain.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions\EasyAbp.EShop.Products.Plugins.FlashSales.Abstractions.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts\EasyAbp.EShop.Products.Plugins.FlashSales.Application.Contracts.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,33 @@ |
|||
using EasyAbp.EShop.Products; |
|||
using EasyAbp.EShop.Products.Plugins.FlashSales; |
|||
using EasyAbp.EShop.Stores; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.AutoMapper; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales; |
|||
|
|||
[DependsOn( |
|||
typeof(EShopProductsApplicationContractsModule), |
|||
typeof(EShopPluginsFlashSalesDomainModule), |
|||
typeof(EShopPluginsFlashSalesApplicationContractsModule), |
|||
typeof(EShopStoresApplicationSharedModule), |
|||
typeof(EShopProductsPluginsFlashSalesAbstractionsModule), |
|||
typeof(EShopProductsPluginsFlashSalesApplicationContractsModule), |
|||
typeof(AbpDddApplicationModule), |
|||
typeof(AbpAutoMapperModule), |
|||
typeof(AbpCachingModule) |
|||
)] |
|||
public class EShopPluginsFlashSalesApplicationModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAutoMapperObjectMapper<EShopPluginsFlashSalesApplicationModule>(); |
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
options.AddMaps<EShopPluginsFlashSalesApplicationModule>(validate: true); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSaleOrderCreationResultEventHandler : IDistributedEventHandler<FlashSaleOrderCreationResultEto>, |
|||
ITransientDependency |
|||
{ |
|||
protected ILogger<FlashSaleOrderCreationResultEventHandler> Logger { get; } |
|||
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
protected IObjectMapper ObjectMapper { get; } |
|||
protected IAbpApplication AbpApplication { get; } |
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
protected IFlashSaleResultRepository FlashSaleResultRepository { get; } |
|||
|
|||
public FlashSaleOrderCreationResultEventHandler( |
|||
ILogger<FlashSaleOrderCreationResultEventHandler> logger, |
|||
IFlashSaleInventoryManager flashSaleInventoryManager, |
|||
IUnitOfWorkManager unitOfWorkManager, |
|||
IObjectMapper objectMapper, |
|||
IAbpApplication abpApplication, |
|||
IFlashSaleCurrentResultCache flashSaleCurrentResultCache, |
|||
IFlashSaleResultRepository flashSaleResultRepository) |
|||
{ |
|||
Logger = logger; |
|||
FlashSaleInventoryManager = flashSaleInventoryManager; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
ObjectMapper = objectMapper; |
|||
AbpApplication = abpApplication; |
|||
FlashSaleCurrentResultCache = flashSaleCurrentResultCache; |
|||
FlashSaleResultRepository = flashSaleResultRepository; |
|||
} |
|||
|
|||
[UnitOfWork] |
|||
public virtual async Task HandleEventAsync(FlashSaleOrderCreationResultEto eventData) |
|||
{ |
|||
var flashSaleResult = await FlashSaleResultRepository.GetAsync(eventData.ResultId); |
|||
|
|||
if (eventData.Success) |
|||
{ |
|||
flashSaleResult.MarkAsSuccessful(eventData.OrderId!.Value); |
|||
|
|||
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true); |
|||
} |
|||
else |
|||
{ |
|||
flashSaleResult.MarkAsFailed(eventData.Reason); |
|||
|
|||
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true); |
|||
} |
|||
|
|||
UnitOfWorkManager.Current.OnCompleted(async () => |
|||
{ |
|||
if (eventData.Success) |
|||
{ |
|||
await ResetFlashSaleCurrentResultCacheAsync(flashSaleResult); |
|||
} |
|||
else |
|||
{ |
|||
// try to roll back the inventory.
|
|||
if (!await FlashSaleInventoryManager.TryRollBackInventoryAsync( |
|||
eventData.TenantId, eventData.ProductInventoryProviderName, eventData.StoreId, |
|||
eventData.ProductId, eventData.ProductSkuId)) |
|||
{ |
|||
Logger.LogWarning("Failed to roll back the flash sale inventory."); |
|||
return; // avoid to remove cache if the rollback failed.
|
|||
} |
|||
|
|||
// remove the cache so the user can try to order again.
|
|||
if (eventData.AllowToTryAgain) |
|||
{ |
|||
await FlashSaleCurrentResultCache.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId); |
|||
} |
|||
else |
|||
{ |
|||
await ResetFlashSaleCurrentResultCacheAsync(flashSaleResult); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected virtual async Task ResetFlashSaleCurrentResultCacheAsync(FlashSaleResult flashSaleResult) |
|||
{ |
|||
using var scope = AbpApplication.ServiceProvider.CreateScope(); |
|||
|
|||
var objectMapper = scope.ServiceProvider.GetRequiredService<IObjectMapper>(); |
|||
var flashSaleCurrentResultCache = scope.ServiceProvider.GetRequiredService<IFlashSaleCurrentResultCache>(); |
|||
|
|||
await flashSaleCurrentResultCache.SetAsync(flashSaleResult.PlanId, flashSaleResult.UserId, |
|||
new FlashSaleCurrentResultCacheItem |
|||
{ |
|||
TenantId = flashSaleResult.TenantId, |
|||
ResultDto = objectMapper.Map<FlashSaleResult, FlashSaleResultDto>(flashSaleResult) |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,452 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using EasyAbp.EShop.Plugins.FlashSales.Options; |
|||
using EasyAbp.EShop.Plugins.FlashSales.Permissions; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using EasyAbp.EShop.Stores.Stores; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DistributedLocking; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.ObjectExtending; |
|||
using Volo.Abp.Uow; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSalePlanAppService : |
|||
MultiStoreCrudAppService<FlashSalePlan, FlashSalePlanDto, Guid, FlashSalePlanGetListInput, FlashSalePlanCreateDto, FlashSalePlanUpdateDto>, |
|||
IFlashSalePlanAppService |
|||
{ |
|||
/// <summary>
|
|||
/// The <see cref="GetPreOrderCacheKeyAsync(FlashSalePlanCacheItem)"/> cache key format.
|
|||
/// <para>{0}: FlashSalePlan ID</para>
|
|||
/// <para>{1}: User ID</para>
|
|||
/// </summary>
|
|||
public const string PreOrderCacheKeyFormat = "eshopflashsales_{0}_{1}"; |
|||
|
|||
protected override string CrossStorePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.CrossStore; |
|||
protected override string GetPolicyName { get; set; } = null; |
|||
protected override string GetListPolicyName { get; set; } = null; |
|||
protected override string CreatePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Create; |
|||
protected override string UpdatePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Update; |
|||
protected override string DeletePolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.Delete; |
|||
protected virtual string PreOrderPolicyName { get; set; } = FlashSalesPermissions.FlashSalePlan.PreOrder; |
|||
|
|||
protected IFlashSalePlanRepository FlashSalePlanRepository { get; } |
|||
|
|||
protected IProductAppService ProductAppService { get; } |
|||
|
|||
protected IDistributedCache<FlashSalePlanPreOrderCacheItem> PreOrderDistributedCache { get; } |
|||
|
|||
protected IDistributedCache<FlashSalePlanCacheItem, Guid> PlanDistributedCache { get; } |
|||
|
|||
protected IDistributedCache<ProductCacheItem, Guid> ProductDistributedCache { get; } |
|||
|
|||
protected IDistributedEventBus DistributedEventBus { get; } |
|||
|
|||
protected IFlashSaleResultRepository FlashSaleResultRepository { get; } |
|||
|
|||
protected IAbpDistributedLock DistributedLock { get; } |
|||
|
|||
protected IFlashSalePlanHasher FlashSalePlanHasher { get; } |
|||
|
|||
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; } |
|||
|
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
|
|||
protected FlashSalesOptions Options { get; } |
|||
|
|||
public FlashSalePlanAppService( |
|||
IFlashSalePlanRepository flashSalePlanRepository, |
|||
IProductAppService productAppService, |
|||
IDistributedCache<FlashSalePlanPreOrderCacheItem> tokenDistributedCache, |
|||
IDistributedCache<FlashSalePlanCacheItem, Guid> planDistributedCache, |
|||
IDistributedCache<ProductCacheItem, Guid> productDistributedCache, |
|||
IDistributedEventBus distributedEventBus, |
|||
IFlashSaleResultRepository flashSaleResultRepository, |
|||
IAbpDistributedLock distributedLock, |
|||
IFlashSalePlanHasher flashSalePlanHasher, |
|||
IFlashSaleInventoryManager flashSaleInventoryManager, |
|||
IFlashSaleCurrentResultCache flashSaleCurrentResultCache, |
|||
IOptions<FlashSalesOptions> options) |
|||
: base(flashSalePlanRepository) |
|||
{ |
|||
FlashSalePlanRepository = flashSalePlanRepository; |
|||
ProductAppService = productAppService; |
|||
PreOrderDistributedCache = tokenDistributedCache; |
|||
PlanDistributedCache = planDistributedCache; |
|||
ProductDistributedCache = productDistributedCache; |
|||
DistributedEventBus = distributedEventBus; |
|||
FlashSaleResultRepository = flashSaleResultRepository; |
|||
DistributedLock = distributedLock; |
|||
FlashSalePlanHasher = flashSalePlanHasher; |
|||
FlashSaleInventoryManager = flashSaleInventoryManager; |
|||
FlashSaleCurrentResultCache = flashSaleCurrentResultCache; |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public override async Task<FlashSalePlanDto> GetAsync(Guid id) |
|||
{ |
|||
var flashSalePlan = await GetEntityByIdAsync(id); |
|||
|
|||
await CheckGetPolicyAsync(); |
|||
|
|||
if (!flashSalePlan.IsPublished) |
|||
{ |
|||
await CheckMultiStorePolicyAsync(flashSalePlan.StoreId, FlashSalesPermissions.FlashSalePlan.Manage); |
|||
} |
|||
|
|||
return await MapToGetOutputDtoAsync(flashSalePlan); |
|||
} |
|||
|
|||
public override async Task<PagedResultDto<FlashSalePlanDto>> GetListAsync(FlashSalePlanGetListInput input) |
|||
{ |
|||
await CheckGetListPolicyAsync(); |
|||
|
|||
if (input.IncludeUnpublished) |
|||
{ |
|||
await CheckMultiStorePolicyAsync(input.StoreId, FlashSalesPermissions.FlashSalePlan.Manage); |
|||
} |
|||
|
|||
return await base.GetListAsync(input); |
|||
} |
|||
|
|||
public override async Task<FlashSalePlanDto> CreateAsync(FlashSalePlanCreateDto input) |
|||
{ |
|||
await CheckMultiStorePolicyAsync(input.StoreId, CreatePolicyName); |
|||
|
|||
var product = await ProductAppService.GetAsync(input.ProductId); |
|||
var productSku = product.GetSkuById(input.ProductSkuId); |
|||
|
|||
await ValidateProductAsync(input.ProductId, product, input.StoreId); |
|||
|
|||
var flashSalePlan = new FlashSalePlan( |
|||
GuidGenerator.Create(), |
|||
CurrentTenant.Id, |
|||
input.StoreId, |
|||
input.BeginTime, |
|||
input.EndTime, |
|||
product.Id, |
|||
productSku.Id, |
|||
input.IsPublished |
|||
); |
|||
|
|||
await FlashSalePlanRepository.InsertAsync(flashSalePlan, autoSave: true); |
|||
|
|||
return await MapToGetOutputDtoAsync(flashSalePlan); |
|||
} |
|||
|
|||
public override async Task<FlashSalePlanDto> UpdateAsync(Guid id, FlashSalePlanUpdateDto input) |
|||
{ |
|||
var flashSalePlan = await GetEntityByIdAsync(id); |
|||
var product = await ProductAppService.GetAsync(input.ProductId); |
|||
var productSku = product.GetSkuById(input.ProductSkuId); |
|||
|
|||
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName); |
|||
|
|||
await ValidateProductAsync(input.ProductId, product, flashSalePlan.StoreId); |
|||
|
|||
if (await ExistRelatedFlashSaleResultsAsync(id) && (input.ProductId != flashSalePlan.ProductId || input.ProductSkuId != flashSalePlan.ProductSkuId)) |
|||
{ |
|||
throw new RelatedFlashSaleResultsExistException(id); |
|||
} |
|||
|
|||
flashSalePlan.SetTimeRange(input.BeginTime, input.EndTime); |
|||
flashSalePlan.SetProductSku(flashSalePlan.StoreId, product.Id, productSku.Id); |
|||
flashSalePlan.SetPublished(input.IsPublished); |
|||
|
|||
flashSalePlan.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp); |
|||
|
|||
await FlashSalePlanRepository.UpdateAsync(flashSalePlan, autoSave: true); |
|||
|
|||
return await MapToGetOutputDtoAsync(flashSalePlan); |
|||
} |
|||
|
|||
public override async Task DeleteAsync(Guid id) |
|||
{ |
|||
var flashSalePlan = await GetEntityByIdAsync(id); |
|||
|
|||
await CheckMultiStorePolicyAsync(flashSalePlan.StoreId, DeletePolicyName); |
|||
|
|||
if (await ExistRelatedFlashSaleResultsAsync(id)) |
|||
{ |
|||
throw new RelatedFlashSaleResultsExistException(id); |
|||
} |
|||
|
|||
await FlashSalePlanRepository.DeleteAsync(flashSalePlan); |
|||
} |
|||
|
|||
protected override async Task<IQueryable<FlashSalePlan>> CreateFilteredQueryAsync(FlashSalePlanGetListInput input) |
|||
{ |
|||
return (await base.CreateFilteredQueryAsync(input)) |
|||
.WhereIf(input.StoreId.HasValue, x => x.StoreId == input.StoreId.Value) |
|||
.WhereIf(input.ProductId.HasValue, x => x.ProductId == input.ProductId.Value) |
|||
.WhereIf(input.ProductSkuId.HasValue, x => x.ProductSkuId == input.ProductSkuId.Value) |
|||
.WhereIf(!input.IncludeUnpublished, x => x.IsPublished) |
|||
.WhereIf(input.Start.HasValue, x => x.BeginTime >= input.Start.Value) |
|||
.WhereIf(input.End.HasValue, x => x.BeginTime <= input.End.Value); |
|||
} |
|||
|
|||
[Authorize] |
|||
[UnitOfWork(IsDisabled = true)] |
|||
public virtual async Task<FlashSalePlanPreOrderDto> PreOrderAsync(Guid id) |
|||
{ |
|||
await CheckPolicyAsync(PreOrderPolicyName); |
|||
|
|||
var plan = await GetFlashSalePlanCacheAsync(id); |
|||
var product = await GetProductCacheAsync(plan.ProductId); |
|||
var productSku = product.GetSkuById(plan.ProductSkuId); |
|||
var expiresTime = DateTimeOffset.Now.Add(Options.PreOrderExpires); |
|||
|
|||
await ValidatePreOrderAsync(plan, product, productSku); |
|||
|
|||
await SetPreOrderCacheAsync(plan, product, productSku, expiresTime); |
|||
|
|||
return new FlashSalePlanPreOrderDto { ExpiresTime = Clock.Normalize(expiresTime.LocalDateTime), ExpiresInSeconds = Options.PreOrderExpires.TotalSeconds }; |
|||
} |
|||
|
|||
[DisableAuditing] |
|||
[UnitOfWork(IsDisabled = true)] |
|||
[Authorize] |
|||
public virtual async Task<FlashSaleOrderResultDto> OrderAsync(Guid id, OrderFlashSalePlanInput flashSalePlanInput) |
|||
{ |
|||
var preOrderCache = await GetPreOrderCacheAsync(id); |
|||
if (preOrderCache == null) |
|||
{ |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.PreOrderExpired); |
|||
} |
|||
|
|||
var plan = await GetFlashSalePlanCacheAsync(id); |
|||
var now = Clock.Now; |
|||
if (plan.BeginTime > now) |
|||
{ |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.FlashSaleNotStarted); |
|||
} |
|||
|
|||
if (now >= plan.EndTime) |
|||
{ |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.FlashSaleIsOver); |
|||
} |
|||
|
|||
await RemovePreOrderCacheAsync(id); |
|||
|
|||
var userId = CurrentUser.GetId(); |
|||
|
|||
var lockKey = $"create-flash-sale-order-{plan.Id}-{userId}"; |
|||
|
|||
await using var handle = await DistributedLock.TryAcquireAsync(lockKey); |
|||
|
|||
if (handle == null) |
|||
{ |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.BusyToCreateFlashSaleOrder); |
|||
} |
|||
|
|||
if (await FlashSaleCurrentResultCache.GetAsync(plan.Id, CurrentUser.GetId()) is not null) |
|||
{ |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.DuplicateFlashSalesOrder); |
|||
} |
|||
|
|||
if (!await FlashSaleInventoryManager.TryReduceInventoryAsync(plan.TenantId, preOrderCache.InventoryProviderName, |
|||
plan.StoreId, plan.ProductId, plan.ProductSkuId)) |
|||
{ |
|||
await FlashSaleCurrentResultCache.RemoveAsync(plan.Id, CurrentUser.GetId()); |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.ProductSkuInventoryExceeded); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var createFlashSaleResultEto = |
|||
await PrepareCreateFlashSaleResultEtoAsync(plan, flashSalePlanInput, userId, Clock.Now, preOrderCache); |
|||
|
|||
await FlashSaleCurrentResultCache.SetAsync(plan.Id, CurrentUser.GetId(), new FlashSaleCurrentResultCacheItem |
|||
{ |
|||
TenantId = CurrentTenant.Id, |
|||
ResultDto = new FlashSaleResultDto |
|||
{ |
|||
Id = createFlashSaleResultEto.ResultId, |
|||
StoreId = createFlashSaleResultEto.Plan.StoreId, |
|||
PlanId = createFlashSaleResultEto.Plan.Id, |
|||
UserId = createFlashSaleResultEto.UserId |
|||
} |
|||
}); |
|||
|
|||
await DistributedEventBus.PublishAsync(createFlashSaleResultEto, false, false); |
|||
|
|||
return new FlashSaleOrderResultDto |
|||
{ |
|||
IsSuccess = true |
|||
}; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.LogWarning("Failed to publish the CreateFlashSaleOrderEto event!"); |
|||
Logger.LogException(e, LogLevel.Warning); |
|||
|
|||
await FlashSaleInventoryManager.TryRollBackInventoryAsync(plan.TenantId, |
|||
preOrderCache.InventoryProviderName, plan.StoreId, plan.ProductId, plan.ProductSkuId); |
|||
|
|||
await FlashSaleCurrentResultCache.RemoveAsync(plan.Id, CurrentUser.GetId()); |
|||
return CreateFailureResultDto(FlashSalesErrorCodes.DistributedEventBusUnavailable); |
|||
} |
|||
} |
|||
|
|||
protected virtual FlashSaleOrderResultDto CreateFailureResultDto(string errorCode) |
|||
{ |
|||
return new FlashSaleOrderResultDto |
|||
{ |
|||
IsSuccess = false, |
|||
ErrorCode = errorCode, |
|||
ErrorMessage = L[errorCode] |
|||
}; |
|||
} |
|||
|
|||
#region PreOrderCache
|
|||
|
|||
protected virtual async Task<FlashSalePlanCacheItem> GetFlashSalePlanCacheAsync(Guid id) |
|||
{ |
|||
return await PlanDistributedCache.GetOrAddAsync(id, async () => |
|||
{ |
|||
var flashSalePlan = await FlashSalePlanRepository.GetAsync(id); |
|||
return ObjectMapper.Map<FlashSalePlan, FlashSalePlanCacheItem>(flashSalePlan); |
|||
}); |
|||
} |
|||
|
|||
protected virtual Task<string> GetPreOrderCacheKeyAsync(Guid planId) |
|||
{ |
|||
return Task.FromResult(string.Format(PreOrderCacheKeyFormat, planId, CurrentUser.Id)); |
|||
} |
|||
|
|||
protected virtual async Task<FlashSalePlanPreOrderCacheItem> GetPreOrderCacheAsync(Guid planId) |
|||
{ |
|||
return await PreOrderDistributedCache.GetAsync(await GetPreOrderCacheKeyAsync(planId)); |
|||
} |
|||
|
|||
protected virtual async Task RemovePreOrderCacheAsync(Guid planId) |
|||
{ |
|||
await PreOrderDistributedCache.RemoveAsync(await GetPreOrderCacheKeyAsync(planId)); |
|||
} |
|||
|
|||
protected virtual async Task SetPreOrderCacheAsync(FlashSalePlanCacheItem plan, ProductDto product, ProductSkuDto productSku, DateTimeOffset expirationTime) |
|||
{ |
|||
var hashToken = await FlashSalePlanHasher.HashAsync(plan.LastModificationTime, product.LastModificationTime, productSku.LastModificationTime); |
|||
|
|||
await PreOrderDistributedCache.SetAsync(await GetPreOrderCacheKeyAsync(plan.Id), new FlashSalePlanPreOrderCacheItem() |
|||
{ |
|||
HashToken = hashToken, |
|||
PlanId = plan.Id, |
|||
ProductId = product.Id, |
|||
ProductSkuId = productSku.Id, |
|||
InventoryProviderName = product.InventoryProviderName, |
|||
}, new DistributedCacheEntryOptions() |
|||
{ |
|||
AbsoluteExpiration = expirationTime |
|||
}); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
protected virtual Task ValidatePreOrderAsync(FlashSalePlanCacheItem plan, ProductDto product, ProductSkuDto productSku) |
|||
{ |
|||
if (!product.IsPublished) |
|||
{ |
|||
throw new BusinessException(FlashSalesErrorCodes.ProductIsNotPublished); |
|||
} |
|||
|
|||
if (product.InventoryStrategy != InventoryStrategy.FlashSales) |
|||
{ |
|||
throw new UnexpectedInventoryStrategyException(InventoryStrategy.FlashSales); |
|||
} |
|||
|
|||
if (!plan.IsPublished) |
|||
{ |
|||
throw new EntityNotFoundException(typeof(FlashSalePlan), plan.Id); |
|||
} |
|||
|
|||
if (Clock.Now >= plan.EndTime) |
|||
{ |
|||
throw new BusinessException(FlashSalesErrorCodes.FlashSaleIsOver); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual async Task<bool> ExistRelatedFlashSaleResultsAsync(Guid planId) |
|||
{ |
|||
return await FlashSaleResultRepository.AnyAsync(x => x.PlanId == planId); |
|||
} |
|||
|
|||
protected virtual Task ValidateProductAsync(Guid productId, ProductDto product, Guid storeId) |
|||
{ |
|||
if (product.StoreId != storeId) |
|||
{ |
|||
throw new ProductIsNotInThisStoreException(productId, storeId); |
|||
} |
|||
|
|||
if (product.InventoryStrategy != InventoryStrategy.FlashSales) |
|||
{ |
|||
throw new UnexpectedInventoryStrategyException(InventoryStrategy.FlashSales); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual Task<CreateFlashSaleResultEto> PrepareCreateFlashSaleResultEtoAsync( |
|||
FlashSalePlanCacheItem plan, OrderFlashSalePlanInput flashSalePlanInput, Guid userId, |
|||
DateTime reducedInventoryTime, FlashSalePlanPreOrderCacheItem preOrderCacheItem) |
|||
{ |
|||
var planEto = ObjectMapper.Map<FlashSalePlanCacheItem, FlashSalePlanEto>(plan); |
|||
planEto.TenantId = CurrentTenant.Id; |
|||
|
|||
var eto = new CreateFlashSaleResultEto |
|||
{ |
|||
TenantId = CurrentTenant.Id, |
|||
UserId = userId, |
|||
ResultId = GuidGenerator.Create(), |
|||
ReducedInventoryTime = reducedInventoryTime, |
|||
CustomerRemark = flashSalePlanInput.CustomerRemark, |
|||
Plan = planEto, |
|||
ProductInventoryProviderName = preOrderCacheItem.InventoryProviderName, |
|||
HashToken = preOrderCacheItem.HashToken |
|||
}; |
|||
|
|||
if (flashSalePlanInput.ExtraProperties != null) |
|||
{ |
|||
flashSalePlanInput.MapExtraPropertiesTo(eto, MappingPropertyDefinitionChecks.Source); |
|||
} |
|||
|
|||
return Task.FromResult(eto); |
|||
} |
|||
|
|||
protected virtual async Task<ProductCacheItem> GetProductCacheAsync(Guid productId) |
|||
{ |
|||
return await ProductDistributedCache.GetOrAddAsync(productId, async () => |
|||
{ |
|||
var productDto = await ProductAppService.GetAsync(productId); |
|||
|
|||
var cacheItem = ObjectMapper.Map<ProductDto, ProductCacheItem>(productDto); |
|||
|
|||
if (cacheItem != null) |
|||
{ |
|||
cacheItem.TenantId = CurrentTenant.Id; |
|||
} |
|||
|
|||
return cacheItem; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities.Events; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSalePlanCacheInvalidator : ILocalEventHandler<EntityChangedEventData<FlashSalePlan>>, ITransientDependency |
|||
{ |
|||
protected IDistributedCache<FlashSalePlanCacheItem, Guid> DistributedCache { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public FlashSalePlanCacheInvalidator( |
|||
IDistributedCache<FlashSalePlanCacheItem, Guid> distributedCache, |
|||
IUnitOfWorkManager unitOfWorkManager) |
|||
{ |
|||
DistributedCache = distributedCache; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(EntityChangedEventData<FlashSalePlan> eventData) |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
|
|||
UnitOfWorkManager.Current?.OnCompleted(async () => |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
}); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DistributedLocking; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.ObjectExtending; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public class CreateFlashSaleResultEventHandler : IDistributedEventHandler<CreateFlashSaleResultEto>, |
|||
ITransientDependency |
|||
{ |
|||
protected IObjectMapper ObjectMapper { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
protected ILogger<CreateFlashSaleResultEventHandler> Logger { get; } |
|||
protected IAbpDistributedLock AbpDistributedLock { get; } |
|||
protected IDistributedEventBus DistributedEventBus { get; } |
|||
protected IAbpApplication AbpApplication { get; } |
|||
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; } |
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
protected IFlashSaleResultRepository FlashSaleResultRepository { get; } |
|||
|
|||
public CreateFlashSaleResultEventHandler( |
|||
IObjectMapper objectMapper, |
|||
ICurrentTenant currentTenant, |
|||
IUnitOfWorkManager unitOfWorkManager, |
|||
ILogger<CreateFlashSaleResultEventHandler> logger, |
|||
IAbpDistributedLock abpDistributedLock, |
|||
IDistributedEventBus distributedEventBus, |
|||
IAbpApplication abpApplication, |
|||
IFlashSaleInventoryManager flashSaleInventoryManager, |
|||
IFlashSaleCurrentResultCache flashSaleCurrentResultCache, |
|||
IFlashSaleResultRepository flashSaleResultRepository) |
|||
{ |
|||
ObjectMapper = objectMapper; |
|||
CurrentTenant = currentTenant; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
Logger = logger; |
|||
AbpDistributedLock = abpDistributedLock; |
|||
DistributedEventBus = distributedEventBus; |
|||
AbpApplication = abpApplication; |
|||
FlashSaleInventoryManager = flashSaleInventoryManager; |
|||
FlashSaleCurrentResultCache = flashSaleCurrentResultCache; |
|||
FlashSaleResultRepository = flashSaleResultRepository; |
|||
} |
|||
|
|||
[UnitOfWork(true)] |
|||
public virtual async Task HandleEventAsync(CreateFlashSaleResultEto eventData) |
|||
{ |
|||
await using var handle = await AbpDistributedLock.TryAcquireAsync(await GetLockKeyAsync(eventData)); |
|||
|
|||
if (handle is null) |
|||
{ |
|||
throw new AbpException("Concurrent flash sale result creation"); |
|||
} |
|||
|
|||
var ongoingResult = await FlashSaleResultRepository.FirstOrDefaultAsync(x => |
|||
x.PlanId == eventData.Plan.Id && |
|||
x.UserId == eventData.UserId && |
|||
x.Status != FlashSaleResultStatus.Failed); |
|||
|
|||
if (ongoingResult is not null) |
|||
{ |
|||
Logger.LogWarning("Duplicate ongoing flash sale result creation"); |
|||
|
|||
await FlashSaleCurrentResultCache.SetAsync(eventData.Plan.Id, eventData.UserId, |
|||
new FlashSaleCurrentResultCacheItem |
|||
{ |
|||
TenantId = ongoingResult.TenantId, |
|||
ResultDto = ObjectMapper.Map<FlashSaleResult, FlashSaleResultDto>(ongoingResult) |
|||
}); |
|||
|
|||
// try to roll back the inventory.
|
|||
UnitOfWorkManager.Current.OnCompleted(async () => |
|||
{ |
|||
using var scope = AbpApplication.ServiceProvider.CreateScope(); |
|||
|
|||
var flashSaleInventoryManager = scope.ServiceProvider.GetRequiredService<IFlashSaleInventoryManager>(); |
|||
|
|||
if (!await flashSaleInventoryManager.TryRollBackInventoryAsync(eventData.TenantId, |
|||
eventData.ProductInventoryProviderName, eventData.Plan.StoreId, |
|||
eventData.Plan.ProductId, eventData.Plan.ProductSkuId)) |
|||
{ |
|||
Logger.LogWarning("Failed to roll back the flash sale inventory."); |
|||
} |
|||
}); |
|||
|
|||
return; // avoid to create a result entity and an order.
|
|||
} |
|||
|
|||
var result = new FlashSaleResult( |
|||
id: eventData.ResultId, |
|||
tenantId: CurrentTenant.Id, |
|||
storeId: eventData.Plan.StoreId, |
|||
planId: eventData.Plan.Id, |
|||
userId: eventData.UserId, |
|||
reducedInventoryTime: eventData.ReducedInventoryTime |
|||
); |
|||
|
|||
var eto = new CreateFlashSaleOrderEto |
|||
{ |
|||
TenantId = eventData.TenantId, |
|||
ResultId = eventData.ResultId, |
|||
UserId = eventData.UserId, |
|||
CustomerRemark = eventData.CustomerRemark, |
|||
Plan = eventData.Plan, |
|||
HashToken = eventData.HashToken |
|||
}; |
|||
|
|||
eventData.MapExtraPropertiesTo(eto, MappingPropertyDefinitionChecks.None); |
|||
|
|||
await DistributedEventBus.PublishAsync(eto); |
|||
|
|||
await FlashSaleResultRepository.InsertAsync(result, autoSave: true); |
|||
} |
|||
|
|||
protected virtual Task<string> GetLockKeyAsync(CreateFlashSaleResultEto eventData) |
|||
{ |
|||
return Task.FromResult($"eshopflashsales-creating-result_{eventData.Plan.Id}-{eventData.UserId}"); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.Options; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public class FlashSaleCurrentResultCache : IFlashSaleCurrentResultCache, ITransientDependency |
|||
{ |
|||
/// <summary>
|
|||
/// The <see cref="GetKeyAsync(Guid,Guid)"/> cache key format.
|
|||
/// <para>{0}: FlashSalePlan ID</para>
|
|||
/// <para>{1}: User ID</para>
|
|||
/// </summary>
|
|||
public const string FlashSaleCurrentResultCacheKeyFormat = "eshopflashsales-current-result_{0}_{1}"; |
|||
|
|||
protected IDistributedCache<FlashSaleCurrentResultCacheItem> Cache { get; } |
|||
protected FlashSalesOptions Options { get; } |
|||
|
|||
public FlashSaleCurrentResultCache( |
|||
IDistributedCache<FlashSaleCurrentResultCacheItem> cache, |
|||
IOptions<FlashSalesOptions> options) |
|||
{ |
|||
Cache = cache; |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public virtual async Task<FlashSaleCurrentResultCacheItem> GetAsync(Guid planId, Guid userId) |
|||
{ |
|||
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId); |
|||
return await Cache.GetAsync(flashSaleCurrentResultCacheKey); |
|||
} |
|||
|
|||
public virtual async Task SetAsync(Guid planId, Guid userId, FlashSaleCurrentResultCacheItem cacheItem) |
|||
{ |
|||
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId); |
|||
|
|||
await Cache.SetAsync(flashSaleCurrentResultCacheKey, cacheItem, |
|||
new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpiration = DateTimeOffset.Now.Add(Options.FlashSaleCurrentResultCacheExpires) |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task RemoveAsync(Guid planId, Guid userId) |
|||
{ |
|||
var flashSaleCurrentResultCacheKey = await GetKeyAsync(planId, userId); |
|||
|
|||
await Cache.RemoveAsync(flashSaleCurrentResultCacheKey); |
|||
} |
|||
|
|||
protected virtual Task<string> GetKeyAsync(Guid planId, Guid userId) |
|||
{ |
|||
return Task.FromResult(string.Format(FlashSaleCurrentResultCacheKeyFormat, planId, userId)); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using EasyAbp.EShop.Plugins.FlashSales.Permissions; |
|||
using EasyAbp.EShop.Stores.Stores; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public class FlashSaleResultAppService : |
|||
MultiStoreCrudAppService<FlashSaleResult, FlashSaleResultDto, Guid, FlashSaleResultGetListInput>, |
|||
IFlashSaleResultAppService |
|||
{ |
|||
protected override string CrossStorePolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.CrossStore; |
|||
protected override string GetPolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.Default; |
|||
protected override string GetListPolicyName { get; set; } = FlashSalesPermissions.FlashSaleResult.Default; |
|||
|
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
|
|||
public FlashSaleResultAppService( |
|||
IFlashSaleCurrentResultCache flashSaleCurrentResultCache, |
|||
IFlashSaleResultRepository flashSaleResultRepository) : base(flashSaleResultRepository) |
|||
{ |
|||
FlashSaleCurrentResultCache = flashSaleCurrentResultCache; |
|||
} |
|||
|
|||
[Authorize] |
|||
public override Task<FlashSaleResultDto> GetAsync(Guid id) |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public virtual async Task<FlashSaleResultDto> GetCurrentAsync(Guid planId) |
|||
{ |
|||
await CheckGetPolicyAsync(); |
|||
|
|||
var cache = await FlashSaleCurrentResultCache.GetAsync(planId, CurrentUser.GetId()); |
|||
|
|||
if (cache is not null) |
|||
{ |
|||
return cache.ResultDto; |
|||
} |
|||
|
|||
var list = await GetListAsync(new FlashSaleResultGetListInput |
|||
{ |
|||
MaxResultCount = 1, |
|||
Sorting = "Status ASC, CreationTime DESC", |
|||
PlanId = planId, |
|||
UserId = CurrentUser.GetId() |
|||
}); |
|||
|
|||
if (list.TotalCount == 0) |
|||
{ |
|||
throw new EntityNotFoundException(typeof(FlashSaleResult)); |
|||
} |
|||
|
|||
return list.Items[0]; |
|||
} |
|||
|
|||
public override async Task<PagedResultDto<FlashSaleResultDto>> GetListAsync(FlashSaleResultGetListInput input) |
|||
{ |
|||
if (input.UserId.HasValue && input.UserId == CurrentUser.Id) |
|||
{ |
|||
await CheckGetListPolicyAsync(); |
|||
} |
|||
else |
|||
{ |
|||
await CheckMultiStorePolicyAsync(input.StoreId, GetListPolicyName); |
|||
} |
|||
|
|||
return await base.GetListAsync(input); |
|||
} |
|||
|
|||
protected override async Task<IQueryable<FlashSaleResult>> CreateFilteredQueryAsync( |
|||
FlashSaleResultGetListInput input) |
|||
{ |
|||
return (await base.CreateFilteredQueryAsync(input)) |
|||
.WhereIf(input.StoreId.HasValue, x => x.StoreId == input.StoreId.Value) |
|||
.WhereIf(input.PlanId.HasValue, x => x.PlanId == input.PlanId.Value) |
|||
.WhereIf(input.Status.HasValue, x => x.Status == input.Status.Value) |
|||
.WhereIf(input.UserId.HasValue, x => x.UserId == input.UserId.Value) |
|||
.WhereIf(input.OrderId.HasValue, x => x.OrderId == input.OrderId.Value); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public interface IFlashSaleCurrentResultCache |
|||
{ |
|||
Task<FlashSaleCurrentResultCacheItem> GetAsync(Guid planId, Guid userId); |
|||
|
|||
Task SetAsync(Guid planId, Guid userId, FlashSaleCurrentResultCacheItem cacheItem); |
|||
|
|||
Task RemoveAsync(Guid planId, Guid userId); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using AutoMapper; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans.Dtos; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults.Dtos; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales; |
|||
|
|||
public class FlashSalesApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public FlashSalesApplicationAutoMapperProfile() |
|||
{ |
|||
/* You can configure your AutoMapper mapping configuration here. |
|||
* Alternatively, you can split your mapping configurations |
|||
* into multiple profile classes for a better organization. */ |
|||
CreateMap<FlashSalePlan, FlashSalePlanDto>() |
|||
.MapExtraProperties(); |
|||
CreateMap<FlashSalePlanCreateDto, FlashSalePlan>(MemberList.Source); |
|||
CreateMap<FlashSalePlanUpdateDto, FlashSalePlan>(MemberList.Source); |
|||
CreateMap<FlashSalePlan, FlashSalePlanCacheItem>() |
|||
.MapExtraProperties(); |
|||
CreateMap<FlashSalePlanCacheItem, FlashSalePlanEto>() |
|||
.MapExtraProperties(); |
|||
|
|||
CreateMap<FlashSaleResult, FlashSaleResultDto>() |
|||
.MapExtraProperties(); |
|||
|
|||
CreateMap<ProductDto, ProductCacheItem>(MemberList.Source) |
|||
.MapExtraProperties(); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.Options; |
|||
|
|||
public class FlashSalesOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Default: 3 minutes
|
|||
/// </summary>
|
|||
public TimeSpan PreOrderExpires { get; set; } = TimeSpan.FromMinutes(3); |
|||
|
|||
/// <summary>
|
|||
/// Default: 5 minutes
|
|||
/// </summary>
|
|||
public TimeSpan FlashSaleCurrentResultCacheExpires { get; set; } = TimeSpan.FromMinutes(5); |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
[ExposeServices(typeof(IFlashSaleInventoryManager), IncludeSelf = true)] |
|||
[Dependency(TryRegister = true)] |
|||
public class FlashSaleInventoryManager : IFlashSaleInventoryManager, ITransientDependency |
|||
{ |
|||
protected IFlashSaleInventoryAppService FlashSaleInventoryReducerAppService { get; } |
|||
|
|||
public FlashSaleInventoryManager(IFlashSaleInventoryAppService flashSaleInventoryReducerAppService) |
|||
{ |
|||
FlashSaleInventoryReducerAppService = flashSaleInventoryReducerAppService; |
|||
} |
|||
|
|||
public virtual async Task<bool> TryReduceInventoryAsync(Guid? tenantId, string providerName, Guid storeId, |
|||
Guid productId, Guid productSkuId) |
|||
{ |
|||
return await FlashSaleInventoryReducerAppService.TryReduceAsync(new ReduceInventoryInput( |
|||
tenantId, providerName, storeId, productId, productSkuId, 1, true)); |
|||
} |
|||
|
|||
public virtual async Task<bool> TryRollBackInventoryAsync(Guid? tenantId, string providerName, Guid storeId, |
|||
Guid productId, Guid productSkuId) |
|||
{ |
|||
return await FlashSaleInventoryReducerAppService.TryIncreaseAsync(new IncreaseInventoryInput( |
|||
tenantId, providerName, storeId, productId, productSkuId, 1, true)); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities.Events.Distributed; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class ProductCacheInvalidator : |
|||
IDistributedEventHandler<EntityUpdatedEto<ProductEto>>, |
|||
IDistributedEventHandler<EntityDeletedEto<ProductEto>>, |
|||
ITransientDependency |
|||
{ |
|||
protected IDistributedCache<ProductCacheItem, Guid> DistributedCache { get; } |
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
public ProductCacheInvalidator( |
|||
IDistributedCache<ProductCacheItem, Guid> distributedCache, |
|||
IUnitOfWorkManager unitOfWorkManager) |
|||
{ |
|||
DistributedCache = distributedCache; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData) |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
|
|||
UnitOfWorkManager.Current?.OnCompleted(async () => |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(EntityDeletedEto<ProductEto> eventData) |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
|
|||
UnitOfWorkManager.Current?.OnCompleted(async () => |
|||
{ |
|||
await DistributedCache.RemoveAsync(eventData.Entity.Id); |
|||
}); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.domain-shared" |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Validation" Version="$(AbpVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.*" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="EasyAbp\EShop\Plugins\FlashSales\Localization\*.json" /> |
|||
<Content Remove="EasyAbp\EShop\Plugins\FlashSales\Localization\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Domain.Shared\EasyAbp.EShop.Products.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Localization; |
|||
using EasyAbp.EShop.Plugins.FlashSales.Localization; |
|||
using Volo.Abp.Localization.ExceptionHandling; |
|||
using Volo.Abp.Validation; |
|||
using Volo.Abp.Validation.Localization; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpValidationModule) |
|||
)] |
|||
public class EShopPluginsFlashSalesDomainSharedModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<EShopPluginsFlashSalesDomainSharedModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<FlashSalesResource>("en") |
|||
.AddBaseTypes(typeof(AbpValidationResource)) |
|||
.AddVirtualJson("EasyAbp/EShop/Plugins/FlashSales/Localization"); |
|||
}); |
|||
|
|||
Configure<AbpExceptionLocalizationOptions>(options => |
|||
{ |
|||
options.MapCodeNamespace(FlashSalesErrorCodes.Namespace, typeof(FlashSalesResource)); |
|||
}); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
[Serializable] |
|||
public class FlashSalePlanEto : FullAuditedEntityEto<Guid>, IMultiTenant |
|||
{ |
|||
public Guid? TenantId { get; set; } |
|||
|
|||
public Guid StoreId { get; set; } |
|||
|
|||
public DateTime BeginTime { get; set; } |
|||
|
|||
public DateTime EndTime { get; set; } |
|||
|
|||
public Guid ProductId { get; set; } |
|||
|
|||
public Guid ProductSkuId { get; set; } |
|||
|
|||
public bool IsPublished { get; set; } |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSalePlanHasher : IFlashSalePlanHasher, ITransientDependency |
|||
{ |
|||
public virtual Task<string> HashAsync(DateTime? planLastModificationTime, DateTime? productLastModificationTime, DateTime? productSkuLastModificationTime) |
|||
{ |
|||
var input = $"{planLastModificationTime?.Ticks}|{productLastModificationTime?.Ticks}|{productSkuLastModificationTime?.Ticks}"; |
|||
|
|||
return Task.FromResult(CreateMd5(input)); |
|||
} |
|||
|
|||
private static string CreateMd5(string input) |
|||
{ |
|||
using var md5 = MD5.Create(); |
|||
|
|||
var inputBytes = Encoding.UTF8.GetBytes(input); |
|||
|
|||
var sb = new StringBuilder(); |
|||
foreach (var t in md5.ComputeHash(inputBytes)) |
|||
{ |
|||
sb.Append(t.ToString("X2")); |
|||
} |
|||
return sb.ToString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public interface IFlashSalePlanHasher |
|||
{ |
|||
Task<string> HashAsync(DateTime? planLastModificationTime, DateTime? productLastModificationTime, DateTime? productSkuLastModificationTime); |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public static class FlashSaleResultFailedReason |
|||
{ |
|||
public const string InvalidHashToken = "InvalidHashToken"; |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
|
|||
public enum FlashSaleResultStatus |
|||
{ |
|||
Pending = 0, |
|||
Successful = 1, |
|||
Failed = 2 |
|||
} |
|||
@ -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)}"; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales; |
|||
|
|||
public class FullAuditedEntityEto<TPrimaryKey> : ExtensibleObject |
|||
{ |
|||
public TPrimaryKey Id { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
|
|||
public Guid? CreatorId { get; set; } |
|||
|
|||
public DateTime? LastModificationTime { get; set; } |
|||
|
|||
public Guid? LastModifierId { get; set; } |
|||
|
|||
public bool IsDeleted { get; set; } |
|||
|
|||
public Guid? DeleterId { get; set; } |
|||
|
|||
public DateTime? DeletionTime { get; set; } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.Localization; |
|||
|
|||
[LocalizationResourceName("EasyAbpEShopPluginsFlashSales")] |
|||
public class FlashSalesResource |
|||
{ |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue