eab978188a
- Changed Protobuf imports from FrontOffice.BFF to CMSMicroservice in Profile components (EditAddressDialog, Index, PaymentCallback, Personal, Settings). - Updated CheckoutSummary, OrderDetail, OrderTracking, and Orders pages to use new UserOrder Protobuf definitions. - Refactored WalletService, PackageService, and other utility services to align with new Protobuf structure. - Adjusted appsettings.json for local development URL. - Removed unused validators and simplified validation logic in AuthDialog. - Enhanced ClubMembershipContractDialog to utilize new OtpToken service for OTP handling.
280 lines
8.2 KiB
C#
280 lines
8.2 KiB
C#
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 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)
|
||
{
|
||
try
|
||
{
|
||
var request = new GetAllProductsByFilterRequest
|
||
{
|
||
Filter = new GetAllProductsByFilterFilter
|
||
{
|
||
Title = query ?? string.Empty,
|
||
Description = query ?? string.Empty,
|
||
ShortInfomation = query ?? string.Empty,
|
||
FullInformation = query ?? string.Empty
|
||
}
|
||
};
|
||
|
||
if (categoryId is { } value)
|
||
{
|
||
request.Filter.CategoryId = value;
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(sortBy))
|
||
{
|
||
request.SortBy = sortBy;
|
||
}
|
||
|
||
var resp = await _client.GetAllProductsByFilterAsync(request);
|
||
return MapAndCache(resp.Models);
|
||
}
|
||
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));
|
||
}
|
||
|
||
return list.OrderBy(p => p.Id).ToList();
|
||
}
|
||
}
|
||
|
||
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: string.IsNullOrWhiteSpace(m.ImagePath) ? string.Empty : UrlUtility.DownloadUrl + 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) =>
|
||
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;
|
||
|
||
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;
|
||
}
|
||
} |