Browse Source

Merge branch 'dev' into booking-plugin

pull/185/head
gdlcf88 4 years ago
parent
commit
86ad3d0af7
  1. 1
      Directory.Build.props
  2. 1
      EShop.sln.DotSettings
  3. 4
      docs/README.md
  4. 4
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/IOrderExtraFeeProvider.cs
  5. 5
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/IOrderLinePriceOverrider.cs
  6. 73
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
  7. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
  8. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
  9. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
  10. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp.EShop.Orders.Domain.csproj
  11. 21
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Settings/OrdersSettingDefinitionProvider.cs
  12. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Settings/OrdersSettings.cs
  13. 14
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Pages/EShop/Orders/Orders/Order/index.css
  14. 19
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Pages/EShop/Orders/Orders/Order/index.js
  15. 71
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  16. 7
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/TestOrderLinePriceOverrider.cs
  17. 4
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
  18. 10
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  19. 4
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Payments/PaymentAppServiceTests.cs
  20. 12
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs
  21. 2
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/RefundOrderEventHandlerTests.cs
  22. 92
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  23. 13
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductsApplicationAutoMapperProfile.cs
  24. 1
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp.EShop.Products.Domain.csproj
  25. 47
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/Product.cs
  26. 57
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductSku.cs
  27. 6
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/ProductAppServiceTests.cs
  28. 30
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/CurrencyTests.cs
  29. 4
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/ProductDomainTests.cs
  30. 22
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/ProductsTestDataBuilder.cs
  31. 31
      samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/en.json
  32. 43
      samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/zh-Hans.json
  33. 43
      samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/zh-Hant.json
  34. 7
      samples/EShopSample/aspnet-core/src/EShopSample.Domain/Data/SampleDataConsts.cs
  35. 181
      samples/EShopSample/aspnet-core/src/EShopSample.Domain/Data/SampleDataSeedContributor.cs
  36. 2
      samples/EShopSample/aspnet-core/src/EShopSample.Domain/EShopSampleDomainModule.cs
  37. 34
      samples/EShopSample/aspnet-core/src/EShopSample.Web/MyMenuViewModelProvider.cs
  38. 115
      samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.cshtml
  39. 137
      samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.cshtml.cs
  40. 53
      samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.css
  41. 128
      samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.js
  42. 299
      samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/SkuSelector.js

1
Directory.Build.props

@ -7,6 +7,7 @@
<EasyAbpAbpTagHelperPlusModuleVersion>1.0.0</EasyAbpAbpTagHelperPlusModuleVersion>
<DaprSdkVersion>1.7.0</DaprSdkVersion>
<OrleansVersion>3.6.2</OrleansVersion>
<NodaMoneyVersion>1.0.5</NodaMoneyVersion>
</PropertyGroup>
</Project>

1
EShop.sln.DotSettings

@ -22,4 +22,5 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Authorizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dapr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=noda/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Skus/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

4
docs/README.md

@ -12,6 +12,10 @@ An abp application module group that provides basic e-shop service.
We have launched an online demo for this module: [https://eshop.samples.easyabp.io](https://eshop.samples.easyabp.io)
![image](https://user-images.githubusercontent.com/30018771/173756329-fe16a753-dddf-4b97-a5e5-a2f4c9d9983f.png)
> You can also clone this repo and run the [demo project](https://github.com/EasyAbp/EShop/tree/dev/samples/EShopSample/aspnet-core/src/EShopSample.Web) locally.
## Installation
1. Follow [the document](https://github.com/EasyAbp/PaymentService#installation) to install the dependent PaymentService module.

4
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/IOrderExtraFeeProvider.cs

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Products.Products.Dtos;
using NodaMoney;
namespace EasyAbp.EShop.Orders.Orders
{
public interface IOrderExtraFeeProvider
{
Task<List<OrderExtraFeeInfoModel>> GetListAsync(Guid customerUserId, CreateOrderDto input, Dictionary<Guid, ProductDto> productDict);
Task<List<OrderExtraFeeInfoModel>> GetListAsync(Guid customerUserId, CreateOrderDto input,
Dictionary<Guid, ProductDto> productDict, Currency effectiveCurrency);
}
}

5
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Application/EasyAbp/EShop/Orders/Orders/IOrderLinePriceOverrider.cs

@ -1,11 +1,12 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Products.Products.Dtos;
using NodaMoney;
namespace EasyAbp.EShop.Orders.Orders;
public interface IOrderLinePriceOverrider
{
Task<decimal?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine, ProductDto product,
ProductSkuDto productSku);
Task<Money?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine, ProductDto product,
ProductSkuDto productSku, Currency effectiveCurrency);
}

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

@ -3,14 +3,18 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Orders.Settings;
using EasyAbp.EShop.Products.ProductDetails.Dtos;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using Microsoft.Extensions.DependencyInjection;
using NodaMoney;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
namespace EasyAbp.EShop.Orders.Orders
@ -20,6 +24,7 @@ namespace EasyAbp.EShop.Orders.Orders
private readonly IClock _clock;
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentTenant _currentTenant;
private readonly ISettingProvider _settingProvider;
private readonly IServiceProvider _serviceProvider;
private readonly IOrderNumberGenerator _orderNumberGenerator;
private readonly IProductSkuDescriptionProvider _productSkuDescriptionProvider;
@ -29,6 +34,7 @@ namespace EasyAbp.EShop.Orders.Orders
IClock clock,
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
ISettingProvider settingProvider,
IServiceProvider serviceProvider,
IOrderNumberGenerator orderNumberGenerator,
IProductSkuDescriptionProvider productSkuDescriptionProvider,
@ -37,6 +43,7 @@ namespace EasyAbp.EShop.Orders.Orders
_clock = clock;
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
_settingProvider = settingProvider;
_serviceProvider = serviceProvider;
_orderNumberGenerator = orderNumberGenerator;
_productSkuDescriptionProvider = productSkuDescriptionProvider;
@ -46,23 +53,20 @@ namespace EasyAbp.EShop.Orders.Orders
public virtual async Task<Order> GenerateAsync(Guid customerUserId, CreateOrderDto input,
Dictionary<Guid, ProductDto> productDict, Dictionary<Guid, ProductDetailDto> productDetailDict)
{
var effectiveCurrency = await GetEffectiveCurrencyAsync();
var orderLines = new List<OrderLine>();
foreach (var inputOrderLine in input.OrderLines)
{
orderLines.Add(await GenerateOrderLineAsync(input, inputOrderLine, productDict, productDetailDict));
}
var storeCurrency = await GetStoreCurrencyAsync(input.StoreId);
if (orderLines.Any(x => x.Currency != storeCurrency))
{
throw new UnexpectedCurrencyException(storeCurrency);
orderLines.Add(await GenerateOrderLineAsync(
input, inputOrderLine, productDict, productDetailDict, effectiveCurrency));
}
var productTotalPrice = orderLines.Select(x => x.TotalPrice).Sum();
var paymentExpireIn = orderLines.Select(x => productDict[x.ProductId].GetSkuPaymentExpireIn(x.ProductSkuId)).Min();
var paymentExpireIn = orderLines.Select(x => productDict[x.ProductId].GetSkuPaymentExpireIn(x.ProductSkuId))
.Min();
var totalPrice = productTotalPrice;
var totalDiscount = orderLines.Select(x => x.TotalDiscount).Sum();
@ -72,7 +76,7 @@ namespace EasyAbp.EShop.Orders.Orders
tenantId: _currentTenant.Id,
storeId: input.StoreId,
customerUserId: customerUserId,
currency: storeCurrency,
currency: effectiveCurrency.Code,
productTotalPrice: productTotalPrice,
totalDiscount: totalDiscount,
totalPrice: totalPrice,
@ -83,7 +87,7 @@ namespace EasyAbp.EShop.Orders.Orders
input.MapExtraPropertiesTo(order, MappingPropertyDefinitionChecks.Destination);
await AddOrderExtraFeesAsync(order, customerUserId, input, productDict);
await AddOrderExtraFeesAsync(order, customerUserId, input, productDict, effectiveCurrency);
order.SetOrderLines(orderLines);
@ -93,27 +97,33 @@ namespace EasyAbp.EShop.Orders.Orders
}
protected virtual async Task AddOrderExtraFeesAsync(Order order, Guid customerUserId,
CreateOrderDto input, Dictionary<Guid, ProductDto> productDict)
CreateOrderDto input, Dictionary<Guid, ProductDto> productDict, Currency effectiveCurrency)
{
var providers = _serviceProvider.GetServices<IOrderExtraFeeProvider>();
foreach (var provider in providers)
{
var infoModels = await provider.GetListAsync(customerUserId, input, productDict);
var infoModels = await provider.GetListAsync(customerUserId, input, productDict, effectiveCurrency);
foreach (var infoModel in infoModels)
{
order.AddOrderExtraFee(infoModel.Fee, infoModel.Name, infoModel.Key);
var fee = new Money(infoModel.Fee, effectiveCurrency);
order.AddOrderExtraFee(fee.Amount, infoModel.Name, infoModel.Key);
}
}
}
protected virtual async Task<OrderLine> GenerateOrderLineAsync(CreateOrderDto input,
CreateOrderLineDto inputOrderLine, Dictionary<Guid, ProductDto> productDict,
Dictionary<Guid, ProductDetailDto> productDetailDict)
Dictionary<Guid, ProductDetailDto> productDetailDict, Currency effectiveCurrency)
{
var product = productDict[inputOrderLine.ProductId];
var productSku = product.GetSkuById(inputOrderLine.ProductSkuId);
if (productSku.Currency != effectiveCurrency.Code)
{
throw new UnexpectedCurrencyException(effectiveCurrency.Code);
}
var productDetailId = productSku.ProductDetailId ?? product.ProductDetailId;
var productDetail = productDetailId.HasValue ? productDetailDict[productDetailId.Value] : null;
@ -123,8 +133,8 @@ namespace EasyAbp.EShop.Orders.Orders
throw new OrderLineInvalidQuantityException(product.Id, productSku.Id, inputOrderLine.Quantity);
}
var unitPrice = await GetUnitPriceAsync(input, inputOrderLine, product, productSku);
var unitPrice = await GetUnitPriceAsync(input, inputOrderLine, product, productSku, effectiveCurrency);
var totalPrice = unitPrice * inputOrderLine.Quantity;
var orderLine = new OrderLine(
@ -142,25 +152,26 @@ namespace EasyAbp.EShop.Orders.Orders
skuDescription: await _productSkuDescriptionProvider.GenerateAsync(product, productSku),
mediaResources: product.MediaResources,
currency: productSku.Currency,
unitPrice: unitPrice,
totalPrice: totalPrice,
unitPrice: unitPrice.Amount,
totalPrice: totalPrice.Amount,
totalDiscount: 0,
actualTotalPrice: totalPrice,
actualTotalPrice: totalPrice.Amount,
quantity: inputOrderLine.Quantity
);
inputOrderLine.MapExtraPropertiesTo(orderLine, MappingPropertyDefinitionChecks.Destination);
return orderLine;
}
protected virtual async Task<decimal> GetUnitPriceAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku)
protected virtual async Task<Money> GetUnitPriceAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku, Currency effectiveCurrency)
{
foreach (var overrider in _orderLinePriceOverriders)
{
var overridenUnitPrice =
await overrider.GetUnitPriceOrNullAsync(input, inputOrderLine, product, productSku);
await overrider.GetUnitPriceOrNullAsync(input, inputOrderLine, product, productSku,
effectiveCurrency);
if (overridenUnitPrice is not null)
{
@ -168,13 +179,17 @@ namespace EasyAbp.EShop.Orders.Orders
}
}
return productSku.Price;
return new Money(productSku.Price, effectiveCurrency);
}
protected virtual Task<string> GetStoreCurrencyAsync(Guid storeId)
protected virtual async Task<Currency> GetEffectiveCurrencyAsync()
{
// Todo: Get real store currency configuration.
return Task.FromResult("CNY");
var currencyCode = Check.NotNullOrWhiteSpace(
await _settingProvider.GetOrNullAsync(OrdersSettings.CurrencyCode),
nameof(OrdersSettings.CurrencyCode)
);
return Currency.FromCode(currencyCode);
}
}
}

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

