From 358738c33372bb1af084d7d80980b2d0489270d1 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 9 Oct 2026 16:12:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=BD=E6=94=B6=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=94=B9=E4=BF=A1=E6=81=AF/=E4=BB=B6=E9=87=8D?= =?UTF-8?q?=E5=B0=BA/=E9=87=8D=E7=AE=97=E8=B4=B9=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=90=8E=E8=87=AA=E5=8A=A8=E9=A2=84=E6=8A=A5?= =?UTF-8?q?=E4=B8=8B=E6=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创建成功后自动下游预报回写承运单号并升为已预约;详情可改揽收信息与件重尺并按卖买价重算。 Co-authored-by: Cursor --- .../Dtos/Orders/PickupOrderDtos.cs | 38 +++ .../Orders/PickupOrderFeePushService.cs | 115 +++++++-- .../Services/Orders/PickupOrderService.cs | 239 ++++++++++++++++++ 3 files changed, 375 insertions(+), 17 deletions(-) diff --git a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs index d9fd2fd..3633ccd 100644 --- a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs +++ b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs @@ -176,6 +176,44 @@ public class PickupOrderRescheduleInput public DateTime ExpectPickupTime { get; set; } } +/// +/// 修改揽收信息(未支付且 draft/scheduled)。 +/// +public class PickupOrderUpdateInfoInput +{ + [Required] + public int Id { get; set; } + + public int? WarehouseId { get; set; } + + public int? SenderAddressId { get; set; } + + public DateTime? ExpectPickupTime { get; set; } + + public string? CarrierService { get; set; } + + public string? CarrierWaybillNo { get; set; } + + [StringLength(512)] + public string? Remark { get; set; } +} + +/// +/// 修改件重尺(未支付;不改费用,需另行重算)。 +/// +public class PickupOrderUpdateMeasureInput +{ + [Required] + public int Id { get; set; } + + [Required] + public decimal TotalActualWeight { get; set; } + + [Required] + [MinLength(1)] + public List PackageSizes { get; set; } = []; +} + public class PickupOrderRejectItem { public int OrderId { get; set; } diff --git a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs index 19eef43..fa7489d 100644 --- a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs +++ b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs @@ -74,15 +74,66 @@ public class PickupOrderFeePushService : IScopedDependency var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo) ?? throw new BusinessException($"揽收单不存在:承运运单号 {carrierWaybillNo}"); - if (pickup.PayStatus == PickupPayStatusCodes.Paid) + EnsureCanReceiveMeasure(pickup); + + return await ApplyMeasureAndBillAsync( + pickup, + input.TotalActualWeight, + sizes, + input.CarrierFreightWeight, + input.CarrierQuotedFreightAmount, + input.CarrierQuotedReceivableAmount, + input.ValueInfoList); + } + + /// + /// 按已保存(或入参)件重尺,用当前承运服务卖价/买价重算出账。 + /// + public async Task RecalcByPickupAsync( + PickupOrder pickup, + decimal? totalActualWeightOverride = null, + IReadOnlyList? sizesOverride = null) + { + EnsureCanReceiveMeasure(pickup); + + var weight = totalActualWeightOverride ?? pickup.TotalActualWeight ?? 0; + if (weight <= 0) { - throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用"); + throw new BusinessException("请先填写实重后再重算费用"); } - if (PickupOrderStatusCodes.IsTerminal(pickup.Status) - && pickup.Status != PickupOrderStatusCodes.Inbound) + var sizes = sizesOverride != null && sizesOverride.Count > 0 + ? NormalizeSizes(sizesOverride.ToList()) + : NormalizeSizes( + OmsDocHelper.DeserializeJson>(pickup.PackageSizesJson)); + if (sizes.Count == 0) { - throw new BusinessException("当前揽收单状态不可接收件重尺"); + throw new BusinessException("请先填写件重尺尺寸后再重算费用"); + } + + return await ApplyMeasureAndBillAsync(pickup, weight, sizes); + } + + public async Task ApplyMeasureAndBillAsync( + PickupOrder pickup, + decimal totalActualWeight, + IReadOnlyList sizes, + decimal? carrierFreightWeight = null, + decimal? carrierQuotedFreightAmount = null, + decimal? carrierQuotedReceivableAmount = null, + List? valueInfoList = null) + { + if (totalActualWeight <= 0) + { + throw new BusinessException("totalActualWeight 必须大于 0"); + } + + var normalizedSizes = sizes is List list + ? NormalizeSizes(list) + : NormalizeSizes(sizes.ToList()); + if (normalizedSizes.Count == 0) + { + throw new BusinessException("packageSizes 不能为空"); } if (string.IsNullOrWhiteSpace(pickup.CarrierService)) @@ -93,14 +144,14 @@ public class PickupOrderFeePushService : IScopedDependency var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim()) ?? throw new BusinessException($"未找到承运服务对应销售产品「{pickup.CarrierService}」"); - var totalVolume = sizes.Sum(x => x.Length * x.Width * x.Height * x.Count); + var totalVolume = normalizedSizes.Sum(x => x.Length * x.Width * x.Height * x.Count); var weightRow = new OrderWeight { Type = OrderWeightTypeCodes.Receive, Length = totalVolume, Width = 1, Height = 1, - ActualWeight = input.TotalActualWeight, + ActualWeight = totalActualWeight, Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1) }; @@ -187,17 +238,33 @@ public class PickupOrderFeePushService : IScopedDependency var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal; var other = Math.Max(0, receivableTotal - freight); - pickup.TotalActualWeight = input.TotalActualWeight; + pickup.TotalActualWeight = totalActualWeight; pickup.VolumetricWeight = volumetricWeight; pickup.ChargeableWeight = chargeableWeight; - pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes); - pickup.CarrierFreightWeight = input.CarrierFreightWeight; - pickup.CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount; - pickup.CarrierQuotedReceivableAmount = input.CarrierQuotedReceivableAmount; - pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput + pickup.PackageSizesJson = OmsDocHelper.SerializeJson(normalizedSizes); + if (carrierFreightWeight.HasValue) { - ValueInfoList = input.ValueInfoList ?? [] - }); + pickup.CarrierFreightWeight = carrierFreightWeight; + } + + if (carrierQuotedFreightAmount.HasValue) + { + pickup.CarrierQuotedFreightAmount = carrierQuotedFreightAmount; + } + + if (carrierQuotedReceivableAmount.HasValue) + { + pickup.CarrierQuotedReceivableAmount = carrierQuotedReceivableAmount; + } + + if (valueInfoList != null) + { + pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput + { + ValueInfoList = valueInfoList + }); + } + pickup.ReceivableFeeJson = OmsDocHelper.SerializeJson(receivableFees); pickup.PayableFeeJson = OmsDocHelper.SerializeJson(payableFees); pickup.FreightAmount = freight; @@ -211,15 +278,29 @@ public class PickupOrderFeePushService : IScopedDependency return new PickupOpenApiPushFeeResult { PickupNo = pickup.PickupNo, - TotalActualWeight = input.TotalActualWeight, + TotalActualWeight = totalActualWeight, ChargeableWeight = chargeableWeight, VolumetricWeight = volumetricWeight, ReceivableAmount = receivableTotal, CarrierPayableAmount = payableTotal, - CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount + CarrierQuotedFreightAmount = pickup.CarrierQuotedFreightAmount }; } + private static void EnsureCanReceiveMeasure(PickupOrder pickup) + { + if (pickup.PayStatus == PickupPayStatusCodes.Paid) + { + throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用"); + } + + if (PickupOrderStatusCodes.IsTerminal(pickup.Status) + && pickup.Status != PickupOrderStatusCodes.Inbound) + { + throw new BusinessException("当前揽收单状态不可接收件重尺"); + } + } + private async Task<(decimal VolumetricWeight, decimal ChargeableWeight)> CalculateChargeableAsync( SalesProduct product, OrderWeight weight) diff --git a/src/Oms.Application/Services/Orders/PickupOrderService.cs b/src/Oms.Application/Services/Orders/PickupOrderService.cs index 4ee5162..b6e5d8a 100644 --- a/src/Oms.Application/Services/Orders/PickupOrderService.cs +++ b/src/Oms.Application/Services/Orders/PickupOrderService.cs @@ -69,6 +69,9 @@ public class PickupOrderService : ApplicationService private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer => LazyServiceProvider.GetRequiredService(); + private PickupOrderFeePushService PickupOrderFeePushService => + LazyServiceProvider.GetRequiredService(); + /// /// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。 /// @@ -274,6 +277,19 @@ public class PickupOrderService : ApplicationService await OrderRepository.UpdateAsync(order); } + // 已选承运服务时:创建后自动预报下游拿承运运单号(失败不影响建单) + if (!string.IsNullOrWhiteSpace(entity.CarrierService)) + { + try + { + await DownstreamForwardCoreAsync(entity); + } + catch + { + // 建单已成功,预报失败由列表「预报下游」重试 + } + } + return await MapOutputAsync(entity, includeOrders: true); } @@ -551,6 +567,213 @@ public class PickupOrderService : ApplicationService return await MapOutputAsync(pickup, includeOrders: true); } + /// + /// 修改揽收信息(未支付且 draft/scheduled)。 + /// + [HttpPost("/order/pickup-order/update-info")] + public async Task UpdateInfoAsync([FromBody] PickupOrderUpdateInfoInput input) + { + var pickup = await GetOwnedPickupOrThrowAsync(input.Id); + EnsureUnpaidEditable(pickup); + if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status)) + { + throw new BusinessException("当前状态不可修改揽收信息"); + } + + var warehouseChanged = false; + var expectTimeChanged = false; + var senderChanged = false; + + if (input.WarehouseId is > 0 && input.WarehouseId.Value != pickup.WarehouseId) + { + var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId.Value); + if (warehouse == null) + { + throw new BusinessException("仓库不存在"); + } + + pickup.WarehouseId = input.WarehouseId.Value; + warehouseChanged = true; + } + + if (input.SenderAddressId is > 0 && input.SenderAddressId.Value != pickup.SenderAddressId) + { + var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync( + input.SenderAddressId.Value, + pickup.CustomerId); + if (senderAddress == null) + { + throw new BusinessException("揽收地址不存在"); + } + + var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType) + ? CustomerAddressTypeCodes.Sender + : senderAddress.AddressType; + if (!CustomerAddressTypeCodes.IsPickupEligible(addressType)) + { + throw new BusinessException("揽收单请选择「国内揽收」类型地址"); + } + + string? senderCountryCode = null; + if (senderAddress.Country.HasValue) + { + var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == senderAddress.Country.Value); + senderCountryCode = country?.CountryCode; + } + + pickup.SenderAddressId = senderAddress.Id; + pickup.SenderName = senderAddress.SenderName ?? string.Empty; + pickup.SenderCompany = senderAddress.Company; + pickup.SenderCountry = senderAddress.Country; + pickup.SenderCountryCode = senderCountryCode; + pickup.SenderProvince = senderAddress.Province; + pickup.SenderCity = senderAddress.City; + pickup.SenderZipCode = senderAddress.ZipCode; + pickup.SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty; + pickup.SenderStreetAddress2 = senderAddress.StreetAddress2; + pickup.SenderStreetAddress3 = senderAddress.StreetAddress3; + pickup.SenderHouseNumber = senderAddress.HouseNumber; + pickup.SenderPhone = senderAddress.Phone; + pickup.SenderTelePhone = senderAddress.TelePhone; + pickup.SenderEmail = senderAddress.Email; + senderChanged = true; + } + + if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default) + { + pickup.ExpectPickupTime = input.ExpectPickupTime.Value; + expectTimeChanged = true; + } + + if (input.CarrierService != null) + { + var carrierService = NormalizeOptional(input.CarrierService); + if (carrierService == null) + { + throw new BusinessException("承运服务不能为空"); + } + + pickup.CarrierService = carrierService; + } + + if (input.CarrierWaybillNo != null) + { + pickup.CarrierWaybillNo = NormalizeOptional(input.CarrierWaybillNo); + } + + if (input.Remark != null) + { + pickup.Remark = NormalizeOptional(input.Remark); + } + + // 写入承运运单号后 draft → scheduled(已预约) + if (!string.IsNullOrWhiteSpace(pickup.CarrierWaybillNo) + && pickup.Status == PickupOrderStatusCodes.Draft) + { + pickup.Status = PickupOrderStatusCodes.Scheduled; + } + + await PickupOrderRepository.UpdateAsync(pickup); + + if (warehouseChanged || expectTimeChanged || senderChanged) + { + var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); + foreach (var line in lines) + { + var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId); + if (order == null) + { + continue; + } + + if (warehouseChanged) + { + order.WarehouseId = pickup.WarehouseId; + } + + if (expectTimeChanged) + { + order.PickupTime = pickup.ExpectPickupTime; + } + + if (senderChanged) + { + order.CustomerSenderAddressId = pickup.SenderAddressId; + } + + await OrderRepository.UpdateAsync(order); + } + } + + return await MapOutputAsync(pickup, includeOrders: true); + } + + /// + /// 修改件重尺(未支付;仅落库,不改费用)。 + /// + [HttpPost("/order/pickup-order/update-measure")] + public async Task UpdateMeasureAsync([FromBody] PickupOrderUpdateMeasureInput input) + { + var pickup = await GetOwnedPickupOrThrowAsync(input.Id); + EnsureUnpaidEditable(pickup); + if (PickupOrderStatusCodes.IsTerminal(pickup.Status) + && pickup.Status != PickupOrderStatusCodes.Inbound) + { + throw new BusinessException("当前状态不可修改件重尺"); + } + + if (input.TotalActualWeight <= 0) + { + throw new BusinessException("实重必须大于 0"); + } + + var sizes = (input.PackageSizes ?? []) + .Select(x => new PickupPackageSizeOutput + { + Length = x.Length, + Width = x.Width, + Height = x.Height, + Count = x.Count <= 0 ? 1 : x.Count + }) + .Where(x => x.Length > 0 && x.Width > 0 && x.Height > 0) + .ToList(); + if (sizes.Count == 0) + { + throw new BusinessException("请至少填写一条有效尺寸(长宽高)"); + } + + pickup.TotalActualWeight = input.TotalActualWeight; + pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes); + pickup.UpdateTime = DateTime.Now; + await PickupOrderRepository.UpdateAsync(pickup); + return await MapOutputAsync(pickup, includeOrders: true); + } + + /// + /// 按当前件重尺与承运服务重算费用。 + /// + [HttpPost("/order/pickup-order/recalc-fee")] + public async Task RecalcFeeAsync([FromBody] PickupOrderIdInput input) + { + var pickup = await GetOwnedPickupOrThrowAsync(input.Id); + EnsureUnpaidEditable(pickup); + await PickupOrderFeePushService.RecalcByPickupAsync(pickup); + return await MapOutputAsync(pickup, includeOrders: true); + } + + private static void EnsureUnpaidEditable(PickupOrder pickup) + { + if (pickup.PayStatus == PickupPayStatusCodes.Paid) + { + throw new BusinessException("已支付不可修改"); + } + + if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed) + { + throw new BusinessException("当前状态不可修改"); + } + } + /// /// 客户支付揽收运费(余额)。 /// @@ -930,6 +1153,14 @@ public class PickupOrderService : ApplicationService public async Task DownstreamForwardAsync([FromBody] PickupOrderIdInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); + return await DownstreamForwardCoreAsync(pickup); + } + + /// + /// 预报下游核心:回写承运运单号,draft 升为 scheduled。 + /// + private async Task DownstreamForwardCoreAsync(PickupOrder pickup) + { if (string.IsNullOrWhiteSpace(pickup.CarrierService)) { throw new BusinessException("请先选择承运服务后再预报下游"); @@ -974,6 +1205,14 @@ public class PickupOrderService : ApplicationService changed = true; } + // 拿到承运运单号后 draft → scheduled(已预约) + if (!string.IsNullOrWhiteSpace(pickup.CarrierWaybillNo) + && pickup.Status == PickupOrderStatusCodes.Draft) + { + pickup.Status = PickupOrderStatusCodes.Scheduled; + changed = true; + } + if (string.IsNullOrWhiteSpace(pickup.CarrierCode) && !string.IsNullOrWhiteSpace(creds.SupplierCode)) {