1.06 #10

Merged
zzh merged 18 commits from test into master 2026-10-19 17:03:15 +08:00
21 changed files with 256 additions and 998 deletions
Showing only changes of commit d72637e472 - Show all commits

View File

@ -1,5 +1,7 @@
using Fake.EventBus.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Oms.Application.Messaging.OrderSideEffects; using Oms.Application.Messaging.OrderSideEffects;
using Oms.Application.Messaging.ParcelCreate; using Oms.Application.Messaging.ParcelCreate;
using Oms.Application.Services.Orders.Batch; using Oms.Application.Services.Orders.Batch;
@ -25,8 +27,15 @@ internal sealed class SmallBagTrackOrderNotifier(
return; return;
} }
var publisher = scope.ServiceProvider.GetRequiredService<IOrderParcelCreateEventPublisher>(); var eventBus = scope.ServiceProvider.GetRequiredService<IDistributedEventBus>();
await publisher.PublishTrackUpsertAsync(order, request, customerId); await eventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
{
OrderId = order.Id,
CustomerId = customerId,
PayloadJson = JsonConvert.SerializeObject(request),
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
Authorization = request.Authorization
});
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -75,8 +84,19 @@ internal sealed class SmallBagTrackOrderNotifier(
try try
{ {
using var scope = scopeFactory.CreateScope(); using var scope = scopeFactory.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IOrderSideEffectEventPublisher>(); if (orderId <= 0 || string.IsNullOrWhiteSpace(orderNo) || string.IsNullOrWhiteSpace(trackCode))
await publisher.PublishLifecycleTrackAppendAsync(orderId, orderNo, trackCode, authorization); {
return;
}
var eventBus = scope.ServiceProvider.GetRequiredService<IDistributedEventBus>();
await eventBus.PublishAsync(new OrderLifecycleTrackAppendIntegrationEvent
{
OrderId = orderId,
OrderNo = orderNo.Trim(),
TrackCode = trackCode.Trim(),
Authorization = string.IsNullOrWhiteSpace(authorization) ? null : authorization.Trim()
});
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,8 +0,0 @@
namespace Oms.Application.Messaging;
public interface IOrderBatchJobPublisher
{
Task PublishAsync(OrderBatchJobMessage message, CancellationToken cancellationToken = default);
bool IsEnabled { get; }
}

View File

@ -1,57 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Fake.UnitOfWork;
namespace Oms.Application.Messaging;
/// <summary>
/// 把创单/预报之后的副作用延后到当前事务提交成功再执行,每一步单独提交。
/// </summary>
internal static class OmsAfterCommit
{
public static Task ScheduleAsync(IUnitOfWorkManager unitOfWorkManager, Func<Task> action)
{
var current = unitOfWorkManager.Current;
if (current != null && !current.IsCompleted)
{
// OnCompleted 仍处在创单事务里,此时再写 Outbox 征用不到连接,事件会丢。
// 等工作单元释放后再开独立事务。
EventHandler<UnitOfWorkEventArgs> handler = null!;
handler = (_, _) =>
{
current.Disposed -= handler;
_ = Task.Run(action);
};
current.Disposed += handler;
return Task.CompletedTask;
}
return action();
}
/// <summary>
/// 新开独立事务执行一步。失败只记日志,不抛回调用方。
/// </summary>
public static async Task<bool> RunStepAsync(
IServiceScopeFactory scopeFactory,
ILogger logger,
string step,
Func<IServiceProvider, Task> work)
{
try
{
using var scope = scopeFactory.CreateScope();
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Independent());
await OmsIntegrationEventPublisher.EnsureOutboxDbContextAsync(unitOfWorkManager);
await work(scope.ServiceProvider);
await unitOfWork.CompleteAsync();
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "订单后置步骤失败 Step={Step}", step);
return false;
}
}
}

View File

@ -1,119 +0,0 @@
using System.Data;
using Fake.EntityFrameworkCore;
using Fake.EventBus.Distributed;
using Fake.EventBus.IntegrationEventLog;
using Fake.UnitOfWork;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Oms.Application.Messaging;
/// <summary>
/// 可靠发布 IntegrationEvent:事务 UoW 内写入 Outbox(由 Fake.EventBus.IntegrationEventLog 投递)。
/// SqlSugar 与 EF 默认各持连接;若环境事务已由 SqlSugar 占用,则改用 RequiresNew 短事务只写 Outbox。
/// </summary>
internal static class OmsIntegrationEventPublisher
{
public static async Task PublishAsync(
IDistributedEventBus eventBus,
IUnitOfWorkManager unitOfWorkManager,
ILogger logger,
IntegrationEvent @event,
CancellationToken cancellationToken = default)
{
if (OmsOutboxUnitOfWork.IsAmbientTransactional(unitOfWorkManager))
{
try
{
await EnsureOutboxDbContextAsync(unitOfWorkManager);
await eventBus.PublishAsync(@event, cancellationToken);
logger.LogDebug(
"Outbox enqueued {EventName} Id={EventId} (ambient TX)",
@event.GetType().Name,
@event.Id);
return;
}
catch (Exception ex) when (ShouldUseIndependentOutbox(ex))
{
// 环境事务已提交、连接对不上,或还没有可征用的数据库事务时,改走独立短事务。
logger.LogWarning(
ex,
"Ambient UoW TX incompatible with EF Outbox; using short outbox TX. Event={EventName} Id={EventId}",
@event.GetType().Name,
@event.Id);
}
}
else
{
logger.LogWarning(
"Publish {EventName} Id={EventId} without ambient transactional UoW; opening short outbox TX",
@event.GetType().Name,
@event.Id);
}
using var uow = unitOfWorkManager.Begin(new UnitOfWorkAttribute
{
IsTransactional = true,
RequiresNew = true,
IsolationLevel = IsolationLevel.ReadCommitted
});
await EnsureOutboxDbContextAsync(unitOfWorkManager);
await eventBus.PublishAsync(@event, cancellationToken);
await uow.CompleteAsync(cancellationToken);
}
public static async Task<bool> TryPublishAsync(
IDistributedEventBus eventBus,
IUnitOfWorkManager unitOfWorkManager,
ILogger logger,
IntegrationEvent @event,
CancellationToken cancellationToken = default)
{
try
{
await PublishAsync(eventBus, unitOfWorkManager, logger, @event, cancellationToken);
return true;
}
catch (Exception ex)
{
logger.LogError(
ex,
"Outbox publish failed {EventName} Id={EventId}",
@event.GetType().Name,
@event.Id);
return false;
}
}
/// <summary>
/// 业务写库前先打开 Outbox DbContext,尽量让 EF 先占住环境事务连接,供 SqlSugar 征用。
/// </summary>
public static Task EnsureOutboxDbContextAsync(IUnitOfWorkManager unitOfWorkManager)
{
var current = unitOfWorkManager.Current
?? throw new InvalidOperationException("No ambient unit of work");
var dbProvider = current.ServiceProvider
.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>();
return dbProvider.GetDbContextAsync();
}
private static bool ShouldUseIndependentOutbox(Exception ex)
{
for (var e = ex; e != null; e = e.InnerException!)
{
var message = e.Message;
if (message.Contains("not associated with the current connection", StringComparison.OrdinalIgnoreCase)
|| message.Contains("associated with the current connection", StringComparison.OrdinalIgnoreCase)
|| message.Contains("可征用的数据库事务", StringComparison.Ordinal)
|| message.Contains("returned null", StringComparison.OrdinalIgnoreCase)
|| message.Contains("No ambient transaction", StringComparison.OrdinalIgnoreCase)
|| message.Contains("已经回滚", StringComparison.Ordinal)
|| message.Contains("已被调用", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}

View File

@ -1,36 +0,0 @@
using Fake.EventBus.Distributed;
using Fake.UnitOfWork;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Oms.Application.Messaging;
/// <summary>
/// 通过 Fake 分布式事件总线(Outbox)发布订单批量任务。
/// </summary>
public sealed class OrderBatchJobPublisher(
IDistributedEventBus eventBus,
IUnitOfWorkManager unitOfWorkManager,
IOptions<OrderBatchRabbitMqOptions> options,
ILogger<OrderBatchJobPublisher> logger) : IOrderBatchJobPublisher
{
public bool IsEnabled => options.Value.Enabled;
public async Task PublishAsync(OrderBatchJobMessage message, CancellationToken cancellationToken = default)
{
if (!options.Value.Enabled)
{
throw new InvalidOperationException("Order batch RabbitMQ is disabled.");
}
var @event = OrderBatchJobIntegrationEvent.FromMessage(message);
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
logger.LogDebug(
"Outbox enqueued order batch event {EventId} messageId={MessageId} action={Action}",
@event.Id,
message.MessageId,
message.Action);
}
}

View File

@ -1,22 +0,0 @@
using Oms.Application.Services.Orders.Issues;
namespace Oms.Application.Messaging.OrderSideEffects;
/// <summary>
/// 订单侧后置副作用发布(生命周期轨迹、自动触发等)。
/// </summary>
public interface IOrderSideEffectEventPublisher
{
Task PublishLifecycleTrackAppendAsync(
int orderId,
string orderNo,
string trackCode,
string? authorization = null,
CancellationToken cancellationToken = default);
Task PublishAutoTriggerFireAsync(
int orderId,
AutoTriggerTiming timing,
int operatorUserId,
CancellationToken cancellationToken = default);
}

View File

@ -1,61 +0,0 @@
using Fake.EventBus.Distributed;
using Fake.UnitOfWork;
using Microsoft.Extensions.Logging;
using Oms.Application.Services.Orders.Issues;
namespace Oms.Application.Messaging.OrderSideEffects;
/// <summary>
/// 订单旁路副作用事件:事务内写 Outbox,Commit 后投递 RabbitMQ。
/// </summary>
public sealed class OrderSideEffectEventPublisher(
IDistributedEventBus eventBus,
IUnitOfWorkManager unitOfWorkManager,
ILogger<OrderSideEffectEventPublisher> logger) : IOrderSideEffectEventPublisher
{
public async Task PublishLifecycleTrackAppendAsync(
int orderId,
string orderNo,
string trackCode,
string? authorization = null,
CancellationToken cancellationToken = default)
{
if (orderId <= 0 || string.IsNullOrWhiteSpace(orderNo) || string.IsNullOrWhiteSpace(trackCode))
{
return;
}
var @event = new OrderLifecycleTrackAppendIntegrationEvent
{
OrderId = orderId,
OrderNo = orderNo.Trim(),
TrackCode = trackCode.Trim(),
Authorization = string.IsNullOrWhiteSpace(authorization) ? null : authorization.Trim()
};
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
public async Task PublishAutoTriggerFireAsync(
int orderId,
AutoTriggerTiming timing,
int operatorUserId,
CancellationToken cancellationToken = default)
{
if (orderId <= 0)
{
return;
}
var @event = new OrderAutoTriggerFireIntegrationEvent
{
OrderId = orderId,
Timing = timing.ToString(),
OperatorUserId = operatorUserId
};
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
}

View File

@ -1,46 +0,0 @@
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.SmallBagTrack;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Messaging.ParcelCreate;
/// <summary>
/// 创单后置副作用:发布到 Fake.EventBus.RabbitMQ;发布失败时回落同步执行。
/// </summary>
public interface IOrderParcelCreateEventPublisher
{
/// <summary>
/// 轨迹订单 + 创建成功轨迹(A0101)合并为一个事件。
/// </summary>
Task PublishTrackUpsertAsync(
Order order,
SmallBagTrackUpsertRequest request,
int? customerId,
CancellationToken cancellationToken = default);
Task PublishLabelGenerateAsync(
Order order,
int operatorUserId = 0,
CancellationToken cancellationToken = default);
Task PublishCustoms9610UpsertAsync(
Order order,
string? platformCode = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 发布揽收挂接事件。返回 true 表示已入队;false 表示已同步回落执行(或参数不足未执行)。
/// </summary>
Task<bool> PublishPickupAttachAsync(
OrderParcelCreateInput input,
int orderId,
CancellationToken cancellationToken = default);
/// <summary>
/// 同步执行揽收挂接(用于创单 HTTP 需要回写 PickupNo 的场景)。
/// </summary>
Task<PickupOrderOutput?> ExecutePickupAttachAsync(
OrderParcelCreateInput input,
int orderId,
CancellationToken cancellationToken = default);
}

View File

@ -1,143 +0,0 @@
using Fake.EventBus.Distributed;
using Fake.UnitOfWork;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Services.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Messaging.ParcelCreate;
/// <summary>
/// 创单后置事件发布器:事务内写 Outbox(Fake.EventBus.IntegrationEventLog),Commit 后投递 RabbitMQ。
/// 揽收 Publish 失败时由调用方同步 ExecutePickupAttachAsync 回落。
/// </summary>
public sealed class OrderParcelCreateEventPublisher(
IDistributedEventBus eventBus,
IUnitOfWorkManager unitOfWorkManager,
IServiceScopeFactory scopeFactory,
ILogger<OrderParcelCreateEventPublisher> logger) : IOrderParcelCreateEventPublisher
{
public async Task PublishTrackUpsertAsync(
Order order,
SmallBagTrackUpsertRequest request,
int? customerId,
CancellationToken cancellationToken = default)
{
var @event = new OrderParcelTrackUpsertIntegrationEvent
{
OrderId = order.Id,
CustomerId = customerId,
PayloadJson = JsonConvert.SerializeObject(request),
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
Authorization = request.Authorization
};
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
public async Task PublishLabelGenerateAsync(
Order order,
int operatorUserId = 0,
CancellationToken cancellationToken = default)
{
var @event = new OrderParcelLabelGenerateIntegrationEvent
{
OrderId = order.Id,
OperatorUserId = operatorUserId
};
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
public async Task PublishCustoms9610UpsertAsync(
Order order,
string? platformCode = null,
CancellationToken cancellationToken = default)
{
var @event = new OrderParcelCustoms9610UpsertIntegrationEvent
{
OrderId = order.Id,
PlatformCode = platformCode
};
await OmsIntegrationEventPublisher.PublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
public async Task<bool> PublishPickupAttachAsync(
OrderParcelCreateInput input,
int orderId,
CancellationToken cancellationToken = default)
{
var @event = BuildPickupEvent(input, orderId);
if (@event == null)
{
return false;
}
return await OmsIntegrationEventPublisher.TryPublishAsync(
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
}
public async Task<PickupOrderOutput?> ExecutePickupAttachAsync(
OrderParcelCreateInput input,
int orderId,
CancellationToken cancellationToken = default)
{
if (orderId <= 0
|| input.SenderAddressId is not > 0
|| input.ExpectPickupTime is null
|| input.ExpectPickupTime == default
|| input.WarehouseId is not > 0)
{
return null;
}
using var scope = scopeFactory.CreateScope();
var pickupService = scope.ServiceProvider.GetRequiredService<PickupOrderService>();
return 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 = [orderId],
Remark = null
});
}
private static OrderParcelPickupAttachIntegrationEvent? BuildPickupEvent(
OrderParcelCreateInput input,
int orderId)
{
if (orderId <= 0
|| input.SenderAddressId is not > 0
|| input.ExpectPickupTime is null
|| input.ExpectPickupTime == default
|| input.WarehouseId is not > 0)
{
return null;
}
return new OrderParcelPickupAttachIntegrationEvent
{
OrderId = 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
};
}
}

View File

@ -1,8 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Messaging.OrderSideEffects;
using Oms.Application.Messaging.ParcelCreate;
namespace Oms.Application; namespace Oms.Application;
@ -11,10 +9,7 @@ public static class OrderBatchMessagingServiceExtensions
public static IServiceCollection AddOmsOrderBatchMessaging(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddOmsOrderBatchMessaging(this IServiceCollection services, IConfiguration configuration)
{ {
services.Configure<OrderBatchRabbitMqOptions>(configuration.GetSection(OrderBatchRabbitMqOptions.SectionName)); services.Configure<OrderBatchRabbitMqOptions>(configuration.GetSection(OrderBatchRabbitMqOptions.SectionName));
services.AddSingleton<IOrderBatchJobPublisher, OrderBatchJobPublisher>();
services.AddSingleton<IOrderBatchQueueInspector, OrderBatchQueueInspector>(); services.AddSingleton<IOrderBatchQueueInspector, OrderBatchQueueInspector>();
services.AddSingleton<IOrderParcelCreateEventPublisher, OrderParcelCreateEventPublisher>();
services.AddSingleton<IOrderSideEffectEventPublisher, OrderSideEffectEventPublisher>();
// 消费端由 Fake.EventBus.RabbitMQ(RabbitMqEventBus HostedService)托管 // 消费端由 Fake.EventBus.RabbitMQ(RabbitMqEventBus HostedService)托管
services.AddHostedService<OrderBatchDlxMonitorHostedService>(); services.AddHostedService<OrderBatchDlxMonitorHostedService>();
return services; return services;

View File

@ -1,5 +1,6 @@
using Fake.Application; using Fake.Application;
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.EventBus.Distributed;
using Fake.ObjectStorage; using Fake.ObjectStorage;
using Newtonsoft.Json; using Newtonsoft.Json;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
@ -24,7 +25,7 @@ public sealed class OrderBatchActionExecutor(
IObjectStorage fileStorage, IObjectStorage fileStorage,
ISmallBagTrackOrderClient smallBagTrackOrderClient, ISmallBagTrackOrderClient smallBagTrackOrderClient,
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder, ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
IOrderParcelCreateEventPublisher parcelCreateEventPublisher, IDistributedEventBus eventBus,
OrderDownstreamCredentialResolver downstreamCredentialResolver, OrderDownstreamCredentialResolver downstreamCredentialResolver,
IItfxForwardClient itfxForwardClient, IItfxForwardClient itfxForwardClient,
OrderFileManager orderFileManager, OrderFileManager orderFileManager,
@ -252,11 +253,14 @@ public sealed class OrderBatchActionExecutor(
trackRequest.Authorization = authorization.Trim(); trackRequest.Authorization = authorization.Trim();
} }
await parcelCreateEventPublisher.PublishTrackUpsertAsync( await eventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
order, {
trackRequest, OrderId = order.Id,
customerId: order.CustomerId, CustomerId = order.CustomerId,
cancellationToken); PayloadJson = JsonConvert.SerializeObject(trackRequest),
AppendInitialTrackOnSuccess = trackRequest.AppendInitialTrackOnSuccess,
Authorization = trackRequest.Authorization
}, cancellationToken);
} }
} }

View File

@ -1,6 +1,6 @@
using Fake.Application; using Fake.Application;
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.UnitOfWork; using Fake.EventBus.Distributed;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -29,9 +29,8 @@ public sealed class OrderBatchJobCoordinator(
ICountryRepository countryRepository, ICountryRepository countryRepository,
IServiceChannelRepository serviceChannelRepository, IServiceChannelRepository serviceChannelRepository,
IObjectStorage fileStorage, IObjectStorage fileStorage,
IOrderBatchJobPublisher publisher, IDistributedEventBus eventBus,
OrderBatchJobProcessor batchJobProcessor, OrderBatchJobProcessor batchJobProcessor,
IUnitOfWorkManager unitOfWorkManager,
IOptions<OrderBatchRabbitMqOptions> batchOptions, IOptions<OrderBatchRabbitMqOptions> batchOptions,
ILogger<OrderBatchJobCoordinator> logger) : IScopedDependency ILogger<OrderBatchJobCoordinator> logger) : IScopedDependency
{ {
@ -44,7 +43,7 @@ public sealed class OrderBatchJobCoordinator(
string? authorization = null, string? authorization = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!publisher.IsEnabled) if (!batchOptions.Value.Enabled)
{ {
throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置"); throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置");
} }
@ -78,12 +77,6 @@ public sealed class OrderBatchJobCoordinator(
} }
} }
// 先打开 EF Outbox 上下文,尽量让环境事务由 EF 占连接,避免 SqlSugar 先开事务后 Outbox 征用失败。
if (OmsOutboxUnitOfWork.IsAmbientTransactional(unitOfWorkManager))
{
await OmsIntegrationEventPublisher.EnsureOutboxDbContextAsync(unitOfWorkManager);
}
var now = DateTime.Now; var now = DateTime.Now;
var job = new OrderBatchJob var job = new OrderBatchJob
{ {
@ -161,7 +154,7 @@ public sealed class OrderBatchJobCoordinator(
MessageId = item.MessageId ?? $"{job.Id}:{item.OrderId}:{action}", MessageId = item.MessageId ?? $"{job.Id}:{item.OrderId}:{action}",
}; };
await publisher.PublishAsync(message, cancellationToken); await eventBus.PublishAsync(OrderBatchJobIntegrationEvent.FromMessage(message), cancellationToken);
} }
logger.LogInformation( logger.LogInformation(
@ -181,7 +174,7 @@ public sealed class OrderBatchJobCoordinator(
int operatorUserId, int operatorUserId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!publisher.IsEnabled) if (!batchOptions.Value.Enabled)
{ {
throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置"); throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置");
} }
@ -194,8 +187,9 @@ public sealed class OrderBatchJobCoordinator(
for (var index = 0; index < items.Count; index++) for (var index = 0; index < items.Count; index++)
{ {
await publisher.PublishAsync( await eventBus.PublishAsync(
BuildPayloadMessage(job, items[index], itemsInput[index], input.CustomerId, operatorUserId), OrderBatchJobIntegrationEvent.FromMessage(
BuildPayloadMessage(job, items[index], itemsInput[index], input.CustomerId, operatorUserId)),
cancellationToken); cancellationToken);
} }
@ -383,7 +377,7 @@ public sealed class OrderBatchJobCoordinator(
public async Task<bool> PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId) public async Task<bool> PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId)
{ {
if (!publisher.IsEnabled) if (!batchOptions.Value.Enabled)
{ {
logger.LogDebug("RabbitMQ disabled, track upsert will use direct call. orderId={OrderId}", orderId); logger.LogDebug("RabbitMQ disabled, track upsert will use direct call. orderId={OrderId}", orderId);
return false; return false;
@ -401,7 +395,7 @@ public sealed class OrderBatchJobCoordinator(
MessageId = $"track:{orderId}:{Guid.NewGuid():N}", MessageId = $"track:{orderId}:{Guid.NewGuid():N}",
}; };
await publisher.PublishAsync(message); await eventBus.PublishAsync(OrderBatchJobIntegrationEvent.FromMessage(message));
return true; return true;
} }
@ -437,7 +431,7 @@ public sealed class OrderBatchJobCoordinator(
int operatorUserId, int operatorUserId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!publisher.IsEnabled) if (!batchOptions.Value.Enabled)
{ {
throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置"); throw new BusinessException("订单批量队列未启用,请检查 RabbitMQ 配置");
} }
@ -472,7 +466,9 @@ public sealed class OrderBatchJobCoordinator(
item.UpdateUserId = operatorUserId; item.UpdateUserId = operatorUserId;
await batchJobRepository.UpdateItemAsync(item, cancellationToken); await batchJobRepository.UpdateItemAsync(item, cancellationToken);
await publisher.PublishAsync(BuildMessageFromJobItem(job, item, operatorUserId), cancellationToken); await eventBus.PublishAsync(
OrderBatchJobIntegrationEvent.FromMessage(BuildMessageFromJobItem(job, item, operatorUserId)),
cancellationToken);
} }
job.Status = OrderBatchJobStatusCodes.Running; job.Status = OrderBatchJobStatusCodes.Running;

View File

@ -1,4 +1,5 @@
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.EventBus.Distributed;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Oms.Application.Messaging.OrderSideEffects; using Oms.Application.Messaging.OrderSideEffects;
using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Consts;
@ -16,7 +17,7 @@ public class AutoTriggerRuntimeService(
OrderAutoTriggerService orderAutoTriggerService, OrderAutoTriggerService orderAutoTriggerService,
OrderExceptionWorkflowService exceptionWorkflowService, OrderExceptionWorkflowService exceptionWorkflowService,
OrderIssueNotificationService issueNotificationService, OrderIssueNotificationService issueNotificationService,
IOrderSideEffectEventPublisher sideEffectEventPublisher, IDistributedEventBus eventBus,
ILogger<AutoTriggerRuntimeService> logger) : IScopedDependency ILogger<AutoTriggerRuntimeService> logger) : IScopedDependency
{ {
private const string InterceptOperation = "拦截"; private const string InterceptOperation = "拦截";
@ -27,7 +28,12 @@ public class AutoTriggerRuntimeService(
public Task EnqueueFireAsync(int orderId, AutoTriggerTiming timing, int operatorUserId) => public Task EnqueueFireAsync(int orderId, AutoTriggerTiming timing, int operatorUserId) =>
orderId <= 0 orderId <= 0
? Task.CompletedTask ? Task.CompletedTask
: sideEffectEventPublisher.PublishAutoTriggerFireAsync(orderId, timing, operatorUserId); : eventBus.PublishAsync(new OrderAutoTriggerFireIntegrationEvent
{
OrderId = orderId,
Timing = timing.ToString(),
OperatorUserId = operatorUserId
});
public async Task FireAsync(Order order, AutoTriggerTiming timing, int operatorUserId) public async Task FireAsync(Order order, AutoTriggerTiming timing, int operatorUserId)
{ {

View File

@ -1,4 +1,6 @@
using Fake.Application; using Fake.Application;
using Fake.EventBus.Distributed;
using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Messaging; using Oms.Application.Messaging;
@ -10,8 +12,11 @@ namespace Oms.Application.Services.Orders;
public partial class OrderService public partial class OrderService
{ {
private IOrderBatchJobPublisher BatchJobPublisher => private IDistributedEventBus EventBus =>
LazyServiceProvider.GetRequiredService<IOrderBatchJobPublisher>(); LazyServiceProvider.GetRequiredService<IDistributedEventBus>();
private bool BatchQueueEnabled =>
LazyServiceProvider.GetRequiredService<IOptions<OrderBatchRabbitMqOptions>>().Value.Enabled;
/// <summary> /// <summary>
/// 批量 Worker 导入单行 Excel 数据(内部方法,勿暴露为 HTTP API)。 /// 批量 Worker 导入单行 Excel 数据(内部方法,勿暴露为 HTTP API)。

View File

@ -554,7 +554,7 @@ public partial class OrderService
var batchInput = await BuildImportBatchJobInputAsync(input); var batchInput = await BuildImportBatchJobInputAsync(input);
var operatorUserId = CurrentUser.GetId<int>(); var operatorUserId = CurrentUser.GetId<int>();
if (BatchJobPublisher.IsEnabled) if (BatchQueueEnabled)
{ {
var job = await BatchJobCoordinator.CreatePayloadJobAsync(batchInput, operatorUserId); var job = await BatchJobCoordinator.CreatePayloadJobAsync(batchInput, operatorUserId);
return new OrderUploadOutput { JobId = job.JobId, Async = true }; return new OrderUploadOutput { JobId = job.JobId, Async = true };

View File

@ -41,7 +41,6 @@ public partial class OrderService
private const string InboundBillingProcessingReason = "入库后计费/排货处理中"; private const string InboundBillingProcessingReason = "入库后计费/排货处理中";
private const string InboundBillingEnqueueFailReason = "入库计费任务投递失败,请重跑入库计费"; private const string InboundBillingEnqueueFailReason = "入库计费任务投递失败,请重跑入库计费";
internal const string ForecastBillingProcessingReason = "预报计费/排货处理中"; internal const string ForecastBillingProcessingReason = "预报计费/排货处理中";
private const string ForecastBillingEnqueueFailReason = "预报计费任务投递失败,请重新预报";
private static readonly ConcurrentDictionary<int, byte> InboundBillingInFlight = new(); private static readonly ConcurrentDictionary<int, byte> InboundBillingInFlight = new();
@ -100,11 +99,15 @@ public partial class OrderService
}; };
} }
/// <summary>
/// /order/parcel/create 的实际步骤。调用方已开启事务;这里抛出的异常会回滚订单和已写入的 Outbox。
/// </summary>
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync( private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(
OrderParcelCreateInput input, OrderParcelCreateInput input,
int? operatorUserId = null, int? operatorUserId = null,
ParcelCreateContext? createContext = null) ParcelCreateContext? createContext = null)
{ {
// 1. 幂等:同一 RequestId 直接返回已有订单,不再建单、不再发事件。
if (!string.IsNullOrWhiteSpace(input.RequestId)) if (!string.IsNullOrWhiteSpace(input.RequestId))
{ {
var existed = await OrderRepository.GetByRequestIdAsync(input.RequestId); var existed = await OrderRepository.GetByRequestIdAsync(input.RequestId);
@ -118,6 +121,7 @@ public partial class OrderService
NormalizeParcelCreateInput(input); NormalizeParcelCreateInput(input);
createContext ??= BuildParcelCreateContext(input); createContext ??= BuildParcelCreateContext(input);
// 2. 硬校验。拦截类自动触发在落库前执行,命中即失败,订单不会留下。
var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId); var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
input.CustomerId = sessionCustomer.Id; input.CustomerId = sessionCustomer.Id;
@ -151,11 +155,9 @@ public partial class OrderService
country.CountryCode, country.CountryCode,
country.ChineseName); country.ChineseName);
// 3. 落库订单、件重尺和收发件等子表。此后才有 OrderId,才能写 Outbox。
var serviceNo = await ResolveServiceNoAsync(); var serviceNo = await ResolveServiceNoAsync();
var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0); var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
Order? created = null;
try
{
var order = BuildDraftOrderEntity( var order = BuildDraftOrderEntity(
input, input,
product, product,
@ -165,20 +167,25 @@ public partial class OrderService
serviceNo, serviceNo,
createContext); createContext);
order = await OrderRepository.InsertAsync(order); order = await OrderRepository.InsertAsync(order);
created = order;
var weights = BuildInitialWeightRows(order.Id, input); var weights = BuildInitialWeightRows(order.Id, input);
await SaveParcelChildrenAsync(order, input, weights, country); await SaveParcelChildrenAsync(order, input, weights, country);
// 客户主动「先保存为草稿」:落库后返回。9610 / 揽收在事务提交后各自执行。 // 4. 保存草稿:不做预报计费。9610、上门揽收与订单一起提交。
if (input.SaveAsDraft) if (input.SaveAsDraft)
{ {
await EventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent
{
OrderId = order.Id,
PlatformCode = input.PlatformCode
});
var draftOutput = await BuildParcelCreateOutputAsync(order); var draftOutput = await BuildParcelCreateOutputAsync(order);
await ScheduleCreateSideEffectsAsync(order.Id, input, draftOutput, includeForecast: false, operatorId); await TryAttachPickupAfterParcelCreateAsync(input, draftOutput);
return draftOutput; return draftOutput;
} }
// 偏远标记轻量同步;计费重/应收/排货走 ForecastBilling MQ(失败不回滚建单)。 // 5. 确认下单:偏远标记当场算完;计费重/应收/排货留给预报任务。
// 状态先停在草稿 +「预报计费/排货处理中」,接口即可返回。
order.IsRemote = await IsRemoteHitAsync( order.IsRemote = await IsRemoteHitAsync(
product, product,
snapshotBinding.Rules, snapshotBinding.Rules,
@ -192,72 +199,31 @@ public partial class OrderService
order.UpdateTime = DateTime.Now; order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order); await OrderRepository.UpdateAsync(order);
// 6. 与订单同一事务写入 Outbox。Commit 后由 Fake 投递;任一条失败则整单回滚。
// 轨迹不在此列:要等预报消费端排货拿到 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);
var output = await BuildParcelCreateOutputAsync(order); var output = await BuildParcelCreateOutputAsync(order);
await ScheduleCreateSideEffectsAsync(order.Id, input, output, includeForecast: true, operatorId); output.ForecastBillingQueued = string.Equals(
order.PredeclareFailReason,
ForecastBillingProcessingReason,
StringComparison.Ordinal);
// 揽收失败只回写 PickupCreateError,不让整单回滚。
await TryAttachPickupAfterParcelCreateAsync(input, output);
return output; return output;
} }
catch (BusinessException)
{
await TryRollbackFailedParcelCreateAsync(created);
throw;
}
catch (Exception ex)
{
// 创建失败不落库草稿:回滚已插入订单,由前端让客户选择继续修改或主动保存草稿。
await TryRollbackFailedParcelCreateAsync(created);
var cleanReason = StripDraftSavedMarker(ex.Message);
if (string.IsNullOrWhiteSpace(cleanReason))
{
cleanReason = "预报未完成";
}
throw new BusinessException(cleanReason);
}
}
private async Task TryRollbackFailedParcelCreateAsync(Order? created)
{
if (created == null)
{
return;
}
try
{
await RollbackFailedParcelCreateAsync(created);
}
catch
{
// 回滚失败不覆盖原始业务错误;半成品订单需运维清理
}
}
/// <summary>
/// 创建流程中途失败时删除已落库的半成品订单,避免自动变成草稿。
/// </summary>
private async Task RollbackFailedParcelCreateAsync(Order order)
{
try
{
var writer = LazyServiceProvider.GetRequiredService<Customs9610OrderWriter>();
// 强制清掉可能已写入的 pending 9610(ExportCustomsMode 临时置空触发删除逻辑)
var mode = order.ExportCustomsMode;
order.ExportCustomsMode = null;
await writer.UpsertFromOrderAsync(order);
order.ExportCustomsMode = mode;
}
catch
{
// ignore
}
order.OrderStatus = OrderStatusCodes.Deleted;
order.PredeclareFailReason = string.Empty;
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
await OrderRepository.DeleteAsync(order);
}
private async Task TryUpsertCustoms9610Async(Order order, string? platformCode = null) private async Task TryUpsertCustoms9610Async(Order order, string? platformCode = null)
{ {
@ -2330,28 +2296,13 @@ public partial class OrderService
var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0); var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
var authorization = CaptureRequestAuthorization(); var authorization = CaptureRequestAuthorization();
var publisher = LazyServiceProvider.GetRequiredService<IOrderBatchJobPublisher>(); await EnqueueForecastBillingAsync(order, order.CouponInstanceId, operatorId, authorization);
if (!publisher.IsEnabled) await EventBus.PublishAsync(new OrderParcelLabelGenerateIntegrationEvent
{ {
var payloadJson = JsonConvert.SerializeObject(new ForecastBillingJobPayload OrderId = order.Id,
{ OperatorUserId = operatorId
CouponInstanceId = order.CouponInstanceId,
Authorization = authorization
}); });
await ProcessForecastBillingForBatchAsync( await AutoTriggerRuntimeService.EnqueueFireAsync(order.Id, AutoTriggerTiming.OrderCreate, operatorId);
order.Id,
payloadJson,
operatorId,
authorization: authorization);
}
else
{
var orderId = order.Id;
var couponInstanceId = order.CouponInstanceId;
await ScheduleForecastEnqueueAsync(orderId, couponInstanceId, operatorId, authorization);
}
await ScheduleForecastSideEffectsAsync(order.Id, operatorId);
// 同步回落失败时给出明确错误;入队成功则保持「处理中」由列表观察结果。 // 同步回落失败时给出明确错误;入队成功则保持「处理中」由列表观察结果。
var refreshed = await OrderRepository.FirstOrDefaultAsync(x => x.Id == order.Id); var refreshed = await OrderRepository.FirstOrDefaultAsync(x => x.Id == order.Id);
@ -2846,7 +2797,7 @@ public partial class OrderService
// 消费端没有 HttpContext,须用创单时捕获的 Authorization,否则轨迹网关返回 401。 // 消费端没有 HttpContext,须用创单时捕获的 Authorization,否则轨迹网关返回 401。
if (allocated && !string.IsNullOrWhiteSpace(order.ChannelCode)) if (allocated && !string.IsNullOrWhiteSpace(order.ChannelCode))
{ {
await ScheduleTrackAfterAllocationAsync(order.Id, trackAuthorization); await PublishForecastTrackAsync(order.Id, trackAuthorization);
} }
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true, includeWeights: true); var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true, includeWeights: true);
@ -2872,154 +2823,6 @@ public partial class OrderService
public string? Authorization { get; set; } public string? Authorization { get; set; }
} }
/// <summary>
/// 创单事务提交后,9610、预报计费、揽收、面单、自动触发各自提交。
/// 轨迹订单等排货成功后再创建。
/// </summary>
private async Task ScheduleCreateSideEffectsAsync(
int orderId,
OrderParcelCreateInput input,
OrderParcelCreateOutput output,
bool includeForecast,
int operatorUserId)
{
var unitOfWorkManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var scopeFactory = LazyServiceProvider.GetRequiredService<IServiceScopeFactory>();
var logger = LazyServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Oms.OrderParcelCreate");
var platformCode = input.PlatformCode;
var couponInstanceId = input.CouponInstanceId;
// 事务提交后 HttpContext 已不可用,这里先捕获登录态供轨迹网关使用。
var authorization = CaptureRequestAuthorization();
await OmsAfterCommit.ScheduleAsync(unitOfWorkManager, async () =>
{
try
{
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "customs-9610", sp =>
sp.GetRequiredService<OrderService>().PublishCustomsForOrderAsync(orderId, platformCode));
var forecastQueued = true;
if (includeForecast)
{
forecastQueued = await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "forecast-billing", async sp =>
{
var service = sp.GetRequiredService<OrderService>();
await service.EnqueueForecastBillingByIdAsync(
orderId,
couponInstanceId,
operatorUserId,
authorization);
});
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "label-generate", sp =>
sp.GetRequiredService<OrderService>().PublishForecastLabelAsync(orderId, operatorUserId));
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "auto-trigger", sp =>
sp.GetRequiredService<OrderService>().PublishForecastAutoTriggerAsync(orderId, operatorUserId));
}
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "pickup-attach", sp =>
sp.GetRequiredService<OrderService>().TryAttachPickupAfterParcelCreateAsync(input, output));
if (includeForecast && !forecastQueued)
{
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "forecast-billing-fail", sp =>
sp.GetRequiredService<OrderService>().MarkForecastEnqueueFailedIfStillProcessingAsync(orderId));
}
}
catch (Exception ex)
{
logger.LogError(ex, "创单后置步骤调度失败 OrderId={OrderId}", orderId);
}
});
}
private async Task ScheduleForecastEnqueueAsync(
int orderId,
int? couponInstanceId,
int operatorUserId,
string? authorization)
{
var unitOfWorkManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var scopeFactory = LazyServiceProvider.GetRequiredService<IServiceScopeFactory>();
var logger = LazyServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Oms.ForecastBilling");
await OmsAfterCommit.ScheduleAsync(unitOfWorkManager, () =>
OmsAfterCommit.RunStepAsync(scopeFactory, logger, "forecast-billing", sp =>
sp.GetRequiredService<OrderService>().EnqueueForecastBillingByIdAsync(
orderId,
couponInstanceId,
operatorUserId,
authorization)));
}
/// <summary>
/// 草稿重新预报提交后,面单和下单自动触发各自入队。轨迹等排货成功后再创建。
/// </summary>
private async Task ScheduleForecastSideEffectsAsync(int orderId, int operatorUserId)
{
var unitOfWorkManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var scopeFactory = LazyServiceProvider.GetRequiredService<IServiceScopeFactory>();
var logger = LazyServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Oms.ForecastBilling");
await OmsAfterCommit.ScheduleAsync(unitOfWorkManager, async () =>
{
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "label-generate", sp =>
sp.GetRequiredService<OrderService>().PublishForecastLabelAsync(orderId, operatorUserId));
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "auto-trigger", sp =>
sp.GetRequiredService<OrderService>().PublishForecastAutoTriggerAsync(orderId, operatorUserId));
});
}
/// <summary>
/// 预报计费事务提交后,再创建轨迹订单(此时已有渠道和 ApiCode)。
/// </summary>
private async Task ScheduleTrackAfterAllocationAsync(int orderId, string? authorization)
{
var unitOfWorkManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var scopeFactory = LazyServiceProvider.GetRequiredService<IServiceScopeFactory>();
var logger = LazyServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Oms.ForecastBilling");
await OmsAfterCommit.ScheduleAsync(unitOfWorkManager, () =>
OmsAfterCommit.RunStepAsync(scopeFactory, logger, "track-upsert", sp =>
sp.GetRequiredService<OrderService>().PublishForecastTrackAsync(orderId, authorization)));
}
private async Task PublishCustomsForOrderAsync(int orderId, string? platformCode)
{
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
if (order == null
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
{
return;
}
await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, platformCode);
}
private async Task EnqueueForecastBillingByIdAsync(
int orderId,
int? couponInstanceId,
int operatorUserId,
string? authorization)
{
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
if (order == null
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
{
return;
}
await EnqueueForecastBillingAsync(order, couponInstanceId, operatorUserId, authorization);
}
private async Task MarkForecastEnqueueFailedIfStillProcessingAsync(int orderId)
{
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
if (order == null
|| !string.Equals(order.PredeclareFailReason, ForecastBillingProcessingReason, StringComparison.Ordinal))
{
return;
}
await MarkParcelDraftFailedAsync(order, ForecastBillingEnqueueFailReason);
}
private async Task PublishForecastTrackAsync(int orderId, string? authorization = null) private async Task PublishForecastTrackAsync(int orderId, string? authorization = null)
{ {
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId); var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
@ -3036,41 +2839,26 @@ public partial class OrderService
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiverInput: null); var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiverInput: null);
request.AppendInitialTrackOnSuccess = true; request.AppendInitialTrackOnSuccess = true;
ApplyTrackAuthorization(request, authorization); ApplyTrackAuthorization(request, authorization);
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id); await EventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
}
private async Task PublishForecastLabelAsync(int orderId, int operatorUserId)
{ {
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId); OrderId = order.Id,
if (order == null CustomerId = customer?.Id,
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase)) PayloadJson = JsonConvert.SerializeObject(request),
{ AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
return; Authorization = request.Authorization
} });
await ParcelCreateEventPublisher.PublishLabelGenerateAsync(order, operatorUserId);
}
private async Task PublishForecastAutoTriggerAsync(int orderId, int operatorUserId)
{
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
if (order == null
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
{
return;
}
await AutoTriggerRuntimeService.EnqueueFireAsync(orderId, AutoTriggerTiming.OrderCreate, operatorUserId);
} }
/// <summary>
/// 预报计费入队。队列开着写 ForecastBilling Outbox;关着则在当前事务里同步跑同一套计费。
/// Authorization 放进任务载荷,供排货成功后转发轨迹网关。
/// </summary>
private async Task EnqueueForecastBillingAsync( private async Task EnqueueForecastBillingAsync(
Order order, Order order,
int? couponInstanceId, int? couponInstanceId,
int operatorUserId, int operatorUserId,
string? authorization = null) string? authorization = null)
{ {
var logger = LazyServiceProvider.GetRequiredService<ILoggerFactory>()
.CreateLogger("Oms.ForecastBilling");
authorization ??= CaptureRequestAuthorization(); authorization ??= CaptureRequestAuthorization();
var payloadJson = JsonConvert.SerializeObject(new ForecastBillingJobPayload var payloadJson = JsonConvert.SerializeObject(new ForecastBillingJobPayload
{ {
@ -3078,31 +2866,13 @@ public partial class OrderService
Authorization = authorization Authorization = authorization
}); });
try if (!BatchQueueEnabled)
{
var publisher = LazyServiceProvider.GetRequiredService<IOrderBatchJobPublisher>();
if (!publisher.IsEnabled)
{
try
{ {
await ProcessForecastBillingForBatchAsync( await ProcessForecastBillingForBatchAsync(
order.Id, order.Id,
payloadJson, payloadJson,
operatorUserId, operatorUserId,
authorization: authorization); authorization: authorization);
}
catch (Exception ex)
{
logger.LogError(
ex,
"预报计费同步执行失败 OrderId={OrderId} ServiceNo={ServiceNo}",
order.Id,
order.ServiceNo);
await MarkParcelDraftFailedAsync(
order,
TrimPendingReason($"预报计费失败:{ex.Message}"));
}
return; return;
} }
@ -3118,23 +2888,6 @@ public partial class OrderService
operatorUserId, operatorUserId,
authorization); authorization);
} }
catch (Exception ex)
{
logger.LogError(
ex,
"预报计费任务投递失败 OrderId={OrderId} ServiceNo={ServiceNo}",
order.Id,
order.ServiceNo);
try
{
await MarkParcelDraftFailedAsync(order, ForecastBillingEnqueueFailReason);
}
catch
{
// ignore
}
}
}
private async Task EnqueueInboundBillingAsync(Order order) private async Task EnqueueInboundBillingAsync(Order order)
{ {
@ -3142,8 +2895,7 @@ public partial class OrderService
.CreateLogger("Oms.InboundBilling"); .CreateLogger("Oms.InboundBilling");
try try
{ {
var publisher = LazyServiceProvider.GetRequiredService<IOrderBatchJobPublisher>(); if (!BatchQueueEnabled)
if (!publisher.IsEnabled)
{ {
// 本地未开队列:同步跑异步计费逻辑,仍不向外抛(入库主流程已完成) // 本地未开队列:同步跑异步计费逻辑,仍不向外抛(入库主流程已完成)
try try
@ -3397,12 +3149,6 @@ public partial class OrderService
}; };
} }
private IOrderParcelCreateEventPublisher ParcelCreateEventPublisher =>
LazyServiceProvider.GetRequiredService<IOrderParcelCreateEventPublisher>();
private IOrderSideEffectEventPublisher SideEffectEventPublisher =>
LazyServiceProvider.GetRequiredService<IOrderSideEffectEventPublisher>();
/// <summary> /// <summary>
/// 小包预报成功后异步同步轨迹订单(POST /ProviderService/Order/UpsertSmallBag)。 /// 小包预报成功后异步同步轨迹订单(POST /ProviderService/Order/UpsertSmallBag)。
/// </summary> /// </summary>
@ -3415,7 +3161,14 @@ public partial class OrderService
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver); var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
request.AppendInitialTrackOnSuccess = appendInitialTrack; request.AppendInitialTrackOnSuccess = appendInitialTrack;
ApplyTrackAuthorization(request); ApplyTrackAuthorization(request);
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id); await EventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
{
OrderId = order.Id,
CustomerId = customer?.Id,
PayloadJson = JsonConvert.SerializeObject(request),
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
Authorization = request.Authorization
});
} }
private void ApplyTrackAuthorization(SmallBagTrackUpsertRequest request, string? authorization = null) private void ApplyTrackAuthorization(SmallBagTrackUpsertRequest request, string? authorization = null)
@ -3486,11 +3239,13 @@ public partial class OrderService
.Authorization .Authorization
.ToString(); .ToString();
await SideEffectEventPublisher.PublishLifecycleTrackAppendAsync( await EventBus.PublishAsync(new OrderLifecycleTrackAppendIntegrationEvent
order.Id, {
order.ServiceNo, OrderId = order.Id,
trackCode, OrderNo = order.ServiceNo,
string.IsNullOrWhiteSpace(authorization) ? null : authorization); TrackCode = trackCode,
Authorization = string.IsNullOrWhiteSpace(authorization) ? null : authorization
});
} }
/// <summary> /// <summary>

