test
All checks were successful
Build and Deploy / deploy (push) Successful in 1m4s

This commit is contained in:
xiaolipro 2026-10-14 17:07:35 +08:00
parent a1d2dece57
commit 7b69bbf207
6 changed files with 93 additions and 47 deletions

View File

@ -4,6 +4,6 @@
<PropertyGroup> <PropertyGroup>
<!-- All Fake packages --> <!-- All Fake packages -->
<!-- 清本地nuget缓存:dotnet nuget locals -c http-cache --> <!-- 清本地nuget缓存:dotnet nuget locals -c http-cache -->
<FakePackageVersion>8.0.5.26</FakePackageVersion> <FakePackageVersion>8.0.5.27</FakePackageVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,7 +1,5 @@
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.EventBus; using Fake.EventBus;
using Fake.UnitOfWork;
using Oms.Application.Messaging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Oms.Application.Integrations.SmallBagTrack; using Oms.Application.Integrations.SmallBagTrack;
@ -9,7 +7,6 @@ namespace Oms.Application.Messaging.OrderSideEffects;
public sealed class OrderLifecycleTrackAppendEventHandler( public sealed class OrderLifecycleTrackAppendEventHandler(
ISmallBagTrackOrderClient trackClient, ISmallBagTrackOrderClient trackClient,
IUnitOfWorkManager unitOfWorkManager,
ILogger<OrderLifecycleTrackAppendEventHandler> logger) ILogger<OrderLifecycleTrackAppendEventHandler> logger)
: IEventHandler<OrderLifecycleTrackAppendIntegrationEvent>, ITransientDependency : IEventHandler<OrderLifecycleTrackAppendIntegrationEvent>, ITransientDependency
{ {
@ -25,7 +22,7 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
return; return;
} }
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); // 纯外部 HTTP,无需事务;占 TX 只会拖长连接占用。
try try
{ {
await trackClient.AppendLifecycleTrackAsync( await trackClient.AppendLifecycleTrackAsync(
@ -44,7 +41,5 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
@event.OrderNo, @event.OrderNo,
@event.TrackCode); @event.TrackCode);
} }
await unitOfWork.CompleteAsync(cancellationToken);
} }
} }

View File

@ -4,6 +4,7 @@ using Fake.UnitOfWork;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Oms.Application.Services.Orders; using Oms.Application.Services.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Messaging.ParcelCreate; namespace Oms.Application.Messaging.ParcelCreate;
@ -24,9 +25,11 @@ public sealed class OrderParcelCustoms9610UpsertEventHandler(
{ {
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == @event.OrderId, cancellationToken); 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; return;
} }

View File

@ -2,7 +2,6 @@ using Fake.Application;
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.EventBus; using Fake.EventBus;
using Fake.UnitOfWork; using Fake.UnitOfWork;
using Oms.Application.Messaging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Oms.Application.Integrations.SmallBagTrack; using Oms.Application.Integrations.SmallBagTrack;
@ -25,7 +24,8 @@ public sealed class OrderParcelTrackUpsertEventHandler(
OrderParcelTrackUpsertIntegrationEvent @event, OrderParcelTrackUpsertIntegrationEvent @event,
CancellationToken cancellationToken) 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); var request = await ResolveRequestAsync(@event, cancellationToken);
request.AppendInitialTrackOnSuccess = @event.AppendInitialTrackOnSuccess; request.AppendInitialTrackOnSuccess = @event.AppendInitialTrackOnSuccess;

View File

