diff --git a/Directory.Build.props b/Directory.Build.props
index 4676e0a..b45eadd 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,6 +4,6 @@
- 8.0.5.26
+ 8.0.5.27
diff --git a/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs b/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs
index f963a48..ddc7e64 100644
--- a/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs
+++ b/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs
@@ -1,7 +1,5 @@
using Fake.DependencyInjection;
using Fake.EventBus;
-using Fake.UnitOfWork;
-using Oms.Application.Messaging;
using Microsoft.Extensions.Logging;
using Oms.Application.Integrations.SmallBagTrack;
@@ -9,7 +7,6 @@ namespace Oms.Application.Messaging.OrderSideEffects;
public sealed class OrderLifecycleTrackAppendEventHandler(
ISmallBagTrackOrderClient trackClient,
- IUnitOfWorkManager unitOfWorkManager,
ILogger logger)
: IEventHandler, ITransientDependency
{
@@ -25,7 +22,7 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
return;
}
- using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
+ // 纯外部 HTTP,无需事务;占 TX 只会拖长连接占用。
try
{
await trackClient.AppendLifecycleTrackAsync(
@@ -44,7 +41,5 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
@event.OrderNo,
@event.TrackCode);
}
-
- await unitOfWork.CompleteAsync(cancellationToken);
}
}
diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs
index 5ce44e7..b188506 100644
--- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs
+++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs
@@ -4,6 +4,7 @@ using Fake.UnitOfWork;
using Oms.Application.Messaging;
using Microsoft.Extensions.Logging;
using Oms.Application.Services.Orders;
+using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Messaging.ParcelCreate;
@@ -24,9 +25,11 @@ public sealed class OrderParcelCustoms9610UpsertEventHandler(
{
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == @event.OrderId, cancellationToken);
- if (order == null)
+ if (order == null
+ || string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
{
- logger.LogWarning("9610 upsert skipped, order missing OrderId={OrderId}", @event.OrderId);
+ logger.LogWarning("9610 upsert skipped, order missing/deleted OrderId={OrderId}", @event.OrderId);
+ await unitOfWork.CompleteAsync(cancellationToken);
return;
}
diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs
index 0fb3f89..c2572b2 100644
--- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs
+++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs
@@ -2,7 +2,6 @@ using Fake.Application;
using Fake.DependencyInjection;
using Fake.EventBus;
using Fake.UnitOfWork;
-using Oms.Application.Messaging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Oms.Application.Integrations.SmallBagTrack;
@@ -25,7 +24,8 @@ public sealed class OrderParcelTrackUpsertEventHandler(
OrderParcelTrackUpsertIntegrationEvent @event,
CancellationToken cancellationToken)
{
- using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
+ // 仅读库 + 外部 HTTP,不要占事务连接;失败抛出让总线重试。
+ using var unitOfWork = unitOfWorkManager.Begin(new UnitOfWorkAttribute { IsTransactional = false });
var request = await ResolveRequestAsync(@event, cancellationToken);
request.AppendInitialTrackOnSuccess = @event.AppendInitialTrackOnSuccess;
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index 9d3b15a..1085937 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using Fake.Application;
+using Fake.UnitOfWork;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -76,6 +77,41 @@ public partial class OrderService
public string? InboundMethod { get; init; }
}
+ private static ParcelCreateContext? BuildParcelCreateContext(OrderParcelCreateInput input)
+ {
+ var inbound = string.IsNullOrWhiteSpace(input.InboundMethod)
+ ? null
+ : input.InboundMethod.Trim();
+ if (inbound == null)
+ {
+ return null;
+ }
+
+ var isDoorPickup = string.Equals(
+ inbound,
+ OrderInboundMethodCodes.DoorPickup,
+ StringComparison.OrdinalIgnoreCase);
+ return new ParcelCreateContext
+ {
+ InboundMethod = inbound,
+ PickupTime = isDoorPickup ? input.ExpectPickupTime : null,
+ CustomerSenderAddressId = isDoorPickup ? input.SenderAddressId : null
+ };
+ }
+
+ ///
+ /// 业务写库前先打开 Outbox DbContext,让 EF 先占环境事务连接,
+ /// 避免 SqlSugar 先开事务后 Outbox UseTransaction 失败而落入「短事务提前 Commit」。
+ ///
+ private async Task EnsureOutboxReadyAsync()
+ {
+ var uowManager = LazyServiceProvider.GetRequiredService();
+ if (OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager))
+ {
+ await OmsIntegrationEventPublisher.EnsureOutboxDbContextAsync(uowManager);
+ }
+ }
+
private async Task CreateParcelCoreAsync(
OrderParcelCreateInput input,
int? operatorUserId = null,
@@ -92,6 +128,7 @@ public partial class OrderService
var bindTime = DateTime.Now;
NormalizeParcelCreateInput(input);
+ createContext ??= BuildParcelCreateContext(input);
var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
input.CustomerId = sessionCustomer.Id;
@@ -130,6 +167,9 @@ public partial class OrderService
Order? created = null;
try
{
+ // 必须在任何 SqlSugar 写之前:保证 9610 / ForecastBilling / Pickup Outbox 与建单同事务提交。
+ await EnsureOutboxReadyAsync();
+
var order = BuildDraftOrderEntity(
input,
product,
@@ -148,7 +188,9 @@ public partial class OrderService
if (input.SaveAsDraft)
{
await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, input.PlatformCode);
- return await BuildParcelCreateOutputAsync(order);
+ var draftOutput = await BuildParcelCreateOutputAsync(order);
+ await TryAttachPickupAfterParcelCreateAsync(input, draftOutput);
+ return draftOutput;
}
// 偏远标记轻量同步;计费重/应收/排货走 ForecastBilling MQ(失败不回滚建单)。
@@ -176,22 +218,18 @@ public partial class OrderService
order.PredeclareFailReason,
ForecastBillingProcessingReason,
StringComparison.Ordinal);
+ await TryAttachPickupAfterParcelCreateAsync(input, output);
return output;
}
+ catch (BusinessException)
+ {
+ await TryRollbackFailedParcelCreateAsync(created);
+ throw;
+ }
catch (Exception ex)
{
// 创建失败不落库草稿:回滚已插入订单,由前端让客户选择继续修改或主动保存草稿。
- if (created != null)
- {
- try
- {
- await RollbackFailedParcelCreateAsync(created);
- }
- catch
- {
- // 回滚失败不覆盖原始业务错误;半成品订单需运维清理
- }
- }
+ await TryRollbackFailedParcelCreateAsync(created);
var cleanReason = StripDraftSavedMarker(ex.Message);
if (string.IsNullOrWhiteSpace(cleanReason))
@@ -203,6 +241,23 @@ public partial class OrderService
}
}
+ private async Task TryRollbackFailedParcelCreateAsync(Order? created)
+ {
+ if (created == null)
+ {
+ return;
+ }
+
+ try
+ {
+ await RollbackFailedParcelCreateAsync(created);
+ }
+ catch
+ {
+ // 回滚失败不覆盖原始业务错误;半成品订单需运维清理
+ }
+ }
+
///
/// 创建流程中途失败时删除已落库的半成品订单,避免自动变成草稿。
///
@@ -2271,6 +2326,8 @@ public partial class OrderService
try
{
+ await EnsureOutboxReadyAsync();
+
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
var address = await ResolveOrderBillingAddressAsync(order);
@@ -2627,6 +2684,12 @@ public partial class OrderService
return;
}
+ // 创单失败回滚 / 软删后,可能仍有短事务提前投递的孤儿任务
+ if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
// 已预报成功则跳过(幂等)
if (string.Equals(order.OrderStatus, OrderStatusCodes.Predeclared, StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrWhiteSpace(order.PredeclareFailReason))
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index 6bcbac0..eaccbe2 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -611,30 +611,7 @@ public partial class OrderService : ApplicationService
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
public async Task CreateParcelAsync(OrderParcelCreateInput input)
{
- var inbound = string.IsNullOrWhiteSpace(input.InboundMethod)
- ? null
- : input.InboundMethod.Trim();
- var createContext = inbound == null
- ? null
- : new ParcelCreateContext
- {
- InboundMethod = inbound,
- PickupTime = string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase)
- ? input.ExpectPickupTime
- : null,
- CustomerSenderAddressId = string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase)
- ? input.SenderAddressId
- : null
- };
-
- var output = await CreateParcelCoreAsync(input, createContext: createContext);
-
- if (string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase))
- {
- await TryAttachPickupAfterParcelCreateAsync(input, output);
- }
-
- return output;
+ return await CreateParcelCoreAsync(input, createContext: BuildParcelCreateContext(input));
}
///
@@ -687,6 +664,14 @@ public partial class OrderService : ApplicationService
return;
}
+ var inbound = string.IsNullOrWhiteSpace(input.InboundMethod)
+ ? null
+ : input.InboundMethod.Trim();
+ if (!string.Equals(inbound, OrderInboundMethodCodes.DoorPickup, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
if (input.SenderAddressId is not > 0)
{
output.PickupCreateError = "上门揽收需选择揽收地址";