View File

@ -603,34 +603,20 @@ public partial class OrderService : ApplicationService
} }
/// <summary> /// <summary>
/// 创建订单包裹(PC/H5 确认下单或保存草稿)。 /// 创建订单包裹(PC/H5 确认下单或保存草稿)。整段在一个事务里,异常直接抛出并回滚。
/// <para> /// <para>
/// 【同步,本接口事务内】RequestId 幂等 → 校验入参/销售产品 → 黑名单 → /// 【本请求内】RequestId 幂等 → 校验 → 黑名单 → 下单前拦截规则 → 落库订单与子表。
/// 自动触发「拦截」规则(命中直接失败)→ 生成运单号 → 落库订单与子表。 /// 保存草稿只再写 9610 与上门揽收;确认下单额外标记「预报处理中」,并写入预报计费、面单、自动触发。
/// 确认下单:同步写偏远标记,状态草稿 +「预报计费/排货处理中」后返回;
/// 保存草稿:仅落库,不做预报计费。失败会回滚订单,不落草稿。
/// </para> /// </para>
/// <para> /// <para>
/// 【创单事务提交后各自投递 MQ(互不等待;失败不回滚订单)】 /// 【同一事务的 Outbox,Commit 后才投递】9610、预报计费、面单、OrderCreate 自动触发、上门揽收。
/// 1) 9610 订单 Upsert(customs-9610)— 草稿与确认下单都会投; /// 队列关闭时预报计费在本事务内同步执行。轨迹不在这里发,等预报消费端排货成功后再发。
/// 2) 预报计费/排货任务(forecast-billing 批量任务)— 仅确认下单;
/// 队列关闭时在后台同步跑同一套计费逻辑;
/// 3) 系统面单 PDF(label-generate)— 仅确认下单;
/// 4) 下单自动触发(OrderCreate:警告/问题件等,不含拦截)— 仅确认下单;
/// 5) 上门揽收挂载(pickup-attach)— 仅上门揽收且地址/时间/仓库齐全时;
/// 投递失败则同步回落建揽收单。
/// </para>
/// <para>
/// 【预报计费消费后】计费重、应收、排货应付、营销快照 → 订单改已预报。
/// 排货成功且已有 ChannelCode 时,再投递轨迹订单 + A0101(track-upsert);
/// 轨迹网关需要登录 Authorization,创单时写入预报任务 Payload,消费端原样转发;
/// 排货失败不阻断预报,也不建轨迹(可页面补做)。
/// </para> /// </para>
/// </summary> /// </summary>
/// <param name="input">入参:订单包裹创建参数。SaveAsDraft=true 为仅保存草稿。</param> /// <param name="input">入参:订单包裹创建参数。SaveAsDraft=true 为仅保存草稿。</param>
/// <returns> /// <returns>
/// 订单包裹创建结果。确认下单时 ForecastBillingQueued 表示预报计费已排队; /// 订单包裹创建结果。确认下单时 ForecastBillingQueued 表示预报计费已入队;
/// 上门揽收时 PickupQueued / PickupNo 视投递或同步回落结果而定。 /// 上门揽收入队成功时 PickupQueued=true,失败只写 PickupCreateError,不回滚订单。
/// </returns> /// </returns>
[HttpPost("/order/parcel/create")] [HttpPost("/order/parcel/create")]
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)] [UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
@ -680,6 +666,9 @@ public partial class OrderService : ApplicationService
} }
} }
/// <summary>
/// 仅上门揽收。地址、期望时间和仓库齐全才入队;发布失败写入返回值,不抛回创单事务。
/// </summary>
private async Task TryAttachPickupAfterParcelCreateAsync( private async Task TryAttachPickupAfterParcelCreateAsync(
OrderParcelCreateInput input, OrderParcelCreateInput input,
OrderParcelCreateOutput output) OrderParcelCreateOutput output)
@ -715,27 +704,20 @@ public partial class OrderService : ApplicationService
return; return;
} }
var publisher = LazyServiceProvider.GetRequiredService<IOrderParcelCreateEventPublisher>();
try try
{ {
// 优先入 Fake.EventBus;发布失败则同步回落建揽收并回写单号。 await EventBus.PublishAsync(new OrderParcelPickupAttachIntegrationEvent
var queued = await publisher.PublishPickupAttachAsync(input, output.OrderId);
if (queued)
{ {
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; output.PickupQueued = true;
return;
}
var pickup = await publisher.ExecutePickupAttachAsync(input, output.OrderId);
if (pickup == null)
{
output.PickupCreateError = "揽收单创建失败";
return;
}
output.PickupNo = pickup.PickupNo;
output.PickupOrderId = pickup.Id;
output.CarrierWaybillNo = pickup.CarrierWaybillNo;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -762,7 +744,7 @@ public partial class OrderService : ApplicationService
throw new BusinessException($"单次批量创建不能超过{ParcelBatchMaxCount}条"); throw new BusinessException($"单次批量创建不能超过{ParcelBatchMaxCount}条");
} }
if (BatchJobPublisher.IsEnabled) if (BatchQueueEnabled)
{ {
return await BatchCreateParcelViaQueueAsync(input); return await BatchCreateParcelViaQueueAsync(input);
} }

View File

@ -6,8 +6,6 @@ using Fake.Testing;
using Fake.UnitOfWork; using Fake.UnitOfWork;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Tests.Messaging.Fakes; using Oms.Tests.Messaging.Fakes;
@ -18,8 +16,8 @@ public class OrderBatchJobOutboxPublisherTests : ApplicationTestWithTools<OmsOut
{ {
private readonly IUnitOfWorkManager _uowManager; private readonly IUnitOfWorkManager _uowManager;
private readonly RecordingPhysicalDistributedEventBus _physicalBus; private readonly RecordingPhysicalDistributedEventBus _physicalBus;
private readonly IDistributedEventBus _eventBus;
private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider; private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider;
private readonly OrderBatchJobPublisher _publisher;
protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options) protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options)
{ {
@ -29,13 +27,9 @@ public class OrderBatchJobOutboxPublisherTests : ApplicationTestWithTools<OmsOut
public OrderBatchJobOutboxPublisherTests() public OrderBatchJobOutboxPublisherTests()
{ {
_uowManager = ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); _uowManager = ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
_eventBus = ServiceProvider.GetRequiredService<IDistributedEventBus>();
_physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>(); _physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>();
_dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>(); _dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>();
_publisher = new OrderBatchJobPublisher(
ServiceProvider.GetRequiredService<IDistributedEventBus>(),
_uowManager,
Options.Create(new OrderBatchRabbitMqOptions { Enabled = true }),
NullLogger<OrderBatchJobPublisher>.Instance);
} }
[Fact] [Fact]
@ -56,7 +50,7 @@ public class OrderBatchJobOutboxPublisherTests : ApplicationTestWithTools<OmsOut
using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional())) using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional()))
{ {
await _dbProvider.GetDbContextAsync(); await _dbProvider.GetDbContextAsync();
await _publisher.PublishAsync(message); await _eventBus.PublishAsync(OrderBatchJobIntegrationEvent.FromMessage(message));
_physicalBus.Published.Should().BeEmpty(); _physicalBus.Published.Should().BeEmpty();
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
@ -68,23 +62,4 @@ public class OrderBatchJobOutboxPublisherTests : ApplicationTestWithTools<OmsOut
evt.MessageId.Should().Be(message.MessageId); evt.MessageId.Should().Be(message.MessageId);
} }
[Fact]
public async Task 队列未启用时_Publish应抛错()
{
var disabled = new OrderBatchJobPublisher(
ServiceProvider.GetRequiredService<IDistributedEventBus>(),
_uowManager,
Options.Create(new OrderBatchRabbitMqOptions { Enabled = false }),
NullLogger<OrderBatchJobPublisher>.Instance);
var act = () => disabled.PublishAsync(new OrderBatchJobMessage
{
Action = OrderBatchActionCodes.ForecastBilling,
OrderId = 1,
MessageId = "x"
});
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*disabled*");
}
} }

