Files
FrontOffice/src/FrontOffice.Main/Utilities/ProductService.cs
T
masoodafar-web 37c7cebf92
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 23m11s
feat: update product service to support in-stock filtering and enhance top-selling product retrieval
- Modified `GetTopSellingAsync` methods in `ProductService` and `DiscountProductService` to include an optional `inStock` parameter for filtering products based on availability.
- Introduced a constant for the top product count in `Index.razor.cs` to improve maintainability and readability.
- Updated the product retrieval logic in `Index.razor.cs` to utilize the new in-stock filtering feature.

These changes enhance the product listing functionality by allowing users to view only in-stock top-selling products, improving the overall shopping experience.
2026-06-30 00:17:30 +03:30

314 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CMSMicroservice.Protobuf.Protos.Products;
using Google.Protobuf.WellKnownTypes;
namespace FrontOffice.Main.Utilities;
public record Product(
long Id,
string Title,
string Description,
string FullInformation,
string ImageUrl,
long Price,
int Discount = 1,
int Rate = 0,
int RemainingCount = 0)
{
public IReadOnlyList<ProductGalleryImage> Gallery { get; init; }
= [];
public IReadOnlyList<ProductCategoryPathInfo> Categories { get; init; }
= [];
}
public record ProductListResult(
List<Product> Products,
int TotalCount,
int TotalPages,
int CurrentPage,
bool HasNext);
public record ProductGalleryImage(
long ProductGalleryId,
long ProductImageId,
string Title,
string ImageUrl,
string ThumbnailUrl);
public record ProductCategoryNodeInfo(
long Id,
string Title,
long? ParentId);
public record ProductCategoryPathInfo(
long CategoryId,
string Title,
IReadOnlyList<ProductCategoryNodeInfo> Nodes)
{
public string DisplayLabel => string.Join(" ", Nodes.Select(node => node.Title));
public ProductCategoryNodeInfo Leaf => Nodes.Last();
}
public class ProductService
{
private readonly ConcurrentDictionary<long, CacheEntry> _cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(1);
private readonly CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient _client;
public ProductService(CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient client)
{
_client = client;
}
public async Task<List<Product>> GetProductsAsync(string? query = null, long? categoryId = null, string? sortBy = null)
{
var result = await GetProductsPagedAsync(query: query, categoryId: categoryId, sortBy: sortBy, page: 1, pageSize: 500);
return result.Products;
}
public Task<ProductListResult> GetTopSellingAsync(int count = 6, bool? inStock = null)
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count, inStock: inStock);
public async Task<ProductListResult> GetProductsPagedAsync(
string? query = null, long? categoryId = null, string? sortBy = null,
int page = 1, int pageSize = 12, bool? inStock = null)
{
try
{
var request = new GetAllProductsByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = page,
PageSize = pageSize
},
Filter = new GetAllProductsByFilterFilter()
};
// فقط Title — CMS فیلترها را AND می‌زند؛ ارسال query در همه فیلدها جستجو را می‌شکند
if (!string.IsNullOrWhiteSpace(query))
{
request.Filter.Title = query.Trim();
}
if (categoryId is { } value)
{
request.Filter.CategoryId = value;
}
if (inStock.HasValue)
{
request.Filter.InStock = inStock.Value;
}
if (!string.IsNullOrEmpty(sortBy))
{
request.SortBy = sortBy;
}
var resp = await _client.GetAllProductsByFilterAsync(request);
var products = MapAndCache(resp.Models);
var totalCount = (int)(resp.MetaData?.TotalCount ?? 0);
var totalPages = (int)(resp.MetaData?.TotalPage ?? 0);
var hasNext = resp.MetaData?.HasNext ?? false;
return new ProductListResult(products, totalCount, totalPages, page, hasNext);
}
catch
{
IEnumerable<Product> list = GetValidCachedProducts();
if (!string.IsNullOrWhiteSpace(query))
{
var q = query.Trim();
list = list.Where(p =>
p.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
p.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
}
var all = list.OrderBy(p => p.Id).ToList();
return new ProductListResult(all, all.Count, 1, 1, false);
}
}
public async Task<Product?> GetByIdAsync(long id)
{
if (TryGetCachedProduct(id, out var cached) && HasDetailedData(cached))
{
return cached;
}
try
{
var resp = await _client.GetProductsAsync(new GetProductsRequest { Id = id });
if (resp == null)
{
return null;
}
return MapAndCache(resp);
}
catch
{
if (cached is not null)
{
return cached;
}
TryGetCachedProduct(id, out var result);
return result;
}
}
private List<Product> MapAndCache(
Google.Protobuf.Collections.RepeatedField<GetAllProductsByFilterResponseModel> models)
{
var list = new List<Product>();
foreach (var m in models)
{
var p = new Product(
Id: m.Id,
Title: m.Title ?? string.Empty,
Description: m.Description ?? string.Empty,
FullInformation: m.FullInformation ?? string.Empty,
ImageUrl: m.ImagePath,
Price: m.Price,
Discount: m.Discount,
Rate: m.Rate,
RemainingCount: m.RemainingCount
);
p = PreserveCachedDetails(p);
CacheProduct(p);
list.Add(p);
}
return list;
}
private Product PreserveCachedDetails(Product product)
{
if (_cache.TryGetValue(product.Id, out var entry))
{
var cached = entry.Product;
var hasGallery = cached.Gallery.Count > 0;
var hasCategories = cached.Categories.Count > 0;
if (hasGallery || hasCategories)
{
product = product with
{
Gallery = hasGallery ? cached.Gallery : product.Gallery,
Categories = hasCategories ? cached.Categories : product.Categories
};
}
}
return product;
}
private Product MapAndCache(GetProductsResponse model)
{
var gallery = model.Gallery
.Select(item => new ProductGalleryImage(
ProductGalleryId: item.ProductGalleryId,
ProductImageId: item.ProductImageId,
Title: item.Title ?? string.Empty,
ImageUrl: BuildUrl(item.ImagePath),
ThumbnailUrl: BuildUrl(item.ImageThumbnailPath)))
.ToList();
var product = new Product(
Id: model.Id,
Title: model.Title ?? string.Empty,
Description: model.Description ?? string.Empty,
FullInformation: model.FullInformation ?? string.Empty,
ImageUrl: BuildUrl(model.ImagePath),
Price: model.Price,
Discount: model.Discount,
Rate: model.Rate,
RemainingCount: model.RemainingCount)
{
Gallery = gallery,
Categories = MapCategoryPaths(model.Categories)
};
CacheProduct(product);
return product;
}
private void CacheProduct(Product product)
{
var entry = new CacheEntry(product, DateTime.UtcNow.Add(CacheDuration));
_cache.AddOrUpdate(product.Id, entry, (_, _) => entry);
}
private bool TryGetCachedProduct(long id, [NotNullWhen(true)] out Product? product)
{
if (_cache.TryGetValue(id, out var entry))
{
if (entry.Expiration > DateTime.UtcNow)
{
product = entry.Product;
return true;
}
_cache.TryRemove(id, out _);
}
product = null;
return false;
}
private IEnumerable<Product> GetValidCachedProducts()
{
var now = DateTime.UtcNow;
return _cache.Values
.Where(entry => entry.Expiration > now)
.Select(entry => entry.Product);
}
private static bool HasDetailedData(Product product)
=> product.Gallery.Count > 0 || product.Categories.Count > 0;
private sealed record CacheEntry(Product Product, DateTime Expiration);
private static string BuildUrl(string? path) =>
path ?? string.Empty;
private static IReadOnlyList<ProductCategoryPathInfo> MapCategoryPaths(IEnumerable<ProductCategoryPath>? categories)
{
if (categories is null)
{
return Array.Empty<ProductCategoryPathInfo>();
}
var result = new List<ProductCategoryPathInfo>();
foreach (var category in categories)
{
var nodes = category.Path
.Select(node => new ProductCategoryNodeInfo(
Id: node.Id,
Title: node.Title ?? string.Empty,
ParentId: node.ParentId
))
.ToList();
if (nodes.Count == 0)
{
continue;
}
result.Add(new ProductCategoryPathInfo(
CategoryId: category.CategoryId,
Title: category.Title ?? string.Empty,
Nodes: nodes));
}
return result;
}
}