694 lines
26 KiB
C#
694 lines
26 KiB
C#
using Fake.Application;
|
||
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.Entities;
|
||
using Oms.Domain.CustomersAggregate.Enums;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Consts;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
using Oms.Domain.Shared.Enums;
|
||
using Oms.Domain.SystemAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Orders;
|
||
|
||
public partial class OrderService
|
||
{
|
||
private const string PriceOperatorType = "price";
|
||
private const string RulesOperatorType = "rules";
|
||
private const int ParcelBatchMaxCount = 2000;
|
||
|
||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
||
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||
|
||
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(OrderParcelCreateInput input)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(input.RequestId))
|
||
{
|
||
var existed = await OrderRepository.GetByRequestIdAsync(input.RequestId);
|
||
if (existed != null)
|
||
{
|
||
return await BuildParcelCreateOutputAsync(existed);
|
||
}
|
||
}
|
||
|
||
var bindTime = DateTime.Now;
|
||
await ValidateParcelCreateInputAsync(input);
|
||
|
||
var product = await SalesProductRepository.GetByProductCodeAsync(input.ProductCode!.Trim());
|
||
if (product == null || product.IsActive != EnableStatus.Enabled)
|
||
{
|
||
throw new BusinessException("销售产品不存在或未启用");
|
||
}
|
||
|
||
var snapshotBinding = await ResolveSnapshotBindingAsync(product, bindTime);
|
||
var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == input.Receiver!.CountryCode);
|
||
if (country == null)
|
||
{
|
||
throw new BusinessException("收件国家不存在");
|
||
}
|
||
|
||
if (await IsBlacklistHitAsync(
|
||
product,
|
||
country.CountryCode,
|
||
input.Receiver?.Province,
|
||
input.Receiver?.City,
|
||
input.Receiver?.ZipCode))
|
||
{
|
||
throw new BusinessException("收件地址命中黑名单区域");
|
||
}
|
||
|
||
var serviceNo = await ResolveServiceNoAsync(input.ServiceNo);
|
||
var order = BuildDraftOrderEntity(input, product, country.CountryCode, snapshotBinding, serviceNo);
|
||
order = await OrderRepository.InsertAsync(order);
|
||
|
||
var weights = BuildInitialWeightRows(order.Id, input);
|
||
await SaveParcelChildrenAsync(order, input, weights);
|
||
|
||
try
|
||
{
|
||
order.IsRemote = await IsRemoteHitAsync(
|
||
product,
|
||
country.CountryCode,
|
||
input.Receiver?.Province,
|
||
input.Receiver?.City,
|
||
input.Receiver?.ZipCode) ? (byte)1 : (byte)0;
|
||
|
||
var forecast = weights.First(x => x.Type == OrderWeightTypeCodes.Forecast);
|
||
await ApplyChargeableWeightAsync(product, forecast);
|
||
SyncWeightRows(weights, forecast);
|
||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||
|
||
var zoneName = await MatchPriceZoneAsync(
|
||
product,
|
||
country.CountryCode,
|
||
input.Receiver?.Province,
|
||
input.Receiver?.City,
|
||
input.Receiver?.ZipCode);
|
||
if (string.IsNullOrWhiteSpace(zoneName))
|
||
{
|
||
throw new BusinessException("价格分区未匹配(规则快照)");
|
||
}
|
||
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
|
||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||
snapshotBinding.Price,
|
||
snapshotBinding.PriceVersion,
|
||
snapshotBinding.PriceEffectiveTime,
|
||
forecast.ChargeableWeight ?? 0,
|
||
zoneName,
|
||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||
customer?.CustomerGroup);
|
||
|
||
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
|
||
{
|
||
throw new BusinessException("产品卖价快照未配置或未匹配到有效价格");
|
||
}
|
||
|
||
if (fees.Count > 0)
|
||
{
|
||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||
}
|
||
|
||
order.OrderStatus = OrderStatusCodes.Predeclared;
|
||
order.PredeclareFailReason = string.Empty;
|
||
order.IsRemote = order.IsRemote;
|
||
order.UpdateTime = DateTime.Now;
|
||
await OrderRepository.UpdateAsync(order);
|
||
|
||
return await BuildParcelCreateOutputAsync(order);
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
await MarkParcelDraftFailedAsync(order, ex.Message);
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
await MarkParcelDraftFailedAsync(order, ex.Message);
|
||
throw new BusinessException(ex.Message);
|
||
}
|
||
}
|
||
|
||
private async Task ValidateParcelCreateInputAsync(OrderParcelCreateInput input)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.ProductCode))
|
||
{
|
||
throw new BusinessException("产品代码不能为空");
|
||
}
|
||
|
||
if (!input.WarehouseId.HasValue || input.WarehouseId <= 0)
|
||
{
|
||
throw new BusinessException("仓库ID不能为空");
|
||
}
|
||
|
||
if (!input.CustomerId.HasValue || input.CustomerId <= 0)
|
||
{
|
||
throw new BusinessException("客户ID不能为空");
|
||
}
|
||
|
||
if (!input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue || !input.ActualWeight.HasValue
|
||
|| input.Length <= 0 || input.Width <= 0 || input.Height <= 0 || input.ActualWeight <= 0)
|
||
{
|
||
throw new BusinessException("件重尺必须大于0");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(input.CargoAttribute)
|
||
|| input.CargoAttribute is not (CargoAttributeCodes.General or CargoAttributeCodes.Electronics or CargoAttributeCodes.Sensitive))
|
||
{
|
||
throw new BusinessException("货物属性不合法");
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(input.ExportCustomsMode)
|
||
&& !ExportCustomsModeCodes.All.Contains(input.ExportCustomsMode))
|
||
{
|
||
throw new BusinessException("出口报关方式不合法");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(input.DeclaredCurrency))
|
||
{
|
||
throw new BusinessException("申报币种不能为空");
|
||
}
|
||
|
||
if (!input.Quantity.HasValue || input.Quantity <= 0)
|
||
{
|
||
throw new BusinessException("包裹数量不能为空");
|
||
}
|
||
|
||
if (input.Receiver == null
|
||
|| string.IsNullOrWhiteSpace(input.Receiver.RecipientsName)
|
||
|| !input.Receiver.CountryCode.HasValue
|
||
|| string.IsNullOrWhiteSpace(input.Receiver.ZipCode)
|
||
|| string.IsNullOrWhiteSpace(input.Receiver.StreetAddress1))
|
||
{
|
||
throw new BusinessException("收件人信息不完整");
|
||
}
|
||
|
||
if (input.Sender == null || string.IsNullOrWhiteSpace(input.Sender.SenderName))
|
||
{
|
||
throw new BusinessException("发件人姓名不能为空");
|
||
}
|
||
|
||
if (input.Items.Count == 0)
|
||
{
|
||
throw new BusinessException("申报明细不能为空");
|
||
}
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(item.EnName)
|
||
|| !item.UnitPrice.HasValue
|
||
|| !item.UnitWeight.HasValue
|
||
|| !item.Quantity.HasValue
|
||
|| item.Quantity <= 0)
|
||
{
|
||
throw new BusinessException("申报明细必填字段不完整");
|
||
}
|
||
}
|
||
|
||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId.Value);
|
||
if (warehouse == null)
|
||
{
|
||
throw new BusinessException("仓库不存在");
|
||
}
|
||
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == input.CustomerId.Value);
|
||
if (customer == null || customer.Status != CustomerStatus.Normal)
|
||
{
|
||
throw new BusinessException("客户不存在或状态不可用");
|
||
}
|
||
}
|
||
|
||
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
|
||
{
|
||
var rulesRecord = await SalesProductRepository.GetEffectivePublishRecordAsync(product.Id, RulesOperatorType, bindTime);
|
||
var priceRecord = await SalesProductRepository.GetEffectivePublishRecordAsync(product.Id, PriceOperatorType, bindTime);
|
||
|
||
SalesProductRulesPublishSnapshotDto rulesSnapshot;
|
||
if (rulesRecord == null)
|
||
{
|
||
rulesSnapshot = await BuildLiveRulesSnapshotAsync(product);
|
||
}
|
||
else
|
||
{
|
||
rulesSnapshot = OmsDocHelper.DeserializeJson<SalesProductRulesPublishSnapshotDto>(rulesRecord.Content)
|
||
?? throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
|
||
}
|
||
|
||
SalesProductPricePublishSnapshotDto priceSnapshot;
|
||
if (priceRecord == null)
|
||
{
|
||
priceSnapshot = await BuildLivePriceSnapshotAsync(product);
|
||
if (priceSnapshot.Prices.Count == 0)
|
||
{
|
||
throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
priceSnapshot = OmsDocHelper.DeserializeJson<SalesProductPricePublishSnapshotDto>(priceRecord.Content)
|
||
?? throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
|
||
if (priceSnapshot.Prices.Count == 0)
|
||
{
|
||
throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
|
||
}
|
||
}
|
||
|
||
if (rulesRecord == null && !product.WeightRuleId.HasValue)
|
||
{
|
||
throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
|
||
}
|
||
|
||
return new ProductSnapshotBinding
|
||
{
|
||
RulesVersion = rulesRecord?.Version ?? $"LIVE-{product.Id}",
|
||
PriceVersion = priceRecord?.Version ?? $"LIVE-{product.Id}",
|
||
RulesEffectiveTime = rulesRecord?.PublishTime ?? bindTime,
|
||
PriceEffectiveTime = priceRecord?.PublishTime ?? bindTime,
|
||
Rules = rulesSnapshot,
|
||
Price = priceSnapshot
|
||
};
|
||
}
|
||
|
||
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
|
||
{
|
||
return new SalesProductRulesPublishSnapshotDto
|
||
{
|
||
Product = new SalesProductSaveInput
|
||
{
|
||
Id = product.Id,
|
||
ProductCode = product.ProductCode,
|
||
ProductName = product.ProductName,
|
||
BlacklistAreaId = product.BlacklistAreaId,
|
||
RemoteAreaId = product.RemoteAreaId,
|
||
PriceAreaId = product.PriceAreaId,
|
||
WeightRuleId = product.WeightRuleId,
|
||
WeightRoundingId = product.WeightRoundingId,
|
||
IsActive = product.IsActive,
|
||
Scheme = product.Scheme
|
||
}
|
||
};
|
||
}
|
||
|
||
private async Task<SalesProductPricePublishSnapshotDto> BuildLivePriceSnapshotAsync(SalesProduct product)
|
||
{
|
||
var prices = await SalesProductRepository.GetPricesAsync(product.Id);
|
||
var priceIds = prices.Select(x => x.Id).ToArray();
|
||
var factorMap = (await SalesProductRepository.GetPriceFactorsAsync(priceIds))
|
||
.GroupBy(x => x.PriceId)
|
||
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
|
||
var detailMap = (await SalesProductRepository.GetPriceDetailsAsync(priceIds))
|
||
.GroupBy(x => x.PriceId)
|
||
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
|
||
|
||
return new SalesProductPricePublishSnapshotDto
|
||
{
|
||
SalesProductId = product.Id,
|
||
Prices = prices.Select(price => new SalesProductPriceListOutput
|
||
{
|
||
Id = price.Id,
|
||
SalesProductId = price.SalesProductId,
|
||
SerialNumber = price.SerialNumber,
|
||
Status = (byte)price.Status,
|
||
PriceName = price.PriceName,
|
||
CostItem = price.CostItem,
|
||
CostUnit = price.CostUnit,
|
||
Currency = price.Currency,
|
||
ChargingTime = price.ChargingTime,
|
||
ChargingCondition = price.ChargingCondition,
|
||
Factors = factorMap.TryGetValue(price.Id, out var factors)
|
||
? factors.Select(MapPriceFactorOutput).ToList()
|
||
: [],
|
||
Details = detailMap.TryGetValue(price.Id, out var details)
|
||
? details.Select(MapPriceDetailOutput).ToList()
|
||
: []
|
||
}).ToList()
|
||
};
|
||
}
|
||
|
||
private static ChannelPriceFactorOutput MapPriceFactorOutput(SalesProductPriceFactor entity)
|
||
{
|
||
return new ChannelPriceFactorOutput
|
||
{
|
||
Id = entity.Id,
|
||
PriceId = entity.PriceId,
|
||
SerialNumber = entity.SerialNumber,
|
||
Factor = entity.Factor,
|
||
FactorValues = OmsDocHelper.DeserializeJson<List<string>>(entity.FactorValues) ?? []
|
||
};
|
||
}
|
||
|
||
private static ChannelPriceDetailOutput MapPriceDetailOutput(SalesProductPriceDetail entity)
|
||
{
|
||
return new ChannelPriceDetailOutput
|
||
{
|
||
Id = entity.Id,
|
||
PriceId = entity.PriceId,
|
||
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson),
|
||
CalculationFormula = entity.CalculationFormula,
|
||
CalculatedValue = entity.CalculatedValue,
|
||
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues)
|
||
};
|
||
}
|
||
|
||
private async Task<bool> IsBlacklistHitAsync(
|
||
SalesProduct product,
|
||
string countryCode,
|
||
string? province,
|
||
string? city,
|
||
string? zipCode)
|
||
{
|
||
if (!product.BlacklistAreaId.HasValue || product.BlacklistAreaId <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.BlacklistAreaId.Value);
|
||
return rules.Any(rule => ParcelBillingCalculator.IsAreaMatched(
|
||
rule,
|
||
countryCode,
|
||
province,
|
||
city,
|
||
zipCode));
|
||
}
|
||
|
||
private async Task<bool> IsRemoteHitAsync(
|
||
SalesProduct product,
|
||
string countryCode,
|
||
string? province,
|
||
string? city,
|
||
string? zipCode)
|
||
{
|
||
if (!product.RemoteAreaId.HasValue || product.RemoteAreaId <= 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.RemoteAreaId.Value);
|
||
return rules.Any(rule => ParcelBillingCalculator.IsAreaMatched(
|
||
rule,
|
||
countryCode,
|
||
province,
|
||
city,
|
||
zipCode));
|
||
}
|
||
|
||
private async Task<string?> MatchPriceZoneAsync(
|
||
SalesProduct product,
|
||
string countryCode,
|
||
string? province,
|
||
string? city,
|
||
string? zipCode)
|
||
{
|
||
if (!product.PriceAreaId.HasValue || product.PriceAreaId <= 0)
|
||
{
|
||
return "默认分区";
|
||
}
|
||
|
||
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == product.PriceAreaId.Value);
|
||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.PriceAreaId.Value);
|
||
var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched(
|
||
rule,
|
||
countryCode,
|
||
province,
|
||
city,
|
||
zipCode));
|
||
return ParcelBillingCalculator.ResolveZoneName(matched, area);
|
||
}
|
||
|
||
private async Task ApplyChargeableWeightAsync(SalesProduct product, OrderWeight forecast)
|
||
{
|
||
try
|
||
{
|
||
if (!product.WeightRuleId.HasValue || product.WeightRuleId <= 0)
|
||
{
|
||
throw new BusinessException("未配置计重规则");
|
||
}
|
||
|
||
var weightRule = await WeightRuleRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRuleId.Value);
|
||
if (weightRule == null)
|
||
{
|
||
throw new BusinessException("未配置计重规则");
|
||
}
|
||
|
||
var ruleInfos = await WeightRuleRepository.GetAllInfosByRuleIdAsync(product.WeightRuleId.Value);
|
||
var (volumetricWeight, rawWeight) = ParcelBillingCalculator.CalculateRawBillableWeight(forecast, ruleInfos);
|
||
forecast.VolumetricWeight = volumetricWeight;
|
||
|
||
if (!product.WeightRoundingId.HasValue || product.WeightRoundingId <= 0)
|
||
{
|
||
throw new BusinessException("未配置计重进位");
|
||
}
|
||
|
||
var rounding = await WeightRoundingRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRoundingId.Value);
|
||
if (rounding == null)
|
||
{
|
||
throw new BusinessException("未配置计重进位");
|
||
}
|
||
|
||
var ranges = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(product.WeightRoundingId.Value);
|
||
forecast.ChargeableWeight = ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
||
}
|
||
catch (BusinessException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
throw new BusinessException(ex.Message);
|
||
}
|
||
}
|
||
|
||
private static List<OrderWeight> BuildInitialWeightRows(int orderId, OrderParcelCreateInput input)
|
||
{
|
||
var cubic = ParcelBillingCalculator.CalculateCubic(input.Length!.Value, input.Width!.Value, input.Height!.Value);
|
||
var forecast = new OrderWeight
|
||
{
|
||
OrderId = orderId,
|
||
Type = OrderWeightTypeCodes.Forecast,
|
||
Length = input.Length,
|
||
Width = input.Width,
|
||
Height = input.Height,
|
||
ActualWeight = input.ActualWeight,
|
||
VolumetricWeight = input.VolumetricWeight ?? 0,
|
||
ChargeableWeight = input.ChargeableWeight ?? input.ActualWeight,
|
||
Cubic = input.Cubic ?? cubic
|
||
};
|
||
|
||
return
|
||
[
|
||
forecast,
|
||
CloneWeightRow(orderId, OrderWeightTypeCodes.Receive, forecast),
|
||
CloneWeightRow(orderId, OrderWeightTypeCodes.Ship, forecast),
|
||
CloneWeightRow(orderId, OrderWeightTypeCodes.Downstream, forecast)
|
||
];
|
||
}
|
||
|
||
private static OrderWeight CloneWeightRow(int orderId, string type, OrderWeight source) =>
|
||
new()
|
||
{
|
||
OrderId = orderId,
|
||
Type = type,
|
||
Length = source.Length,
|
||
Width = source.Width,
|
||
Height = source.Height,
|
||
ActualWeight = source.ActualWeight,
|
||
VolumetricWeight = source.VolumetricWeight,
|
||
ChargeableWeight = source.ChargeableWeight,
|
||
Cubic = source.Cubic
|
||
};
|
||
|
||
private static void SyncWeightRows(List<OrderWeight> weights, OrderWeight forecast)
|
||
{
|
||
foreach (var weight in weights)
|
||
{
|
||
weight.Length = forecast.Length;
|
||
weight.Width = forecast.Width;
|
||
weight.Height = forecast.Height;
|
||
weight.ActualWeight = forecast.ActualWeight;
|
||
weight.VolumetricWeight = forecast.VolumetricWeight;
|
||
weight.ChargeableWeight = forecast.ChargeableWeight;
|
||
weight.Cubic = forecast.Cubic;
|
||
}
|
||
}
|
||
|
||
private static Order BuildDraftOrderEntity(
|
||
OrderParcelCreateInput input,
|
||
SalesProduct product,
|
||
string consigneeCountry,
|
||
ProductSnapshotBinding snapshotBinding,
|
||
string serviceNo)
|
||
{
|
||
return new Order
|
||
{
|
||
RequestId = input.RequestId,
|
||
ClientNo = input.ClientNo,
|
||
TrackNo = input.TrackNo,
|
||
ServiceNo = serviceNo,
|
||
ProductCode = input.ProductCode,
|
||
ChannelCode = input.ChannelCode ?? product.ProductCode,
|
||
ConsigneeCountry = consigneeCountry,
|
||
CargoAttribute = input.CargoAttribute ?? CargoAttributeCodes.General,
|
||
ExportCustomsMode = input.ExportCustomsMode,
|
||
WarehouseId = input.WarehouseId,
|
||
CustomerId = input.CustomerId,
|
||
IsCod = input.IsCod ? (byte)1 : (byte)0,
|
||
CodCurrency = input.CodCurrency,
|
||
CodSum = input.CodSum,
|
||
DeclaredCurrency = input.DeclaredCurrency,
|
||
DeclaredSum = input.DeclaredSum,
|
||
Quantity = input.Quantity,
|
||
IsIntercept = 0,
|
||
IsRemote = 0,
|
||
PredeclareFailReason = string.Empty,
|
||
SalesProductRulesVersion = snapshotBinding.RulesVersion,
|
||
SalesProductPriceVersion = snapshotBinding.PriceVersion,
|
||
OrderStatus = OrderStatusCodes.Draft
|
||
};
|
||
}
|
||
|
||
private async Task SaveParcelChildrenAsync(Order order, OrderParcelCreateInput input, List<OrderWeight> weights)
|
||
{
|
||
if (input.Sender != null)
|
||
{
|
||
var sender = await OrderRepository.InsertSenderAsync(new OrderSender
|
||
{
|
||
OrderId = order.Id,
|
||
SenderName = input.Sender.SenderName,
|
||
Company = input.Sender.Company,
|
||
Country = input.Sender.CountryCode,
|
||
Province = input.Sender.Province,
|
||
City = input.Sender.City,
|
||
ZipCode = input.Sender.ZipCode,
|
||
StreetAddress1 = input.Sender.StreetAddress1,
|
||
StreetAddress2 = input.Sender.StreetAddress2,
|
||
StreetAddress3 = input.Sender.StreetAddress3,
|
||
HouseNumber = input.Sender.HouseNumber,
|
||
Phone = input.Sender.Phone,
|
||
TelePhone = input.Sender.TelePhone,
|
||
Email = input.Sender.Email,
|
||
IsDefault = 0
|
||
});
|
||
order.SenderId = sender.Id;
|
||
}
|
||
|
||
if (input.Receiver != null)
|
||
{
|
||
var recipient = await OrderRepository.InsertRecipientAsync(new OrderRecipient
|
||
{
|
||
OrderId = order.Id,
|
||
RecipientsName = input.Receiver.RecipientsName,
|
||
CompanyName = input.Receiver.CompanyName,
|
||
Country = input.Receiver.CountryCode,
|
||
Phone = input.Receiver.Phone,
|
||
TelePhone = input.Receiver.TelePhone,
|
||
ZipCode = input.Receiver.ZipCode,
|
||
Province = input.Receiver.Province,
|
||
City = input.Receiver.City,
|
||
StreetNumber = input.Receiver.StreetNumber,
|
||
StreetAddress1 = input.Receiver.StreetAddress1,
|
||
StreetAddress2 = input.Receiver.StreetAddress2,
|
||
StreetAddress3 = input.Receiver.StreetAddress3,
|
||
Email = input.Receiver.Email,
|
||
TaxType = input.Receiver.TaxType,
|
||
TaxNo = input.Receiver.TaxNo,
|
||
CertificateType = input.Receiver.CertificateType,
|
||
CertificateNo = input.Receiver.CertificateNo,
|
||
Eori = input.Receiver.Eori,
|
||
Vat = input.Receiver.Vat
|
||
});
|
||
order.RecipientId = recipient.Id;
|
||
}
|
||
|
||
if (input.Items.Count > 0)
|
||
{
|
||
await OrderRepository.InsertDeclarationsAsync(input.Items.Select(x => new OrderDeclaration
|
||
{
|
||
OrderId = order.Id,
|
||
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,
|
||
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
|
||
}));
|
||
}
|
||
|
||
await OrderRepository.InsertWeightsAsync(weights);
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
private async Task<string> ResolveServiceNoAsync(string? serviceNo)
|
||
{
|
||
var normalized = serviceNo?.Trim();
|
||
if (!string.IsNullOrWhiteSpace(normalized))
|
||
{
|
||
if (await OrderRepository.HasServiceNoAsync(normalized))
|
||
{
|
||
throw new BusinessException("运单号已存在");
|
||
}
|
||
|
||
return normalized;
|
||
}
|
||
|
||
for (var i = 0; i < 5; i++)
|
||
{
|
||
var generated = $"LP{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
|
||
if (!await OrderRepository.HasServiceNoAsync(generated))
|
||
{
|
||
return generated;
|
||
}
|
||
}
|
||
|
||
throw new BusinessException("运单号生成失败");
|
||
}
|
||
|
||
private async Task MarkParcelDraftFailedAsync(Order order, string reason)
|
||
{
|
||
order.OrderStatus = OrderStatusCodes.Draft;
|
||
order.PredeclareFailReason = reason;
|
||
order.UpdateTime = DateTime.Now;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
|
||
{
|
||
var mapped = (await MapOrderOutputsAsync([order])).First();
|
||
return new OrderParcelCreateOutput
|
||
{
|
||
ClientNo = order.ClientNo,
|
||
ServiceNo = order.ServiceNo,
|
||
TrackNo = order.TrackNo,
|
||
LabelUrl = order.OrderStatus == OrderStatusCodes.Predeclared
|
||
? await CreateOrderPrintFileAsync(mapped)
|
||
: string.Empty
|
||
};
|
||
}
|
||
}
|