View File

@ -6,7 +6,7 @@ using Fake.Testing;
using Fake.UnitOfWork; using Fake.UnitOfWork;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json;
using Oms.Application.Integrations.SmallBagTrack; using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Messaging.ParcelCreate; using Oms.Application.Messaging.ParcelCreate;
@ -22,7 +22,6 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
private readonly RecordingPhysicalDistributedEventBus _physicalBus; private readonly RecordingPhysicalDistributedEventBus _physicalBus;
private readonly IOutboxEventLogService _outbox; private readonly IOutboxEventLogService _outbox;
private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider; private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider;
private readonly OrderParcelCreateEventPublisher _publisher;
protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options) protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options)
{ {
@ -36,11 +35,6 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
_physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>(); _physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>();
_outbox = ServiceProvider.GetRequiredService<IOutboxEventLogService>(); _outbox = ServiceProvider.GetRequiredService<IOutboxEventLogService>();
_dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>(); _dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>();
_publisher = new OrderParcelCreateEventPublisher(
_distributedEventBus,
_uowManager,
ServiceProvider.GetRequiredService<IServiceScopeFactory>(),
NullLogger<OrderParcelCreateEventPublisher>.Instance);
} }
[Fact] [Fact]
@ -58,7 +52,11 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional())) using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional()))
{ {
await _dbProvider.GetDbContextAsync(); await _dbProvider.GetDbContextAsync();
await _publisher.PublishCustoms9610UpsertAsync(order, "lazada"); await _distributedEventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent
{
OrderId = order.Id,
PlatformCode = "lazada"
});
_physicalBus.Published.Should().BeEmpty(); _physicalBus.Published.Should().BeEmpty();
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
@ -86,8 +84,19 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional())) using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional()))
{ {
await _dbProvider.GetDbContextAsync(); await _dbProvider.GetDbContextAsync();
await _publisher.PublishTrackUpsertAsync(order, request, customerId: 9); await _distributedEventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
await _publisher.PublishLabelGenerateAsync(order, operatorUserId: 7); {
OrderId = order.Id,
CustomerId = 9,
PayloadJson = JsonConvert.SerializeObject(request),
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
Authorization = request.Authorization
});
await _distributedEventBus.PublishAsync(new OrderParcelLabelGenerateIntegrationEvent
{
OrderId = order.Id,
OperatorUserId = 7
});
_physicalBus.Published.Should().BeEmpty(); _physicalBus.Published.Should().BeEmpty();
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
@ -108,7 +117,10 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional())) using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional()))
{ {
await _dbProvider.GetDbContextAsync(); await _dbProvider.GetDbContextAsync();
await _publisher.PublishCustoms9610UpsertAsync(order); await _distributedEventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent
{
OrderId = order.Id
});
await uow.RollbackAsync(); await uow.RollbackAsync();
} }
@ -116,16 +128,16 @@ public class OrderParcelCreateOutboxPublisherTests : ApplicationTestWithTools<Om
} }
[Fact] [Fact]
public async Task 无环境事务时_短事务仍能写入并投递() public async Task 无环境事务时_总线应拒绝发布()
{ {
_physicalBus.Clear();
_uowManager.Current.Should().BeNull(); _uowManager.Current.Should().BeNull();
var order = new Order { Id = 404, ServiceNo = "SN404" }; var act = () => _distributedEventBus.PublishAsync(new OrderParcelCustoms9610UpsertIntegrationEvent
await _publisher.PublishCustoms9610UpsertAsync(order, "shopee"); {
OrderId = 404,
PlatformCode = "shopee"
});
_physicalBus.Published.Should().ContainSingle() await act.Should().ThrowAsync<InvalidOperationException>();
.Which.Should().BeOfType<OrderParcelCustoms9610UpsertIntegrationEvent>()
.Which.OrderId.Should().Be(404);
} }
} }

