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>
<!-- All Fake packages -->
<!-- 清本地nuget缓存:dotnet nuget locals -c http-cache -->
<FakePackageVersion>8.0.5.26</FakePackageVersion>
<FakePackageVersion>8.0.5.27</FakePackageVersion>
</PropertyGroup>
</Project>

View File

@ -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<OrderLifecycleTrackAppendEventHandler> logger)
: IEventHandler<OrderLifecycleTrackAppendIntegrationEvent>, 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);
}
}

View File

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

View File

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

View File

@ -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
};
}
/// <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(
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
{
// 回滚失败不覆盖原始业务错误;半成品订单需运维清理
}
}
/// <summary>
/// 创建流程中途失败时删除已落库的半成品订单,避免自动变成草稿。
/// </summary>
@ -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))

View File

@ -611,30 +611,7 @@ public partial class OrderService : ApplicationService
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
public async Task<OrderParcelCreateOutput> 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));
}
/// <summary>
@ -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 = "上门揽收需选择揽收地址";