Add category service and product category support

This commit is contained in:
masoodafar-web
2025-11-28 11:00:59 +03:30
parent f8ad0a6845
commit 518285531a
16 changed files with 345 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories;
public sealed record GetAllCategoriesQuery : IRequest<GetAllCategoriesResponseDto>;

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CMSMicroservice.Protobuf.Protos.Category;
using FrontOffice.BFF.Application.Common.Interfaces;
using MediatR;
namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories;
public class GetAllCategoriesQueryHandler : IRequestHandler<GetAllCategoriesQuery, GetAllCategoriesResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllCategoriesQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllCategoriesResponseDto> Handle(GetAllCategoriesQuery request,
CancellationToken cancellationToken)
{
var response = await _context.Category.GetAllCategoryByFilterAsync(new GetAllCategoryByFilterRequest
{
Filter = new GetAllCategoryByFilterFilter(),
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 1000
}
}, cancellationToken: cancellationToken);
var categories = response.Models?
.Select(model => new CategoryDto
{
Id = model.Id,
Title = model.Title ?? string.Empty,
ParentId = model.ParentId,
ImagePath = model.ImagePath,
IsActive = model.IsActive
})
.ToList() ?? new List<CategoryDto>();
return new GetAllCategoriesResponseDto
{
Categories = categories
};
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories;
public class GetAllCategoriesResponseDto
{
public List<CategoryDto> Categories { get; set; } = new();
}
public class CategoryDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public long? ParentId { get; set; }
public string? ImagePath { get; set; }
public bool IsActive { get; set; }
}

View File

@@ -1,5 +1,7 @@
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.OtpToken;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductImages;
@@ -27,6 +29,8 @@ public interface IApplicationContractContext
ProductsContract.ProductsContractClient Product { get; }
ProductGallerysContract.ProductGallerysContractClient ProductGallerys { get; }
ProductImagesContract.ProductImagesContractClient ProductImages { get; }
CategoryContract.CategoryContractClient Category { get; }
PruductCategoryContract.PruductCategoryContractClient ProductCategory { get; }
UserCartsContract.UserCartsContractClient UserCart { get; }
UserContract.UserContractClient User { get; }
UserContractContract.UserContractContractClient UserContract { get; }

View File

@@ -26,4 +26,6 @@ public record GetAllProductsByFilterQuery : IRequest<GetAllProductsByFilterRespo
public int? Discount { get; set; }
//
public int? Rate { get; set; }
// فیلتر بر اساس دسته‌بندی
public long? CategoryId { get; set; }
}

View File

@@ -1,5 +1,11 @@
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Products;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FrontOffice.BFF.Application.Common.Interfaces;
using Mapster;
using MediatR;
using CmsPaginationState = CMSMicroservice.Protobuf.Protos.PaginationState;
using CmsProductsProtos = CMSMicroservice.Protobuf.Protos.Products;
namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetAllProductsByFilter;
@@ -16,8 +22,50 @@ public class
public async Task<GetAllProductsByFilterResponseDto> Handle(GetAllProductsByFilterQuery request,
CancellationToken cancellationToken)
{
var result = await _context.Product.GetAllProductsByFilterAsync(request.Adapt<GetAllProductsByFilterRequest>(),
var grpcRequest = new CmsProductsProtos.GetAllProductsByFilterRequest
{
PaginationState = request.PaginationState is { } pagination
? new CmsPaginationState
{
PageNumber = pagination.PageNumber,
PageSize = pagination.PageSize
}
: null,
SortBy = request.SortBy,
Filter = BuildFilter(request.Filter)
};
var result = await _context.Product.GetAllProductsByFilterAsync(grpcRequest,
cancellationToken: cancellationToken);
if (request.Filter?.CategoryId is { } categoryId)
{
var matchingModels = result.Models
.Where(model => model.CategoryIds.Contains(categoryId))
.ToList();
result.Models.Clear();
result.Models.AddRange(matchingModels);
}
return result.Adapt<GetAllProductsByFilterResponseDto>();
}
private static CmsProductsProtos.GetAllProductsByFilterFilter? BuildFilter(GetAllProductsByFilterFilter? filter)
{
if (filter is null)
{
return null;
}
return new CmsProductsProtos.GetAllProductsByFilterFilter
{
Id = filter.Id,
Title = filter.Title,
Description = filter.Description,
ShortInfomation = filter.ShortInfomation,
FullInformation = filter.FullInformation,
Price = filter.Price,
Discount = filter.Discount,
Rate = filter.Rate
};
}
}