@ -9,6 +9,10 @@
"OrderOrderNumber": "Order number",
"OrderCustomerUserId": "Customer user ID",
"OrderOrderStatus": "Status",
"OrderOrderStatusPending": "Pending",
"OrderOrderStatusProcessing": "Processing",
"OrderOrderStatusCompleted": "Completed",
"OrderOrderStatusCanceled": "Canceled",
"OrderCurrency": "Currency",
"OrderProductTotalPrice": "Product total price",
"OrderTotalDiscount": "Total discount",
@ -49,6 +53,8 @@
"EasyAbp.EShop.Orders:InvalidRefundAmount": "The refund amount ({amount}) is invalid.",
"EasyAbp.EShop.Orders:InvalidRefundQuantity": "The refund quantity ({quantity}) is invalid.",
"EasyAbp.EShop.Orders:OrderIsInWrongStage": "The order {orderId} is in the wrong stage.",
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "Exist unexpected flash-sales product"
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "Exist unexpected flash-sales product",
"DisplayName:EasyAbp.EShop.Orders.CurrencyCode": "Currency code",
"Description:EasyAbp.EShop.Orders.CurrencyCode": "ISO 4217 code (see https://en.wikipedia.org/wiki/ISO_4217)"
}
}

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

@ -9,6 +9,10 @@
"OrderOrderNumber": "订单号",
"OrderCustomerUserId": "客户 ID",
"OrderOrderStatus": "状态",
"OrderOrderStatusPending": "待支付",
"OrderOrderStatusProcessing": "进行中",
"OrderOrderStatusCompleted": "已完成",
"OrderOrderStatusCanceled": "已取消",
"OrderCurrency": "币种",
"OrderProductTotalPrice": "商品总价",
"OrderTotalDiscount": "总折扣",
@ -49,6 +53,8 @@
"EasyAbp.EShop.Orders:InvalidRefundAmount": "退款金额({amount})无效",
"EasyAbp.EShop.Orders:InvalidRefundQuantity": "退款数量({quantity})无效",
"EasyAbp.EShop.Orders:OrderIsInWrongStage": "订单{orderId}处于错误的阶段",
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "清单中不允许存在闪购产品"
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "清单中不允许存在闪购产品",
"DisplayName:EasyAbp.EShop.Orders.CurrencyCode": "货币代码",
"Description:EasyAbp.EShop.Orders.CurrencyCode": "ISO 4217 货币代码 (详见 https://en.wikipedia.org/wiki/ISO_4217)"
}
}

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

@ -9,6 +9,10 @@
"OrderOrderNumber": "訂單號",
"OrderCustomerUserId": "客戶 ID",
"OrderOrderStatus": "狀態",
"OrderOrderStatusPending": "待支付",
"OrderOrderStatusProcessing": "進行中",
"OrderOrderStatusCompleted": "已完成",
"OrderOrderStatusCanceled": "已取消",
"OrderCurrency": "幣種",
"OrderProductTotalPrice": "商品總價",
"OrderTotalDiscount": "總折扣",
@ -49,6 +53,8 @@
"EasyAbp.EShop.Orders:InvalidRefundAmount": "退款金額({amount})無效",
"EasyAbp.EShop.Orders:InvalidRefundQuantity": "退款數量({quantity})無效",
"EasyAbp.EShop.Orders:OrderIsInWrongStage": "訂單{orderId}處於錯誤的階段",
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "清單中不允許存在閃購產品"
"EasyAbp.EShop.Orders:ExistFlashSalesProduct": "清單中不允許存在閃購產品",
"DisplayName:EasyAbp.EShop.Orders.CurrencyCode": "貨幣代碼",
"Description:EasyAbp.EShop.Orders.CurrencyCode": "ISO 4217 貨幣代碼 (詳見 https://en.wikipedia.org/wiki/ISO_4217)"
}
}

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

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NodaMoney" Version="$(NodaMoneyVersion)" />
<PackageReference Include="Volo.Abp.AutoMapper" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.BackgroundJobs.Abstractions" Version="$(AbpVersion)" />

21
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Settings/OrdersSettingDefinitionProvider.cs

@ -1,14 +1,33 @@
using Volo.Abp.Settings;
using EasyAbp.EShop.Orders.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Settings;
namespace EasyAbp.EShop.Orders.Settings
{
public class OrdersSettingDefinitionProvider : SettingDefinitionProvider
{
public static string DefaultCurrency { get; set; } = "USD";
public override void Define(ISettingDefinitionContext context)
{
/* Define module settings here.
* Use names from OrdersSettings class.
*/
context.Add(
new SettingDefinition(
OrdersSettings.CurrencyCode,
DefaultCurrency,
L($"DisplayName:{OrdersSettings.CurrencyCode}"),
L($"Description:{OrdersSettings.CurrencyCode}"),
isVisibleToClients: true
)
);
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<OrdersResource>(name);
}
}
}

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Settings/OrdersSettings.cs

@ -7,5 +7,7 @@
/* Add constants for setting names. Example:
* public const string MySettingName = GroupName + ".MySettingName";
*/
public const string CurrencyCode = GroupName + ".CurrencyCode";
}
}

14
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Pages/EShop/Orders/Orders/Order/index.css

@ -0,0 +1,14 @@
.status-pending-text {
color: orange;
}
.status-processing-text {
color: green;
}
.status-completed-text {
color: blue;
}
.status-canceled-text {
}

19
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Pages/EShop/Orders/Orders/Order/index.js