View File

@ -6,7 +6,6 @@ using Fake.Testing;
using Fake.UnitOfWork; using Fake.UnitOfWork;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Messaging.OrderSideEffects; using Oms.Application.Messaging.OrderSideEffects;
using Oms.Application.Services.Orders.Issues; using Oms.Application.Services.Orders.Issues;
@ -20,7 +19,6 @@ public class OrderSideEffectOutboxPublisherTests : ApplicationTestWithTools<OmsO
private readonly IDistributedEventBus _distributedEventBus; private readonly IDistributedEventBus _distributedEventBus;
private readonly RecordingPhysicalDistributedEventBus _physicalBus; private readonly RecordingPhysicalDistributedEventBus _physicalBus;
private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider; private readonly IEfDbContextProvider<IntegrationEventLogContext> _dbProvider;
private readonly OrderSideEffectEventPublisher _publisher;
protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options) protected override void SetApplicationCreationOptions(FakeApplicationCreationOptions options)
{ {
@ -33,10 +31,6 @@ public class OrderSideEffectOutboxPublisherTests : ApplicationTestWithTools<OmsO
_distributedEventBus = ServiceProvider.GetRequiredService<IDistributedEventBus>(); _distributedEventBus = ServiceProvider.GetRequiredService<IDistributedEventBus>();
_physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>(); _physicalBus = ServiceProvider.GetRequiredService<RecordingPhysicalDistributedEventBus>();
_dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>(); _dbProvider = ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>();
_publisher = new OrderSideEffectEventPublisher(
_distributedEventBus,
_uowManager,
NullLogger<OrderSideEffectEventPublisher>.Instance);
} }
[Fact] [Fact]
@ -47,8 +41,18 @@ public class OrderSideEffectOutboxPublisherTests : ApplicationTestWithTools<OmsO
using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional())) using (var uow = _uowManager.Begin(OmsOutboxUnitOfWork.Transactional()))
{ {
await _dbProvider.GetDbContextAsync(); await _dbProvider.GetDbContextAsync();
await _publisher.PublishAutoTriggerFireAsync(11, AutoTriggerTiming.OrderCreate, 3); await _distributedEventBus.PublishAsync(new OrderAutoTriggerFireIntegrationEvent
await _publisher.PublishLifecycleTrackAppendAsync(11, "SN11", "A0301"); {
OrderId = 11,
Timing = AutoTriggerTiming.OrderCreate.ToString(),
OperatorUserId = 3
});
await _distributedEventBus.PublishAsync(new OrderLifecycleTrackAppendIntegrationEvent
{
OrderId = 11,
OrderNo = "SN11",
TrackCode = "A0301"
});
_physicalBus.Published.Should().BeEmpty(); _physicalBus.Published.Should().BeEmpty();
await uow.CompleteAsync(); await uow.CompleteAsync();
} }

