test #11

Merged
zzh merged 5 commits from test into master 2026-10-22 18:59:18 +08:00
5 changed files with 301 additions and 29 deletions
Showing only changes of commit 7ed8fd8e8a - Show all commits

View File

@ -364,6 +364,60 @@ public class PlatformStoreOrderMatchInventoryOutput
public string? ClientNo { get; set; } 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 public class PlatformStoreOrderBatchCreateLogisticsInput
{ {
[Required] [Required]
@ -373,6 +427,9 @@ public class PlatformStoreOrderBatchCreateLogisticsInput
public int? CustomerId { get; set; } public int? CustomerId { get; set; }
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
[Required] [Required]
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = []; public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
} }
@ -387,6 +444,9 @@ public class PlatformStoreOrderBatchCreateFulfillInput
public int? CustomerId { get; set; } public int? CustomerId { get; set; }
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
[Required] [Required]
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = []; public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
} }
@ -396,6 +456,9 @@ public class PlatformStoreOrderBatchCreateItemInput
[Required] [Required]
public int StoreOrderId { get; set; } public int StoreOrderId { get; set; }
/// <summary>覆盖本单客户单号;空则用 ParcelBasic.ClientNo 或平台订单号。</summary>
public string? ClientNo { get; set; }
public OrderParcelReceiverInput? Receiver { get; set; } public OrderParcelReceiverInput? Receiver { get; set; }
public List<OrderStockBindSaveItemInput> BindItems { get; set; } = []; public List<OrderStockBindSaveItemInput> BindItems { get; set; } = [];

View File

