fix: 运单列表 enrichment 改为顺序查询,避免 MySqlConnection 并发复用 #13

Merged
zzh merged 7 commits from test into master 2026-10-24 13:36:05 +08:00
4 changed files with 335 additions and 1 deletions
Showing only changes of commit d22fe7220d - Show all commits

View File

@ -16,6 +16,7 @@ using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate; using Oms.Domain.OrdersAggregate;
using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Consts;
using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Entities; using Oms.Domain.SystemAggregate.Entities;
@ -296,6 +297,298 @@ public partial class OrderService
return outputs; return outputs;
} }
/// <summary>
/// 列表页专用映射:不加载申报明细/费用明细,并行 enrichment,减小 JSON 与 DB 开销。
/// </summary>
private async Task<List<OrderOutput>> MapOrderListOutputsAsync(IReadOnlyCollection<Order> orders)
{
if (orders.Count == 0)
{
return [];
}
var orderIds = orders.Select(x => x.Id).ToArray();
var warehouseIds = orders.Where(x => x.WarehouseId.HasValue).Select(x => x.WarehouseId!.Value).Distinct().ToArray();
var productCodes = orders
.Where(x => !string.IsNullOrWhiteSpace(x.ProductCode))
.Select(x => x.ProductCode!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var channelCodes = orders
.Where(x => !string.IsNullOrWhiteSpace(x.ChannelCode))
.Select(x => x.ChannelCode!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var countryCodes = orders
.Where(x => !string.IsNullOrWhiteSpace(x.ConsigneeCountry))
.Select(x => x.ConsigneeCountry!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var customerIds = orders
.Where(x => x.CustomerId is > 0)
.Select(x => x.CustomerId!.Value)
.Distinct()
.ToArray();
var pickupOrderIds = orders
.Where(x => x.PickupOrderId is > 0)
.Select(x => x.PickupOrderId!.Value)
.Distinct()
.ToArray();
var sendersTask = OrderRepository.GetSendersByOrderIdsAsync(orderIds);
var recipientsTask = OrderRepository.GetRecipientsByOrderIdsAsync(orderIds);
var weightsTask = OrderRepository.GetWeightsByOrderIdsAsync(orderIds);
var receivableSumTask = OrderRepository.GetReceivableSumByOrderIdsAsync(orderIds);
var warehousesTask = warehouseIds.Length == 0
? Task.FromResult(new List<WarehouseAddress>())
: WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
var productsTask = productCodes.Length == 0
? Task.FromResult(new List<SalesProduct>())
: SalesProductRepository.GetListAsync(x => productCodes.Contains(x.ProductCode));
var channelsTask = channelCodes.Length == 0
? Task.FromResult(new List<ServiceChannel>())
: ServiceChannelRepository.GetListAsync(x => channelCodes.Contains(x.ChannelCode));
var countriesTask = countryCodes.Length == 0
? Task.FromResult(new List<Country>())
: CountryRepository.GetListAsync(x => countryCodes.Contains(x.CountryCode));
var auditUserNameTask = UserDisplayNameResolver.ResolveManyAsync(
orders.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
var customersTask = customerIds.Length == 0
? Task.FromResult(new List<Customer>())
: CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var pickupOrdersTask = pickupOrderIds.Length == 0
? Task.FromResult(new List<PickupOrder>())
: PickupOrderRepository.GetListAsync(x => pickupOrderIds.Contains(x.Id));
var bindSummaryTask = LoadStockBindListSummariesAsync(orderIds);
await Task.WhenAll(
sendersTask,
recipientsTask,
weightsTask,
receivableSumTask,
warehousesTask,
productsTask,
channelsTask,
countriesTask,
auditUserNameTask,
customersTask,
pickupOrdersTask,
bindSummaryTask);
var senders = await sendersTask;
var recipients = await recipientsTask;
var weights = await weightsTask;
var receivableSumMap = await receivableSumTask;
var warehouses = await warehousesTask;
var products = await productsTask;
var channels = await channelsTask;
var countries = await countriesTask;
var auditUserNameMap = await auditUserNameTask;
var customers = await customersTask;
var pickupOrders = await pickupOrdersTask;
var bindSummaryMap = await bindSummaryTask;
var senderMap = senders.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First());
var recipientMap = recipients.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First());
var weightMap = weights.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
var warehouseMap = warehouses.ToDictionary(x => x.Id);
var productMap = products.ToDictionary(x => x.ProductCode, StringComparer.OrdinalIgnoreCase);
var channelMap = channels.ToDictionary(x => x.ChannelCode, StringComparer.OrdinalIgnoreCase);
var countryMap = countries.ToDictionary(x => x.CountryCode, StringComparer.OrdinalIgnoreCase);
var customerMap = customers.ToDictionary(x => x.Id);
var pickupNoMap = pickupOrders.ToDictionary(x => x.Id, x => x.PickupNo);
var carrierWaybillNoMap = pickupOrders
.Where(x => !string.IsNullOrWhiteSpace(x.CarrierWaybillNo))
.ToDictionary(x => x.Id, x => x.CarrierWaybillNo!);
var addressCountryIds = senders
.Select(x => x.Country)
.Concat(recipients.Select(x => x.Country))
.Where(x => x.HasValue)
.Select(x => x!.Value)
.Distinct()
.ToArray();
var addressCountries = addressCountryIds.Length == 0
? []
: await CountryRepository.GetListAsync(x => addressCountryIds.Contains(x.Id));
var countryIdCodeMap = addressCountries.ToDictionary(
x => x.Id,
x => x.CountryCode ?? string.Empty);
var outputs = orders.Select(order =>
{
senderMap.TryGetValue(order.Id, out var sender);
recipientMap.TryGetValue(order.Id, out var recipient);
weightMap.TryGetValue(order.Id, out var orderWeights);
var chargeableWeight = ResolveDisplayChargeableWeight(orderWeights);
decimal? receivableAmount = receivableSumMap.TryGetValue(order.Id, out var sum)
? sum
: null;
var additionalServices = NormalizeAdditionalServiceCodes(
OmsDocHelper.DeserializeJson<List<string>>(order.AdditionalServices));
var hasInsurance = additionalServices.Any(x =>
string.Equals(x, "insurance", StringComparison.OrdinalIgnoreCase));
string? customerName = null;
string? customerCode = null;
if (order.CustomerId is > 0 && customerMap.TryGetValue(order.CustomerId.Value, out var customer))
{
customerName = customer.CustomerName;
customerCode = customer.CustomerCode;
}
SalesProduct? product = null;
if (!string.IsNullOrWhiteSpace(order.ProductCode))
{
productMap.TryGetValue(order.ProductCode.Trim(), out product);
}
bindSummaryMap.TryGetValue(order.Id, out var bindSummary);
return new OrderOutput
{
Id = order.Id,
ClientNo = order.ClientNo,
TrackNo = order.TrackNo,
DownstreamServiceNo = order.DownstreamServiceNo,
ServiceNo = order.ServiceNo,
ProductCode = order.ProductCode,
ProductName = product?.ProductName,
MaxTimeliness = product?.MaxTimeliness,
ChannelCode = order.ChannelCode,
DownstreamSupplierCode = order.DownstreamSupplierCode,
DownstreamSupplierName = order.DownstreamSupplierName,
DownstreamAppKey = order.DownstreamAppKey,
DownstreamAppId = order.DownstreamAppId,
ChannelForecastCode = order.ChannelForecastCode,
ChannelName = !string.IsNullOrWhiteSpace(order.ChannelCode)
&& channelMap.TryGetValue(order.ChannelCode.Trim(), out var channel)
? channel.ChannelName
: null,
ConsigneeCountry = order.ConsigneeCountry,
ConsigneeCountryName = ResolveConsigneeCountryName(order, countryMap),
CargoAttribute = order.CargoAttribute,
WarehouseId = order.WarehouseId,
Warehouse = order.WarehouseId.HasValue && warehouseMap.TryGetValue(order.WarehouseId.Value, out var warehouse)
? warehouse.WarehouseName
: null,
CustomerId = order.CustomerId,
CustomerName = customerName,
CustomerCode = customerCode,
OrderSource = string.IsNullOrWhiteSpace(order.OrderSource)
? OrderSourceCodes.Manual
: order.OrderSource,
PickupTime = order.PickupTime,
CustomerSenderAddressId = order.CustomerSenderAddressId,
InboundMethod = order.InboundMethod,
FulfillmentType = string.IsNullOrWhiteSpace(order.FulfillmentType)
? FulfillmentTypeCodes.Direct
: order.FulfillmentType,
FulfillmentSource = order.FulfillmentSource,
StockBindStatus = bindSummary?.BindStatus ?? "none",
StockBindLineCount = bindSummary?.LineCount ?? 0,
StockBindQtyTotal = bindSummary?.QtyTotal ?? 0,
DomesticTrackingNos = bindSummary?.TrackingNosText,
FulfillProgress = FulfillmentProgressCodes.Resolve(
FulfillmentTypeCodes.IsWarehouse(order.FulfillmentType),
bindSummary?.BindStatus ?? "none",
bindSummary?.LineCount ?? 0,
bindSummary?.HasInTransitParcel ?? false),
PrimaryBinCode = bindSummary?.PrimaryBinCode,
PrimarySkuCodes = bindSummary?.PrimarySkuCodes,
PickupOrderId = order.PickupOrderId,
PickupNo = order.PickupOrderId is > 0
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
? pickupNo
: null,
CarrierWaybillNo = order.PickupOrderId is > 0
&& carrierWaybillNoMap.TryGetValue(order.PickupOrderId.Value, out var carrierWaybillNo)
? carrierWaybillNo
: null,
RequestId = order.RequestId,
ExportCustomsMode = order.ExportCustomsMode,
ExportQualificationId = order.ExportQualificationId,
PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason)
? null
: order.PredeclareFailReason.Trim(),
AllocationPendingReason = string.IsNullOrWhiteSpace(order.AllocationPendingReason)
? null
: order.AllocationPendingReason.Trim(),
AllocationScheme = string.IsNullOrWhiteSpace(order.AllocationScheme)
? null
: order.AllocationScheme.Trim(),
// 分页已 IgnoreColumns(AllocationTraceJson);与写入路径一并落库的 TraceTime 作为布尔依据
HasAllocationTrace = order.AllocationTraceTime.HasValue
|| !string.IsNullOrWhiteSpace(order.AllocationTraceJson),
DeclaredCurrency = order.DeclaredCurrency,
DeclaredSum = order.DeclaredSum,
Quantity = order.Quantity,
IsCod = order.IsCod > 0,
CodCurrency = order.CodCurrency,
CodSum = order.CodSum,
OrderStatus = OmsDocHelper.GetOrderStatusName(order.OrderStatus),
Status = order.OrderStatus,
HandleStatus = OrderHandleStatusCodes.IsProcessed(order.HandleStatus)
? OrderHandleStatusCodes.Processed
: OrderHandleStatusCodes.Pending,
HandleStatusName = OrderHandleStatusCodes.GetDisplayName(order.HandleStatus),
IssueType = order.IssueType,
IssueTypeName = IssueTypeCodes.GetDisplayName(order.IssueType),
IssueTitle = order.IssueTitle,
IssueContent = order.IssueContent,
IssueTemplateId = order.IssueTemplateId,
IssueRemark = order.IssueRemark,
IssueTime = order.IssueTime,
ClaimType = order.ClaimType,
ClaimTypeName = ClaimTypeCodes.GetDisplayName(order.ClaimType),
ClaimAmount = order.ClaimAmount,
ClaimCurrency = order.ClaimCurrency,
ClaimReason = order.ClaimReason,
ClaimTime = order.ClaimTime,
PreviousOrderStatus = order.PreviousOrderStatus,
ReceiveTime = order.ReceiveTime,
ShipTime = order.ShipTime,
PayTime = order.PayTime,
IsPaid = order.IsPaid > 0,
SignTime = order.SignTime,
IsIntercept = order.IsIntercept > 0,
IsRemote = order.IsRemote > 0,
AdditionalServices = additionalServices,
HasInsurance = hasInsurance,
ForecastReceivableAmount = order.ForecastReceivableAmount,
ForecastPayableAmount = order.ForecastPayableAmount,
ReceivableSubtotal = order.ReceivableSubtotal,
LevelDiscountAmount = order.LevelDiscountAmount,
CouponDiscountAmount = order.CouponDiscountAmount,
ActualPayAmount = order.PayableAmount,
CouponInstanceId = order.CouponInstanceId,
SalesProductRulesVersion = string.IsNullOrWhiteSpace(order.SalesProductRulesVersion)
? null
: order.SalesProductRulesVersion.Trim(),
SalesProductPriceVersion = string.IsNullOrWhiteSpace(order.SalesProductPriceVersion)
? null
: order.SalesProductPriceVersion.Trim(),
CreateTime = order.CreateTime,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.CreateUserId, auditUserNameMap),
UpdateTime = OmsDocHelper.NullIfDefault(order.UpdateTime),
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.UpdateUserId, auditUserNameMap),
Sender = MapSender(sender, countryIdCodeMap),
Receiver = MapReceiver(recipient, countryIdCodeMap),
Items = [],
WeightList = [],
FeeList = [],
ChargeableWeight = chargeableWeight,
ReceivableAmount = receivableAmount,
};
}).ToList();
if (OmsSessionHelper.IsCustomerLogin(CurrentUser))
{
RedactChannelFieldsForCustomer(outputs);
}
return outputs;
}
private sealed class StockBindListSummary private sealed class StockBindListSummary
{ {
public string BindStatus { get; init; } = "none"; public string BindStatus { get; init; } = "none";

View File

@ -70,7 +70,11 @@ public partial class OrderService : ApplicationService
pageIndex, pageIndex,
pageSize, pageSize,
filter); filter);
return new OmsPageResult<OrderOutput> { Items = await MapOrderOutputsAsync(orders), Total = total }; return new OmsPageResult<OrderOutput>
{
Items = await MapOrderListOutputsAsync(orders),
Total = total,
};
} }
/// <summary> /// <summary>

