From 3f5ca9c3ee3d058cf256ab753bd50e54a234ee68 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Thu, 26 Feb 2026 10:50:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=93=81=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=94=B9=E4=B8=BA=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Handlers/SearchProductsQueryHandler.cs | 72 ++++----------- .../Repositories/IProductRepository.cs | 68 +++++++++++++++ .../App/Repositories/EfProductRepository.cs | 87 +++++++++++++++++++ 3 files changed, 173 insertions(+), 54 deletions(-) diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs index 8e7fa3c..4ffecec 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs @@ -23,62 +23,26 @@ public sealed class SearchProductsQueryHandler( { var tenantId = _tenantProvider.GetCurrentTenantId(); - // 沽清查询需要包含 OffShelf 状态后再做二次过滤。 - var repositoryStatus = request.IsSoldOut == true ? null : request.Status; - var products = await _productRepository.SearchAsync( - tenantId, - request.StoreId, - request.CategoryId, - repositoryStatus, - cancellationToken); - - IEnumerable filtered = products; - - if (!string.IsNullOrWhiteSpace(request.Keyword)) + // 沽清查询不走 Status 过滤,保持“仅沽清”口径。 + var status = request.IsSoldOut == true ? null : request.Status; + var filter = new ProductSearchFilter { - var keyword = request.Keyword.Trim().ToLowerInvariant(); - filtered = filtered.Where(item => - item.Name.ToLower().Contains(keyword) || - item.SpuCode.ToLower().Contains(keyword)); - } - - if (request.Kind.HasValue) - { - filtered = filtered.Where(item => item.Kind == request.Kind.Value); - } - - 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(items, request.Page, request.PageSize, filteredList.Count); - } - - private static IOrderedEnumerable ApplySorting( - IReadOnlyCollection 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) + TenantId = tenantId, + StoreId = request.StoreId, + CategoryId = request.CategoryId, + Status = status, + Kind = request.Kind, + Keyword = request.Keyword, + IsSoldOut = request.IsSoldOut, + Page = request.Page, + PageSize = request.PageSize, + SortBy = request.SortBy, + SortDescending = request.SortDescending }; + + var (products, totalCount) = await _productRepository.SearchPagedAsync(filter, cancellationToken); + var items = products.Select(MapToDto).ToList(); + return new PagedResult(items, request.Page, request.PageSize, totalCount); } private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new() diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs index 3271639..b0b119c 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs @@ -18,6 +18,13 @@ public interface IProductRepository /// Task> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null); + /// + /// 分页查询商品列表。 + /// + Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + ProductSearchFilter filter, + CancellationToken cancellationToken = default); + /// /// 获取租户下的商品分类。 /// @@ -499,3 +506,64 @@ public interface IProductRepository /// 异步任务。 Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default); } + +/// +/// 商品分页查询过滤条件。 +/// +public sealed record ProductSearchFilter +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 门店 ID(可选)。 + /// + public long? StoreId { get; init; } + + /// + /// 分类 ID(可选)。 + /// + public long? CategoryId { get; init; } + + /// + /// 状态过滤。 + /// + public ProductStatus? Status { get; init; } + + /// + /// 商品类型过滤。 + /// + public ProductKind? Kind { get; init; } + + /// + /// 关键字(名称/SPU)。 + /// + public string? Keyword { get; init; } + + /// + /// 是否仅沽清商品。 + /// + public bool? IsSoldOut { get; init; } + + /// + /// 页码(从 1 开始)。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(name/price/status/createdAt)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs index cdcbf31..633bea7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs @@ -60,6 +60,93 @@ public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductR return products; } + /// + public async Task<(IReadOnlyList 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 ApplySearchSort( + IQueryable 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) + }; + } + /// public async Task> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default) {