From 6c3a094126d2f1268f3d70c6c03715fa21e90679 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 10 Oct 2026 18:43:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8B=E6=B8=B8=20getFile=20?= =?UTF-8?q?=E8=BD=AC=E5=AD=98=20COS=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=9B=86=E6=88=90=E4=BA=8B=E4=BB=B6=20Outbox=20=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增获取下游文件接口;事件发布统一走事务性 UoW,避免批量预报等按钮报 Outbox 错误。 Co-authored-by: Cursor --- .../Dtos/Orders/ItfxGetFileBody.cs | 27 +++ .../Dtos/Orders/OrderDownstreamForwardDtos.cs | 55 +++++- .../Itfx/ItfxGetFileDataParser.cs | 171 ++++++++++++++++++ .../OrderAutoTriggerFireEventHandler.cs | 3 +- .../OrderLifecycleTrackAppendEventHandler.cs | 3 +- ...rderParcelCustoms9610UpsertEventHandler.cs | 3 +- .../OrderParcelLabelGenerateEventHandler.cs | 3 +- .../OrderParcelPickupAttachEventHandler.cs | 3 +- .../OrderParcelTrackUpsertEventHandler.cs | 3 +- .../Services/Orders/OrderFileManager.cs | 160 +++++++++++++++- .../Services/Orders/OrderService.BatchJob.cs | 15 ++ .../Orders/OrderService.DownstreamForward.cs | 68 +++++++ 12 files changed, 505 insertions(+), 9 deletions(-) create mode 100644 src/Oms.Application/Dtos/Orders/ItfxGetFileBody.cs create mode 100644 src/Oms.Application/Integrations/Itfx/ItfxGetFileDataParser.cs diff --git a/src/Oms.Application/Dtos/Orders/ItfxGetFileBody.cs b/src/Oms.Application/Dtos/Orders/ItfxGetFileBody.cs new file mode 100644 index 0000000..f97311c --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/ItfxGetFileBody.cs @@ -0,0 +1,27 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// itfx getFile(ForwardWithSubscribe)标准请求体。 +/// +public class ItfxGetFileBody +{ + /// + /// 客户单号。 + /// + public string? ClientNo { get; set; } + + /// + /// 跟踪号。 + /// + public string? TrackNo { get; set; } + + /// + /// 服务商单号。 + /// + public string? ServiceNo { get; set; } + + /// + /// 文件类型:下游面单=21,末端面单=31,POD=180。 + /// + public int Type { get; set; } +} diff --git a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs index c69e78c..0ace4b1 100644 --- a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs +++ b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs @@ -1,5 +1,3 @@ -using Oms.Application.Dtos.Orders; - namespace Oms.Application.Dtos.Orders; /// @@ -43,3 +41,56 @@ public class OrderDownstreamForwardResult /// public string? TrackNo { get; set; } } + +/// +/// 下游 getFile 入参。 +/// +public class OrderDownstreamGetFileInput +{ + /// + /// 订单 Id。 + /// + public int Id { get; set; } + + /// + /// 文件类型:下游面单=21,末端面单=31,POD=180。 + /// + public int Type { get; set; } +} + +/// +/// 下游 getFile 转发结果。 +/// +public class OrderDownstreamGetFileResult +{ + public bool Success { get; set; } + + public string SupplierCode { get; set; } = string.Empty; + + public int Type { get; set; } + + public string? FileType { get; set; } + + public string Url { get; set; } = string.Empty; + + public string RequestJson { get; set; } = string.Empty; + + public int? StatusCode { get; set; } + + public string ResponseBody { get; set; } = string.Empty; + + /// + /// 转存到自有 COS 后的公开地址。 + /// + public string? StoredFileUrl { get; set; } + + /// + /// 回写后的服务商单号(Data.serviceNo)。 + /// + public string? DownstreamServiceNo { get; set; } + + /// + /// 回写后的跟踪号(Data.trackNo)。 + /// + public string? TrackNo { get; set; } +} diff --git a/src/Oms.Application/Integrations/Itfx/ItfxGetFileDataParser.cs b/src/Oms.Application/Integrations/Itfx/ItfxGetFileDataParser.cs new file mode 100644 index 0000000..4764ab0 --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxGetFileDataParser.cs @@ -0,0 +1,171 @@ +using System.Text.Json; +using Oms.Domain.OrdersAggregate.Consts; +using Oms.Domain.OrdersAggregate.Entities; + +namespace Oms.Application.Integrations.Itfx; + +/// +/// 解析 itfx getFile 成功响应中的 Data,并回写订单单号。 +/// +internal static class ItfxGetFileDataParser +{ + public const string StandardCode = "getFile"; + + public const int TypeDownstreamLabel = 21; + public const int TypeLastMileLabel = 31; + public const int TypePod = 180; + + public sealed class GetFileData + { + public string? ServiceNo { get; init; } + + public string? TrackNo { get; init; } + + public string? ClientNo { get; init; } + + public string? Url { get; init; } + + public int? Type { get; init; } + } + + public static bool TryResolveFileType(int type, out string fileType) + { + fileType = type switch + { + TypeDownstreamLabel => OrderFileTypeCodes.DownstreamLabel, + TypeLastMileLabel => OrderFileTypeCodes.LastMileLabel, + TypePod => OrderFileTypeCodes.Pod, + _ => string.Empty + }; + return !string.IsNullOrEmpty(fileType); + } + + public static bool TryParse(string? responseBody, out GetFileData? data) + { + data = null; + if (string.IsNullOrWhiteSpace(responseBody)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(responseBody); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + return false; + } + + if (!TryGetPropertyIgnoreCase(root, out var dataElement, "data", "Data") + || dataElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + data = new GetFileData + { + ServiceNo = ReadString(dataElement, "serviceNo", "ServiceNo"), + TrackNo = ReadString(dataElement, "trackNo", "TrackNo"), + ClientNo = ReadString(dataElement, "clientNo", "ClientNo"), + Url = ReadString(dataElement, "url", "Url"), + Type = ReadInt(dataElement, "type", "Type"), + }; + return !string.IsNullOrWhiteSpace(data.ServiceNo) + || !string.IsNullOrWhiteSpace(data.TrackNo) + || !string.IsNullOrWhiteSpace(data.ClientNo) + || !string.IsNullOrWhiteSpace(data.Url); + } + catch + { + return false; + } + } + + /// + /// 服务商单号 ← Data.serviceNo,跟踪号 ← Data.trackNo;空值不覆盖。 + /// + public static bool ApplyToOrder(Order order, GetFileData data) + { + var changed = false; + + if (!string.IsNullOrWhiteSpace(data.ServiceNo)) + { + var value = data.ServiceNo.Trim(); + if (!string.Equals(order.DownstreamServiceNo, value, StringComparison.Ordinal)) + { + order.DownstreamServiceNo = value; + changed = true; + } + } + + if (!string.IsNullOrWhiteSpace(data.TrackNo)) + { + var value = data.TrackNo.Trim(); + if (!string.Equals(order.TrackNo, value, StringComparison.Ordinal)) + { + order.TrackNo = value; + changed = true; + } + } + + return changed; + } + + private static bool TryGetPropertyIgnoreCase(JsonElement root, out JsonElement value, params string[] names) + { + foreach (var name in names) + { + if (root.TryGetProperty(name, out value)) + { + return true; + } + } + + foreach (var prop in root.EnumerateObject()) + { + foreach (var name in names) + { + if (prop.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + value = prop.Value; + return true; + } + } + } + + value = default; + return false; + } + + private static string? ReadString(JsonElement root, params string[] names) + { + if (!TryGetPropertyIgnoreCase(root, out var prop, names)) + { + return null; + } + + return prop.ValueKind switch + { + JsonValueKind.String => prop.GetString()?.Trim(), + JsonValueKind.Number => prop.GetRawText(), + JsonValueKind.Null => null, + _ => null + }; + } + + private static int? ReadInt(JsonElement root, params string[] names) + { + if (!TryGetPropertyIgnoreCase(root, out var prop, names)) + { + return null; + } + + return prop.ValueKind switch + { + JsonValueKind.Number when prop.TryGetInt32(out var n) => n, + JsonValueKind.String when int.TryParse(prop.GetString(), out var n) => n, + _ => null + }; + } +} diff --git a/src/Oms.Application/Messaging/OrderSideEffects/OrderAutoTriggerFireEventHandler.cs b/src/Oms.Application/Messaging/OrderSideEffects/OrderAutoTriggerFireEventHandler.cs index 670c52e..bebb13f 100644 --- a/src/Oms.Application/Messaging/OrderSideEffects/OrderAutoTriggerFireEventHandler.cs +++ b/src/Oms.Application/Messaging/OrderSideEffects/OrderAutoTriggerFireEventHandler.cs @@ -1,6 +1,7 @@ using Fake.DependencyInjection; using Fake.EventBus; using Fake.UnitOfWork; +using Oms.Application.Messaging; using Microsoft.Extensions.Logging; using Oms.Application.Services.Orders.Issues; @@ -26,7 +27,7 @@ public sealed class OrderAutoTriggerFireEventHandler( return; } - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); try { await autoTriggerRuntimeService.FireByOrderIdAsync( diff --git a/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs b/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs index 2114097..f963a48 100644 --- a/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs +++ b/src/Oms.Application/Messaging/OrderSideEffects/OrderLifecycleTrackAppendEventHandler.cs @@ -1,6 +1,7 @@ using Fake.DependencyInjection; using Fake.EventBus; using Fake.UnitOfWork; +using Oms.Application.Messaging; using Microsoft.Extensions.Logging; using Oms.Application.Integrations.SmallBagTrack; @@ -24,7 +25,7 @@ public sealed class OrderLifecycleTrackAppendEventHandler( return; } - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); try { await trackClient.AppendLifecycleTrackAsync( diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs index bb292d2..5ce44e7 100644 --- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs +++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelCustoms9610UpsertEventHandler.cs @@ -1,6 +1,7 @@ using Fake.DependencyInjection; using Fake.EventBus; using Fake.UnitOfWork; +using Oms.Application.Messaging; using Microsoft.Extensions.Logging; using Oms.Application.Services.Orders; using Oms.Domain.OrdersAggregate.Repositories; @@ -21,7 +22,7 @@ public sealed class OrderParcelCustoms9610UpsertEventHandler( OrderParcelCustoms9610UpsertIntegrationEvent @event, CancellationToken cancellationToken) { - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == @event.OrderId, cancellationToken); if (order == null) { diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelLabelGenerateEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelLabelGenerateEventHandler.cs index 8e29720..78936ca 100644 --- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelLabelGenerateEventHandler.cs +++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelLabelGenerateEventHandler.cs @@ -1,6 +1,7 @@ using Fake.DependencyInjection; using Fake.EventBus; using Fake.UnitOfWork; +using Oms.Application.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Oms.Application.Services.Orders; @@ -20,7 +21,7 @@ public sealed class OrderParcelLabelGenerateEventHandler( OrderParcelLabelGenerateIntegrationEvent @event, CancellationToken cancellationToken) { - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); using var scope = scopeFactory.CreateScope(); var orderService = scope.ServiceProvider.GetRequiredService(); var result = await orderService.GenerateLabelPdfForBatchAsync(@event.OrderId, cancellationToken); diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelPickupAttachEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelPickupAttachEventHandler.cs index 488e95c..adcaf40 100644 --- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelPickupAttachEventHandler.cs +++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelPickupAttachEventHandler.cs @@ -1,6 +1,7 @@ using Fake.DependencyInjection; using Fake.EventBus; using Fake.UnitOfWork; +using Oms.Application.Messaging; using Microsoft.Extensions.Logging; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; @@ -21,7 +22,7 @@ public sealed class OrderParcelPickupAttachEventHandler( OrderParcelPickupAttachIntegrationEvent @event, CancellationToken cancellationToken) { - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); try { await pickupOrderService.CreateForParcelInternalAsync(new PickupOrderCreateInput diff --git a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs index 6347a3c..0fb3f89 100644 --- a/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs +++ b/src/Oms.Application/Messaging/ParcelCreate/OrderParcelTrackUpsertEventHandler.cs @@ -2,6 +2,7 @@ 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; @@ -24,7 +25,7 @@ public sealed class OrderParcelTrackUpsertEventHandler( OrderParcelTrackUpsertIntegrationEvent @event, CancellationToken cancellationToken) { - using var unitOfWork = unitOfWorkManager.Begin(); + using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional()); var request = await ResolveRequestAsync(@event, cancellationToken); request.AppendInitialTrackOnSuccess = @event.AppendInitialTrackOnSuccess; diff --git a/src/Oms.Application/Services/Orders/OrderFileManager.cs b/src/Oms.Application/Services/Orders/OrderFileManager.cs index 3cf553b..356230a 100644 --- a/src/Oms.Application/Services/Orders/OrderFileManager.cs +++ b/src/Oms.Application/Services/Orders/OrderFileManager.cs @@ -3,6 +3,7 @@ using Fake.DependencyInjection; using Fake.ObjectStorage; using Microsoft.AspNetCore.Http; using Oms.Application.Dtos.Orders; +using Oms.Application.Services.Storage; using Oms.Application.Storage; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; @@ -15,7 +16,8 @@ namespace Oms.Application.Services.Orders; /// public sealed class OrderFileManager( IOrderFileRepository orderFileRepository, - IObjectStorage fileStorage) : IScopedDependency + IObjectStorage fileStorage, + IHttpClientFactory httpClientFactory) : IScopedDependency { private static readonly HashSet ImageExt = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp" }; @@ -205,6 +207,162 @@ public sealed class OrderFileManager( return MapItem(saved); } + /// + /// 下载远端文件转存自有对象存储后写入订单附件。 + /// + public async Task AddOrReplaceFromRemoteUrlAsync( + int orderId, + string fileType, + string remoteUrl, + int operatorUserId, + bool replaceExisting = true, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(remoteUrl)) + { + throw new BusinessException("下游文件地址为空"); + } + + if (!OrderFileTypeCodes.IsValid(fileType)) + { + throw new BusinessException("不支持的附件类型"); + } + + var type = OrderFileTypeCodes.Normalize(fileType); + var sourceUrl = remoteUrl.Trim(); + var stored = await DownloadAndStoreRemoteAsync(orderId, type, sourceUrl, cancellationToken); + + if (replaceExisting) + { + await orderFileRepository.SoftDeleteByOrderAndTypeAsync(orderId, type, cancellationToken); + } + else + { + var existing = await orderFileRepository.GetByOrderAndTypeListAsync(orderId, type, cancellationToken); + var dup = existing.FirstOrDefault(x => + string.Equals(x.FileUrl, stored.Key, StringComparison.OrdinalIgnoreCase) + || string.Equals(x.FileUrl, sourceUrl, StringComparison.OrdinalIgnoreCase)); + if (dup != null) + { + return MapItem(dup); + } + + if (existing.Count >= OrderFileTypeCodes.GetMaxCount(type)) + { + var oldest = existing.OrderBy(x => x.Id).First(); + oldest.IsDeleted = true; + oldest.UpdateTime = DateTime.Now; + await orderFileRepository.UpdateAsync(oldest, cancellationToken); + } + } + + var existingAfter = replaceExisting + ? [] + : await orderFileRepository.GetByOrderAndTypeListAsync(orderId, type, cancellationToken); + var entity = new OrderFile + { + OrderId = orderId, + FileType = type, + FileName = stored.FileName, + FileUrl = stored.Key, + ContentType = stored.ContentType, + FileSize = stored.Size, + Sort = existingAfter.Count, + Source = OrderFileSourceCodes.Downstream, + Remark = sourceUrl.Length > 500 ? sourceUrl[..500] : sourceUrl, + CreateUserId = operatorUserId, + CreateTime = DateTime.Now + }; + var saved = await orderFileRepository.InsertAsync(entity, cancellationToken); + return MapItem(saved); + } + + private async Task<(string Key, string? FileName, string? ContentType, long Size)> DownloadAndStoreRemoteAsync( + int orderId, + string fileType, + string remoteUrl, + CancellationToken cancellationToken) + { + byte[] bytes; + string? contentType; + try + { + var client = httpClientFactory.CreateClient(FileStorageService.UrlFetchClientName); + using var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl); + request.Headers.TryAddWithoutValidation("Accept", "*/*"); + using var response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new BusinessException($"下载下游文件失败:HTTP {(int)response.StatusCode}"); + } + + bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken); + contentType = response.Content.Headers.ContentType?.MediaType; + } + catch (BusinessException) + { + throw; + } + catch (Exception ex) + { + throw new BusinessException($"下载下游文件失败:{ex.Message}"); + } + + if (bytes.Length == 0) + { + throw new BusinessException("下游文件内容为空"); + } + + if (bytes.Length > MaxPdfBytes) + { + throw new BusinessException("下游文件不能超过 10MB"); + } + + var ext = GuessExtension(remoteUrl, contentType); + var fileName = TryFileNameFromUrl(remoteUrl) ?? $"{fileType}{ext}"; + var storedName = $"{Guid.NewGuid():N}{ext}"; + var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}"; + await using var stream = new MemoryStream(bytes); + var key = await fileStorage.SaveObjectAsync(stream, relativeKey, contentType, cancellationToken); + return (key, fileName, contentType, bytes.Length); + } + + private static string GuessExtension(string url, string? contentType) + { + var fromUrl = Path.GetExtension( + (url.Contains('?', StringComparison.Ordinal) + ? url[..url.IndexOf('?', StringComparison.Ordinal)] + : url)); + if (!string.IsNullOrWhiteSpace(fromUrl) && fromUrl.Length <= 8) + { + return fromUrl.ToLowerInvariant(); + } + + if (!string.IsNullOrWhiteSpace(contentType)) + { + if (contentType.Contains("pdf", StringComparison.OrdinalIgnoreCase)) + { + return ".pdf"; + } + + if (contentType.Contains("png", StringComparison.OrdinalIgnoreCase)) + { + return ".png"; + } + + if (contentType.Contains("jpeg", StringComparison.OrdinalIgnoreCase) + || contentType.Contains("jpg", StringComparison.OrdinalIgnoreCase)) + { + return ".jpg"; + } + } + + return ".pdf"; + } + public async Task> UploadAsync( int orderId, string fileType, diff --git a/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs b/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs index 61120b2..13dafbf 100644 --- a/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs +++ b/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs @@ -1,5 +1,6 @@ using Fake.Application; using Fake.Security.Claims; +using Fake.UnitOfWork; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; @@ -21,6 +22,7 @@ public partial class OrderService /// 创建订单批量异步任务并入队 RabbitMQ。 /// [HttpPost("/order/batch/jobs")] + [UnitOfWork(IsTransactional = true)] public Task CreateBatchJobAsync([FromBody] OrderBatchJobCreateInput input) { return BatchJobCoordinator.CreateAndEnqueueAsync(input, CurrentUser.GetId()); @@ -73,6 +75,7 @@ public partial class OrderService /// 异步批量创建轨迹订单(替代前端逐条调用)。 /// [HttpPost("/order/batch/track-upsert")] + [UnitOfWork(IsTransactional = true)] public Task BatchTrackUpsertAsync([FromBody] OrderBatchJobCreateInput input) { input.Action = OrderBatchActionCodes.TrackUpsertSmallBag; @@ -94,6 +97,7 @@ public partial class OrderService /// 异步批量预报下游订单。 /// [HttpPost("/order/batch/downstream-forward")] + [UnitOfWork(IsTransactional = true)] public Task BatchDownstreamForwardAsync([FromBody] OrderBatchJobCreateInput input) { input.Action = OrderBatchActionCodes.DownstreamForwardCreateOne; @@ -111,6 +115,7 @@ public partial class OrderService string.IsNullOrWhiteSpace(authorization) ? null : authorization); } + [UnitOfWork(IsTransactional = true)] private Task EnqueueOrderBatchAsync(string action, OrderBatchJobCreateInput input) { input.Action = action; @@ -121,6 +126,7 @@ public partial class OrderService /// 异步批量支付订单。 /// [HttpPost("/order/batch/pay")] + [UnitOfWork(IsTransactional = true)] public Task BatchPayAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.Pay, input); @@ -128,6 +134,7 @@ public partial class OrderService /// 异步批量预报订单。 /// [HttpPost("/order/batch/pre-declare")] + [UnitOfWork(IsTransactional = true)] public Task BatchPreDeclareAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.PreDeclare, input); @@ -135,6 +142,7 @@ public partial class OrderService /// 异步批量拦截订单。 /// [HttpPost("/order/batch/intercept")] + [UnitOfWork(IsTransactional = true)] public Task BatchInterceptAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.Intercept, input); @@ -142,6 +150,7 @@ public partial class OrderService /// 异步批量取消拦截。 /// [HttpPost("/order/batch/cancel-intercept")] + [UnitOfWork(IsTransactional = true)] public Task BatchCancelInterceptAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.CancelIntercept, input); @@ -149,6 +158,7 @@ public partial class OrderService /// 异步批量删除订单。 /// [HttpPost("/order/batch/delete")] + [UnitOfWork(IsTransactional = true)] public Task BatchDeleteAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.Delete, input); @@ -156,6 +166,7 @@ public partial class OrderService /// 异步批量打印面单。 /// [HttpPost("/order/batch/print")] + [UnitOfWork(IsTransactional = true)] public Task BatchPrintAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.Print, input); @@ -163,6 +174,7 @@ public partial class OrderService /// 异步批量标记问题件。 /// [HttpPost("/order/batch/mark-problematic")] + [UnitOfWork(IsTransactional = true)] public Task BatchMarkProblematicAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.MarkProblematic, input); @@ -170,6 +182,7 @@ public partial class OrderService /// 异步批量重新排货。 /// [HttpPost("/order/batch/reallocate")] + [UnitOfWork(IsTransactional = true)] public Task BatchReallocateAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.Reallocate, input); @@ -177,6 +190,7 @@ public partial class OrderService /// 异步批量标记已认领。 /// [HttpPost("/order/batch/mark-claimed")] + [UnitOfWork(IsTransactional = true)] public Task BatchMarkClaimedAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.MarkClaimed, input); @@ -184,6 +198,7 @@ public partial class OrderService /// 异步批量更新处理状态。 /// [HttpPost("/order/batch/handle-status-update")] + [UnitOfWork(IsTransactional = true)] public Task BatchHandleStatusUpdateAsync([FromBody] OrderBatchJobCreateInput input) => EnqueueOrderBatchAsync(OrderBatchActionCodes.HandleStatusUpdate, input); diff --git a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs index b8eb919..4c74d7c 100644 --- a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs +++ b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs @@ -1,5 +1,6 @@ using Fake.Application; using Fake.Domain.Exceptions; +using Fake.UnitOfWork; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Integrations.Itfx; @@ -36,6 +37,7 @@ public partial class OrderService /// 按订单 Id 组装 createOrderOne 并转发至 itfx 下游。 /// [HttpPost("/order/downstream-forward-create-one")] + [UnitOfWork(IsTransactional = true)] public async Task DownstreamForwardCreateOneAsync([FromBody] OrderActionInput input) { var (order, body) = await LoadDownstreamCreateOneContextAsync(input.Id); @@ -82,6 +84,72 @@ public partial class OrderService }; } + /// + /// 按订单 Id 组装 getFile 并转发至 itfx 下游;成功时转存文件并回写单号。 + /// + [HttpPost("/order/downstream-forward-get-file")] + public async Task DownstreamForwardGetFileAsync( + [FromBody] OrderDownstreamGetFileInput input) + { + if (!ItfxGetFileDataParser.TryResolveFileType(input.Type, out var fileType)) + { + throw new BusinessException("不支持的文件类型,请选择下游面单/末端面单/POD"); + } + + var order = await GetOrderOrThrowAsync(input.Id); + var creds = await OrderDownstreamCredentialResolver.ResolveAsync(order); + var body = new ItfxGetFileBody + { + ClientNo = order.ClientNo?.Trim() ?? string.Empty, + TrackNo = order.TrackNo?.Trim() ?? string.Empty, + ServiceNo = order.DownstreamServiceNo?.Trim() ?? string.Empty, + Type = input.Type + }; + + var forward = await ItfxForwardClient.ForwardWithSubscribeAsync( + ItfxGetFileDataParser.StandardCode, + creds.SupplierCode, + creds.AppKey, + creds.AppSecret, + body); + + string? storedFileUrl = null; + if (forward.Success + && ItfxGetFileDataParser.TryParse(forward.ResponseBody, out var data) + && data != null) + { + if (ItfxGetFileDataParser.ApplyToOrder(order, data)) + { + await OrderRepository.UpdateAsync(order); + } + + if (!string.IsNullOrWhiteSpace(data.Url)) + { + var saved = await OrderFileManager.AddOrReplaceFromRemoteUrlAsync( + order.Id, + fileType, + data.Url!, + CurrentUser.IsAuthenticated ? CurrentUser.GetId() : 0); + storedFileUrl = saved.FileUrl; + } + } + + return new OrderDownstreamGetFileResult + { + Success = forward.Success, + SupplierCode = creds.SupplierCode, + Type = input.Type, + FileType = fileType, + Url = forward.Url, + RequestJson = forward.RequestJson, + StatusCode = forward.StatusCode, + ResponseBody = forward.ResponseBody, + StoredFileUrl = storedFileUrl, + DownstreamServiceNo = order.DownstreamServiceNo, + TrackNo = order.TrackNo, + }; + } + private async Task<(Domain.OrdersAggregate.Entities.Order order, ItfxCreateOrderOneBody body)> LoadDownstreamCreateOneContextAsync( int orderId) {