@ -31,7 +31,24 @@ $(function () {
},
{ data: "orderNumber" },
{ data: "customerUserId" },
{ data: "orderStatus" },
{
data: "orderStatus",
render: function (data, type, row) {
if (data === 1) {
return '<span class="status-pending-text">' + l('OrderOrderStatusPending') + '</span>'
}
if (data === 2) {
return '<span class="status-processing-text">' + l('OrderOrderStatusProcessing') + '</span>'
}
if (data === 4) {
return '<span class="status-completed-text">' + l('OrderOrderStatusCompleted') + '</span>'
}
if (data === 8) {
return '<span class="status-canceled-text">' + l('OrderOrderStatusCanceled') + '</span>'
}
return ''
}
},
{ data: "currency" },
{ data: "actualTotalPrice" },
]

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Orders.Settings;
using EasyAbp.EShop.Products.ProductDetails;
using EasyAbp.EShop.Products.ProductDetails.Dtos;
using EasyAbp.EShop.Products.Products;
@ -12,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Xunit;
@ -53,7 +55,7 @@ namespace EasyAbp.EShop.Orders.Orders
OrderMaxQuantity = 100,
AttributeOptionIds = new List<Guid>(),
Price = 1m,
Currency = "CNY",
Currency = "USD",
ProductDetailId = null
},
new ProductSkuDto
@ -64,7 +66,7 @@ namespace EasyAbp.EShop.Orders.Orders
OrderMaxQuantity = 100,
AttributeOptionIds = new List<Guid>(),
Price = 2m,
Currency = "CNY",
Currency = "USD",
ProductDetailId = OrderTestData.ProductDetail2Id
},
new ProductSkuDto
@ -75,7 +77,7 @@ namespace EasyAbp.EShop.Orders.Orders
OrderMaxQuantity = 100,
AttributeOptionIds = new List<Guid>(),
Price = 3m,
Currency = "CNY",
Currency = "USD",
ProductDetailId = OrderTestData.ProductDetail2Id
}
},
@ -151,7 +153,7 @@ namespace EasyAbp.EShop.Orders.Orders
context.Orders.Count().ShouldBe(1);
var order = context.Orders.Include(x => x.OrderLines).First();
order.ShouldNotBeNull();
order.Currency.ShouldBe("CNY");
order.Currency.ShouldBe("USD");
order.CanceledTime.ShouldBeNull();
order.CancellationReason.ShouldBeNullOrEmpty();
order.CompletionTime.ShouldBeNull();
@ -185,7 +187,7 @@ namespace EasyAbp.EShop.Orders.Orders
orderLine1.TotalPrice.ShouldBe(10m);
orderLine1.TotalDiscount.ShouldBe(0m);
orderLine1.ActualTotalPrice.ShouldBe(10m);
orderLine1.Currency.ShouldBe("CNY");
orderLine1.Currency.ShouldBe("USD");
orderLine1.Quantity.ShouldBe(10);
orderLine1.ProductModificationTime.ShouldBe(OrderTestData.ProductLastModificationTime);
orderLine1.ProductDetailModificationTime.ShouldBe(OrderTestData.ProductDetailLastModificationTime);
@ -532,17 +534,62 @@ namespace EasyAbp.EShop.Orders.Orders
}
};
Product1.InventoryStrategy = InventoryStrategy.FlashSales;
try
{
Product1.InventoryStrategy = InventoryStrategy.FlashSales;
await WithUnitOfWorkAsync(async () =>
await WithUnitOfWorkAsync(async () =>
{
var exception =
await Should.ThrowAsync<BusinessException>(() => _orderAppService.CreateAsync(createOrderDto));
exception.Code.ShouldBe(OrdersErrorCodes.ExistFlashSalesProduct);
});
Product1.InventoryStrategy = InventoryStrategy.NoNeed;
}
catch
{
var exception =
await Should.ThrowAsync<BusinessException>(() => _orderAppService.CreateAsync(createOrderDto));
Product1.InventoryStrategy = InventoryStrategy.NoNeed;
throw;
}
}
exception.Code.ShouldBe(OrdersErrorCodes.ExistFlashSalesProduct);
});
[Fact]
public async Task Should_Throw_If_Product_Sku_Uses_Unexpected_Currency()
{
var createOrderDto = new CreateOrderDto
{
StoreId = OrderTestData.Store1Id,
OrderLines = new List<CreateOrderLineDto>
{
new()
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku1Id,
Quantity = 1
}
}
};
try
{
OrdersSettingDefinitionProvider.DefaultCurrency = "CNY"; // The effective value is "USD"
await WithUnitOfWorkAsync(async () =>
{
var exception =
await Should.ThrowAsync<BusinessException>(() => _orderAppService.CreateAsync(createOrderDto));
Product1.InventoryStrategy = InventoryStrategy.NoNeed;
exception.Code.ShouldBe(OrdersErrorCodes.UnexpectedCurrency);
});
OrdersSettingDefinitionProvider.DefaultCurrency = "USD";
}
catch
{
OrdersSettingDefinitionProvider.DefaultCurrency = "USD";
throw;
}
}
}
}

7
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/TestOrderLinePriceOverrider.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Products.Products.Dtos;
using NodaMoney;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Orders.Orders;
@ -9,12 +10,12 @@ public class TestOrderLinePriceOverrider : IOrderLinePriceOverrider, ITransientD
{
public static decimal Sku3UnitPrice { get; set; } = 100m;
public async Task<decimal?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku)
public async Task<Money?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku, Currency effectiveCurrency)
{
if (inputOrderLine.ProductSkuId == OrderTestData.ProductSku3Id)
{
return Sku3UnitPrice;
return new Money(Sku3UnitPrice, effectiveCurrency);
}
return null;

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

@ -20,7 +20,7 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
null,
OrderTestData.Store1Id,
Guid.NewGuid(),
"CNY",
"USD",
1m,
0m,
1.5m,
@ -41,7 +41,7 @@ public class InventoryReductionResultTests : OrdersDomainTestBase
null,
null,
null,
"CNY",
"USD",
0.5m,
1m,
0m,

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

@ -28,7 +28,7 @@ namespace EasyAbp.EShop.Orders.Orders
null,
OrderTestData.Store1Id,
Guid.NewGuid(),
"CNY",
"USD",
1m,
0m,
1.5m,
@ -49,7 +49,7 @@ namespace EasyAbp.EShop.Orders.Orders
null,
null,
null,
"CNY",
"USD",
0.5m,
1m,
0m,
@ -82,7 +82,7 @@ namespace EasyAbp.EShop.Orders.Orders
Id = Guid.NewGuid(),
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "CNY",
Currency = "USD",
RefundAmount = 0.3m,
RefundItems = new List<EShopRefundItemEto>
{
@ -140,7 +140,7 @@ namespace EasyAbp.EShop.Orders.Orders
Id = Guid.NewGuid(),
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "CNY",
Currency = "USD",
RefundAmount = -1m,
RefundItems = new List<EShopRefundItemEto>
{
@ -181,7 +181,7 @@ namespace EasyAbp.EShop.Orders.Orders
Id = Guid.NewGuid(),
TenantId = null,
PaymentId = OrderTestData.Payment1Id,
Currency = "CNY",
Currency = "USD",
RefundAmount = 0.3m,
RefundItems = new List<EShopRefundItemEto>
{

4
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Payments/PaymentAppServiceTests.cs

@ -27,7 +27,7 @@ namespace EasyAbp.EShop.Payments.Payments
orderService.GetAsync(PaymentsTestData.Order1).Returns(Task.FromResult(new OrderDto
{
Id = PaymentsTestData.Order1,
Currency = "CNY",
Currency = "USD",
ActualTotalPrice = 0,
StoreId = PaymentsTestData.Store1,
OrderLines = new List<OrderLineDto>
@ -35,7 +35,7 @@ namespace EasyAbp.EShop.Payments.Payments
new OrderLineDto
{
Id = PaymentsTestData.OrderLine1,
Currency = "CNY",
Currency = "USD",
ActualTotalPrice = 0,
Quantity = 1
}

12
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs

@ -53,7 +53,7 @@ namespace EasyAbp.EShop.Payments.Refunds
var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull();
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1);
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "CNY");
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD");
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m);
paymentType.GetProperty(nameof(Payment.PaymentItems))?.SetValue(payment, new List<PaymentItem> {paymentItem});
@ -83,7 +83,7 @@ namespace EasyAbp.EShop.Payments.Refunds
var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull();
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1);
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "CNY");
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD");
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m);
// pending refund amount
paymentType.GetProperty(nameof(Payment.PendingRefundAmount))?.SetValue(payment, 1m);
@ -108,7 +108,7 @@ namespace EasyAbp.EShop.Payments.Refunds
orderService.GetAsync(PaymentsTestData.Order1).Returns(Task.FromResult(new OrderDto
{
Id = PaymentsTestData.Order1,
Currency = "CNY",
Currency = "USD",
ActualTotalPrice = 0,
StoreId = PaymentsTestData.Store1,
OrderLines = new List<OrderLineDto>
@ -116,7 +116,7 @@ namespace EasyAbp.EShop.Payments.Refunds
new()
{
Id = PaymentsTestData.OrderLine1,
Currency = "CNY",
Currency = "USD",
ActualTotalPrice = 1m,
Quantity = 1
}
@ -476,7 +476,7 @@ namespace EasyAbp.EShop.Payments.Refunds
PaymentId = PaymentsTestData.Payment1,
RefundPaymentMethod = null,
ExternalTradingCode = "testcode",
Currency = "CNY",
Currency = "USD",
RefundAmount = 1.5m,
DisplayReason = "DisplayReason",
CustomerRemark = "CustomerRemark",
@ -490,7 +490,7 @@ namespace EasyAbp.EShop.Payments.Refunds
refundDto.PaymentId.ShouldBe(PaymentsTestData.Payment1);
refundDto.ExternalTradingCode.ShouldBe("testcode");
refundDto.Currency.ShouldBe("CNY");
refundDto.Currency.ShouldBe("USD");
refundDto.RefundAmount.ShouldBe(1.5m);
refundDto.DisplayReason.ShouldBe("DisplayReason");
refundDto.CustomerRemark.ShouldBe("CustomerRemark");

2
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/RefundOrderEventHandlerTests.cs

@ -49,7 +49,7 @@ public class RefundOrderEventHandlerTests : PaymentsDomainTestBase
var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull();
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1);
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "CNY");
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "USD");
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m);
paymentType.GetProperty(nameof(Payment.PaymentItems))
?.SetValue(payment, new List<PaymentItem> { paymentItem });

92
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs

@ -70,13 +70,6 @@ namespace EasyAbp.EShop.Products.Products
.ThenBy(x => x.Id);
}
protected override Product MapToEntity(CreateUpdateProductDto createInput)
{
var product = base.MapToEntity(createInput);
return product;
}
public override async Task<ProductDto> CreateAsync(CreateUpdateProductDto input)
{
var product = await MapToEntityAsync(input);
@ -356,7 +349,7 @@ namespace EasyAbp.EShop.Products.Products
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
}
private static void CheckProductIsNotStatic(Product product)
protected virtual void CheckProductIsNotStatic(Product product)
{
if (product.IsStatic)
{
@ -372,7 +365,7 @@ namespace EasyAbp.EShop.Products.Products
CheckProductIsNotStatic(product);
var sku = ObjectMapper.Map<CreateProductSkuDto, ProductSku>(input);
var sku = await MapToProductSkuAsync(input);
EntityHelper.TrySetId(sku, GuidGenerator.Create);
@ -388,7 +381,8 @@ namespace EasyAbp.EShop.Products.Products
return dto;
}
public async Task<ProductDto> UpdateSkuAsync(Guid productId, Guid productSkuId, UpdateProductSkuDto input)
public virtual async Task<ProductDto> UpdateSkuAsync(Guid productId, Guid productSkuId,
UpdateProductSkuDto input)
{
var product = await GetEntityByIdAsync(productId);
@ -398,7 +392,7 @@ namespace EasyAbp.EShop.Products.Products
var sku = product.ProductSkus.Single(x => x.Id == productSkuId);
ObjectMapper.Map(input, sku);
await MapToProductSkuAsync(input, sku);
await _productManager.UpdateSkuAsync(product, sku);
@ -412,7 +406,7 @@ namespace EasyAbp.EShop.Products.Products
return dto;
}
public async Task<ProductDto> DeleteSkuAsync(Guid productId, Guid productSkuId)
public virtual async Task<ProductDto> DeleteSkuAsync(Guid productId, Guid productSkuId)
{
var product = await GetEntityByIdAsync(productId);
@ -485,6 +479,80 @@ namespace EasyAbp.EShop.Products.Products
return SortAttributesAndOptions(productDto);
}
protected override Task<Product> MapToEntityAsync(CreateUpdateProductDto createInput)
{
return Task.FromResult(new Product(
GuidGenerator.Create(),
CurrentTenant.Id,
createInput.StoreId,
createInput.ProductGroupName,
createInput.ProductDetailId,
createInput.UniqueName,
createInput.DisplayName,
createInput.InventoryStrategy,
createInput.InventoryProviderName,
createInput.IsPublished,
false,
createInput.IsHidden,
createInput.PaymentExpireIn,
createInput.MediaResources,
createInput.DisplayOrder));
}
protected override Task MapToEntityAsync(CreateUpdateProductDto updateInput, Product entity)
{
entity.Update(
updateInput.StoreId,
updateInput.ProductGroupName,
updateInput.ProductDetailId,
updateInput.UniqueName,
updateInput.DisplayName,
updateInput.InventoryStrategy,
updateInput.InventoryProviderName,
updateInput.IsPublished,
false,
updateInput.IsHidden,
updateInput.PaymentExpireIn,
updateInput.MediaResources,
updateInput.DisplayOrder);
return Task.CompletedTask;
}
protected virtual async Task<ProductSku> MapToProductSkuAsync(CreateProductSkuDto createInput)
{
return new ProductSku(
GuidGenerator.Create(),
await _attributeOptionIdsSerializer.SerializeAsync(createInput.AttributeOptionIds),
createInput.Name,
createInput.Currency,
createInput.OriginalPrice,
createInput.Price,
createInput.OrderMinQuantity,
createInput.OrderMaxQuantity,
createInput.PaymentExpireIn,
createInput.MediaResources,
createInput.ProductDetailId
);
}
protected virtual Task MapToProductSkuAsync(UpdateProductSkuDto updateInput, ProductSku entity)
{
entity.Update(
updateInput.Name,
updateInput.Currency,
updateInput.OriginalPrice,
updateInput.Price,
updateInput.OrderMinQuantity,
updateInput.OrderMaxQuantity,
updateInput.PaymentExpireIn,
updateInput.MediaResources,
updateInput.ProductDetailId
);
return Task.CompletedTask;
}
protected virtual ProductDto SortAttributesAndOptions(ProductDto productDto)
{
productDto.ProductAttributes = productDto.ProductAttributes.OrderBy(x => x.DisplayOrder).ToList();

13
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductsApplicationAutoMapperProfile.cs

@ -41,23 +41,10 @@ namespace EasyAbp.EShop.Products
.Ignore(dto => dto.Sold)
.AfterMap(async (src, dest) => dest.AttributeOptionIds =
(await attributeOptionIdsSerializer.DeserializeAsync(src.SerializedAttributeOptionIds)).ToList());
CreateMap<CreateUpdateProductDto, Product>(MemberList.Source)
.ForSourceMember(dto => dto.StoreId, opt => opt.DoNotValidate())
.ForSourceMember(dto => dto.CategoryIds, opt => opt.DoNotValidate())
.Ignore(p => p.ProductAttributes)
.Ignore(p => p.ProductSkus)
.AfterMap((src, dest) => dest.InitializeNullCollections());
CreateMap<CreateUpdateProductDetailDto, ProductDetail>(MemberList.Source)
.ForSourceMember(dto => dto.StoreId, opt => opt.DoNotValidate());
CreateMap<CreateUpdateProductAttributeDto, ProductAttribute>(MemberList.Source);
CreateMap<CreateUpdateProductAttributeOptionDto, ProductAttributeOption>(MemberList.Source);
CreateMap<CreateProductSkuDto, ProductSku>(MemberList.Source)
.ForSourceMember(dto => dto.AttributeOptionIds, opt => opt.DoNotValidate())
.Ignore(entity => entity.SerializedAttributeOptionIds)
.AfterMap(async (src, dest) =>
dest.SetSerializedAttributeOptionIds(
await attributeOptionIdsSerializer.SerializeAsync(src.AttributeOptionIds)));
CreateMap<UpdateProductSkuDto, ProductSku>(MemberList.Source);
CreateMap<Category, CategoryDto>();
CreateMap<Category, CategorySummaryDto>();
CreateMap<ProductCategory, ProductCategoryDto>();

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

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

47
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/Product.cs

@ -2,6 +2,7 @@ using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using EasyAbp.EShop.Products.Options.ProductGroups;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
@ -13,13 +14,16 @@ namespace EasyAbp.EShop.Products.Products
public virtual Guid StoreId { get; protected set; }
[NotNull] public virtual string ProductGroupName { get; protected set; }
[NotNull]
public virtual string ProductGroupName { get; protected set; }
public virtual Guid? ProductDetailId { get; protected set; }
[CanBeNull] public virtual string UniqueName { get; protected set; }
[CanBeNull]
public virtual string UniqueName { get; protected set; }
[NotNull] public virtual string DisplayName { get; protected set; }
[NotNull]
public virtual string DisplayName { get; protected set; }
public virtual InventoryStrategy InventoryStrategy { get; protected set; }
@ -29,7 +33,8 @@ namespace EasyAbp.EShop.Products.Products
/// </summary>
public virtual string InventoryProviderName { get; protected set; }
[CanBeNull] public virtual string MediaResources { get; protected set; }
[CanBeNull]
public virtual string MediaResources { get; protected set; }
public virtual int DisplayOrder { get; protected set; }
@ -69,10 +74,10 @@ namespace EasyAbp.EShop.Products.Products
{
TenantId = tenantId;
StoreId = storeId;
ProductGroupName = productGroupName;
ProductGroupName = Check.NotNullOrWhiteSpace(productGroupName, nameof(productGroupName));
ProductDetailId = productDetailId;
UniqueName = uniqueName?.Trim();
DisplayName = displayName;
DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName));
InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished;
@ -86,10 +91,34 @@ namespace EasyAbp.EShop.Products.Products
ProductSkus = new List<ProductSku>();
}
public void InitializeNullCollections()
public void Update(
Guid storeId,
[NotNull] string productGroupName,
Guid? productDetailId,
[CanBeNull] string uniqueName,
[NotNull] string displayName,
InventoryStrategy inventoryStrategy,
[CanBeNull] string inventoryProviderName,
bool isPublished,
bool isStatic,
bool isHidden,
TimeSpan? paymentExpireIn,
[CanBeNull] string mediaResources,
int displayOrder)
{
ProductAttributes ??= new List<ProductAttribute>();
ProductSkus ??= new List<ProductSku>();
StoreId = storeId;
ProductGroupName = Check.NotNullOrWhiteSpace(productGroupName, nameof(productGroupName));
ProductDetailId = productDetailId;
UniqueName = uniqueName?.Trim();
DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName));
InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished;
IsStatic = isStatic;
IsHidden = isHidden;
PaymentExpireIn = paymentExpireIn;
MediaResources = mediaResources;
DisplayOrder = displayOrder;
}
public void TrimUniqueName()

57
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductSku.cs

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using NodaMoney;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities.Auditing;
@ -11,28 +12,28 @@ namespace EasyAbp.EShop.Products.Products
{
[NotNull]
public virtual string SerializedAttributeOptionIds { get; protected set; }
[CanBeNull]
public virtual string Name { get; protected set; }
[NotNull]
public virtual string Currency { get; protected set; }
public virtual decimal? OriginalPrice { get; protected set; }
public virtual decimal Price { get; protected set; }
public virtual int OrderMinQuantity { get; protected set; }
public virtual int OrderMaxQuantity { get; protected set; }
public virtual TimeSpan? PaymentExpireIn { get; protected set; }
[CanBeNull]
public virtual string MediaResources { get; protected set; }
public virtual Guid? ProductDetailId { get; protected set; }
[JsonInclude]
public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }
@ -41,7 +42,7 @@ namespace EasyAbp.EShop.Products.Products
ExtraProperties = new ExtraPropertyDictionary();
this.SetDefaultsForExtraProperties();
}
public ProductSku(
Guid id,
[NotNull] string serializedAttributeOptionIds,
@ -55,17 +56,21 @@ namespace EasyAbp.EShop.Products.Products
[CanBeNull] string mediaResources,
Guid? productDetailId) : base(id)
{
SerializedAttributeOptionIds = serializedAttributeOptionIds;
Check.NotNullOrWhiteSpace(currency, nameof(currency));
var nodaCurrency = NodaMoney.Currency.FromCode(currency);
SerializedAttributeOptionIds =
Check.NotNullOrWhiteSpace(serializedAttributeOptionIds, nameof(serializedAttributeOptionIds));
Name = name?.Trim();
Currency = currency;
OriginalPrice = originalPrice;
Price = price;
Currency = nodaCurrency.Code;
OriginalPrice = originalPrice.HasValue ? new Money(originalPrice.Value, nodaCurrency).Amount : null;
Price = new Money(price, nodaCurrency).Amount;
OrderMinQuantity = orderMinQuantity;
OrderMaxQuantity = orderMaxQuantity;
PaymentExpireIn = paymentExpireIn;
MediaResources = mediaResources;
ProductDetailId = productDetailId;
ExtraProperties = new ExtraPropertyDictionary();
this.SetDefaultsForExtraProperties();
}
@ -75,9 +80,29 @@ namespace EasyAbp.EShop.Products.Products
Name = Name?.Trim();
}
public void SetSerializedAttributeOptionIds(string serializedAttributeOptionIds)
public void Update(
[CanBeNull] string name,
[NotNull] string currency,
decimal? originalPrice,
decimal price,
int orderMinQuantity,
int orderMaxQuantity,
TimeSpan? paymentExpireIn,
[CanBeNull] string mediaResources,
Guid? productDetailId)
{
SerializedAttributeOptionIds = serializedAttributeOptionIds;
Check.NotNullOrWhiteSpace(currency, nameof(currency));
var nodaCurrency = NodaMoney.Currency.FromCode(currency);
Name = name?.Trim();
Currency = nodaCurrency.Code;
OriginalPrice = originalPrice.HasValue ? new Money(originalPrice.Value, nodaCurrency).Amount : null;
Price = new Money(price, nodaCurrency).Amount;
OrderMinQuantity = orderMinQuantity;
OrderMaxQuantity = orderMaxQuantity;
PaymentExpireIn = paymentExpireIn;
MediaResources = mediaResources;
ProductDetailId = productDetailId;
}
}
}

