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(); /// /// 编辑订单详情(基本信息、发件人、收件人)。 /// 跟踪号变更时同步调用轨迹 Upsert(不走 Outbox)。 /// [HttpPost("/order/update")] public async Task 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 SyncSmallBagTrackUpsertWhenTrackNoChangedAsync(order, previousTrackNo); } return (await MapOrderOutputsAsync([order])).First(); } /// /// 修改订单销售产品:未付款且草稿/已预报/待排货/待支付可改; /// 有计费重时按新产品排货池自动重排货并重算应收/应付。 /// /// 入参:订单 ID、新销售产品编码。 /// 更新后的订单详情。 [HttpPost("/order/change-product")] public async Task ChangeProductAsync(OrderChangeProductInput input) { var newProductCode = input.ProductCode?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(newProductCode)) { throw new BusinessException("销售产品编码不能为空"); } var order = await GetOrderOrThrowAsync(input.Id); EnsureChangeProductAllowed(order); var oldProductCode = order.ProductCode?.Trim() ?? string.Empty; if (string.Equals(oldProductCode, newProductCode, StringComparison.OrdinalIgnoreCase)) { return (await MapOrderOutputsAsync([order])).First(); } var product = await SalesProductRepository.GetByProductCodeAsync(newProductCode); if (product == null || product.IsActive != EnableStatus.Enabled) { throw new BusinessException("销售产品不存在或未启用"); } var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true); order.ProductCode = product.ProductCode; order.ChannelCode = null; order.LabelTemplateId = null; order.AllocationPendingReason = null; order.AllocationTraceJson = null; order.AllocationTraceTime = null; order.AllocationScheme = null; order.DownstreamSupplierCode = null; order.DownstreamSupplierName = null; order.DownstreamAppKey = null; order.DownstreamAppId = null; order.ChannelForecastCode = null; var selectedServices = NormalizeAdditionalServiceCodes( OmsDocHelper.DeserializeJson>(order.AdditionalServices)); var allowed = ParseAdditionalServiceCodes(product.AdditionalServices); var kept = selectedServices.Where(code => allowed.Contains(code)).ToList(); order.AdditionalServices = SerializeAdditionalServices(kept); var snapshotBinding = await ResolveSnapshotBindingAsync(product, DateTime.Now); order.SalesProductRulesVersion = snapshotBinding.RulesVersion; order.SalesProductPriceVersion = snapshotBinding.PriceVersion; order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]); var hasChargeableWeight = TryGetPayableChargeableWeight(order, weights, out _); if (hasChargeableWeight) { var isDraft = string.Equals( order.OrderStatus, OrderStatusCodes.Draft, StringComparison.OrdinalIgnoreCase); // 草稿不在「重新排货」闸门内;有计费重时仍按新产品默认方案确认渠道并重写应付。 await ReallocateCoreAsync( order, scheme: AllocationSchemeCodes.CostLowest, serviceChannelId: null, enforceReallocateGate: !isDraft); order = await GetOrderOrThrowAsync(order.Id); EnsureReceivableMutable(order); await RecalculateReceivableCoreAsync(order); } order = await GetOrderOrThrowAsync(order.Id); var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true); var remark = string.IsNullOrWhiteSpace(oldProductCode) ? $"销售产品 → {product.ProductCode}" : $"销售产品 {oldProductCode} → {product.ProductCode}"; await OperationLogWriter.TryWriteAsync( order, OrderOperationActionCodes.ChangeProduct, beforeSnap, afterSnap, remark); return (await MapOrderOutputsAsync([order])).First(); } private static void EnsureChangeProductAllowed(Order order) { if (order.IsPaid != 0) { throw new BusinessException("订单已付款,不能修改销售产品"); } if (order.OrderStatus is not ( OrderStatusCodes.Draft or OrderStatusCodes.Predeclared or OrderStatusCodes.PendingAllocation or OrderStatusCodes.PendingPayment)) { throw new BusinessException( $"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus ?? string.Empty)}」,仅草稿/已预报/待排货/待支付可改销售产品"); } } private static bool TryGetPayableChargeableWeight( Order order, IReadOnlyList weights, out decimal chargeable) { chargeable = 0; try { var row = ResolvePayableWeightRow(order, weights); if (!row.ChargeableWeight.HasValue || row.ChargeableWeight <= 0) { return false; } chargeable = row.ChargeableWeight.Value; return true; } catch (BusinessException) { return false; } } /// /// 编辑订单件重尺并重算计费重。 /// [HttpPost("/order/weight/update")] public async Task 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(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(); } /// /// 重算订单应收费用。 /// [HttpPost("/order/fee/recalculate-receivable")] public async Task 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(); } /// /// 重算订单应付费用。 /// [HttpPost("/order/fee/recalculate-payable")] public async Task 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(); } /// /// 切换订单绑定的销售产品规则/价格发布版本(显式改契约后再重算)。 /// [HttpPost("/order/fee/bind-snapshot-versions")] public async Task 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(); 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(); } /// /// 解析绑定目标版本:CURRENT 取当前有效发布,否则按版本号校验存在。 /// private async Task 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(); } /// /// 重跑入库计费(应收 wipe+upsert + 排货应付)。用于队列投递失败或待排货补救。 /// [HttpPost("/order/fee/rerun-inbound-billing")] public async Task 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(); } /// /// 下载对账件重尺(downstream)导入模板。导入后只重算应付,不改应收。 /// [HttpGet("/order/weight/downstream-import-template")] public async Task DownloadDownstreamWeightImportTemplateAsync() { var bytes = await OrderDownstreamWeightImportHelper.BuildTemplateBytesAsync(); return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = OrderDownstreamWeightImportHelper.TemplateFileName }; } /// /// 导入对账件重尺:写入 Type=downstream,锁定当前渠道重算应付;不改应收。 /// [HttpPost("/order/weight/downstream-import")] public async Task 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; } /// /// 查询排货过程:优先返回快照;无快照或 live=true 时按当前规则现算(不落库)。 /// [HttpGet("/order/allocation-trace")] public async Task GetAllocationTraceAsync( [FromQuery] int id, [FromQuery] bool live = false) { var order = await GetOrderOrThrowAsync(id); if (!live && !string.IsNullOrWhiteSpace(order.AllocationTraceJson)) { try { var snapshot = JsonConvert.DeserializeObject(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 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 }; } } /// /// 指定渠道可选项:取选中订单销售产品排货池(含客户过滤)的交集。 /// [HttpPost("/order/fee/reallocate-channel-options")] public async Task> GetReallocateChannelOptionsAsync( [FromBody] OrderReallocateChannelOptionsInput input) { var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList(); if (orderIds.Count == 0) { return new OmsItemsResult { Items = [] }; } var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); if (orders.Count == 0) { return new OmsItemsResult { Items = [] }; } HashSet? intersection = null; var channelMeta = new Dictionary(); foreach (var order in orders) { if (string.IsNullOrWhiteSpace(order.ProductCode)) { return new OmsItemsResult { Items = [] }; } var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); if (product == null) { return new OmsItemsResult { 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(); 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 { Items = items }; } /// /// 单票重新排货:仅重写应付与服务渠道,不动应收。 /// [HttpPost("/order/fee/reallocate")] public async Task 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(); } /// /// 批量重新排货(同步兜底,队列关闭时使用)。 /// [HttpPost("/order/fee/reallocate-batch")] public async Task 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; } /// /// 未付款订单更新附加服务并重算当前阶段应收(可选尝试重算应付)。 /// [HttpPost("/order/additional-services/update")] public async Task 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(); } /// /// 附加服务变更后重算当前阶段应收(订单须未付款)。 /// 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); } /// /// 手动添加订单费用(应收或应付)。 /// [HttpPost("/order/fee/add")] public async Task 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); } /// /// 写入对账件重尺并锁定当前渠道重算应付(不改应收)。 /// 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 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; } } } /// /// 重新排货核心:按方案/指定渠道确认服务渠道并重写应付。 /// 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 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> 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>(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); } } }