diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs index e4a3261..19a593c 100644 --- a/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs @@ -5,6 +5,11 @@ namespace Oms.Application.Dtos.Orders; /// public class OrderParcelCreateOutput { + /// + /// 订单主键。 + /// + public int OrderId { get; set; } + /// /// 客户单号。 /// diff --git a/src/Oms.Application/Services/Orders/Batch/IOrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/IOrderBatchActionExecutor.cs index b0009ba..ca96f39 100644 --- a/src/Oms.Application/Services/Orders/Batch/IOrderBatchActionExecutor.cs +++ b/src/Oms.Application/Services/Orders/Batch/IOrderBatchActionExecutor.cs @@ -47,8 +47,14 @@ public sealed class OrderBatchActionExecuteResult ResultJson = resultJson, }; - public static OrderBatchActionExecuteResult BusinessFail(string reason) => - new() { Success = false, IsBusinessFailure = true, FailReason = reason }; + public static OrderBatchActionExecuteResult BusinessFail(string reason, int? createdOrderId = null) => + new() + { + Success = false, + IsBusinessFailure = true, + FailReason = reason, + CreatedOrderId = createdOrderId, + }; public static OrderBatchActionExecuteResult SystemFail(string reason) => new() { Success = false, IsBusinessFailure = false, FailReason = reason }; diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchCreateActionHandler.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchCreateActionHandler.cs index e1ca238..6f49390 100644 --- a/src/Oms.Application/Services/Orders/Batch/OrderBatchCreateActionHandler.cs +++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchCreateActionHandler.cs @@ -32,12 +32,20 @@ public sealed class OrderBatchCreateActionHandler(IServiceScopeFactory scopeFact { using var scope = scopeFactory.CreateScope(); var orderService = scope.ServiceProvider.GetRequiredService(); - await orderService.ImportRowForBatchAsync(payload, message.OperatorUserId, cancellationToken); - return OrderBatchActionExecuteResult.Ok(); + var output = await orderService.ImportRowForBatchAsync(payload, message.OperatorUserId, cancellationToken); + return OrderBatchActionExecuteResult.Ok( + createdOrderId: output.OrderId, + serviceNo: output.ServiceNo, + clientNo: output.ClientNo, + trackNo: output.TrackNo, + labelUrl: output.LabelUrl); } catch (BusinessException ex) { - return OrderBatchActionExecuteResult.BusinessFail(ex.Message); + using var scope = scopeFactory.CreateScope(); + var orderService = scope.ServiceProvider.GetRequiredService(); + var createdOrderId = await orderService.TryResolveImportOrderIdAsync(payload, cancellationToken); + return OrderBatchActionExecuteResult.BusinessFail(ex.Message, createdOrderId); } catch (Exception ex) { diff --git a/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs b/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs index 7534d13..1059450 100644 --- a/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs +++ b/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs @@ -16,7 +16,7 @@ public partial class OrderService /// /// 批量 Worker 导入单行 Excel 数据。 /// - public async Task ImportRowForBatchAsync( + public async Task ImportRowForBatchAsync( OrderBatchImportRowPayload payload, int operatorUserId, CancellationToken cancellationToken = default) @@ -30,7 +30,44 @@ public partial class OrderService ?? throw new BusinessException("行数据无效"); var input = OrderTemplateRowMapper.MapToParcelCreateInput(sourceValues, mapping, payload.CustomerId); - await CreateParcelForBatchAsync(input, operatorUserId, cancellationToken); + return await CreateParcelForBatchAsync(input, operatorUserId, cancellationToken); + } + + public async Task TryResolveImportOrderIdAsync( + OrderBatchImportRowPayload payload, + CancellationToken cancellationToken = default) + { + try + { + var template = await OrderTemplateRepository.FirstOrDefaultAsync(x => x.Id == payload.TemplateId, cancellationToken); + if (template == null) + { + return null; + } + + var mapping = OrderTemplateImportMapping.Resolve(template); + var sourceValues = JsonConvert.DeserializeObject>(payload.RowDataJson); + if (sourceValues == null || sourceValues.Count == 0) + { + return null; + } + + var input = OrderTemplateRowMapper.MapToParcelCreateInput(sourceValues, mapping, payload.CustomerId); + if (string.IsNullOrWhiteSpace(input.ClientNo)) + { + return null; + } + + var order = await OrderRepository.GetByCustomerAndClientNoAsync( + payload.CustomerId, + input.ClientNo.Trim(), + cancellationToken); + return order?.Id; + } + catch + { + return null; + } } /// diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index bd4ab61..df87eee 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -1438,6 +1438,7 @@ public partial class OrderService var mapped = (await MapOrderOutputsAsync([order])).First(); return new OrderParcelCreateOutput { + OrderId = order.Id, ClientNo = order.ClientNo, ServiceNo = order.ServiceNo, TrackNo = order.TrackNo, diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateExcelReader.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateExcelReader.cs index 2584490..5be4cb4 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateExcelReader.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateExcelReader.cs @@ -38,10 +38,11 @@ public static class OrderTemplateExcelReader kv => kv.Value?.ToString()?.Trim() ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var mappedColumns = mapping.Mappings - .Select(x => x.SourceColumn) + var mappedSourceColumns = mapping.Mappings + .Select(x => x.SourceColumn?.Trim()) .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + .Select(x => x!) + .ToList(); var rows = new List(); for (var rowIndex = mapping.DataStartRowIndex - 1; rowIndex < allRows.Count; rowIndex++) @@ -60,7 +61,7 @@ public static class OrderTemplateExcelReader continue; } - if (!mappedColumns.Contains(column.Value)) + if (!IsMappedSourceHeader(column.Value, mappedSourceColumns)) { continue; } @@ -73,7 +74,7 @@ public static class OrderTemplateExcelReader sourceValues[column.Value] = cellValue; } - if (!HasMappedValue(sourceValues, mappedColumns)) + if (!HasMappedValue(sourceValues, mappedSourceColumns)) { continue; } @@ -100,13 +101,25 @@ public static class OrderTemplateExcelReader ?? sheetNames[0]; } + private static bool IsMappedSourceHeader(string headerText, IReadOnlyList mappedSourceColumns) + { + if (mappedSourceColumns.Contains(headerText, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + + var normalizedHeader = OrderTemplateHeaderMatcher.NormalizeHeader(headerText); + return mappedSourceColumns.Any(column => + string.Equals(OrderTemplateHeaderMatcher.NormalizeHeader(column), normalizedHeader, StringComparison.OrdinalIgnoreCase)); + } + private static bool HasMappedValue( IReadOnlyDictionary sourceValues, - IReadOnlySet mappedColumns) + IReadOnlyList mappedSourceColumns) { - foreach (var column in mappedColumns) + foreach (var column in mappedSourceColumns) { - if (!sourceValues.TryGetValue(column, out var value) || value == null) + if (!TryGetMappedValue(sourceValues, column, out var value) || value == null) { continue; } @@ -120,6 +133,32 @@ public static class OrderTemplateExcelReader return false; } + private static bool TryGetMappedValue( + IReadOnlyDictionary sourceValues, + string sourceColumn, + out object? value) + { + if (sourceValues.TryGetValue(sourceColumn, out value) && value != null) + { + return true; + } + + var normalizedColumn = OrderTemplateHeaderMatcher.NormalizeHeader(sourceColumn); + foreach (var item in sourceValues) + { + if (!string.Equals(OrderTemplateHeaderMatcher.NormalizeHeader(item.Key), normalizedColumn, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + value = item.Value; + return value != null; + } + + value = null; + return false; + } + private static bool IsRowEmpty(IDictionary row) { return row.Values.All(value => string.IsNullOrWhiteSpace(value?.ToString())); diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs index c5904b2..cac5baf 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs @@ -133,7 +133,7 @@ public static class OrderTemplateHeaderMatcher .ToList(); } - private static string Normalize(string? text) + public static string NormalizeHeader(string? text) { if (string.IsNullOrWhiteSpace(text)) { @@ -142,4 +142,6 @@ public static class OrderTemplateHeaderMatcher return NoisePattern.Replace(text.Trim(), string.Empty); } + + private static string Normalize(string? text) => NormalizeHeader(text); } diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs index 572e87c..d318551 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs @@ -59,7 +59,7 @@ public static class OrderTemplateRowMapper continue; } - if (!sourceValues.TryGetValue(sourceColumn, out var rawValue) || rawValue == null) + if (!TryGetMappedValue(sourceValues, sourceColumn, out var rawValue) || rawValue == null) { continue; } @@ -77,6 +77,32 @@ public static class OrderTemplateRowMapper return input; } + private static bool TryGetMappedValue( + IReadOnlyDictionary sourceValues, + string sourceColumn, + out object? rawValue) + { + if (sourceValues.TryGetValue(sourceColumn, out rawValue) && rawValue != null) + { + return true; + } + + var normalizedColumn = OrderTemplateHeaderMatcher.NormalizeHeader(sourceColumn); + foreach (var item in sourceValues) + { + if (!string.Equals(OrderTemplateHeaderMatcher.NormalizeHeader(item.Key), normalizedColumn, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + rawValue = item.Value; + return rawValue != null; + } + + rawValue = null; + return false; + } + private static object? CoerceValue(string fieldKey, object rawValue, string? dataType) { if (rawValue is string text && string.IsNullOrWhiteSpace(text))