6
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/ProductAppServiceTests.cs

@ -105,7 +105,7 @@ namespace EasyAbp.EShop.Products.Products
var response = await _productAppService.CreateSkuAsync(productId, new CreateProductSkuDto
{
AttributeOptionIds = new List<Guid> {productAttributeOptionId},
Currency = "CNY",
Currency = "USD",
Price = 1m,
OrderMinQuantity = 1,
OrderMaxQuantity = 10
@ -117,7 +117,7 @@ namespace EasyAbp.EShop.Products.Products
response.ProductSkus.Count.ShouldBe(1);
var responseSku = response.ProductSkus.First();
responseSku.Currency.ShouldBe("CNY");
responseSku.Currency.ShouldBe("USD");
responseSku.Price.ShouldBe(1m);
responseSku.AttributeOptionIds.Count.ShouldBe(1);
responseSku.AttributeOptionIds.First().ShouldBe(productAttributeOptionId);
@ -202,7 +202,7 @@ namespace EasyAbp.EShop.Products.Products
AttributeOptionIds = new List<Guid>
{ ProductsTestData.Product1Attribute1Option4Id, ProductsTestData.Product1Attribute2Option1Id },
ProductDetailId = wrongProductDetailId,
Currency = "CNY",
Currency = "USD",
Price = 10m,
OrderMinQuantity = 1,
OrderMaxQuantity = 10

30
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/CurrencyTests.cs

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using NodaMoney;
using Shouldly;
using Xunit;
namespace EasyAbp.EShop.Products.Products;
public class CurrencyTests : ProductsDomainTestBase
{
[Fact]
public Task Should_Rounding_If_Amount_Is_Out_Of_Decimals()
{
var money = new Money(1.115m, Currency.FromCode("CNY"));
money.Currency.ShouldBe(Currency.FromCode("CNY"));
money.Amount.ShouldBe(1.12m);
return Task.CompletedTask;
}
[Fact]
public Task Should_Throw_If_Currency_Is_Undefined()
{
var exception = Should.Throw<ArgumentException>(() => new Money(1.115m, Currency.FromCode("BTC")));
exception.Message.ShouldBe("BTC is an unknown currency code!");
return Task.CompletedTask;
}
}

4
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/ProductDomainTests.cs

@ -186,7 +186,7 @@ namespace EasyAbp.EShop.Products.Products
await ProductManager.CreateAsync(product2);
var fakeSku = new ProductSku(Guid.NewGuid(), "", null, "", null, 0m, 1, 1, null, null, null);
var fakeSku = new ProductSku(Guid.NewGuid(), "[]", null, "USD", null, 0m, 1, 1, null, null, null);
var inventoryDataModel = await ProductManager.GetInventoryDataAsync(product2, fakeSku);
@ -197,7 +197,7 @@ namespace EasyAbp.EShop.Products.Products
private async Task<ProductSku> CreateTestSkuAsync(IEnumerable<Guid> attributeOptionIds)
{
return new ProductSku(Guid.NewGuid(), await AttributeOptionIdsSerializer.SerializeAsync(attributeOptionIds),
"test-sku", "CNY", null, 0m, 1, 10, null, null, null);
"test-sku", "USD", null, 0m, 1, 10, null, null, null);
}
}
}

22
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/ProductsTestDataBuilder.cs

@ -47,21 +47,21 @@ namespace EasyAbp.EShop.Products
var product = new Product(ProductsTestData.Product1Id, null, ProductsTestData.Store1Id, "Default",
productDetail1.Id, "Cake", "Cake", InventoryStrategy.NoNeed, null, true, false, false, null, null, 0);
var attribute1 = new ProductAttribute(ProductsTestData.Product1Attribute1Id, "Size", null, 2);
var attribute2 = new ProductAttribute(ProductsTestData.Product1Attribute2Id, "Color", null, 1);
var attribute1 = new ProductAttribute(ProductsTestData.Product1Attribute1Id, "Size", null, 1);
var attribute2 = new ProductAttribute(ProductsTestData.Product1Attribute2Id, "Color", null, 2);
attribute1.ProductAttributeOptions.AddRange(new[]
{
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option4Id, "XL", null, 1),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option2Id, "M", null, 3),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option1Id, "S", null, 4),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option3Id, "L", null, 2),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option4Id, "XL", null, 4),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option2Id, "M", null, 2),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option1Id, "S", null, 1),
new ProductAttributeOption(ProductsTestData.Product1Attribute1Option3Id, "L", null, 3),
});
attribute2.ProductAttributeOptions.AddRange(new[]
{
new ProductAttributeOption(ProductsTestData.Product1Attribute2Option2Id, "Green", null, 1),
new ProductAttributeOption(ProductsTestData.Product1Attribute2Option1Id, "Red", null, 2),
new ProductAttributeOption(ProductsTestData.Product1Attribute2Option2Id, "Green", null, 2),
new ProductAttributeOption(ProductsTestData.Product1Attribute2Option1Id, "Red", null, 1),
});
product.ProductAttributes.Add(attribute2);
@ -72,17 +72,17 @@ namespace EasyAbp.EShop.Products
var productSku1 = new ProductSku(ProductsTestData.Product1Sku1Id,
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ ProductsTestData.Product1Attribute1Option1Id, ProductsTestData.Product1Attribute2Option1Id }),
null, "CNY", null, 1m, 1, 10, null, null, null);
null, "USD", null, 1m, 1, 10, null, null, null);
var productSku2 = new ProductSku(ProductsTestData.Product1Sku2Id,
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ ProductsTestData.Product1Attribute1Option2Id, ProductsTestData.Product1Attribute2Option1Id }),
null, "CNY", null, 2m, 1, 10, null, null, null);
null, "USD", null, 2m, 1, 10, null, null, null);
var productSku3 = new ProductSku(ProductsTestData.Product1Sku3Id,
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ ProductsTestData.Product1Attribute1Option3Id, ProductsTestData.Product1Attribute2Option2Id }),
null, "CNY", null, 3m, 1, 10, null, null, null);
null, "USD", null, 3m, 1, 10, null, null, null);
await _productManager.CreateSkuAsync(product, productSku1);
await _productManager.CreateSkuAsync(product, productSku2);

31
samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/en.json

@ -4,6 +4,35 @@
"Menu:EasyAbpEShop": "EShop",
"Menu:Home": "Home",
"Welcome": "Welcome",
"LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io."
"LongWelcomeMessage": "Welcome to the application. This is a demo project of the EShop module. For more information, visit easyabp.io.",
"Product": "Product",
"Cake": "Cake",
"OrderCake": "Order a cake",
"CreateOrder": "Create order",
"PayForOrder": "Pay for order",
"TopUp-1-USD": "Top-up 1.00 USD",
"OrderHistory": "Order history",
"Flavor": "Flavor",
"Size": "Size",
"Chocolate": "Chocolate",
"Vanilla": "Vanilla",
"YourChoice": "Your choice",
"SkuId": "SKU ID",
"OrderCreated": "Order created",
"OrderCanceled": "Order canceled",
"TopUpSucceeded": "Top-up succeeded",
"PaymentSucceeded": "Payment succeeded",
"InsufficientBalance": "Insufficient account balance",
"OrderNumber": "Order number",
"Created": "Created",
"OrderStatus": "Status",
"OrderStatusPending": "Pending",
"OrderStatusProcessing": "Processing",
"OrderStatusCompleted": "Completed",
"OrderStatusCanceled": "Canceled",
"TotalPrice": "Price",
"AccountBalance": "Account balance",
"TimeToAutoCancel": "Order auto-cancel in",
"CancelOrder": "Cancel order"
}
}

43
samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/zh-Hans.json

@ -1,9 +1,38 @@
{
"culture": "zh-Hans",
"texts": {
"culture": "zh-Hans",
"texts": {
"Menu:EasyAbpEShop": "EShop 商城",
"Menu:Home": "首页",
"Welcome": "欢迎",
"LongWelcomeMessage": "欢迎来到该应用程序. 这是一个基于ABP框架的启动项目. 有关更多信息, 请访问 abp.io."
}
}
"Menu:Home": "首页",
"Welcome": "欢迎",
"LongWelcomeMessage": "欢迎来到该应用程序. 这是一个 EShop 模块的示例项目. 有关更多信息, 请访问 easyabp.io.",
"Product": "商品",
"Cake": "蛋糕",
"OrderCake": "下单买蛋糕",
"CreateOrder": "创建订单",
"PayForOrder": "支付订单",
"TopUp-1-USD": "充值 1.00 美元",
"OrderHistory": "历史订单",
"Flavor": "口味",
"Size": "分量",
"Chocolate": "巧克力味",
"Vanilla": "香草味",
"YourChoice": "你的选择",
"SkuId": "SKU ID",
"OrderCreated": "订单创建成功",
"OrderCanceled": "订单已取消",
"TopUpSucceeded": "充值成功",
"PaymentSucceeded": "支付成功",
"InsufficientBalance": "账户余额不足",
"OrderNumber": "订单编号",
"Created": "创建时间",
"OrderStatus": "状态",
"OrderStatusPending": "待支付",
"OrderStatusProcessing": "进行中",
"OrderStatusCompleted": "已完成",
"OrderStatusCanceled": "已取消",
"TotalPrice": "价格",
"AccountBalance": "账户余额",
"TimeToAutoCancel": "剩余可支付时间",
"CancelOrder": "取消订单"
}
}