View File

@ -1,7 +1,7 @@
using FluentAssertions; using FluentAssertions;
using Fake.Application; using Fake.Application;
using Fake.EventBus.Distributed;
using Fake.ObjectStorage; using Fake.ObjectStorage;
using Fake.UnitOfWork;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
@ -23,13 +23,12 @@ public class OrderBatchJobCoordinatorGovernanceTests
private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose); private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose);
private readonly Mock<IServiceChannelRepository> _serviceChannelRepository = new(MockBehavior.Loose); private readonly Mock<IServiceChannelRepository> _serviceChannelRepository = new(MockBehavior.Loose);
private readonly Mock<IObjectStorage> _fileStorage = new(MockBehavior.Loose); private readonly Mock<IObjectStorage> _fileStorage = new(MockBehavior.Loose);
private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict); private readonly Mock<IDistributedEventBus> _eventBus = new(MockBehavior.Strict);
private readonly OrderBatchJobCoordinator _coordinator; private readonly OrderBatchJobCoordinator _coordinator;
public OrderBatchJobCoordinatorGovernanceTests() public OrderBatchJobCoordinatorGovernanceTests()
{ {
_publisher.SetupGet(x => x.IsEnabled).Returns(true); var batchOptions = Options.Create(new OrderBatchRabbitMqOptions { Enabled = true });
var batchOptions = Options.Create(new OrderBatchRabbitMqOptions());
var actionExecutor = new Mock<IOrderBatchActionExecutor>(MockBehavior.Loose); var actionExecutor = new Mock<IOrderBatchActionExecutor>(MockBehavior.Loose);
var processor = new OrderBatchJobProcessor( var processor = new OrderBatchJobProcessor(
_batchJobRepository.Object, _batchJobRepository.Object,
@ -37,17 +36,14 @@ public class OrderBatchJobCoordinatorGovernanceTests
actionExecutor.Object, actionExecutor.Object,
batchOptions, batchOptions,
NullLogger<OrderBatchJobProcessor>.Instance); NullLogger<OrderBatchJobProcessor>.Instance);
var unitOfWorkManager = new Mock<IUnitOfWorkManager>(MockBehavior.Loose);
unitOfWorkManager.SetupGet(x => x.Current).Returns((IUnitOfWork?)null);
_coordinator = new OrderBatchJobCoordinator( _coordinator = new OrderBatchJobCoordinator(
_batchJobRepository.Object, _batchJobRepository.Object,
_orderRepository.Object, _orderRepository.Object,
_countryRepository.Object, _countryRepository.Object,
_serviceChannelRepository.Object, _serviceChannelRepository.Object,
_fileStorage.Object, _fileStorage.Object,
_publisher.Object, _eventBus.Object,
processor, processor,
unitOfWorkManager.Object,
batchOptions, batchOptions,
NullLogger<OrderBatchJobCoordinator>.Instance); NullLogger<OrderBatchJobCoordinator>.Instance);
} }
@ -71,8 +67,8 @@ public class OrderBatchJobCoordinatorGovernanceTests
_batchJobRepository _batchJobRepository
.Setup(x => x.UpdateJobAsync(job, It.IsAny<CancellationToken>())) .Setup(x => x.UpdateJobAsync(job, It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
_publisher _eventBus
.Setup(x => x.PublishAsync(It.IsAny<OrderBatchJobMessage>(), It.IsAny<CancellationToken>())) .Setup(x => x.PublishAsync(It.IsAny<OrderBatchJobIntegrationEvent>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var output = await _coordinator.RetryFailedAsync(job.Id, null, 99); var output = await _coordinator.RetryFailedAsync(job.Id, null, 99);
@ -83,9 +79,9 @@ public class OrderBatchJobCoordinatorGovernanceTests
failedItem.RetryCount.Should().Be(0); failedItem.RetryCount.Should().Be(0);
failedItem.FailReason.Should().BeNull(); failedItem.FailReason.Should().BeNull();
job.Status.Should().Be(OrderBatchJobStatusCodes.Running); job.Status.Should().Be(OrderBatchJobStatusCodes.Running);
_publisher.Verify( _eventBus.Verify(
x => x.PublishAsync( x => x.PublishAsync(
It.Is<OrderBatchJobMessage>(m => m.JobItemId == failedItem.Id), It.Is<OrderBatchJobIntegrationEvent>(m => m.JobItemId == failedItem.Id),
It.IsAny<CancellationToken>()), It.IsAny<CancellationToken>()),
Times.Once); Times.Once);
} }