View File

@@ -1,6 +1,10 @@
using CMSMicroservice.Protobuf.Protos.Products;
using System.Collections.Generic;
using System.Linq;
using CMSCategory = CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductImages;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory;
namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetProducts;
public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProductsResponseDto>
@@ -57,6 +61,94 @@ public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProd
}
}
await PopulateCategoryPathsAsync(dto, request.Id, cancellationToken);
return dto;
}
private async Task PopulateCategoryPathsAsync(GetProductsResponseDto dto, long productId, CancellationToken cancellationToken)
{
var categoryRequest = new CMSCategory.GetAllCategoryByFilterRequest
{
Filter = new CMSCategory.GetAllCategoryByFilterFilter(),
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 1000
}
};
var categoriesResponse = await _context.Category.GetAllCategoryByFilterAsync(categoryRequest, cancellationToken: cancellationToken);
var categoryLookup = categoriesResponse.Models?
.ToDictionary(c => c.Id) ?? new Dictionary<long, CMSCategory.GetAllCategoryByFilterResponseModel>();
var productCategoryResponse = await _context.ProductCategory.GetAllPruductCategoryByFilterAsync(
new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
{
Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
{
ProductId = productId
},
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 1000
}
}, cancellationToken: cancellationToken);
var productCategoryIds = productCategoryResponse?.Models?
.Select(pc => pc.CategoryId)
.Distinct()
.ToList() ?? new List<long>();
foreach (var categoryId in productCategoryIds)
{
if (!categoryLookup.TryGetValue(categoryId, out var categoryModel))
{
continue;
}
var pathNodes = BuildCategoryPath(categoryModel, categoryLookup);
if (!pathNodes.Any())
{
continue;
}
dto.Categories.Add(new ProductCategoryPathDto
{
CategoryId = categoryModel.Id,
Title = categoryModel.Title ?? string.Empty,
Path = pathNodes
});
}
}
private static List<CategoryNodeDto> BuildCategoryPath(CMSCategory.GetAllCategoryByFilterResponseModel category,
Dictionary<long, CMSCategory.GetAllCategoryByFilterResponseModel> lookup)
{
var path = new List<CategoryNodeDto>();
var visited = new HashSet<long>();
var current = category;
while (current != null && visited.Add(current.Id))
{
path.Add(new CategoryNodeDto
{
Id = current.Id,
Title = current.Title ?? string.Empty,
ParentId = current.ParentId
});
if (current.ParentId is long parentId && lookup.TryGetValue(parentId, out var parent))
{
current = parent;
continue;
}
break;
}
path.Reverse();
return path;
}
}

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetProducts;
public class GetProductsResponseDto
{
@@ -29,6 +31,7 @@ public class GetProductsResponseDto
public int RemainingCount { get; set; }
//
public List<ProductGalleryItemDto> Gallery { get; set; } = new();
public List<ProductCategoryPathDto> Categories { get; set; } = new();
}
@@ -40,4 +43,18 @@ public class ProductGalleryItemDto
public string ImagePath { get; set; } = string.Empty;
public string ImageThumbnailPath { get; set; } = string.Empty;
}
public class ProductCategoryPathDto
{
public long CategoryId { get; set; }
public string Title { get; set; } = string.Empty;
public List<CategoryNodeDto> Path { get; set; } = new();
}
public class CategoryNodeDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public long? ParentId { get; set; }
}

View File

@@ -1,5 +1,7 @@
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.OtpToken;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductImages;
@@ -51,6 +53,8 @@ public class ApplicationContractContext : IApplicationContractContext
public ProductsContract.ProductsContractClient Product => GetService<ProductsContract.ProductsContractClient>();
public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService<ProductGallerysContract.ProductGallerysContractClient>();
public ProductImagesContract.ProductImagesContractClient ProductImages => GetService<ProductImagesContract.ProductImagesContractClient>();
public CategoryContract.CategoryContractClient Category => GetService<CategoryContract.CategoryContractClient>();
public PruductCategoryContract.PruductCategoryContractClient ProductCategory => GetService<PruductCategoryContract.PruductCategoryContractClient>();
public UserCartsContract.UserCartsContractClient UserCart => GetService<UserCartsContract.UserCartsContractClient>();
public UserContract.UserContractClient User => GetService<UserContract.UserContractClient>();

View File

