feat: 增加分页排序与FluentValidation
This commit is contained in:
@@ -49,12 +49,23 @@ public sealed class DeliveriesController : BaseApiController
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("delivery:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DeliveryOrderDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<DeliveryOrderDto>>> List([FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<DeliveryOrderDto>>> List(
|
||||
[FromQuery] long? orderId,
|
||||
[FromQuery] DeliveryStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchDeliveryOrdersQuery
|
||||
{
|
||||
OrderId = orderId,
|
||||
Status = status
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<DeliveryOrderDto>>.Ok(result);
|
||||
|
||||
@@ -49,9 +49,22 @@ public sealed class MerchantsController : BaseApiController
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantDto>>> List([FromQuery] MerchantStatus? status, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantDto>>> List(
|
||||
[FromQuery] MerchantStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchMerchantsQuery { Status = status }, cancellationToken);
|
||||
var result = await _mediator.Send(new SearchMerchantsQuery
|
||||
{
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantDto>>.Ok(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,22 @@ public sealed class OrdersController : BaseApiController
|
||||
[FromQuery] OrderStatus? status,
|
||||
[FromQuery] PaymentStatus? paymentStatus,
|
||||
[FromQuery] string? orderNo,
|
||||
CancellationToken cancellationToken)
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchOrdersQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
Status = status,
|
||||
PaymentStatus = paymentStatus,
|
||||
OrderNo = orderNo
|
||||
OrderNo = orderNo,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<OrderDto>>.Ok(result);
|
||||
|
||||
@@ -49,12 +49,23 @@ public sealed class PaymentsController : BaseApiController
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("payment:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PaymentDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<PaymentDto>>> List([FromQuery] long? orderId, [FromQuery] PaymentStatus? status, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<PaymentDto>>> List(
|
||||
[FromQuery] long? orderId,
|
||||
[FromQuery] PaymentStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchPaymentsQuery
|
||||
{
|
||||
OrderId = orderId,
|
||||
Status = status
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<PaymentDto>>.Ok(result);
|
||||
|
||||
@@ -49,13 +49,25 @@ public sealed class ProductsController : BaseApiController
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("product:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductDto>>> List([FromQuery] long? storeId, [FromQuery] long? categoryId, [FromQuery] ProductStatus? status, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductDto>>> List(
|
||||
[FromQuery] long? storeId,
|
||||
[FromQuery] long? categoryId,
|
||||
[FromQuery] ProductStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchProductsQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
CategoryId = categoryId,
|
||||
Status = status
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<ProductDto>>.Ok(result);
|
||||
|
||||
@@ -49,12 +49,23 @@ public sealed class StoresController : BaseApiController
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreDto>>> List([FromQuery] long? merchantId, [FromQuery] StoreStatus? status, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreDto>>> List(
|
||||
[FromQuery] long? merchantId,
|
||||
[FromQuery] StoreStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchStoresQuery
|
||||
{
|
||||
MerchantId = merchantId,
|
||||
Status = status
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<StoreDto>>.Ok(result);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Common.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR 请求验证行为,统一触发 FluentValidation。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators = validators;
|
||||
|
||||
/// <summary>
|
||||
/// 执行验证并在通过时继续后续处理。
|
||||
/// </summary>
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_validators.Any())
|
||||
{
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
|
||||
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f is not null).ToList();
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,9 @@ public sealed class DeliveryOrderDto
|
||||
/// 事件列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<DeliveryEventDto> Events { get; init; } = Array.Empty<DeliveryEventDto>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository delive
|
||||
PickedUpAt = deliveryOrder.PickedUpAt,
|
||||
DeliveredAt = deliveryOrder.DeliveredAt,
|
||||
FailureReason = deliveryOrder.FailureReason,
|
||||
CreatedAt = deliveryOrder.CreatedAt,
|
||||
Events = events.Select(x => new DeliveryEventDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -47,6 +47,7 @@ public sealed class GetDeliveryOrderByIdQueryHandler(
|
||||
PickedUpAt = deliveryOrder.PickedUpAt,
|
||||
DeliveredAt = deliveryOrder.DeliveredAt,
|
||||
FailureReason = deliveryOrder.FailureReason,
|
||||
CreatedAt = deliveryOrder.CreatedAt,
|
||||
Events = events.Select(x => new DeliveryEventDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -23,7 +23,13 @@ public sealed class SearchDeliveryOrdersQueryHandler(
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken);
|
||||
|
||||
return orders.Select(order => new DeliveryOrderDto
|
||||
var sorted = ApplySorting(orders, request.SortBy, request.SortDescending);
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
return paged.Select(order => new DeliveryOrderDto
|
||||
{
|
||||
Id = order.Id,
|
||||
TenantId = order.TenantId,
|
||||
@@ -37,7 +43,21 @@ public sealed class SearchDeliveryOrdersQueryHandler(
|
||||
DispatchedAt = order.DispatchedAt,
|
||||
PickedUpAt = order.PickedUpAt,
|
||||
DeliveredAt = order.DeliveredAt,
|
||||
FailureReason = order.FailureReason
|
||||
FailureReason = order.FailureReason,
|
||||
CreatedAt = order.CreatedAt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<Domain.Deliveries.Entities.DeliveryOrder> ApplySorting(
|
||||
IReadOnlyCollection<Domain.Deliveries.Entities.DeliveryOrder> orders,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"status" => sortDescending ? orders.OrderByDescending(x => x.Status) : orders.OrderBy(x => x.Status),
|
||||
"provider" => sortDescending ? orders.OrderByDescending(x => x.Provider) : orders.OrderBy(x => x.Provider),
|
||||
_ => sortDescending ? orders.OrderByDescending(x => x.CreatedAt) : orders.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class UpdateDeliveryOrderCommandHandler(
|
||||
PickedUpAt = deliveryOrder.PickedUpAt,
|
||||
DeliveredAt = deliveryOrder.DeliveredAt,
|
||||
FailureReason = deliveryOrder.FailureReason,
|
||||
CreatedAt = deliveryOrder.CreatedAt,
|
||||
Events = events.Select(x => new DeliveryEventDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -18,4 +18,24 @@ public sealed class SearchDeliveryOrdersQuery : IRequest<IReadOnlyList<DeliveryO
|
||||
/// 配送状态。
|
||||
/// </summary>
|
||||
public DeliveryStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(createdAt/status/provider)。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否倒序。
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Deliveries.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 创建配送单命令验证器。
|
||||
/// </summary>
|
||||
public sealed class CreateDeliveryOrderCommandValidator : AbstractValidator<CreateDeliveryOrderCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public CreateDeliveryOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId).GreaterThan(0);
|
||||
RuleFor(x => x.ProviderOrderId).MaximumLength(64);
|
||||
RuleFor(x => x.CourierName).MaximumLength(64);
|
||||
RuleFor(x => x.CourierPhone).MaximumLength(32);
|
||||
RuleFor(x => x.FailureReason).MaximumLength(256);
|
||||
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Deliveries.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 配送单列表查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchDeliveryOrdersQueryValidator : AbstractValidator<SearchDeliveryOrdersQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchDeliveryOrdersQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
RuleFor(x => x.SortBy).MaximumLength(64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Deliveries.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 更新配送单命令验证器。
|
||||
/// </summary>
|
||||
public sealed class UpdateDeliveryOrderCommandValidator : AbstractValidator<UpdateDeliveryOrderCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public UpdateDeliveryOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.DeliveryOrderId).GreaterThan(0);
|
||||
RuleFor(x => x.OrderId).GreaterThan(0);
|
||||
RuleFor(x => x.ProviderOrderId).MaximumLength(64);
|
||||
RuleFor(x => x.CourierName).MaximumLength(64);
|
||||
RuleFor(x => x.CourierPhone).MaximumLength(32);
|
||||
RuleFor(x => x.FailureReason).MaximumLength(256);
|
||||
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TakeoutSaaS.Application.App.Common.Behaviors;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Extensions;
|
||||
|
||||
@@ -17,6 +19,8 @@ public static class AppApplicationServiceCollectionExtensions
|
||||
public static IServiceCollection AddAppApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddMediatR(Assembly.GetExecutingAssembly());
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -60,4 +60,9 @@ public sealed class MerchantDto
|
||||
/// 入驻时间。
|
||||
/// </summary>
|
||||
public DateTime? JoinedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -138,4 +138,9 @@ public sealed class OrderDto
|
||||
/// 退款申请。
|
||||
/// </summary>
|
||||
public IReadOnlyList<RefundRequestDto> Refunds { get; init; } = Array.Empty<RefundRequestDto>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ public sealed class CreateOrderCommandHandler(
|
||||
RequestedAt = x.RequestedAt,
|
||||
ProcessedAt = x.ProcessedAt,
|
||||
ReviewNotes = x.ReviewNotes
|
||||
}).ToList()
|
||||
}).ToList(),
|
||||
CreatedAt = order.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ public sealed class GetOrderByIdQueryHandler(
|
||||
RequestedAt = x.RequestedAt,
|
||||
ProcessedAt = x.ProcessedAt,
|
||||
ReviewNotes = x.ReviewNotes
|
||||
}).ToList()
|
||||
}).ToList(),
|
||||
CreatedAt = order.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,13 @@ public sealed class SearchOrdersQueryHandler(
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return orders.Select(order => new OrderDto
|
||||
var sorted = ApplySorting(orders, request.SortBy, request.SortDescending);
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
return paged.Select(order => new OrderDto
|
||||
{
|
||||
Id = order.Id,
|
||||
TenantId = order.TenantId,
|
||||
@@ -59,7 +65,22 @@ public sealed class SearchOrdersQueryHandler(
|
||||
FinishedAt = order.FinishedAt,
|
||||
CancelledAt = order.CancelledAt,
|
||||
CancelReason = order.CancelReason,
|
||||
Remark = order.Remark
|
||||
Remark = order.Remark,
|
||||
CreatedAt = order.CreatedAt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<Domain.Orders.Entities.Order> ApplySorting(
|
||||
IReadOnlyCollection<Domain.Orders.Entities.Order> orders,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"paidat" => sortDescending ? orders.OrderByDescending(x => x.PaidAt) : orders.OrderBy(x => x.PaidAt),
|
||||
"status" => sortDescending ? orders.OrderByDescending(x => x.Status) : orders.OrderBy(x => x.Status),
|
||||
"payableamount" => sortDescending ? orders.OrderByDescending(x => x.PayableAmount) : orders.OrderBy(x => x.PayableAmount),
|
||||
_ => sortDescending ? orders.OrderByDescending(x => x.CreatedAt) : orders.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public sealed class UpdateOrderCommandHandler(
|
||||
RequestedAt = x.RequestedAt,
|
||||
ProcessedAt = x.ProcessedAt,
|
||||
ReviewNotes = x.ReviewNotes
|
||||
}).ToList()
|
||||
}).ToList(),
|
||||
CreatedAt = order.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,4 +29,24 @@ public sealed class SearchOrdersQuery : IRequest<IReadOnlyList<OrderDto>>
|
||||
/// 订单号(模糊或精确,由调用方控制)。
|
||||
/// </summary>
|
||||
public string? OrderNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(createdAt/paidAt/status/payableAmount)。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否倒序。
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 创建订单命令验证器。
|
||||
/// </summary>
|
||||
public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public CreateOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32);
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CustomerPhone).MaximumLength(32);
|
||||
RuleFor(x => x.CustomerName).MaximumLength(64);
|
||||
RuleFor(x => x.TableNo).MaximumLength(32);
|
||||
RuleFor(x => x.QueueNumber).MaximumLength(32);
|
||||
RuleFor(x => x.CancelReason).MaximumLength(256);
|
||||
RuleFor(x => x.Remark).MaximumLength(512);
|
||||
RuleFor(x => x.ItemsAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.DiscountAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0);
|
||||
|
||||
RuleFor(x => x.Items)
|
||||
.NotEmpty()
|
||||
.WithMessage("订单明细不能为空");
|
||||
|
||||
RuleForEach(x => x.Items).ChildRules(item =>
|
||||
{
|
||||
item.RuleFor(i => i.ProductId).GreaterThan(0);
|
||||
item.RuleFor(i => i.ProductName).NotEmpty().MaximumLength(128);
|
||||
item.RuleFor(i => i.SkuName).MaximumLength(128);
|
||||
item.RuleFor(i => i.Unit).MaximumLength(16);
|
||||
item.RuleFor(i => i.Quantity).GreaterThan(0);
|
||||
item.RuleFor(i => i.UnitPrice).GreaterThanOrEqualTo(0);
|
||||
item.RuleFor(i => i.DiscountAmount).GreaterThanOrEqualTo(0);
|
||||
item.RuleFor(i => i.SubTotal).GreaterThanOrEqualTo(0);
|
||||
item.RuleFor(i => i.AttributesJson).MaximumLength(4000);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 订单列表查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchOrdersQueryValidator : AbstractValidator<SearchOrdersQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchOrdersQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
RuleFor(x => x.SortBy).MaximumLength(64);
|
||||
RuleFor(x => x.OrderNo).MaximumLength(32);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Orders.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Orders.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 更新订单命令验证器。
|
||||
/// </summary>
|
||||
public sealed class UpdateOrderCommandValidator : AbstractValidator<UpdateOrderCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public UpdateOrderCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId).GreaterThan(0);
|
||||
RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32);
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CustomerPhone).MaximumLength(32);
|
||||
RuleFor(x => x.CustomerName).MaximumLength(64);
|
||||
RuleFor(x => x.TableNo).MaximumLength(32);
|
||||
RuleFor(x => x.QueueNumber).MaximumLength(32);
|
||||
RuleFor(x => x.CancelReason).MaximumLength(256);
|
||||
RuleFor(x => x.Remark).MaximumLength(512);
|
||||
RuleFor(x => x.ItemsAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.DiscountAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,9 @@ public sealed class PaymentDto
|
||||
/// 退款记录。
|
||||
/// </summary>
|
||||
public IReadOnlyList<PaymentRefundDto> Refunds { get; init; } = Array.Empty<PaymentRefundDto>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public sealed class CreatePaymentCommandHandler(IPaymentRepository paymentReposi
|
||||
PaidAt = payment.PaidAt,
|
||||
Remark = payment.Remark,
|
||||
Payload = payment.Payload,
|
||||
CreatedAt = payment.CreatedAt,
|
||||
Refunds = refunds.Select(x => new PaymentRefundDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -45,6 +45,7 @@ public sealed class GetPaymentByIdQueryHandler(
|
||||
PaidAt = payment.PaidAt,
|
||||
Remark = payment.Remark,
|
||||
Payload = payment.Payload,
|
||||
CreatedAt = payment.CreatedAt,
|
||||
Refunds = refunds.Select(x => new PaymentRefundDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||
using TakeoutSaaS.Application.App.Payments.Queries;
|
||||
using TakeoutSaaS.Domain.Payments.Entities;
|
||||
using TakeoutSaaS.Domain.Payments.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
@@ -28,7 +29,13 @@ public sealed class SearchPaymentsQueryHandler(
|
||||
payments = payments.Where(x => x.OrderId == request.OrderId.Value).ToList();
|
||||
}
|
||||
|
||||
return payments.Select(payment => new PaymentDto
|
||||
var sorted = ApplySorting(payments, request.SortBy, request.SortDescending);
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
return paged.Select(payment => new PaymentDto
|
||||
{
|
||||
Id = payment.Id,
|
||||
TenantId = payment.TenantId,
|
||||
@@ -40,7 +47,22 @@ public sealed class SearchPaymentsQueryHandler(
|
||||
ChannelTransactionId = payment.ChannelTransactionId,
|
||||
PaidAt = payment.PaidAt,
|
||||
Remark = payment.Remark,
|
||||
Payload = payment.Payload
|
||||
Payload = payment.Payload,
|
||||
CreatedAt = payment.CreatedAt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<Domain.Payments.Entities.PaymentRecord> ApplySorting(
|
||||
IReadOnlyCollection<Domain.Payments.Entities.PaymentRecord> payments,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"paidat" => sortDescending ? payments.OrderByDescending(x => x.PaidAt) : payments.OrderBy(x => x.PaidAt),
|
||||
"status" => sortDescending ? payments.OrderByDescending(x => x.Status) : payments.OrderBy(x => x.Status),
|
||||
"amount" => sortDescending ? payments.OrderByDescending(x => x.Amount) : payments.OrderBy(x => x.Amount),
|
||||
_ => sortDescending ? payments.OrderByDescending(x => x.CreatedAt) : payments.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public sealed class UpdatePaymentCommandHandler(
|
||||
PaidAt = payment.PaidAt,
|
||||
Remark = payment.Remark,
|
||||
Payload = payment.Payload,
|
||||
CreatedAt = payment.CreatedAt,
|
||||
Refunds = refunds.Select(x => new PaymentRefundDto
|
||||
{
|
||||
Id = x.Id,
|
||||
|
||||
@@ -18,4 +18,24 @@ public sealed class SearchPaymentsQuery : IRequest<IReadOnlyList<PaymentDto>>
|
||||
/// 支付状态。
|
||||
/// </summary>
|
||||
public PaymentStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(createdAt/paidAt/status/amount)。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否倒序。
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Payments.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Payments.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 创建支付记录命令验证器。
|
||||
/// </summary>
|
||||
public sealed class CreatePaymentCommandValidator : AbstractValidator<CreatePaymentCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public CreatePaymentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId).GreaterThan(0);
|
||||
RuleFor(x => x.Amount).GreaterThan(0);
|
||||
RuleFor(x => x.TradeNo).MaximumLength(64);
|
||||
RuleFor(x => x.ChannelTransactionId).MaximumLength(64);
|
||||
RuleFor(x => x.Remark).MaximumLength(256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Payments.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Payments.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 支付记录查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchPaymentsQueryValidator : AbstractValidator<SearchPaymentsQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchPaymentsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
RuleFor(x => x.SortBy).MaximumLength(64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Payments.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Payments.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 更新支付记录命令验证器。
|
||||
/// </summary>
|
||||
public sealed class UpdatePaymentCommandValidator : AbstractValidator<UpdatePaymentCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public UpdatePaymentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PaymentId).GreaterThan(0);
|
||||
RuleFor(x => x.OrderId).GreaterThan(0);
|
||||
RuleFor(x => x.Amount).GreaterThan(0);
|
||||
RuleFor(x => x.TradeNo).MaximumLength(64);
|
||||
RuleFor(x => x.ChannelTransactionId).MaximumLength(64);
|
||||
RuleFor(x => x.Remark).MaximumLength(256);
|
||||
}
|
||||
}
|
||||
@@ -112,4 +112,9 @@ public sealed class ProductDto
|
||||
/// 是否推荐。
|
||||
/// </summary>
|
||||
public bool IsFeatured { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -111,4 +111,9 @@ public sealed class StoreDto
|
||||
/// 支持配送。
|
||||
/// </summary>
|
||||
public bool SupportsDelivery { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ public sealed class CreateStoreCommandHandler(IStoreRepository storeRepository,
|
||||
DeliveryRadiusKm = store.DeliveryRadiusKm,
|
||||
SupportsDineIn = store.SupportsDineIn,
|
||||
SupportsPickup = store.SupportsPickup,
|
||||
SupportsDelivery = store.SupportsDelivery
|
||||
SupportsDelivery = store.SupportsDelivery,
|
||||
CreatedAt = store.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public sealed class GetStoreByIdQueryHandler(
|
||||
DeliveryRadiusKm = store.DeliveryRadiusKm,
|
||||
SupportsDineIn = store.SupportsDineIn,
|
||||
SupportsPickup = store.SupportsPickup,
|
||||
SupportsDelivery = store.SupportsDelivery
|
||||
SupportsDelivery = store.SupportsDelivery,
|
||||
CreatedAt = store.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,9 +28,27 @@ public sealed class SearchStoresQueryHandler(
|
||||
stores = stores.Where(x => x.MerchantId == request.MerchantId.Value).ToList();
|
||||
}
|
||||
|
||||
return stores
|
||||
.Select(MapToDto)
|
||||
var sorted = ApplySorting(stores, 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.Stores.Entities.Store> ApplySorting(
|
||||
IReadOnlyCollection<Domain.Stores.Entities.Store> stores,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => sortDescending ? stores.OrderByDescending(x => x.Name) : stores.OrderBy(x => x.Name),
|
||||
"code" => sortDescending ? stores.OrderByDescending(x => x.Code) : stores.OrderBy(x => x.Code),
|
||||
"status" => sortDescending ? stores.OrderByDescending(x => x.Status) : stores.OrderBy(x => x.Status),
|
||||
_ => sortDescending ? stores.OrderByDescending(x => x.CreatedAt) : stores.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
private static StoreDto MapToDto(Domain.Stores.Entities.Store store) => new()
|
||||
@@ -54,6 +72,7 @@ public sealed class SearchStoresQueryHandler(
|
||||
DeliveryRadiusKm = store.DeliveryRadiusKm,
|
||||
SupportsDineIn = store.SupportsDineIn,
|
||||
SupportsPickup = store.SupportsPickup,
|
||||
SupportsDelivery = store.SupportsDelivery
|
||||
SupportsDelivery = store.SupportsDelivery,
|
||||
CreatedAt = store.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ public sealed class UpdateStoreCommandHandler(
|
||||
DeliveryRadiusKm = store.DeliveryRadiusKm,
|
||||
SupportsDineIn = store.SupportsDineIn,
|
||||
SupportsPickup = store.SupportsPickup,
|
||||
SupportsDelivery = store.SupportsDelivery
|
||||
SupportsDelivery = store.SupportsDelivery,
|
||||
CreatedAt = store.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,4 +18,24 @@ public sealed class SearchStoresQuery : IRequest<IReadOnlyList<StoreDto>>
|
||||
/// 状态过滤。
|
||||
/// </summary>
|
||||
public StoreStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(name/code/status/createdAt)。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否倒序。
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店命令验证器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreCommandValidator : AbstractValidator<CreateStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public CreateStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.MerchantId).GreaterThan(0);
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(32);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.Phone).MaximumLength(32);
|
||||
RuleFor(x => x.ManagerName).MaximumLength(64);
|
||||
RuleFor(x => x.Province).MaximumLength(64);
|
||||
RuleFor(x => x.City).MaximumLength(64);
|
||||
RuleFor(x => x.District).MaximumLength(64);
|
||||
RuleFor(x => x.Address).MaximumLength(256);
|
||||
RuleFor(x => x.Announcement).MaximumLength(512);
|
||||
RuleFor(x => x.Tags).MaximumLength(256);
|
||||
RuleFor(x => x.DeliveryRadiusKm).GreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 门店列表查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchStoresQueryValidator : AbstractValidator<SearchStoresQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchStoresQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
RuleFor(x => x.SortBy).MaximumLength(64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店命令验证器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreCommandValidator : AbstractValidator<UpdateStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public UpdateStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.MerchantId).GreaterThan(0);
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(32);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.Phone).MaximumLength(32);
|
||||
RuleFor(x => x.ManagerName).MaximumLength(64);
|
||||
RuleFor(x => x.Province).MaximumLength(64);
|
||||
RuleFor(x => x.City).MaximumLength(64);
|
||||
RuleFor(x => x.District).MaximumLength(64);
|
||||
RuleFor(x => x.Address).MaximumLength(256);
|
||||
RuleFor(x => x.Announcement).MaximumLength(512);
|
||||
RuleFor(x => x.Tags).MaximumLength(256);
|
||||
RuleFor(x => x.DeliveryRadiusKm).GreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user