feat: 商品列表查询改为数据库分页过滤
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m53s
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m53s
This commit is contained in:
@@ -23,62 +23,26 @@ public sealed class SearchProductsQueryHandler(
|
|||||||
{
|
{
|
||||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||||
|
|
||||||
// 沽清查询需要包含 OffShelf 状态后再做二次过滤。
|
// 沽清查询不走 Status 过滤,保持“仅沽清”口径。
|
||||||
var repositoryStatus = request.IsSoldOut == true ? null : request.Status;
|
var status = request.IsSoldOut == true ? null : request.Status;
|
||||||
var products = await _productRepository.SearchAsync(
|
var filter = new ProductSearchFilter
|
||||||
tenantId,
|
|
||||||
request.StoreId,
|
|
||||||
request.CategoryId,
|
|
||||||
repositoryStatus,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
IEnumerable<Domain.Products.Entities.Product> filtered = products;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
|
||||||
{
|
{
|
||||||
var keyword = request.Keyword.Trim().ToLowerInvariant();
|
TenantId = tenantId,
|
||||||
filtered = filtered.Where(item =>
|
StoreId = request.StoreId,
|
||||||
item.Name.ToLower().Contains(keyword) ||
|
CategoryId = request.CategoryId,
|
||||||
item.SpuCode.ToLower().Contains(keyword));
|
Status = status,
|
||||||
}
|
Kind = request.Kind,
|
||||||
|
Keyword = request.Keyword,
|
||||||
if (request.Kind.HasValue)
|
IsSoldOut = request.IsSoldOut,
|
||||||
{
|
Page = request.Page,
|
||||||
filtered = filtered.Where(item => item.Kind == request.Kind.Value);
|
PageSize = request.PageSize,
|
||||||
}
|
SortBy = request.SortBy,
|
||||||
|
SortDescending = request.SortDescending
|
||||||
if (request.IsSoldOut == true)
|
|
||||||
{
|
|
||||||
filtered = filtered.Where(item => item.SoldoutMode.HasValue);
|
|
||||||
}
|
|
||||||
else if (request.Status == Domain.Products.Enums.ProductStatus.OffShelf)
|
|
||||||
{
|
|
||||||
filtered = filtered.Where(item => !item.SoldoutMode.HasValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
var filteredList = filtered.ToList();
|
|
||||||
var sorted = ApplySorting(filteredList, request.SortBy, request.SortDescending);
|
|
||||||
var paged = sorted
|
|
||||||
.Skip((request.Page - 1) * request.PageSize)
|
|
||||||
.Take(request.PageSize)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var items = paged.Select(MapToDto).ToList();
|
|
||||||
return new PagedResult<ProductDto>(items, request.Page, request.PageSize, filteredList.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IOrderedEnumerable<Domain.Products.Entities.Product> ApplySorting(
|
|
||||||
IReadOnlyCollection<Domain.Products.Entities.Product> products,
|
|
||||||
string? sortBy,
|
|
||||||
bool sortDescending)
|
|
||||||
{
|
|
||||||
return sortBy?.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"name" => sortDescending ? products.OrderByDescending(x => x.Name) : products.OrderBy(x => x.Name),
|
|
||||||
"price" => sortDescending ? products.OrderByDescending(x => x.Price) : products.OrderBy(x => x.Price),
|
|
||||||
"status" => sortDescending ? products.OrderByDescending(x => x.Status) : products.OrderBy(x => x.Status),
|
|
||||||
_ => sortDescending ? products.OrderByDescending(x => x.CreatedAt) : products.OrderBy(x => x.CreatedAt)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var (products, totalCount) = await _productRepository.SearchPagedAsync(filter, cancellationToken);
|
||||||
|
var items = products.Select(MapToDto).ToList();
|
||||||
|
return new PagedResult<ProductDto>(items, request.Page, request.PageSize, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new()
|
private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new()
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ public interface IProductRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null);
|
Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询商品列表。
|
||||||
|
/// </summary>
|
||||||
|
Task<(IReadOnlyList<Product> Items, int Total)> SearchPagedAsync(
|
||||||
|
ProductSearchFilter filter,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取租户下的商品分类。
|
/// 获取租户下的商品分类。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -499,3 +506,64 @@ public interface IProductRepository
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品分页查询过滤条件。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ProductSearchFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? CategoryId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态过滤。
|
||||||
|
/// </summary>
|
||||||
|
public ProductStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品类型过滤。
|
||||||
|
/// </summary>
|
||||||
|
public ProductKind? Kind { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(名称/SPU)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否仅沽清商品。
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsSoldOut { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码(从 1 开始)。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段(name/price/status/createdAt)。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否倒序。
|
||||||
|
/// </summary>
|
||||||
|
public bool SortDescending { get; init; } = true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,6 +60,93 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR
|
|||||||
return products;
|
return products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<Product> Items, int Total)> SearchPagedAsync(
|
||||||
|
ProductSearchFilter filter,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (filter.TenantId <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("TenantId 不能为空且必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = filter.Page <= 0 ? 1 : filter.Page;
|
||||||
|
var pageSize = filter.PageSize <= 0 ? 20 : filter.PageSize;
|
||||||
|
|
||||||
|
var query = context.Products
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == filter.TenantId);
|
||||||
|
|
||||||
|
if (filter.StoreId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.StoreId == filter.StoreId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.CategoryId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.CategoryId == filter.CategoryId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Status.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Status == filter.Status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Keyword))
|
||||||
|
{
|
||||||
|
var keyword = $"%{filter.Keyword.Trim()}%";
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.Name, keyword) ||
|
||||||
|
EF.Functions.ILike(x.SpuCode, keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Kind.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Kind == filter.Kind.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.IsSoldOut == true)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.SoldoutMode.HasValue);
|
||||||
|
}
|
||||||
|
else if (filter.Status == ProductStatus.OffShelf)
|
||||||
|
{
|
||||||
|
// 状态为 OffShelf 时,排除“沽清”以保持前台“下架”口径。
|
||||||
|
query = query.Where(x => !x.SoldoutMode.HasValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted = ApplySearchSort(query, filter.SortBy, filter.SortDescending);
|
||||||
|
var total = await sorted.CountAsync(cancellationToken);
|
||||||
|
var items = await sorted
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (items, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOrderedQueryable<Product> ApplySearchSort(
|
||||||
|
IQueryable<Product> query,
|
||||||
|
string? sortBy,
|
||||||
|
bool sortDescending)
|
||||||
|
{
|
||||||
|
return sortBy?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"name" => sortDescending
|
||||||
|
? query.OrderByDescending(x => x.Name).ThenByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Name).ThenBy(x => x.Id),
|
||||||
|
"price" => sortDescending
|
||||||
|
? query.OrderByDescending(x => x.Price).ThenByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Price).ThenBy(x => x.Id),
|
||||||
|
"status" => sortDescending
|
||||||
|
? query.OrderByDescending(x => x.Status).ThenByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.Status).ThenBy(x => x.Id),
|
||||||
|
_ => sortDescending
|
||||||
|
? query.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
|
||||||
|
: query.OrderBy(x => x.CreatedAt).ThenBy(x => x.Id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user