改用仓储链式 Where 修复分页列表状态过滤失效;保存并返回 ConsigneeCountryName;预报成功后异步 UpsertSmallBag;仅草稿/已预报可删除。 Co-authored-by: Cursor <cursoragent@cursor.com>
623 lines
20 KiB
C#
623 lines
20 KiB
C#
using Fake.Application;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Oms.Application.Dtos.Channels;
|
|
using Oms.Application.Dtos.Orders;
|
|
using Oms.Application.Shared;
|
|
using Oms.Domain.ChannelsAggregate.Entities;
|
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
|
using Oms.Domain.CustomersAggregate.Enums;
|
|
using Oms.Domain.OrdersAggregate.Consts;
|
|
using Oms.Domain.OrdersAggregate.Entities;
|
|
using Oms.Domain.Shared.Enums;
|
|
|
|
namespace Oms.Application.Services.Orders;
|
|
|
|
public partial class OrderService
|
|
{
|
|
private IServiceChannelRepository ServiceChannelRepository =>
|
|
LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
|
|
|
/// <summary>
|
|
/// 编辑订单详情(基本信息、发件人、收件人)。
|
|
/// </summary>
|
|
[HttpPost("/order/update")]
|
|
public async Task<OrderOutput> UpdateOrderDetailAsync(OrderUpdateInput input)
|
|
{
|
|
var order = await GetOrderOrThrowAsync(input.Id);
|
|
|
|
if (input.ClientNo != null)
|
|
{
|
|
order.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
|
|
}
|
|
|
|
if (input.TrackNo != null)
|
|
{
|
|
order.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
|
|
}
|
|
|
|
if (input.ChannelCode != null)
|
|
{
|
|
order.ChannelCode = string.IsNullOrWhiteSpace(input.ChannelCode) ? null : input.ChannelCode.Trim();
|
|
}
|
|
|
|
if (input.CargoAttribute != null)
|
|
{
|
|
order.CargoAttribute = input.CargoAttribute.Trim();
|
|
}
|
|
|
|
if (input.WarehouseId.HasValue)
|
|
{
|
|
order.WarehouseId = input.WarehouseId.Value > 0 ? input.WarehouseId : null;
|
|
}
|
|
|
|
if (input.IsCod.HasValue)
|
|
{
|
|
order.IsCod = input.IsCod.Value ? (byte)1 : (byte)0;
|
|
}
|
|
|
|
if (input.CodCurrency != null)
|
|
{
|
|
order.CodCurrency = input.CodCurrency;
|
|
}
|
|
|
|
if (input.CodSum.HasValue)
|
|
{
|
|
order.CodSum = input.CodSum;
|
|
}
|
|
|
|
if (input.DeclaredCurrency != null)
|
|
{
|
|
order.DeclaredCurrency = input.DeclaredCurrency;
|
|
}
|
|
|
|
if (input.DeclaredSum.HasValue)
|
|
{
|
|
order.DeclaredSum = input.DeclaredSum;
|
|
}
|
|
|
|
if (input.Quantity.HasValue)
|
|
{
|
|
order.Quantity = input.Quantity;
|
|
}
|
|
|
|
order.UpdateTime = DateTime.Now;
|
|
await OrderRepository.UpdateAsync(order);
|
|
|
|
if (input.Sender != null && order.SenderId.HasValue)
|
|
{
|
|
var senders = await OrderRepository.GetSendersByOrderIdsAsync([order.Id]);
|
|
var sender = senders.FirstOrDefault();
|
|
if (sender != null)
|
|
{
|
|
ApplySenderInput(sender, input.Sender);
|
|
if (input.Sender.CountryCode != null)
|
|
{
|
|
var country = await GetCountryOrThrowAsync(input.Sender.CountryCode, "发件国家不存在");
|
|
sender.Country = country.Id;
|
|
}
|
|
|
|
await OrderRepository.UpdateSenderAsync(sender);
|
|
}
|
|
}
|
|
|
|
if (input.Receiver != null && order.RecipientId.HasValue)
|
|
{
|
|
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
|
var recipient = recipients.FirstOrDefault();
|
|
if (recipient != null)
|
|
{
|
|
await ApplyReceiverInputAsync(order, recipient, input.Receiver);
|
|
await OrderRepository.UpdateRecipientAsync(recipient);
|
|
}
|
|
}
|
|
|
|
if (input.Items != null)
|
|
{
|
|
await OrderRepository.ReplaceDeclarationsAsync(
|
|
order.Id,
|
|
input.Items.Select(x => MapDeclarationInput(order.Id, x)));
|
|
}
|
|
|
|
return (await MapOrderOutputsAsync([order])).First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 编辑订单件重尺并重算计费重。
|
|
/// </summary>
|
|
[HttpPost("/order/weight/update")]
|
|
public async Task<OrderOutput> UpdateOrderWeightsAsync(OrderWeightUpdateInput input)
|
|
{
|
|
if (input.Weights.Count == 0)
|
|
{
|
|
throw new BusinessException("件重尺不能为空");
|
|
}
|
|
|
|
var order = await GetOrderOrThrowAsync(input.Id);
|
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
|
{
|
|
throw new BusinessException("订单未绑定销售产品,无法重算计费重");
|
|
}
|
|
|
|
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
|
if (product == null)
|
|
{
|
|
throw new BusinessException("销售产品不存在");
|
|
}
|
|
|
|
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
|
|
.OrderBy(x => x.Id)
|
|
.ToList();
|
|
if (weights.Count == 0)
|
|
{
|
|
throw new BusinessException("订单件重尺不存在");
|
|
}
|
|
|
|
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
|
var updatedTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var item in input.Weights)
|
|
{
|
|
var type = item.Type?.Trim();
|
|
if (string.IsNullOrWhiteSpace(type))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var row = weights.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
|
|
if (row == null)
|
|
{
|
|
throw new BusinessException($"缺少件重尺类型 {type}");
|
|
}
|
|
|
|
if (item.Length.HasValue)
|
|
{
|
|
row.Length = item.Length;
|
|
}
|
|
|
|
if (item.Width.HasValue)
|
|
{
|
|
row.Width = item.Width;
|
|
}
|
|
|
|
if (item.Height.HasValue)
|
|
{
|
|
row.Height = item.Height;
|
|
}
|
|
|
|
if (item.ActualWeight.HasValue)
|
|
{
|
|
row.ActualWeight = item.ActualWeight;
|
|
}
|
|
|
|
if (row.Length.HasValue && row.Width.HasValue && row.Height.HasValue)
|
|
{
|
|
row.Cubic = ParcelBillingCalculator.CalculateCubic(row.Length.Value, row.Width.Value, row.Height.Value);
|
|
}
|
|
|
|
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, row);
|
|
updatedTypes.Add(type);
|
|
}
|
|
|
|
if (updatedTypes.Contains(OrderWeightTypeCodes.Forecast))
|
|
{
|
|
var forecast = weights.First(x => x.Type == OrderWeightTypeCodes.Forecast);
|
|
SyncWeightRows(weights, forecast);
|
|
}
|
|
|
|
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
|
order.UpdateTime = DateTime.Now;
|
|
await OrderRepository.UpdateAsync(order);
|
|
|
|
return (await MapOrderOutputsAsync([order])).First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重算订单应收费用。
|
|
/// </summary>
|
|
[HttpPost("/order/fee/recalculate-receivable")]
|
|
public async Task<OrderOutput> RecalculateReceivableFeesAsync(OrderActionInput input)
|
|
{
|
|
var order = await GetOrderOrThrowAsync(input.Id);
|
|
await RecalculateReceivableCoreAsync(order);
|
|
return (await MapOrderOutputsAsync([order])).First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重算订单应付费用。
|
|
/// </summary>
|
|
[HttpPost("/order/fee/recalculate-payable")]
|
|
public async Task<OrderOutput> RecalculatePayableFeesAsync(OrderActionInput input)
|
|
{
|
|
var order = await GetOrderOrThrowAsync(input.Id);
|
|
await RecalculatePayableCoreAsync(order);
|
|
return (await MapOrderOutputsAsync([order])).First();
|
|
}
|
|
|
|
private async Task RecalculateReceivableCoreAsync(Order order)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
|
{
|
|
throw new BusinessException("订单未绑定销售产品,无法重算应收");
|
|
}
|
|
|
|
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
|
if (product == null)
|
|
{
|
|
throw new BusinessException("销售产品不存在");
|
|
}
|
|
|
|
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
|
var (weightType, chargingStage) = ResolveReceivableBillingStage(order);
|
|
var weightRow = weights.FirstOrDefault(x => x.Type == weightType)
|
|
?? throw new BusinessException($"缺少件重尺类型 {weightType}");
|
|
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
|
|
{
|
|
throw new BusinessException("计费重无效,请先完善件重尺");
|
|
}
|
|
|
|
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
|
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
|
var recipient = recipients.FirstOrDefault();
|
|
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
|
product,
|
|
snapshotBinding.Rules,
|
|
snapshotBinding.Price,
|
|
order.ConsigneeCountry ?? string.Empty,
|
|
recipient?.Province,
|
|
recipient?.City,
|
|
recipient?.ZipCode);
|
|
if (zoneKeys == null || zoneKeys.Count == 0)
|
|
{
|
|
throw new BusinessException("价格分区未匹配(规则快照)");
|
|
}
|
|
|
|
var customer = order.CustomerId.HasValue
|
|
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
|
: null;
|
|
|
|
await UpsertReceivableFeesAsync(
|
|
order,
|
|
snapshotBinding,
|
|
weightRow.ChargeableWeight ?? 0,
|
|
zoneKeys,
|
|
order.CargoAttribute ?? CargoAttributeCodes.General,
|
|
customer?.CustomerGroup,
|
|
chargingStage,
|
|
replaceExistingReceivable: true);
|
|
|
|
order.UpdateTime = DateTime.Now;
|
|
await OrderRepository.UpdateAsync(order);
|
|
}
|
|
|
|
private async Task RecalculatePayableCoreAsync(Order order)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
|
{
|
|
throw new BusinessException("订单未绑定销售产品,无法重算应付");
|
|
}
|
|
|
|
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
|
if (product == null)
|
|
{
|
|
throw new BusinessException("销售产品不存在");
|
|
}
|
|
|
|
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
|
var weightRow = ResolvePayableWeightRow(order, weights);
|
|
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
|
|
{
|
|
throw new BusinessException("计费重无效,请先完善件重尺");
|
|
}
|
|
|
|
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
|
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
|
var recipient = recipients.FirstOrDefault();
|
|
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
|
product,
|
|
snapshotBinding.Rules,
|
|
snapshotBinding.Price,
|
|
order.ConsigneeCountry ?? string.Empty,
|
|
recipient?.Province,
|
|
recipient?.City,
|
|
recipient?.ZipCode);
|
|
if (zoneKeys == null || zoneKeys.Count == 0)
|
|
{
|
|
throw new BusinessException("价格分区未匹配(规则快照)");
|
|
}
|
|
|
|
var (_, chargingStage) = ResolveReceivableBillingStage(order);
|
|
var customer = order.CustomerId.HasValue
|
|
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
|
: null;
|
|
|
|
await ConfirmServiceChannelAndPayableAsync(
|
|
order,
|
|
product,
|
|
snapshotBinding,
|
|
zoneKeys,
|
|
weightRow.ChargeableWeight ?? 0,
|
|
chargingStage,
|
|
customer?.CustomerGroup);
|
|
|
|
order.UpdateTime = DateTime.Now;
|
|
await OrderRepository.UpdateAsync(order);
|
|
}
|
|
|
|
private static OrderWeight ResolvePayableWeightRow(Order order, IReadOnlyList<OrderWeight> weights)
|
|
{
|
|
var (_, chargingStage) = ResolveReceivableBillingStage(order);
|
|
var weightType = chargingStage switch
|
|
{
|
|
OrderFeeChargingStageCodes.Inbound => OrderWeightTypeCodes.Receive,
|
|
_ => OrderWeightTypeCodes.Downstream
|
|
};
|
|
|
|
return weights.FirstOrDefault(x => x.Type == weightType)
|
|
?? weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream)
|
|
?? throw new BusinessException($"缺少件重尺类型 {weightType}");
|
|
}
|
|
|
|
private async Task<List<ServiceChannelPriceListOutput>> BuildChannelPriceOutputsAsync(int serviceChannelId)
|
|
{
|
|
var prices = await ServiceChannelRepository.GetPricesAsync(serviceChannelId);
|
|
var priceIds = prices.Select(x => x.Id).ToArray();
|
|
var factorMap = (await ServiceChannelRepository.GetPriceFactorsAsync(priceIds))
|
|
.GroupBy(x => x.PriceId)
|
|
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
|
|
var detailMap = (await ServiceChannelRepository.GetPriceDetailsAsync(priceIds))
|
|
.GroupBy(x => x.PriceId)
|
|
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
|
|
|
|
return prices.Select(price => new ServiceChannelPriceListOutput
|
|
{
|
|
Id = price.Id,
|
|
ServiceChannelId = price.ServiceChannelId,
|
|
SerialNumber = price.SerialNumber,
|
|
Status = (byte)price.Status,
|
|
PriceName = price.PriceName,
|
|
CostItem = price.CostItem,
|
|
CostUnit = price.CostUnit,
|
|
Currency = price.Currency,
|
|
ChargingCondition = price.ChargingCondition,
|
|
Factors = factorMap.TryGetValue(price.Id, out var factors)
|
|
? factors.Select(MapChannelPriceFactorOutput).ToList()
|
|
: [],
|
|
Details = detailMap.TryGetValue(price.Id, out var details)
|
|
? details.Select(MapChannelPriceDetailOutput).ToList()
|
|
: []
|
|
}).ToList();
|
|
}
|
|
|
|
private static ChannelPriceFactorOutput MapChannelPriceFactorOutput(ServiceChannelPriceFactor entity) =>
|
|
new()
|
|
{
|
|
Id = entity.Id,
|
|
PriceId = entity.PriceId,
|
|
SerialNumber = entity.SerialNumber,
|
|
Factor = entity.Factor,
|
|
FactorValues = OmsDocHelper.DeserializeJson<List<string>>(entity.FactorValues) ?? []
|
|
};
|
|
|
|
private static ChannelPriceDetailOutput MapChannelPriceDetailOutput(ServiceChannelPriceDetail entity) =>
|
|
new()
|
|
{
|
|
Id = entity.Id,
|
|
PriceId = entity.PriceId,
|
|
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson),
|
|
CalculationFormula = entity.CalculationFormula,
|
|
CalculatedValue = entity.CalculatedValue,
|
|
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues)
|
|
};
|
|
|
|
private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) =>
|
|
order.OrderStatus switch
|
|
{
|
|
OrderStatusCodes.Received or OrderStatusCodes.PendingPayment or OrderStatusCodes.PendingShip
|
|
=> (OrderWeightTypeCodes.Receive, OrderFeeChargingStageCodes.Inbound),
|
|
OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping
|
|
or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed
|
|
=> (OrderWeightTypeCodes.Ship, OrderFeeChargingStageCodes.Outbound),
|
|
_ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast)
|
|
};
|
|
|
|
private static OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x)
|
|
{
|
|
return new OrderDeclaration
|
|
{
|
|
OrderId = orderId,
|
|
CnName = x.CnName,
|
|
EnName = x.EnName,
|
|
Sku = x.Sku,
|
|
Unit = x.Unit,
|
|
UnitPrice = x.UnitPrice,
|
|
UnitWeight = x.UnitWeight,
|
|
Quantity = x.Quantity,
|
|
Amount = x.Amount,
|
|
Weight = x.Weight,
|
|
HsCode = x.ExHsCode ?? x.ImHsCode,
|
|
ImHsCode = x.ImHsCode,
|
|
ExHsCode = x.ExHsCode ?? x.ImHsCode,
|
|
SkuImage = x.SkuImage,
|
|
Category = x.Category,
|
|
Brand = x.Brand,
|
|
BrandType = x.BrandType,
|
|
IsMagnetic = x.IsMagnetic ? (byte)1 : (byte)0,
|
|
Distribution = x.Distribution,
|
|
Material = x.Material,
|
|
EnMaterial = x.EnMaterial,
|
|
Purpose = x.Purpose,
|
|
Attributes = x.Attributes,
|
|
OriginCountry = x.OriginCountry,
|
|
SalesLink = x.SalesLink,
|
|
Model = x.Model,
|
|
Specifications = x.Specifications,
|
|
Comment = x.Comment
|
|
};
|
|
}
|
|
|
|
private static void ApplySenderInput(OrderSender sender, OrderParcelSenderInput input)
|
|
{
|
|
if (input.SenderName != null)
|
|
{
|
|
sender.SenderName = input.SenderName;
|
|
}
|
|
|
|
if (input.Company != null)
|
|
{
|
|
sender.Company = input.Company;
|
|
}
|
|
|
|
if (input.Phone != null)
|
|
{
|
|
sender.Phone = input.Phone;
|
|
}
|
|
|
|
if (input.TelePhone != null)
|
|
{
|
|
sender.TelePhone = input.TelePhone;
|
|
}
|
|
|
|
if (input.Email != null)
|
|
{
|
|
sender.Email = input.Email;
|
|
}
|
|
|
|
if (input.Province != null)
|
|
{
|
|
sender.Province = input.Province;
|
|
}
|
|
|
|
if (input.City != null)
|
|
{
|
|
sender.City = input.City;
|
|
}
|
|
|
|
if (input.ZipCode != null)
|
|
{
|
|
sender.ZipCode = input.ZipCode;
|
|
}
|
|
|
|
if (input.StreetAddress1 != null)
|
|
{
|
|
sender.StreetAddress1 = input.StreetAddress1;
|
|
}
|
|
|
|
if (input.StreetAddress2 != null)
|
|
{
|
|
sender.StreetAddress2 = input.StreetAddress2;
|
|
}
|
|
|
|
if (input.StreetAddress3 != null)
|
|
{
|
|
sender.StreetAddress3 = input.StreetAddress3;
|
|
}
|
|
|
|
if (input.HouseNumber != null)
|
|
{
|
|
sender.HouseNumber = input.HouseNumber;
|
|
}
|
|
}
|
|
|
|
private async Task ApplyReceiverInputAsync(Order order, OrderRecipient recipient, OrderParcelReceiverInput input)
|
|
{
|
|
if (input.RecipientsName != null)
|
|
{
|
|
recipient.RecipientsName = input.RecipientsName;
|
|
}
|
|
|
|
if (input.CompanyName != null)
|
|
{
|
|
recipient.CompanyName = input.CompanyName;
|
|
}
|
|
|
|
if (input.Phone != null)
|
|
{
|
|
recipient.Phone = input.Phone;
|
|
}
|
|
|
|
if (input.TelePhone != null)
|
|
{
|
|
recipient.TelePhone = input.TelePhone;
|
|
}
|
|
|
|
if (input.Email != null)
|
|
{
|
|
recipient.Email = input.Email;
|
|
}
|
|
|
|
if (input.Province != null)
|
|
{
|
|
recipient.Province = input.Province;
|
|
}
|
|
|
|
if (input.City != null)
|
|
{
|
|
recipient.City = input.City;
|
|
}
|
|
|
|
if (input.StreetNumber != null)
|
|
{
|
|
recipient.StreetNumber = input.StreetNumber;
|
|
}
|
|
|
|
if (input.StreetAddress1 != null)
|
|
{
|
|
recipient.StreetAddress1 = input.StreetAddress1;
|
|
}
|
|
|
|
if (input.StreetAddress2 != null)
|
|
{
|
|
recipient.StreetAddress2 = input.StreetAddress2;
|
|
}
|
|
|
|
if (input.StreetAddress3 != null)
|
|
{
|
|
recipient.StreetAddress3 = input.StreetAddress3;
|
|
}
|
|
|
|
if (input.ZipCode != null)
|
|
{
|
|
recipient.ZipCode = input.ZipCode;
|
|
}
|
|
|
|
if (input.TaxType.HasValue)
|
|
{
|
|
recipient.TaxType = input.TaxType;
|
|
}
|
|
|
|
if (input.TaxNo != null)
|
|
{
|
|
recipient.TaxNo = input.TaxNo;
|
|
}
|
|
|
|
if (input.CertificateType.HasValue)
|
|
{
|
|
recipient.CertificateType = input.CertificateType;
|
|
}
|
|
|
|
if (input.CertificateNo != null)
|
|
{
|
|
recipient.CertificateNo = input.CertificateNo;
|
|
}
|
|
|
|
if (input.Eori != null)
|
|
{
|
|
recipient.Eori = input.Eori;
|
|
}
|
|
|
|
if (input.Vat != null)
|
|
{
|
|
recipient.Vat = input.Vat;
|
|
}
|
|
|
|
if (input.CountryCode != null)
|
|
{
|
|
var country = await GetCountryOrThrowAsync(input.CountryCode, "收件国家不存在");
|
|
recipient.Country = country.Id;
|
|
order.ConsigneeCountry = country.CountryCode;
|
|
order.ConsigneeCountryName = country.ChineseName;
|
|
await OrderRepository.UpdateAsync(order);
|
|
}
|
|
}
|
|
}
|