feat: implement tenant product category management APIs
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 44s
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 44s
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 分类绑定商品命令。
|
||||
/// </summary>
|
||||
public sealed class BindCategoryProductsCommand : IRequest<ProductCategoryProductBindResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品 ID 列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<long> ProductIds { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 变更分类状态命令。
|
||||
/// </summary>
|
||||
public sealed class ChangeProductCategoryStatusCommand : IRequest<ProductCategoryManageItemDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "enabled";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除分类命令。
|
||||
/// </summary>
|
||||
public sealed class DeleteProductCategoryCommand : IRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long CategoryId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 保存分类命令。
|
||||
/// </summary>
|
||||
public sealed class SaveProductCategoryCommand : IRequest<ProductCategoryManageItemDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID(编辑时传)。
|
||||
/// </summary>
|
||||
public long? CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 分类描述。
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图标地址。
|
||||
/// </summary>
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Channels { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 排序。
|
||||
/// </summary>
|
||||
public int Sort { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "enabled";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 分类排序项。
|
||||
/// </summary>
|
||||
public sealed class SortProductCategoryItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int Sort { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类排序命令。
|
||||
/// </summary>
|
||||
public sealed class SortProductCategoryCommand : IRequest<IReadOnlyList<ProductCategoryManageItemDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序项。
|
||||
/// </summary>
|
||||
public IReadOnlyList<SortProductCategoryItem> Items { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 分类解绑商品命令。
|
||||
/// </summary>
|
||||
public sealed class UnbindCategoryProductCommand : IRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品 ID。
|
||||
/// </summary>
|
||||
public long ProductId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 分类列表项。
|
||||
/// </summary>
|
||||
public class ProductCategoryListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商品数量。
|
||||
/// </summary>
|
||||
public int ProductCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int Sort { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 分类管理列表项。
|
||||
/// </summary>
|
||||
public sealed class ProductCategoryManageItemDto : ProductCategoryListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类描述。
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图标地址。
|
||||
/// </summary>
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "enabled";
|
||||
|
||||
/// <summary>
|
||||
/// 销售渠道。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Channels { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 分类绑定商品结果。
|
||||
/// </summary>
|
||||
public sealed class ProductCategoryProductBindResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 成功条数。
|
||||
/// </summary>
|
||||
public int SuccessCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败条数。
|
||||
/// </summary>
|
||||
public int FailedCount { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 商品选择器项。
|
||||
/// </summary>
|
||||
public sealed class ProductPickerItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 商品 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称。
|
||||
/// </summary>
|
||||
public string CategoryName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 价格。
|
||||
/// </summary>
|
||||
public decimal Price { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SPU 编码。
|
||||
/// </summary>
|
||||
public string SpuCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态(on_sale/off_shelf/sold_out)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "off_shelf";
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类绑定商品命令处理器。
|
||||
/// </summary>
|
||||
public sealed class BindCategoryProductsCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<BindCategoryProductsCommand, ProductCategoryProductBindResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ProductCategoryProductBindResultDto> Handle(BindCategoryProductsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var category = await productRepository.FindCategoryByIdAsync(request.CategoryId, tenantId, cancellationToken);
|
||||
if (category is null || category.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "分类不存在");
|
||||
}
|
||||
|
||||
var distinctProductIds = request.ProductIds
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (distinctProductIds.Count == 0)
|
||||
{
|
||||
return new ProductCategoryProductBindResultDto
|
||||
{
|
||||
TotalCount = 0,
|
||||
SuccessCount = 0,
|
||||
FailedCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
var successCount = await productRepository.BatchUpdateProductCategoryAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.CategoryId,
|
||||
distinctProductIds,
|
||||
cancellationToken);
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new ProductCategoryProductBindResultDto
|
||||
{
|
||||
TotalCount = distinctProductIds.Count,
|
||||
SuccessCount = successCount,
|
||||
FailedCount = Math.Max(0, distinctProductIds.Count - successCount)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类状态变更处理器。
|
||||
/// </summary>
|
||||
public sealed class ChangeProductCategoryStatusCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ChangeProductCategoryStatusCommand, ProductCategoryManageItemDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ProductCategoryManageItemDto> Handle(ChangeProductCategoryStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ProductCategoryManageMapping.TryParseStatus(request.Status, out var isEnabled))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var category = await productRepository.FindCategoryByIdAsync(request.CategoryId, tenantId, cancellationToken);
|
||||
if (category is null || category.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "分类不存在");
|
||||
}
|
||||
|
||||
category.IsEnabled = isEnabled;
|
||||
await productRepository.UpdateCategoryAsync(category, cancellationToken);
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var productCounts = await productRepository.CountProductsByCategoryIdsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[category.Id],
|
||||
cancellationToken);
|
||||
|
||||
return new ProductCategoryManageItemDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Sort = category.SortOrder,
|
||||
ProductCount = productCounts.GetValueOrDefault(category.Id),
|
||||
Description = category.Description ?? string.Empty,
|
||||
Icon = category.Icon ?? string.Empty,
|
||||
Status = ProductCategoryManageMapping.ToStatusText(category.IsEnabled),
|
||||
Channels = ProductCategoryManageMapping.DeserializeChannels(category.ChannelsJson)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除分类命令处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteProductCategoryCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<DeleteProductCategoryCommand>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Handle(DeleteProductCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var category = await productRepository.FindCategoryByIdAsync(request.CategoryId, tenantId, cancellationToken);
|
||||
if (category is null || category.StoreId != request.StoreId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var categoryCounts = await productRepository.CountProductsByCategoryIdsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[request.CategoryId],
|
||||
cancellationToken);
|
||||
if (categoryCounts.GetValueOrDefault(request.CategoryId) > 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "分类下仍有商品,不能删除");
|
||||
}
|
||||
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
if (categories.Count <= 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "至少保留一个分类");
|
||||
}
|
||||
|
||||
await productRepository.DeleteCategoryAsync(request.CategoryId, tenantId, cancellationToken);
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryListQueryHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetProductCategoryListQuery, IReadOnlyList<ProductCategoryListItemDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductCategoryListItemDto>> Handle(GetProductCategoryListQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
if (categories.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var categoryIds = categories.Select(x => x.Id).ToList();
|
||||
var productCounts = await productRepository.CountProductsByCategoryIdsAsync(tenantId, request.StoreId, categoryIds, cancellationToken);
|
||||
return categories
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Id)
|
||||
.Select(category => new ProductCategoryListItemDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Sort = category.SortOrder,
|
||||
ProductCount = productCounts.GetValueOrDefault(category.Id)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
using TakeoutSaaS.Domain.Products.Entities;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类管理列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryManageListQueryHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetProductCategoryManageListQuery, IReadOnlyList<ProductCategoryManageItemDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductCategoryManageItemDto>> Handle(GetProductCategoryManageListQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
if (categories.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
IEnumerable<ProductCategory> filtered = categories;
|
||||
|
||||
if (ProductCategoryManageMapping.TryParseStatus(request.Status, out var parsedStatus))
|
||||
{
|
||||
filtered = filtered.Where(item => item.IsEnabled == parsedStatus);
|
||||
}
|
||||
|
||||
var normalizedKeyword = request.Keyword?.Trim().ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||
{
|
||||
filtered = filtered.Where(item =>
|
||||
item.Name.ToLower().Contains(normalizedKeyword) ||
|
||||
(item.Description != null && item.Description.ToLower().Contains(normalizedKeyword)));
|
||||
}
|
||||
|
||||
var filteredList = filtered
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.Id)
|
||||
.ToList();
|
||||
if (filteredList.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var productCounts = await productRepository.CountProductsByCategoryIdsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
filteredList.Select(item => item.Id).ToList(),
|
||||
cancellationToken);
|
||||
|
||||
return filteredList
|
||||
.Select(category => new ProductCategoryManageItemDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Sort = category.SortOrder,
|
||||
ProductCount = productCounts.GetValueOrDefault(category.Id),
|
||||
Description = category.Description ?? string.Empty,
|
||||
Icon = category.Icon ?? string.Empty,
|
||||
Status = ProductCategoryManageMapping.ToStatusText(category.IsEnabled),
|
||||
Channels = ProductCategoryManageMapping.DeserializeChannels(category.ChannelsJson)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Domain.Products.Entities;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 保存分类命令处理器。
|
||||
/// </summary>
|
||||
public sealed class SaveProductCategoryCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SaveProductCategoryCommand, ProductCategoryManageItemDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ProductCategoryManageItemDto> Handle(SaveProductCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var normalizedName = request.Name.Trim();
|
||||
var isDuplicate = await productRepository.ExistsCategoryNameAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
normalizedName,
|
||||
request.CategoryId,
|
||||
cancellationToken);
|
||||
if (isDuplicate)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "分类名称已存在");
|
||||
}
|
||||
|
||||
if (!ProductCategoryManageMapping.TryParseStatus(request.Status, out var isEnabled))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法");
|
||||
}
|
||||
|
||||
var normalizedChannels = ProductCategoryManageMapping.NormalizeChannels(request.Channels);
|
||||
if (normalizedChannels.Count == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "请至少选择一个销售渠道");
|
||||
}
|
||||
|
||||
ProductCategory category;
|
||||
if (request.CategoryId.HasValue)
|
||||
{
|
||||
var existing = await productRepository.FindCategoryByIdAsync(request.CategoryId.Value, tenantId, cancellationToken);
|
||||
if (existing is null || existing.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "分类不存在");
|
||||
}
|
||||
|
||||
category = existing;
|
||||
category.Name = normalizedName;
|
||||
category.Description = request.Description.Trim();
|
||||
category.Icon = request.Icon.Trim();
|
||||
category.SortOrder = Math.Max(1, request.Sort);
|
||||
category.IsEnabled = isEnabled;
|
||||
category.ChannelsJson = ProductCategoryManageMapping.SerializeChannels(normalizedChannels);
|
||||
await productRepository.UpdateCategoryAsync(category, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
category = new ProductCategory
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
Name = normalizedName,
|
||||
Description = request.Description.Trim(),
|
||||
Icon = request.Icon.Trim(),
|
||||
SortOrder = Math.Max(1, request.Sort),
|
||||
IsEnabled = isEnabled,
|
||||
ChannelsJson = ProductCategoryManageMapping.SerializeChannels(normalizedChannels)
|
||||
};
|
||||
await productRepository.AddCategoryAsync(category, cancellationToken);
|
||||
}
|
||||
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var productCounts = await productRepository.CountProductsByCategoryIdsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[category.Id],
|
||||
cancellationToken);
|
||||
|
||||
return new ProductCategoryManageItemDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
ProductCount = productCounts.GetValueOrDefault(category.Id),
|
||||
Sort = category.SortOrder,
|
||||
Description = category.Description ?? string.Empty,
|
||||
Icon = category.Icon ?? string.Empty,
|
||||
Status = ProductCategoryManageMapping.ToStatusText(category.IsEnabled),
|
||||
Channels = ProductCategoryManageMapping.DeserializeChannels(category.ChannelsJson)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
using TakeoutSaaS.Domain.Products.Enums;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 商品选择器查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchProductPickerQueryHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchProductPickerQuery, IReadOnlyList<ProductPickerItemDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductPickerItemDto>> Handle(SearchProductPickerQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
var categoryNameLookup = categories.ToDictionary(category => category.Id, category => category.Name);
|
||||
|
||||
var products = await productRepository.SearchPickerAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.CategoryId,
|
||||
request.Keyword,
|
||||
request.Limit,
|
||||
cancellationToken);
|
||||
|
||||
return products
|
||||
.Select(product => new ProductPickerItemDto
|
||||
{
|
||||
Id = product.Id,
|
||||
CategoryId = product.CategoryId,
|
||||
CategoryName = categoryNameLookup.GetValueOrDefault(product.CategoryId, string.Empty),
|
||||
Name = product.Name,
|
||||
Price = product.Price,
|
||||
SpuCode = product.SpuCode,
|
||||
Status = ToPickerStatus(product.Status)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string ToPickerStatus(ProductStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
ProductStatus.OnSale => "on_sale",
|
||||
ProductStatus.Archived => "sold_out",
|
||||
_ => "off_shelf"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类排序命令处理器。
|
||||
/// </summary>
|
||||
public sealed class SortProductCategoryCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SortProductCategoryCommand, IReadOnlyList<ProductCategoryManageItemDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductCategoryManageItemDto>> Handle(SortProductCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
if (categories.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var sortMap = request.Items
|
||||
.Where(item => item.Sort > 0)
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(group => group.Key, group => group.Last().Sort);
|
||||
|
||||
var sorted = categories
|
||||
.Select(category => new
|
||||
{
|
||||
Category = category,
|
||||
Sort = sortMap.GetValueOrDefault(category.Id, category.SortOrder)
|
||||
})
|
||||
.OrderBy(item => item.Sort)
|
||||
.ThenBy(item => item.Category.SortOrder)
|
||||
.ThenBy(item => item.Category.Id)
|
||||
.Select((item, index) =>
|
||||
{
|
||||
item.Category.SortOrder = index + 1;
|
||||
return item.Category;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var category in sorted)
|
||||
{
|
||||
await productRepository.UpdateCategoryAsync(category, cancellationToken);
|
||||
}
|
||||
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var productCounts = await productRepository.CountProductsByCategoryIdsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
sorted.Select(item => item.Id).ToList(),
|
||||
cancellationToken);
|
||||
|
||||
return sorted
|
||||
.Select(category => new ProductCategoryManageItemDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Sort = category.SortOrder,
|
||||
ProductCount = productCounts.GetValueOrDefault(category.Id),
|
||||
Description = category.Description ?? string.Empty,
|
||||
Icon = category.Icon ?? string.Empty,
|
||||
Status = ProductCategoryManageMapping.ToStatusText(category.IsEnabled),
|
||||
Channels = ProductCategoryManageMapping.DeserializeChannels(category.ChannelsJson)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 分类解绑商品命令处理器。
|
||||
/// </summary>
|
||||
public sealed class UnbindCategoryProductCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<UnbindCategoryProductCommand>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Handle(UnbindCategoryProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var category = await productRepository.FindCategoryByIdAsync(request.CategoryId, tenantId, cancellationToken);
|
||||
if (category is null || category.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "分类不存在");
|
||||
}
|
||||
|
||||
var categories = await productRepository.GetCategoriesByStoreAsync(tenantId, request.StoreId, false, cancellationToken);
|
||||
var fallbackCategory = categories
|
||||
.Where(item => item.Id != request.CategoryId)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.Id)
|
||||
.FirstOrDefault();
|
||||
if (fallbackCategory is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "无可用目标分类");
|
||||
}
|
||||
|
||||
var moved = await productRepository.MoveProductToCategoryAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.CategoryId,
|
||||
fallbackCategory.Id,
|
||||
request.ProductId,
|
||||
cancellationToken);
|
||||
if (!moved)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "商品不存在");
|
||||
}
|
||||
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products;
|
||||
|
||||
/// <summary>
|
||||
/// 分类管理映射辅助。
|
||||
/// </summary>
|
||||
internal static class ProductCategoryManageMapping
|
||||
{
|
||||
private static readonly string[] ChannelOrder = ["wm", "pickup", "dine_in"];
|
||||
private static readonly HashSet<string> AllowedChannels = [.. ChannelOrder];
|
||||
|
||||
/// <summary>
|
||||
/// 解析状态字符串。
|
||||
/// </summary>
|
||||
public static bool TryParseStatus(string? status, out bool isEnabled)
|
||||
{
|
||||
var normalized = NormalizeStatus(status);
|
||||
if (normalized is null)
|
||||
{
|
||||
isEnabled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
isEnabled = normalized == "enabled";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态转字符串。
|
||||
/// </summary>
|
||||
public static string ToStatusText(bool isEnabled)
|
||||
{
|
||||
return isEnabled ? "enabled" : "disabled";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道集合标准化。
|
||||
/// </summary>
|
||||
public static List<string> NormalizeChannels(IEnumerable<string>? channels)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
(channels ?? [])
|
||||
.Select(channel => channel?.Trim().ToLowerInvariant() ?? string.Empty)
|
||||
.Where(channel => AllowedChannels.Contains(channel)));
|
||||
|
||||
return ChannelOrder
|
||||
.Where(normalized.Contains)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道集合序列化。
|
||||
/// </summary>
|
||||
public static string SerializeChannels(IEnumerable<string>? channels)
|
||||
{
|
||||
var normalized = NormalizeChannels(channels);
|
||||
if (normalized.Count == 0)
|
||||
{
|
||||
normalized = ["wm"];
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(normalized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道集合反序列化。
|
||||
/// </summary>
|
||||
public static List<string> DeserializeChannels(string? channelsJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channelsJson))
|
||||
{
|
||||
return ["wm"];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<List<string>>(channelsJson);
|
||||
var normalized = NormalizeChannels(parsed);
|
||||
return normalized.Count > 0 ? normalized : ["wm"];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ["wm"];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断状态值是否合法。
|
||||
/// </summary>
|
||||
public static bool IsValidStatus(string? status)
|
||||
{
|
||||
return NormalizeStatus(status) is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断渠道值是否合法。
|
||||
/// </summary>
|
||||
public static bool IsValidChannel(string? channel)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return AllowedChannels.Contains(channel.Trim().ToLowerInvariant());
|
||||
}
|
||||
|
||||
private static string? NormalizeStatus(string? status)
|
||||
{
|
||||
var normalized = status?.Trim().ToLowerInvariant();
|
||||
return normalized is "enabled" or "disabled" ? normalized : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询分类列表。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryListQuery : IRequest<IReadOnlyList<ProductCategoryListItemDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询分类管理列表。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryManageListQuery : IRequest<IReadOnlyList<ProductCategoryManageItemDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string? Status { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询商品选择器列表。
|
||||
/// </summary>
|
||||
public sealed class SearchProductPickerQuery : IRequest<IReadOnlyList<ProductPickerItemDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public long? CategoryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
public int Limit { get; init; } = 200;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类绑定商品命令验证器。
|
||||
/// </summary>
|
||||
public sealed class BindCategoryProductsCommandValidator : AbstractValidator<BindCategoryProductsCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public BindCategoryProductsCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0);
|
||||
RuleFor(x => x.ProductIds).NotEmpty();
|
||||
RuleForEach(x => x.ProductIds).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类状态变更命令验证器。
|
||||
/// </summary>
|
||||
public sealed class ChangeProductCategoryStatusCommandValidator : AbstractValidator<ChangeProductCategoryStatusCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ChangeProductCategoryStatusCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0);
|
||||
RuleFor(x => x.Status)
|
||||
.Must(ProductCategoryManageMapping.IsValidStatus)
|
||||
.WithMessage("status 必须为 enabled 或 disabled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 删除分类命令验证器。
|
||||
/// </summary>
|
||||
public sealed class DeleteProductCategoryCommandValidator : AbstractValidator<DeleteProductCategoryCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public DeleteProductCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类列表查询验证器。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryListQueryValidator : AbstractValidator<GetProductCategoryListQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public GetProductCategoryListQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类管理列表查询验证器。
|
||||
/// </summary>
|
||||
public sealed class GetProductCategoryManageListQueryValidator : AbstractValidator<GetProductCategoryManageListQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public GetProductCategoryManageListQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Status)
|
||||
.Must(status => string.IsNullOrWhiteSpace(status) || ProductCategoryManageMapping.IsValidStatus(status))
|
||||
.WithMessage("status 必须为空或 enabled/disabled");
|
||||
RuleFor(x => x.Keyword).MaximumLength(64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 保存分类命令验证器。
|
||||
/// </summary>
|
||||
public sealed class SaveProductCategoryCommandValidator : AbstractValidator<SaveProductCategoryCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SaveProductCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0).When(x => x.CategoryId.HasValue);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||
RuleFor(x => x.Description).MaximumLength(256);
|
||||
RuleFor(x => x.Icon).MaximumLength(512);
|
||||
RuleFor(x => x.Sort).GreaterThan(0);
|
||||
RuleFor(x => x.Status)
|
||||
.Must(ProductCategoryManageMapping.IsValidStatus)
|
||||
.WithMessage("status 必须为 enabled 或 disabled");
|
||||
|
||||
RuleFor(x => x.Channels)
|
||||
.NotEmpty()
|
||||
.WithMessage("channels 至少选择一个渠道");
|
||||
RuleForEach(x => x.Channels)
|
||||
.Must(ProductCategoryManageMapping.IsValidChannel)
|
||||
.WithMessage("channels 包含非法值");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 商品选择器查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchProductPickerQueryValidator : AbstractValidator<SearchProductPickerQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchProductPickerQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0).When(x => x.CategoryId.HasValue);
|
||||
RuleFor(x => x.Keyword).MaximumLength(64);
|
||||
RuleFor(x => x.Limit).InclusiveBetween(1, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类排序命令验证器。
|
||||
/// </summary>
|
||||
public sealed class SortProductCategoryCommandValidator : AbstractValidator<SortProductCategoryCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SortProductCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Items).NotNull();
|
||||
RuleForEach(x => x.Items).SetValidator(new SortProductCategoryItemValidator());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类排序项验证器。
|
||||
/// </summary>
|
||||
public sealed class SortProductCategoryItemValidator : AbstractValidator<SortProductCategoryItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SortProductCategoryItemValidator()
|
||||
{
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0);
|
||||
RuleFor(x => x.Sort).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 分类解绑商品命令验证器。
|
||||
/// </summary>
|
||||
public sealed class UnbindCategoryProductCommandValidator : AbstractValidator<UnbindCategoryProductCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public UnbindCategoryProductCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.CategoryId).GreaterThan(0);
|
||||
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user