fix(order): move all-orders list to database pagination
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m50s

This commit is contained in:
2026-02-27 10:32:21 +08:00
parent b1c51c7712
commit 502f80473e
3 changed files with 201 additions and 85 deletions

View File

@@ -21,34 +21,29 @@ public sealed class SearchOrderAllListQueryHandler(
public async Task<PagedResult<OrderAllListItemDto>> Handle(SearchOrderAllListQuery request, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var page = Math.Max(1, request.Page);
var pageSize = Math.Clamp(request.PageSize, 1, 200);
var orders = await orderRepository.SearchAllOrdersAsync(
var (pagedOrders, totalCount) = await orderRepository.SearchAllOrdersPageAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.Status,
request.RefundedOnly,
request.DeliveryType,
request.PaymentMethod,
request.Keyword,
request.SortBy,
request.SortDescending,
page,
pageSize,
cancellationToken);
var filteredOrders = orders.ToList();
var refundedSet = await LoadRefundedOrderIdsAsync(filteredOrders, tenantId, cancellationToken);
if (request.RefundedOnly)
{
filteredOrders = filteredOrders
.Where(order => refundedSet.Contains(order.Id))
.ToList();
}
var sorted = ApplySorting(filteredOrders, request.SortBy, request.SortDescending);
var page = Math.Max(1, request.Page);
var pageSize = Math.Clamp(request.PageSize, 1, 200);
var pagedOrders = sorted
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var refundedSet = await LoadRefundedOrderIdsAsync(
pagedOrders.Select(order => order.Id).ToList(),
tenantId,
cancellationToken);
var itemsLookup = await orderRepository.GetItemsByOrderIdsAsync(
pagedOrders.Select(order => order.Id).ToList(),
@@ -75,15 +70,14 @@ public sealed class SearchOrderAllListQueryHandler(
})
.ToList();
return new PagedResult<OrderAllListItemDto>(rows, page, pageSize, filteredOrders.Count);
return new PagedResult<OrderAllListItemDto>(rows, page, pageSize, totalCount);
}
private async Task<HashSet<long>> LoadRefundedOrderIdsAsync(
IReadOnlyCollection<Order> orders,
IReadOnlyCollection<long> orderIds,
long tenantId,
CancellationToken cancellationToken)
{
var orderIds = orders.Select(order => order.Id).ToList();
if (orderIds.Count == 0)
{
return [];
@@ -116,23 +110,4 @@ public sealed class SearchOrderAllListQueryHandler(
return $"{first}等{totalQuantity}件";
}
private static IEnumerable<Order> ApplySorting(
IReadOnlyCollection<Order> orders,
string? sortBy,
bool sortDescending)
{
return sortBy?.Trim().ToLowerInvariant() switch
{
"amount" => sortDescending
? orders.OrderByDescending(ResolveDisplayAmount).ThenByDescending(order => order.CreatedAt)
: orders.OrderBy(ResolveDisplayAmount).ThenBy(order => order.CreatedAt),
"status" => sortDescending
? orders.OrderByDescending(order => order.Status).ThenByDescending(order => order.CreatedAt)
: orders.OrderBy(order => order.Status).ThenBy(order => order.CreatedAt),
_ => sortDescending
? orders.OrderByDescending(order => order.CreatedAt)
: orders.OrderBy(order => order.CreatedAt)
};
}
}

View File

@@ -63,6 +63,40 @@ public interface IOrderRepository
string? keyword,
CancellationToken cancellationToken = default);
/// <summary>
/// 全部订单分页筛选查询。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="storeId">门店 ID。</param>
/// <param name="startAt">开始时间(含)。</param>
/// <param name="endAt">结束时间(不含)。</param>
/// <param name="status">订单状态。</param>
/// <param name="refundedOnly">是否仅退款。</param>
/// <param name="deliveryType">履约方式。</param>
/// <param name="paymentMethod">支付方式。</param>
/// <param name="keyword">关键词(订单号/手机号)。</param>
/// <param name="sortBy">排序字段。</param>
/// <param name="sortDescending">是否倒序。</param>
/// <param name="page">页码。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页订单与总数。</returns>
Task<(IReadOnlyList<Order> Items, int TotalCount)> SearchAllOrdersPageAsync(
long tenantId,
long? storeId,
DateTime? startAt,
DateTime? endAt,
OrderStatus? status,
bool refundedOnly,
DeliveryType? deliveryType,
PaymentMethod? paymentMethod,
string? keyword,
string? sortBy,
bool sortDescending,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取订单明细行。
/// </summary>

View File

@@ -71,52 +71,16 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
string? keyword,
CancellationToken cancellationToken = default)
{
var query = context.Orders
.AsNoTracking()
.Where(x => x.TenantId == tenantId);
if (storeId.HasValue)
{
query = query.Where(x => x.StoreId == storeId.Value);
}
if (startAt.HasValue)
{
query = query.Where(x => x.CreatedAt >= startAt.Value);
}
if (endAt.HasValue)
{
query = query.Where(x => x.CreatedAt < endAt.Value);
}
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
if (deliveryType.HasValue)
{
query = query.Where(x => x.DeliveryType == deliveryType.Value);
}
if (paymentMethod.HasValue)
{
query = query.Where(x =>
context.PaymentRecords.Any(record =>
record.TenantId == tenantId &&
record.OrderId == x.Id &&
record.Method == paymentMethod.Value &&
(record.Status == PaymentStatus.Paid || record.Status == PaymentStatus.Refunded)));
}
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalized = keyword.Trim();
query = query.Where(x =>
x.OrderNo.Contains(normalized) ||
(x.CustomerPhone != null && x.CustomerPhone.Contains(normalized)));
}
var query = BuildSearchAllOrdersQuery(
tenantId,
storeId,
startAt,
endAt,
status,
false,
deliveryType,
paymentMethod,
keyword);
return await query
.OrderByDescending(x => x.CreatedAt)
@@ -124,6 +88,48 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<(IReadOnlyList<Order> Items, int TotalCount)> SearchAllOrdersPageAsync(
long tenantId,
long? storeId,
DateTime? startAt,
DateTime? endAt,
OrderStatus? status,
bool refundedOnly,
DeliveryType? deliveryType,
PaymentMethod? paymentMethod,
string? keyword,
string? sortBy,
bool sortDescending,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var normalizedPage = Math.Max(1, page);
var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
var query = BuildSearchAllOrdersQuery(
tenantId,
storeId,
startAt,
endAt,
status,
refundedOnly,
deliveryType,
paymentMethod,
keyword);
var total = await query.CountAsync(cancellationToken);
var sorted = ApplyOrderSorting(query, sortBy, sortDescending);
var items = await sorted
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
return (items, total);
}
/// <inheritdoc />
public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{
@@ -306,4 +312,105 @@ public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepos
context.Orders.Remove(existing);
}
private IQueryable<Order> BuildSearchAllOrdersQuery(
long tenantId,
long? storeId,
DateTime? startAt,
DateTime? endAt,
OrderStatus? status,
bool refundedOnly,
DeliveryType? deliveryType,
PaymentMethod? paymentMethod,
string? keyword)
{
var query = context.Orders
.AsNoTracking()
.Where(x => x.TenantId == tenantId);
if (storeId.HasValue)
{
query = query.Where(x => x.StoreId == storeId.Value);
}
if (startAt.HasValue)
{
query = query.Where(x => x.CreatedAt >= startAt.Value);
}
if (endAt.HasValue)
{
query = query.Where(x => x.CreatedAt < endAt.Value);
}
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
if (deliveryType.HasValue)
{
query = query.Where(x => x.DeliveryType == deliveryType.Value);
}
if (paymentMethod.HasValue)
{
query = query.Where(x =>
context.PaymentRecords.Any(record =>
record.TenantId == tenantId &&
record.OrderId == x.Id &&
record.Method == paymentMethod.Value &&
(record.Status == PaymentStatus.Paid || record.Status == PaymentStatus.Refunded)));
}
if (refundedOnly)
{
query = query.Where(x =>
context.RefundRequests.Any(refund =>
refund.TenantId == tenantId &&
refund.OrderId == x.Id &&
refund.Status == RefundStatus.Refunded));
}
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalized = keyword.Trim();
query = query.Where(x =>
x.OrderNo.Contains(normalized) ||
(x.CustomerPhone != null && x.CustomerPhone.Contains(normalized)));
}
return query;
}
private static IOrderedQueryable<Order> ApplyOrderSorting(
IQueryable<Order> query,
string? sortBy,
bool sortDescending)
{
return sortBy?.Trim().ToLowerInvariant() switch
{
"amount" => sortDescending
? query
.OrderByDescending(order => order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount)
.ThenByDescending(order => order.CreatedAt)
.ThenByDescending(order => order.Id)
: query
.OrderBy(order => order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount)
.ThenBy(order => order.CreatedAt)
.ThenBy(order => order.Id),
"status" => sortDescending
? query
.OrderByDescending(order => order.Status)
.ThenByDescending(order => order.CreatedAt)
.ThenByDescending(order => order.Id)
: query
.OrderBy(order => order.Status)
.ThenBy(order => order.CreatedAt)
.ThenBy(order => order.Id),
_ => sortDescending
? query.OrderByDescending(order => order.CreatedAt).ThenByDescending(order => order.Id)
: query.OrderBy(order => order.CreatedAt).ThenBy(order => order.Id)
};
}
}