feat: 增加分页排序与FluentValidation

This commit is contained in:
2025-12-02 10:50:43 +08:00
parent 93141fbf0c
commit 97bf6cacb0
63 changed files with 904 additions and 49 deletions

View File

@@ -112,4 +112,9 @@ public sealed class ProductDto
/// 是否推荐。
/// </summary>
public bool IsFeatured { get; init; }
/// <summary>
/// 创建时间。
/// </summary>
public DateTime CreatedAt { get; init; }
}

View File

@@ -72,6 +72,7 @@ public sealed class CreateProductCommandHandler(IProductRepository productReposi
EnableDineIn = product.EnableDineIn,
EnablePickup = product.EnablePickup,
EnableDelivery = product.EnableDelivery,
IsFeatured = product.IsFeatured
IsFeatured = product.IsFeatured,
CreatedAt = product.CreatedAt
};
}

View File

@@ -47,6 +47,7 @@ public sealed class GetProductByIdQueryHandler(
EnableDineIn = product.EnableDineIn,
EnablePickup = product.EnablePickup,
EnableDelivery = product.EnableDelivery,
IsFeatured = product.IsFeatured
IsFeatured = product.IsFeatured,
CreatedAt = product.CreatedAt
};
}

View File

@@ -28,7 +28,27 @@ public sealed class SearchProductsQueryHandler(
products = products.Where(x => x.StoreId == request.StoreId.Value).ToList();
}
return products.Select(MapToDto).ToList();
var sorted = ApplySorting(products, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
return paged.Select(MapToDto).ToList();
}
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)
};
}
private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new()
@@ -52,6 +72,7 @@ public sealed class SearchProductsQueryHandler(
EnableDineIn = product.EnableDineIn,
EnablePickup = product.EnablePickup,
EnableDelivery = product.EnableDelivery,
IsFeatured = product.IsFeatured
IsFeatured = product.IsFeatured,
CreatedAt = product.CreatedAt
};
}

View File

@@ -82,6 +82,7 @@ public sealed class UpdateProductCommandHandler(
EnableDineIn = product.EnableDineIn,
EnablePickup = product.EnablePickup,
EnableDelivery = product.EnableDelivery,
IsFeatured = product.IsFeatured
IsFeatured = product.IsFeatured,
CreatedAt = product.CreatedAt
};
}

View File

@@ -23,4 +23,24 @@ public sealed class SearchProductsQuery : IRequest<IReadOnlyList<ProductDto>>
/// 状态过滤。
/// </summary>
public ProductStatus? Status { get; init; }
/// <summary>
/// 页码。
/// </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;
}

View File

@@ -0,0 +1,29 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Products.Commands;
namespace TakeoutSaaS.Application.App.Products.Validators;
/// <summary>
/// 创建商品命令验证器。
/// </summary>
public sealed class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public CreateProductCommandValidator()
{
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.CategoryId).GreaterThan(0);
RuleFor(x => x.SpuCode).NotEmpty().MaximumLength(32);
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
RuleFor(x => x.Subtitle).MaximumLength(256);
RuleFor(x => x.Unit).MaximumLength(16);
RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue);
RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue);
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
RuleFor(x => x.CoverImage).MaximumLength(256);
RuleFor(x => x.GalleryImages).MaximumLength(1024);
}
}

View File

@@ -0,0 +1,20 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Products.Queries;
namespace TakeoutSaaS.Application.App.Products.Validators;
/// <summary>
/// 商品列表查询验证器。
/// </summary>
public sealed class SearchProductsQueryValidator : AbstractValidator<SearchProductsQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public SearchProductsQueryValidator()
{
RuleFor(x => x.Page).GreaterThan(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
RuleFor(x => x.SortBy).MaximumLength(64);
}
}

View File

@@ -0,0 +1,30 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Products.Commands;
namespace TakeoutSaaS.Application.App.Products.Validators;
/// <summary>
/// 更新商品命令验证器。
/// </summary>
public sealed class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public UpdateProductCommandValidator()
{
RuleFor(x => x.ProductId).GreaterThan(0);
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.CategoryId).GreaterThan(0);
RuleFor(x => x.SpuCode).NotEmpty().MaximumLength(32);
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
RuleFor(x => x.Subtitle).MaximumLength(256);
RuleFor(x => x.Unit).MaximumLength(16);
RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue);
RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue);
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
RuleFor(x => x.CoverImage).MaximumLength(256);
RuleFor(x => x.GalleryImages).MaximumLength(1024);
}
}