43
samples/EShopSample/aspnet-core/src/EShopSample.Domain.Shared/Localization/EShopSample/zh-Hant.json

@ -1,9 +1,38 @@
{
"culture": "zh-Hant",
"texts": {
"culture": "zh-Hant",
"texts": {
"Menu:EasyAbpEShop": "EShop 商城",
"Menu:Home": "首頁",
"Welcome": "歡迎",
"LongWelcomeMessage": "歡迎來到此應用程式. 這是一個基於ABP框架的起始專案. 有關更多訊息, 請瀏覽 abp.io."
}
}
"Menu:Home": "首頁",
"Welcome": "歡迎",
"LongWelcomeMessage": "歡迎來到此應用程式. 這是一個 EShop 模組的示例專案. 有關更多訊息, 請瀏覽 easyabp.io.",
"Product": "商品",
"Cake": "蛋糕",
"OrderCake": "下單買蛋糕",
"CreateOrder": "創建訂單",
"PayForOrder": "支付訂單",
"TopUp-1-USD": "充值 1.00 美元",
"OrderHistory": "歷史訂單",
"Flavor": "口味",
"Size": "分量",
"Chocolate": "巧克力味",
"Vanilla": "香草味",
"YourChoice": "你的選擇",
"SkuId": "SKU ID",
"OrderCreated": "訂單創建成功",
"OrderCanceled": "訂單已取消",
"TopUpSucceeded": "充值成功",
"PaymentSucceeded": "支付成功",
"InsufficientBalance": "賬戶餘額不足",
"OrderNumber": "訂單編號",
"Created": "創建時間",
"OrderStatus": "狀態",
"OrderStatusPending": "待支付",
"OrderStatusProcessing": "進行中",
"OrderStatusCompleted": "已完成",
"OrderStatusCanceled": "已取消",
"TotalPrice": "價格",
"AccountBalance": "賬戶餘額",
"TimeToAutoCancel": "剩餘可支付時間",
"CancelOrder": "取消訂單"
}
}

7
samples/EShopSample/aspnet-core/src/EShopSample.Domain/Data/SampleDataConsts.cs

@ -0,0 +1,7 @@
namespace EShopSample.Data;
public static class SampleDataConsts
{
public const string FoodsCategoryUniqueName = "Foods";
public const string CakeProductUniqueName = "Cake";
}

181
samples/EShopSample/aspnet-core/src/EShopSample.Domain/Data/SampleDataSeedContributor.cs

@ -0,0 +1,181 @@
using System;
using System.Threading.Tasks;
using EasyAbp.EShop.Products;
using EasyAbp.EShop.Products.Categories;
using EasyAbp.EShop.Products.ProductCategories;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Stores.Settings;
using EasyAbp.EShop.Stores.Stores;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
namespace EShopSample.Data;
public class SampleDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentTenant _currentTenant;
private readonly IStoreRepository _storeRepository;
private readonly IProductManager _productManager;
private readonly IProductRepository _productRepository;
private readonly ICategoryManager _categoryManager;
private readonly ICategoryRepository _categoryRepository;
private readonly IProductCategoryRepository _productCategoryRepository;
private readonly ISettingProvider _settingProvider;
private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer;
public SampleDataSeedContributor(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IStoreRepository storeRepository,
IProductManager productManager,
IProductRepository productRepository,
ICategoryManager categoryManager,
ICategoryRepository categoryRepository,
IProductCategoryRepository productCategoryRepository,
ISettingProvider settingProvider,
IAttributeOptionIdsSerializer attributeOptionIdsSerializer)
{
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
_storeRepository = storeRepository;
_productManager = productManager;
_productRepository = productRepository;
_categoryManager = categoryManager;
_categoryRepository = categoryRepository;
_productCategoryRepository = productCategoryRepository;
_settingProvider = settingProvider;
_attributeOptionIdsSerializer = attributeOptionIdsSerializer;
}
[UnitOfWork(true)]
public virtual async Task SeedAsync(DataSeedContext context)
{
using var changeTenant = _currentTenant.Change(context.TenantId);
await SeedStoresAsync();
await SeedCategoriesAsync();
await SeedProductsAsync();
await SeedProductCategoryMappingsAsync();
}
protected virtual async Task SeedProductCategoryMappingsAsync()
{
var product = await _productRepository.GetAsync(
x => x.UniqueName == SampleDataConsts.CakeProductUniqueName);
var category = await _categoryRepository.GetAsync(
x => x.UniqueName == SampleDataConsts.FoodsCategoryUniqueName);
if (await _productCategoryRepository.AnyAsync(x => x.ProductId == product.Id && x.CategoryId == category.Id))
{
return;
}
await _productCategoryRepository.InsertAsync(
new ProductCategory(_guidGenerator.Create(), _currentTenant.Id, category.Id, product.Id));
}
public virtual async Task SeedStoresAsync()
{
var storeName = await _settingProvider.GetOrNullAsync(StoresSettings.DefaultStoreName);
if (await _storeRepository.AnyAsync(x => x.Name == storeName))
{
return;
}
await _storeRepository.InsertAsync(new Store(_guidGenerator.Create(), _currentTenant.Id, storeName), true);
}
public virtual async Task SeedCategoriesAsync()
{
if (await _categoryRepository.AnyAsync(x => x.UniqueName == SampleDataConsts.FoodsCategoryUniqueName))
{
return;
}
var category = await _categoryManager.CreateAsync(null, SampleDataConsts.FoodsCategoryUniqueName, "Foods",
"Some delicious foods.", null, false);
await _categoryRepository.InsertAsync(category, true);
}
public virtual async Task SeedProductsAsync()
{
if (await _productRepository.AnyAsync(x => x.UniqueName == SampleDataConsts.CakeProductUniqueName))
{
return;
}
var defaultStore = await _storeRepository.FindDefaultStoreAsync();
var product = new Product(
_guidGenerator.Create(),
_currentTenant.Id,
defaultStore.Id,
ProductsConsts.DefaultProductGroupName,
null,
SampleDataConsts.CakeProductUniqueName,
"Cake",
InventoryStrategy.NoNeed,
null,
true,
false,
false,
TimeSpan.FromMinutes(15),
null,
0);
var attribute1 = new ProductAttribute(_guidGenerator.Create(), "Size", null, 2);
var attribute2 = new ProductAttribute(_guidGenerator.Create(), "Flavor", null, 1);
attribute1.ProductAttributeOptions.AddRange(new[]
{
new ProductAttributeOption(_guidGenerator.Create(), "S", null, 1),
new ProductAttributeOption(_guidGenerator.Create(), "M", null, 2),
new ProductAttributeOption(_guidGenerator.Create(), "L", null, 3),
});
attribute2.ProductAttributeOptions.AddRange(new[]
{
new ProductAttributeOption(_guidGenerator.Create(), "Chocolate", null, 1),
new ProductAttributeOption(_guidGenerator.Create(), "Vanilla", null, 2),
});
product.ProductAttributes.Add(attribute1);
product.ProductAttributes.Add(attribute2);
await _productManager.CreateAsync(product);
var productSku1 = new ProductSku(_guidGenerator.Create(),
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ attribute1.ProductAttributeOptions[0].Id, attribute2.ProductAttributeOptions[0].Id }),
null, "USD", null, 1m, 1, 10, null, null, null);
var productSku2 = new ProductSku(_guidGenerator.Create(),
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ attribute1.ProductAttributeOptions[1].Id, attribute2.ProductAttributeOptions[0].Id }),
null, "USD", null, 2m, 1, 10, null, null, null);
var productSku3 = new ProductSku(_guidGenerator.Create(),
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ attribute1.ProductAttributeOptions[1].Id, attribute2.ProductAttributeOptions[1].Id }),
null, "USD", null, 3m, 1, 10, null, null, null);
var productSku4 = new ProductSku(_guidGenerator.Create(),
await _attributeOptionIdsSerializer.SerializeAsync(new[]
{ attribute1.ProductAttributeOptions[2].Id, attribute2.ProductAttributeOptions[1].Id }),
null, "USD", null, 4m, 1, 10, null, null, null);
await _productManager.CreateSkuAsync(product, productSku1);
await _productManager.CreateSkuAsync(product, productSku2);
await _productManager.CreateSkuAsync(product, productSku3);
await _productManager.CreateSkuAsync(product, productSku4);
}
}

2
samples/EShopSample/aspnet-core/src/EShopSample.Domain/EShopSampleDomainModule.cs

@ -80,7 +80,7 @@ namespace EShopSample
{
options.AccountGroups.Configure<DefaultAccountGroup>(accountGroup =>
{
accountGroup.Currency = "CNY";
accountGroup.Currency = "USD";
});
});
}

34
samples/EShopSample/aspnet-core/src/EShopSample.Web/MyMenuViewModelProvider.cs

@ -0,0 +1,34 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Layout;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite.Themes.LeptonXLite.Components.Menu;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ObjectMapping;
using Volo.Abp.UI.Navigation;
namespace EShopSample.Web;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(MenuViewModelProvider))]
public class MyMenuViewModelProvider : MenuViewModelProvider
{
public MyMenuViewModelProvider(IMenuManager menuManager, IPageLayout pageLayout,
IObjectMapper<AbpAspNetCoreMvcUiLeptonXLiteThemeModule> objectMapper) : base(menuManager, pageLayout,
objectMapper)
{
}
protected override bool SetActiveMenuItems(IList<MenuItemViewModel> items, string activeMenuItemName)
{
foreach (var item in items)
{
if (SetActiveMenuItems(item.Items, activeMenuItemName) || item.MenuItem.Name == activeMenuItemName)
{
item.IsActive = true;
return true;
}
}
return false;
}
}

115
samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.cshtml