View File

@ -70,6 +70,13 @@ public interface IOrderRepository : IRepository<Order>
Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default); Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default);
/// <summary>
/// 列表页:按订单聚合应收金额合计(不加载费用明细行)。
/// </summary>
Task<Dictionary<int, decimal>> GetReceivableSumByOrderIdsAsync(
IEnumerable<int> orderIds,
CancellationToken cancellationToken = default);
Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync( Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
int pageIndex, int pageIndex,
int pageSize, int pageSize,

View File

@ -23,7 +23,9 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
var query = ApplyListFilter(db.Queryable<Order>(), filter); var query = ApplyListFilter(db.Queryable<Order>(), filter);
var total = await query.CountAsync(cancellationToken); var total = await query.CountAsync(cancellationToken);
query = ApplyListSort(query, filter); query = ApplyListSort(query, filter);
// 列表仅需 HasAllocationTrace 布尔语义,勿拉取大字段 AllocationTraceJson
var items = await query var items = await query
.IgnoreColumns(x => x.AllocationTraceJson)
.ToPageListAsync(pageIndex, pageSize, cancellationToken); .ToPageListAsync(pageIndex, pageSize, cancellationToken);
return (items, total); return (items, total);
} }
@ -1104,6 +1106,34 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
/// <summary>
/// 列表页:按订单聚合应收金额合计(不加载费用明细行)。
/// </summary>
public async Task<Dictionary<int, decimal>> GetReceivableSumByOrderIdsAsync(
IEnumerable<int> orderIds,
CancellationToken cancellationToken = default)
{
var ids = orderIds.Distinct().ToArray();
if (ids.Length == 0)
{
return new Dictionary<int, decimal>();
}
var db = await GetDbContextAsync(cancellationToken);
var rows = await db.Queryable<OrderFee>()
.Where(x => ids.Contains(x.OrderId)
&& !x.IsDeleted
&& x.Type == OrderFeeTypeCodes.Receivable)
.GroupBy(x => x.OrderId)
.Select(x => new
{
OrderId = x.OrderId,
Amount = SqlFunc.AggregateSum(x.Amount),
})
.ToListAsync(cancellationToken);
return rows.ToDictionary(x => x.OrderId, x => x.Amount);
}
public async Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync( public async Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
int pageIndex, int pageIndex,
int pageSize, int pageSize,