@ -174,11 +174,11 @@ public partial class OrderService
// 4. 保存草稿:不做预报计费。9610、上门揽收与订单一起提交。 // 4. 保存草稿:不做预报计费。9610、上门揽收与订单一起提交。
if (input.SaveAsDraft) if (input.SaveAsDraft)
{ {
await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent await RunParcelCreateSideEffectsAsync(
{ order,
OrderId = order.Id, input,
PlatformCode = input.PlatformCode operatorId,
}); includeForecastAndLabel: false);
var draftOutput = await BuildParcelCreateOutputAsync(order); var draftOutput = await BuildParcelCreateOutputAsync(order);
await TryAttachPickupAfterParcelCreateAsync(input, draftOutput); await TryAttachPickupAfterParcelCreateAsync(input, draftOutput);
return draftOutput; return draftOutput;
@ -199,21 +199,13 @@ public partial class OrderService
order.UpdateTime = DateTime.Now; order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order); await OrderRepository.UpdateAsync(order);
// 6. 与订单同一事务写入 Outbox。Commit 后由 Fake 投递;任一条失败则整单回滚。 // 6. 有事务 UoW 时写 Outbox;无事务(店铺订单创建运单等)则同步执行,避免 Outbox 拒发。
// 轨迹不在此列:要等预报消费端排货拿到 ChannelCode 再发。 // 轨迹不在此列:要等预报消费端排货拿到 ChannelCode 再发。
var authorization = CaptureRequestAuthorization(); await RunParcelCreateSideEffectsAsync(
await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent order,
{ input,
OrderId = order.Id, operatorId,
PlatformCode = input.PlatformCode includeForecastAndLabel: true);
});
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);
var output = await BuildParcelCreateOutputAsync(order); var output = await BuildParcelCreateOutputAsync(order);
output.ForecastBillingQueued = string.Equals( output.ForecastBillingQueued = string.Equals(
@ -225,6 +217,87 @@ public partial class OrderService
return output; 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) private async Task TryUpsertCustoms9610Async(Order order, string? platformCode = null)
{ {
try try

View File

@ -14,6 +14,7 @@ using Oms.Application.Labeling;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage; using Oms.Application.Storage;
using Newtonsoft.Json; using Newtonsoft.Json;
using Oms.Application.Messaging;
using Oms.Application.Messaging.ParcelCreate; using Oms.Application.Messaging.ParcelCreate;
using Oms.Application.Services.Orders.Batch; using Oms.Application.Services.Orders.Batch;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
@ -722,16 +723,37 @@ public partial class OrderService : ApplicationService
try try
{ {
await EventBus.PublishAsync(new OrderParcelPickupAttachIntegrationEvent var uowManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
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<PickupOrderService>();
await pickupService.CreateForParcelInternalAsync(new PickupOrderCreateInput
{ {
OrderId = output.OrderId,
CustomerId = input.CustomerId, CustomerId = input.CustomerId,
SenderAddressId = input.SenderAddressId.Value, SenderAddressId = input.SenderAddressId.Value,
Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection),
WarehouseId = input.WarehouseId.Value, WarehouseId = input.WarehouseId.Value,
PickupDirection = input.PickupDirection,
CarrierCode = input.CarrierCode, CarrierCode = input.CarrierCode,
CarrierService = input.CarrierService, CarrierService = input.CarrierService,
ExpectPickupTime = input.ExpectPickupTime.Value ExpectPickupTime = input.ExpectPickupTime.Value,
OrderIds = [output.OrderId],
Remark = null
}); });
output.PickupQueued = true; output.PickupQueued = true;
} }

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Fake.Application; using Fake.Application;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.PlatformStore;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.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> /// <summary>
/// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&amp;代发)。 /// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&amp;代发)。
/// </summary> /// </summary>

View File

@ -411,7 +411,8 @@ public class PlatformStoreOrderService : ApplicationService
input.WarehouseId, input.WarehouseId,
input.Items, input.Items,
requireBind: false, requireBind: false,
requireWmsProductGroup: false); requireWmsProductGroup: false,
parcelBasic: input.ParcelBasic);
} }
/// <summary> /// <summary>
@ -432,7 +433,8 @@ public class PlatformStoreOrderService : ApplicationService
input.WarehouseId, input.WarehouseId,
input.Items, input.Items,
requireBind: true, requireBind: true,
requireWmsProductGroup: false); requireWmsProductGroup: false,
parcelBasic: input.ParcelBasic);
} }
/// <summary> /// <summary>
@ -454,7 +456,8 @@ public class PlatformStoreOrderService : ApplicationService
input.WarehouseId, input.WarehouseId,
input.Items, input.Items,
requireBind: true, requireBind: true,
requireWmsProductGroup: true); requireWmsProductGroup: true,
parcelBasic: input.ParcelBasic);
} }
private async Task<PlatformStoreOrderBatchCreateLogisticsOutput> BatchCreateLogisticsInternalAsync( private async Task<PlatformStoreOrderBatchCreateLogisticsOutput> BatchCreateLogisticsInternalAsync(
@ -463,7 +466,8 @@ public class PlatformStoreOrderService : ApplicationService
int? warehouseId, int? warehouseId,
List<PlatformStoreOrderBatchCreateItemInput> items, List<PlatformStoreOrderBatchCreateItemInput> items,
bool requireBind, bool requireBind,
bool requireWmsProductGroup = false) bool requireWmsProductGroup = false,
PlatformStoreOrderParcelBasicInput? parcelBasic = null)
{ {
if (items == null || items.Count == 0) if (items == null || items.Count == 0)
{ {
@ -512,7 +516,9 @@ public class PlatformStoreOrderService : ApplicationService
warehouseId, warehouseId,
resolvedProductCode, resolvedProductCode,
itemInput.Receiver, itemInput.Receiver,
requireBind ? itemInput.BindItems : null); requireBind ? itemInput.BindItems : null,
parcelBasic,
itemInput.ClientNo);
item.Success = true; item.Success = true;
item.Message = requireWmsProductGroup item.Message = requireWmsProductGroup
? $"已创建代发订单 {result.ServiceNo} 并绑定" ? $"已创建代发订单 {result.ServiceNo} 并绑定"
@ -548,7 +554,9 @@ public class PlatformStoreOrderService : ApplicationService
int? warehouseId, int? warehouseId,
string productCode, string productCode,
OrderParcelReceiverInput? receiver, OrderParcelReceiverInput? receiver,
List<OrderStockBindSaveItemInput>? bindItems) List<OrderStockBindSaveItemInput>? bindItems,
PlatformStoreOrderParcelBasicInput? parcelBasic = null,
string? clientNoOverride = null)
{ {
var order = await GetOrderOrThrowAsync(customerId, storeOrderId); var order = await GetOrderOrThrowAsync(customerId, storeOrderId);
await EnsureCanCreateLogisticsAsync(order); await EnsureCanCreateLogisticsAsync(order);
@ -572,6 +580,11 @@ public class PlatformStoreOrderService : ApplicationService
throw new BusinessException("请指定发货仓库"); throw new BusinessException("请指定发货仓库");
} }
if (parcelBasic != null && resolvedWarehouseId is not > 0)
{
throw new BusinessException("请选择仓库");
}
var parcelInput = PlatformStoreOrderParcelBuilder.BuildFromPlatformLines( var parcelInput = PlatformStoreOrderParcelBuilder.BuildFromPlatformLines(
order, order,
lines, lines,
@ -580,6 +593,7 @@ public class PlatformStoreOrderService : ApplicationService
resolvedWarehouseId, resolvedWarehouseId,
customerId, customerId,
receiver); receiver);
PlatformStoreOrderParcelBuilder.ApplyParcelBasic(parcelInput, parcelBasic, clientNoOverride);
var parcelOutput = await OrderService.CreateParcelForBatchAsync( var parcelOutput = await OrderService.CreateParcelForBatchAsync(
parcelInput, parcelInput,