@ -1,16 +1,33 @@
@page
@inherits EShopSample.Web.Pages.EShopSamplePage
@using EasyAbp.EShop.Orders.Orders
@model EShopSample.Web.Pages.IndexModel
@section styles {
<abp-style-bundle>
<abp-style src="/Pages/Index.css" />
<abp-style src="/Pages/Index.css"/>
</abp-style-bundle>
}
@section scripts {
<abp-script-bundle>
<abp-script src="/Pages/Index.js" />
<abp-script src="/Pages/Index.js"/>
<abp-script src="/Pages/SkuSelector.js"/>
</abp-script-bundle>
}
<script>
let storeId = '@Model.Store?.Id'
let orderId = '@Model.Order?.Id'
let cakeProductId = '@Model.CakeProduct?.Id'
let cakeProductSkuId = null
let accountId = '@Model.Wallet?.Id'
let currentBalance = @(Model.Wallet?.Balance ?? 0m)
let totalPrice = @(Model.Order?.ActualTotalPrice ?? 0m)
let skuInfo = '@(Model.CakeProduct is not null ? Html.Raw(Model.GetJsonSkuInfo()) : "[]")'
let secondsToAutoCancel = @Model.GetSecondsToAutoCancel()
</script>
<div class="jumbotron text-center">
<h1>@L["Welcome"]</h1>
<div class="row">
@ -19,9 +36,99 @@
<hr class="my-4"/>
</div>
</div>
<a href="https://abp.io?ref=tmpl" target="_blank" class="btn btn-primary px-4">abp.io</a>
@if (!CurrentUser.IsAuthenticated)
{
<a abp-button="Primary" href="/Account/Login" class="px-4"><i class="fa fa-sign-in"></i> @L["Login"]</a>
<a abp-button="Primary" href="/Account/Login" class="px-4">
<i class="fa fa-sign-in"></i> @L["Login"]
</a>
}
else
{
<div class="row">
<div class="col-md-6 mx-auto">
<abp-tabs>
<abp-tab name="OrderCakeTab" title="@L["OrderCake"].Value" active="@(Model.Order is null)">
<h1>@L[Model.CakeProduct.DisplayName]</h1>
<hr>
<div id="skuSelector"></div>
<hr>
<div id="selectedSku" class="my-1"></div>
<div id="selectedSkuId" class="my-1"></div>
<div id="selectedSkuPrice" class="my-1"></div>
<hr>
<abp-button id="CreateOrderButton" button-type="Primary">@L["CreateOrder"]</abp-button>
</abp-tab>
<abp-tab name="PayForOrderTab" title="@L["PayForOrder"].Value" active="@(Model.Order?.OrderStatus is OrderStatus.Pending)">
@if (Model.Order is not null)
{
<abp-table hoverable-rows="true" responsive-sm="true">
<tr>
<td>@L["OrderNumber"]</td>
<td>@Model.Order.OrderNumber</td>
</tr>
<tr>
<td>@L["Product"]</td>
<td>@L[Model.CakeProduct.DisplayName]</td>
</tr>
@foreach (var optionId in Model.CakeProduct.ProductSkus.First(x => x.Id == Model.Order.OrderLines[0].ProductSkuId).AttributeOptionIds)
{
var option = Model.CakeProduct.ProductAttributes.SelectMany(x => x.ProductAttributeOptions)
.Single(x => x.Id == optionId);
var attr = Model.CakeProduct.ProductAttributes.Single(x => x.ProductAttributeOptions.Contains(option));
<tr>
<td>@L[attr.DisplayName]</td>
<td>@L[option.DisplayName]</td>
</tr>
}
<tr>
<td>@L["TotalPrice"]</td>
<td>@Model.Order.ActualTotalPrice.ToString("F2") @Model.Order.Currency</td>
</tr>
<tr>
<td>@L["AccountBalance"]</td>
<td>@Model.Wallet.Balance.ToString("F2") @Model.Order.Currency</td>
</tr>
</abp-table>
<div class="row">
<div class="col-md-6 mx-auto">
<p>
@L["TimeToAutoCancel"] <span id="timeToAutoCancel">?</span>
</p>
</div>
</div>
<abp-button id="CancelOrderButton" button-type="Info">@L["CancelOrder"]</abp-button>
<abp-button id="TopUpButton" button-type="Secondary">@L["TopUp-1-USD"]</abp-button>
<abp-button id="PayForOrderButton" button-type="Primary">@L["PayForOrder"]</abp-button>
}
</abp-tab>
<abp-tab name="OrderHistoryTab" title="@L["OrderHistory"].Value">
<abp-table hoverable-rows="true" responsive-sm="true">
<thead>
<tr>
<th scope="Column">#</th>
<th scope="Column">@L["OrderNumber"]</th>
<th scope="Column">@L["Created"]</th>
<th scope="Column">@L["OrderStatus"]</th>
</tr>
</thead>
<tbody>
@{
var index = Model.OrderList.TotalCount;
foreach (var order in Model.OrderList.Items)
{
<tr>
<th scope="Row">@(index--)</th>
<td>@order.OrderNumber</td>
<td>@order.CreationTime</td>
<td><span class="@Model.GetOrderStatusColumnClass(order)">@L[$"OrderStatus{order.OrderStatus}"]</span></td>
</tr>
}
}
</tbody>
</abp-table>
</abp-tab>
</abp-tabs>
</div>
</div>
}
</div>

137
samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.cshtml.cs

@ -1,10 +1,141 @@
namespace EShopSample.Web.Pages
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders;
using EasyAbp.EShop.Orders.Orders.Dtos;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using EasyAbp.EShop.Stores.Stores;
using EasyAbp.EShop.Stores.Stores.Dtos;
using EasyAbp.PaymentService.Prepayment.Accounts;
using EasyAbp.PaymentService.Prepayment.Accounts.Dtos;
using EShopSample.Data;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Users;
namespace EShopSample.Web.Pages;
public class IndexModel : EShopSamplePageModel
{
public class IndexModel : EShopSamplePageModel
private readonly IOrderAppService _orderAppService;
private readonly IStoreAppService _storeAppService;
private readonly IProductAppService _productAppService;
private readonly IAccountAppService _accountAppService;
public OrderDto Order { get; set; }
public PagedResultDto<OrderDto> OrderList { get; set; }
public StoreDto Store { get; set; }
public ProductDto CakeProduct { get; set; }
public AccountDto Wallet { get; set; }
public IndexModel(
IOrderAppService orderAppService,
IStoreAppService storeAppService,
IProductAppService productAppService,
IAccountAppService accountAppService)
{
public void OnGet()
_orderAppService = orderAppService;
_storeAppService = storeAppService;
_productAppService = productAppService;
_accountAppService = accountAppService;
}
public async Task OnGetAsync()
{
if (CurrentUser.Id is null)
{
return;
}
OrderList = await _orderAppService.GetListAsync(new GetOrderListDto
{
MaxResultCount = 5,
CustomerUserId = CurrentUser.GetId(),
Sorting = "CreationTime DESC"
});
Order = OrderList.Items.FirstOrDefault(x => x.OrderStatus is OrderStatus.Pending);
Store = await _storeAppService.GetDefaultAsync();
CakeProduct = await _productAppService.GetByUniqueNameAsync(Store.Id, SampleDataConsts.CakeProductUniqueName);
Wallet = (await _accountAppService.GetListAsync(new GetAccountListInput { UserId = CurrentUser.Id })).Items[0];
}
public string GetJsonSkuInfo()
{
var sb = new StringBuilder("[");
foreach (var sku in CakeProduct.ProductSkus)
{
sb.Append('{');
sb.Append("\"skus\":");
sb.Append('{');
foreach (var optionId in sku.AttributeOptionIds)
{
var option = CakeProduct.ProductAttributes.SelectMany(x => x.ProductAttributeOptions)
.Single(x => x.Id == optionId);
var attribute = CakeProduct.ProductAttributes.Single(x => x.ProductAttributeOptions.Contains(option));
sb.Append($"\"{L[attribute.DisplayName].Value}\":\"{L[option.DisplayName].Value}\"");
if (optionId != sku.AttributeOptionIds.Last())
{
sb.Append(',');
}
}
sb.Append('}');
sb.Append($",\"skuId\":\"{sku.Id}\"");
sb.Append($",\"skuPrice\":{sku.Price}");
sb.Append($",\"skuCurrency\":\"{sku.Currency}\"");
sb.Append('}');
if (sku != CakeProduct.ProductSkus.Last())
{
sb.Append(',');
}
}
sb.Append(']');
return sb.ToString();
}
public int GetSecondsToAutoCancel()
{
if (Order is null)
{
return 0;
}
return Order.PaymentExpiration.HasValue
? Convert.ToInt32((Order.PaymentExpiration.Value - Clock.Now).TotalSeconds)
: 0;
}
public string GetOrderStatusColumnClass(OrderDto order)
{
if (order is null)
{
return string.Empty;
}
return order.OrderStatus switch
{
OrderStatus.Pending => "status-pending-text",
OrderStatus.Processing => "status-processing-text",
OrderStatus.Completed => "status-completed-text",
OrderStatus.Canceled => "status-canceled-text",
_ => throw new ArgumentOutOfRangeException()
};
}
}

53
samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.css

@ -1,3 +1,52 @@
body {
#skuSelector dt {
width: 100px;
text-align: right;
font-weight: normal;
padding-top: 6px;
}
#skuSelector dl {
clear: both;
overflow: hidden;
}
#skuSelector dl.hl {
background: #ddd;
}
#skuSelector dt, #skuSelector dd {
float: left;
line-height: 18px;
margin-left: 10px;
margin-bottom: 0;
}
#skuSelector button {
font-size: 14px;
font-weight: bold;
padding: 4px 4px;
}
#skuSelector .disabled {
color: #999;
border: 1px dashed #666;
}
#skuSelector .active {
color: red;
}
.status-pending-text {
color: orange;
}
.status-processing-text {
color: green;
}
.status-completed-text {
color: blue;
}
.status-canceled-text {
}

128
samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/Index.js

