fix: 创单成功后把预报计费和后续事件拆开提交
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
7b69bbf207
commit
79fa20a9ca
@ -53,8 +53,7 @@ public sealed class LabelPdfGenerationService(
|
|||||||
|
|
||||||
var relativeUrl = await SavePdfAsync(order.ServiceNo, pdfBytes, cancellationToken);
|
var relativeUrl = await SavePdfAsync(order.ServiceNo, pdfBytes, cancellationToken);
|
||||||
order.LabelTemplateId = templateId;
|
order.LabelTemplateId = templateId;
|
||||||
order.UpdateTime = DateTime.Now;
|
await orderRepository.UpdateLabelTemplateIdAsync(order.Id, templateId.Value, cancellationToken);
|
||||||
await orderRepository.UpdateAsync(order);
|
|
||||||
|
|
||||||
await orderFileManager.ReplaceSystemLabelAsync(
|
await orderFileManager.ReplaceSystemLabelAsync(
|
||||||
order.Id,
|
order.Id,
|
||||||
|
|||||||
49
src/Oms.Application/Messaging/OmsAfterCommit.cs
Normal file
49
src/Oms.Application/Messaging/OmsAfterCommit.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Fake.UnitOfWork;
|
||||||
|
|
||||||
|
namespace Oms.Application.Messaging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把创单/预报之后的副作用延后到当前事务提交成功再执行,每一步单独提交。
|
||||||
|
/// </summary>
|
||||||
|
internal static class OmsAfterCommit
|
||||||
|
{
|
||||||
|
public static async Task ScheduleAsync(IUnitOfWorkManager unitOfWorkManager, Func<Task> action)
|
||||||
|
{
|
||||||
|
var current = unitOfWorkManager.Current;
|
||||||
|
if (current != null && !current.IsCompleted)
|
||||||
|
{
|
||||||
|
current.OnCompleted(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,16 @@ public static class OmsOutboxUnitOfWork
|
|||||||
IsolationLevel = IsolationLevel.ReadCommitted
|
IsolationLevel = IsolationLevel.ReadCommitted
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 与当前业务事务脱开的短事务,用于创单/预报成功之后的独立事件。
|
||||||
|
/// </summary>
|
||||||
|
public static UnitOfWorkAttribute Independent() => new()
|
||||||
|
{
|
||||||
|
IsTransactional = true,
|
||||||
|
RequiresNew = true,
|
||||||
|
IsolationLevel = IsolationLevel.ReadCommitted
|
||||||
|
};
|
||||||
|
|
||||||
public static bool IsAmbientTransactional(IUnitOfWorkManager unitOfWorkManager) =>
|
public static bool IsAmbientTransactional(IUnitOfWorkManager unitOfWorkManager) =>
|
||||||
unitOfWorkManager.Current?.Context.IsTransactional == true;
|
unitOfWorkManager.Current?.Context.IsTransactional == true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public sealed class OrderBatchJobEventHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForecastBilling 成功后继续写轨迹/面单/自动触发 Outbox,须事务性 UoW
|
// 业务结果在本事务提交;轨迹/面单/自动触发由处理过程登记到提交后单独执行。
|
||||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||||
await processor.ProcessAsync(@event.ToMessage(), cancellationToken);
|
await processor.ProcessAsync(@event.ToMessage(), cancellationToken);
|
||||||
await unitOfWork.CompleteAsync(cancellationToken);
|
await unitOfWork.CompleteAsync(cancellationToken);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
|
using Oms.Application.Services.Orders;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.OrdersAggregate.Repositories;
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
@ -94,6 +95,7 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
await OrderBatchJobItemFeedback.EnrichIdentifiersAsync(item, action, orderRepository, cancellationToken);
|
await OrderBatchJobItemFeedback.EnrichIdentifiersAsync(item, action, orderRepository, cancellationToken);
|
||||||
OrderBatchJobItemFeedback.ApplyBusinessFailure(item, result, action);
|
OrderBatchJobItemFeedback.ApplyBusinessFailure(item, result, action);
|
||||||
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
||||||
|
await TryStampForecastBillingFailureAsync(message, item.FailReason, cancellationToken);
|
||||||
|
|
||||||
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
||||||
return;
|
return;
|
||||||
@ -111,6 +113,7 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
action,
|
action,
|
||||||
item.RetryCount,
|
item.RetryCount,
|
||||||
item.FailReason);
|
item.FailReason);
|
||||||
|
await TryStampForecastBillingFailureAsync(message, item.FailReason, cancellationToken);
|
||||||
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -148,6 +151,43 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预报计费重试耗尽或业务失败时,把原因写回订单,避免一直显示「处理中」。
|
||||||
|
/// 更具体的失败原因已经写上时不再覆盖。
|
||||||
|
/// </summary>
|
||||||
|
private async Task TryStampForecastBillingFailureAsync(
|
||||||
|
OrderBatchJobMessage message,
|
||||||
|
string? reason,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!string.Equals(message.Action, OrderBatchActionCodes.ForecastBilling, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.OrderId <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == message.OrderId, cancellationToken);
|
||||||
|
if (order == null
|
||||||
|
|| !string.Equals(
|
||||||
|
order.PredeclareFailReason,
|
||||||
|
OrderService.ForecastBillingProcessingReason,
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = string.IsNullOrWhiteSpace(reason) ? "预报计费失败" : reason.Trim();
|
||||||
|
if (text.Length > 512)
|
||||||
|
{
|
||||||
|
text = text[..512];
|
||||||
|
}
|
||||||
|
|
||||||
|
order.OrderStatus = OrderStatusCodes.Draft;
|
||||||
|
order.PredeclareFailReason = text;
|
||||||
|
order.UpdateTime = DateTime.Now;
|
||||||
|
await orderRepository.UpdateAsync(order, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RefreshJobSummaryAsync(int jobId, CancellationToken cancellationToken)
|
private async Task RefreshJobSummaryAsync(int jobId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var job = await batchJobRepository.GetByIdAsync(jobId, cancellationToken);
|
var job = await batchJobRepository.GetByIdAsync(jobId, cancellationToken);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Fake.Application;
|
|||||||
using Fake.UnitOfWork;
|
using Fake.UnitOfWork;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Oms.Application.Dtos.Channels;
|
using Oms.Application.Dtos.Channels;
|
||||||
@ -39,7 +40,7 @@ public partial class OrderService
|
|||||||
private const int ParcelBatchMaxCount = 2000;
|
private const int ParcelBatchMaxCount = 2000;
|
||||||
private const string InboundBillingProcessingReason = "入库后计费/排货处理中";
|
private const string InboundBillingProcessingReason = "入库后计费/排货处理中";
|
||||||
private const string InboundBillingEnqueueFailReason = "入库计费任务投递失败,请重跑入库计费";
|
private const string InboundBillingEnqueueFailReason = "入库计费任务投递失败,请重跑入库计费";
|
||||||
private const string ForecastBillingProcessingReason = "预报计费/排货处理中";
|
internal const string ForecastBillingProcessingReason = "预报计费/排货处理中";
|
||||||
private const string ForecastBillingEnqueueFailReason = "预报计费任务投递失败,请重新预报";
|
private const string ForecastBillingEnqueueFailReason = "预报计费任务投递失败,请重新预报";
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<int, byte> InboundBillingInFlight = new();
|
private static readonly ConcurrentDictionary<int, byte> InboundBillingInFlight = new();
|
||||||
@ -99,19 +100,6 @@ public partial class OrderService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 业务写库前先打开 Outbox DbContext,让 EF 先占环境事务连接,
|
|
||||||
/// 避免 SqlSugar 先开事务后 Outbox UseTransaction 失败而落入「短事务提前 Commit」。
|
|
||||||
/// </summary>
|
|
||||||
private async Task EnsureOutboxReadyAsync()
|
|
||||||
{
|
|
||||||
var uowManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
|
|
||||||
if (OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager))
|
|
||||||
{
|
|
||||||
await OmsIntegrationEventPublisher.EnsureOutboxDbContextAsync(uowManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(
|
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(
|
||||||
OrderParcelCreateInput input,
|
OrderParcelCreateInput input,
|
||||||
int? operatorUserId = null,
|
int? operatorUserId = null,
|
||||||
@ -164,12 +152,10 @@ public partial class OrderService
|
|||||||
country.ChineseName);
|
country.ChineseName);
|
||||||
|
|
||||||
var serviceNo = await ResolveServiceNoAsync();
|
var serviceNo = await ResolveServiceNoAsync();
|
||||||
|
var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
|
||||||
Order? created = null;
|
Order? created = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 必须在任何 SqlSugar 写之前:保证 9610 / ForecastBilling / Pickup Outbox 与建单同事务提交。
|
|
||||||
await EnsureOutboxReadyAsync();
|
|
||||||
|
|
||||||
var order = BuildDraftOrderEntity(
|
var order = BuildDraftOrderEntity(
|
||||||
input,
|
input,
|
||||||
product,
|
product,
|
||||||
@ -184,12 +170,11 @@ public partial class OrderService
|
|||||||
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 / 揽收在事务提交后各自执行。
|
||||||
if (input.SaveAsDraft)
|
if (input.SaveAsDraft)
|
||||||
{
|
{
|
||||||
await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, input.PlatformCode);
|
|
||||||
var draftOutput = await BuildParcelCreateOutputAsync(order);
|
var draftOutput = await BuildParcelCreateOutputAsync(order);
|
||||||
await TryAttachPickupAfterParcelCreateAsync(input, draftOutput);
|
await ScheduleCreateSideEffectsAsync(order.Id, input, draftOutput, includeForecast: false, operatorId);
|
||||||
return draftOutput;
|
return draftOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,18 +192,8 @@ public partial class OrderService
|
|||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, input.PlatformCode);
|
|
||||||
await EnqueueForecastBillingAsync(
|
|
||||||
order,
|
|
||||||
input.CouponInstanceId,
|
|
||||||
operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0));
|
|
||||||
|
|
||||||
var output = await BuildParcelCreateOutputAsync(order);
|
var output = await BuildParcelCreateOutputAsync(order);
|
||||||
output.ForecastBillingQueued = string.Equals(
|
await ScheduleCreateSideEffectsAsync(order.Id, input, output, includeForecast: true, operatorId);
|
||||||
order.PredeclareFailReason,
|
|
||||||
ForecastBillingProcessingReason,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
await TryAttachPickupAfterParcelCreateAsync(input, output);
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
catch (BusinessException)
|
catch (BusinessException)
|
||||||
@ -2326,8 +2301,6 @@ public partial class OrderService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await EnsureOutboxReadyAsync();
|
|
||||||
|
|
||||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||||
var address = await ResolveOrderBillingAddressAsync(order);
|
var address = await ResolveOrderBillingAddressAsync(order);
|
||||||
|
|
||||||
@ -2355,10 +2328,24 @@ public partial class OrderService
|
|||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
await EnqueueForecastBillingAsync(
|
var operatorId = operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
|
||||||
order,
|
var publisher = LazyServiceProvider.GetRequiredService<IOrderBatchJobPublisher>();
|
||||||
order.CouponInstanceId,
|
if (!publisher.IsEnabled)
|
||||||
operatorUserId ?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0));
|
{
|
||||||
|
var payloadJson = JsonConvert.SerializeObject(new ForecastBillingJobPayload
|
||||||
|
{
|
||||||
|
CouponInstanceId = order.CouponInstanceId
|
||||||
|
});
|
||||||
|
await ProcessForecastBillingForBatchAsync(order.Id, payloadJson, operatorId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var orderId = order.Id;
|
||||||
|
var couponInstanceId = order.CouponInstanceId;
|
||||||
|
await ScheduleForecastEnqueueAsync(orderId, couponInstanceId, operatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@ -2681,10 +2668,10 @@ public partial class OrderService
|
|||||||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken);
|
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken);
|
||||||
if (order == null)
|
if (order == null)
|
||||||
{
|
{
|
||||||
return;
|
// 创单事务尚未提交时消息可能先到。抛出让总线重试,避免当成成功 Ack 后订单永远停在处理中。
|
||||||
|
throw new InvalidOperationException($"预报计费时订单尚未可见,稍后重试 OrderId={orderId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创单失败回滚 / 软删后,可能仍有短事务提前投递的孤儿任务
|
|
||||||
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -2852,21 +2839,6 @@ public partial class OrderService
|
|||||||
operatorName: null,
|
operatorName: null,
|
||||||
operatorType: "staff",
|
operatorType: "staff",
|
||||||
source: "job");
|
source: "job");
|
||||||
|
|
||||||
var customer = order.CustomerId.HasValue
|
|
||||||
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
|
||||||
: null;
|
|
||||||
await PublishParcelCreateTrackAndLabelAsync(
|
|
||||||
order,
|
|
||||||
customer,
|
|
||||||
receiver: null,
|
|
||||||
appendInitialTrack: true,
|
|
||||||
operatorUserId);
|
|
||||||
|
|
||||||
await AutoTriggerRuntimeService.EnqueueFireAsync(
|
|
||||||
order.Id,
|
|
||||||
AutoTriggerTiming.OrderCreate,
|
|
||||||
operatorUserId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ForecastBillingJobPayload
|
private sealed class ForecastBillingJobPayload
|
||||||
@ -2874,6 +2846,188 @@ public partial class OrderService
|
|||||||
public int? CouponInstanceId { get; set; }
|
public int? CouponInstanceId { 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;
|
||||||
|
|
||||||
|
await OmsAfterCommit.ScheduleAsync(unitOfWorkManager, async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var customsTask = OmsAfterCommit.RunStepAsync(scopeFactory, logger, "customs-9610", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().PublishCustomsForOrderAsync(orderId, platformCode));
|
||||||
|
var forecastTask = includeForecast
|
||||||
|
? OmsAfterCommit.RunStepAsync(scopeFactory, logger, "forecast-billing", async sp =>
|
||||||
|
{
|
||||||
|
var service = sp.GetRequiredService<OrderService>();
|
||||||
|
await service.EnqueueForecastBillingByIdAsync(orderId, couponInstanceId, operatorUserId);
|
||||||
|
var latest = await sp.GetRequiredService<IOrderRepository>()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == orderId);
|
||||||
|
if (latest != null)
|
||||||
|
{
|
||||||
|
output.OrderStatus = latest.OrderStatus;
|
||||||
|
output.PredeclareFailReason = latest.PredeclareFailReason;
|
||||||
|
output.ForecastBillingQueued = string.Equals(
|
||||||
|
latest.PredeclareFailReason,
|
||||||
|
ForecastBillingProcessingReason,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: Task.FromResult(true);
|
||||||
|
var pickupTask = OmsAfterCommit.RunStepAsync(scopeFactory, logger, "pickup-attach", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().TryAttachPickupAfterParcelCreateAsync(input, output));
|
||||||
|
var trackTask = includeForecast
|
||||||
|
? OmsAfterCommit.RunStepAsync(scopeFactory, logger, "track-upsert", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().PublishForecastTrackAsync(orderId))
|
||||||
|
: Task.FromResult(true);
|
||||||
|
var labelTask = includeForecast
|
||||||
|
? OmsAfterCommit.RunStepAsync(scopeFactory, logger, "label-generate", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().PublishForecastLabelAsync(orderId, operatorUserId))
|
||||||
|
: Task.FromResult(true);
|
||||||
|
var triggerTask = includeForecast
|
||||||
|
? OmsAfterCommit.RunStepAsync(scopeFactory, logger, "auto-trigger", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().PublishForecastAutoTriggerAsync(orderId, operatorUserId))
|
||||||
|
: Task.FromResult(true);
|
||||||
|
|
||||||
|
await Task.WhenAll(customsTask, forecastTask, pickupTask, trackTask, labelTask, triggerTask);
|
||||||
|
|
||||||
|
if (includeForecast && !await forecastTask)
|
||||||
|
{
|
||||||
|
await OmsAfterCommit.RunStepAsync(scopeFactory, logger, "forecast-billing-fail", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().MarkForecastEnqueueFailedIfStillProcessingAsync(orderId));
|
||||||
|
output.ForecastBillingQueued = false;
|
||||||
|
output.PredeclareFailReason = ForecastBillingEnqueueFailReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "创单后置步骤调度失败 OrderId={OrderId}", orderId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScheduleForecastEnqueueAsync(int orderId, int? couponInstanceId, int operatorUserId)
|
||||||
|
{
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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, "track-upsert", sp =>
|
||||||
|
sp.GetRequiredService<OrderService>().PublishForecastTrackAsync(orderId));
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
|
||||||
|
if (order == null
|
||||||
|
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = order.CustomerId.HasValue
|
||||||
|
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||||
|
: null;
|
||||||
|
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiverInput: null);
|
||||||
|
request.AppendInitialTrackOnSuccess = true;
|
||||||
|
ApplyTrackAuthorization(request);
|
||||||
|
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishForecastLabelAsync(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 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);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task EnqueueForecastBillingAsync(
|
private async Task EnqueueForecastBillingAsync(
|
||||||
Order order,
|
Order order,
|
||||||
int? couponInstanceId,
|
int? couponInstanceId,
|
||||||
@ -3206,40 +3360,8 @@ public partial class OrderService
|
|||||||
private IOrderSideEffectEventPublisher SideEffectEventPublisher =>
|
private IOrderSideEffectEventPublisher SideEffectEventPublisher =>
|
||||||
LazyServiceProvider.GetRequiredService<IOrderSideEffectEventPublisher>();
|
LazyServiceProvider.GetRequiredService<IOrderSideEffectEventPublisher>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创单/草稿预报成功后:轨迹订单+A0101 合并事件,系统面单独立事件(Fake.EventBus.RabbitMQ)。
|
|
||||||
/// </summary>
|
|
||||||
private async Task PublishParcelCreateTrackAndLabelAsync(
|
|
||||||
Order order,
|
|
||||||
Customer? customer,
|
|
||||||
OrderParcelReceiverInput? receiver,
|
|
||||||
bool appendInitialTrack,
|
|
||||||
int? operatorUserId = null)
|
|
||||||
{
|
|
||||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
|
||||||
request.AppendInitialTrackOnSuccess = appendInitialTrack;
|
|
||||||
var authorization = LazyServiceProvider
|
|
||||||
.GetRequiredService<IHttpContextAccessor>()
|
|
||||||
.HttpContext?
|
|
||||||
.Request
|
|
||||||
.Headers
|
|
||||||
.Authorization
|
|
||||||
.ToString();
|
|
||||||
if (!string.IsNullOrWhiteSpace(authorization))
|
|
||||||
{
|
|
||||||
request.Authorization = authorization;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id);
|
|
||||||
await ParcelCreateEventPublisher.PublishLabelGenerateAsync(
|
|
||||||
order,
|
|
||||||
operatorUserId
|
|
||||||
?? (CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 小包预报成功后异步同步轨迹订单(POST /ProviderService/Order/UpsertSmallBag)。
|
/// 小包预报成功后异步同步轨迹订单(POST /ProviderService/Order/UpsertSmallBag)。
|
||||||
/// 入库等非创单路径仍可调用;创单路径请走 <see cref="PublishParcelCreateTrackAndLabelAsync"/>。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task EnqueueSmallBagTrackUpsertAsync(
|
private async Task EnqueueSmallBagTrackUpsertAsync(
|
||||||
Order order,
|
Order order,
|
||||||
@ -3249,6 +3371,12 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
||||||
request.AppendInitialTrackOnSuccess = appendInitialTrack;
|
request.AppendInitialTrackOnSuccess = appendInitialTrack;
|
||||||
|
ApplyTrackAuthorization(request);
|
||||||
|
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyTrackAuthorization(SmallBagTrackUpsertRequest request)
|
||||||
|
{
|
||||||
var authorization = LazyServiceProvider
|
var authorization = LazyServiceProvider
|
||||||
.GetRequiredService<IHttpContextAccessor>()
|
.GetRequiredService<IHttpContextAccessor>()
|
||||||
.HttpContext?
|
.HttpContext?
|
||||||
@ -3260,8 +3388,6 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
request.Authorization = authorization;
|
request.Authorization = authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ParcelCreateEventPublisher.PublishTrackUpsertAsync(order, request, customer?.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -132,4 +132,9 @@ public interface IOrderRepository : IRepository<Order>
|
|||||||
DateTime startInclusive,
|
DateTime startInclusive,
|
||||||
DateTime endExclusive,
|
DateTime endExclusive,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 只更新标签模板 Id,避免面单生成覆盖预报计费正在写入的订单字段。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateLabelTemplateIdAsync(int orderId, int labelTemplateId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1021,6 +1021,15 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLabelTemplateIdAsync(int orderId, int labelTemplateId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
await db.Updateable<Order>()
|
||||||
|
.SetColumns(x => new Order { LabelTemplateId = labelTemplateId, UpdateTime = DateTime.Now })
|
||||||
|
.Where(x => x.Id == orderId)
|
||||||
|
.ExecuteCommandAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
public async Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var db = await GetDbContextAsync(cancellationToken);
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user