admin/src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs
zzh 314c5ac02f
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
feat: 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码
- t_order_declaration 新增 ProductId、NonStdProductId、StdProductId(迁移 migrate_order_declaration_product_ids_20261019.sql)
- 下单/改单/开放接口/Excel 模板入参、订单详情出参、下游 createOrderOne 报文同步字段
- 自动触发公式目录增加三个字段
2026-10-19 15:48:16 +08:00

1549 lines
56 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 System.Globalization;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.ChannelsAggregate.Consts;
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);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeParties: true);
if (input.ClientNo != null)
{
var newClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
if (!string.IsNullOrWhiteSpace(newClientNo)
&& !string.Equals(newClientNo, order.ClientNo, StringComparison.OrdinalIgnoreCase)
&& order.CustomerId.HasValue)
{
var duplicated = await OrderRepository.GetByCustomerAndClientNoAsync(order.CustomerId.Value, newClientNo);
if (duplicated != null && duplicated.Id != order.Id)
{
throw new BusinessException("客户单号已存在");
}
}
order.ClientNo = newClientNo;
}
var previousTrackNo = order.TrackNo;
if (input.TrackNo != null)
{
order.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
}
if (input.DownstreamServiceNo != null)
{
order.DownstreamServiceNo = string.IsNullOrWhiteSpace(input.DownstreamServiceNo)
? null
: input.DownstreamServiceNo.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)));
}
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeParties: true);
// 前端保存基本信息时总会带 items;仅在无字段级 diff 时用备注兜底,避免掩盖真实字段变更
var fieldChanges = OrderOperationLogSnapshot.Diff(beforeSnap, afterSnap);
var remark = input.Items != null && fieldChanges.Count == 0 ? "申报信息已更新" : null;
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.EditBasic,
beforeSnap,
afterSnap,
remark);
if (input.TrackNo != null)
{
await EnqueueSmallBagTrackUpsertWhenTrackNoChangedAsync(order, previousTrackNo);
}
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);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
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);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.EditWeight,
beforeSnap,
afterSnap);
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);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
EnsureReceivableMutable(order);
await RecalculateReceivableCoreAsync(order);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.RecalcReceivable,
beforeSnap,
afterSnap);
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);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await RecalculatePayableCoreAsync(order);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.RecalcPayable,
beforeSnap,
afterSnap);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 切换订单绑定的销售产品规则/价格发布版本(显式改契约后再重算)。
/// </summary>
[HttpPost("/order/fee/bind-snapshot-versions")]
public async Task<OrderOutput> BindSnapshotVersionsAsync([FromBody] OrderBindSnapshotVersionsInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
if (string.IsNullOrWhiteSpace(order.ProductCode))
{
throw new BusinessException("订单未绑定销售产品,无法切换版本");
}
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
if (product == null)
{
throw new BusinessException("销售产品不存在");
}
var wantRules = !string.IsNullOrWhiteSpace(input.RulesVersion);
var wantPrice = !string.IsNullOrWhiteSpace(input.PriceVersion);
if (!wantRules && !wantPrice)
{
throw new BusinessException("请至少指定规则版本或价格版本");
}
var remarkParts = new List<string>();
if (wantRules)
{
var resolved = await ResolveBindTargetVersionAsync(
product,
RulesOperatorType,
input.RulesVersion!.Trim(),
"规则");
if (!string.Equals(order.SalesProductRulesVersion, resolved, StringComparison.Ordinal))
{
remarkParts.Add($"规则版本 {order.SalesProductRulesVersion ?? "-"}→{resolved}");
order.SalesProductRulesVersion = resolved;
}
}
if (wantPrice)
{
var resolved = await ResolveBindTargetVersionAsync(
product,
PriceOperatorType,
input.PriceVersion!.Trim(),
"价格");
if (!string.Equals(order.SalesProductPriceVersion, resolved, StringComparison.Ordinal))
{
remarkParts.Add($"价格版本 {order.SalesProductPriceVersion ?? "-"}→{resolved}");
order.SalesProductPriceVersion = resolved;
}
}
if (remarkParts.Count == 0 && !input.RecalculateReceivable && !input.RecalculatePayable)
{
throw new BusinessException("目标版本与当前绑定相同,且未勾选重算");
}
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
if (input.RecalculateReceivable)
{
EnsureReceivableMutable(order);
await RecalculateReceivableCoreAsync(order);
remarkParts.Add("并重算应收");
}
if (input.RecalculatePayable)
{
await RecalculatePayableCoreAsync(order);
remarkParts.Add("并重算应付");
}
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.BindSnapshotVersions,
beforeSnap,
afterSnap,
remarkParts.Count == 0 ? null : string.Join(';', remarkParts));
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 解析绑定目标版本:CURRENT 取当前有效发布,否则按版本号校验存在。
/// </summary>
private async Task<string> ResolveBindTargetVersionAsync(
SalesProduct product,
string operatorType,
string versionOrCurrent,
string label)
{
if (string.Equals(versionOrCurrent, "CURRENT", StringComparison.OrdinalIgnoreCase))
{
var effective = await SalesProductRepository.GetEffectivePublishRecordAsync(
product.Id,
operatorType,
DateTime.Now);
if (effective == null || string.IsNullOrWhiteSpace(effective.Version))
{
throw new BusinessException($"未命中当前有效{label}发布版本");
}
return effective.Version.Trim();
}
var record = await SalesProductRepository.GetPublishRecordByVersionAsync(
product.Id,
operatorType,
versionOrCurrent);
if (record == null || string.IsNullOrWhiteSpace(record.Version))
{
throw new BusinessException($"未找到{label}发布版本:{versionOrCurrent}");
}
return record.Version.Trim();
}
/// <summary>
/// 重跑入库计费(应收 wipe+upsert + 排货应付)。用于队列投递失败或待排货补救。
/// </summary>
[HttpPost("/order/fee/rerun-inbound-billing")]
public async Task<OrderOutput> RerunInboundBillingAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
EnsureReceivableMutable(order);
await ProcessInboundBillingForBatchAsync(order.Id);
order = await GetOrderOrThrowAsync(input.Id);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 下载对账件重尺(downstream)导入模板。导入后只重算应付,不改应收。
/// </summary>
[HttpGet("/order/weight/downstream-import-template")]
public async Task<FileContentResult> DownloadDownstreamWeightImportTemplateAsync()
{
var bytes = await OrderDownstreamWeightImportHelper.BuildTemplateBytesAsync();
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = OrderDownstreamWeightImportHelper.TemplateFileName
};
}
/// <summary>
/// 导入对账件重尺:写入 Type=downstream,锁定当前渠道重算应付;不改应收。
/// </summary>
[HttpPost("/order/weight/downstream-import")]
public async Task<OrderDownstreamWeightImportOutput> ImportDownstreamWeightsAsync(
[FromForm] OrderDownstreamWeightImportInput input)
{
if (input.File == null || input.File.Length == 0)
{
throw new BusinessException("请上传 Excel 文件");
}
await using var stream = input.File.OpenReadStream();
List<(int RowNumber, OrderDownstreamWeightImportRow Row)> parsedRows;
try
{
parsedRows = await OrderDownstreamWeightImportHelper.ParseImportRowsAsync(stream);
}
catch (Exception ex)
{
throw new BusinessException($"Excel 解析失败:{ex.Message}");
}
if (parsedRows.Count == 0)
{
throw new BusinessException("Excel 中无有效数据行");
}
if (parsedRows.Count > OrderDownstreamWeightImportHelper.MaxRows)
{
throw new BusinessException(
$"单次最多导入 {OrderDownstreamWeightImportHelper.MaxRows} 行,当前 {parsedRows.Count} 行");
}
var output = new OrderDownstreamWeightImportOutput();
foreach (var (rowNumber, row) in parsedRows)
{
try
{
OrderDownstreamWeightImportHelper.ValidateRow(row);
await ApplyDownstreamWeightAndRecalculatePayableAsync(row);
output.SuccessCount++;
}
catch (Exception ex)
{
output.FailCount++;
output.Failures.Add(new OrderDownstreamWeightImportFailRow
{
RowNumber = rowNumber,
WaybillNo = row.WaybillNo,
Reason = ex is BusinessException or InvalidOperationException
? ex.Message
: $"导入失败:{ex.Message}"
});
}
}
return output;
}
/// <summary>
/// 查询排货过程:优先返回快照;无快照或 live=true 时按当前规则现算(不落库)。
/// </summary>
[HttpGet("/order/allocation-trace")]
public async Task<OrderAllocationTraceOutput> GetAllocationTraceAsync(
[FromQuery] int id,
[FromQuery] bool live = false)
{
var order = await GetOrderOrThrowAsync(id);
if (!live && !string.IsNullOrWhiteSpace(order.AllocationTraceJson))
{
try
{
var snapshot = JsonConvert.DeserializeObject<OrderAllocationTraceOutput>(order.AllocationTraceJson);
if (snapshot != null)
{
snapshot.OrderId = order.Id;
snapshot.FromSnapshot = true;
snapshot.TraceTime = order.AllocationTraceTime ?? snapshot.TraceTime;
if (string.IsNullOrWhiteSpace(snapshot.Scheme))
{
snapshot.Scheme = order.AllocationScheme;
snapshot.SchemeName = ResolveAllocationSchemeName(order.AllocationScheme);
}
snapshot.Candidates = SortAllocationCandidates(snapshot.Scheme, snapshot.Candidates ?? []);
return snapshot;
}
}
catch
{
// 快照损坏则走现算
}
}
return await BuildLiveAllocationTraceAsync(order);
}
private async Task<OrderAllocationTraceOutput> BuildLiveAllocationTraceAsync(Order order)
{
if (string.IsNullOrWhiteSpace(order.ProductCode))
{
return new OrderAllocationTraceOutput
{
OrderId = order.Id,
FromSnapshot = false,
FailReason = "订单未绑定销售产品",
TraceTime = DateTime.Now
};
}
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
if (product == null)
{
return new OrderAllocationTraceOutput
{
OrderId = order.Id,
FromSnapshot = false,
FailReason = "销售产品不存在",
TraceTime = DateTime.Now
};
}
try
{
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
var weightRow = ResolvePayableWeightRow(order, weights);
var chargeable = weightRow.ChargeableWeight ?? 0;
// 现算与重新排货一致:规则分区 + 渠道买价均取当前有效发布。
var snapshotBinding = await ResolveSnapshotBindingAsync(product, DateTime.Now);
var billing = await ResolveOrderBillingContextAsync(
order,
product,
chargeable,
snapshotBinding);
var (_, chargingStage) = ResolveReceivableBillingStage(order);
string? schemeOverride = null;
int? forceChannelId = null;
if (AllocationSchemeCodes.IsDesignated(order.AllocationScheme)
&& !string.IsNullOrWhiteSpace(order.ChannelCode))
{
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x =>
x.ChannelCode == order.ChannelCode.Trim());
if (channel != null)
{
schemeOverride = AllocationSchemeCodes.Designated;
forceChannelId = channel.Id;
}
}
else if (AllocationSchemeCodes.IsAutoScheme(order.AllocationScheme))
{
schemeOverride = order.AllocationScheme;
}
var evaluation = await EvaluateAllocationAsync(
order,
product,
billing.SnapshotBinding,
billing.ZoneKeys,
chargeable,
chargingStage,
schemeOverride,
forceChannelId);
var output = BuildAllocationTraceOutput(
order.Id,
evaluation,
fromSnapshot: false,
selectedChannelCode: evaluation.Selected?.Channel.ChannelCode ?? order.ChannelCode);
output.FromSnapshot = false;
if (string.IsNullOrWhiteSpace(output.FailReason)
&& !string.IsNullOrWhiteSpace(order.AllocationPendingReason)
&& evaluation.Selected == null)
{
output.FailReason = order.AllocationPendingReason;
}
return output;
}
catch (Exception ex)
{
return new OrderAllocationTraceOutput
{
OrderId = order.Id,
FromSnapshot = false,
Scheme = order.AllocationScheme,
SchemeName = ResolveAllocationSchemeName(order.AllocationScheme),
FailReason = ex is BusinessException be ? be.Message : ex.Message,
TraceTime = DateTime.Now
};
}
}
/// <summary>
/// 指定渠道可选项:取选中订单销售产品排货池(含客户过滤)的交集。
/// </summary>
[HttpPost("/order/fee/reallocate-channel-options")]
public async Task<OmsItemsResult<OrderReallocateChannelOptionOutput>> GetReallocateChannelOptionsAsync(
[FromBody] OrderReallocateChannelOptionsInput input)
{
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
if (orderIds.Count == 0)
{
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
}
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
if (orders.Count == 0)
{
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
}
HashSet<int>? intersection = null;
var channelMeta = new Dictionary<int, (string Code, string? Name)>();
foreach (var order in orders)
{
if (string.IsNullOrWhiteSpace(order.ProductCode))
{
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
}
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
if (product == null)
{
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
}
var binding = await ResolveSnapshotBindingAsync(product, DateTime.Now);
var providers = await ResolveAllocationProvidersAsync(product, binding.Rules);
var ruleContext = await OrderAutoTriggerService.BuildPostCommitContextAsync(order);
var filtered = AllocationResolver.FilterProvidersDetailed(
providers,
order.CustomerId,
ruleContext).Passed;
var channelIds = new HashSet<int>();
foreach (var provider in filtered)
{
if (provider.ServiceChannelId is not > 0)
{
continue;
}
var channelId = provider.ServiceChannelId.Value;
if (!channelMeta.TryGetValue(channelId, out _))
{
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
if (channel == null || channel.IsActive != EnableStatus.Enabled)
{
continue;
}
channelMeta[channelId] = (channel.ChannelCode, channel.ChannelName);
}
channelIds.Add(channelId);
}
intersection = intersection == null
? channelIds
: intersection.Intersect(channelIds).ToHashSet();
if (intersection.Count == 0)
{
break;
}
}
var items = (intersection ?? [])
.Where(id => channelMeta.ContainsKey(id))
.Select(id => new OrderReallocateChannelOptionOutput
{
ServiceChannelId = id,
ChannelCode = channelMeta[id].Code,
ChannelName = channelMeta[id].Name
})
.OrderBy(x => x.ChannelCode, StringComparer.OrdinalIgnoreCase)
.ToList();
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = items };
}
/// <summary>
/// 单票重新排货:仅重写应付与服务渠道,不动应收。
/// </summary>
[HttpPost("/order/fee/reallocate")]
public async Task<OrderOutput> ReallocateAsync([FromBody] OrderReallocateInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await ReallocateCoreAsync(order, input.Scheme, input.ServiceChannelId);
order = await GetOrderOrThrowAsync(input.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
var remark = string.IsNullOrWhiteSpace(input.Scheme) ? null : $"scheme={input.Scheme.Trim()}";
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.Reallocate,
beforeSnap,
afterSnap,
remark);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 批量重新排货(同步兜底,队列关闭时使用)。
/// </summary>
[HttpPost("/order/fee/reallocate-batch")]
public async Task<OrderBatchActionResultOutput> ReallocateBatchAsync([FromBody] OrderReallocateBatchInput input)
{
NormalizeReallocateOptions(input.Scheme, input.ServiceChannelId, out var scheme, out var channelId);
var result = new OrderBatchActionResultOutput();
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
var remark = string.IsNullOrWhiteSpace(scheme) ? null : $"scheme={scheme}";
foreach (var orderId in orderIds)
{
try
{
var order = await GetOrderOrThrowAsync(orderId);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await ReallocateCoreAsync(order, scheme, channelId);
order = await GetOrderOrThrowAsync(orderId);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.Reallocate,
beforeSnap,
afterSnap,
remark);
result.SuccessCount++;
}
catch (Exception ex)
{
result.Failures.Add(new OrderBatchActionFailureOutput
{
OrderId = orderId,
Reason = ex is BusinessException be ? be.Message : ex.Message
});
}
}
return result;
}
/// <summary>
/// 未付款订单更新附加服务并重算当前阶段应收(可选尝试重算应付)。
/// </summary>
[HttpPost("/order/additional-services/update")]
public async Task<OrderOutput> UpdateAdditionalServicesAsync(OrderAdditionalServicesUpdateInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
EnsureReceivableMutable(order);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
if (string.IsNullOrWhiteSpace(order.ProductCode))
{
throw new BusinessException("订单未绑定销售产品,无法更新附加服务");
}
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
if (product == null)
{
throw new BusinessException("销售产品不存在");
}
EnsureAdditionalServicesAllowed(product, input.AdditionalServices);
order.AdditionalServices = SerializeAdditionalServices(input.AdditionalServices);
await RebillReceivableAfterAdditionalServicesChangeAsync(order, product);
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.UpdateAdditionalServices,
beforeSnap,
afterSnap);
return (await MapOrderOutputsAsync([order])).First();
}
/// <summary>
/// 附加服务变更后重算当前阶段应收(订单须未付款)。
/// </summary>
private async Task RebillReceivableAfterAdditionalServicesChangeAsync(Order order, SalesProduct product)
{
EnsureReceivableMutable(order);
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 billing = await ResolveOrderBillingContextAsync(
order,
product,
weightRow.ChargeableWeight ?? 0,
snapshotBinding);
if (chargingStage == OrderFeeChargingStageCodes.Inbound)
{
await OrderRepository.DeleteReceivableFeesAsync(order.Id);
await UpsertReceivableFeesAsync(order, billing, chargingStage);
}
else
{
await UpsertReceivableFeesAsync(
order,
billing,
chargingStage,
replaceExistingReceivable: true);
}
await RefreshMarketingPayableAmountAsync(order);
await TryConfirmServiceChannelAndPayableAsync(order, product, billing, chargingStage);
}
/// <summary>
/// 手动添加订单费用(应收或应付)。
/// </summary>
[HttpPost("/order/fee/add")]
public async Task<OrderOutput> AddOrderFeeAsync(OrderFeeAddInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
var type = input.Type?.Trim().ToLowerInvariant() ?? string.Empty;
if (type is not (OrderFeeTypeCodes.Receivable or OrderFeeTypeCodes.Payable))
{
throw new BusinessException("费用类型无效");
}
if (type == OrderFeeTypeCodes.Receivable)
{
EnsureReceivableMutable(order);
}
var costItem = input.CostItem?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(costItem))
{
throw new BusinessException("费用项不能为空");
}
if (OrderFeeDiscountCostItems.IsDiscount(costItem))
{
throw new BusinessException("优惠券抵扣、会员折扣由支付自动生成,不能手工添加");
}
if (input.Amount <= 0)
{
throw new BusinessException("金额必须大于0");
}
var currency = input.Currency?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(currency))
{
throw new BusinessException("币种不能为空");
}
var chargingStage = input.ChargingStage?.Trim() ?? string.Empty;
if (chargingStage is not (
OrderFeeChargingStageCodes.Forecast
or OrderFeeChargingStageCodes.Inbound
or OrderFeeChargingStageCodes.Outbound))
{
throw new BusinessException("计费阶段无效");
}
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
var fee = new OrderFee
{
Type = type,
CostItem = costItem,
Amount = input.Amount,
Currency = currency,
ChargingStage = chargingStage,
DownstreamProvider = string.IsNullOrWhiteSpace(input.DownstreamProvider)
? null
: input.DownstreamProvider.Trim(),
PriceNo = string.IsNullOrWhiteSpace(input.PriceNo) ? null : input.PriceNo.Trim(),
PriceEffectiveTime = input.PriceEffectiveTime,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
};
await OrderRepository.UpsertFeesAsync(order.Id, [fee]);
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
var typeName = type == OrderFeeTypeCodes.Receivable ? "应收" : "应付";
var amountText = input.Amount.ToString("0.####", CultureInfo.InvariantCulture);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.AddFee,
beforeSnap,
afterSnap,
$"新增{typeName}「{costItem}」{currency} {amountText}");
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 billing = await ResolveOrderBillingContextAsync(
order,
product,
weightRow.ChargeableWeight ?? 0,
snapshotBinding);
await UpsertReceivableFeesAsync(
order,
billing,
chargingStage,
replaceExistingReceivable: true);
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
}
private async Task RecalculatePayableCoreAsync(Order order)
{
// 已有渠道时锁定渠道只重算金额,避免「重算应付」误换渠道清下游单号。
await ReallocateCoreAsync(
order,
scheme: null,
serviceChannelId: null,
enforceReallocateGate: false,
lockCurrentChannel: true);
}
/// <summary>
/// 写入对账件重尺并锁定当前渠道重算应付(不改应收)。
/// </summary>
private async Task ApplyDownstreamWeightAndRecalculatePayableAsync(OrderDownstreamWeightImportRow row)
{
var order = await OrderRepository.GetByWaybillNoAsync(row.WaybillNo!.Trim());
if (order == null)
{
throw new BusinessException("未找到对应订单,请检查单号");
}
if (string.IsNullOrWhiteSpace(order.ChannelCode))
{
throw new BusinessException("订单未确认服务渠道,无法按对账重量重算应付");
}
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();
var downstream = weights.FirstOrDefault(x =>
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase));
if (downstream == null)
{
throw new BusinessException("订单缺少下游件重尺记录");
}
// 尺寸可选:未填则沿用下游已有,仍缺则回落收货→发货→预报。
var length = row.Length ?? downstream.Length;
var width = row.Width ?? downstream.Width;
var height = row.Height ?? downstream.Height;
if (length is not > 0 || width is not > 0 || height is not > 0)
{
FillDimensionsFromFallback(weights, ref length, ref width, ref height);
}
if (length is not > 0 || width is not > 0 || height is not > 0)
{
throw new BusinessException("件重尺尺寸不完整,请在 Excel 填写长/宽/高,或先完善订单收货/发货尺寸");
}
ApplyMeasuredDimensions(downstream, length.Value, width.Value, height.Value, row.ActualWeight!.Value);
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, downstream);
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
await ReallocateCoreAsync(
order,
scheme: null,
serviceChannelId: null,
enforceReallocateGate: false,
lockCurrentChannel: true,
preferDownstreamWeight: true);
}
private static void FillDimensionsFromFallback(
IReadOnlyList<OrderWeight> weights,
ref decimal? length,
ref decimal? width,
ref decimal? height)
{
foreach (var type in new[]
{
OrderWeightTypeCodes.Receive,
OrderWeightTypeCodes.Ship,
OrderWeightTypeCodes.Forecast
})
{
var src = weights.FirstOrDefault(x =>
string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
if (src == null)
{
continue;
}
if (length is not > 0)
{
length = src.Length;
}
if (width is not > 0)
{
width = src.Width;
}
if (height is not > 0)
{
height = src.Height;
}
if (length is > 0 && width is > 0 && height is > 0)
{
return;
}
}
}
/// <summary>
/// 重新排货核心:按方案/指定渠道确认服务渠道并重写应付。
/// </summary>
private async Task ReallocateCoreAsync(
Order order,
string? scheme,
int? serviceChannelId,
bool enforceReallocateGate = true,
bool lockCurrentChannel = false,
bool preferDownstreamWeight = false)
{
string? schemeOverride = null;
int? forceChannelId = null;
if (enforceReallocateGate)
{
EnsureReallocateAllowed(order);
NormalizeReallocateOptions(scheme, serviceChannelId, out var normalizedScheme, out forceChannelId);
schemeOverride = AllocationSchemeCodes.IsDesignated(normalizedScheme)
? AllocationSchemeCodes.Designated
: normalizedScheme;
}
else if (lockCurrentChannel && !string.IsNullOrWhiteSpace(order.ChannelCode))
{
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x =>
x.ChannelCode == order.ChannelCode.Trim());
if (channel == null)
{
throw new BusinessException("订单服务渠道不存在,无法重算应付");
}
forceChannelId = channel.Id;
schemeOverride = AllocationSchemeCodes.Designated;
}
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 = preferDownstreamWeight
? weights.FirstOrDefault(x =>
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase))
?? throw new BusinessException("缺少件重尺类型 downstream")
: ResolvePayableWeightRow(order, weights);
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
{
throw new BusinessException("计费重无效,请先完善件重尺");
}
// 重新排货:产品规则/分区与渠道买价均取当前有效发布,避免沿用下单时旧快照。
// 对账重算 / 普通重算应付:沿用订单快照绑定(分区等),买价仍由排货评估取当前有效发布。
// 锁渠道重算应付:允许渠道不在旧规则排货池内(渠道已确认,只重算买价)。
var snapshotBinding = enforceReallocateGate
? await ResolveSnapshotBindingAsync(product, DateTime.Now)
: await ResolveSnapshotBindingFromOrderAsync(product, order);
var billing = await ResolveOrderBillingContextAsync(
order,
product,
weightRow.ChargeableWeight ?? 0,
snapshotBinding);
var (_, chargingStage) = ResolveReceivableBillingStage(order);
// 对账件重尺导入:费用阶段记 outbound(终稿成本),与入库应收阶段解耦。
if (preferDownstreamWeight)
{
chargingStage = OrderFeeChargingStageCodes.Outbound;
}
await ConfirmServiceChannelAndPayableAsync(
order,
product,
billing,
chargingStage,
schemeOverride,
forceChannelId,
allowDesignatedOutsidePool: lockCurrentChannel);
// 重新排货成功后写回规则版本,使后续重算应付与当前排货池一致;卖价版本保持契约,不随排货改写。
if (enforceReallocateGate)
{
order.SalesProductRulesVersion = snapshotBinding.RulesVersion;
}
if (string.Equals(order.OrderStatus, OrderStatusCodes.PendingAllocation, StringComparison.OrdinalIgnoreCase))
{
order.OrderStatus = OrderStatusCodes.PendingPayment;
}
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
}
private static void EnsureReallocateAllowed(Order order)
{
// 已付款也可排货:只重算应付与服务渠道,不改应收。
if (order.OrderStatus is not (
OrderStatusCodes.Predeclared
or OrderStatusCodes.PendingAllocation
or OrderStatusCodes.PendingPayment
or OrderStatusCodes.PendingShip))
{
throw new BusinessException(
$"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus ?? string.Empty)}」,仅已预报/待排货/待支付/待发货可重新排货");
}
}
private static void NormalizeReallocateOptions(
string? scheme,
int? serviceChannelId,
out string normalizedScheme,
out int? forceServiceChannelId)
{
normalizedScheme = (scheme ?? string.Empty).Trim();
forceServiceChannelId = null;
if (AllocationSchemeCodes.IsDesignated(normalizedScheme))
{
if (serviceChannelId is not > 0)
{
throw new BusinessException("指定渠道时请选择服务渠道");
}
forceServiceChannelId = serviceChannelId;
return;
}
if (!AllocationSchemeCodes.IsAutoScheme(normalizedScheme))
{
throw new BusinessException("排货方案无效,请选择价格优先/时效优先/综合最优/指定渠道");
}
if (serviceChannelId is > 0)
{
// 自动方案忽略渠道入参,避免误传
forceServiceChannelId = null;
}
}
private static OrderWeight ResolvePayableWeightRow(Order order, IReadOnlyList<OrderWeight> weights)
{
var (_, chargingStage) = ResolveReceivableBillingStage(order);
var weightType = chargingStage switch
{
OrderFeeChargingStageCodes.Inbound => OrderWeightTypeCodes.Receive,
OrderFeeChargingStageCodes.Outbound => OrderWeightTypeCodes.Ship,
_ => OrderWeightTypeCodes.Forecast
};
return weights.FirstOrDefault(x =>
string.Equals(x.Type, weightType, StringComparison.OrdinalIgnoreCase))
?? 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.PendingAllocation 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 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,
DeclareElement = x.DeclareElement,
ProductId = x.ProductId?.Trim(),
NonStdProductId = x.NonStdProductId?.Trim(),
StdProductId = x.StdProductId?.Trim(),
SkuImage = OmsObjectStorageExtensions.ToStorageKey(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);
}
}
}