@ -1,3 +1,129 @@
$(function () {
abp.log.debug('Index.js initialized!');
const l = abp.localization.getResource('EasyAbpEShopEShopSample');
const orderService = easyAbp.eShop.orders.orders.order;
const eShopPaymentService = easyAbp.eShop.payments.payments.payment;
const paymentService = easyAbp.paymentService.payments.payment;
const accountService = easyAbp.paymentService.prepayment.accounts.account;
$('#CreateOrderButton').click(function (e) {
e.preventDefault();
if (!cakeProductSkuId) return
let btn = $(this)
btn.buttonBusy(true);
orderService.create({
storeId: storeId,
orderLines: [{
productId: cakeProductId,
productSkuId: cakeProductSkuId,
quantity: 1
}]
}).then(function () {
abp.message.success(l('OrderCreated'))
.then(function () {
window.location.reload();
})
}).catch(function () {
btn.buttonBusy(false);
});
});
let redirectToOrderHistory = function () {
document.location.href = document.location.origin + '?showOrderHistory=true'
}
$('#CancelOrderButton').click(function (e) {
e.preventDefault();
orderService.cancel(orderId, {})
.then(function () {
abp.message.success(l('OrderCanceled'))
.then(function () {
redirectToOrderHistory()
})
});
});
$('#TopUpButton').click(function (e) {
e.preventDefault();
accountService.changeBalance(accountId, {changedBalance: 1.00})
.then(function () {
abp.message.success(l('TopUpSucceeded'))
.then(function () {
window.location.reload();
})
});
});
$('#PayForOrderButton').click(function (e) {
e.preventDefault();
if (currentBalance < totalPrice) {
abp.message.error(l('InsufficientBalance'))
.then(function () {
window.location.reload();
})
return
}
eShopPaymentService.create({
paymentMethod: "Prepayment",
orderIds: [orderId]
}).then(function () {
getPaymentIdAndPay()
});
});
let getPaymentIdAndPay = function () {
orderService.get(orderId).then(function (order) {
if (!order) return
if (!order.paymentId) {
setTimeout(getPaymentIdAndPay, 500)
return
}
paymentService.pay(order.paymentId, {
extraProperties: {
"AccountId": accountId
}
}).then(function () {
abp.message.success(l('PaymentSucceeded'))
.then(function () {
redirectToOrderHistory()
})
})
})
}
let updateTimeToAutoCancel = function () {
$('#timeToAutoCancel').text(new Date(secondsToAutoCancel * 1000).toISOString().substring(11, 19))
}
let cancelCurrentPayment = function () {
if (!orderId) return
orderService.get(orderId).then(function (order) {
if (order.paymentId) {
paymentService.cancel(order.paymentId)
}
})
}
function tryGoToOrderHistoryTab() {
if (location.search.indexOf('showOrderHistory=true') < 0) return
$('#OrderHistoryTab-tab').tab("show")
history.pushState(null, null, location.origin)
// document.location.href = document.location.href.split('#')[0]
}
let init = function () {
tryGoToOrderHistoryTab()
updateTimeToAutoCancel()
cancelCurrentPayment()
}
init()
let interval = setInterval(function () {
if (secondsToAutoCancel <= 0) {
clearInterval(interval)
return
}
secondsToAutoCancel--
updateTimeToAutoCancel()
}, 1000);
});

299
samples/EShopSample/aspnet-core/src/EShopSample.Web/Pages/SkuSelector.js

@ -0,0 +1,299 @@
// Copied from https://codepen.io/keelii/pen/RoOzgb
const l = abp.localization.getResource('EasyAbpEShopEShopSample');
var data = JSON.parse(skuInfo)
var res = {}
var spliter = '\u2299'
var r = {}
var keys = []
var selectedCache = []
function combineAttr(data, keys) {
var allKeys = []
var result = {}
for (var i = 0; i < data.length; i++) {
var item = data[i].skus
var values = []
for (var j = 0; j < keys.length; j++) {
var key = keys[j]
if (!result[key]) result[key] = []
if (result[key].indexOf(item[key]) < 0) result[key].push(item[key])
values.push(item[key])
}
allKeys.push({
path: values.join(spliter),
sku: data[i].skuId,
price: data[i].skuPrice,
currency: data[i].skuCurrency
})
}
return {
result: result,
items: allKeys
}
}
function render(data) {
var output = ''
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var items = data[key]
output += '<dl data-type="' + key + '" data-idx="' + i + '">'
output += '<dt>' + key + ':</dt>'
output += '<dd>'
for (var j = 0; j < items.length; j++) {
var item = items[j]
var cName = j == 0 ? 'active' : ''
if (j == 0) {
selectedCache.push(item)
}
output += '<button data-title="' + item + '" class="' + cName + '" value="' + item + '">' + item + '</button> '
}
output += '</dd>'
output += '</dl>'
}
output += '</dl>'
$('#skuSelector').html(output)
}
function getAllKeys(arr) {
var result = []
for (var i = 0; i < arr.length; i++) {
result.push(arr[i].path)
}
return result
}
function powerset(arr) {
var ps = [[]];
for (var i = 0; i < arr.length; i++) {
for (var j = 0, len = ps.length; j < len; j++) {
ps.push(ps[j].concat(arr[i]));
}
}
return ps;
}
function buildResult(items) {
var allKeys = getAllKeys(items)
for (var i = 0; i < allKeys.length; i++) {
var curr = allKeys[i]
var sku = items[i].sku
var price = items[i].price
var values = curr.split(spliter)
var allSets = powerset(values)
for (var j = 0; j < allSets.length; j++) {
var set = allSets[j]
var key = set.join(spliter)
if (res[key]) {
res[key].sku = items[i].sku
res[key].price = items[i].price
res[key].currency = items[i].currency
} else {
res[key] = {
sku: items[i].sku,
price: items[i].price,
currency: items[i].currency
}
}
}
}
}
function trimSpliter(str, spliter) {
var reLeft = new RegExp('^' + spliter + '+', 'g');
var reRight = new RegExp(spliter + '+$', 'g');
var reSpliter = new RegExp(spliter + '+', 'g');
return str.replace(reLeft, '')
.replace(reRight, '')
.replace(reSpliter, spliter)
}
function getSelectedItem() {
var result = []
$('dl[data-type]').each(function () {
var $selected = $(this).find('.active')
if ($selected.length) {
result.push($selected.val())
} else {
result.push('')
}
})
return result
}
function updateStatus(selected) {
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var data = r.result[key]
var hasActive = !!selected[i]
var copy = selected.slice()
for (var j = 0; j < data.length; j++) {
var item = data[j]
if (selected[i] == item) continue
copy[i] = item
var curr = trimSpliter(copy.join(spliter), spliter)
var $item = $('dl').filter('[data-type="' + key + '"]').find('[value="' + item + '"]')
var titleStr = '[' + copy.join('-') + ']'
if (res[curr]) {
$item.removeClass('disabled')
setTitle($item.get(0))
} else {
$item.addClass('disabled').attr('title', titleStr + ' 无此属性搭配')
}
}
}
}
function handleNormalClick($this) {
$this.siblings().removeClass('active')
$this.addClass('active')
}
function handleDisableClick($this) {
var $currAttr = $this.parents('dl').eq(0)
var idx = $currAttr.data('idx')
var type = $currAttr.data('type')
var value = $this.val()
$this.removeClass('disabled')
selectedCache[idx] = value
console.log(selectedCache)
$('dl').not($currAttr).find('button').removeClass('active')
updateStatus(getSelectedItem())
for (var i = 0; i < keys.length; i++) {
var item = keys[i]
var $curr = $('dl[data-type="' + item + '"]')
if (item == type) continue
var $lastSelected = $curr.find('button[value="' + selectedCache[i] + '"]')
if (!$lastSelected.hasClass('disabled')) {
$lastSelected.addClass('active')
updateStatus(getSelectedItem())
}
}
}
function highLightAttr() {
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var $curr = $('dl[data-type="' + key + '"]')
if ($curr.find('.active').length < 1) {
$curr.addClass('hl')
} else {
$curr.removeClass('hl')
}
}
}
function bindEvent() {
$('#skuSelector').undelegate().delegate('button', 'click', function (e) {
var $this = $(this)
var isActive = $this.hasClass('.active')
var isDisable = $this.hasClass('disabled')
if (!isActive) {
handleNormalClick($this)
if (isDisable) {
handleDisableClick($this)
} else {
selectedCache[$this.parents('dl').eq(0).data('idx')] = $this.val()
}
updateStatus(getSelectedItem())
highLightAttr()
showResult()
}
})
$('button').each(function () {
var value = $(this).val()
if (!res[value] && !$(this).hasClass('active')) {
$(this).addClass('disabled')
}
})
}
function showResult() {
var result = getSelectedItem()
var s = []
for (var i = 0; i < result.length; i++) {
var item = result[i];
if (!!item) {
s.push(item)
}
}
if (s.length == keys.length) {
var curr = res[s.join(spliter)]
if (curr && curr.sku) {
cakeProductSkuId = curr.sku
$('#selectedSku').html('<b>' + l('YourChoice') + '</b><br>' + s.join('\u3000-\u3000'))
$('#selectedSkuId').html('<b>' + l('SkuId') + '</b><br>' + curr.sku)
$('#selectedSkuPrice').html('<b>' + l('TotalPrice') + '</b><br>' + curr.price.toFixed(2) + ' ' + curr.currency)
}
}
}
function updateData() {
data = JSON.parse(skuInfo)
init(data)
}
function setTitle(el) {
var title = $(el).data('title');
if (title) $(el).attr('title', title);
}
function setAllTitle() {
$('#skuSelector').find('button').each(setTitle)
}
function initSkuSelector(data) {
res = {}
r = {}
keys = []
selectedCache = []
for (var attr_key in data[0].skus) {
if (!data[0].skus.hasOwnProperty(attr_key)) continue;
keys.push(attr_key)
}
setAllTitle();
r = combineAttr(data, keys)
render(r.result)
buildResult(r.items)
updateStatus(getSelectedItem())
showResult()
bindEvent()
}
initSkuSelector(data)
Loading…
Cancel
Save