From 7ed8fd8e8a609465d608c07a01d02e2c5a8a15ee Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 20 Oct 2026 11:23:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=97=E9=93=BA=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E8=BF=90=E5=8D=95=E6=94=AF=E6=8C=81=E5=9F=BA=E6=9C=AC=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=A6=86=E7=9B=96=EF=BC=8C=E6=97=A0=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E6=97=B6=E5=90=8C=E6=AD=A5=E6=89=A7=E8=A1=8C=E5=88=9B=E5=8D=95?= =?UTF-8?q?=E5=89=AF=E4=BD=9C=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Dtos/PlatformStore/PlatformStoreDtos.cs | 63 ++++++++++ .../Orders/OrderService.ParcelBilling.cs | 111 +++++++++++++++--- .../Services/Orders/OrderService.cs | 30 ++++- .../Orders/PlatformStoreOrderParcelBuilder.cs | 100 ++++++++++++++++ .../Orders/PlatformStoreOrderService.cs | 26 +++- 5 files changed, 301 insertions(+), 29 deletions(-) diff --git a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs index f7a61ca..332da0d 100644 --- a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs +++ b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs @@ -364,6 +364,60 @@ public class PlatformStoreOrderMatchInventoryOutput public string? ClientNo { get; set; } } +/// +/// 店铺订单创建运单时填写的运单基本信息(对齐下单页基本信息)。 +/// +public class PlatformStoreOrderParcelBasicInput +{ + /// 客户单号;空则沿用平台订单号。 + public string? ClientNo { get; set; } + + /// 货物属性:general / electronics / sensitive。 + public string? CargoAttribute { get; set; } + + /// 实重(kg)。 + public decimal? ActualWeight { get; set; } + + /// 包裹数量。 + public int? Quantity { get; set; } + + /// 长(cm)。 + public decimal? Length { get; set; } + + /// 宽(cm)。 + public decimal? Width { get; set; } + + /// 高(cm)。 + public decimal? Height { get; set; } + + /// 出口报关方式。 + public string? ExportCustomsMode { get; set; } + + /// 出口资质备案 Id(9610Summary 时必填)。 + public int? ExportQualificationId { get; set; } + + /// 电商平台编码(9610Summary 时必填)。 + public string? PlatformCode { get; set; } + + /// 到仓方式:selfShip / doorPickup。 + public string? InboundMethod { get; set; } + + /// 上门揽收发件地址簿 Id。 + public int? SenderAddressId { get; set; } + + /// 期望揽收时间。 + public DateTime? ExpectPickupTime { get; set; } + + /// 揽收方向。 + public string? PickupDirection { get; set; } + + /// 承运商编码。 + public string? CarrierCode { get; set; } + + /// 承运服务。 + public string? CarrierService { get; set; } +} + public class PlatformStoreOrderBatchCreateLogisticsInput { [Required] @@ -373,6 +427,9 @@ public class PlatformStoreOrderBatchCreateLogisticsInput public int? CustomerId { get; set; } + /// 运单基本信息(创建运单/代发运单必填)。 + public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; } + [Required] public List Items { get; set; } = []; } @@ -387,6 +444,9 @@ public class PlatformStoreOrderBatchCreateFulfillInput public int? CustomerId { get; set; } + /// 运单基本信息(创建运单/代发运单必填)。 + public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; } + [Required] public List Items { get; set; } = []; } @@ -396,6 +456,9 @@ public class PlatformStoreOrderBatchCreateItemInput [Required] public int StoreOrderId { get; set; } + /// 覆盖本单客户单号;空则用 ParcelBasic.ClientNo 或平台订单号。 + public string? ClientNo { get; set; } + public OrderParcelReceiverInput? Receiver { get; set; } public List BindItems { get; set; } = []; diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index 33d7ea0..a364dbf 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -174,11 +174,11 @@ public partial class OrderService // 4. 保存草稿:不做预报计费。9610、上门揽收与订单一起提交。 if (input.SaveAsDraft) { - await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent - { - OrderId = order.Id, - PlatformCode = input.PlatformCode - }); + await RunParcelCreateSideEffectsAsync( + order, + input, + operatorId, + includeForecastAndLabel: false); var draftOutput = await BuildParcelCreateOutputAsync(order); await TryAttachPickupAfterParcelCreateAsync(input, draftOutput); return draftOutput; @@ -199,21 +199,13 @@ public partial class OrderService order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); - // 6. 与订单同一事务写入 Outbox。Commit 后由 Fake 投递;任一条失败则整单回滚。 + // 6. 有事务 UoW 时写 Outbox;无事务(店铺订单创建运单等)则同步执行,避免 Outbox 拒发。 // 轨迹不在此列:要等预报消费端排货拿到 ChannelCode 再发。 - var authorization = CaptureRequestAuthorization(); - await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent - { - OrderId = order.Id, - PlatformCode = input.PlatformCode - }); - await EnqueueForecastBillingAsync(order, input.CouponInstanceId, operatorId, authorization); - await EventBus.PublishAsync(new OrderParcelLabelGenerateIntegrationEvent - { - OrderId = order.Id, - OperatorUserId = operatorId - }); - await AutoTriggerRuntimeService.EnqueueFireAsync(order.Id, AutoTriggerTiming.OrderCreate, operatorId); + await RunParcelCreateSideEffectsAsync( + order, + input, + operatorId, + includeForecastAndLabel: true); var output = await BuildParcelCreateOutputAsync(order); output.ForecastBillingQueued = string.Equals( @@ -225,6 +217,87 @@ public partial class OrderService return output; } + /// + /// 创单后置:9610 / 预报计费 / 面单 / 自动触发。 + /// 环境有事务 UoW 时走 Outbox;否则同步执行(店铺订单创建运单无 [UnitOfWork])。 + /// + private async Task RunParcelCreateSideEffectsAsync( + Order order, + OrderParcelCreateInput input, + int operatorId, + bool includeForecastAndLabel) + { + var uowManager = LazyServiceProvider.GetRequiredService(); + var useOutbox = OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager); + var authorization = CaptureRequestAuthorization(); + + if (useOutbox) + { + await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent + { + OrderId = order.Id, + PlatformCode = input.PlatformCode + }); + if (includeForecastAndLabel) + { + await EnqueueForecastBillingAsync(order, input.CouponInstanceId, operatorId, authorization); + await EventBus.PublishAsync(new OrderParcelLabelGenerateIntegrationEvent + { + OrderId = order.Id, + OperatorUserId = operatorId + }); + await AutoTriggerRuntimeService.EnqueueFireAsync( + order.Id, + AutoTriggerTiming.OrderCreate, + operatorId); + } + + return; + } + + await TryUpsertCustoms9610Async(order, input.PlatformCode); + if (!includeForecastAndLabel) + { + return; + } + + var payloadJson = JsonConvert.SerializeObject(new ForecastBillingJobPayload + { + CouponInstanceId = input.CouponInstanceId, + Authorization = authorization + }); + await ProcessForecastBillingForBatchAsync( + order.Id, + payloadJson, + operatorId, + authorization: authorization); + + try + { + var labelResult = await GenerateLabelPdfForBatchAsync(order.Id); + if (!labelResult.Success) + { + LazyServiceProvider.GetRequiredService() + .CreateLogger("Oms.ParcelCreate") + .LogWarning( + "Label generate failed OrderId={OrderId} Error={Error}", + order.Id, + labelResult.Error); + } + } + catch (Exception ex) + { + LazyServiceProvider.GetRequiredService() + .CreateLogger("Oms.ParcelCreate") + .LogWarning(ex, "Label generate failed OrderId={OrderId}", order.Id); + } + + await AutoTriggerRuntimeService.FireByOrderIdAsync( + order.Id, + AutoTriggerTiming.OrderCreate, + operatorId); + } + private async Task TryUpsertCustoms9610Async(Order order, string? platformCode = null) { try diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index 61eaddd..d4e4090 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -14,6 +14,7 @@ using Oms.Application.Labeling; using Oms.Application.Shared; using Oms.Application.Storage; using Newtonsoft.Json; +using Oms.Application.Messaging; using Oms.Application.Messaging.ParcelCreate; using Oms.Application.Services.Orders.Batch; using Oms.Domain.ChannelsAggregate.Repositories; @@ -722,16 +723,37 @@ public partial class OrderService : ApplicationService try { - await EventBus.PublishAsync(new OrderParcelPickupAttachIntegrationEvent + var uowManager = LazyServiceProvider.GetRequiredService(); + if (OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager)) + { + await EventBus.PublishAsync(new OrderParcelPickupAttachIntegrationEvent + { + OrderId = output.OrderId, + CustomerId = input.CustomerId, + SenderAddressId = input.SenderAddressId.Value, + WarehouseId = input.WarehouseId.Value, + PickupDirection = input.PickupDirection, + CarrierCode = input.CarrierCode, + CarrierService = input.CarrierService, + ExpectPickupTime = input.ExpectPickupTime.Value + }); + output.PickupQueued = true; + return; + } + + // 无事务 UoW(店铺订单创建运单)时同步挂接揽收,避免 Outbox 拒发 + var pickupService = LazyServiceProvider.GetRequiredService(); + await pickupService.CreateForParcelInternalAsync(new PickupOrderCreateInput { - OrderId = output.OrderId, CustomerId = input.CustomerId, SenderAddressId = input.SenderAddressId.Value, + Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection), WarehouseId = input.WarehouseId.Value, - PickupDirection = input.PickupDirection, CarrierCode = input.CarrierCode, CarrierService = input.CarrierService, - ExpectPickupTime = input.ExpectPickupTime.Value + ExpectPickupTime = input.ExpectPickupTime.Value, + OrderIds = [output.OrderId], + Remark = null }); output.PickupQueued = true; } diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs index 244ad2f..1e460e5 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using Fake.Application; using Oms.Application.Dtos.Orders; +using Oms.Application.Dtos.PlatformStore; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.SystemAggregate.Entities; @@ -59,6 +60,105 @@ public static class PlatformStoreOrderParcelBuilder }; } + /// + /// 将下单页同款「运单基本信息」覆盖到组装好的小包入参。 + /// + public static void ApplyParcelBasic( + OrderParcelCreateInput parcelInput, + PlatformStoreOrderParcelBasicInput? basic, + string? clientNoOverride = null) + { + if (!string.IsNullOrWhiteSpace(clientNoOverride)) + { + parcelInput.ClientNo = clientNoOverride.Trim(); + } + else if (!string.IsNullOrWhiteSpace(basic?.ClientNo)) + { + parcelInput.ClientNo = basic.ClientNo.Trim(); + } + + if (basic == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(basic.CargoAttribute)) + { + parcelInput.CargoAttribute = basic.CargoAttribute.Trim(); + } + + if (basic.ActualWeight is > 0) + { + parcelInput.ActualWeight = basic.ActualWeight; + parcelInput.ChargeableWeight = basic.ActualWeight; + } + + if (basic.Quantity is > 0) + { + parcelInput.Quantity = basic.Quantity; + } + + if (basic.Length is > 0) + { + parcelInput.Length = basic.Length; + } + + if (basic.Width is > 0) + { + parcelInput.Width = basic.Width; + } + + if (basic.Height is > 0) + { + parcelInput.Height = basic.Height; + } + + if (!string.IsNullOrWhiteSpace(basic.ExportCustomsMode)) + { + parcelInput.ExportCustomsMode = basic.ExportCustomsMode.Trim(); + } + + if (basic.ExportQualificationId is > 0) + { + parcelInput.ExportQualificationId = basic.ExportQualificationId; + } + + if (!string.IsNullOrWhiteSpace(basic.PlatformCode)) + { + parcelInput.PlatformCode = basic.PlatformCode.Trim(); + } + + if (!string.IsNullOrWhiteSpace(basic.InboundMethod)) + { + parcelInput.InboundMethod = basic.InboundMethod.Trim(); + } + + if (basic.SenderAddressId is > 0) + { + parcelInput.SenderAddressId = basic.SenderAddressId; + } + + if (basic.ExpectPickupTime.HasValue && basic.ExpectPickupTime != default) + { + parcelInput.ExpectPickupTime = basic.ExpectPickupTime; + } + + if (!string.IsNullOrWhiteSpace(basic.PickupDirection)) + { + parcelInput.PickupDirection = basic.PickupDirection.Trim(); + } + + if (!string.IsNullOrWhiteSpace(basic.CarrierCode)) + { + parcelInput.CarrierCode = basic.CarrierCode.Trim(); + } + + if (!string.IsNullOrWhiteSpace(basic.CarrierService)) + { + parcelInput.CarrierService = basic.CarrierService.Trim(); + } + } + /// /// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&代发)。 /// diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs index 2f99043..a6ed453 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs @@ -411,7 +411,8 @@ public class PlatformStoreOrderService : ApplicationService input.WarehouseId, input.Items, requireBind: false, - requireWmsProductGroup: false); + requireWmsProductGroup: false, + parcelBasic: input.ParcelBasic); } /// @@ -432,7 +433,8 @@ public class PlatformStoreOrderService : ApplicationService input.WarehouseId, input.Items, requireBind: true, - requireWmsProductGroup: false); + requireWmsProductGroup: false, + parcelBasic: input.ParcelBasic); } /// @@ -454,7 +456,8 @@ public class PlatformStoreOrderService : ApplicationService input.WarehouseId, input.Items, requireBind: true, - requireWmsProductGroup: true); + requireWmsProductGroup: true, + parcelBasic: input.ParcelBasic); } private async Task BatchCreateLogisticsInternalAsync( @@ -463,7 +466,8 @@ public class PlatformStoreOrderService : ApplicationService int? warehouseId, List items, bool requireBind, - bool requireWmsProductGroup = false) + bool requireWmsProductGroup = false, + PlatformStoreOrderParcelBasicInput? parcelBasic = null) { if (items == null || items.Count == 0) { @@ -512,7 +516,9 @@ public class PlatformStoreOrderService : ApplicationService warehouseId, resolvedProductCode, itemInput.Receiver, - requireBind ? itemInput.BindItems : null); + requireBind ? itemInput.BindItems : null, + parcelBasic, + itemInput.ClientNo); item.Success = true; item.Message = requireWmsProductGroup ? $"已创建代发订单 {result.ServiceNo} 并绑定" @@ -548,7 +554,9 @@ public class PlatformStoreOrderService : ApplicationService int? warehouseId, string productCode, OrderParcelReceiverInput? receiver, - List? bindItems) + List? bindItems, + PlatformStoreOrderParcelBasicInput? parcelBasic = null, + string? clientNoOverride = null) { var order = await GetOrderOrThrowAsync(customerId, storeOrderId); await EnsureCanCreateLogisticsAsync(order); @@ -572,6 +580,11 @@ public class PlatformStoreOrderService : ApplicationService throw new BusinessException("请指定发货仓库"); } + if (parcelBasic != null && resolvedWarehouseId is not > 0) + { + throw new BusinessException("请选择仓库"); + } + var parcelInput = PlatformStoreOrderParcelBuilder.BuildFromPlatformLines( order, lines, @@ -580,6 +593,7 @@ public class PlatformStoreOrderService : ApplicationService resolvedWarehouseId, customerId, receiver); + PlatformStoreOrderParcelBuilder.ApplyParcelBasic(parcelInput, parcelBasic, clientNoOverride); var parcelOutput = await OrderService.CreateParcelForBatchAsync( parcelInput,