fix: 创单成功后把预报计费和后续事件拆开提交
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s

预报计费失败不再把订单留在处理中,轨迹、面单和自动触发在创单提交后各自入队。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-14 19:23:07 +08:00
parent 7b69bbf207
commit 79fa20a9ca
8 changed files with 329 additions and 91 deletions

View File

@ -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,

View 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;
}
}
}

View File

@ -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;
} }

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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);
} }

View File

@ -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);