feat: 店铺创建运单支持基本信息覆盖,无事务时同步执行创单副作用
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
264c3a5dcb
commit
7ed8fd8e8a
@ -364,6 +364,60 @@ public class PlatformStoreOrderMatchInventoryOutput
|
||||
public string? ClientNo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺订单创建运单时填写的运单基本信息(对齐下单页基本信息)。
|
||||
/// </summary>
|
||||
public class PlatformStoreOrderParcelBasicInput
|
||||
{
|
||||
/// <summary>客户单号;空则沿用平台订单号。</summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
/// <summary>货物属性:general / electronics / sensitive。</summary>
|
||||
public string? CargoAttribute { get; set; }
|
||||
|
||||
/// <summary>实重(kg)。</summary>
|
||||
public decimal? ActualWeight { get; set; }
|
||||
|
||||
/// <summary>包裹数量。</summary>
|
||||
public int? Quantity { get; set; }
|
||||
|
||||
/// <summary>长(cm)。</summary>
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
/// <summary>宽(cm)。</summary>
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
/// <summary>高(cm)。</summary>
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
/// <summary>出口报关方式。</summary>
|
||||
public string? ExportCustomsMode { get; set; }
|
||||
|
||||
/// <summary>出口资质备案 Id(9610Summary 时必填)。</summary>
|
||||
public int? ExportQualificationId { get; set; }
|
||||
|
||||
/// <summary>电商平台编码(9610Summary 时必填)。</summary>
|
||||
public string? PlatformCode { get; set; }
|
||||
|
||||
/// <summary>到仓方式:selfShip / doorPickup。</summary>
|
||||
public string? InboundMethod { get; set; }
|
||||
|
||||
/// <summary>上门揽收发件地址簿 Id。</summary>
|
||||
public int? SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>期望揽收时间。</summary>
|
||||
public DateTime? ExpectPickupTime { get; set; }
|
||||
|
||||
/// <summary>揽收方向。</summary>
|
||||
public string? PickupDirection { get; set; }
|
||||
|
||||
/// <summary>承运商编码。</summary>
|
||||
public string? CarrierCode { get; set; }
|
||||
|
||||
/// <summary>承运服务。</summary>
|
||||
public string? CarrierService { get; set; }
|
||||
}
|
||||
|
||||
public class PlatformStoreOrderBatchCreateLogisticsInput
|
||||
{
|
||||
[Required]
|
||||
@ -373,6 +427,9 @@ public class PlatformStoreOrderBatchCreateLogisticsInput
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
|
||||
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
|
||||
}
|
||||
@ -387,6 +444,9 @@ public class PlatformStoreOrderBatchCreateFulfillInput
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
|
||||
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
|
||||
}
|
||||
@ -396,6 +456,9 @@ public class PlatformStoreOrderBatchCreateItemInput
|
||||
[Required]
|
||||
public int StoreOrderId { get; set; }
|
||||
|
||||
/// <summary>覆盖本单客户单号;空则用 ParcelBasic.ClientNo 或平台订单号。</summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public OrderParcelReceiverInput? Receiver { get; set; }
|
||||
|
||||
public List<OrderStockBindSaveItemInput> BindItems { get; set; } = [];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创单后置:9610 / 预报计费 / 面单 / 自动触发。
|
||||
/// 环境有事务 UoW 时走 Outbox;否则同步执行(店铺订单创建运单无 [UnitOfWork])。
|
||||
/// </summary>
|
||||
private async Task RunParcelCreateSideEffectsAsync(
|
||||
Order order,
|
||||
OrderParcelCreateInput input,
|
||||
int operatorId,
|
||||
bool includeForecastAndLabel)
|
||||
{
|
||||
var uowManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
|
||||
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<ILoggerFactory>()
|
||||
.CreateLogger("Oms.ParcelCreate")
|
||||
.LogWarning(
|
||||
"Label generate failed OrderId={OrderId} Error={Error}",
|
||||
order.Id,
|
||||
labelResult.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LazyServiceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.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
|
||||
|
||||
@ -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;
|
||||
@ -721,6 +722,9 @@ public partial class OrderService : ApplicationService
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var uowManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
|
||||
if (OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager))
|
||||
{
|
||||
await EventBus.PublishAsync(new OrderParcelPickupAttachIntegrationEvent
|
||||
{
|
||||
@ -734,6 +738,24 @@ public partial class OrderService : ApplicationService
|
||||
ExpectPickupTime = input.ExpectPickupTime.Value
|
||||
});
|
||||
output.PickupQueued = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 无事务 UoW(店铺订单创建运单)时同步挂接揽收,避免 Outbox 拒发
|
||||
var pickupService = LazyServiceProvider.GetRequiredService<PickupOrderService>();
|
||||
await pickupService.CreateForParcelInternalAsync(new PickupOrderCreateInput
|
||||
{
|
||||
CustomerId = input.CustomerId,
|
||||
SenderAddressId = input.SenderAddressId.Value,
|
||||
Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection),
|
||||
WarehouseId = input.WarehouseId.Value,
|
||||
CarrierCode = input.CarrierCode,
|
||||
CarrierService = input.CarrierService,
|
||||
ExpectPickupTime = input.ExpectPickupTime.Value,
|
||||
OrderIds = [output.OrderId],
|
||||
Remark = null
|
||||
});
|
||||
output.PickupQueued = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将下单页同款「运单基本信息」覆盖到组装好的小包入参。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&代发)。
|
||||
/// </summary>
|
||||
|
||||
@ -411,7 +411,8 @@ public class PlatformStoreOrderService : ApplicationService
|
||||
input.WarehouseId,
|
||||
input.Items,
|
||||
requireBind: false,
|
||||
requireWmsProductGroup: false);
|
||||
requireWmsProductGroup: false,
|
||||
parcelBasic: input.ParcelBasic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -432,7 +433,8 @@ public class PlatformStoreOrderService : ApplicationService
|
||||
input.WarehouseId,
|
||||
input.Items,
|
||||
requireBind: true,
|
||||
requireWmsProductGroup: false);
|
||||
requireWmsProductGroup: false,
|
||||
parcelBasic: input.ParcelBasic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -454,7 +456,8 @@ public class PlatformStoreOrderService : ApplicationService
|
||||
input.WarehouseId,
|
||||
input.Items,
|
||||
requireBind: true,
|
||||
requireWmsProductGroup: true);
|
||||
requireWmsProductGroup: true,
|
||||
parcelBasic: input.ParcelBasic);
|
||||
}
|
||||
|
||||
private async Task<PlatformStoreOrderBatchCreateLogisticsOutput> BatchCreateLogisticsInternalAsync(
|
||||
@ -463,7 +466,8 @@ public class PlatformStoreOrderService : ApplicationService
|
||||
int? warehouseId,
|
||||
List<PlatformStoreOrderBatchCreateItemInput> 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<OrderStockBindSaveItemInput>? bindItems)
|
||||
List<OrderStockBindSaveItemInput>? 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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user