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))