admin/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
zzh 3a65e8c58f feat: 价格发布快照固化分区规则,查价优先从快照匹配不再查库
发布/LIVE 快照写入 PriceAreaId、PriceAreaInfos;US+06000 命中分区键 3 单测通过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 11:20:33 +08:00

774 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.Entities;
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;
NormalizeParcelCreateInput(input);
var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
input.CustomerId = sessionCustomer.Id;
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 GetCountryOrThrowAsync(input.Receiver!.CountryCode);
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, country);
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 zoneKeys = await ResolvePriceZoneKeysAsync(
product,
snapshotBinding.Rules,
snapshotBinding.Price,
country.CountryCode,
input.Receiver?.Province,
input.Receiver?.City,
input.Receiver?.ZipCode);
if (zoneKeys == null || zoneKeys.Count == 0)
{
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,
zoneKeys,
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 static void NormalizeParcelCreateInput(OrderParcelCreateInput input)
{
input.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
input.ServiceNo = string.IsNullOrWhiteSpace(input.ServiceNo) ? null : input.ServiceNo.Trim();
input.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
input.RequestId = string.IsNullOrWhiteSpace(input.RequestId) ? null : input.RequestId.Trim();
}
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)
|| string.IsNullOrWhiteSpace(input.Receiver.CountryCode)
|| 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 Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
{
return Task.FromResult(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());
var snapshot = 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()
};
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(snapshot, product, KpiAreaRepository);
return snapshot;
}
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 static int? ResolveEffectivePriceAreaId(
SalesProduct product,
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
SalesProductPricePublishSnapshotDto? priceSnapshot)
{
if (product.PriceAreaId is > 0)
{
return product.PriceAreaId;
}
if (rulesSnapshot?.Product?.PriceAreaId is > 0)
{
return rulesSnapshot.Product.PriceAreaId;
}
if (priceSnapshot?.PriceAreaId is > 0)
{
return priceSnapshot.PriceAreaId;
}
return null;
}
private async Task<IReadOnlyList<string>?> ResolvePriceZoneKeysAsync(
SalesProduct product,
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
SalesProductPricePublishSnapshotDto? priceSnapshot,
string countryCode,
string? province,
string? city,
string? zipCode)
{
if (priceSnapshot?.PriceAreaInfos is { Count: > 0 })
{
return ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(
priceSnapshot,
countryCode,
province,
city,
zipCode);
}
var priceAreaId = ResolveEffectivePriceAreaId(product, rulesSnapshot, priceSnapshot);
if (!priceAreaId.HasValue)
{
return null;
}
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value);
if (area == null)
{
return null;
}
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value);
var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched(
rule,
countryCode,
province,
city,
zipCode));
if (matched == null)
{
return null;
}
var keys = ParcelBillingCalculator.BuildPriceZoneKeys(matched, area);
return keys.Count > 0 ? keys : null;
}
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,
Country receiverCountry)
{
if (input.Sender != null)
{
int? senderCountryId = null;
if (!string.IsNullOrWhiteSpace(input.Sender.CountryCode))
{
senderCountryId = (await GetCountryOrThrowAsync(input.Sender.CountryCode, "发件国家不存在")).Id;
}
var sender = await OrderRepository.InsertSenderAsync(new OrderSender
{
OrderId = order.Id,
SenderName = input.Sender.SenderName,
Company = input.Sender.Company,
Country = senderCountryId,
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 = receiverCountry.Id,
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
};
}
/// <summary>
/// 从会话解析有效客户(客户登录自动补全;管理员须传 customerId)。
/// </summary>
private Task<Customer> ResolveEffectiveCustomerAsync(int? requestedCustomerId = null) =>
OmsSessionHelper.ResolveSessionCustomerAsync(
CustomerRepository,
CurrentUser,
requestedCustomerId);
}