@@ -21,6 +21,7 @@
<ProjectReference Include="..\FrontOffice.BFF.Application\FrontOffice.BFF.Application.csproj" />
<ProjectReference Include="..\FrontOffice.BFF.Infrastructure\FrontOffice.BFF.Infrastructure.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Products.Protobuf\FrontOffice.BFF.Products.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.ShopingCart.Protobuf\FrontOffice.BFF.ShopingCart.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Transaction.Protobuf\FrontOffice.BFF.Transaction.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />

View File

@@ -0,0 +1,20 @@
using FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories;
using FrontOffice.BFF.Category.Protobuf.Protos.Category;
using FrontOffice.BFF.WebApi.Common.Services;
namespace FrontOffice.BFF.WebApi.Services;
public class CategoriesService : CategoryContract.CategoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public CategoriesService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<GetCategoriesResponse> GetAllCategories(GetCategoriesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetCategoriesRequest, GetAllCategoriesQuery, GetCategoriesResponse>(request, context);
}
}

View File

@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrontOffice.BFF.Transaction
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.Products.Protobuf", "Protobufs\FrontOffice.BFF.Products.Protobuf\FrontOffice.BFF.Products.Protobuf.csproj", "{CB77669F-5B48-4AC6-B20E-A928660E93F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.Category.Protobuf", "Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj", "{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.ShopingCart.Protobuf", "Protobufs\FrontOffice.BFF.ShopingCart.Protobuf\FrontOffice.BFF.ShopingCart.Protobuf.csproj", "{DC61324B-D389-4A1D-B048-D0AA43A6BBE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.UserWallet.Protobuf", "Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj", "{03F99CE9-F952-47B0-B71A-1F4865E52443}"
@@ -82,6 +84,10 @@ Global
{03F99CE9-F952-47B0-B71A-1F4865E52443}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03F99CE9-F952-47B0-B71A-1F4865E52443}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03F99CE9-F952-47B0-B71A-1F4865E52443}.Release|Any CPU.Build.0 = Release|Any CPU
{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -95,5 +101,6 @@ Global
{CB77669F-5B48-4AC6-B20E-A928660E93F8} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{DC61324B-D389-4A1D-B048-D0AA43A6BBE7} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{03F99CE9-F952-47B0-B71A-1F4865E52443} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1</Version>
<PackageId>Foursat.FrontOffice.BFF.Category.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols>
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Grpc.Core.Api" Version="2.54.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0" />
<PackageReference Include="Grpc.Tools" Version="2.55.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\category.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
syntax = "proto3";
package category;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "FrontOffice.BFF.Category.Protobuf.Protos.Category";
service CategoryContract {
rpc GetAllCategories(GetCategoriesRequest) returns (GetCategoriesResponse);
}
message GetCategoriesRequest {
google.protobuf.Int64Value parent_id = 1;
}
message GetCategoriesResponse {
repeated CategoryDto categories = 1;
}
message CategoryDto {
int64 id = 1;
string name = 2;
string title = 3;
google.protobuf.StringValue description = 4;
google.protobuf.StringValue image_path = 5;
google.protobuf.Int64Value parent_id = 6;
bool is_active = 7;
int32 sort_order = 8;
}

View File

@@ -3,7 +3,7 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.12</Version>
<Version>0.0.13</Version>
<PackageId>Foursat.FrontOffice.BFF.Products.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols>

View File

@@ -45,6 +45,7 @@ message GetProductsResponse
int32 view_count = 12;
int32 remaining_count = 13;
repeated ProductGalleryItem gallery = 14;
repeated ProductCategoryPath categories = 15;
}
message GetAllProductsByFilterRequest
{
@@ -62,6 +63,7 @@ message GetAllProductsByFilterFilter
google.protobuf.Int64Value price = 6;
google.protobuf.Int32Value discount = 7;
google.protobuf.Int32Value rate = 8;
google.protobuf.Int64Value category_id = 9;
}
message GetAllProductsByFilterResponse
{
@@ -83,6 +85,7 @@ message GetAllProductsByFilterResponseModel
int32 sale_count = 11;
int32 view_count = 12;
int32 remaining_count = 13;
repeated ProductCategoryPath categories = 14;
}
message ProductGalleryItem
@@ -94,6 +97,20 @@ message ProductGalleryItem
string image_thumbnail_path = 5;
}
message ProductCategoryPath
{
int64 category_id = 1;
string title = 2;
repeated CategoryNode path = 3;
}
message CategoryNode
{
int64 id = 1;
string title = 2;
google.protobuf.Int64Value parent_id = 3;
}
message PaginationState
{
int32 page_number = 1;