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

@@ -60,4 +60,9 @@ public sealed class MerchantDto
/// 入驻时间。
/// </summary>
public DateTime? JoinedAt { get; init; }
/// <summary>
/// 创建时间。
/// </summary>
public DateTime CreatedAt { get; init; }
}

View File

@@ -49,6 +49,7 @@ public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRep
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
JoinedAt = merchant.JoinedAt,
CreatedAt = merchant.CreatedAt
};
}

View File

@@ -36,7 +36,8 @@ public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepo
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
JoinedAt = merchant.JoinedAt,
CreatedAt = merchant.CreatedAt
};
}
}

View File

@@ -23,20 +23,44 @@ public sealed class SearchMerchantsQueryHandler(
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
return merchants
.Select(merchant => new MerchantDto
{
Id = merchant.Id,
TenantId = merchant.TenantId,
BrandName = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
})
var sorted = ApplySorting(merchants, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
return paged.Select(merchant => new MerchantDto
{
Id = merchant.Id,
TenantId = merchant.TenantId,
BrandName = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt,
CreatedAt = merchant.CreatedAt
}).ToList();
}
private static IOrderedEnumerable<Domain.Merchants.Entities.Merchant> ApplySorting(
IReadOnlyCollection<Domain.Merchants.Entities.Merchant> merchants,
string? sortBy,
bool sortDescending)
{
return sortBy?.ToLowerInvariant() switch
{
"brandname" => sortDescending
? merchants.OrderByDescending(x => x.BrandName)
: merchants.OrderBy(x => x.BrandName),
"status" => sortDescending
? merchants.OrderByDescending(x => x.Status)
: merchants.OrderBy(x => x.Status),
_ => sortDescending
? merchants.OrderByDescending(x => x.CreatedAt)
: merchants.OrderBy(x => x.CreatedAt)
};
}
}

View File

@@ -60,6 +60,7 @@ public sealed class UpdateMerchantCommandHandler(
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
JoinedAt = merchant.JoinedAt,
CreatedAt = merchant.CreatedAt
};
}

View File

@@ -13,4 +13,24 @@ public sealed class SearchMerchantsQuery : IRequest<IReadOnlyList<MerchantDto>>
/// 按状态过滤。
/// </summary>
public MerchantStatus? Status { get; init; }
/// <summary>
/// 页码(从 1 开始)。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
/// <summary>
/// 排序字段brandName/status/createdAt
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// 是否倒序。
/// </summary>
public bool SortDescending { get; init; } = true;
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Merchants.Commands;
namespace TakeoutSaaS.Application.App.Merchants.Validators;
/// <summary>
/// 创建商户命令验证器。
/// </summary>
public sealed class CreateMerchantCommandValidator : AbstractValidator<CreateMerchantCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public CreateMerchantCommandValidator()
{
RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128);
RuleFor(x => x.BrandAlias).MaximumLength(64);
RuleFor(x => x.LogoUrl).MaximumLength(256);
RuleFor(x => x.Category).MaximumLength(64);
RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32);
RuleFor(x => x.ContactEmail).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ContactEmail));
}
}

View File

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

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Merchants.Commands;
namespace TakeoutSaaS.Application.App.Merchants.Validators;
/// <summary>
/// 更新商户命令验证器。
/// </summary>
public sealed class UpdateMerchantCommandValidator : AbstractValidator<UpdateMerchantCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public UpdateMerchantCommandValidator()
{
RuleFor(x => x.MerchantId).GreaterThan(0);
RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128);
RuleFor(x => x.BrandAlias).MaximumLength(64);
RuleFor(x => x.LogoUrl).MaximumLength(256);
RuleFor(x => x.Category).MaximumLength(64);
RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32);
RuleFor(x => x.ContactEmail).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ContactEmail));
}
}