@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Fake.Application; using Fake.Application;
using Fake.UnitOfWork;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -76,6 +77,41 @@ public partial class OrderService
public string? InboundMethod { get; init; } 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
};
}
/// <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,
@ -92,6 +128,7 @@ public partial class OrderService
var bindTime = DateTime.Now; var bindTime = DateTime.Now;
NormalizeParcelCreateInput(input); NormalizeParcelCreateInput(input);
createContext ??= BuildParcelCreateContext(input);
var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId); var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
input.CustomerId = sessionCustomer.Id; input.CustomerId = sessionCustomer.Id;
@ -130,6 +167,9 @@ public partial class OrderService
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,
@ -148,7 +188,9 @@ public partial class OrderService
if (input.SaveAsDraft) if (input.SaveAsDraft)
{ {
await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, input.PlatformCode); await ParcelCreateEventPublisher.PublishCustoms9610UpsertAsync(order, input.PlatformCode);
return await BuildParcelCreateOutputAsync(order); var draftOutput = await BuildParcelCreateOutputAsync(order);
await TryAttachPickupAfterParcelCreateAsync(input, draftOutput);
return draftOutput;
} }
// 偏远标记轻量同步;计费重/应收/排货走 ForecastBilling MQ(失败不回滚建单)。 // 偏远标记轻量同步;计费重/应收/排货走 ForecastBilling MQ(失败不回滚建单)。
@ -176,22 +218,18 @@ public partial class OrderService
order.PredeclareFailReason, order.PredeclareFailReason,
ForecastBillingProcessingReason, ForecastBillingProcessingReason,
StringComparison.Ordinal); StringComparison.Ordinal);
await TryAttachPickupAfterParcelCreateAsync(input, output);
return output; return output;
} }
catch (BusinessException)
{
await TryRollbackFailedParcelCreateAsync(created);
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
// 创建失败不落库草稿:回滚已插入订单,由前端让客户选择继续修改或主动保存草稿。 // 创建失败不落库草稿:回滚已插入订单,由前端让客户选择继续修改或主动保存草稿。
if (created != null) await TryRollbackFailedParcelCreateAsync(created);
{
try
{
await RollbackFailedParcelCreateAsync(created);
}
catch
{
// 回滚失败不覆盖原始业务错误;半成品订单需运维清理
}
}
var cleanReason = StripDraftSavedMarker(ex.Message); var cleanReason = StripDraftSavedMarker(ex.Message);
if (string.IsNullOrWhiteSpace(cleanReason)) 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
{
// 回滚失败不覆盖原始业务错误;半成品订单需运维清理
}
}
/// <summary> /// <summary>
/// 创建流程中途失败时删除已落库的半成品订单,避免自动变成草稿。 /// 创建流程中途失败时删除已落库的半成品订单,避免自动变成草稿。
/// </summary> /// </summary>
@ -2271,6 +2326,8 @@ 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);
@ -2627,6 +2684,12 @@ public partial class OrderService
return; return;
} }
// 创单失败回滚 / 软删后,可能仍有短事务提前投递的孤儿任务
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
{
return;
}
// 已预报成功则跳过(幂等) // 已预报成功则跳过(幂等)
if (string.Equals(order.OrderStatus, OrderStatusCodes.Predeclared, StringComparison.OrdinalIgnoreCase) if (string.Equals(order.OrderStatus, OrderStatusCodes.Predeclared, StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrWhiteSpace(order.PredeclareFailReason)) && string.IsNullOrWhiteSpace(order.PredeclareFailReason))

View File

@ -611,30 +611,7 @@ public partial class OrderService : ApplicationService
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)] [UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
public async Task<OrderParcelCreateOutput> CreateParcelAsync(OrderParcelCreateInput input) public async Task<OrderParcelCreateOutput> CreateParcelAsync(OrderParcelCreateInput input)
{ {
var inbound = string.IsNullOrWhiteSpace(input.InboundMethod) return await CreateParcelCoreAsync(input, createContext: BuildParcelCreateContext(input));
? 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;
} }
/// <summary> /// <summary>
@ -687,6 +664,14 @@ public partial class OrderService : ApplicationService
return; 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) if (input.SenderAddressId is not > 0)
{ {
output.PickupCreateError = "上门揽收需选择揽收地址"; output.PickupCreateError = "上门揽收需选择揽收地址";